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'
|
} from './session-registry'
|
||||||
import { upsertDesktopAccount } from './transport/api-client'
|
import { upsertDesktopAccount } from './transport/api-client'
|
||||||
import { STANDARD_USER_AGENT } from './user-agent'
|
import { STANDARD_USER_AGENT } from './user-agent'
|
||||||
|
import { attachWebContextMenu } from './web-context-menu'
|
||||||
import {
|
import {
|
||||||
getWorkbenchWindowWebContents,
|
getWorkbenchWindowWebContents,
|
||||||
isWorkbenchWindowReadyForDetection,
|
isWorkbenchWindowReadyForDetection,
|
||||||
@@ -4191,6 +4192,7 @@ function createBoundWindow(
|
|||||||
})
|
})
|
||||||
|
|
||||||
window.webContents.setUserAgent(STANDARD_USER_AGENT)
|
window.webContents.setUserAgent(STANDARD_USER_AGENT)
|
||||||
|
attachWebContextMenu(window.webContents)
|
||||||
attachWindowDiagnostics(window, title)
|
attachWindowDiagnostics(window, title)
|
||||||
|
|
||||||
window.webContents.setWindowOpenHandler(({ url }) => {
|
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 { BrowserWindow, WebContentsView, ipcMain, nativeTheme } from 'electron/main'
|
||||||
|
|
||||||
import { STANDARD_USER_AGENT } from './user-agent'
|
import { STANDARD_USER_AGENT } from './user-agent'
|
||||||
|
import { attachWebContextMenu } from './web-context-menu'
|
||||||
|
|
||||||
interface WorkbenchWindowOptions {
|
interface WorkbenchWindowOptions {
|
||||||
title: string
|
title: string
|
||||||
@@ -817,6 +818,7 @@ export async function openWorkbenchWindow(options: WorkbenchWindowOptions): Prom
|
|||||||
})
|
})
|
||||||
|
|
||||||
view.webContents.setUserAgent(STANDARD_USER_AGENT)
|
view.webContents.setUserAgent(STANDARD_USER_AGENT)
|
||||||
|
attachWebContextMenu(view.webContents)
|
||||||
window.contentView.addChildView(view)
|
window.contentView.addChildView(view)
|
||||||
|
|
||||||
const record: WorkbenchWindowRecord = {
|
const record: WorkbenchWindowRecord = {
|
||||||
|
|||||||
Reference in New Issue
Block a user