feat(props): editable block Name row + collapsible XYZ groups

- Block reference Name (Misc) is editable: the dropdown re-points the
  insert to another definition; typing a new name renames the definition
  everywhere (record re-key, Block marker, every INSERT reference).
  Anonymous (*) and xref blocks stay read-only, with the same guards
  added to Scene::rename_block.
- Custom single-control dropdown (text field + caret) built on the
  shared floating_below overlay so the list always opens downward,
  unlike iced's space-based menu placement.
- Consecutive "<Base> X/Y/Z" rows (Position, Scale, Start, Center, ...)
  collapse into one expandable summary row across all entities; the
  expand state persists across rebuilds and selection changes.
- The BEDIT block tab no longer offers the layout Rename/Delete
  context menu.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-07-22 11:11:10 +03:00
commit b5dbef5776
10 changed files with 583 additions and 51 deletions

View file

@ -229,15 +229,30 @@ impl Scene {
/// Rename a block definition: re-key its record, update the Block marker's
/// name, and repoint every INSERT that referenced the old name so all
/// instances keep resolving. Returns false if `old` is missing, `new` is
/// already taken, or the names are equal (case-insensitive). (#261)
/// instances keep resolving. Returns false if `old` is missing or
/// anonymous/xref, `new` is invalid or already taken, or the names are
/// equal (case-insensitive). (#261)
pub fn rename_block(&mut self, old: &str, new: &str) -> bool {
if !crate::scene::valid_block_name(new) {
return false;
}
if old.eq_ignore_ascii_case(new) {
return false;
}
if self.document.block_records.get(new).is_some() {
return false;
}
// Anonymous (*) names are program-owned and re-numbered on save; an
// xref('|') symbol name is bound to the referenced file.
if self
.document
.block_records
.get(old)
.map(|br| br.is_anonymous() || br.flags.is_xref || br.name.contains('|'))
.unwrap_or(true)
{
return false;
}
let Some(mut br) = self.document.block_records.remove(old) else {
return false;
};

View file

@ -204,6 +204,20 @@ pub fn vp_effective_scale(custom_scale: f64, view_height: f64, vp_height: f64) -
1.0
}
/// A block name a user may assign: non-empty, no control characters, none of
/// the symbol-table-reserved characters (`*` marks anonymous blocks, `|`
/// xref-dependent ones; the rest are the DXF symbol-name exclusions).
pub(crate) fn valid_block_name(name: &str) -> bool {
!name.is_empty()
&& !name.chars().any(|c| {
c.is_control()
|| matches!(
c,
'<' | '>' | '/' | '\\' | '"' | ':' | ';' | '?' | '*' | '|' | ',' | '=' | '`'
)
})
}
/// Pre-built entity caches returned by [`build_derived_caches`].
/// Produced in the file-load background task so the UI thread only assigns.
#[derive(Debug, Clone)]

View file

@ -17,6 +17,13 @@ pub enum PropValue {
selected: String,
options: Vec<String>,
},
/// Editable text plus a dropdown of existing options (block reference
/// Name row): picking an option re-points the reference, submitting a
/// new name renames the definition.
EditChoice {
value: String,
options: Vec<String>,
},
/// ACI/RGB/ByLayer/ByBlock color — rendered as a color picker.
ColorChoice(AcadColor),
/// Color varies across the current multi-selection.

View file

@ -293,6 +293,7 @@ impl Scene {
PropValue::ReadOnly(s) | PropValue::EditText(s) => s,
PropValue::LayerChoice(s) => s,
PropValue::Choice { selected, .. } => selected,
PropValue::EditChoice { value, .. } => value,
PropValue::ColorChoice(c) => Self::format_color(c),
PropValue::LwChoice(lw) => Self::format_lineweight(lw),
PropValue::LinetypeChoice(s) => s,