Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Desktop: Keyboard shortcut editor #3525

Merged
merged 53 commits into from
Sep 6, 2020
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
f09a896
Blank config screen
Jul 10, 2020
4763157
Basic UI structure
Jul 18, 2020
2b8bf90
Initial keyboard shortcut editing workflow
Jul 20, 2020
5ba4c90
Remove KeymapUtils.ts in favor of including domToElectronKey, domToEl…
Jul 24, 2020
66c5375
Refactor into subfolders
Jul 25, 2020
5a46b5d
Improve styling
Aug 2, 2020
cd5d574
Merge remote-tracking branch 'upstream/dev' into keyboard-shortcut-ed…
Aug 4, 2020
0f91db7
Merge remote-tracking branch 'upstream/dev' into keyboard-shortcut-ed…
Aug 4, 2020
8440a9c
Integrate with KeymapService
Aug 14, 2020
3465af9
Merge remote-tracking branch 'upstream/dev' into keyboard-shortcut-ed…
Aug 14, 2020
fa08c28
Make compatible with macOS
Aug 15, 2020
57a68b9
Fix Goto Anything shortcut not updating
Aug 15, 2020
de493e6
Simple filtering
Aug 15, 2020
51bc13f
Fix false warning when disabling a shortcut
Aug 15, 2020
52dbc6e
Fix command labels not translating
Aug 20, 2020
0b309d9
Rename property theme to themeId
Aug 20, 2020
8ca3aa0
Use handlers
Aug 21, 2020
42c7122
Refresh menu by emitting an event
Aug 21, 2020
1e63636
Wrap content in div
Aug 21, 2020
264dd21
Remove unneccesary parameter
Aug 21, 2020
5294b68
Accessibility label
Aug 21, 2020
c36f805
Remove interface extend
Aug 21, 2020
88122ab
Change label "Reset to default"
Aug 21, 2020
d98878d
Use shim.fsDriver() for file operations
Aug 21, 2020
9758161
Rename getCommands() to getCommandNames()
Aug 21, 2020
1b6ac9f
Custom hook useKeymap
Aug 21, 2020
a12f4a1
Minor refactoring
Aug 21, 2020
3005f58
Remove indirect method calls
Aug 22, 2020
8a70a97
Rename state variable to make the code clear
Aug 22, 2020
cda1a8f
Pass data as an event object to handler functions
Aug 22, 2020
d2aa3f9
Enable options parameter overriding on CommandService.commandByName()…
Aug 22, 2020
c86d97d
Implement error class KeymapError (For highlighting invalid keymap en…
Aug 22, 2020
4e3b675
Cancel button
Aug 22, 2020
670075f
Better errors; Improve styling
Aug 23, 2020
06723da
Remove obsolete tests
Aug 23, 2020
374ddc3
Fix typo in error message
Aug 23, 2020
3252dde
Tweak styling
Aug 23, 2020
d2b346d
Re-enable clearing the shortcut
Aug 23, 2020
206bd48
Keep only one shortcut recorder open at once
Aug 23, 2020
3568c70
Merge remote-tracking branch 'upstream/dev' into keyboard-shortcut-ed…
Aug 23, 2020
43b2bc1
Minor refactoring
Aug 25, 2020
e274acd
Improve UI/UX
Aug 27, 2020
bc36748
Minor refactoring
Aug 28, 2020
989b3d2
Fix not being allowed to disable shortcuts
Aug 28, 2020
6ab5ccc
Import/Export keymap configuration
Aug 30, 2020
471210f
Refactor and code clean-up
Aug 30, 2020
d5732ff
Add label for Search
Aug 30, 2020
8ca20ad
Remove unneccessary translated string
Aug 30, 2020
e6643b9
Remove unnecessary method from useKeymap hook in favor of directly ca…
Aug 30, 2020
a6d793b
Add tooltips
Sep 1, 2020
80b831d
Fix a bug where it failed to find both of duplicate keymap items
Sep 1, 2020
88f5300
Remove search label
Sep 5, 2020
0bd89c9
Merge branch 'dev' into keyboard-shortcut-editor
Sep 5, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ Modules/TinyMCE/langs/
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
CliClient/app/LinkSelector.js
CliClient/build/LinkSelector.js
CliClient/tests-build/synchronizer_LockHandler.js
CliClient/tests-build/synchronizer_MigrationHandler.js
CliClient/tests/synchronizer_LockHandler.js
CliClient/tests/synchronizer_MigrationHandler.js
ElectronClient/commands/focusElement.js
Expand All @@ -71,6 +73,12 @@ ElectronClient/commands/stopExternalEditing.js
ElectronClient/global.d.js
ElectronClient/gui/ErrorBoundary.js
ElectronClient/gui/Header/commands/focusSearch.js
ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js
ElectronClient/gui/KeymapConfig/ShortcutRecorder.js
ElectronClient/gui/KeymapConfig/styles/index.js
ElectronClient/gui/KeymapConfig/utils/getLabel.js
ElectronClient/gui/KeymapConfig/utils/useEditing.js
ElectronClient/gui/KeymapConfig/utils/useKeymap.js
ElectronClient/gui/MainScreen/commands/editAlarm.js
ElectronClient/gui/MainScreen/commands/exportPdf.js
ElectronClient/gui/MainScreen/commands/hideModalMessage.js
Expand Down
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ Tools/commit_hook.txt
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
CliClient/app/LinkSelector.js
CliClient/build/LinkSelector.js
CliClient/tests-build/synchronizer_LockHandler.js
CliClient/tests-build/synchronizer_MigrationHandler.js
CliClient/tests/synchronizer_LockHandler.js
CliClient/tests/synchronizer_MigrationHandler.js
ElectronClient/commands/focusElement.js
Expand All @@ -62,6 +64,12 @@ ElectronClient/commands/stopExternalEditing.js
ElectronClient/global.d.js
ElectronClient/gui/ErrorBoundary.js
ElectronClient/gui/Header/commands/focusSearch.js
ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js
ElectronClient/gui/KeymapConfig/ShortcutRecorder.js
ElectronClient/gui/KeymapConfig/styles/index.js
ElectronClient/gui/KeymapConfig/utils/getLabel.js
ElectronClient/gui/KeymapConfig/utils/useEditing.js
ElectronClient/gui/KeymapConfig/utils/useKeymap.js
ElectronClient/gui/MainScreen/commands/editAlarm.js
ElectronClient/gui/MainScreen/commands/exportPdf.js
ElectronClient/gui/MainScreen/commands/hideModalMessage.js
Expand Down
12 changes: 0 additions & 12 deletions CliClient/tests/services_KeymapService.js
Original file line number Diff line number Diff line change
Expand Up @@ -282,13 +282,7 @@ describe('services_KeymapService', () => {

for (let i = 0; i < customKeymaps_Darwin.length; i++) {
const customKeymap = customKeymaps_Darwin[i];
const defaultAccelerators = customKeymap.map(({ command }) => keymapService.getAccelerator(command));

expect(() => keymapService.setKeymap(customKeymap)).toThrow();
// All items should be reset to default values
for (let j = 0; j < customKeymap.length; j++) {
expect(keymapService.getAccelerator(customKeymap[j].command)).toEqual(defaultAccelerators[j]);
}
}

const customKeymaps_Linux = [
Expand All @@ -306,13 +300,7 @@ describe('services_KeymapService', () => {

for (let i = 0; i < customKeymaps_Linux.length; i++) {
const customKeymap = customKeymaps_Linux[i];
const defaultAccelerators = customKeymap.map(({ command }) => keymapService.getAccelerator(command));

expect(() => keymapService.setKeymap(customKeymap)).toThrow();

for (let j = 0; j < customKeymap.length; j++) {
expect(keymapService.getAccelerator(customKeymap[j].command)).toEqual(defaultAccelerators[j]);
}
}
});
});
Expand Down
4 changes: 3 additions & 1 deletion ElectronClient/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ class Application extends BaseApplication {

this.commandService_commandsEnabledStateChange = this.commandService_commandsEnabledStateChange.bind(this);
CommandService.instance().on('commandsEnabledStateChange', this.commandService_commandsEnabledStateChange);

KeymapService.instance().on('keymapChange', this.refreshMenu.bind(this));
}

commandService_commandsEnabledStateChange() {
Expand Down Expand Up @@ -1087,7 +1089,7 @@ class Application extends BaseApplication {
const keymapService = KeymapService.instance();

try {
await KeymapService.instance().loadKeymap(`${dir}/keymap-desktop.json`);
await keymapService.loadKeymap(`${dir}/keymap-desktop.json`);
} catch (err) {
bridge().showErrorMessageBox(err.message);
}
Expand Down
2 changes: 2 additions & 0 deletions ElectronClient/gui/ConfigScreen.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const shared = require('lib/components/shared/config-shared.js');
const ConfigMenuBar = require('./ConfigMenuBar.min.js');
const { EncryptionConfigScreen } = require('./EncryptionConfigScreen.min');
const { ClipperConfigScreen } = require('./ClipperConfigScreen.min');
const { KeymapConfigScreen } = require('./KeymapConfig/KeymapConfigScreen');

class ConfigScreenComponent extends React.Component {
constructor() {
Expand Down Expand Up @@ -68,6 +69,7 @@ class ConfigScreenComponent extends React.Component {
screenFromName(screenName) {
if (screenName === 'encryption') return <EncryptionConfigScreen theme={this.props.theme}/>;
if (screenName === 'server') return <ClipperConfigScreen theme={this.props.theme}/>;
if (screenName === 'keymap') return <KeymapConfigScreen themeId={this.props.theme}/>;

throw new Error(`Invalid screen name: ${screenName}`);
}
Expand Down
144 changes: 144 additions & 0 deletions ElectronClient/gui/KeymapConfig/KeymapConfigScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import * as React from 'react';
import { useState } from 'react';

import KeymapService, { KeymapItem } from '../../lib/services/KeymapService';
import { ShortcutRecorder } from './ShortcutRecorder';
import getLabel from './utils/getLabel';
import useKeymap from './utils/useKeymap';
import useEditing from './utils/useEditing';
import styles_ from './styles';

const { _ } = require('lib/locale.js');
const keymapService = KeymapService.instance();

export interface KeymapConfigScreenProps {
themeId: number
}

export const KeymapConfigScreen = ({ themeId }: KeymapConfigScreenProps) => {
const styles = styles_(themeId);

const [filter, setFilter] = useState('');
const [keymap, setAccelerator, error] = useKeymap();
const [editing, enableEditing, disableEditing] = useEditing();

const handleSave = (event: { commandName: string, accelerator: string }) => {
const { commandName, accelerator } = event;
setAccelerator(commandName, accelerator);
disableEditing(commandName);
};

const hanldeReset = (event: { commandName: string }) => {
const { commandName } = event;
setAccelerator(commandName, keymapService.getDefaultAccelerator(commandName));
disableEditing(commandName);
};

const handleCancel = (event: { commandName: string }) => {
const { commandName } = event;
disableEditing(commandName);
};
laurent22 marked this conversation as resolved.
Show resolved Hide resolved

const renderAccelerator = (accelerator: string) => {
return (
<div>
{accelerator.split('+').map(part => <kbd style={styles.kbd} key={part}>{part}</kbd>).reduce(
(accumulator, part) => (accumulator.length ? [...accumulator, ' + ', part] : [part]),
[]
)}
</div>
);
};

const renderStatus = (command: string) => {
if (!error) return null;
const { invalidAccelerator, invalidKeymapItem, duplicateAccelerators, duplicateKeymapItems } = error.details;

if ((invalidAccelerator && invalidKeymapItem.command === command) ||
(duplicateAccelerators && (duplicateKeymapItems[0].command === command || duplicateKeymapItems[1].command === command))) {
return '❌';
anjulalk marked this conversation as resolved.
Show resolved Hide resolved
} else {
return null;
}
};

const renderKeymapRow = ({ command, accelerator }: KeymapItem) => {
const handleClick = () => enableEditing(command);
const cellContent = editing[command]
? <ShortcutRecorder
onSave={handleSave}
onReset={hanldeReset}
onCancel={handleCancel}
initialAccelerator={accelerator}
commandName={command}
themeId={themeId}
/>
: <div onClick={handleClick} style={styles.tableCell}>
<div style={styles.tableCellShortcut}>
{accelerator
? renderAccelerator(accelerator)
: <div style={styles.disabled}>{_('Disabled')}</div>
}
</div>
<div style={styles.tableCellStatus}>
{renderStatus(command)}
</div>
</div>;

return (
<tr key={command}>
<td style={styles.tableCommandColumn}>
{getLabel(command)}
</td>
<td style={styles.tableShortcutColumn}>
{cellContent}
</td>
</tr>
);
};

return (
<div>
{error &&
<div style={styles.warning}>
<p style={styles.text}>
<span>
{error.details.duplicateAccelerators
? _('Keymap configuration contains duplicates. Change or disable one of the shortcuts to continue.')
: error.details.invalidAccelerator
? _('Keymap configuration contains an invalid shortcut. Change or disable it to continue.')
: error.message // Highly unlikely that any other error will occur at this point
}
anjulalk marked this conversation as resolved.
Show resolved Hide resolved
</span>
</p>
</div>
}
<div style={styles.container}>
<div style={styles.actionsContainer}>
<input
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a Search label for accessibility purposes.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the Search label hasn't been added yet?

value={filter}
onChange={event => setFilter(event.target.value)}
placeholder={_('Search...')}
style={styles.filterInput}
aria-label="Search"
/>
</div>
<table style={styles.table}>
<thead>
<tr>
<th style={styles.tableCommandColumn}>{_('Command')}</th>
<th style={styles.tableShortcutColumn}>{_('Keyboard Shortcut')}</th>
</tr>
</thead>
<tbody>
{keymap.filter(({ command }) => {
const filterLowerCase = filter.toLowerCase();
return (command.toLowerCase().includes(filterLowerCase) || getLabel(command).toLowerCase().includes(filterLowerCase));
}).map(item => renderKeymapRow(item))}
</tbody>
</table>
</div>
</div>
);
};

61 changes: 61 additions & 0 deletions ElectronClient/gui/KeymapConfig/ShortcutRecorder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import * as React from 'react';
import { useState, KeyboardEvent } from 'react';

import KeymapService from '../../lib/services/KeymapService';
import styles_ from './styles';

const { _ } = require('lib/locale.js');
const keymapService = KeymapService.instance();

export interface ShortcutRecorderProps {
onSave: (event: { commandName: string, accelerator: string }) => void,
onReset: (event: { commandName: string }) => void,
onCancel: (event: { commandName: string }) => void,
initialAccelerator: string
commandName: string,
themeId: number
}

export const ShortcutRecorder = ({ onSave, onReset, onCancel, initialAccelerator, commandName, themeId }: ShortcutRecorderProps) => {
const styles = styles_(themeId);
const [accelerator, setAccelerator] = useState(initialAccelerator);

const handleKeydown = (event: KeyboardEvent<HTMLDivElement>) => {
event.preventDefault();
const newAccelerator = keymapService.domToElectronAccelerator(event);

switch (newAccelerator) {
case 'Enter':
return onSave({ commandName, accelerator });
case 'Escape':
return onCancel({ commandName });
case 'Backspace':
case 'Delete':
return setAccelerator('');
default:
setAccelerator(newAccelerator);
}
};

return (
<div style={styles.recorderContainer}>
<input
value={accelerator}
placeholder={_('Press the shortcut')}
onKeyDown={handleKeydown}
style={styles.recorderInput}
readOnly
autoFocus
/>
<button style={styles.inlineButton} onClick={() => onSave({ commandName, accelerator })}>
{_('Save')}
</button>
<button style={styles.inlineButton} onClick={() => onReset({ commandName })}>
{_('Restore')}
</button>
<button style={styles.inlineButton} onClick={() => onCancel({ commandName })}>
{_('Cancel')}
</button>
</div>
);
};
77 changes: 77 additions & 0 deletions ElectronClient/gui/KeymapConfig/styles/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
const { buildStyle } = require('lib/theme');

export default function styles(themeId: number) {
return buildStyle('KeymapConfigScreen', themeId, (theme: any) => {
return {
container: {
...theme.containerStyle,
padding: 16,
},
actionsContainer: {
display: 'flex',
flexDirection: 'row',
},
recorderContainer: {
padding: 2,
},
filterInput: {
...theme.inputStyle,
flexGrow: 1,
},
recorderInput: {
...theme.inputStyle,
minHeight: 29,
},
table: {
...theme.containerStyle,
marginTop: 16,
overflow: 'auto',
width: '100%',
},
tableShortcutColumn: {
...theme.textStyle,
width: '60%',
},
tableCommandColumn: {
...theme.textStyle,
width: 'auto',
},
tableCell: {
display: 'flex',
flexDirection: 'row',
cursor: 'pointer',
},
tableCellShortcut: {
flexGrow: 1,
},
tableCellStatus: {
// No specific styling at the moment
},
kbd: {
fontFamily: 'sans-serif',
border: '1px solid',
borderRadius: 4,
backgroundColor: theme.raisedBackgroundColor,
padding: 2,
paddingLeft: 6,
paddingRight: 6,
},
disabled: {
color: theme.colorFaded,
fontStyle: 'italic',
},
inlineButton: {
...theme.buttonStyle,
marginLeft: 8,
},
warning: {
...theme.textStyle,
backgroundColor: theme.warningBackgroundColor,
paddingLeft: 16,
paddingRight: 16,
paddingTop: 2,
paddingBottom: 2,
},
};
});
}
Loading