From cb57af4b2a06310fd1fb9682c16898de1510130c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20H=C3=A4usler?= Date: Sun, 27 Mar 2022 04:54:02 +0200 Subject: [PATCH] Refactor menu actions and shortcut handling (#3032) * Refactor menu actions and shortcut handling * Remove duplicated command id * Update docs --- docs/KEYBINDINGS_LINUX.md | 2 +- docs/KEYBINDINGS_OSX.md | 2 +- docs/KEYBINDINGS_WINDOWS.md | 2 +- src/common/commands/constants.js | 110 ++++++++++++++++ src/main/app/accessor.js | 19 ++- src/main/app/env.js | 10 ++ src/main/commands/file.js | 11 ++ src/main/commands/index.js | 53 ++++++++ src/main/commands/tab.js | 36 ++++++ src/main/keyboard/keybindingsDarwin.js | 2 +- src/main/keyboard/keybindingsLinux.js | 2 +- src/main/keyboard/keybindingsWindows.js | 2 +- src/main/keyboard/shortcutHandler.js | 95 ++++++-------- src/main/menu/actions/edit.js | 83 ++++++++++++ src/main/menu/actions/file.js | 29 ++++- src/main/menu/actions/format.js | 73 ++++++++++- src/main/menu/actions/index.js | 17 +++ src/main/menu/actions/marktext.js | 55 ++++++-- src/main/menu/actions/paragraph.js | 113 +++++++++++++++- src/main/menu/actions/view.js | 111 ++++++++++++---- src/main/menu/actions/window.js | 18 +++ src/main/menu/index.js | 163 +++--------------------- src/main/menu/templates/dock.js | 6 +- src/main/menu/templates/edit.js | 65 +++++----- src/main/menu/templates/file.js | 8 +- src/main/menu/templates/format.js | 48 +++---- src/main/menu/templates/marktext.js | 16 +-- src/main/menu/templates/paragraph.js | 80 ++++++------ src/main/menu/templates/view.js | 70 +++------- src/main/menu/templates/window.js | 8 +- src/main/preferences/schema.json | 1 + src/renderer/commands/descriptions.js | 4 +- src/renderer/commands/index.js | 6 +- src/renderer/pages/app.vue | 1 - src/renderer/store/editor.js | 50 ++------ src/renderer/store/layout.js | 23 ++-- src/renderer/store/listenForMain.js | 10 -- src/renderer/store/preferences.js | 24 +++- src/renderer/store/project.js | 2 +- 39 files changed, 940 insertions(+), 490 deletions(-) create mode 100644 src/common/commands/constants.js create mode 100644 src/main/commands/file.js create mode 100644 src/main/commands/index.js create mode 100644 src/main/commands/tab.js create mode 100644 src/main/menu/actions/index.js diff --git a/docs/KEYBINDINGS_LINUX.md b/docs/KEYBINDINGS_LINUX.md index 50b44148..b9341958 100644 --- a/docs/KEYBINDINGS_LINUX.md +++ b/docs/KEYBINDINGS_LINUX.md @@ -8,7 +8,7 @@ MarkText key bindings for Linux. Please see [general key bindings](KEYBINDINGS.m | Id | Default | Description | |:------------------- | --------------------------------------------- | ------------------------------------- | -| `file.new-file` | Ctrl+N | New file | +| `file.new-window` | Ctrl+N | New window | | `file.new-tab` | Ctrl+T | New tab | | `file.open-file` | Ctrl+O | Open markdown file | | `file.open-folder` | Ctrl+Shift+O | Open folder | diff --git a/docs/KEYBINDINGS_OSX.md b/docs/KEYBINDINGS_OSX.md index dd34fd9c..b26c4e5d 100644 --- a/docs/KEYBINDINGS_OSX.md +++ b/docs/KEYBINDINGS_OSX.md @@ -17,7 +17,7 @@ MarkText key bindings for macOS. Please see [general key bindings](KEYBINDINGS.m | Id | Default | Description | |:------------------- | ------------------------------------------------ | ------------------------------------- | -| `file.new-file` | Command+N | New file | +| `file.new-window` | Command+N | New window | | `file.new-tab` | Command+T | New tab | | `file.open-file` | Command+O | Open markdown file | | `file.open-folder` | Command+Shift+O | Open folder | diff --git a/docs/KEYBINDINGS_WINDOWS.md b/docs/KEYBINDINGS_WINDOWS.md index efc87c6b..c639686c 100644 --- a/docs/KEYBINDINGS_WINDOWS.md +++ b/docs/KEYBINDINGS_WINDOWS.md @@ -8,7 +8,7 @@ MarkText key bindings for Windows. Please see [general key bindings](KEYBINDINGS | Id | Default | Description | |:------------------- | --------------------------------------------- | ------------------------------------- | -| `file.new-file` | Ctrl+N | New file | +| `file.new-window` | Ctrl+N | New window | | `file.new-tab` | Ctrl+T | New tab | | `file.open-file` | Ctrl+O | Open markdown file | | `file.open-folder` | Ctrl+Shift+O | Open folder | diff --git a/src/common/commands/constants.js b/src/common/commands/constants.js new file mode 100644 index 00000000..aa869497 --- /dev/null +++ b/src/common/commands/constants.js @@ -0,0 +1,110 @@ +const COMMANDS = Object.freeze({ + EDIT_COPY: 'edit.copy', + EDIT_COPY_AS_HTML: 'edit.copy-as-html', + EDIT_COPY_AS_MARKDOWN: 'edit.copy-as-markdown', + EDIT_CREATE_PARAGRAPH: 'edit.create-paragraph', + EDIT_CUT: 'edit.cut', + EDIT_DELETE_PARAGRAPH: 'edit.delete-paragraph', + EDIT_DUPLICATE: 'edit.duplicate', + EDIT_FIND: 'edit.find', + EDIT_FIND_IN_FOLDER: 'edit.find-in-folder', + EDIT_FIND_NEXT: 'edit.find-next', + EDIT_FIND_PREVIOUS: 'edit.find-previous', + EDIT_PASTE: 'edit.paste', + EDIT_PASTE_AS_PLAINTEXT: 'edit.paste-as-plaintext', + EDIT_REDO: 'edit.redo', + EDIT_REPLACE: 'edit.replace', + EDIT_SCREENSHOT: 'edit.screenshot', + EDIT_SELECT_ALL: 'edit.select-all', + EDIT_UNDO: 'edit.undo', + + FILE_CHECK_UPDATE: 'file.check-update', + FILE_CLOSE_TAB: 'file.close-tab', + FILE_CLOSE_WINDOW: 'file.close-window', + FILE_EXPORT_FILE: 'file.export-file', + FILE_IMPORT_FILE: 'file.import-file', + FILE_MOVE_FILE: 'file.move-file', + FILE_NEW_FILE: 'file.new-window', + FILE_NEW_TAB: 'file.new-tab', + FILE_OPEN_FILE: 'file.open-file', + FILE_OPEN_FOLDER: 'file.open-folder', + FILE_PREFERENCES: 'file.preferences', + FILE_PRINT: 'file.print', + FILE_QUICK_OPEN: 'file.quick-open', + FILE_QUIT: 'file.quit', + FILE_RENAME_FILE: 'file.rename-file', + FILE_SAVE: 'file.save', + FILE_SAVE_AS: 'file.save-as', + // FILE_TOGGLE_AUTO_SAVE: 'file.toggle-auto-save', + + FORMAT_CLEAR_FORMAT: 'format.clear-format', + FORMAT_EMPHASIS: 'format.emphasis', + FORMAT_HIGHLIGHT: 'format.highlight', + FORMAT_HYPERLINK: 'format.hyperlink', + FORMAT_IMAGE: 'format.image', + FORMAT_INLINE_CODE: 'format.inline-code', + FORMAT_INLINE_MATH: 'format.inline-math', + FORMAT_STRIKE: 'format.strike', + FORMAT_STRONG: 'format.strong', + FORMAT_SUBSCRIPT: 'format.subscript', + FORMAT_SUPERSCRIPT: 'format.superscript', + FORMAT_UNDERLINE: 'format.underline', + + MT_HIDE: 'mt.hide', + MT_HIDE_OTHERS: 'mt.hide-others', + + PARAGRAPH_BULLET_LIST: 'paragraph.bullet-list', + PARAGRAPH_CODE_FENCE: 'paragraph.code-fence', + PARAGRAPH_DEGRADE_HEADING: 'paragraph.degrade-heading', + PARAGRAPH_FRONT_MATTER: 'paragraph.front-matter', + PARAGRAPH_HEADING_1: 'paragraph.heading-1', + PARAGRAPH_HEADING_2: 'paragraph.heading-2', + PARAGRAPH_HEADING_3: 'paragraph.heading-3', + PARAGRAPH_HEADING_4: 'paragraph.heading-4', + PARAGRAPH_HEADING_5: 'paragraph.heading-5', + PARAGRAPH_HEADING_6: 'paragraph.heading-6', + PARAGRAPH_HORIZONTAL_LINE: 'paragraph.horizontal-line', + PARAGRAPH_HTML_BLOCK: 'paragraph.html-block', + PARAGRAPH_LOOSE_LIST_ITEM: 'paragraph.loose-list-item', + PARAGRAPH_MATH_FORMULA: 'paragraph.math-formula', + PARAGRAPH_ORDERED_LIST: 'paragraph.order-list', + PARAGRAPH_PARAGRAPH: 'paragraph.paragraph', + PARAGRAPH_QUOTE_BLOCK: 'paragraph.quote-block', + PARAGRAPH_TABLE: 'paragraph.table', + PARAGRAPH_TASK_LIST: 'paragraph.task-list', + PARAGRAPH_INCREASE_HEADING: 'paragraph.upgrade-heading', + + TABS_CYCLE_BACKWARD: 'tabs.cycle-backward', + TABS_CYCLE_FORWARD: 'tabs.cycle-forward', + TABS_SWITCH_TO_EIGHTH: 'tabs.switch-to-eighth', + TABS_SWITCH_TO_FIFTH: 'tabs.switch-to-fifth', + TABS_SWITCH_TO_FIRST: 'tabs.switch-to-first', + TABS_SWITCH_TO_FOURTH: 'tabs.switch-to-fourth', + TABS_SWITCH_TO_LEFT: 'tabs.switch-to-left', + TABS_SWITCH_TO_NINTH: 'tabs.switch-to-ninth', + TABS_SWITCH_TO_RIGHT: 'tabs.switch-to-right', + TABS_SWITCH_TO_SECOND: 'tabs.switch-to-second', + TABS_SWITCH_TO_SEVENTH: 'tabs.switch-to-seventh', + TABS_SWITCH_TO_SIXTH: 'tabs.switch-to-sixth', + TABS_SWITCH_TO_TENTH: 'tabs.switch-to-tenth', + TABS_SWITCH_TO_THIRD: 'tabs.switch-to-third', + + VIEW_COMMAND_PALETTE: 'view.command-palette', + VIEW_DEV_RELOAD: 'view.dev-reload', + VIEW_FOCUS_MODE: 'view.focus-mode', + VIEW_FORCE_RELOAD_IMAGES: 'view.reload-images', + VIEW_SOURCE_CODE_MODE: 'view.source-code-mode', + VIEW_TOGGLE_DEV_TOOLS: 'view.toggle-dev-tools', + VIEW_TOGGLE_SIDEBAR: 'view.toggle-sidebar', + VIEW_TOGGLE_TABBAR: 'view.toggle-tabbar', + VIEW_TOGGLE_TOC: 'view.toggle-toc', + VIEW_TYPEWRITER_MODE: 'view.typewriter-mode', + + WINDOW_MINIMIZE: 'window.minimize', + WINDOW_TOGGLE_ALWAYS_ON_TOP: 'window.toggle-always-on-top', + WINDOW_TOGGLE_FULL_SCREEN: 'window.toggle-full-screen', + WINDOW_ZOOM_IN: 'window.zoom-in', + WINDOW_ZOOM_OUT: 'window.zoom-out' +}) + +export default COMMANDS diff --git a/src/main/app/accessor.js b/src/main/app/accessor.js index 6a701735..f071a322 100644 --- a/src/main/app/accessor.js +++ b/src/main/app/accessor.js @@ -3,6 +3,8 @@ import Preference from '../preferences' import DataCenter from '../dataCenter' import Keybindings from '../keyboard/shortcutHandler' import AppMenu from '../menu' +import { loadMenuCommands } from '../menu/actions' +import { CommandManager, loadDefaultCommands } from '../commands' class Accessor { /** @@ -13,12 +15,27 @@ class Accessor { this.env = appEnvironment this.paths = appEnvironment.paths // export paths to make it better accessible + this.preferences = new Preference(this.paths) this.dataCenter = new DataCenter(this.paths) - this.keybindings = new Keybindings(userDataPath) + + this.commandManager = CommandManager + this._loadCommands() + + this.keybindings = new Keybindings(this.commandManager, appEnvironment) this.menu = new AppMenu(this.preferences, this.keybindings, userDataPath) this.windowManager = new WindowManager(this.menu, this.preferences) } + + _loadCommands () { + const { commandManager } = this + loadDefaultCommands(commandManager) + loadMenuCommands(commandManager) + + if (this.env.isDevMode) { + commandManager.__verifyDefaultCommands() + } + } } export default Accessor diff --git a/src/main/app/env.js b/src/main/app/env.js index e47e7b2a..50c77058 100644 --- a/src/main/app/env.js +++ b/src/main/app/env.js @@ -14,6 +14,7 @@ export class AppEnvironment { this._id = envId++ this._appPaths = new AppPaths(options.userDataPath) this._debug = !!options.debug + this._isDevMode = !!options.isDevMode this._verbose = !!options.verbose this._safeMode = !!options.safeMode } @@ -41,6 +42,13 @@ export class AppEnvironment { return this._debug } + /** + * @returns {boolean} + */ + get isDevMode () { + return this._isDevMode + } + /** * @returns {boolean} */ @@ -65,6 +73,7 @@ export class AppEnvironment { const setupEnvironment = args => { patchEnvPath() + const isDevMode = process.env.NODE_ENV !== 'production' const debug = args['--debug'] || !!process.env.MARKTEXT_DEBUG || process.env.NODE_ENV !== 'production' const verbose = args['--verbose'] || 0 const safeMode = args['--safe'] @@ -72,6 +81,7 @@ const setupEnvironment = args => { const appEnvironment = new AppEnvironment({ debug, + isDevMode, verbose, safeMode, userDataPath diff --git a/src/main/commands/file.js b/src/main/commands/file.js new file mode 100644 index 00000000..ffe81087 --- /dev/null +++ b/src/main/commands/file.js @@ -0,0 +1,11 @@ +import { COMMANDS } from './index' + +const openQuickOpenDialog = win => { + if (win && win.webContents) { + win.webContents.send('mt::execute-command-by-id', 'file.quick-open') + } +} + +export const loadFileCommands = commandManager => { + commandManager.add(COMMANDS.FILE_QUICK_OPEN, openQuickOpenDialog) +} diff --git a/src/main/commands/index.js b/src/main/commands/index.js new file mode 100644 index 00000000..3ae07978 --- /dev/null +++ b/src/main/commands/index.js @@ -0,0 +1,53 @@ +import COMMAND_CONSTANTS from 'common/commands/constants' +import { loadFileCommands } from './file' +import { loadTabCommands } from './tab' + +export const COMMANDS = COMMAND_CONSTANTS + +export const loadDefaultCommands = commandManager => { + loadFileCommands(commandManager) + loadTabCommands(commandManager) +} + +class CommandManager { + constructor () { + this._commands = new Map() + } + + add (id, callback) { + const { _commands } = this + if (_commands.has(id)) { + throw new Error(`Command with id="${id}" already exists.`) + } + _commands.set(id, callback) + } + + remove (id) { + return this._commands.delete(id) + } + + has (id) { + return this._commands.has(id) + } + + execute (id, ...args) { + const command = this._commands.get(id) + if (!command) { + throw new Error(`No command found with id="${id}".`) + } + return command(...args) + } + + __verifyDefaultCommands () { + const { _commands } = this + Object.keys(COMMANDS).forEach(propertyName => { + const id = COMMANDS[propertyName] + if (!_commands.has(id)) { + console.error(`[DEBUG] Default command with id="${id}" isn't available!`) + } + }) + } +} + +const commandManagerInstance = new CommandManager() +export { commandManagerInstance as CommandManager } diff --git a/src/main/commands/tab.js b/src/main/commands/tab.js new file mode 100644 index 00000000..28b3ea59 --- /dev/null +++ b/src/main/commands/tab.js @@ -0,0 +1,36 @@ +import { COMMANDS } from './index' + +const switchToLeftTab = win => { + if (win && win.webContents) { + win.webContents.send('mt::tabs-cycle-left') + } +} + +const switchToRightTab = win => { + if (win && win.webContents) { + win.webContents.send('mt::tabs-cycle-right') + } +} + +const switchTabByIndex = (win, index) => { + if (win && win.webContents) { + win.webContents.send('mt::switch-tab-by-index', index) + } +} + +export const loadTabCommands = commandManager => { + commandManager.add(COMMANDS.TABS_CYCLE_BACKWARD, switchToLeftTab) + commandManager.add(COMMANDS.TABS_CYCLE_FORWARD, switchToRightTab) + commandManager.add(COMMANDS.TABS_SWITCH_TO_LEFT, switchToLeftTab) + commandManager.add(COMMANDS.TABS_SWITCH_TO_RIGHT, switchToRightTab) + commandManager.add(COMMANDS.TABS_SWITCH_TO_FIRST, win => switchTabByIndex(win, 0)) + commandManager.add(COMMANDS.TABS_SWITCH_TO_SECOND, win => switchTabByIndex(win, 1)) + commandManager.add(COMMANDS.TABS_SWITCH_TO_THIRD, win => switchTabByIndex(win, 2)) + commandManager.add(COMMANDS.TABS_SWITCH_TO_FOURTH, win => switchTabByIndex(win, 3)) + commandManager.add(COMMANDS.TABS_SWITCH_TO_FIFTH, win => switchTabByIndex(win, 4)) + commandManager.add(COMMANDS.TABS_SWITCH_TO_SIXTH, win => switchTabByIndex(win, 5)) + commandManager.add(COMMANDS.TABS_SWITCH_TO_SEVENTH, win => switchTabByIndex(win, 6)) + commandManager.add(COMMANDS.TABS_SWITCH_TO_EIGHTH, win => switchTabByIndex(win, 7)) + commandManager.add(COMMANDS.TABS_SWITCH_TO_NINTH, win => switchTabByIndex(win, 8)) + commandManager.add(COMMANDS.TABS_SWITCH_TO_TENTH, win => switchTabByIndex(win, 9)) +} diff --git a/src/main/keyboard/keybindingsDarwin.js b/src/main/keyboard/keybindingsDarwin.js index 335ac85b..03963aa8 100644 --- a/src/main/keyboard/keybindingsDarwin.js +++ b/src/main/keyboard/keybindingsDarwin.js @@ -10,7 +10,7 @@ export default new Map([ ['file.preferences', 'Command+,'], // located under MarkText menu in macOS only // File menu - ['file.new-file', 'Command+N'], + ['file.new-window', 'Command+N'], ['file.new-tab', 'Command+T'], ['file.open-file', 'Command+O'], ['file.open-folder', 'Command+Shift+O'], diff --git a/src/main/keyboard/keybindingsLinux.js b/src/main/keyboard/keybindingsLinux.js index 8e4303ae..6329b2f8 100644 --- a/src/main/keyboard/keybindingsLinux.js +++ b/src/main/keyboard/keybindingsLinux.js @@ -13,7 +13,7 @@ export default new Map([ ['mt.hide-others', ''], // File menu - ['file.new-file', 'Ctrl+N'], + ['file.new-window', 'Ctrl+N'], ['file.new-tab', 'Ctrl+T'], ['file.open-file', 'Ctrl+O'], ['file.open-folder', 'Ctrl+Shift+O'], diff --git a/src/main/keyboard/keybindingsWindows.js b/src/main/keyboard/keybindingsWindows.js index c4c6200c..eaada384 100644 --- a/src/main/keyboard/keybindingsWindows.js +++ b/src/main/keyboard/keybindingsWindows.js @@ -10,7 +10,7 @@ export default new Map([ ['mt.hide-others', ''], // File menu - ['file.new-file', 'Ctrl+N'], + ['file.new-window', 'Ctrl+N'], ['file.new-tab', 'Ctrl+T'], ['file.open-file', 'Ctrl+O'], ['file.open-folder', 'Ctrl+Shift+O'], diff --git a/src/main/keyboard/shortcutHandler.js b/src/main/keyboard/shortcutHandler.js index c76d59f5..fbd4dd41 100644 --- a/src/main/keyboard/shortcutHandler.js +++ b/src/main/keyboard/shortcutHandler.js @@ -1,4 +1,4 @@ -import { shell, Menu } from 'electron' +import { shell } from 'electron' import fs from 'fs' import fsPromises from 'fs/promises' import path from 'path' @@ -14,15 +14,26 @@ import keybindingsWindows from './keybindingsWindows' class Keybindings { /** - * @param {string} userDataPath The user data path. + * @param {CommandManager} commandManager The command manager instance. + * @param {AppEnvironment} appEnvironment The application environment instance. */ - constructor (userDataPath) { + constructor (commandManager, appEnvironment) { + const { userDataPath } = appEnvironment.paths this.configPath = path.join(userDataPath, 'keybindings.json') + this.commandManager = commandManager this.userKeybindings = new Map() this.keys = this.getDefaultKeybindings() this._prepareKeyMapper() + if (appEnvironment.isDevMode) { + for (const [id, accelerator] of this.keys) { + if (!commandManager.has(id)) { + console.error(`[DEBUG] Command with id="${id}" isn't available for accelerator="${accelerator}".`) + } + } + } + // Load user-defined keybindings this._loadLocalKeybindings() } @@ -35,21 +46,32 @@ class Keybindings { return name } - registerKeyHandlers (win, acceleratorMap) { - for (const item of acceleratorMap) { - let { accelerator } = item - if (accelerator == null || accelerator === '') { - continue - } + registerAccelerator (win, accelerator, callback) { + if (!win || !accelerator || !callback) { + throw new Error(`addKeyHandler: invalid arguments (accelerator="${accelerator}").`) + } - // Regisiter shortcuts on the BrowserWindow instead of using Chromium's native menu. - // This makes it possible to receive key down events before Chromium/Electron and we - // can handle reserved Chromium shortcuts. Afterwards prevent the default action of - // the event so the native menu is not triggered. - electronLocalshortcut.register(win, accelerator, () => { - callMenuCallback(item, win) - return true // prevent default action - }) + // Register shortcuts on the BrowserWindow instead of using Chromium's native menu. + // This makes it possible to receive key down events before Chromium/Electron and we + // can handle reserved Chromium shortcuts. Afterwards prevent the default action of + // the event so the native menu is not triggered. + electronLocalshortcut.register(win, accelerator, () => { + callback(win) + return true // prevent default action + }) + } + + unregisterAccelerator (win, accelerator) { + electronLocalshortcut.unregister(win, accelerator) + } + + registerEditorKeyHandlers (win) { + for (const [id, accelerator] of this.keys) { + if (accelerator && accelerator.length > 1) { + this.registerAccelerator(win, accelerator, () => { + this.commandManager.execute(id, win) + }) + } } } @@ -213,43 +235,4 @@ class Keybindings { } } -export const parseMenu = menuTemplate => { - const { submenu, accelerator, click, id, visible } = menuTemplate - const items = [] - if (Array.isArray(menuTemplate)) { - for (const item of menuTemplate) { - const subitems = parseMenu(item) - if (subitems) items.push(...subitems) - } - } else if (submenu) { - const subitems = parseMenu(submenu) - if (subitems) items.push(...subitems) - } else if ((visible === undefined || visible) && accelerator && click) { - items.push({ - accelerator, - click, - id // may be null - }) - } - return items.length === 0 ? null : items -} - -const callMenuCallback = (menuInfo, win) => { - const { click, id } = menuInfo - if (click) { - let menuItem = null - if (id) { - const menus = Menu.getApplicationMenu() - menuItem = menus.getMenuItemById(id) - } - - // Allow all shortcuts/menus without id and only enabled menus with id (GH#980). - if (!menuItem || menuItem.enabled !== false) { - click(menuItem, win) - } - } else { - console.error('ERROR: callback function is not defined.') - } -} - export default Keybindings diff --git a/src/main/menu/actions/edit.js b/src/main/menu/actions/edit.js index 19ecde9e..37460457 100644 --- a/src/main/menu/actions/edit.js +++ b/src/main/menu/actions/edit.js @@ -1,8 +1,10 @@ import path from 'path' import { ipcMain, BrowserWindow } from 'electron' import log from 'electron-log' +import { COMMANDS } from '../../commands' import { searchFilesAndDir } from '../../utils/imagePathAutoComplement' +// TODO(Refactor): Move to filesystem and provide generic API to search files in directories. ipcMain.on('mt::ask-for-image-auto-path', (e, { pathname, src, id }) => { const win = BrowserWindow.fromWebContents(e.sender) if (!src || typeof src !== 'string') { @@ -26,6 +28,64 @@ ipcMain.on('mt::ask-for-image-auto-path', (e, { pathname, src, id }) => { }) }) +// --- Menu actions ------------------------------------------------------------- + +export const editorUndo = win => { + edit(win, 'undo') +} + +export const editorRedo = win => { + edit(win, 'redo') +} + +export const editorCopyAsMarkdown = win => { + edit(win, 'copyAsMarkdown') +} + +export const editorCopyAsHtml = win => { + edit(win, 'copyAsHtml') +} + +export const editorPasteAsPlainText = win => { + edit(win, 'pasteAsPlainText') +} + +export const editorSelectAll = win => { + edit(win, 'selectAll') +} + +export const editorDuplicate = win => { + edit(win, 'duplicate') +} + +export const editorCreateParagraph = win => { + edit(win, 'createParagraph') +} + +export const editorDeleteParagraph = win => { + edit(win, 'deleteParagraph') +} + +export const editorFind = win => { + edit(win, 'find') +} + +export const editorFindNext = win => { + edit(win, 'findNext') +} + +export const editorFindPrevious = win => { + edit(win, 'findPrev') +} + +export const editorReplace = win => { + edit(win, 'undo') +} + +export const findInFolder = win => { + edit(win, 'findInFolder') +} + export const edit = (win, type) => { if (win && win.webContents) { win.webContents.send('mt::editor-edit-action', type) @@ -60,6 +120,29 @@ export const lineEnding = (win, lineEnding) => { } } +// --- Commands ------------------------------------------------------------- + +export const loadEditCommands = commandManager => { + commandManager.add(COMMANDS.EDIT_COPY, nativeCopy) + commandManager.add(COMMANDS.EDIT_COPY_AS_HTML, editorCopyAsHtml) + commandManager.add(COMMANDS.EDIT_COPY_AS_MARKDOWN, editorCopyAsMarkdown) + commandManager.add(COMMANDS.EDIT_CREATE_PARAGRAPH, editorCreateParagraph) + commandManager.add(COMMANDS.EDIT_CUT, nativeCut) + commandManager.add(COMMANDS.EDIT_DELETE_PARAGRAPH, editorDeleteParagraph) + commandManager.add(COMMANDS.EDIT_DUPLICATE, editorDuplicate) + commandManager.add(COMMANDS.EDIT_FIND, editorFind) + commandManager.add(COMMANDS.EDIT_FIND_IN_FOLDER, findInFolder) + commandManager.add(COMMANDS.EDIT_FIND_NEXT, editorFindNext) + commandManager.add(COMMANDS.EDIT_FIND_PREVIOUS, editorFindPrevious) + commandManager.add(COMMANDS.EDIT_PASTE, nativePaste) + commandManager.add(COMMANDS.EDIT_PASTE_AS_PLAINTEXT, editorPasteAsPlainText) + commandManager.add(COMMANDS.EDIT_REDO, editorRedo) + commandManager.add(COMMANDS.EDIT_REPLACE, editorReplace) + commandManager.add(COMMANDS.EDIT_SCREENSHOT, screenshot) + commandManager.add(COMMANDS.EDIT_SELECT_ALL, editorSelectAll) + commandManager.add(COMMANDS.EDIT_UNDO, editorUndo) +} + // --- IPC events ------------------------------------------------------------- // NOTE: Don't use static `getMenuItemById` here, instead request the menu by diff --git a/src/main/menu/actions/file.js b/src/main/menu/actions/file.js index 4197d811..b743735a 100644 --- a/src/main/menu/actions/file.js +++ b/src/main/menu/actions/file.js @@ -1,9 +1,12 @@ import fs from 'fs-extra' import path from 'path' -import { BrowserWindow, dialog, ipcMain, shell } from 'electron' +import { BrowserWindow, app, dialog, ipcMain, shell } from 'electron' import log from 'electron-log' import { isDirectory, isFile, exists } from 'common/filesystem' import { MARKDOWN_EXTENSIONS, isMarkdownFile } from 'common/filesystem/paths' +import { checkUpdates, userSetting } from './marktext' +import { showTabBar } from './view' +import { COMMANDS } from '../../commands' import { EXTENSION_HASN, PANDOC_EXTENSIONS, URL_REG } from '../../config' import { normalizeAndResolvePath, writeFile } from '../../filesystem' import { writeMarkdownFile } from '../../filesystem/markdown' @@ -501,7 +504,7 @@ export const importFile = async win => { } } -export const print = win => { +export const printDocument = win => { if (win) { win.webContents.send('mt::show-export-dialog', 'print') } @@ -545,6 +548,7 @@ export const openFileOrFolder = (win, pathname) => { export const newBlankTab = win => { if (win && win.webContents) { win.webContents.send('mt::new-untitled-tab') + showTabBar(win) } } @@ -596,3 +600,24 @@ export const rename = win => { export const clearRecentlyUsed = () => { ipcMain.emit('menu-clear-recently-used') } + +// --- Commands ------------------------------------------------------------- + +export const loadFileCommands = commandManager => { + commandManager.add(COMMANDS.FILE_CHECK_UPDATE, checkUpdates) + commandManager.add(COMMANDS.FILE_CLOSE_TAB, closeTab) + commandManager.add(COMMANDS.FILE_CLOSE_WINDOW, closeWindow) + commandManager.add(COMMANDS.FILE_EXPORT_FILE, exportFile) + commandManager.add(COMMANDS.FILE_IMPORT_FILE, importFile) + commandManager.add(COMMANDS.FILE_MOVE_FILE, moveTo) + commandManager.add(COMMANDS.FILE_NEW_FILE, newEditorWindow) + commandManager.add(COMMANDS.FILE_NEW_TAB, newBlankTab) + commandManager.add(COMMANDS.FILE_OPEN_FILE, openFile) + commandManager.add(COMMANDS.FILE_OPEN_FOLDER, openFolder) + commandManager.add(COMMANDS.FILE_PREFERENCES, userSetting) + commandManager.add(COMMANDS.FILE_PRINT, printDocument) + commandManager.add(COMMANDS.FILE_QUIT, app.quit) + commandManager.add(COMMANDS.FILE_RENAME_FILE, rename) + commandManager.add(COMMANDS.FILE_SAVE, save) + commandManager.add(COMMANDS.FILE_SAVE_AS, saveAs) +} diff --git a/src/main/menu/actions/format.js b/src/main/menu/actions/format.js index 34124b16..13eca57d 100644 --- a/src/main/menu/actions/format.js +++ b/src/main/menu/actions/format.js @@ -1,4 +1,6 @@ -const MENU_ID_FORMAT_MAP = { +import { COMMANDS } from '../../commands' + +const MENU_ID_FORMAT_MAP = Object.freeze({ strongMenuItem: 'strong', emphasisMenuItem: 'em', inlineCodeMenuItem: 'inline_code', @@ -6,14 +8,79 @@ const MENU_ID_FORMAT_MAP = { hyperlinkMenuItem: 'link', imageMenuItem: 'image', inlineMathMenuItem: 'inline_math' -} +}) -export const format = (win, type) => { +const format = (win, type) => { if (win && win.webContents) { win.webContents.send('mt::editor-format-action', { type }) } } +export const clearFormat = win => { + format(win, 'clear') +} + +export const emphasis = win => { + format(win, 'em') +} + +export const highlight = win => { + format(win, 'mark') +} + +export const hyperlink = win => { + format(win, 'link') +} + +export const image = win => { + format(win, 'image') +} + +export const inlineCode = win => { + format(win, 'inline_code') +} + +export const inlineMath = win => { + format(win, 'inline_math') +} + +export const strikethrough = win => { + format(win, 'del') +} + +export const strong = win => { + format(win, 'strong') +} + +export const subscript = win => { + format(win, 'sub') +} + +export const superscript = win => { + format(win, 'sup') +} + +export const underline = win => { + format(win, 'u') +} + +// --- Commands ------------------------------------------------------------- + +export const loadFormatCommands = commandManager => { + commandManager.add(COMMANDS.FORMAT_CLEAR_FORMAT, clearFormat) + commandManager.add(COMMANDS.FORMAT_EMPHASIS, emphasis) + commandManager.add(COMMANDS.FORMAT_HIGHLIGHT, highlight) + commandManager.add(COMMANDS.FORMAT_HYPERLINK, hyperlink) + commandManager.add(COMMANDS.FORMAT_IMAGE, image) + commandManager.add(COMMANDS.FORMAT_INLINE_CODE, inlineCode) + commandManager.add(COMMANDS.FORMAT_INLINE_MATH, inlineMath) + commandManager.add(COMMANDS.FORMAT_STRIKE, strikethrough) + commandManager.add(COMMANDS.FORMAT_STRONG, strong) + commandManager.add(COMMANDS.FORMAT_SUBSCRIPT, subscript) + commandManager.add(COMMANDS.FORMAT_SUPERSCRIPT, superscript) + commandManager.add(COMMANDS.FORMAT_UNDERLINE, underline) +} + // --- IPC events ------------------------------------------------------------- // NOTE: Don't use static `getMenuItemById` here, instead request the menu by diff --git a/src/main/menu/actions/index.js b/src/main/menu/actions/index.js new file mode 100644 index 00000000..187b5c16 --- /dev/null +++ b/src/main/menu/actions/index.js @@ -0,0 +1,17 @@ +import { loadEditCommands } from './edit' +import { loadFileCommands } from './file' +import { loadFormatCommands } from './format' +import { loadMarktextCommands } from './marktext' +import { loadParagraphCommands } from './paragraph' +import { loadViewCommands } from './view' +import { loadWindowCommands } from './window' + +export const loadMenuCommands = commandManager => { + loadEditCommands(commandManager) + loadFileCommands(commandManager) + loadFormatCommands(commandManager) + loadMarktextCommands(commandManager) + loadParagraphCommands(commandManager) + loadViewCommands(commandManager) + loadWindowCommands(commandManager) +} diff --git a/src/main/menu/actions/marktext.js b/src/main/menu/actions/marktext.js index 6fdfb143..8ec9eeea 100644 --- a/src/main/menu/actions/marktext.js +++ b/src/main/menu/actions/marktext.js @@ -1,5 +1,7 @@ import { autoUpdater } from 'electron-updater' -import { ipcMain, BrowserWindow } from 'electron' +import { ipcMain, BrowserWindow, Menu } from 'electron' +import { COMMANDS } from '../../commands' +import { isOsx } from '../../config' let runningUpdate = false let win = null @@ -36,18 +38,6 @@ autoUpdater.on('update-downloaded', () => { setImmediate(() => autoUpdater.quitAndInstall()) }) -export const userSetting = () => { - ipcMain.emit('app-create-settings-window') -} - -export const checkUpdates = browserWindow => { - if (!runningUpdate) { - runningUpdate = true - win = browserWindow - autoUpdater.checkForUpdates() - } -} - ipcMain.on('mt::NEED_UPDATE', (e, { needUpdate }) => { if (needUpdate) { autoUpdater.downloadUpdate() @@ -60,3 +50,42 @@ ipcMain.on('mt::check-for-update', e => { const win = BrowserWindow.fromWebContents(e.sender) checkUpdates(win) }) + +// -------------------------------------------------------- + +export const userSetting = () => { + ipcMain.emit('app-create-settings-window') +} + +export const checkUpdates = browserWindow => { + if (!runningUpdate) { + runningUpdate = true + win = browserWindow + autoUpdater.checkForUpdates() + } +} + +export const osxHide = () => { + if (isOsx) { + Menu.sendActionToFirstResponder('hide:') + } +} + +export const osxHideAll = () => { + if (isOsx) { + Menu.sendActionToFirstResponder('hideOtherApplications:') + } +} + +export const osxShowAll = () => { + if (isOsx) { + Menu.sendActionToFirstResponder('unhideAllApplications:') + } +} + +// --- Commands ------------------------------------------------------------- + +export const loadMarktextCommands = commandManager => { + commandManager.add(COMMANDS.MT_HIDE, osxHide) + commandManager.add(COMMANDS.MT_HIDE_OTHERS, osxHideAll) +} diff --git a/src/main/menu/actions/paragraph.js b/src/main/menu/actions/paragraph.js index fa4890b9..7bba4dab 100644 --- a/src/main/menu/actions/paragraph.js +++ b/src/main/menu/actions/paragraph.js @@ -1,3 +1,5 @@ +import { COMMANDS } from '../../commands' + const DISABLE_LABELS = [ // paragraph menu items 'heading1MenuItem', 'heading2MenuItem', 'heading3MenuItem', 'heading4MenuItem', @@ -8,7 +10,7 @@ const DISABLE_LABELS = [ 'hyperlinkMenuItem', 'imageMenuItem' ] -const MENU_ID_MAP = { +const MENU_ID_MAP = Object.freeze({ heading1MenuItem: 'h1', heading2MenuItem: 'h2', heading3MenuItem: 'h3', @@ -26,14 +28,119 @@ const MENU_ID_MAP = { paragraphMenuItem: 'p', horizontalLineMenuItem: 'hr', frontMatterMenuItem: 'frontmatter' // 'pre' -} +}) -export const paragraph = (win, type) => { +const transformEditorElement = (win, type) => { if (win && win.webContents) { win.webContents.send('mt::editor-paragraph-action', { type }) } } +export const bulletList = win => { + transformEditorElement(win, 'ul-bullet') +} + +export const codeFence = win => { + transformEditorElement(win, 'pre') +} + +export const degradeHeading = win => { + transformEditorElement(win, 'degrade heading') +} + +export const frontMatter = win => { + transformEditorElement(win, 'front-matter') +} + +export const heading1 = win => { + transformEditorElement(win, 'heading 1') +} + +export const heading2 = win => { + transformEditorElement(win, 'heading 2') +} + +export const heading3 = win => { + transformEditorElement(win, 'heading 3') +} + +export const heading4 = win => { + transformEditorElement(win, 'heading 4') +} + +export const heading5 = win => { + transformEditorElement(win, 'heading 5') +} + +export const heading6 = win => { + transformEditorElement(win, 'heading 6') +} + +export const horizontalLine = win => { + transformEditorElement(win, 'hr') +} + +export const htmlBlock = win => { + transformEditorElement(win, 'html') +} + +export const looseListItem = win => { + transformEditorElement(win, 'loose-list-item') +} + +export const mathFormula = win => { + transformEditorElement(win, 'mathblock') +} + +export const orderedList = win => { + transformEditorElement(win, 'ol-order') +} + +export const paragraph = win => { + transformEditorElement(win, 'paragraph') +} + +export const quoteBlock = win => { + transformEditorElement(win, 'blockquote') +} + +export const table = win => { + transformEditorElement(win, 'table') +} + +export const taskList = win => { + transformEditorElement(win, 'ul-task') +} + +export const increaseHeading = win => { + transformEditorElement(win, 'upgrade heading') +} + +// --- Commands ------------------------------------------------------------- + +export const loadParagraphCommands = commandManager => { + commandManager.add(COMMANDS.PARAGRAPH_BULLET_LIST, bulletList) + commandManager.add(COMMANDS.PARAGRAPH_CODE_FENCE, codeFence) + commandManager.add(COMMANDS.PARAGRAPH_DEGRADE_HEADING, degradeHeading) + commandManager.add(COMMANDS.PARAGRAPH_FRONT_MATTER, frontMatter) + commandManager.add(COMMANDS.PARAGRAPH_HEADING_1, heading1) + commandManager.add(COMMANDS.PARAGRAPH_HEADING_2, heading2) + commandManager.add(COMMANDS.PARAGRAPH_HEADING_3, heading3) + commandManager.add(COMMANDS.PARAGRAPH_HEADING_4, heading4) + commandManager.add(COMMANDS.PARAGRAPH_HEADING_5, heading5) + commandManager.add(COMMANDS.PARAGRAPH_HEADING_6, heading6) + commandManager.add(COMMANDS.PARAGRAPH_HORIZONTAL_LINE, horizontalLine) + commandManager.add(COMMANDS.PARAGRAPH_HTML_BLOCK, htmlBlock) + commandManager.add(COMMANDS.PARAGRAPH_LOOSE_LIST_ITEM, looseListItem) + commandManager.add(COMMANDS.PARAGRAPH_MATH_FORMULA, mathFormula) + commandManager.add(COMMANDS.PARAGRAPH_ORDERED_LIST, orderedList) + commandManager.add(COMMANDS.PARAGRAPH_PARAGRAPH, paragraph) + commandManager.add(COMMANDS.PARAGRAPH_QUOTE_BLOCK, quoteBlock) + commandManager.add(COMMANDS.PARAGRAPH_TABLE, table) + commandManager.add(COMMANDS.PARAGRAPH_TASK_LIST, taskList) + commandManager.add(COMMANDS.PARAGRAPH_INCREASE_HEADING, increaseHeading) +} + // --- IPC events ------------------------------------------------------------- // NOTE: Don't use static `getMenuItemById` here, instead request the menu by diff --git a/src/main/menu/actions/view.js b/src/main/menu/actions/view.js index 8ea4624e..71d4a4d8 100644 --- a/src/main/menu/actions/view.js +++ b/src/main/menu/actions/view.js @@ -1,40 +1,95 @@ -import { getMenuItemById } from '../../menu' +import { ipcMain } from 'electron' +import { COMMANDS } from '../../commands' const typewriterModeMenuItemId = 'typewriterModeMenuItem' const focusModeMenuItemId = 'focusModeMenuItem' -export const showCommandPalette = win => { - win.webContents.send('mt::show-command-palette') -} - -export const typeMode = (win, type, item) => { - if (!win) { - return - } - const { checked } = item - win.webContents.send('mt::editor-change-view', { type, checked }) - - if (type === 'sourceCode') { - const typewriterModeMenuItem = getMenuItemById(typewriterModeMenuItemId) - const focusModeMenuItem = getMenuItemById(focusModeMenuItemId) - typewriterModeMenuItem.enabled = !checked - focusModeMenuItem.enabled = !checked - } -} - -export const layout = (item, win, type, value) => { +const toggleTypeMode = (win, type) => { if (win && win.webContents) { - win.webContents.send('mt::set-view-layout', { [type]: value || item.checked }) + win.webContents.send('mt::toggle-view-mode-entry', type) } } +const setLayout = (win, type, value) => { + if (win && win.webContents) { + win.webContents.send('mt::set-view-layout', { [type]: value }) + } +} + +const toggleLayout = (win, type) => { + if (win && win.webContents) { + win.webContents.send('mt::toggle-view-layout-entry', type) + } +} + +export const debugToggleDevTools = win => { + if (win && global.MARKTEXT_DEBUG) { + win.webContents.toggleDevTools() + } +} + +export const debugReloadWindow = win => { + if (win && global.MARKTEXT_DEBUG) { + ipcMain.emit('window-reload-by-id', win.id) + } +} + +export const showCommandPalette = win => { + if (win && win.webContents) { + win.webContents.send('mt::show-command-palette') + } +} + +export const toggleFocusMode = win => { + toggleTypeMode(win, 'focus') +} + +export const toggleSourceCodeMode = win => { + toggleTypeMode(win, 'sourceCode') +} + +export const toggleSidebar = win => { + toggleLayout(win, 'showSideBar') +} + +export const toggleTabBar = win => { + toggleLayout(win, 'showTabBar') +} + export const showTabBar = win => { - const tabBarMenuItem = getMenuItemById('tabBarMenuItem') - if (tabBarMenuItem && !tabBarMenuItem.checked && tabBarMenuItem.click) { - tabBarMenuItem.click(tabBarMenuItem, win) + setLayout(win, 'showTabBar', true) +} + +export const showTableOfContents = win => { + setLayout(win, 'rightColumn', 'toc') +} + +export const toggleTypewriterMode = win => { + toggleTypeMode(win, 'typewriter') +} + +export const reloadImageCache = win => { + if (win && win.webContents) { + win.webContents.send('mt::invalidate-image-cache') } } +// --- Commands ------------------------------------------------------------- + +export const loadViewCommands = commandManager => { + commandManager.add(COMMANDS.VIEW_COMMAND_PALETTE, showCommandPalette) + commandManager.add(COMMANDS.VIEW_FOCUS_MODE, toggleFocusMode) + commandManager.add(COMMANDS.VIEW_FORCE_RELOAD_IMAGES, reloadImageCache) + commandManager.add(COMMANDS.VIEW_SOURCE_CODE_MODE, toggleSourceCodeMode) + commandManager.add(COMMANDS.VIEW_TOGGLE_SIDEBAR, toggleSidebar) + commandManager.add(COMMANDS.VIEW_TOGGLE_TABBAR, toggleTabBar) + commandManager.add(COMMANDS.VIEW_TOGGLE_TOC, showTableOfContents) + commandManager.add(COMMANDS.VIEW_TYPEWRITER_MODE, toggleTypewriterMode) + + commandManager.add(COMMANDS.VIEW_DEV_RELOAD, debugReloadWindow) + commandManager.add(COMMANDS.VIEW_TOGGLE_DEV_TOOLS, debugToggleDevTools) +} + // --- IPC events ------------------------------------------------------------- // NOTE: Don't use static `getMenuItemById` here, instead request the menu by @@ -46,6 +101,10 @@ export const showTabBar = win => { * @param {*} changes Array of changed view settings (e.g. [ {showSideBar: true} ]). */ export const viewLayoutChanged = (applicationMenu, changes) => { + const disableMenuByName = (id, value) => { + const menuItem = applicationMenu.getMenuItemById(id) + menuItem.enabled = value + } const changeMenuByName = (id, value) => { const menuItem = applicationMenu.getMenuItemById(id) menuItem.checked = value @@ -62,6 +121,8 @@ export const viewLayoutChanged = (applicationMenu, changes) => { break case 'sourceCode': changeMenuByName('sourceCodeModeMenuItem', value) + disableMenuByName(focusModeMenuItemId, !value) + disableMenuByName(typewriterModeMenuItemId, !value) break case 'typewriter': changeMenuByName(typewriterModeMenuItemId, value) diff --git a/src/main/menu/actions/window.js b/src/main/menu/actions/window.js index 6864fb1b..bb573f46 100644 --- a/src/main/menu/actions/window.js +++ b/src/main/menu/actions/window.js @@ -1,5 +1,7 @@ import { ipcMain, Menu } from 'electron' import { isOsx } from '../../config' +import { COMMANDS } from '../../commands' +import { zoomIn, zoomOut } from '../../windows/utils' export const minimizeWindow = win => { if (win) { @@ -16,3 +18,19 @@ export const toggleAlwaysOnTop = win => { ipcMain.emit('window-toggle-always-on-top', win) } } + +export const toggleFullScreen = win => { + if (win) { + win.setFullScreen(!win.isFullScreen()) + } +} + +// --- Commands ------------------------------------------------------------- + +export const loadWindowCommands = commandManager => { + commandManager.add(COMMANDS.WINDOW_MINIMIZE, minimizeWindow) + commandManager.add(COMMANDS.WINDOW_TOGGLE_ALWAYS_ON_TOP, toggleAlwaysOnTop) + commandManager.add(COMMANDS.WINDOW_TOGGLE_FULL_SCREEN, toggleFullScreen) + commandManager.add(COMMANDS.WINDOW_ZOOM_IN, zoomIn) + commandManager.add(COMMANDS.WINDOW_ZOOM_OUT, zoomOut) +} diff --git a/src/main/menu/index.js b/src/main/menu/index.js index 344f3f2b..3b640e25 100644 --- a/src/main/menu/index.js +++ b/src/main/menu/index.js @@ -4,7 +4,6 @@ import { app, ipcMain, Menu } from 'electron' import log from 'electron-log' import { ensureDirSync, isDirectory2, isFile2 } from 'common/filesystem' import { isLinux, isOsx, isWindows } from '../config' -import { parseMenu } from '../keyboard/shortcutHandler' import { updateSidebarMenu } from '../menu/actions/edit' import { updateFormatMenu } from '../menu/actions/format' import { updateSelectionMenus } from '../menu/actions/paragraph' @@ -144,9 +143,9 @@ class AppMenu { addEditorMenu (window, options = {}) { const isSourceMode = !!options.sourceCodeModeEnabled const { windowMenus } = this - windowMenus.set(window.id, this._buildEditorMenu(true)) + windowMenus.set(window.id, this._buildEditorMenu()) - const { menu, shortcutMap } = windowMenus.get(window.id) + const { menu } = windowMenus.get(window.id) // Set source-code editor if preferred. const sourceCodeModeMenuItem = menu.getMenuItemById('sourceCodeModeMenuItem') @@ -158,7 +157,19 @@ class AppMenu { typewriterModeMenuItem.enabled = false focusModeMenuItem.enabled = false } - this._keybindings.registerKeyHandlers(window, shortcutMap) + + const { _keybindings } = this + _keybindings.registerEditorKeyHandlers(window) + + if (isWindows) { + // WORKAROUND: Window close event isn't triggered on Windows if `setIgnoreMenuShortcuts(true)` is used (Electron#32674). + // NB: Remove this immediately if upstream is fixed because the event may be emitted twice. + _keybindings.registerAccelerator(window, 'Alt+F4', win => { + if (win && !win.isDestroyed()) { + win.close() + } + }) + } } /** @@ -233,7 +244,7 @@ class AppMenu { const { menu: oldMenu, type } = value if (type !== MenuType.EDITOR) return - const { menu: newMenu } = this._buildEditorMenu(false, recentUsedDocuments) + const { menu: newMenu } = this._buildEditorMenu(recentUsedDocuments) // all other menu items are set automatically updateMenuItem(oldMenu, newMenu, 'sourceCodeModeMenuItem') @@ -325,152 +336,14 @@ class AppMenu { }) } - /** - * Append misc shortcuts the the given shortcut map. - * - * @param {*} lineEnding The shortcut map. - */ - _appendMiscShortcuts = shortcutMap => { - shortcutMap.push({ - accelerator: this._keybindings.getAccelerator('tabs.cycle-forward'), - click: (menuItem, win) => { - win.webContents.send('mt::tabs-cycle-right') - }, - id: null - }) - shortcutMap.push({ - accelerator: this._keybindings.getAccelerator('tabs.cycle-backward'), - click: (menuItem, win) => { - win.webContents.send('mt::tabs-cycle-left') - }, - id: null - }) - shortcutMap.push({ - accelerator: this._keybindings.getAccelerator('tabs.switch-to-left'), - click: (menuItem, win) => { - win.webContents.send('mt::tabs-cycle-left') - }, - id: null - }) - shortcutMap.push({ - accelerator: this._keybindings.getAccelerator('tabs.switch-to-right'), - click: (menuItem, win) => { - win.webContents.send('mt::tabs-cycle-right') - }, - id: null - }) - shortcutMap.push({ - accelerator: this._keybindings.getAccelerator('tabs.switch-to-first'), - click: (menuItem, win) => { - win.webContents.send('mt::switch-first-tab') - }, - id: null - }) - shortcutMap.push({ - accelerator: this._keybindings.getAccelerator('tabs.switch-to-second'), - click: (menuItem, win) => { - win.webContents.send('mt::switch-second-tab') - }, - id: null - }) - shortcutMap.push({ - accelerator: this._keybindings.getAccelerator('tabs.switch-to-third'), - click: (menuItem, win) => { - win.webContents.send('mt::switch-third-tab') - }, - id: null - }) - shortcutMap.push({ - accelerator: this._keybindings.getAccelerator('tabs.switch-to-fourth'), - click: (menuItem, win) => { - win.webContents.send('mt::switch-fourth-tab') - }, - id: null - }) - shortcutMap.push({ - accelerator: this._keybindings.getAccelerator('tabs.switch-to-fifth'), - click: (menuItem, win) => { - win.webContents.send('mt::switch-fifth-tab') - }, - id: null - }) - shortcutMap.push({ - accelerator: this._keybindings.getAccelerator('tabs.switch-to-sixth'), - click: (menuItem, win) => { - win.webContents.send('mt::switch-sixth-tab') - }, - id: null - }) - shortcutMap.push({ - accelerator: this._keybindings.getAccelerator('tabs.switch-to-seventh'), - click: (menuItem, win) => { - win.webContents.send('mt::switch-seventh-tab') - }, - id: null - }) - shortcutMap.push({ - accelerator: this._keybindings.getAccelerator('tabs.switch-to-eighth'), - click: (menuItem, win) => { - win.webContents.send('mt::switch-eighth-tab') - }, - id: null - }) - shortcutMap.push({ - accelerator: this._keybindings.getAccelerator('tabs.switch-to-ninth'), - click: (menuItem, win) => { - win.webContents.send('mt::switch-ninth-tab') - }, - id: null - }) - shortcutMap.push({ - accelerator: this._keybindings.getAccelerator('tabs.switch-to-tenth'), - click: (menuItem, win) => { - win.webContents.send('mt::switch-tenth-tab') - }, - id: null - }) - shortcutMap.push({ - accelerator: this._keybindings.getAccelerator('file.quick-open'), - click: (menuItem, win) => { - win.webContents.send('mt::execute-command-by-id', 'file.quick-open') - }, - id: null - }) - - if (isWindows) { - // WORKAROUND: Window close event isn't triggered on Windows if `setIgnoreMenuShortcuts(true)` is used (Electron#32674). - // NB: Remove this immediately if upstream is fixed because the event may be emitted twice. - shortcutMap.push({ - accelerator: 'Alt+F4', - click: (menuItem, win) => { - if (win && !win.isDestroyed()) { - win.close() - } - }, - id: null - }) - } - } - - _buildEditorMenu (createShortcutMap, recentUsedDocuments = null) { + _buildEditorMenu (recentUsedDocuments = null) { if (!recentUsedDocuments) { recentUsedDocuments = this.getRecentlyUsedDocuments() } const menuTemplate = configureMenu(this._keybindings, this._preferences, recentUsedDocuments) const menu = Menu.buildFromTemplate(menuTemplate) - - let shortcutMap = null - if (createShortcutMap) { - shortcutMap = parseMenu(menuTemplate) - this._appendMiscShortcuts(shortcutMap) - } - - return { - shortcutMap, - menu, - type: MenuType.EDITOR - } + return { menu, type: MenuType.EDITOR } } _buildSettingMenu () { diff --git a/src/main/menu/templates/dock.js b/src/main/menu/templates/dock.js index 9f000c18..d2ca61f9 100644 --- a/src/main/menu/templates/dock.js +++ b/src/main/menu/templates/dock.js @@ -4,7 +4,11 @@ import * as actions from '../actions/file' const dockMenu = Menu.buildFromTemplate([{ label: 'Open...', click (menuItem, browserWindow) { - actions.openFile(browserWindow) + if (browserWindow) { + actions.openFile(browserWindow) + } else { + actions.newEditorWindow() + } } }, { label: 'Clear Recent', diff --git a/src/main/menu/templates/edit.js b/src/main/menu/templates/edit.js index 93cf5ddd..332a703c 100755 --- a/src/main/menu/templates/edit.js +++ b/src/main/menu/templates/edit.js @@ -1,38 +1,39 @@ import * as actions from '../actions/edit' import { isOsx } from '../../config' +import { COMMANDS } from '../../commands' export default function (keybindings) { return { label: '&Edit', submenu: [{ label: 'Undo', - accelerator: keybindings.getAccelerator('edit.undo'), + accelerator: keybindings.getAccelerator(COMMANDS.EDIT_UNDO), click: (menuItem, browserWindow) => { - actions.edit(browserWindow, 'undo') + actions.editorUndo(browserWindow) } }, { label: 'Redo', - accelerator: keybindings.getAccelerator('edit.redo'), + accelerator: keybindings.getAccelerator(COMMANDS.EDIT_REDO), click: (menuItem, browserWindow) => { - actions.edit(browserWindow, 'redo') + actions.editorRedo(browserWindow) } }, { type: 'separator' }, { label: 'Cut', - accelerator: keybindings.getAccelerator('edit.cut'), + accelerator: keybindings.getAccelerator(COMMANDS.EDIT_CUT), click (menuItem, browserWindow) { actions.nativeCut(browserWindow) } }, { label: 'Copy', - accelerator: keybindings.getAccelerator('edit.copy'), + accelerator: keybindings.getAccelerator(COMMANDS.EDIT_COPY), click (menuItem, browserWindow) { actions.nativeCopy(browserWindow) } }, { label: 'Paste', - accelerator: keybindings.getAccelerator('edit.paste'), + accelerator: keybindings.getAccelerator(COMMANDS.EDIT_PASTE), click (menuItem, browserWindow) { actions.nativePaste(browserWindow) } @@ -40,83 +41,83 @@ export default function (keybindings) { type: 'separator' }, { label: 'Copy as Markdown', - accelerator: keybindings.getAccelerator('edit.copy-as-markdown'), + accelerator: keybindings.getAccelerator(COMMANDS.EDIT_COPY_AS_MARKDOWN), click (menuItem, browserWindow) { - actions.edit(browserWindow, 'copyAsMarkdown') + actions.editorCopyAsMarkdown(browserWindow) } }, { label: 'Copy as HTML', - accelerator: keybindings.getAccelerator('edit.copy-as-html'), + accelerator: keybindings.getAccelerator(COMMANDS.EDIT_COPY_AS_HTML), click (menuItem, browserWindow) { - actions.edit(browserWindow, 'copyAsHtml') + actions.editorCopyAsHtml(browserWindow) } }, { label: 'Paste as Plain Text', - accelerator: keybindings.getAccelerator('edit.paste-as-plaintext'), + accelerator: keybindings.getAccelerator(COMMANDS.EDIT_PASTE_AS_PLAINTEXT), click (menuItem, browserWindow) { - actions.edit(browserWindow, 'pasteAsPlainText') + actions.editorPasteAsPlainText(browserWindow) } }, { type: 'separator' }, { label: 'Select All', - accelerator: keybindings.getAccelerator('edit.select-all'), + accelerator: keybindings.getAccelerator(COMMANDS.EDIT_SELECT_ALL), click (menuItem, browserWindow) { - actions.edit(browserWindow, 'selectAll') + actions.editorSelectAll(browserWindow) } }, { type: 'separator' }, { label: 'Duplicate', - accelerator: keybindings.getAccelerator('edit.duplicate'), + accelerator: keybindings.getAccelerator(COMMANDS.EDIT_DUPLICATE), click (menuItem, browserWindow) { - actions.edit(browserWindow, 'duplicate') + actions.editorDuplicate(browserWindow) } }, { label: 'Create Paragraph', - accelerator: keybindings.getAccelerator('edit.create-paragraph'), + accelerator: keybindings.getAccelerator(COMMANDS.EDIT_CREATE_PARAGRAPH), click (menuItem, browserWindow) { - actions.edit(browserWindow, 'createParagraph') + actions.editorCreateParagraph(browserWindow) } }, { label: 'Delete Paragraph', - accelerator: keybindings.getAccelerator('edit.delete-paragraph'), + accelerator: keybindings.getAccelerator(COMMANDS.EDIT_DELETE_PARAGRAPH), click (menuItem, browserWindow) { - actions.edit(browserWindow, 'deleteParagraph') + actions.editorDeleteParagraph(browserWindow) } }, { type: 'separator' }, { label: 'Find', - accelerator: keybindings.getAccelerator('edit.find'), + accelerator: keybindings.getAccelerator(COMMANDS.EDIT_FIND), click (menuItem, browserWindow) { - actions.edit(browserWindow, 'find') + actions.editorFind(browserWindow) } }, { label: 'Find Next', - accelerator: keybindings.getAccelerator('edit.find-next'), + accelerator: keybindings.getAccelerator(COMMANDS.EDIT_FIND_NEXT), click (menuItem, browserWindow) { - actions.edit(browserWindow, 'findNext') + actions.editorFindNext(browserWindow) } }, { label: 'Find Previous', - accelerator: keybindings.getAccelerator('edit.find-previous'), + accelerator: keybindings.getAccelerator(COMMANDS.EDIT_FIND_PREVIOUS), click (menuItem, browserWindow) { - actions.edit(browserWindow, 'findPrev') + actions.editorFindPrevious(browserWindow) } }, { label: 'Replace', - accelerator: keybindings.getAccelerator('edit.replace'), + accelerator: keybindings.getAccelerator(COMMANDS.EDIT_REPLACE), click (menuItem, browserWindow) { - actions.edit(browserWindow, 'replace') + actions.editorReplace(browserWindow) } }, { type: 'separator' }, { label: 'Find in Folder', - accelerator: keybindings.getAccelerator('edit.find-in-folder'), + accelerator: keybindings.getAccelerator(COMMANDS.EDIT_FIND_IN_FOLDER), click (menuItem, browserWindow) { - actions.edit(browserWindow, 'findInFolder') + actions.findInFolder(browserWindow) } }, { type: 'separator' @@ -124,7 +125,7 @@ export default function (keybindings) { label: 'Screenshot', id: 'screenshot', visible: isOsx, - accelerator: keybindings.getAccelerator('edit.screenshot'), + accelerator: keybindings.getAccelerator(COMMANDS.EDIT_SCREENSHOT), click (menuItem, browserWindow) { actions.screenshot(browserWindow) } diff --git a/src/main/menu/templates/file.js b/src/main/menu/templates/file.js index cbdec764..b62ac0c3 100755 --- a/src/main/menu/templates/file.js +++ b/src/main/menu/templates/file.js @@ -1,7 +1,6 @@ import { app } from 'electron' import * as actions from '../actions/file' import { userSetting } from '../actions/marktext' -import { showTabBar } from '../actions/view' import { isOsx } from '../../config' export default function (keybindings, userPreference, recentlyUsedFiles) { @@ -13,11 +12,10 @@ export default function (keybindings, userPreference, recentlyUsedFiles) { accelerator: keybindings.getAccelerator('file.new-tab'), click (menuItem, browserWindow) { actions.newBlankTab(browserWindow) - showTabBar(browserWindow) } }, { label: 'New Window', - accelerator: keybindings.getAccelerator('file.new-file'), + accelerator: keybindings.getAccelerator('file.new-window'), click (menuItem, browserWindow) { actions.newEditorWindow() } @@ -77,8 +75,6 @@ export default function (keybindings, userPreference, recentlyUsedFiles) { fileMenu.submenu.push({ type: 'separator' - }, { - type: 'separator' }, { label: 'Save', accelerator: keybindings.getAccelerator('file.save'), @@ -139,7 +135,7 @@ export default function (keybindings, userPreference, recentlyUsedFiles) { label: 'Print', accelerator: keybindings.getAccelerator('file.print'), click (menuItem, browserWindow) { - actions.print(browserWindow) + actions.printDocument(browserWindow) } }, { type: 'separator', diff --git a/src/main/menu/templates/format.js b/src/main/menu/templates/format.js index d3d9ea80..58aa72ef 100644 --- a/src/main/menu/templates/format.js +++ b/src/main/menu/templates/format.js @@ -9,24 +9,24 @@ export default function (keybindings) { label: 'Bold', type: 'checkbox', accelerator: keybindings.getAccelerator('format.strong'), - click (menuItem, browserWindow) { - actions.format(browserWindow, 'strong') + click (menuItem, focusedWindow) { + actions.strong(focusedWindow) } }, { id: 'emphasisMenuItem', label: 'Italic', type: 'checkbox', accelerator: keybindings.getAccelerator('format.emphasis'), - click (menuItem, browserWindow) { - actions.format(browserWindow, 'em') + click (menuItem, focusedWindow) { + actions.emphasis(focusedWindow) } }, { id: 'underlineMenuItem', label: 'Underline', type: 'checkbox', accelerator: keybindings.getAccelerator('format.underline'), - click (menuItem, browserWindow) { - actions.format(browserWindow, 'u') + click (menuItem, focusedWindow) { + actions.underline(focusedWindow) } }, { type: 'separator' @@ -35,24 +35,24 @@ export default function (keybindings) { label: 'Superscript', type: 'checkbox', accelerator: keybindings.getAccelerator('format.superscript'), - click (menuItem, browserWindow) { - actions.format(browserWindow, 'sup') + click (menuItem, focusedWindow) { + actions.superscript(focusedWindow) } }, { id: 'subscriptMenuItem', label: 'Subscript', type: 'checkbox', accelerator: keybindings.getAccelerator('format.subscript'), - click (menuItem, browserWindow) { - actions.format(browserWindow, 'sub') + click (menuItem, focusedWindow) { + actions.subscript(focusedWindow) } }, { id: 'highlightMenuItem', label: 'Highlight', type: 'checkbox', accelerator: keybindings.getAccelerator('format.highlight'), - click (menuItem, browserWindow) { - actions.format(browserWindow, 'mark') + click (menuItem, focusedWindow) { + actions.highlight(focusedWindow) } }, { type: 'separator' @@ -61,16 +61,16 @@ export default function (keybindings) { label: 'Inline Code', type: 'checkbox', accelerator: keybindings.getAccelerator('format.inline-code'), - click (menuItem, browserWindow) { - actions.format(browserWindow, 'inline_code') + click (menuItem, focusedWindow) { + actions.inlineCode(focusedWindow) } }, { id: 'inlineMathMenuItem', label: 'Inline Math', type: 'checkbox', accelerator: keybindings.getAccelerator('format.inline-math'), - click (menuItem, browserWindow) { - actions.format(browserWindow, 'inline_math') + click (menuItem, focusedWindow) { + actions.inlineMath(focusedWindow) } }, { type: 'separator' @@ -79,32 +79,32 @@ export default function (keybindings) { label: 'Strikethrough', type: 'checkbox', accelerator: keybindings.getAccelerator('format.strike'), - click (menuItem, browserWindow) { - actions.format(browserWindow, 'del') + click (menuItem, focusedWindow) { + actions.strikethrough(focusedWindow) } }, { id: 'hyperlinkMenuItem', label: 'Hyperlink', type: 'checkbox', accelerator: keybindings.getAccelerator('format.hyperlink'), - click (menuItem, browserWindow) { - actions.format(browserWindow, 'link') + click (menuItem, focusedWindow) { + actions.hyperlink(focusedWindow) } }, { id: 'imageMenuItem', label: 'Image', type: 'checkbox', accelerator: keybindings.getAccelerator('format.image'), - click (menuItem, browserWindow) { - actions.format(browserWindow, 'image') + click (menuItem, focusedWindow) { + actions.image(focusedWindow) } }, { type: 'separator' }, { label: 'Clear Formatting', accelerator: keybindings.getAccelerator('format.clear-format'), - click (menuItem, browserWindow) { - actions.format(browserWindow, 'clear') + click (menuItem, focusedWindow) { + actions.clearFormat(focusedWindow) } }] } diff --git a/src/main/menu/templates/marktext.js b/src/main/menu/templates/marktext.js index 696c2da6..92678189 100755 --- a/src/main/menu/templates/marktext.js +++ b/src/main/menu/templates/marktext.js @@ -1,4 +1,4 @@ -import { app, Menu } from 'electron' +import { app } from 'electron' import { showAboutDialog } from '../actions/help' import * as actions from '../actions/marktext' @@ -9,13 +9,13 @@ export default function (keybindings) { label: 'MarkText', submenu: [{ label: 'About MarkText', - click (menuItem, browserWindow) { - showAboutDialog(browserWindow) + click (menuItem, focusedWindow) { + showAboutDialog(focusedWindow) } }, { label: 'Check for updates...', - click (menuItem, browserWindow) { - actions.checkUpdates(browserWindow) + click (menuItem, focusedWindow) { + actions.checkUpdates(focusedWindow) } }, { label: 'Preferences', @@ -35,18 +35,18 @@ export default function (keybindings) { label: 'Hide MarkText', accelerator: keybindings.getAccelerator('mt.hide'), click () { - Menu.sendActionToFirstResponder('hide:') + actions.osxHide() } }, { label: 'Hide Others', accelerator: keybindings.getAccelerator('mt.hide-others'), click () { - Menu.sendActionToFirstResponder('hideOtherApplications:') + actions.osxHideAll() } }, { label: 'Show All', click () { - Menu.sendActionToFirstResponder('unhideAllApplications:') + actions.osxShowAll() } }, { type: 'separator' diff --git a/src/main/menu/templates/paragraph.js b/src/main/menu/templates/paragraph.js index b3a3e54f..37e5a864 100644 --- a/src/main/menu/templates/paragraph.js +++ b/src/main/menu/templates/paragraph.js @@ -9,48 +9,48 @@ export default function (keybindings) { label: 'Heading 1', type: 'checkbox', accelerator: keybindings.getAccelerator('paragraph.heading-1'), - click (menuItem, browserWindow) { - actions.paragraph(browserWindow, 'heading 1') + click (menuItem, focusedWindow) { + actions.heading1(focusedWindow) } }, { id: 'heading2MenuItem', label: 'Heading 2', type: 'checkbox', accelerator: keybindings.getAccelerator('paragraph.heading-2'), - click (menuItem, browserWindow) { - actions.paragraph(browserWindow, 'heading 2') + click (menuItem, focusedWindow) { + actions.heading2(focusedWindow) } }, { id: 'heading3MenuItem', label: 'Heading 3', type: 'checkbox', accelerator: keybindings.getAccelerator('paragraph.heading-3'), - click (menuItem, browserWindow) { - actions.paragraph(browserWindow, 'heading 3') + click (menuItem, focusedWindow) { + actions.heading3(focusedWindow) } }, { id: 'heading4MenuItem', label: 'Heading 4', type: 'checkbox', accelerator: keybindings.getAccelerator('paragraph.heading-4'), - click (menuItem, browserWindow) { - actions.paragraph(browserWindow, 'heading 4') + click (menuItem, focusedWindow) { + actions.heading4(focusedWindow) } }, { id: 'heading5MenuItem', label: 'Heading 5', type: 'checkbox', accelerator: keybindings.getAccelerator('paragraph.heading-5'), - click (menuItem, browserWindow) { - actions.paragraph(browserWindow, 'heading 5') + click (menuItem, focusedWindow) { + actions.heading5(focusedWindow) } }, { id: 'heading6MenuItem', label: 'Heading 6', type: 'checkbox', accelerator: keybindings.getAccelerator('paragraph.heading-6'), - click (menuItem, browserWindow) { - actions.paragraph(browserWindow, 'heading 6') + click (menuItem, focusedWindow) { + actions.heading6(focusedWindow) } }, { type: 'separator' @@ -58,15 +58,15 @@ export default function (keybindings) { id: 'upgradeHeadingMenuItem', label: 'Promote Heading', accelerator: keybindings.getAccelerator('paragraph.upgrade-heading'), - click (menuItem, browserWindow) { - actions.paragraph(browserWindow, 'upgrade heading') + click (menuItem, focusedWindow) { + actions.increaseHeading(focusedWindow) } }, { id: 'degradeHeadingMenuItem', label: 'Demote Heading', accelerator: keybindings.getAccelerator('paragraph.degrade-heading'), - click (menuItem, browserWindow) { - actions.paragraph(browserWindow, 'degrade heading') + click (menuItem, focusedWindow) { + actions.degradeHeading(focusedWindow) } }, { type: 'separator' @@ -75,40 +75,40 @@ export default function (keybindings) { label: 'Table', type: 'checkbox', accelerator: keybindings.getAccelerator('paragraph.table'), - click (menuItem, browserWindow) { - actions.paragraph(browserWindow, 'table') + click (menuItem, focusedWindow) { + actions.table(focusedWindow) } }, { id: 'codeFencesMenuItem', label: 'Code Fences', type: 'checkbox', accelerator: keybindings.getAccelerator('paragraph.code-fence'), - click (menuItem, browserWindow) { - actions.paragraph(browserWindow, 'pre') + click (menuItem, focusedWindow) { + actions.codeFence(focusedWindow) } }, { id: 'quoteBlockMenuItem', label: 'Quote Block', type: 'checkbox', accelerator: keybindings.getAccelerator('paragraph.quote-block'), - click (menuItem, browserWindow) { - actions.paragraph(browserWindow, 'blockquote') + click (menuItem, focusedWindow) { + actions.quoteBlock(focusedWindow) } }, { id: 'mathBlockMenuItem', label: 'Math Block', type: 'checkbox', accelerator: keybindings.getAccelerator('paragraph.math-formula'), - click (menuItem, browserWindow) { - actions.paragraph(browserWindow, 'mathblock') + click (menuItem, focusedWindow) { + actions.mathFormula(focusedWindow) } }, { id: 'htmlBlockMenuItem', label: 'Html Block', type: 'checkbox', accelerator: keybindings.getAccelerator('paragraph.html-block'), - click (menuItem, browserWindow) { - actions.paragraph(browserWindow, 'html') + click (menuItem, focusedWindow) { + actions.htmlBlock(focusedWindow) } }, { type: 'separator' @@ -117,24 +117,24 @@ export default function (keybindings) { label: 'Ordered List', type: 'checkbox', accelerator: keybindings.getAccelerator('paragraph.order-list'), - click (menuItem, browserWindow) { - actions.paragraph(browserWindow, 'ol-order') + click (menuItem, focusedWindow) { + actions.orderedList(focusedWindow) } }, { id: 'bulletListMenuItem', label: 'Bullet List', type: 'checkbox', accelerator: keybindings.getAccelerator('paragraph.bullet-list'), - click (menuItem, browserWindow) { - actions.paragraph(browserWindow, 'ul-bullet') + click (menuItem, focusedWindow) { + actions.bulletList(focusedWindow) } }, { id: 'taskListMenuItem', label: 'Task List', type: 'checkbox', accelerator: keybindings.getAccelerator('paragraph.task-list'), - click (menuItem, browserWindow) { - actions.paragraph(browserWindow, 'ul-task') + click (menuItem, focusedWindow) { + actions.taskList(focusedWindow) } }, { type: 'separator' @@ -143,8 +143,8 @@ export default function (keybindings) { label: 'Loose List Item', type: 'checkbox', accelerator: keybindings.getAccelerator('paragraph.loose-list-item'), - click (menuItem, browserWindow) { - actions.paragraph(browserWindow, 'loose-list-item') + click (menuItem, focusedWindow) { + actions.looseListItem(focusedWindow) } }, { type: 'separator' @@ -153,24 +153,24 @@ export default function (keybindings) { label: 'Paragraph', type: 'checkbox', accelerator: keybindings.getAccelerator('paragraph.paragraph'), - click (menuItem, browserWindow) { - actions.paragraph(browserWindow, 'paragraph') + click (menuItem, focusedWindow) { + actions.paragraph(focusedWindow) } }, { id: 'horizontalLineMenuItem', label: 'Horizontal Rule', type: 'checkbox', accelerator: keybindings.getAccelerator('paragraph.horizontal-line'), - click (menuItem, browserWindow) { - actions.paragraph(browserWindow, 'hr') + click (menuItem, focusedWindow) { + actions.horizontalLine(focusedWindow) } }, { id: 'frontMatterMenuItem', label: 'Front Matter', type: 'checkbox', accelerator: keybindings.getAccelerator('paragraph.front-matter'), - click (menuItem, browserWindow) { - actions.paragraph(browserWindow, 'front-matter') + click (menuItem, focusedWindow) { + actions.frontMatter(focusedWindow) } }] } diff --git a/src/main/menu/templates/view.js b/src/main/menu/templates/view.js index a8d5f09c..b506d5d8 100755 --- a/src/main/menu/templates/view.js +++ b/src/main/menu/templates/view.js @@ -1,4 +1,3 @@ -import { ipcMain } from 'electron' import * as actions from '../actions/view' export default function (keybindings) { @@ -7,8 +6,8 @@ export default function (keybindings) { submenu: [{ label: 'Command Palette...', accelerator: keybindings.getAccelerator('view.command-palette'), - click (menuItem, browserWindow) { - actions.showCommandPalette(browserWindow) + click (menuItem, focusedWindow) { + actions.showCommandPalette(focusedWindow) } }, { type: 'separator' @@ -18,12 +17,8 @@ export default function (keybindings) { accelerator: keybindings.getAccelerator('view.source-code-mode'), type: 'checkbox', checked: false, - click (item, browserWindow, event) { - // if we call this function, the checked state is not set - if (!event) { - item.checked = !item.checked - } - actions.typeMode(browserWindow, 'sourceCode', item) + click (item, focusedWindow) { + actions.toggleSourceCodeMode(focusedWindow) } }, { id: 'typewriterModeMenuItem', @@ -31,12 +26,8 @@ export default function (keybindings) { accelerator: keybindings.getAccelerator('view.typewriter-mode'), type: 'checkbox', checked: false, - click (item, browserWindow, event) { - // if we call this function, the checked state is not set - if (!event) { - item.checked = !item.checked - } - actions.typeMode(browserWindow, 'typewriter', item) + click (item, focusedWindow) { + actions.toggleTypewriterMode(focusedWindow) } }, { id: 'focusModeMenuItem', @@ -44,12 +35,8 @@ export default function (keybindings) { accelerator: keybindings.getAccelerator('view.focus-mode'), type: 'checkbox', checked: false, - click (item, browserWindow, event) { - // if we call this function, the checked state is not set - if (!event) { - item.checked = !item.checked - } - actions.typeMode(browserWindow, 'focus', item) + click (item, focusedWindow) { + actions.toggleFocusMode(focusedWindow) } }, { type: 'separator' @@ -59,13 +46,8 @@ export default function (keybindings) { accelerator: keybindings.getAccelerator('view.toggle-sidebar'), type: 'checkbox', checked: false, - click (item, browserWindow, event) { - // if we call this function, the checked state is not set - if (!event) { - item.checked = !item.checked - } - - actions.layout(item, browserWindow, 'showSideBar') + click (item, focusedWindow) { + actions.toggleSidebar(focusedWindow) } }, { label: 'Show Tab Bar', @@ -73,51 +55,41 @@ export default function (keybindings) { accelerator: keybindings.getAccelerator('view.toggle-tabbar'), type: 'checkbox', checked: false, - click (item, browserWindow, event) { - // if we call this function, the checked state is not set - if (!event) { - item.checked = !item.checked - } - - actions.layout(item, browserWindow, 'showTabBar') + click (item, focusedWindow) { + actions.toggleTabBar(focusedWindow) } }, { label: 'Toggle Table of Contents', id: 'tocMenuItem', accelerator: keybindings.getAccelerator('view.toggle-toc'), - click (_, browserWindow) { - actions.layout(null, browserWindow, 'rightColumn', 'toc') + click (_, focusedWindow) { + actions.showTableOfContents(focusedWindow) } }, { label: 'Reload Images', accelerator: keybindings.getAccelerator('view.reload-images'), click (item, focusedWindow) { - if (focusedWindow) { - focusedWindow.webContents.send('mt::invalidate-image-cache', {}) - } + actions.reloadImageCache(focusedWindow) } - }, { - type: 'separator' }] } if (global.MARKTEXT_DEBUG) { + viewMenu.submenu.push({ + type: 'separator' + }) viewMenu.submenu.push({ label: 'Show Developer Tools', accelerator: keybindings.getAccelerator('view.toggle-dev-tools'), - click (item, focusedWindow) { - if (focusedWindow) { - focusedWindow.webContents.toggleDevTools() - } + click (item, win) { + actions.debugToggleDevTools(win) } }) viewMenu.submenu.push({ label: 'Reload window', accelerator: keybindings.getAccelerator('view.dev-reload'), click (item, focusedWindow) { - if (focusedWindow) { - ipcMain.emit('window-reload-by-id', focusedWindow.id) - } + actions.debugReloadWindow(focusedWindow) } }) } diff --git a/src/main/menu/templates/window.js b/src/main/menu/templates/window.js index 2e4e8d52..f37fd953 100755 --- a/src/main/menu/templates/window.js +++ b/src/main/menu/templates/window.js @@ -1,5 +1,5 @@ import { Menu } from 'electron' -import { minimizeWindow, toggleAlwaysOnTop } from '../actions/window' +import { minimizeWindow, toggleAlwaysOnTop, toggleFullScreen } from '../actions/window' import { zoomIn, zoomOut } from '../../windows/utils' import { isOsx } from '../../config' @@ -40,9 +40,9 @@ export default function (keybindings) { }, { label: 'Show in Full Screen', accelerator: keybindings.getAccelerator('window.toggle-full-screen'), - click (item, focusedWindow) { - if (focusedWindow) { - focusedWindow.setFullScreen(!focusedWindow.isFullScreen()) + click (item, browserWindow) { + if (browserWindow) { + toggleFullScreen(browserWindow) } } }] diff --git a/src/main/preferences/schema.json b/src/main/preferences/schema.json index 6cbf587f..1cf93be6 100644 --- a/src/main/preferences/schema.json +++ b/src/main/preferences/schema.json @@ -346,6 +346,7 @@ }, "spellcheckerLanguage": { "description": "Spelling--The spell checker language", + "type": "string", "pattern": "^[a-z]{2,3}(?:[-](?:[0-9]|[a-zA-Z]){2,}){0,2}$", "default": "en-US" }, diff --git a/src/renderer/commands/descriptions.js b/src/renderer/commands/descriptions.js index bb549be0..4a58e0d3 100644 --- a/src/renderer/commands/descriptions.js +++ b/src/renderer/commands/descriptions.js @@ -5,7 +5,7 @@ const commandDescriptions = Object.freeze({ 'mt.hide': 'MarkText: Hide MarkText', 'mt.hide-others': 'MarkText: Hide Others', - 'file.new-file': 'File: New Window', + 'file.new-window': 'File: New Window', 'file.new-tab': 'File: New Tab', 'file.open-file': 'File: Open file', 'file.open-folder': 'File: Open Folder', @@ -102,7 +102,7 @@ const commandDescriptions = Object.freeze({ // # Menu descriptions but not available as command // # - 'view.reload-images': 'View: Clear cache and reload images', + 'view.reload-images': 'View: Force reload images', // ============================================ // # Additional command descriptions diff --git a/src/renderer/commands/index.js b/src/renderer/commands/index.js index b8669441..6d064c6c 100644 --- a/src/renderer/commands/index.js +++ b/src/renderer/commands/index.js @@ -43,7 +43,7 @@ const commands = [ ipcRenderer.emit('mt::new-untitled-tab', null) } }, { - id: 'file.new-file', + id: 'file.new-window', execute: async () => { ipcRenderer.send('mt::cmd-new-editor-window') } @@ -581,12 +581,12 @@ const commands = [ }, { id: 'view.toggle-sidebar', execute: async () => { - bus.$emit('view:toggle-view-layout-entry', 'showSideBar') + bus.$emit('view:toggle-layout-entry', 'showSideBar') } }, { id: 'view.toggle-tabbar', execute: async () => { - bus.$emit('view:toggle-view-layout-entry', 'showTabBar') + bus.$emit('view:toggle-layout-entry', 'showTabBar') } }, diff --git a/src/renderer/pages/app.vue b/src/renderer/pages/app.vue index a680a7ad..f85843a9 100644 --- a/src/renderer/pages/app.vue +++ b/src/renderer/pages/app.vue @@ -125,7 +125,6 @@ export default { dispatch('LISTEN_FOR_TWEET') // module: layout dispatch('LISTEN_FOR_LAYOUT') - dispatch('LISTEN_FOR_REQUEST_LAYOUT') // module: listenForMain dispatch('LISTEN_FOR_EDIT') dispatch('LISTEN_FOR_VIEW') diff --git a/src/renderer/store/editor.js b/src/renderer/store/editor.js index 6726503f..bc3005a9 100644 --- a/src/renderer/store/editor.js +++ b/src/renderer/store/editor.js @@ -646,7 +646,7 @@ const actions = { showSideBar: !!sideBarVisibility, showTabBar: !!tabBarVisibility }) - dispatch('SET_LAYOUT_MENU_ITEM') + dispatch('DISPATCH_LAYOUT_MENU_ITEMS') commit('SET_MODE', { type: 'sourceCode', @@ -701,35 +701,8 @@ const actions = { }, LISTEN_FOR_SWITCH_TABS ({ commit, state, dispatch }) { - ipcRenderer.on('mt::switch-first-tab', e => { - dispatch('SWITCH_TABS', 1) - }) - ipcRenderer.on('mt::switch-second-tab', e => { - dispatch('SWITCH_TABS', 2) - }) - ipcRenderer.on('mt::switch-third-tab', e => { - dispatch('SWITCH_TABS', 3) - }) - ipcRenderer.on('mt::switch-fourth-tab', e => { - dispatch('SWITCH_TABS', 4) - }) - ipcRenderer.on('mt::switch-fifth-tab', e => { - dispatch('SWITCH_TABS', 5) - }) - ipcRenderer.on('mt::switch-sixth-tab', e => { - dispatch('SWITCH_TABS', 6) - }) - ipcRenderer.on('mt::switch-seventh-tab', e => { - dispatch('SWITCH_TABS', 7) - }) - ipcRenderer.on('mt::switch-eighth-tab', e => { - dispatch('SWITCH_TABS', 8) - }) - ipcRenderer.on('mt::switch-ninth-tab', e => { - dispatch('SWITCH_TABS', 9) - }) - ipcRenderer.on('mt::switch-tenth-tab', e => { - dispatch('SWITCH_TABS', 10) + ipcRenderer.on('mt::switch-tab-by-index', (event, index) => { + dispatch('SWITCH_TAB_BY_INDEX', index) }) }, @@ -796,29 +769,30 @@ const actions = { console.error(`CYCLE_TABS: Cannot find next tab (index="${nextTabIndex}").`) return } + commit('SET_CURRENT_FILE', nextTab) dispatch('UPDATE_LINE_ENDING_MENU') }, - // switch tabs with Alt+#num. - SWITCH_TABS ({ commit, dispatch, state }, num) { + SWITCH_TAB_BY_INDEX ({ commit, dispatch, state }, nextTabIndex) { const { tabs, currentFile } = state - if (tabs.length <= 1 || num > tabs.length) { + if (nextTabIndex < 0 || nextTabIndex >= tabs.length) { + console.warn('Invalid tab index:', nextTabIndex) return } const currentIndex = tabs.findIndex(t => t.id === currentFile.id) if (currentIndex === -1) { - console.error('CYCLE_TABS: Cannot find current tab index.') + console.error('Cannot find current tab index.') return } - const nextTabIndex = num - 1 const nextTab = tabs[nextTabIndex] if (!nextTab || !nextTab.id) { - console.error(`CYCLE_TABS: Cannot find next tab (index="${nextTabIndex}").`) + console.error(`Cannot find tab by index="${nextTabIndex}".`) return } + commit('SET_CURRENT_FILE', nextTab) dispatch('UPDATE_LINE_ENDING_MENU') }, @@ -916,7 +890,7 @@ const actions = { const { tabs } = state if (always || tabs.length === 1) { commit('SET_LAYOUT', { showTabBar: true }) - dispatch('SET_LAYOUT_MENU_ITEM') + dispatch('DISPATCH_LAYOUT_MENU_ITEMS') } }, @@ -1231,7 +1205,7 @@ const actions = { }, LISTEN_FOR_RELOAD_IMAGES () { - ipcRenderer.on('mt::invalidate-image-cache', (e) => { + ipcRenderer.on('mt::invalidate-image-cache', () => { bus.$emit('invalidate-image-cache') }) } diff --git a/src/renderer/store/layout.js b/src/renderer/store/layout.js index 8bf95131..b45f83bc 100644 --- a/src/renderer/store/layout.js +++ b/src/renderer/store/layout.js @@ -33,7 +33,7 @@ const mutations = { } const actions = { - LISTEN_FOR_LAYOUT ({ state, commit }) { + LISTEN_FOR_LAYOUT ({ state, commit, dispatch }) { ipcRenderer.on('mt::set-view-layout', (e, layout) => { if (layout.rightColumn) { commit('SET_LAYOUT', { @@ -44,26 +44,27 @@ const actions = { } else { commit('SET_LAYOUT', layout) } + dispatch('DISPATCH_LAYOUT_MENU_ITEMS') }) - bus.$on('view:toggle-view-layout-entry', entryName => { + ipcRenderer.on('mt::toggle-view-layout-entry', (event, entryName) => { + commit('TOGGLE_LAYOUT_ENTRY', entryName) + dispatch('DISPATCH_LAYOUT_MENU_ITEMS') + }) + + bus.$on('view:toggle-layout-entry', entryName => { commit('TOGGLE_LAYOUT_ENTRY', entryName) - const item = {} - item[entryName] = state[entryName] const { windowId } = global.marktext.env - ipcRenderer.send('mt::view-layout-changed', windowId, item) + ipcRenderer.send('mt::view-layout-changed', windowId, { [entryName]: state[entryName] }) }) }, - LISTEN_FOR_REQUEST_LAYOUT ({ dispatch }) { - ipcRenderer.on('mt::request-for-view-layout', () => { - dispatch('SET_LAYOUT_MENU_ITEM') - }) - }, - SET_LAYOUT_MENU_ITEM ({ state }) { + + DISPATCH_LAYOUT_MENU_ITEMS ({ state }) { const { windowId } = global.marktext.env const { showTabBar, showSideBar } = state ipcRenderer.send('mt::view-layout-changed', windowId, { showTabBar, showSideBar }) }, + CHANGE_SIDE_BAR_WIDTH ({ commit }, width) { commit('SET_SIDE_BAR_WIDTH', width) } diff --git a/src/renderer/store/listenForMain.js b/src/renderer/store/listenForMain.js index aa4100cd..82820f13 100644 --- a/src/renderer/store/listenForMain.js +++ b/src/renderer/store/listenForMain.js @@ -1,7 +1,6 @@ import { ipcRenderer } from 'electron' import bus from '../bus' -// messages from main process, and do not change the state const state = {} const getters = {} @@ -21,15 +20,6 @@ const actions = { }) }, - LISTEN_FOR_VIEW ({ commit }) { - ipcRenderer.on('mt::editor-change-view', (e, data) => { - commit('SET_MODE', data) - }) - ipcRenderer.on('mt::show-command-palette', () => { - bus.$emit('show-command-palette') - }) - }, - LISTEN_FOR_SHOW_DIALOG ({ commit }) { ipcRenderer.on('mt::about-dialog', e => { bus.$emit('aboutDialog') diff --git a/src/renderer/store/preferences.js b/src/renderer/store/preferences.js index c83d7bcb..209b0aa8 100644 --- a/src/renderer/store/preferences.js +++ b/src/renderer/store/preferences.js @@ -117,7 +117,7 @@ const mutations = { } const actions = { - ASK_FOR_USER_PREFERENCE ({ commit, state, rootState }) { + ASK_FOR_USER_PREFERENCE ({ commit }) { ipcRenderer.send('mt::ask-for-user-preference') ipcRenderer.send('mt::ask-for-user-data') @@ -143,15 +143,27 @@ const actions = { ipcRenderer.send('mt::select-default-directory-to-open') }, + LISTEN_FOR_VIEW ({ commit, dispatch }) { + ipcRenderer.on('mt::show-command-palette', () => { + bus.$emit('show-command-palette') + }) + ipcRenderer.on('mt::toggle-view-mode-entry', (event, entryName) => { + commit('TOGGLE_VIEW_MODE', entryName) + dispatch('DISPATCH_EDITOR_VIEW_STATE', { [entryName]: state[entryName] }) + }) + }, + // Toggle a view option and notify main process to toggle menu item. - LISTEN_TOGGLE_VIEW ({ commit, state }) { + LISTEN_TOGGLE_VIEW ({ commit, dispatch, state }) { bus.$on('view:toggle-view-entry', entryName => { commit('TOGGLE_VIEW_MODE', entryName) - const item = {} - item[entryName] = state[entryName] - const { windowId } = global.marktext.env - ipcRenderer.send('mt::view-layout-changed', windowId, item) + dispatch('DISPATCH_EDITOR_VIEW_STATE', { [entryName]: state[entryName] }) }) + }, + + DISPATCH_EDITOR_VIEW_STATE (_, viewState) { + const { windowId } = global.marktext.env + ipcRenderer.send('mt::view-layout-changed', windowId, viewState) } } diff --git a/src/renderer/store/project.js b/src/renderer/store/project.js index bdd8845d..6cc70f85 100644 --- a/src/renderer/store/project.js +++ b/src/renderer/store/project.js @@ -82,7 +82,7 @@ const actions = { showSideBar: true, showTabBar: true }) - dispatch('SET_LAYOUT_MENU_ITEM') + dispatch('DISPATCH_LAYOUT_MENU_ITEMS') }) }, LISTEN_FOR_UPDATE_PROJECT ({ commit, state, dispatch }) {