feat(desktop-client): add web context menu for bound and workbench windows
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -72,6 +72,7 @@ import {
|
||||
} from './session-registry'
|
||||
import { upsertDesktopAccount } from './transport/api-client'
|
||||
import { STANDARD_USER_AGENT } from './user-agent'
|
||||
import { attachWebContextMenu } from './web-context-menu'
|
||||
import {
|
||||
getWorkbenchWindowWebContents,
|
||||
isWorkbenchWindowReadyForDetection,
|
||||
@@ -4191,6 +4192,7 @@ function createBoundWindow(
|
||||
})
|
||||
|
||||
window.webContents.setUserAgent(STANDARD_USER_AGENT)
|
||||
attachWebContextMenu(window.webContents)
|
||||
attachWindowDiagnostics(window, title)
|
||||
|
||||
window.webContents.setWindowOpenHandler(({ url }) => {
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
import type {
|
||||
ContextMenuParams,
|
||||
MenuItemConstructorOptions,
|
||||
WebContents,
|
||||
} from 'electron/main'
|
||||
import { clipboard, shell } from 'electron'
|
||||
import { Menu } from 'electron/main'
|
||||
|
||||
type WebContextMenuOptions = {
|
||||
allowNavigation?: boolean
|
||||
}
|
||||
|
||||
function isRemoteURL(url: string | undefined): boolean {
|
||||
return /^https?:\/\//i.test(url ?? '')
|
||||
}
|
||||
|
||||
function hasVisibleText(value: string | undefined): boolean {
|
||||
return Boolean(value?.trim())
|
||||
}
|
||||
|
||||
function pushSeparator(items: MenuItemConstructorOptions[]): void {
|
||||
if (items.length > 0 && items.at(-1)?.type !== 'separator') {
|
||||
items.push({ type: 'separator' })
|
||||
}
|
||||
}
|
||||
|
||||
function trimTrailingSeparators(items: MenuItemConstructorOptions[]): MenuItemConstructorOptions[] {
|
||||
while (items.at(-1)?.type === 'separator') {
|
||||
items.pop()
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
function writeClipboardText(text: string): void {
|
||||
if (text) {
|
||||
clipboard.writeText(text)
|
||||
}
|
||||
}
|
||||
|
||||
function buildContextMenuTemplate(
|
||||
webContents: WebContents,
|
||||
params: ContextMenuParams,
|
||||
options: Required<WebContextMenuOptions>,
|
||||
): MenuItemConstructorOptions[] {
|
||||
const items: MenuItemConstructorOptions[] = []
|
||||
const editFlags = params.editFlags
|
||||
const canCopy = editFlags.canCopy || hasVisibleText(params.selectionText)
|
||||
const canSelectAll = editFlags.canSelectAll || params.isEditable
|
||||
|
||||
if (params.isEditable) {
|
||||
items.push(
|
||||
{
|
||||
label: '撤销',
|
||||
enabled: editFlags.canUndo,
|
||||
click: () => webContents.undo(),
|
||||
},
|
||||
{
|
||||
label: '重做',
|
||||
enabled: editFlags.canRedo,
|
||||
click: () => webContents.redo(),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: '剪切',
|
||||
enabled: editFlags.canCut,
|
||||
click: () => webContents.cut(),
|
||||
},
|
||||
{
|
||||
label: '复制',
|
||||
enabled: canCopy,
|
||||
click: () => webContents.copy(),
|
||||
},
|
||||
{
|
||||
label: '粘贴',
|
||||
enabled: editFlags.canPaste,
|
||||
click: () => webContents.paste(),
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
enabled: editFlags.canDelete,
|
||||
click: () => webContents.delete(),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: '全选',
|
||||
enabled: canSelectAll,
|
||||
click: () => webContents.selectAll(),
|
||||
},
|
||||
)
|
||||
} else if (canCopy) {
|
||||
items.push({
|
||||
label: '复制',
|
||||
click: () => webContents.copy(),
|
||||
})
|
||||
}
|
||||
|
||||
if (hasVisibleText(params.selectionText)) {
|
||||
pushSeparator(items)
|
||||
items.push({
|
||||
label: '复制所选文字',
|
||||
click: () => writeClipboardText(params.selectionText),
|
||||
})
|
||||
}
|
||||
|
||||
if (isRemoteURL(params.linkURL)) {
|
||||
pushSeparator(items)
|
||||
items.push(
|
||||
{
|
||||
label: '复制链接',
|
||||
click: () => writeClipboardText(params.linkURL),
|
||||
},
|
||||
{
|
||||
label: '在浏览器中打开链接',
|
||||
click: () => {
|
||||
void shell.openExternal(params.linkURL)
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (params.mediaType === 'image' && params.hasImageContents) {
|
||||
pushSeparator(items)
|
||||
items.push({
|
||||
label: '复制图片',
|
||||
click: () => webContents.copyImageAt(params.x, params.y),
|
||||
})
|
||||
if (isRemoteURL(params.srcURL)) {
|
||||
items.push({
|
||||
label: '复制图片地址',
|
||||
click: () => writeClipboardText(params.srcURL),
|
||||
})
|
||||
}
|
||||
} else if (isRemoteURL(params.srcURL)) {
|
||||
pushSeparator(items)
|
||||
items.push({
|
||||
label: '复制媒体地址',
|
||||
click: () => writeClipboardText(params.srcURL),
|
||||
})
|
||||
}
|
||||
|
||||
if (options.allowNavigation) {
|
||||
pushSeparator(items)
|
||||
items.push(
|
||||
{
|
||||
label: '后退',
|
||||
enabled: webContents.navigationHistory.canGoBack(),
|
||||
click: () => webContents.navigationHistory.goBack(),
|
||||
},
|
||||
{
|
||||
label: '前进',
|
||||
enabled: webContents.navigationHistory.canGoForward(),
|
||||
click: () => webContents.navigationHistory.goForward(),
|
||||
},
|
||||
{
|
||||
label: '重新加载',
|
||||
enabled: !webContents.isDestroyed(),
|
||||
click: () => webContents.reload(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return trimTrailingSeparators(items)
|
||||
}
|
||||
|
||||
export function attachWebContextMenu(
|
||||
webContents: WebContents,
|
||||
options: WebContextMenuOptions = {},
|
||||
): void {
|
||||
const resolvedOptions: Required<WebContextMenuOptions> = {
|
||||
allowNavigation: options.allowNavigation ?? true,
|
||||
}
|
||||
|
||||
webContents.on('context-menu', (event, params) => {
|
||||
const template = buildContextMenuTemplate(webContents, params, resolvedOptions)
|
||||
if (template.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
Menu.buildFromTemplate(template).popup({
|
||||
frame: params.frame ?? undefined,
|
||||
sourceType: params.menuSourceType,
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import type { Session, WebContents } from 'electron/main'
|
||||
import { BrowserWindow, WebContentsView, ipcMain, nativeTheme } from 'electron/main'
|
||||
|
||||
import { STANDARD_USER_AGENT } from './user-agent'
|
||||
import { attachWebContextMenu } from './web-context-menu'
|
||||
|
||||
interface WorkbenchWindowOptions {
|
||||
title: string
|
||||
@@ -817,6 +818,7 @@ export async function openWorkbenchWindow(options: WorkbenchWindowOptions): Prom
|
||||
})
|
||||
|
||||
view.webContents.setUserAgent(STANDARD_USER_AGENT)
|
||||
attachWebContextMenu(view.webContents)
|
||||
window.contentView.addChildView(view)
|
||||
|
||||
const record: WorkbenchWindowRecord = {
|
||||
|
||||
Reference in New Issue
Block a user