feat(attedit): show attribute prompts in the editor

Each row in the attribute editor now shows the attribute's prompt — the
text defined on the block's ATTDEF — falling back to the tag when the
block defines no prompt. The prompt is read from the block definition,
since attribute instances carry only tag + value.

Bumps the acadrust pin to pull the matching reader fix: its DWG object
reader was discarding the ATTDEF prompt, so every prompt came back empty.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-07-01 08:23:54 +03:00
commit 0eb347bf47
5 changed files with 71 additions and 35 deletions

2
Cargo.lock generated
View file

@ -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",

View file

@ -361,9 +361,11 @@ pub(super) struct OpenCADStudio {
attr_editor_handle: Option<acadrust::Handle>,
/// 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`].

View file

@ -1214,33 +1214,38 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
/// 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::<Vec<_>>();
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<Message> {
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<Message> {
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<String, String> {
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
}

View file

@ -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()
}

View file

@ -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,