Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/browser/parts/views/viewPane.ts
5272 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 './media/paneviewlet.css';
7
import * as nls from '../../../../nls.js';
8
import { Event, Emitter } from '../../../../base/common/event.js';
9
import { asCssVariable, foreground } from '../../../../platform/theme/common/colorRegistry.js';
10
import { after, append, $, trackFocus, EventType, addDisposableListener, Dimension, reset, isAncestorOfActiveElement, isActiveElement } from '../../../../base/browser/dom.js';
11
import { createCSSRule } from '../../../../base/browser/domStylesheets.js';
12
import { asCssValueWithDefault, asCSSUrl } from '../../../../base/browser/cssValue.js';
13
import { DisposableMap, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';
14
import { Action, IAction, IActionRunner } from '../../../../base/common/actions.js';
15
import { ActionsOrientation, IActionViewItem, prepareActions } from '../../../../base/browser/ui/actionbar/actionbar.js';
16
import { Registry } from '../../../../platform/registry/common/platform.js';
17
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
18
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
19
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
20
import { ThemeIcon } from '../../../../base/common/themables.js';
21
import { IPaneOptions, Pane, IPaneStyles } from '../../../../base/browser/ui/splitview/paneview.js';
22
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
23
import { Extensions as ViewContainerExtensions, IView, IViewDescriptorService, ViewContainerLocation, IViewsRegistry, IViewContentDescriptor, defaultViewIcon, ViewContainerLocationToString } from '../../../common/views.js';
24
import { IViewsService } from '../../../services/views/common/viewsService.js';
25
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
26
import { assertReturnsDefined, PartialExcept } from '../../../../base/common/types.js';
27
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
28
import { MenuId, Action2, IAction2Options, SubmenuItemAction } from '../../../../platform/actions/common/actions.js';
29
import { createActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';
30
import { parseLinkedText } from '../../../../base/common/linkedText.js';
31
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
32
import { Button } from '../../../../base/browser/ui/button/button.js';
33
import { Link } from '../../../../platform/opener/browser/link.js';
34
import { Orientation } from '../../../../base/browser/ui/sash/sash.js';
35
import { ProgressBar } from '../../../../base/browser/ui/progressbar/progressbar.js';
36
import { AbstractProgressScope, ScopedProgressIndicator } from '../../../services/progress/browser/progressIndicator.js';
37
import { IProgressIndicator } from '../../../../platform/progress/common/progress.js';
38
import { DomScrollableElement } from '../../../../base/browser/ui/scrollbar/scrollableElement.js';
39
import { ScrollbarVisibility } from '../../../../base/common/scrollable.js';
40
import { URI } from '../../../../base/common/uri.js';
41
import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js';
42
import { Codicon } from '../../../../base/common/codicons.js';
43
import { IDropdownMenuActionViewItemOptions } from '../../../../base/browser/ui/dropdown/dropdownActionViewItem.js';
44
import { WorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js';
45
import { FilterWidget, IFilterWidgetOptions } from './viewFilter.js';
46
import { BaseActionViewItem } from '../../../../base/browser/ui/actionbar/actionViewItems.js';
47
import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js';
48
import { defaultButtonStyles, defaultProgressBarStyles } from '../../../../platform/theme/browser/defaultStyles.js';
49
import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';
50
import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js';
51
import type { IManagedHover } from '../../../../base/browser/ui/hover/hover.js';
52
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
53
import { IListStyles } from '../../../../base/browser/ui/list/listWidget.js';
54
import { PANEL_BACKGROUND, PANEL_SECTION_DRAG_AND_DROP_BACKGROUND, PANEL_STICKY_SCROLL_BACKGROUND, PANEL_STICKY_SCROLL_BORDER, PANEL_STICKY_SCROLL_SHADOW, SIDE_BAR_BACKGROUND, SIDE_BAR_DRAG_AND_DROP_BACKGROUND, SIDE_BAR_STICKY_SCROLL_BACKGROUND, SIDE_BAR_STICKY_SCROLL_BORDER, SIDE_BAR_STICKY_SCROLL_SHADOW } from '../../../common/theme.js';
55
import { IAccessibleViewInformationService } from '../../../services/accessibility/common/accessibleViewInformationService.js';
56
import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js';
57
import { ViewMenuActions } from './viewMenuActions.js';
58
59
export enum ViewPaneShowActions {
60
/** Show the actions when the view is hovered. This is the default behavior. */
61
Default,
62
63
/** Always shows the actions when the view is expanded */
64
WhenExpanded,
65
66
/** Always shows the actions */
67
Always,
68
}
69
70
export interface IViewPaneOptions extends IPaneOptions {
71
readonly id: string;
72
readonly showActions?: ViewPaneShowActions;
73
readonly titleMenuId?: MenuId;
74
readonly donotForwardArgs?: boolean;
75
// The title of the container pane when it is merged with the view container
76
readonly singleViewPaneContainerTitle?: string;
77
}
78
79
export interface IFilterViewPaneOptions extends IViewPaneOptions {
80
filterOptions: IFilterWidgetOptions;
81
}
82
83
export const VIEWPANE_FILTER_ACTION = new Action('viewpane.action.filter');
84
85
const viewPaneContainerExpandedIcon = registerIcon('view-pane-container-expanded', Codicon.chevronDown, nls.localize('viewPaneContainerExpandedIcon', 'Icon for an expanded view pane container.'));
86
const viewPaneContainerCollapsedIcon = registerIcon('view-pane-container-collapsed', Codicon.chevronRight, nls.localize('viewPaneContainerCollapsedIcon', 'Icon for a collapsed view pane container.'));
87
88
const viewsRegistry = Registry.as<IViewsRegistry>(ViewContainerExtensions.ViewsRegistry);
89
90
interface IItem {
91
readonly descriptor: IViewContentDescriptor;
92
visible: boolean;
93
}
94
95
interface IViewWelcomeDelegate {
96
readonly id: string;
97
readonly onDidChangeViewWelcomeState: Event<void>;
98
shouldShowWelcome(): boolean;
99
}
100
101
class ViewWelcomeController {
102
103
private defaultItem: IItem | undefined;
104
private items: IItem[] = [];
105
106
get enabled(): boolean { return this._enabled; }
107
private _enabled: boolean = false;
108
private element: HTMLElement | undefined;
109
private scrollableElement: DomScrollableElement | undefined;
110
private _wide: boolean = false;
111
112
private readonly disposables = new DisposableStore();
113
private readonly enabledDisposables = this.disposables.add(new DisposableStore());
114
private readonly renderDisposables = this.disposables.add(new DisposableStore());
115
116
constructor(
117
private readonly container: HTMLElement,
118
private readonly delegate: IViewWelcomeDelegate,
119
@IInstantiationService private instantiationService: IInstantiationService,
120
@IOpenerService protected openerService: IOpenerService,
121
@IContextKeyService private contextKeyService: IContextKeyService,
122
@ILifecycleService lifecycleService: ILifecycleService
123
) {
124
this.disposables.add(Event.runAndSubscribe(this.delegate.onDidChangeViewWelcomeState, () => this.onDidChangeViewWelcomeState()));
125
this.disposables.add(lifecycleService.onWillShutdown(() => this.dispose())); // Fixes https://github.com/microsoft/vscode/issues/208878
126
}
127
128
layout(height: number, width: number) {
129
if (!this._enabled) {
130
return;
131
}
132
133
this.element!.style.height = `${height}px`;
134
this.element!.style.width = `${width}px`;
135
this._wide = width > 640;
136
this.element!.classList.toggle('wide', this._wide);
137
this.scrollableElement!.scanDomNode();
138
}
139
140
focus() {
141
if (!this._enabled) {
142
return;
143
}
144
145
this.element!.focus();
146
}
147
148
private onDidChangeViewWelcomeState(): void {
149
const enabled = this.delegate.shouldShowWelcome();
150
151
if (this._enabled === enabled) {
152
return;
153
}
154
155
this._enabled = enabled;
156
157
if (!enabled) {
158
this.enabledDisposables.clear();
159
return;
160
}
161
162
this.container.classList.add('welcome');
163
const viewWelcomeContainer = append(this.container, $('.welcome-view'));
164
this.element = $('.welcome-view-content', { tabIndex: 0, role: 'region', 'aria-label': nls.localize('welcomeViewAriaLabel', "Welcome") });
165
if (this._wide) {
166
this.element.classList.add('wide');
167
}
168
this.scrollableElement = new DomScrollableElement(this.element, { alwaysConsumeMouseWheel: true, horizontal: ScrollbarVisibility.Hidden, vertical: ScrollbarVisibility.Visible, });
169
append(viewWelcomeContainer, this.scrollableElement.getDomNode());
170
171
this.enabledDisposables.add(toDisposable(() => {
172
this.container.classList.remove('welcome');
173
this.scrollableElement!.dispose();
174
viewWelcomeContainer.remove();
175
this.scrollableElement = undefined;
176
this.element = undefined;
177
}));
178
179
this.contextKeyService.onDidChangeContext(this.onDidChangeContext, this, this.enabledDisposables);
180
Event.chain(viewsRegistry.onDidChangeViewWelcomeContent, $ => $.filter(id => id === this.delegate.id))
181
(this.onDidChangeViewWelcomeContent, this, this.enabledDisposables);
182
this.onDidChangeViewWelcomeContent();
183
}
184
185
private onDidChangeViewWelcomeContent(): void {
186
const descriptors = viewsRegistry.getViewWelcomeContent(this.delegate.id);
187
188
this.items = [];
189
190
for (const descriptor of descriptors) {
191
if (descriptor.when === 'default') {
192
this.defaultItem = { descriptor, visible: true };
193
} else {
194
const visible = descriptor.when ? this.contextKeyService.contextMatchesRules(descriptor.when) : true;
195
this.items.push({ descriptor, visible });
196
}
197
}
198
199
this.render();
200
}
201
202
private onDidChangeContext(): void {
203
let didChange = false;
204
205
for (const item of this.items) {
206
if (!item.descriptor.when || item.descriptor.when === 'default') {
207
continue;
208
}
209
210
const visible = this.contextKeyService.contextMatchesRules(item.descriptor.when);
211
212
if (item.visible === visible) {
213
continue;
214
}
215
216
item.visible = visible;
217
didChange = true;
218
}
219
220
if (didChange) {
221
this.render();
222
}
223
}
224
225
private render(): void {
226
this.renderDisposables.clear();
227
this.element!.textContent = '';
228
229
const contents = this.getContentDescriptors();
230
231
if (contents.length === 0) {
232
this.container.classList.remove('welcome');
233
this.scrollableElement!.scanDomNode();
234
return;
235
}
236
237
let buttonsCount = 0;
238
for (const { content, precondition, renderSecondaryButtons } of contents) {
239
const lines = content.split('\n');
240
241
for (let line of lines) {
242
line = line.trim();
243
244
if (!line) {
245
continue;
246
}
247
248
const linkedText = parseLinkedText(line);
249
250
if (linkedText.nodes.length === 1 && typeof linkedText.nodes[0] !== 'string') {
251
const node = linkedText.nodes[0];
252
const buttonContainer = append(this.element!, $('.button-container'));
253
const button = new Button(buttonContainer, { title: node.title, supportIcons: true, secondary: !!(renderSecondaryButtons && buttonsCount > 0), ...defaultButtonStyles, });
254
button.label = node.label;
255
button.onDidClick(_ => {
256
this.openerService.open(node.href, { allowCommands: true });
257
}, null, this.renderDisposables);
258
this.renderDisposables.add(button);
259
buttonsCount++;
260
261
if (precondition) {
262
const updateEnablement = () => button.enabled = this.contextKeyService.contextMatchesRules(precondition);
263
updateEnablement();
264
265
const keys = new Set(precondition.keys());
266
const onDidChangeContext = Event.filter(this.contextKeyService.onDidChangeContext, e => e.affectsSome(keys));
267
onDidChangeContext(updateEnablement, null, this.renderDisposables);
268
}
269
} else {
270
const p = append(this.element!, $('p'));
271
272
for (const node of linkedText.nodes) {
273
if (typeof node === 'string') {
274
append(p, ...renderLabelWithIcons(node));
275
} else {
276
const link = this.renderDisposables.add(this.instantiationService.createInstance(Link, p, node, {}));
277
278
if (precondition && node.href.startsWith('command:')) {
279
const updateEnablement = () => link.enabled = this.contextKeyService.contextMatchesRules(precondition);
280
updateEnablement();
281
282
const keys = new Set(precondition.keys());
283
const onDidChangeContext = Event.filter(this.contextKeyService.onDidChangeContext, e => e.affectsSome(keys));
284
onDidChangeContext(updateEnablement, null, this.renderDisposables);
285
}
286
}
287
}
288
}
289
}
290
}
291
292
this.container.classList.add('welcome');
293
this.scrollableElement!.scanDomNode();
294
}
295
296
private getContentDescriptors(): IViewContentDescriptor[] {
297
const visibleItems = this.items.filter(v => v.visible);
298
299
if (visibleItems.length === 0 && this.defaultItem) {
300
return [this.defaultItem.descriptor];
301
}
302
303
return visibleItems.map(v => v.descriptor);
304
}
305
306
dispose(): void {
307
this.disposables.dispose();
308
}
309
}
310
311
export abstract class ViewPane extends Pane implements IView {
312
313
private static readonly AlwaysShowActionsConfig = 'workbench.view.alwaysShowHeaderActions';
314
315
private _onDidFocus = this._register(new Emitter<void>());
316
readonly onDidFocus: Event<void> = this._onDidFocus.event;
317
318
private _onDidBlur = this._register(new Emitter<void>());
319
readonly onDidBlur: Event<void> = this._onDidBlur.event;
320
321
private _onDidChangeBodyVisibility = this._register(new Emitter<boolean>());
322
readonly onDidChangeBodyVisibility: Event<boolean> = this._onDidChangeBodyVisibility.event;
323
324
protected _onDidChangeTitleArea = this._register(new Emitter<void>());
325
readonly onDidChangeTitleArea: Event<void> = this._onDidChangeTitleArea.event;
326
327
protected _onDidChangeViewWelcomeState = this._register(new Emitter<void>());
328
readonly onDidChangeViewWelcomeState: Event<void> = this._onDidChangeViewWelcomeState.event;
329
330
private _isVisible: boolean = false;
331
readonly id: string;
332
333
private _title: string;
334
public get title(): string {
335
return this._title;
336
}
337
338
private _titleDescription: string | undefined;
339
public get titleDescription(): string | undefined {
340
return this._titleDescription;
341
}
342
343
private _singleViewPaneContainerTitle: string | undefined;
344
public get singleViewPaneContainerTitle(): string | undefined {
345
return this._singleViewPaneContainerTitle;
346
}
347
348
readonly menuActions: ViewMenuActions;
349
350
private progressBar?: ProgressBar;
351
private progressIndicator?: IProgressIndicator;
352
353
private toolbar?: WorkbenchToolBar;
354
private readonly showActions: ViewPaneShowActions;
355
private headerContainer?: HTMLElement;
356
private titleContainer?: HTMLElement;
357
private titleContainerHover?: IManagedHover;
358
private titleDescriptionContainer?: HTMLElement;
359
private titleDescriptionContainerHover?: IManagedHover;
360
private iconContainer?: HTMLElement;
361
private iconContainerHover?: IManagedHover;
362
protected twistiesContainer?: HTMLElement;
363
private viewWelcomeController?: ViewWelcomeController;
364
365
private readonly headerActionViewItems: DisposableMap<string, IActionViewItem> = this._register(new DisposableMap());
366
367
protected readonly scopedContextKeyService: IContextKeyService;
368
369
constructor(
370
options: IViewPaneOptions,
371
@IKeybindingService protected keybindingService: IKeybindingService,
372
@IContextMenuService protected contextMenuService: IContextMenuService,
373
@IConfigurationService protected readonly configurationService: IConfigurationService,
374
@IContextKeyService protected contextKeyService: IContextKeyService,
375
@IViewDescriptorService protected viewDescriptorService: IViewDescriptorService,
376
@IInstantiationService protected instantiationService: IInstantiationService,
377
@IOpenerService protected openerService: IOpenerService,
378
@IThemeService protected themeService: IThemeService,
379
@IHoverService protected readonly hoverService: IHoverService,
380
protected readonly accessibleViewInformationService?: IAccessibleViewInformationService
381
) {
382
super({ ...options, ...{ orientation: viewDescriptorService.getViewLocationById(options.id) === ViewContainerLocation.Panel ? Orientation.HORIZONTAL : Orientation.VERTICAL } });
383
384
this.id = options.id;
385
this._title = options.title;
386
this._titleDescription = options.titleDescription;
387
this._singleViewPaneContainerTitle = options.singleViewPaneContainerTitle;
388
this.showActions = options.showActions ?? ViewPaneShowActions.Default;
389
390
this.scopedContextKeyService = this._register(contextKeyService.createScoped(this.element));
391
this.scopedContextKeyService.createKey('view', this.id);
392
const viewLocationKey = this.scopedContextKeyService.createKey('viewLocation', ViewContainerLocationToString(viewDescriptorService.getViewLocationById(this.id)!));
393
this._register(Event.filter(viewDescriptorService.onDidChangeLocation, e => e.views.some(view => view.id === this.id))(() => viewLocationKey.set(ViewContainerLocationToString(viewDescriptorService.getViewLocationById(this.id)!))));
394
395
const childInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])));
396
this.menuActions = this._register(childInstantiationService.createInstance(ViewMenuActions, options.titleMenuId ?? MenuId.ViewTitle, MenuId.ViewTitleContext, { shouldForwardArgs: !options.donotForwardArgs, renderShortTitle: true }));
397
this._register(this.menuActions.onDidChange(() => this.updateActions()));
398
}
399
400
override get headerVisible(): boolean {
401
return super.headerVisible;
402
}
403
404
override set headerVisible(visible: boolean) {
405
super.headerVisible = visible;
406
this.element.classList.toggle('merged-header', !visible);
407
}
408
409
setVisible(visible: boolean): void {
410
if (this._isVisible !== visible) {
411
this._isVisible = visible;
412
413
if (this.isExpanded()) {
414
this._onDidChangeBodyVisibility.fire(visible);
415
}
416
}
417
}
418
419
isVisible(): boolean {
420
return this._isVisible;
421
}
422
423
isBodyVisible(): boolean {
424
return this._isVisible && this.isExpanded();
425
}
426
427
override setExpanded(expanded: boolean): boolean {
428
const changed = super.setExpanded(expanded);
429
if (changed) {
430
this._onDidChangeBodyVisibility.fire(expanded);
431
}
432
this.updateTwistyIcon();
433
return changed;
434
}
435
436
override render(): void {
437
super.render();
438
439
const focusTracker = trackFocus(this.element);
440
this._register(focusTracker);
441
this._register(focusTracker.onDidFocus(() => this._onDidFocus.fire()));
442
this._register(focusTracker.onDidBlur(() => this._onDidBlur.fire()));
443
}
444
445
protected renderHeader(container: HTMLElement): void {
446
this.headerContainer = container;
447
448
this.twistiesContainer = append(container, $(`.twisty-container${ThemeIcon.asCSSSelector(this.getTwistyIcon(this.isExpanded()))}`));
449
450
this.renderHeaderTitle(container, this.title);
451
452
const actions = append(container, $('.actions'));
453
actions.classList.toggle('show-always', this.showActions === ViewPaneShowActions.Always);
454
actions.classList.toggle('show-expanded', this.showActions === ViewPaneShowActions.WhenExpanded);
455
this.toolbar = this.instantiationService.createInstance(WorkbenchToolBar, actions, {
456
orientation: ActionsOrientation.HORIZONTAL,
457
actionViewItemProvider: (action, options) => {
458
const item = this.createActionViewItem(action, options);
459
if (item) {
460
this.headerActionViewItems.set(item.action.id, item);
461
}
462
return item;
463
},
464
ariaLabel: nls.localize('viewToolbarAriaLabel', "{0} actions", this.title),
465
getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id),
466
renderDropdownAsChildElement: true,
467
actionRunner: this.getActionRunner(),
468
resetMenu: this.menuActions.menuId
469
});
470
471
this._register(this.toolbar);
472
this.setActions();
473
474
this._register(addDisposableListener(actions, EventType.CLICK, e => e.preventDefault()));
475
476
const viewContainerModel = this.viewDescriptorService.getViewContainerByViewId(this.id);
477
if (viewContainerModel) {
478
this._register(this.viewDescriptorService.getViewContainerModel(viewContainerModel).onDidChangeContainerInfo(({ title }) => this.updateTitle(this.title)));
479
} else {
480
console.error(`View container model not found for view ${this.id}`);
481
}
482
483
const onDidRelevantConfigurationChange = Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(ViewPane.AlwaysShowActionsConfig));
484
this._register(onDidRelevantConfigurationChange(this.updateActionsVisibility, this));
485
this.updateActionsVisibility();
486
}
487
488
protected override updateHeader(): void {
489
super.updateHeader();
490
this.updateTwistyIcon();
491
}
492
493
private updateTwistyIcon(): void {
494
if (this.twistiesContainer) {
495
this.twistiesContainer.classList.remove(...ThemeIcon.asClassNameArray(this.getTwistyIcon(!this._expanded)));
496
this.twistiesContainer.classList.add(...ThemeIcon.asClassNameArray(this.getTwistyIcon(this._expanded)));
497
}
498
}
499
500
protected getTwistyIcon(expanded: boolean): ThemeIcon {
501
return expanded ? viewPaneContainerExpandedIcon : viewPaneContainerCollapsedIcon;
502
}
503
504
override style(styles: IPaneStyles): void {
505
super.style(styles);
506
507
const icon = this.getIcon();
508
if (this.iconContainer) {
509
const fgColor = asCssValueWithDefault(styles.headerForeground, asCssVariable(foreground));
510
if (URI.isUri(icon)) {
511
// Apply background color to activity bar item provided with iconUrls
512
this.iconContainer.style.backgroundColor = fgColor;
513
this.iconContainer.style.color = '';
514
} else {
515
// Apply foreground color to activity bar items provided with codicons
516
this.iconContainer.style.color = fgColor;
517
this.iconContainer.style.backgroundColor = '';
518
}
519
}
520
}
521
522
private getIcon(): ThemeIcon | URI {
523
return this.viewDescriptorService.getViewDescriptorById(this.id)?.containerIcon || defaultViewIcon;
524
}
525
526
protected renderHeaderTitle(container: HTMLElement, title: string): void {
527
this.iconContainer = append(container, $('.icon', undefined));
528
const icon = this.getIcon();
529
530
let cssClass: string | undefined = undefined;
531
if (URI.isUri(icon)) {
532
cssClass = `view-${this.id.replace(/[\.\:]/g, '-')}`;
533
const iconClass = `.pane-header .icon.${cssClass}`;
534
535
createCSSRule(iconClass, `
536
mask: ${asCSSUrl(icon)} no-repeat 50% 50%;
537
mask-size: 24px;
538
-webkit-mask: ${asCSSUrl(icon)} no-repeat 50% 50%;
539
-webkit-mask-size: 16px;
540
`);
541
} else if (ThemeIcon.isThemeIcon(icon)) {
542
cssClass = ThemeIcon.asClassName(icon);
543
}
544
545
if (cssClass) {
546
this.iconContainer.classList.add(...cssClass.split(' '));
547
}
548
549
const calculatedTitle = this.calculateTitle(title);
550
this.titleContainer = append(container, $('h3.title', {}, calculatedTitle));
551
this.titleContainerHover = this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.titleContainer, calculatedTitle));
552
553
if (this._titleDescription) {
554
this.setTitleDescription(this._titleDescription);
555
}
556
557
this.iconContainerHover = this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.iconContainer, calculatedTitle));
558
this.iconContainer.setAttribute('aria-label', this._getAriaLabel(calculatedTitle, this._titleDescription));
559
}
560
561
private _getAriaLabel(title: string, description: string | undefined): string {
562
const viewHasAccessibilityHelpContent = this.viewDescriptorService.getViewDescriptorById(this.id)?.accessibilityHelpContent;
563
const accessibleViewHasShownForView = this.accessibleViewInformationService?.hasShownAccessibleView(this.id);
564
if (!viewHasAccessibilityHelpContent || accessibleViewHasShownForView) {
565
if (description) {
566
return `${title} - ${description}`;
567
} else {
568
return title;
569
}
570
}
571
572
return nls.localize('viewAccessibilityHelp', 'Use Alt+F1 for accessibility help {0}', title);
573
}
574
575
protected updateTitle(title: string): void {
576
const calculatedTitle = this.calculateTitle(title);
577
if (this.titleContainer) {
578
this.titleContainer.textContent = calculatedTitle;
579
this.titleContainerHover?.update(calculatedTitle);
580
}
581
582
this.updateAriaHeaderLabel(calculatedTitle, this._titleDescription);
583
584
this._title = title;
585
this._onDidChangeTitleArea.fire();
586
}
587
588
private updateAriaHeaderLabel(title: string, description: string | undefined) {
589
const ariaLabel = this._getAriaLabel(title, description);
590
if (this.iconContainer) {
591
this.iconContainerHover?.update(title);
592
this.iconContainer.setAttribute('aria-label', ariaLabel);
593
}
594
this.ariaHeaderLabel = this.getAriaHeaderLabel(ariaLabel);
595
}
596
597
private setTitleDescription(description: string | undefined) {
598
if (this.titleDescriptionContainer) {
599
this.titleDescriptionContainer.textContent = description ?? '';
600
this.titleDescriptionContainerHover?.update(description ?? '');
601
}
602
else if (description && this.titleContainer) {
603
this.titleDescriptionContainer = after(this.titleContainer, $('span.description', {}, description));
604
this.titleDescriptionContainerHover = this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.titleDescriptionContainer, description));
605
}
606
}
607
608
protected updateTitleDescription(description?: string | undefined): void {
609
this.setTitleDescription(description);
610
this.updateAriaHeaderLabel(this._title, description);
611
this._titleDescription = description;
612
this._onDidChangeTitleArea.fire();
613
}
614
615
private calculateTitle(title: string): string {
616
const viewContainer = this.viewDescriptorService.getViewContainerByViewId(this.id)!;
617
const model = this.viewDescriptorService.getViewContainerModel(viewContainer);
618
const viewDescriptor = this.viewDescriptorService.getViewDescriptorById(this.id);
619
const isDefault = this.viewDescriptorService.getDefaultContainerById(this.id) === viewContainer;
620
621
if (!isDefault && viewDescriptor?.containerTitle && model.title !== viewDescriptor.containerTitle && title !== viewDescriptor.containerTitle) {
622
return `${viewDescriptor.containerTitle}: ${title}`;
623
}
624
625
return title;
626
}
627
628
protected renderBody(container: HTMLElement): void {
629
this.viewWelcomeController = this._register(this.instantiationService.createInstance(ViewWelcomeController, container, this));
630
}
631
632
protected layoutBody(height: number, width: number): void {
633
this.viewWelcomeController?.layout(height, width);
634
}
635
636
onDidScrollRoot() {
637
// noop
638
}
639
640
getProgressIndicator() {
641
if (this.progressBar === undefined) {
642
this.progressBar = this._register(new ProgressBar(this.element, defaultProgressBarStyles));
643
this.progressBar.hide();
644
}
645
646
if (this.progressIndicator === undefined) {
647
const that = this;
648
this.progressIndicator = this._register(new ScopedProgressIndicator(assertReturnsDefined(this.progressBar), this._register(new class extends AbstractProgressScope {
649
constructor() {
650
super(that.id, that.isBodyVisible());
651
this._register(that.onDidChangeBodyVisibility(isVisible => isVisible ? this.onScopeOpened(that.id) : this.onScopeClosed(that.id)));
652
}
653
}())));
654
}
655
return this.progressIndicator;
656
}
657
658
protected getProgressLocation(): string {
659
return this.viewDescriptorService.getViewContainerByViewId(this.id)!.id;
660
}
661
662
protected getLocationBasedColors(): IViewPaneLocationColors {
663
return getLocationBasedViewColors(this.viewDescriptorService.getViewLocationById(this.id));
664
}
665
666
focus(): void {
667
if (this.viewWelcomeController?.enabled) {
668
this.viewWelcomeController.focus();
669
} else if (this.element) {
670
this.element.focus();
671
}
672
if (isActiveElement(this.element) || isAncestorOfActiveElement(this.element)) {
673
this._onDidFocus.fire();
674
}
675
}
676
677
private setActions(): void {
678
if (this.toolbar) {
679
const primaryActions = [...this.menuActions.getPrimaryActions()];
680
if (this.shouldShowFilterInHeader()) {
681
primaryActions.unshift(VIEWPANE_FILTER_ACTION);
682
}
683
this.toolbar.setActions(prepareActions(primaryActions), prepareActions(this.menuActions.getSecondaryActions()));
684
this.toolbar.context = this.getActionsContext();
685
}
686
}
687
688
private updateActionsVisibility(): void {
689
if (!this.headerContainer) {
690
return;
691
}
692
const shouldAlwaysShowActions = this.configurationService.getValue<boolean>('workbench.view.alwaysShowHeaderActions');
693
this.headerContainer.classList.toggle('actions-always-visible', shouldAlwaysShowActions);
694
}
695
696
protected updateActions(): void {
697
this.setActions();
698
this._onDidChangeTitleArea.fire();
699
}
700
701
createActionViewItem(action: IAction, options?: IDropdownMenuActionViewItemOptions): IActionViewItem | undefined {
702
if (action.id === VIEWPANE_FILTER_ACTION.id) {
703
const that = this;
704
return new class extends BaseActionViewItem {
705
constructor() { super(null, action); }
706
override setFocusable(): void { /* noop input elements are focusable by default */ }
707
override get trapsArrowNavigation(): boolean { return true; }
708
override render(container: HTMLElement): void {
709
container.classList.add('viewpane-filter-container');
710
const filter = that.getFilterWidget()!;
711
append(container, filter.element);
712
filter.relayout();
713
}
714
};
715
}
716
return createActionViewItem(this.instantiationService, action, { ...options, ...{ menuAsChild: action instanceof SubmenuItemAction } });
717
}
718
719
getActionsContext(): unknown {
720
return undefined;
721
}
722
723
getActionRunner(): IActionRunner | undefined {
724
return undefined;
725
}
726
727
getOptimalWidth(): number {
728
return 0;
729
}
730
731
saveState(): void {
732
// Subclasses to implement for saving state
733
}
734
735
shouldShowWelcome(): boolean {
736
return false;
737
}
738
739
getFilterWidget(): FilterWidget | undefined {
740
return undefined;
741
}
742
743
shouldShowFilterInHeader(): boolean {
744
return false;
745
}
746
}
747
748
export abstract class FilterViewPane extends ViewPane {
749
750
readonly filterWidget: FilterWidget;
751
private dimension: Dimension | undefined;
752
protected filterContainer: HTMLElement | undefined;
753
754
constructor(
755
options: IFilterViewPaneOptions,
756
@IKeybindingService keybindingService: IKeybindingService,
757
@IContextMenuService contextMenuService: IContextMenuService,
758
@IConfigurationService configurationService: IConfigurationService,
759
@IContextKeyService contextKeyService: IContextKeyService,
760
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
761
@IInstantiationService instantiationService: IInstantiationService,
762
@IOpenerService openerService: IOpenerService,
763
@IThemeService themeService: IThemeService,
764
@IHoverService hoverService: IHoverService,
765
accessibleViewService?: IAccessibleViewInformationService
766
) {
767
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService, accessibleViewService);
768
const childInstantiationService = this._register(instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])));
769
this.filterWidget = this._register(childInstantiationService.createInstance(FilterWidget, options.filterOptions));
770
this._register(this.filterWidget.onDidAcceptFilterText(() => this.focusBodyContent()));
771
}
772
773
override getFilterWidget(): FilterWidget {
774
return this.filterWidget;
775
}
776
777
protected override renderBody(container: HTMLElement): void {
778
super.renderBody(container);
779
this.filterContainer = append(container, $('.viewpane-filter-container'));
780
}
781
782
protected override layoutBody(height: number, width: number): void {
783
super.layoutBody(height, width);
784
785
this.dimension = new Dimension(width, height);
786
const wasFilterShownInHeader = !this.filterContainer?.hasChildNodes();
787
const shouldShowFilterInHeader = this.shouldShowFilterInHeader();
788
if (wasFilterShownInHeader !== shouldShowFilterInHeader) {
789
if (shouldShowFilterInHeader) {
790
reset(this.filterContainer!);
791
}
792
this.updateActions();
793
if (!shouldShowFilterInHeader) {
794
append(this.filterContainer!, this.filterWidget.element);
795
}
796
}
797
if (!shouldShowFilterInHeader) {
798
height = height - 44;
799
}
800
this.filterWidget.layout(width);
801
this.layoutBodyContent(height, width);
802
}
803
804
override shouldShowFilterInHeader(): boolean {
805
return !(this.dimension && this.dimension.width < 600 && this.dimension.height > 100);
806
}
807
808
protected abstract layoutBodyContent(height: number, width: number): void;
809
810
protected focusBodyContent(): void {
811
this.focus();
812
}
813
}
814
815
export interface IViewPaneLocationColors {
816
background: string;
817
overlayBackground: string;
818
listOverrideStyles: PartialExcept<IListStyles, 'listBackground' | 'treeStickyScrollBackground'>;
819
}
820
821
export function getLocationBasedViewColors(location: ViewContainerLocation | null): IViewPaneLocationColors {
822
let background, overlayBackground, stickyScrollBackground, stickyScrollBorder, stickyScrollShadow;
823
824
switch (location) {
825
case ViewContainerLocation.Panel:
826
background = PANEL_BACKGROUND;
827
overlayBackground = PANEL_SECTION_DRAG_AND_DROP_BACKGROUND;
828
stickyScrollBackground = PANEL_STICKY_SCROLL_BACKGROUND;
829
stickyScrollBorder = PANEL_STICKY_SCROLL_BORDER;
830
stickyScrollShadow = PANEL_STICKY_SCROLL_SHADOW;
831
break;
832
833
case ViewContainerLocation.Sidebar:
834
case ViewContainerLocation.AuxiliaryBar:
835
default:
836
background = SIDE_BAR_BACKGROUND;
837
overlayBackground = SIDE_BAR_DRAG_AND_DROP_BACKGROUND;
838
stickyScrollBackground = SIDE_BAR_STICKY_SCROLL_BACKGROUND;
839
stickyScrollBorder = SIDE_BAR_STICKY_SCROLL_BORDER;
840
stickyScrollShadow = SIDE_BAR_STICKY_SCROLL_SHADOW;
841
}
842
843
return {
844
background,
845
overlayBackground,
846
listOverrideStyles: {
847
listBackground: background,
848
treeStickyScrollBackground: stickyScrollBackground,
849
treeStickyScrollBorder: stickyScrollBorder,
850
treeStickyScrollShadow: stickyScrollShadow
851
}
852
};
853
}
854
855
export abstract class ViewAction<T extends IView> extends Action2 {
856
override readonly desc: Readonly<IAction2Options> & { viewId: string };
857
constructor(desc: Readonly<IAction2Options> & { viewId: string }) {
858
super(desc);
859
this.desc = desc;
860
}
861
862
run(accessor: ServicesAccessor, ...args: unknown[]): unknown {
863
const view = accessor.get(IViewsService).getActiveViewWithId(this.desc.viewId);
864
if (view) {
865
return this.runInView(accessor, <T>view, ...args);
866
}
867
return undefined;
868
}
869
870
abstract runInView(accessor: ServicesAccessor, view: T, ...args: unknown[]): unknown;
871
}
872
873