TEXTEDIT prompt, Undo and Mode; MTEXT takes lines until an empty one (#234) (#239)

TEXTEDIT's prompt no longer reads "or or". Undo reverts the last edit made
in this TEXTEDIT, and Mode switches between Multiple (keep selecting) and
Single (end after one edit). At the command line and in scripts, MTEXT
takes one paragraph per input line until an empty line, instead of ending
after the first line and running the next one as a command. The Text
Editor window sends its contents and then Enter, so it still ends MTEXT.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Valentin Klein
2026-10-10 14:37:52 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent a6b728b0f7
commit ca882b38e2
5 changed files with 126 additions and 10 deletions
+11 -3
View File
@@ -1315,6 +1315,9 @@ impl Interactive for TextM {
struct MTextM {
first: Option<Vec2>,
second: Option<Vec2>,
/// Paragraphs typed so far: one per input line, an empty line ends the text (as on
/// AutoCAD's command line and in scripts).
lines: Vec<String>,
}
impl Interactive for MTextM {
@@ -1327,14 +1330,19 @@ impl Interactive for MTextM {
(Some(a), None) => Prompt::new("Specify opposite corner", Accept::POINT)
.kw(&["Height", "Justify", "Line spacing", "Rotation", "Style", "Width", "Columns"])
.base(a),
_ => Prompt::new("Enter text (use \\P for new paragraphs)", Accept::TEXT),
_ if self.lines.is_empty() => Prompt::new("Enter text (use \\P for new paragraphs)", Accept::TEXT),
_ => Prompt::new("Enter next line of text (empty line to finish)", Accept::TEXT),
}
}
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
match (self.first, self.second, i) {
(None, _, Input::Point(p)) => self.first = Some(p),
(Some(_), None, Input::Point(p)) => self.second = Some(p),
(Some(a), Some(b), Input::Text(t)) => {
(Some(_), Some(_), Input::Text(t)) if !t.is_empty() => self.lines.push(t),
(Some(a), Some(b), Input::Text(_) | Input::Enter) => {
if self.lines.is_empty() {
return Ok(Step::Done);
}
let d = s.doc()?;
let h = d.header.f64("TEXTSIZE", 0.2);
let style = d.header.str("TEXTSTYLE", "Standard");
@@ -1346,7 +1354,7 @@ impl Interactive for MTextM {
attach: 1,
rotation: 0.0,
style,
contents: t,
contents: self.lines.join("\\P"),
line_spacing: 1.0,
}))?;
return Ok(Step::Done);
+38 -5
View File
@@ -2269,6 +2269,12 @@ fn run_textedit(s: &mut Session, p: &Value) -> Result<Value> {
#[derive(Default)]
struct TextEditM {
h: Option<(Handle, String)>,
/// Edits made in this run, newest last: each object as it was before (the Undo option).
edits: Vec<(Handle, EntityKind)>,
/// Single mode: end after one edit (the Mode option; Multiple by default).
single: bool,
/// Asking for the edit mode.
mode: bool,
}
impl Interactive for TextEditM {
@@ -2276,12 +2282,26 @@ impl Interactive for TextEditM {
"TEXTEDIT"
}
fn prompt(&self, _s: &Session) -> Prompt {
if self.mode {
let current = if self.single { "Single" } else { "Multiple" };
return Prompt::new("Enter a text edit mode option", curves::KW).kw(&["Single", "Multiple"]).default(current);
}
match &self.h {
None => Prompt::new("Select an annotation object or", Accept::POINT).kw(&["Undo", "Mode"]),
None => Prompt::new("Select an annotation object", Accept::POINT).kw(&["Undo", "Mode"]),
Some((_, old)) => Prompt::new("Enter new text", Accept::TEXT).default(old.chars().take(40).collect::<String>()),
}
}
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
if self.mode {
match i {
Input::Keyword(k) if k == "Single" => self.single = true,
Input::Keyword(k) if k == "Multiple" => self.single = false,
Input::Enter => {}
_ => return Err(other("Enter Single or Multiple.")),
}
self.mode = false;
return Ok(Step::Continue);
}
match (&self.h, i) {
(None, Input::Point(p)) => {
let h = curves::pick_at(s, p).ok_or_else(|| other("*Invalid selection*"))?;
@@ -2290,16 +2310,29 @@ impl Interactive for TextEditM {
Ok(Step::Continue)
}
(Some((h, _)), Input::Text(t) | Input::Keyword(t)) => {
set_text(s, *h, &t)?;
let h = *h;
let before = curves::entity(s, h)?.kind;
set_text(s, h, &t)?;
self.edits.push((h, before));
self.h = None;
Ok(Step::Continue)
Ok(if self.single { Step::Done } else { Step::Continue })
}
(Some(_), Input::Enter) => {
self.h = None;
Ok(Step::Continue)
Ok(if self.single { Step::Done } else { Step::Continue })
}
(None, Input::Enter) => Ok(Step::Done),
(None, Input::Keyword(_)) => Ok(Step::Continue),
(None, Input::Keyword(k)) if k == "Undo" => {
match self.edits.pop() {
Some((h, before)) => set_kind(s, h, before)?,
None => s.echo("Everything has been undone"),
}
Ok(Step::Continue)
}
(None, Input::Keyword(k)) if k == "Mode" => {
self.mode = true;
Ok(Step::Continue)
}
_ => Ok(Step::Continue),
}
}
+69
View File
@@ -0,0 +1,69 @@
//! MTEXT contents from scripts and the command line; TEXTEDIT's prompt and options.
use cadcraft_engine::Session;
use cadcraft_engine::doc::{EntityKind, Space};
fn kinds(s: &Session) -> Vec<EntityKind> {
s.doc().ok().and_then(|d| d.space(&Space::Model)).map(|st| st.iter().map(|e| e.kind.clone()).collect()).unwrap_or_default()
}
fn mtexts(s: &Session) -> Vec<String> {
kinds(s)
.into_iter()
.filter_map(|k| match k {
EntityKind::MText(t) => Some(t.contents),
_ => None,
})
.collect()
}
fn texts(s: &Session) -> Vec<String> {
kinds(s)
.into_iter()
.filter_map(|k| match k {
EntityKind::Text(t) => Some(t.value),
_ => None,
})
.collect()
}
#[test]
fn script_mtext_takes_lines_until_an_empty_line() {
let mut s = Session::new();
s.script("MTEXT 0,10 20,0\nFirst line\nSecond line\n\nLINE 0,0 5,5\n\n").unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
assert_eq!(mtexts(&s), vec!["First line\\PSecond line".to_string()]);
assert_eq!(kinds(&s).len(), 2, "{:?}", kinds(&s));
// Typed at the command line: spaces stay in the text, Enter on an empty line ends it.
let mut s = Session::new();
s.cmdline("MTEXT 0,10 20,0").unwrap();
s.cmdline("Hello world").unwrap();
assert!(s.running.is_some());
s.cmdline("").unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
assert_eq!(mtexts(&s), vec!["Hello world".to_string()]);
}
#[test]
fn textedit_prompt_mode_and_undo() {
let mut s = Session::new();
s.cmdline("TEXT 0,0 1 0 old").unwrap();
s.cmdline("").unwrap();
s.cmdline("TEXTEDIT").unwrap();
let p = s.prompt_text();
assert!(!p.contains("or or"), "{p}");
// Edit, then Undo restores the old text.
s.cmdline("0.5,0.5").unwrap();
s.cmdline("new").unwrap();
assert_eq!(texts(&s), vec!["new".to_string()]);
s.cmdline("u").unwrap();
assert_eq!(texts(&s), vec!["old".to_string()]);
// Mode Single: the command ends after one edit.
s.cmdline("m").unwrap();
s.cmdline("s").unwrap();
s.cmdline("0.5,0.5").unwrap();
s.cmdline("once").unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
assert_eq!(texts(&s), vec!["once".to_string()]);
}
+7 -1
View File
@@ -388,7 +388,13 @@ fn mtext_editor(app: &mut CadApp, ctx: &egui::Context) {
ctx.data_mut(|d| d.insert_temp(id, buf));
if let Some(t) = submit {
ctx.data_mut(|d| d.remove::<String>(id));
let _ = app.session.input(cadcraft_engine::Input::Text(t));
// The editor holds the whole contents: one input, then Enter ends MTEXT's text entry.
if !t.is_empty() {
let _ = app.session.input(cadcraft_engine::Input::Text(t));
}
if app.session.running.as_ref().is_some_and(|r| r.id == "mtext") {
let _ = app.session.input(cadcraft_engine::Input::Enter);
}
} else if cancel {
ctx.data_mut(|d| d.remove::<String>(id));
app.session.cancel();
+1 -1
View File
@@ -612,7 +612,7 @@
Select a line. Виберіть відрізок.
Select a polyline. Виберіть полілінію.
Select a text, mtext, attribute definition or dimension. Виберіть текст, багаторядковий текст, визначення атрибута або розмір.
Select an annotation object or Виберіть об'єкт анотації або
Select an annotation object Виберіть об'єкт анотації
Select an arc or circle Виберіть дугу або коло
Select an arc. Виберіть дугу.
Select an object Виберіть об'єкт
1 # CADCraft Ukrainian interface catalog — original translations by @dmatviichuk.
612
613
614
615
616
617
618