feat(input): add editable shortcut table

Persist all active bindings and route keyboard events through the configured table.\n\nRefs #449
This commit is contained in:
Hakan Seven committed 2026-08-04 15:35:06 +03:00
1 parent 3f6959d7d0
commit 2f86bfde8e
9 files changed
+448 -337

No files matched your search

+24 -9
View File
@@ -261,10 +261,10 @@ impl OpenCADStudio {
);
return Some(Task::none());
}
let mut keys: Vec<(&String, &String)> = self.shortcut_overrides.iter().collect();
let mut keys: Vec<(&String, &String)> = self.shortcut_bindings.iter().collect();
keys.sort_by(|a, b| a.0.cmp(b.0));
let text: String = keys.iter().map(|(k, v)| format!("{k} {v}\n")).collect();
let count = self.shortcut_overrides.len();
let count = self.shortcut_bindings.len();
match std::fs::write(path, text) {
Ok(()) => self.command_line.push_output(crate::tf!(
"CUIEXPORT: wrote {count} shortcut(s) to \"{path}\"."
@@ -303,11 +303,15 @@ impl OpenCADStudio {
continue;
}
if let Some((k, v)) = line.split_once(char::is_whitespace) {
self.shortcut_overrides
.insert(k.trim().to_uppercase(), v.trim().to_uppercase());
n += 1;
let key = crate::app::shortcuts::normalize_key(k);
if !key.is_empty() {
self.shortcut_bindings
.insert(key, v.trim().to_uppercase());
n += 1;
}
}
}
self.persist_settings_if_changed();
self.command_line.push_output(crate::tf!(
"CUIIMPORT: loaded {n} shortcut(s) from \"{path}\"."
).as_ref());
@@ -335,16 +339,27 @@ impl OpenCADStudio {
if key.is_empty() || cmd_str.is_empty() {
self.command_line.push_error(crate::t!("Usage: SHORTCUTS SET <key> <command> e.g. SHORTCUTS SET CTRL+D DIST").as_ref());
} else {
self.shortcut_overrides.insert(key.clone(), cmd_str.clone());
self.command_line
.push_output(crate::tf!("Shortcut set: {key} → {cmd_str}").as_ref());
let key = crate::app::shortcuts::normalize_key(&key);
if key.is_empty() {
self.command_line.push_error(crate::t!("Usage: SHORTCUTS SET <key> <command> e.g. SHORTCUTS SET CTRL+D DIST").as_ref());
} else {
self.shortcut_bindings.insert(key.clone(), cmd_str.clone());
self.persist_settings_if_changed();
self.command_line
.push_output(crate::tf!("Shortcut set: {key} → {cmd_str}").as_ref());
}
}
}
"CLEAR" | "DELETE" | "REMOVE" => {
let key = parts.get(1).map(|s| s.to_uppercase()).unwrap_or_default();
if key.is_empty() {
self.command_line.push_error(crate::t!("Usage: SHORTCUTS CLEAR <key>").as_ref());
} else if self.shortcut_overrides.remove(&key).is_some() {
} else if self
.shortcut_bindings
.remove(&crate::app::shortcuts::normalize_key(&key))
.is_some()
{
self.persist_settings_if_changed();
self.command_line
.push_output(crate::tf!("Shortcut '{key}' removed.").as_ref());
} else {
+17
View File
@@ -36,6 +36,8 @@ pub struct AppConfig {
/// Print dialog preferences (only the persisted fields; runtime state is
/// skipped by `PlotDialogState`'s serde attributes).
pub plot: PlotDialogState,
/// Complete editable keyboard shortcut table.
pub shortcuts: ShortcutConfig,
}
impl Default for AppConfig {
@@ -50,6 +52,21 @@ impl Default for AppConfig {
annotation_auto_scale: -4,
ribbon: RibbonConfig::default(),
plot: PlotDialogState::default(),
shortcuts: ShortcutConfig::default(),
}
}
}
#[derive(Clone, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct ShortcutConfig {
pub bindings: std::collections::BTreeMap<String, String>,
}
impl Default for ShortcutConfig {
fn default() -> Self {
Self {
bindings: super::shortcuts::default_bindings(),
}
}
}
+17 -3
View File
@@ -19,6 +19,7 @@ pub mod plugin_host;
mod properties;
mod recent;
mod settings;
mod shortcuts;
mod style_ops;
mod text_inline;
mod update;
@@ -790,8 +791,10 @@ pub(super) struct OpenCADStudio {
theme_color_inputs: [String; 6],
// ── Keyboard Shortcut Editor ──────────────────────────────────────────
/// User-defined function-key overrides: "F3" → command string.
shortcut_overrides: rustc_hash::FxHashMap<String, String>,
/// Complete editable key → command/action table.
shortcut_bindings: rustc_hash::FxHashMap<String, String>,
/// Working rows shown by the shortcut editor until Apply is pressed.
shortcut_editor_rows: Vec<(String, String)>,
// ── Command Aliases ───────────────────────────────────────────────────
/// Command-line aliases: uppercase abbreviation → uppercase command
@@ -2237,6 +2240,16 @@ pub enum Message {
ShortcutsPanelOpen,
#[allow(dead_code)]
ShortcutsPanelClose,
ShortcutEditorInput {
idx: usize,
field: crate::ui::window::shortcuts::ShortcutField,
value: String,
},
ShortcutEditorAdd,
ShortcutEditorRemove(usize),
ShortcutEditorApply,
/// Canonical key emitted by the global keyboard subscription.
ShortcutPressed(String),
// ── Command Alias Editor (ALIASEDIT) ────────────────────────────────
/// Open the command-alias editor modal, seeding rows from the alias table.
AliasEditorOpen,
@@ -3000,7 +3013,8 @@ impl OpenCADStudio {
ui_theme: config::UiThemeConfig::default(),
theme_color_inputs: config::UiThemePalette::default().hex_values(),
// Keyboard shortcuts
shortcut_overrides: rustc_hash::FxHashMap::default(),
shortcut_bindings: rustc_hash::FxHashMap::default(),
shortcut_editor_rows: Vec::new(),
// Command aliases (populated from ocad.pgp just after construction)
command_aliases: rustc_hash::FxHashMap::default(),
alias_editor_rows: Vec::new(),
+187
View File
@@ -0,0 +1,187 @@
//! Editable keyboard shortcut table shared by the CUI dialog and key events.
use super::{Message, OpenCADStudio};
use iced::Task;
use rustc_hash::FxHashMap;
use std::collections::BTreeMap;
#[cfg(target_os = "macos")]
const ACCEL: &str = "CMD";
#[cfg(not(target_os = "macos"))]
const ACCEL: &str = "CTRL";
/// Every active binding shipped with a fresh configuration. Values are command
/// names where possible; the small set of input actions is resolved below.
pub(super) fn default_bindings() -> BTreeMap<String, String> {
[
("F1".to_string(), "HELP"),
("F2".to_string(), "COMMANDHISTORY"),
("F3".to_string(), "TOGGLEOSNAP"),
("F7".to_string(), "GRID"),
("F8".to_string(), "ORTHO"),
("F9".to_string(), "SNAP"),
("F10".to_string(), "POLAR"),
("F11".to_string(), "OTRACK"),
("F12".to_string(), "DYNINPUT"),
(format!("{ACCEL}+0"), "CLEANSCREEN"),
(format!("{ACCEL}+1"), "PROPERTIES"),
(format!("{ACCEL}+N"), "NEW"),
(format!("{ACCEL}+O"), "OPEN"),
(format!("{ACCEL}+P"), "PLOT"),
(format!("{ACCEL}+Q"), "QUIT"),
(format!("{ACCEL}+S"), "SAVE"),
(format!("{ACCEL}+SHIFT+S"), "SAVEAS"),
(format!("{ACCEL}+Z"), "UNDO"),
(format!("{ACCEL}+SHIFT+Z"), "REDO"),
(format!("{ACCEL}+Y"), "REDO"),
(format!("{ACCEL}+F"), "FIND"),
(format!("{ACCEL}+H"), "FIND"),
(format!("{ACCEL}+A"), "SELECTALL"),
(format!("{ACCEL}+C"), "COPYCLIP"),
(format!("{ACCEL}+SHIFT+C"), "COPYBASE"),
(format!("{ACCEL}+X"), "CUTCLIP"),
(format!("{ACCEL}+V"), "PASTECLIP"),
(format!("{ACCEL}+SHIFT+V"), "PASTEBLOCK"),
("ENTER".to_string(), "FINALIZE"),
("SPACE".to_string(), "COMMANDSPACE"),
("ESCAPE".to_string(), "CANCEL"),
("DELETE".to_string(), "DELETESELECTED"),
("BACKSPACE".to_string(), "BACKSPACE"),
("TAB".to_string(), "DYNTAB"),
("UP".to_string(), "HISTORYPREV"),
("DOWN".to_string(), "HISTORYNEXT"),
("LEFT".to_string(), "CARETLEFT"),
("RIGHT".to_string(), "CARETRIGHT"),
]
.into_iter()
.map(|(key, action)| (key, action.to_string()))
.collect()
}
/// Canonical form used by the editor, command interface, config and key event
/// resolver. Modifier order is stable so equivalent user input shares one row.
pub(super) fn normalize_key(value: &str) -> String {
let mut ctrl = false;
let mut cmd = false;
let mut alt = false;
let mut shift = false;
let mut key = String::new();
for part in value.split('+').map(str::trim).filter(|part| !part.is_empty()) {
match part.to_uppercase().as_str() {
"CTRL" | "CONTROL" => ctrl = true,
"CMD" | "COMMAND" | "META" | "SUPER" => cmd = true,
"ALT" | "OPTION" => alt = true,
"SHIFT" => shift = true,
"ESC" => key = "ESCAPE".to_string(),
"RETURN" => key = "ENTER".to_string(),
"ARROWUP" => key = "UP".to_string(),
"ARROWDOWN" => key = "DOWN".to_string(),
"ARROWLEFT" => key = "LEFT".to_string(),
"ARROWRIGHT" => key = "RIGHT".to_string(),
other => key = other.to_string(),
}
}
let mut parts = Vec::with_capacity(5);
if ctrl {
parts.push("CTRL");
}
if cmd {
parts.push("CMD");
}
if alt {
parts.push("ALT");
}
if shift {
parts.push("SHIFT");
}
if !key.is_empty() {
parts.push(&key);
}
parts.join("+")
}
/// Keys that used to bypass focused widgets. Clipboard and selection commands
/// intentionally stay out so text fields retain their native shortcuts.
pub(super) fn is_global_key(key: &str) -> bool {
let base = key.rsplit('+').next().unwrap_or(key);
base.strip_prefix('F').is_some_and(|digits| {
!digits.is_empty() && digits.chars().all(|ch| ch.is_ascii_digit())
})
|| matches!(base, "ESCAPE")
|| (key.starts_with(ACCEL)
&& !matches!(
key.rsplit('+').next(),
Some("A" | "C" | "V" | "X")
))
}
fn is_named_key(key: &str) -> bool {
matches!(
key,
"ENTER"
| "SPACE"
| "ESCAPE"
| "DELETE"
| "BACKSPACE"
| "TAB"
| "UP"
| "DOWN"
| "LEFT"
| "RIGHT"
| "HOME"
| "END"
| "PAGEUP"
| "PAGEDOWN"
| "INSERT"
) || key.strip_prefix('F').is_some_and(|digits| {
!digits.is_empty() && digits.chars().all(|ch| ch.is_ascii_digit())
})
}
impl OpenCADStudio {
pub(super) fn apply_shortcut_editor_rows(&mut self) {
let bindings: FxHashMap<String, String> = self
.shortcut_editor_rows
.iter()
.filter_map(|(key, action)| {
let key = normalize_key(key);
let action = action.trim().to_uppercase();
(!key.is_empty() && !action.is_empty()).then_some((key, action))
})
.collect();
self.shortcut_bindings = bindings;
self.persist_settings_if_changed();
}
pub(super) fn run_shortcut(&mut self, key: &str) -> Task<Message> {
let action = self.shortcut_bindings.get(key).or_else(|| {
let base = key.rsplit('+').next()?;
is_named_key(base).then(|| self.shortcut_bindings.get(base)).flatten()
});
let Some(action) = action.cloned() else {
return Task::none();
};
let message = match action.as_str() {
"FINALIZE" => Message::CommandFinalize,
"COMMANDSPACE" => Message::CommandSpace,
"CANCEL" => Message::CommandEscape,
"DELETESELECTED" => Message::DeleteSelected,
"BACKSPACE" => Message::CommandBackspace,
"DYNTAB" => Message::DynTabNext,
"HISTORYPREV" => Message::CommandHistoryPrev,
"HISTORYNEXT" => Message::CommandHistoryNext,
"CARETLEFT" => Message::MTextCaretMove(-1),
"CARETRIGHT" => Message::MTextCaretMove(1),
"COMMANDHISTORY" => Message::CommandHistoryToggle,
"TOGGLEOSNAP" => Message::ToggleSnapEnabled,
"OTRACK" => Message::ToggleOTrack,
"DYNINPUT" => Message::ToggleDynInput,
"SELECTALL" => Message::SelectAllShortcut,
"PASTECLIP" => Message::PasteShortcut,
other => Message::Command(other.to_string()),
};
self.update(message)
}
}
+8
View File
@@ -530,6 +530,13 @@ impl OpenCADStudio {
collapse: self.ribbon.collapse_mode(),
},
plot: self.plot_dialog.clone(),
shortcuts: crate::app::config::ShortcutConfig {
bindings: self
.shortcut_bindings
.iter()
.map(|(key, command)| (key.clone(), command.clone()))
.collect(),
},
}
}
@@ -565,6 +572,7 @@ impl OpenCADStudio {
self.annotation_auto_scale = cfg.annotation_auto_scale.clamp(-4, 4);
self.ribbon.set_collapse_mode(cfg.ribbon.collapse);
self.plot_dialog = cfg.plot;
self.shortcut_bindings = cfg.shortcuts.bindings.into_iter().collect();
}
/// Write the config only when it changed since the last write, so a toggle
+42 -4
View File
@@ -29,6 +29,7 @@ fn is_modal_blocked_key_msg(msg: &Message) -> bool {
| Message::TogglePolar
| Message::ToggleOTrack
| Message::ToggleDynInput
| Message::ShortcutPressed(_)
| Message::TabNew
| Message::OpenFile
| Message::SaveFile
@@ -171,9 +172,8 @@ impl OpenCADStudio {
}
// Closing (✕) discards edits made since the last Apply — matching the
// style editors. Committing happens only through the Apply button.
Some(Aliases) => {
self.alias_editor_rows.clear();
}
Some(Aliases) => self.alias_editor_rows.clear(),
Some(Shortcuts) => self.shortcut_editor_rows.clear(),
Some(LayerStateEditor) => {
self.layer_state_edit_draft = None;
self.layer_state_edit_filter.clear();
@@ -206,7 +206,9 @@ impl OpenCADStudio {
// the modal's own text fields keep working because they emit their own
// (non-blocked) messages. (#126)
if self.active_modal.is_some() {
if matches!(msg, Message::CommandEscape) {
if matches!(msg, Message::CommandEscape)
|| matches!(&msg, Message::ShortcutPressed(key) if key.rsplit('+').next() == Some("ESCAPE"))
{
return self.update(Message::CloseModal);
}
if is_modal_blocked_key_msg(&msg) {
@@ -4475,6 +4477,13 @@ impl OpenCADStudio {
// ── Keyboard Shortcuts Panel ──────────────────────────────────────
Message::ShortcutsPanelOpen => {
let mut rows: Vec<(String, String)> = self
.shortcut_bindings
.iter()
.map(|(key, command)| (key.clone(), command.clone()))
.collect();
rows.sort_by(|a, b| a.0.cmp(&b.0));
self.shortcut_editor_rows = rows;
self.active_modal = Some(super::ModalKind::Shortcuts);
Task::none()
}
@@ -4482,6 +4491,35 @@ impl OpenCADStudio {
self.close_active_modal();
Task::none()
}
Message::ShortcutEditorInput { idx, field, value } => {
use crate::ui::window::shortcuts::ShortcutField;
if let Some(row) = self.shortcut_editor_rows.get_mut(idx) {
match field {
ShortcutField::Key => row.0 = value.to_uppercase(),
ShortcutField::Command => row.1 = value.to_uppercase(),
}
}
Task::none()
}
Message::ShortcutEditorAdd => {
self.shortcut_editor_rows
.push((String::new(), String::new()));
Task::none()
}
Message::ShortcutEditorRemove(idx) => {
if idx < self.shortcut_editor_rows.len() {
self.shortcut_editor_rows.remove(idx);
}
Task::none()
}
Message::ShortcutEditorApply => {
self.apply_shortcut_editor_rows();
self.command_line.push_info(
crate::tf!("{} shortcut(s) applied.", self.shortcut_bindings.len()).as_ref(),
);
Task::none()
}
Message::ShortcutPressed(key) => self.run_shortcut(&key),
// ── Command Alias Editor (ALIASEDIT) ──────────────────────────────
Message::AliasEditorOpen => {
+60 -139
View File
@@ -114,6 +114,47 @@ fn hatch_pattern_key_event(
}
}
fn shortcut_key_name(key: &keyboard::Key, modifiers: keyboard::Modifiers) -> Option<String> {
let key = match key {
keyboard::Key::Character(value) if !value.is_empty() => value.to_uppercase(),
keyboard::Key::Named(named) => {
let name = format!("{named:?}");
match name.as_str() {
"ArrowUp" => "UP".to_string(),
"ArrowDown" => "DOWN".to_string(),
"ArrowLeft" => "LEFT".to_string(),
"ArrowRight" => "RIGHT".to_string(),
"PageUp" => "PAGEUP".to_string(),
"PageDown" => "PAGEDOWN".to_string(),
"Enter" | "Space" | "Escape" | "Delete" | "Backspace" | "Tab" | "Home"
| "End" | "Insert" => name.to_uppercase(),
_ if name.starts_with('F')
&& name[1..].chars().all(|ch| ch.is_ascii_digit()) =>
{
name
}
_ => return None,
}
}
_ => return None,
};
let mut parts = Vec::with_capacity(5);
if modifiers.control() {
parts.push("CTRL".to_string());
}
if modifiers.logo() {
parts.push("CMD".to_string());
}
if modifiers.alt() {
parts.push("ALT".to_string());
}
if modifiers.shift() {
parts.push("SHIFT".to_string());
}
parts.push(key);
Some(parts.join("+"))
}
/// `ViewportRenderMode` enum carries the raw DXF integers, not a label,
/// so wrap it locally with a friendly name renderer.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -1842,12 +1883,10 @@ impl OpenCADStudio {
text,
..
}) => {
// Platform-aware accelerator modifier: Cmd on macOS,
// Ctrl on Windows/Linux. Using `command()` rather than
// `control()` makes Cmd+C/V/S/Z etc. work on Mac, per
// the platform's keyboard conventions.
#[cfg(target_arch = "wasm32")]
let accel = modifiers.command();
let shift = modifiers.shift();
let shortcut_modifier =
modifiers.control() || modifiers.alt() || modifiers.logo();
// Any key that produces a printable glyph types it,
// even when its logical key resolves to navigation
// (NumLock-on Numpad8 / Numpad2 arrive as
@@ -1860,7 +1899,7 @@ impl OpenCADStudio {
// (a comma on German/European layouts), which the
// coordinate parser rejects. Force it to a decimal point
// from the physical key, independent of layout.
if !accel
if !shortcut_modifier
&& status == Status::Ignored
&& matches!(
physical_key,
@@ -1872,7 +1911,7 @@ impl OpenCADStudio {
{
return Some(Message::CommandAppendChar(".".to_string()));
}
if !accel && status == Status::Ignored {
if !shortcut_modifier && status == Status::Ignored {
if let Some(t) = text.as_deref() {
if !t.is_empty()
&& t.chars().all(|c| !c.is_control() && !c.is_whitespace())
@@ -1881,141 +1920,23 @@ impl OpenCADStudio {
}
}
}
match key {
// Space is a literal space inside the MText preview
// but finalises a command otherwise; the handler
// decides based on editor state.
keyboard::Key::Named(keyboard::key::Named::Space)
if status == Status::Ignored =>
{
Some(Message::CommandSpace)
}
keyboard::Key::Named(keyboard::key::Named::Enter)
if status == Status::Ignored =>
{
Some(Message::CommandFinalize)
}
keyboard::Key::Named(keyboard::key::Named::Escape) => {
Some(Message::CommandEscape)
}
keyboard::Key::Named(keyboard::key::Named::Delete)
if status == Status::Ignored =>
{
Some(Message::DeleteSelected)
}
keyboard::Key::Named(keyboard::key::Named::Backspace)
if status == Status::Ignored =>
{
Some(Message::CommandBackspace)
}
keyboard::Key::Named(keyboard::key::Named::Tab)
if status == Status::Ignored =>
{
Some(Message::DynTabNext)
}
keyboard::Key::Named(keyboard::key::Named::ArrowUp)
if status == Status::Ignored =>
{
Some(Message::CommandHistoryPrev)
}
keyboard::Key::Named(keyboard::key::Named::ArrowDown)
if status == Status::Ignored =>
{
Some(Message::CommandHistoryNext)
}
// Caret movement in the MText preview (no-op
// otherwise; these arrows are unused elsewhere).
keyboard::Key::Named(keyboard::key::Named::ArrowLeft)
if status == Status::Ignored =>
{
Some(Message::MTextCaretMove(-1))
}
keyboard::Key::Named(keyboard::key::Named::ArrowRight)
if status == Status::Ignored =>
{
Some(Message::MTextCaretMove(1))
}
keyboard::Key::Named(keyboard::key::Named::F1) => {
Some(Message::Command("HELP".to_string()))
}
keyboard::Key::Named(keyboard::key::Named::F2) => {
Some(Message::CommandHistoryToggle)
}
keyboard::Key::Named(keyboard::key::Named::F3) => {
Some(Message::ToggleSnapEnabled)
}
keyboard::Key::Named(keyboard::key::Named::F7) => {
Some(Message::ToggleGrid)
}
keyboard::Key::Named(keyboard::key::Named::F8) => {
Some(Message::ToggleOrtho)
}
keyboard::Key::Named(keyboard::key::Named::F9) => {
Some(Message::ToggleGridSnap)
}
keyboard::Key::Named(keyboard::key::Named::F10) => {
Some(Message::TogglePolar)
}
keyboard::Key::Named(keyboard::key::Named::F11) => {
Some(Message::ToggleOTrack)
}
keyboard::Key::Named(keyboard::key::Named::F12) => {
Some(Message::ToggleDynInput)
}
keyboard::Key::Character(c) if accel => match c.as_str() {
"0" => Some(Message::ToggleCleanScreen),
"1" => Some(Message::ToggleProperties),
"n" => Some(Message::TabNew),
"o" => Some(Message::OpenFile),
"p" => Some(Message::Command("PLOT".to_string())),
"q" => Some(Message::Command("QUIT".to_string())),
"s" if !shift => Some(Message::SaveFile),
"s" if shift => Some(Message::SaveAs),
"z" if !shift => Some(Message::Undo),
"z" if shift => Some(Message::Redo),
"y" => Some(Message::Redo),
"f" | "h" => Some(Message::FindReplaceOpen),
// Ctrl/Cmd+A: select all layer rows when the
// Layer Manager is open, else all objects. The
// update handler branches on the active modal.
"a" if status == Status::Ignored => {
Some(Message::SelectAllShortcut)
// A focused web text field needs the browser clipboard;
// drawing shortcuts only run for ignored C/V events.
#[cfg(target_arch = "wasm32")]
if accel && status == Status::Captured {
if let keyboard::Key::Character(value) = &key {
if value.eq_ignore_ascii_case("v") {
return Some(Message::WebFieldPaste);
}
// Clipboard accelerators defer to a focused
// text widget: when the command-line history
// editor (or any text field) captures Ctrl+C/
// X/V it copies/cuts/pastes its own text and
// marks the event Captured, so we must NOT also
// fire the drawing's COPYCLIP/CUTCLIP/paste.
// Only when nothing captured (the drawing has
// focus, status Ignored) do these run. (#232)
"c" if shift && status == Status::Ignored => {
Some(Message::Command("COPYBASE".to_string()))
if value.eq_ignore_ascii_case("c") {
return Some(Message::WebFieldCopy);
}
"c" if status == Status::Ignored => {
Some(Message::Command("COPYCLIP".to_string()))
}
"x" if status == Status::Ignored => {
Some(Message::Command("CUTCLIP".to_string()))
}
"v" if shift && status == Status::Ignored => {
Some(Message::Command("PASTEBLOCK".to_string()))
}
"v" if status == Status::Ignored => Some(Message::PasteShortcut),
// Web: a focused text field captured Ctrl+C/V,
// but iced's clipboard is a no-op there — route
// through the async browser clipboard (#346).
#[cfg(target_arch = "wasm32")]
"v" => Some(Message::WebFieldPaste),
#[cfg(target_arch = "wasm32")]
"c" => Some(Message::WebFieldCopy),
_ => None,
},
// Printable glyphs are already handled by the
// text guard above the match; anything reaching
// here is a non-typing key we don't bind.
_ => None,
}
}
let shortcut = shortcut_key_name(&key, modifiers)?;
(status == Status::Ignored
|| crate::app::shortcuts::is_global_key(&shortcut))
.then_some(Message::ShortcutPressed(shortcut))
}
_ => None,
}
+1 -1
View File
@@ -67,7 +67,7 @@ impl OpenCADStudio {
520,
|flow| {
crate::ui::window::shortcuts::view_window(
&self.shortcut_overrides,
&self.shortcut_editor_rows,
flow,
)
},