@theia/toolbar 1.45.1 → 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.
- package/README.md +32 -32
- package/lib/browser/abstract-toolbar-contribution.d.ts +16 -16
- package/lib/browser/abstract-toolbar-contribution.js +68 -68
- package/lib/browser/application-shell-with-toolbar-override.d.ts +15 -15
- package/lib/browser/application-shell-with-toolbar-override.js +101 -101
- package/lib/browser/codicons.d.ts +1 -1
- package/lib/browser/codicons.js +20 -20
- package/lib/browser/font-awesome-icons.d.ts +1 -1
- package/lib/browser/font-awesome-icons.js +20 -20
- package/lib/browser/package.spec.js +18 -18
- package/lib/browser/toolbar-command-contribution.d.ts +25 -25
- package/lib/browser/toolbar-command-contribution.js +211 -211
- package/lib/browser/toolbar-command-quick-input-service.d.ts +19 -19
- package/lib/browser/toolbar-command-quick-input-service.js +112 -112
- package/lib/browser/toolbar-constants.d.ts +23 -23
- package/lib/browser/toolbar-constants.js +75 -75
- package/lib/browser/toolbar-controller.d.ts +34 -34
- package/lib/browser/toolbar-controller.js +186 -186
- package/lib/browser/toolbar-defaults.d.ts +3 -3
- package/lib/browser/toolbar-defaults.js +60 -60
- package/lib/browser/toolbar-frontend-module.d.ts +4 -4
- package/lib/browser/toolbar-frontend-module.js +25 -25
- package/lib/browser/toolbar-icon-selector-dialog.d.ts +65 -65
- package/lib/browser/toolbar-icon-selector-dialog.js +235 -235
- package/lib/browser/toolbar-interfaces.d.ts +45 -45
- package/lib/browser/toolbar-interfaces.js +42 -42
- package/lib/browser/toolbar-preference-contribution.d.ts +9 -9
- package/lib/browser/toolbar-preference-contribution.js +34 -34
- package/lib/browser/toolbar-preference-schema.d.ts +5 -5
- package/lib/browser/toolbar-preference-schema.js +73 -73
- package/lib/browser/toolbar-storage-provider.d.ts +47 -47
- package/lib/browser/toolbar-storage-provider.js +357 -357
- package/lib/browser/toolbar.d.ts +56 -56
- package/lib/browser/toolbar.js +380 -380
- package/package.json +11 -11
- package/src/browser/abstract-toolbar-contribution.tsx +53 -53
- package/src/browser/application-shell-with-toolbar-override.ts +98 -98
- package/src/browser/codicons.ts +18 -18
- package/src/browser/font-awesome-icons.ts +18 -18
- package/src/browser/package.spec.ts +19 -19
- package/src/browser/style/toolbar.css +255 -255
- package/src/browser/toolbar-command-contribution.ts +211 -211
- package/src/browser/toolbar-command-quick-input-service.ts +86 -86
- package/src/browser/toolbar-constants.ts +79 -79
- package/src/browser/toolbar-controller.ts +185 -185
- package/src/browser/toolbar-defaults.ts +58 -58
- package/src/browser/toolbar-frontend-module.ts +30 -30
- package/src/browser/toolbar-icon-selector-dialog.tsx +296 -296
- package/src/browser/toolbar-interfaces.ts +76 -76
- package/src/browser/toolbar-preference-contribution.ts +38 -38
- package/src/browser/toolbar-preference-schema.ts +75 -75
- package/src/browser/toolbar-storage-provider.ts +352 -352
- package/src/browser/toolbar.tsx +424 -424
|
@@ -1,30 +1,30 @@
|
|
|
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 '../../src/browser/style/toolbar.css';
|
|
18
|
-
import { ContainerModule, interfaces } from '@theia/core/shared/inversify';
|
|
19
|
-
import { bindToolbarApplicationShell } from './application-shell-with-toolbar-override';
|
|
20
|
-
import { bindToolbar } from './toolbar-command-contribution';
|
|
21
|
-
|
|
22
|
-
export default new ContainerModule((
|
|
23
|
-
bind: interfaces.Bind,
|
|
24
|
-
unbind: interfaces.Unbind,
|
|
25
|
-
_isBound: interfaces.IsBound,
|
|
26
|
-
rebind: interfaces.Rebind,
|
|
27
|
-
) => {
|
|
28
|
-
bindToolbarApplicationShell(bind, rebind, unbind);
|
|
29
|
-
bindToolbar(bind);
|
|
30
|
-
});
|
|
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 '../../src/browser/style/toolbar.css';
|
|
18
|
+
import { ContainerModule, interfaces } from '@theia/core/shared/inversify';
|
|
19
|
+
import { bindToolbarApplicationShell } from './application-shell-with-toolbar-override';
|
|
20
|
+
import { bindToolbar } from './toolbar-command-contribution';
|
|
21
|
+
|
|
22
|
+
export default new ContainerModule((
|
|
23
|
+
bind: interfaces.Bind,
|
|
24
|
+
unbind: interfaces.Unbind,
|
|
25
|
+
_isBound: interfaces.IsBound,
|
|
26
|
+
rebind: interfaces.Rebind,
|
|
27
|
+
) => {
|
|
28
|
+
bindToolbarApplicationShell(bind, rebind, unbind);
|
|
29
|
+
bindToolbar(bind);
|
|
30
|
+
});
|
|
@@ -1,296 +1,296 @@
|
|
|
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 { createRoot, Root } from '@theia/core/shared/react-dom/client';
|
|
19
|
-
import { injectable, interfaces, inject, postConstruct } from '@theia/core/shared/inversify';
|
|
20
|
-
import debounce = require('@theia/core/shared/lodash.debounce');
|
|
21
|
-
import { ReactDialog } from '@theia/core/lib/browser/dialogs/react-dialog';
|
|
22
|
-
import { DEFAULT_SCROLL_OPTIONS, Dialog, DialogProps, Message } from '@theia/core/lib/browser';
|
|
23
|
-
import { Command, Disposable, nls } from '@theia/core';
|
|
24
|
-
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
|
25
|
-
import { Deferred } from '@theia/core/lib/common/promise-util';
|
|
26
|
-
import PerfectScrollbar from 'perfect-scrollbar';
|
|
27
|
-
import { FuzzySearch } from '@theia/core/lib/browser/tree/fuzzy-search';
|
|
28
|
-
import { codicons } from './codicons';
|
|
29
|
-
import { fontAwesomeIcons } from './font-awesome-icons';
|
|
30
|
-
import { IconSet } from './toolbar-interfaces';
|
|
31
|
-
import { ReactInteraction, ReactKeyboardEvent } from './toolbar-constants';
|
|
32
|
-
|
|
33
|
-
export interface ToolbarIconDialogFactory {
|
|
34
|
-
(command: Command): ToolbarIconSelectorDialog;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export const ToolbarIconDialogFactory = Symbol('ToolbarIconDialogFactory');
|
|
38
|
-
export const ToolbarCommand = Symbol('ToolbarCommand');
|
|
39
|
-
export const FontAwesomeIcons = Symbol('FontAwesomeIcons');
|
|
40
|
-
export const CodiconIcons = Symbol('CodiconIcons');
|
|
41
|
-
|
|
42
|
-
const FIFTY_MS = 50;
|
|
43
|
-
@injectable()
|
|
44
|
-
export class ToolbarIconSelectorDialog extends ReactDialog<string | undefined> {
|
|
45
|
-
@inject(ToolbarCommand) protected readonly toolbarCommand: Command;
|
|
46
|
-
@inject(FileService) protected readonly fileService: FileService;
|
|
47
|
-
@inject(FontAwesomeIcons) protected readonly faIcons: string[];
|
|
48
|
-
@inject(CodiconIcons) protected readonly codiconIcons: string[];
|
|
49
|
-
@inject(FuzzySearch) protected readonly fuzzySearch: FuzzySearch;
|
|
50
|
-
|
|
51
|
-
static ID = 'toolbar-icon-selector-dialog';
|
|
52
|
-
protected deferredScrollContainer = new Deferred<HTMLDivElement>();
|
|
53
|
-
override scrollOptions: PerfectScrollbar.Options = { ...DEFAULT_SCROLL_OPTIONS };
|
|
54
|
-
protected filterRef: HTMLInputElement;
|
|
55
|
-
|
|
56
|
-
protected selectedIcon: string | undefined;
|
|
57
|
-
protected activeIconPrefix: IconSet = IconSet.CODICON;
|
|
58
|
-
protected iconSets = new Map<string, string[]>();
|
|
59
|
-
protected filteredIcons: string[] = [];
|
|
60
|
-
protected doShowFilterPlaceholder = false;
|
|
61
|
-
protected debounceHandleSearch = debounce(this.doHandleSearch.bind(this), FIFTY_MS, { trailing: true });
|
|
62
|
-
protected controlPanelRoot: Root;
|
|
63
|
-
|
|
64
|
-
constructor(
|
|
65
|
-
@inject(DialogProps) protected override readonly props: DialogProps,
|
|
66
|
-
) {
|
|
67
|
-
super(props);
|
|
68
|
-
this.controlPanelRoot = createRoot(this.controlPanel);
|
|
69
|
-
this.toDispose.push(Disposable.create(() => this.controlPanelRoot.unmount()));
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
protected override onUpdateRequest(msg: Message): void {
|
|
73
|
-
super.onUpdateRequest(msg);
|
|
74
|
-
this.controlPanelRoot.render(this.renderControls());
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
@postConstruct()
|
|
78
|
-
protected init(): void {
|
|
79
|
-
this.node.id = ToolbarIconSelectorDialog.ID;
|
|
80
|
-
this.iconSets.set(IconSet.FA, this.faIcons);
|
|
81
|
-
this.iconSets.set(IconSet.CODICON, this.codiconIcons);
|
|
82
|
-
this.activeIconPrefix = IconSet.CODICON;
|
|
83
|
-
const initialIcons = this.iconSets.get(this.activeIconPrefix);
|
|
84
|
-
if (initialIcons) {
|
|
85
|
-
this.filteredIcons = initialIcons;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
override async getScrollContainer(): Promise<HTMLElement> {
|
|
90
|
-
return this.deferredScrollContainer.promise;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
protected assignScrollContainerRef = (element: HTMLDivElement): void => this.doAssignScrollContainerRef(element);
|
|
94
|
-
protected doAssignScrollContainerRef(element: HTMLDivElement): void {
|
|
95
|
-
this.deferredScrollContainer.resolve(element);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
protected assignFilterRef = (element: HTMLInputElement): void => this.doAssignFilterRef(element);
|
|
99
|
-
protected doAssignFilterRef(element: HTMLInputElement): void {
|
|
100
|
-
this.filterRef = element;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
get value(): string | undefined {
|
|
104
|
-
return this.selectedIcon;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
protected handleSelectOnChange = async (e: React.ChangeEvent<HTMLSelectElement>): Promise<void> => this.doHandleSelectOnChange(e);
|
|
108
|
-
protected async doHandleSelectOnChange(e: React.ChangeEvent<HTMLSelectElement>): Promise<void> {
|
|
109
|
-
const { value } = e.target;
|
|
110
|
-
this.activeIconPrefix = value as IconSet;
|
|
111
|
-
this.filteredIcons = [];
|
|
112
|
-
await this.doHandleSearch();
|
|
113
|
-
this.update();
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
protected renderIconSelectorOptions(): React.ReactNode {
|
|
117
|
-
return (
|
|
118
|
-
<div className='icon-selector-options'>
|
|
119
|
-
<div className='icon-set-selector-wrapper'>
|
|
120
|
-
{nls.localize('theia/toolbar/iconSet', 'Icon Set')}
|
|
121
|
-
{': '}
|
|
122
|
-
<select
|
|
123
|
-
className='toolbar-icon-select theia-select'
|
|
124
|
-
onChange={this.handleSelectOnChange}
|
|
125
|
-
defaultValue={IconSet.CODICON}
|
|
126
|
-
>
|
|
127
|
-
<option key={IconSet.CODICON} value={IconSet.CODICON}>Codicon</option>
|
|
128
|
-
<option key={IconSet.FA} value={IconSet.FA}>Font Awesome</option>
|
|
129
|
-
</select>
|
|
130
|
-
</div>
|
|
131
|
-
<div className='icon-fuzzy-filter'>
|
|
132
|
-
<input
|
|
133
|
-
ref={this.assignFilterRef}
|
|
134
|
-
placeholder={nls.localize('theia/toolbar/filterIcons', 'Filter Icons')}
|
|
135
|
-
type='text'
|
|
136
|
-
className='icon-filter-input theia-input'
|
|
137
|
-
onChange={this.debounceHandleSearch}
|
|
138
|
-
spellCheck={false}
|
|
139
|
-
/>
|
|
140
|
-
</div>
|
|
141
|
-
</div >
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
protected renderIconGrid(): React.ReactNode {
|
|
146
|
-
return (
|
|
147
|
-
<div
|
|
148
|
-
className='toolbar-scroll-container'
|
|
149
|
-
ref={this.assignScrollContainerRef}
|
|
150
|
-
>
|
|
151
|
-
<div
|
|
152
|
-
className={`toolbar-icon-dialog-content ${this.doShowFilterPlaceholder ? '' : 'grid'}`}
|
|
153
|
-
>
|
|
154
|
-
{!this.doShowFilterPlaceholder ? this.filteredIcons?.map(icon => (
|
|
155
|
-
<div
|
|
156
|
-
className='icon-wrapper'
|
|
157
|
-
key={icon}
|
|
158
|
-
role='button'
|
|
159
|
-
onClick={this.handleOnIconClick}
|
|
160
|
-
onBlur={this.handleOnIconBlur}
|
|
161
|
-
tabIndex={0}
|
|
162
|
-
data-id={`${this.activeIconPrefix} ${icon}`}
|
|
163
|
-
title={icon}
|
|
164
|
-
onKeyPress={this.handleOnIconClick}
|
|
165
|
-
>
|
|
166
|
-
<div className={`${this.activeIconPrefix} ${icon}`} />
|
|
167
|
-
</div>
|
|
168
|
-
))
|
|
169
|
-
: <div className='search-placeholder'>{nls.localizeByDefault('No results found')}</div>}
|
|
170
|
-
</div>
|
|
171
|
-
</div>
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
protected render(): React.ReactNode {
|
|
176
|
-
return (
|
|
177
|
-
<>
|
|
178
|
-
{this.renderIconSelectorOptions()}
|
|
179
|
-
{this.renderIconGrid()}
|
|
180
|
-
</>
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
protected async doHandleSearch(): Promise<void> {
|
|
185
|
-
const query = this.filterRef.value;
|
|
186
|
-
const pattern = query;
|
|
187
|
-
const items = this.iconSets.get(this.activeIconPrefix);
|
|
188
|
-
if (items) {
|
|
189
|
-
if (pattern.length) {
|
|
190
|
-
const transform = (item: string): string => item;
|
|
191
|
-
const filterResults = await this.fuzzySearch.filter({ pattern, items, transform });
|
|
192
|
-
this.filteredIcons = filterResults.map(result => result.item);
|
|
193
|
-
if (!this.filteredIcons.length) {
|
|
194
|
-
this.doShowFilterPlaceholder = true;
|
|
195
|
-
} else {
|
|
196
|
-
this.doShowFilterPlaceholder = false;
|
|
197
|
-
}
|
|
198
|
-
} else {
|
|
199
|
-
this.doShowFilterPlaceholder = false;
|
|
200
|
-
this.filteredIcons = items;
|
|
201
|
-
}
|
|
202
|
-
this.update();
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
protected handleOnIconClick = (e: ReactInteraction<HTMLDivElement>): void => this.doHandleOnIconClick(e);
|
|
207
|
-
protected doHandleOnIconClick(e: ReactInteraction<HTMLDivElement>): void {
|
|
208
|
-
e.currentTarget.classList.add('selected');
|
|
209
|
-
if (ReactKeyboardEvent.is(e) && e.key !== 'Enter') {
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
const iconId = e.currentTarget.getAttribute('data-id');
|
|
213
|
-
if (iconId) {
|
|
214
|
-
this.selectedIcon = iconId;
|
|
215
|
-
this.update();
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
protected handleOnIconBlur = (e: React.FocusEvent<HTMLDivElement>): void => this.doHandleOnIconBlur(e);
|
|
220
|
-
protected doHandleOnIconBlur(e: React.FocusEvent<HTMLDivElement>): void {
|
|
221
|
-
e.currentTarget.classList.remove('selected');
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
protected doAccept = (e: ReactInteraction<HTMLButtonElement>): void => {
|
|
225
|
-
const dataId = e.currentTarget.getAttribute('data-id');
|
|
226
|
-
if (dataId === 'default-accept') {
|
|
227
|
-
this.selectedIcon = this.toolbarCommand.iconClass;
|
|
228
|
-
}
|
|
229
|
-
this.accept();
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
protected doClose = (): void => {
|
|
233
|
-
this.selectedIcon = undefined;
|
|
234
|
-
this.close();
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
protected renderControls(): React.ReactElement {
|
|
238
|
-
return (
|
|
239
|
-
<div className='toolbar-icon-controls'>
|
|
240
|
-
<div>
|
|
241
|
-
{this.toolbarCommand.iconClass
|
|
242
|
-
&& (
|
|
243
|
-
<button
|
|
244
|
-
type='button'
|
|
245
|
-
className='theia-button main default-button'
|
|
246
|
-
data-id='default-accept'
|
|
247
|
-
onClick={this.doAccept}
|
|
248
|
-
>
|
|
249
|
-
<span>
|
|
250
|
-
{`${nls.localize('theia/toolbar/useDefaultIcon', 'Use Default Icon')}:`}
|
|
251
|
-
</span>
|
|
252
|
-
<div className={`toolbar-default-icon ${this.toolbarCommand.iconClass}`} />
|
|
253
|
-
</button>
|
|
254
|
-
)}
|
|
255
|
-
</div>
|
|
256
|
-
<div>
|
|
257
|
-
<button
|
|
258
|
-
type='button'
|
|
259
|
-
disabled={!this.selectedIcon}
|
|
260
|
-
className='theia-button main'
|
|
261
|
-
onClick={this.doAccept}
|
|
262
|
-
>
|
|
263
|
-
{nls.localize('theia/toolbar/selectIcon', 'Select Icon')}
|
|
264
|
-
</button>
|
|
265
|
-
<button
|
|
266
|
-
type='button'
|
|
267
|
-
className='theia-button secondary'
|
|
268
|
-
onClick={this.doClose}
|
|
269
|
-
>
|
|
270
|
-
{Dialog.CANCEL}
|
|
271
|
-
</button>
|
|
272
|
-
|
|
273
|
-
</div>
|
|
274
|
-
</div>
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
export const ICON_DIALOG_WIDTH = 600;
|
|
280
|
-
export const ICON_DIALOG_PADDING = 24;
|
|
281
|
-
|
|
282
|
-
export const bindToolbarIconDialog = (bind: interfaces.Bind): void => {
|
|
283
|
-
bind(ToolbarIconDialogFactory).toFactory(ctx => (command: Command): ToolbarIconSelectorDialog => {
|
|
284
|
-
const child = ctx.container.createChild();
|
|
285
|
-
child.bind(DialogProps).toConstantValue({
|
|
286
|
-
title: nls.localize('theia/toolbar/iconSelectDialog', "Select an Icon for '{0}'", command.label),
|
|
287
|
-
maxWidth: ICON_DIALOG_WIDTH + ICON_DIALOG_PADDING,
|
|
288
|
-
});
|
|
289
|
-
child.bind(FontAwesomeIcons).toConstantValue(fontAwesomeIcons);
|
|
290
|
-
child.bind(CodiconIcons).toConstantValue(codicons);
|
|
291
|
-
child.bind(ToolbarCommand).toConstantValue(command);
|
|
292
|
-
child.bind(FuzzySearch).toSelf().inSingletonScope();
|
|
293
|
-
child.bind(ToolbarIconSelectorDialog).toSelf().inSingletonScope();
|
|
294
|
-
return child.get(ToolbarIconSelectorDialog);
|
|
295
|
-
});
|
|
296
|
-
};
|
|
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 { createRoot, Root } from '@theia/core/shared/react-dom/client';
|
|
19
|
+
import { injectable, interfaces, inject, postConstruct } from '@theia/core/shared/inversify';
|
|
20
|
+
import debounce = require('@theia/core/shared/lodash.debounce');
|
|
21
|
+
import { ReactDialog } from '@theia/core/lib/browser/dialogs/react-dialog';
|
|
22
|
+
import { DEFAULT_SCROLL_OPTIONS, Dialog, DialogProps, Message } from '@theia/core/lib/browser';
|
|
23
|
+
import { Command, Disposable, nls } from '@theia/core';
|
|
24
|
+
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
|
25
|
+
import { Deferred } from '@theia/core/lib/common/promise-util';
|
|
26
|
+
import PerfectScrollbar from 'perfect-scrollbar';
|
|
27
|
+
import { FuzzySearch } from '@theia/core/lib/browser/tree/fuzzy-search';
|
|
28
|
+
import { codicons } from './codicons';
|
|
29
|
+
import { fontAwesomeIcons } from './font-awesome-icons';
|
|
30
|
+
import { IconSet } from './toolbar-interfaces';
|
|
31
|
+
import { ReactInteraction, ReactKeyboardEvent } from './toolbar-constants';
|
|
32
|
+
|
|
33
|
+
export interface ToolbarIconDialogFactory {
|
|
34
|
+
(command: Command): ToolbarIconSelectorDialog;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const ToolbarIconDialogFactory = Symbol('ToolbarIconDialogFactory');
|
|
38
|
+
export const ToolbarCommand = Symbol('ToolbarCommand');
|
|
39
|
+
export const FontAwesomeIcons = Symbol('FontAwesomeIcons');
|
|
40
|
+
export const CodiconIcons = Symbol('CodiconIcons');
|
|
41
|
+
|
|
42
|
+
const FIFTY_MS = 50;
|
|
43
|
+
@injectable()
|
|
44
|
+
export class ToolbarIconSelectorDialog extends ReactDialog<string | undefined> {
|
|
45
|
+
@inject(ToolbarCommand) protected readonly toolbarCommand: Command;
|
|
46
|
+
@inject(FileService) protected readonly fileService: FileService;
|
|
47
|
+
@inject(FontAwesomeIcons) protected readonly faIcons: string[];
|
|
48
|
+
@inject(CodiconIcons) protected readonly codiconIcons: string[];
|
|
49
|
+
@inject(FuzzySearch) protected readonly fuzzySearch: FuzzySearch;
|
|
50
|
+
|
|
51
|
+
static ID = 'toolbar-icon-selector-dialog';
|
|
52
|
+
protected deferredScrollContainer = new Deferred<HTMLDivElement>();
|
|
53
|
+
override scrollOptions: PerfectScrollbar.Options = { ...DEFAULT_SCROLL_OPTIONS };
|
|
54
|
+
protected filterRef: HTMLInputElement;
|
|
55
|
+
|
|
56
|
+
protected selectedIcon: string | undefined;
|
|
57
|
+
protected activeIconPrefix: IconSet = IconSet.CODICON;
|
|
58
|
+
protected iconSets = new Map<string, string[]>();
|
|
59
|
+
protected filteredIcons: string[] = [];
|
|
60
|
+
protected doShowFilterPlaceholder = false;
|
|
61
|
+
protected debounceHandleSearch = debounce(this.doHandleSearch.bind(this), FIFTY_MS, { trailing: true });
|
|
62
|
+
protected controlPanelRoot: Root;
|
|
63
|
+
|
|
64
|
+
constructor(
|
|
65
|
+
@inject(DialogProps) protected override readonly props: DialogProps,
|
|
66
|
+
) {
|
|
67
|
+
super(props);
|
|
68
|
+
this.controlPanelRoot = createRoot(this.controlPanel);
|
|
69
|
+
this.toDispose.push(Disposable.create(() => this.controlPanelRoot.unmount()));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
protected override onUpdateRequest(msg: Message): void {
|
|
73
|
+
super.onUpdateRequest(msg);
|
|
74
|
+
this.controlPanelRoot.render(this.renderControls());
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@postConstruct()
|
|
78
|
+
protected init(): void {
|
|
79
|
+
this.node.id = ToolbarIconSelectorDialog.ID;
|
|
80
|
+
this.iconSets.set(IconSet.FA, this.faIcons);
|
|
81
|
+
this.iconSets.set(IconSet.CODICON, this.codiconIcons);
|
|
82
|
+
this.activeIconPrefix = IconSet.CODICON;
|
|
83
|
+
const initialIcons = this.iconSets.get(this.activeIconPrefix);
|
|
84
|
+
if (initialIcons) {
|
|
85
|
+
this.filteredIcons = initialIcons;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
override async getScrollContainer(): Promise<HTMLElement> {
|
|
90
|
+
return this.deferredScrollContainer.promise;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
protected assignScrollContainerRef = (element: HTMLDivElement): void => this.doAssignScrollContainerRef(element);
|
|
94
|
+
protected doAssignScrollContainerRef(element: HTMLDivElement): void {
|
|
95
|
+
this.deferredScrollContainer.resolve(element);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
protected assignFilterRef = (element: HTMLInputElement): void => this.doAssignFilterRef(element);
|
|
99
|
+
protected doAssignFilterRef(element: HTMLInputElement): void {
|
|
100
|
+
this.filterRef = element;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
get value(): string | undefined {
|
|
104
|
+
return this.selectedIcon;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
protected handleSelectOnChange = async (e: React.ChangeEvent<HTMLSelectElement>): Promise<void> => this.doHandleSelectOnChange(e);
|
|
108
|
+
protected async doHandleSelectOnChange(e: React.ChangeEvent<HTMLSelectElement>): Promise<void> {
|
|
109
|
+
const { value } = e.target;
|
|
110
|
+
this.activeIconPrefix = value as IconSet;
|
|
111
|
+
this.filteredIcons = [];
|
|
112
|
+
await this.doHandleSearch();
|
|
113
|
+
this.update();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
protected renderIconSelectorOptions(): React.ReactNode {
|
|
117
|
+
return (
|
|
118
|
+
<div className='icon-selector-options'>
|
|
119
|
+
<div className='icon-set-selector-wrapper'>
|
|
120
|
+
{nls.localize('theia/toolbar/iconSet', 'Icon Set')}
|
|
121
|
+
{': '}
|
|
122
|
+
<select
|
|
123
|
+
className='toolbar-icon-select theia-select'
|
|
124
|
+
onChange={this.handleSelectOnChange}
|
|
125
|
+
defaultValue={IconSet.CODICON}
|
|
126
|
+
>
|
|
127
|
+
<option key={IconSet.CODICON} value={IconSet.CODICON}>Codicon</option>
|
|
128
|
+
<option key={IconSet.FA} value={IconSet.FA}>Font Awesome</option>
|
|
129
|
+
</select>
|
|
130
|
+
</div>
|
|
131
|
+
<div className='icon-fuzzy-filter'>
|
|
132
|
+
<input
|
|
133
|
+
ref={this.assignFilterRef}
|
|
134
|
+
placeholder={nls.localize('theia/toolbar/filterIcons', 'Filter Icons')}
|
|
135
|
+
type='text'
|
|
136
|
+
className='icon-filter-input theia-input'
|
|
137
|
+
onChange={this.debounceHandleSearch}
|
|
138
|
+
spellCheck={false}
|
|
139
|
+
/>
|
|
140
|
+
</div>
|
|
141
|
+
</div >
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
protected renderIconGrid(): React.ReactNode {
|
|
146
|
+
return (
|
|
147
|
+
<div
|
|
148
|
+
className='toolbar-scroll-container'
|
|
149
|
+
ref={this.assignScrollContainerRef}
|
|
150
|
+
>
|
|
151
|
+
<div
|
|
152
|
+
className={`toolbar-icon-dialog-content ${this.doShowFilterPlaceholder ? '' : 'grid'}`}
|
|
153
|
+
>
|
|
154
|
+
{!this.doShowFilterPlaceholder ? this.filteredIcons?.map(icon => (
|
|
155
|
+
<div
|
|
156
|
+
className='icon-wrapper'
|
|
157
|
+
key={icon}
|
|
158
|
+
role='button'
|
|
159
|
+
onClick={this.handleOnIconClick}
|
|
160
|
+
onBlur={this.handleOnIconBlur}
|
|
161
|
+
tabIndex={0}
|
|
162
|
+
data-id={`${this.activeIconPrefix} ${icon}`}
|
|
163
|
+
title={icon}
|
|
164
|
+
onKeyPress={this.handleOnIconClick}
|
|
165
|
+
>
|
|
166
|
+
<div className={`${this.activeIconPrefix} ${icon}`} />
|
|
167
|
+
</div>
|
|
168
|
+
))
|
|
169
|
+
: <div className='search-placeholder'>{nls.localizeByDefault('No results found')}</div>}
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
protected render(): React.ReactNode {
|
|
176
|
+
return (
|
|
177
|
+
<>
|
|
178
|
+
{this.renderIconSelectorOptions()}
|
|
179
|
+
{this.renderIconGrid()}
|
|
180
|
+
</>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
protected async doHandleSearch(): Promise<void> {
|
|
185
|
+
const query = this.filterRef.value;
|
|
186
|
+
const pattern = query;
|
|
187
|
+
const items = this.iconSets.get(this.activeIconPrefix);
|
|
188
|
+
if (items) {
|
|
189
|
+
if (pattern.length) {
|
|
190
|
+
const transform = (item: string): string => item;
|
|
191
|
+
const filterResults = await this.fuzzySearch.filter({ pattern, items, transform });
|
|
192
|
+
this.filteredIcons = filterResults.map(result => result.item);
|
|
193
|
+
if (!this.filteredIcons.length) {
|
|
194
|
+
this.doShowFilterPlaceholder = true;
|
|
195
|
+
} else {
|
|
196
|
+
this.doShowFilterPlaceholder = false;
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
this.doShowFilterPlaceholder = false;
|
|
200
|
+
this.filteredIcons = items;
|
|
201
|
+
}
|
|
202
|
+
this.update();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
protected handleOnIconClick = (e: ReactInteraction<HTMLDivElement>): void => this.doHandleOnIconClick(e);
|
|
207
|
+
protected doHandleOnIconClick(e: ReactInteraction<HTMLDivElement>): void {
|
|
208
|
+
e.currentTarget.classList.add('selected');
|
|
209
|
+
if (ReactKeyboardEvent.is(e) && e.key !== 'Enter') {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const iconId = e.currentTarget.getAttribute('data-id');
|
|
213
|
+
if (iconId) {
|
|
214
|
+
this.selectedIcon = iconId;
|
|
215
|
+
this.update();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
protected handleOnIconBlur = (e: React.FocusEvent<HTMLDivElement>): void => this.doHandleOnIconBlur(e);
|
|
220
|
+
protected doHandleOnIconBlur(e: React.FocusEvent<HTMLDivElement>): void {
|
|
221
|
+
e.currentTarget.classList.remove('selected');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
protected doAccept = (e: ReactInteraction<HTMLButtonElement>): void => {
|
|
225
|
+
const dataId = e.currentTarget.getAttribute('data-id');
|
|
226
|
+
if (dataId === 'default-accept') {
|
|
227
|
+
this.selectedIcon = this.toolbarCommand.iconClass;
|
|
228
|
+
}
|
|
229
|
+
this.accept();
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
protected doClose = (): void => {
|
|
233
|
+
this.selectedIcon = undefined;
|
|
234
|
+
this.close();
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
protected renderControls(): React.ReactElement {
|
|
238
|
+
return (
|
|
239
|
+
<div className='toolbar-icon-controls'>
|
|
240
|
+
<div>
|
|
241
|
+
{this.toolbarCommand.iconClass
|
|
242
|
+
&& (
|
|
243
|
+
<button
|
|
244
|
+
type='button'
|
|
245
|
+
className='theia-button main default-button'
|
|
246
|
+
data-id='default-accept'
|
|
247
|
+
onClick={this.doAccept}
|
|
248
|
+
>
|
|
249
|
+
<span>
|
|
250
|
+
{`${nls.localize('theia/toolbar/useDefaultIcon', 'Use Default Icon')}:`}
|
|
251
|
+
</span>
|
|
252
|
+
<div className={`toolbar-default-icon ${this.toolbarCommand.iconClass}`} />
|
|
253
|
+
</button>
|
|
254
|
+
)}
|
|
255
|
+
</div>
|
|
256
|
+
<div>
|
|
257
|
+
<button
|
|
258
|
+
type='button'
|
|
259
|
+
disabled={!this.selectedIcon}
|
|
260
|
+
className='theia-button main'
|
|
261
|
+
onClick={this.doAccept}
|
|
262
|
+
>
|
|
263
|
+
{nls.localize('theia/toolbar/selectIcon', 'Select Icon')}
|
|
264
|
+
</button>
|
|
265
|
+
<button
|
|
266
|
+
type='button'
|
|
267
|
+
className='theia-button secondary'
|
|
268
|
+
onClick={this.doClose}
|
|
269
|
+
>
|
|
270
|
+
{Dialog.CANCEL}
|
|
271
|
+
</button>
|
|
272
|
+
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export const ICON_DIALOG_WIDTH = 600;
|
|
280
|
+
export const ICON_DIALOG_PADDING = 24;
|
|
281
|
+
|
|
282
|
+
export const bindToolbarIconDialog = (bind: interfaces.Bind): void => {
|
|
283
|
+
bind(ToolbarIconDialogFactory).toFactory(ctx => (command: Command): ToolbarIconSelectorDialog => {
|
|
284
|
+
const child = ctx.container.createChild();
|
|
285
|
+
child.bind(DialogProps).toConstantValue({
|
|
286
|
+
title: nls.localize('theia/toolbar/iconSelectDialog', "Select an Icon for '{0}'", command.label),
|
|
287
|
+
maxWidth: ICON_DIALOG_WIDTH + ICON_DIALOG_PADDING,
|
|
288
|
+
});
|
|
289
|
+
child.bind(FontAwesomeIcons).toConstantValue(fontAwesomeIcons);
|
|
290
|
+
child.bind(CodiconIcons).toConstantValue(codicons);
|
|
291
|
+
child.bind(ToolbarCommand).toConstantValue(command);
|
|
292
|
+
child.bind(FuzzySearch).toSelf().inSingletonScope();
|
|
293
|
+
child.bind(ToolbarIconSelectorDialog).toSelf().inSingletonScope();
|
|
294
|
+
return child.get(ToolbarIconSelectorDialog);
|
|
295
|
+
});
|
|
296
|
+
};
|