2024-10-17 23:34:02 +02:00
|
|
|
// 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;
|
|
|
|
}
|
2024-12-17 00:24:32 +01:00
|
|
|
|
|
|
|
export function waveKeyToElectronKey(waveKey: string): string {
|
|
|
|
const waveParts = waveKey.split(":");
|
|
|
|
const electronParts: Array<string> = waveParts.map((part: string) => {
|
2024-12-18 00:25:34 +01:00
|
|
|
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);
|
2024-12-17 00:24:32 +01:00
|
|
|
if (part == "ArrowUp") {
|
|
|
|
return "Up";
|
|
|
|
}
|
|
|
|
if (part == "ArrowDown") {
|
|
|
|
return "Down";
|
|
|
|
}
|
|
|
|
if (part == "ArrowLeft") {
|
|
|
|
return "Left";
|
|
|
|
}
|
|
|
|
if (part == "ArrowRight") {
|
|
|
|
return "Right";
|
|
|
|
}
|
2024-12-18 00:25:34 +01:00
|
|
|
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();
|
|
|
|
}
|
2024-12-17 00:24:32 +01:00
|
|
|
|
|
|
|
return part;
|
|
|
|
});
|
|
|
|
return electronParts.join("+");
|
|
|
|
}
|