fix #348: configurable system-wide shortcuts

This commit is contained in:
antelle 2019-09-14 16:36:30 +02:00
parent 3859beb543
commit 56d84e1d18
15 changed files with 299 additions and 77 deletions

View File

@ -291,6 +291,9 @@ const Launcher = {
} else {
this.pendingFileToOpen = file;
}
},
setGlobalShortcuts(appSettings) {
this.remoteApp().setGlobalShortcuts(appSettings);
}
};

View File

@ -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(
'{}',
'<code>' + FeatureDetector.actionShortcutSymbol() + 'V</code>'
'<code>' + Shortcuts.actionShortcutSymbol() + 'V</code>'
);
OtpQrReader.startListenClipoard();
const buttons = [

View File

@ -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
? '<span class="thin">ctrl + </span>'
: 'ctrl+';
},
altShortcutSymbol(formatting) {
return FeatureDetector.isMac
? '⌥'
: formatting
? '<span class="thin">alt + </span>'
: 'alt+';
},
shiftShortcutSymbol(formatting) {
return FeatureDetector.isMac
? '⇧'
: formatting
? '<span class="thin">shift + </span>'
: 'shift+';
},
ctrlShortcutSymbol(formatting) {
return FeatureDetector.isMac
? '⌃'
: formatting
? '<span class="thin">ctrl + </span>'
: '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;

View File

@ -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.",

View File

@ -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 ? '<span class="thin">ctrl + </span>' : 'ctrl-';
},
altShortcutSymbol(formatting) {
return this.isMac ? '⌥' : formatting ? '<span class="thin">alt + </span>' : 'alt-';
},
shiftShortcutSymbol(formatting) {
return this.isMac ? '⇧' : formatting ? '<span class="thin">shift + </span>' : 'shift-';
},
globalShortcutSymbol(formatting) {
return this.isMac
? '⌃⌥'
: formatting
? '<span class="thin">shift+alt+</span>'
: '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;
},

View File

@ -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();

View File

@ -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]) {

View File

@ -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,

View File

@ -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({
: ' <span class="muted-color">(' +
Locale.searchShiftClickOr +
' ' +
FeatureDetector.altShortcutSymbol(true) +
Shortcuts.altShortcutSymbol(true) +
'N)</span>';
this.createOptions = [
{ value: 'entry', icon: 'key', text: Format.capFirst(Locale.entry) + entryDesc },

View File

@ -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'),

View File

@ -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 = $('<div/>').addClass('shortcut__editor');
$('<div/>')
.text(Locale.setShEdit)
.appendTo(shortcutEditor);
const shortcutEditorInput = $('<input/>')
.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();
}
});
}
});

View File

@ -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 {

View File

@ -9,7 +9,7 @@
<div><span class="shortcut">{{{cmd}}}B</span> {{res 'setShCopyUser'}}</div>
<div><span class="shortcut">{{{cmd}}}U</span> {{res 'setShCopyUrl'}}</div>
{{#if autoTypeSupported}}
<div><span class="shortcut">{{{cmd}}}T</span> {{res 'setShAutoType'}}</div>
<div><span class="shortcut">{{{cmd}}}T</span> {{res 'setShAutoType'}}</div>
{{/if}}
<div><span class="shortcut">&uarr;</span> {{res 'setShPrev'}}</div>
<div><span class="shortcut">&darr;</span> {{res 'setShNext'}}</div>
@ -19,10 +19,14 @@
<div><span class="shortcut">{{{cmd}}}G</span> {{res 'setShGen'}}</div>
<div><span class="shortcut">{{{cmd}}},</span> {{res 'setShSet'}}</div>
<div><span class="shortcut">{{{cmd}}}L</span> {{res 'setShLock'}}</div>
{{#if globalShortcutsSupported}}
<div><span class="shortcut {{#if globalIsLarge}}shortcut-large{{/if}}">{{{global}}}C</span> {{res 'setShCopyPassGlobal'}}</div>
<div><span class="shortcut {{#if globalIsLarge}}shortcut-large{{/if}}">{{{global}}}B</span> {{res 'setShCopyUserGlobal'}}</div>
<div><span class="shortcut {{#if globalIsLarge}}shortcut-large{{/if}}">{{{global}}}U</span> {{res 'setShCopyUrlGlobal'}}</div>
<div><span class="shortcut {{#if globalIsLarge}}shortcut-large{{/if}}">{{{global}}}T</span> {{res 'setShAutoTypeGlobal'}}</div>
{{#if globalShortcuts}}
<div><button class="shortcut btn-silent {{#if globalIsLarge}}shortcut-large{{/if}}"
data-shortcut="copyPassword">{{{globalShortcuts.copyPassword}}}</button> {{res 'setShCopyPassGlobal'}}</div>
<div><button class="shortcut btn-silent {{#if globalIsLarge}}shortcut-large{{/if}}"
data-shortcut="copyUser">{{{globalShortcuts.copyUser}}}</button> {{res 'setShCopyUserGlobal'}}</div>
<div><button class="shortcut btn-silent {{#if globalIsLarge}}shortcut-large{{/if}}"
data-shortcut="copyUrl">{{{globalShortcuts.copyUrl}}}</button> {{res 'setShCopyUrlGlobal'}}</div>
<div><button class="shortcut btn-silent {{#if globalIsLarge}}shortcut-large{{/if}}"
data-shortcut="autoType">{{{globalShortcuts.autoType}}}</button> {{res 'setShAutoTypeGlobal'}}</div>
{{/if}}
</div>

View File

@ -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() {

View File

@ -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