@theia/toolbar 1.34.2 → 1.34.3

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.
Files changed (54) hide show
  1. package/LICENSE +641 -641
  2. package/README.md +32 -32
  3. package/lib/browser/abstract-toolbar-contribution.d.ts +16 -16
  4. package/lib/browser/abstract-toolbar-contribution.js +68 -68
  5. package/lib/browser/application-shell-with-toolbar-override.d.ts +14 -14
  6. package/lib/browser/application-shell-with-toolbar-override.js +98 -98
  7. package/lib/browser/codicons.d.ts +1 -1
  8. package/lib/browser/codicons.js +20 -20
  9. package/lib/browser/font-awesome-icons.d.ts +1 -1
  10. package/lib/browser/font-awesome-icons.js +20 -20
  11. package/lib/browser/package.spec.js +18 -18
  12. package/lib/browser/toolbar-command-contribution.d.ts +25 -25
  13. package/lib/browser/toolbar-command-contribution.js +211 -211
  14. package/lib/browser/toolbar-command-quick-input-service.d.ts +19 -19
  15. package/lib/browser/toolbar-command-quick-input-service.js +108 -108
  16. package/lib/browser/toolbar-constants.d.ts +23 -23
  17. package/lib/browser/toolbar-constants.js +75 -75
  18. package/lib/browser/toolbar-controller.d.ts +33 -33
  19. package/lib/browser/toolbar-controller.js +184 -184
  20. package/lib/browser/toolbar-defaults.d.ts +3 -3
  21. package/lib/browser/toolbar-defaults.js +60 -60
  22. package/lib/browser/toolbar-frontend-module.d.ts +4 -4
  23. package/lib/browser/toolbar-frontend-module.js +25 -25
  24. package/lib/browser/toolbar-icon-selector-dialog.d.ts +65 -65
  25. package/lib/browser/toolbar-icon-selector-dialog.js +235 -235
  26. package/lib/browser/toolbar-interfaces.d.ts +45 -45
  27. package/lib/browser/toolbar-interfaces.js +42 -42
  28. package/lib/browser/toolbar-preference-contribution.d.ts +9 -9
  29. package/lib/browser/toolbar-preference-contribution.js +34 -34
  30. package/lib/browser/toolbar-preference-schema.d.ts +5 -5
  31. package/lib/browser/toolbar-preference-schema.js +72 -72
  32. package/lib/browser/toolbar-storage-provider.d.ts +46 -46
  33. package/lib/browser/toolbar-storage-provider.js +354 -354
  34. package/lib/browser/toolbar.d.ts +56 -56
  35. package/lib/browser/toolbar.js +380 -380
  36. package/package.json +10 -10
  37. package/src/browser/abstract-toolbar-contribution.tsx +53 -53
  38. package/src/browser/application-shell-with-toolbar-override.ts +94 -94
  39. package/src/browser/codicons.ts +18 -18
  40. package/src/browser/font-awesome-icons.ts +18 -18
  41. package/src/browser/package.spec.ts +19 -19
  42. package/src/browser/style/toolbar.css +253 -253
  43. package/src/browser/toolbar-command-contribution.ts +211 -211
  44. package/src/browser/toolbar-command-quick-input-service.ts +86 -86
  45. package/src/browser/toolbar-constants.ts +79 -79
  46. package/src/browser/toolbar-controller.ts +182 -182
  47. package/src/browser/toolbar-defaults.ts +58 -58
  48. package/src/browser/toolbar-frontend-module.ts +30 -30
  49. package/src/browser/toolbar-icon-selector-dialog.tsx +296 -296
  50. package/src/browser/toolbar-interfaces.ts +76 -76
  51. package/src/browser/toolbar-preference-contribution.ts +38 -38
  52. package/src/browser/toolbar-preference-schema.ts +74 -74
  53. package/src/browser/toolbar-storage-provider.ts +348 -348
  54. package/src/browser/toolbar.tsx +421 -421
