Skip to content

Commit be233d0

Browse files
committed
Use OS-GUI's new shortcutLabel and ariaKeyShortcuts properties
1 parent bf780b8 commit be233d0

1 file changed

Lines changed: 140 additions & 28 deletions

File tree

src/menus.js

Lines changed: 140 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const menus = {
2323
[localize("&File")]: [
2424
{
2525
label: localize("&New"),
26-
shortcut: window.is_electron_app ? "Ctrl+N" : "Ctrl+Alt+N", // Ctrl+N opens a new browser window
26+
...shortcut(window.is_electron_app ? "Ctrl+N" : "Ctrl+Alt+N"), // Ctrl+N opens a new browser window
2727
speech_recognition: [
2828
"new", "new file", "new document", "create new document", "create a new document", "start new document", "start a new document",
2929
],
@@ -32,7 +32,7 @@ const menus = {
3232
},
3333
{
3434
label: localize("&Open"),
35-
shortcut: "Ctrl+O",
35+
...shortcut("Ctrl+O"),
3636
speech_recognition: [
3737
"open", "open document", "open file", "open an image file", "open a document", "open a file",
3838
"load document", "load a document", "load an image file", "load an image",
@@ -44,7 +44,7 @@ const menus = {
4444
},
4545
{
4646
label: localize("&Save"),
47-
shortcut: "Ctrl+S",
47+
...shortcut("Ctrl+S"),
4848
speech_recognition: [
4949
"save", "save document", "save file", "save image", "save picture", "save image file",
5050
// "save a document", "save a file", "save an image", "save an image file", // too "save as"-like
@@ -60,7 +60,7 @@ const menus = {
6060
label: localize("Save &As"),
6161
// in mspaint, no shortcut is listed; it supports F12 (but in a browser that opens the dev tools)
6262
// it doesn't support Ctrl+Shift+S but that's a good & common modern shortcut
63-
shortcut: "Ctrl+Shift+S",
63+
...shortcut("Ctrl+Shift+S"),
6464
speech_recognition: [
6565
// this is ridiculous
6666
// this would be really simple in JSGF format
@@ -170,7 +170,7 @@ const menus = {
170170
},
171171
{
172172
label: localize("&Print"),
173-
shortcut: "Ctrl+P", // relies on browser's print shortcut being Ctrl+P
173+
...shortcut("Ctrl+P"), // relies on browser's print shortcut being Ctrl+P
174174
speech_recognition: [
175175
"print", "send to printer", "show print dialog",
176176
"print page", "print image", "print picture", "print drawing",
@@ -222,7 +222,7 @@ const menus = {
222222
MENU_DIVIDER,
223223
{
224224
label: localize("E&xit"),
225-
shortcut: window.is_electron_app ? "Alt+F4" : "", // Alt+F4 closes the browser window (in most window managers)
225+
...shortcut(window.is_electron_app ? "Alt+F4" : ""), // Alt+F4 closes the browser window (in most window managers)
226226
speech_recognition: [
227227
"exit application", "exit paint", "close paint window",
228228
],
@@ -273,7 +273,7 @@ const menus = {
273273
[localize("&Edit")]: [
274274
{
275275
label: localize("&Undo"),
276-
shortcut: "Ctrl+Z",
276+
...shortcut("Ctrl+Z"),
277277
speech_recognition: [
278278
"undo", "undo that",
279279
],
@@ -283,7 +283,7 @@ const menus = {
283283
},
284284
{
285285
label: localize("&Repeat"),
286-
shortcut: "F4", // also supported: Ctrl+Shift+Z, Ctrl+Y
286+
...shortcut("F4"), // also supported: Ctrl+Shift+Z, Ctrl+Y
287287
speech_recognition: [
288288
"repeat", "redo",
289289
],
@@ -293,7 +293,7 @@ const menus = {
293293
},
294294
{
295295
label: localize("&History"),
296-
shortcut: "Ctrl+Shift+Y",
296+
...shortcut("Ctrl+Shift+Y"),
297297
speech_recognition: [
298298
"show history", "history",
299299
],
@@ -303,7 +303,7 @@ const menus = {
303303
MENU_DIVIDER,
304304
{
305305
label: localize("Cu&t"),
306-
shortcut: "Ctrl+X",
306+
...shortcut("Ctrl+X"),
307307
speech_recognition: [
308308
"cut", "cut selection", "cut selection to clipboard", "cut the selection", "cut the selection to clipboard", "cut the selection to the clipboard",
309309
],
@@ -317,7 +317,7 @@ const menus = {
317317
},
318318
{
319319
label: localize("&Copy"),
320-
shortcut: "Ctrl+C",
320+
...shortcut("Ctrl+C"),
321321
speech_recognition: [
322322
"copy", "copy selection", "copy selection to clipboard", "copy the selection", "copy the selection to clipboard", "copy the selection to the clipboard",
323323
],
@@ -331,7 +331,7 @@ const menus = {
331331
},
332332
{
333333
label: localize("&Paste"),
334-
shortcut: "Ctrl+V",
334+
...shortcut("Ctrl+V"),
335335
speech_recognition: [
336336
"paste", "paste from clipboard", "paste from the clipboard", "insert clipboard", "insert clipboard contents", "insert the contents of the clipboard", "paste what's on the clipboard",
337337
],
@@ -345,7 +345,7 @@ const menus = {
345345
},
346346
{
347347
label: localize("C&lear Selection"),
348-
shortcut: "Del",
348+
...shortcut("Del"),
349349
speech_recognition: [
350350
"delete", "clear selection", "delete selection", "delete selected", "delete selected area", "clear selected area", "erase selected", "erase selected area",
351351
],
@@ -355,7 +355,7 @@ const menus = {
355355
},
356356
{
357357
label: localize("Select &All"),
358-
shortcut: "Ctrl+A",
358+
...shortcut("Ctrl+A"),
359359
speech_recognition: [
360360
"select all", "select everything",
361361
"select the whole image", "select the whole picture", "select the whole drawing", "select the whole canvas", "select the whole document",
@@ -390,7 +390,7 @@ const menus = {
390390
[localize("&View")]: [
391391
{
392392
label: localize("&Tool Box"),
393-
shortcut: window.is_electron_app ? "Ctrl+T" : "", // Ctrl+T opens a new browser tab, Ctrl+Alt+T opens a Terminal in Ubuntu, and Ctrl+Shift+Alt+T feels silly.
393+
...shortcut(window.is_electron_app ? "Ctrl+T" : ""), // Ctrl+T opens a new browser tab, Ctrl+Alt+T opens a Terminal in Ubuntu, and Ctrl+Shift+Alt+T feels silly.
394394
speech_recognition: [
395395
"toggle tool box", "toggle tools box", "toggle toolbox", "toggle tool palette", "toggle tools palette",
396396
// @TODO: hide/show
@@ -405,7 +405,7 @@ const menus = {
405405
},
406406
{
407407
label: localize("&Color Box"),
408-
shortcut: "Ctrl+L", // focuses browser address bar, but Firefox and Chrome both allow overriding the default behavior
408+
...shortcut("Ctrl+L"), // focuses browser address bar, but Firefox and Chrome both allow overriding the default behavior
409409
speech_recognition: [
410410
"toggle color box", "toggle colors box", "toggle palette", "toggle color palette", "toggle colors palette",
411411
// @TODO: hide/show
@@ -455,7 +455,7 @@ const menus = {
455455
submenu: [
456456
{
457457
label: localize("&Normal Size"),
458-
shortcut: window.is_electron_app ? "Ctrl+PgUp" : "", // Ctrl+PageUp cycles thru browser tabs in Chrome & Firefox; can be overridden in Chrome in fullscreen only
458+
...shortcut(window.is_electron_app ? "Ctrl+PgUp" : ""), // Ctrl+PageUp cycles thru browser tabs in Chrome & Firefox; can be overridden in Chrome in fullscreen only
459459
speech_recognition: [
460460
"reset zoom", "zoom to normal size",
461461
"zoom to 100%", "set zoom to 100%", "set zoom 100%",
@@ -471,7 +471,7 @@ const menus = {
471471
},
472472
{
473473
label: localize("&Large Size"),
474-
shortcut: window.is_electron_app ? "Ctrl+PgDn" : "", // Ctrl+PageDown cycles thru browser tabs in Chrome & Firefox; can be overridden in Chrome in fullscreen only
474+
...shortcut(window.is_electron_app ? "Ctrl+PgDn" : ""), // Ctrl+PageDown cycles thru browser tabs in Chrome & Firefox; can be overridden in Chrome in fullscreen only
475475
speech_recognition: [
476476
"zoom to large size",
477477
"zoom to 400%", "set zoom to 400%", "set zoom 400%",
@@ -530,7 +530,7 @@ const menus = {
530530
MENU_DIVIDER,
531531
{
532532
label: localize("Show &Grid"),
533-
shortcut: "Ctrl+G",
533+
...shortcut("Ctrl+G"),
534534
speech_recognition: [
535535
"toggle show grid",
536536
"toggle grid", "toggle gridlines", "toggle grid lines", "toggle grid cells",
@@ -562,7 +562,7 @@ const menus = {
562562
},
563563
{
564564
label: localize("&View Bitmap"),
565-
shortcut: "Ctrl+F",
565+
...shortcut("Ctrl+F"),
566566
speech_recognition: [
567567
"view bitmap", "show bitmap",
568568
"fullscreen", "full-screen", "full screen",
@@ -576,7 +576,7 @@ const menus = {
576576
MENU_DIVIDER,
577577
{
578578
label: localize("&Fullscreen"),
579-
shortcut: "F11", // relies on browser's shortcut
579+
...shortcut("F11"), // relies on browser's shortcut
580580
speech_recognition: [
581581
// won't work with speech recognition, needs a user gesture
582582
],
@@ -603,7 +603,7 @@ const menus = {
603603
// @TODO: speech recognition: terms that apply to selection
604604
{
605605
label: localize("&Flip/Rotate"),
606-
shortcut: (window.is_electron_app && !window.electron_is_dev) ? "Ctrl+R" : "Ctrl+Alt+R", // Ctrl+R reloads the browser tab (or Electron window in dev mode via electron-debug)
606+
...shortcut((window.is_electron_app && !window.electron_is_dev) ? "Ctrl+R" : "Ctrl+Alt+R"), // Ctrl+R reloads the browser tab (or Electron window in dev mode via electron-debug)
607607
speech_recognition: [
608608
"flip",
609609
"rotate",
@@ -615,7 +615,7 @@ const menus = {
615615
},
616616
{
617617
label: localize("&Stretch/Skew"),
618-
shortcut: window.is_electron_app ? "Ctrl+W" : "Ctrl+Alt+W", // Ctrl+W closes the browser tab
618+
...shortcut(window.is_electron_app ? "Ctrl+W" : "Ctrl+Alt+W"), // Ctrl+W closes the browser tab
619619
speech_recognition: [
620620
"stretch", "scale", "resize image",
621621
"skew",
@@ -627,7 +627,7 @@ const menus = {
627627
},
628628
{
629629
label: localize("&Invert Colors"),
630-
shortcut: "Ctrl+I",
630+
...shortcut("Ctrl+I"),
631631
speech_recognition: [
632632
"invert",
633633
"invert colors",
@@ -640,7 +640,7 @@ const menus = {
640640
},
641641
{
642642
label: `${localize("&Attributes")}...`,
643-
shortcut: "Ctrl+E",
643+
...shortcut("Ctrl+E"),
644644
speech_recognition: [
645645
"attributes", "image attributes", "picture attributes", "image options", "picture options",
646646
"dimensions", "image dimensions", "picture dimensions",
@@ -654,7 +654,7 @@ const menus = {
654654
},
655655
{
656656
label: localize("&Clear Image"),
657-
shortcut: (window.is_electron_app || !looksLikeChrome) ? "Ctrl+Shift+N" : "", // Ctrl+Shift+N opens incognito window in chrome
657+
...shortcut((window.is_electron_app || !looksLikeChrome) ? "Ctrl+Shift+N" : ""), // Ctrl+Shift+N opens incognito window in chrome
658658
speech_recognition: [
659659
"clear image", "clear canvas", "clear picture", "clear page", "clear drawing",
660660
// @TODO: erase?
@@ -788,7 +788,7 @@ const menus = {
788788
{
789789
emoji_icon: "⌚",
790790
label: localize("&History"),
791-
shortcut: "Ctrl+Shift+Y",
791+
...shortcut("Ctrl+Shift+Y"),
792792
speech_recognition: [
793793
// This is a duplicate menu item (for easy access), so it doesn't need speech recognition data here.
794794
],
@@ -798,7 +798,7 @@ const menus = {
798798
{
799799
emoji_icon: "🎞️",
800800
label: localize("&Render History As GIF"),
801-
shortcut: "Ctrl+Shift+G",
801+
...shortcut("Ctrl+Shift+G"),
802802
speech_recognition: [
803803
// @TODO: animated gif, blah
804804
"render history as gif", "render history as a gif", "render history animation", "make history animation", "make animation of history", "make animation of document history", "make animation from document history",
@@ -1392,3 +1392,115 @@ for (const [top_level_menu_key, menu] of Object.entries(menus)) {
13921392

13931393
export { menus };
13941394

1395+
/**
1396+
* Expands a shortcut label into an object with the label and a corresponding ARIA key shortcuts value.
1397+
* Could handle "CtrlOrCmd" like Electron does, here, or just treat "Ctrl" as control or command.
1398+
* Of course it would be more ergonomic if OS-GUI.js handled this sort of thing,
1399+
* and I have thought about rewriting the OS-GUI API to mimic Electron's.
1400+
* I also have some munging logic in electron-main.js related to this.
1401+
* @param {string} shortcutLabel
1402+
* @returns {{shortcutLabel?: string, ariaKeyShortcuts?: string}}
1403+
*/
1404+
function shortcut(shortcutLabel) {
1405+
if (!shortcutLabel) return {};
1406+
const ariaKeyShortcuts = shortcutLabel.replace(/Ctrl/g, "Control").replace(/\bDel\b/, "Delete");//.replace(/\bEsc\b/, "Escape").replace(/\bIns\b/, "Insert");
1407+
if (!validateAriaKeyshortcuts(ariaKeyShortcuts)) {
1408+
console.error(`Invalid ARIA key shortcuts: ${JSON.stringify(ariaKeyShortcuts)} (from shortcut label: ${JSON.stringify(shortcutLabel)}) (or validator is incomplete)`);
1409+
}
1410+
return {
1411+
shortcutLabel,
1412+
ariaKeyShortcuts,
1413+
};
1414+
}
1415+
1416+
/**
1417+
* Validates an aria-keyshortcuts value.
1418+
*
1419+
* AI-generated code (ChatGPT), prompted with the spec section: https://w3c.github.io/aria/#aria-keyshortcuts
1420+
*
1421+
* @param {string} value
1422+
* @returns {boolean} valid
1423+
*/
1424+
function validateAriaKeyshortcuts(value) {
1425+
// Define valid modifier and non-modifier keys based on UI Events KeyboardEvent key Values spec
1426+
const modifiers = ["Alt", "Control", "Shift", "Meta", "AltGraph"];
1427+
const nonModifiers = [
1428+
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
1429+
"N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
1430+
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0",
1431+
"Delete",
1432+
"Enter", "Tab", "ArrowRight", "ArrowLeft", "ArrowUp", "ArrowDown",
1433+
"PageUp", "PageDown", "End", "Home", "Escape", "Space", "Plus",
1434+
"Minus", "Comma", "Period", "Slash", "Backslash", "Quote", "Semicolon",
1435+
"BracketLeft", "BracketRight", "F1", "F2", "F3", "F4", "F5", "F6",
1436+
"F7", "F8", "F9", "F10", "F11", "F12"
1437+
// Add more non-modifier keys as needed
1438+
];
1439+
1440+
// Split the value into individual shortcuts
1441+
const shortcuts = value.split(" ");
1442+
1443+
// Function to validate a single shortcut
1444+
function validateShortcut(shortcut) {
1445+
const keys = shortcut.split("+");
1446+
1447+
if (keys.length === 0) {
1448+
return false;
1449+
}
1450+
1451+
let nonModifierFound = false;
1452+
1453+
// Check each key in the shortcut
1454+
for (let i = 0; i < keys.length; i++) {
1455+
const key = keys[i];
1456+
1457+
if (modifiers.includes(key)) {
1458+
if (nonModifierFound) {
1459+
// Modifier key found after a non-modifier key
1460+
return false;
1461+
}
1462+
} else if (nonModifiers.includes(key)) {
1463+
if (nonModifierFound) {
1464+
// Multiple non-modifier keys found
1465+
return false;
1466+
}
1467+
nonModifierFound = true;
1468+
} else {
1469+
// Invalid key
1470+
return false;
1471+
}
1472+
}
1473+
1474+
// Ensure at least one non-modifier key is present
1475+
return nonModifierFound;
1476+
}
1477+
1478+
// Validate all shortcuts
1479+
for (let i = 0; i < shortcuts.length; i++) {
1480+
if (!validateShortcut(shortcuts[i])) {
1481+
return false;
1482+
}
1483+
}
1484+
1485+
return true;
1486+
}
1487+
1488+
/** @type {[string, boolean][]} */
1489+
const ariaKeyShortcutsTestCases = [
1490+
["Control+A Shift+Alt+B", true],
1491+
["Control+Shift+1", true],
1492+
["Shift+Alt+T Control+5", true],
1493+
["T", true],
1494+
["ArrowLeft", true],
1495+
["Shift+T Alt+Control", false],
1496+
["T+Shift", false],
1497+
["Alt", false],
1498+
["IncredibleKey", false],
1499+
["Ctrl+Shift+A", false],
1500+
];
1501+
for (const [ariaKeyShortcuts, expectedValidity] of ariaKeyShortcutsTestCases) {
1502+
const returnedValidity = validateAriaKeyshortcuts(ariaKeyShortcuts);
1503+
if (returnedValidity !== expectedValidity) {
1504+
console.error(`validateAriaKeyshortcuts("${ariaKeyShortcuts}") returned ${returnedValidity} but expected ${expectedValidity}`);
1505+
}
1506+
}

0 commit comments

Comments
 (0)