Path: blob/main/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts
3296 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// Border color648const borderColor = this.getColor(styleOverride?.border ?? (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? STATUS_BAR_BORDER : STATUS_BAR_NO_FOLDER_BORDER)) || this.getColor(contrastBorder);649if (borderColor) {650container.classList.add('status-border-top');651container.style.setProperty('--status-border-top-color', borderColor);652} else {653container.classList.remove('status-border-top');654container.style.removeProperty('--status-border-top-color');655}656657// Colors and focus outlines via dynamic stylesheet658659const statusBarFocusColor = this.getColor(STATUS_BAR_FOCUS_BORDER);660661if (!this.styleElement) {662this.styleElement = createStyleSheet(container);663}664665this.styleElement.textContent = `666667/* Status bar focus outline */668.monaco-workbench .part.statusbar:focus {669outline-color: ${statusBarFocusColor};670}671672/* Status bar item focus outline */673.monaco-workbench .part.statusbar > .items-container > .statusbar-item a:focus-visible {674outline: 1px solid ${this.getColor(activeContrastBorder) ?? itemBorderColor};675outline-offset: ${borderColor ? '-2px' : '-1px'};676}677678/* Notification Beak */679.monaco-workbench .part.statusbar > .items-container > .statusbar-item.has-beak > .status-bar-item-beak-container:before {680border-bottom-color: ${borderColor ?? backgroundColor};681}682`;683}684685override layout(width: number, height: number, top: number, left: number): void {686super.layout(width, height, top, left);687super.layoutContents(width, height);688}689690overrideStyle(style: IStatusbarStyleOverride): IDisposable {691this.styleOverrides.add(style);692this.updateStyles();693694return toDisposable(() => {695this.styleOverrides.delete(style);696this.updateStyles();697});698}699700toJSON(): object {701return {702type: Parts.STATUSBAR_PART703};704}705706override dispose(): void {707this._onWillDispose.fire();708709super.dispose();710}711}712713export class MainStatusbarPart extends StatusbarPart {714715constructor(716@IInstantiationService instantiationService: IInstantiationService,717@IThemeService themeService: IThemeService,718@IWorkspaceContextService contextService: IWorkspaceContextService,719@IStorageService storageService: IStorageService,720@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,721@IContextMenuService contextMenuService: IContextMenuService,722@IContextKeyService contextKeyService: IContextKeyService,723) {724super(Parts.STATUSBAR_PART, instantiationService, themeService, contextService, storageService, layoutService, contextMenuService, contextKeyService);725}726}727728export interface IAuxiliaryStatusbarPart extends IStatusbarEntryContainer, IView {729readonly container: HTMLElement;730readonly height: number;731}732733export class AuxiliaryStatusbarPart extends StatusbarPart implements IAuxiliaryStatusbarPart {734735private static COUNTER = 1;736737readonly height = StatusbarPart.HEIGHT;738739constructor(740readonly container: HTMLElement,741@IInstantiationService instantiationService: IInstantiationService,742@IThemeService themeService: IThemeService,743@IWorkspaceContextService contextService: IWorkspaceContextService,744@IStorageService storageService: IStorageService,745@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,746@IContextMenuService contextMenuService: IContextMenuService,747@IContextKeyService contextKeyService: IContextKeyService,748) {749const id = AuxiliaryStatusbarPart.COUNTER++;750super(`workbench.parts.auxiliaryStatus.${id}`, instantiationService, themeService, contextService, storageService, layoutService, contextMenuService, contextKeyService);751}752}753754export class StatusbarService extends MultiWindowParts<StatusbarPart> implements IStatusbarService {755756declare readonly _serviceBrand: undefined;757758readonly mainPart: MainStatusbarPart;759760private readonly _onDidCreateAuxiliaryStatusbarPart = this._register(new Emitter<AuxiliaryStatusbarPart>());761private readonly onDidCreateAuxiliaryStatusbarPart = this._onDidCreateAuxiliaryStatusbarPart.event;762763constructor(764@IInstantiationService private readonly instantiationService: IInstantiationService,765@IStorageService storageService: IStorageService,766@IThemeService themeService: IThemeService767) {768super('workbench.statusBarService', themeService, storageService);769770this.mainPart = this._register(this.instantiationService.createInstance(MainStatusbarPart));771this._register(this.registerPart(this.mainPart));772773this.onDidChangeEntryVisibility = this.mainPart.onDidChangeEntryVisibility;774}775776//#region Auxiliary Statusbar Parts777778createAuxiliaryStatusbarPart(container: HTMLElement, instantiationService: IInstantiationService): IAuxiliaryStatusbarPart {779780// Container781const statusbarPartContainer = $('footer.part.statusbar', {782'role': 'status',783'aria-live': 'off',784'tabIndex': '0'785});786statusbarPartContainer.style.position = 'relative';787container.appendChild(statusbarPartContainer);788789// Statusbar Part790const statusbarPart = instantiationService.createInstance(AuxiliaryStatusbarPart, statusbarPartContainer);791const disposable = this.registerPart(statusbarPart);792793statusbarPart.create(statusbarPartContainer);794795Event.once(statusbarPart.onWillDispose)(() => disposable.dispose());796797// Emit internal event798this._onDidCreateAuxiliaryStatusbarPart.fire(statusbarPart);799800return statusbarPart;801}802803createScoped(statusbarEntryContainer: IStatusbarEntryContainer, disposables: DisposableStore): IStatusbarService {804return disposables.add(this.instantiationService.createInstance(ScopedStatusbarService, statusbarEntryContainer));805}806807//#endregion808809//#region Service Implementation810811readonly onDidChangeEntryVisibility: Event<{ id: string; visible: boolean }>;812813addEntry(entry: IStatusbarEntry, id: string, alignment: StatusbarAlignment, priorityOrLocation: number | IStatusbarEntryLocation | IStatusbarEntryPriority = 0): IStatusbarEntryAccessor {814if (entry.showInAllWindows) {815return this.doAddEntryToAllWindows(entry, id, alignment, priorityOrLocation);816}817818return this.mainPart.addEntry(entry, id, alignment, priorityOrLocation);819}820821private doAddEntryToAllWindows(originalEntry: IStatusbarEntry, id: string, alignment: StatusbarAlignment, priorityOrLocation: number | IStatusbarEntryLocation | IStatusbarEntryPriority = 0): IStatusbarEntryAccessor {822const entryDisposables = new DisposableStore();823824const accessors = new Set<IStatusbarEntryAccessor>();825826let entry = originalEntry;827function addEntry(part: StatusbarPart | AuxiliaryStatusbarPart): void {828const partDisposables = new DisposableStore();829partDisposables.add(part.onWillDispose(() => partDisposables.dispose()));830831const accessor = partDisposables.add(part.addEntry(entry, id, alignment, priorityOrLocation));832accessors.add(accessor);833partDisposables.add(toDisposable(() => accessors.delete(accessor)));834835entryDisposables.add(partDisposables);836partDisposables.add(toDisposable(() => entryDisposables.delete(partDisposables)));837}838839for (const part of this.parts) {840addEntry(part);841}842843entryDisposables.add(this.onDidCreateAuxiliaryStatusbarPart(part => addEntry(part)));844845return {846update: (updatedEntry: IStatusbarEntry) => {847entry = updatedEntry;848849for (const update of accessors) {850update.update(updatedEntry);851}852},853dispose: () => entryDisposables.dispose()854};855}856857isEntryVisible(id: string): boolean {858return this.mainPart.isEntryVisible(id);859}860861updateEntryVisibility(id: string, visible: boolean): void {862for (const part of this.parts) {863part.updateEntryVisibility(id, visible);864}865}866867overrideEntry(id: string, override: Partial<IStatusbarEntry>): IDisposable {868const disposables = new DisposableStore();869870for (const part of this.parts) {871disposables.add(part.overrideEntry(id, override));872}873874return disposables;875}876877focus(preserveEntryFocus?: boolean): void {878this.activePart.focus(preserveEntryFocus);879}880881focusNextEntry(): void {882this.activePart.focusNextEntry();883}884885focusPreviousEntry(): void {886this.activePart.focusPreviousEntry();887}888889isEntryFocused(): boolean {890return this.activePart.isEntryFocused();891}892893overrideStyle(style: IStatusbarStyleOverride): IDisposable {894const disposables = new DisposableStore();895896for (const part of this.parts) {897disposables.add(part.overrideStyle(style));898}899900return disposables;901}902903//#endregion904}905906export class ScopedStatusbarService extends Disposable implements IStatusbarService {907908declare readonly _serviceBrand: undefined;909910constructor(911private readonly statusbarEntryContainer: IStatusbarEntryContainer,912@IStatusbarService private readonly statusbarService: IStatusbarService913) {914super();915916this.onDidChangeEntryVisibility = this.statusbarEntryContainer.onDidChangeEntryVisibility;917}918919createAuxiliaryStatusbarPart(container: HTMLElement, instantiationService: IInstantiationService): IAuxiliaryStatusbarPart {920return this.statusbarService.createAuxiliaryStatusbarPart(container, instantiationService);921}922923createScoped(statusbarEntryContainer: IStatusbarEntryContainer, disposables: DisposableStore): IStatusbarService {924return this.statusbarService.createScoped(statusbarEntryContainer, disposables);925}926927getPart(): IStatusbarEntryContainer {928return this.statusbarEntryContainer;929}930931readonly onDidChangeEntryVisibility: Event<{ id: string; visible: boolean }>;932933addEntry(entry: IStatusbarEntry, id: string, alignment: StatusbarAlignment, priorityOrLocation: number | IStatusbarEntryLocation | IStatusbarEntryPriority = 0): IStatusbarEntryAccessor {934return this.statusbarEntryContainer.addEntry(entry, id, alignment, priorityOrLocation);935}936937isEntryVisible(id: string): boolean {938return this.statusbarEntryContainer.isEntryVisible(id);939}940941updateEntryVisibility(id: string, visible: boolean): void {942this.statusbarEntryContainer.updateEntryVisibility(id, visible);943}944945overrideEntry(id: string, override: Partial<IStatusbarEntry>): IDisposable {946return this.statusbarEntryContainer.overrideEntry(id, override);947}948949focus(preserveEntryFocus?: boolean): void {950this.statusbarEntryContainer.focus(preserveEntryFocus);951}952953focusNextEntry(): void {954this.statusbarEntryContainer.focusNextEntry();955}956957focusPreviousEntry(): void {958this.statusbarEntryContainer.focusPreviousEntry();959}960961isEntryFocused(): boolean {962return this.statusbarEntryContainer.isEntryFocused();963}964965overrideStyle(style: IStatusbarStyleOverride): IDisposable {966return this.statusbarEntryContainer.overrideStyle(style);967}968}969970registerSingleton(IStatusbarService, StatusbarService, InstantiationType.Eager);971972973