Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as DOM from '../../../../base/browser/dom.js';
7
import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';
8
import { ActionBar, ActionsOrientation } from '../../../../base/browser/ui/actionbar/actionbar.js';
9
import { BaseActionViewItem, IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js';
10
import type { IManagedHover } from '../../../../base/browser/ui/hover/hover.js';
11
import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';
12
import { HistoryInputBox, IHistoryInputOptions } from '../../../../base/browser/ui/inputbox/inputBox.js';
13
import { Widget } from '../../../../base/browser/ui/widget.js';
14
import { Action, IAction } from '../../../../base/common/actions.js';
15
import { Emitter, Event } from '../../../../base/common/event.js';
16
import { MarkdownString } from '../../../../base/common/htmlContent.js';
17
import { KeyCode } from '../../../../base/common/keyCodes.js';
18
import { Disposable } from '../../../../base/common/lifecycle.js';
19
import { Schemas } from '../../../../base/common/network.js';
20
import { isEqual } from '../../../../base/common/resources.js';
21
import { ThemeIcon } from '../../../../base/common/themables.js';
22
import { URI } from '../../../../base/common/uri.js';
23
import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from '../../../../editor/browser/editorBrowser.js';
24
import { IEditorDecorationsCollection } from '../../../../editor/common/editorCommon.js';
25
import { ILanguageService } from '../../../../editor/common/languages/language.js';
26
import { IModelDeltaDecoration, TrackedRangeStickiness } from '../../../../editor/common/model.js';
27
import { localize } from '../../../../nls.js';
28
import { ConfigurationTarget } from '../../../../platform/configuration/common/configuration.js';
29
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
30
import { IContextMenuService, IContextViewService } from '../../../../platform/contextview/browser/contextView.js';
31
import { ContextScopedHistoryInputBox } from '../../../../platform/history/browser/contextScopedHistoryWidget.js';
32
import { showHistoryKeybindingHint } from '../../../../platform/history/browser/historyWidgetKeybindingHint.js';
33
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
34
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
35
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
36
import { ILabelService } from '../../../../platform/label/common/label.js';
37
import { asCssVariable, badgeBackground, badgeForeground, contrastBorder } from '../../../../platform/theme/common/colorRegistry.js';
38
import { isWorkspaceFolder, IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from '../../../../platform/workspace/common/workspace.js';
39
import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js';
40
import { settingsEditIcon, settingsScopeDropDownIcon } from './preferencesIcons.js';
41
42
export class FolderSettingsActionViewItem extends BaseActionViewItem {
43
44
private _folder: IWorkspaceFolder | null;
45
private _folderSettingCounts = new Map<string, number>();
46
47
private container!: HTMLElement;
48
private anchorElement!: HTMLElement;
49
private anchorElementHover!: IManagedHover;
50
private labelElement!: HTMLElement;
51
private detailsElement!: HTMLElement;
52
private dropDownElement!: HTMLElement;
53
54
constructor(
55
action: IAction,
56
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
57
@IContextMenuService private readonly contextMenuService: IContextMenuService,
58
@IHoverService private readonly hoverService: IHoverService,
59
) {
60
super(null, action);
61
const workspace = this.contextService.getWorkspace();
62
this._folder = workspace.folders.length === 1 ? workspace.folders[0] : null;
63
this._register(this.contextService.onDidChangeWorkspaceFolders(() => this.onWorkspaceFoldersChanged()));
64
}
65
66
get folder(): IWorkspaceFolder | null {
67
return this._folder;
68
}
69
70
set folder(folder: IWorkspaceFolder | null) {
71
this._folder = folder;
72
this.update();
73
}
74
75
setCount(settingsTarget: URI, count: number): void {
76
const workspaceFolder = this.contextService.getWorkspaceFolder(settingsTarget);
77
if (!workspaceFolder) {
78
throw new Error('unknown folder');
79
}
80
const folder = workspaceFolder.uri;
81
this._folderSettingCounts.set(folder.toString(), count);
82
this.update();
83
}
84
85
override render(container: HTMLElement): void {
86
this.element = container;
87
88
this.container = container;
89
this.labelElement = DOM.$('.action-title');
90
this.detailsElement = DOM.$('.action-details');
91
this.dropDownElement = DOM.$('.dropdown-icon.hide' + ThemeIcon.asCSSSelector(settingsScopeDropDownIcon));
92
this.anchorElement = DOM.$('a.action-label.folder-settings', {
93
role: 'button',
94
'aria-haspopup': 'true',
95
'tabindex': '0'
96
}, this.labelElement, this.detailsElement, this.dropDownElement);
97
this.anchorElementHover = this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.anchorElement, ''));
98
this._register(DOM.addDisposableListener(this.anchorElement, DOM.EventType.MOUSE_DOWN, e => DOM.EventHelper.stop(e)));
99
this._register(DOM.addDisposableListener(this.anchorElement, DOM.EventType.CLICK, e => this.onClick(e)));
100
this._register(DOM.addDisposableListener(this.container, DOM.EventType.KEY_UP, e => this.onKeyUp(e)));
101
102
DOM.append(this.container, this.anchorElement);
103
104
this.update();
105
}
106
107
private onKeyUp(event: KeyboardEvent): void {
108
const keyboardEvent = new StandardKeyboardEvent(event);
109
switch (keyboardEvent.keyCode) {
110
case KeyCode.Enter:
111
case KeyCode.Space:
112
this.onClick(event);
113
return;
114
}
115
}
116
117
override onClick(event: DOM.EventLike): void {
118
DOM.EventHelper.stop(event, true);
119
if (!this.folder || this._action.checked) {
120
this.showMenu();
121
} else {
122
this._action.run(this._folder);
123
}
124
}
125
126
protected override updateEnabled(): void {
127
this.update();
128
}
129
130
protected override updateChecked(): void {
131
this.update();
132
}
133
134
private onWorkspaceFoldersChanged(): void {
135
const oldFolder = this._folder;
136
const workspace = this.contextService.getWorkspace();
137
if (oldFolder) {
138
this._folder = workspace.folders.filter(folder => isEqual(folder.uri, oldFolder.uri))[0] || workspace.folders[0];
139
}
140
this._folder = this._folder ? this._folder : workspace.folders.length === 1 ? workspace.folders[0] : null;
141
142
this.update();
143
144
if (this._action.checked) {
145
this._action.run(this._folder);
146
}
147
}
148
149
private update(): void {
150
let total = 0;
151
this._folderSettingCounts.forEach(n => total += n);
152
153
const workspace = this.contextService.getWorkspace();
154
if (this._folder) {
155
this.labelElement.textContent = this._folder.name;
156
this.anchorElementHover.update(this._folder.name);
157
const detailsText = this.labelWithCount(this._action.label, total);
158
this.detailsElement.textContent = detailsText;
159
this.dropDownElement.classList.toggle('hide', workspace.folders.length === 1 || !this._action.checked);
160
} else {
161
const labelText = this.labelWithCount(this._action.label, total);
162
this.labelElement.textContent = labelText;
163
this.detailsElement.textContent = '';
164
this.anchorElementHover.update(this._action.label);
165
this.dropDownElement.classList.remove('hide');
166
}
167
168
this.anchorElement.classList.toggle('checked', this._action.checked);
169
this.container.classList.toggle('disabled', !this._action.enabled);
170
}
171
172
private showMenu(): void {
173
this.contextMenuService.showContextMenu({
174
getAnchor: () => this.container,
175
getActions: () => this.getDropdownMenuActions(),
176
getActionViewItem: () => undefined,
177
onHide: () => {
178
this.anchorElement.blur();
179
}
180
});
181
}
182
183
private getDropdownMenuActions(): IAction[] {
184
const actions: IAction[] = [];
185
const workspaceFolders = this.contextService.getWorkspace().folders;
186
if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE && workspaceFolders.length > 0) {
187
actions.push(...workspaceFolders.map((folder, index) => {
188
const folderCount = this._folderSettingCounts.get(folder.uri.toString());
189
return {
190
id: 'folderSettingsTarget' + index,
191
label: this.labelWithCount(folder.name, folderCount),
192
tooltip: this.labelWithCount(folder.name, folderCount),
193
checked: !!this.folder && isEqual(this.folder.uri, folder.uri),
194
enabled: true,
195
class: undefined,
196
run: () => this._action.run(folder)
197
};
198
}));
199
}
200
return actions;
201
}
202
203
private labelWithCount(label: string, count: number | undefined): string {
204
// Append the count if it's >0 and not undefined
205
if (count) {
206
label += ` (${count})`;
207
}
208
209
return label;
210
}
211
}
212
213
export type SettingsTarget = ConfigurationTarget.APPLICATION | ConfigurationTarget.USER_LOCAL | ConfigurationTarget.USER_REMOTE | ConfigurationTarget.WORKSPACE | URI;
214
215
export interface ISettingsTargetsWidgetOptions {
216
enableRemoteSettings?: boolean;
217
}
218
219
export class SettingsTargetsWidget extends Widget {
220
221
private settingsSwitcherBar!: ActionBar;
222
private userLocalSettings!: Action;
223
private userRemoteSettings!: Action;
224
private workspaceSettings!: Action;
225
private folderSettingsAction!: Action;
226
private folderSettings!: FolderSettingsActionViewItem;
227
private options: ISettingsTargetsWidgetOptions;
228
229
private _settingsTarget: SettingsTarget | null = null;
230
231
private readonly _onDidTargetChange = this._register(new Emitter<SettingsTarget>());
232
readonly onDidTargetChange: Event<SettingsTarget> = this._onDidTargetChange.event;
233
234
constructor(
235
parent: HTMLElement,
236
options: ISettingsTargetsWidgetOptions | undefined,
237
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
238
@IInstantiationService private readonly instantiationService: IInstantiationService,
239
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
240
@ILabelService private readonly labelService: ILabelService,
241
@ILanguageService private readonly languageService: ILanguageService
242
) {
243
super();
244
this.options = options ?? {};
245
this.create(parent);
246
this._register(this.contextService.onDidChangeWorkbenchState(() => this.onWorkbenchStateChanged()));
247
this._register(this.contextService.onDidChangeWorkspaceFolders(() => this.update()));
248
}
249
250
private resetLabels() {
251
const remoteAuthority = this.environmentService.remoteAuthority;
252
const hostLabel = remoteAuthority && this.labelService.getHostLabel(Schemas.vscodeRemote, remoteAuthority);
253
this.userLocalSettings.label = localize('userSettings', "User");
254
this.userRemoteSettings.label = localize('userSettingsRemote', "Remote") + (hostLabel ? ` [${hostLabel}]` : '');
255
this.workspaceSettings.label = localize('workspaceSettings', "Workspace");
256
this.folderSettingsAction.label = localize('folderSettings', "Folder");
257
}
258
259
private create(parent: HTMLElement): void {
260
const settingsTabsWidget = DOM.append(parent, DOM.$('.settings-tabs-widget'));
261
this.settingsSwitcherBar = this._register(new ActionBar(settingsTabsWidget, {
262
orientation: ActionsOrientation.HORIZONTAL,
263
focusOnlyEnabledItems: true,
264
ariaLabel: localize('settingsSwitcherBarAriaLabel', "Settings Switcher"),
265
ariaRole: 'tablist',
266
actionViewItemProvider: (action: IAction, options: IActionViewItemOptions) => action.id === 'folderSettings' ? this.folderSettings : undefined
267
}));
268
269
this.userLocalSettings = this._register(new Action('userSettings', '', '.settings-tab', true, () => this.updateTarget(ConfigurationTarget.USER_LOCAL)));
270
this.userLocalSettings.tooltip = localize('userSettings', "User");
271
272
this.userRemoteSettings = this._register(new Action('userSettingsRemote', '', '.settings-tab', true, () => this.updateTarget(ConfigurationTarget.USER_REMOTE)));
273
const remoteAuthority = this.environmentService.remoteAuthority;
274
const hostLabel = remoteAuthority && this.labelService.getHostLabel(Schemas.vscodeRemote, remoteAuthority);
275
this.userRemoteSettings.tooltip = localize('userSettingsRemote', "Remote") + (hostLabel ? ` [${hostLabel}]` : '');
276
277
this.workspaceSettings = this._register(new Action('workspaceSettings', '', '.settings-tab', false, () => this.updateTarget(ConfigurationTarget.WORKSPACE)));
278
279
this.folderSettingsAction = this._register(new Action('folderSettings', '', '.settings-tab', false, async folder => {
280
this.updateTarget(isWorkspaceFolder(folder) ? folder.uri : ConfigurationTarget.USER_LOCAL);
281
}));
282
this.folderSettings = this._register(this.instantiationService.createInstance(FolderSettingsActionViewItem, this.folderSettingsAction));
283
284
this.resetLabels();
285
this.update();
286
287
this.settingsSwitcherBar.push([this.userLocalSettings, this.userRemoteSettings, this.workspaceSettings, this.folderSettingsAction]);
288
}
289
290
get settingsTarget(): SettingsTarget | null {
291
return this._settingsTarget;
292
}
293
294
set settingsTarget(settingsTarget: SettingsTarget | null) {
295
this._settingsTarget = settingsTarget;
296
this.userLocalSettings.checked = ConfigurationTarget.USER_LOCAL === this.settingsTarget;
297
this.userRemoteSettings.checked = ConfigurationTarget.USER_REMOTE === this.settingsTarget;
298
this.workspaceSettings.checked = ConfigurationTarget.WORKSPACE === this.settingsTarget;
299
if (this.settingsTarget instanceof URI) {
300
this.folderSettings.action.checked = true;
301
this.folderSettings.folder = this.contextService.getWorkspaceFolder(this.settingsTarget as URI);
302
} else {
303
this.folderSettings.action.checked = false;
304
}
305
}
306
307
setResultCount(settingsTarget: SettingsTarget, count: number): void {
308
if (settingsTarget === ConfigurationTarget.WORKSPACE) {
309
let label = localize('workspaceSettings', "Workspace");
310
if (count) {
311
label += ` (${count})`;
312
}
313
314
this.workspaceSettings.label = label;
315
} else if (settingsTarget === ConfigurationTarget.USER_LOCAL) {
316
let label = localize('userSettings', "User");
317
if (count) {
318
label += ` (${count})`;
319
}
320
321
this.userLocalSettings.label = label;
322
} else if (settingsTarget instanceof URI) {
323
this.folderSettings.setCount(settingsTarget, count);
324
}
325
}
326
327
updateLanguageFilterIndicators(filter: string | undefined) {
328
this.resetLabels();
329
if (filter) {
330
const languageToUse = this.languageService.getLanguageName(filter);
331
if (languageToUse) {
332
const languageSuffix = ` [${languageToUse}]`;
333
this.userLocalSettings.label += languageSuffix;
334
this.userRemoteSettings.label += languageSuffix;
335
this.workspaceSettings.label += languageSuffix;
336
this.folderSettingsAction.label += languageSuffix;
337
}
338
}
339
}
340
341
private onWorkbenchStateChanged(): void {
342
this.folderSettings.folder = null;
343
this.update();
344
if (this.settingsTarget === ConfigurationTarget.WORKSPACE && this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) {
345
this.updateTarget(ConfigurationTarget.USER_LOCAL);
346
}
347
}
348
349
updateTarget(settingsTarget: SettingsTarget): Promise<void> {
350
const isSameTarget = this.settingsTarget === settingsTarget ||
351
settingsTarget instanceof URI &&
352
this.settingsTarget instanceof URI &&
353
isEqual(this.settingsTarget, settingsTarget);
354
355
if (!isSameTarget) {
356
this.settingsTarget = settingsTarget;
357
this._onDidTargetChange.fire(this.settingsTarget);
358
}
359
360
return Promise.resolve(undefined);
361
}
362
363
private async update(): Promise<void> {
364
this.settingsSwitcherBar.domNode.classList.toggle('empty-workbench', this.contextService.getWorkbenchState() === WorkbenchState.EMPTY);
365
this.userRemoteSettings.enabled = !!(this.options.enableRemoteSettings && this.environmentService.remoteAuthority);
366
this.workspaceSettings.enabled = this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY;
367
this.folderSettings.action.enabled = this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE && this.contextService.getWorkspace().folders.length > 0;
368
369
this.workspaceSettings.tooltip = localize('workspaceSettings', "Workspace");
370
}
371
}
372
373
export interface SearchOptions extends IHistoryInputOptions {
374
focusKey?: IContextKey<boolean>;
375
showResultCount?: boolean;
376
ariaLive?: string;
377
ariaLabelledBy?: string;
378
}
379
380
export class SearchWidget extends Widget {
381
382
domNode!: HTMLElement;
383
384
private countElement!: HTMLElement;
385
private searchContainer!: HTMLElement;
386
inputBox!: HistoryInputBox;
387
private controlsDiv!: HTMLElement;
388
389
private readonly _onDidChange: Emitter<string> = this._register(new Emitter<string>());
390
public get onDidChange(): Event<string> { return this._onDidChange.event; }
391
392
private readonly _onFocus: Emitter<void> = this._register(new Emitter<void>());
393
public get onFocus(): Event<void> { return this._onFocus.event; }
394
395
constructor(parent: HTMLElement, protected options: SearchOptions,
396
@IContextViewService private readonly contextViewService: IContextViewService,
397
@IInstantiationService protected instantiationService: IInstantiationService,
398
@IContextKeyService private readonly contextKeyService: IContextKeyService,
399
@IKeybindingService protected readonly keybindingService: IKeybindingService
400
) {
401
super();
402
this.create(parent);
403
}
404
405
private create(parent: HTMLElement) {
406
this.domNode = DOM.append(parent, DOM.$('div.settings-header-widget'));
407
this.createSearchContainer(DOM.append(this.domNode, DOM.$('div.settings-search-container')));
408
this.controlsDiv = DOM.append(this.domNode, DOM.$('div.settings-search-controls'));
409
410
if (this.options.showResultCount) {
411
this.countElement = DOM.append(this.controlsDiv, DOM.$('.settings-count-widget'));
412
413
this.countElement.style.backgroundColor = asCssVariable(badgeBackground);
414
this.countElement.style.color = asCssVariable(badgeForeground);
415
this.countElement.style.border = `1px solid ${asCssVariable(contrastBorder)}`;
416
}
417
418
this.inputBox.inputElement.setAttribute('aria-live', this.options.ariaLive || 'off');
419
if (this.options.ariaLabelledBy) {
420
this.inputBox.inputElement.setAttribute('aria-labelledBy', this.options.ariaLabelledBy);
421
}
422
const focusTracker = this._register(DOM.trackFocus(this.inputBox.inputElement));
423
this._register(focusTracker.onDidFocus(() => this._onFocus.fire()));
424
425
const focusKey = this.options.focusKey;
426
if (focusKey) {
427
this._register(focusTracker.onDidFocus(() => focusKey.set(true)));
428
this._register(focusTracker.onDidBlur(() => focusKey.set(false)));
429
}
430
}
431
432
private createSearchContainer(searchContainer: HTMLElement) {
433
this.searchContainer = searchContainer;
434
const searchInput = DOM.append(this.searchContainer, DOM.$('div.settings-search-input'));
435
this.inputBox = this._register(this.createInputBox(searchInput));
436
this._register(this.inputBox.onDidChange(value => this._onDidChange.fire(value)));
437
}
438
439
protected createInputBox(parent: HTMLElement): HistoryInputBox {
440
const showHistoryHint = () => showHistoryKeybindingHint(this.keybindingService);
441
return new ContextScopedHistoryInputBox(parent, this.contextViewService, { ...this.options, showHistoryHint }, this.contextKeyService);
442
}
443
444
showMessage(message: string): void {
445
// Avoid setting the aria-label unnecessarily, the screenreader will read the count every time it's set, since it's aria-live:assertive. #50968
446
if (this.countElement && message !== this.countElement.textContent) {
447
this.countElement.textContent = message;
448
this.inputBox.inputElement.setAttribute('aria-label', message);
449
this.inputBox.inputElement.style.paddingRight = this.getControlsWidth() + 'px';
450
}
451
}
452
453
layout(dimension: DOM.Dimension) {
454
if (dimension.width < 400) {
455
this.countElement?.classList.add('hide');
456
457
this.inputBox.inputElement.style.paddingRight = '0px';
458
} else {
459
this.countElement?.classList.remove('hide');
460
461
this.inputBox.inputElement.style.paddingRight = this.getControlsWidth() + 'px';
462
}
463
}
464
465
private getControlsWidth(): number {
466
const countWidth = this.countElement ? DOM.getTotalWidth(this.countElement) : 0;
467
return countWidth + 20;
468
}
469
470
focus() {
471
this.inputBox.focus();
472
if (this.getValue()) {
473
this.inputBox.select();
474
}
475
}
476
477
hasFocus(): boolean {
478
return this.inputBox.hasFocus();
479
}
480
481
clear() {
482
this.inputBox.value = '';
483
}
484
485
getValue(): string {
486
return this.inputBox.value;
487
}
488
489
setValue(value: string): string {
490
return this.inputBox.value = value;
491
}
492
493
override dispose(): void {
494
this.options.focusKey?.set(false);
495
super.dispose();
496
}
497
}
498
499
export class EditPreferenceWidget<T> extends Disposable {
500
501
private _line: number = -1;
502
private _preferences: T[] = [];
503
504
private readonly _editPreferenceDecoration: IEditorDecorationsCollection;
505
506
private readonly _onClick = this._register(new Emitter<IEditorMouseEvent>());
507
readonly onClick: Event<IEditorMouseEvent> = this._onClick.event;
508
509
constructor(private editor: ICodeEditor) {
510
super();
511
this._editPreferenceDecoration = this.editor.createDecorationsCollection();
512
this._register(this.editor.onMouseDown((e: IEditorMouseEvent) => {
513
if (e.target.type !== MouseTargetType.GUTTER_GLYPH_MARGIN || e.target.detail.isAfterLines || !this.isVisible()) {
514
return;
515
}
516
this._onClick.fire(e);
517
}));
518
}
519
520
get preferences(): T[] {
521
return this._preferences;
522
}
523
524
getLine(): number {
525
return this._line;
526
}
527
528
show(line: number, hoverMessage: string, preferences: T[]): void {
529
this._preferences = preferences;
530
const newDecoration: IModelDeltaDecoration[] = [];
531
this._line = line;
532
newDecoration.push({
533
options: {
534
description: 'edit-preference-widget-decoration',
535
glyphMarginClassName: ThemeIcon.asClassName(settingsEditIcon),
536
glyphMarginHoverMessage: new MarkdownString().appendText(hoverMessage),
537
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
538
},
539
range: {
540
startLineNumber: line,
541
startColumn: 1,
542
endLineNumber: line,
543
endColumn: 1
544
}
545
});
546
this._editPreferenceDecoration.set(newDecoration);
547
}
548
549
hide(): void {
550
this._editPreferenceDecoration.clear();
551
}
552
553
isVisible(): boolean {
554
return this._editPreferenceDecoration.length > 0;
555
}
556
557
override dispose(): void {
558
this.hide();
559
super.dispose();
560
}
561
}
562
563