merge: TEXTEDIT command + TEXTEDITMODE sysvar (#130)
Merge PR #130 (KarimJerbi) and resolve review findings: - Conflict (settings.rs): keep current persisted-settings layout, append texteditmode field. - Conflict (command/mod.rs): keep both new CmdResult variants — main's live-entity ops and the PR's text-edit suspend/resume. - Bug: invalid TEXTEDITMODE value no longer aborts via the Measurement arm; it now re-prompts with the error and stays active. - Dedup: single parse_texteditmode() helper replaces the value parsing duplicated across commands.rs, settings.rs and textedit.rs. - Strip trailing whitespace. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
commit
8c48233dcf
11 changed files with 399 additions and 14 deletions
|
|
@ -1699,6 +1699,41 @@ impl OpenCADStudio {
|
||||||
self.ribbon.deactivate_tool();
|
self.ribbon.deactivate_tool();
|
||||||
return self.begin_text_edit(handle);
|
return self.begin_text_edit(handle);
|
||||||
}
|
}
|
||||||
|
CmdResult::SuspendForTextEdit { handle } => {
|
||||||
|
let is_editable = crate::app::text_inline::can_edit_text(handle, &self.tabs[i].scene.document);
|
||||||
|
if !is_editable {
|
||||||
|
self.command_line.push_error("TEXTEDIT: selected entity is not text.");
|
||||||
|
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
|
||||||
|
if let Some(p) = prompt {
|
||||||
|
self.command_line.push_info(&p);
|
||||||
|
}
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
let cmd = self.tabs[i].active_cmd.take();
|
||||||
|
self.tabs[i].suspended_cmd = cmd;
|
||||||
|
self.tabs[i].snap_result = None;
|
||||||
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
|
self.restore_pre_cmd_tangent();
|
||||||
|
self.ribbon.deactivate_tool();
|
||||||
|
return self.begin_text_edit(handle);
|
||||||
|
}
|
||||||
|
CmdResult::UndoDocument => {
|
||||||
|
let active = self.tabs[i].active_cmd.take();
|
||||||
|
self.undo_active_tab();
|
||||||
|
self.tabs[i].active_cmd = active;
|
||||||
|
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
|
||||||
|
if let Some(p) = prompt {
|
||||||
|
self.command_line.push_info(&p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CmdResult::SetTexteditMode(val) => {
|
||||||
|
self.texteditmode = val;
|
||||||
|
let display_val = if val { 1 } else { 0 };
|
||||||
|
self.command_line.push_output(&format!("TEXTEDITMODE set to {display_val}"));
|
||||||
|
self.tabs[i].active_cmd = None;
|
||||||
|
self.tabs[i].snap_result = None;
|
||||||
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
|
}
|
||||||
CmdResult::DdeditEntity { handle, new_text } => {
|
CmdResult::DdeditEntity { handle, new_text } => {
|
||||||
let mut updated = false;
|
let mut updated = false;
|
||||||
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
|
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
|
||||||
|
|
|
||||||
|
|
@ -1444,6 +1444,22 @@ impl OpenCADStudio {
|
||||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"TEXTEDIT" | "TEDIT" => {
|
||||||
|
use crate::modules::annotate::textedit::TexteditCommand;
|
||||||
|
let mode_str = if self.texteditmode { "Single" } else { "Multiple" };
|
||||||
|
self.command_line.push_output(&format!("Current settings: Edit mode = {}", mode_str));
|
||||||
|
let new_cmd = TexteditCommand::new(self.texteditmode);
|
||||||
|
self.command_line.push_info(&new_cmd.prompt());
|
||||||
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||||
|
}
|
||||||
|
|
||||||
|
"TEXTEDITMODE" => {
|
||||||
|
use crate::modules::annotate::textedit::TexteditmodeCommand;
|
||||||
|
let cmd = TexteditmodeCommand::new(self.texteditmode);
|
||||||
|
self.command_line.push_info(&cmd.prompt());
|
||||||
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||||
|
}
|
||||||
|
|
||||||
"DIMALIGNED" | "DAL" => {
|
"DIMALIGNED" | "DAL" => {
|
||||||
use crate::modules::annotate::aligned_dim::AlignedDimensionCommand;
|
use crate::modules::annotate::aligned_dim::AlignedDimensionCommand;
|
||||||
let cmd = AlignedDimensionCommand::new();
|
let cmd = AlignedDimensionCommand::new();
|
||||||
|
|
@ -4942,6 +4958,23 @@ impl OpenCADStudio {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
cmd if cmd.starts_with("TEXTEDITMODE ") => {
|
||||||
|
let val_str = cmd.trim_start_matches("TEXTEDITMODE").trim().to_lowercase();
|
||||||
|
if val_str.is_empty() {
|
||||||
|
let v = if self.texteditmode { 1 } else { 0 };
|
||||||
|
self.command_line.push_output(&format!("TEXTEDITMODE = {v}"));
|
||||||
|
} else if let Some(v) =
|
||||||
|
crate::modules::annotate::textedit::parse_texteditmode(&val_str)
|
||||||
|
{
|
||||||
|
self.texteditmode = v;
|
||||||
|
let n = if v { 1 } else { 0 };
|
||||||
|
self.command_line
|
||||||
|
.push_output(&format!("TEXTEDITMODE set to {n}"));
|
||||||
|
} else {
|
||||||
|
self.command_line
|
||||||
|
.push_error("Requires 0 OR 1 OR MULTIPLE OR SINGLE");
|
||||||
|
}
|
||||||
|
}
|
||||||
cmd if cmd == "PDSIZE" || cmd.starts_with("PDSIZE ") => {
|
cmd if cmd == "PDSIZE" || cmd.starts_with("PDSIZE ") => {
|
||||||
let val_str = cmd.trim_start_matches("PDSIZE").trim();
|
let val_str = cmd.trim_start_matches("PDSIZE").trim();
|
||||||
if val_str.is_empty() {
|
if val_str.is_empty() {
|
||||||
|
|
|
||||||
|
|
@ -160,6 +160,7 @@ pub(super) struct DocumentTab {
|
||||||
pub(super) is_start: bool,
|
pub(super) is_start: bool,
|
||||||
/// Per-plugin document state (`plugin::BuiltinPlugin` manifest id → state).
|
/// Per-plugin document state (`plugin::BuiltinPlugin` manifest id → state).
|
||||||
pub(super) plugin_state: HashMap<&'static str, Box<dyn Any + Send + Sync>>,
|
pub(super) plugin_state: HashMap<&'static str, Box<dyn Any + Send + Sync>>,
|
||||||
|
pub(super) suspended_cmd: Option<Box<dyn CadCommand>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DocumentTab {
|
impl DocumentTab {
|
||||||
|
|
@ -213,6 +214,7 @@ impl DocumentTab {
|
||||||
last_synced_camera_gen: 0,
|
last_synced_camera_gen: 0,
|
||||||
is_start: false,
|
is_start: false,
|
||||||
plugin_state: HashMap::new(),
|
plugin_state: HashMap::new(),
|
||||||
|
suspended_cmd: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -242,6 +242,8 @@ pub(super) struct OpenCADStudio {
|
||||||
show_grid: bool,
|
show_grid: bool,
|
||||||
/// Dynamic input overlay (F12): show coordinate tooltip near cursor.
|
/// Dynamic input overlay (F12): show coordinate tooltip near cursor.
|
||||||
dyn_input: bool,
|
dyn_input: bool,
|
||||||
|
/// Controls whether the TEXTEDIT command repeats automatically (0 = Multiple, 1 = Single).
|
||||||
|
pub texteditmode: bool,
|
||||||
/// `true` after a bare `VPORTS` in model space — the next command-line
|
/// `true` after a bare `VPORTS` in model space — the next command-line
|
||||||
/// entry is treated as the tiled-config option (SIngle/2H/2V/4).
|
/// entry is treated as the tiled-config option (SIngle/2H/2V/4).
|
||||||
awaiting_vports: bool,
|
awaiting_vports: bool,
|
||||||
|
|
@ -1686,6 +1688,7 @@ impl OpenCADStudio {
|
||||||
polar_increment_deg: 45.0,
|
polar_increment_deg: 45.0,
|
||||||
show_grid: false,
|
show_grid: false,
|
||||||
dyn_input: true,
|
dyn_input: true,
|
||||||
|
texteditmode: false,
|
||||||
awaiting_vports: false,
|
awaiting_vports: false,
|
||||||
tile_drag: None,
|
tile_drag: None,
|
||||||
dyn_user_reshaped: false,
|
dyn_user_reshaped: false,
|
||||||
|
|
|
||||||
|
|
@ -628,15 +628,15 @@ impl super::OpenCADStudio {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Commit the editor — create a new MText or update the edited one.
|
/// Commit the editor — create a new MText or update the edited one.
|
||||||
pub(super) fn mtext_commit(&mut self) {
|
pub(super) fn mtext_commit(&mut self) -> bool {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let Some(ed) = self.mtext_editor.take() else { return };
|
let Some(ed) = self.mtext_editor.take() else { return false };
|
||||||
let body_empty = ed.content.text().trim().is_empty();
|
let body_empty = ed.content.text().trim().is_empty();
|
||||||
let mt = ed.build_mtext();
|
let mt = ed.build_mtext();
|
||||||
if body_empty {
|
if body_empty {
|
||||||
// Empty content: drop a new entity; leave an edited one untouched.
|
// Empty content: drop a new entity; leave an edited one untouched.
|
||||||
self.refresh_properties();
|
self.refresh_properties();
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
if let Some(h) = ed.editing {
|
if let Some(h) = ed.editing {
|
||||||
self.push_undo_snapshot(i, "MTEXT");
|
self.push_undo_snapshot(i, "MTEXT");
|
||||||
|
|
@ -666,6 +666,7 @@ impl super::OpenCADStudio {
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
}
|
}
|
||||||
self.refresh_properties();
|
self.refresh_properties();
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Discard the editor without changing the drawing.
|
/// Discard the editor without changing the drawing.
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,8 @@ pub struct UserSettings {
|
||||||
/// Linked plugin source repositories (`owner/repo`) the marketplace installs
|
/// Linked plugin source repositories (`owner/repo`) the marketplace installs
|
||||||
/// from.
|
/// from.
|
||||||
pub plugin_repos: Vec<String>,
|
pub plugin_repos: Vec<String>,
|
||||||
|
/// Controls whether the TEXTEDIT command repeats automatically (0 = Multiple, 1 = Single).
|
||||||
|
pub texteditmode: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for UserSettings {
|
impl Default for UserSettings {
|
||||||
|
|
@ -105,6 +107,7 @@ impl Default for UserSettings {
|
||||||
default_assoc_prompted: false,
|
default_assoc_prompted: false,
|
||||||
disabled_plugins: Vec::new(),
|
disabled_plugins: Vec::new(),
|
||||||
plugin_repos: Vec::new(),
|
plugin_repos: Vec::new(),
|
||||||
|
texteditmode: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -138,6 +141,13 @@ impl UserSettings {
|
||||||
"osnap" => s.snap_enabled = val == "1",
|
"osnap" => s.snap_enabled = val == "1",
|
||||||
"otrack" => s.otrack = val == "1",
|
"otrack" => s.otrack = val == "1",
|
||||||
"default_assoc_prompted" => s.default_assoc_prompted = val == "1",
|
"default_assoc_prompted" => s.default_assoc_prompted = val == "1",
|
||||||
|
"texteditmode" => {
|
||||||
|
if let Some(v) =
|
||||||
|
crate::modules::annotate::textedit::parse_texteditmode(val)
|
||||||
|
{
|
||||||
|
s.texteditmode = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
"disabled_plugins" => {
|
"disabled_plugins" => {
|
||||||
s.disabled_plugins = val
|
s.disabled_plugins = val
|
||||||
.split(',')
|
.split(',')
|
||||||
|
|
@ -179,7 +189,7 @@ impl UserSettings {
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(",");
|
.join(",");
|
||||||
let body = format!(
|
let body = format!(
|
||||||
"dyn={}\northo={}\npolar={}\npolar_increment_deg={}\nosnap={}\notrack={}\ndefault_assoc_prompted={}\nsnap_modes={}\ndisabled_plugins={}\nplugin_repos={}\n",
|
"dyn={}\northo={}\npolar={}\npolar_increment_deg={}\nosnap={}\notrack={}\ndefault_assoc_prompted={}\nsnap_modes={}\ndisabled_plugins={}\nplugin_repos={}\ntexteditmode={}\n",
|
||||||
b(self.dyn_input),
|
b(self.dyn_input),
|
||||||
b(self.ortho),
|
b(self.ortho),
|
||||||
b(self.polar),
|
b(self.polar),
|
||||||
|
|
@ -190,6 +200,7 @@ impl UserSettings {
|
||||||
modes,
|
modes,
|
||||||
self.disabled_plugins.join(","),
|
self.disabled_plugins.join(","),
|
||||||
self.plugin_repos.join(","),
|
self.plugin_repos.join(","),
|
||||||
|
self.texteditmode,
|
||||||
);
|
);
|
||||||
let _ = std::fs::write(path, body);
|
let _ = std::fs::write(path, body);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,25 @@ pub struct TextInlineState {
|
||||||
/// Canvas-space anchor where the field is drawn (the insertion-point click).
|
/// Canvas-space anchor where the field is drawn (the insertion-point click).
|
||||||
pub screen_anchor: iced::Point,
|
pub screen_anchor: iced::Point,
|
||||||
}
|
}
|
||||||
|
pub(super) fn can_edit_text(mut handle: Handle, document: &acadrust::CadDocument) -> bool {
|
||||||
|
for _ in 0..8 {
|
||||||
|
match document.get_entity(handle) {
|
||||||
|
Some(acadrust::EntityType::Leader(l)) => {
|
||||||
|
let ann = l.annotation_handle;
|
||||||
|
if ann.is_null() || ann == handle {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
handle = ann;
|
||||||
|
}
|
||||||
|
_ => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(entity) = document.get_entity(handle) {
|
||||||
|
read_text_field(entity).is_some()
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl super::OpenCADStudio {
|
impl super::OpenCADStudio {
|
||||||
/// Open the right in-place editor for `handle`: the plain box for single-
|
/// Open the right in-place editor for `handle`: the plain box for single-
|
||||||
|
|
@ -175,12 +194,12 @@ impl super::OpenCADStudio {
|
||||||
|
|
||||||
/// Commit the editor: create a new TEXT entity or update the edited slot.
|
/// Commit the editor: create a new TEXT entity or update the edited slot.
|
||||||
/// Empty content drops a new entity and leaves an edited one untouched.
|
/// Empty content drops a new entity and leaves an edited one untouched.
|
||||||
pub(super) fn text_inline_commit(&mut self) {
|
pub(super) fn text_inline_commit(&mut self) -> bool {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let Some(ed) = self.text_inline.take() else { return };
|
let Some(ed) = self.text_inline.take() else { return false };
|
||||||
if ed.value.trim().is_empty() && ed.editing.is_none() {
|
if ed.value.trim().is_empty() && ed.editing.is_none() {
|
||||||
self.refresh_properties();
|
self.refresh_properties();
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
if let Some(h) = ed.editing {
|
if let Some(h) = ed.editing {
|
||||||
self.push_undo_snapshot(i, "TEXT");
|
self.push_undo_snapshot(i, "TEXT");
|
||||||
|
|
@ -211,6 +230,7 @@ impl super::OpenCADStudio {
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
}
|
}
|
||||||
self.refresh_properties();
|
self.refresh_properties();
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Discard the editor without changing the drawing.
|
/// Discard the editor without changing the drawing.
|
||||||
|
|
|
||||||
|
|
@ -195,6 +195,7 @@ impl OpenCADStudio {
|
||||||
v
|
v
|
||||||
},
|
},
|
||||||
plugin_repos: self.plugin_repos.clone(),
|
plugin_repos: self.plugin_repos.clone(),
|
||||||
|
texteditmode: self.texteditmode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -210,9 +211,23 @@ impl OpenCADStudio {
|
||||||
self.default_assoc_prompted = s.default_assoc_prompted;
|
self.default_assoc_prompted = s.default_assoc_prompted;
|
||||||
self.disabled_plugins = s.disabled_plugins.iter().cloned().collect();
|
self.disabled_plugins = s.disabled_plugins.iter().cloned().collect();
|
||||||
self.plugin_repos = s.plugin_repos.clone();
|
self.plugin_repos = s.plugin_repos.clone();
|
||||||
|
self.texteditmode = s.texteditmode;
|
||||||
self.rebuild_ribbon_modules();
|
self.rebuild_ribbon_modules();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if a suspended command exists on the active tab and resume it
|
||||||
|
/// with the outcome of the text editor.
|
||||||
|
pub(super) fn post_editor_closed(&mut self, committed: bool) -> Task<Message> {
|
||||||
|
let i = self.active_tab;
|
||||||
|
if let Some(mut cmd) = self.tabs[i].suspended_cmd.take() {
|
||||||
|
let res = cmd.on_editor_closed(committed);
|
||||||
|
self.tabs[i].active_cmd = Some(cmd);
|
||||||
|
self.apply_cmd_result(res)
|
||||||
|
} else {
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Rebuild the ribbon's tab list from the registry, dropping the tabs of any
|
/// Rebuild the ribbon's tab list from the registry, dropping the tabs of any
|
||||||
/// disabled plugins. Call after `disabled_plugins` changes.
|
/// disabled plugins. Call after `disabled_plugins` changes.
|
||||||
pub(super) fn rebuild_ribbon_modules(&mut self) {
|
pub(super) fn rebuild_ribbon_modules(&mut self) {
|
||||||
|
|
@ -1806,12 +1821,12 @@ impl OpenCADStudio {
|
||||||
// Open MText editor swallows Escape (cancel without committing).
|
// Open MText editor swallows Escape (cancel without committing).
|
||||||
if self.mtext_editor.is_some() {
|
if self.mtext_editor.is_some() {
|
||||||
self.mtext_cancel();
|
self.mtext_cancel();
|
||||||
return Task::none();
|
return self.post_editor_closed(false);
|
||||||
}
|
}
|
||||||
// The in-place TEXT editor likewise cancels on Escape.
|
// The in-place TEXT editor likewise cancels on Escape.
|
||||||
if self.text_inline.is_some() {
|
if self.text_inline.is_some() {
|
||||||
self.text_inline_cancel();
|
self.text_inline_cancel();
|
||||||
return Task::none();
|
return self.post_editor_closed(false);
|
||||||
}
|
}
|
||||||
// Grip popup intercepts Escape — dismisses the menu
|
// Grip popup intercepts Escape — dismisses the menu
|
||||||
// without doing anything else.
|
// without doing anything else.
|
||||||
|
|
@ -4905,12 +4920,12 @@ impl OpenCADStudio {
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::MTextOk => {
|
Message::MTextOk => {
|
||||||
self.mtext_commit();
|
let committed = self.mtext_commit();
|
||||||
Task::none()
|
self.post_editor_closed(committed)
|
||||||
}
|
}
|
||||||
Message::MTextCancel => {
|
Message::MTextCancel => {
|
||||||
self.mtext_cancel();
|
self.mtext_cancel();
|
||||||
Task::none()
|
self.post_editor_closed(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::TextInlineInput(s) => {
|
Message::TextInlineInput(s) => {
|
||||||
|
|
@ -4971,8 +4986,8 @@ impl OpenCADStudio {
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::TextInlineOk => {
|
Message::TextInlineOk => {
|
||||||
self.text_inline_commit();
|
let committed = self.text_inline_commit();
|
||||||
Task::none()
|
self.post_editor_closed(committed)
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::DrawOrderSubmenuToggle => {
|
Message::DrawOrderSubmenuToggle => {
|
||||||
|
|
|
||||||
|
|
@ -240,6 +240,12 @@ pub enum CmdResult {
|
||||||
entity: EntityType,
|
entity: EntityType,
|
||||||
finish: bool,
|
finish: bool,
|
||||||
},
|
},
|
||||||
|
/// Suspends command execution, moves it to suspended_cmd, and opens the text editor for the given handle.
|
||||||
|
SuspendForTextEdit { handle: Handle },
|
||||||
|
/// Requests a standard document-level undo while keeping the command active.
|
||||||
|
UndoDocument,
|
||||||
|
/// Sets the TEXTEDITMODE system variable and ends the command.
|
||||||
|
SetTexteditMode(bool),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What kind of value the active command is currently asking for. Drives
|
/// What kind of value the active command is currently asking for. Drives
|
||||||
|
|
@ -427,6 +433,11 @@ pub trait CadCommand: Send {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Called when the text editor closes, either because the user committed or cancelled the edit.
|
||||||
|
fn on_editor_closed(&mut self, _committed: bool) -> CmdResult {
|
||||||
|
CmdResult::Cancel
|
||||||
|
}
|
||||||
|
|
||||||
/// Called when the user clicks and `needs_entity_pick()` is true.
|
/// Called when the user clicks and `needs_entity_pick()` is true.
|
||||||
/// `handle` is the nearest wire's entity handle (Handle::NULL if nothing found).
|
/// `handle` is the nearest wire's entity handle (Handle::NULL if nothing found).
|
||||||
fn on_entity_pick(&mut self, _handle: Handle, _pt: Vec3) -> CmdResult {
|
fn on_entity_pick(&mut self, _handle: Handle, _pt: Vec3) -> CmdResult {
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ pub mod qdim;
|
||||||
pub mod radius_dim;
|
pub mod radius_dim;
|
||||||
pub mod table_cmd;
|
pub mod table_cmd;
|
||||||
pub mod text;
|
pub mod text;
|
||||||
|
pub mod textedit;
|
||||||
pub mod tolerance_cmd;
|
pub mod tolerance_cmd;
|
||||||
|
|
||||||
use crate::modules::{CadModule, RibbonGroup, RibbonItem, StyleKey};
|
use crate::modules::{CadModule, RibbonGroup, RibbonItem, StyleKey};
|
||||||
|
|
|
||||||
253
src/modules/annotate/textedit.rs
Normal file
253
src/modules/annotate/textedit.rs
Normal file
|
|
@ -0,0 +1,253 @@
|
||||||
|
// TEXTEDIT — edit multiline text, single-line text, or dimension text in-place.
|
||||||
|
//
|
||||||
|
// Workflow:
|
||||||
|
// 1. Enters a loop prompting to select an annotation object.
|
||||||
|
// 2. Accepts keyword options: Undo (to revert the last edit) and Mode (to switch Single/Multiple).
|
||||||
|
// 3. In Multiple mode (default), editing an object suspends the command, opens the editor,
|
||||||
|
// and when closed, resumes the command loop.
|
||||||
|
// 4. In Single mode, editing an object exits the command immediately.
|
||||||
|
|
||||||
|
use acadrust::Handle;
|
||||||
|
use glam::Vec3;
|
||||||
|
|
||||||
|
use crate::command::{CadCommand, CmdResult};
|
||||||
|
|
||||||
|
/// Parse a TEXTEDITMODE value. Accepts `0`/`m`/`multiple`/`false` (Multiple → false)
|
||||||
|
/// and `1`/`s`/`single`/`true` (Single → true), case-insensitive. Returns `None`
|
||||||
|
/// for anything else so callers can re-prompt or report the error.
|
||||||
|
pub fn parse_texteditmode(s: &str) -> Option<bool> {
|
||||||
|
match s.trim().to_lowercase().as_str() {
|
||||||
|
"0" | "m" | "multiple" | "false" => Some(false),
|
||||||
|
"1" | "s" | "single" | "true" => Some(true),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
pub enum Step {
|
||||||
|
PickObject,
|
||||||
|
EnterMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
pub enum TextEditMode {
|
||||||
|
Single,
|
||||||
|
Multiple,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TexteditCommand {
|
||||||
|
mode: TextEditMode,
|
||||||
|
edit_count: usize,
|
||||||
|
step: Step,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TexteditCommand {
|
||||||
|
pub fn new(texteditmode: bool) -> Self {
|
||||||
|
let mode = if texteditmode {
|
||||||
|
TextEditMode::Single
|
||||||
|
} else {
|
||||||
|
TextEditMode::Multiple
|
||||||
|
};
|
||||||
|
Self {
|
||||||
|
mode,
|
||||||
|
edit_count: 0,
|
||||||
|
step: Step::PickObject,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CadCommand for TexteditCommand {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"TEXTEDIT"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt(&self) -> String {
|
||||||
|
match self.step {
|
||||||
|
Step::PickObject => {
|
||||||
|
if self.edit_count == 0 {
|
||||||
|
"TEXTEDIT Select an annotation object or [Undo Mode]:".to_string()
|
||||||
|
} else {
|
||||||
|
"TEXTEDIT Select an annotation object or [Undo Mode] <exit>:".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Step::EnterMode => {
|
||||||
|
format!(
|
||||||
|
"TEXTEDIT Enter text edit mode [Single/Multiple] <{}>:",
|
||||||
|
match self.mode {
|
||||||
|
TextEditMode::Single => "Single",
|
||||||
|
TextEditMode::Multiple => "Multiple",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn needs_entity_pick(&self) -> bool {
|
||||||
|
self.step == Step::PickObject
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_entity_pick(&mut self, handle: Handle, _pt: Vec3) -> CmdResult {
|
||||||
|
if handle.is_null() {
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
}
|
||||||
|
CmdResult::SuspendForTextEdit { handle }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wants_text_input(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
|
let text = text.trim();
|
||||||
|
match self.step {
|
||||||
|
Step::PickObject => {
|
||||||
|
if text.is_empty() {
|
||||||
|
if self.edit_count > 0 {
|
||||||
|
return Some(CmdResult::Cancel);
|
||||||
|
} else {
|
||||||
|
return Some(CmdResult::NeedPoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let lower = text.to_lowercase();
|
||||||
|
if lower == "u" || lower == "undo" {
|
||||||
|
if self.edit_count > 0 {
|
||||||
|
self.edit_count -= 1;
|
||||||
|
return Some(CmdResult::UndoDocument);
|
||||||
|
} else {
|
||||||
|
return Some(CmdResult::NeedPoint);
|
||||||
|
}
|
||||||
|
} else if lower == "m" || lower == "mode" {
|
||||||
|
self.step = Step::EnterMode;
|
||||||
|
return Some(CmdResult::NeedPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
|
Step::EnterMode => {
|
||||||
|
if text.is_empty() {
|
||||||
|
self.step = Step::PickObject;
|
||||||
|
return Some(CmdResult::NeedPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
let lower = text.to_lowercase();
|
||||||
|
if lower == "s" || lower == "single" || lower == "1" {
|
||||||
|
self.mode = TextEditMode::Single;
|
||||||
|
self.step = Step::PickObject;
|
||||||
|
} else if lower == "m" || lower == "multiple" || lower == "0" {
|
||||||
|
self.mode = TextEditMode::Multiple;
|
||||||
|
self.step = Step::PickObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_point(&mut self, _pt: Vec3) -> CmdResult {
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
|
match self.step {
|
||||||
|
Step::PickObject => {
|
||||||
|
if self.edit_count > 0 {
|
||||||
|
CmdResult::Cancel
|
||||||
|
} else {
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Step::EnterMode => {
|
||||||
|
self.step = Step::PickObject;
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_escape(&mut self) -> CmdResult {
|
||||||
|
CmdResult::Cancel
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_editor_closed(&mut self, committed: bool) -> CmdResult {
|
||||||
|
if committed {
|
||||||
|
self.edit_count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.mode {
|
||||||
|
TextEditMode::Single => CmdResult::Cancel,
|
||||||
|
TextEditMode::Multiple => CmdResult::NeedPoint,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TexteditmodeCommand {
|
||||||
|
current: bool,
|
||||||
|
/// Set when the last value entry was unrecognized, so the next prompt
|
||||||
|
/// leads with the error instead of silently re-asking.
|
||||||
|
invalid: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TexteditmodeCommand {
|
||||||
|
pub fn new(current: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
current,
|
||||||
|
invalid: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CadCommand for TexteditmodeCommand {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"TEXTEDITMODE"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt(&self) -> String {
|
||||||
|
let v = if self.current { 1 } else { 0 };
|
||||||
|
let prefix = if self.invalid {
|
||||||
|
"Requires 0 OR 1 OR MULTIPLE OR SINGLE. "
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
format!("{prefix}TEXTEDITMODE Enter new value for TEXTEDITMODE <{v}>:")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wants_text_input(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
|
let text = text.trim();
|
||||||
|
if text.is_empty() {
|
||||||
|
return Some(CmdResult::Cancel);
|
||||||
|
}
|
||||||
|
|
||||||
|
match parse_texteditmode(text) {
|
||||||
|
Some(v) => Some(CmdResult::SetTexteditMode(v)),
|
||||||
|
// Unrecognized value: stay in the command and re-prompt with the
|
||||||
|
// error, rather than aborting via the Measurement arm.
|
||||||
|
None => {
|
||||||
|
self.invalid = true;
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_point(&mut self, _pt: Vec3) -> CmdResult {
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
|
CmdResult::Cancel
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_escape(&mut self) -> CmdResult {
|
||||||
|
CmdResult::Cancel
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dyn_field(&self) -> crate::command::DynField {
|
||||||
|
crate::command::DynField::Scalar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Autocomplete registry ─────────────────────────────────
|
||||||
|
inventory::submit!(crate::command::CommandRegistration { names: &["TEXTEDIT", "TEDIT", "TEXTEDITMODE"] });
|
||||||
Loading…
Reference in a new issue