Keyboard Shortcuts: Export Text saves the shortcut list as a text file

Edit > Keyboard Shortcuts gains Export Text..., which saves the set shown
in the dialog, as edited so far, as a UTF-8 text file the user names in a
save dialog with a .txt filter. shortcuts.exportText {path?} does the same
for the current set over MCP and the control channel.

The file holds a "Keyboard Shortcuts" line and the set's name, then a
Tools and a Menu Commands section with the rows the dialog lists, in its
order and named as it shows them: the name, a tab and the shortcuts as the
dialog shows them on the current platform. A row with no shortcut ends
after the tab. A path that is not a string, or a file that cannot be
written, returns an error and nothing is written.

Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-11-08 11:42:48 +07:00
committed by bflatastic
parent 670880f686
commit 8f16d153e4
34 changed files with 248 additions and 1 deletions
+2
View File
@@ -1060,6 +1060,8 @@ font-technology Variable Променливи
Export for Screens Settings Настройки за експортиране за екрани
Export for Screens… Експортиране за екрани…
Export Keyboard Shortcuts… Експортиране на клавишни комбинации…
Export Keyboard Shortcuts as Text… Експортиране на клавишни комбинации като текст…
Export Text… Експортиране на текст…
Export Option Опция за експортиране
Export PDF Експортиране в PDF
Export PDF Presets Предварителни настройки за PDF
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -938,6 +938,8 @@ lighting Light Svjetlo
Expand Layers Panel Rows Proširi redove panela slojeva
Export CSS… Izvezi CSS…
Export Keyboard Shortcuts… Izvezi prečice na tastaturi…
Export Keyboard Shortcuts as Text… Izvezi prečice na tastaturi kao tekst…
Export Text… Izvezi tekst…
File Info Dialog Dijalog informacija o datoteci
Go to Artboard Idi na radnu ploču
First Artboard Prva radna ploča
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -1466,6 +1466,8 @@
Export for Screens Settings Configuració d’exportació per a pantalles
Export for Screens… Exporta per a pantalles…
Export Keyboard Shortcuts… Exporta les dreceres de teclat…
Export Keyboard Shortcuts as Text… Exporta les dreceres de teclat com a text…
Export Text… Exporta el text…
Export Layer Exporta la capa
Export Library Exporta la biblioteca
Export Library… Exporta la biblioteca…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -957,6 +957,8 @@ lighting Light Světlo
Expand Layers Panel Rows Rozbalit řádky panelu Vrstvy
Export CSS… Exportovat CSS…
Export Keyboard Shortcuts… Exportovat klávesové zkratky…
Export Keyboard Shortcuts as Text… Exportovat klávesové zkratky jako text…
Export Text… Exportovat text…
File Info Dialog Dialogové okno Informace o souboru
Go to Artboard Přejít na pracovní plochu
First Artboard První pracovní plocha
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -878,6 +878,8 @@ lighting Light Lys
Expand Layers Panel Rows Udvid rækker i lagpanelet
Export CSS… Eksportér CSS…
Export Keyboard Shortcuts… Eksportér tastaturgenveje…
Export Keyboard Shortcuts as Text… Eksportér tastaturgenveje som tekst…
Export Text… Eksportér tekst…
File Info Dialog Dialogboks til filoplysninger
Go to Artboard Gå til tegnebræt
Graphic Style Options… Indstillinger for grafisk typografi…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -1060,6 +1060,8 @@ font-technology Variable Variabel
Export for Screens Settings Einstellungen für „Für Bildschirme exportieren“
Export for Screens… Für Bildschirme exportieren…
Export Keyboard Shortcuts… Tastaturbefehle exportieren…
Export Keyboard Shortcuts as Text… Tastaturbefehle als Text exportieren…
Export Text… Text exportieren…
Export Option Exportoption
Export PDF PDF exportieren
Export PDF Presets PDF-Vorgaben exportieren
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -1033,6 +1033,8 @@ font-technology Variable Μεταβλητές
Export for Screens Settings Ρυθμίσεις εξαγωγής για οθόνες
Export for Screens… Εξαγωγή για οθόνες…
Export Keyboard Shortcuts… Εξαγωγή συντομεύσεων πληκτρολογίου…
Export Keyboard Shortcuts as Text… Εξαγωγή συντομεύσεων πληκτρολογίου ως κείμενο…
Export Text… Εξαγωγή κειμένου…
Export Option Επιλογή εξαγωγής
Export PDF Εξαγωγή PDF
Export PDF Presets Εξαγωγή προεπιλογών PDF
Can't render this file because it contains an unexpected character in line 163 and column 5.
+2
View File
@@ -1089,6 +1089,8 @@ font-technology Variable Variables
Export for Screens Settings Ajustes de Exportar para pantallas
Export for Screens… Exportar para pantallas…
Export Keyboard Shortcuts… Exportar métodos abreviados de teclado…
Export Keyboard Shortcuts as Text… Exportar métodos abreviados de teclado como texto…
Export Text… Exportar texto…
Export Option Opción de exportación
Export PDF Exportar PDF
Export PDF Presets Exportar preajustes de PDF
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -936,6 +936,8 @@ lighting Light Valo
Expand Layers Panel Rows Laajenna tasopaneelin rivit
Export CSS… Vie CSS…
Export Keyboard Shortcuts… Vie pikanäppäimet…
Export Keyboard Shortcuts as Text… Vie pikanäppäimet tekstinä…
Export Text… Vie teksti…
File Info Dialog Tiedoston tiedot -valintaikkuna
Go to Artboard Siirry piirtoalustaan
First Artboard Ensimmäinen piirtoalusta
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -1084,6 +1084,8 @@ font-technology Variable Variables
Export for Screens Settings Paramètres d’Exporter pour les écrans
Export for Screens… Exporter pour les écrans…
Export Keyboard Shortcuts… Exporter les raccourcis clavier…
Export Keyboard Shortcuts as Text… Exporter les raccourcis clavier en texte…
Export Text… Exporter le texte…
Export Option Option d’exportation
Export PDF Exporter en PDF
Export PDF Presets Exporter les paramètres prédéfinis PDF
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -1631,6 +1631,8 @@
Export for Screens Settings स्क्रीन सेटिंग के लिए निर्यात
Export for Screens… स्क्रीन के लिए निर्यात…
Export Keyboard Shortcuts… कीबोर्ड शॉर्टकट निर्यात करें…
Export Keyboard Shortcuts as Text… कीबोर्ड शॉर्टकट टेक्स्ट के रूप में निर्यात करें…
Export Text… टेक्स्ट निर्यात करें…
Export Layer निर्यात लेयर
Export Library लाइब्रेरी
Export Library… लाइब्रेरी निर्यात करें…
Can't render this file because it is too large.
+2
View File
@@ -1367,6 +1367,8 @@ font-technology Variable Változó
Export for Screens Settings Exportálás képernyőkre – beállítások
Export for Screens… Exportálás képernyőkre…
Export Keyboard Shortcuts… Billentyűparancsok exportálása…
Export Keyboard Shortcuts as Text… Billentyűparancsok exportálása szövegként…
Export Text… Szöveg exportálása…
Export Library Könyvtár exportálása
Export Library… Könyvtár exportálása…
Export Option Exportálási beállítás
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -878,6 +878,8 @@ lighting Light Cahaya
Expand Layers Panel Rows Perluas baris panel Lapisan
Export CSS… Ekspor CSS…
Export Keyboard Shortcuts… Ekspor pintasan papan ketik…
Export Keyboard Shortcuts as Text… Ekspor pintasan papan ketik sebagai teks…
Export Text… Ekspor teks…
File Info Dialog Dialog info file
Go to Artboard Buka artboard
Graphic Style Options… Opsi gaya grafis…
1 # Indonesian (id) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
878
879
880
881
882
883
884
885
+2
View File
@@ -1090,6 +1090,8 @@ font-technology Variable Variabili
Export for Screens Settings Impostazioni Esporta per schermi
Export for Screens… Esporta per schermi…
Export Keyboard Shortcuts… Esporta scelte rapide da tastiera…
Export Keyboard Shortcuts as Text… Esporta scelte rapide da tastiera come testo…
Export Text… Esporta testo…
Export Option Opzione di esportazione
Export PDF Esporta PDF
Export PDF Presets Esporta preset PDF
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -1581,6 +1581,8 @@ digits Malayalam マラヤーラム数字
Export Document ドキュメントを書き出し
Export EPS EPS を書き出し
Export Keyboard Shortcuts… キーボードショートカットを書き出し…
Export Keyboard Shortcuts as Text… キーボードショートカットをテキストで書き出し…
Export Text… テキストを書き出し…
Export Option 書き出しオプション
Export PDF PDF を書き出し
Export PDF Presets PDF プリセットを書き出し
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -831,6 +831,8 @@ lighting Light სინათლე
Expand Layers Panel Rows შრეების პანელის სტრიქონების გაშლა
Export CSS… CSS-ის ექსპორტირება…
Export Keyboard Shortcuts… კლავიატურის მალსახმობების ექსპორტირება…
Export Keyboard Shortcuts as Text… კლავიატურის მალსახმობების ტექსტად ექსპორტირება…
Export Text… ტექსტის ექსპორტი…
File Info Dialog ფაილის ინფორმაციის დიალოგი
Go to Artboard სამუშაო დაფაზე გადასვლა
Graphic Style Options… გრაფიკული სტილის პარამეტრები…
Can't render this file because it is too large.
+2
View File
@@ -1571,6 +1571,8 @@ digits Malayalam 말라얄람어
Export Document 문서 내보내기
Export EPS EPS 내보내기
Export Keyboard Shortcuts… 단축키 내보내기…
Export Keyboard Shortcuts as Text… 단축키를 텍스트로 내보내기…
Export Text… 텍스트 내보내기…
Export Option 내보내기 옵션
Export PDF PDF 내보내기
Export PDF Presets PDF 프리셋 내보내기
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -892,6 +892,8 @@ lighting Light Licht
Expand Layers Panel Rows Rijen in het Lagen-paneel uitvouwen
Export CSS… CSS exporteren…
Export Keyboard Shortcuts… Sneltoetsen exporteren…
Export Keyboard Shortcuts as Text… Sneltoetsen als tekst exporteren…
Export Text… Tekst exporteren…
File Info Dialog Dialoogvenster bestandsinfo
Go to Artboard Ga naar tekengebied
Graphic Style Options… Opties voor grafische stijl…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -894,6 +894,8 @@ lighting Light Światło
Expand Layers Panel Rows Rozwiń wiersze panelu Warstwy
Export CSS… Eksportuj CSS…
Export Keyboard Shortcuts… Eksportuj skróty klawiaturowe…
Export Keyboard Shortcuts as Text… Eksportuj skróty klawiaturowe jako tekst…
Export Text… Eksportuj tekst…
File Info Dialog Okno informacji o pliku
Go to Artboard Przejdź do obszaru kompozycji
First Artboard Pierwszy obszar kompozycji
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -981,6 +981,8 @@
Export Document Exportar Documento
Export EPS Exportar EPS
Export Keyboard Shortcuts… Exportar Atalhos de Teclado…
Export Keyboard Shortcuts as Text… Exportar Atalhos de Teclado como Texto…
Export Text… Exportar Texto…
Export Option Opção de Exportação
Export PDF Exportar PDF
Export PDF Presets Exportar Predefinidos de PDF
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2
View File
@@ -1087,6 +1087,8 @@ font-technology Variable Вариативные
Export for Screens Settings Параметры экспорта для экранов
Export for Screens… Экспортировать для экранов…
Export Keyboard Shortcuts… Экспортировать сочетания клавиш…
Export Keyboard Shortcuts as Text… Экспортировать сочетания клавиш как текст…
Export Text… Экспорт текста…
Export Option Параметр экспорта
Export PDF Экспортировать PDF
Export PDF Presets Экспортировать наборы PDF
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -1068,6 +1068,8 @@ font-technology Variable Variabla
Export for Screens Settings Inställningar för Exportera för skärmar
Export for Screens… Exportera för skärmar…
Export Keyboard Shortcuts… Exportera kortkommandon…
Export Keyboard Shortcuts as Text… Exportera kortkommandon som text…
Export Text… Exportera text…
Export Option Exportalternativ
Export PDF Exportera PDF
Export PDF Presets Exportera PDF-förinställningar
Can't render this file because it contains an unexpected character in line 5 and column 25.
+2
View File
@@ -1060,6 +1060,8 @@ font-technology Variable Değişken
Export for Screens Settings Ekran Ayarları için Dışa Aktarma
Export for Screens… Ekranlar için Dışa Aktarma…
Export Keyboard Shortcuts… Klavye Kısayollarını Dışa Aktar…
Export Keyboard Shortcuts as Text… Klavye Kısayollarını Metin Olarak Dışa Aktar…
Export Text… Metni Dışa Aktar…
Export Option Dışa Aktarma Seçeneği
Export PDF PDF'yi dışa aktar
Export PDF Presets PDF Hazır Ayarlarını Dışa Aktar
Can't render this file because it contains an unexpected character in line 5 and column 25.
+2
View File
@@ -1084,6 +1084,8 @@ font-technology Variable Варіативні
Export for Screens Settings Параметри експорту для екранів
Export for Screens… Експортувати для екранів…
Export Keyboard Shortcuts… Експортувати сполучення клавіш…
Export Keyboard Shortcuts as Text… Експортувати сполучення клавіш як текст…
Export Text… Експорт тексту…
Export Option Параметр експорту
Export PDF Експортувати PDF
Export PDF Presets Експортувати набори PDF
Can't render this file because it contains an unexpected character in line 177 and column 5.
+2
View File
@@ -1244,6 +1244,8 @@ font-technology Variable Biến thiên
Export for Screens Settings Cài đặt xuất cho màn hình
Export for Screens… Xuất cho màn hình…
Export Keyboard Shortcuts… Xuất phím tắt…
Export Keyboard Shortcuts as Text… Xuất phím tắt dưới dạng văn bản…
Export Text… Xuất văn bản…
Export Library Xuất thư viện
Export Library… Xuất thư viện…
Export Option Tùy chọn xuất
Can't render this file because it contains an unexpected character in line 199 and column 5.
+2
View File
@@ -1030,6 +1030,8 @@
Export for Screens Settings 导出为屏幕所用格式设置
Export for Screens… 导出为屏幕所用格式…
Export Keyboard Shortcuts… 导出键盘快捷键…
Export Keyboard Shortcuts as Text… 将键盘快捷键导出为文本…
Export Text… 导出文本…
Export Option 导出选项
Export PDF 导出 PDF
Export PDF Presets 导出 PDF 预设
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -1054,6 +1054,8 @@ font-technology Variable 可變
Export for Screens Settings 匯出為螢幕適用設定
Export for Screens… 匯出為螢幕適用…
Export Keyboard Shortcuts… 匯出鍵盤快速鍵…
Export Keyboard Shortcuts as Text… 將鍵盤快速鍵匯出為文字…
Export Text… 匯出文字…
Export Option 匯出選項
Export PDF 匯出 PDF
Export PDF Presets 匯出 PDF 預設集
Can't render this file because it contains an unexpected character in line 6 and column 47.
+6
View File
@@ -167,6 +167,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"{name: \"VectorCraft Defaults\" | \"Classic Defaults\"} (names of earlier versions are accepted)",
),
("shortcuts.export", "Export Keyboard Shortcuts…", "", "{path?}"),
(
"shortcuts.exportText",
"Export Keyboard Shortcuts as Text…",
"",
"{path?} save the current set's tools and menu commands with their shortcuts as a UTF-8 text file (Keyboard Shortcuts › Export Text…): the set's name, then a Tools and a Menu Commands section in the dialog's order, one `name<TAB>shortcut` line per row, empty after the tab when it has none; without `path`, pick a file",
),
("shortcuts.import", "Import Keyboard Shortcuts…", "", "{path? | data?}"),
(
"ui.shortcutImportDialog",
+1
View File
@@ -76,6 +76,7 @@ const DOCUMENT_FREE: &[&str] = &[
"ui.libraryPanel.locate",
"shortcuts.import",
"shortcuts.export",
"shortcuts.exportText",
"ui.shortcutImportDialog",
crate::panels::actions::SAVE_SET,
crate::panels::actions::LOAD_SET,
+138 -1
View File
@@ -700,6 +700,25 @@ pub fn import_json(v: &Value) -> Result<(String, Overrides), String> {
Ok((v.get("set").and_then(Value::as_str).map_or(CUSTOM, set_name).to_string(), out))
}
/// Export Text: the set's name, then the dialog's Tools and Menu Commands lists in its order, one
/// row per line with the name and the shortcuts as the dialog shows them, separated by a tab. A
/// row with no shortcut ends after the tab.
pub fn export_text(set: &str, overrides: &Overrides) -> String {
let km = keymap_with(overrides);
let backtick = backtick_text();
let shown_set = if PRESETS.contains(&set) || set == CUSTOM { tl!(set) } else { set };
let mut out = format!("{}\n{} {shown_set}\n", tl!("Keyboard Shortcuts"), tl!("Set:"));
for (tools, heading) in [(true, "Tools"), (false, "Menu Commands")] {
out += &format!("\n{}\n", tl!(heading));
for e in entries().iter().filter(|e| e.is_tool == tools && listed(km.overrides(), e)) {
let name = if e.is_tool || e.group.is_empty() { tl!(&e.label).to_string() } else { format!("{} › {}", tl!(&e.group), tl!(&e.label)) };
let sc = dialog_shortcuts_with(&km, &e.key, backtick.as_deref()).unwrap_or_default();
out += &format!("{name}\t{sc}\n");
}
}
out
}
// ---------- UI commands ----------
pub fn open(app: &mut VectorcraftApp) {
@@ -800,6 +819,31 @@ pub fn run_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<Resu
},
}
}
"shortcuts.exportText" => {
let path = match p.get("path") {
None | Some(Value::Null) => None,
Some(Value::String(path)) => Some(path.clone()),
Some(_) => {
let key = "path";
return Some(Err(format!("`{key}` must be a string")));
}
};
let text = export_text(&app.ui.shortcut_set, &app.ui.shortcut_overrides);
let pick =
crate::FilePick { filters: vec![crate::FilePick::filter("Text", &["txt"])], ..crate::FilePick::named("VectorCraft Shortcuts.txt") };
match path.or_else(|| crate::picks::save(app, &pick)) {
Some(path) => match app.services.write.as_mut() {
Some(w) => w(&path, text.as_bytes()).map(|_| json!({"path": path})),
None => Err("no file writer".into()),
},
// Its save dialog is open: the file is written when it answers.
None if app.file_dialog_open() => Err("cancelled".into()),
None => match app.services.download.as_mut() {
Some(dl) => dl("VectorCraft Shortcuts.txt", text.as_bytes()).map(|()| Value::Null),
None => Ok(json!({"text": text})),
},
}
}
"shortcuts.import" => {
let data = if let Some(v) = p.get("data") {
Ok(v.clone())
@@ -947,6 +991,9 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
if ui.button(tl!("Export…")).clicked() {
d.fields.insert("__export".into(), json!(true));
}
if ui.button(tl!("Export Text…")).clicked() {
d.fields.insert("__exportText".into(), json!(true));
}
if ui.button(tl!("Reset to Defaults")).clicked() {
km.to_mut().reset_all();
d.fields.insert("set".into(), json!(PRESETS[0]));
@@ -1090,6 +1137,7 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
}
let import = d.fields.remove("__import").is_some();
let export = d.fields.remove("__export").is_some();
let export_text = d.fields.remove("__exportText").is_some();
app.ui.dialog = Some(d);
// Their file dialogs, shown off the UI thread, import into or export from the dialog as it is
// when they answer.
@@ -1099,6 +1147,9 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
if export {
crate::picks::button(app, export_from_dialog);
}
if export_text {
crate::picks::button(app, export_text_from_dialog);
}
if cancel {
app.ui.dialog = None;
} else if ok && let Err(e) = confirm(app) {
@@ -1137,6 +1188,16 @@ fn import_into_dialog(app: &mut VectorcraftApp) {
/// The dialog's Export…: its working set, as edited so far.
fn export_from_dialog(app: &mut VectorcraftApp) {
export_dialog_set(app, "shortcuts.export");
}
/// The dialog's Export Text…: its working set, as edited so far, as a text file.
fn export_text_from_dialog(app: &mut VectorcraftApp) {
export_dialog_set(app, "shortcuts.exportText");
}
/// Runs `command` on the dialog's working set and set name.
fn export_dialog_set(app: &mut VectorcraftApp, command: &str) {
if !shortcuts_dialog(app) {
app.status(crate::picks::dialog_gone());
return;
@@ -1144,7 +1205,7 @@ fn export_from_dialog(app: &mut VectorcraftApp) {
let (ov, set) = app.ui.dialog.as_ref().map(|d| (dialog_overrides(d), d.str("set"))).unwrap_or_default();
let saved = std::mem::replace(&mut app.ui.shortcut_overrides, ov);
let saved_set = std::mem::replace(&mut app.ui.shortcut_set, set);
if let Some(Err(e)) = run_command(app, "shortcuts.export", &json!({})) {
if let Some(Err(e)) = run_command(app, command, &json!({})) {
app.status(e);
}
app.ui.shortcut_overrides = saved;
@@ -1453,6 +1514,82 @@ mod tests {
assert!(import_json(&json!({})).is_err());
}
/// An Export Text line: the row's name as the dialog shows it, a tab and `shortcut`.
fn text_row(key: &str, shortcut: &str) -> String {
let e = entry(key).unwrap_or_else(|| panic!("no entry {key}"));
if e.is_tool || e.group.is_empty() { format!("{}\t{shortcut}", e.label) } else { format!("{} › {}\t{shortcut}", e.group, e.label) }
}
/// Export Text writes the set's name, then the Tools and the Menu Commands rows in the dialog's
/// order, each with its shortcut as the dialog shows it.
#[test]
fn export_text_lists_the_dialogs_rows_with_their_shortcuts() {
let mut ov = Overrides::new();
crate::keymap::set_override(&mut ov, "edit.preferences", "Ctrl+Alt+Shift+9");
let text = export_text("VectorCraft Defaults", &ov);
let lines: Vec<&str> = text.lines().collect();
assert_eq!(lines[..4], ["Keyboard Shortcuts", "Set: VectorCraft Defaults", "", "Tools"]);
let menu_at = lines.iter().position(|l| *l == "Menu Commands").expect("a Menu Commands section");
assert_eq!(lines[menu_at - 1], "", "a blank line before Menu Commands");
let km = keymap_with(&ov);
let names = |tools: bool| -> Vec<String> {
entries().iter().filter(|e| e.is_tool == tools && listed(km.overrides(), e)).map(|e| text_row(&e.key, "")).collect()
};
let names_of = |rows: &[&str]| -> Vec<String> { rows.iter().map(|l| format!("{}\t", l.split('\t').next().unwrap_or_default())).collect() };
assert_eq!(names_of(&lines[4..menu_at - 1]), names(true), "the Tools rows, in the dialog's order");
assert_eq!(names_of(&lines[menu_at + 1..]), names(false), "the Menu Commands rows, in the dialog's order");
let pen = menus::pretty_shortcut(default_of("tool:pen").expect("the Pen tool has a default key"));
assert!(lines[4..menu_at].contains(&text_row("tool:pen", &pen).as_str()), "{text}");
let prefs = menus::pretty_shortcut(&effective_with(&km, "edit.preferences").expect("the shortcut set"));
assert!(prefs.ends_with('9'), "{prefs}");
assert!(lines[menu_at..].contains(&text_row("edit.preferences", &prefs).as_str()), "{text}");
assert!(text.ends_with('\n'));
}
/// A command without a shortcut is listed with nothing after the tab: one without a default key
/// and one whose shortcut the set clears.
#[test]
fn export_text_lists_a_command_without_a_shortcut_with_an_empty_shortcut() {
let mut ov = Overrides::new();
crate::keymap::set_override(&mut ov, "edit.preferences", "");
assert!(default_of("align.horizontalLeft").is_none());
let text = export_text(CUSTOM, &ov);
let lines: Vec<&str> = text.lines().collect();
assert_eq!(lines[1], "Set: Custom");
for key in ["align.horizontalLeft", "edit.preferences"] {
assert!(lines.contains(&text_row(key, "").as_str()), "{key}: {text}");
}
}
/// `shortcuts.exportText` writes the set's name as the set was named, and a path that can't be
/// written or isn't a string gives an error and writes nothing.
#[test]
fn export_text_command_writes_the_sets_name_and_rejects_a_bad_path() {
type Files = std::rc::Rc<std::cell::RefCell<Vec<(String, String)>>>;
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
let written: Files = Default::default();
let w = written.clone();
app.services.write = Some(Box::new(move |path: &str, b: &[u8]| {
if path.starts_with("/missing/") {
return Err(format!("can't write {path}"));
}
w.borrow_mut().push((path.to_string(), String::from_utf8(b.to_vec()).map_err(|e| e.to_string())?));
Ok(())
}));
app.run("shortcuts.import", json!({"data": export_json("Studio", &Overrides::new())})).unwrap();
assert_eq!(app.run("shortcuts.exportText", json!({"path": "/sets/Studio.txt"})), Ok(json!({"path": "/sets/Studio.txt"})));
assert_eq!(
app.run("shortcuts.exportText", json!({"path": "/missing/dir/Studio.txt"})),
Err("can't write /missing/dir/Studio.txt".to_string())
);
assert_eq!(app.run("shortcuts.exportText", json!({"path": 5})), Err("`path` must be a string".to_string()));
let files = written.borrow();
let [(path, text)] = files.as_slice() else { panic!("{files:?}") };
assert_eq!(path, "/sets/Studio.txt");
assert!(text.starts_with("Keyboard Shortcuts\nSet: Studio\n\nTools\n"), "{text}");
assert_eq!(*text, export_text("Studio", &Overrides::new()));
}
/// The dialog's Import… leaves an action its function key when the imported set gives the key
/// to a command, through both Cancel and OK.
#[test]
+32
View File
@@ -405,6 +405,38 @@ fn keyboard_shortcuts_import_and_export_ask_off_the_ui_thread() {
assert_eq!(*written.borrow(), ["/keys/out.json", "/keys/command.json"]);
}
/// The Keyboard Shortcuts dialog's Export Text… asks off the UI thread for a `.txt` file and writes
/// the dialog's working set, as edited so far, as text where the dialog answers.
#[test]
fn keyboard_shortcuts_export_text_writes_the_working_set_as_text() {
let (mut app, shown, _) = app();
let texts: Rc<RefCell<Vec<(String, String)>>> = Default::default();
let t = texts.clone();
app.services.write = Some(Box::new(move |path: &str, b: &[u8]| {
t.borrow_mut().push((path.to_string(), String::from_utf8_lossy(b).into_owned()));
Ok(())
}));
let ov = crate::keymap::overrides_from(&[("view.grid", "Ctrl+Alt+G")]);
app.run("edit.keyboardShortcuts", json!({})).unwrap();
let d = app.ui.dialog.as_mut().expect("Keyboard Shortcuts");
d.fields.insert("overrides".into(), crate::keymap::stored::to_value(&ov));
d.fields.insert("set".into(), json!("Custom"));
click(&mut app, "Export Text…", dialogs);
match shown.requests().as_slice() {
[PickRequest::Save(pick)] => assert_eq!(pick.filters, FilePick::filters([("Text", &["txt"][..])])),
other => panic!("{other:?}"),
}
shown.answer(&["/keys/list.txt"]);
poll(&mut app);
let texts = texts.borrow();
let [(path, text)] = texts.as_slice() else { panic!("{texts:?}") };
assert_eq!(path, "/keys/list.txt");
assert_eq!(*text, crate::shortcut_editor::export_text("Custom", &ov));
assert!(text.contains(&format!("\t{}\n", crate::menus::pretty_shortcut("Ctrl+Alt+G"))), "{text}");
assert!(app.ui.shortcut_overrides.is_empty(), "the working set is kept only on OK");
assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some("shortcuts"), "the dialog stays open");
}
/// Keyboard Shortcuts' Import… opens a shortcut set another app saved as text (`.kys`) in Import
/// Shortcut Set over the dialog when its file dialog answers, and the dialog's working set stays in
/// Keyboard Shortcuts, which Cancel shows again.
+2
View File
@@ -88,6 +88,8 @@ fn save_panels_show_the_file_type_they_write() {
assert_eq!(last_shown(&shown), FilePick::filters([("PostScript", &["ps"][..])]), "Print to a PostScript file");
app.run("shortcuts.export", json!({})).unwrap();
assert_eq!(last_shown(&shown), FilePick::filters([("Shortcut sets", &["json"][..])]), "Keyboard Shortcuts › Export…");
app.run("shortcuts.exportText", json!({})).unwrap();
assert_eq!(last_shown(&shown), FilePick::filters([("Text", &["txt"][..])]), "Keyboard Shortcuts › Export Text…");
let presets = [
("pdf.preset.save", json!({"name": "Mine"}), "pdf.preset.export", "PDF presets", pdfcmds::PRESET_EXTS),
("print.presets.save", json!({"name": "Mine"}), "print.presets.export", "Print presets", printpresets::PRESET_EXTS),
+14
View File
@@ -3401,6 +3401,20 @@ They list such a command while the user has changed its shortcut (`overridden: t
shortcut for that command runs what a click on the item runs: Object › Path › Clean Up…, Print Ink Fixes… and Edit ›
Find and Replace… open their dialogs.
`shortcuts.exportText {path?}` saves the current set's shortcut list as a UTF-8 text file, as Edit › Keyboard
Shortcuts › Export Text… does with the dialog's set as edited so far. The file starts with a "Keyboard Shortcuts" line
and a "Set:" line with the set's name, followed by a Tools section and a Menu Commands section. Each section lists the
rows Edit › Keyboard Shortcuts lists, in its order and with names as it shows them: one line per row, with the name, a
tab and the shortcuts as the dialog shows them on the current platform. A row with no shortcut ends after the tab.
Without `path` a save dialog asks for the file. A `path` that is not a string, or a file that cannot be written,
returns an error, and nothing is written. The layout is VectorCraft's own. The reference app's [Export keyboard
shortcuts list as a text document](https://helpx.adobe.com/illustrator/desktop/get-started/preferences-and-settings/export-keyboard-shortcuts-list.html)
page describes saving the list from the Keyboard Shortcuts dialog as a text file, and does not describe its layout.
```json
{"name":"run_command","arguments":{"command":"shortcuts.exportText","params":{"path":"/Users/me/Shortcuts.txt"}}}
```
The desktop app adds its active artboard, the one the Artboards panel highlights, to the `object.align`,
`object.distribute`, `object.distributeSpacing` and `select.allOnArtboard` calls it runs without `artboard`, as it does
for `edit.pasteInPlace`, `edit.pasteInFront` and `edit.pasteInBack`, and to each `command.batch`. A batch passes its
+1
View File
@@ -765,6 +765,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- Cmd/Ctrl+F6 (`window.nextDocument`) and Cmd/Ctrl+Shift+F6 (`window.previousDocument`) make the next and the previous open document the current one, in tab order and as a click on its tab does, on the keys the reference app's [Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html) page lists for navigating to the next and the previous document. After the last document comes the first, and before the first document comes the last. From the Home screen the keys go to the first and the last document. Keyboard Shortcuts lists both under Window.
- Cmd/Ctrl+Alt+Shift+U (`document.switchUnits`) switches the document to the next unit of measurement, in one undo step, on the key the reference app's [Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html) page lists for switching units of measurement. The page does not give an order. VectorCraft steps through the units in the order a right-click on a ruler lists them (Points, Picas, Inches, Millimeters, Centimeters, Pixels, Feet & Inches, Meters, Yards, Feet) and goes from Feet back to Points.
- Shortcut sets other apps save as text (`.kys`) open in Import Shortcut Set from File › Open, from Edit › Keyboard Shortcuts › Import… in the desktop app, or from a drop on the window. It matches each entry to a tool, command or panel by name and decodes the file's key and modifier codes where VectorCraft's default shortcuts agree on them; the user answers the rest, and Apply fills the Keyboard Shortcuts dialog's working set. The answers are kept with the preferences for the next import (`shortcutSet.read`, `importMatches.*`).
- Edit › Keyboard Shortcuts › Export Text… saves the set shown in the dialog as a UTF-8 text file, as the reference app's [Export keyboard shortcuts list as a text document](https://helpx.adobe.com/illustrator/desktop/get-started/preferences-and-settings/export-keyboard-shortcuts-list.html) page describes (`shortcuts.exportText {path?}`). The layout is VectorCraft's own: the set's name, then the Tools and Menu Commands rows in the dialog's order, each with its name, a tab and its shortcuts as the dialog shows them, and nothing after the tab for a row with no shortcut. Tests `shortcut_editor::tests::export_text_*` and `tests::picks::keyboard_shortcuts_export_text_writes_the_working_set_as_text`.
- On Windows and Linux the app bar is the window's title bar (its own minimize, maximize and close buttons, drag to move, edges to resize), with the dragon app icon as the brand mark; dialogs and menus size to their content. As on a native menu bar, once one menu is open, moving the pointer onto another title opens that one.
- Edit › Keyboard Shortcuts lists items of the menu bar and panel menus, as the reference app's [Customize keyboard shortcuts](https://web.archive.org/web/20221205131209id_/https://helpx.adobe.com/illustrator/using/customizing-keyboard-shortcuts.html) page describes for its Menu Commands. A menu command with no default shortcut that needs a value a key press does not give, such as File › Document Setup › Units (`document.setUnits {units}`), is listed only while the user has changed its shortcut. A command stays listed when a menu bar item runs it without a value or when it opens a dialog run without one, as the Variables panel's commands do.
- A shortcut on the key left of 1 runs whatever character that key types, and Edit › Keyboard Shortcuts, the menus in the window, the command palette and tool tips show it as that character once the key has been pressed: `"` on a Turkish Q layout, `~` on a US layout as before. Shortcuts saved as `~` keep working (#4559).