mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-08-26 09:02:23 +00:00
203 lines
5.3 KiB
TypeScript
203 lines
5.3 KiB
TypeScript
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
|
|
|
import type { NativeMenuMessages } from './nativeMenuMessages';
|
|
|
|
export type ContextMenuSurfaceKind =
|
|
'main-renderer' | 'preview-guest' | 'automation-view';
|
|
|
|
export type ContextMenuWebContents = Pick<
|
|
Electron.WebContents,
|
|
| 'copy'
|
|
| 'cut'
|
|
| 'inspectElement'
|
|
| 'isDestroyed'
|
|
| 'off'
|
|
| 'on'
|
|
| 'paste'
|
|
| 'redo'
|
|
| 'selectAll'
|
|
| 'undo'
|
|
>;
|
|
|
|
export interface ContextMenuPopup {
|
|
popup: (options: Electron.PopupOptions) => void;
|
|
}
|
|
|
|
export interface ContextMenuApi<
|
|
TMenu extends ContextMenuPopup = ContextMenuPopup,
|
|
> {
|
|
buildFromTemplate: (template: Electron.MenuItemConstructorOptions[]) => TMenu;
|
|
}
|
|
|
|
export interface ContextMenuTemplateOptions {
|
|
contents: ContextMenuWebContents;
|
|
isDevelopment: boolean;
|
|
messages: NativeMenuMessages;
|
|
params: Pick<
|
|
Electron.ContextMenuParams,
|
|
'editFlags' | 'isEditable' | 'menuSourceType' | 'x' | 'y'
|
|
>;
|
|
surfaceKind: ContextMenuSurfaceKind;
|
|
}
|
|
|
|
export interface InstallContextMenuOptions<
|
|
TMenu extends ContextMenuPopup = ContextMenuPopup,
|
|
> {
|
|
contents: ContextMenuWebContents;
|
|
getMessages: () => NativeMenuMessages;
|
|
isDevelopment: boolean;
|
|
menuApi: ContextMenuApi<TMenu>;
|
|
ownerWindow: Electron.BaseWindow;
|
|
surfaceKind: ContextMenuSurfaceKind;
|
|
}
|
|
|
|
type ContextCommand =
|
|
'undo' | 'redo' | 'cut' | 'copy' | 'paste' | 'select-all' | 'inspect';
|
|
|
|
function commandItem(
|
|
surfaceKind: ContextMenuSurfaceKind,
|
|
command: ContextCommand,
|
|
label: string,
|
|
enabled: boolean,
|
|
click: () => void
|
|
): Electron.MenuItemConstructorOptions {
|
|
return {
|
|
id: `context.${surfaceKind}.${command}`,
|
|
label,
|
|
enabled,
|
|
click,
|
|
};
|
|
}
|
|
|
|
const separator = (): Electron.MenuItemConstructorOptions => ({
|
|
type: 'separator',
|
|
});
|
|
|
|
/**
|
|
* Build a fresh template for one invocation. Every action closes over the exact
|
|
* WebContents that emitted the event; focus changes while the native menu is
|
|
* open cannot redirect editing into another surface.
|
|
*/
|
|
export function buildContextMenuTemplate({
|
|
contents,
|
|
isDevelopment,
|
|
messages,
|
|
params,
|
|
surfaceKind,
|
|
}: ContextMenuTemplateOptions): Electron.MenuItemConstructorOptions[] {
|
|
const { editFlags } = params;
|
|
const supportsEditing =
|
|
surfaceKind !== 'automation-view' && params.isEditable;
|
|
const template: Electron.MenuItemConstructorOptions[] = supportsEditing
|
|
? [
|
|
commandItem(surfaceKind, 'undo', messages.undo, editFlags.canUndo, () =>
|
|
contents.undo()
|
|
),
|
|
commandItem(surfaceKind, 'redo', messages.redo, editFlags.canRedo, () =>
|
|
contents.redo()
|
|
),
|
|
separator(),
|
|
commandItem(surfaceKind, 'cut', messages.cut, editFlags.canCut, () =>
|
|
contents.cut()
|
|
),
|
|
commandItem(surfaceKind, 'copy', messages.copy, editFlags.canCopy, () =>
|
|
contents.copy()
|
|
),
|
|
commandItem(
|
|
surfaceKind,
|
|
'paste',
|
|
messages.paste,
|
|
editFlags.canPaste,
|
|
() => contents.paste()
|
|
),
|
|
separator(),
|
|
commandItem(
|
|
surfaceKind,
|
|
'select-all',
|
|
messages.selectAll,
|
|
editFlags.canSelectAll,
|
|
() => contents.selectAll()
|
|
),
|
|
]
|
|
: [
|
|
commandItem(surfaceKind, 'copy', messages.copy, editFlags.canCopy, () =>
|
|
contents.copy()
|
|
),
|
|
commandItem(
|
|
surfaceKind,
|
|
'select-all',
|
|
messages.selectAll,
|
|
editFlags.canSelectAll,
|
|
() => contents.selectAll()
|
|
),
|
|
];
|
|
|
|
if (isDevelopment && surfaceKind !== 'automation-view') {
|
|
template.push(
|
|
separator(),
|
|
commandItem(
|
|
surfaceKind,
|
|
'inspect',
|
|
messages.inspectElement,
|
|
!contents.isDestroyed(),
|
|
() => contents.inspectElement(params.x, params.y)
|
|
)
|
|
);
|
|
}
|
|
|
|
return template;
|
|
}
|
|
|
|
/** Attach a native context menu and return an idempotent listener disposer. */
|
|
export function installContextMenu<
|
|
TMenu extends ContextMenuPopup = ContextMenuPopup,
|
|
>({
|
|
contents,
|
|
getMessages,
|
|
isDevelopment,
|
|
menuApi,
|
|
ownerWindow,
|
|
surfaceKind,
|
|
}: InstallContextMenuOptions<TMenu>): () => void {
|
|
const listener = (
|
|
_event: Electron.Event,
|
|
params: Electron.ContextMenuParams
|
|
) => {
|
|
if (contents.isDestroyed()) return;
|
|
|
|
const menu = menuApi.buildFromTemplate(
|
|
buildContextMenuTemplate({
|
|
contents,
|
|
isDevelopment,
|
|
messages: getMessages(),
|
|
params,
|
|
surfaceKind,
|
|
})
|
|
);
|
|
menu.popup({
|
|
window: ownerWindow,
|
|
sourceType: params.menuSourceType,
|
|
});
|
|
};
|
|
|
|
contents.on('context-menu', listener);
|
|
|
|
let disposed = false;
|
|
return () => {
|
|
if (disposed) return;
|
|
disposed = true;
|
|
if (!contents.isDestroyed()) contents.off('context-menu', listener);
|
|
};
|
|
}
|