@@ -1,421 +1,421 @@
1
- // *****************************************************************************
2
- // Copyright (C) 2022 Ericsson and others.
3
- //
4
- // This program and the accompanying materials are made available under the
5
- // terms of the Eclipse Public License v. 2.0 which is available at
6
- // http://www.eclipse.org/legal/epl-2.0.
7
- //
8
- // This Source Code may also be made available under the following Secondary
9
- // Licenses when the conditions for such availability set forth in the Eclipse
10
- // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
- // with the GNU Classpath Exception which is available at
12
- // https://www.gnu.org/software/classpath/license.html.
13
- //
14
- // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15
- // *****************************************************************************
16
-
17
- import * as React from '@theia/core/shared/react';
18
- import { Anchor, ContextMenuAccess, KeybindingRegistry, PreferenceService, Widget, WidgetManager } from '@theia/core/lib/browser';
19
- import { LabelIcon } from '@theia/core/lib/browser/label-parser';
20
- import { TabBarToolbar, TabBarToolbarFactory, TabBarToolbarItem } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
21
- import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
22
- import { MenuPath, ProgressService } from '@theia/core';
23
- import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
24
- import { ProgressBarFactory } from '@theia/core/lib/browser/progress-bar-factory';
25
- import { Deferred } from '@theia/core/lib/common/promise-util';
26
- import {
27
- ToolbarItem,
28
- ToolbarAlignment,
29
- ToolbarAlignmentString,
30
- ToolbarItemPosition,
31
- } from './toolbar-interfaces';
32
- import { ToolbarController } from './toolbar-controller';
33
- import { ToolbarMenus } from './toolbar-constants';
34
-
35
- const TOOLBAR_BACKGROUND_DATA_ID = 'toolbar-wrapper';
36
- export const TOOLBAR_PROGRESSBAR_ID = 'main-toolbar-progress';
37
- @injectable()
38
- export class ToolbarImpl extends TabBarToolbar {
39
- @inject(TabBarToolbarFactory) protected readonly tabbarToolbarFactory: TabBarToolbarFactory;
40
- @inject(WidgetManager) protected readonly widgetManager: WidgetManager;
41
- @inject(FrontendApplicationStateService) protected readonly appState: FrontendApplicationStateService;
42
- @inject(ToolbarController) protected readonly model: ToolbarController;
43
- @inject(PreferenceService) protected readonly preferenceService: PreferenceService;
44
- @inject(KeybindingRegistry) protected readonly keybindingRegistry: KeybindingRegistry;
45
- @inject(ProgressBarFactory) protected readonly progressFactory: ProgressBarFactory;
46
- @inject(ProgressService) protected readonly progressService: ProgressService;
47
-
48
- protected currentlyDraggedItem: HTMLDivElement | undefined;
49
- protected draggedStartingPosition: ToolbarItemPosition | undefined;
50
- protected deferredRef = new Deferred<HTMLDivElement>();
51
- protected isBusyDeferred = new Deferred<void>();
52
-
53
- @postConstruct()
54
- async init(): Promise<void> {
55
- this.hide();
56
- await this.model.ready.promise;
57
-
58
- this.updateInlineItems();
59
- this.update();
60
- this.model.onToolbarModelDidUpdate(() => {
61
- this.updateInlineItems();
62
- this.update();
63
- });
64
- this.model.onToolbarDidChangeBusyState(isBusy => {
65
- if (isBusy) {
66
- this.isBusyDeferred = new Deferred<void>();
67
- this.progressService.withProgress('', TOOLBAR_PROGRESSBAR_ID, async () => this.isBusyDeferred.promise);
68
- } else {
69
- this.isBusyDeferred.resolve();
70
- }
71
- });
72
-
73
- await this.deferredRef.promise;
74
- this.progressFactory({ container: this.node, insertMode: 'append', locationId: TOOLBAR_PROGRESSBAR_ID });
75
- }
76
-
77
- protected updateInlineItems(): void {
78
- this.inline.clear();
79
- const { items } = this.model.toolbarItems;
80
- for (const column of Object.keys(items)) {
81
- for (const group of items[column as ToolbarAlignment]) {
82
- for (const item of group) {
83
- this.inline.set(item.id, item);
84
- }
85
- }
86
- }
87
- }
88
-
89
- protected handleContextMenu = (e: React.MouseEvent<HTMLDivElement>): ContextMenuAccess => this.doHandleContextMenu(e);
90
- protected doHandleContextMenu(event: React.MouseEvent<HTMLDivElement>): ContextMenuAccess {
91
- event.preventDefault();
92
- event.stopPropagation();
93
- const contextMenuArgs = this.getContextMenuArgs(event);
94
- const { menuPath, anchor } = this.getMenuDetailsForClick(event);
95
- return this.contextMenuRenderer.render({
96
- args: contextMenuArgs,
97
- menuPath,
98
- anchor,
99
- });
100
- }
101
-
102
- protected getMenuDetailsForClick(event: React.MouseEvent<HTMLDivElement>): { menuPath: MenuPath; anchor: Anchor } {
103
- const clickId = event.currentTarget.getAttribute('data-id');
104
- let menuPath: MenuPath;
105
- let anchor: Anchor;
106
- if (clickId === TOOLBAR_BACKGROUND_DATA_ID) {
107
- menuPath = ToolbarMenus.TOOLBAR_BACKGROUND_CONTEXT_MENU;
108
- const { clientX, clientY } = event;
109
- anchor = { x: clientX, y: clientY };
110
- } else {
111
- menuPath = ToolbarMenus.TOOLBAR_ITEM_CONTEXT_MENU;
112
- const { left, bottom } = event.currentTarget.getBoundingClientRect();
113
- anchor = { x: left, y: bottom };
114
- }
115
- return { menuPath, anchor };
116
- }
117
-
118
- protected getContextMenuArgs(event: React.MouseEvent): Array<string | Widget> {
119
- const args: Array<string | Widget> = [this];
120
- // data-position is the stringified position of a given toolbar item, this allows
121
- // the model to be aware of start/stop positions during drag & drop and CRUD operations
122
- const position = event.currentTarget.getAttribute('data-position');
123
- const id = event.currentTarget.getAttribute('data-id');
124
- if (position) {
125
- args.push(JSON.parse(position));
126
- } else if (id) {
127
- args.push(id);
128
- }
129
- return args;
130
- }
131
-
132
- protected renderGroupsInColumn(groups: ToolbarItem[][], alignment: ToolbarAlignment): React.ReactNode[] {
133
- const nodes: React.ReactNode[] = [];
134
- groups.forEach((group, groupIndex) => {
135
- if (nodes.length && group.length) {
136
- nodes.push(<div key={`toolbar-separator-${groupIndex}`} className='separator' />);
137
- }
138
- group.forEach((item, itemIndex) => {
139
- const position = { alignment, groupIndex, itemIndex };
140
- nodes.push(this.renderItemWithDraggableWrapper(item, position));
141
- });
142
- });
143
- return nodes;
144
- }
145
-
146
- protected assignRef = (element: HTMLDivElement): void => this.doAssignRef(element);
147
- protected doAssignRef(element: HTMLDivElement): void {
148
- this.deferredRef.resolve(element);
149
- }
150
-
151
- protected override render(): React.ReactNode {
152
- const leftGroups = this.model.toolbarItems?.items[ToolbarAlignment.LEFT];
153
- const centerGroups = this.model.toolbarItems?.items[ToolbarAlignment.CENTER];
154
- const rightGroups = this.model.toolbarItems?.items[ToolbarAlignment.RIGHT];
155
- return (
156
- <div
157
- className='toolbar-wrapper'
158
- onContextMenu={this.handleContextMenu}
159
- data-id={TOOLBAR_BACKGROUND_DATA_ID}
160
- role='menu'
161
- tabIndex={0}
162
- ref={this.assignRef}
163
- >
164
- {this.renderColumnWrapper(ToolbarAlignment.LEFT, leftGroups)}
165
- {this.renderColumnWrapper(ToolbarAlignment.CENTER, centerGroups)}
166
- {this.renderColumnWrapper(ToolbarAlignment.RIGHT, rightGroups)}
167
- </div>
168
- );
169
- }
170
-
171
- protected renderColumnWrapper(alignment: ToolbarAlignment, columnGroup: ToolbarItem[][]): React.ReactNode {
172
- let children: React.ReactNode;
173
- if (alignment === ToolbarAlignment.LEFT) {
174
- children = (
175
- <>
176
- {this.renderGroupsInColumn(columnGroup, alignment)}
177
- {this.renderColumnSpace(alignment)}
178
- </>
179
- );
180
- } else if (alignment === ToolbarAlignment.CENTER) {
181
- const isCenterColumnEmpty = !columnGroup.length;
182
- if (isCenterColumnEmpty) {
183
- children = this.renderColumnSpace(alignment, 'left');
184
- } else {
185
- children = (
186
- <>
187
- {this.renderColumnSpace(alignment, 'left')}
188
- {this.renderGroupsInColumn(columnGroup, alignment)}
189
- {this.renderColumnSpace(alignment, 'right')}
190
- </>
191
- );
192
- }
193
- } else if (alignment === ToolbarAlignment.RIGHT) {
194
- children = (
195
- <>
196
- {this.renderColumnSpace(alignment)}
197
- {this.renderGroupsInColumn(columnGroup, alignment)}
198
- </>
199
- );
200
- }
201
- return (
202
- <div
203
- role='group'
204
- className={`toolbar-column ${alignment}`}
205
- >
206
- {children}
207
- </div>);
208
- }
209
-
210
- protected renderColumnSpace(alignment: ToolbarAlignment, position?: 'left' | 'right'): React.ReactNode {
211
- return (
212
- <div
213
- className='empty-column-space'
214
- data-column={`${alignment}`}
215
- data-center-position={position}
216
- onDrop={this.handleOnDrop}
217
- onDragEnter={this.handleOnDragEnter}
218
- onDragLeave={this.handleOnDragLeave}
219
- key={`column-space-${alignment}-${position}`}
220
- />
221
- );
222
- }
223
-
224
- protected renderItemWithDraggableWrapper(item: ToolbarItem, position: ToolbarItemPosition): React.ReactNode {
225
- const stringifiedPosition = JSON.stringify(position);
226
- let toolbarItemClassNames = '';
227
- let renderBody: React.ReactNode;
228
- if (TabBarToolbarItem.is(item)) {
229
- toolbarItemClassNames = [TabBarToolbar.Styles.TAB_BAR_TOOLBAR_ITEM, 'enabled'].join(' ');
230
- renderBody = this.renderItem(item);
231
- } else {
232
- const contribution = this.model.getContributionByID(item.id);
233
- if (contribution) {
234
- renderBody = contribution.render();
235
- }
236
- }
237
- return (
238
- <div
239
- role='button'
240
- tabIndex={0}
241
- data-id={item.id}
242
- id={item.id}
243
- data-position={stringifiedPosition}
244
- key={`${item.id}-${stringifiedPosition}`}
245
- className={`${toolbarItemClassNames} toolbar-item action-label`}
246
- onMouseDown={this.onMouseDownEvent}
247
- onMouseUp={this.onMouseUpEvent}
248
- onMouseOut={this.onMouseUpEvent}
249
- draggable={true}
250
- onDragStart={this.handleOnDragStart}
251
- onClick={this.executeCommand}
252
- onDragOver={this.handleOnDragEnter}
253
- onDragLeave={this.handleOnDragLeave}
254
- onContextMenu={this.handleContextMenu}
255
- onDragEnd={this.handleOnDragEnd}
256
- onDrop={this.handleOnDrop}
257
- >
258
- {renderBody}
259
- <div className='hover-overlay' />
260
- </div>
261
- );
262
- }
263
-
264
- protected override renderItem(
265
- item: TabBarToolbarItem,
266
- ): React.ReactNode {
267
- const classNames = [];
268
- if (item.text) {
269
- for (const labelPart of this.labelParser.parse(item.text)) {
270
- if (typeof labelPart !== 'string' && LabelIcon.is(labelPart)) {
271
- const className = `fa fa-${labelPart.name}${labelPart.animation ? ' fa-' + labelPart.animation : ''}`;
272
- classNames.push(...className.split(' '));
273
- }
274
- }
275
- }
276
- const command = this.commands.getCommand(item.command);
277
- const iconClass = (typeof item.icon === 'function' && item.icon()) || item.icon || command?.iconClass;
278
- if (iconClass) {
279
- classNames.push(iconClass);
280
- }
281
- let itemTooltip = '';
282
- if (item.tooltip) {
283
- itemTooltip = item.tooltip;
284
- } else if (command?.label) {
285
- itemTooltip = command.label;
286
- }
287
- const keybindingString = this.resolveKeybindingForCommand(command?.id);
288
- itemTooltip = `${itemTooltip}${keybindingString}`;
289
-
290
- return (
291
- <div
292
- id={item.id}
293
- className={classNames.join(' ')}
294
- title={itemTooltip}
295
- />
296
- );
297
- }
298
-
299
- protected resolveKeybindingForCommand(commandID: string | undefined): string {
300
- if (!commandID) {
301
- return '';
302
- }
303
- const keybindings = this.keybindingRegistry.getKeybindingsForCommand(commandID);
304
- if (keybindings.length > 0) {
305
- const binding = keybindings[0];
306
- const bindingKeySequence = this.keybindingRegistry.resolveKeybinding(binding);
307
- const keyCode = bindingKeySequence[0];
308
- return ` (${this.keybindingRegistry.acceleratorForKeyCode(keyCode, '+')})`;
309
- }
310
- return '';
311
- }
312
-
313
- protected handleOnDragStart = (e: React.DragEvent<HTMLDivElement>): void => this.doHandleOnDragStart(e);
314
- protected doHandleOnDragStart(e: React.DragEvent<HTMLDivElement>): void {
315
- const draggedElement = e.currentTarget;
316
- draggedElement.classList.add('dragging');
317
- e.dataTransfer.setDragImage(draggedElement, 0, 0);
318
- const position = JSON.parse(e.currentTarget.getAttribute('data-position') ?? '');
319
- this.currentlyDraggedItem = e.currentTarget;
320
- this.draggedStartingPosition = position;
321
- }
322
-
323
- protected handleOnDragEnter = (e: React.DragEvent<HTMLDivElement>): void => this.doHandleItemOnDragEnter(e);
324
- protected doHandleItemOnDragEnter(e: React.DragEvent<HTMLDivElement>): void {
325
- e.preventDefault();
326
- e.stopPropagation();
327
- const targetItemDOMElement = e.currentTarget;
328
- const targetItemHoverOverlay = targetItemDOMElement.querySelector('.hover-overlay');
329
- const targetItemId = e.currentTarget.getAttribute('data-id');
330
- if (targetItemDOMElement.classList.contains('empty-column-space')) {
331
- targetItemDOMElement.classList.add('drag-over');
332
- } else if (targetItemDOMElement.classList.contains('toolbar-item') && targetItemHoverOverlay) {
333
- const { clientX } = e;
334
- const { left, right } = e.currentTarget.getBoundingClientRect();
335
- const targetMiddleX = (left + right) / 2;
336
- if (targetItemId !== this.currentlyDraggedItem?.getAttribute('data-id')) {
337
- targetItemHoverOverlay.classList.add('drag-over');
338
- if (clientX <= targetMiddleX) {
339
- targetItemHoverOverlay.classList.add('location-left');
340
- targetItemHoverOverlay.classList.remove('location-right');
341
- } else {
342
- targetItemHoverOverlay.classList.add('location-right');
343
- targetItemHoverOverlay.classList.remove('location-left');
344
- }
345
- }
346
- }
347
- }
348
-
349
- protected handleOnDragLeave = (e: React.DragEvent<HTMLDivElement>): void => this.doHandleOnDragLeave(e);
350
- protected doHandleOnDragLeave(e: React.DragEvent<HTMLDivElement>): void {
351
- e.preventDefault();
352
- e.stopPropagation();
353
- const targetItemDOMElement = e.currentTarget;
354
- const targetItemHoverOverlay = targetItemDOMElement.querySelector('.hover-overlay');
355
- if (targetItemDOMElement.classList.contains('empty-column-space')) {
356
- targetItemDOMElement.classList.remove('drag-over');
357
- } else if (targetItemHoverOverlay && targetItemDOMElement.classList.contains('toolbar-item')) {
358
- targetItemHoverOverlay?.classList.remove('drag-over', 'location-left', 'location-right');
359
- }
360
- }
361
-
362
- protected handleOnDrop = (e: React.DragEvent<HTMLDivElement>): void => this.doHandleOnDrop(e);
363
- protected doHandleOnDrop(e: React.DragEvent<HTMLDivElement>): void {
364
- e.preventDefault();
365
- e.stopPropagation();
366
- const targetItemDOMElement = e.currentTarget;
367
- const targetItemHoverOverlay = targetItemDOMElement.querySelector('.hover-overlay');
368
- if (targetItemDOMElement.classList.contains('empty-column-space')) {
369
- this.handleDropInEmptySpace(targetItemDOMElement);
370
- targetItemDOMElement.classList.remove('drag-over');
371
- } else if (targetItemHoverOverlay && targetItemDOMElement.classList.contains('toolbar-item')) {
372
- this.handleDropInExistingGroup(targetItemDOMElement);
373
- targetItemHoverOverlay.classList.remove('drag-over', 'location-left', 'location-right');
374
- }
375
- this.currentlyDraggedItem = undefined;
376
- this.draggedStartingPosition = undefined;
377
- }
378
-
379
- protected handleDropInExistingGroup(element: EventTarget & HTMLDivElement): void {
380
- const position = element.getAttribute('data-position');
381
- const targetDirection = element.querySelector('.hover-overlay')?.classList.toString()
382
- .split(' ')
383
- .find(className => className.includes('location'));
384
- const dropPosition = JSON.parse(position ?? '');
385
- if (this.currentlyDraggedItem && targetDirection
386
- && this.draggedStartingPosition && !this.arePositionsEquivalent(this.draggedStartingPosition, dropPosition)) {
387
- this.model.swapValues(
388
- this.draggedStartingPosition,
389
- dropPosition,
390
- targetDirection as 'location-left' | 'location-right',
391
- );
392
- }
393
- }
394
-
395
- protected handleDropInEmptySpace(element: EventTarget & HTMLDivElement): void {
396
- const column = element.getAttribute('data-column');
397
- if (ToolbarAlignmentString.is(column) && this.draggedStartingPosition) {
398
- if (column === ToolbarAlignment.CENTER) {
399
- const centerPosition = element.getAttribute('data-center-position');
400
- this.model.moveItemToEmptySpace(this.draggedStartingPosition, column, centerPosition as 'left' | 'right');
401
- } else {
402
- this.model.moveItemToEmptySpace(this.draggedStartingPosition, column);
403
- }
404
- }
405
- }
406
-
407
- protected arePositionsEquivalent(start: ToolbarItemPosition, end: ToolbarItemPosition): boolean {
408
- return start.alignment === end.alignment
409
- && start.groupIndex === end.groupIndex
410
- && start.itemIndex === end.itemIndex;
411
- }
412
-
413
- protected handleOnDragEnd = (e: React.DragEvent<HTMLDivElement>): void => this.doHandleOnDragEnd(e);
414
- protected doHandleOnDragEnd(e: React.DragEvent<HTMLDivElement>): void {
415
- e.preventDefault();
416
- e.stopPropagation();
417
- this.currentlyDraggedItem = undefined;
418
- this.draggedStartingPosition = undefined;
419
- e.currentTarget.classList.remove('dragging');
420
- }
421
- }
1
+ // *****************************************************************************
2
+ // Copyright (C) 2022 Ericsson and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import * as React from '@theia/core/shared/react';
18
+ import { Anchor, ContextMenuAccess, KeybindingRegistry, PreferenceService, Widget, WidgetManager } from '@theia/core/lib/browser';
19
+ import { LabelIcon } from '@theia/core/lib/browser/label-parser';
20
+ import { TabBarToolbar, TabBarToolbarFactory, TabBarToolbarItem } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
21
+ import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
22
+ import { MenuPath, ProgressService } from '@theia/core';
23
+ import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
24
+ import { ProgressBarFactory } from '@theia/core/lib/browser/progress-bar-factory';
25
+ import { Deferred } from '@theia/core/lib/common/promise-util';
26
+ import {
27
+ ToolbarItem,
28
+ ToolbarAlignment,
29
+ ToolbarAlignmentString,
30
+ ToolbarItemPosition,
31
+ } from './toolbar-interfaces';
32
+ import { ToolbarController } from './toolbar-controller';
33
+ import { ToolbarMenus } from './toolbar-constants';
34
+
35
+ const TOOLBAR_BACKGROUND_DATA_ID = 'toolbar-wrapper';
36
+ export const TOOLBAR_PROGRESSBAR_ID = 'main-toolbar-progress';
37
+ @injectable()
38
+ export class ToolbarImpl extends TabBarToolbar {
39
+ @inject(TabBarToolbarFactory) protected readonly tabbarToolbarFactory: TabBarToolbarFactory;
40
+ @inject(WidgetManager) protected readonly widgetManager: WidgetManager;
41
+ @inject(FrontendApplicationStateService) protected readonly appState: FrontendApplicationStateService;
42
+ @inject(ToolbarController) protected readonly model: ToolbarController;
43
+ @inject(PreferenceService) protected readonly preferenceService: PreferenceService;
44
+ @inject(KeybindingRegistry) protected readonly keybindingRegistry: KeybindingRegistry;
45
+ @inject(ProgressBarFactory) protected readonly progressFactory: ProgressBarFactory;
46
+ @inject(ProgressService) protected readonly progressService: ProgressService;
47
+
48
+ protected currentlyDraggedItem: HTMLDivElement | undefined;
49
+ protected draggedStartingPosition: ToolbarItemPosition | undefined;
50
+ protected deferredRef = new Deferred<HTMLDivElement>();
51
+ protected isBusyDeferred = new Deferred<void>();
52
+
53
+ @postConstruct()
54
+ async init(): Promise<void> {
55
+ this.hide();
56
+ await this.model.ready.promise;
57
+
58
+ this.updateInlineItems();
59
+ this.update();
60
+ this.model.onToolbarModelDidUpdate(() => {
61
+ this.updateInlineItems();
62
+ this.update();
63
+ });
64
+ this.model.onToolbarDidChangeBusyState(isBusy => {
65
+ if (isBusy) {
66
+ this.isBusyDeferred = new Deferred<void>();
67
+ this.progressService.withProgress('', TOOLBAR_PROGRESSBAR_ID, async () => this.isBusyDeferred.promise);
68
+ } else {
69
+ this.isBusyDeferred.resolve();
70
+ }
71
+ });
72
+
73
+ await this.deferredRef.promise;
74
+ this.progressFactory({ container: this.node, insertMode: 'append', locationId: TOOLBAR_PROGRESSBAR_ID });
75
+ }
76
+
77
+ protected updateInlineItems(): void {
78
+ this.inline.clear();
79
+ const { items } = this.model.toolbarItems;
80
+ for (const column of Object.keys(items)) {
81
+ for (const group of items[column as ToolbarAlignment]) {
82
+ for (const item of group) {
83
+ this.inline.set(item.id, item);
84
+ }
85
+ }
86
+ }
87
+ }
88
+
89
+ protected handleContextMenu = (e: React.MouseEvent<HTMLDivElement>): ContextMenuAccess => this.doHandleContextMenu(e);
90
+ protected doHandleContextMenu(event: React.MouseEvent<HTMLDivElement>): ContextMenuAccess {
91
+ event.preventDefault();
92
+ event.stopPropagation();
93
+ const contextMenuArgs = this.getContextMenuArgs(event);
94
+ const { menuPath, anchor } = this.getMenuDetailsForClick(event);
95
+ return this.contextMenuRenderer.render({
96
+ args: contextMenuArgs,
97
+ menuPath,
98
+ anchor,
99
+ });
100
+ }
101
+
102
+ protected getMenuDetailsForClick(event: React.MouseEvent<HTMLDivElement>): { menuPath: MenuPath; anchor: Anchor } {
103
+ const clickId = event.currentTarget.getAttribute('data-id');
104
+ let menuPath: MenuPath;
105
+ let anchor: Anchor;
106
+ if (clickId === TOOLBAR_BACKGROUND_DATA_ID) {
107
+ menuPath = ToolbarMenus.TOOLBAR_BACKGROUND_CONTEXT_MENU;
108
+ const { clientX, clientY } = event;
109
+ anchor = { x: clientX, y: clientY };
110
+ } else {
111
+ menuPath = ToolbarMenus.TOOLBAR_ITEM_CONTEXT_MENU;
112
+ const { left, bottom } = event.currentTarget.getBoundingClientRect();
113
+ anchor = { x: left, y: bottom };
114
+ }
115
+ return { menuPath, anchor };
116
+ }
117
+
118
+ protected getContextMenuArgs(event: React.MouseEvent): Array<string | Widget> {
119
+ const args: Array<string | Widget> = [this];
120
+ // data-position is the stringified position of a given toolbar item, this allows
121
+ // the model to be aware of start/stop positions during drag & drop and CRUD operations
122
+ const position = event.currentTarget.getAttribute('data-position');
123
+ const id = event.currentTarget.getAttribute('data-id');
124
+ if (position) {
125
+ args.push(JSON.parse(position));
126
+ } else if (id) {
127
+ args.push(id);
128
+ }
129
+ return args;
130
+ }
131
+
132
+ protected renderGroupsInColumn(groups: ToolbarItem[][], alignment: ToolbarAlignment): React.ReactNode[] {
133
+ const nodes: React.ReactNode[] = [];
134
+ groups.forEach((group, groupIndex) => {
135
+ if (nodes.length && group.length) {
136
+ nodes.push(<div key={`toolbar-separator-${groupIndex}`} className='separator' />);
137
+ }
138
+ group.forEach((item, itemIndex) => {
139
+ const position = { alignment, groupIndex, itemIndex };
140
+ nodes.push(this.renderItemWithDraggableWrapper(item, position));
141
+ });
142
+ });
143
+ return nodes;
144
+ }
145
+
146
+ protected assignRef = (element: HTMLDivElement): void => this.doAssignRef(element);
147
+ protected doAssignRef(element: HTMLDivElement): void {
148
+ this.deferredRef.resolve(element);
149
+ }
150
+
151
+ protected override render(): React.ReactNode {
152
+ const leftGroups = this.model.toolbarItems?.items[ToolbarAlignment.LEFT];
153
+ const centerGroups = this.model.toolbarItems?.items[ToolbarAlignment.CENTER];
154
+ const rightGroups = this.model.toolbarItems?.items[ToolbarAlignment.RIGHT];
155
+ return (
156
+ <div
157
+ className='toolbar-wrapper'
158
+ onContextMenu={this.handleContextMenu}
159
+ data-id={TOOLBAR_BACKGROUND_DATA_ID}
160
+ role='menu'
161
+ tabIndex={0}
162
+ ref={this.assignRef}
163
+ >
164
+ {this.renderColumnWrapper(ToolbarAlignment.LEFT, leftGroups)}
165
+ {this.renderColumnWrapper(ToolbarAlignment.CENTER, centerGroups)}
166
+ {this.renderColumnWrapper(ToolbarAlignment.RIGHT, rightGroups)}
167
+ </div>
168
+ );
169
+ }
170
+
171
+ protected renderColumnWrapper(alignment: ToolbarAlignment, columnGroup: ToolbarItem[][]): React.ReactNode {
172
+ let children: React.ReactNode;
173
+ if (alignment === ToolbarAlignment.LEFT) {
174
+ children = (
175
+ <>
176
+ {this.renderGroupsInColumn(columnGroup, alignment)}
177
+ {this.renderColumnSpace(alignment)}
178
+ </>
179
+ );
180
+ } else if (alignment === ToolbarAlignment.CENTER) {
181
+ const isCenterColumnEmpty = !columnGroup.length;
182
+ if (isCenterColumnEmpty) {
183
+ children = this.renderColumnSpace(alignment, 'left');
184
+ } else {
185
+ children = (
186
+ <>
187
+ {this.renderColumnSpace(alignment, 'left')}
188
+ {this.renderGroupsInColumn(columnGroup, alignment)}
189
+ {this.renderColumnSpace(alignment, 'right')}
190
+ </>
191
+ );
192
+ }
193
+ } else if (alignment === ToolbarAlignment.RIGHT) {
194
+ children = (
195
+ <>
196
+ {this.renderColumnSpace(alignment)}
197
+ {this.renderGroupsInColumn(columnGroup, alignment)}
198
+ </>
199
+ );
200
+ }
201
+ return (
202
+ <div
203
+ role='group'
204
+ className={`toolbar-column ${alignment}`}
205
+ >
206
+ {children}
207
+ </div>);
208
+ }
209
+
210
+ protected renderColumnSpace(alignment: ToolbarAlignment, position?: 'left' | 'right'): React.ReactNode {
211
+ return (
212
+ <div
213
+ className='empty-column-space'
214
+ data-column={`${alignment}`}
215
+ data-center-position={position}
216
+ onDrop={this.handleOnDrop}
217
+ onDragEnter={this.handleOnDragEnter}
218
+ onDragLeave={this.handleOnDragLeave}
219
+ key={`column-space-${alignment}-${position}`}
220
+ />
221
+ );
222
+ }
223
+
224
+ protected renderItemWithDraggableWrapper(item: ToolbarItem, position: ToolbarItemPosition): React.ReactNode {
225
+ const stringifiedPosition = JSON.stringify(position);
226
+ let toolbarItemClassNames = '';
227
+ let renderBody: React.ReactNode;
228
+ if (TabBarToolbarItem.is(item)) {
229
+ toolbarItemClassNames = [TabBarToolbar.Styles.TAB_BAR_TOOLBAR_ITEM, 'enabled'].join(' ');
230
+ renderBody = this.renderItem(item);
231
+ } else {
232
+ const contribution = this.model.getContributionByID(item.id);
233
+ if (contribution) {
234
+ renderBody = contribution.render();
235
+ }
236
+ }
237
+ return (
238
+ <div
239
+ role='button'
240
+ tabIndex={0}
241
+ data-id={item.id}
242
+ id={item.id}
243
+ data-position={stringifiedPosition}
244
+ key={`${item.id}-${stringifiedPosition}`}
245
+ className={`${toolbarItemClassNames} toolbar-item action-label`}
246
+ onMouseDown={this.onMouseDownEvent}
247
+ onMouseUp={this.onMouseUpEvent}
248
+ onMouseOut={this.onMouseUpEvent}
249
+ draggable={true}
250
+ onDragStart={this.handleOnDragStart}
251
+ onClick={this.executeCommand}
252
+ onDragOver={this.handleOnDragEnter}
253
+ onDragLeave={this.handleOnDragLeave}
254
+ onContextMenu={this.handleContextMenu}
255
+ onDragEnd={this.handleOnDragEnd}
256
+ onDrop={this.handleOnDrop}
257
+ >
258
+ {renderBody}
259
+ <div className='hover-overlay' />
260
+ </div>
261
+ );
262
+ }
263
+
264
+ protected override renderItem(
265
+ item: TabBarToolbarItem,
266
+ ): React.ReactNode {
267
+ const classNames = [];
268
+ if (item.text) {
269
+ for (const labelPart of this.labelParser.parse(item.text)) {
270
+ if (typeof labelPart !== 'string' && LabelIcon.is(labelPart)) {
271
+ const className = `fa fa-${labelPart.name}${labelPart.animation ? ' fa-' + labelPart.animation : ''}`;
272
+ classNames.push(...className.split(' '));
273
+ }
274
+ }
275
+ }
276
+ const command = this.commands.getCommand(item.command);
277
+ const iconClass = (typeof item.icon === 'function' && item.icon()) || item.icon || command?.iconClass;
278
+ if (iconClass) {
279
+ classNames.push(iconClass);
280
+ }
281
+ let itemTooltip = '';
282
+ if (item.tooltip) {
283
+ itemTooltip = item.tooltip;
284
+ } else if (command?.label) {
285
+ itemTooltip = command.label;
286
+ }
287
+ const keybindingString = this.resolveKeybindingForCommand(command?.id);
288
+ itemTooltip = `${itemTooltip}${keybindingString}`;
289
+
290
+ return (
291
+ <div
292
+ id={item.id}
293
+ className={classNames.join(' ')}
294
+ title={itemTooltip}
295
+ />
296
+ );
297
+ }
298
+
299
+ protected resolveKeybindingForCommand(commandID: string | undefined): string {
300
+ if (!commandID) {
301
+ return '';
302
+ }
303
+ const keybindings = this.keybindingRegistry.getKeybindingsForCommand(commandID);
304
+ if (keybindings.length > 0) {
305
+ const binding = keybindings[0];
306
+ const bindingKeySequence = this.keybindingRegistry.resolveKeybinding(binding);
307
+ const keyCode = bindingKeySequence[0];
308
+ return ` (${this.keybindingRegistry.acceleratorForKeyCode(keyCode, '+')})`;
309
+ }
310
+ return '';
311
+ }
312
+
313
+ protected handleOnDragStart = (e: React.DragEvent<HTMLDivElement>): void => this.doHandleOnDragStart(e);
314
+ protected doHandleOnDragStart(e: React.DragEvent<HTMLDivElement>): void {
315
+ const draggedElement = e.currentTarget;
316
+ draggedElement.classList.add('dragging');
317
+ e.dataTransfer.setDragImage(draggedElement, 0, 0);
318
+ const position = JSON.parse(e.currentTarget.getAttribute('data-position') ?? '');
319
+ this.currentlyDraggedItem = e.currentTarget;
320
+ this.draggedStartingPosition = position;
321
+ }
322
+
323
+ protected handleOnDragEnter = (e: React.DragEvent<HTMLDivElement>): void => this.doHandleItemOnDragEnter(e);
324
+ protected doHandleItemOnDragEnter(e: React.DragEvent<HTMLDivElement>): void {
325
+ e.preventDefault();
326
+ e.stopPropagation();
327
+ const targetItemDOMElement = e.currentTarget;
328
+ const targetItemHoverOverlay = targetItemDOMElement.querySelector('.hover-overlay');
329
+ const targetItemId = e.currentTarget.getAttribute('data-id');
330
+ if (targetItemDOMElement.classList.contains('empty-column-space')) {
331
+ targetItemDOMElement.classList.add('drag-over');
332
+ } else if (targetItemDOMElement.classList.contains('toolbar-item') && targetItemHoverOverlay) {
333
+ const { clientX } = e;
334
+ const { left, right } = e.currentTarget.getBoundingClientRect();
335
+ const targetMiddleX = (left + right) / 2;
336
+ if (targetItemId !== this.currentlyDraggedItem?.getAttribute('data-id')) {
337
+ targetItemHoverOverlay.classList.add('drag-over');
338
+ if (clientX <= targetMiddleX) {
339
+ targetItemHoverOverlay.classList.add('location-left');
340
+ targetItemHoverOverlay.classList.remove('location-right');
341
+ } else {
342
+ targetItemHoverOverlay.classList.add('location-right');
343
+ targetItemHoverOverlay.classList.remove('location-left');
344
+ }
345
+ }
346
+ }
347
+ }
348
+
349
+ protected handleOnDragLeave = (e: React.DragEvent<HTMLDivElement>): void => this.doHandleOnDragLeave(e);
350
+ protected doHandleOnDragLeave(e: React.DragEvent<HTMLDivElement>): void {
351
+ e.preventDefault();
352
+ e.stopPropagation();
353
+ const targetItemDOMElement = e.currentTarget;
354
+ const targetItemHoverOverlay = targetItemDOMElement.querySelector('.hover-overlay');
355
+ if (targetItemDOMElement.classList.contains('empty-column-space')) {
356
+ targetItemDOMElement.classList.remove('drag-over');
357
+ } else if (targetItemHoverOverlay && targetItemDOMElement.classList.contains('toolbar-item')) {
358
+ targetItemHoverOverlay?.classList.remove('drag-over', 'location-left', 'location-right');
359
+ }
360
+ }
361
+
362
+ protected handleOnDrop = (e: React.DragEvent<HTMLDivElement>): void => this.doHandleOnDrop(e);
363
+ protected doHandleOnDrop(e: React.DragEvent<HTMLDivElement>): void {
364
+ e.preventDefault();
365
+ e.stopPropagation();
366
+ const targetItemDOMElement = e.currentTarget;
367
+ const targetItemHoverOverlay = targetItemDOMElement.querySelector('.hover-overlay');
368
+ if (targetItemDOMElement.classList.contains('empty-column-space')) {
369
+ this.handleDropInEmptySpace(targetItemDOMElement);
370
+ targetItemDOMElement.classList.remove('drag-over');
371
+ } else if (targetItemHoverOverlay && targetItemDOMElement.classList.contains('toolbar-item')) {
372
+ this.handleDropInExistingGroup(targetItemDOMElement);
373
+ targetItemHoverOverlay.classList.remove('drag-over', 'location-left', 'location-right');
374
+ }
375
+ this.currentlyDraggedItem = undefined;
376
+ this.draggedStartingPosition = undefined;
377
+ }
378
+
379
+ protected handleDropInExistingGroup(element: EventTarget & HTMLDivElement): void {
380
+ const position = element.getAttribute('data-position');
381
+ const targetDirection = element.querySelector('.hover-overlay')?.classList.toString()
382
+ .split(' ')
383
+ .find(className => className.includes('location'));
384
+ const dropPosition = JSON.parse(position ?? '');
385
+ if (this.currentlyDraggedItem && targetDirection
386
+ && this.draggedStartingPosition && !this.arePositionsEquivalent(this.draggedStartingPosition, dropPosition)) {
387
+ this.model.swapValues(
388
+ this.draggedStartingPosition,
389
+ dropPosition,
390
+ targetDirection as 'location-left' | 'location-right',
391
+ );
392
+ }
393
+ }
394
+
395
+ protected handleDropInEmptySpace(element: EventTarget & HTMLDivElement): void {
396
+ const column = element.getAttribute('data-column');
397
+ if (ToolbarAlignmentString.is(column) && this.draggedStartingPosition) {
398
+ if (column === ToolbarAlignment.CENTER) {
399
+ const centerPosition = element.getAttribute('data-center-position');
400
+ this.model.moveItemToEmptySpace(this.draggedStartingPosition, column, centerPosition as 'left' | 'right');
401
+ } else {
402
+ this.model.moveItemToEmptySpace(this.draggedStartingPosition, column);
403
+ }
404
+ }
405
+ }
406
+
407
+ protected arePositionsEquivalent(start: ToolbarItemPosition, end: ToolbarItemPosition): boolean {
408
+ return start.alignment === end.alignment
409
+ && start.groupIndex === end.groupIndex
410
+ && start.itemIndex === end.itemIndex;
411
+ }
412
+
413
+ protected handleOnDragEnd = (e: React.DragEvent<HTMLDivElement>): void => this.doHandleOnDragEnd(e);
414
+ protected doHandleOnDragEnd(e: React.DragEvent<HTMLDivElement>): void {
415
+ e.preventDefault();
416
+ e.stopPropagation();
417
+ this.currentlyDraggedItem = undefined;
418
+ this.draggedStartingPosition = undefined;
419
+ e.currentTarget.classList.remove('dragging');
420
+ }
421
+ }