mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 01:52:46 +00:00
Shortcuts: F2, F3 and F4 also cut, copy and paste on Windows and Linux
Squashed from #5041 onto main.
This commit is contained in:
@@ -508,7 +508,7 @@ pub fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
shortcut_editor::open(app);
|
||||
}
|
||||
if let Some(k) = app.ui.dialog.as_mut() {
|
||||
k.fields.insert("overrides".into(), crate::keymap::stored::to_value(km.overrides()));
|
||||
k.fields.insert("overrides".into(), crate::keymap::stored::to_value(&shortcut_editor::saved_overrides(km.overrides(), &ov)));
|
||||
k.fields.insert("set".into(), json!(set));
|
||||
k.fields.insert("__message".into(), json!(tl!("Imported. Press OK to keep the imported set.")));
|
||||
}
|
||||
|
||||
@@ -500,6 +500,25 @@ fn apply_rows(app: &mut VectorcraftApp, start: &str, rows: &[(&str, &str, &str,
|
||||
(out, dialog(app).fields["overrides"].clone())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resetting_an_imported_function_key_restores_its_edit_default() {
|
||||
if crate::keymap::platform().is_mac() {
|
||||
return;
|
||||
}
|
||||
for (alias, edit) in [("F2", "edit.cut"), ("F3", "edit.copy"), ("F4", "edit.paste")] {
|
||||
let mut app = app();
|
||||
let (out, value) = apply_rows(&mut app, "defaults", &[("Acme Duplicate", "Menus", "edit.duplicate", alias)]);
|
||||
assert_eq!(out["applied"], 1);
|
||||
let mut saved = crate::keymap::stored::from_value(value);
|
||||
assert!(!crate::keymap::has_override(&saved, edit), "{alias} saved edit override");
|
||||
shortcut_editor::reset_one(&mut saved, "edit.duplicate");
|
||||
assert_eq!(
|
||||
crate::keymap::lookup(&crate::keymap::keymap_with(&saved), &crate::keymap::vc_chord(alias).unwrap()),
|
||||
Some(crate::keymap::command_of(edit)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Cmd and Ctrl are one key off the Mac: the review lets both rows through, and the later one takes
|
||||
/// the shortcut.
|
||||
#[test]
|
||||
|
||||
@@ -132,7 +132,7 @@ fn vc_binding(text: &str) -> Option<Binding> {
|
||||
|
||||
/// Every listed entry as the crate sees it, in dispatch priority: commands (engine, then UI),
|
||||
/// panels, tools. Each tool is its own group: tools never cycle.
|
||||
fn catalogue() -> Vec<Bindable> {
|
||||
fn catalogue(p: Platform) -> Vec<Bindable> {
|
||||
let rank = |e: &Entry| {
|
||||
if e.is_tool {
|
||||
2
|
||||
@@ -159,7 +159,7 @@ fn catalogue() -> Vec<Bindable> {
|
||||
if let Some(d) = shortcut_editor::default_of(&e.key).and_then(vc_binding) {
|
||||
b = b.with_default(d);
|
||||
}
|
||||
for alias in default_aliases(&e.key) {
|
||||
for alias in default_aliases(&e.key, p) {
|
||||
if let Some(d) = vc_binding(alias) {
|
||||
b = b.with_default(d);
|
||||
}
|
||||
@@ -170,14 +170,22 @@ fn catalogue() -> Vec<Bindable> {
|
||||
}
|
||||
|
||||
/// Additional defaults for commands whose primary shortcut remains in their command spec.
|
||||
fn default_aliases(key: &str) -> &'static [&'static str] {
|
||||
fn default_aliases(key: &str, p: Platform) -> &'static [&'static str] {
|
||||
match key {
|
||||
"edit.cut" if !p.is_mac() => &["F2"],
|
||||
"edit.copy" if !p.is_mac() => &["F3"],
|
||||
"edit.paste" if !p.is_mac() => &["F4"],
|
||||
"paint.lastColor" => &["Shift+,"],
|
||||
"paint.lastGradient" => &["Shift+."],
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
|
||||
/// Cut, Copy and Paste with their primary shortcut and the function key [`default_aliases`] adds
|
||||
/// outside macOS.
|
||||
pub(crate) const EDIT_FUNCTION_KEYS: [(&str, &str, &str); 3] =
|
||||
[("edit.cut", "Cmd+X", "F2"), ("edit.copy", "Cmd+C", "F3"), ("edit.paste", "Cmd+V", "F4")];
|
||||
|
||||
/// On the web, the browser's operating system once egui reports it ([`note_os`]).
|
||||
static WEB_PLATFORM: RwLock<Option<Platform>> = RwLock::new(None);
|
||||
|
||||
@@ -220,13 +228,38 @@ pub fn template() -> &'static Keymap {
|
||||
}
|
||||
|
||||
/// The default keymap of `p`. The chords the operating system takes can't be assigned.
|
||||
fn keymap_for(p: Platform) -> Keymap {
|
||||
Keymap::new(catalogue(), Scopes::new(), p).with_reserved(craft_keymap::reserved::system(p))
|
||||
pub(crate) fn keymap_for(p: Platform) -> Keymap {
|
||||
Keymap::new(catalogue(p), Scopes::new(), p).with_reserved(craft_keymap::reserved::system(p))
|
||||
}
|
||||
|
||||
/// The keymap with `overrides` applied.
|
||||
pub fn keymap_with(overrides: &Overrides) -> Keymap {
|
||||
template().clone().with_overrides(overrides.clone())
|
||||
let defaults = template();
|
||||
defaults.clone().with_overrides(edit_alias_overrides(overrides, defaults.platform()))
|
||||
}
|
||||
|
||||
/// When a saved override assigns F2, F3 or F4 elsewhere without changing its edit command,
|
||||
/// keep only that edit command's primary shortcut so the saved assignment runs.
|
||||
pub(crate) fn edit_alias_overrides(overrides: &Overrides, p: Platform) -> Overrides {
|
||||
let mut resolved = overrides.clone();
|
||||
if p.is_mac() {
|
||||
return resolved;
|
||||
}
|
||||
for (key, primary, alias) in EDIT_FUNCTION_KEYS {
|
||||
if overrides.get(key).is_some() {
|
||||
continue;
|
||||
}
|
||||
let Some(binding) = vc_binding(alias) else { continue };
|
||||
let assigned_elsewhere = overrides.iter().any(|(other, texts)| {
|
||||
other != key
|
||||
&& shortcut_editor::entry(&entry_of_stored(other)).is_some()
|
||||
&& texts.iter().any(|text| Binding::parse(text).is_ok_and(|b| b == binding))
|
||||
});
|
||||
if assigned_elsewhere {
|
||||
let _ = resolved.insert(key, vec![vc_to_crate(primary)]);
|
||||
}
|
||||
}
|
||||
resolved
|
||||
}
|
||||
|
||||
/// The command a chord runs, the first for tools sharing it.
|
||||
@@ -424,7 +457,7 @@ mod tests {
|
||||
.filter_map(|e| shortcut_editor::default_of(&e.key).map(|d| (e.key.as_str(), d)))
|
||||
.filter(|(key, d)| {
|
||||
let mut expected: Vec<Binding> = vc_binding(d).into_iter().collect();
|
||||
expected.extend(default_aliases(key).iter().filter_map(|alias| vc_binding(alias)));
|
||||
expected.extend(default_aliases(key, platform()).iter().filter_map(|alias| vc_binding(alias)));
|
||||
expected != template().default_bindings(&command_of(key))
|
||||
})
|
||||
.collect();
|
||||
@@ -451,6 +484,50 @@ mod tests {
|
||||
assert!(template().conflicts().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_function_keys_follow_platform_and_reset_restores_both_bindings() {
|
||||
for p in [Platform::Windows, Platform::Linux, Platform::Mac] {
|
||||
let km = keymap_for(p);
|
||||
for (key, primary, alternate) in [("edit.cut", "Cmd+X", "F2"), ("edit.copy", "Cmd+C", "F3"), ("edit.paste", "Cmd+V", "F4")] {
|
||||
let command = command_of(key);
|
||||
let mut expected = vec![vc_binding(primary).unwrap()];
|
||||
if !p.is_mac() {
|
||||
expected.push(vc_binding(alternate).unwrap());
|
||||
}
|
||||
assert_eq!(km.default_bindings(&command), expected, "{p:?} {key}");
|
||||
assert_eq!(lookup(&km, &vc_chord(primary).unwrap()), Some(command.clone()), "{p:?} {primary}");
|
||||
assert_eq!(lookup(&km, &vc_chord(alternate).unwrap()), (!p.is_mac()).then_some(command.clone()), "{p:?} {alternate}");
|
||||
assert_eq!(effective_with(&km, key).as_deref(), Some(primary));
|
||||
let mut changed = km.clone();
|
||||
shortcut_editor::assign_in(&mut changed, key, Some("Ctrl+Alt+Shift+F12"), true).unwrap();
|
||||
assert_eq!(changed.bindings(&command), vec![vc_binding("Ctrl+Alt+Shift+F12").unwrap()]);
|
||||
changed.reset(&command);
|
||||
assert_eq!(changed.bindings(&command), expected, "{p:?} {key} reset");
|
||||
let mut overrides = Overrides::new();
|
||||
set_override(&mut overrides, key, "Ctrl+Alt+Shift+F12");
|
||||
shortcut_editor::reset_one(&mut overrides, key);
|
||||
assert_eq!(km.clone().with_overrides(overrides).bindings(&command), expected, "{p:?} {key} use default");
|
||||
}
|
||||
assert!(km.conflicts().is_empty(), "{p:?}");
|
||||
assert!(km.problems().is_empty(), "{p:?}");
|
||||
|
||||
for (alias, edit, primary) in [("F2", "edit.cut", "Cmd+X"), ("F3", "edit.copy", "Cmd+C"), ("F4", "edit.paste", "Cmd+V")] {
|
||||
let mut saved = Overrides::new();
|
||||
set_override(&mut saved, "tool:pen", alias);
|
||||
let assigned = keymap_for(p).with_overrides(edit_alias_overrides(&saved, p));
|
||||
assert_eq!(lookup(&assigned, &vc_chord(alias).unwrap()), Some(command_of("tool:pen")), "{p:?} saved {alias} assignment");
|
||||
assert_eq!(lookup(&assigned, &vc_chord(primary).unwrap()), Some(command_of(edit)), "{p:?} primary {edit} binding");
|
||||
shortcut_editor::reset_one(&mut saved, "tool:pen");
|
||||
let reset = keymap_for(p).with_overrides(edit_alias_overrides(&saved, p));
|
||||
assert_eq!(lookup(&reset, &vc_chord(alias).unwrap()), (!p.is_mac()).then_some(command_of(edit)), "{p:?} reset saved {alias}");
|
||||
}
|
||||
let mut unknown = Overrides::new();
|
||||
set_override(&mut unknown, "missing.command", "F2");
|
||||
let km = keymap_for(p).with_overrides(edit_alias_overrides(&unknown, p));
|
||||
assert_eq!(lookup(&km, &vc_chord("F2").unwrap()), (!p.is_mac()).then_some(command_of("edit.cut")), "{p:?} unknown saved entry");
|
||||
}
|
||||
}
|
||||
|
||||
/// The chords the operating system takes are refused, and no default uses one.
|
||||
#[test]
|
||||
fn system_shortcuts_are_refused() {
|
||||
|
||||
@@ -65,14 +65,14 @@ pub(crate) use ui_mirror;
|
||||
/// The keymap with the user's overrides, and the effective text of every entry they change.
|
||||
pub(crate) struct Mirror {
|
||||
pub keymap: Keymap,
|
||||
source_overrides: Overrides,
|
||||
text: HashMap<String, Option<&'static str>>,
|
||||
}
|
||||
|
||||
impl Mirror {
|
||||
fn new(overrides: Overrides) -> Mirror {
|
||||
let keymap = crate::keymap::template().clone().with_overrides(overrides);
|
||||
let text = keymap
|
||||
.overrides()
|
||||
let keymap = crate::keymap::keymap_with(&overrides);
|
||||
let text = overrides
|
||||
.iter()
|
||||
.map(|(k, _)| {
|
||||
let key = entry_of_stored(k);
|
||||
@@ -80,7 +80,7 @@ impl Mirror {
|
||||
(key, t)
|
||||
})
|
||||
.collect();
|
||||
Mirror { keymap, text }
|
||||
Mirror { keymap, source_overrides: overrides, text }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ pub fn sync(ui: &UiState) {
|
||||
let same = store()
|
||||
.read()
|
||||
.map(|m| match &*m {
|
||||
Some(m) => *m.keymap.overrides() == ui.shortcut_overrides && m.keymap.platform() == crate::keymap::platform(),
|
||||
Some(m) => m.source_overrides == ui.shortcut_overrides && m.keymap.platform() == crate::keymap::platform(),
|
||||
None => ui.shortcut_overrides.is_empty(),
|
||||
})
|
||||
.unwrap_or(false);
|
||||
@@ -612,10 +612,32 @@ pub fn conflicts_in(km: &Keymap) -> Vec<(String, Vec<String>)> {
|
||||
pub fn assign(overrides: &mut Overrides, key: &str, chord: Option<&str>, force: bool) -> Result<Vec<String>, String> {
|
||||
let mut km = keymap_with(overrides);
|
||||
let removed = assign_in(&mut km, key, chord, force)?;
|
||||
*overrides = km.overrides().clone();
|
||||
*overrides = saved_overrides(km.overrides(), overrides);
|
||||
Ok(removed)
|
||||
}
|
||||
|
||||
/// The overrides to save from a working keymap's `current` overrides. An entry for Cut, Copy or
|
||||
/// Paste that `source`, the set before the change, did not hold is left out when it is the entry
|
||||
/// [`crate::keymap::keymap_with`] makes from the other entries of `source` or of `current`.
|
||||
pub(crate) fn saved_overrides(current: &Overrides, source: &Overrides) -> Overrides {
|
||||
let mut saved = current.clone();
|
||||
for (key, _, _) in crate::keymap::EDIT_FUNCTION_KEYS {
|
||||
let Some(now) = current.get(key) else { continue };
|
||||
if source.get(key).is_some() {
|
||||
continue;
|
||||
}
|
||||
let made = |from: &Overrides| {
|
||||
let mut rest = from.clone();
|
||||
rest.remove(key);
|
||||
crate::keymap::edit_alias_overrides(&rest, crate::keymap::platform()).get(key).is_some_and(|t| t == now)
|
||||
};
|
||||
if made(current) || made(source) {
|
||||
saved.remove(key);
|
||||
}
|
||||
}
|
||||
saved
|
||||
}
|
||||
|
||||
/// [`assign`] on a keymap.
|
||||
pub fn assign_in(km: &mut Keymap, key: &str, chord: Option<&str>, force: bool) -> Result<Vec<String>, String> {
|
||||
let command = command_of(key);
|
||||
@@ -789,7 +811,7 @@ pub fn run_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<Resu
|
||||
Err(e) => return Some(Err(e)),
|
||||
};
|
||||
let km = keymap_with(&app.ui.shortcut_overrides);
|
||||
let ov = km.overrides();
|
||||
let ov = &app.ui.shortcut_overrides;
|
||||
Ok(Value::Array(
|
||||
entries()
|
||||
.iter()
|
||||
@@ -943,7 +965,7 @@ impl DialogView {
|
||||
let backtick = backtick_text();
|
||||
let texts = entries().iter().map(|e| dialog_shortcuts_with(&keymap, &e.key, backtick.as_deref())).collect();
|
||||
let conflicts = conflicts_in(&keymap).len();
|
||||
DialogView { source: stored::to_value(keymap.overrides()), backtick, keymap, texts, conflicts }
|
||||
DialogView { source: stored::to_value(&overrides), backtick, keymap, texts, conflicts }
|
||||
}
|
||||
|
||||
/// The view of `d`'s working set, from the cache unless the set or the backtick key's
|
||||
@@ -972,6 +994,7 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
let mut ok = false;
|
||||
let mut cancel = false;
|
||||
drop_hidden(&mut d, km.overrides());
|
||||
let saved = dialog_overrides(&d);
|
||||
let recording = d.str("__recording");
|
||||
// Record a chord (before widgets see the keys). Modifiers held down are part of the next key's
|
||||
// chord, not a chord of their own.
|
||||
@@ -1089,7 +1112,7 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
d.fields.insert("__selected".into(), json!(e.key));
|
||||
}
|
||||
let label = if rec == e.key { tl!("Press keys…").to_string() } else { sc.unwrap_or_else(|| "—".into()) };
|
||||
let changed = crate::keymap::has_override(km.overrides(), &e.key);
|
||||
let changed = crate::keymap::has_override(&saved, &e.key);
|
||||
let txt = egui::RichText::new(label).color(if changed { t.accent } else { t.text });
|
||||
let b = ui
|
||||
.add_sized([150.0, 20.0], egui::Button::new(txt).selected(rec == e.key))
|
||||
@@ -1161,7 +1184,7 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
});
|
||||
});
|
||||
if let Cow::Owned(km) = &km {
|
||||
d.fields.insert("overrides".into(), stored::to_value(km.overrides()));
|
||||
d.fields.insert("overrides".into(), stored::to_value(&saved_overrides(km.overrides(), &saved)));
|
||||
}
|
||||
let import = d.fields.remove("__import").is_some();
|
||||
let export = d.fields.remove("__export").is_some();
|
||||
@@ -1667,6 +1690,126 @@ mod tests {
|
||||
assert_eq!(view.conflicts, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_dialog_rows_show_primary_and_function_keys() {
|
||||
for p in [craft_keymap::Platform::Windows, craft_keymap::Platform::Linux, craft_keymap::Platform::Mac] {
|
||||
let km = crate::keymap::keymap_for(p);
|
||||
for (key, primary, alternate) in [("edit.cut", "Cmd+X", "F2"), ("edit.copy", "Cmd+C", "F3"), ("edit.paste", "Cmd+V", "F4")] {
|
||||
let shown = dialog_shortcuts_with(&km, key, None).unwrap();
|
||||
let primary = menus::pretty_shortcut(primary);
|
||||
assert!(shown.contains(&primary), "{p:?} {key}: {shown}");
|
||||
assert_eq!(shown.contains(alternate), !p.is_mac(), "{p:?} {key}: {shown}");
|
||||
}
|
||||
}
|
||||
for (key, primary) in [("edit.cut", "Cmd+X"), ("edit.copy", "Cmd+C"), ("edit.paste", "Cmd+V")] {
|
||||
let shown = menus::item_shortcut(key, &Value::Null).map(menus::pretty_shortcut);
|
||||
assert_eq!(shown, Some(menus::pretty_shortcut(primary)), "{key} menu shortcut");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn saved_function_key_assignment_does_not_change_cut_in_the_editor() {
|
||||
if crate::keymap::platform().is_mac() {
|
||||
return;
|
||||
}
|
||||
let mut saved = Overrides::new();
|
||||
crate::keymap::set_override(&mut saved, "tool:pen", "F2");
|
||||
let mirror = Mirror::new(saved.clone());
|
||||
assert!(!mirror.text.contains_key("edit.cut"));
|
||||
assert!(conflicts_in(&mirror.keymap).is_empty());
|
||||
|
||||
let view = DialogView::new(saved.clone());
|
||||
assert_eq!(view.source, stored::to_value(&saved));
|
||||
assert_eq!(view.conflicts, 0);
|
||||
assert_eq!(dialog_shortcuts_with(&view.keymap, "edit.cut", None), Some(menus::pretty_shortcut("Cmd+X")));
|
||||
|
||||
let mut other = saved.clone();
|
||||
assign(&mut other, "edit.preferences", Some("Cmd+Alt+Shift+F12"), false).unwrap();
|
||||
assert!(!crate::keymap::has_override(&other, "edit.cut"));
|
||||
let (set, imported) = import_json(&export_json("Mine", &other)).unwrap();
|
||||
assert_eq!(set, "Mine");
|
||||
assert_eq!(imported, other);
|
||||
assert!(!crate::keymap::has_override(&imported, "edit.cut"));
|
||||
|
||||
let mut km = view.keymap.clone();
|
||||
assign_in(&mut km, "edit.preferences", Some("Cmd+Alt+Shift+F12"), false).unwrap();
|
||||
assert_eq!(saved_overrides(km.overrides(), &saved), other);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_stolen_edit_function_keys_restores_their_defaults() {
|
||||
if crate::keymap::platform().is_mac() {
|
||||
return;
|
||||
}
|
||||
for (alias, edit) in [("F2", "edit.cut"), ("F3", "edit.copy"), ("F4", "edit.paste")] {
|
||||
let mut saved = Overrides::new();
|
||||
assert_eq!(assign(&mut saved, "tool:pen", Some(alias), true).unwrap(), vec![edit]);
|
||||
assert!(!crate::keymap::has_override(&saved, edit), "{alias} saved edit override");
|
||||
reset_one(&mut saved, "tool:pen");
|
||||
assert_eq!(crate::keymap::lookup(&keymap_with(&saved), &crate::keymap::vc_chord(alias).unwrap()), Some(command_of(edit)));
|
||||
|
||||
let mut d = Dialog::new("shortcuts", json!({"overrides": {}}));
|
||||
let source = dialog_overrides(&d);
|
||||
let mut km = keymap_with(&source);
|
||||
record(&mut d, &mut km, "tool:pen", alias);
|
||||
d.fields.insert("overrides".into(), stored::to_value(&saved_overrides(km.overrides(), &source)));
|
||||
let mut dialog_saved = dialog_overrides(&d);
|
||||
assert!(!crate::keymap::has_override(&dialog_saved, edit), "{alias} dialog edit override");
|
||||
reset_one(&mut dialog_saved, "tool:pen");
|
||||
assert_eq!(crate::keymap::lookup(&keymap_with(&dialog_saved), &crate::keymap::vc_chord(alias).unwrap()), Some(command_of(edit)));
|
||||
|
||||
let mut changed = Overrides::new();
|
||||
crate::keymap::set_override(&mut changed, edit, "Ctrl+Alt+Shift+F12");
|
||||
assign(&mut changed, "tool:pen", Some(alias), true).unwrap();
|
||||
assert_eq!(crate::keymap::override_text(&changed, edit).as_deref(), Some("Ctrl+Alt+Shift+F12"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reassigning_a_saved_function_key_gives_it_back_to_its_edit_command() {
|
||||
if crate::keymap::platform().is_mac() {
|
||||
return;
|
||||
}
|
||||
for (alias, edit) in [("F2", "edit.cut"), ("F3", "edit.copy"), ("F4", "edit.paste")] {
|
||||
let mut saved = Overrides::new();
|
||||
crate::keymap::set_override(&mut saved, "edit.preferences", alias);
|
||||
assign(&mut saved, "edit.preferences", Some("Cmd+Alt+Shift+F12"), false).unwrap();
|
||||
assert!(!crate::keymap::has_override(&saved, edit), "{alias} saved edit override");
|
||||
assert_eq!(crate::keymap::lookup(&keymap_with(&saved), &crate::keymap::vc_chord(alias).unwrap()), Some(command_of(edit)));
|
||||
|
||||
let mut source = Overrides::new();
|
||||
crate::keymap::set_override(&mut source, "edit.preferences", alias);
|
||||
let mut d = Dialog::new("shortcuts", json!({"overrides": stored::to_value(&source)}));
|
||||
let mut km = keymap_with(&source);
|
||||
record(&mut d, &mut km, "edit.preferences", "Cmd+Alt+Shift+F12");
|
||||
let dialog_saved = saved_overrides(km.overrides(), &source);
|
||||
assert!(!crate::keymap::has_override(&dialog_saved, edit), "{alias} dialog edit override");
|
||||
assert_eq!(crate::keymap::lookup(&keymap_with(&dialog_saved), &crate::keymap::vc_chord(alias).unwrap()), Some(command_of(edit)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assigning_an_edit_function_key_elsewhere_reports_the_conflict() {
|
||||
if crate::keymap::platform().is_mac() {
|
||||
return;
|
||||
}
|
||||
for (alias, edit) in [("F2", "edit.cut"), ("F3", "edit.copy"), ("F4", "edit.paste")] {
|
||||
let label = entry(edit).map(|e| e.label.clone()).unwrap();
|
||||
assert_eq!(conflicts_with(&Overrides::new(), alias, "tool:pen"), vec![edit.to_string()], "{alias}");
|
||||
let mut ov = Overrides::new();
|
||||
let err = assign(&mut ov, "tool:pen", Some(alias), false).unwrap_err();
|
||||
assert!(err.ends_with(&format!("is already used by {label}")), "{alias}: {err}");
|
||||
assert!(!crate::keymap::has_override(&ov, "tool:pen"), "{alias}: a refused assignment saves nothing");
|
||||
|
||||
let mut d = Dialog::new("shortcuts", json!({"overrides": {}}));
|
||||
let mut km = keymap_with(&Overrides::new());
|
||||
record(&mut d, &mut km, "tool:pen", alias);
|
||||
assert_eq!(d.str("__conflict"), edit, "{alias}");
|
||||
assert!(d.str("__message").contains(&label), "{alias}: {}", d.str("__message"));
|
||||
assert_eq!(crate::keymap::lookup(&km, &crate::keymap::vc_chord(alias).unwrap()), Some(command_of("tool:pen")), "{alias}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entries_cover_tools_and_menu_commands() {
|
||||
assert!(entries().iter().any(|e| e.key == "tool:selection"));
|
||||
|
||||
@@ -28,6 +28,53 @@ fn named_panel_default_chords_toggle_their_panels() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_function_keys_dispatch_outside_text_editing_only() {
|
||||
if crate::keymap::platform().is_mac() {
|
||||
return;
|
||||
}
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
|
||||
let shape = app.session.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap()["id"].clone();
|
||||
app.session.execute("select.set", &json!({"ids": [shape]})).unwrap();
|
||||
crate::shortcut_editor::sync(&app.ui);
|
||||
frame(&mut app, press(Key::F3, Modifiers::NONE));
|
||||
assert_eq!(app.session.clipboard.nodes.len(), 1, "F3 copies the selection");
|
||||
frame(&mut app, press(Key::F2, Modifiers::NONE));
|
||||
assert!(app.session.active().unwrap().selection.objects.is_empty(), "F2 cuts the selection");
|
||||
frame(&mut app, press(Key::F4, Modifiers::NONE));
|
||||
assert_eq!(app.session.active().unwrap().selection.objects.len(), 1, "F4 pastes the copied object");
|
||||
|
||||
let id = app.session.execute("text.create", &json!({"x": 20, "y": 50, "text": "Hi"})).unwrap()["id"].as_u64().unwrap();
|
||||
app.select_tool("type");
|
||||
app.session.apply_actions(vec![vectorcraft_tools::Action::Notify("text.editNew".into())]).unwrap();
|
||||
assert!(app.session.tool_wants_text());
|
||||
for key in [Key::F2, Key::F3, Key::F4] {
|
||||
frame(&mut app, press(key, Modifiers::NONE));
|
||||
assert_eq!(text_and_layers(&mut app, id).0, "Hi", "{key:?} leaves edited text alone");
|
||||
assert!(app.session.tool_wants_text(), "{key:?} keeps editing");
|
||||
}
|
||||
|
||||
app.select_tool("selection");
|
||||
app.session.execute("select.set", &json!({"ids": [id]})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
let mut field = String::from("Field text");
|
||||
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| {
|
||||
let response = ui.text_edit_singleline(&mut field);
|
||||
ui.ctx().memory_mut(|m| m.request_focus(response.id));
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
for key in [Key::F2, Key::F3, Key::F4] {
|
||||
let mut out = ctx.run_ui(egui::RawInput { events: press(key, Modifiers::NONE), ..Default::default() }, |ui| {
|
||||
handle(&mut app, ui.ctx());
|
||||
ui.text_edit_singleline(&mut field);
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
assert_eq!(field, "Field text", "{key:?} leaves focused text alone");
|
||||
assert_eq!(app.session.active().unwrap().selection.objects.len(), 1, "{key:?} leaves the canvas selection alone");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn other_panel_shortcuts_keep_plain_panel_params() {
|
||||
for panel in ["character", "swatches"] {
|
||||
|
||||
@@ -760,6 +760,8 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- Shift+Tab (`window.panels`) shows and hides the dock and the floating panels and leaves the toolbar and the Control bar as they are; hiding the panels with Tab or Shift+Tab also closes a panel popped out of the icon column.
|
||||
- The second Tab (`window.dock`) shows again only the bars the first Tab hid, after a restart too: a Control bar that was off stays off, a toolbar hidden with Window › Tools stays hidden, and panels shown with Shift+Tab in between stay shown.
|
||||
- The function key shortcuts for Brushes, Color, Layers, Info, Gradient, Stroke, Attributes, Graphic Styles, Appearance, Align, Transform, Pathfinder, Transparency, and Symbols activate a background tab on the first press, hide its expanded dock or floating group when that tab is in front, and restore the group with all its tabs together on the next press; in an icon column they open and close the flyout while keeping the icons. A reassigned chord keeps that behavior, and Window > a hidden sibling restores the group.
|
||||
- On Windows and Linux, F2, F3 and F4 run Cut, Copy and Paste alongside Ctrl+X, Ctrl+C and Ctrl+V, and in the web build they do so everywhere except macOS. The Edit menu keeps the Ctrl shortcuts, and Keyboard Shortcuts lists both keys of each row. On macOS only Cmd+X, Cmd+C and Cmd+V are the defaults. F2, F3 and F4 do not run these commands in a focused text field or while the Type tool edits text. A saved shortcut set that assigns one of these keys to another command keeps that assignment. Public reference: [Default keyboard shortcuts](https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html).
|
||||
- Cmd/Ctrl+A follows the keyboard: in a panel, dialog or search field it selects the field's text, while the Type tool edits it selects all of that text (also `select.all` over MCP), and only otherwise every object. On macOS the menu bar's Select All, Cut, Copy and Paste (keys or clicks) act on a focused field too, and elsewhere the in-window menus' do: the field that had the keyboard when the menu opened gets them, with its selection, and the keyboard back (#516). Not yet: Select All across threaded frames (one frame's text is selected).
|
||||
- Cmd/Ctrl+A follows the keyboard: in a panel, dialog or search field it selects the field's text, while the Type tool edits it selects all of that text (also `select.all` over MCP), and only otherwise every object. On macOS the menu bar's Select All, Cut, Copy and Paste (keys or clicks) act on a focused field too, and elsewhere the in-window menus' do: the field that had the keyboard when the menu opened gets them, with its selection, and the keyboard back (#516). In a frame of threaded text, Select All selects the whole story, highlighted in every frame: Copy and Cut take all of it, typing or Delete replaces it as one undo step, and a fill, a size step or a Character or Paragraph panel change applies to every frame's text (VectorCraft's own behavior; `textedit::select_all_in_a_threaded_frame_selects_the_whole_story`, `textedit::typing_over_the_whole_story_replaces_every_frames_text_in_one_step`).
|
||||
- With the whole story of threaded text selected by Select All, Remove Line Breaks, Remove Paragraph Breaks and Type › Bullets and Numbering › Convert to Text act on the text of every frame as one undo step, and the story stays selected after Convert to Text; a selection inside one frame changes only that frame (VectorCraft's own behavior; `textedit::remove_breaks_takes_the_whole_story_that_select_all_selected`, `textedit::convert_to_text_takes_the_whole_story_that_select_all_selected`).
|
||||
- With the whole story of threaded text selected by Select All, the Character, Paragraph, OpenType, Tabs and Text Styles panels, the Justification dialog and the font menu change the frames that are in the edited frame's thread at the time of the change, locked and hidden frames included. A frame that has left the thread is not changed, and a frame added to the thread is (VectorCraft's own behavior; `panels::character::tests::story_changes_follow_the_edited_frames_thread_after_the_first_saved_frame_leaves`, `story_changes_leave_out_a_frame_that_left_and_take_in_a_frame_that_joined`, `story_changes_stay_in_the_edited_frame_once_its_thread_is_removed`, `story_changes_take_in_locked_and_hidden_frames`, `panels::tabs::tests::indent_markers_after_select_all_keep_each_story_paragraphs_indents`, `dialogs::justification::tests::justification_dialog_follows_the_story_thread_at_ok`).
|
||||
|
||||
Reference in New Issue
Block a user