Skip to content

Commit 11ade90

Browse files
authored
[vscode] Support ViewBadge and tree/web view badge property (#12330)
Extend BadgeWidget with tooltip support Provide support for badge on tree based views and webview views Contributed on behalf of STMicroelectronics Signed-off-by: Remi SCHNEKENBURGER <[email protected]>
1 parent 9cee5f5 commit 11ade90

File tree

13 files changed

+217
-17
lines changed

13 files changed

+217
-17
lines changed

packages/core/src/browser/view-container.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ export interface DescriptionWidget {
5959

6060
export interface BadgeWidget {
6161
badge?: number;
62+
badgeTooltip?: string;
6263
onDidChangeBadge: CommonEvent<void>;
64+
onDidChangeBadgeTooltip: CommonEvent<void>;
6365
}
6466

6567
export namespace DescriptionWidget {
@@ -70,7 +72,7 @@ export namespace DescriptionWidget {
7072

7173
export namespace BadgeWidget {
7274
export function is(arg: unknown): arg is BadgeWidget {
73-
return isObject(arg) && 'onDidChangeBadge' in arg;
75+
return isObject(arg) && 'onDidChangeBadge' in arg && 'onDidChangeBadgeTooltip' in arg;
7476
}
7577
}
7678

@@ -927,6 +929,8 @@ export class ViewContainerPart extends BaseWidget {
927929
readonly onDidChangeDescription = this.onDidChangeDescriptionEmitter.event;
928930
protected readonly onDidChangeBadgeEmitter = new Emitter<void>();
929931
readonly onDidChangeBadge = this.onDidChangeBadgeEmitter.event;
932+
protected readonly onDidChangeBadgeTooltipEmitter = new Emitter<void>();
933+
readonly onDidChangeBadgeTooltip = this.onDidChangeBadgeTooltipEmitter.event;
930934

931935
protected readonly toolbar: TabBarToolbar;
932936

@@ -962,7 +966,8 @@ export class ViewContainerPart extends BaseWidget {
962966
}
963967

964968
if (BadgeWidget.is(this.wrapped)) {
965-
this.wrapped?.onDidChangeBadge(() => this.onDidChangeBadgeEmitter.fire(), undefined, this.toDispose);
969+
this.wrapped.onDidChangeBadge(() => this.onDidChangeBadgeEmitter.fire(), undefined, this.toDispose);
970+
this.wrapped.onDidChangeBadgeTooltip(() => this.onDidChangeBadgeTooltipEmitter.fire(), undefined, this.toDispose);
966971
}
967972

968973
const { header, body, disposable } = this.createContent();
@@ -982,6 +987,7 @@ export class ViewContainerPart extends BaseWidget {
982987
this.onTitleChangedEmitter,
983988
this.onDidChangeDescriptionEmitter,
984989
this.onDidChangeBadgeEmitter,
990+
this.onDidChangeBadgeTooltipEmitter,
985991
this.registerContextMenu(),
986992
this.onDidFocusEmitter,
987993
// focus event does not bubble, capture it
@@ -1170,14 +1176,17 @@ export class ViewContainerPart extends BaseWidget {
11701176
description.innerText = DescriptionWidget.is(this.wrapped) && !this.collapsed && this.wrapped.description || '';
11711177
};
11721178
const updateBadge = () => {
1173-
const visibleToolBarItems = this.toolbarRegistry.visibleItems(this.wrapped).length > 0;
1174-
const badge = BadgeWidget.is(this.wrapped) && this.wrapped.badge;
1175-
if (typeof badge === 'number' && !visibleToolBarItems) {
1176-
badgeSpan.innerText = badge.toString();
1177-
badgeContainer.style.display = badgeContainerDisplay;
1178-
} else {
1179-
badgeContainer.style.display = 'none';
1179+
if (BadgeWidget.is(this.wrapped)) {
1180+
const visibleToolBarItems = this.toolbarRegistry.visibleItems(this.wrapped).length > 0;
1181+
const badge = this.wrapped.badge;
1182+
if (badge && !visibleToolBarItems) {
1183+
badgeSpan.innerText = badge.toString();
1184+
badgeSpan.title = this.wrapped.badgeTooltip || '';
1185+
badgeContainer.style.display = badgeContainerDisplay;
1186+
return;
1187+
}
11801188
}
1189+
badgeContainer.style.display = 'none';
11811190
};
11821191

11831192
updateTitle();
@@ -1191,6 +1200,7 @@ export class ViewContainerPart extends BaseWidget {
11911200
this.onDidMove(updateTitle),
11921201
this.onDidChangeDescription(updateDescription),
11931202
this.onDidChangeBadge(updateBadge),
1203+
this.onDidChangeBadgeTooltip(updateBadge),
11941204
this.onCollapsed(updateDescription)
11951205
]);
11961206
header.appendChild(title);

packages/plugin-ext/src/common/plugin-api-rpc.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -744,6 +744,7 @@ export interface TreeViewsMain {
744744
$setMessage(treeViewId: string, message: string): void;
745745
$setTitle(treeViewId: string, title: string): void;
746746
$setDescription(treeViewId: string, description: string): void;
747+
$setBadge(treeViewId: string, badge: theia.ViewBadge | undefined): void;
747748
}
748749
export class DataTransferFileDTO {
749750
constructor(readonly name: string, readonly contentId: string, readonly uri?: UriComponents) { }
@@ -1710,6 +1711,7 @@ export interface WebviewsMain {
17101711
$reveal(handle: string, showOptions: theia.WebviewPanelShowOptions): void;
17111712
$setTitle(handle: string, value: string): void;
17121713
$setIconPath(handle: string, value: IconUrl | undefined): void;
1714+
$setBadge(handle: string, badge: theia.ViewBadge | undefined): void;
17131715
$setHtml(handle: string, value: string): void;
17141716
$setOptions(handle: string, options: theia.WebviewOptions): void;
17151717
$postMessage(handle: string, value: any): Thenable<boolean>;
@@ -1735,6 +1737,7 @@ export interface WebviewViewsMain extends Disposable {
17351737

17361738
$setWebviewViewTitle(handle: string, value: string | undefined): void;
17371739
$setWebviewViewDescription(handle: string, value: string | undefined): void;
1740+
$setBadge(handle: string, badge: theia.ViewBadge | undefined): void;
17381741

17391742
$show(handle: string, preserveFocus: boolean): void;
17401743
}

packages/plugin-ext/src/main/browser/view/plugin-view-registry.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,14 @@ export class PluginViewRegistry implements FrontendApplicationContribution {
397397
get description(): string | undefined { return _description; },
398398
set description(value: string | undefined) { _description = value; },
399399

400+
get badge(): number | undefined { return webview.badge; },
401+
set badge(badge: number | undefined) { webview.badge = badge; },
402+
403+
get badgeTooltip(): string | undefined { return webview.badgeTooltip; },
404+
set badgeTooltip(badgeTooltip: string | undefined) { webview.badgeTooltip = badgeTooltip; },
405+
onDidChangeBadge: webview.onDidChangeBadge,
406+
onDidChangeBadgeTooltip: webview.onDidChangeBadgeTooltip,
407+
400408
dispose: webview.dispose,
401409
show: webview.show
402410
};

packages/plugin-ext/src/main/browser/view/plugin-view-widget.ts

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { CommandRegistry } from '@theia/core/lib/common/command';
2121
import { StatefulWidget } from '@theia/core/lib/browser/shell/shell-layout-restorer';
2222
import { Message } from '@theia/core/shared/@phosphor/messaging';
2323
import { TreeViewWidget } from './tree-view-widget';
24-
import { DescriptionWidget } from '@theia/core/lib/browser/view-container';
24+
import { BadgeWidget, DescriptionWidget } from '@theia/core/lib/browser/view-container';
2525
import { DisposableCollection, Emitter, Event } from '@theia/core/lib/common';
2626
import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
2727

@@ -32,16 +32,20 @@ export class PluginViewWidgetIdentifier {
3232
}
3333

3434
@injectable()
35-
export class PluginViewWidget extends Panel implements StatefulWidget, DescriptionWidget {
35+
export class PluginViewWidget extends Panel implements StatefulWidget, DescriptionWidget, BadgeWidget {
3636

3737
currentViewContainerId?: string;
3838

3939
protected _message?: string;
4040
protected _description: string = '';
41+
protected _badge?: number | undefined;
42+
protected _badgeTooltip?: string | undefined;
4143
protected _suppressUpdateViewVisibility = false;
4244
protected updatingViewVisibility = false;
4345
protected onDidChangeDescriptionEmitter = new Emitter<void>();
44-
protected toDispose = new DisposableCollection(this.onDidChangeDescriptionEmitter);
46+
protected onDidChangeBadgeEmitter = new Emitter<void>();
47+
protected onDidChangeBadgeTooltipEmitter = new Emitter<void>();
48+
protected toDispose = new DisposableCollection(this.onDidChangeDescriptionEmitter, this.onDidChangeBadgeEmitter, this.onDidChangeBadgeTooltipEmitter);
4549

4650
@inject(MenuModelRegistry)
4751
protected readonly menus: MenuModelRegistry;
@@ -73,6 +77,14 @@ export class PluginViewWidget extends Panel implements StatefulWidget, Descripti
7377
return this.onDidChangeDescriptionEmitter.event;
7478
}
7579

80+
get onDidChangeBadge(): Event<void> {
81+
return this.onDidChangeBadgeEmitter.event;
82+
}
83+
84+
get onDidChangeBadgeTooltip(): Event<void> {
85+
return this.onDidChangeBadgeTooltipEmitter.event;
86+
}
87+
7688
protected override onActivateRequest(msg: Message): void {
7789
super.onActivateRequest(msg);
7890
const widget = this.widgets[0];
@@ -138,6 +150,32 @@ export class PluginViewWidget extends Panel implements StatefulWidget, Descripti
138150
this.onDidChangeDescriptionEmitter.fire();
139151
}
140152

153+
get badge(): number | undefined {
154+
const widget = this.widgets[0];
155+
if (BadgeWidget.is(widget)) {
156+
return widget.badge;
157+
}
158+
return this._badge;
159+
}
160+
161+
set badge(badge: number | undefined) {
162+
this._badge = badge;
163+
this.onDidChangeBadgeEmitter.fire();
164+
}
165+
166+
get badgeTooltip(): string | undefined {
167+
const widget = this.widgets[0];
168+
if (BadgeWidget.is(widget)) {
169+
return widget.badgeTooltip;
170+
}
171+
return this._badgeTooltip;
172+
}
173+
174+
set badgeTooltip(badgeTooltip: string | undefined) {
175+
this._badgeTooltip = badgeTooltip;
176+
this.onDidChangeBadgeTooltipEmitter.fire();
177+
}
178+
141179
private updateWidgetMessage(): void {
142180
const widget = this.widgets[0];
143181
if (widget) {
@@ -149,6 +187,10 @@ export class PluginViewWidget extends Panel implements StatefulWidget, Descripti
149187

150188
override addWidget(widget: Widget): void {
151189
super.addWidget(widget);
190+
if (BadgeWidget.is(widget)) {
191+
widget.onDidChangeBadge(() => this.onDidChangeBadgeEmitter.fire());
192+
widget.onDidChangeBadgeTooltip(() => this.onDidChangeBadgeTooltipEmitter.fire());
193+
}
152194
this.updateWidgetMessage();
153195
}
154196

packages/plugin-ext/src/main/browser/view/tree-views-main.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { TreeViewWidget, TreeViewNode, PluginTreeModel, TreeViewWidgetOptions }
3030
import { PluginViewWidget } from './plugin-view-widget';
3131
import { BinaryBuffer } from '@theia/core/lib/common/buffer';
3232
import { DnDFileContentStore } from './dnd-file-content-store';
33+
import { ViewBadge } from '@theia/plugin';
3334

3435
export class TreeViewsMainImpl implements TreeViewsMain, Disposable {
3536

@@ -173,6 +174,14 @@ export class TreeViewsMainImpl implements TreeViewsMain, Disposable {
173174
}
174175
}
175176

177+
async $setBadge(treeViewId: string, badge: ViewBadge | undefined): Promise<void> {
178+
const viewPanel = await this.viewRegistry.getView(treeViewId);
179+
if (viewPanel) {
180+
viewPanel.badge = badge?.value;
181+
viewPanel.badgeTooltip = badge?.tooltip;
182+
}
183+
}
184+
176185
protected handleTreeEvents(treeViewId: string, treeViewWidget: TreeViewWidget): void {
177186
this.toDispose.push(treeViewWidget.model.onExpansionChanged(event => {
178187
this.proxy.$setExpanded(treeViewId, event.id, event.expanded);

packages/plugin-ext/src/main/browser/webview-views/webview-views-main.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { CancellationToken } from '@theia/core/lib/common/cancellation';
2828
import { WebviewsMainImpl } from '../webviews-main';
2929
import { Widget, WidgetManager } from '@theia/core/lib/browser';
3030
import { PluginViewRegistry } from '../view/plugin-view-registry';
31+
import { ViewBadge } from '@theia/plugin';
3132

3233
export class WebviewViewsMainImpl implements WebviewViewsMain, Disposable {
3334

@@ -126,6 +127,14 @@ export class WebviewViewsMainImpl implements WebviewViewsMain, Disposable {
126127
webviewView.description = value;
127128
}
128129

130+
async $setBadge(handle: string, badge: ViewBadge | undefined): Promise<void> {
131+
const webviewView = this.getWebviewView(handle);
132+
if (webviewView) {
133+
webviewView.badge = badge?.value;
134+
webviewView.badgeTooltip = badge?.tooltip;
135+
}
136+
}
137+
129138
$show(handle: string, preserveFocus: boolean): void {
130139
const webviewView = this.getWebviewView(handle);
131140
webviewView.show(preserveFocus);

packages/plugin-ext/src/main/browser/webview-views/webview-views.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,13 @@ import { WebviewWidget } from '../webview/webview';
2525
export interface WebviewView {
2626
title?: string;
2727
description?: string;
28+
badge?: number | undefined;
29+
badgeTooltip?: string | undefined;
2830
readonly webview: WebviewWidget;
2931
readonly onDidChangeVisibility: Event<boolean>;
3032
readonly onDidDispose: Event<void>;
33+
readonly onDidChangeBadge: Event<void>;
34+
readonly onDidChangeBadgeTooltip: Event<void>;
3135

3236
dispose(): void;
3337
show(preserveFocus: boolean): void;

packages/plugin-ext/src/main/browser/webview/webview.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import { IconUrl } from '../../../common/plugin-protocol';
3333
import { Deferred } from '@theia/core/lib/common/promise-util';
3434
import { WebviewEnvironment } from './webview-environment';
3535
import URI from '@theia/core/lib/common/uri';
36-
import { Emitter } from '@theia/core/lib/common/event';
36+
import { Emitter, Event } from '@theia/core/lib/common/event';
3737
import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
3838
import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
3939
import { Schemes } from '../../../common/uri-components';
@@ -50,6 +50,7 @@ import { FileOperationError, FileOperationResult } from '@theia/filesystem/lib/c
5050
import { BinaryBufferReadableStream } from '@theia/core/lib/common/buffer';
5151
import { ViewColumn } from '../../../plugin/types-impl';
5252
import { ExtractableWidget } from '@theia/core/lib/browser/widgets/extractable-widget';
53+
import { BadgeWidget } from '@theia/core/lib/browser/view-container';
5354

5455
// Style from core
5556
const TRANSPARENT_OVERLAY_STYLE = 'theia-transparent-overlay';
@@ -87,7 +88,7 @@ export class WebviewWidgetIdentifier {
8788
export const WebviewWidgetExternalEndpoint = Symbol('WebviewWidgetExternalEndpoint');
8889

8990
@injectable()
90-
export class WebviewWidget extends BaseWidget implements StatefulWidget, ExtractableWidget {
91+
export class WebviewWidget extends BaseWidget implements StatefulWidget, ExtractableWidget, BadgeWidget {
9192

9293
private static readonly standardSupportedLinkSchemes = new Set([
9394
Schemes.http,
@@ -178,6 +179,11 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget, Extract
178179
isExtractable: boolean = true;
179180
secondaryWindow: Window | undefined = undefined;
180181

182+
protected _badge?: number | undefined;
183+
protected _badgeTooltip?: string | undefined;
184+
protected onDidChangeBadgeEmitter = new Emitter<void>();
185+
protected onDidChangeBadgeTooltipEmitter = new Emitter<void>();
186+
181187
@postConstruct()
182188
protected init(): void {
183189
this.node.tabIndex = 0;
@@ -186,6 +192,8 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget, Extract
186192
this.addClass(WebviewWidget.Styles.WEBVIEW);
187193

188194
this.toDispose.push(this.onMessageEmitter);
195+
this.toDispose.push(this.onDidChangeBadgeEmitter);
196+
this.toDispose.push(this.onDidChangeBadgeTooltipEmitter);
189197

190198
this.transparentOverlay = document.createElement('div');
191199
this.transparentOverlay.classList.add(TRANSPARENT_OVERLAY_STYLE);
@@ -204,6 +212,32 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget, Extract
204212
}));
205213
}
206214

215+
get onDidChangeBadge(): Event<void> {
216+
return this.onDidChangeBadgeEmitter.event;
217+
}
218+
219+
get onDidChangeBadgeTooltip(): Event<void> {
220+
return this.onDidChangeBadgeTooltipEmitter.event;
221+
}
222+
223+
get badge(): number | undefined {
224+
return this._badge;
225+
}
226+
227+
set badge(badge: number | undefined) {
228+
this._badge = badge;
229+
this.onDidChangeBadgeEmitter.fire();
230+
}
231+
232+
get badgeTooltip(): string | undefined {
233+
return this._badgeTooltip;
234+
}
235+
236+
set badgeTooltip(badgeTooltip: string | undefined) {
237+
this._badgeTooltip = badgeTooltip;
238+
this.onDidChangeBadgeTooltipEmitter.fire();
239+
}
240+
207241
protected override onBeforeAttach(msg: Message): void {
208242
super.onBeforeAttach(msg);
209243
this.doShow();

packages/plugin-ext/src/main/browser/webviews-main.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { URI } from '@theia/core/shared/vscode-uri';
1919
import { interfaces } from '@theia/core/shared/inversify';
2020
import { WebviewsMain, MAIN_RPC_CONTEXT, WebviewsExt, WebviewPanelViewState } from '../../common/plugin-api-rpc';
2121
import { RPCProtocol } from '../../common/rpc-protocol';
22-
import { WebviewOptions, WebviewPanelOptions, WebviewPanelShowOptions } from '@theia/plugin';
22+
import { ViewBadge, WebviewOptions, WebviewPanelOptions, WebviewPanelShowOptions } from '@theia/plugin';
2323
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
2424
import { WebviewWidget, WebviewWidgetIdentifier } from './webview/webview';
2525
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
@@ -160,6 +160,14 @@ export class WebviewsMainImpl implements WebviewsMain, Disposable {
160160
webview.title.label = value;
161161
}
162162

163+
async $setBadge(handle: string, badge: ViewBadge | undefined): Promise<void> {
164+
const webview = await this.getWebview(handle);
165+
if (webview) {
166+
webview.badge = badge?.value;
167+
webview.badgeTooltip = badge?.tooltip;
168+
}
169+
}
170+
163171
async $setIconPath(handle: string, iconUrl: IconUrl | undefined): Promise<void> {
164172
const webview = await this.getWebview(handle);
165173
webview.setIconUrl(iconUrl);

0 commit comments

Comments
 (0)