Skip to content

Commit

Permalink
status - improve hover feedback for compact entries (#129037)
Browse files Browse the repository at this point in the history
  • Loading branch information
bpasero committed Sep 18, 2021
1 parent f01d9ab commit 54cdd9c
Show file tree
Hide file tree
Showing 9 changed files with 112 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export class CapabilitiesStatus extends Disposable {

this._statusItem = this._register(vscode.languages.createLanguageStatusItem('typescript.capabilities', jsTsLanguageModes));

this._statusItem.name = localize('capabilitiesStatus.name', "IntelliSense IntelliSense Status");
this._statusItem.name = localize('capabilitiesStatus.name', "IntelliSense Status");

this._register(this._client.onTsServerStarted(() => this.update()));
this._register(this._client.onDidChangeCapabilities(() => this.update()));
Expand Down
2 changes: 1 addition & 1 deletion src/vs/base/browser/ui/dropdown/dropdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export class BaseDropdown extends ActionRunner {
private contents?: HTMLElement;

private visible: boolean | undefined;
private _onDidChangeVisibility = new Emitter<boolean>();
private _onDidChangeVisibility = this._register(new Emitter<boolean>());
readonly onDidChangeVisibility = this._onDidChangeVisibility.event;

constructor(container: HTMLElement, options: IBaseDropdownOptions) {
Expand Down
18 changes: 15 additions & 3 deletions src/vs/base/browser/ui/iconLabel/iconLabelHover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { IHoverDelegate, IHoverDelegateOptions, IHoverDelegateTarget, IHoverWidg
import { IIconLabelMarkdownString } from 'vs/base/browser/ui/iconLabel/iconLabel';
import { RunOnceScheduler } from 'vs/base/common/async';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { IMarkdownString } from 'vs/base/common/htmlContent';
import { IMarkdownString, isMarkdownString } from 'vs/base/common/htmlContent';
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { isFunction, isString } from 'vs/base/common/types';
import { localize } from 'vs/nls';
Expand Down Expand Up @@ -93,9 +93,9 @@ class UpdatableHoverWidget implements IDisposable {
private show(content: ResolvedMarkdownTooltipContent): void {
const oldHoverWidget = this._hoverWidget;

if (content) {
if (this.hasContent(content)) {
const hoverOptions: IHoverDelegateOptions = {
content: content,
content,
target: this.target,
showPointer: this.hoverDelegate.placement === 'element',
hoverPosition: HoverPosition.BELOW,
Expand All @@ -107,6 +107,18 @@ class UpdatableHoverWidget implements IDisposable {
oldHoverWidget?.dispose();
}

private hasContent(content: ResolvedMarkdownTooltipContent): content is NonNullable<ResolvedMarkdownTooltipContent> {
if (!content) {
return false;
}

if (isMarkdownString(content)) {
return this.hasContent(content.value);
}

return true;
}

get isDisposed() {
return this._hoverWidget?.isDisposed;
}
Expand Down
4 changes: 2 additions & 2 deletions src/vs/workbench/browser/parts/banner/bannerPart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ registerThemingParticipant((theme, collector) => {
const CONTEXT_BANNER_FOCUSED = new RawContextKey<boolean>('bannerFocused', false, localize('bannerFocused', "Whether the banner has keyboard focus"));

export class BannerPart extends Part implements IBannerService {

declare readonly _serviceBrand: undefined;

// #region IView
Expand All @@ -80,8 +81,7 @@ export class BannerPart extends Part implements IBannerService {
return this.visible ? this.height : 0;
}

private _onDidChangeSize = new Emitter<{ width: number; height: number; } | undefined>();

private _onDidChangeSize = this._register(new Emitter<{ width: number; height: number; } | undefined>());
override get onDidChange() { return this._onDidChangeSize.event; }

//#endregion
Expand Down
4 changes: 4 additions & 0 deletions src/vs/workbench/browser/parts/statusbar/statusbarItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ export class StatusbarEntryItem extends Disposable {
return assertIsDefined(this.entry).name;
}

get hasCommand(): boolean {
return typeof this.entry?.command !== 'undefined';
}

constructor(
private container: HTMLElement,
entry: IStatusbarEntry,
Expand Down
1 change: 1 addition & 0 deletions src/vs/workbench/browser/parts/statusbar/statusbarModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export function isStatusbarEntryLocation(thing: unknown): thing is IStatusbarEnt
export interface IStatusbarViewModelEntry {
readonly id: string;
readonly name: string;
readonly hasCommand: boolean;
readonly alignment: StatusbarAlignment;
readonly priority: IStatusbarEntryPriority;
readonly container: HTMLElement;
Expand Down
77 changes: 59 additions & 18 deletions src/vs/workbench/browser/parts/statusbar/statusbarPart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import 'vs/css!./media/statusbarpart';
import { localize } from 'vs/nls';
import { dispose } from 'vs/base/common/lifecycle';
import { DisposableStore, dispose, MutableDisposable } from 'vs/base/common/lifecycle';
import { Part } from 'vs/workbench/browser/part';
import { EventType as TouchEventType, Gesture, GestureEvent } from 'vs/base/browser/touch';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
Expand Down Expand Up @@ -95,6 +95,8 @@ export class StatusbarPart extends Part implements IStatusbarService {
}
}(this.configurationService, this.hoverService);

private readonly compactEntriesDisposable = this._register(new MutableDisposable<DisposableStore>());

constructor(
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IThemeService themeService: IThemeService,
Expand All @@ -114,7 +116,7 @@ export class StatusbarPart extends Part implements IStatusbarService {
private registerListeners(): void {

// Entry visibility changes
this._register(this.onDidChangeEntryVisibility(() => this.updateClasses()));
this._register(this.onDidChangeEntryVisibility(() => this.updateCompactEntries()));

// Workbench state changes
this._register(this.contextService.onDidChangeWorkbenchState(() => this.updateStyles()));
Expand Down Expand Up @@ -176,6 +178,7 @@ export class StatusbarPart extends Part implements IStatusbarService {
readonly labelContainer = item.labelContainer;

get name() { return item.name; }
get hasCommand() { return item.hasCommand; }
};

// Add to view model
Expand Down Expand Up @@ -337,8 +340,8 @@ export class StatusbarPart extends Part implements IStatusbarService {
target.appendChild(entry.container);
}

// Update CSS classes
this.updateClasses();
// Update compact entries
this.updateCompactEntries();
}

private appendStatusbarEntry(entry: IStatusbarViewModelEntry): void {
Expand All @@ -357,14 +360,14 @@ export class StatusbarPart extends Part implements IStatusbarService {
target.insertBefore(entry.container, entries[index + 1].container); // insert before next element otherwise
}

// Update CSS classes
this.updateClasses();
// Update compact entries
this.updateCompactEntries();
}

private updateClasses(): void {
private updateCompactEntries(): void {
const entries = this.viewModel.entries;

// Clear compact related CSS classes if any
// Find visible entries and clear compact related CSS classes if any
const mapIdToVisibleEntry = new Map<string, IStatusbarViewModelEntry>();
for (const entry of entries) {
if (!this.viewModel.isHidden(entry.id)) {
Expand All @@ -374,21 +377,58 @@ export class StatusbarPart extends Part implements IStatusbarService {
entry.container.classList.remove('compact-left', 'compact-right');
}

// Update entries with compact related CSS classes as needed
// Figure out groups of entries with `compact` alignment
const compactEntryGroups = new Map<string, Set<IStatusbarViewModelEntry>>();
for (const entry of mapIdToVisibleEntry.values()) {
if (
isStatusbarEntryLocation(entry.priority.primary) && // entry references another entry as location
entry.priority.primary.compact // entry wants to be compact
) {
const location = mapIdToVisibleEntry.get(entry.priority.primary.id);
if (location) {
if (entry.priority.primary.alignment === StatusbarAlignment.LEFT) {
location.container.classList.add('compact-left');
entry.container.classList.add('compact-right');
} else {
location.container.classList.add('compact-right');
entry.container.classList.add('compact-left');
const locationId = entry.priority.primary.id;
const location = mapIdToVisibleEntry.get(locationId);
if (!location) {
continue; // skip if location does not exist
}

// Build a map of entries that are compact among each other
let compactEntryGroup = compactEntryGroups.get(locationId);
if (!compactEntryGroup) {
compactEntryGroup = new Set<IStatusbarViewModelEntry>([entry, location]);
compactEntryGroups.set(locationId, compactEntryGroup);
} else {
compactEntryGroup.add(entry);
}

// Adjust CSS classes to move compact items closer together
if (entry.priority.primary.alignment === StatusbarAlignment.LEFT) {
location.container.classList.add('compact-left');
entry.container.classList.add('compact-right');
} else {
location.container.classList.add('compact-right');
entry.container.classList.add('compact-left');
}
}
}


// Install mouse listeners to update hover feedback for
// all compact entries that belong to each other
const statusBarItemHoverBackground = this.getColor(STATUS_BAR_ITEM_HOVER_BACKGROUND)?.toString();
this.compactEntriesDisposable.value = new DisposableStore();
if (statusBarItemHoverBackground && this.theme.type !== ColorScheme.HIGH_CONTRAST) {
for (const [, compactEntryGroup] of compactEntryGroups) {
for (const compactEntry of compactEntryGroup) {
if (!compactEntry.hasCommand) {
continue; // only show hover feedback when we have a command
}

this.compactEntriesDisposable.value.add(addDisposableListener(compactEntry.labelContainer, EventType.MOUSE_OVER, () => {
compactEntryGroup.forEach(compactEntry => compactEntry.labelContainer.style.backgroundColor = statusBarItemHoverBackground);
}));

this.compactEntriesDisposable.value.add(addDisposableListener(compactEntry.labelContainer, EventType.MOUSE_OUT, () => {
compactEntryGroup.forEach(compactEntry => compactEntry.labelContainer.style.backgroundColor = '');
}));
}
}
}
Expand Down Expand Up @@ -502,7 +542,8 @@ registerThemingParticipant((theme, collector) => {

const statusBarItemActiveBackground = theme.getColor(STATUS_BAR_ITEM_ACTIVE_BACKGROUND);
if (statusBarItemActiveBackground) {
collector.addRule(`.monaco-workbench .part.statusbar > .items-container > .statusbar-item a:active:not(.disabled) { background-color: ${statusBarItemActiveBackground}; }`);
// using !important for this rule to win over any background color that is set via JS code for compact items in a group
collector.addRule(`.monaco-workbench .part.statusbar > .items-container > .statusbar-item a:active:not(.disabled) { background-color: ${statusBarItemActiveBackground} !important; }`);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/vs/workbench/services/hover/browser/hoverWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,7 @@ class CompositeMouseTracker extends Widget {
private _isMouseIn: boolean = false;
private _mouseTimeout: number | undefined;

private readonly _onMouseOut = new Emitter<void>();
private readonly _onMouseOut = this._register(new Emitter<void>());
get onMouseOut(): Event<void> { return this._onMouseOut.event; }

constructor(
Expand Down

0 comments on commit 54cdd9c

Please sign in to comment.