forked from 1b-agent/opencadstudio-upstream
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 <[email protected]>
This commit is contained in:
1 parent
5f2e920b75
commit
86a59d6c7e
12 files changed
+346
-192
No files matched your search
@@ -1,39 +1,25 @@
|
||||
// ATTEDIT command — edit attribute values of a selected INSERT entity.
|
||||
// ATTEDIT command — pick a block reference with attributes, then open the
|
||||
// attribute editor dialog on it.
|
||||
//
|
||||
// Workflow:
|
||||
// Step 1: pick an INSERT entity that has attributes.
|
||||
// Step 2+: for each attribute tag, show "TAG = <value>" and accept new
|
||||
// text via text input. Enter with empty string keeps the old value.
|
||||
// After all attributes are processed, commit via ReplaceMany.
|
||||
// This command only performs the entity pick. Once a block is chosen it
|
||||
// reports the handle through `attedit_pending_handle`; the command host
|
||||
// (`command_driver`) ends the command and opens the editor dialog. When a
|
||||
// suitable block is already selected, the ATTEDIT dispatch opens the dialog
|
||||
// directly and never starts this command (see `app::commands::inquiry`).
|
||||
|
||||
use acadrust::EntityType;
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
|
||||
pub struct AtteditCommand {
|
||||
step: Step,
|
||||
}
|
||||
|
||||
enum Step {
|
||||
/// Waiting for the user to pick an INSERT entity.
|
||||
SelectInsert,
|
||||
/// Editing the attribute at index `idx` of the collected insert data.
|
||||
EditAttr {
|
||||
handle: acadrust::Handle,
|
||||
/// (tag, current_value) pairs collected from the insert.
|
||||
attrs: Vec<(String, String)>,
|
||||
/// Index of the attribute currently being edited.
|
||||
idx: usize,
|
||||
},
|
||||
/// The picked block reference, once the user clicks one.
|
||||
picked: Option<acadrust::Handle>,
|
||||
}
|
||||
|
||||
impl AtteditCommand {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: Step::SelectInsert,
|
||||
}
|
||||
Self { picked: None }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,77 +29,23 @@ impl CadCommand for AtteditCommand {
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
Step::SelectInsert => "ATTEDIT Select block with attributes:".to_string(),
|
||||
Step::EditAttr { attrs, idx, .. } => {
|
||||
let (tag, val) = &attrs[*idx];
|
||||
format!(
|
||||
"ATTEDIT {} = <{}> (Enter to keep, type new value):",
|
||||
tag, val
|
||||
)
|
||||
}
|
||||
}
|
||||
"ATTEDIT Select block with attributes:".to_string()
|
||||
}
|
||||
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
matches!(self.step, Step::SelectInsert)
|
||||
self.picked.is_none()
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: acadrust::Handle, _pt: DVec3) -> CmdResult {
|
||||
if handle.is_null() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
// We can't inspect the document here — store the handle and let the
|
||||
// command host inject the attribute list via `init_with_attrs`.
|
||||
// Instead, signal the host to call prepare_attedit().
|
||||
self.step = Step::EditAttr {
|
||||
handle,
|
||||
attrs: vec![], // will be filled by init_with_attrs() in cmd_result.rs
|
||||
idx: 0,
|
||||
};
|
||||
// Record the pick and yield: the host reads `attedit_pending_handle`
|
||||
// and opens the editor dialog for this block.
|
||||
self.picked = Some(handle);
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
if let Step::EditAttr { ref attrs, idx, .. } = self.step {
|
||||
!attrs.is_empty() && idx < attrs.len()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn wants_text_with_spaces(&self) -> bool {
|
||||
// Editing an attribute value — same free-form semantics as the
|
||||
// text-content prompts in TEXT / MTEXT / DDEDIT.
|
||||
self.wants_text_input()
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let Step::EditAttr { handle, attrs, idx } = &mut self.step else {
|
||||
return None;
|
||||
};
|
||||
let handle = *handle;
|
||||
|
||||
// Update the current attribute value if the user typed something.
|
||||
if !text.trim().is_empty() {
|
||||
attrs[*idx].1 = text.trim().to_string();
|
||||
}
|
||||
*idx += 1;
|
||||
|
||||
if *idx >= attrs.len() {
|
||||
// All attributes done — build new Insert entity.
|
||||
// We signal via a special CmdResult; the host will call apply_attedit().
|
||||
let pairs = attrs.clone();
|
||||
return Some(CmdResult::ReplaceEntity(
|
||||
handle,
|
||||
vec![make_attedit_sentinel(handle, &pairs)],
|
||||
));
|
||||
}
|
||||
|
||||
// More attributes to edit.
|
||||
None
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
@@ -125,55 +57,7 @@ impl CadCommand for AtteditCommand {
|
||||
}
|
||||
|
||||
fn attedit_pending_handle(&self) -> Option<acadrust::Handle> {
|
||||
if let Step::EditAttr { handle, attrs, .. } = &self.step {
|
||||
if attrs.is_empty() {
|
||||
return Some(*handle);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn attedit_set_attrs(&mut self, new_attrs: Vec<(String, String)>) {
|
||||
if let Step::EditAttr { attrs, idx, .. } = &mut self.step {
|
||||
*attrs = new_attrs;
|
||||
*idx = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Make a sentinel entity carrying the edited attribute values.
|
||||
/// Encodes all (tag=value) pairs in the layer field as "tag1\x01val1\x02tag2\x01val2...".
|
||||
fn make_attedit_sentinel(_handle: acadrust::Handle, pairs: &[(String, String)]) -> EntityType {
|
||||
let encoded: String = pairs
|
||||
.iter()
|
||||
.map(|(t, v)| format!("{}\x01{}", t, v))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\x02");
|
||||
let mut xl = acadrust::entities::XLine::new(
|
||||
acadrust::types::Vector3::zero(),
|
||||
acadrust::types::Vector3::new(1.0, 0.0, 0.0),
|
||||
);
|
||||
xl.common.layer = format!("__ATTEDIT__{}", encoded);
|
||||
EntityType::XLine(xl)
|
||||
}
|
||||
|
||||
/// Apply edited attribute values to an INSERT entity in the document.
|
||||
/// Called from `cmd_result.rs` when the sentinel is detected.
|
||||
pub fn apply_attedit(doc: &mut acadrust::CadDocument, handle: acadrust::Handle, encoded: &str) {
|
||||
let Some(EntityType::Insert(ins)) = doc.get_entity_mut(handle) else {
|
||||
return;
|
||||
};
|
||||
for pair in encoded.split('\x02') {
|
||||
let mut parts = pair.splitn(2, '\x01');
|
||||
let Some(tag) = parts.next() else {
|
||||
continue;
|
||||
};
|
||||
let Some(val) = parts.next() else {
|
||||
continue;
|
||||
};
|
||||
if let Some(attrib) = ins.attributes.iter_mut().find(|a| a.tag == tag) {
|
||||
attrib.set_value(val);
|
||||
}
|
||||
self.picked
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user