Skip to content

Commit dede983

Browse files
committed
Update widget and hover for sessions
1 parent a437b74 commit dede983

7 files changed

Lines changed: 676 additions & 253 deletions

File tree

src/vs/sessions/contrib/accountMenu/browser/account.contribution.ts

Lines changed: 59 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { ContextKeyExpr, IContextKeyService } from '../../../../platform/context
1212
import { IDefaultAccountService } from '../../../../platform/defaultAccount/common/defaultAccount.js';
1313
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
1414
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js';
15-
import { appendUpdateMenuItems as registerUpdateMenuItems, CONTEXT_UPDATE_STATE } from '../../../../workbench/contrib/update/browser/update.js';
15+
import { appendUpdateMenuItems as registerUpdateMenuItems } from '../../../../workbench/contrib/update/browser/update.js';
1616
import { Menus } from '../../../browser/menus.js';
1717
import { IActionViewItemService } from '../../../../platform/actions/browser/actionViewItemService.js';
1818
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
@@ -23,9 +23,10 @@ import { IAction } from '../../../../base/common/actions.js';
2323
import { Button } from '../../../../base/browser/ui/button/button.js';
2424
import { defaultButtonStyles } from '../../../../platform/theme/browser/defaultStyles.js';
2525
import { Codicon } from '../../../../base/common/codicons.js';
26-
import { Downloading, IUpdateService, State, StateType } from '../../../../platform/update/common/update.js';
27-
import { asCssVariable } from '../../../../platform/theme/common/colorUtils.js';
28-
import { sessionsUpdateButtonDownloadingBackground, sessionsUpdateButtonDownloadedBackground } from '../../../common/theme.js';
26+
import { IUpdateService, StateType } from '../../../../platform/update/common/update.js';
27+
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
28+
import { IProductService } from '../../../../platform/product/common/productService.js';
29+
import { UpdateHoverWidget } from './updateHoverWidget.js';
2930

