Shortcut menus: command line, drawing and layout tabs, Layers and Properties palettes (fixes #512) (#518)

Squash of PR #518 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Valentin Klein
2026-10-11 12:09:56 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 48cce34a52
commit fa0ce3c70f
11 changed files with 454 additions and 13 deletions
+6 -4
View File
@@ -192,7 +192,7 @@ pub fn file_tabs(app: &mut CadApp, ui: &mut egui::Ui) {
app.ui.start_tab = false;
}
x += 24.0;
let mut tab = |ui: &mut egui::Ui, label: &str, active: bool, closable: bool, w: f32, id: egui::Id| -> (bool, bool) {
let mut tab = |ui: &mut egui::Ui, label: &str, active: bool, closable: bool, w: f32, id: egui::Id| -> (bool, bool, egui::Response) {
let tr = Rect::from_min_size(pos2(x, r.top()), vec2(w, r.height()));
let resp = ui.interact(tr, id, Sense::click());
ui.painter().rect_filled(
@@ -224,9 +224,9 @@ pub fn file_tabs(app: &mut CadApp, ui: &mut egui::Ui) {
icons::describe_control(ui, &resp, label, Some(active), true);
ui.painter().vline(tr.right(), tr.y_range(), Stroke::new(1.0, t.border));
x += w;
(resp.clicked(), close)
(resp.clicked(), close, resp)
};
let (c, _) = tab(ui, crate::tl!("Start"), app.ui.start_tab || app.session.docs.is_empty(), false, 110.0, ui.id().with("tab_start"));
let (c, _, _) = tab(ui, crate::tl!("Start"), app.ui.start_tab || app.session.docs.is_empty(), false, 110.0, ui.id().with("tab_start"));
if c {
app.ui.start_tab = true;
}
@@ -240,7 +240,8 @@ pub fn file_tabs(app: &mut CadApp, ui: &mut egui::Ui) {
.map(|(i, d)| (i, format!("{}{}", d.title, if d.is_dirty() { "*" } else { "" }), i == app.session.active))
.collect();
for (i, title, active) in titles {
let (c, x) = tab(ui, &title, active && !app.ui.start_tab, true, 170.0, ui.id().with(("tab", i)));
let (c, x, resp) = tab(ui, &title, active && !app.ui.start_tab, true, 170.0, ui.id().with(("tab", i)));
crate::shortcut_menus::attach(app, &resp, |app| crate::shortcut_menus::drawing_tab_entries(app, i));
if c {
switch_to = Some(i);
}
@@ -311,6 +312,7 @@ pub fn status_bar(app: &mut CadApp, ui: &mut egui::Ui) {
if resp.clicked() {
switch = Some(name.clone());
}
crate::shortcut_menus::attach(app, &resp, |app| crate::shortcut_menus::layout_tab_entries(app, name));
x += w + 2.0;
if i == 0 {
let pr = Rect::from_min_size(pos2(x, r.top() + 5.0), vec2(16.0, 16.0));
+6
View File
@@ -52,6 +52,12 @@ impl CadApp {
}
}
/// Close every drawing but the one at `keep`, asking about each one with unsaved changes.
pub fn request_close_others(&mut self, keep: usize) {
let uids = self.session.docs.iter().enumerate().filter(|(i, _)| *i != keep).map(|(_, d)| d.uid).collect();
self.begin_close(uids);
}
/// Close every drawing as the user asked for it, asking about each one with unsaved changes.
pub fn request_close_all(&mut self) {
let uids = self.session.docs.iter().map(|d| d.uid).collect();
+1
View File
@@ -453,6 +453,7 @@ fn history(app: &mut CadApp, ui: &mut egui::Ui, canvas: Rect, bar: Rect) {
if resp.clicked() && resp.interact_pointer_pos().is_some_and(on_toggle) {
let _ = app.run("ui.toggle.history", serde_json::json!({}));
}
crate::shortcut_menus::attach(app, &resp, crate::shortcut_menus::command_line_entries);
if let Some(r) = zones(app, canvas, bar).window {
history_window(app, ui, r);
}
+3 -4
View File
@@ -345,10 +345,9 @@ pub fn show(app: &mut crate::CadApp, ui: &mut egui::Ui) {
.show(ui, |ui| crate::palettes::layers_section(app, ui));
}
Panel::Properties => {
egui::ScrollArea::vertical()
.id_salt("dock-properties")
.auto_shrink([false, false])
.show(ui, |ui| crate::palettes::properties_section(app, ui));
egui::ScrollArea::vertical().id_salt("dock-properties").auto_shrink([false, false]).show(ui, |ui| {
crate::shortcut_menus::palette(app, ui, crate::palettes::properties_section, crate::shortcut_menus::properties_entries)
});
}
},
);
+9
View File
@@ -112,6 +112,7 @@
Clipboard Буфер обміну
Close Закрити
Close All Закрити всі
Close All Other Drawings Закрити всі інші креслення
Closed Замкнений
Coincident Збіг
Collapse Tool Sets Згорнути набори інструментів
@@ -122,6 +123,7 @@
Color Index Індекс кольору
Color... Колір...
Command History Історія команд
Clear Command History Очистити історію команд
Command Line Командний рядок
Command Line History Lines Рядки історії командного рядка
Command Reference Довідник команд
@@ -138,6 +140,8 @@
Contributors Учасники
Control Vertices Керувальні вершини
Copy Копіювати
Copy Full File Path Копіювати повний шлях до файлу
Copy History Копіювати історію
Copy Layout Копіювати аркуш
Copy Nested Objects Копіювати вкладені об'єкти
Copy Selection Копіювати вибране
@@ -302,6 +306,7 @@
Freeze/Thaw in all viewports Заморозити/розморозити в усіх видових екранах
Freeze/Thaw in the current viewport Заморозити/розморозити в поточному видовому екрані
From Від точки
From Template... З шаблону...
General Загальні
Geometric Center Геометричний центр
Geometric Constraint Геометричне обмеження
@@ -396,6 +401,7 @@
Layer is empty (double-click to make current) Шар порожній (двічі клацніть, щоб зробити поточним)
Layers Шари
Layers Palette Палітра шарів
Hide Palette Сховати палітру
Layers displayed: {shown} of {total}. Current layer: {layer} Показано шарів: {shown} із {total}. Поточний шар: {layer}
Layout Аркуш
Layout list Список аркушів
@@ -413,6 +419,7 @@
Lines +/− Рядки +/−
Lines added Додані рядки
Lines deleted Видалені рядки
Lines of Command History Рядки історії команд
Linetype Тип лінії
Linetype Scale Масштаб типу лінії
Linetype scale Масштаб типу лінії
@@ -948,6 +955,7 @@
Text override Перевизначення тексту
Text style Текстовий стиль
Thaw All Layers Розморозити всі шари
Thaw Layer Розморозити шар
There is nothing here yet. Тут поки що нічого немає.
Thickness Висота видавлювання
Time Час
@@ -964,6 +972,7 @@
True Color Колір RGB
Turn All Layers On Увімкнути всі шари
Turn Off Layer Вимкнути шар
Turn On Layer Увімкнути шар
Turn Quick Properties off (QPMODE) Вимкнути швидкі властивості (QPMODE)
Type Тип
Type a command Введіть команду
1 # CADCraft Ukrainian interface catalog — original translations by @dmatviichuk.
112
113
114
115
116
117
118
123
124
125
126
127
128
129
140
141
142
143
144
145
146
147
306
307
308
309
310
311
312
401
402
403
404
405
406
407
419
420
421
422
423
424
425
955
956
957
958
959
960
961
972
973
974
975
976
977
978
+1
View File
@@ -31,6 +31,7 @@ pub mod palettes;
pub mod parametric;
pub mod plotstyles;
pub mod quick;
pub mod shortcut_menus;
pub mod theme;
pub mod viewcube;
pub mod vpscale;
+5
View File
@@ -52,6 +52,7 @@ pub const UI_COMMANDS: &[(&str, &str, &[&str], Option<&str>)] = &[
("ui.dialog.commands", "Command Reference", &["Help", "CADCraft Help"], Some("F1")),
("ui.toggle.history", "Command History", &["Window", "Command History"], Some("F2")),
("ui.cmdline.lines", "Command Line History Lines", &[], None),
("ui.cmdline.clear", "Clear Command History", &[], None),
("ui.dialog.language", "Interface Language", &["Edit", "Interface Language"], None),
("ui.language", "Language", &[], None),
("ui.noop", "", &[], None),
@@ -263,6 +264,10 @@ fn run_ui_command_inner(app: &mut CadApp, id: &str, params: &Value) -> Option<Re
}
Ok(json!({ "lines": app.ui.history_lines }))
}
"ui.cmdline.clear" => {
app.session.log.clear();
Ok(Value::Null)
}
"ui.dialog.language"
| "ui.dialog.layers"
| "ui.dialog.blocks"
+3 -1
View File
@@ -421,9 +421,11 @@ pub(crate) fn layers_section(app: &mut CadApp, ui: &mut egui::Ui) {
}
let (sr, _) = ui.allocate_exact_size(vec2(12.0, 12.0), Sense::hover());
swatch(ui.painter(), sr, *color);
if ui.selectable_label(name.eq_ignore_ascii_case(&cur), name).clicked() {
let r = ui.selectable_label(name.eq_ignore_ascii_case(&cur), name);
if r.clicked() {
set_current = Some(name.clone());
}
crate::shortcut_menus::attach(app, &r, |app| crate::shortcut_menus::layer_entries(app, name));
});
}
}
+245
View File
@@ -0,0 +1,245 @@
//! Right-click menus outside the drawing area, after AutoCAD's shortcut menus: the command line,
//! the drawing (file) tabs, the layout tabs, and the Layers and Properties palettes. (The
//! drawing area's menus are [`crate::context_menu`].)
//!
//! Every item goes through a command: an item id is a command id, started like a menu item, or
//! one of the forms [`choose`] reads: `run:<command>:<JSON params>`, `layout:<op>:<name>`,
//! `drawing:<op>:<index>` and `cmdline:<op>`. Items for commands that aren't registered are
//! left out.
use cadcraft_engine::{Input, find_command};
use serde_json::{Value, json};
use crate::CadApp;
use crate::menus::{Entry, activate};
/// How many recent commands the command line menu lists.
const RECENT: usize = 8;
/// The command line history heights offered (lines).
const LINES: &[usize] = &[0, 1, 3, 5, 8, 12];
fn item(app: &CadApp, id: &str, label: Option<&str>) -> Option<Entry> {
let spec = find_command(id).map(|s| (s.label, s.shortcut, (s.enabled)(&app.session).is_ok()));
let ui = crate::menus::UI_COMMANDS.iter().find(|c| c.0 == id).map(|c| (c.1, c.3, true));
let (default, shortcut, enabled) = spec.or(ui)?;
Some(Entry::Item { label: label.unwrap_or(default).to_string(), id: id.to_string(), shortcut: shortcut.map(str::to_string), enabled })
}
/// An item that is not a command of its own (it runs one with parameters, or acts on a tab).
fn action(label: &str, id: String, enabled: bool) -> Entry {
Entry::Item { label: label.into(), id, shortcut: None, enabled }
}
/// An item running `command` with `params`, when the command exists.
fn run(label: &str, command: &str, params: Value) -> Option<Entry> {
let known = find_command(command).is_some() || crate::menus::is_ui_command(command);
known.then(|| action(label, format!("run:{command}:{params}"), true))
}
fn push(groups: &mut Vec<Vec<Entry>>, group: Vec<Entry>) {
if !group.is_empty() {
groups.push(group);
}
}
/// The command line's menu: Recent Commands, Dynamic Input, the history window and its height,
/// Copy History and Clear.
pub fn command_line_entries(app: &CadApp) -> Vec<Vec<Entry>> {
let mut groups = Vec::new();
let recent: Vec<Entry> = app
.session
.recent_commands(RECENT)
.iter()
.filter_map(|id| find_command(id))
.map(|c| Entry::Item { label: c.id.to_ascii_uppercase(), id: c.id.to_string(), shortcut: None, enabled: (c.enabled)(&app.session).is_ok() })
.collect();
if !recent.is_empty() {
groups.push(vec![Entry::Sub { label: "Recent Commands".into(), children: recent }]);
}
push(&mut groups, ["dynmode", "ui.toggle.history"].iter().filter_map(|id| item(app, id, None)).collect());
let lines: Vec<Entry> = LINES.iter().filter_map(|n| run(&n.to_string(), "ui.cmdline.lines", json!({ "lines": n }))).collect();
if !lines.is_empty() {
groups.push(vec![Entry::Sub { label: "Lines of Command History".into(), children: lines }]);
}
let mut edit = vec![action("Copy History", "cmdline:copy".into(), !app.session.log.is_empty())];
edit.extend(item(app, "ui.cmdline.clear", None));
groups.push(edit);
groups
}
/// A drawing tab's menu (`index` into the open drawings): New, Open, Save, Save As, Save All,
/// Close, Close All Other Drawings and Copy Full File Path. Save and Save As act on that drawing.
pub fn drawing_tab_entries(app: &CadApp, index: usize) -> Vec<Vec<Entry>> {
let path = app.session.docs.get(index).and_then(|d| d.path.clone());
let others = app.session.docs.len() > 1;
let mut groups = Vec::new();
push(&mut groups, ["new", "ui.open"].iter().filter_map(|id| item(app, id, None)).collect());
let save: Vec<Entry> = [("qsave", "Save"), ("ui.saveas", "Save As...")]
.iter()
.filter_map(|(id, label)| item(app, id, Some(label)).map(|_| action(label, format!("drawing:{id}:{index}"), true)))
.chain(item(app, "saveall", None))
.collect();
push(&mut groups, save);
let close = vec![
action("Close", format!("drawing:close:{index}"), true),
action("Close All Other Drawings", format!("drawing:closeothers:{index}"), others),
];
push(&mut groups, close);
groups.push(vec![action("Copy Full File Path", format!("drawing:copypath:{index}"), path.is_some())]);
groups
}
/// A layout tab's menu (`name` is the layout, "Model" the model tab): New Layout, From Template,
/// Delete, Rename and Copy (layouts only), Page Setup Manager and Print. Delete, Rename and Copy
/// go through LAYOUT's prompts, so Rename and Copy ask for the new name on the command line.
pub fn layout_tab_entries(app: &CadApp, name: &str) -> Vec<Vec<Entry>> {
let model = name == "Model";
let layout = |op: &str, label: &str| action(label, format!("layout:{op}:{name}"), true);
let mut groups = Vec::new();
let mut new: Vec<Entry> = item(app, "layout.new", None).map(|_| layout("new", "New Layout")).into_iter().collect();
if find_command("layout").is_some() {
new.push(layout("template", "From Template..."));
if !model {
new.extend([layout("delete", "Delete Layout"), layout("rename", "Rename Layout"), layout("copy", "Copy Layout")]);
}
}
push(&mut groups, new);
let out: Vec<Entry> = [("pagesetup", "Page Setup Manager..."), ("plot", "Print...")]
.iter()
.filter(|(id, _)| find_command(id).is_some())
.map(|(id, label)| layout(id, label))
.collect();
push(&mut groups, out);
groups
}
/// A layer's menu in the Layers palette: Set Current, On/Off, Freeze/Thaw, Lock/Unlock, Delete and
/// the Layer Properties Manager.
pub fn layer_entries(app: &CadApp, name: &str) -> Vec<Vec<Entry>> {
let Ok(d) = app.session.doc() else { return Vec::new() };
let Some(l) = d.layers.iter().find(|l| l.name.eq_ignore_ascii_case(name)) else { return Vec::new() };
let set = |label: &str, key: &str, value: bool| run(label, "layer.set", json!({ "name": name, key: value }));
let mut groups = Vec::new();
push(&mut groups, run("Set Current", "layer.current", json!({ "name": name })).into_iter().collect());
push(
&mut groups,
[
if l.on { set("Turn Off Layer", "on", false) } else { set("Turn On Layer", "on", true) },
if l.frozen { set("Thaw Layer", "frozen", false) } else { set("Freeze Layer", "frozen", true) },
if l.locked { set("Unlock Layer", "locked", false) } else { set("Lock Layer", "locked", true) },
]
.into_iter()
.flatten()
.collect(),
);
push(&mut groups, run("Delete Layer", "layer.delete", json!({ "name": name })).into_iter().collect());
push(&mut groups, item(app, "ui.dialog.layers", None).into_iter().collect());
groups
}
/// The Properties palette's menu: Quick Select, Select All, Deselect All, and hiding the palette.
pub fn properties_entries(app: &CadApp) -> Vec<Vec<Entry>> {
let mut groups = Vec::new();
push(&mut groups, ["qselect", "selectall", "ai_deselect"].iter().filter_map(|id| item(app, id, None)).collect());
push(&mut groups, run("Hide Palette", "ui.dock", json!({ "operation": "close", "panel": "properties" })).into_iter().collect());
groups
}
/// Run a chosen item. `ctx` is for the items that copy text to the system clipboard.
pub fn choose(app: &mut CadApp, ctx: &egui::Context, id: &str) {
if let Some(rest) = id.strip_prefix("run:") {
let Some((command, params)) = rest.split_once(':') else { return };
let params = serde_json::from_str(params).unwrap_or(Value::Null);
if let Err(e) = app.run(command, params) {
app.session.echo(e);
}
} else if id == "cmdline:copy" {
ctx.copy_text(app.session.log.join("\n"));
} else if let Some(rest) = id.strip_prefix("drawing:") {
let Some((op, index)) = rest.split_once(':') else { return };
let Ok(index) = index.parse::<usize>() else { return };
drawing_action(app, ctx, op, index);
} else if let Some(rest) = id.strip_prefix("layout:") {
let Some((op, name)) = rest.split_once(':') else { return };
layout_action(app, op, name);
} else {
activate(app, id);
}
}
fn drawing_action(app: &mut CadApp, ctx: &egui::Context, op: &str, index: usize) {
match op {
"close" => app.request_close(Some(index)),
"closeothers" => app.request_close_others(index),
"copypath" => {
if let Some(p) = app.session.docs.get(index).and_then(|d| d.path.clone()) {
ctx.copy_text(p);
}
}
// Save, Save As: on that drawing.
command => {
if index != app.session.active {
let _ = app.run("document.switch", json!({ "index": index }));
}
app.ui.start_tab = false;
activate(app, command);
}
}
}
/// Give the running command a keyword, then type `text` (a name; the prompt takes it whole).
fn keyword_then(app: &mut CadApp, keyword: &str, text: Option<&str>) {
if let Err(e) = app.session.input(Input::Keyword(keyword.into())) {
app.session.echo(e.to_string());
return;
}
if let Some(t) = text {
app.cmdline(t);
}
}
fn layout_action(app: &mut CadApp, op: &str, name: &str) {
match op {
"new" => {
if let Ok(v) = app.run("layout.new", json!({}))
&& let Some(n) = v.get("name").and_then(Value::as_str)
{
let _ = app.run("layout.set", json!({ "name": n }));
}
}
"pagesetup" | "plot" => {
let _ = app.run("layout.set", json!({ "name": name }));
activate(app, op);
}
// LAYOUT's options, as typed: Template asks for the file, Rename and Copy for the new name.
"template" | "delete" | "rename" | "copy" => {
activate(app, "layout");
if app.session.running.as_ref().is_some_and(|r| r.id == "layout") {
let keyword = match op {
"template" => "Template",
"delete" => "Delete",
"rename" => "Rename",
_ => "Copy",
};
keyword_then(app, keyword, (op != "template").then_some(name));
}
}
_ => {}
}
}
/// Show `groups` as the context menu of `resp` and run the chosen item.
pub fn attach(app: &mut CadApp, resp: &egui::Response, groups: impl FnOnce(&CadApp) -> Vec<Vec<Entry>>) {
let mut clicked = None;
resp.context_menu(|ui| crate::context_menu::groups_ui(ui, &groups(app), &mut clicked));
if let Some(id) = clicked {
choose(app, &resp.ctx, &id);
}
}
/// A palette's content with a right-click menu on its background (rows with menus of their own
/// keep theirs).
pub fn palette(app: &mut CadApp, ui: &mut egui::Ui, content: fn(&mut CadApp, &mut egui::Ui), groups: fn(&CadApp) -> Vec<Vec<Entry>>) {
let r = ui.scope_builder(egui::UiBuilder::new().sense(egui::Sense::click()), |ui| content(app, ui));
attach(app, &r.response, groups);
}
+167
View File
@@ -0,0 +1,167 @@
//! Right-click menus outside the drawing area (#512): the command line, drawing tabs, layout
//! tabs, and the Layers and Properties palettes.
use cadcraft_engine::Session;
use cadcraft_ui_egui::menus::Entry;
use cadcraft_ui_egui::{CadApp, Services, shortcut_menus as sm};
use serde_json::json;
/// (label, id, enabled) of the items, with submenu items prefixed by "Sub/".
fn flatten(groups: &[Vec<Entry>]) -> Vec<(String, String, bool)> {
fn walk(prefix: &str, es: &[Entry], out: &mut Vec<(String, String, bool)>) {
for e in es {
match e {
Entry::Item { label, id, enabled, .. } => out.push((format!("{prefix}{label}"), id.clone(), *enabled)),
Entry::Sub { label, children } => walk(&format!("{prefix}{label}/"), children, out),
}
}
}
let mut out = Vec::new();
for g in groups {
walk("", g, &mut out);
}
out
}
/// The id of the item labelled `label`, and whether it is enabled.
fn find(groups: &[Vec<Entry>], label: &str) -> Option<(String, bool)> {
flatten(groups).into_iter().find(|(l, _, _)| l == label).map(|(_, id, e)| (id, e))
}
fn choose(app: &mut CadApp, groups: &[Vec<Entry>], label: &str) {
let (id, enabled) = find(groups, label).unwrap_or_else(|| panic!("{label} missing from {:?}", flatten(groups)));
assert!(enabled, "{label} is disabled");
sm::choose(app, &egui::Context::default(), &id);
}
fn app() -> CadApp {
CadApp::new(Session::new(), Services::default())
}
fn layout_names(app: &CadApp) -> Vec<String> {
app.session.doc().unwrap().layouts.iter().map(|l| l.name.clone()).collect()
}
#[test]
fn command_line_menu() {
let mut app = app();
app.cmdline("LINE 0,0 10,0");
app.cmdline("");
app.cmdline("CIRCLE 0,0 2");
let g = sm::command_line_entries(&app);
assert_eq!(find(&g, "Recent Commands/CIRCLE").map(|f| f.0).as_deref(), Some("circle"));
assert!(find(&g, "Recent Commands/LINE").is_some());
assert!(find(&g, "Dynamic Input").is_some() && find(&g, "Command History").is_some());
assert!(find(&g, "Copy History").is_some_and(|(_, e)| e));
choose(&mut app, &g, "Lines of Command History/5");
assert_eq!(app.ui.history_lines, 5);
choose(&mut app, &g, "Recent Commands/LINE");
assert_eq!(app.session.running.as_ref().map(|r| r.id.as_str()), Some("line"));
app.session.cancel();
choose(&mut app, &g, "Clear Command History");
assert!(app.session.log.is_empty());
assert!(find(&sm::command_line_entries(&app), "Copy History").is_some_and(|(_, e)| !e), "nothing to copy");
}
#[test]
fn drawing_tab_menu() {
let mut app = app();
app.run("new", json!({})).unwrap();
app.run("new", json!({})).unwrap();
assert_eq!(app.session.docs.len(), 3);
let g = sm::drawing_tab_entries(&app, 1);
for l in ["New Drawing...", "Open...", "Save", "Save As...", "Close", "Close All Other Drawings"] {
assert!(find(&g, l).is_some_and(|(_, e)| e), "{l} missing or disabled in {:?}", flatten(&g));
}
assert!(find(&g, "Copy Full File Path").is_some_and(|(_, e)| !e), "an unsaved drawing has no path");
let keep = app.session.docs[1].uid;
choose(&mut app, &g, "Close All Other Drawings");
assert_eq!(app.session.docs.iter().map(|d| d.uid).collect::<Vec<_>>(), [keep]);
let g = sm::drawing_tab_entries(&app, 0);
assert!(find(&g, "Close All Other Drawings").is_some_and(|(_, e)| !e), "no other drawing");
choose(&mut app, &g, "Close");
assert!(app.session.docs.is_empty());
}
#[test]
fn layout_tab_menu() {
let mut app = app();
let before = layout_names(&app);
assert!(before.len() >= 2, "{before:?}");
let model = sm::layout_tab_entries(&app, "Model");
assert!(find(&model, "New Layout").is_some() && find(&model, "From Template...").is_some());
assert!(find(&model, "Delete Layout").is_none() && find(&model, "Rename Layout").is_none(), "the model tab stays");
// Rename asks for the new name on the command line.
let first = before[0].clone();
let g = sm::layout_tab_entries(&app, &first);
choose(&mut app, &g, "Rename Layout");
assert!(app.session.prompt_text().contains("new layout name"), "{}", app.session.prompt_text());
app.cmdline("Sheet A");
assert!(layout_names(&app).contains(&"Sheet A".to_string()), "{:?}", layout_names(&app));
let second = before[1].clone();
let g = sm::layout_tab_entries(&app, &second);
choose(&mut app, &g, "Delete Layout");
assert!(!layout_names(&app).contains(&second));
let n = layout_names(&app).len();
choose(&mut app, &model, "New Layout");
assert_eq!(layout_names(&app).len(), n + 1);
assert!(matches!(app.session.state().unwrap().space, cadcraft_doc::Space::Paper(_)), "the new layout is current");
// Page Setup acts on the right-clicked tab.
let g = sm::layout_tab_entries(&app, "Sheet A");
assert!(find(&g, "Page Setup Manager...").is_some() && find(&g, "Print...").is_some());
}
#[test]
fn layer_and_properties_palette_menus() {
let mut app = app();
app.run("layer.new", json!({ "name": "Walls" })).unwrap();
let g = sm::layer_entries(&app, "Walls");
choose(&mut app, &g, "Turn Off Layer");
let walls = |app: &CadApp| app.session.doc().unwrap().layers.iter().find(|l| l.name == "Walls").cloned().unwrap();
assert!(!walls(&app).on);
let g = sm::layer_entries(&app, "Walls");
assert!(find(&g, "Turn On Layer").is_some() && find(&g, "Freeze Layer").is_some() && find(&g, "Lock Layer").is_some());
choose(&mut app, &g, "Set Current");
assert_eq!(app.session.doc().unwrap().header.str("CLAYER", "0"), "Walls");
assert!(sm::layer_entries(&app, "NoSuchLayer").is_empty());
let g = sm::properties_entries(&app);
for l in ["Quick Select...", "Select All", "Deselect All", "Hide Palette"] {
assert!(find(&g, l).is_some(), "{l} missing from {:?}", flatten(&g));
}
}
#[test]
fn every_label_has_a_ukrainian_translation_and_every_id_a_command() {
let mut app = app();
app.cmdline("LINE 0,0 10,0");
app.cmdline("");
let mut all = flatten(&sm::command_line_entries(&app));
all.extend(flatten(&sm::drawing_tab_entries(&app, 0)));
all.extend(flatten(&sm::layout_tab_entries(&app, "Model")));
all.extend(flatten(&sm::layout_tab_entries(&app, "Layout1")));
all.extend(flatten(&sm::layer_entries(&app, "0")));
all.extend(flatten(&sm::properties_entries(&app)));
let mut missing = Vec::new();
for (label, id, _) in &all {
let segments: Vec<&str> = label.split('/').collect();
// Command names (Recent Commands) and line counts aren't translated.
let n = segments.len() - usize::from(segments.len() > 1);
for s in &segments[..n] {
if cadcraft_ui_egui::i18n::tr("uk", s) == *s {
missing.push(s.to_string());
}
}
let command = id.strip_prefix("run:").and_then(|r| r.split_once(':')).map_or(id.as_str(), |(c, _)| c);
let special = ["drawing:", "layout:", "cmdline:"].iter().any(|p| id.starts_with(p));
assert!(
special || cadcraft_engine::find_command(command).is_some() || cadcraft_ui_egui::menus::is_ui_command(command),
"{label}: {id} isn't a command"
);
}
assert!(missing.is_empty(), "untranslated menu labels: {missing:?}");
}
+8 -4
View File
@@ -188,10 +188,14 @@ down-arrow options menu (DYNPROMPT), and DYNDIM.
shortcuts. AutoCAD for Mac's defaults are similar in number; F4 (3D osnap), F5 (isoplane), F6
(dynamic UCS) are missing.
- Delete erases the selection on macOS (#64); Escape cancels.
- Right-click on the canvas is Enter while a command runs; otherwise it opens a shortcut menu
(Repeat, Clipboard ▸ Cut/Copy/Copy with Base Point/Paste/Paste as Block/Paste to Original
Coordinates, the selection's editing commands, Undo/Redo, Pan/Zoom; #325). Still missing:
AutoCAD's per-command menus and the menus of the command line, palettes and tabs.
- Right-click on the canvas is Enter while a command runs; holding it (250 ms, AutoCAD's
time-sensitive right-click) opens the command menu: Enter, Cancel, Recent Input, the prompt's
options, Snap Overrides, Pan/Zoom. Otherwise it opens a shortcut menu (Repeat, Recent Commands,
Clipboard ▸, Isolate ▸, the selection's editing commands, Draw Order ▸, Group, the selected
object type's edit commands, Select Similar, Deselect All, Undo/Redo, Pan/Zoom; #325, #512).
The command line, drawing tabs, layout tabs, Layers palette rows and the Properties palette
have menus too (#512). Still missing: in-place layout tab rename, Move or Copy, Select All
Layouts.
## Revision history