Path: blob/main/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts
5317 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import './media/statusbarpart.css';6import { localize } from '../../../../nls.js';7import { Disposable, DisposableStore, disposeIfDisposable, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';8import { MultiWindowParts, Part } from '../../part.js';9import { EventType as TouchEventType, Gesture, GestureEvent } from '../../../../base/browser/touch.js';10import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';11import { StatusbarAlignment, IStatusbarService, IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarStyleOverride, isStatusbarEntryLocation, IStatusbarEntryLocation, isStatusbarEntryPriority, IStatusbarEntryPriority } from '../../../services/statusbar/browser/statusbar.js';12import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';13import { IAction, Separator, toAction } from '../../../../base/common/actions.js';14import { IThemeService } from '../../../../platform/theme/common/themeService.js';15import { STATUS_BAR_BACKGROUND, STATUS_BAR_FOREGROUND, STATUS_BAR_NO_FOLDER_BACKGROUND, STATUS_BAR_ITEM_HOVER_BACKGROUND, STATUS_BAR_BORDER, STATUS_BAR_NO_FOLDER_FOREGROUND, STATUS_BAR_NO_FOLDER_BORDER, STATUS_BAR_ITEM_COMPACT_HOVER_BACKGROUND, STATUS_BAR_ITEM_FOCUS_BORDER, STATUS_BAR_FOCUS_BORDER } from '../../../common/theme.js';16import { IWorkspaceContextService, WorkbenchState } from '../../../../platform/workspace/common/workspace.js';17import { contrastBorder, activeContrastBorder } from '../../../../platform/theme/common/colorRegistry.js';18import { EventHelper, addDisposableListener, EventType, clearNode, getWindow, isHTMLElement, $ } from '../../../../base/browser/dom.js';19import { createStyleSheet } from '../../../../base/browser/domStylesheets.js';20import { IStorageService } from '../../../../platform/storage/common/storage.js';21import { Parts, IWorkbenchLayoutService } from '../../../services/layout/browser/layoutService.js';22import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';23import { equals } from '../../../../base/common/arrays.js';24import { StandardMouseEvent } from '../../../../base/browser/mouseEvent.js';25import { ToggleStatusbarVisibilityAction } from '../../actions/layoutActions.js';26import { assertReturnsDefined } from '../../../../base/common/types.js';27import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';28import { isHighContrast } from '../../../../platform/theme/common/theme.js';29import { hash } from '../../../../base/common/hash.js';30import { WorkbenchHoverDelegate } from '../../../../platform/hover/browser/hover.js';31import { HideStatusbarEntryAction, ManageExtensionAction, ToggleStatusbarEntryVisibilityAction } from './statusbarActions.js';32import { IStatusbarViewModelEntry, StatusbarViewModel } from './statusbarModel.js';33import { StatusbarEntryItem } from './statusbarItem.js';34import { StatusBarFocused } from '../../../common/contextkeys.js';35import { Emitter, Event } from '../../../../base/common/event.js';36import { IView } from '../../../../base/browser/ui/grid/grid.js';37import { isManagedHoverTooltipHTMLElement, isManagedHoverTooltipMarkdownString } from '../../../../base/browser/ui/hover/hover.js';3839export interface IStatusbarEntryContainer extends IDisposable {4041/**42* An event that is triggered when an entry's visibility is changed.43*/44readonly onDidChangeEntryVisibility: Event<{ id: string; visible: boolean }>;4546/**47* Adds an entry to the statusbar with the given alignment and priority. Use the returned accessor48* to update or remove the statusbar entry.49*50* @param id identifier of the entry is needed to allow users to hide entries via settings51* @param alignment either LEFT or RIGHT side in the status bar52* @param priority items get arranged from highest priority to lowest priority from left to right53* in their respective alignment slot54*/55addEntry(entry: IStatusbarEntry, id: string, alignment: StatusbarAlignment, priority?: number | IStatusbarEntryPriority): IStatusbarEntryAccessor;56addEntry(entry: IStatusbarEntry, id: string, alignment: StatusbarAlignment, priority?: number | IStatusbarEntryPriority | IStatusbarEntryLocation): IStatusbarEntryAccessor;5758/**59* Adds an entry to the statusbar with the given alignment relative to another entry. Use the returned60* accessor to update or remove the statusbar entry.61*62* @param id identifier of the entry is needed to allow users to hide entries via settings63* @param alignment either LEFT or RIGHT side in the status bar64* @param location a reference to another entry to position relative to65*/66addEntry(entry: IStatusbarEntry, id: string, alignment: StatusbarAlignment, location?: IStatusbarEntryLocation): IStatusbarEntryAccessor;6768/**69* Return if an entry is visible or not.70*/71isEntryVisible(id: string): boolean;7273/**74* Allows to update an entry's visibility with the provided ID.75*/76updateEntryVisibility(id: string, visible: boolean): void;7778/**79* Allows to override the appearance of an entry with the provided ID.80*/81overrideEntry(id: string, override: Partial<IStatusbarEntry>): IDisposable;8283/**84* Focused the status bar. If one of the status bar entries was focused, focuses it directly.85*/86focus(preserveEntryFocus?: boolean): void;8788/**89* Focuses the next status bar entry. If none focused, focuses the first.90*/91focusNextEntry(): void;9293/**94* Focuses the previous status bar entry. If none focused, focuses the last.95*/96focusPreviousEntry(): void;9798/**99* Returns true if a status bar entry is focused.100*/101isEntryFocused(): boolean;102103/**104* Temporarily override statusbar style.105*/106overrideStyle(style: IStatusbarStyleOverride): IDisposable;107}108109interface IPendingStatusbarEntry {110readonly id: string;111readonly alignment: StatusbarAlignment;112readonly priority: IStatusbarEntryPriority;113114entry: IStatusbarEntry;115accessor?: IStatusbarEntryAccessor;116}117118class StatusbarPart extends Part implements IStatusbarEntryContainer {119120static readonly HEIGHT = 22;121122//#region IView123124readonly minimumWidth: number = 0;125readonly maximumWidth: number = Number.POSITIVE_INFINITY;126readonly minimumHeight: number = StatusbarPart.HEIGHT;127readonly maximumHeight: number = StatusbarPart.HEIGHT;128129//#endregion130131private styleElement: HTMLStyleElement | undefined;132133private pendingEntries: IPendingStatusbarEntry[] = [];134135private readonly viewModel: StatusbarViewModel;136137readonly onDidChangeEntryVisibility: Event<{ id: string; visible: boolean }>;138139private readonly _onWillDispose = this._register(new Emitter<void>());140readonly onWillDispose = this._onWillDispose.event;141142private readonly onDidOverrideEntry = this._register(new Emitter<string>());143private readonly entryOverrides = new Map<string, Partial<IStatusbarEntry>>();144145private leftItemsContainer: HTMLElement | undefined;146private rightItemsContainer: HTMLElement | undefined;147148private readonly hoverDelegate: WorkbenchHoverDelegate;149150private readonly compactEntriesDisposable = this._register(new MutableDisposable<DisposableStore>());151private readonly styleOverrides = new Set<IStatusbarStyleOverride>();152153constructor(154id: string,155@IInstantiationService private readonly instantiationService: IInstantiationService,156@IThemeService themeService: IThemeService,157@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,158@IStorageService storageService: IStorageService,159@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,160@IContextMenuService private readonly contextMenuService: IContextMenuService,161@IContextKeyService private readonly contextKeyService: IContextKeyService,162) {163super(id, { hasTitle: false }, themeService, storageService, layoutService);164165this.viewModel = this._register(new StatusbarViewModel(storageService));166this.onDidChangeEntryVisibility = this.viewModel.onDidChangeEntryVisibility;167168this.hoverDelegate = this._register(this.instantiationService.createInstance(WorkbenchHoverDelegate, 'element', {169instantHover: true,170dynamicDelay(content) {171if (172typeof content === 'function' ||173isHTMLElement(content) ||174(isManagedHoverTooltipMarkdownString(content) && typeof content.markdown === 'function') ||175isManagedHoverTooltipHTMLElement(content)176) {177// override the delay for content that is rich (e.g. html or long running)178// so that it appears more instantly. these hovers carry more important179// information and should not be delayed by preference.180return 500;181}182183return undefined;184}185}, (_, focus?: boolean) => (186{187persistence: {188hideOnKeyDown: true,189sticky: focus190},191appearance: {192maxHeightRatio: 0.9193}194}195)));196197this.registerListeners();198}199200private registerListeners(): void {201202// Entry visibility changes203this._register(this.onDidChangeEntryVisibility(() => this.updateCompactEntries()));204205// Workbench state changes206this._register(this.contextService.onDidChangeWorkbenchState(() => this.updateStyles()));207}208209overrideEntry(id: string, override: Partial<IStatusbarEntry>): IDisposable {210this.entryOverrides.set(id, override);211this.onDidOverrideEntry.fire(id);212213return toDisposable(() => {214const currentOverride = this.entryOverrides.get(id);215if (currentOverride === override) {216this.entryOverrides.delete(id);217this.onDidOverrideEntry.fire(id);218}219});220}221222private withEntryOverride(entry: IStatusbarEntry, id: string): IStatusbarEntry {223const override = this.entryOverrides.get(id);224if (override) {225entry = { ...entry, ...override };226}227228return entry;229}230231addEntry(entry: IStatusbarEntry, id: string, alignment: StatusbarAlignment, priorityOrLocation: number | IStatusbarEntryLocation | IStatusbarEntryPriority = 0): IStatusbarEntryAccessor {232let priority: IStatusbarEntryPriority;233if (isStatusbarEntryPriority(priorityOrLocation)) {234priority = priorityOrLocation;235} else {236priority = {237primary: priorityOrLocation,238secondary: hash(id) // derive from identifier to accomplish uniqueness239};240}241242// As long as we have not been created into a container yet, record all entries243// that are pending so that they can get created at a later point244if (!this.element) {245return this.doAddPendingEntry(entry, id, alignment, priority);246}247248// Otherwise add to view249return this.doAddEntry(entry, id, alignment, priority);250}251252private doAddPendingEntry(entry: IStatusbarEntry, id: string, alignment: StatusbarAlignment, priority: IStatusbarEntryPriority): IStatusbarEntryAccessor {253const pendingEntry: IPendingStatusbarEntry = { entry, id, alignment, priority };254this.pendingEntries.push(pendingEntry);255256const accessor: IStatusbarEntryAccessor = {257update: (entry: IStatusbarEntry) => {258if (pendingEntry.accessor) {259pendingEntry.accessor.update(entry);260} else {261pendingEntry.entry = entry;262}263},264265dispose: () => {266if (pendingEntry.accessor) {267pendingEntry.accessor.dispose();268} else {269this.pendingEntries = this.pendingEntries.filter(entry => entry !== pendingEntry);270}271}272};273274return accessor;275}276277private doAddEntry(entry: IStatusbarEntry, id: string, alignment: StatusbarAlignment, priority: IStatusbarEntryPriority): IStatusbarEntryAccessor {278const disposables = new DisposableStore();279280// View model item281const itemContainer = this.doCreateStatusItem(id, alignment);282const item = disposables.add(this.instantiationService.createInstance(StatusbarEntryItem, itemContainer, this.withEntryOverride(entry, id), this.hoverDelegate));283284// View model entry285const viewModelEntry: IStatusbarViewModelEntry = new class implements IStatusbarViewModelEntry {286readonly id = id;287readonly extensionId = entry.extensionId;288readonly alignment = alignment;289readonly priority = priority;290readonly container = itemContainer;291readonly labelContainer = item.labelContainer;292293get name() { return item.name; }294get hasCommand() { return item.hasCommand; }295};296297// Add to view model298const { needsFullRefresh } = this.doAddOrRemoveModelEntry(viewModelEntry, true);299if (needsFullRefresh) {300this.appendStatusbarEntries();301} else {302this.appendStatusbarEntry(viewModelEntry);303}304305let lastEntry = entry;306const accessor: IStatusbarEntryAccessor = {307update: entry => {308lastEntry = entry;309item.update(this.withEntryOverride(entry, id));310},311dispose: () => {312const { needsFullRefresh } = this.doAddOrRemoveModelEntry(viewModelEntry, false);313if (needsFullRefresh) {314this.appendStatusbarEntries();315} else {316itemContainer.remove();317this.updateCompactEntries();318}319disposables.dispose();320}321};322323// React to overrides324disposables.add(this.onDidOverrideEntry.event(overrideEntryId => {325if (overrideEntryId === id) {326accessor.update(lastEntry);327}328}));329330return accessor;331}332333private doCreateStatusItem(id: string, alignment: StatusbarAlignment, ...extraClasses: string[]): HTMLElement {334const itemContainer = $('.statusbar-item', { id });335336if (extraClasses) {337itemContainer.classList.add(...extraClasses);338}339340if (alignment === StatusbarAlignment.RIGHT) {341itemContainer.classList.add('right');342} else {343itemContainer.classList.add('left');344}345346return itemContainer;347}348349private doAddOrRemoveModelEntry(entry: IStatusbarViewModelEntry, add: boolean) {350351// Update model but remember previous entries352const entriesBefore = this.viewModel.entries;353if (add) {354this.viewModel.add(entry);355} else {356this.viewModel.remove(entry);357}358const entriesAfter = this.viewModel.entries;359360// Apply operation onto the entries from before361if (add) {362entriesBefore.splice(entriesAfter.indexOf(entry), 0, entry);363} else {364entriesBefore.splice(entriesBefore.indexOf(entry), 1);365}366367// Figure out if a full refresh is needed by comparing arrays368const needsFullRefresh = !equals(entriesBefore, entriesAfter);369370return { needsFullRefresh };371}372373isEntryVisible(id: string): boolean {374return !this.viewModel.isHidden(id);375}376377updateEntryVisibility(id: string, visible: boolean): void {378if (visible) {379this.viewModel.show(id);380} else {381this.viewModel.hide(id);382}383}384385focusNextEntry(): void {386this.viewModel.focusNextEntry();387}388389focusPreviousEntry(): void {390this.viewModel.focusPreviousEntry();391}392393isEntryFocused(): boolean {394return this.viewModel.isEntryFocused();395}396397focus(preserveEntryFocus = true): void {398this.getContainer()?.focus();399const lastFocusedEntry = this.viewModel.lastFocusedEntry;400if (preserveEntryFocus && lastFocusedEntry) {401setTimeout(() => lastFocusedEntry.labelContainer.focus(), 0); // Need a timeout, for some reason without it the inner label container will not get focused402}403}404405protected override createContentArea(parent: HTMLElement): HTMLElement {406this.element = parent;407408// Track focus within container409const scopedContextKeyService = this._register(this.contextKeyService.createScoped(this.element));410StatusBarFocused.bindTo(scopedContextKeyService).set(true);411412// Left items container413this.leftItemsContainer = $('.left-items.items-container');414this.element.appendChild(this.leftItemsContainer);415this.element.tabIndex = 0;416417// Right items container418this.rightItemsContainer = $('.right-items.items-container');419this.element.appendChild(this.rightItemsContainer);420421// Context menu support422this._register(addDisposableListener(parent, EventType.CONTEXT_MENU, e => this.showContextMenu(e)));423this._register(Gesture.addTarget(parent));424this._register(addDisposableListener(parent, TouchEventType.Contextmenu, e => this.showContextMenu(e)));425426// Initial status bar entries427this.createInitialStatusbarEntries();428429return this.element;430}431432private createInitialStatusbarEntries(): void {433434// Add items in order according to alignment435this.appendStatusbarEntries();436437// Fill in pending entries if any438while (this.pendingEntries.length) {439const pending = this.pendingEntries.shift();440if (pending) {441pending.accessor = this.addEntry(pending.entry, pending.id, pending.alignment, pending.priority.primary);442}443}444}445446private appendStatusbarEntries(): void {447const leftItemsContainer = assertReturnsDefined(this.leftItemsContainer);448const rightItemsContainer = assertReturnsDefined(this.rightItemsContainer);449450// Clear containers451clearNode(leftItemsContainer);452clearNode(rightItemsContainer);453454// Append all455for (const entry of [456...this.viewModel.getEntries(StatusbarAlignment.LEFT),457...this.viewModel.getEntries(StatusbarAlignment.RIGHT).reverse() // reversing due to flex: row-reverse458]) {459const target = entry.alignment === StatusbarAlignment.LEFT ? leftItemsContainer : rightItemsContainer;460461target.appendChild(entry.container);462}463464// Update compact entries465this.updateCompactEntries();466}467468private appendStatusbarEntry(entry: IStatusbarViewModelEntry): void {469const entries = this.viewModel.getEntries(entry.alignment);470471if (entry.alignment === StatusbarAlignment.RIGHT) {472entries.reverse(); // reversing due to flex: row-reverse473}474475const target = assertReturnsDefined(entry.alignment === StatusbarAlignment.LEFT ? this.leftItemsContainer : this.rightItemsContainer);476477const index = entries.indexOf(entry);478if (index + 1 === entries.length) {479target.appendChild(entry.container); // append at the end if last480} else {481target.insertBefore(entry.container, entries[index + 1].container); // insert before next element otherwise482}483484// Update compact entries485this.updateCompactEntries();486}487488private updateCompactEntries(): void {489const entries = this.viewModel.entries;490491// Find visible entries and clear compact related CSS classes if any492const mapIdToVisibleEntry = new Map<string, IStatusbarViewModelEntry>();493for (const entry of entries) {494if (!this.viewModel.isHidden(entry.id)) {495mapIdToVisibleEntry.set(entry.id, entry);496}497498entry.container.classList.remove('compact-left', 'compact-right');499}500501// Figure out groups of entries with `compact` alignment502const compactEntryGroups = new Map<string, Map<string, IStatusbarViewModelEntry>>();503for (const entry of mapIdToVisibleEntry.values()) {504if (505isStatusbarEntryLocation(entry.priority.primary) && // entry references another entry as location506entry.priority.primary.compact // entry wants to be compact507) {508const locationId = entry.priority.primary.location.id;509const location = mapIdToVisibleEntry.get(locationId);510if (!location) {511continue; // skip if location does not exist512}513514// Build a map of entries that are compact among each other515let compactEntryGroup = compactEntryGroups.get(locationId);516if (!compactEntryGroup) {517518// It is possible that this entry references another entry519// that itself references an entry. In that case, we want520// to add it to the entries of the referenced entry.521522for (const group of compactEntryGroups.values()) {523if (group.has(locationId)) {524compactEntryGroup = group;525break;526}527}528529if (!compactEntryGroup) {530compactEntryGroup = new Map<string, IStatusbarViewModelEntry>();531compactEntryGroups.set(locationId, compactEntryGroup);532}533}534compactEntryGroup.set(entry.id, entry);535compactEntryGroup.set(location.id, location);536537// Adjust CSS classes to move compact items closer together538if (entry.priority.primary.alignment === StatusbarAlignment.LEFT) {539location.container.classList.add('compact-left');540entry.container.classList.add('compact-right');541} else {542location.container.classList.add('compact-right');543entry.container.classList.add('compact-left');544}545}546}547548// Install mouse listeners to update hover feedback for549// all compact entries that belong to each other550const statusBarItemHoverBackground = this.getColor(STATUS_BAR_ITEM_HOVER_BACKGROUND);551const statusBarItemCompactHoverBackground = this.getColor(STATUS_BAR_ITEM_COMPACT_HOVER_BACKGROUND);552this.compactEntriesDisposable.value = new DisposableStore();553if (statusBarItemHoverBackground && statusBarItemCompactHoverBackground && !isHighContrast(this.theme.type)) {554for (const [, compactEntryGroup] of compactEntryGroups) {555for (const compactEntry of compactEntryGroup.values()) {556if (!compactEntry.hasCommand) {557continue; // only show hover feedback when we have a command558}559560this.compactEntriesDisposable.value.add(addDisposableListener(compactEntry.labelContainer, EventType.MOUSE_OVER, () => {561compactEntryGroup.forEach(compactEntry => compactEntry.labelContainer.style.backgroundColor = statusBarItemHoverBackground);562compactEntry.labelContainer.style.backgroundColor = statusBarItemCompactHoverBackground;563}));564565this.compactEntriesDisposable.value.add(addDisposableListener(compactEntry.labelContainer, EventType.MOUSE_OUT, () => {566compactEntryGroup.forEach(compactEntry => compactEntry.labelContainer.style.backgroundColor = '');567}));568}569}570}571}572573private showContextMenu(e: MouseEvent | GestureEvent): void {574EventHelper.stop(e, true);575576const event = new StandardMouseEvent(getWindow(this.element), e);577578let actions: IAction[] | undefined = undefined;579this.contextMenuService.showContextMenu({580getAnchor: () => event,581getActions: () => {582actions = this.getContextMenuActions(event);583584return actions;585},586onHide: () => {587if (actions) {588disposeIfDisposable(actions);589}590}591});592}593594private getContextMenuActions(event: StandardMouseEvent): IAction[] {595const actions: IAction[] = [];596597// Provide an action to hide the status bar at last598actions.push(toAction({ id: ToggleStatusbarVisibilityAction.ID, label: localize('hideStatusBar', "Hide Status Bar"), run: () => this.instantiationService.invokeFunction(accessor => new ToggleStatusbarVisibilityAction().run(accessor)) }));599actions.push(new Separator());600601// Show an entry per known status entry602// Note: even though entries have an identifier, there can be multiple entries603// having the same identifier (e.g. from extensions). So we make sure to only604// show a single entry per identifier we handled.605const handledEntries = new Set<string>();606for (const entry of this.viewModel.entries) {607if (!handledEntries.has(entry.id)) {608actions.push(new ToggleStatusbarEntryVisibilityAction(entry.id, entry.name, this.viewModel));609handledEntries.add(entry.id);610}611}612613// Figure out if mouse is over an entry614let statusEntryUnderMouse: IStatusbarViewModelEntry | undefined = undefined;615for (let element: HTMLElement | null = event.target; element; element = element.parentElement) {616const entry = this.viewModel.findEntry(element);617if (entry) {618statusEntryUnderMouse = entry;619break;620}621}622623if (statusEntryUnderMouse) {624actions.push(new Separator());625if (statusEntryUnderMouse.extensionId) {626actions.push(this.instantiationService.createInstance(ManageExtensionAction, statusEntryUnderMouse.extensionId));627}628actions.push(new HideStatusbarEntryAction(statusEntryUnderMouse.id, statusEntryUnderMouse.name, this.viewModel));629}630631return actions;632}633634override updateStyles(): void {635super.updateStyles();636637const container = assertReturnsDefined(this.getContainer());638const styleOverride: IStatusbarStyleOverride | undefined = [...this.styleOverrides].sort((a, b) => a.priority - b.priority)[0];639640// Background / foreground colors641const backgroundColor = this.getColor(styleOverride?.background ?? (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? STATUS_BAR_BACKGROUND : STATUS_BAR_NO_FOLDER_BACKGROUND)) || '';642container.style.backgroundColor = backgroundColor;643const foregroundColor = this.getColor(styleOverride?.foreground ?? (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? STATUS_BAR_FOREGROUND : STATUS_BAR_NO_FOLDER_FOREGROUND)) || '';644container.style.color = foregroundColor;645const itemBorderColor = this.getColor(STATUS_BAR_ITEM_FOCUS_BORDER);646647// Update compact entries to refresh hover colors based on current theme648this.updateCompactEntries();649650// Border color651const borderColor = this.getColor(styleOverride?.border ?? (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? STATUS_BAR_BORDER : STATUS_BAR_NO_FOLDER_BORDER)) || this.getColor(contrastBorder);652if (borderColor) {653container.classList.add('status-border-top');654container.style.setProperty('--status-border-top-color', borderColor);655} else {656container.classList.remove('status-border-top');657container.style.removeProperty('--status-border-top-color');658}659660// Colors and focus outlines via dynamic stylesheet661662const statusBarFocusColor = this.getColor(STATUS_BAR_FOCUS_BORDER);663664if (!this.styleElement) {665this.styleElement = createStyleSheet(container, undefined, this._store);666}667668this.styleElement.textContent = `669670/* Status bar focus outline */671.monaco-workbench .part.statusbar:focus {672outline-color: ${statusBarFocusColor};673}674675/* Status bar item focus outline */676.monaco-workbench .part.statusbar > .items-container > .statusbar-item a:focus-visible {677outline: 1px solid ${this.getColor(activeContrastBorder) ?? itemBorderColor};678outline-offset: ${borderColor ? '-2px' : '-1px'};679}680681/* Notification Beak */682.monaco-workbench .part.statusbar > .items-container > .statusbar-item.has-beak > .status-bar-item-beak-container:before {683border-bottom-color: ${borderColor ?? backgroundColor};684}685`;686}687688override layout(width: number, height: number, top: number, left: number): void {689super.layout(width, height, top, left);690super.layoutContents(width, height);691}692693overrideStyle(style: IStatusbarStyleOverride): IDisposable {694this.styleOverrides.add(style);695this.updateStyles();696697return toDisposable(() => {698this.styleOverrides.delete(style);699this.updateStyles();700});701}702703toJSON(): object {704return {705type: Parts.STATUSBAR_PART706};707}708709override dispose(): void {710this._onWillDispose.fire();711712super.dispose();713}714}715716export class MainStatusbarPart extends StatusbarPart {717718constructor(719@IInstantiationService instantiationService: IInstantiationService,720@IThemeService themeService: IThemeService,721@IWorkspaceContextService contextService: IWorkspaceContextService,722@IStorageService storageService: IStorageService,723@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,724@IContextMenuService contextMenuService: IContextMenuService,725@IContextKeyService contextKeyService: IContextKeyService,726) {727super(Parts.STATUSBAR_PART, instantiationService, themeService, contextService, storageService, layoutService, contextMenuService, contextKeyService);728}729}730731export interface IAuxiliaryStatusbarPart extends IStatusbarEntryContainer, IView {732readonly container: HTMLElement;733readonly height: number;734}735736export class AuxiliaryStatusbarPart extends StatusbarPart implements IAuxiliaryStatusbarPart {737738private static COUNTER = 1;739740readonly height = StatusbarPart.HEIGHT;741742constructor(743readonly container: HTMLElement,744@IInstantiationService instantiationService: IInstantiationService,745@IThemeService themeService: IThemeService,746@IWorkspaceContextService contextService: IWorkspaceContextService,747@IStorageService storageService: IStorageService,748@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,749@IContextMenuService contextMenuService: IContextMenuService,750@IContextKeyService contextKeyService: IContextKeyService,751) {752const id = AuxiliaryStatusbarPart.COUNTER++;753super(`workbench.parts.auxiliaryStatus.${id}`, instantiationService, themeService, contextService, storageService, layoutService, contextMenuService, contextKeyService);754}755}756757export class StatusbarService extends MultiWindowParts<StatusbarPart> implements IStatusbarService {758759declare readonly _serviceBrand: undefined;760761readonly mainPart: MainStatusbarPart;762763private readonly _onDidCreateAuxiliaryStatusbarPart = this._register(new Emitter<AuxiliaryStatusbarPart>());764private readonly onDidCreateAuxiliaryStatusbarPart = this._onDidCreateAuxiliaryStatusbarPart.event;765766constructor(767@IInstantiationService private readonly instantiationService: IInstantiationService,768@IStorageService storageService: IStorageService,769@IThemeService themeService: IThemeService770) {771super('workbench.statusBarService', themeService, storageService);772773this.mainPart = this._register(this.instantiationService.createInstance(MainStatusbarPart));774this._register(this.registerPart(this.mainPart));775776this.onDidChangeEntryVisibility = this.mainPart.onDidChangeEntryVisibility;777}778779//#region Auxiliary Statusbar Parts780781createAuxiliaryStatusbarPart(container: HTMLElement, instantiationService: IInstantiationService): IAuxiliaryStatusbarPart {782783// Container784const statusbarPartContainer = $('footer.part.statusbar', {785'role': 'status',786'aria-live': 'off',787'tabIndex': '0'788});789statusbarPartContainer.style.position = 'relative';790container.appendChild(statusbarPartContainer);791792// Statusbar Part793const statusbarPart = instantiationService.createInstance(AuxiliaryStatusbarPart, statusbarPartContainer);794const disposable = this.registerPart(statusbarPart);795796statusbarPart.create(statusbarPartContainer);797798Event.once(statusbarPart.onWillDispose)(() => disposable.dispose());799800// Emit internal event801this._onDidCreateAuxiliaryStatusbarPart.fire(statusbarPart);802803return statusbarPart;804}805806createScoped(statusbarEntryContainer: IStatusbarEntryContainer, disposables: DisposableStore): IStatusbarService {807return disposables.add(this.instantiationService.createInstance(ScopedStatusbarService, statusbarEntryContainer));808}809810//#endregion811812//#region Service Implementation813814readonly onDidChangeEntryVisibility: Event<{ id: string; visible: boolean }>;815816addEntry(entry: IStatusbarEntry, id: string, alignment: StatusbarAlignment, priorityOrLocation: number | IStatusbarEntryLocation | IStatusbarEntryPriority = 0): IStatusbarEntryAccessor {817if (entry.showInAllWindows) {818return this.doAddEntryToAllWindows(entry, id, alignment, priorityOrLocation);819}820821return this.mainPart.addEntry(entry, id, alignment, priorityOrLocation);822}823824private doAddEntryToAllWindows(originalEntry: IStatusbarEntry, id: string, alignment: StatusbarAlignment, priorityOrLocation: number | IStatusbarEntryLocation | IStatusbarEntryPriority = 0): IStatusbarEntryAccessor {825const entryDisposables = new DisposableStore();826827const accessors = new Set<IStatusbarEntryAccessor>();828829let entry = originalEntry;830function addEntry(part: StatusbarPart | AuxiliaryStatusbarPart): void {831const partDisposables = new DisposableStore();832partDisposables.add(part.onWillDispose(() => partDisposables.dispose()));833834const accessor = partDisposables.add(part.addEntry(entry, id, alignment, priorityOrLocation));835accessors.add(accessor);836partDisposables.add(toDisposable(() => accessors.delete(accessor)));837838entryDisposables.add(partDisposables);839partDisposables.add(toDisposable(() => entryDisposables.delete(partDisposables)));840}841842for (const part of this.parts) {843addEntry(part);844}845846entryDisposables.add(this.onDidCreateAuxiliaryStatusbarPart(part => addEntry(part)));847848return {849update: (updatedEntry: IStatusbarEntry) => {850entry = updatedEntry;851852for (const update of accessors) {853update.update(updatedEntry);854}855},856dispose: () => entryDisposables.dispose()857};858}859860isEntryVisible(id: string): boolean {861return this.mainPart.isEntryVisible(id);862}863864updateEntryVisibility(id: string, visible: boolean): void {865for (const part of this.parts) {866part.updateEntryVisibility(id, visible);867}868}869870overrideEntry(id: string, override: Partial<IStatusbarEntry>): IDisposable {871const disposables = new DisposableStore();872873for (const part of this.parts) {874disposables.add(part.overrideEntry(id, override));875}876877return disposables;878}879880focus(preserveEntryFocus?: boolean): void {881this.activePart.focus(preserveEntryFocus);882}883884focusNextEntry(): void {885this.activePart.focusNextEntry();886}887888focusPreviousEntry(): void {889this.activePart.focusPreviousEntry();890}891892isEntryFocused(): boolean {893return this.activePart.isEntryFocused();894}895896overrideStyle(style: IStatusbarStyleOverride): IDisposable {897const disposables = new DisposableStore();898899for (const part of this.parts) {900disposables.add(part.overrideStyle(style));901}902903return disposables;904}905906//#endregion907}908909export class ScopedStatusbarService extends Disposable implements IStatusbarService {910911declare readonly _serviceBrand: undefined;912913constructor(914private readonly statusbarEntryContainer: IStatusbarEntryContainer,915@IStatusbarService private readonly statusbarService: IStatusbarService916) {917super();918919this.onDidChangeEntryVisibility = this.statusbarEntryContainer.onDidChangeEntryVisibility;920}921922createAuxiliaryStatusbarPart(container: HTMLElement, instantiationService: IInstantiationService): IAuxiliaryStatusbarPart {923return this.statusbarService.createAuxiliaryStatusbarPart(container, instantiationService);924}925926createScoped(statusbarEntryContainer: IStatusbarEntryContainer, disposables: DisposableStore): IStatusbarService {927return this.statusbarService.createScoped(statusbarEntryContainer, disposables);928}929930getPart(): IStatusbarEntryContainer {931return this.statusbarEntryContainer;932}933934readonly onDidChangeEntryVisibility: Event<{ id: string; visible: boolean }>;935936addEntry(entry: IStatusbarEntry, id: string, alignment: StatusbarAlignment, priorityOrLocation: number | IStatusbarEntryLocation | IStatusbarEntryPriority = 0): IStatusbarEntryAccessor {937return this.statusbarEntryContainer.addEntry(entry, id, alignment, priorityOrLocation);938}939940isEntryVisible(id: string): boolean {941return this.statusbarEntryContainer.isEntryVisible(id);942}943944updateEntryVisibility(id: string, visible: boolean): void {945this.statusbarEntryContainer.updateEntryVisibility(id, visible);946}947948overrideEntry(id: string, override: Partial<IStatusbarEntry>): IDisposable {949return this.statusbarEntryContainer.overrideEntry(id, override);950}951952focus(preserveEntryFocus?: boolean): void {953this.statusbarEntryContainer.focus(preserveEntryFocus);954}955956focusNextEntry(): void {957this.statusbarEntryContainer.focusNextEntry();958}959960focusPreviousEntry(): void {961this.statusbarEntryContainer.focusPreviousEntry();962}963964isEntryFocused(): boolean {965return this.statusbarEntryContainer.isEntryFocused();966}967968overrideStyle(style: IStatusbarStyleOverride): IDisposable {969return this.statusbarEntryContainer.overrideStyle(style);970}971}972973registerSingleton(IStatusbarService, StatusbarService, InstantiationType.Eager);974975976