3031
// --- Account Menu Items --- //
3132
const AccountMenu = new MenuId('SessionsAccountMenu');
@@ -83,20 +84,26 @@ MenuRegistry.appendMenuItem(AccountMenu, {
8384
// Update actions
8485
registerUpdateMenuItems(AccountMenu, '3_updates');
8586

86-
class AccountWidget extends ActionViewItem {
87+
export class AccountWidget extends ActionViewItem {
8788

8889
private accountButton: Button | undefined;
90+
private updateButton: Button | undefined;
91+
private readonly updateHoverWidget: UpdateHoverWidget;
8992
private readonly viewItemDisposables = this._register(new DisposableStore());
9093

9194
constructor(
9295
action: IAction,
9396
options: IBaseActionViewItemOptions,
9497
@IDefaultAccountService private readonly defaultAccountService: IDefaultAccountService,
98+
@IUpdateService private readonly updateService: IUpdateService,
9599
@IContextMenuService private readonly contextMenuService: IContextMenuService,
96100
@IMenuService private readonly menuService: IMenuService,
97101
@IContextKeyService private readonly contextKeyService: IContextKeyService,
102+
@IHoverService private readonly hoverService: IHoverService,
103+
@IProductService private readonly productService: IProductService,
98104
) {
99105
super(undefined, action, { ...options, icon: false, label: false });
106+
this.updateHoverWidget = new UpdateHoverWidget(this.updateService, this.productService, this.hoverService);
100107
}
101108

102109
protected override getTooltip(): string | undefined {
@@ -121,14 +128,33 @@ class AccountWidget extends ActionViewItem {
121128
}));
122129
this.accountButton.element.classList.add('account-widget-account-button', 'sidebar-action-button');
123130

131+
// Update button (right)
132+
const updateContainer = append(container, $('.account-widget-update'));
133+
this.updateButton = this.viewItemDisposables.add(new Button(updateContainer, {
134+
...defaultButtonStyles,
135+
secondary: true,
136+
title: false,
137+
supportIcons: true,
138+
buttonSecondaryBackground: 'transparent',
139+
buttonSecondaryHoverBackground: undefined,
140+
buttonSecondaryForeground: undefined,
141+
buttonSecondaryBorder: undefined,
142+
}));
143+
this.updateButton.element.classList.add('account-widget-update-button', 'sidebar-action-button');
144+
this.viewItemDisposables.add(this.updateHoverWidget.attachTo(this.updateButton.element));
145+
124146
this.updateAccountButton();
125147
this.viewItemDisposables.add(this.defaultAccountService.onDidChangeDefaultAccount(() => this.updateAccountButton()));
148+
this.updateUpdateButton();
149+
this.viewItemDisposables.add(this.updateService.onStateChange(() => this.updateUpdateButton()));
126150

127151
this.viewItemDisposables.add(this.accountButton.onDidClick(e => {
128152
e?.preventDefault();
129153
e?.stopPropagation();
130154
this.showAccountMenu(this.accountButton!.element);
131155
}));
156+
157+
this.viewItemDisposables.add(this.updateButton.onDidClick(() => this.update()));
132158
}
133159

134160
private showAccountMenu(anchor: HTMLElement): void {
@@ -156,134 +182,57 @@ class AccountWidget extends ActionViewItem {
156182
: `$(${Codicon.account.id}) ${localize('signInLabel', "Sign In")}`;
157183
}
158184

159-
160-
override onClick(): void {
161-
// Handled by custom click handlers
162-
}
163-
}
164-
165-
export class UpdateWidget extends ActionViewItem {
166-
167-
private updateButton: Button | undefined;
168-
private readonly viewItemDisposables = this._register(new DisposableStore());
169-
170-
constructor(
171-
action: IAction,
172-
options: IBaseActionViewItemOptions,
173-
@IUpdateService private readonly updateService: IUpdateService,
174-
) {
175-
super(undefined, action, { ...options, icon: false, label: false });
176-
}
177-
178-
protected override getTooltip(): string | undefined {
179-
return undefined;
180-
}
181-
182-
override render(container: HTMLElement): void {
183-
super.render(container);
184-
container.classList.add('update-widget', 'sidebar-action');
185-
186-
const updateContainer = append(container, $('.update-widget-action'));
187-
this.updateButton = this.viewItemDisposables.add(new Button(updateContainer, {
188-
...defaultButtonStyles,
189-
secondary: true,
190-
title: false,
191-
supportIcons: true,
192-
buttonSecondaryBackground: 'transparent',
193-
buttonSecondaryHoverBackground: undefined,
194-
buttonSecondaryForeground: undefined,
195-
buttonSecondaryBorder: undefined,
196-
}));
197-
this.updateButton.element.classList.add('update-widget-button', 'sidebar-action-button');
198-
this.viewItemDisposables.add(this.updateButton.onDidClick(() => this.update()));
199-
200-
this.updateUpdateButton();
201-
this.viewItemDisposables.add(this.updateService.onStateChange(() => this.updateUpdateButton()));
202-
}
203-
204-
private isUpdateReady(): boolean {
205-
return this.updateService.state.type === StateType.Ready;
206-
}
207-
208-
private isUpdatePending(): boolean {
209-
const type = this.updateService.state.type;
210-
return type === StateType.AvailableForDownload
211-
|| type === StateType.CheckingForUpdates
212-
|| type === StateType.Downloading
213-
|| type === StateType.Downloaded
214-
|| type === StateType.Updating
215-
|| type === StateType.Overwriting;
216-
}
217-
218185
private updateUpdateButton(): void {
219186
if (!this.updateButton) {
220187
return;
221188
}
222189

223190
const state = this.updateService.state;
224-
if (this.isUpdatePending() && !this.isUpdateReady()) {
225-
this.updateButton.enabled = false;
226-
this.updateButton.label = `$(${Codicon.loading.id}~spin) ${this.getUpdateProgressMessage(state.type)}`;
227-
this.updateDownloadProgress(state);
228-
} else {
229-
this.updateButton.enabled = true;
230-
this.updateButton.label = `$(${Codicon.debugRestart.id}) ${localize('update', "Update")}`;
231-
232-
const el = this.updateButton.element;
233-
if (state.type === StateType.Ready) {
234-
const color = asCssVariable(sessionsUpdateButtonDownloadedBackground);
235-
el.style.backgroundImage = `linear-gradient(to right, ${color} 100%, transparent 100%)`;
236-
} else {
237-
// Ensure non-update states (e.g. Idle, Disabled, Uninitialized) do not look like a completed download
238-
el.style.backgroundImage = '';
239-
}
191+
if (this.shouldHideUpdateButton(state.type)) {
192+
this.clearUpdateButtonStyling();
193+
this.updateButton.element.classList.add('hidden');
194+
return;
240195
}
241-
}
242196

243-
private updateDownloadProgress(state: State): void {
244-
if (!this.updateButton) {
197+
this.updateButton.element.classList.remove('hidden');
198+
this.updateButton.element.style.backgroundImage = '';
199+
this.updateButton.enabled = state.type === StateType.Ready;
200+
this.updateButton.label = this.getUpdateProgressMessage(state.type);
201+
202+
if (state.type === StateType.Ready) {
203+
this.updateButton.element.classList.add('account-widget-update-button-ready');
245204
return;
246205
}
247206

248-
const el = this.updateButton.element;
249-
250-
if (state.type === StateType.Downloading) {
251-
const { downloadedBytes, totalBytes } = state as Downloading;
252-
if (downloadedBytes !== undefined && totalBytes && totalBytes > 0) {
253-
const percent = Math.min(100, Math.round((downloadedBytes / totalBytes) * 100));
254-
const color = asCssVariable(sessionsUpdateButtonDownloadingBackground);
255-
el.style.backgroundImage = `linear-gradient(to right, ${color} ${percent}%, transparent ${percent}%)`;
256-
} else {
257-
// Indeterminate: show a subtle pulsing background
258-
const color = asCssVariable(sessionsUpdateButtonDownloadingBackground);
259-
el.style.backgroundImage = `linear-gradient(to right, ${color} 0%, transparent 100%)`;
260-
}
261-
} else if (state.type === StateType.Downloaded) {
262-
const color = asCssVariable(sessionsUpdateButtonDownloadedBackground);
263-
el.style.backgroundImage = `linear-gradient(to right, ${color} 100%, transparent 100%)`;
264-
} else {
265-
this.clearDownloadProgress();
266-
}
207+
this.updateButton.element.classList.remove('account-widget-update-button-ready');
208+
}
209+
210+
private shouldHideUpdateButton(type: StateType): boolean {
211+
return type === StateType.Uninitialized
212+
|| type === StateType.Idle
213+
|| type === StateType.Disabled
214+
|| type === StateType.CheckingForUpdates;
267215
}
268216

269-
private clearDownloadProgress(): void {
217+
private clearUpdateButtonStyling(): void {
270218
if (this.updateButton) {
271219
this.updateButton.element.style.backgroundImage = '';
220+
this.updateButton.element.classList.remove('account-widget-update-button-ready');
272221
}
273222
}
274223

275224
private getUpdateProgressMessage(type: StateType): string {
276225
switch (type) {
277-
case StateType.CheckingForUpdates:
278-
return localize('checkingForUpdates', "Checking for Updates...");
226+
case StateType.Ready:
227+
return localize('update', "Update");
228+
case StateType.AvailableForDownload:
279229
case StateType.Downloading:
280-
return localize('downloadingUpdate', "Downloading Update...");
230+
case StateType.Overwriting:
231+
return localize('downloadingUpdate', "Downloading...");
281232
case StateType.Downloaded:
282-
return localize('installingUpdate', "Installing Update...");
233+
return localize('installingUpdate', "Installing...");
283234
case StateType.Updating:
284235
return localize('updatingApp', "Updating...");
285-
case StateType.Overwriting:
286-
return localize('overwritingUpdate', "Downloading Update...");
287236
default:
288237
return localize('updating', "Updating...");
289238
}
@@ -293,6 +242,7 @@ export class UpdateWidget extends ActionViewItem {
293242
await this.updateService.quitAndInstall();
294243
}
295244

245+
296246
override onClick(): void {
297247
// Handled by custom click handlers
298248
}
@@ -315,11 +265,6 @@ class AccountWidgetContribution extends Disposable implements IWorkbenchContribu
315265
return instantiationService.createInstance(AccountWidget, action, options);
316266
}, undefined));
317267

318-
const sessionsUpdateWidgetAction = 'sessions.action.updateWidget';
319-
this._register(actionViewItemService.register(Menus.SidebarFooter, sessionsUpdateWidgetAction, (action, options) => {
320-
return instantiationService.createInstance(UpdateWidget, action, options);
321-
}, undefined));
322-
323268
// Register the action with menu item after the view item provider
324269
// so the toolbar picks up the custom widget
325270
this._register(registerAction2(class extends Action2 {
@@ -339,30 +284,6 @@ class AccountWidgetContribution extends Disposable implements IWorkbenchContribu
339284
}
340285
}));
341286

342-
this._register(registerAction2(class extends Action2 {
343-
constructor() {
344-
super({
345-
id: sessionsUpdateWidgetAction,
346-
title: localize2('sessionsUpdateWidget', 'Sessions Update'),
347-
menu: {
348-
id: Menus.SidebarFooter,
349-
group: 'navigation',
350-
order: 0,
351-
when: ContextKeyExpr.or(
352-
CONTEXT_UPDATE_STATE.isEqualTo(StateType.Ready),
353-
CONTEXT_UPDATE_STATE.isEqualTo(StateType.AvailableForDownload),
354-
CONTEXT_UPDATE_STATE.isEqualTo(StateType.Downloading),
355-
CONTEXT_UPDATE_STATE.isEqualTo(StateType.Downloaded),
356-
CONTEXT_UPDATE_STATE.isEqualTo(StateType.Updating),
357-
CONTEXT_UPDATE_STATE.isEqualTo(StateType.Overwriting),
358-
)
359-
}
360-
});
361-
}
362-
async run(): Promise<void> {
363-
// Handled by the custom view item
364-
}
365-
}));
366287
}
367288
}
368289

0 commit comments

Comments
 (0)