terminal context menu

This commit is contained in:
sawka 2024-06-19 15:42:19 -07:00
parent 58684744b0
commit 5c6cfbc112
4 changed files with 112 additions and 5 deletions

View File

@ -21,6 +21,8 @@ const AuthKeyFile = "waveterm.authkey";
const DevServerEndpoint = "http://127.0.0.1:8190"; const DevServerEndpoint = "http://127.0.0.1:8190";
const ProdServerEndpoint = "http://127.0.0.1:1719"; const ProdServerEndpoint = "http://127.0.0.1:1719";
type WaveBrowserWindow = Electron.BrowserWindow & { waveWindowId: string };
let waveSrvReadyResolve = (value: boolean) => {}; let waveSrvReadyResolve = (value: boolean) => {};
let waveSrvReady: Promise<boolean> = new Promise((resolve, _) => { let waveSrvReady: Promise<boolean> = new Promise((resolve, _) => {
waveSrvReadyResolve = resolve; waveSrvReadyResolve = resolve;
@ -130,7 +132,7 @@ function runWaveSrv(): Promise<boolean> {
return rtnPromise; return rtnPromise;
} }
async function mainResizeHandler(_: any, windowId: string, win: Electron.BrowserWindow) { async function mainResizeHandler(_: any, windowId: string, win: WaveBrowserWindow) {
if (win == null || win.isDestroyed() || win.fullScreen) { if (win == null || win.isDestroyed() || win.fullScreen) {
return; return;
} }
@ -187,7 +189,7 @@ function shFrameNavHandler(event: Electron.Event<Electron.WebContentsWillFrameNa
console.log("frame navigation canceled"); console.log("frame navigation canceled");
} }
function createWindow(client: Client, waveWindow: WaveWindow): Electron.BrowserWindow { function createWindow(client: Client, waveWindow: WaveWindow): WaveBrowserWindow {
const primaryDisplay = electron.screen.getPrimaryDisplay(); const primaryDisplay = electron.screen.getPrimaryDisplay();
let winHeight = waveWindow.winsize.height; let winHeight = waveWindow.winsize.height;
let winWidth = waveWindow.winsize.width; let winWidth = waveWindow.winsize.width;
@ -205,7 +207,7 @@ function createWindow(client: Client, waveWindow: WaveWindow): Electron.BrowserW
if (winY + winHeight > primaryDisplay.workAreaSize.height) { if (winY + winHeight > primaryDisplay.workAreaSize.height) {
winY = Math.floor((primaryDisplay.workAreaSize.height - winHeight) / 2); winY = Math.floor((primaryDisplay.workAreaSize.height - winHeight) / 2);
} }
const win = new electron.BrowserWindow({ const bwin = new electron.BrowserWindow({
x: winX, x: winX,
y: winY, y: winY,
titleBarStyle: "hiddenInset", titleBarStyle: "hiddenInset",
@ -224,6 +226,8 @@ function createWindow(client: Client, waveWindow: WaveWindow): Electron.BrowserW
autoHideMenuBar: true, autoHideMenuBar: true,
backgroundColor: "#000000", backgroundColor: "#000000",
}); });
(bwin as any).waveWindowId = waveWindow.oid;
const win: WaveBrowserWindow = bwin as WaveBrowserWindow;
win.once("ready-to-show", () => { win.once("ready-to-show", () => {
win.show(); win.show();
}); });
@ -283,6 +287,56 @@ electron.ipcMain.on("getCursorPoint", (event) => {
event.returnValue = retVal; event.returnValue = retVal;
}); });
electron.ipcMain.on("openNewWindow", (event) => {});
electron.ipcMain.on("context-editmenu", (_, { x, y }, opts) => {
if (opts == null) {
opts = {};
}
console.log("context-editmenu");
const menu = new electron.Menu();
if (!opts.onlyPaste) {
if (opts.showCut) {
const menuItem = new electron.MenuItem({ label: "Cut", role: "cut" });
menu.append(menuItem);
}
const menuItem = new electron.MenuItem({ label: "Copy", role: "copy" });
menu.append(menuItem);
}
const menuItem = new electron.MenuItem({ label: "Paste", role: "paste" });
menu.append(menuItem);
menu.popup({ x, y });
});
electron.ipcMain.on("contextmenu-show", (event, menuDefArr: ElectronContextMenuItem[], { x, y }) => {
if (menuDefArr == null || menuDefArr.length == 0) {
return;
}
const menu = convertMenuDefArrToMenu(menuDefArr);
menu.popup({ x, y });
event.returnValue = true;
});
function convertMenuDefArrToMenu(menuDefArr: ElectronContextMenuItem[]): electron.Menu {
const menuItems: electron.MenuItem[] = [];
for (const menuDef of menuDefArr) {
const menuItemTemplate: electron.MenuItemConstructorOptions = {
role: menuDef.role as any,
label: menuDef.label,
type: menuDef.type,
click: (_, window) => {
window?.webContents.send("contextmenu-click", menuDef.id);
},
};
if (menuDef.submenu != null) {
menuItemTemplate.submenu = convertMenuDefArrToMenu(menuDef.submenu);
}
const menuItem = new electron.MenuItem(menuItemTemplate);
menuItems.push(menuItem);
}
return electron.Menu.buildFromTemplate(menuItems);
}
(async () => { (async () => {
const startTs = Date.now(); const startTs = Date.now();
const instanceLock = electronApp.requestSingleInstanceLock(); const instanceLock = electronApp.requestSingleInstanceLock();

View File

@ -7,4 +7,8 @@ contextBridge.exposeInMainWorld("api", {
isDev: () => ipcRenderer.sendSync("isDev"), isDev: () => ipcRenderer.sendSync("isDev"),
isDevServer: () => ipcRenderer.sendSync("isDevServer"), isDevServer: () => ipcRenderer.sendSync("isDevServer"),
getCursorPoint: () => ipcRenderer.sendSync("getCursorPoint"), getCursorPoint: () => ipcRenderer.sendSync("getCursorPoint"),
openNewWindow: () => ipcRenderer.send("openNewWindow"),
contextEditMenu: (position, opts) => ipcRenderer.send("context-editmenu", position, opts),
showContextMenu: (menu, position) => ipcRenderer.send("contextmenu-show", menu, position),
onContextMenuClick: (callback) => ipcRenderer.on("contextmenu-click", callback),
}); });

View File

@ -2,7 +2,8 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { Workspace } from "@/app/workspace/workspace"; import { Workspace } from "@/app/workspace/workspace";
import { atoms, globalStore } from "@/store/global"; import { atoms, getApi, globalStore } from "@/store/global";
import * as util from "@/util/util";
import * as jotai from "jotai"; import * as jotai from "jotai";
import { Provider } from "jotai"; import { Provider } from "jotai";
@ -19,6 +20,29 @@ const App = () => {
); );
}; };
function handleContextMenu(e: React.MouseEvent<HTMLDivElement>) {
let isInNonTermInput = false;
const activeElem = document.activeElement;
if (activeElem != null && activeElem.nodeName == "TEXTAREA") {
if (!activeElem.classList.contains("xterm-helper-textarea")) {
isInNonTermInput = true;
}
}
if (activeElem != null && activeElem.nodeName == "INPUT" && activeElem.getAttribute("type") == "text") {
isInNonTermInput = true;
}
const opts: ContextMenuOpts = {};
if (isInNonTermInput) {
opts.showCut = true;
}
const sel = window.getSelection();
if (!util.isBlank(sel?.toString()) || isInNonTermInput) {
getApi().contextEditMenu({ x: e.clientX, y: e.clientY }, opts);
} else {
getApi().contextEditMenu({ x: e.clientX, y: e.clientY }, { onlyPaste: true });
}
}
const AppInner = () => { const AppInner = () => {
const client = jotai.useAtomValue(atoms.client); const client = jotai.useAtomValue(atoms.client);
const windowData = jotai.useAtomValue(atoms.waveWindow); const windowData = jotai.useAtomValue(atoms.waveWindow);
@ -31,7 +55,7 @@ const AppInner = () => {
); );
} }
return ( return (
<div className="mainapp"> <div className="mainapp" onContextMenu={handleContextMenu}>
<DndProvider backend={HTML5Backend}> <DndProvider backend={HTML5Backend}>
<div className="titlebar"></div> <div className="titlebar"></div>
<Workspace /> <Workspace />

View File

@ -6,6 +6,11 @@ declare global {
blockId: string; blockId: string;
}; };
type ContextMenuOpts = {
showCut?: boolean;
onlyPaste?: boolean;
};
type ElectronApi = { type ElectronApi = {
/** /**
* Determines whether the current app instance is a development build. * Determines whether the current app instance is a development build.
@ -22,6 +27,26 @@ declare global {
* @returns A point value. * @returns A point value.
*/ */
getCursorPoint: () => Electron.Point; getCursorPoint: () => Electron.Point;
contextEditMenu: (position: { x: number; y: number }, opts: ContextMenuOpts) => void;
showContextMenu: (menu: ElectronContextMenuItem[], position: { x: number; y: number }) => void;
onContextMenuClick: (callback: (id: string) => void) => void;
};
type ElectronContextMenuItem = {
id: string; // unique id, used for communication
label: string;
role?: string; // electron role (optional)
type?: "separator" | "normal" | "submenu";
submenu?: ElectronContextMenuItem[];
};
type ContextMenuItem = {
label?: string;
type?: "separator" | "normal" | "submenu";
role?: string; // electron role (optional)
click?: () => void; // not required if role is set
submenu?: ContextMenuItem[];
}; };
type SubjectWithRef<T> = rxjs.Subject<T> & { refCount: number; release: () => void }; type SubjectWithRef<T> = rxjs.Subject<T> & { refCount: number; release: () => void };