FIELD: Field dialog, FIELD command and the ATTDEF field buttons

A Field dialog with the fields the application evaluates and the
reference format choices, FIELD placing a field as multi-line text, the
attribute definition dialogs' field buttons, field attachment on commit,
system variable / file size / regional date providers for field
evaluation, and the codec patch with the field authoring API.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ramox81andClaude Opus 5.5 committed 2026-10-02 09:56:54 +03:00
1 parent 53f2983e9c
commit 87211f4654
40 files changed
+1859 -14

No files matched your search

+32 -3
View File
@@ -54,6 +54,7 @@ impl OpenCADStudio {
if let Some(state) = self.attdef_dialog.as_mut() {
if let Some(value) = value {
state.default = value;
state.field = None;
}
self.active_modal = Some(crate::app::ModalKind::AttDef);
}
@@ -134,6 +135,7 @@ impl OpenCADStudio {
align_below: false,
can_align_below: session.last.is_some(),
error: None,
field: None,
});
drop(session);
self.tabs[i].active_cmd = None;
@@ -154,6 +156,8 @@ impl OpenCADStudio {
default: a.default_value.clone(),
constant: a.flags.constant,
error: None,
field: None,
field_changed: false,
});
self.active_modal = Some(crate::app::ModalKind::AttDefEdit);
}
@@ -164,7 +168,15 @@ impl OpenCADStudio {
match m {
AttdefDialogMsg::EditTag(v) => edit.tag = v,
AttdefDialogMsg::EditPrompt(v) => edit.prompt = v,
AttdefDialogMsg::EditDefault(v) => edit.default = v,
AttdefDialogMsg::EditDefault(v) => {
edit.default = v;
edit.field = None;
edit.field_changed = true;
}
AttdefDialogMsg::EditInsertField => {
self.active_modal = None;
self.open_field_dialog(crate::ui::window::field_dialog::FieldTarget::AttdefEdit);
}
AttdefDialogMsg::Help => self.command_line.push_info(
crate::t!("Changes the tag, prompt and default value of an attribute definition.")
.as_ref(),
@@ -189,7 +201,15 @@ impl OpenCADStudio {
AttdefDialogMsg::Annotative(v) => state.annotative = v,
AttdefDialogMsg::Tag(v) => state.tag = v,
AttdefDialogMsg::Prompt(v) => state.prompt = v,
AttdefDialogMsg::Default(v) => state.default = v,
AttdefDialogMsg::Default(v) => {
state.default = v;
state.field = None;
}
AttdefDialogMsg::InsertField => {
self.active_modal = None;
self.open_field_dialog(crate::ui::window::field_dialog::FieldTarget::AttdefDefault);
return Task::none();
}
AttdefDialogMsg::Justify(c) => state.justify = c.0,
AttdefDialogMsg::Style(v) => state.style = v,
AttdefDialogMsg::Height(v) => state.height = v,
@@ -279,6 +299,7 @@ impl OpenCADStudio {
oblique_angle: defaults.oblique_angle,
boundary_width: width,
line_spacing: 1.0,
field: state.field.clone(),
};
self.attdef_dialog = None;
self.close_active_modal();
@@ -336,7 +357,7 @@ impl OpenCADStudio {
self.tabs[i].scene.document.get_entity(state.handle),
Some(codec::EntityType::AttributeDefinition(a))
if a.tag != attdef::normalize_tag(&state.tag) || a.prompt != state.prompt || a.default_value != state.default
);
) || state.field_changed;
if changed && !self.reject_locked_edit(i, state.handle) {
self.push_undo_snapshot(i, "DDEDIT");
if let Some(codec::EntityType::AttributeDefinition(a)) =
@@ -348,6 +369,14 @@ impl OpenCADStudio {
}
a.default_value = state.default.clone();
}
if state.field_changed {
crate::entities::field::set_text_field(
&mut self.tabs[i].scene.document,
state.handle,
state.field.clone(),
&state.default,
);
}
self.tabs[i]
.scene
.bump_entities(&[(state.handle, crate::scene::ChangeKind::Modified)]);
+206
View File
@@ -0,0 +1,206 @@
//! FIELD and the Field dialog behind the ƒ buttons of the attribute
//! definition dialogs.
use crate::app::{Message, OpenCADStudio};
use crate::command::CadCommand;
use crate::modules::annotate::field_cmd::{FieldObjectPickCommand, FieldPlaceCommand};
use crate::ui::window::field_dialog::{
fields_of, object_properties, FieldDialogMsg, FieldDialogState, FieldTarget, DATE_FORMATS,
NAMED_TYPES,
};
use iced::Task;
impl OpenCADStudio {
pub(super) fn dispatch_field(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
match cmd {
"FIELD" => {
self.open_field_dialog(FieldTarget::NewText);
Some(Task::none())
}
cmd if cmd == "_FIELD_OBJECT" || cmd.starts_with("_FIELD_OBJECT ") => {
let handle = cmd
.split_whitespace()
.nth(1)
.and_then(|h| u64::from_str_radix(h, 16).ok())
.map(codec::Handle::new);
if let Some(state) = self.field_dialog.as_mut() {
if let Some(entity) = handle.and_then(|h| self.tabs[i].scene.document.get_entity(h)) {
let props = object_properties(entity);
state.object = Some((entity.common().handle, crate::t!(crate::entities::names::ui_name(entity)).into_owned()));
state.object_prop = props.first().copied();
state.object_props = props;
}
self.active_modal = Some(crate::app::ModalKind::Field);
self.refresh_field_preview();
}
Some(Task::none())
}
_ => None,
}
}
/// Open the Field dialog for `target`.
pub(in crate::app) fn open_field_dialog(&mut self, target: FieldTarget) {
let mut state = FieldDialogState::new(target);
state.examples = DATE_FORMATS
.iter()
.map(|f| self.field_value(&format!("\\AcVar Date \\f \"{f}\""), &[]))
.collect();
self.field_dialog = Some(state);
self.fill_named_objects();
self.refresh_field_preview();
self.active_modal = Some(crate::app::ModalKind::Field);
}
fn field_value(&self, code: &str, objects: &[codec::Handle]) -> String {
crate::entities::field::evaluate(&self.tabs[self.active_tab].scene.document, code, objects, None)
.unwrap_or_else(|| "----".to_string())
}
fn refresh_field_preview(&mut self) {
let Some(state) = self.field_dialog.as_ref() else {
return;
};
let (code, objects) = state.code();
let preview = if state.complete() { self.field_value(&code, &objects) } else { String::new() };
if let Some(state) = self.field_dialog.as_mut() {
state.preview = preview;
}
}
/// The names of the chosen named-object type, sorted.
fn fill_named_objects(&mut self) {
let Some(state) = self.field_dialog.as_mut() else {
return;
};
let document = &self.tabs[self.active_tab].scene.document;
let mut names: Vec<(String, codec::Handle)> = match NAMED_TYPES[state.named_type] {
"Block" => document
.block_records
.iter()
.filter(|b| !b.name.starts_with('*'))
.map(|b| (b.name.clone(), b.handle))
.collect(),
"Dimstyle" => document.dim_styles.iter().map(|s| (s.name.clone(), s.handle)).collect(),
"Layer" => document.layers.iter().map(|l| (l.name.clone(), l.handle)).collect(),
"Linetype" => document.line_types.iter().map(|l| (l.name.clone(), l.handle)).collect(),
"Textstyle" => document
.text_styles
.iter()
.filter(|s| !s.name.is_empty())
.map(|s| (s.name.clone(), s.handle))
.collect(),
"View" => document.views.iter().map(|v| (v.name.clone(), v.handle)).collect(),
_ => Vec::new(),
};
names.sort_by_key(|(n, _)| n.to_lowercase());
state.named_names = names;
state.named = None;
}
pub(in crate::app) fn on_field_dialog(&mut self, m: FieldDialogMsg) -> Task<Message> {
let i = self.active_tab;
let Some(state) = self.field_dialog.as_mut() else {
return Task::none();
};
match m {
FieldDialogMsg::Category(c) => {
state.category = c;
let names = fields_of(c);
if !names.contains(&state.name) {
state.name = names.first().copied().unwrap_or("Date");
}
}
FieldDialogMsg::Name(n) => {
state.name = n;
state.text_case = 0;
}
FieldDialogMsg::DateFormat(f) => state.date_format = f,
FieldDialogMsg::DateExample(k) => {
if let Some(f) = DATE_FORMATS.get(k) {
state.date_format = f.to_string();
}
}
FieldDialogMsg::TextCase(k) => state.text_case = k,
FieldDialogMsg::FileParts(b) => state.file_parts = b,
FieldDialogMsg::FileExtension(v) => state.file_extension = v,
FieldDialogMsg::SizeUnit(k) => state.size_unit = k,
FieldDialogMsg::SysVar(v) => state.sysvar = v,
FieldDialogMsg::Diesel(v) => state.diesel = v,
FieldDialogMsg::NamedType(k) => {
state.named_type = k;
self.fill_named_objects();
}
FieldDialogMsg::Named(k) => state.named = Some(k),
FieldDialogMsg::ObjectProp(p) => state.object_prop = Some(p),
FieldDialogMsg::SelectObject => {
// The dialog waits while one object is picked.
self.active_modal = None;
let command = FieldObjectPickCommand;
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
return Task::none();
}
FieldDialogMsg::Help => self.command_line.push_info(
crate::t!("Inserts a field: text that updates from the drawing, the date or another source.").as_ref(),
),
FieldDialogMsg::Ok => return self.field_dialog_ok(i),
}
self.refresh_field_preview();
Task::none()
}
fn field_dialog_ok(&mut self, i: usize) -> Task<Message> {
let Some(state) = self.field_dialog.take() else {
return Task::none();
};
let (code, objects) = state.code();
let value = state.preview.clone();
let field = Some((code, objects));
match state.target {
FieldTarget::NewText => {
self.close_active_modal();
let defaults =
crate::scene::creation_style::current_text_defaults(&self.tabs[i].scene.document);
let command = FieldPlaceCommand::new(
value,
field.expect("set above"),
defaults.style_name,
defaults.height,
);
self.reset_command_start_state(i);
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
self.push_ucs_to_cmd(i);
return self.focus_cmd_input();
}
FieldTarget::AttdefDefault => {
if let Some(dialog) = self.attdef_dialog.as_mut() {
dialog.default = value;
dialog.field = field;
}
self.active_modal = Some(crate::app::ModalKind::AttDef);
}
FieldTarget::AttdefEdit => {
if let Some(edit) = self.attdef_edit.as_mut() {
edit.default = value;
edit.field = field;
edit.field_changed = true;
}
self.active_modal = Some(crate::app::ModalKind::AttDefEdit);
}
}
Task::none()
}
/// The dialogs a Field dialog was opened from come back when it closes.
pub(in crate::app) fn close_field_dialog(&mut self) {
let target = self.field_dialog.take().map(|s| s.target);
self.close_active_modal();
self.active_modal = match target {
Some(FieldTarget::AttdefDefault) if self.attdef_dialog.is_some() => Some(crate::app::ModalKind::AttDef),
Some(FieldTarget::AttdefEdit) if self.attdef_edit.is_some() => Some(crate::app::ModalKind::AttDefEdit),
_ => None,
};
}
}
+4
View File
@@ -5,6 +5,7 @@ use iced::Task;
use std::path::PathBuf;
mod attdef;
mod field;
mod blocks;
mod xref_attach;
mod dim;
@@ -282,6 +283,9 @@ impl OpenCADStudio {
if let Some(t) = self.dispatch_attdef(cmd, i) {
return Some(t);
}
if let Some(t) = self.dispatch_field(cmd, i) {
return Some(t);
}
if let Some(t) = self.dispatch_draw(cmd, i) {
return Some(t);
}