@theia/toolbar 1.45.0 → 1.46.0-next.72

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