mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-02 18:39:05 +01:00
terminal context menu
This commit is contained in:
parent
58684744b0
commit
5c6cfbc112
@ -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();
|
||||||
|
@ -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),
|
||||||
});
|
});
|
||||||
|
@ -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 />
|
||||||
|
25
frontend/types/custom.d.ts
vendored
25
frontend/types/custom.d.ts
vendored
@ -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 };
|
||||||
|
Loading…
Reference in New Issue
Block a user