diff --git a/Cargo.lock b/Cargo.lock index fd02cccf0..045fe3072 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,7 +71,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" [[package]] name = "acadrust" version = "0.4.0" -source = "git+https://github.com/HakanSeven12/acadrust?branch=main#846eb9f0dc25fe22ca4a212dce33a59d444f52a5" +source = "git+https://github.com/HakanSeven12/acadrust?branch=main#e257af9d5989bb24b7ec64982b315a0f35740a1f" dependencies = [ "ahash 0.8.12", "anyhow", diff --git a/src/app/mod.rs b/src/app/mod.rs index 633f3ae3a..725ed6fd6 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -361,9 +361,11 @@ pub(super) struct OpenCADStudio { attr_editor_handle: Option, /// Block name shown in the editor's title bar. attr_editor_block: String, - /// Working copy of the block's attributes as `(tag, value)`, in the same - /// order as `Insert::attributes`. Edited live; written back on OK. - attr_editor_fields: Vec<(String, String)>, + /// Working copy of the block's attributes as `(tag, prompt, value)`, in the + /// same order as `Insert::attributes`. The prompt is read from the block's + /// matching ATTDEF (blank if none). Only the value is edited; the whole row + /// is written back on OK. + attr_editor_fields: Vec<(String, String, String)>, /// Plugin ids the user turned off in the Plugin Manager. Disabled plugins /// keep their manifest listed but drop their ribbon tab and command /// dispatch. Persisted via [`settings::UserSettings::disabled_plugins`]. diff --git a/src/app/update/command.rs b/src/app/update/command.rs index dc2f96fde..e1335ea75 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -1214,33 +1214,38 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { /// 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) { + let doc = &self.tabs[i].scene.document; + // Ok((block, fields)) to open; Err(msg) to report and stay closed. The + // borrow of `doc` ends with this match, before any `self` mutation. + let result = match doc.get_entity(handle) { Some(acadrust::EntityType::Insert(ins)) if !ins.attributes.is_empty() => { - let block = ins.block_name.clone(); + // The prompt text lives on the block's ATTDEFs, not on the + // attribute instances — map tag → prompt from the definition. + let prompts = block_attr_prompts(doc, &ins.block_name); let fields = ins .attributes .iter() - .map(|a| (a.tag.clone(), a.get_value().to_string())) + .map(|a| { + let prompt = prompts.get(&a.tag).cloned().unwrap_or_default(); + (a.tag.clone(), prompt, a.get_value().to_string()) + }) .collect::>(); - Some((block, fields)) + Ok((ins.block_name.clone(), 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 + Err("ATTEDIT This block has no attributes.") } + _ => Err("ATTEDIT Select a block with attributes."), }; - 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; + match result { + Ok((block, fields)) => { + 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; + } + Err(msg) => self.command_line.push_error(msg), } } @@ -1279,14 +1284,14 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { 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()) { + 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 { + 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()); @@ -1307,3 +1312,21 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { Task::none() } } + +/// Map each attribute tag to the prompt text declared on the block's matching +/// ATTDEF. Attribute instances (ATTRIB) carry only tag + value; the prompt is +/// defined once on the block definition. Tags with no definition are absent. +fn block_attr_prompts( + doc: &acadrust::CadDocument, + block_name: &str, +) -> rustc_hash::FxHashMap { + let mut map = rustc_hash::FxHashMap::default(); + if let Some(br) = doc.block_records.get(block_name) { + for &eh in &br.entity_handles { + if let Some(acadrust::EntityType::AttributeDefinition(ad)) = doc.get_entity(eh) { + map.insert(ad.tag.clone(), ad.prompt.clone()); + } + } + } + map +} diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 2bb1ea058..78185d7e0 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -2270,7 +2270,7 @@ impl OpenCADStudio { } Message::AttrEditorInput { idx, value } => { if let Some(field) = self.attr_editor_fields.get_mut(idx) { - field.1 = value; + field.2 = value; } Task::none() } diff --git a/src/ui/window/attribute_editor.rs b/src/ui/window/attribute_editor.rs index 37024d3e4..f44a3909b 100644 --- a/src/ui/window/attribute_editor.rs +++ b/src/ui/window/attribute_editor.rs @@ -2,10 +2,10 @@ //! reference (INSERT). Opened by double-clicking a block that carries //! attributes, or by the ATTEDIT command with such a block selected. //! -//! Each attribute is one row: its tag (read-only) and an editable value box. -//! OK writes every value back to the block; Cancel (the ✕ or button) discards. -//! The heavy lifting — applying edits, undo, repaint — lives in the update -//! handler (`Message::AttrEditorOk`); this module is pure layout. +//! Each attribute is one row: its prompt/tag (read-only) and an editable value +//! box. OK writes every value back to the block; Cancel (the ✕ or button) +//! discards. The heavy lifting — applying edits, undo, repaint — lives in the +//! update handler (`Message::AttrEditorOk`); this module is pure layout. use crate::app::Message; use iced::widget::{button, column, container, row, scrollable, text, text_input, Space}; @@ -30,12 +30,23 @@ fn field_style(_t: &Theme, _s: text_input::Status) -> text_input::Style { } /// Build the attribute editor dialog body. `block` is the reference's block -/// name (shown as a subtitle); `fields` are the `(tag, value)` pairs in -/// attribute order — the row index is the routing key back to -/// `Message::AttrEditorInput`. -pub fn view_window<'a>(block: &'a str, fields: &'a [(String, String)]) -> Element<'a, Message> { +/// name (shown as a subtitle); `fields` are the `(tag, prompt, value)` triples +/// in attribute order — the row index is the routing key back to +/// `Message::AttrEditorInput`. Each row is labelled with the prompt (falling +/// back to the tag when the block defines none), so the label reads as it would +/// when inserting the block. +pub fn view_window<'a>( + block: &'a str, + fields: &'a [(String, String, String)], +) -> Element<'a, Message> { let mut list = column![].spacing(6); - for (idx, (tag, value)) in fields.iter().enumerate() { + for (idx, (tag, prompt, value)) in fields.iter().enumerate() { + let label = if prompt.trim().is_empty() { + tag.as_str() + } else { + prompt.as_str() + }; + let value_box = text_input("", value) .on_input(move |v| Message::AttrEditorInput { idx, value: v }) .on_submit(Message::AttrEditorOk) @@ -45,7 +56,7 @@ pub fn view_window<'a>(block: &'a str, fields: &'a [(String, String)]) -> Elemen .width(Length::Fill); let attr_row = row![ - container(text(tag.as_str()).size(13).color(WHITE)) + container(text(label).size(13).color(WHITE)) .width(170) .padding([4, 6]), value_box,