From 37da787e91f01c6cfee3d4c50992782d9e072d92 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 9 Mar 2020 19:23:54 +0100 Subject: [PATCH] Implement toggle aware icons and labels in command actions --- src/vs/code/electron-main/window.ts | 10 ++-- .../browser/menuEntryActionViewItem.ts | 12 ++--- src/vs/platform/actions/common/actions.ts | 54 ++++++++++++++++--- .../electron-main/electronMainService.ts | 4 +- src/vs/platform/electron/node/electron.ts | 4 +- .../platform/windows/electron-main/windows.ts | 4 +- .../quickopen/browser/commandsHandler.ts | 12 ++--- .../electron-browser/remote.contribution.ts | 8 +-- src/vs/workbench/electron-browser/window.ts | 12 ++--- 9 files changed, 81 insertions(+), 39 deletions(-) diff --git a/src/vs/code/electron-main/window.ts b/src/vs/code/electron-main/window.ts index de0a7dff13a..a7f4792dc4e 100644 --- a/src/vs/code/electron-main/window.ts +++ b/src/vs/code/electron-main/window.ts @@ -22,7 +22,7 @@ import { INativeWindowConfiguration } from 'vs/platform/windows/node/window'; import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService'; import { IBackupMainService } from 'vs/platform/backup/electron-main/backup'; -import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; +import { ISerializableMenuItemAction } from 'vs/platform/actions/common/actions'; import * as perf from 'vs/base/common/performance'; import { resolveMarketplaceHeaders } from 'vs/platform/extensionManagement/common/extensionGalleryService'; import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; @@ -1094,7 +1094,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { } } - updateTouchBar(groups: ISerializableCommandAction[][]): void { + updateTouchBar(groups: ISerializableMenuItemAction[][]): void { if (!isMacintosh) { return; // only supported on macOS } @@ -1123,10 +1123,10 @@ export class CodeWindow extends Disposable implements ICodeWindow { this._win.setTouchBar(new TouchBar({ items: this.touchBarGroups })); } - private createTouchBarGroup(items: ISerializableCommandAction[] = []): TouchBarSegmentedControl { + private createTouchBarGroup(): TouchBarSegmentedControl { // Group Segments - const segments = this.createTouchBarGroupSegments(items); + const segments = this.createTouchBarGroupSegments(); // Group Control const control = new TouchBar.TouchBarSegmentedControl({ @@ -1141,7 +1141,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { return control; } - private createTouchBarGroupSegments(items: ISerializableCommandAction[] = []): ITouchBarSegment[] { + private createTouchBarGroupSegments(items: ISerializableMenuItemAction[] = []): ITouchBarSegment[] { const segments: ITouchBarSegment[] = items.map(item => { let icon: NativeImage | undefined; if (item.icon && !ThemeIcon.isThemeIcon(item.icon) && item.icon?.dark?.scheme === 'file') { diff --git a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts index 6b561aaef76..b66e2612d09 100644 --- a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts +++ b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts @@ -12,7 +12,7 @@ import { IdGenerator } from 'vs/base/common/idGenerator'; import { IDisposable, toDisposable, MutableDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { isLinux, isWindows } from 'vs/base/common/platform'; import { localize } from 'vs/nls'; -import { ICommandAction, IMenu, IMenuActionOptions, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { IMenu, IMenuActionOptions, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -148,7 +148,7 @@ export class MenuEntryActionViewItem extends ActionViewItem { @INotificationService protected _notificationService: INotificationService, @IContextMenuService _contextMenuService: IContextMenuService ) { - super(undefined, _action, { icon: !!(_action.class || _action.item.icon), label: !_action.class && !_action.item.icon }); + super(undefined, _action, { icon: !!(_action.class || _action.icon), label: !_action.class && !_action.icon }); this._altKey = AlternativeKeyEmitter.getInstance(_contextMenuService); } @@ -171,7 +171,7 @@ export class MenuEntryActionViewItem extends ActionViewItem { render(container: HTMLElement): void { super.render(container); - this._updateItemClass(this._action.item); + this._updateItemClass(this._action); let mouseOver = false; @@ -226,15 +226,15 @@ export class MenuEntryActionViewItem extends ActionViewItem { if (this.options.icon) { if (this._commandAction !== this._action) { if (this._action.alt) { - this._updateItemClass(this._action.alt.item); + this._updateItemClass(this._action.alt); } } else if ((this._action).alt) { - this._updateItemClass(this._action.item); + this._updateItemClass(this._action); } } } - _updateItemClass(item: ICommandAction): void { + _updateItemClass(item: MenuItemAction): void { this._itemClassDispose.value = undefined; if (ThemeIcon.isThemeIcon(item.icon)) { diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index ab37df3e9fa..f4dcad91966 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -20,16 +20,37 @@ export interface ILocalizedString { original: string; } +export type Icon = { dark?: URI; light?: URI; } | ThemeIcon; +export type ToggleAwareIcon = { toggled?: Icon, untoggled?: Icon }; +export type ToggleAwareTitle = { toggled?: string | ILocalizedString, untoggled?: string | ILocalizedString }; + export interface ICommandAction { id: string; - title: string | ILocalizedString; + title: string | ILocalizedString | ToggleAwareTitle; category?: string | ILocalizedString; - icon?: { dark?: URI; light?: URI; } | ThemeIcon; + icon?: Icon | ToggleAwareIcon; precondition?: ContextKeyExpression; toggled?: ContextKeyExpression; } -export type ISerializableCommandAction = UriDto; +export function isToggleAwareTitle(thing: unknown): thing is ToggleAwareTitle { + return thing && typeof thing === 'object' + && ((typeof (thing as ToggleAwareTitle).toggled === 'string' || typeof (thing as ToggleAwareTitle).toggled === 'object') + || (typeof (thing as ToggleAwareTitle).untoggled === 'string' || typeof (thing as ToggleAwareTitle).untoggled === 'object')); +} + +export function isIcon(thing: unknown): thing is Icon { + if (ThemeIcon.isThemeIcon(thing)) { + return true; + } + return thing && typeof thing === 'object' + && ((thing as { dark?: URI, light?: URI }).dark instanceof URI || (thing as { dark?: URI, light?: URI }).light instanceof URI); +} + +export function isToggleAwareIcon(thing: unknown): thing is ToggleAwareIcon { + return thing && typeof thing === 'object' + && (isIcon((thing as ToggleAwareIcon).toggled) || isIcon((thing as ToggleAwareIcon).untoggled)); +} export interface IMenuItem { command: ICommandAction; @@ -260,9 +281,18 @@ export class SubmenuItemAction extends Action { } } +export type ISerializableMenuItemAction = UriDto<{ + id: string; + title: string | ILocalizedString; + category: string | ILocalizedString | undefined; + icon: Icon | undefined; +}>; + export class MenuItemAction extends ExecuteCommandAction { - readonly item: ICommandAction; + readonly title: string | ILocalizedString; + readonly category: string | ILocalizedString | undefined; + readonly icon: Icon | undefined; readonly alt: MenuItemAction | undefined; private _options: IMenuActionOptions; @@ -274,14 +304,17 @@ export class MenuItemAction extends ExecuteCommandAction { @IContextKeyService contextKeyService: IContextKeyService, @ICommandService commandService: ICommandService ) { - typeof item.title === 'string' ? super(item.id, item.title, commandService) : super(item.id, item.title.value, commandService); + super(item.id, '', commandService); + this.title = (isToggleAwareTitle(item.title) ? this._checked ? item.title.toggled : item.title.untoggled : item.title) || ''; + this._label = typeof this.title === 'string' ? this.title : this.title.value; this._cssClass = undefined; this._enabled = !item.precondition || contextKeyService.contextMatchesRules(item.precondition); this._checked = Boolean(item.toggled && contextKeyService.contextMatchesRules(item.toggled)); + this.category = item.category; + this.icon = isToggleAwareIcon(item.icon) ? this.checked ? item.icon.toggled : item.icon.untoggled : item.icon; this._options = options || {}; - this.item = item; this.alt = alt ? new MenuItemAction(alt, undefined, this._options, contextKeyService, commandService) : undefined; } @@ -305,6 +338,15 @@ export class MenuItemAction extends ExecuteCommandAction { return super.run(...runArgs); } + + serialize(): ISerializableMenuItemAction { + return { + id: this.id, + title: this.title, + category: this.category, + icon: this.icon + }; + } } export class SyncActionDescriptor { diff --git a/src/vs/platform/electron/electron-main/electronMainService.ts b/src/vs/platform/electron/electron-main/electronMainService.ts index 27ef89f17e2..4e7cf3e5e22 100644 --- a/src/vs/platform/electron/electron-main/electronMainService.ts +++ b/src/vs/platform/electron/electron-main/electronMainService.ts @@ -12,7 +12,7 @@ import { IOpenedWindow, OpenContext, IWindowOpenable, IOpenEmptyWindowOptions } import { INativeOpenDialogOptions } from 'vs/platform/dialogs/node/dialogs'; import { isMacintosh } from 'vs/base/common/platform'; import { IElectronService } from 'vs/platform/electron/node/electron'; -import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; +import { ISerializableMenuItemAction } from 'vs/platform/actions/common/actions'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { AddFirstParameterToFunctions } from 'vs/base/common/types'; import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; @@ -279,7 +279,7 @@ export class ElectronMainService implements IElectronMainService { return true; } - async updateTouchBar(windowId: number | undefined, items: ISerializableCommandAction[][]): Promise { + async updateTouchBar(windowId: number | undefined, items: ISerializableMenuItemAction[][]): Promise { const window = this.windowById(windowId); if (window) { window.updateTouchBar(items); diff --git a/src/vs/platform/electron/node/electron.ts b/src/vs/platform/electron/node/electron.ts index 8803fd16b39..41c1275befd 100644 --- a/src/vs/platform/electron/node/electron.ts +++ b/src/vs/platform/electron/node/electron.ts @@ -8,7 +8,7 @@ import { MessageBoxOptions, MessageBoxReturnValue, OpenDevToolsOptions, SaveDial import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IWindowOpenable, IOpenEmptyWindowOptions, IOpenedWindow } from 'vs/platform/windows/common/windows'; import { INativeOpenDialogOptions } from 'vs/platform/dialogs/node/dialogs'; -import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; +import { ISerializableMenuItemAction } from 'vs/platform/actions/common/actions'; import { INativeOpenWindowOptions } from 'vs/platform/windows/node/window'; export const IElectronService = createDecorator('electronService'); @@ -60,7 +60,7 @@ export interface IElectronService { setRepresentedFilename(path: string): Promise; setDocumentEdited(edited: boolean): Promise; openExternal(url: string): Promise; - updateTouchBar(items: ISerializableCommandAction[][]): Promise; + updateTouchBar(items: ISerializableMenuItemAction[][]): Promise; // macOS Touchbar newWindowTab(): Promise; diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index 3e05c84fcd9..22c0b239887 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -10,7 +10,7 @@ import { Event } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IProcessEnvironment } from 'vs/base/common/platform'; import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; -import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; +import { ISerializableMenuItemAction } from 'vs/platform/actions/common/actions'; import { URI } from 'vs/base/common/uri'; import { Rectangle, BrowserWindow } from 'electron'; import { IDisposable } from 'vs/base/common/lifecycle'; @@ -82,7 +82,7 @@ export interface ICodeWindow extends IDisposable { handleTitleDoubleClick(): void; - updateTouchBar(items: ISerializableCommandAction[][]): void; + updateTouchBar(items: ISerializableMenuItemAction[][]): void; serializeWindowState(): IWindowState; } diff --git a/src/vs/workbench/contrib/quickopen/browser/commandsHandler.ts b/src/vs/workbench/contrib/quickopen/browser/commandsHandler.ts index a41fb8930e9..19b4cba19a8 100644 --- a/src/vs/workbench/contrib/quickopen/browser/commandsHandler.ts +++ b/src/vs/workbench/contrib/quickopen/browser/commandsHandler.ts @@ -569,10 +569,10 @@ export class CommandsHandler extends QuickOpenHandler implements IDisposable { const entries: ActionCommandEntry[] = []; for (let action of actions) { - const title = typeof action.item.title === 'string' ? action.item.title : action.item.title.value; + const title = typeof action.title === 'string' ? action.title : action.title.value; let category, label = title; - if (action.item.category) { - category = typeof action.item.category === 'string' ? action.item.category : action.item.category.value; + if (action.category) { + category = typeof action.category === 'string' ? action.category : action.category.value; label = localize('cat.title', "{0}: {1}", category, title); } @@ -580,8 +580,8 @@ export class CommandsHandler extends QuickOpenHandler implements IDisposable { const labelHighlights = wordFilter(searchValue, label); // Add an 'alias' in original language when running in different locale - const aliasTitle = (!Language.isDefaultVariant() && typeof action.item.title !== 'string') ? action.item.title.original : undefined; - const aliasCategory = (!Language.isDefaultVariant() && category && action.item.category && typeof action.item.category !== 'string') ? action.item.category.original : undefined; + const aliasTitle = (!Language.isDefaultVariant() && typeof action.title !== 'string') ? action.title.original : undefined; + const aliasCategory = (!Language.isDefaultVariant() && category && action.category && typeof action.category !== 'string') ? action.category.original : undefined; let alias; if (aliasTitle && category) { alias = aliasCategory ? `${aliasCategory}: ${aliasTitle}` : `${category}: ${aliasTitle}`; @@ -591,7 +591,7 @@ export class CommandsHandler extends QuickOpenHandler implements IDisposable { const aliasHighlights = alias ? wordFilter(searchValue, alias) : null; if (labelHighlights || aliasHighlights) { - 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))); + 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))); } } } diff --git a/src/vs/workbench/contrib/remote/electron-browser/remote.contribution.ts b/src/vs/workbench/contrib/remote/electron-browser/remote.contribution.ts index 67df9f76198..d6ea00c8a27 100644 --- a/src/vs/workbench/contrib/remote/electron-browser/remote.contribution.ts +++ b/src/vs/workbench/contrib/remote/electron-browser/remote.contribution.ts @@ -200,14 +200,14 @@ export class RemoteWindowActiveIndicator extends Disposable implements IWorkbenc } for (let action of actionGroup[1]) { if (action instanceof MenuItemAction) { - let label = typeof action.item.title === 'string' ? action.item.title : action.item.title.value; - if (action.item.category) { - const category = typeof action.item.category === 'string' ? action.item.category : action.item.category.value; + let label = typeof action.title === 'string' ? action.title : action.title.value; + if (action.category) { + const category = typeof action.category === 'string' ? action.category : action.category.value; label = nls.localize('cat.title', "{0}: {1}", category, label); } items.push({ type: 'item', - id: action.item.id, + id: action.id, label }); } diff --git a/src/vs/workbench/electron-browser/window.ts b/src/vs/workbench/electron-browser/window.ts index 6f7117b9068..094333c1055 100644 --- a/src/vs/workbench/electron-browser/window.ts +++ b/src/vs/workbench/electron-browser/window.ts @@ -23,7 +23,7 @@ import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; import { KeyboardMapperFactory } from 'vs/workbench/services/keybinding/electron-browser/nativeKeymapService'; import { ipcRenderer as ipc, webFrame, crashReporter, CrashReporterStartOptions, Event as IpcEvent } from 'electron'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing'; -import { IMenuService, MenuId, IMenu, MenuItemAction, ICommandAction, SubmenuItemAction, MenuRegistry } from 'vs/platform/actions/common/actions'; +import { IMenuService, MenuId, IMenu, MenuItemAction, SubmenuItemAction, MenuRegistry, ISerializableMenuItemAction } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { RunOnceScheduler } from 'vs/base/common/async'; @@ -68,7 +68,7 @@ export class NativeWindow extends Disposable { private touchBarMenu: IMenu | undefined; private readonly touchBarDisposables = this._register(new DisposableStore()); - private lastInstalledTouchedBar: ICommandAction[][] | undefined; + private lastInstalledTouchedBar: ISerializableMenuItemAction[][] | undefined; private readonly customTitleContextMenuDisposable = this._register(new DisposableStore()); @@ -504,18 +504,18 @@ export class NativeWindow extends Disposable { this.touchBarDisposables.add(createAndFillInActionBarActions(this.touchBarMenu, undefined, actions)); // Convert into command action multi array - const items: ICommandAction[][] = []; - let group: ICommandAction[] = []; + const items: ISerializableMenuItemAction[][] = []; + let group: ISerializableMenuItemAction[] = []; if (!disabled) { for (const action of actions) { // Command if (action instanceof MenuItemAction) { - if (ignoredItems.indexOf(action.item.id) >= 0) { + if (ignoredItems.indexOf(action.id) >= 0) { continue; // ignored } - group.push(action.item); + group.push(action.serialize()); } // Separator -- GitLab