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