Skip to content

Commit 37da787

Browse files
committed
Implement toggle aware icons and labels in command actions
1 parent 849aadb commit 37da787

9 files changed

Lines changed: 81 additions & 39 deletions

File tree

src/vs/code/electron-main/window.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { INativeWindowConfiguration } from 'vs/platform/windows/node/window';
2222
import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
2323
import { IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService';
2424
import { IBackupMainService } from 'vs/platform/backup/electron-main/backup';
25-
import { ISerializableCommandAction } from 'vs/platform/actions/common/actions';
25+
import { ISerializableMenuItemAction } from 'vs/platform/actions/common/actions';
2626
import * as perf from 'vs/base/common/performance';
2727
import { resolveMarketplaceHeaders } from 'vs/platform/extensionManagement/common/extensionGalleryService';
2828
import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService';
@@ -1094,7 +1094,7 @@ export class CodeWindow extends Disposable implements ICodeWindow {
10941094
}
10951095
}
10961096

1097-
updateTouchBar(groups: ISerializableCommandAction[][]): void {
1097+
updateTouchBar(groups: ISerializableMenuItemAction[][]): void {
10981098
if (!isMacintosh) {
10991099
return; // only supported on macOS
11001100
}
@@ -1123,10 +1123,10 @@ export class CodeWindow extends Disposable implements ICodeWindow {
11231123
this._win.setTouchBar(new TouchBar({ items: this.touchBarGroups }));
11241124
}
11251125

1126-
private createTouchBarGroup(items: ISerializableCommandAction[] = []): TouchBarSegmentedControl {
1126+
private createTouchBarGroup(): TouchBarSegmentedControl {
11271127

11281128
// Group Segments
1129-
const segments = this.createTouchBarGroupSegments(items);
1129+
const segments = this.createTouchBarGroupSegments();
11301130

11311131
// Group Control
11321132
const control = new TouchBar.TouchBarSegmentedControl({
@@ -1141,7 +1141,7 @@ export class CodeWindow extends Disposable implements ICodeWindow {
11411141
return control;
11421142
}
11431143

1144-
private createTouchBarGroupSegments(items: ISerializableCommandAction[] = []): ITouchBarSegment[] {
1144+
private createTouchBarGroupSegments(items: ISerializableMenuItemAction[] = []): ITouchBarSegment[] {
11451145
const segments: ITouchBarSegment[] = items.map(item => {
11461146
let icon: NativeImage | undefined;
11471147
if (item.icon && !ThemeIcon.isThemeIcon(item.icon) && item.icon?.dark?.scheme === 'file') {

src/vs/platform/actions/browser/menuEntryActionViewItem.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { IdGenerator } from 'vs/base/common/idGenerator';
1212
import { IDisposable, toDisposable, MutableDisposable, DisposableStore } from 'vs/base/common/lifecycle';
1313
import { isLinux, isWindows } from 'vs/base/common/platform';
1414
import { localize } from 'vs/nls';
15-
import { ICommandAction, IMenu, IMenuActionOptions, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions';
15+
import { IMenu, IMenuActionOptions, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions';
1616
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
1717
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
1818
import { INotificationService } from 'vs/platform/notification/common/notification';
@@ -148,7 +148,7 @@ export class MenuEntryActionViewItem extends ActionViewItem {
148148
@INotificationService protected _notificationService: INotificationService,
149149
@IContextMenuService _contextMenuService: IContextMenuService
150150
) {
151-
super(undefined, _action, { icon: !!(_action.class || _action.item.icon), label: !_action.class && !_action.item.icon });
151+
super(undefined, _action, { icon: !!(_action.class || _action.icon), label: !_action.class && !_action.icon });
152152
this._altKey = AlternativeKeyEmitter.getInstance(_contextMenuService);
153153
}
154154

@@ -171,7 +171,7 @@ export class MenuEntryActionViewItem extends ActionViewItem {
171171
render(container: HTMLElement): void {
172172
super.render(container);
173173

174-
this._updateItemClass(this._action.item);
174+
this._updateItemClass(this._action);
175175

176176
let mouseOver = false;
177177

@@ -226,15 +226,15 @@ export class MenuEntryActionViewItem extends ActionViewItem {
226226
if (this.options.icon) {
227227
if (this._commandAction !== this._action) {
228228
if (this._action.alt) {
229-
this._updateItemClass(this._action.alt.item);
229+
this._updateItemClass(this._action.alt);
230230
}
231231
} else if ((<MenuItemAction>this._action).alt) {
232-
this._updateItemClass(this._action.item);
232+
this._updateItemClass(this._action);
233233
}
234234
}
235235
}
236236

237-
_updateItemClass(item: ICommandAction): void {
237+
_updateItemClass(item: MenuItemAction): void {
238238
this._itemClassDispose.value = undefined;
239239

240240
if (ThemeIcon.isThemeIcon(item.icon)) {

src/vs/platform/actions/common/actions.ts

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,37 @@ export interface ILocalizedString {
2020
original: string;
2121
}
2222

23+
export type Icon = { dark?: URI; light?: URI; } | ThemeIcon;
24+
export type ToggleAwareIcon = { toggled?: Icon, untoggled?: Icon };
25+
export type ToggleAwareTitle = { toggled?: string | ILocalizedString, untoggled?: string | ILocalizedString };
26+
2327
export interface ICommandAction {
2428
id: string;
25-
title: string | ILocalizedString;
29+
title: string | ILocalizedString | ToggleAwareTitle;
2630
category?: string | ILocalizedString;
27-
icon?: { dark?: URI; light?: URI; } | ThemeIcon;
31+
icon?: Icon | ToggleAwareIcon;
2832
precondition?: ContextKeyExpression;
2933
toggled?: ContextKeyExpression;
3034
}
3135

32-
export type ISerializableCommandAction = UriDto<ICommandAction>;
36+
export function isToggleAwareTitle(thing: unknown): thing is ToggleAwareTitle {
37+
return thing && typeof thing === 'object'
38+
&& ((typeof (thing as ToggleAwareTitle).toggled === 'string' || typeof (thing as ToggleAwareTitle).toggled === 'object')
39+
|| (typeof (thing as ToggleAwareTitle).untoggled === 'string' || typeof (thing as ToggleAwareTitle).untoggled === 'object'));
40+
}
41+
42+
export function isIcon(thing: unknown): thing is Icon {
43+
if (ThemeIcon.isThemeIcon(thing)) {
44+
return true;
45+
}
46+
return thing && typeof thing === 'object'
47+
&& ((thing as { dark?: URI, light?: URI }).dark instanceof URI || (thing as { dark?: URI, light?: URI }).light instanceof URI);
48+
}
49+
50+
export function isToggleAwareIcon(thing: unknown): thing is ToggleAwareIcon {
51+
return thing && typeof thing === 'object'
52+
&& (isIcon((thing as ToggleAwareIcon).toggled) || isIcon((thing as ToggleAwareIcon).untoggled));
53+
}
3354

3455
export interface IMenuItem {
3556
command: ICommandAction;
@@ -260,9 +281,18 @@ export class SubmenuItemAction extends Action {
260281
}
261282
}
262283

284+
export type ISerializableMenuItemAction = UriDto<{
285+
id: string;
286+
title: string | ILocalizedString;
287+
category: string | ILocalizedString | undefined;
288+
icon: Icon | undefined;
289+
}>;
290+
263291
export class MenuItemAction extends ExecuteCommandAction {
264292

265-
readonly item: ICommandAction;
293+
readonly title: string | ILocalizedString;
294+
readonly category: string | ILocalizedString | undefined;
295+
readonly icon: Icon | undefined;
266296
readonly alt: MenuItemAction | undefined;
267297

268298
private _options: IMenuActionOptions;
@@ -274,14 +304,17 @@ export class MenuItemAction extends ExecuteCommandAction {
274304
@IContextKeyService contextKeyService: IContextKeyService,
275305
@ICommandService commandService: ICommandService
276306
) {
277-
typeof item.title === 'string' ? super(item.id, item.title, commandService) : super(item.id, item.title.value, commandService);
307+
super(item.id, '', commandService);
308+
this.title = (isToggleAwareTitle(item.title) ? this._checked ? item.title.toggled : item.title.untoggled : item.title) || '';
309+
this._label = typeof this.title === 'string' ? this.title : this.title.value;
278310
this._cssClass = undefined;
279311
this._enabled = !item.precondition || contextKeyService.contextMatchesRules(item.precondition);
280312
this._checked = Boolean(item.toggled && contextKeyService.contextMatchesRules(item.toggled));
313+
this.category = item.category;
314+
this.icon = isToggleAwareIcon(item.icon) ? this.checked ? item.icon.toggled : item.icon.untoggled : item.icon;
281315

282316
this._options = options || {};
283317

284-
this.item = item;
285318
this.alt = alt ? new MenuItemAction(alt, undefined, this._options, contextKeyService, commandService) : undefined;
286319
}
287320

@@ -305,6 +338,15 @@ export class MenuItemAction extends ExecuteCommandAction {
305338

306339
return super.run(...runArgs);
307340
}
341+
342+
serialize(): ISerializableMenuItemAction {
343+
return {
344+
id: this.id,
345+
title: this.title,
346+
category: this.category,
347+
icon: this.icon
348+
};
349+
}
308350
}
309351

310352
export class SyncActionDescriptor {

src/vs/platform/electron/electron-main/electronMainService.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { IOpenedWindow, OpenContext, IWindowOpenable, IOpenEmptyWindowOptions }
1212
import { INativeOpenDialogOptions } from 'vs/platform/dialogs/node/dialogs';
1313
import { isMacintosh } from 'vs/base/common/platform';
1414
import { IElectronService } from 'vs/platform/electron/node/electron';
15-
import { ISerializableCommandAction } from 'vs/platform/actions/common/actions';
15+
import { ISerializableMenuItemAction } from 'vs/platform/actions/common/actions';
1616
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
1717
import { AddFirstParameterToFunctions } from 'vs/base/common/types';
1818
import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs';
@@ -279,7 +279,7 @@ export class ElectronMainService implements IElectronMainService {
279279
return true;
280280
}
281281

282-
async updateTouchBar(windowId: number | undefined, items: ISerializableCommandAction[][]): Promise<void> {
282+
async updateTouchBar(windowId: number | undefined, items: ISerializableMenuItemAction[][]): Promise<void> {
283283
const window = this.windowById(windowId);
284284
if (window) {
285285
window.updateTouchBar(items);

src/vs/platform/electron/node/electron.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { MessageBoxOptions, MessageBoxReturnValue, OpenDevToolsOptions, SaveDial
88
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
99
import { IWindowOpenable, IOpenEmptyWindowOptions, IOpenedWindow } from 'vs/platform/windows/common/windows';
1010
import { INativeOpenDialogOptions } from 'vs/platform/dialogs/node/dialogs';
11-
import { ISerializableCommandAction } from 'vs/platform/actions/common/actions';
11+
import { ISerializableMenuItemAction } from 'vs/platform/actions/common/actions';
1212
import { INativeOpenWindowOptions } from 'vs/platform/windows/node/window';
1313

1414
export const IElectronService = createDecorator<IElectronService>('electronService');
@@ -60,7 +60,7 @@ export interface IElectronService {
6060
setRepresentedFilename(path: string): Promise<void>;
6161
setDocumentEdited(edited: boolean): Promise<void>;
6262
openExternal(url: string): Promise<boolean>;
63-
updateTouchBar(items: ISerializableCommandAction[][]): Promise<void>;
63+
updateTouchBar(items: ISerializableMenuItemAction[][]): Promise<void>;
6464

6565
// macOS Touchbar
6666
newWindowTab(): Promise<void>;

src/vs/platform/windows/electron-main/windows.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { Event } from 'vs/base/common/event';
1010
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
1111
import { IProcessEnvironment } from 'vs/base/common/platform';
1212
import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
13-
import { ISerializableCommandAction } from 'vs/platform/actions/common/actions';
13+
import { ISerializableMenuItemAction } from 'vs/platform/actions/common/actions';
1414
import { URI } from 'vs/base/common/uri';
1515
import { Rectangle, BrowserWindow } from 'electron';
1616
import { IDisposable } from 'vs/base/common/lifecycle';
@@ -82,7 +82,7 @@ export interface ICodeWindow extends IDisposable {
8282

8383
handleTitleDoubleClick(): void;
8484

85-
updateTouchBar(items: ISerializableCommandAction[][]): void;
85+
updateTouchBar(items: ISerializableMenuItemAction[][]): void;
8686

8787
serializeWindowState(): IWindowState;
8888
}

src/vs/workbench/contrib/quickopen/browser/commandsHandler.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -569,19 +569,19 @@ export class CommandsHandler extends QuickOpenHandler implements IDisposable {
569569
const entries: ActionCommandEntry[] = [];
570570

571571
for (let action of actions) {
572-
const title = typeof action.item.title === 'string' ? action.item.title : action.item.title.value;
572+
const title = typeof action.title === 'string' ? action.title : action.title.value;
573573
let category, label = title;
574-
if (action.item.category) {
575-
category = typeof action.item.category === 'string' ? action.item.category : action.item.category.value;
574+
if (action.category) {
575+
category = typeof action.category === 'string' ? action.category : action.category.value;
576576
label = localize('cat.title', "{0}: {1}", category, title);
577577
}
578578

579579
if (label) {
580580
const labelHighlights = wordFilter(searchValue, label);
581581

582582
// Add an 'alias' in original language when running in different locale
583-
const aliasTitle = (!Language.isDefaultVariant() && typeof action.item.title !== 'string') ? action.item.title.original : undefined;
584-
const aliasCategory = (!Language.isDefaultVariant() && category && action.item.category && typeof action.item.category !== 'string') ? action.item.category.original : undefined;
583+
const aliasTitle = (!Language.isDefaultVariant() && typeof action.title !== 'string') ? action.title.original : undefined;
584+
const aliasCategory = (!Language.isDefaultVariant() && category && action.category && typeof action.category !== 'string') ? action.category.original : undefined;
585585
let alias;
586586
if (aliasTitle && category) {
587587
alias = aliasCategory ? `${aliasCategory}: ${aliasTitle}` : `${category}: ${aliasTitle}`;
@@ -591,7 +591,7 @@ export class CommandsHandler extends QuickOpenHandler implements IDisposable {
591591
const aliasHighlights = alias ? wordFilter(searchValue, alias) : null;
592592

593593
if (labelHighlights || aliasHighlights) {
594-
entries.push(this.instantiationService.createInstance(ActionCommandEntry, action.id, this.keybindingService.lookupKeybinding(action.item.id), label, alias, { label: labelHighlights, alias: aliasHighlights }, action, (id: string) => this.onBeforeRunCommand(id)));
594+
entries.push(this.instantiationService.createInstance(ActionCommandEntry, action.id, this.keybindingService.lookupKeybinding(action.id), label, alias, { label: labelHighlights, alias: aliasHighlights }, action, (id: string) => this.onBeforeRunCommand(id)));
595595
}
596596
}
597597
}

src/vs/workbench/contrib/remote/electron-browser/remote.contribution.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -200,14 +200,14 @@ export class RemoteWindowActiveIndicator extends Disposable implements IWorkbenc
200200
}
201201
for (let action of actionGroup[1]) {
202202
if (action instanceof MenuItemAction) {
203-
let label = typeof action.item.title === 'string' ? action.item.title : action.item.title.value;
204-
if (action.item.category) {
205-
const category = typeof action.item.category === 'string' ? action.item.category : action.item.category.value;
203+
let label = typeof action.title === 'string' ? action.title : action.title.value;
204+
if (action.category) {
205+
const category = typeof action.category === 'string' ? action.category : action.category.value;
206206
label = nls.localize('cat.title', "{0}: {1}", category, label);
207207
}
208208
items.push({
209209
type: 'item',
210-
id: action.item.id,
210+
id: action.id,
211211
label
212212
});
213213
}

src/vs/workbench/electron-browser/window.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { IResourceEditorInput } from 'vs/platform/editor/common/editor';
2323
import { KeyboardMapperFactory } from 'vs/workbench/services/keybinding/electron-browser/nativeKeymapService';
2424
import { ipcRenderer as ipc, webFrame, crashReporter, CrashReporterStartOptions, Event as IpcEvent } from 'electron';
2525
import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing';
26-
import { IMenuService, MenuId, IMenu, MenuItemAction, ICommandAction, SubmenuItemAction, MenuRegistry } from 'vs/platform/actions/common/actions';
26+
import { IMenuService, MenuId, IMenu, MenuItemAction, SubmenuItemAction, MenuRegistry, ISerializableMenuItemAction } from 'vs/platform/actions/common/actions';
2727
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
2828
import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
2929
import { RunOnceScheduler } from 'vs/base/common/async';
@@ -68,7 +68,7 @@ export class NativeWindow extends Disposable {
6868

6969
private touchBarMenu: IMenu | undefined;
7070
private readonly touchBarDisposables = this._register(new DisposableStore());
71-
private lastInstalledTouchedBar: ICommandAction[][] | undefined;
71+
private lastInstalledTouchedBar: ISerializableMenuItemAction[][] | undefined;
7272

7373
private readonly customTitleContextMenuDisposable = this._register(new DisposableStore());
7474

@@ -504,18 +504,18 @@ export class NativeWindow extends Disposable {
504504
this.touchBarDisposables.add(createAndFillInActionBarActions(this.touchBarMenu, undefined, actions));
505505

506506
// Convert into command action multi array
507-
const items: ICommandAction[][] = [];
508-
let group: ICommandAction[] = [];
507+
const items: ISerializableMenuItemAction[][] = [];
508+
let group: ISerializableMenuItemAction[] = [];
509509
if (!disabled) {
510510
for (const action of actions) {
511511

512512
// Command
513513
if (action instanceof MenuItemAction) {
514-
if (ignoredItems.indexOf(action.item.id) >= 0) {
514+
if (ignoredItems.indexOf(action.id) >= 0) {
515515
continue; // ignored
516516
}
517517

518-
group.push(action.item);
518+
group.push(action.serialize());
519519
}
520520

521521
// Separator

0 commit comments

Comments
 (0)