waveterm/emain/emain-util.ts
Sylvie Crowe 51bd45bd2b
Global Hotkey (#1534)
Sets up a configurable global hotkey to focus the last window used in
the application. Note that this is established at startup and
configuration changes will not be applied until rebooting the app.
2024-12-16 15:24:32 -08:00

190 lines
6.3 KiB
TypeScript

// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as electron from "electron";
import { getWebServerEndpoint } from "../frontend/util/endpoints";
export const WaveAppPathVarName = "WAVETERM_APP_PATH";
export function delay(ms): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function setCtrlShift(wc: Electron.WebContents, state: boolean) {
wc.send("control-shift-state-update", state);
}
export function handleCtrlShiftFocus(sender: Electron.WebContents, focused: boolean) {
if (!focused) {
setCtrlShift(sender, false);
}
}
export function handleCtrlShiftState(sender: Electron.WebContents, waveEvent: WaveKeyboardEvent) {
if (waveEvent.type == "keyup") {
if (waveEvent.key === "Control" || waveEvent.key === "Shift") {
setCtrlShift(sender, false);
}
if (waveEvent.key == "Meta") {
if (waveEvent.control && waveEvent.shift) {
setCtrlShift(sender, true);
}
}
return;
}
if (waveEvent.type == "keydown") {
if (waveEvent.key === "Control" || waveEvent.key === "Shift" || waveEvent.key === "Meta") {
if (waveEvent.control && waveEvent.shift && !waveEvent.meta) {
// Set the control and shift without the Meta key
setCtrlShift(sender, true);
} else {
// Unset if Meta is pressed
setCtrlShift(sender, false);
}
}
return;
}
}
export function shNavHandler(event: Electron.Event<Electron.WebContentsWillNavigateEventParams>, url: string) {
if (url.startsWith("http://127.0.0.1:5173/index.html") || url.startsWith("http://localhost:5173/index.html")) {
// this is a dev-mode hot-reload, ignore it
console.log("allowing hot-reload of index.html");
return;
}
event.preventDefault();
if (url.startsWith("https://") || url.startsWith("http://") || url.startsWith("file://")) {
console.log("open external, shNav", url);
electron.shell.openExternal(url);
} else {
console.log("navigation canceled", url);
}
}
export function shFrameNavHandler(event: Electron.Event<Electron.WebContentsWillFrameNavigateEventParams>) {
if (!event.frame?.parent) {
// only use this handler to process iframe events (non-iframe events go to shNavHandler)
return;
}
const url = event.url;
console.log(`frame-navigation url=${url} frame=${event.frame.name}`);
if (event.frame.name == "webview") {
// "webview" links always open in new window
// this will *not* effect the initial load because srcdoc does not count as an electron navigation
console.log("open external, frameNav", url);
event.preventDefault();
electron.shell.openExternal(url);
return;
}
if (
event.frame.name == "pdfview" &&
(url.startsWith("blob:file:///") || url.startsWith(getWebServerEndpoint() + "/wave/stream-file?"))
) {
// allowed
return;
}
event.preventDefault();
console.log("frame navigation canceled");
}
function isWindowFullyVisible(bounds: electron.Rectangle): boolean {
const displays = electron.screen.getAllDisplays();
// Helper function to check if a point is inside any display
function isPointInDisplay(x: number, y: number) {
for (const display of displays) {
const { x: dx, y: dy, width, height } = display.bounds;
if (x >= dx && x < dx + width && y >= dy && y < dy + height) {
return true;
}
}
return false;
}
// Check all corners of the window
const topLeft = isPointInDisplay(bounds.x, bounds.y);
const topRight = isPointInDisplay(bounds.x + bounds.width, bounds.y);
const bottomLeft = isPointInDisplay(bounds.x, bounds.y + bounds.height);
const bottomRight = isPointInDisplay(bounds.x + bounds.width, bounds.y + bounds.height);
return topLeft && topRight && bottomLeft && bottomRight;
}
function findDisplayWithMostArea(bounds: electron.Rectangle): electron.Display {
const displays = electron.screen.getAllDisplays();
let maxArea = 0;
let bestDisplay = null;
for (let display of displays) {
const { x, y, width, height } = display.bounds;
const overlapX = Math.max(0, Math.min(bounds.x + bounds.width, x + width) - Math.max(bounds.x, x));
const overlapY = Math.max(0, Math.min(bounds.y + bounds.height, y + height) - Math.max(bounds.y, y));
const overlapArea = overlapX * overlapY;
if (overlapArea > maxArea) {
maxArea = overlapArea;
bestDisplay = display;
}
}
return bestDisplay;
}
function adjustBoundsToFitDisplay(bounds: electron.Rectangle, display: electron.Display): electron.Rectangle {
const { x: dx, y: dy, width: dWidth, height: dHeight } = display.workArea;
let { x, y, width, height } = bounds;
// Adjust width and height to fit within the display's work area
width = Math.min(width, dWidth);
height = Math.min(height, dHeight);
// Adjust x to ensure the window fits within the display
if (x < dx) {
x = dx;
} else if (x + width > dx + dWidth) {
x = dx + dWidth - width;
}
// Adjust y to ensure the window fits within the display
if (y < dy) {
y = dy;
} else if (y + height > dy + dHeight) {
y = dy + dHeight - height;
}
return { x, y, width, height };
}
export function ensureBoundsAreVisible(bounds: electron.Rectangle): electron.Rectangle {
if (!isWindowFullyVisible(bounds)) {
let targetDisplay = findDisplayWithMostArea(bounds);
if (!targetDisplay) {
targetDisplay = electron.screen.getPrimaryDisplay();
}
return adjustBoundsToFitDisplay(bounds, targetDisplay);
}
return bounds;
}
export function waveKeyToElectronKey(waveKey: string): string {
const waveParts = waveKey.split(":");
const electronParts: Array<string> = waveParts.map((part: string) => {
if (part == "ArrowUp") {
return "Up";
}
if (part == "ArrowDown") {
return "Down";
}
if (part == "ArrowLeft") {
return "Left";
}
if (part == "ArrowRight") {
return "Right";
}
return part;
});
return electronParts.join("+");
}