diff --git a/src/vs/base/browser/contextmenu.ts b/src/vs/base/browser/contextmenu.ts index 8f67c23030664f..bbf7a2dbb43622 100644 --- a/src/vs/base/browser/contextmenu.ts +++ b/src/vs/base/browser/contextmenu.ts @@ -17,7 +17,7 @@ export interface IContextMenuEvent { } export interface IContextMenuDelegate { - getAnchor(): HTMLElement | { x: number; y: number; width?: number; height?: number }; + getAnchor(): HTMLElement | { x: number; y: number; width?: number; height?: number; isCurrentCursor?: boolean }; getActions(): readonly IAction[]; getCheckedActionsRepresentation?(action: IAction): 'radio' | 'checkbox'; getActionViewItem?(action: IAction, options: IActionViewItemOptions): IActionViewItem | undefined; diff --git a/src/vs/base/browser/ui/contextview/contextview.ts b/src/vs/base/browser/ui/contextview/contextview.ts index 04931cca3f09d2..1a1572e4b2981c 100644 --- a/src/vs/base/browser/ui/contextview/contextview.ts +++ b/src/vs/base/browser/ui/contextview/contextview.ts @@ -21,6 +21,7 @@ export interface IAnchor { y: number; width?: number; height?: number; + isCurrentCursor?: boolean; } export const enum AnchorAlignment { diff --git a/src/vs/base/browser/ui/list/list.ts b/src/vs/base/browser/ui/list/list.ts index f776ba7ecb2ea1..3b8ab29693cf10 100644 --- a/src/vs/base/browser/ui/list/list.ts +++ b/src/vs/base/browser/ui/list/list.ts @@ -61,7 +61,7 @@ export interface IListContextMenuEvent { readonly browserEvent: UIEvent; readonly element: T | undefined; readonly index: number | undefined; - readonly anchor: HTMLElement | { readonly x: number; readonly y: number }; + readonly anchor: HTMLElement | { readonly x: number; readonly y: number; readonly isCurrentCursor?: boolean }; } export interface IIdentityProvider { diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index ae34c857e7154b..9d89a475113096 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -1353,7 +1353,7 @@ export class List implements ISpliceable, IDisposable { const fromMouse = this.disposables.add(Event.chain(this.view.onContextMenu)) .filter(_ => !didJustPressContextMenuKey) - .map(({ element, index, browserEvent }) => ({ element, index, anchor: { x: browserEvent.pageX + 1, y: browserEvent.pageY }, browserEvent })) + .map(({ element, index, browserEvent }) => ({ element, index, anchor: { x: browserEvent.pageX + 1, y: browserEvent.pageY, isCurrentCursor: true }, browserEvent })) .event; return Event.any>(fromKeyDown, fromKeyUp, fromMouse); diff --git a/src/vs/base/browser/ui/tree/tree.ts b/src/vs/base/browser/ui/tree/tree.ts index 94e8650f2defa7..c6991066cb044b 100644 --- a/src/vs/base/browser/ui/tree/tree.ts +++ b/src/vs/base/browser/ui/tree/tree.ts @@ -176,7 +176,7 @@ export interface ITreeMouseEvent { export interface ITreeContextMenuEvent { readonly browserEvent: UIEvent; readonly element: T | null; - readonly anchor: HTMLElement | { readonly x: number; readonly y: number }; + readonly anchor: HTMLElement | { readonly x: number; readonly y: number; readonly isCurrentCursor?: boolean }; } export interface ITreeNavigator { diff --git a/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts b/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts index 4eef122381b1f3..a92fc523e85876 100644 --- a/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts +++ b/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts @@ -103,7 +103,7 @@ export class ContextMenuController implements IEditorContribution { e.event.stopPropagation(); if (e.target.type === MouseTargetType.SCROLLBAR) { - return this._showScrollbarContextMenu({ x: e.event.posx - 1, width: 2, y: e.event.posy - 1, height: 2 }); + return this._showScrollbarContextMenu({ x: e.event.posx - 1, width: 2, y: e.event.posy - 1, height: 2, isCurrentCursor: true }); } if (e.target.type !== MouseTargetType.CONTENT_TEXT && e.target.type !== MouseTargetType.CONTENT_EMPTY && e.target.type !== MouseTargetType.TEXTAREA) { @@ -131,7 +131,7 @@ export class ContextMenuController implements IEditorContribution { // Unless the user triggerd the context menu through Shift+F10, use the mouse position as menu position let anchor: IAnchor | null = null; if (e.target.type !== MouseTargetType.TEXTAREA) { - anchor = { x: e.event.posx - 1, width: 2, y: e.event.posy - 1, height: 2 }; + anchor = { x: e.event.posx - 1, width: 2, y: e.event.posy - 1, height: 2, isCurrentCursor: true }; } // Show the context menu diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts index 6fbd655eb67785..13057ba6c41d7b 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts @@ -152,7 +152,8 @@ abstract class AbstractGlobalActivityActionViewItem extends ActivityActionViewIt const event = new StandardMouseEvent(e); const anchor = { x: event.posx, - y: event.posy + y: event.posy, + isCurrentCursor: true, }; this.contextMenuService.showContextMenu({ diff --git a/src/vs/workbench/browser/parts/compositeBar.ts b/src/vs/workbench/browser/parts/compositeBar.ts index 366941297647c0..a3c7c0d04125f5 100644 --- a/src/vs/workbench/browser/parts/compositeBar.ts +++ b/src/vs/workbench/browser/parts/compositeBar.ts @@ -656,7 +656,7 @@ export class CompositeBar extends Widget implements ICompositeBar { const event = new StandardMouseEvent(e); this.contextMenuService.showContextMenu({ - getAnchor: () => { return { x: event.posx, y: event.posy }; }, + getAnchor: () => { return { x: event.posx, y: event.posy, isCurrentCursor: true }; }, getActions: () => this.getContextMenuActions(e) }); } diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 740e037f6d408d..a61064ccc62f1d 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -374,10 +374,10 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } // Find target anchor - let anchor: HTMLElement | { x: number; y: number } = this.element; + let anchor: HTMLElement | { x: number; y: number; isCurrentCursor?: boolean } = this.element; if (e instanceof MouseEvent) { const event = new StandardMouseEvent(e); - anchor = { x: event.posx, y: event.posy }; + anchor = { x: event.posx, y: event.posy, isCurrentCursor: true }; } // Show it diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index ecb998d62740a9..d6160b61470d21 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -454,10 +454,10 @@ export class TabsTitleControl extends TitleControl { EventHelper.stop(e); // Find target anchor - let anchor: HTMLElement | { x: number; y: number } = tabsContainer; + let anchor: HTMLElement | { x: number; y: number; isCurrentCursor?: boolean } = tabsContainer; if (e instanceof MouseEvent) { const event = new StandardMouseEvent(e); - anchor = { x: event.posx, y: event.posy }; + anchor = { x: event.posx, y: event.posy, isCurrentCursor: true }; } // Show it diff --git a/src/vs/workbench/browser/parts/editor/titleControl.ts b/src/vs/workbench/browser/parts/editor/titleControl.ts index 5d77c66dfc5e0b..4f3d6dfbe9b57f 100644 --- a/src/vs/workbench/browser/parts/editor/titleControl.ts +++ b/src/vs/workbench/browser/parts/editor/titleControl.ts @@ -379,10 +379,10 @@ export abstract class TitleControl extends Themable { applyAvailableEditorIds(this.editorAvailableEditorIds, editor, this.editorResolverService); // Find target anchor - let anchor: HTMLElement | { x: number; y: number } = node; + let anchor: HTMLElement | { x: number; y: number; isCurrentCursor?: boolean } = node; if (e instanceof MouseEvent) { const event = new StandardMouseEvent(e); - anchor = { x: event.posx, y: event.posy }; + anchor = { x: event.posx, y: event.posy, isCurrentCursor: true }; } // Show it diff --git a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts index 67a88f8eb1784c..466fc731cdcf0e 100644 --- a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts +++ b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts @@ -282,7 +282,7 @@ export class SidebarPart extends CompositePart implements IPaneCo if (activeViewlet) { const contextMenuActions = activeViewlet ? activeViewlet.getContextMenuActions() : []; if (contextMenuActions.length) { - const anchor: { x: number; y: number } = { x: event.posx, y: event.posy }; + const anchor: { x: number; y: number; isCurrentCursor?: boolean } = { x: event.posx, y: event.posy, isCurrentCursor: true }; this.contextMenuService.showContextMenu({ getAnchor: () => anchor, getActions: () => contextMenuActions.slice(), diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index 66e1c66e166e50..491d1371c2c96f 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -453,7 +453,7 @@ export class StatusbarPart extends Part implements IStatusbarService { let actions: IAction[] | undefined = undefined; this.contextMenuService.showContextMenu({ - getAnchor: () => ({ x: event.posx, y: event.posy }), + getAnchor: () => ({ x: event.posx, y: event.posy, isCurrentCursor: true }), getActions: () => { actions = this.getContextMenuActions(event); diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 64a6afb239d5e2..9f8446779475ae 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -399,7 +399,7 @@ export class TitlebarPart extends Part implements ITitleService { protected onContextMenu(e: MouseEvent, menuId: MenuId): void { // Find target anchor const event = new StandardMouseEvent(e); - const anchor = { x: event.posx, y: event.posy }; + const anchor = { x: event.posx, y: event.posy, isCurrentCursor: true }; // Show it this.contextMenuService.showContextMenu({ diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index ff766b9acb8150..f06a2445b3ab78 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -577,7 +577,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { event.stopPropagation(); event.preventDefault(); - const anchor: { x: number; y: number } = { x: event.posx, y: event.posy }; + const anchor: { x: number; y: number; isCurrentCursor?: boolean } = { x: event.posx, y: event.posy, isCurrentCursor: true }; this.contextMenuService.showContextMenu({ getAnchor: () => anchor, getActions: () => this.menuActions?.getContextMenuActions() ?? [] @@ -743,7 +743,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { const actions: IAction[] = viewPane.menuActions.getContextMenuActions(); - const anchor: { x: number; y: number } = { x: event.posx, y: event.posy }; + const anchor: { x: number; y: number; isCurrentCursor?: boolean } = { x: event.posx, y: event.posy, isCurrentCursor: true }; this.contextMenuService.showContextMenu({ getAnchor: () => anchor, getActions: () => actions diff --git a/src/vs/workbench/contrib/codeEditor/browser/editorLineNumberMenu.ts b/src/vs/workbench/contrib/codeEditor/browser/editorLineNumberMenu.ts index 43eea66d38cdd9..c5344be01d0195 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/editorLineNumberMenu.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/editorLineNumberMenu.ts @@ -77,7 +77,7 @@ export class EditorLineNumberContextMenu extends Disposable implements IEditorCo return; } - const anchor = { x: e.event.posx, y: e.event.posy }; + const anchor = { x: e.event.posx, y: e.event.posy, isCurrentCursor: true }; const lineNumber = e.target.position.lineNumber; const contextKeyService = this.contextKeyService.createOverlay([['editorLineNumber', lineNumber]]); diff --git a/src/vs/workbench/contrib/comments/browser/commentsController.ts b/src/vs/workbench/contrib/comments/browser/commentsController.ts index 7e6fe3265cd268..82ccf2d1851de3 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsController.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsController.ts @@ -829,7 +829,7 @@ export class CommentController implements IEditorContribution { if (newCommentInfos.length > 1) { if (e && range) { - const anchor = { x: e.event.posx, y: e.event.posy }; + const anchor = { x: e.event.posx, y: e.event.posy, isCurrentCursor: true }; this.contextMenuService.showContextMenu({ getAnchor: () => anchor, diff --git a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts index 0fc7574fbeaa23..02cf8343528016 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts @@ -728,7 +728,7 @@ class InlineBreakpointWidget implements IContentWidget, IDisposable { })); this.toDispose.push(dom.addDisposableListener(this.domNode, dom.EventType.CONTEXT_MENU, e => { const event = new StandardMouseEvent(e); - const anchor = { x: event.posx, y: event.posy }; + const anchor = { x: event.posx, y: event.posy, isCurrentCursor: true }; const actions = this.getContextMenuActions(); this.contextMenuService.showContextMenu({ getAnchor: () => anchor, diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts index 7105545524c249..1c3286bb7a40bd 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts @@ -354,7 +354,7 @@ class EditSettingRenderer extends Disposable { private onEditSettingClicked(editPreferenceWidget: EditPreferenceWidget, e: IEditorMouseEvent): void { EventHelper.stop(e.event, true); - const anchor = { x: e.event.posx, y: e.event.posy }; + const anchor = { x: e.event.posx, y: e.event.posy, isCurrentCursor: true }; const actions = this.getSettings(editPreferenceWidget.getLine()).length === 1 ? this.getActions(editPreferenceWidget.preferences[0], this.getConfigurationsMap()[editPreferenceWidget.preferences[0].key]) : editPreferenceWidget.preferences.map(setting => new SubmenuAction(`preferences.submenu.${setting.key}`, setting.key, this.getActions(setting, this.getConfigurationsMap()[setting.key]))); this.contextMenuService.showContextMenu({ diff --git a/src/vs/workbench/contrib/terminal/browser/terminalContextMenu.ts b/src/vs/workbench/contrib/terminal/browser/terminalContextMenu.ts index d847bcdff562fb..e13450cc771aa7 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalContextMenu.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalContextMenu.ts @@ -12,7 +12,7 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView export function openContextMenu(event: MouseEvent, parent: HTMLElement, menu: IMenu, contextMenuService: IContextMenuService, extraActions?: IAction[]): void { const standardEvent = new StandardMouseEvent(event); - const anchor: { x: number; y: number } = { x: standardEvent.posx, y: standardEvent.posy }; + const anchor: { x: number; y: number; isCurrentCursor?: boolean } = { x: standardEvent.posx, y: standardEvent.posy, isCurrentCursor: true }; const actions: IAction[] = []; createAndFillInContextMenuActions(menu, undefined, actions); diff --git a/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts b/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts index 073f4766e9f515..fe19b03cb180f7 100644 --- a/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts +++ b/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts @@ -103,8 +103,8 @@ class NativeContextMenuService extends Disposable implements IContextMenuService const menu = this.createMenu(delegate, actions, onHide); const anchor = delegate.getAnchor(); - let x: number; - let y: number; + let x: number | undefined; + let y: number | undefined; let zoom = getZoomFactor(); if (dom.isHTMLElement(anchor)) { @@ -156,17 +156,28 @@ class NativeContextMenuService extends Disposable implements IContextMenuService y += 4 / zoom; } } else { - const pos: { x: number; y: number } = anchor; + const pos = anchor; x = pos.x + 1; /* prevent first item from being selected automatically under mouse */ y = pos.y; + if (pos.isCurrentCursor) { + // If (x, y) indicates the current cursor position, we set + // both to undefined and let Electron figure out the accurate value. + // (https://www.electronjs.org/docs/latest/api/menu#menupopupoptions) + // This avoids the numeric loss when applying *zoom and Math.floor, + // which leads to the closest being accidentally clicked. + // https://github.com/microsoft/vscode/issues/113175 + x = y = undefined; + } } - x *= zoom; - y *= zoom; + if (x !== undefined && y !== undefined) { + x = Math.floor(x * zoom); + y = Math.floor(y * zoom); + } popup(menu, { - x: Math.floor(x), - y: Math.floor(y), + x: x, + y: y, positioningItem: delegate.autoSelectFirstItem ? 0 : undefined, }, () => onHide());