From 56d84e1d18940f358832f155d1bbaf3df8fbc610 Mon Sep 17 00:00:00 2001 From: antelle Date: Sat, 14 Sep 2019 16:36:30 +0200 Subject: [PATCH] fix #348: configurable system-wide shortcuts --- app/scripts/comp/launcher-electron.js | 3 + app/scripts/comp/otp-qr-reader.js | 5 +- app/scripts/comp/shortcuts.js | 147 ++++++++++++++++++ app/scripts/locales/base.json | 1 + app/scripts/util/feature-detector.js | 34 ---- .../views/auto-type/auto-type-select-view.js | 8 +- .../views/details/details-attachment-view.js | 4 +- .../views/details/details-auto-type-view.js | 6 +- app/scripts/views/list-search-view.js | 3 +- .../views/settings/settings-file-view.js | 4 +- .../views/settings/settings-shortcuts-view.js | 92 ++++++++++- app/styles/areas/_settings.scss | 19 ++- app/templates/settings/settings-shortcuts.hbs | 16 +- desktop/app.js | 33 ++-- release-notes.md | 1 + 15 files changed, 299 insertions(+), 77 deletions(-) create mode 100644 app/scripts/comp/shortcuts.js diff --git a/app/scripts/comp/launcher-electron.js b/app/scripts/comp/launcher-electron.js index 34b3f312..5cac3a79 100644 --- a/app/scripts/comp/launcher-electron.js +++ b/app/scripts/comp/launcher-electron.js @@ -291,6 +291,9 @@ const Launcher = { } else { this.pendingFileToOpen = file; } + }, + setGlobalShortcuts(appSettings) { + this.remoteApp().setGlobalShortcuts(appSettings); } }; diff --git a/app/scripts/comp/otp-qr-reader.js b/app/scripts/comp/otp-qr-reader.js index 6948faf1..591bdf6f 100644 --- a/app/scripts/comp/otp-qr-reader.js +++ b/app/scripts/comp/otp-qr-reader.js @@ -3,6 +3,7 @@ const Alerts = require('./alerts'); const Locale = require('../util/locale'); const Logger = require('../util/logger'); const FeatureDetector = require('../util/feature-detector'); +const Shortcuts = require('../comp/shortcuts'); const Otp = require('../util/otp'); const QrCode = require('jsqrcode'); @@ -14,7 +15,7 @@ const OtpQrReader = { fileInput: null, read() { - let screenshotKey = FeatureDetector.screenshotToClipboardShortcut(); + let screenshotKey = Shortcuts.screenshotToClipboardShortcut(); if (screenshotKey) { screenshotKey = Locale.detSetupOtpAlertBodyWith.replace( '{}', @@ -25,7 +26,7 @@ const OtpQrReader = { ? '' : Locale.detSetupOtpAlertBodyWith.replace( '{}', - '' + FeatureDetector.actionShortcutSymbol() + 'V' + '' + Shortcuts.actionShortcutSymbol() + 'V' ); OtpQrReader.startListenClipoard(); const buttons = [ diff --git a/app/scripts/comp/shortcuts.js b/app/scripts/comp/shortcuts.js new file mode 100644 index 00000000..da41c2ff --- /dev/null +++ b/app/scripts/comp/shortcuts.js @@ -0,0 +1,147 @@ +const FeatureDetector = require('../util/feature-detector'); +const Keys = require('../const/keys'); +const Format = require('../util/format'); +const AppSettingsModel = require('../models/app-settings-model'); +const Launcher = require('./launcher'); + +let allowedKeys; + +function getAllowedKeys() { + if (!allowedKeys) { + allowedKeys = {}; + for (const [name, code] of Object.entries(Keys)) { + const keyName = name.replace('DOM_VK_', ''); + if (/^([0-9A-Z]|F\d{1,2})$/.test(keyName)) { + allowedKeys[code] = keyName; + } + } + } + return allowedKeys; +} + +const globalShortcuts = { + copyPassword: { mac: 'Ctrl+Alt+C', all: 'Shift+Alt+C' }, + copyUser: { mac: 'Ctrl+Alt+B', all: 'Shift+Alt+B' }, + copyUrl: { mac: 'Ctrl+Alt+U', all: 'Shift+Alt+U' }, + autoType: { mac: 'Ctrl+Alt+T', all: 'Shift+Alt+T' } +}; + +const Shortcuts = { + keyEventToShortcut(event) { + const modifiers = []; + if (event.ctrlKey) { + modifiers.push('Ctrl'); + } + if (event.altKey) { + modifiers.push('Alt'); + } + if (event.shiftKey) { + modifiers.push('Shift'); + } + if (FeatureDetector.isMac && event.metaKey) { + modifiers.push('Meta'); + } + const keyName = getAllowedKeys()[event.which]; + return { + value: modifiers.join('+') + '+' + (keyName || '…'), + valid: modifiers.length > 0 && !!keyName + }; + }, + presentShortcut(shortcutValue, formatting) { + return shortcutValue + .split(/\+/g) + .map(part => { + switch (part) { + case 'Ctrl': + return this.ctrlShortcutSymbol(formatting); + case 'Alt': + return this.altShortcutSymbol(formatting); + case 'Shift': + return this.shiftShortcutSymbol(formatting); + case 'Meta': + return this.actionShortcutSymbol(formatting); + default: + return part; + } + }) + .join(''); + }, + actionShortcutSymbol(formatting) { + return FeatureDetector.isMac + ? '⌘' + : formatting + ? 'ctrl + ' + : 'ctrl+'; + }, + altShortcutSymbol(formatting) { + return FeatureDetector.isMac + ? '⌥' + : formatting + ? 'alt + ' + : 'alt+'; + }, + shiftShortcutSymbol(formatting) { + return FeatureDetector.isMac + ? '⇧' + : formatting + ? 'shift + ' + : 'shift+'; + }, + ctrlShortcutSymbol(formatting) { + return FeatureDetector.isMac + ? '⌃' + : formatting + ? 'ctrl + ' + : 'ctrl+'; + }, + globalShortcutText(type, formatting) { + return this.presentShortcut(this.globalShortcut(type), formatting); + }, + globalShortcut(type) { + const appSettingsShortcut = AppSettingsModel.instance.get( + this.globalShortcutAppSettingsKey(type) + ); + if (appSettingsShortcut) { + return appSettingsShortcut; + } + const globalShortcut = globalShortcuts[type]; + if (globalShortcut) { + if (FeatureDetector.isMac && globalShortcut.mac) { + return globalShortcut.mac; + } + return globalShortcut.all; + } + return undefined; + }, + setGlobalShortcut(type, value) { + if (!globalShortcuts[type]) { + throw new Error('Bad shortcut: ' + type); + } + if (value) { + AppSettingsModel.instance.set(this.globalShortcutAppSettingsKey(type), value); + } else { + AppSettingsModel.instance.unset(this.globalShortcutAppSettingsKey(type)); + } + Launcher.setGlobalShortcuts(AppSettingsModel.instance.attributes); + }, + globalShortcutAppSettingsKey(type) { + return 'globalShortcut' + Format.capFirst(type); + }, + screenshotToClipboardShortcut() { + if (FeatureDetector.isiOS) { + return 'Sleep+Home'; + } + if (FeatureDetector.isMobile) { + return ''; + } + if (FeatureDetector.isMac) { + return 'Command-Shift-Control-4'; + } + if (FeatureDetector.isWindows) { + return 'Alt+PrintScreen'; + } + return ''; + } +}; + +module.exports = Shortcuts; diff --git a/app/scripts/locales/base.json b/app/scripts/locales/base.json index 800f02e0..f4f02e3a 100644 --- a/app/scripts/locales/base.json +++ b/app/scripts/locales/base.json @@ -513,6 +513,7 @@ "setShCopyUrlGlobal": "copy website (when the app is in background)", "setShAutoTypeGlobal": "auto-type (when the app is in background)", "setShLock": "lock database", + "setShEdit": "Press a new key combination to set it as shortcut", "setPlInstallTitle": "Install new plugins", "setPlInstallDesc": "KeeWeb plugins add features, themes, and languages to KeeWeb. Plugins run with the same privileges as KeeWeb, they can access and manage all your passwords. Never install plugins you don't trust.", diff --git a/app/scripts/util/feature-detector.js b/app/scripts/util/feature-detector.js index 76a2fd29..8bca393d 100644 --- a/app/scripts/util/feature-detector.js +++ b/app/scripts/util/feature-detector.js @@ -17,40 +17,6 @@ const FeatureDetector = { !/^http(s?):\/\/((localhost:8085)|((app|beta)\.keeweb\.info))/.test(location.href), needFixClicks: /Edge\/14/.test(navigator.appVersion), - actionShortcutSymbol(formatting) { - return this.isMac ? '⌘' : formatting ? 'ctrl + ' : 'ctrl-'; - }, - altShortcutSymbol(formatting) { - return this.isMac ? '⌥' : formatting ? 'alt + ' : 'alt-'; - }, - shiftShortcutSymbol(formatting) { - return this.isMac ? '⇧' : formatting ? 'shift + ' : 'shift-'; - }, - globalShortcutSymbol(formatting) { - return this.isMac - ? '⌃⌥' - : formatting - ? 'shift+alt+' - : 'shift-alt-'; - }, - globalShortcutIsLarge() { - return !this.isMac; - }, - screenshotToClipboardShortcut() { - if (this.isiOS) { - return 'Sleep+Home'; - } - if (this.isMobile) { - return ''; - } - if (this.isMac) { - return 'Command-Shift-Control-4'; - } - if (this.isWindows) { - return 'Alt+PrintScreen'; - } - return ''; - }, supportsTitleBarStyles() { return this.isMac; }, diff --git a/app/scripts/views/auto-type/auto-type-select-view.js b/app/scripts/views/auto-type/auto-type-select-view.js index 95dbe491..a4a2f429 100644 --- a/app/scripts/views/auto-type/auto-type-select-view.js +++ b/app/scripts/views/auto-type/auto-type-select-view.js @@ -5,7 +5,7 @@ const Locale = require('../../util/locale'); const AppSettingsModel = require('../../models/app-settings-model'); const EntryPresenter = require('../../presenters/entry-presenter'); const Scrollable = require('../../mixins/scrollable'); -const FeatureDetector = require('../../util/feature-detector'); +const Shortcuts = require('../../comp/shortcuts'); const DropdownView = require('../dropdown-view'); const Format = require('../../util/format'); @@ -108,9 +108,9 @@ const AutoTypePopupView = Backbone.View.extend({ filterText: this.model.filter.text, topMessage, itemsHtml, - actionSymbol: FeatureDetector.actionShortcutSymbol(true), - altSymbol: FeatureDetector.altShortcutSymbol(true), - shiftSymbol: FeatureDetector.shiftShortcutSymbol(true), + actionSymbol: Shortcuts.actionShortcutSymbol(true), + altSymbol: Shortcuts.altShortcutSymbol(true), + shiftSymbol: Shortcuts.shiftShortcutSymbol(true), keyEnter: Locale.keyEnter }); document.activeElement.blur(); diff --git a/app/scripts/views/details/details-attachment-view.js b/app/scripts/views/details/details-attachment-view.js index eca1179b..e2d8ac75 100644 --- a/app/scripts/views/details/details-attachment-view.js +++ b/app/scripts/views/details/details-attachment-view.js @@ -1,5 +1,5 @@ const Backbone = require('backbone'); -const FeatureDetector = require('../../util/feature-detector'); +const Shortcuts = require('../../comp/shortcuts'); const DetailsAttachmentView = Backbone.View.extend({ template: require('templates/details/details-attachment.hbs'), @@ -9,7 +9,7 @@ const DetailsAttachmentView = Backbone.View.extend({ render(complete) { this.renderTemplate({}, true); const shortcut = this.$el.find('.details__attachment-preview-download-text-shortcut'); - shortcut.html(FeatureDetector.actionShortcutSymbol(false)); + shortcut.html(Shortcuts.actionShortcutSymbol(false)); const blob = new Blob([this.model.getBinary()], { type: this.model.mimeType }); const dataEl = this.$el.find('.details__attachment-preview-data'); switch ((this.model.mimeType || '').split('/')[0]) { diff --git a/app/scripts/views/details/details-auto-type-view.js b/app/scripts/views/details/details-auto-type-view.js index 33bc5d37..1d90d986 100644 --- a/app/scripts/views/details/details-auto-type-view.js +++ b/app/scripts/views/details/details-auto-type-view.js @@ -1,7 +1,7 @@ const Backbone = require('backbone'); const AutoTypeHintView = require('../auto-type-hint-view'); const Locale = require('../../util/locale'); -const FeatureDetector = require('../../util/feature-detector'); +const Shortcuts = require('../../comp/shortcuts'); const AutoType = require('../../auto-type'); const DetailsAutoTypeView = Backbone.View.extend({ @@ -22,8 +22,8 @@ const DetailsAutoTypeView = Backbone.View.extend({ render() { const detAutoTypeShortcutsDesc = Locale.detAutoTypeShortcutsDesc - .replace('{}', FeatureDetector.actionShortcutSymbol() + 'T') - .replace('{}', FeatureDetector.globalShortcutSymbol() + 'T'); + .replace('{}', Shortcuts.actionShortcutSymbol() + 'T') + .replace('{}', Shortcuts.globalShortcutText('autoType')); this.renderTemplate({ enabled: this.model.getEffectiveEnableAutoType(), obfuscation: this.model.autoTypeObfuscation, diff --git a/app/scripts/views/list-search-view.js b/app/scripts/views/list-search-view.js index 79911134..9e2254bf 100644 --- a/app/scripts/views/list-search-view.js +++ b/app/scripts/views/list-search-view.js @@ -3,6 +3,7 @@ const Keys = require('../const/keys'); const KeyHandler = require('../comp/key-handler'); const DropdownView = require('./dropdown-view'); const FeatureDetector = require('../util/feature-detector'); +const Shortcuts = require('../comp/shortcuts'); const Format = require('../util/format'); const Locale = require('../util/locale'); const Comparators = require('../util/comparators'); @@ -139,7 +140,7 @@ const ListSearchView = Backbone.View.extend({ : ' (' + Locale.searchShiftClickOr + ' ' + - FeatureDetector.altShortcutSymbol(true) + + Shortcuts.altShortcutSymbol(true) + 'N)'; this.createOptions = [ { value: 'entry', icon: 'key', text: Format.capFirst(Locale.entry) + entryDesc }, diff --git a/app/scripts/views/settings/settings-file-view.js b/app/scripts/views/settings/settings-file-view.js index 48a2872d..4a333fd8 100644 --- a/app/scripts/views/settings/settings-file-view.js +++ b/app/scripts/views/settings/settings-file-view.js @@ -1,6 +1,6 @@ const Backbone = require('backbone'); const OpenConfigView = require('../open-config-view'); -const FeatureDetector = require('../../util/feature-detector'); +const Shortcuts = require('../../comp/shortcuts'); const PasswordGenerator = require('../../util/password-generator'); const Alerts = require('../../comp/alerts'); const Launcher = require('../../comp/launcher'); @@ -84,7 +84,7 @@ const SettingsFileView = Backbone.View.extend({ storageProviders.sort((x, y) => (x.uipos || Infinity) - (y.uipos || Infinity)); const backup = this.model.get('backup'); this.renderTemplate({ - cmd: FeatureDetector.actionShortcutSymbol(true), + cmd: Shortcuts.actionShortcutSymbol(true), supportFiles: !!Launcher, desktopLink: Links.Desktop, name: this.model.get('name'), diff --git a/app/scripts/views/settings/settings-shortcuts-view.js b/app/scripts/views/settings/settings-shortcuts-view.js index 9508030a..7dc0cd7f 100644 --- a/app/scripts/views/settings/settings-shortcuts-view.js +++ b/app/scripts/views/settings/settings-shortcuts-view.js @@ -1,18 +1,98 @@ const Backbone = require('backbone'); +const Locale = require('../../util/locale'); +const Keys = require('../../const/keys'); const Launcher = require('../../comp/launcher'); +const Shortcuts = require('../../comp/shortcuts'); const FeatureDetector = require('../../util/feature-detector'); const SettingsShortcutsView = Backbone.View.extend({ template: require('templates/settings/settings-shortcuts.hbs'), + systemShortcuts: [ + 'Meta+A', + 'Alt+A', + 'Alt+C', + 'Alt+D', + 'Meta+F', + 'Meta+C', + 'Meta+B', + 'Meta+U', + 'Meta+T', + 'Alt+N', + 'Meta+O', + 'Meta+S', + 'Meta+G', + 'Meta+,', + 'Meta+L' + ], + + events: { + 'click button.shortcut': 'shortcutClick' + }, + render() { this.renderTemplate({ - cmd: FeatureDetector.actionShortcutSymbol(true), - alt: FeatureDetector.altShortcutSymbol(true), - global: FeatureDetector.globalShortcutSymbol(true), - globalIsLarge: FeatureDetector.globalShortcutIsLarge(), - globalShortcutsSupported: !!Launcher, - autoTypeSupported: !!Launcher + cmd: Shortcuts.actionShortcutSymbol(true), + alt: Shortcuts.altShortcutSymbol(true), + globalIsLarge: !FeatureDetector.isMac, + autoTypeSupported: !!Launcher, + globalShortcuts: Launcher + ? { + copyPassword: Shortcuts.globalShortcutText('copyPassword', true), + copyUser: Shortcuts.globalShortcutText('copyUser', true), + copyUrl: Shortcuts.globalShortcutText('copyUrl', true), + autoType: Shortcuts.globalShortcutText('autoType', true) + } + : undefined + }); + }, + + shortcutClick(e) { + const globalShortcutType = e.target.dataset.shortcut; + + const shortcutEditor = $('
').addClass('shortcut__editor'); + $('
') + .text(Locale.setShEdit) + .appendTo(shortcutEditor); + const shortcutEditorInput = $('') + .addClass('shortcut__editor-input') + .val(Shortcuts.globalShortcutText(globalShortcutType)) + .appendTo(shortcutEditor); + if (!FeatureDetector.isMac) { + shortcutEditorInput.addClass('shortcut__editor-input--large'); + } + + shortcutEditor.insertAfter($(e.target).parent()); + shortcutEditorInput.focus(); + shortcutEditorInput.on('blur', () => shortcutEditor.remove()); + shortcutEditorInput.on('keypress', e => e.preventDefault()); + shortcutEditorInput.on('keydown', e => { + e.preventDefault(); + e.stopImmediatePropagation(); + + if (e.which === Keys.DOM_VK_DELETE || e.which === Keys.DOM_VK_BACK_SPACE) { + Shortcuts.setGlobalShortcut(globalShortcutType, undefined); + this.render(); + return; + } + if (e.which === Keys.DOM_VK_ESCAPE) { + shortcutEditorInput.blur(); + return; + } + + const shortcut = Shortcuts.keyEventToShortcut(e); + const presentableShortcutText = Shortcuts.presentShortcut(shortcut.value); + + shortcutEditorInput.val(presentableShortcutText); + + const exists = this.systemShortcuts.includes(shortcut.text); + shortcutEditorInput.toggleClass('input--error', exists); + + const isValid = shortcut.valid && !exists; + if (isValid) { + Shortcuts.setGlobalShortcut(globalShortcutType, shortcut.value); + this.render(); + } }); } }); diff --git a/app/styles/areas/_settings.scss b/app/styles/areas/_settings.scss index 5993c4dc..2197bf95 100644 --- a/app/styles/areas/_settings.scss +++ b/app/styles/areas/_settings.scss @@ -26,16 +26,31 @@ border: 1px solid var(--muted-color); display: inline-block; border-radius: $base-border-radius; - width: 40px; + width: 8em; text-align: center; padding: $base-padding; margin: 0 $base-padding-h $base-padding-v $base-padding-h; + line-height: 1.5em; + min-width: unset; + box-sizing: border-box; + vertical-align: baseline; &-large { - width: 80px; + width: 12em; } &:first-of-type { margin-left: 0; } + &__editor { + margin-bottom: $base-padding-v; + &-input { + text-align: center; + margin: $base-padding-v 0 $medium-padding-v; + width: 15em; + &--large { + width: 30em; + } + } + } } &__back-button { diff --git a/app/templates/settings/settings-shortcuts.hbs b/app/templates/settings/settings-shortcuts.hbs index be84d5bb..023b763a 100644 --- a/app/templates/settings/settings-shortcuts.hbs +++ b/app/templates/settings/settings-shortcuts.hbs @@ -9,7 +9,7 @@
{{{cmd}}}B {{res 'setShCopyUser'}}
{{{cmd}}}U {{res 'setShCopyUrl'}}
{{#if autoTypeSupported}} -
{{{cmd}}}T {{res 'setShAutoType'}}
+
{{{cmd}}}T {{res 'setShAutoType'}}
{{/if}}
{{res 'setShPrev'}}
{{res 'setShNext'}}
@@ -19,10 +19,14 @@
{{{cmd}}}G {{res 'setShGen'}}
{{{cmd}}}, {{res 'setShSet'}}
{{{cmd}}}L {{res 'setShLock'}}
- {{#if globalShortcutsSupported}} -
{{{global}}}C {{res 'setShCopyPassGlobal'}}
-
{{{global}}}B {{res 'setShCopyUserGlobal'}}
-
{{{global}}}U {{res 'setShCopyUrlGlobal'}}
-
{{{global}}}T {{res 'setShAutoTypeGlobal'}}
+ {{#if globalShortcuts}} +
{{res 'setShCopyPassGlobal'}}
+
{{res 'setShCopyUserGlobal'}}
+
{{res 'setShCopyUrlGlobal'}}
+
{{res 'setShAutoTypeGlobal'}}
{{/if}}
diff --git a/desktop/app.js b/desktop/app.js index a6858820..7ec1d0f9 100644 --- a/desktop/app.js +++ b/desktop/app.js @@ -65,10 +65,11 @@ app.on('window-all-closed', () => { }); app.on('ready', () => { appReady = true; + const appSettings = readAppSettings() || {}; setAppOptions(); setSystemAppearance(); - createMainWindow(); - setGlobalShortcuts(); + createMainWindow(appSettings); + setGlobalShortcuts(appSettings); subscribePowerEvents(); deleteOldTempFiles(); hookRequestHeaders(); @@ -135,6 +136,7 @@ app.getMainWindow = function() { return mainWindow; }; app.emitBackboneEvent = emitBackboneEvent; +app.setGlobalShortcuts = setGlobalShortcuts; function setAppOptions() { app.commandLine.appendSwitch('disable-background-timer-throttling'); @@ -156,8 +158,7 @@ function setSystemAppearance() { } } -function createMainWindow() { - const appSettings = readAppSettings() || {}; +function createMainWindow(appSettings) { const isMacDarkTheme = appSettings.theme === 'macdark'; const windowOptions = { show: false, @@ -396,23 +397,25 @@ function notifyOpenFile() { } } -function setGlobalShortcuts() { - const shortcutModifiers = process.platform === 'darwin' ? 'Ctrl+Alt+' : 'Shift+Alt+'; - const shortcuts = { - C: 'copy-password', - B: 'copy-user', - U: 'copy-url', - T: 'auto-type' +function setGlobalShortcuts(appSettings) { + const defaultShortcutModifiers = process.platform === 'darwin' ? 'Ctrl+Alt+' : 'Shift+Alt+'; + const defaultShortcuts = { + CopyPassword: { shortcut: defaultShortcutModifiers + 'C', event: 'copy-password' }, + CopyUser: { shortcut: defaultShortcutModifiers + 'B', event: 'copy-user' }, + CopyUrl: { shortcut: defaultShortcutModifiers + 'U', event: 'copy-url' }, + AutoType: { shortcut: defaultShortcutModifiers + 'T', event: 'auto-type' } }; - Object.keys(shortcuts).forEach(key => { - const shortcut = shortcutModifiers + key; - const eventName = shortcuts[key]; + electron.globalShortcut.unregisterAll(); + for (const [key, shortcutDef] of Object.entries(defaultShortcuts)) { + const fromSettings = appSettings[`globalShortcut${key}`]; + const shortcut = fromSettings || shortcutDef.shortcut; + const eventName = shortcutDef.event; try { electron.globalShortcut.register(shortcut, () => { emitBackboneEvent(eventName); }); } catch (e) {} - }); + } } function subscribePowerEvents() { diff --git a/release-notes.md b/release-notes.md index 120a22ec..3ce21227 100644 --- a/release-notes.md +++ b/release-notes.md @@ -6,6 +6,7 @@ Release notes `+` #1243: auto-type any field `+` #1255: file format version and kdf selection in settings `*` #502: increased the default value of encryption rounds +`+` #348: configurable system-wide shortcuts `*` devtools are now opened with alt-cmd-I `-` fix #764: multiple attachments display `-` fix multi-line fields display in history