Improve window.withProgress in status bar to use a static spin icon (fix #108657)

This commit is contained in:
Benjamin Pasero 2020-10-19 08:04:47 +02:00
parent 74ef0a92fe
commit 8d7ad831e5
7 changed files with 100 additions and 24 deletions

View file

@ -18,7 +18,7 @@ export function renderCodicons(text: string): Array<HTMLSpanElement | string> {
textStart = (match.index || 0) + match[0].length;
const [, escaped, codicon, name, animation] = match;
elements.push(escaped ? `$(${codicon})` : dom.$(`span.codicon.codicon-${name}${animation ? `.codicon-animation-${animation}` : ''}`));
elements.push(escaped ? `$(${codicon})` : renderCodicon(name, animation));
}
if (textStart < text.length) {
@ -26,3 +26,7 @@ export function renderCodicons(text: string): Array<HTMLSpanElement | string> {
}
return elements;
}
export function renderCodicon(name: string, animation: string): HTMLSpanElement {
return dom.$(`span.codicon.codicon-${name}${animation ? `.codicon-animation-${animation}` : ''}`);
}

View file

@ -998,8 +998,15 @@ export function prepend<T extends Node>(parent: HTMLElement, child: T): T {
/**
* Removes all children from `parent` and appends `children`
*/
export function reset(parent: HTMLElement, ...children: Array<Node | string>) {
export function reset(parent: HTMLElement, ...children: Array<Node | string>): void {
parent.innerText = '';
appendChildren(parent, ...children);
}
/**
* Appends `children` to `parent`
*/
export function appendChildren(parent: HTMLElement, ...children: Array<Node | string>): void {
for (const child of children) {
if (child instanceof Node) {
parent.appendChild(child);

View file

@ -21,7 +21,7 @@ import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/
import { contrastBorder, activeContrastBorder } from 'vs/platform/theme/common/colorRegistry';
import { isThemeColor } from 'vs/editor/common/editorCommon';
import { Color } from 'vs/base/common/color';
import { EventHelper, createStyleSheet, addDisposableListener, EventType, hide, show, isAncestor } from 'vs/base/browser/dom';
import { EventHelper, createStyleSheet, addDisposableListener, EventType, hide, show, isAncestor, appendChildren } from 'vs/base/browser/dom';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IStorageService, StorageScope, IWorkspaceStorageChangeEvent } from 'vs/platform/storage/common/storage';
import { Parts, IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
@ -39,6 +39,7 @@ import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/co
import { ServicesAccessor } from 'vs/editor/browser/editorExtensions';
import { RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { ColorScheme } from 'vs/platform/theme/common/theme';
import { renderCodicon, renderCodicons } from 'vs/base/browser/codicons';
interface IPendingStatusbarEntry {
id: string;
@ -64,17 +65,18 @@ class StatusbarViewModel extends Disposable {
static readonly HIDDEN_ENTRIES_KEY = 'workbench.statusbar.hidden';
private readonly _onDidChangeEntryVisibility = this._register(new Emitter<{ id: string, visible: boolean }>());
readonly onDidChangeEntryVisibility = this._onDidChangeEntryVisibility.event;
private readonly _entries: IStatusbarViewModelEntry[] = [];
get entries(): IStatusbarViewModelEntry[] { return this._entries; }
private hidden!: Set<string>;
private _lastFocusedEntry: IStatusbarViewModelEntry | undefined;
get lastFocusedEntry(): IStatusbarViewModelEntry | undefined {
return this._lastFocusedEntry && !this.isHidden(this._lastFocusedEntry.id) ? this._lastFocusedEntry : undefined;
}
private _lastFocusedEntry: IStatusbarViewModelEntry | undefined;
private readonly _onDidChangeEntryVisibility = this._register(new Emitter<{ id: string, visible: boolean }>());
readonly onDidChangeEntryVisibility = this._onDidChangeEntryVisibility.event;
private hidden!: Set<string>;
constructor(private readonly storageService: IStorageService) {
super();
@ -706,12 +708,67 @@ export class StatusbarPart extends Part implements IStatusbarService {
}
}
class StatusBarCodiconLabel extends CodiconLabel {
private readonly progressCodicon = renderCodicon('sync', 'spin');
private currentText = '';
private currentShowProgress = false;
constructor(
private readonly container: HTMLElement
) {
super(container);
}
set showProgress(showProgress: boolean) {
if (this.currentShowProgress !== showProgress) {
this.currentShowProgress = showProgress;
this.text = this.currentText;
}
}
set text(text: string) {
// Progress: insert progress codicon as first element as needed
// but keep it stable so that the animation does not reset
if (this.currentShowProgress) {
// Append as needed
if (this.container.firstChild !== this.progressCodicon) {
this.container.appendChild(this.progressCodicon);
}
// Remove others
for (const node of Array.from(this.container.childNodes)) {
if (node !== this.progressCodicon) {
node.remove();
}
}
// If we have text to show, add a space to separate from progress
let textContent = text ?? '';
if (textContent) {
textContent = ` ${textContent}`;
}
// Append new elements
appendChildren(this.container, ...renderCodicons(textContent));
}
// No Progress: no special handling
else {
super.text = text;
}
}
}
class StatusbarEntryItem extends Disposable {
private entry!: IStatusbarEntry;
readonly labelContainer: HTMLElement;
private readonly label: StatusBarCodiconLabel;
labelContainer!: HTMLElement;
private label!: CodiconLabel;
private entry: IStatusbarEntry | undefined = undefined;
private readonly foregroundListener = this._register(new MutableDisposable());
private readonly backgroundListener = this._register(new MutableDisposable());
@ -729,26 +786,25 @@ class StatusbarEntryItem extends Disposable {
) {
super();
this.create();
this.update(entry);
}
private create(): void {
// Label Container
this.labelContainer = document.createElement('a');
this.labelContainer.tabIndex = -1; // allows screen readers to read title, but still prevents tab focus.
this.labelContainer.setAttribute('role', 'button');
// Label
this.label = new CodiconLabel(this.labelContainer);
// Label (with support for progress)
this.label = new StatusBarCodiconLabel(this.labelContainer);
// Add to parent
this.container.appendChild(this.labelContainer);
this.update(entry);
}
update(entry: IStatusbarEntry): void {
// Update: Progress
this.label.showProgress = !!entry.showProgress;
// Update: Text
if (!this.entry || entry.text !== this.entry.text) {
this.label.text = entry.text;
@ -760,8 +816,9 @@ class StatusbarEntryItem extends Disposable {
}
}
// Set the aria label on both elements so screen readers would read
// the correct thing without duplication #96210
if (!this.entry || entry.ariaLabel !== this.entry.ariaLabel) {
// Set the aria label on both elements so screen readers would read the correct thing without duplication #96210
this.container.setAttribute('aria-label', entry.ariaLabel);
this.labelContainer.setAttribute('aria-label', entry.ariaLabel);
}

View file

@ -82,7 +82,8 @@ export class ExtensionHostProfileService extends Disposable implements IExtensio
if (visible) {
const indicator: IStatusbarEntry = {
text: '$(sync~spin) ' + nls.localize('profilingExtensionHost', "Profiling Extension Host"),
text: nls.localize('profilingExtensionHost', "Profiling Extension Host"),
showProgress: true,
ariaLabel: nls.localize('profilingExtensionHost', "Profiling Extension Host"),
tooltip: nls.localize('selectAndStartDebug', "Click to stop profiling."),
command: 'workbench.action.extensionHostProfilder.stop'
@ -91,7 +92,7 @@ export class ExtensionHostProfileService extends Disposable implements IExtensio
const timeStarted = Date.now();
const handle = setInterval(() => {
if (this.profilingStatusBarIndicator) {
this.profilingStatusBarIndicator.update({ ...indicator, text: '$(sync~spin) ' + nls.localize('profilingExtensionHostTime', "Profiling Extension Host ({0} sec)", Math.round((new Date().getTime() - timeStarted) / 1000)), });
this.profilingStatusBarIndicator.update({ ...indicator, text: nls.localize('profilingExtensionHostTime', "Profiling Extension Host ({0} sec)", Math.round((new Date().getTime() - timeStarted) / 1000)), });
}
}, 1000);
this.profilingStatusBarIndicatorLabelUpdater.value = toDisposable(() => clearInterval(handle));

View file

@ -197,7 +197,7 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
const hostLabel = this.labelService.getHostLabel(Schemas.vscodeRemote, this.remoteAuthority) || this.remoteAuthority;
switch (this.connectionState) {
case 'initializing':
this.renderRemoteStatusIndicator(`$(sync~spin) ${nls.localize('host.open', "Opening Remote...")}`, nls.localize('host.open', "Opening Remote..."));
this.renderRemoteStatusIndicator(nls.localize('host.open', "Opening Remote..."), nls.localize('host.open', "Opening Remote..."), undefined, true /* progress */);
break;
case 'disconnected':
this.renderRemoteStatusIndicator(`$(alert) ${nls.localize('disconnectedFrom', "Disconnected from {0}", hostLabel)}`, nls.localize('host.tooltipDisconnected', "Disconnected from {0}", hostLabel));
@ -219,7 +219,7 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
}
}
private renderRemoteStatusIndicator(text: string, tooltip?: string, command?: string): void {
private renderRemoteStatusIndicator(text: string, tooltip?: string, command?: string, showProgress?: boolean): void {
const name = nls.localize('remoteHost', "Remote Host");
if (typeof command !== 'string' && this.remoteMenu.getActions().length > 0) {
command = RemoteStatusIndicator.REMOTE_ACTIONS_COMMAND_ID;
@ -230,6 +230,7 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
color: themeColorFromId(STATUS_BAR_HOST_NAME_FOREGROUND),
ariaLabel: name,
text,
showProgress,
tooltip,
command
};

View file

@ -151,7 +151,8 @@ export class ProgressService extends Disposable implements IProgressService {
}
const statusEntryProperties: IStatusbarEntry = {
text: `$(sync~spin) ${text}`,
text,
showProgress: true,
ariaLabel: text,
tooltip: title,
command: progressCommand

View file

@ -63,6 +63,11 @@ export interface IStatusbarEntry {
* Whether to show a beak above the status bar entry.
*/
readonly showBeak?: boolean;
/**
* Will enable a spinning icon in front of the text to indicate progress.
*/
readonly showProgress?: boolean;
}
export interface IStatusbarService {