mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-26 11:56:50 -06:00
macOS Edit menu keys edit a focused text field (#2139)
* macOS Edit menu keys edit a focused text field The menu bar takes ⌘C, ⌘X, ⌘V, ⌘A, ⌘Z and ⇧⌘Z before any window sees them, so they ran Copy, Paste or Undo on the drawing while the user was typing in a field. While a text field has the keyboard, in the main window or a floating panel's own window, the menu now hands the field its key instead. craft-libs is pinned at #318's head for typing_window and send_keys. * Edit menu items a text field takes stay enabled while one has the keyboard Edit ▸ Paste follows the drawing clipboard, so with nothing copied in the drawing it was greyed out even while a text field could paste.
This commit is contained in:
@@ -118,7 +118,7 @@ impl NativeMenu {
|
||||
if cmd == "ui.discord" {
|
||||
ctx.open_url(egui::OpenUrl::new_tab("https://discord.gg/artcraft"));
|
||||
} else {
|
||||
menus::activate(app, &cmd.clone());
|
||||
menus::activate_native(app, ctx, &cmd.clone());
|
||||
}
|
||||
ctx.request_repaint();
|
||||
}
|
||||
@@ -142,8 +142,9 @@ impl NativeMenu {
|
||||
return;
|
||||
}
|
||||
self.last_refresh = now;
|
||||
let typing = menus::text_field_window(ctx).is_some();
|
||||
for (cmd, _, item) in self.items.values() {
|
||||
let en = cadcraft_engine::find_command(cmd).is_none_or(|c| (c.enabled)(&app.session).is_ok());
|
||||
let en = (typing && menus::edits_text(cmd)) || cadcraft_engine::find_command(cmd).is_none_or(|c| (c.enabled)(&app.session).is_ok());
|
||||
item.set_enabled(en);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -649,6 +649,61 @@ pub fn activate(app: &mut CadApp, id: &str) {
|
||||
app.start(id);
|
||||
}
|
||||
|
||||
/// Activate the menu command `id` from a native menu bar, which takes the Edit menu's key
|
||||
/// equivalents (⌘C, ⌘X, ⌘V, ⌘A, ⌘Z, ⇧⌘Z) before any window sees them: while a text field has
|
||||
/// the keyboard, in the main window or a floating panel's own window, the field gets the key
|
||||
/// and the drawing command does not run.
|
||||
pub fn activate_native(app: &mut CadApp, ctx: &egui::Context, id: &str) {
|
||||
let (Some(window), Some(text)) = (text_field_window(ctx), text_command(id, ctx.os().is_mac())) else {
|
||||
activate(app, id);
|
||||
return;
|
||||
};
|
||||
match text {
|
||||
TextCommand::Paste => ctx.send_viewport_cmd_to(window, egui::ViewportCommand::RequestPaste),
|
||||
TextCommand::Events(events) if window == egui::ViewportId::ROOT => {
|
||||
app.synthetic.extend(events);
|
||||
ctx.request_repaint();
|
||||
}
|
||||
TextCommand::Events(events) => craft_ui::docking::send_keys(ctx, window, events),
|
||||
}
|
||||
}
|
||||
|
||||
/// The window whose text field has the keyboard: a floating panel's own OS window, or the main
|
||||
/// window.
|
||||
pub fn text_field_window(ctx: &egui::Context) -> Option<egui::ViewportId> {
|
||||
craft_ui::docking::typing_window(ctx).or_else(|| ctx.text_edit_focused().then_some(egui::ViewportId::ROOT))
|
||||
}
|
||||
|
||||
/// Whether a focused text field takes the menu command `id` ([`activate_native`]): a native
|
||||
/// menu keeps such items enabled while one has the keyboard, whatever the drawing allows.
|
||||
pub fn edits_text(id: &str) -> bool {
|
||||
text_command(id, false).is_some()
|
||||
}
|
||||
|
||||
/// What a focused text field does for an Edit menu command.
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum TextCommand {
|
||||
/// The clipboard text, which the window asks for and receives as `Paste`.
|
||||
Paste,
|
||||
Events(Vec<egui::Event>),
|
||||
}
|
||||
|
||||
fn text_command(id: &str, mac: bool) -> Option<TextCommand> {
|
||||
let key = |key, shift| {
|
||||
let modifiers = egui::Modifiers { mac_cmd: mac, ctrl: !mac, command: true, shift, alt: false };
|
||||
TextCommand::Events([true, false].map(|pressed| egui::Event::Key { key, physical_key: None, pressed, repeat: false, modifiers }).into())
|
||||
};
|
||||
Some(match id {
|
||||
"copyclip" => TextCommand::Events(vec![egui::Event::Copy]),
|
||||
"cutclip" => TextCommand::Events(vec![egui::Event::Cut]),
|
||||
"pasteclip" => TextCommand::Paste,
|
||||
"u" => key(egui::Key::Z, false),
|
||||
"redo" => key(egui::Key::Z, true),
|
||||
"selectall" => key(egui::Key::A, false),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Run `id` with default parameters without cancelling the active command, as the drafting
|
||||
/// toggles (F3, F7-F12, the status bar buttons) are in AutoCAD.
|
||||
pub fn run_transparent(app: &mut CadApp, id: &str) {
|
||||
|
||||
@@ -17,6 +17,7 @@ mod lisp_files;
|
||||
mod measure_distance_snapping;
|
||||
mod menubar_hover;
|
||||
mod mspace_viewport_view;
|
||||
mod native_menu_text_keys;
|
||||
mod options_dialog;
|
||||
mod parameters_rename;
|
||||
mod pickdrag;
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
//! The macOS menu bar takes the Edit menu's key equivalents before the window does: with a text
|
||||
//! field focused they edit the field, and only without one do they run the drawing command.
|
||||
|
||||
use cadcraft_engine::Session;
|
||||
use cadcraft_ui_egui::{CadApp, Services, menus};
|
||||
use egui::{accesskit::Role, vec2};
|
||||
use egui_kittest::Harness;
|
||||
use egui_kittest::kittest::{NodeT, Queryable};
|
||||
use serde_json::json;
|
||||
|
||||
fn harness() -> Harness<'static, CadApp> {
|
||||
let mut app = CadApp::new(Session::default(), Services::default());
|
||||
app.ui.start_tab = false;
|
||||
app.ui.in_window_menu = false;
|
||||
let mut h = Harness::builder().with_size(vec2(1280.0, 800.0)).build_ui_state(
|
||||
|ui, app: &mut CadApp| {
|
||||
app.logic(&ui.ctx().clone());
|
||||
app.ui(ui);
|
||||
},
|
||||
app,
|
||||
);
|
||||
h.ctx.set_os(egui::os::OperatingSystem::Mac);
|
||||
h.run_steps(5);
|
||||
h
|
||||
}
|
||||
|
||||
/// A native menu choice, then the frames that deliver what it queued for the main window.
|
||||
fn menu(h: &mut Harness<'_, CadApp>, id: &str) {
|
||||
let ctx = h.ctx.clone();
|
||||
menus::activate_native(h.state_mut(), &ctx, id);
|
||||
let mut raw = egui::RawInput::default();
|
||||
h.state_mut().raw_input_hook(&ctx, &mut raw);
|
||||
h.input_mut().events.extend(raw.events);
|
||||
h.run_steps(3);
|
||||
}
|
||||
|
||||
fn expression<'a>(h: &'a Harness<'_, CadApp>) -> egui_kittest::Node<'a> {
|
||||
h.get_by_role_and_label(Role::Tab, "QuickCalc").parent().unwrap().get_all_by_role(Role::TextInput).next().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_menu_keys_edit_the_focused_text_field_and_otherwise_the_drawing() {
|
||||
let mut h = harness();
|
||||
h.state_mut().run("line", json!({"points": [[0, 0], [10, 0]]})).unwrap();
|
||||
let drawing = h.state().session.doc().unwrap().clone();
|
||||
menus::activate(h.state_mut(), "ui.toggle.quickcalc");
|
||||
h.run_steps(5);
|
||||
expression(&h).click();
|
||||
h.run_steps(3);
|
||||
expression(&h).type_text("2+3");
|
||||
h.run_steps(3);
|
||||
assert_eq!(menus::text_field_window(&h.ctx), Some(egui::ViewportId::ROOT));
|
||||
assert!(menus::edits_text("pasteclip") && !menus::edits_text("line"));
|
||||
menu(&mut h, "selectall");
|
||||
menu(&mut h, "cutclip");
|
||||
assert_eq!(expression(&h).value().as_deref(), Some(""), "Select All and Cut act on the field");
|
||||
assert!(h.state().session.running.is_none());
|
||||
assert_eq!(h.state().session.doc().unwrap(), &drawing);
|
||||
|
||||
h.ctx.memory_mut(|m| m.stop_text_input());
|
||||
h.run_steps(3);
|
||||
assert_eq!(menus::text_field_window(&h.ctx), None);
|
||||
menu(&mut h, "u");
|
||||
assert_ne!(h.state().session.doc().unwrap(), &drawing, "without a focused field, Undo undoes the line");
|
||||
}
|
||||
Reference in New Issue
Block a user