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:
parent
86a59d6c7e
commit
0eb347bf47
5 changed files with 71 additions and 35 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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`].
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue