Path: blob/main/src/vs/workbench/electron-browser/actions/windowActions.ts
3294 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, IAction2Options, 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}6667abstract class BaseZoomAction extends Action2 {6869private static readonly ZOOM_LEVEL_SETTING_KEY = 'window.zoomLevel';70private static readonly ZOOM_PER_WINDOW_SETTING_KEY = 'window.zoomPerWindow';7172constructor(desc: Readonly<IAction2Options>) {73super(desc);74}7576protected async setZoomLevel(accessor: ServicesAccessor, levelOrReset: number | true): Promise<void> {77const configurationService = accessor.get(IConfigurationService);7879let target: ApplyZoomTarget;80if (configurationService.getValue(BaseZoomAction.ZOOM_PER_WINDOW_SETTING_KEY) !== false) {81target = ApplyZoomTarget.ACTIVE_WINDOW;82} else {83target = ApplyZoomTarget.ALL_WINDOWS;84}8586let level: number;87if (typeof levelOrReset === 'number') {88level = Math.round(levelOrReset); // prevent fractional zoom levels89} else {9091// reset to 0 when we apply to all windows92if (target === ApplyZoomTarget.ALL_WINDOWS) {93level = 0;94}9596// otherwise, reset to the default zoom level97else {98const defaultLevel = configurationService.getValue(BaseZoomAction.ZOOM_LEVEL_SETTING_KEY);99if (typeof defaultLevel === 'number') {100level = defaultLevel;101} else {102level = 0;103}104}105}106107if (level > MAX_ZOOM_LEVEL || level < MIN_ZOOM_LEVEL) {108return; // https://github.com/microsoft/vscode/issues/48357109}110111if (target === ApplyZoomTarget.ALL_WINDOWS) {112await configurationService.updateValue(BaseZoomAction.ZOOM_LEVEL_SETTING_KEY, level);113}114115applyZoom(level, target);116}117}118119export class ZoomInAction extends BaseZoomAction {120121constructor() {122super({123id: 'workbench.action.zoomIn',124title: {125...localize2('zoomIn', "Zoom In"),126mnemonicTitle: localize({ key: 'miZoomIn', comment: ['&& denotes a mnemonic'] }, "&&Zoom In"),127},128category: Categories.View,129f1: true,130keybinding: {131weight: KeybindingWeight.WorkbenchContrib,132primary: KeyMod.CtrlCmd | KeyCode.Equal,133secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Equal, KeyMod.CtrlCmd | KeyCode.NumpadAdd]134},135menu: {136id: MenuId.MenubarAppearanceMenu,137group: '5_zoom',138order: 1139}140});141}142143override run(accessor: ServicesAccessor): Promise<void> {144return super.setZoomLevel(accessor, getZoomLevel(getActiveWindow()) + 1);145}146}147148export class ZoomOutAction extends BaseZoomAction {149150constructor() {151super({152id: 'workbench.action.zoomOut',153title: {154...localize2('zoomOut', "Zoom Out"),155mnemonicTitle: localize({ key: 'miZoomOut', comment: ['&& denotes a mnemonic'] }, "&&Zoom Out"),156},157category: Categories.View,158f1: true,159keybinding: {160weight: KeybindingWeight.WorkbenchContrib,161primary: KeyMod.CtrlCmd | KeyCode.Minus,162secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Minus, KeyMod.CtrlCmd | KeyCode.NumpadSubtract],163linux: {164primary: KeyMod.CtrlCmd | KeyCode.Minus,165secondary: [KeyMod.CtrlCmd | KeyCode.NumpadSubtract]166}167},168menu: {169id: MenuId.MenubarAppearanceMenu,170group: '5_zoom',171order: 2172}173});174}175176override run(accessor: ServicesAccessor): Promise<void> {177return super.setZoomLevel(accessor, getZoomLevel(getActiveWindow()) - 1);178}179}180181export class ZoomResetAction extends BaseZoomAction {182183constructor() {184super({185id: 'workbench.action.zoomReset',186title: {187...localize2('zoomReset', "Reset Zoom"),188mnemonicTitle: localize({ key: 'miZoomReset', comment: ['&& denotes a mnemonic'] }, "&&Reset Zoom"),189},190category: Categories.View,191f1: true,192keybinding: {193weight: KeybindingWeight.WorkbenchContrib,194primary: KeyMod.CtrlCmd | KeyCode.Numpad0195},196menu: {197id: MenuId.MenubarAppearanceMenu,198group: '5_zoom',199order: 3200}201});202}203204override run(accessor: ServicesAccessor): Promise<void> {205return super.setZoomLevel(accessor, true);206}207}208209abstract class BaseSwitchWindow extends Action2 {210211private readonly closeWindowAction: IQuickInputButton = {212iconClass: ThemeIcon.asClassName(Codicon.removeClose),213tooltip: localize('close', "Close Window")214};215216private readonly closeDirtyWindowAction: IQuickInputButton = {217iconClass: 'dirty-window ' + ThemeIcon.asClassName(Codicon.closeDirty),218tooltip: localize('close', "Close Window"),219alwaysVisible: true220};221222constructor(desc: Readonly<IAction2Options>) {223super(desc);224}225226protected abstract isQuickNavigate(): boolean;227228override async run(accessor: ServicesAccessor): Promise<void> {229const quickInputService = accessor.get(IQuickInputService);230const keybindingService = accessor.get(IKeybindingService);231const modelService = accessor.get(IModelService);232const languageService = accessor.get(ILanguageService);233const nativeHostService = accessor.get(INativeHostService);234235const currentWindowId = getActiveWindow().vscodeWindowId;236237const windows = await nativeHostService.getWindows({ includeAuxiliaryWindows: true });238239const mainWindows = new Set<IOpenedMainWindow>();240const mapMainWindowToAuxiliaryWindows = new Map<number, Set<IOpenedAuxiliaryWindow>>();241for (const window of windows) {242if (isOpenedAuxiliaryWindow(window)) {243let auxiliaryWindows = mapMainWindowToAuxiliaryWindows.get(window.parentId);244if (!auxiliaryWindows) {245auxiliaryWindows = new Set<IOpenedAuxiliaryWindow>();246mapMainWindowToAuxiliaryWindows.set(window.parentId, auxiliaryWindows);247}248auxiliaryWindows.add(window);249} else {250mainWindows.add(window);251}252}253254interface IWindowPickItem extends IQuickPickItem {255readonly windowId: number;256}257258function isWindowPickItem(candidate: unknown): candidate is IWindowPickItem {259const windowPickItem = candidate as IWindowPickItem | undefined;260261return typeof windowPickItem?.windowId === 'number';262}263264const picks: Array<QuickPickInput<IWindowPickItem>> = [];265for (const window of mainWindows) {266const auxiliaryWindows = mapMainWindowToAuxiliaryWindows.get(window.id);267if (mapMainWindowToAuxiliaryWindows.size > 0) {268picks.push({ type: 'separator', label: auxiliaryWindows ? localize('windowGroup', "window group") : undefined });269}270271const resource = window.filename ? URI.file(window.filename) : isSingleFolderWorkspaceIdentifier(window.workspace) ? window.workspace.uri : isWorkspaceIdentifier(window.workspace) ? window.workspace.configPath : undefined;272const fileKind = window.filename ? FileKind.FILE : isSingleFolderWorkspaceIdentifier(window.workspace) ? FileKind.FOLDER : isWorkspaceIdentifier(window.workspace) ? FileKind.ROOT_FOLDER : FileKind.FILE;273const pick: IWindowPickItem = {274windowId: window.id,275label: window.title,276ariaLabel: window.dirty ? localize('windowDirtyAriaLabel', "{0}, window with unsaved changes", window.title) : window.title,277iconClasses: getIconClasses(modelService, languageService, resource, fileKind),278description: (currentWindowId === window.id) ? localize('current', "Current Window") : undefined,279buttons: currentWindowId !== window.id ? window.dirty ? [this.closeDirtyWindowAction] : [this.closeWindowAction] : undefined280};281picks.push(pick);282283if (auxiliaryWindows) {284for (const auxiliaryWindow of auxiliaryWindows) {285const pick: IWindowPickItem = {286windowId: auxiliaryWindow.id,287label: auxiliaryWindow.title,288iconClasses: getIconClasses(modelService, languageService, auxiliaryWindow.filename ? URI.file(auxiliaryWindow.filename) : undefined, FileKind.FILE),289description: (currentWindowId === auxiliaryWindow.id) ? localize('current', "Current Window") : undefined,290buttons: [this.closeWindowAction]291};292picks.push(pick);293}294}295}296297const pick = await quickInputService.pick(picks, {298contextKey: 'inWindowsPicker',299activeItem: (() => {300for (let i = 0; i < picks.length; i++) {301const pick = picks[i];302if (isWindowPickItem(pick) && pick.windowId === currentWindowId) {303let nextPick = picks[i + 1]; // try to select next window unless it's a separator304if (isWindowPickItem(nextPick)) {305return nextPick;306}307308nextPick = picks[i + 2]; // otherwise try to select the next window after the separator309if (isWindowPickItem(nextPick)) {310return nextPick;311}312}313}314315return undefined;316})(),317placeHolder: localize('switchWindowPlaceHolder', "Select a window to switch to"),318quickNavigate: this.isQuickNavigate() ? { keybindings: keybindingService.lookupKeybindings(this.desc.id) } : undefined,319hideInput: this.isQuickNavigate(),320onDidTriggerItemButton: async context => {321await nativeHostService.closeWindow({ targetWindowId: context.item.windowId });322context.removeItem();323}324});325326if (pick) {327nativeHostService.focusWindow({ targetWindowId: pick.windowId });328}329}330}331332export class SwitchWindowAction extends BaseSwitchWindow {333334constructor() {335super({336id: 'workbench.action.switchWindow',337title: localize2('switchWindow', 'Switch Window...'),338f1: true,339keybinding: {340weight: KeybindingWeight.WorkbenchContrib,341primary: 0,342mac: { primary: KeyMod.WinCtrl | KeyCode.KeyW }343}344});345}346347protected isQuickNavigate(): boolean {348return false;349}350}351352export class QuickSwitchWindowAction extends BaseSwitchWindow {353354constructor() {355super({356id: 'workbench.action.quickSwitchWindow',357title: localize2('quickSwitchWindow', 'Quick Switch Window...'),358f1: false // hide quick pickers from command palette to not confuse with the other entry that shows a input field359});360}361362protected isQuickNavigate(): boolean {363return true;364}365}366367function canRunNativeTabsHandler(accessor: ServicesAccessor): boolean {368if (!isMacintosh) {369return false;370}371372const configurationService = accessor.get(IConfigurationService);373return configurationService.getValue<unknown>('window.nativeTabs') === true;374}375376export const NewWindowTabHandler: ICommandHandler = function (accessor: ServicesAccessor) {377if (!canRunNativeTabsHandler(accessor)) {378return;379}380381return accessor.get(INativeHostService).newWindowTab();382};383384export const ShowPreviousWindowTabHandler: ICommandHandler = function (accessor: ServicesAccessor) {385if (!canRunNativeTabsHandler(accessor)) {386return;387}388389return accessor.get(INativeHostService).showPreviousWindowTab();390};391392export const ShowNextWindowTabHandler: ICommandHandler = function (accessor: ServicesAccessor) {393if (!canRunNativeTabsHandler(accessor)) {394return;395}396397return accessor.get(INativeHostService).showNextWindowTab();398};399400export const MoveWindowTabToNewWindowHandler: ICommandHandler = function (accessor: ServicesAccessor) {401if (!canRunNativeTabsHandler(accessor)) {402return;403}404405return accessor.get(INativeHostService).moveWindowTabToNewWindow();406};407408export const MergeWindowTabsHandlerHandler: ICommandHandler = function (accessor: ServicesAccessor) {409if (!canRunNativeTabsHandler(accessor)) {410return;411}412413return accessor.get(INativeHostService).mergeAllWindowTabs();414};415416export const ToggleWindowTabsBarHandler: ICommandHandler = function (accessor: ServicesAccessor) {417if (!canRunNativeTabsHandler(accessor)) {418return;419}420421return accessor.get(INativeHostService).toggleWindowTabsBar();422};423424export class ToggleWindowAlwaysOnTopAction extends Action2 {425426static readonly ID = 'workbench.action.toggleWindowAlwaysOnTop';427428constructor() {429super({430id: ToggleWindowAlwaysOnTopAction.ID,431title: localize2('toggleWindowAlwaysOnTop', "Toggle Window Always on Top"),432f1: true,433precondition: IsAuxiliaryWindowFocusedContext434});435}436437override async run(accessor: ServicesAccessor): Promise<void> {438const nativeHostService = accessor.get(INativeHostService);439440const targetWindow = getActiveWindow();441if (!isAuxiliaryWindow(targetWindow.window)) {442return; // Currently, we only support toggling always on top for auxiliary windows443}444445return nativeHostService.toggleWindowAlwaysOnTop({ targetWindowId: getActiveWindow().vscodeWindowId });446}447}448449export class EnableWindowAlwaysOnTopAction extends Action2 {450451static readonly ID = 'workbench.action.enableWindowAlwaysOnTop';452453constructor() {454super({455id: EnableWindowAlwaysOnTopAction.ID,456title: localize('enableWindowAlwaysOnTop', "Turn On Always on Top"),457icon: Codicon.pin,458menu: {459id: MenuId.LayoutControlMenu,460when: ContextKeyExpr.and(IsWindowAlwaysOnTopContext.toNegated(), IsAuxiliaryWindowContext),461order: 1462}463});464}465466override async run(accessor: ServicesAccessor): Promise<void> {467const nativeHostService = accessor.get(INativeHostService);468469const targetWindow = getActiveWindow();470if (!isAuxiliaryWindow(targetWindow.window)) {471return; // Currently, we only support toggling always on top for auxiliary windows472}473474return nativeHostService.setWindowAlwaysOnTop(true, { targetWindowId: targetWindow.vscodeWindowId });475}476}477478export class DisableWindowAlwaysOnTopAction extends Action2 {479480static readonly ID = 'workbench.action.disableWindowAlwaysOnTop';481482constructor() {483super({484id: DisableWindowAlwaysOnTopAction.ID,485title: localize('disableWindowAlwaysOnTop', "Turn Off Always on Top"),486icon: Codicon.pinned,487menu: {488id: MenuId.LayoutControlMenu,489when: ContextKeyExpr.and(IsWindowAlwaysOnTopContext, IsAuxiliaryWindowContext),490order: 1491}492});493}494495override async run(accessor: ServicesAccessor): Promise<void> {496const nativeHostService = accessor.get(INativeHostService);497498const targetWindow = getActiveWindow();499if (!isAuxiliaryWindow(targetWindow.window)) {500return; // Currently, we only support toggling always on top for auxiliary windows501}502503return nativeHostService.setWindowAlwaysOnTop(false, { targetWindowId: targetWindow.vscodeWindowId });504}505}506507508