@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.
- 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,352 +1,352 @@
|
|
|
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 jsoncParser from 'jsonc-parser';
|
|
18
|
-
import { Command, deepClone, Disposable, DisposableCollection, Emitter, MessageService, nls } from '@theia/core';
|
|
19
|
-
import { injectable, postConstruct, inject, interfaces } from '@theia/core/shared/inversify';
|
|
20
|
-
import { MonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service';
|
|
21
|
-
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
|
|
22
|
-
import { MonacoWorkspace } from '@theia/monaco/lib/browser/monaco-workspace';
|
|
23
|
-
import * as monaco from '@theia/monaco-editor-core';
|
|
24
|
-
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
|
25
|
-
import { EditorManager } from '@theia/editor/lib/browser';
|
|
26
|
-
import { Widget } from '@theia/core/lib/browser';
|
|
27
|
-
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
|
28
|
-
import { Deferred } from '@theia/core/lib/common/promise-util';
|
|
29
|
-
import URI from '@theia/core/lib/common/uri';
|
|
30
|
-
import {
|
|
31
|
-
DeflatedToolbarTree,
|
|
32
|
-
ToolbarTreeSchema,
|
|
33
|
-
ToolbarItem,
|
|
34
|
-
ToolbarItemDeflated,
|
|
35
|
-
ToolbarAlignment,
|
|
36
|
-
ToolbarItemPosition,
|
|
37
|
-
LateInjector,
|
|
38
|
-
} from './toolbar-interfaces';
|
|
39
|
-
import { UserToolbarURI } from './toolbar-constants';
|
|
40
|
-
import { isToolbarPreferences } from './toolbar-preference-schema';
|
|
41
|
-
|
|
42
|
-
export const TOOLBAR_BAD_JSON_ERROR_MESSAGE = 'There was an error reading your toolbar.json file. Please check if it is corrupt'
|
|
43
|
-
+ ' by right-clicking the toolbar and selecting "Customize Toolbar". You can also reset it to its defaults by selecting'
|
|
44
|
-
+ ' "Restore Toolbar Defaults"';
|
|
45
|
-
@injectable()
|
|
46
|
-
export class ToolbarStorageProvider implements Disposable {
|
|
47
|
-
@inject(FrontendApplicationStateService) protected readonly appState: FrontendApplicationStateService;
|
|
48
|
-
@inject(MonacoTextModelService) protected readonly textModelService: MonacoTextModelService;
|
|
49
|
-
@inject(FileService) protected readonly fileService: FileService;
|
|
50
|
-
@inject(MessageService) protected readonly messageService: MessageService;
|
|
51
|
-
@inject(LateInjector) protected lateInjector: <T>(id: interfaces.ServiceIdentifier<T>) => T;
|
|
52
|
-
@inject(UserToolbarURI) protected readonly USER_TOOLBAR_URI: URI;
|
|
53
|
-
|
|
54
|
-
get ready(): Promise<void> {
|
|
55
|
-
return this._ready.promise;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
protected readonly _ready = new Deferred<void>();
|
|
59
|
-
|
|
60
|
-
// Injecting this directly causes a circular dependency, so we're using a custom utility
|
|
61
|
-
// to inject this after the application has started up
|
|
62
|
-
protected monacoWorkspace: MonacoWorkspace;
|
|
63
|
-
protected editorManager: EditorManager;
|
|
64
|
-
protected model: MonacoEditorModel | undefined;
|
|
65
|
-
protected toDispose = new DisposableCollection();
|
|
66
|
-
protected toolbarItemsUpdatedEmitter = new Emitter<void>();
|
|
67
|
-
readonly onToolbarItemsChanged = this.toolbarItemsUpdatedEmitter.event;
|
|
68
|
-
toolbarItems: DeflatedToolbarTree | undefined;
|
|
69
|
-
|
|
70
|
-
@postConstruct()
|
|
71
|
-
protected init(): void {
|
|
72
|
-
this.doInit();
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
protected async doInit(): Promise<void> {
|
|
76
|
-
const reference = await this.textModelService.createModelReference(this.USER_TOOLBAR_URI);
|
|
77
|
-
this.model = reference.object;
|
|
78
|
-
this.toDispose.push(reference);
|
|
79
|
-
this.toDispose.push(Disposable.create(() => this.model = undefined));
|
|
80
|
-
this.readConfiguration();
|
|
81
|
-
if (this.model) {
|
|
82
|
-
this.toDispose.push(this.model.onDidChangeContent(() => this.readConfiguration()));
|
|
83
|
-
this.toDispose.push(this.model.onDirtyChanged(() => this.readConfiguration()));
|
|
84
|
-
this.toDispose.push(this.model.onDidChangeValid(() => this.readConfiguration()));
|
|
85
|
-
}
|
|
86
|
-
this.toDispose.push(this.toolbarItemsUpdatedEmitter);
|
|
87
|
-
await this.appState.reachedState('ready');
|
|
88
|
-
this.monacoWorkspace = this.lateInjector(MonacoWorkspace);
|
|
89
|
-
this.editorManager = this.lateInjector(EditorManager);
|
|
90
|
-
this._ready.resolve();
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
protected readConfiguration(): void {
|
|
94
|
-
if (!this.model || this.model.dirty) {
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
try {
|
|
98
|
-
if (this.model.valid) {
|
|
99
|
-
const content = this.model.getText();
|
|
100
|
-
this.toolbarItems = this.parseContent(content);
|
|
101
|
-
} else {
|
|
102
|
-
this.toolbarItems = undefined;
|
|
103
|
-
}
|
|
104
|
-
this.toolbarItemsUpdatedEmitter.fire();
|
|
105
|
-
} catch (e) {
|
|
106
|
-
console.error(`Failed to load toolbar config from '${this.USER_TOOLBAR_URI}'.`, e);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
async removeItem(position: ToolbarItemPosition): Promise<boolean> {
|
|
111
|
-
if (this.toolbarItems) {
|
|
112
|
-
const { alignment, groupIndex, itemIndex } = position;
|
|
113
|
-
const modifiedConfiguration = deepClone(this.toolbarItems);
|
|
114
|
-
modifiedConfiguration.items[alignment][groupIndex].splice(itemIndex, 1);
|
|
115
|
-
const sanitizedConfiguration = this.removeEmptyGroupsFromToolbar(modifiedConfiguration);
|
|
116
|
-
return this.writeToFile([], sanitizedConfiguration);
|
|
117
|
-
}
|
|
118
|
-
return false;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
async addItem(command: Command, alignment: ToolbarAlignment): Promise<boolean> {
|
|
122
|
-
if (this.toolbarItems) {
|
|
123
|
-
const itemFromCommand: ToolbarItem = {
|
|
124
|
-
id: command.id,
|
|
125
|
-
command: command.id,
|
|
126
|
-
icon: command.iconClass,
|
|
127
|
-
};
|
|
128
|
-
const groupIndex = this.toolbarItems?.items[alignment].length;
|
|
129
|
-
if (groupIndex) {
|
|
130
|
-
const lastItemIndex = this.toolbarItems?.items[alignment][groupIndex - 1].length;
|
|
131
|
-
const modifiedConfiguration = deepClone(this.toolbarItems);
|
|
132
|
-
modifiedConfiguration.items[alignment][groupIndex - 1].push(itemFromCommand);
|
|
133
|
-
return !!lastItemIndex && this.writeToFile([], modifiedConfiguration);
|
|
134
|
-
}
|
|
135
|
-
return this.addItemToEmptyColumn(itemFromCommand, alignment);
|
|
136
|
-
}
|
|
137
|
-
return false;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async swapValues(
|
|
141
|
-
oldPosition: ToolbarItemPosition,
|
|
142
|
-
newPosition: ToolbarItemPosition,
|
|
143
|
-
direction: 'location-left' | 'location-right',
|
|
144
|
-
): Promise<boolean> {
|
|
145
|
-
if (this.toolbarItems) {
|
|
146
|
-
const { alignment, groupIndex, itemIndex } = oldPosition;
|
|
147
|
-
const draggedItem = this.toolbarItems?.items[alignment][groupIndex][itemIndex];
|
|
148
|
-
const newItemIndex = direction === 'location-right' ? newPosition.itemIndex + 1 : newPosition.itemIndex;
|
|
149
|
-
const modifiedConfiguration = deepClone(this.toolbarItems);
|
|
150
|
-
if (newPosition.alignment === oldPosition.alignment && newPosition.groupIndex === oldPosition.groupIndex) {
|
|
151
|
-
modifiedConfiguration.items[newPosition.alignment][newPosition.groupIndex].splice(newItemIndex, 0, draggedItem);
|
|
152
|
-
if (newPosition.itemIndex > oldPosition.itemIndex) {
|
|
153
|
-
modifiedConfiguration.items[oldPosition.alignment][oldPosition.groupIndex].splice(oldPosition.itemIndex, 1);
|
|
154
|
-
} else {
|
|
155
|
-
modifiedConfiguration.items[oldPosition.alignment][oldPosition.groupIndex].splice(oldPosition.itemIndex + 1, 1);
|
|
156
|
-
}
|
|
157
|
-
} else {
|
|
158
|
-
modifiedConfiguration.items[oldPosition.alignment][oldPosition.groupIndex].splice(oldPosition.itemIndex, 1);
|
|
159
|
-
modifiedConfiguration.items[newPosition.alignment][newPosition.groupIndex].splice(newItemIndex, 0, draggedItem);
|
|
160
|
-
}
|
|
161
|
-
const sanitizedConfiguration = this.removeEmptyGroupsFromToolbar(modifiedConfiguration);
|
|
162
|
-
return this.writeToFile([], sanitizedConfiguration);
|
|
163
|
-
}
|
|
164
|
-
return false;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
async addItemToEmptyColumn(item: ToolbarItemDeflated, alignment: ToolbarAlignment): Promise<boolean> {
|
|
168
|
-
if (this.toolbarItems) {
|
|
169
|
-
const modifiedConfiguration = deepClone(this.toolbarItems);
|
|
170
|
-
modifiedConfiguration.items[alignment].push([item]);
|
|
171
|
-
return this.writeToFile([], modifiedConfiguration);
|
|
172
|
-
}
|
|
173
|
-
return false;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
async moveItemToEmptySpace(
|
|
177
|
-
oldPosition: ToolbarItemPosition,
|
|
178
|
-
newAlignment: ToolbarAlignment,
|
|
179
|
-
centerPosition?: 'left' | 'right',
|
|
180
|
-
): Promise<boolean> {
|
|
181
|
-
const { alignment: oldAlignment, itemIndex: oldItemIndex } = oldPosition;
|
|
182
|
-
let oldGroupIndex = oldPosition.groupIndex;
|
|
183
|
-
if (this.toolbarItems) {
|
|
184
|
-
const draggedItem = this.toolbarItems.items[oldAlignment][oldGroupIndex][oldItemIndex];
|
|
185
|
-
const newGroupIndex = this.toolbarItems.items[oldAlignment].length;
|
|
186
|
-
const modifiedConfiguration = deepClone(this.toolbarItems);
|
|
187
|
-
if (newAlignment === ToolbarAlignment.LEFT) {
|
|
188
|
-
modifiedConfiguration.items[newAlignment].push([draggedItem]);
|
|
189
|
-
} else if (newAlignment === ToolbarAlignment.CENTER) {
|
|
190
|
-
if (centerPosition === 'left') {
|
|
191
|
-
modifiedConfiguration.items[newAlignment].unshift([draggedItem]);
|
|
192
|
-
if (newAlignment === oldAlignment) {
|
|
193
|
-
oldGroupIndex = oldGroupIndex + 1;
|
|
194
|
-
}
|
|
195
|
-
} else if (centerPosition === 'right') {
|
|
196
|
-
modifiedConfiguration.items[newAlignment].splice(newGroupIndex + 1, 0, [draggedItem]);
|
|
197
|
-
}
|
|
198
|
-
} else if (newAlignment === ToolbarAlignment.RIGHT) {
|
|
199
|
-
modifiedConfiguration.items[newAlignment].unshift([draggedItem]);
|
|
200
|
-
if (newAlignment === oldAlignment) {
|
|
201
|
-
oldGroupIndex = oldGroupIndex + 1;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
modifiedConfiguration.items[oldAlignment][oldGroupIndex].splice(oldItemIndex, 1);
|
|
205
|
-
const sanitizedConfiguration = this.removeEmptyGroupsFromToolbar(modifiedConfiguration);
|
|
206
|
-
return this.writeToFile([], sanitizedConfiguration);
|
|
207
|
-
}
|
|
208
|
-
return false;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
async insertGroup(position: ToolbarItemPosition, insertDirection: 'left' | 'right'): Promise<boolean> {
|
|
212
|
-
if (this.toolbarItems) {
|
|
213
|
-
const { alignment, groupIndex, itemIndex } = position;
|
|
214
|
-
const modifiedConfiguration = deepClone(this.toolbarItems);
|
|
215
|
-
const originalColumn = modifiedConfiguration.items[alignment];
|
|
216
|
-
if (originalColumn) {
|
|
217
|
-
const existingGroup = originalColumn[groupIndex];
|
|
218
|
-
const existingGroupLength = existingGroup.length;
|
|
219
|
-
let poppedGroup: ToolbarItemDeflated[] = [];
|
|
220
|
-
let numItemsToRemove: number;
|
|
221
|
-
if (insertDirection === 'left' && itemIndex !== 0) {
|
|
222
|
-
numItemsToRemove = existingGroupLength - itemIndex;
|
|
223
|
-
poppedGroup = existingGroup.splice(itemIndex, numItemsToRemove);
|
|
224
|
-
originalColumn.splice(groupIndex, 1, existingGroup, poppedGroup);
|
|
225
|
-
} else if (insertDirection === 'right' && itemIndex !== existingGroupLength - 1) {
|
|
226
|
-
numItemsToRemove = itemIndex + 1;
|
|
227
|
-
poppedGroup = existingGroup.splice(0, numItemsToRemove);
|
|
228
|
-
originalColumn.splice(groupIndex, 1, poppedGroup, existingGroup);
|
|
229
|
-
}
|
|
230
|
-
const sanitizedConfiguration = this.removeEmptyGroupsFromToolbar(modifiedConfiguration);
|
|
231
|
-
return this.writeToFile([], sanitizedConfiguration);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
return false;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
protected removeEmptyGroupsFromToolbar(
|
|
238
|
-
toolbarItems: DeflatedToolbarTree | undefined,
|
|
239
|
-
): DeflatedToolbarTree | undefined {
|
|
240
|
-
if (toolbarItems) {
|
|
241
|
-
const modifiedConfiguration = deepClone(toolbarItems);
|
|
242
|
-
const columns = [ToolbarAlignment.LEFT, ToolbarAlignment.CENTER, ToolbarAlignment.RIGHT];
|
|
243
|
-
columns.forEach(column => {
|
|
244
|
-
const groups = toolbarItems.items[column];
|
|
245
|
-
groups.forEach((group, index) => {
|
|
246
|
-
if (group.length === 0) {
|
|
247
|
-
modifiedConfiguration.items[column].splice(index, 1);
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
|
-
});
|
|
251
|
-
return modifiedConfiguration;
|
|
252
|
-
}
|
|
253
|
-
return undefined;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
async clearAll(): Promise<boolean> {
|
|
257
|
-
if (this.model) {
|
|
258
|
-
const textModel = this.model.textEditorModel;
|
|
259
|
-
await this.monacoWorkspace.applyBackgroundEdit(this.model, [
|
|
260
|
-
{
|
|
261
|
-
range: textModel.getFullModelRange(),
|
|
262
|
-
// eslint-disable-next-line no-null/no-null
|
|
263
|
-
text: null,
|
|
264
|
-
forceMoveMarkers: false,
|
|
265
|
-
},
|
|
266
|
-
]);
|
|
267
|
-
}
|
|
268
|
-
this.toolbarItemsUpdatedEmitter.fire();
|
|
269
|
-
return true;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
protected async writeToFile(path: jsoncParser.JSONPath, value: unknown, insertion = false): Promise<boolean> {
|
|
273
|
-
if (this.model) {
|
|
274
|
-
try {
|
|
275
|
-
const content = this.model.getText().trim();
|
|
276
|
-
const textModel = this.model.textEditorModel;
|
|
277
|
-
const editOperations: monaco.editor.IIdentifiedSingleEditOperation[] = [];
|
|
278
|
-
const { insertSpaces, tabSize, defaultEOL } = textModel.getOptions();
|
|
279
|
-
for (const edit of jsoncParser.modify(content, path, value, {
|
|
280
|
-
isArrayInsertion: insertion,
|
|
281
|
-
formattingOptions: {
|
|
282
|
-
insertSpaces,
|
|
283
|
-
tabSize,
|
|
284
|
-
eol: defaultEOL === monaco.editor.DefaultEndOfLine.LF ? '\n' : '\r\n',
|
|
285
|
-
},
|
|
286
|
-
})) {
|
|
287
|
-
const start = textModel.getPositionAt(edit.offset);
|
|
288
|
-
const end = textModel.getPositionAt(edit.offset + edit.length);
|
|
289
|
-
editOperations.push({
|
|
290
|
-
range: monaco.Range.fromPositions(start, end),
|
|
291
|
-
// eslint-disable-next-line no-null/no-null
|
|
292
|
-
text: edit.content || null,
|
|
293
|
-
forceMoveMarkers: false,
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
await this.monacoWorkspace.applyBackgroundEdit(this.model, editOperations, false);
|
|
297
|
-
await this.model.save();
|
|
298
|
-
return true;
|
|
299
|
-
} catch (e) {
|
|
300
|
-
const message = nls.localize('theia/toolbar/failedUpdate', "Failed to update the value of '{0}' in '{1}'.", path.join('.'), this.USER_TOOLBAR_URI.path.toString());
|
|
301
|
-
this.messageService.error(nls.localize('theia/toolbar/jsonError', TOOLBAR_BAD_JSON_ERROR_MESSAGE));
|
|
302
|
-
console.error(`${message}`, e);
|
|
303
|
-
return false;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
return false;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
protected parseContent(fileContent: string): DeflatedToolbarTree | undefined {
|
|
310
|
-
const rawConfig = this.parse(fileContent);
|
|
311
|
-
if (!isToolbarPreferences(rawConfig)) {
|
|
312
|
-
return undefined;
|
|
313
|
-
}
|
|
314
|
-
return rawConfig;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
protected parse(fileContent: string): DeflatedToolbarTree | undefined {
|
|
318
|
-
let strippedContent = fileContent.trim();
|
|
319
|
-
if (!strippedContent) {
|
|
320
|
-
return undefined;
|
|
321
|
-
}
|
|
322
|
-
strippedContent = jsoncParser.stripComments(strippedContent);
|
|
323
|
-
return jsoncParser.parse(strippedContent);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
async openOrCreateJSONFile(state: ToolbarTreeSchema, doOpen = false): Promise<Widget | undefined> {
|
|
327
|
-
const fileExists = await this.fileService.exists(this.USER_TOOLBAR_URI);
|
|
328
|
-
let doWriteStateToFile = false;
|
|
329
|
-
if (fileExists) {
|
|
330
|
-
const fileContent = await this.fileService.read(this.USER_TOOLBAR_URI);
|
|
331
|
-
if (fileContent.value.trim() === '') {
|
|
332
|
-
doWriteStateToFile = true;
|
|
333
|
-
}
|
|
334
|
-
} else {
|
|
335
|
-
await this.fileService.create(this.USER_TOOLBAR_URI);
|
|
336
|
-
doWriteStateToFile = true;
|
|
337
|
-
}
|
|
338
|
-
if (doWriteStateToFile) {
|
|
339
|
-
await this.writeToFile([], state);
|
|
340
|
-
}
|
|
341
|
-
this.readConfiguration();
|
|
342
|
-
if (doOpen) {
|
|
343
|
-
const widget = await this.editorManager.open(this.USER_TOOLBAR_URI);
|
|
344
|
-
return widget;
|
|
345
|
-
}
|
|
346
|
-
return undefined;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
dispose(): void {
|
|
350
|
-
this.toDispose.dispose();
|
|
351
|
-
}
|
|
352
|
-
}
|
|
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 jsoncParser from 'jsonc-parser';
|
|
18
|
+
import { Command, deepClone, Disposable, DisposableCollection, Emitter, MessageService, nls } from '@theia/core';
|
|
19
|
+
import { injectable, postConstruct, inject, interfaces } from '@theia/core/shared/inversify';
|
|
20
|
+
import { MonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service';
|
|
21
|
+
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
|
|
22
|
+
import { MonacoWorkspace } from '@theia/monaco/lib/browser/monaco-workspace';
|
|
23
|
+
import * as monaco from '@theia/monaco-editor-core';
|
|
24
|
+
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
|
25
|
+
import { EditorManager } from '@theia/editor/lib/browser';
|
|
26
|
+
import { Widget } from '@theia/core/lib/browser';
|
|
27
|
+
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
|
28
|
+
import { Deferred } from '@theia/core/lib/common/promise-util';
|
|
29
|
+
import URI from '@theia/core/lib/common/uri';
|
|
30
|
+
import {
|
|
31
|
+
DeflatedToolbarTree,
|
|
32
|
+
ToolbarTreeSchema,
|
|
33
|
+
ToolbarItem,
|
|
34
|
+
ToolbarItemDeflated,
|
|
35
|
+
ToolbarAlignment,
|
|
36
|
+
ToolbarItemPosition,
|
|
37
|
+
LateInjector,
|
|
38
|
+
} from './toolbar-interfaces';
|
|
39
|
+
import { UserToolbarURI } from './toolbar-constants';
|
|
40
|
+
import { isToolbarPreferences } from './toolbar-preference-schema';
|
|
41
|
+
|
|
42
|
+
export const TOOLBAR_BAD_JSON_ERROR_MESSAGE = 'There was an error reading your toolbar.json file. Please check if it is corrupt'
|
|
43
|
+
+ ' by right-clicking the toolbar and selecting "Customize Toolbar". You can also reset it to its defaults by selecting'
|
|
44
|
+
+ ' "Restore Toolbar Defaults"';
|
|
45
|
+
@injectable()
|
|
46
|
+
export class ToolbarStorageProvider implements Disposable {
|
|
47
|
+
@inject(FrontendApplicationStateService) protected readonly appState: FrontendApplicationStateService;
|
|
48
|
+
@inject(MonacoTextModelService) protected readonly textModelService: MonacoTextModelService;
|
|
49
|
+
@inject(FileService) protected readonly fileService: FileService;
|
|
50
|
+
@inject(MessageService) protected readonly messageService: MessageService;
|
|
51
|
+
@inject(LateInjector) protected lateInjector: <T>(id: interfaces.ServiceIdentifier<T>) => T;
|
|
52
|
+
@inject(UserToolbarURI) protected readonly USER_TOOLBAR_URI: URI;
|
|
53
|
+
|
|
54
|
+
get ready(): Promise<void> {
|
|
55
|
+
return this._ready.promise;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
protected readonly _ready = new Deferred<void>();
|
|
59
|
+
|
|
60
|
+
// Injecting this directly causes a circular dependency, so we're using a custom utility
|
|
61
|
+
// to inject this after the application has started up
|
|
62
|
+
protected monacoWorkspace: MonacoWorkspace;
|
|
63
|
+
protected editorManager: EditorManager;
|
|
64
|
+
protected model: MonacoEditorModel | undefined;
|
|
65
|
+
protected toDispose = new DisposableCollection();
|
|
66
|
+
protected toolbarItemsUpdatedEmitter = new Emitter<void>();
|
|
67
|
+
readonly onToolbarItemsChanged = this.toolbarItemsUpdatedEmitter.event;
|
|
68
|
+
toolbarItems: DeflatedToolbarTree | undefined;
|
|
69
|
+
|
|
70
|
+
@postConstruct()
|
|
71
|
+
protected init(): void {
|
|
72
|
+
this.doInit();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
protected async doInit(): Promise<void> {
|
|
76
|
+
const reference = await this.textModelService.createModelReference(this.USER_TOOLBAR_URI);
|
|
77
|
+
this.model = reference.object;
|
|
78
|
+
this.toDispose.push(reference);
|
|
79
|
+
this.toDispose.push(Disposable.create(() => this.model = undefined));
|
|
80
|
+
this.readConfiguration();
|
|
81
|
+
if (this.model) {
|
|
82
|
+
this.toDispose.push(this.model.onDidChangeContent(() => this.readConfiguration()));
|
|
83
|
+
this.toDispose.push(this.model.onDirtyChanged(() => this.readConfiguration()));
|
|
84
|
+
this.toDispose.push(this.model.onDidChangeValid(() => this.readConfiguration()));
|
|
85
|
+
}
|
|
86
|
+
this.toDispose.push(this.toolbarItemsUpdatedEmitter);
|
|
87
|
+
await this.appState.reachedState('ready');
|
|
88
|
+
this.monacoWorkspace = this.lateInjector(MonacoWorkspace);
|
|
89
|
+
this.editorManager = this.lateInjector(EditorManager);
|
|
90
|
+
this._ready.resolve();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
protected readConfiguration(): void {
|
|
94
|
+
if (!this.model || this.model.dirty) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
if (this.model.valid) {
|
|
99
|
+
const content = this.model.getText();
|
|
100
|
+
this.toolbarItems = this.parseContent(content);
|
|
101
|
+
} else {
|
|
102
|
+
this.toolbarItems = undefined;
|
|
103
|
+
}
|
|
104
|
+
this.toolbarItemsUpdatedEmitter.fire();
|
|
105
|
+
} catch (e) {
|
|
106
|
+
console.error(`Failed to load toolbar config from '${this.USER_TOOLBAR_URI}'.`, e);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async removeItem(position: ToolbarItemPosition): Promise<boolean> {
|
|
111
|
+
if (this.toolbarItems) {
|
|
112
|
+
const { alignment, groupIndex, itemIndex } = position;
|
|
113
|
+
const modifiedConfiguration = deepClone(this.toolbarItems);
|
|
114
|
+
modifiedConfiguration.items[alignment][groupIndex].splice(itemIndex, 1);
|
|
115
|
+
const sanitizedConfiguration = this.removeEmptyGroupsFromToolbar(modifiedConfiguration);
|
|
116
|
+
return this.writeToFile([], sanitizedConfiguration);
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async addItem(command: Command, alignment: ToolbarAlignment): Promise<boolean> {
|
|
122
|
+
if (this.toolbarItems) {
|
|
123
|
+
const itemFromCommand: ToolbarItem = {
|
|
124
|
+
id: command.id,
|
|
125
|
+
command: command.id,
|
|
126
|
+
icon: command.iconClass,
|
|
127
|
+
};
|
|
128
|
+
const groupIndex = this.toolbarItems?.items[alignment].length;
|
|
129
|
+
if (groupIndex) {
|
|
130
|
+
const lastItemIndex = this.toolbarItems?.items[alignment][groupIndex - 1].length;
|
|
131
|
+
const modifiedConfiguration = deepClone(this.toolbarItems);
|
|
132
|
+
modifiedConfiguration.items[alignment][groupIndex - 1].push(itemFromCommand);
|
|
133
|
+
return !!lastItemIndex && this.writeToFile([], modifiedConfiguration);
|
|
134
|
+
}
|
|
135
|
+
return this.addItemToEmptyColumn(itemFromCommand, alignment);
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async swapValues(
|
|
141
|
+
oldPosition: ToolbarItemPosition,
|
|
142
|
+
newPosition: ToolbarItemPosition,
|
|
143
|
+
direction: 'location-left' | 'location-right',
|
|
144
|
+
): Promise<boolean> {
|
|
145
|
+
if (this.toolbarItems) {
|
|
146
|
+
const { alignment, groupIndex, itemIndex } = oldPosition;
|
|
147
|
+
const draggedItem = this.toolbarItems?.items[alignment][groupIndex][itemIndex];
|
|
148
|
+
const newItemIndex = direction === 'location-right' ? newPosition.itemIndex + 1 : newPosition.itemIndex;
|
|
149
|
+
const modifiedConfiguration = deepClone(this.toolbarItems);
|
|
150
|
+
if (newPosition.alignment === oldPosition.alignment && newPosition.groupIndex === oldPosition.groupIndex) {
|
|
151
|
+
modifiedConfiguration.items[newPosition.alignment][newPosition.groupIndex].splice(newItemIndex, 0, draggedItem);
|
|
152
|
+
if (newPosition.itemIndex > oldPosition.itemIndex) {
|
|
153
|
+
modifiedConfiguration.items[oldPosition.alignment][oldPosition.groupIndex].splice(oldPosition.itemIndex, 1);
|
|
154
|
+
} else {
|
|
155
|
+
modifiedConfiguration.items[oldPosition.alignment][oldPosition.groupIndex].splice(oldPosition.itemIndex + 1, 1);
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
modifiedConfiguration.items[oldPosition.alignment][oldPosition.groupIndex].splice(oldPosition.itemIndex, 1);
|
|
159
|
+
modifiedConfiguration.items[newPosition.alignment][newPosition.groupIndex].splice(newItemIndex, 0, draggedItem);
|
|
160
|
+
}
|
|
161
|
+
const sanitizedConfiguration = this.removeEmptyGroupsFromToolbar(modifiedConfiguration);
|
|
162
|
+
return this.writeToFile([], sanitizedConfiguration);
|
|
163
|
+
}
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async addItemToEmptyColumn(item: ToolbarItemDeflated, alignment: ToolbarAlignment): Promise<boolean> {
|
|
168
|
+
if (this.toolbarItems) {
|
|
169
|
+
const modifiedConfiguration = deepClone(this.toolbarItems);
|
|
170
|
+
modifiedConfiguration.items[alignment].push([item]);
|
|
171
|
+
return this.writeToFile([], modifiedConfiguration);
|
|
172
|
+
}
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async moveItemToEmptySpace(
|
|
177
|
+
oldPosition: ToolbarItemPosition,
|
|
178
|
+
newAlignment: ToolbarAlignment,
|
|
179
|
+
centerPosition?: 'left' | 'right',
|
|
180
|
+
): Promise<boolean> {
|
|
181
|
+
const { alignment: oldAlignment, itemIndex: oldItemIndex } = oldPosition;
|
|
182
|
+
let oldGroupIndex = oldPosition.groupIndex;
|
|
183
|
+
if (this.toolbarItems) {
|
|
184
|
+
const draggedItem = this.toolbarItems.items[oldAlignment][oldGroupIndex][oldItemIndex];
|
|
185
|
+
const newGroupIndex = this.toolbarItems.items[oldAlignment].length;
|
|
186
|
+
const modifiedConfiguration = deepClone(this.toolbarItems);
|
|
187
|
+
if (newAlignment === ToolbarAlignment.LEFT) {
|
|
188
|
+
modifiedConfiguration.items[newAlignment].push([draggedItem]);
|
|
189
|
+
} else if (newAlignment === ToolbarAlignment.CENTER) {
|
|
190
|
+
if (centerPosition === 'left') {
|
|
191
|
+
modifiedConfiguration.items[newAlignment].unshift([draggedItem]);
|
|
192
|
+
if (newAlignment === oldAlignment) {
|
|
193
|
+
oldGroupIndex = oldGroupIndex + 1;
|
|
194
|
+
}
|
|
195
|
+
} else if (centerPosition === 'right') {
|
|
196
|
+
modifiedConfiguration.items[newAlignment].splice(newGroupIndex + 1, 0, [draggedItem]);
|
|
197
|
+
}
|
|
198
|
+
} else if (newAlignment === ToolbarAlignment.RIGHT) {
|
|
199
|
+
modifiedConfiguration.items[newAlignment].unshift([draggedItem]);
|
|
200
|
+
if (newAlignment === oldAlignment) {
|
|
201
|
+
oldGroupIndex = oldGroupIndex + 1;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
modifiedConfiguration.items[oldAlignment][oldGroupIndex].splice(oldItemIndex, 1);
|
|
205
|
+
const sanitizedConfiguration = this.removeEmptyGroupsFromToolbar(modifiedConfiguration);
|
|
206
|
+
return this.writeToFile([], sanitizedConfiguration);
|
|
207
|
+
}
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async insertGroup(position: ToolbarItemPosition, insertDirection: 'left' | 'right'): Promise<boolean> {
|
|
212
|
+
if (this.toolbarItems) {
|
|
213
|
+
const { alignment, groupIndex, itemIndex } = position;
|
|
214
|
+
const modifiedConfiguration = deepClone(this.toolbarItems);
|
|
215
|
+
const originalColumn = modifiedConfiguration.items[alignment];
|
|
216
|
+
if (originalColumn) {
|
|
217
|
+
const existingGroup = originalColumn[groupIndex];
|
|
218
|
+
const existingGroupLength = existingGroup.length;
|
|
219
|
+
let poppedGroup: ToolbarItemDeflated[] = [];
|
|
220
|
+
let numItemsToRemove: number;
|
|
221
|
+
if (insertDirection === 'left' && itemIndex !== 0) {
|
|
222
|
+
numItemsToRemove = existingGroupLength - itemIndex;
|
|
223
|
+
poppedGroup = existingGroup.splice(itemIndex, numItemsToRemove);
|
|
224
|
+
originalColumn.splice(groupIndex, 1, existingGroup, poppedGroup);
|
|
225
|
+
} else if (insertDirection === 'right' && itemIndex !== existingGroupLength - 1) {
|
|
226
|
+
numItemsToRemove = itemIndex + 1;
|
|
227
|
+
poppedGroup = existingGroup.splice(0, numItemsToRemove);
|
|
228
|
+
originalColumn.splice(groupIndex, 1, poppedGroup, existingGroup);
|
|
229
|
+
}
|
|
230
|
+
const sanitizedConfiguration = this.removeEmptyGroupsFromToolbar(modifiedConfiguration);
|
|
231
|
+
return this.writeToFile([], sanitizedConfiguration);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
protected removeEmptyGroupsFromToolbar(
|
|
238
|
+
toolbarItems: DeflatedToolbarTree | undefined,
|
|
239
|
+
): DeflatedToolbarTree | undefined {
|
|
240
|
+
if (toolbarItems) {
|
|
241
|
+
const modifiedConfiguration = deepClone(toolbarItems);
|
|
242
|
+
const columns = [ToolbarAlignment.LEFT, ToolbarAlignment.CENTER, ToolbarAlignment.RIGHT];
|
|
243
|
+
columns.forEach(column => {
|
|
244
|
+
const groups = toolbarItems.items[column];
|
|
245
|
+
groups.forEach((group, index) => {
|
|
246
|
+
if (group.length === 0) {
|
|
247
|
+
modifiedConfiguration.items[column].splice(index, 1);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
return modifiedConfiguration;
|
|
252
|
+
}
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async clearAll(): Promise<boolean> {
|
|
257
|
+
if (this.model) {
|
|
258
|
+
const textModel = this.model.textEditorModel;
|
|
259
|
+
await this.monacoWorkspace.applyBackgroundEdit(this.model, [
|
|
260
|
+
{
|
|
261
|
+
range: textModel.getFullModelRange(),
|
|
262
|
+
// eslint-disable-next-line no-null/no-null
|
|
263
|
+
text: null,
|
|
264
|
+
forceMoveMarkers: false,
|
|
265
|
+
},
|
|
266
|
+
]);
|
|
267
|
+
}
|
|
268
|
+
this.toolbarItemsUpdatedEmitter.fire();
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
protected async writeToFile(path: jsoncParser.JSONPath, value: unknown, insertion = false): Promise<boolean> {
|
|
273
|
+
if (this.model) {
|
|
274
|
+
try {
|
|
275
|
+
const content = this.model.getText().trim();
|
|
276
|
+
const textModel = this.model.textEditorModel;
|
|
277
|
+
const editOperations: monaco.editor.IIdentifiedSingleEditOperation[] = [];
|
|
278
|
+
const { insertSpaces, tabSize, defaultEOL } = textModel.getOptions();
|
|
279
|
+
for (const edit of jsoncParser.modify(content, path, value, {
|
|
280
|
+
isArrayInsertion: insertion,
|
|
281
|
+
formattingOptions: {
|
|
282
|
+
insertSpaces,
|
|
283
|
+
tabSize,
|
|
284
|
+
eol: defaultEOL === monaco.editor.DefaultEndOfLine.LF ? '\n' : '\r\n',
|
|
285
|
+
},
|
|
286
|
+
})) {
|
|
287
|
+
const start = textModel.getPositionAt(edit.offset);
|
|
288
|
+
const end = textModel.getPositionAt(edit.offset + edit.length);
|
|
289
|
+
editOperations.push({
|
|
290
|
+
range: monaco.Range.fromPositions(start, end),
|
|
291
|
+
// eslint-disable-next-line no-null/no-null
|
|
292
|
+
text: edit.content || null,
|
|
293
|
+
forceMoveMarkers: false,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
await this.monacoWorkspace.applyBackgroundEdit(this.model, editOperations, false);
|
|
297
|
+
await this.model.save();
|
|
298
|
+
return true;
|
|
299
|
+
} catch (e) {
|
|
300
|
+
const message = nls.localize('theia/toolbar/failedUpdate', "Failed to update the value of '{0}' in '{1}'.", path.join('.'), this.USER_TOOLBAR_URI.path.toString());
|
|
301
|
+
this.messageService.error(nls.localize('theia/toolbar/jsonError', TOOLBAR_BAD_JSON_ERROR_MESSAGE));
|
|
302
|
+
console.error(`${message}`, e);
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
protected parseContent(fileContent: string): DeflatedToolbarTree | undefined {
|
|
310
|
+
const rawConfig = this.parse(fileContent);
|
|
311
|
+
if (!isToolbarPreferences(rawConfig)) {
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
return rawConfig;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
protected parse(fileContent: string): DeflatedToolbarTree | undefined {
|
|
318
|
+
let strippedContent = fileContent.trim();
|
|
319
|
+
if (!strippedContent) {
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
322
|
+
strippedContent = jsoncParser.stripComments(strippedContent);
|
|
323
|
+
return jsoncParser.parse(strippedContent);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async openOrCreateJSONFile(state: ToolbarTreeSchema, doOpen = false): Promise<Widget | undefined> {
|
|
327
|
+
const fileExists = await this.fileService.exists(this.USER_TOOLBAR_URI);
|
|
328
|
+
let doWriteStateToFile = false;
|
|
329
|
+
if (fileExists) {
|
|
330
|
+
const fileContent = await this.fileService.read(this.USER_TOOLBAR_URI);
|
|
331
|
+
if (fileContent.value.trim() === '') {
|
|
332
|
+
doWriteStateToFile = true;
|
|
333
|
+
}
|
|
334
|
+
} else {
|
|
335
|
+
await this.fileService.create(this.USER_TOOLBAR_URI);
|
|
336
|
+
doWriteStateToFile = true;
|
|
337
|
+
}
|
|
338
|
+
if (doWriteStateToFile) {
|
|
339
|
+
await this.writeToFile([], state);
|
|
340
|
+
}
|
|
341
|
+
this.readConfiguration();
|
|
342
|
+
if (doOpen) {
|
|
343
|
+
const widget = await this.editorManager.open(this.USER_TOOLBAR_URI);
|
|
344
|
+
return widget;
|
|
345
|
+
}
|
|
346
|
+
return undefined;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
dispose(): void {
|
|
350
|
+
this.toDispose.dispose();
|
|
351
|
+
}
|
|
352
|
+
}
|