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:
parent
5f2e920b75
commit
86a59d6c7e
12 changed files with 346 additions and 192 deletions
|
|
@ -27,6 +27,10 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||
if self.tabs.get(idx).map_or(false, |t| t.is_start) {
|
||||
return Task::none();
|
||||
}
|
||||
// Closing a tab shifts indices / the active tab. The attribute
|
||||
// editor holds a document-local handle into one tab, so drop it
|
||||
// now rather than risk it applying to a different tab's document.
|
||||
self.cancel_attr_editor();
|
||||
if self.tabs.get(idx).map_or(false, |t| t.dirty) {
|
||||
self.pending_close = Some(crate::app::PendingClose::Tab(idx));
|
||||
return self.open_unsaved_dialog_window();
|
||||
|
|
@ -1204,4 +1208,102 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||
self.refresh_properties();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
/// Open the attribute editor dialog for the given INSERT, loading a working
|
||||
/// copy of its attribute values. No-op (with a hint) when the block has no
|
||||
/// attributes. Entry points: double-clicking such a block, or ATTEDIT.
|
||||
pub(crate) fn open_attribute_editor(&mut self, handle: acadrust::Handle) {
|
||||
let i = self.active_tab;
|
||||
let loaded = match self.tabs[i].scene.document.get_entity(handle) {
|
||||
Some(acadrust::EntityType::Insert(ins)) if !ins.attributes.is_empty() => {
|
||||
let block = ins.block_name.clone();
|
||||
let fields = ins
|
||||
.attributes
|
||||
.iter()
|
||||
.map(|a| (a.tag.clone(), a.get_value().to_string()))
|
||||
.collect::<Vec<_>>();
|
||||
Some((block, fields))
|
||||
}
|
||||
Some(acadrust::EntityType::Insert(_)) => {
|
||||
self.command_line
|
||||
.push_error("ATTEDIT This block has no attributes.");
|
||||
None
|
||||
}
|
||||
_ => {
|
||||
self.command_line
|
||||
.push_error("ATTEDIT Select a block with attributes.");
|
||||
None
|
||||
}
|
||||
};
|
||||
if let Some((block, fields)) = loaded {
|
||||
self.attr_editor_block = block;
|
||||
self.attr_editor_fields = fields;
|
||||
self.attr_editor_handle = Some(handle);
|
||||
self.active_modal = Some(crate::app::ModalKind::AttributeEditor);
|
||||
self.modal_offset = iced::Vector::ZERO;
|
||||
}
|
||||
}
|
||||
|
||||
/// Close the attribute editor without applying, if it is open. Used when
|
||||
/// the tab it belongs to closes or the active tab switches — its handle is
|
||||
/// document-local and must not outlive its tab.
|
||||
pub(super) fn cancel_attr_editor(&mut self) {
|
||||
if self.active_modal == Some(crate::app::ModalKind::AttributeEditor) {
|
||||
self.active_modal = None;
|
||||
self.modal_offset = iced::Vector::ZERO;
|
||||
}
|
||||
self.attr_editor_handle = None;
|
||||
self.attr_editor_block.clear();
|
||||
self.attr_editor_fields.clear();
|
||||
}
|
||||
|
||||
/// Apply every edited attribute value back to the block and close the
|
||||
/// editor (OK). Values are written verbatim (attribute text is free-form).
|
||||
/// The edits are positional — same order the dialog was populated in — so
|
||||
/// blocks with duplicate tags stay correct; a tag-based fallback covers the
|
||||
/// unlikely case the attribute list changed underneath the open dialog.
|
||||
pub(super) fn on_attr_editor_ok(&mut self) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
let Some(handle) = self.attr_editor_handle.take() else {
|
||||
self.active_modal = None;
|
||||
return Task::none();
|
||||
};
|
||||
let fields = std::mem::take(&mut self.attr_editor_fields);
|
||||
self.attr_editor_block.clear();
|
||||
self.active_modal = None;
|
||||
self.modal_offset = iced::Vector::ZERO;
|
||||
|
||||
self.push_undo_snapshot(i, "ATTEDIT");
|
||||
let mut changed = false;
|
||||
if let Some(acadrust::EntityType::Insert(ins)) =
|
||||
self.tabs[i].scene.document.get_entity_mut(handle)
|
||||
{
|
||||
if fields.len() == ins.attributes.len() {
|
||||
for (attr, (_, val)) in ins.attributes.iter_mut().zip(fields.iter()) {
|
||||
if attr.get_value() != val {
|
||||
attr.set_value(val.clone());
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (tag, val) in &fields {
|
||||
if let Some(attr) = ins.attributes.iter_mut().find(|a| &a.tag == tag) {
|
||||
if attr.get_value() != val {
|
||||
attr.set_value(val.clone());
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
self.invalidate_property_targets(i, &[handle]);
|
||||
self.tabs[i].dirty = true;
|
||||
} else {
|
||||
// Nothing changed — drop the snapshot pushed a moment ago.
|
||||
let _ = self.tabs[i].history.undo_stack.pop();
|
||||
}
|
||||
self.refresh_properties();
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue