mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-17 15:24:40 +01:00
Browser Zoom (#299161)
* Browser Zoom * Remove logic from editor * Feedback * Small comment change * Zoom factors, not percentages * Comment on keybinding * Add keybindings back to actions * Add browserZoomLabel helper for zoom percentage display * First AI changes * AI pass with zoom level hierarchy * Fix zoom not applying to other origins after default zoom change * Promote per-origin zoom to user setting workbench.browser.zoom.perOriginZoomLevels * Remove unnecessary configuration migration for zoom setting * Add 'Match VS Code' default zoom level option for Integrated Browser * Add missing localize import to platform/browserView/common/browserView.ts * Switch per-origin zoom tracking to per-host (http/https only) * Rename zoom settings: defaultZoomLevel→pageZoom, perHostZoomLevels→zoomLevels; mark zoomLevels as advanced * Update setting description and scope * Improve zoom service: lazy synchronizer, pre-computed label map, RunOnceScheduler, always forceApply on navigate * Remove self-evident and redundant comments * Refactor zoom to two independent cascades (ephemeral/persistent each fall back to default independently) * Use IStorageService for per-host browser zoom instead of settings * Remove VS Code product name from browser zoom code
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
import { Event } from '../../../base/common/event.js';
|
||||
import { VSBuffer } from '../../../base/common/buffer.js';
|
||||
import { URI } from '../../../base/common/uri.js';
|
||||
import { localize } from '../../../nls.js';
|
||||
|
||||
const commandPrefix = 'workbench.action.browser';
|
||||
export enum BrowserViewCommandId {
|
||||
@@ -58,7 +59,7 @@ export interface IBrowserViewState {
|
||||
lastFavicon: string | undefined;
|
||||
lastError: IBrowserViewLoadError | undefined;
|
||||
storageScope: BrowserViewStorageScope;
|
||||
zoomFactor: number;
|
||||
browserZoomIndex: number;
|
||||
}
|
||||
|
||||
export interface IBrowserViewNavigationEvent {
|
||||
@@ -143,6 +144,16 @@ export enum BrowserViewStorageScope {
|
||||
|
||||
export const ipcBrowserViewChannelName = 'browserView';
|
||||
|
||||
/**
|
||||
* Discrete zoom levels matching Edge/Chrome.
|
||||
* Note: When those browsers say "33%" and "67%" zoom, they really mean 33.33...% and 66.66...%
|
||||
*/
|
||||
export const browserZoomFactors = [0.25, 1 / 3, 0.5, 2 / 3, 0.75, 0.8, 0.9, 1, 1.1, 1.25, 1.5, 1.75, 2, 2.5, 3, 4, 5] as const;
|
||||
export const browserZoomDefaultIndex = browserZoomFactors.indexOf(1);
|
||||
export function browserZoomLabel(zoomFactor: number): string {
|
||||
return localize('browserZoomPercent', "{0}%", Math.round(zoomFactor * 100));
|
||||
}
|
||||
|
||||
/**
|
||||
* This should match the isolated world ID defined in `preload-browserView.ts`.
|
||||
*/
|
||||
@@ -311,6 +322,9 @@ export interface IBrowserViewService {
|
||||
*/
|
||||
clearStorage(id: string): Promise<void>;
|
||||
|
||||
/** Set the browser zoom index (independent from VS Code zoom). */
|
||||
setBrowserZoomIndex(id: string, zoomIndex: number): Promise<void>;
|
||||
|
||||
/**
|
||||
* Update the keybinding accelerators used in browser view context menus.
|
||||
* @param keybindings A map of command ID to accelerator label
|
||||
|
||||
@@ -8,7 +8,7 @@ import { FileAccess } from '../../../base/common/network.js';
|
||||
import { Disposable } from '../../../base/common/lifecycle.js';
|
||||
import { Emitter, Event } from '../../../base/common/event.js';
|
||||
import { VSBuffer } from '../../../base/common/buffer.js';
|
||||
import { IBrowserViewBounds, IBrowserViewDevToolsStateEvent, IBrowserViewFocusEvent, IBrowserViewKeyDownEvent, IBrowserViewState, IBrowserViewNavigationEvent, IBrowserViewLoadingEvent, IBrowserViewLoadError, IBrowserViewTitleChangeEvent, IBrowserViewFaviconChangeEvent, IBrowserViewNewPageRequest, IBrowserViewCaptureScreenshotOptions, IBrowserViewFindInPageOptions, IBrowserViewFindInPageResult, IBrowserViewVisibilityEvent, BrowserNewPageLocation, browserViewIsolatedWorldId } from '../common/browserView.js';
|
||||
import { IBrowserViewBounds, IBrowserViewDevToolsStateEvent, IBrowserViewFocusEvent, IBrowserViewKeyDownEvent, IBrowserViewState, IBrowserViewNavigationEvent, IBrowserViewLoadingEvent, IBrowserViewLoadError, IBrowserViewTitleChangeEvent, IBrowserViewFaviconChangeEvent, IBrowserViewNewPageRequest, IBrowserViewCaptureScreenshotOptions, IBrowserViewFindInPageOptions, IBrowserViewFindInPageResult, IBrowserViewVisibilityEvent, BrowserNewPageLocation, browserViewIsolatedWorldId, browserZoomFactors, browserZoomDefaultIndex } from '../common/browserView.js';
|
||||
import { EVENT_KEY_CODE_MAP, KeyCode, KeyMod, SCAN_CODE_STR_TO_EVENT_KEY_CODE } from '../../../base/common/keyCodes.js';
|
||||
import { IWindowsMainService } from '../../windows/electron-main/windows.js';
|
||||
import { ICodeWindow } from '../../window/electron-main/window.js';
|
||||
@@ -46,6 +46,7 @@ export class BrowserView extends Disposable implements ICDPTarget {
|
||||
private _lastFavicon: string | undefined = undefined;
|
||||
private _lastError: IBrowserViewLoadError | undefined = undefined;
|
||||
private _lastUserGestureTimestamp: number = -Infinity;
|
||||
private _browserZoomIndex: number = browserZoomDefaultIndex;
|
||||
|
||||
private _debugger: BrowserViewDebugger;
|
||||
private _window: ICodeWindow | IAuxiliaryWindow | undefined;
|
||||
@@ -278,6 +279,12 @@ export class BrowserView extends Disposable implements ICDPTarget {
|
||||
webContents.on('did-navigate', fireNavigationEvent);
|
||||
webContents.on('did-navigate-in-page', fireNavigationEvent);
|
||||
|
||||
// Chromium resets the zoom factor to its per-origin default (100%) when
|
||||
// navigating to a new document. Re-apply our stored zoom to override it.
|
||||
webContents.on('did-navigate', () => {
|
||||
this._view.webContents.setZoomFactor(browserZoomFactors[this._browserZoomIndex]);
|
||||
});
|
||||
|
||||
// Focus events
|
||||
webContents.on('focus', () => {
|
||||
this._onDidChangeFocus.fire({ focused: true });
|
||||
@@ -366,7 +373,7 @@ export class BrowserView extends Disposable implements ICDPTarget {
|
||||
lastFavicon: this._lastFavicon,
|
||||
lastError: this._lastError,
|
||||
storageScope: this.session.storageScope,
|
||||
zoomFactor: webContents.getZoomFactor()
|
||||
browserZoomIndex: this._browserZoomIndex
|
||||
};
|
||||
}
|
||||
|
||||
@@ -390,7 +397,6 @@ export class BrowserView extends Disposable implements ICDPTarget {
|
||||
}
|
||||
}
|
||||
|
||||
this._view.webContents.setZoomFactor(bounds.zoomFactor);
|
||||
this._view.setBorderRadius(Math.round(bounds.cornerRadius * bounds.zoomFactor));
|
||||
this._view.setBounds({
|
||||
x: Math.round(bounds.x * bounds.zoomFactor),
|
||||
@@ -400,6 +406,12 @@ export class BrowserView extends Disposable implements ICDPTarget {
|
||||
});
|
||||
}
|
||||
|
||||
setBrowserZoomIndex(zoomIndex: number): void {
|
||||
this._browserZoomIndex = Math.max(0, Math.min(zoomIndex, browserZoomFactors.length - 1));
|
||||
const browserZoomFactor = browserZoomFactors[this._browserZoomIndex];
|
||||
this._view.webContents.setZoomFactor(browserZoomFactor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the visibility of this view
|
||||
*/
|
||||
|
||||
@@ -328,6 +328,10 @@ export class BrowserViewMainService extends Disposable implements IBrowserViewMa
|
||||
return this._getBrowserView(id).clearStorage();
|
||||
}
|
||||
|
||||
async setBrowserZoomIndex(id: string, zoomIndex: number): Promise<void> {
|
||||
return this._getBrowserView(id).setBrowserZoomIndex(zoomIndex);
|
||||
}
|
||||
|
||||
async clearGlobalStorage(): Promise<void> {
|
||||
const browserSession = BrowserSession.getOrCreateGlobal();
|
||||
await browserSession.electronSession.clearData();
|
||||
|
||||
@@ -27,13 +27,25 @@ import {
|
||||
IBrowserViewCaptureScreenshotOptions,
|
||||
IBrowserViewFindInPageOptions,
|
||||
IBrowserViewFindInPageResult,
|
||||
IBrowserViewVisibilityEvent
|
||||
IBrowserViewVisibilityEvent,
|
||||
browserZoomDefaultIndex,
|
||||
browserZoomFactors
|
||||
} from '../../../../platform/browserView/common/browserView.js';
|
||||
import { IWorkspaceContextService, WorkbenchState } from '../../../../platform/workspace/common/workspace.js';
|
||||
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
|
||||
import { isLocalhostAuthority } from '../../../../platform/url/common/trustedDomains.js';
|
||||
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
|
||||
import { IWorkspaceTrustManagementService } from '../../../../platform/workspace/common/workspaceTrust.js';
|
||||
import { IBrowserZoomService } from './browserZoomService.js';
|
||||
|
||||
/** Extracts the host from a URL string for zoom tracking purposes. */
|
||||
function parseZoomHost(url: string): string | undefined {
|
||||
const parsed = URL.parse(url);
|
||||
if (!parsed?.host || (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')) {
|
||||
return undefined;
|
||||
}
|
||||
return parsed.host;
|
||||
}
|
||||
|
||||
type IntegratedBrowserNavigationEvent = {
|
||||
navigationType: 'urlInput' | 'goBack' | 'goForward' | 'reload';
|
||||
@@ -116,8 +128,11 @@ export interface IBrowserViewModel extends IDisposable {
|
||||
readonly storageScope: BrowserViewStorageScope;
|
||||
readonly sharedWithAgent: boolean;
|
||||
readonly zoomFactor: number;
|
||||
readonly canZoomIn: boolean;
|
||||
readonly canZoomOut: boolean;
|
||||
|
||||
readonly onDidChangeSharedWithAgent: Event<boolean>;
|
||||
readonly onDidChangeZoom: Event<void>;
|
||||
readonly onDidNavigate: Event<IBrowserViewNavigationEvent>;
|
||||
readonly onDidChangeLoadingState: Event<IBrowserViewLoadingEvent>;
|
||||
readonly onDidChangeFocus: Event<IBrowserViewFocusEvent>;
|
||||
@@ -148,6 +163,9 @@ export interface IBrowserViewModel extends IDisposable {
|
||||
getSelectedText(): Promise<string>;
|
||||
clearStorage(): Promise<void>;
|
||||
setSharedWithAgent(shared: boolean): Promise<void>;
|
||||
zoomIn(): Promise<void>;
|
||||
zoomOut(): Promise<void>;
|
||||
resetZoom(): Promise<void>;
|
||||
}
|
||||
|
||||
export class BrowserViewModel extends Disposable implements IBrowserViewModel {
|
||||
@@ -163,12 +181,17 @@ export class BrowserViewModel extends Disposable implements IBrowserViewModel {
|
||||
private _canGoForward: boolean = false;
|
||||
private _error: IBrowserViewLoadError | undefined = undefined;
|
||||
private _storageScope: BrowserViewStorageScope = BrowserViewStorageScope.Ephemeral;
|
||||
private _isEphemeral: boolean = false;
|
||||
private _zoomHost: string | undefined = undefined;
|
||||
private _sharedWithAgent: boolean = false;
|
||||
private _zoomFactor: number = 1;
|
||||
private _browserZoomIndex: number = browserZoomDefaultIndex;
|
||||
|
||||
private readonly _onDidChangeSharedWithAgent = this._register(new Emitter<boolean>());
|
||||
readonly onDidChangeSharedWithAgent: Event<boolean> = this._onDidChangeSharedWithAgent.event;
|
||||
|
||||
private readonly _onDidChangeZoom = this._register(new Emitter<void>());
|
||||
readonly onDidChangeZoom: Event<void> = this._onDidChangeZoom.event;
|
||||
|
||||
private readonly _onWillDispose = this._register(new Emitter<void>());
|
||||
readonly onWillDispose: Event<void> = this._onWillDispose.event;
|
||||
|
||||
@@ -182,6 +205,7 @@ export class BrowserViewModel extends Disposable implements IBrowserViewModel {
|
||||
@IPlaywrightService private readonly playwrightService: IPlaywrightService,
|
||||
@IDialogService private readonly dialogService: IDialogService,
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
@IBrowserZoomService private readonly zoomService: IBrowserZoomService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -199,7 +223,9 @@ export class BrowserViewModel extends Disposable implements IBrowserViewModel {
|
||||
get error(): IBrowserViewLoadError | undefined { return this._error; }
|
||||
get storageScope(): BrowserViewStorageScope { return this._storageScope; }
|
||||
get sharedWithAgent(): boolean { return this._sharedWithAgent; }
|
||||
get zoomFactor(): number { return this._zoomFactor; }
|
||||
get zoomFactor(): number { return browserZoomFactors[this._browserZoomIndex]; }
|
||||
get canZoomIn(): boolean { return this._browserZoomIndex < browserZoomFactors.length - 1; }
|
||||
get canZoomOut(): boolean { return this._browserZoomIndex > 0; }
|
||||
|
||||
get onDidNavigate(): Event<IBrowserViewNavigationEvent> {
|
||||
return this.browserViewService.onDynamicDidNavigate(this.id);
|
||||
@@ -282,7 +308,26 @@ export class BrowserViewModel extends Disposable implements IBrowserViewModel {
|
||||
this._error = state.lastError;
|
||||
this._storageScope = state.storageScope;
|
||||
this._sharedWithAgent = await this.playwrightService.isPageTracked(this.id);
|
||||
this._zoomFactor = state.zoomFactor;
|
||||
this._browserZoomIndex = state.browserZoomIndex;
|
||||
|
||||
this._isEphemeral = this._storageScope === BrowserViewStorageScope.Ephemeral;
|
||||
this._zoomHost = parseZoomHost(this._url);
|
||||
|
||||
const effectiveZoomIndex = this.zoomService.getEffectiveZoomIndex(this._zoomHost, this._isEphemeral);
|
||||
if (effectiveZoomIndex !== this._browserZoomIndex) {
|
||||
await this.setBrowserZoomIndex(effectiveZoomIndex);
|
||||
}
|
||||
|
||||
this._register(this.zoomService.onDidChangeZoom(({ host, isEphemeralChange }) => {
|
||||
if (isEphemeralChange && !this._isEphemeral) {
|
||||
return;
|
||||
}
|
||||
if (host === undefined || host === this._zoomHost) {
|
||||
void this.setBrowserZoomIndex(
|
||||
this.zoomService.getEffectiveZoomIndex(this._zoomHost, this._isEphemeral)
|
||||
);
|
||||
}
|
||||
}));
|
||||
|
||||
// Set up state synchronization
|
||||
|
||||
@@ -292,10 +337,18 @@ export class BrowserViewModel extends Disposable implements IBrowserViewModel {
|
||||
this._favicon = undefined;
|
||||
}
|
||||
|
||||
this._zoomHost = parseZoomHost(e.url);
|
||||
this._url = e.url;
|
||||
this._title = e.title;
|
||||
this._canGoBack = e.canGoBack;
|
||||
this._canGoForward = e.canGoForward;
|
||||
|
||||
// Always forceApply because Chromium resets zoom on cross-origin navigation,
|
||||
// and an origin change may not correspond to a host change (e.g. http→https).
|
||||
void this.setBrowserZoomIndex(
|
||||
this.zoomService.getEffectiveZoomIndex(this._zoomHost, this._isEphemeral),
|
||||
true
|
||||
);
|
||||
}));
|
||||
|
||||
this._register(this.onDidChangeLoadingState(e => {
|
||||
@@ -329,7 +382,6 @@ export class BrowserViewModel extends Disposable implements IBrowserViewModel {
|
||||
}
|
||||
|
||||
async layout(bounds: IBrowserViewBounds): Promise<void> {
|
||||
this._zoomFactor = bounds.zoomFactor;
|
||||
return this.browserViewService.layout(this.id, bounds);
|
||||
}
|
||||
|
||||
@@ -395,6 +447,49 @@ export class BrowserViewModel extends Disposable implements IBrowserViewModel {
|
||||
return this.browserViewService.clearStorage(this.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param forceApply When true, the IPC call is made even if the local cached zoom index
|
||||
* already matches the requested value. Pass true after cross-document navigation because
|
||||
* Chromium resets the zoom to its per-origin default, making the cache stale.
|
||||
*/
|
||||
private async setBrowserZoomIndex(zoomIndex: number, forceApply = false): Promise<void> {
|
||||
const clamped = Math.max(0, Math.min(zoomIndex, browserZoomFactors.length - 1));
|
||||
if (!forceApply && clamped === this._browserZoomIndex) {
|
||||
return;
|
||||
}
|
||||
this._browserZoomIndex = clamped;
|
||||
await this.browserViewService.setBrowserZoomIndex(this.id, this._browserZoomIndex);
|
||||
this._onDidChangeZoom.fire();
|
||||
}
|
||||
|
||||
async zoomIn(): Promise<void> {
|
||||
if (!this.canZoomIn) {
|
||||
return;
|
||||
}
|
||||
await this.setBrowserZoomIndex(this._browserZoomIndex + 1);
|
||||
if (this._zoomHost) {
|
||||
this.zoomService.setHostZoomIndex(this._zoomHost, this._browserZoomIndex, this._isEphemeral);
|
||||
}
|
||||
}
|
||||
|
||||
async zoomOut(): Promise<void> {
|
||||
if (!this.canZoomOut) {
|
||||
return;
|
||||
}
|
||||
await this.setBrowserZoomIndex(this._browserZoomIndex - 1);
|
||||
if (this._zoomHost) {
|
||||
this.zoomService.setHostZoomIndex(this._zoomHost, this._browserZoomIndex, this._isEphemeral);
|
||||
}
|
||||
}
|
||||
|
||||
async resetZoom(): Promise<void> {
|
||||
const defaultIndex = this.zoomService.getEffectiveZoomIndex(undefined, false);
|
||||
await this.setBrowserZoomIndex(defaultIndex);
|
||||
if (this._zoomHost) {
|
||||
this.zoomService.setHostZoomIndex(this._zoomHost, defaultIndex, this._isEphemeral);
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly SHARE_DONT_ASK_KEY = 'browserView.shareWithAgent.dontAskAgain';
|
||||
|
||||
async setSharedWithAgent(shared: boolean): Promise<void> {
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
|
||||
import { browserZoomDefaultIndex, browserZoomFactors } from '../../../../platform/browserView/common/browserView.js';
|
||||
import { zoomLevelToZoomFactor } from '../../../../platform/window/common/window.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
|
||||
|
||||
export const IBrowserZoomService = createDecorator<IBrowserZoomService>('browserZoomService');
|
||||
|
||||
/** Storage key for the per-host persistent zoom map. */
|
||||
const BROWSER_ZOOM_PER_HOST_STORAGE_KEY = 'browserView.zoomPerHost';
|
||||
|
||||
/**
|
||||
* Special value for the default zoom level setting that instructs the browser view
|
||||
* to dynamically match the closest zoom level to the application's current UI zoom.
|
||||
*/
|
||||
export const MATCH_WINDOW_ZOOM_LABEL = 'Match Window';
|
||||
|
||||
export interface IBrowserZoomChangeEvent {
|
||||
/**
|
||||
* The host (e.g. `"example.com"`) whose zoom changed, or `undefined`
|
||||
* when the global default zoom level changed.
|
||||
*/
|
||||
readonly host: string | undefined;
|
||||
|
||||
/**
|
||||
* Whether the change came from an ephemeral session.
|
||||
* - `true` → only ephemeral views need to react.
|
||||
* - `false` → all views (ephemeral and non-ephemeral) for the host may be affected.
|
||||
*/
|
||||
readonly isEphemeralChange: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages two independent cascading zoom hierarchies for integrated browser views:
|
||||
*
|
||||
* Normal views: `persistent per-host override` ?? `configured default`
|
||||
* Ephemeral views: `ephemeral per-host override` ?? `configured default`
|
||||
*
|
||||
* Ephemeral views never see persistent overrides directly. Instead, when a persistent
|
||||
* value changes, it is copied into the ephemeral map so that ephemeral views
|
||||
* immediately reflect the new level. Conversely, ephemeral changes never affect
|
||||
* normal views.
|
||||
*
|
||||
* Per-host values that equal the current default are always removed (both persistent
|
||||
* and ephemeral), so the view tracks the default going forward.
|
||||
*/
|
||||
export interface IBrowserZoomService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
/** Fired whenever the effective zoom for a host may have changed. */
|
||||
readonly onDidChangeZoom: Event<IBrowserZoomChangeEvent>;
|
||||
|
||||
/**
|
||||
* Returns the effective zoom index for the given host and session type.
|
||||
* Pass `host = undefined` to obtain only the configured default zoom index.
|
||||
*/
|
||||
getEffectiveZoomIndex(host: string | undefined, isEphemeral: boolean): number;
|
||||
|
||||
/**
|
||||
* Set the zoom for a host.
|
||||
*
|
||||
* Non-ephemeral: persisted to storage. Also propagated into
|
||||
* the ephemeral map so ephemeral views immediately reflect the change.
|
||||
*
|
||||
* Ephemeral: stored in memory only, dropped on restart.
|
||||
*
|
||||
* In both cases, if the value equals the current default, the entry is removed so the
|
||||
* view tracks the default going forward.
|
||||
*/
|
||||
setHostZoomIndex(host: string, zoomIndex: number, isEphemeral: boolean): void;
|
||||
|
||||
/**
|
||||
* Notifies the service of the application's current UI zoom factor.
|
||||
* Must be called once on startup and again whenever the window zoom changes.
|
||||
* Only relevant when the default zoom level is set to `MATCH_WINDOW_LABEL`.
|
||||
*/
|
||||
notifyWindowZoomChanged(windowZoomFactor: number): void;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Implementation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Pre-computed map from percentage label (e.g. "125%") to index into browserZoomFactors. */
|
||||
const ZOOM_LABEL_TO_INDEX = new Map<string, number>(
|
||||
browserZoomFactors.map((f, i) => [`${Math.round(f * 100)}%`, i])
|
||||
);
|
||||
|
||||
export class BrowserZoomService extends Disposable implements IBrowserZoomService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private readonly _onDidChangeZoom = this._register(new Emitter<IBrowserZoomChangeEvent>());
|
||||
readonly onDidChangeZoom: Event<IBrowserZoomChangeEvent> = this._onDidChangeZoom.event;
|
||||
|
||||
/**
|
||||
* In-memory cache of the persistent per-host map.
|
||||
* Backed by IStorageService.
|
||||
*/
|
||||
private _persistentZoomMap: Record<string, number>;
|
||||
|
||||
/** In-memory only; dropped on restart. */
|
||||
private readonly _ephemeralZoomMap = new Map<string, number>();
|
||||
|
||||
private _windowZoomFactor: number = zoomLevelToZoomFactor(0); // default: zoom level 0 → factor 1.0
|
||||
|
||||
constructor(
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this._persistentZoomMap = this._readPersistentZoomMap();
|
||||
|
||||
this._register(this.configurationService.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration('workbench.browser.zoom.pageZoom')) {
|
||||
this._onDidChangeZoom.fire({ host: undefined, isEphemeralChange: false });
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
getEffectiveZoomIndex(host: string | undefined, isEphemeral: boolean): number {
|
||||
if (host !== undefined) {
|
||||
if (isEphemeral) {
|
||||
const ephemeralIndex = this._ephemeralZoomMap.get(host);
|
||||
if (ephemeralIndex !== undefined) {
|
||||
return this._clamp(ephemeralIndex);
|
||||
}
|
||||
} else {
|
||||
const persistentIndex = this._persistentZoomMap[host];
|
||||
if (persistentIndex !== undefined) {
|
||||
return this._clamp(persistentIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this._getDefaultZoomIndex();
|
||||
}
|
||||
|
||||
setHostZoomIndex(host: string, zoomIndex: number, isEphemeral: boolean): void {
|
||||
const clamped = this._clamp(zoomIndex);
|
||||
const defaultIndex = this._getDefaultZoomIndex();
|
||||
const matchesDefault = clamped === defaultIndex;
|
||||
|
||||
if (isEphemeral) {
|
||||
if (matchesDefault) {
|
||||
if (!this._ephemeralZoomMap.has(host)) {
|
||||
return;
|
||||
}
|
||||
this._ephemeralZoomMap.delete(host);
|
||||
} else {
|
||||
if (this._ephemeralZoomMap.get(host) === clamped) {
|
||||
return;
|
||||
}
|
||||
this._ephemeralZoomMap.set(host, clamped);
|
||||
}
|
||||
this._onDidChangeZoom.fire({ host, isEphemeralChange: true });
|
||||
} else {
|
||||
let persistentChanged = false;
|
||||
if (matchesDefault) {
|
||||
if (Object.prototype.hasOwnProperty.call(this._persistentZoomMap, host)) {
|
||||
delete this._persistentZoomMap[host];
|
||||
persistentChanged = true;
|
||||
}
|
||||
} else if (this._persistentZoomMap[host] !== clamped) {
|
||||
this._persistentZoomMap[host] = clamped;
|
||||
persistentChanged = true;
|
||||
}
|
||||
|
||||
// Propagate to ephemeral map so ephemeral views immediately reflect the new level.
|
||||
let ephemeralChanged = false;
|
||||
if (matchesDefault) {
|
||||
ephemeralChanged = this._ephemeralZoomMap.delete(host);
|
||||
} else if (this._ephemeralZoomMap.get(host) !== clamped) {
|
||||
this._ephemeralZoomMap.set(host, clamped);
|
||||
ephemeralChanged = true;
|
||||
}
|
||||
|
||||
if (!persistentChanged && !ephemeralChanged) {
|
||||
return;
|
||||
}
|
||||
if (persistentChanged) {
|
||||
this._writePersistentZoomMap();
|
||||
}
|
||||
this._onDidChangeZoom.fire({ host, isEphemeralChange: false });
|
||||
}
|
||||
}
|
||||
|
||||
notifyWindowZoomChanged(windowZoomFactor: number): void {
|
||||
this._windowZoomFactor = windowZoomFactor;
|
||||
const label = this.configurationService.getValue<string>('workbench.browser.zoom.pageZoom');
|
||||
if (label === MATCH_WINDOW_ZOOM_LABEL) {
|
||||
this._onDidChangeZoom.fire({ host: undefined, isEphemeralChange: false });
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private _getDefaultZoomIndex(): number {
|
||||
const label = this.configurationService.getValue<string>('workbench.browser.zoom.pageZoom');
|
||||
if (label === MATCH_WINDOW_ZOOM_LABEL) {
|
||||
return this._getMatchWindowZoomIndex();
|
||||
}
|
||||
return ZOOM_LABEL_TO_INDEX.get(label) ?? browserZoomDefaultIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the browser zoom index whose factor is closest to the application's current UI zoom
|
||||
* factor, measuring distance on a log scale (since window zoom levels are powers of 1.2).
|
||||
*/
|
||||
private _getMatchWindowZoomIndex(): number {
|
||||
const windowFactor = this._windowZoomFactor;
|
||||
let bestIndex = browserZoomDefaultIndex;
|
||||
let bestDist = Infinity;
|
||||
for (let i = 0; i < browserZoomFactors.length; i++) {
|
||||
const dist = Math.abs(Math.log(browserZoomFactors[i]) - Math.log(windowFactor));
|
||||
if (dist < bestDist) {
|
||||
bestDist = dist;
|
||||
bestIndex = i;
|
||||
}
|
||||
}
|
||||
return bestIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the persistent per-host zoom map from storage.
|
||||
* The stored format is a JSON object mapping host strings to zoom indices.
|
||||
*/
|
||||
private _readPersistentZoomMap(): Record<string, number> {
|
||||
const raw = this.storageService.get(BROWSER_ZOOM_PER_HOST_STORAGE_KEY, StorageScope.PROFILE);
|
||||
if (!raw) {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
||||
return {};
|
||||
}
|
||||
const result: Record<string, number> = {};
|
||||
for (const [host, index] of Object.entries(parsed)) {
|
||||
if (typeof index === 'number' && index >= 0 && index < browserZoomFactors.length) {
|
||||
result[host] = index;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
private _writePersistentZoomMap(): void {
|
||||
const hasEntries = Object.keys(this._persistentZoomMap).length > 0;
|
||||
if (hasEntries) {
|
||||
this.storageService.store(BROWSER_ZOOM_PER_HOST_STORAGE_KEY, JSON.stringify(this._persistentZoomMap), StorageScope.PROFILE, StorageTarget.MACHINE);
|
||||
} else {
|
||||
this.storageService.remove(BROWSER_ZOOM_PER_HOST_STORAGE_KEY, StorageScope.PROFILE);
|
||||
}
|
||||
}
|
||||
|
||||
private _clamp(index: number): number {
|
||||
return Math.max(0, Math.min(Math.trunc(index), browserZoomFactors.length - 1));
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,8 @@ export const CONTEXT_BROWSER_HAS_URL = new RawContextKey<boolean>('browserHasUrl
|
||||
export const CONTEXT_BROWSER_HAS_ERROR = new RawContextKey<boolean>('browserHasError', false, localize('browser.hasError', "Whether the browser has a load error"));
|
||||
export const CONTEXT_BROWSER_DEVTOOLS_OPEN = new RawContextKey<boolean>('browserDevToolsOpen', false, localize('browser.devToolsOpen', "Whether developer tools are open for the current browser view"));
|
||||
export const CONTEXT_BROWSER_ELEMENT_SELECTION_ACTIVE = new RawContextKey<boolean>('browserElementSelectionActive', false, localize('browser.elementSelectionActive', "Whether element selection is currently active"));
|
||||
export const CONTEXT_BROWSER_CAN_ZOOM_IN = new RawContextKey<boolean>('browserCanZoomIn', true, localize('browser.canZoomIn', "Whether the browser can zoom in further"));
|
||||
export const CONTEXT_BROWSER_CAN_ZOOM_OUT = new RawContextKey<boolean>('browserCanZoomOut', true, localize('browser.canZoomOut', "Whether the browser can zoom out further"));
|
||||
|
||||
// Re-export find widget context keys for use in actions
|
||||
export { CONTEXT_BROWSER_FIND_WIDGET_FOCUSED, CONTEXT_BROWSER_FIND_WIDGET_VISIBLE };
|
||||
@@ -254,6 +256,8 @@ export class BrowserEditor extends EditorPane {
|
||||
private _hasErrorContext!: IContextKey<boolean>;
|
||||
private _devToolsOpenContext!: IContextKey<boolean>;
|
||||
private _elementSelectionActiveContext!: IContextKey<boolean>;
|
||||
private _canZoomInContext!: IContextKey<boolean>;
|
||||
private _canZoomOutContext!: IContextKey<boolean>;
|
||||
|
||||
private _model: IBrowserViewModel | undefined;
|
||||
private readonly _inputDisposables = this._register(new DisposableStore());
|
||||
@@ -295,6 +299,8 @@ export class BrowserEditor extends EditorPane {
|
||||
this._hasErrorContext = CONTEXT_BROWSER_HAS_ERROR.bindTo(contextKeyService);
|
||||
this._devToolsOpenContext = CONTEXT_BROWSER_DEVTOOLS_OPEN.bindTo(contextKeyService);
|
||||
this._elementSelectionActiveContext = CONTEXT_BROWSER_ELEMENT_SELECTION_ACTIVE.bindTo(contextKeyService);
|
||||
this._canZoomInContext = CONTEXT_BROWSER_CAN_ZOOM_IN.bindTo(contextKeyService);
|
||||
this._canZoomOutContext = CONTEXT_BROWSER_CAN_ZOOM_OUT.bindTo(contextKeyService);
|
||||
|
||||
// Currently this is always true since it is scoped to the editor container
|
||||
CONTEXT_BROWSER_FOCUSED.bindTo(contextKeyService);
|
||||
@@ -399,6 +405,7 @@ export class BrowserEditor extends EditorPane {
|
||||
|
||||
this._storageScopeContext.set(this._model.storageScope);
|
||||
this._devToolsOpenContext.set(this._model.isDevToolsOpen);
|
||||
this.updateZoomContext();
|
||||
this._updateSharingState(true);
|
||||
|
||||
// Update find widget with new model
|
||||
@@ -417,6 +424,10 @@ export class BrowserEditor extends EditorPane {
|
||||
this._updateSharingState(false);
|
||||
}));
|
||||
|
||||
this._inputDisposables.add(this._model.onDidChangeZoom(() => {
|
||||
this.updateZoomContext();
|
||||
}));
|
||||
|
||||
// Initialize UI state and context keys from model
|
||||
this.updateNavigationState({
|
||||
url: this._model.url,
|
||||
@@ -505,7 +516,7 @@ export class BrowserEditor extends EditorPane {
|
||||
this.checkOverlays();
|
||||
}));
|
||||
|
||||
// Listen for zoom level changes and update browser view zoom factor
|
||||
// Listen for workbench zoom level changes and update browser view placeholder screenshot's zoom factor
|
||||
this._inputDisposables.add(onDidChangeZoomLevel(targetWindowId => {
|
||||
if (targetWindowId === this.window.vscodeWindowId) {
|
||||
// Update CSS variable for size calculations
|
||||
@@ -717,6 +728,25 @@ export class BrowserEditor extends EditorPane {
|
||||
return this._model?.clearStorage();
|
||||
}
|
||||
|
||||
async zoomIn(): Promise<void> {
|
||||
await this._model?.zoomIn();
|
||||
}
|
||||
|
||||
async zoomOut(): Promise<void> {
|
||||
await this._model?.zoomOut();
|
||||
}
|
||||
|
||||
async resetZoom(): Promise<void> {
|
||||
await this._model?.resetZoom();
|
||||
}
|
||||
|
||||
private updateZoomContext(): void {
|
||||
if (this._model) {
|
||||
this._canZoomInContext.set(this._model.canZoomIn);
|
||||
this._canZoomOutContext.set(this._model.canZoomOut);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the find widget, optionally pre-populated with selected text from the browser view
|
||||
*/
|
||||
@@ -1263,6 +1293,8 @@ export class BrowserEditor extends EditorPane {
|
||||
this._storageScopeContext.reset();
|
||||
this._devToolsOpenContext.reset();
|
||||
this._elementSelectionActiveContext.reset();
|
||||
this._canZoomInContext.reset();
|
||||
this._canZoomOutContext.reset();
|
||||
|
||||
this._navigationBar.clear();
|
||||
this.setBackgroundImage(undefined);
|
||||
|
||||
@@ -20,13 +20,17 @@ import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase
|
||||
import { Schemas } from '../../../../base/common/network.js';
|
||||
import { IBrowserViewWorkbenchService } from '../common/browserView.js';
|
||||
import { BrowserViewWorkbenchService } from './browserViewWorkbenchService.js';
|
||||
import { BrowserViewStorageScope } from '../../../../platform/browserView/common/browserView.js';
|
||||
import { BrowserZoomService, IBrowserZoomService, MATCH_WINDOW_ZOOM_LABEL } from '../common/browserZoomService.js';
|
||||
import { browserZoomFactors, BrowserViewStorageScope } from '../../../../platform/browserView/common/browserView.js';
|
||||
import { IExternalOpener, IOpenerService } from '../../../../platform/opener/common/opener.js';
|
||||
import { isLocalhostAuthority } from '../../../../platform/url/common/trustedDomains.js';
|
||||
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
|
||||
import { IEditorService } from '../../../services/editor/common/editorService.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { PolicyCategory } from '../../../../base/common/policy.js';
|
||||
import { getZoomLevel, onDidChangeZoomLevel } from '../../../../base/browser/browser.js';
|
||||
import { mainWindow } from '../../../../base/browser/window.js';
|
||||
import { zoomLevelToZoomFactor } from '../../../../platform/window/common/window.js';
|
||||
import { CancellationToken } from '../../../../base/common/cancellation.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
|
||||
@@ -145,7 +149,26 @@ class LocalhostLinkOpenerContribution extends Disposable implements IWorkbenchCo
|
||||
|
||||
registerWorkbenchContribution2(LocalhostLinkOpenerContribution.ID, LocalhostLinkOpenerContribution, WorkbenchPhase.BlockStartup);
|
||||
|
||||
/**
|
||||
* Bridges the application's UI zoom level changes into IBrowserZoomService so that
|
||||
* views using the 'Match Window' default zoom level stay in sync.
|
||||
*/
|
||||
class WindowZoomSynchronizer extends Disposable implements IWorkbenchContribution {
|
||||
static readonly ID = 'workbench.contrib.browserView.windowZoomSynchronizer';
|
||||
|
||||
constructor(@IBrowserZoomService browserZoomService: IBrowserZoomService) {
|
||||
super();
|
||||
browserZoomService.notifyWindowZoomChanged(zoomLevelToZoomFactor(getZoomLevel(mainWindow)));
|
||||
this._register(onDidChangeZoomLevel(() => {
|
||||
browserZoomService.notifyWindowZoomChanged(zoomLevelToZoomFactor(getZoomLevel(mainWindow)));
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
registerWorkbenchContribution2(WindowZoomSynchronizer.ID, WindowZoomSynchronizer, WorkbenchPhase.Eventually);
|
||||
|
||||
registerSingleton(IBrowserViewWorkbenchService, BrowserViewWorkbenchService, InstantiationType.Delayed);
|
||||
registerSingleton(IBrowserZoomService, BrowserZoomService, InstantiationType.Delayed);
|
||||
|
||||
Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).registerConfiguration({
|
||||
...workbenchConfigurationNodeBase,
|
||||
@@ -180,6 +203,24 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).regis
|
||||
},
|
||||
}
|
||||
},
|
||||
'workbench.browser.zoom.pageZoom': {
|
||||
type: 'string',
|
||||
enum: [MATCH_WINDOW_ZOOM_LABEL, ...browserZoomFactors.map(f => `${Math.round(f * 100)}%`)],
|
||||
markdownEnumDescriptions: [
|
||||
localize(
|
||||
{ comment: ['This is the description for a setting enum value.'], key: 'browser.defaultZoomLevel.matchWindow' },
|
||||
'Matches the application\'s current UI zoom level.'
|
||||
),
|
||||
...browserZoomFactors.map(() => ''),
|
||||
],
|
||||
default: MATCH_WINDOW_ZOOM_LABEL,
|
||||
markdownDescription: localize(
|
||||
{ comment: ['This is the description for a setting.'], key: 'browser.pageZoom' },
|
||||
'Default zoom level for all sites in the Integrated Browser.'
|
||||
),
|
||||
// Zoom can change from machine to machine, so we don't need the workspace-level nor syncing that WINDOW has.
|
||||
scope: ConfigurationScope.MACHINE
|
||||
},
|
||||
'workbench.browser.dataStorage': {
|
||||
type: 'string',
|
||||
enum: [
|
||||
|
||||
@@ -11,7 +11,7 @@ import { KeybindingWeight } from '../../../../platform/keybinding/common/keybind
|
||||
import { KeyMod, KeyCode } from '../../../../base/common/keyCodes.js';
|
||||
import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js';
|
||||
import { Codicon } from '../../../../base/common/codicons.js';
|
||||
import { BrowserEditor, CONTEXT_BROWSER_CAN_GO_BACK, CONTEXT_BROWSER_CAN_GO_FORWARD, CONTEXT_BROWSER_DEVTOOLS_OPEN, CONTEXT_BROWSER_FOCUSED, CONTEXT_BROWSER_HAS_ERROR, CONTEXT_BROWSER_HAS_URL, CONTEXT_BROWSER_STORAGE_SCOPE, CONTEXT_BROWSER_ELEMENT_SELECTION_ACTIVE, CONTEXT_BROWSER_FIND_WIDGET_FOCUSED, CONTEXT_BROWSER_FIND_WIDGET_VISIBLE } from './browserEditor.js';
|
||||
import { BrowserEditor, CONTEXT_BROWSER_CAN_GO_BACK, CONTEXT_BROWSER_CAN_GO_FORWARD, CONTEXT_BROWSER_CAN_ZOOM_IN, CONTEXT_BROWSER_CAN_ZOOM_OUT, CONTEXT_BROWSER_DEVTOOLS_OPEN, CONTEXT_BROWSER_FOCUSED, CONTEXT_BROWSER_HAS_ERROR, CONTEXT_BROWSER_HAS_URL, CONTEXT_BROWSER_STORAGE_SCOPE, CONTEXT_BROWSER_ELEMENT_SELECTION_ACTIVE, CONTEXT_BROWSER_FIND_WIDGET_FOCUSED, CONTEXT_BROWSER_FIND_WIDGET_VISIBLE } from './browserEditor.js';
|
||||
import { BrowserViewUri } from '../../../../platform/browserView/common/browserViewUri.js';
|
||||
import { IBrowserViewWorkbenchService } from '../common/browserView.js';
|
||||
import { BrowserViewCommandId, BrowserViewStorageScope } from '../../../../platform/browserView/common/browserView.js';
|
||||
@@ -26,8 +26,9 @@ const BROWSER_EDITOR_ACTIVE = ContextKeyExpr.equals('activeEditor', BrowserEdito
|
||||
|
||||
const BrowserCategory = localize2('browserCategory', "Browser");
|
||||
const ActionGroupTabs = '1_tabs';
|
||||
const ActionGroupPage = '2_page';
|
||||
const ActionGroupSettings = '3_settings';
|
||||
const ActionGroupZoom = '2_zoom';
|
||||
const ActionGroupPage = '3_page';
|
||||
const ActionGroupSettings = '4_settings';
|
||||
|
||||
interface IOpenBrowserOptions {
|
||||
url?: string;
|
||||
@@ -445,7 +446,7 @@ class ClearEphemeralBrowserStorageAction extends Action2 {
|
||||
precondition: ContextKeyExpr.equals(CONTEXT_BROWSER_STORAGE_SCOPE.key, BrowserViewStorageScope.Ephemeral),
|
||||
menu: {
|
||||
id: MenuId.BrowserActionsToolbar,
|
||||
group: '3_settings',
|
||||
group: ActionGroupSettings,
|
||||
order: 1,
|
||||
when: ContextKeyExpr.equals(CONTEXT_BROWSER_STORAGE_SCOPE.key, BrowserViewStorageScope.Ephemeral)
|
||||
}
|
||||
@@ -483,6 +484,106 @@ class OpenBrowserSettingsAction extends Action2 {
|
||||
}
|
||||
}
|
||||
|
||||
// Zoom actions
|
||||
|
||||
class ZoomInAction extends Action2 {
|
||||
static readonly ID = 'workbench.action.browser.zoomIn';
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: ZoomInAction.ID,
|
||||
title: localize2('browser.zoomInAction', 'Zoom In'),
|
||||
category: BrowserCategory,
|
||||
icon: Codicon.zoomIn,
|
||||
f1: true,
|
||||
precondition: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_HAS_URL, CONTEXT_BROWSER_HAS_ERROR.negate()),
|
||||
menu: {
|
||||
id: MenuId.BrowserActionsToolbar,
|
||||
group: ActionGroupZoom,
|
||||
order: 1,
|
||||
when: CONTEXT_BROWSER_CAN_ZOOM_IN,
|
||||
},
|
||||
keybinding: {
|
||||
when: CONTEXT_BROWSER_FOCUSED,
|
||||
weight: KeybindingWeight.WorkbenchContrib + 75,
|
||||
primary: KeyMod.CtrlCmd | KeyCode.Equal,
|
||||
secondary: [KeyMod.CtrlCmd | KeyCode.NumpadAdd],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor, browserEditor = accessor.get(IEditorService).activeEditorPane): Promise<void> {
|
||||
if (browserEditor instanceof BrowserEditor) {
|
||||
await browserEditor.zoomIn();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ZoomOutAction extends Action2 {
|
||||
static readonly ID = 'workbench.action.browser.zoomOut';
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: ZoomOutAction.ID,
|
||||
title: localize2('browser.zoomOutAction', 'Zoom Out'),
|
||||
category: BrowserCategory,
|
||||
icon: Codicon.zoomOut,
|
||||
f1: true,
|
||||
precondition: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_HAS_URL, CONTEXT_BROWSER_HAS_ERROR.negate()),
|
||||
menu: {
|
||||
id: MenuId.BrowserActionsToolbar,
|
||||
group: ActionGroupZoom,
|
||||
order: 2,
|
||||
when: CONTEXT_BROWSER_CAN_ZOOM_OUT,
|
||||
},
|
||||
keybinding: {
|
||||
when: CONTEXT_BROWSER_FOCUSED,
|
||||
weight: KeybindingWeight.WorkbenchContrib + 75,
|
||||
primary: KeyMod.CtrlCmd | KeyCode.Minus,
|
||||
secondary: [KeyMod.CtrlCmd | KeyCode.NumpadSubtract],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor, browserEditor = accessor.get(IEditorService).activeEditorPane): Promise<void> {
|
||||
if (browserEditor instanceof BrowserEditor) {
|
||||
await browserEditor.zoomOut();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ResetZoomAction extends Action2 {
|
||||
static readonly ID = 'workbench.action.browser.resetZoom';
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: ResetZoomAction.ID,
|
||||
title: localize2('browser.resetZoomAction', 'Reset Zoom'),
|
||||
category: BrowserCategory,
|
||||
icon: Codicon.screenNormal,
|
||||
f1: true,
|
||||
precondition: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_HAS_URL, CONTEXT_BROWSER_HAS_ERROR.negate()),
|
||||
menu: {
|
||||
id: MenuId.BrowserActionsToolbar,
|
||||
group: ActionGroupZoom,
|
||||
order: 3,
|
||||
},
|
||||
keybinding: {
|
||||
when: CONTEXT_BROWSER_FOCUSED,
|
||||
weight: KeybindingWeight.WorkbenchContrib + 75,
|
||||
// We use Numpad0 and not Digit0 here to match the workbench zoom reset keybinding, and to avoid conflicts with keybinding to focus sidebar.
|
||||
primary: KeyMod.CtrlCmd | KeyCode.Numpad0,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor, browserEditor = accessor.get(IEditorService).activeEditorPane): Promise<void> {
|
||||
if (browserEditor instanceof BrowserEditor) {
|
||||
await browserEditor.resetZoom();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find actions
|
||||
|
||||
class ShowBrowserFindAction extends Action2 {
|
||||
@@ -617,6 +718,9 @@ registerAction2(ClearGlobalBrowserStorageAction);
|
||||
registerAction2(ClearWorkspaceBrowserStorageAction);
|
||||
registerAction2(ClearEphemeralBrowserStorageAction);
|
||||
registerAction2(OpenBrowserSettingsAction);
|
||||
registerAction2(ZoomInAction);
|
||||
registerAction2(ZoomOutAction);
|
||||
registerAction2(ResetZoomAction);
|
||||
registerAction2(ShowBrowserFindAction);
|
||||
registerAction2(HideBrowserFindAction);
|
||||
registerAction2(BrowserFindNextAction);
|
||||
|
||||
Reference in New Issue
Block a user