waveterm/emain/emain-util.ts
Sylvie Crowe dbc2be1c1e
Global Hotkey Docs (#1548)
Adds the following changes:
- renames "key:globalhotkey" to "app:globalhotkey"
- adds globalhostkey documentation (especially in regard to what keys are allowed)
- improves the algorithm that converts electron keybindings from wave keybindings
- fixes some regexp problems
2024-12-17 15:25:34 -08:00

256 lines
8.2 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) => {
const digitRegexpMatch = new RegExp("^c{Digit([0-9])}$").exec(part);
const numpadRegexpMatch = new RegExp("^c{Numpad([0-9])}$").exec(part);
const lowercaseCharMatch = new RegExp("^([a-z])$").exec(part);
if (part == "ArrowUp") {
return "Up";
}
if (part == "ArrowDown") {
return "Down";
}
if (part == "ArrowLeft") {
return "Left";
}
if (part == "ArrowRight") {
return "Right";
}
if (part == "Soft1") {
return "F21";
}
if (part == "Soft2") {
return "F22";
}
if (part == "Soft3") {
return "F23";
}
if (part == "Soft4") {
return "F24";
}
if (part == " ") {
return "Space";
}
if (part == "CapsLock") {
return "Capslock";
}
if (part == "NumLock") {
return "Numlock";
}
if (part == "ScrollLock") {
return "Scrolllock";
}
if (part == "AudioVolumeUp") {
return "VolumeUp";
}
if (part == "AudioVolumeDown") {
return "VolumeDown";
}
if (part == "AudioVolumeMute") {
return "VolumeMute";
}
if (part == "MediaTrackNext") {
return "MediaNextTrack";
}
if (part == "MediaTrackPrevious") {
return "MediaPreviousTrack";
}
if (part == "Decimal") {
return "numdec";
}
if (part == "Add") {
return "numadd";
}
if (part == "Subtract") {
return "numsub";
}
if (part == "Multiply") {
return "nummult";
}
if (part == "Divide") {
return "numdiv";
}
if (digitRegexpMatch && digitRegexpMatch.length > 1) {
return digitRegexpMatch[1];
}
if (numpadRegexpMatch && numpadRegexpMatch.length > 1) {
return `num${numpadRegexpMatch[1]}`;
}
if (lowercaseCharMatch && lowercaseCharMatch.length > 1) {
return lowercaseCharMatch[1].toUpperCase();
}
return part;
});
return electronParts.join("+");
}