mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-29 21:36:52 -06:00
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:
co-authored by
Claude Opus 5.5
parent
48cce34a52
commit
fa0ce3c70f
@@ -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));
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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 Введіть команду
|
||||
|
||||
|
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user