((exports) => { const looksLikeChrome = !!(window.chrome && (window.chrome.loadTimes || window.chrome.csi)); // NOTE: Microsoft Edge includes window.chrome.app // (also this browser detection logic could likely use some more nuance) const menus = { [localize("&File")]: [ { item: localize("&New"), shortcut: window.is_electron_app ? "Ctrl+N" : "Ctrl+Alt+N", // Ctrl+N opens a new browser window speech_recognition: [ "new", "new file", "new document", "create new document", "create a new document", "start new document", "start a new document", ], action: () => { open_empty_window(); }, description: localize("Creates a new document."), }, { item: localize("&Open"), shortcut: "Ctrl+O", speech_recognition: [ "open", "open document", "open file", "open an image file", "open a document", "open a file", "load document", "load a document", "load an image file", "load an image", "show file picker", "show file chooser", "show file browser", "show finder", "browser for file", "browse for a file", "browse for an image", "browse for an image file", ], action: () => { open_in_new_window(); }, description: localize("Opens an existing document."), }, { item: localize("&Save"), shortcut: "Ctrl+S", speech_recognition: [ "save", "save document", "save file", "save image", "save picture", "save image file", // "save a document", "save a file", "save an image", "save an image file", // too "save as"-like "save the document", "save the file", "save the image", "save the image file", "download", "download document", "download file", "download image", "download picture", "download image file", "download the document", "download the file", "download the image", "download the image file", ], action: () => { file_save(); }, description: localize("Saves the active document."), }, { item: localize("Save &As"), // in mspaint, no shortcut is listed; it supports F12 (but in a browser that opens the dev tools) // it doesn't support Ctrl+Shift+S but that's a good & common modern shortcut shortcut: "Ctrl+Shift+S", speech_recognition: [ // this is ridiculous // this would be really simple in JSGF format "save as", "save as a new file", "save as a new picture", "save as a new image", "save a new file", "save new file", "save a new document", "save a new image file", "save a new image", "save a new picture", "save as a copy", "save a copy", "save as copy", "save under a new name", "save with a new name", "save document as a copy", "save document copy", "save document as copy", "save document under a new name", "save document with a new name", "save image as a copy", "save image copy", "save image as copy", "save image under a new name", "save image with a new name", "save file as a copy", "save file copy", "save file as copy", "save file under a new name", "save file with a new name", "save image file as a copy", "save image file copy", "save image file as copy", "save image file under a new name", "save image file with a new name", ], action: () => { file_save_as(); }, description: localize("Saves the active document with a new name."), }, MENU_DIVIDER, { item: localize("&Load From URL"), // shortcut: "", // no shortcut: Ctrl+L is taken, and you can paste a URL with Ctrl+V, so it's not really needed speech_recognition: [ "load from url", "load from a url", "load from address", "load from an address", "load from a web address", // this is ridiculous // this would be really simple in JSGF format "load an image from a URL", "load an image from an address", "load an image from a web address", "load image from a URL", "load image from an address", "load image from a web address", "load an image from URL", "load an image from address", "load an image from web address", "load image from URL", "load image from address", "load image from web address", "load an picture from a URL", "load an picture from an address", "load an picture from a web address", "load picture from a URL", "load picture from an address", "load picture from a web address", "load an picture from URL", "load an picture from address", "load an picture from web address", "load picture from URL", "load picture from address", "load picture from web address", ], action: () => { file_load_from_url(); }, description: localize("Opens an image from the web."), }, { item: localize("&Upload To Imgur"), speech_recognition: [ "upload to imgur", "upload image to imgur", "upload picture to imgur", ], action: () => { // include the selection in the saved image deselect(); main_canvas.toBlob((blob) => { sanity_check_blob(blob, () => { show_imgur_uploader(blob); }); }); }, description: localize("Uploads the active document to Imgur"), }, // MENU_DIVIDER, // { // item: localize("Manage Storage"), // speech_recognition: [ // "manage storage", "show storage", "open storage window", "manage sessions", "show sessions", "show local sessions", "local sessions", "storage manager", "show storage manager", "open storage manager", // "show autosaves", "show saves", "show saved documents", "show saved files", "show saved pictures", "show saved images", "show local storage", // "autosaves", "autosave", "saved documents", "saved files", "saved pictures", "saved images", "local storage", // ], // action: () => { manage_storage(); }, // description: localize("Manages storage of previously created or opened pictures."), // }, // MENU_DIVIDER, // { // item: localize("Print Pre&view"), // speech_recognition: [ // "preview print", "print preview", "show print preview", "show preview of print", // ], // action: () => { // print(); // }, // description: localize("Prints the active document and sets printing options."), // //description: localize("Displays full pages."), // }, // { // item: localize("Page Se&tup"), // speech_recognition: [ // "setup page for print", "setup page for printing", "set-up page for print", "set-up page for printing", "set up page for print", "set up page for printing", // "page setup", "printing setup", "page set-up", "printing set-up", "page set up", "printing set up", // ], // action: () => { // print(); // }, // description: localize("Prints the active document and sets printing options."), // //description: localize("Changes the page layout."), // }, // { // item: localize("&Print"), // shortcut: "Ctrl+P", // relies on browser's print shortcut being Ctrl+P // speech_recognition: [ // "print", "send to printer", "show print dialog", // "print page", "print image", "print picture", "print drawing", // "print out page", "print out image", "print out picture", "print out drawing", // "print out the page", "print out the image", "print out the picture", "print out the drawing", // "send page to printer", "send image to printer", "send picture to printer", "send drawing to printer", // "send page to the printer", "send image to the printer", "send picture to the printer", "send drawing to the printer", // "send the page to the printer", "send the image to the printer", "send the picture to the printer", "send the drawing to the printer", // "send the page to printer", "send the image to printer", "send the picture to printer", "send the drawing to printer", // ], // action: () => { // print(); // }, // description: localize("Prints the active document and sets printing options."), // }, // MENU_DIVIDER, // { // item: localize("Set As &Wallpaper (Tiled)"), // speech_recognition: [ // "set as wallpaper", // "set as wallpaper tiled", // "set image as wallpaper tiled", "set picture as wallpaper tiled", "set drawing as wallpaper tiled", // "use as wallpaper tiled", // "use image as wallpaper tiled", "use picture as wallpaper tiled", "use drawing as wallpaper tiled", // "tile image as wallpaper", "tile picture as wallpaper", "tile drawing as wallpaper", // ], // action: () => { systemHooks.setWallpaperTiled(main_canvas); }, // description: localize("Tiles this bitmap as the desktop background."), // }, // { // item: localize("Set As Wallpaper (&Centered)"), // in mspaint it's Wa&llpaper // speech_recognition: [ // "set as wallpaper centered", // "set image as wallpaper centered", "set picture as wallpaper centered", "set drawing as wallpaper centered", // "use as wallpaper centered", // "use image as wallpaper centered", "use picture as wallpaper centered", "use drawing as wallpaper centered", // "center image as wallpaper", "center picture as wallpaper", "center drawing as wallpaper", // ], // action: () => { systemHooks.setWallpaperCentered(main_canvas); }, // description: localize("Centers this bitmap as the desktop background."), // }, // MENU_DIVIDER, // { // item: localize("Recent File"), // enabled: false, // @TODO for desktop app // description: localize(""), // }, // MENU_DIVIDER, // { // item: localize("E&xit"), // shortcut: window.is_electron_app ? "Alt+F4" : "", // Alt+F4 closes the browser window (in most window managers) // speech_recognition: [ // "exit application", "exit paint", "close paint window", // ], // action: () => { // try { // // API contract is containing page can override window.close() // // Note that e.g. (()=>{}).bind().toString() gives "function () { [native code] }" // // so the window.close() must not use bind() (not that that's common practice anyway) // if (frameElement && window.close && !/\{\s*\[native code\]\s*\}/.test(window.close.toString())) { // window.close(); // return; // } // } catch (e) { // // In a cross-origin iframe, most likely // // @TODO: establish postMessage API // } // // In a cross-origin iframe, or same origin but without custom close(), or top level: // // Not all browsers support close() for closing a tab, // // so redirect instead. Exit to the official web desktop. // window.location = "https://98.js.org/"; // }, // description: localize("Quits Paint."), // } ], [localize("&Edit")]: [ { item: localize("&Undo"), shortcut: "Ctrl+Z", speech_recognition: [ "undo", "undo that", ], enabled: () => undos.length >= 1, action: () => { undo(); }, description: localize("Undoes the last action."), }, { item: localize("&Repeat"), shortcut: "F4", // also supported: Ctrl+Shift+Z, Ctrl+Y speech_recognition: [ "repeat", "redo", ], enabled: () => redos.length >= 1, action: () => { redo(); }, description: localize("Redoes the previously undone action."), }, { item: localize("&History"), shortcut: "Ctrl+Shift+Y", speech_recognition: [ "show history", "history", ], action: () => { show_document_history(); }, description: localize("Shows the document history and lets you navigate to states not accessible with Undo or Repeat."), }, MENU_DIVIDER, { item: localize("Cu&t"), shortcut: "Ctrl+X", speech_recognition: [ "cut", "cut selection", "cut selection to clipboard", "cut the selection", "cut the selection to clipboard", "cut the selection to the clipboard", ], enabled: () => // @TODO: support cutting text with this menu item as well (e.g. for the text tool) !!selection, action: () => { edit_cut(true); }, description: localize("Cuts the selection and puts it on the Clipboard."), }, { item: localize("&Copy"), shortcut: "Ctrl+C", speech_recognition: [ "copy", "copy selection", "copy selection to clipboard", "copy the selection", "copy the selection to clipboard", "copy the selection to the clipboard", ], enabled: () => // @TODO: support copying text with this menu item as well (e.g. for the text tool) !!selection, action: () => { edit_copy(true); }, description: localize("Copies the selection and puts it on the Clipboard."), }, { item: localize("&Paste"), shortcut: "Ctrl+V", speech_recognition: [ "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", ], enabled: () => // @TODO: disable if nothing in clipboard or wrong type (if we can access that) true, action: () => { edit_paste(true); }, description: localize("Inserts the contents of the Clipboard."), }, { item: localize("C&lear Selection"), shortcut: "Del", speech_recognition: [ "delete", "clear selection", "delete selection", "delete selected", "delete selected area", "clear selected area", "erase selected", "erase selected area", ], enabled: () => !!selection, action: () => { delete_selection(); }, description: localize("Deletes the selection."), }, { item: localize("Select &All"), shortcut: "Ctrl+A", speech_recognition: [ "select all", "select everything", "select the whole image", "select the whole picture", "select the whole drawing", "select the whole canvas", "select the whole document", "select the entire image", "select the entire picture", "select the entire drawing", "select the entire canvas", "select the entire document", ], action: () => { select_all(); }, description: localize("Selects everything."), }, MENU_DIVIDER, { item: `${localize("C&opy To")}...`, speech_recognition: [ "copy to file", "copy selection to file", "copy selection to a file", "save selection", "save selection as file", "save selection as image", "save selection as picture", "save selection as image file", "save selection as document", "save selection as a file", "save selection as a image", "save selection as a picture", "save selection as a image file", "save selection as a document", "save selection to file", "save selection to image", "save selection to picture", "save selection to image file", "save selection to document", "save selection to a file", "save selection to a image", "save selection to a picture", "save selection to a image file", "save selection to a document", ], enabled: () => !!selection, action: () => { save_selection_to_file(); }, description: localize("Copies the selection to a file."), }, { item: `${localize("Paste &From")}...`, speech_recognition: [ "paste a file", "paste from a file", "insert a file", "insert an image file", ], action: () => { choose_file_to_paste(); }, description: localize("Pastes a file into the selection."), } ], [localize("&View")]: [ { item: localize("&Tool Box"), 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. speech_recognition: [ "toggle tool box", "toggle tools box", "toggle toolbox", "toggle tool palette", "toggle tools palette", // @TODO: hide/show ], checkbox: { toggle: () => { $toolbox.toggle(); }, check: () => $toolbox.is(":visible"), }, description: localize("Shows or hides the tool box."), }, { item: localize("&Color Box"), shortcut: "Ctrl+L", // focuses browser address bar, but Firefox and Chrome both allow overriding the default behavior speech_recognition: [ "toggle color box", "toggle colors box", "toggle palette", "toggle color palette", "toggle colors palette", // @TODO: hide/show ], checkbox: { toggle: () => { $colorbox.toggle(); }, check: () => $colorbox.is(":visible"), }, description: localize("Shows or hides the color box."), }, { item: localize("&Status Bar"), speech_recognition: [ "toggle status bar", "toggle status text", "toggle status area", "toggle status indicator", // @TODO: hide/show ], checkbox: { toggle: () => { $status_area.toggle(); }, check: () => $status_area.is(":visible"), }, description: localize("Shows or hides the status bar."), }, { item: localize("T&ext Toolbar"), speech_recognition: [ "toggle text toolbar", "toggle font toolbar", "toggle text tool bar", "toggle font tool bar", "toggle font box", "toggle fonts box", "toggle text options box", "toggle text tool options box", "toggle font options box", "toggle font window", "toggle fonts window", "toggle text options window", "toggle text tool options window", "toggle font options window", // @TODO: hide/show ], enabled: false, // @TODO: toggle fonts box checkbox: {}, description: localize("Shows or hides the text toolbar."), }, MENU_DIVIDER, { item: localize("&Zoom"), submenu: [ { item: localize("&Normal Size"), shortcut: window.is_electron_app ? "Ctrl+PgUp" : "", // Ctrl+PageUp cycles thru browser tabs in Chrome & Firefox; can be overridden in Chrome in fullscreen only speech_recognition: [ "reset zoom", "zoom to normal size", "zoom to 100%", "set zoom to 100%", "set zoom 100%", "zoom to 1x", "set zoom to 1x", "set zoom 1x", "zoom level to 100%", "set zoom level to 100%", "set zoom level 100%", "zoom level to 1x", "set zoom level to 1x", "set zoom level 1x", ], description: localize("Zooms the picture to 100%."), enabled: () => magnification !== 1, action: () => { set_magnification(1); }, }, { item: localize("&Large Size"), shortcut: window.is_electron_app ? "Ctrl+PgDn" : "", // Ctrl+PageDown cycles thru browser tabs in Chrome & Firefox; can be overridden in Chrome in fullscreen only speech_recognition: [ "zoom to large size", "zoom to 400%", "set zoom to 400%", "set zoom 400%", "zoom to 4x", "set zoom to 4x", "set zoom 4x", "zoom level to 400%", "set zoom level to 400%", "set zoom level 400%", "zoom level to 4x", "set zoom level to 4x", "set zoom level 4x", ], description: localize("Zooms the picture to 400%."), enabled: () => magnification !== 4, action: () => { set_magnification(4); }, }, { item: localize("Zoom To &Window"), speech_recognition: [ "zoom to window", "zoom to view", "zoom to fit", "zoom to fit within window", "zoom to fit within view", "zoom to fit within the window", "zoom to fit within the view", "zoom to fit in window", "zoom to fit in view", "zoom to fit in the window", "zoom to fit in the view", "auto zoom", "fit zoom", "zoom to max", "zoom to maximum", "zoom to max size", "zoom to maximum size", "zoom so canvas fits", "zoom so picture fits", "zoom so image fits", "zoom so document fits", "zoom so whole canvas is visible", "zoom so whole picture is visible", "zoom so whole image is visible", "zoom so whole document is visible", "zoom so the whole canvas is visible", "zoom so the whole picture is visible", "zoom so the whole image is visible", "zoom so the whole document is visible", "fit to window", "fit to view", "fit in window", "fit in view", "fit within window", "fit within view", "fit picture to window", "fit picture to view", "fit picture in window", "fit picture in view", "fit picture within window", "fit picture within view", "fit image to window", "fit image to view", "fit image in window", "fit image in view", "fit image within window", "fit image within view", "fit canvas to window", "fit canvas to view", "fit canvas in window", "fit canvas in view", "fit canvas within window", "fit canvas within view", "fit document to window", "fit document to view", "fit document in window", "fit document in view", "fit document within window", "fit document within view", ], description: localize("Zooms the picture to fit within the view."), action: () => { const rect = $canvas_area[0].getBoundingClientRect(); const margin = 30; // leave a margin so scrollbars won't appear let mag = Math.min( (rect.width - margin) / main_canvas.width, (rect.height - margin) / main_canvas.height, ); // round to an integer percent for the View > Zoom > Custom... dialog, which shows non-integers as invalid mag = Math.floor(100 * mag) / 100; set_magnification(mag); }, }, { item: `${localize("C&ustom")}...`, description: localize("Zooms the picture."), speech_recognition: [ "zoom custom", "custom zoom", "set custom zoom", "set custom zoom level", "zoom to custom level", "zoom to custom", "zoom level", "set zoom level", ], action: () => { show_custom_zoom_window(); }, }, MENU_DIVIDER, { item: localize("Show &Grid"), shortcut: "Ctrl+G", speech_recognition: [ "toggle show grid", "toggle grid", "toggle gridlines", "toggle grid lines", "toggle grid cells", // @TODO: hide/show ], enabled: () => magnification >= 4, checkbox: { toggle: () => { toggle_grid(); }, check: () => show_grid, }, description: localize("Shows or hides the grid."), }, { item: localize("Show T&humbnail"), speech_recognition: [ "toggle show thumbnail", "toggle thumbnail", "toggle thumbnail view", "toggle thumbnail box", "toggle thumbnail window", "toggle preview", "toggle image preview", "toggle picture preview", "toggle picture in picture", "toggle picture in picture view", "toggle picture in picture box", "toggle picture in picture window", // @TODO: hide/show ], checkbox: { toggle: () => { toggle_thumbnail(); }, check: () => show_thumbnail, }, description: localize("Shows or hides the thumbnail view of the picture."), } ] }, { item: localize("&View Bitmap"), shortcut: "Ctrl+F", speech_recognition: [ "view bitmap", "show bitmap", "fullscreen", "full-screen", "full screen", "show picture fullscreen", "show picture full-screen", "show picture full screen", "show image fullscreen", "show image full-screen", "show image full screen", // @TODO: exit fullscreen ], action: () => { view_bitmap(); }, description: localize("Displays the entire picture."), }, MENU_DIVIDER, { item: localize("&Fullscreen"), shortcut: "F11", // relies on browser's shortcut speech_recognition: [ // won't work with speech recognition, needs a user gesture ], enabled: () => document.fullscreenEnabled || document.webkitFullscreenEnabled, checkbox: { check: () => document.fullscreenElement || document.webkitFullscreenElement, toggle: () => { if (document.fullscreenElement || document.webkitFullscreenElement) { if (document.exitFullscreen) { document.exitFullscreen(); } else if (document.webkitExitFullscreen) { document.webkitExitFullscreen(); } } else { if (document.documentElement.requestFullscreen) { document.documentElement.requestFullscreen(); } else if (document.documentElement.webkitRequestFullscreen) { document.documentElement.webkitRequestFullscreen(); } } // check() would need to be async or faked with a timeout, // if the menus stayed open. @TODO: make all checkboxes close menus menu_bar.closeMenus(); }, }, description: localize("Makes the application take up the entire screen."), }, ], [localize("&Image")]: [ // @TODO: speech recognition: terms that apply to selection { item: localize("&Flip/Rotate"), 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) speech_recognition: [ "flip", "rotate", "flip/rotate", "flip slash rotate", "flip and rotate", "flip or rotate", "flip rotate", // @TODO: parameters to command ], action: () => { image_flip_and_rotate(); }, description: localize("Flips or rotates the picture or a selection."), }, { item: localize("&Stretch/Skew"), shortcut: window.is_electron_app ? "Ctrl+W" : "Ctrl+Alt+W", // Ctrl+W closes the browser tab speech_recognition: [ "stretch", "scale", "resize image", "skew", "stretch/skew", "stretch slash skew", "stretch and skew", "stretch or skew", "stretch skew", // @TODO: parameters to command ], action: () => { image_stretch_and_skew(); }, description: localize("Stretches or skews the picture or a selection."), }, { item: localize("&Invert Colors"), shortcut: "Ctrl+I", speech_recognition: [ "invert", "invert colors", "invert image", "invert picture", "invert drawing", "invert image colors", "invert picture colors", "invert drawing colors", "invert colors of image", "invert colors of picture", "invert colors of drawing", ], action: () => { image_invert_colors(); }, description: localize("Inverts the colors of the picture or a selection."), }, { item: `${localize("&Attributes")}...`, shortcut: "Ctrl+E", speech_recognition: [ "attributes", "image attributes", "picture attributes", "image options", "picture options", "dimensions", "image dimensions", "picture dimensions", "resize canvas", "resize document", "resize page", // not resize image/picture because that implies scaling, handled by Stretch/Skew "set image size", "set picture size", "set canvas size", "set document size", "set page size", "image size", "picture size", "canvas size", "document size", "page size", "configure image size", "configure picture size", "configure canvas size", "configure document size", "configure page size", ], action: () => { image_attributes(); }, description: localize("Changes the attributes of the picture."), }, { item: localize("&Clear Image"), shortcut: (window.is_electron_app || !looksLikeChrome) ? "Ctrl+Shift+N" : "", // Ctrl+Shift+N opens incognito window in chrome speech_recognition: [ "clear image", "clear canvas", "clear picture", "clear page", "clear drawing", // @TODO: erase? ], // (mspaint says "Ctrl+Shft+N") action: () => { !selection && clear(); }, enabled: () => !selection, description: localize("Clears the picture."), // action: ()=> { // if (selection) { // delete_selection(); // } else { // clear(); // } // }, // mspaint says localize("Clears the picture or selection."), but grays out the option when there's a selection }, { item: localize("&Draw Opaque"), speech_recognition: [ "toggle draw opaque", "toggle transparent selection", "toggle transparent selections", "toggle transparent selection mode", "toggle transparent selections mode", "toggle opaque selection", "toggle opaque selections", "toggle opaque selection mode", "toggle opaque selections mode", // toggle opaque? toggle opacity? // @TODO: hide/show / "draw opaque" / "draw transparent"/translucent? ], checkbox: { toggle: () => { tool_transparent_mode = !tool_transparent_mode; $G.trigger("option-changed"); }, check: () => !tool_transparent_mode, }, description: localize("Makes the current selection either opaque or transparent."), } ], [localize("&Help")]: [ { item: localize("&Help Topics"), speech_recognition: [ "help topics", "help me", "show help", "help", "show help window", "show help topics", "open help", "help viewer", "show help viewer", "open help viewer", ], action: () => { show_help(); }, description: localize("Displays Help for the current task or command."), }, MENU_DIVIDER, { item: localize("&About Paint"), speech_recognition: [ "about paint", "about js paint", "about jspaint", "show about window", "open about window", "about window", "app info", "about the app", "app information", "information about the app", "application info", "about the application", "application information", "information about the application", "who made this", "who did this", "who did this xd", ], action: () => { show_about_paint(); }, description: localize("Displays information about this application."), //description: localize("Displays program information, version number, and copyright."), } ], }; for (const [top_level_menu_key, menu] of Object.entries(menus)) { const top_level_menu_name = top_level_menu_key.replace(/&/, ""); const add_literal_navigation_speech_recognition = (menu, ancestor_names) => { for (const menu_item of menu) { if (menu_item !== MENU_DIVIDER) { const menu_item_name = menu_item.item.replace(/&|\.\.\.|\(|\)/g, ""); // console.log(menu_item_name); let menu_item_matchers = [menu_item_name]; if (menu_item_name.match(/\//)) { menu_item_matchers = [ menu_item_name, menu_item_name.replace(/\//, " "), menu_item_name.replace(/\//, " and "), menu_item_name.replace(/\//, " or "), menu_item_name.replace(/\//, " slash "), ]; } menu_item_matchers = menu_item_matchers.map((menu_item_matcher) => { return `${ancestor_names} ${menu_item_matcher}`; }); menu_item.speech_recognition = (menu_item.speech_recognition || []).concat(menu_item_matchers); // console.log(menu_item_matchers, menu_item.speech_recognition); if (menu_item.submenu) { add_literal_navigation_speech_recognition(menu_item.submenu, `${ancestor_names} ${menu_item_name}`); } } } }; add_literal_navigation_speech_recognition(menu, top_level_menu_name); } exports.menus = menus; })(window);