@jupyterlab/shortcuts-extension 4.0.0-alpha.2 → 4.0.0-alpha.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/components/ShortcutInput.d.ts +78 -0
- package/lib/components/ShortcutInput.js +348 -0
- package/lib/components/ShortcutInput.js.map +1 -0
- package/lib/components/ShortcutItem.d.ts +62 -0
- package/lib/components/ShortcutItem.js +282 -0
- package/lib/components/ShortcutItem.js.map +1 -0
- package/lib/components/ShortcutList.d.ts +23 -0
- package/lib/components/ShortcutList.js +19 -0
- package/lib/components/ShortcutList.js.map +1 -0
- package/lib/components/ShortcutTitleItem.d.ts +9 -0
- package/lib/components/ShortcutTitleItem.js +16 -0
- package/lib/components/ShortcutTitleItem.js.map +1 -0
- package/lib/components/ShortcutUI.d.ts +56 -0
- package/lib/components/ShortcutUI.js +363 -0
- package/lib/components/ShortcutUI.js.map +1 -0
- package/lib/components/TopNav.d.ts +48 -0
- package/lib/components/TopNav.js +92 -0
- package/lib/components/TopNav.js.map +1 -0
- package/lib/components/index.d.ts +2 -0
- package/lib/components/index.js +6 -0
- package/lib/components/index.js.map +1 -0
- package/lib/index.js +48 -5
- package/lib/index.js.map +1 -1
- package/lib/renderer.d.ts +4 -0
- package/lib/renderer.js +10 -0
- package/lib/renderer.js.map +1 -0
- package/package.json +34 -15
- package/src/components/ShortcutInput.tsx +501 -0
- package/src/components/ShortcutItem.tsx +491 -0
- package/src/components/ShortcutList.tsx +61 -0
- package/src/components/ShortcutTitleItem.tsx +33 -0
- package/src/components/ShortcutUI.tsx +512 -0
- package/src/components/TopNav.tsx +193 -0
- package/src/components/index.ts +7 -0
- package/src/index.ts +297 -0
- package/src/renderer.tsx +13 -0
- package/style/base.css +393 -0
- package/style/index.css +10 -0
- package/style/index.js +10 -0
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
7
|
+
|
|
8
|
+
import { ArrayExt, StringExt } from '@lumino/algorithm';
|
|
9
|
+
|
|
10
|
+
import { ReadonlyJSONArray } from '@lumino/coreutils';
|
|
11
|
+
|
|
12
|
+
import { ShortcutList } from './ShortcutList';
|
|
13
|
+
|
|
14
|
+
import { IShortcutUIexternal, TopNav } from './TopNav';
|
|
15
|
+
|
|
16
|
+
import * as React from 'react';
|
|
17
|
+
import { ErrorObject, ShortcutObject, TakenByObject } from './ShortcutInput';
|
|
18
|
+
|
|
19
|
+
const enum MatchType {
|
|
20
|
+
Label,
|
|
21
|
+
Category,
|
|
22
|
+
Split,
|
|
23
|
+
Default
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Props for ShortcutUI component */
|
|
27
|
+
export interface IShortcutUIProps {
|
|
28
|
+
external: IShortcutUIexternal;
|
|
29
|
+
height: number;
|
|
30
|
+
width: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** State for ShortcutUI component */
|
|
34
|
+
export interface IShortcutUIState {
|
|
35
|
+
shortcutList: { [index: string]: ShortcutObject };
|
|
36
|
+
filteredShortcutList: ShortcutObject[];
|
|
37
|
+
shortcutsFetched: boolean;
|
|
38
|
+
searchQuery: string;
|
|
39
|
+
showSelectors: boolean;
|
|
40
|
+
currentSort: string;
|
|
41
|
+
keyBindingsUsed: { [index: string]: TakenByObject };
|
|
42
|
+
contextMenu: any;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Normalize the query text for a fuzzy search. */
|
|
46
|
+
function normalizeQuery(text: string): string {
|
|
47
|
+
return text.replace(/\s+/g, '').toLowerCase();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Perform a fuzzy search on a single command item. */
|
|
51
|
+
function fuzzySearch(item: any, query: string): any | null {
|
|
52
|
+
// Create the source text to be searched.
|
|
53
|
+
const category = item.category.toLowerCase();
|
|
54
|
+
const label = item['label'].toLowerCase();
|
|
55
|
+
const source = `${category} ${label}`;
|
|
56
|
+
|
|
57
|
+
// Set up the match score and indices array.
|
|
58
|
+
let score = Infinity;
|
|
59
|
+
let indices: number[] | null = null;
|
|
60
|
+
|
|
61
|
+
// The regex for search word boundaries
|
|
62
|
+
const rgx = /\b\w/g;
|
|
63
|
+
|
|
64
|
+
// Search the source by word boundary.
|
|
65
|
+
// eslint-disable-next-line
|
|
66
|
+
while (true) {
|
|
67
|
+
// Find the next word boundary in the source.
|
|
68
|
+
const rgxMatch = rgx.exec(source);
|
|
69
|
+
|
|
70
|
+
// Break if there is no more source context.
|
|
71
|
+
if (!rgxMatch) {
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Run the string match on the relevant substring.
|
|
76
|
+
const match = StringExt.matchSumOfDeltas(source, query, rgxMatch.index);
|
|
77
|
+
|
|
78
|
+
// Break if there is no match.
|
|
79
|
+
if (!match) {
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Update the match if the score is better.
|
|
84
|
+
if (match && match.score <= score) {
|
|
85
|
+
score = match.score;
|
|
86
|
+
indices = match.indices;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Bail if there was no match.
|
|
91
|
+
if (!indices || score === Infinity) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Compute the pivot index between category and label text.
|
|
96
|
+
const pivot = category.length + 1;
|
|
97
|
+
|
|
98
|
+
// Find the slice index to separate matched indices.
|
|
99
|
+
const j = ArrayExt.lowerBound(indices, pivot, (a, b) => a - b);
|
|
100
|
+
|
|
101
|
+
// Extract the matched category and label indices.
|
|
102
|
+
const categoryIndices = indices.slice(0, j);
|
|
103
|
+
const labelIndices = indices.slice(j);
|
|
104
|
+
|
|
105
|
+
// Adjust the label indices for the pivot offset.
|
|
106
|
+
for (let i = 0, n = labelIndices.length; i < n; ++i) {
|
|
107
|
+
labelIndices[i] -= pivot;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Handle a pure label match.
|
|
111
|
+
if (categoryIndices.length === 0) {
|
|
112
|
+
return {
|
|
113
|
+
matchType: MatchType.Label,
|
|
114
|
+
categoryIndices: null,
|
|
115
|
+
labelIndices,
|
|
116
|
+
score,
|
|
117
|
+
item
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Handle a pure category match.
|
|
122
|
+
if (labelIndices.length === 0) {
|
|
123
|
+
return {
|
|
124
|
+
matchType: MatchType.Category,
|
|
125
|
+
categoryIndices,
|
|
126
|
+
labelIndices: null,
|
|
127
|
+
score,
|
|
128
|
+
item
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Handle a split match.
|
|
133
|
+
return {
|
|
134
|
+
matchType: MatchType.Split,
|
|
135
|
+
categoryIndices,
|
|
136
|
+
labelIndices,
|
|
137
|
+
score,
|
|
138
|
+
item
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Perform a fuzzy match on an array of command items. */
|
|
143
|
+
function matchItems(items: any, query: string): any {
|
|
144
|
+
// Normalize the query text to lower case with no whitespace.
|
|
145
|
+
query = normalizeQuery(query);
|
|
146
|
+
|
|
147
|
+
// Create the array to hold the scores.
|
|
148
|
+
let scores: any[] = [];
|
|
149
|
+
// Iterate over the items and match against the query.
|
|
150
|
+
let itemList = Object.keys(items);
|
|
151
|
+
for (let i = 0, n = itemList.length; i < n; ++i) {
|
|
152
|
+
let item = items[itemList[i]];
|
|
153
|
+
|
|
154
|
+
// If the query is empty, all items are matched by default.
|
|
155
|
+
if (!query) {
|
|
156
|
+
scores.push({
|
|
157
|
+
matchType: MatchType.Default,
|
|
158
|
+
categoryIndices: null,
|
|
159
|
+
labelIndices: null,
|
|
160
|
+
score: 0,
|
|
161
|
+
item
|
|
162
|
+
});
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Run the fuzzy search for the item and query.
|
|
167
|
+
let score = fuzzySearch(item, query);
|
|
168
|
+
|
|
169
|
+
// Ignore the item if it is not a match.
|
|
170
|
+
if (!score) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Add the score to the results.
|
|
175
|
+
scores.push(score);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Return the final array of scores.
|
|
179
|
+
return scores;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Transform SettingRegistry's shortcut list to list of ShortcutObjects */
|
|
183
|
+
function getShortcutObjects(
|
|
184
|
+
external: IShortcutUIexternal,
|
|
185
|
+
settings: ISettingRegistry.ISettings
|
|
186
|
+
): { [index: string]: ShortcutObject } {
|
|
187
|
+
const shortcuts = settings.composite.shortcuts as ReadonlyJSONArray;
|
|
188
|
+
let shortcutObjects: { [index: string]: ShortcutObject } = {};
|
|
189
|
+
shortcuts.forEach((shortcut: any) => {
|
|
190
|
+
let key = shortcut.command + '_' + shortcut.selector;
|
|
191
|
+
if (Object.keys(shortcutObjects).indexOf(key) !== -1) {
|
|
192
|
+
let currentCount = shortcutObjects[key].numberOfShortcuts;
|
|
193
|
+
shortcutObjects[key].keys[currentCount] = shortcut.keys;
|
|
194
|
+
shortcutObjects[key].numberOfShortcuts++;
|
|
195
|
+
} else {
|
|
196
|
+
let shortcutObject = new ShortcutObject();
|
|
197
|
+
shortcutObject.commandName = shortcut.command;
|
|
198
|
+
let label = external.getLabel(shortcut.command);
|
|
199
|
+
if (!label) {
|
|
200
|
+
label = shortcut.command.split(':')[1];
|
|
201
|
+
}
|
|
202
|
+
shortcutObject.label = label;
|
|
203
|
+
shortcutObject.category = shortcut.command.split(':')[0];
|
|
204
|
+
shortcutObject.keys[0] = shortcut.keys;
|
|
205
|
+
shortcutObject.selector = shortcut.selector;
|
|
206
|
+
// TODO needs translation
|
|
207
|
+
shortcutObject.source = 'Default';
|
|
208
|
+
shortcutObject.id = key;
|
|
209
|
+
shortcutObject.numberOfShortcuts = 1;
|
|
210
|
+
shortcutObjects[key] = shortcutObject;
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
// find all the shortcuts that have custom settings
|
|
214
|
+
const userShortcuts: any = settings.user.shortcuts;
|
|
215
|
+
userShortcuts.forEach((userSetting: any) => {
|
|
216
|
+
const command: string = userSetting.command;
|
|
217
|
+
const selector: string = userSetting.selector;
|
|
218
|
+
const keyTo = command + '_' + selector;
|
|
219
|
+
if (shortcutObjects[keyTo]) {
|
|
220
|
+
// TODO needs translation
|
|
221
|
+
shortcutObjects[keyTo].source = 'Custom';
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
return shortcutObjects;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Get list of all shortcut keybindings currently in use
|
|
228
|
+
* An object where keys are unique keyBinding_selector and values are shortcut objects **/
|
|
229
|
+
function getKeyBindingsUsed(shortcutObjects: {
|
|
230
|
+
[index: string]: ShortcutObject;
|
|
231
|
+
}): { [index: string]: TakenByObject } {
|
|
232
|
+
let keyBindingsUsed: { [index: string]: TakenByObject } = {};
|
|
233
|
+
|
|
234
|
+
Object.keys(shortcutObjects).forEach((shortcut: string) => {
|
|
235
|
+
Object.keys(shortcutObjects[shortcut].keys).forEach((key: any) => {
|
|
236
|
+
const takenBy = new TakenByObject(shortcutObjects[shortcut]);
|
|
237
|
+
takenBy.takenByKey = key;
|
|
238
|
+
|
|
239
|
+
keyBindingsUsed[
|
|
240
|
+
shortcutObjects[shortcut].keys[key].join(' ') +
|
|
241
|
+
'_' +
|
|
242
|
+
shortcutObjects[shortcut].selector
|
|
243
|
+
] = takenBy;
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
return keyBindingsUsed;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Top level React component for widget */
|
|
250
|
+
export class ShortcutUI extends React.Component<
|
|
251
|
+
IShortcutUIProps,
|
|
252
|
+
IShortcutUIState
|
|
253
|
+
> {
|
|
254
|
+
constructor(props: IShortcutUIProps) {
|
|
255
|
+
super(props);
|
|
256
|
+
this.state = {
|
|
257
|
+
shortcutList: {},
|
|
258
|
+
filteredShortcutList: new Array<ShortcutObject>(),
|
|
259
|
+
shortcutsFetched: false,
|
|
260
|
+
searchQuery: '',
|
|
261
|
+
showSelectors: false,
|
|
262
|
+
currentSort: 'category',
|
|
263
|
+
keyBindingsUsed: {},
|
|
264
|
+
contextMenu: this.props.external.createMenu()
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Fetch shortcut list on mount */
|
|
269
|
+
componentDidMount(): void {
|
|
270
|
+
void this._refreshShortcutList();
|
|
271
|
+
}
|
|
272
|
+
/** Fetch shortcut list from SettingRegistry */
|
|
273
|
+
private async _refreshShortcutList(): Promise<void> {
|
|
274
|
+
const shortcuts: ISettingRegistry.ISettings =
|
|
275
|
+
await this.props.external.getAllShortCutSettings();
|
|
276
|
+
const shortcutObjects = getShortcutObjects(this.props.external, shortcuts);
|
|
277
|
+
this.setState(
|
|
278
|
+
{
|
|
279
|
+
shortcutList: shortcutObjects,
|
|
280
|
+
filteredShortcutList: this.searchFilterShortcuts(shortcutObjects),
|
|
281
|
+
shortcutsFetched: true
|
|
282
|
+
},
|
|
283
|
+
() => {
|
|
284
|
+
let keyBindingsUsed = getKeyBindingsUsed(shortcutObjects);
|
|
285
|
+
this.setState({ keyBindingsUsed });
|
|
286
|
+
this.sortShortcuts();
|
|
287
|
+
}
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Set the current seach query */
|
|
292
|
+
updateSearchQuery = (event: MouseEvent): void => {
|
|
293
|
+
this.setState(
|
|
294
|
+
{
|
|
295
|
+
searchQuery: (event.target as any)['value']
|
|
296
|
+
},
|
|
297
|
+
() =>
|
|
298
|
+
this.setState(
|
|
299
|
+
{
|
|
300
|
+
filteredShortcutList: this.searchFilterShortcuts(
|
|
301
|
+
this.state.shortcutList
|
|
302
|
+
)
|
|
303
|
+
},
|
|
304
|
+
() => {
|
|
305
|
+
this.sortShortcuts();
|
|
306
|
+
}
|
|
307
|
+
)
|
|
308
|
+
);
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
/** Filter shortcut list using current search query */
|
|
312
|
+
private searchFilterShortcuts(shortcutObjects: any): ShortcutObject[] {
|
|
313
|
+
const filteredShortcuts = matchItems(
|
|
314
|
+
shortcutObjects,
|
|
315
|
+
this.state.searchQuery
|
|
316
|
+
).map((item: any) => {
|
|
317
|
+
return item.item;
|
|
318
|
+
});
|
|
319
|
+
return filteredShortcuts;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Reset all shortcuts to their defaults */
|
|
323
|
+
resetShortcuts = async (): Promise<void> => {
|
|
324
|
+
const settings = await this.props.external.getAllShortCutSettings();
|
|
325
|
+
for (const key of Object.keys(settings.user)) {
|
|
326
|
+
await this.props.external.removeShortCut(key);
|
|
327
|
+
}
|
|
328
|
+
await this._refreshShortcutList();
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
/** Set new shortcut for command, refresh state */
|
|
332
|
+
handleUpdate = async (
|
|
333
|
+
shortcutObject: ShortcutObject,
|
|
334
|
+
keys: string[]
|
|
335
|
+
): Promise<void> => {
|
|
336
|
+
const settings: ISettingRegistry.ISettings =
|
|
337
|
+
await this.props.external.getAllShortCutSettings();
|
|
338
|
+
const userShortcuts = settings.user.shortcuts as ReadonlyJSONArray;
|
|
339
|
+
const newUserShortcuts = [];
|
|
340
|
+
let found = false;
|
|
341
|
+
for (let shortcut of userShortcuts as any) {
|
|
342
|
+
if (
|
|
343
|
+
shortcut['command'] === shortcutObject.commandName &&
|
|
344
|
+
shortcut['selector'] === shortcutObject.selector
|
|
345
|
+
) {
|
|
346
|
+
newUserShortcuts.push({
|
|
347
|
+
command: shortcut['command'],
|
|
348
|
+
selector: shortcut['selector'],
|
|
349
|
+
keys: keys
|
|
350
|
+
});
|
|
351
|
+
found = true;
|
|
352
|
+
} else {
|
|
353
|
+
newUserShortcuts.push(shortcut);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (!found) {
|
|
357
|
+
newUserShortcuts.push({
|
|
358
|
+
command: shortcutObject.commandName,
|
|
359
|
+
selector: shortcutObject.selector,
|
|
360
|
+
keys: keys
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
await settings.set('shortcuts', newUserShortcuts);
|
|
364
|
+
await this._refreshShortcutList();
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
/** Delete shortcut for command, refresh state */
|
|
368
|
+
deleteShortcut = async (
|
|
369
|
+
shortcutObject: ShortcutObject,
|
|
370
|
+
shortcutId: string
|
|
371
|
+
): Promise<void> => {
|
|
372
|
+
await this.handleUpdate(shortcutObject, ['']);
|
|
373
|
+
await this._refreshShortcutList();
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
/** Reset a specific shortcut to its default settings */
|
|
377
|
+
resetShortcut = async (shortcutObject: ShortcutObject): Promise<void> => {
|
|
378
|
+
const settings: ISettingRegistry.ISettings =
|
|
379
|
+
await this.props.external.getAllShortCutSettings();
|
|
380
|
+
const userShortcuts = settings.user.shortcuts as ReadonlyJSONArray;
|
|
381
|
+
const newUserShortcuts = [];
|
|
382
|
+
for (let shortcut of userShortcuts as any) {
|
|
383
|
+
if (
|
|
384
|
+
shortcut['command'] !== shortcutObject.commandName ||
|
|
385
|
+
shortcut['selector'] !== shortcutObject.selector
|
|
386
|
+
) {
|
|
387
|
+
newUserShortcuts.push(shortcut);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
await settings.set('shortcuts', newUserShortcuts);
|
|
391
|
+
await this._refreshShortcutList();
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
/** Toggles showing command selectors */
|
|
395
|
+
toggleSelectors = (): void => {
|
|
396
|
+
this.setState({ showSelectors: !this.state.showSelectors });
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
/** Set the current list sort order */
|
|
400
|
+
updateSort = (value: string): void => {
|
|
401
|
+
if (value !== this.state.currentSort) {
|
|
402
|
+
this.setState({ currentSort: value }, this.sortShortcuts);
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
/** Sort shortcut list using current sort property */
|
|
407
|
+
sortShortcuts(): void {
|
|
408
|
+
const shortcuts: ShortcutObject[] = this.state.filteredShortcutList;
|
|
409
|
+
let filterCritera = this.state.currentSort;
|
|
410
|
+
if (filterCritera === 'command') {
|
|
411
|
+
filterCritera = 'label';
|
|
412
|
+
}
|
|
413
|
+
if (filterCritera !== '') {
|
|
414
|
+
shortcuts.sort((a: ShortcutObject, b: ShortcutObject) => {
|
|
415
|
+
const compareA: string = a.get(filterCritera);
|
|
416
|
+
const compareB: string = b.get(filterCritera);
|
|
417
|
+
if (compareA < compareB) {
|
|
418
|
+
return -1;
|
|
419
|
+
} else if (compareA > compareB) {
|
|
420
|
+
return 1;
|
|
421
|
+
} else {
|
|
422
|
+
return a['label'] < b['label'] ? -1 : a['label'] > b['label'] ? 1 : 0;
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
this.setState({ filteredShortcutList: shortcuts });
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** Sort shortcut list so that an error row is right below the one currently being set */
|
|
430
|
+
sortConflict = (
|
|
431
|
+
newShortcut: ShortcutObject,
|
|
432
|
+
takenBy: TakenByObject
|
|
433
|
+
): void => {
|
|
434
|
+
const shortcutList = this.state.filteredShortcutList;
|
|
435
|
+
|
|
436
|
+
if (
|
|
437
|
+
shortcutList.filter(shortcut => shortcut.id === 'error_row').length === 0
|
|
438
|
+
) {
|
|
439
|
+
const errorRow = new ErrorObject();
|
|
440
|
+
errorRow.takenBy = takenBy;
|
|
441
|
+
errorRow.id = 'error_row';
|
|
442
|
+
|
|
443
|
+
shortcutList.splice(shortcutList.indexOf(newShortcut) + 1, 0, errorRow);
|
|
444
|
+
|
|
445
|
+
errorRow.hasConflict = true;
|
|
446
|
+
this.setState({ filteredShortcutList: shortcutList });
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
/** Remove conflict flag from all shortcuts */
|
|
451
|
+
clearConflicts = (): void => {
|
|
452
|
+
/** Remove error row */
|
|
453
|
+
const shortcutList = this.state.filteredShortcutList.filter(
|
|
454
|
+
shortcut => shortcut.id !== 'error_row'
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
shortcutList.forEach((shortcut: ShortcutObject) => {
|
|
458
|
+
shortcut.hasConflict = false;
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
this.setState({ filteredShortcutList: shortcutList });
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
contextMenu = (event: React.MouseEvent, commandIDs: string[]): void => {
|
|
465
|
+
event.persist();
|
|
466
|
+
this.setState(
|
|
467
|
+
{
|
|
468
|
+
contextMenu: this.props.external.createMenu()
|
|
469
|
+
},
|
|
470
|
+
() => {
|
|
471
|
+
event.preventDefault();
|
|
472
|
+
for (let command of commandIDs) {
|
|
473
|
+
this.state.contextMenu.addItem({ command });
|
|
474
|
+
}
|
|
475
|
+
this.state.contextMenu.open(event.clientX, event.clientY);
|
|
476
|
+
}
|
|
477
|
+
);
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
render(): JSX.Element | null {
|
|
481
|
+
if (!this.state.shortcutsFetched) {
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
return (
|
|
485
|
+
<div className="jp-Shortcuts-ShortcutUI" id="jp-shortcutui">
|
|
486
|
+
<TopNav
|
|
487
|
+
updateSearchQuery={this.updateSearchQuery}
|
|
488
|
+
resetShortcuts={this.resetShortcuts}
|
|
489
|
+
toggleSelectors={this.toggleSelectors}
|
|
490
|
+
showSelectors={this.state.showSelectors}
|
|
491
|
+
updateSort={this.updateSort}
|
|
492
|
+
currentSort={this.state.currentSort}
|
|
493
|
+
width={this.props.width}
|
|
494
|
+
external={this.props.external}
|
|
495
|
+
/>
|
|
496
|
+
<ShortcutList
|
|
497
|
+
shortcuts={this.state.filteredShortcutList}
|
|
498
|
+
resetShortcut={this.resetShortcut}
|
|
499
|
+
handleUpdate={this.handleUpdate}
|
|
500
|
+
deleteShortcut={this.deleteShortcut}
|
|
501
|
+
showSelectors={this.state.showSelectors}
|
|
502
|
+
keyBindingsUsed={this.state.keyBindingsUsed}
|
|
503
|
+
sortConflict={this.sortConflict}
|
|
504
|
+
clearConflicts={this.clearConflicts}
|
|
505
|
+
height={this.props.height}
|
|
506
|
+
contextMenu={this.contextMenu}
|
|
507
|
+
external={this.props.external}
|
|
508
|
+
/>
|
|
509
|
+
</div>
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
7
|
+
import { ITranslator } from '@jupyterlab/translation';
|
|
8
|
+
import { InputGroup } from '@jupyterlab/ui-components';
|
|
9
|
+
import { CommandRegistry } from '@lumino/commands';
|
|
10
|
+
import { IDisposable } from '@lumino/disposable';
|
|
11
|
+
import { Menu } from '@lumino/widgets';
|
|
12
|
+
import * as React from 'react';
|
|
13
|
+
|
|
14
|
+
import { ShortcutTitleItem } from './ShortcutTitleItem';
|
|
15
|
+
|
|
16
|
+
export interface IAdvancedOptionsProps {
|
|
17
|
+
toggleSelectors: Function;
|
|
18
|
+
showSelectors: boolean;
|
|
19
|
+
resetShortcuts: Function;
|
|
20
|
+
menu: Menu;
|
|
21
|
+
translator: ITranslator;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ISymbolsProps {}
|
|
25
|
+
|
|
26
|
+
/** All external actions, setting commands, getting command list ... */
|
|
27
|
+
export interface IShortcutUIexternal {
|
|
28
|
+
translator: ITranslator;
|
|
29
|
+
getAllShortCutSettings: () => Promise<ISettingRegistry.ISettings>;
|
|
30
|
+
removeShortCut: (key: string) => Promise<void>;
|
|
31
|
+
createMenu: () => Menu;
|
|
32
|
+
hasCommand: (id: string) => boolean;
|
|
33
|
+
addCommand: (
|
|
34
|
+
id: string,
|
|
35
|
+
options: CommandRegistry.ICommandOptions
|
|
36
|
+
) => IDisposable;
|
|
37
|
+
getLabel: (id: string) => string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export namespace CommandIDs {
|
|
41
|
+
export const showSelectors = 'shortcutui:showSelectors';
|
|
42
|
+
export const resetAll = 'shortcutui:resetAll';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function Symbols(props: ISymbolsProps): JSX.Element {
|
|
46
|
+
return (
|
|
47
|
+
<div className="jp-Shortcuts-Symbols">
|
|
48
|
+
<table>
|
|
49
|
+
<tbody>
|
|
50
|
+
<tr>
|
|
51
|
+
<td>
|
|
52
|
+
<kbd>Cmd</kbd>
|
|
53
|
+
</td>
|
|
54
|
+
<td>⌘</td>
|
|
55
|
+
<td>
|
|
56
|
+
<kbd>Ctrl</kbd>
|
|
57
|
+
</td>
|
|
58
|
+
<td>⌃</td>
|
|
59
|
+
</tr>
|
|
60
|
+
<tr>
|
|
61
|
+
<td>
|
|
62
|
+
<kbd>Alt</kbd>
|
|
63
|
+
</td>
|
|
64
|
+
<td>⌥</td>
|
|
65
|
+
<td>
|
|
66
|
+
<kbd>Shift</kbd>
|
|
67
|
+
</td>
|
|
68
|
+
<td>⇧</td>
|
|
69
|
+
</tr>
|
|
70
|
+
</tbody>
|
|
71
|
+
</table>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function AdvancedOptions(props: IAdvancedOptionsProps): JSX.Element {
|
|
77
|
+
const trans = props.translator.load('jupyterlab');
|
|
78
|
+
return (
|
|
79
|
+
<div className="jp-Shortcuts-AdvancedOptions">
|
|
80
|
+
<a
|
|
81
|
+
className="jp-Shortcuts-AdvancedOptionsLink"
|
|
82
|
+
onClick={() => props.toggleSelectors()}
|
|
83
|
+
>
|
|
84
|
+
{props.showSelectors
|
|
85
|
+
? trans.__('Hide Selectors')
|
|
86
|
+
: trans.__('Show Selectors')}
|
|
87
|
+
</a>
|
|
88
|
+
<a
|
|
89
|
+
className="jp-Shortcuts-AdvancedOptionsLink"
|
|
90
|
+
onClick={() => props.resetShortcuts()}
|
|
91
|
+
>
|
|
92
|
+
{trans.__('Reset All')}
|
|
93
|
+
</a>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** State for TopNav component */
|
|
99
|
+
export interface ITopNavProps {
|
|
100
|
+
resetShortcuts: Function;
|
|
101
|
+
updateSearchQuery: Function;
|
|
102
|
+
toggleSelectors: Function;
|
|
103
|
+
showSelectors: boolean;
|
|
104
|
+
updateSort: Function;
|
|
105
|
+
currentSort: string;
|
|
106
|
+
width: number;
|
|
107
|
+
external: IShortcutUIexternal;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** React component for top navigation */
|
|
111
|
+
export class TopNav extends React.Component<ITopNavProps> {
|
|
112
|
+
menu: Menu;
|
|
113
|
+
constructor(props: ITopNavProps) {
|
|
114
|
+
super(props);
|
|
115
|
+
|
|
116
|
+
this.addMenuCommands();
|
|
117
|
+
this.menu = this.props.external.createMenu();
|
|
118
|
+
this.menu.addItem({ command: CommandIDs.showSelectors });
|
|
119
|
+
this.menu.addItem({ command: CommandIDs.resetAll });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
addMenuCommands() {
|
|
123
|
+
const trans = this.props.external.translator.load('jupyterlab');
|
|
124
|
+
if (!this.props.external.hasCommand(CommandIDs.showSelectors)) {
|
|
125
|
+
this.props.external.addCommand(CommandIDs.showSelectors, {
|
|
126
|
+
label: trans.__('Toggle Selectors'),
|
|
127
|
+
caption: trans.__('Toggle command selectors'),
|
|
128
|
+
execute: () => {
|
|
129
|
+
this.props.toggleSelectors();
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!this.props.external.hasCommand(CommandIDs.resetAll)) {
|
|
135
|
+
this.props.external.addCommand(CommandIDs.resetAll, {
|
|
136
|
+
label: trans.__('Reset All'),
|
|
137
|
+
caption: trans.__('Reset all shortcuts'),
|
|
138
|
+
execute: () => {
|
|
139
|
+
this.props.resetShortcuts();
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
getShortCutTitleItem(title: string) {
|
|
146
|
+
return (
|
|
147
|
+
<div className="jp-Shortcuts-Cell">
|
|
148
|
+
<ShortcutTitleItem
|
|
149
|
+
title={title}
|
|
150
|
+
updateSort={this.props.updateSort}
|
|
151
|
+
active={this.props.currentSort}
|
|
152
|
+
/>
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
render() {
|
|
158
|
+
const trans = this.props.external.translator.load('jupyterlab');
|
|
159
|
+
return (
|
|
160
|
+
<div className="jp-Shortcuts-Top">
|
|
161
|
+
<div className="jp-Shortcuts-TopNav">
|
|
162
|
+
<Symbols />
|
|
163
|
+
<InputGroup
|
|
164
|
+
className="jp-Shortcuts-Search"
|
|
165
|
+
type="text"
|
|
166
|
+
onChange={event => this.props.updateSearchQuery(event)}
|
|
167
|
+
placeholder={trans.__('Search…')}
|
|
168
|
+
rightIcon="ui-components:search"
|
|
169
|
+
/>
|
|
170
|
+
<AdvancedOptions
|
|
171
|
+
toggleSelectors={this.props.toggleSelectors}
|
|
172
|
+
showSelectors={this.props.showSelectors}
|
|
173
|
+
resetShortcuts={this.props.resetShortcuts}
|
|
174
|
+
menu={this.menu}
|
|
175
|
+
translator={this.props.external.translator}
|
|
176
|
+
/>
|
|
177
|
+
</div>
|
|
178
|
+
<div className="jp-Shortcuts-HeaderRowContainer">
|
|
179
|
+
<div className="jp-Shortcuts-HeaderRow">
|
|
180
|
+
{this.getShortCutTitleItem(trans.__('Category'))}
|
|
181
|
+
{this.getShortCutTitleItem(trans.__('Command'))}
|
|
182
|
+
<div className="jp-Shortcuts-Cell">
|
|
183
|
+
<div className="title-div">{trans.__('Shortcut')}</div>
|
|
184
|
+
</div>
|
|
185
|
+
{this.getShortCutTitleItem(trans.__('Source'))}
|
|
186
|
+
{this.props.showSelectors &&
|
|
187
|
+
this.getShortCutTitleItem(trans.__('Selectors'))}
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|