diff --git a/packages/plugin-ext/src/main/browser/menus/plugin-menu-command-adapter.ts b/packages/plugin-ext/src/main/browser/menus/plugin-menu-command-adapter.ts index f0cfaba0a22ff..8dab5d8a80068 100644 --- a/packages/plugin-ext/src/main/browser/menus/plugin-menu-command-adapter.ts +++ b/packages/plugin-ext/src/main/browser/menus/plugin-menu-command-adapter.ts @@ -103,6 +103,7 @@ export class PluginMenuCommandAdapter implements MenuCommandAdapter { ['timeline/item/context', (...args) => this.toTimelineArgs(...args)], ['view/item/context', (...args) => this.toTreeArgs(...args)], ['view/title', noArgs], + ['webview/context', noArgs] ]).forEach(([contributionPoint, adapter]) => { if (adapter) { const paths = codeToTheiaMappings.get(contributionPoint); diff --git a/packages/plugin-ext/src/main/browser/menus/vscode-theia-menu-mappings.ts b/packages/plugin-ext/src/main/browser/menus/vscode-theia-menu-mappings.ts index 3b15628359f8a..c7149296b9b43 100644 --- a/packages/plugin-ext/src/main/browser/menus/vscode-theia-menu-mappings.ts +++ b/packages/plugin-ext/src/main/browser/menus/vscode-theia-menu-mappings.ts @@ -29,7 +29,7 @@ import { ScmTreeWidget } from '@theia/scm/lib/browser/scm-tree-widget'; import { TIMELINE_ITEM_CONTEXT_MENU } from '@theia/timeline/lib/browser/timeline-tree-widget'; import { COMMENT_CONTEXT, COMMENT_THREAD_CONTEXT, COMMENT_TITLE } from '../comments/comment-thread-widget'; import { VIEW_ITEM_CONTEXT_MENU } from '../view/tree-view-widget'; -import { WebviewWidget } from '../webview/webview'; +import { WEBVIEW_CONTEXT_MENU, WebviewWidget } from '../webview/webview'; import { EDITOR_LINENUMBER_CONTEXT_MENU } from '@theia/editor/lib/browser/editor-linenumber-contribution'; import { TEST_VIEW_CONTEXT_MENU } from '@theia/test/lib/browser/view/test-view-contribution'; @@ -58,7 +58,8 @@ export const implementedVSCodeContributionPoints = [ 'timeline/item/context', 'testing/item/context', 'view/item/context', - 'view/title' + 'view/title', + 'webview/context' ] as const; export type ContributionPoint = (typeof implementedVSCodeContributionPoints)[number]; @@ -85,6 +86,7 @@ export const codeToTheiaMappings = new Map([ ['timeline/item/context', [TIMELINE_ITEM_CONTEXT_MENU]], ['view/item/context', [VIEW_ITEM_CONTEXT_MENU]], ['view/title', [PLUGIN_VIEW_TITLE_MENU]], + ['webview/context', [WEBVIEW_CONTEXT_MENU]] ]); type CodeEditorWidget = EditorWidget | WebviewWidget; diff --git a/packages/plugin-ext/src/main/browser/webview/pre/main.js b/packages/plugin-ext/src/main/browser/webview/pre/main.js index 59105add0f051..004f20920b4fe 100644 --- a/packages/plugin-ext/src/main/browser/webview/pre/main.js +++ b/packages/plugin-ext/src/main/browser/webview/pre/main.js @@ -339,10 +339,35 @@ delete window.frameElement; clientY: e.clientY, ctrlKey: e.ctrlKey, metaKey: e.metaKey, - shiftKey: e.shiftKey + shiftKey: e.shiftKey, + // @ts-ignore the dataset should exist if the target is an element }); }; + const handleContextMenu = (e) => { + if (e.defaultPrevented) { + return; + } + + e.preventDefault(); + + host.postMessage('did-context-menu', { + clientX: e.clientX, + clientY: e.clientY, + context: findVscodeContext(e.target) + }); + }; + + function findVscodeContext(node) { + if (node) { + if (node.dataset?.vscodeContext) { + return JSON.parse(node.dataset.vscodeContext); + } + return findVscodeContext(node.parentElement); + } + return {}; + } + function preventDefaultBrowserHotkeys(e) { var isOSX = navigator.platform.toUpperCase().indexOf('MAC') >= 0; @@ -602,7 +627,7 @@ delete window.frameElement; newFrame.contentWindow.addEventListener('keydown', handleInnerKeydown); newFrame.contentWindow.addEventListener('mousedown', handleInnerMousedown); newFrame.contentWindow.addEventListener('mouseup', handleInnerMouseup); - newFrame.contentWindow.addEventListener('contextmenu', e => e.preventDefault()); + newFrame.contentWindow.addEventListener('contextmenu', handleContextMenu); if (host.onIframeLoaded) { host.onIframeLoaded(newFrame); diff --git a/packages/plugin-ext/src/main/browser/webview/webview.ts b/packages/plugin-ext/src/main/browser/webview/webview.ts index f7956a6ee1e71..2d35997b5cd97 100644 --- a/packages/plugin-ext/src/main/browser/webview/webview.ts +++ b/packages/plugin-ext/src/main/browser/webview/webview.ts @@ -51,10 +51,16 @@ import { BinaryBufferReadableStream } from '@theia/core/lib/common/buffer'; import { ViewColumn } from '../../../plugin/types-impl'; import { ExtractableWidget } from '@theia/core/lib/browser/widgets/extractable-widget'; import { BadgeWidget } from '@theia/core/lib/browser/view-container'; +import { MenuPath } from '@theia/core'; +import { ContextMenuRenderer } from '@theia/core/lib/browser'; +import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; +import { PluginViewWidget } from '../view/plugin-view-widget'; // Style from core const TRANSPARENT_OVERLAY_STYLE = 'theia-transparent-overlay'; +export const WEBVIEW_CONTEXT_MENU: MenuPath = ['webview-context-menu']; + /* eslint-disable @typescript-eslint/no-explicit-any */ export const enum WebviewMessageChannels { @@ -70,7 +76,8 @@ export const enum WebviewMessageChannels { didKeydown = 'did-keydown', didMouseDown = 'did-mousedown', didMouseUp = 'did-mouseup', - onconsole = 'onconsole' + onconsole = 'onconsole', + didcontextmenu = 'did-context-menu' } export interface WebviewContentOptions { @@ -152,6 +159,12 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget, Extract @inject(WebviewResourceCache) protected readonly resourceCache: WebviewResourceCache; + @inject(ContextMenuRenderer) + protected readonly contextMenuRenderer: ContextMenuRenderer; + + @inject(ContextKeyService) + protected readonly contextKeyService: ContextKeyService; + viewState: WebviewPanelViewState = { visible: false, active: false, @@ -357,6 +370,10 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget, Extract this.dispatchMouseEvent('mouseup', data); })); + this.toHide.push(this.on(WebviewMessageChannels.didcontextmenu, (event: { clientX: number, clientY: number, context: any }) => { + this.handleContextMenu(event); + })); + this.style(); this.toHide.push(this.themeDataProvider.onDidChangeThemeData(() => this.style())); @@ -380,6 +397,20 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget, Extract })); } + handleContextMenu(event: { clientX: number, clientY: number, context: any }): void { + const domRect = this.node.getBoundingClientRect(); + this.contextKeyService.with(this.parent instanceof PluginViewWidget ? + { webviewId: this.parent.options.viewId, ...event.context } : {}, + () => { + this.contextMenuRenderer.render({ + menuPath: WEBVIEW_CONTEXT_MENU, + anchor: { + x: domRect.x + event.clientX, y: domRect.y + event.clientY + } + }); + }); + } + protected async getRedirect(url: string): Promise { const uri = new URI(url); const localhost = this.externalUriService.parseLocalhost(uri);