feat(attedit): attribute editor dialog for blocks

Double-clicking a block reference that has attributes, or running ATTEDIT
on one, now opens an editor dialog listing every attribute (tag + editable
value) with OK / Cancel. OK writes the values back to the block, with undo
and a repaint; Cancel discards. A block with no attributes still enters
in-place block edit (REFEDIT) on double-click as before. (#192)

ATTEDIT opens the dialog directly when a suitable block is already
selected; otherwise it runs the pick command and the dialog opens once a
block is chosen. This replaces the earlier command-line, per-attribute
prompt flow, which is removed along with its __ATTEDIT__ sentinel path and
the now-unused attedit_set_attrs trait hook; the ATTEDIT command is reduced
to a plain block picker.

The dialog is a tab-scoped in-canvas modal (Plan B): its working copy holds
a document-local handle, so closing that tab or switching away dismisses the
editor rather than risk applying edits to another tab's document.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-07-01 08:05:25 +03:00
commit 86a59d6c7e
12 changed files with 346 additions and 192 deletions

View file

@ -104,6 +104,12 @@ impl OpenCADStudio {
// Dismissing these via ✕ is the cancel/decline path.
Some(Unsaved) => self.pending_close = None,
Some(AssocPrompt) => self.mark_assoc_prompted(),
// Cancel: drop the working copy without touching the block.
Some(AttributeEditor) => {
self.attr_editor_handle = None;
self.attr_editor_block.clear();
self.attr_editor_fields.clear();
}
_ => {}
}
self.active_modal = None;
@ -645,6 +651,11 @@ impl OpenCADStudio {
Message::TabSwitch(idx) => {
if idx < self.tabs.len() {
if idx != self.active_tab {
// The attribute editor is tab-scoped; leaving its tab
// drops it (its handle is that document's, not this one's).
self.cancel_attr_editor();
}
self.active_tab = idx;
self.sync_ribbon_layers();
self.sync_ribbon_styles();
@ -2253,6 +2264,17 @@ impl OpenCADStudio {
self.close_active_modal();
Task::none()
}
Message::AttrEditorOpen(handle) => {
self.open_attribute_editor(handle);
Task::none()
}
Message::AttrEditorInput { idx, value } => {
if let Some(field) = self.attr_editor_fields.get_mut(idx) {
field.1 = value;
}
Task::none()
}
Message::AttrEditorOk => self.on_attr_editor_ok(),
Message::ModalGrab => {
// Start a drag; the first ModalDragMove seeds the reference.
self.modal_dragging = true;