Path: blob/main/src/vs/workbench/electron-browser/actions/windowActions.ts
5220 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import './media/actions.css';6import { URI } from '../../../base/common/uri.js';7import { localize, localize2 } from '../../../nls.js';8import { ApplyZoomTarget, MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL, applyZoom } from '../../../platform/window/electron-browser/window.js';9import { IKeybindingService } from '../../../platform/keybinding/common/keybinding.js';10import { getZoomLevel } from '../../../base/browser/browser.js';11import { FileKind } from '../../../platform/files/common/files.js';12import { IModelService } from '../../../editor/common/services/model.js';13import { ILanguageService } from '../../../editor/common/languages/language.js';14import { IQuickInputService, IQuickInputButton, IQuickPickItem, QuickPickInput } from '../../../platform/quickinput/common/quickInput.js';15import { getIconClasses } from '../../../editor/common/services/getIconClasses.js';16import { ICommandHandler } from '../../../platform/commands/common/commands.js';17import { ServicesAccessor } from '../../../platform/instantiation/common/instantiation.js';18import { IConfigurationService } from '../../../platform/configuration/common/configuration.js';19import { INativeHostService } from '../../../platform/native/common/native.js';20import { Codicon } from '../../../base/common/codicons.js';21import { ThemeIcon } from '../../../base/common/themables.js';22import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from '../../../platform/workspace/common/workspace.js';23import { Action2, MenuId } from '../../../platform/actions/common/actions.js';24import { Categories } from '../../../platform/action/common/actionCommonCategories.js';25import { KeyCode, KeyMod } from '../../../base/common/keyCodes.js';26import { KeybindingWeight } from '../../../platform/keybinding/common/keybindingsRegistry.js';27import { isMacintosh } from '../../../base/common/platform.js';28import { getActiveWindow } from '../../../base/browser/dom.js';29import { IOpenedAuxiliaryWindow, IOpenedMainWindow, isOpenedAuxiliaryWindow } from '../../../platform/window/common/window.js';30import { IsAuxiliaryWindowContext, IsAuxiliaryWindowFocusedContext, IsWindowAlwaysOnTopContext } from '../../common/contextkeys.js';31import { isAuxiliaryWindow } from '../../../base/browser/window.js';32import { ContextKeyExpr } from '../../../platform/contextkey/common/contextkey.js';3334export class CloseWindowAction extends Action2 {3536static readonly ID = 'workbench.action.closeWindow';3738constructor() {39super({40id: CloseWindowAction.ID,41title: {42...localize2('closeWindow', "Close Window"),43mnemonicTitle: localize({ key: 'miCloseWindow', comment: ['&& denotes a mnemonic'] }, "Clos&&e Window"),44},45f1: true,46keybinding: {47weight: KeybindingWeight.WorkbenchContrib,48mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyW },49linux: { primary: KeyMod.Alt | KeyCode.F4, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyW] },50win: { primary: KeyMod.Alt | KeyCode.F4, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyW] }51},52menu: {53id: MenuId.MenubarFileMenu,54group: '6_close',55order: 456}57});58}5960override async run(accessor: ServicesAccessor): Promise<void> {61const nativeHostService = accessor.get(INativeHostService);6263return nativeHostService.closeWindow({ targetWindowId: getActiveWindow().vscodeWindowId });64}65}6667export class CloseOtherWindowsAction extends Action2 {6869private static readonly ID = 'workbench.action.closeOtherWindows';7071constructor() {72super({73id: CloseOtherWindowsAction.ID,74title: localize2('closeOtherWindows', "Close Other Windows"),75f1: true76});77}7879override async run(accessor: ServicesAccessor): Promise<void> {80const nativeHostService = accessor.get(INativeHostService);8182const currentWindowId = getActiveWindow().vscodeWindowId;83const windows = await nativeHostService.getWindows({ includeAuxiliaryWindows: false });8485for (const window of windows) {86if (window.id !== currentWindowId) {87nativeHostService.closeWindow({ targetWindowId: window.id });88}89}90}91}9293abstract class BaseZoomAction extends Action2 {9495private static readonly ZOOM_LEVEL_SETTING_KEY = 'window.zoomLevel';96private static readonly ZOOM_PER_WINDOW_SETTING_KEY = 'window.zoomPerWindow';9798protected async setZoomLevel(accessor: ServicesAccessor, levelOrReset: number | true): Promise<void> {99const configurationService = accessor.get(IConfigurationService);100101let target: ApplyZoomTarget;102if (configurationService.getValue(BaseZoomAction.ZOOM_PER_WINDOW_SETTING_KEY) !== false) {103target = ApplyZoomTarget.ACTIVE_WINDOW;104} else {105target = ApplyZoomTarget.ALL_WINDOWS;106}107108let level: number;109if (typeof levelOrReset === 'number') {110level = Math.round(levelOrReset); // prevent fractional zoom levels111} else {112113// reset to 0 when we apply to all windows114if (target === ApplyZoomTarget.ALL_WINDOWS) {115level = 0;116}117118// otherwise, reset to the default zoom level119else {120const defaultLevel = configurationService.getValue(BaseZoomAction.ZOOM_LEVEL_SETTING_KEY);121if (typeof defaultLevel === 'number') {122level = defaultLevel;123} else {124level = 0;125}126}127}128129if (level > MAX_ZOOM_LEVEL || level < MIN_ZOOM_LEVEL) {130return; // https://github.com/microsoft/vscode/issues/48357131}132133if (target === ApplyZoomTarget.ALL_WINDOWS) {134await configurationService.updateValue(BaseZoomAction.ZOOM_LEVEL_SETTING_KEY, level);135}136137applyZoom(level, target);138}139}140141export class ZoomInAction extends BaseZoomAction {142143constructor() {144super({145id: 'workbench.action.zoomIn',146title: {147...localize2('zoomIn', "Zoom In"),148mnemonicTitle: localize({ key: 'miZoomIn', comment: ['&& denotes a mnemonic'] }, "&&Zoom In"),149},150category: Categories.View,151f1: true,152keybinding: {153weight: KeybindingWeight.WorkbenchContrib,154primary: KeyMod.CtrlCmd | KeyCode.Equal,155secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Equal, KeyMod.CtrlCmd | KeyCode.NumpadAdd]156},157menu: {158id: MenuId.MenubarAppearanceMenu,159group: '5_zoom',160order: 1161}162});163}164165override run(accessor: ServicesAccessor): Promise<void> {166return super.setZoomLevel(accessor, getZoomLevel(getActiveWindow()) + 1);167}168}169170export class ZoomOutAction extends BaseZoomAction {171172constructor() {173super({174id: 'workbench.action.zoomOut',175title: {176...localize2('zoomOut', "Zoom Out"),177mnemonicTitle: localize({ key: 'miZoomOut', comment: ['&& denotes a mnemonic'] }, "&&Zoom Out"),178},179category: Categories.View,180f1: true,181keybinding: {182weight: KeybindingWeight.WorkbenchContrib,183primary: KeyMod.CtrlCmd | KeyCode.Minus,184secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Minus, KeyMod.CtrlCmd | KeyCode.NumpadSubtract],185linux: {186primary: KeyMod.CtrlCmd | KeyCode.Minus,187secondary: [KeyMod.CtrlCmd | KeyCode.NumpadSubtract]188}189},190menu: {191id: MenuId.MenubarAppearanceMenu,192group: '5_zoom',193order: 2194}195});196}197198override run(accessor: ServicesAccessor): Promise<void> {199return super.setZoomLevel(accessor, getZoomLevel(getActiveWindow()) - 1);200}201}202203export class ZoomResetAction extends BaseZoomAction {204205constructor() {206super({207id: 'workbench.action.zoomReset',208title: {209...localize2('zoomReset', "Reset Zoom"),210mnemonicTitle: localize({ key: 'miZoomReset', comment: ['&& denotes a mnemonic'] }, "&&Reset Zoom"),211},212category: Categories.View,213f1: true,214keybinding: {215weight: KeybindingWeight.WorkbenchContrib,216primary: KeyMod.CtrlCmd | KeyCode.Numpad0217},218menu: {219id: MenuId.MenubarAppearanceMenu,220group: '5_zoom',221order: 3222}223});224}225226override run(accessor: ServicesAccessor): Promise<void> {227return super.setZoomLevel(accessor, true);228}229}230231abstract class BaseSwitchWindow extends Action2 {232233private readonly closeWindowAction: IQuickInputButton = {234iconClass: ThemeIcon.asClassName(Codicon.removeClose),235tooltip: localize('close', "Close Window")236};237238private readonly closeDirtyWindowAction: IQuickInputButton = {239iconClass: 'dirty-window ' + ThemeIcon.asClassName(Codicon.closeDirty),240tooltip: localize('close', "Close Window"),241alwaysVisible: true242};243244private readonly closeActiveWindowAction: IQuickInputButton = {245iconClass: 'active-window ' + ThemeIcon.asClassName(Codicon.windowActive),246tooltip: localize('closeActive', "Close Active Window"),247alwaysVisible: true248};249250protected abstract isQuickNavigate(): boolean;251252override async run(accessor: ServicesAccessor): Promise<void> {253const quickInputService = accessor.get(IQuickInputService);254const keybindingService = accessor.get(IKeybindingService);255const modelService = accessor.get(IModelService);256const languageService = accessor.get(ILanguageService);257const nativeHostService = accessor.get(INativeHostService);258259const currentWindowId = getActiveWindow().vscodeWindowId;260261const windows = await nativeHostService.getWindows({ includeAuxiliaryWindows: true });262263const mainWindows = new Set<IOpenedMainWindow>();264const mapMainWindowToAuxiliaryWindows = new Map<number, Set<IOpenedAuxiliaryWindow>>();265for (const window of windows) {266if (isOpenedAuxiliaryWindow(window)) {267let auxiliaryWindows = mapMainWindowToAuxiliaryWindows.get(window.parentId);268if (!auxiliaryWindows) {269auxiliaryWindows = new Set<IOpenedAuxiliaryWindow>();270mapMainWindowToAuxiliaryWindows.set(window.parentId, auxiliaryWindows);271}272auxiliaryWindows.add(window);273} else {274mainWindows.add(window);275}276}277278interface IWindowPickItem extends IQuickPickItem {279readonly windowId: number;280}281282function isWindowPickItem(candidate: unknown): candidate is IWindowPickItem {283const windowPickItem = candidate as IWindowPickItem | undefined;284285return typeof windowPickItem?.windowId === 'number';286}287288const picks: Array<QuickPickInput<IWindowPickItem>> = [];289for (const window of mainWindows) {290const auxiliaryWindows = mapMainWindowToAuxiliaryWindows.get(window.id);291if (mapMainWindowToAuxiliaryWindows.size > 0) {292picks.push({ type: 'separator', label: auxiliaryWindows ? localize('windowGroup', "window group") : undefined });293}294295const resource = window.filename ? URI.file(window.filename) : isSingleFolderWorkspaceIdentifier(window.workspace) ? window.workspace.uri : isWorkspaceIdentifier(window.workspace) ? window.workspace.configPath : undefined;296const fileKind = window.filename ? FileKind.FILE : isSingleFolderWorkspaceIdentifier(window.workspace) ? FileKind.FOLDER : isWorkspaceIdentifier(window.workspace) ? FileKind.ROOT_FOLDER : FileKind.FILE;297const pick: IWindowPickItem = {298windowId: window.id,299label: window.title,300ariaLabel: window.dirty ? localize('windowDirtyAriaLabel', "{0}, window with unsaved changes", window.title) : window.title,301iconClasses: getIconClasses(modelService, languageService, resource, fileKind),302description: (currentWindowId === window.id) ? localize('current', "Current Window") : undefined,303buttons: window.dirty ? [this.closeDirtyWindowAction] : currentWindowId === window.id ? [this.closeActiveWindowAction] : [this.closeWindowAction]304};305picks.push(pick);306307if (auxiliaryWindows) {308for (const auxiliaryWindow of auxiliaryWindows) {309const pick: IWindowPickItem = {310windowId: auxiliaryWindow.id,311label: auxiliaryWindow.title,312iconClasses: getIconClasses(modelService, languageService, auxiliaryWindow.filename ? URI.file(auxiliaryWindow.filename) : undefined, FileKind.FILE),313description: (currentWindowId === auxiliaryWindow.id) ? localize('current', "Current Window") : undefined,314buttons: currentWindowId === auxiliaryWindow.id ? [this.closeActiveWindowAction] : [this.closeWindowAction]315};316picks.push(pick);317}318}319}320321const pick = await quickInputService.pick(picks, {322contextKey: 'inWindowsPicker',323activeItem: (() => {324for (let i = 0; i < picks.length; i++) {325const pick = picks[i];326if (isWindowPickItem(pick) && pick.windowId === currentWindowId) {327let nextPick = picks[i + 1]; // try to select next window unless it's a separator328if (isWindowPickItem(nextPick)) {329return nextPick;330}331332nextPick = picks[i + 2]; // otherwise try to select the next window after the separator333if (isWindowPickItem(nextPick)) {334return nextPick;335}336}337}338339return undefined;340})(),341placeHolder: localize('switchWindowPlaceHolder', "Select a window to switch to"),342quickNavigate: this.isQuickNavigate() ? { keybindings: keybindingService.lookupKeybindings(this.desc.id) } : undefined,343hideInput: this.isQuickNavigate(),344onDidTriggerItemButton: async context => {345await nativeHostService.closeWindow({ targetWindowId: context.item.windowId });346context.removeItem();347}348});349350if (pick) {351nativeHostService.focusWindow({ targetWindowId: pick.windowId });352}353}354}355356export class SwitchWindowAction extends BaseSwitchWindow {357358constructor() {359super({360id: 'workbench.action.switchWindow',361title: localize2('switchWindow', 'Switch Window...'),362f1: true,363keybinding: {364weight: KeybindingWeight.WorkbenchContrib,365primary: 0,366mac: { primary: KeyMod.WinCtrl | KeyCode.KeyW }367}368});369}370371protected isQuickNavigate(): boolean {372return false;373}374}375376export class QuickSwitchWindowAction extends BaseSwitchWindow {377378constructor() {379super({380id: 'workbench.action.quickSwitchWindow',381title: localize2('quickSwitchWindow', 'Quick Switch Window...'),382f1: false // hide quick pickers from command palette to not confuse with the other entry that shows a input field383});384}385386protected isQuickNavigate(): boolean {387return true;388}389}390391function canRunNativeTabsHandler(accessor: ServicesAccessor): boolean {392if (!isMacintosh) {393return false;394}395396const configurationService = accessor.get(IConfigurationService);397return configurationService.getValue<unknown>('window.nativeTabs') === true;398}399400export const NewWindowTabHandler: ICommandHandler = function (accessor: ServicesAccessor) {401if (!canRunNativeTabsHandler(accessor)) {402return;403}404405return accessor.get(INativeHostService).newWindowTab();406};407408export const ShowPreviousWindowTabHandler: ICommandHandler = function (accessor: ServicesAccessor) {409if (!canRunNativeTabsHandler(accessor)) {410return;411}412413return accessor.get(INativeHostService).showPreviousWindowTab();414};415416export const ShowNextWindowTabHandler: ICommandHandler = function (accessor: ServicesAccessor) {417if (!canRunNativeTabsHandler(accessor)) {418return;419}420421return accessor.get(INativeHostService).showNextWindowTab();422};423424export const MoveWindowTabToNewWindowHandler: ICommandHandler = function (accessor: ServicesAccessor) {425if (!canRunNativeTabsHandler(accessor)) {426return;427}428429return accessor.get(INativeHostService).moveWindowTabToNewWindow();430};431432export const MergeWindowTabsHandlerHandler: ICommandHandler = function (accessor: ServicesAccessor) {433if (!canRunNativeTabsHandler(accessor)) {434return;435}436437return accessor.get(INativeHostService).mergeAllWindowTabs();438};439440export const ToggleWindowTabsBarHandler: ICommandHandler = function (accessor: ServicesAccessor) {441if (!canRunNativeTabsHandler(accessor)) {442return;443}444445return accessor.get(INativeHostService).toggleWindowTabsBar();446};447448export class ToggleWindowAlwaysOnTopAction extends Action2 {449450static readonly ID = 'workbench.action.toggleWindowAlwaysOnTop';451452constructor() {453super({454id: ToggleWindowAlwaysOnTopAction.ID,455title: localize2('toggleWindowAlwaysOnTop', "Toggle Window Always on Top"),456f1: true,457precondition: IsAuxiliaryWindowFocusedContext458});459}460461override async run(accessor: ServicesAccessor): Promise<void> {462const nativeHostService = accessor.get(INativeHostService);463464const targetWindow = getActiveWindow();465if (!isAuxiliaryWindow(targetWindow.window)) {466return; // Currently, we only support toggling always on top for auxiliary windows467}468469return nativeHostService.toggleWindowAlwaysOnTop({ targetWindowId: getActiveWindow().vscodeWindowId });470}471}472473export class EnableWindowAlwaysOnTopAction extends Action2 {474475static readonly ID = 'workbench.action.enableWindowAlwaysOnTop';476477constructor() {478super({479id: EnableWindowAlwaysOnTopAction.ID,480title: localize('enableWindowAlwaysOnTop', "Turn On Always on Top"),481icon: Codicon.pin,482menu: {483id: MenuId.LayoutControlMenu,484when: ContextKeyExpr.and(IsWindowAlwaysOnTopContext.toNegated(), IsAuxiliaryWindowContext),485order: 1486}487});488}489490override async run(accessor: ServicesAccessor): Promise<void> {491const nativeHostService = accessor.get(INativeHostService);492493const targetWindow = getActiveWindow();494if (!isAuxiliaryWindow(targetWindow.window)) {495return; // Currently, we only support toggling always on top for auxiliary windows496}497498return nativeHostService.setWindowAlwaysOnTop(true, { targetWindowId: targetWindow.vscodeWindowId });499}500}501502export class DisableWindowAlwaysOnTopAction extends Action2 {503504static readonly ID = 'workbench.action.disableWindowAlwaysOnTop';505506constructor() {507super({508id: DisableWindowAlwaysOnTopAction.ID,509title: localize('disableWindowAlwaysOnTop', "Turn Off Always on Top"),510icon: Codicon.pinned,511menu: {512id: MenuId.LayoutControlMenu,513when: ContextKeyExpr.and(IsWindowAlwaysOnTopContext, IsAuxiliaryWindowContext),514order: 1515}516});517}518519override async run(accessor: ServicesAccessor): Promise<void> {520const nativeHostService = accessor.get(INativeHostService);521522const targetWindow = getActiveWindow();523if (!isAuxiliaryWindow(targetWindow.window)) {524return; // Currently, we only support toggling always on top for auxiliary windows525}526527return nativeHostService.setWindowAlwaysOnTop(false, { targetWindowId: targetWindow.vscodeWindowId });528}529}530531532