From 4fa6c4944510eed447f20cfb3c078c693fcdf8c3 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 16 Jul 2026 13:19:04 +0300 Subject: [PATCH] feat(commands): prompt ATTDISP and LAYERSTATE by keyword Add KeywordCommand, a generic stepped front-end for keyword sub-verb commands: bare NAME shows the sub-verbs as clickable buttons, and picking one either dispatches `NAME ` (action verbs like ATTDISP On/Off/Normal) or prompts for an argument and dispatches `NAME ` (LAYERSTATE Save/Restore/Delete ). It delegates to the existing inline handler, so the logic stays in one place. First two of bucket B converted; the rest (VIEW, LINETYPE, SCALELISTEDIT, SETVAR, LAYDEL/LAYMRG, and the selection-first XDATA/UNDERLAY/ADJUST/DRAWORDER/CHPROP) can follow the same pattern. --- src/app/commands/draw.rs | 16 ++++++- src/app/commands/layers.rs | 20 ++++++-- src/command.rs | 96 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 5 deletions(-) diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index c8ca72813..835e50284 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -149,7 +149,21 @@ impl OpenCADStudio { // ATTDISP ON — make all AttributeDefinitions visible // ATTDISP OFF — make all AttributeDefinitions invisible // ATTDISP NORMAL — restore: show only those without the invisible flag - cmd if cmd == "ATTDISP" || cmd.starts_with("ATTDISP ") => { + "ATTDISP" => { + use crate::command::KeywordCommand; + let c = KeywordCommand::new( + "ATTDISP", + "ATTDISP attribute display [On / Off / Normal]:", + vec![ + ("On", "ON", None), + ("Off", "OFF", None), + ("Normal", "NORMAL", None), + ], + ); + self.command_line.push_info(&c.prompt()); + self.tabs[i].active_cmd = Some(Box::new(c)); + } + cmd if cmd.starts_with("ATTDISP ") => { let sub = cmd.split_whitespace().nth(1).unwrap_or("").to_uppercase(); match sub.as_str() { "ON" | "OFF" | "NORMAL" => { diff --git a/src/app/commands/layers.rs b/src/app/commands/layers.rs index 403edb1f4..53c8d78da 100644 --- a/src/app/commands/layers.rs +++ b/src/app/commands/layers.rs @@ -191,10 +191,22 @@ impl OpenCADStudio { // LAYERSTATE — save / restore named snapshots of all layer states // (on/off, freeze, lock, colour, linetype, lineweight). // LAYERSTATE SAVE | RESTORE | DELETE | ? (list) - cmd if cmd == "LAYERSTATE" - || cmd == "LAS" - || cmd == "LMAN" - || cmd.starts_with("LAYERSTATE ") + "LAYERSTATE" | "LAS" | "LMAN" => { + use crate::command::KeywordCommand; + let c = KeywordCommand::new( + "LAYERSTATE", + "LAYERSTATE [List / Save / Restore / Delete]:", + vec![ + ("List", "LIST", None), + ("Save", "SAVE", Some("LAYERSTATE SAVE new state name:")), + ("Restore", "RESTORE", Some("LAYERSTATE RESTORE state name:")), + ("Delete", "DELETE", Some("LAYERSTATE DELETE state name:")), + ], + ); + self.command_line.push_info(&c.prompt()); + self.tabs[i].active_cmd = Some(Box::new(c)); + } + cmd if cmd.starts_with("LAYERSTATE ") || cmd.starts_with("LAS ") || cmd.starts_with("LMAN ") => { diff --git a/src/command.rs b/src/command.rs index 1eca915c8..b696d338f 100644 --- a/src/command.rs +++ b/src/command.rs @@ -313,6 +313,102 @@ impl CadCommand for UserRegCommand { } } +/// Generic interactive front-end for a keyword sub-verb command (ATTDISP, +/// LAYERSTATE, SCALELISTEDIT, VIEW…). Bare `` enters this command, which +/// shows the sub-verbs as clickable buttons; picking one either dispatches +/// ` ` straight away (an action verb) or, when the verb needs an +/// argument, prompts for it and then dispatches ` `. +/// Delegates to the existing inline ` …` handler so the logic stays in +/// one place, the way [`ValuePromptCommand`] does for single values. +pub struct KeywordCommand { + name: &'static str, + prompt: &'static str, + /// `(button label, keyword, value prompt)`. A `Some` value prompt means the + /// verb takes one argument collected in a second step; `None` acts alone. + options: Vec<(&'static str, &'static str, Option<&'static str>)>, + /// Set once a value-taking verb is chosen: `(keyword, value prompt)`. + pending: Option<(&'static str, &'static str)>, +} + +impl KeywordCommand { + pub fn new( + name: &'static str, + prompt: &'static str, + options: Vec<(&'static str, &'static str, Option<&'static str>)>, + ) -> Self { + Self { + name, + prompt, + options, + pending: None, + } + } +} + +impl CadCommand for KeywordCommand { + fn name(&self) -> &'static str { + self.name + } + + fn prompt(&self) -> String { + match self.pending { + Some((_, value_prompt)) => value_prompt.to_string(), + None => self.prompt.to_string(), + } + } + + fn options(&self) -> Vec { + match self.pending { + Some(_) => Vec::new(), + None => self + .options + .iter() + .map(|(label, keyword, _)| CmdOption::new(label, keyword)) + .collect(), + } + } + + fn wants_text_input(&self) -> bool { + true + } + + fn on_text_input(&mut self, text: &str) -> Option { + let t = text.trim(); + if t.is_empty() { + return None; + } + match self.pending { + // Second step: the argument for the already-chosen verb. + Some((keyword, _)) => Some(CmdResult::Dispatch(format!("{} {keyword} {t}", self.name))), + // First step: match the typed / clicked token to a sub-verb. + None => { + let up = t.to_uppercase(); + let Some((_, keyword, value_prompt)) = + self.options.iter().find(|(_, k, _)| k.eq_ignore_ascii_case(&up)) + else { + // Unknown verb — keep prompting rather than dispatch garbage. + return None; + }; + match value_prompt { + Some(vp) => { + self.pending = Some((keyword, vp)); + None + } + None => Some(CmdResult::Dispatch(format!("{} {keyword}", self.name))), + } + } + } + } + + fn on_point(&mut self, _pt: DVec3) -> CmdResult { + CmdResult::NeedPoint + } + + fn on_enter(&mut self) -> CmdResult { + CmdResult::Cancel + } +} + // ── Result token ────────────────────────────────────────────────────────── /// Returned by every `CadCommand` method to tell main.rs what to do.