Skip to content
Open
165 changes: 153 additions & 12 deletions emain/emain-window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import { ClientService, ObjectService, WindowService, WorkspaceService } from "@/app/store/services";
import { RpcApi } from "@/app/store/wshclientapi";
import { waveEventSubscribeSingle } from "@/app/store/wps";
import { fireAndForget } from "@/util/util";
import { BaseWindow, BaseWindowConstructorOptions, dialog, globalShortcut, ipcMain, screen } from "electron";
import { globalEvents } from "emain/emain-events";
Expand Down Expand Up @@ -101,6 +102,13 @@ export const waveWindowMap = new Map<string, WaveBrowserWindow>(); // waveWindow
// e.g. it persists when the app itself is not focused
export let focusedWaveWindow: WaveBrowserWindow = null;

// quake window for toggle hotkey (show/hide behavior)
let quakeWindow: WaveBrowserWindow | null = null;

export function getQuakeWindow(): WaveBrowserWindow | null {
return quakeWindow;
}

let cachedClientId: string = null;
let hasCompletedFirstRelaunch = false;

Expand Down Expand Up @@ -332,6 +340,9 @@ export class WaveBrowserWindow extends BaseWindow {
if (focusedWaveWindow == this) {
focusedWaveWindow = null;
}
if (quakeWindow == this) {
quakeWindow = null;
}
this.removeAllChildViews();
if (getGlobalIsRelaunching()) {
console.log("win relaunching", this.waveWindowId);
Expand Down Expand Up @@ -704,6 +715,7 @@ export async function createBrowserWindow(
}
console.log("createBrowserWindow", waveWindow.oid, workspace.oid, workspace);
const bwin = new WaveBrowserWindow(waveWindow, fullConfig, opts);

if (workspace.activetabid) {
await bwin.setActiveTab(workspace.activetabid, false, opts.isPrimaryStartupWindow ?? false);
}
Expand Down Expand Up @@ -832,6 +844,9 @@ export async function createNewWaveWindow() {
unamePlatform,
isPrimaryStartupWindow: false,
});
if (quakeWindow == null) {
quakeWindow = win;
}
win.show();
recreatedWindow = true;
}
Expand All @@ -845,6 +860,9 @@ export async function createNewWaveWindow() {
unamePlatform,
isPrimaryStartupWindow: false,
});
if (quakeWindow == null) {
quakeWindow = newBrowserWindow;
}
newBrowserWindow.show();
}

Expand Down Expand Up @@ -887,6 +905,10 @@ export async function relaunchBrowserWindows() {
foregroundWindow: windowId === primaryWindowId,
});
wins.push(win);
if (windowId === primaryWindowId) {
quakeWindow = win;
console.log("designated quake window", win.waveWindowId);
}
}
hasCompletedFirstRelaunch = true;
for (const win of wins) {
Expand All @@ -895,22 +917,141 @@ export async function relaunchBrowserWindows() {
}
}

export function registerGlobalHotkey(rawGlobalHotKey: string) {
function getDisplayForQuakeToggle() {
// We cannot reliably query the OS-wide active window in Electron.
// Cursor position is the best cross-platform proxy for the user's active display.
const cursorPoint = screen.getCursorScreenPoint();
const displayAtCursor = screen
.getAllDisplays()
.find(
(display) =>
cursorPoint.x >= display.bounds.x &&
cursorPoint.x < display.bounds.x + display.bounds.width &&
cursorPoint.y >= display.bounds.y &&
cursorPoint.y < display.bounds.y + display.bounds.height
);
return displayAtCursor ?? screen.getDisplayNearestPoint(cursorPoint);
}

function moveWindowToDisplay(win: WaveBrowserWindow, targetDisplay: Electron.Display) {
if (!win || !targetDisplay || win.isDestroyed()) {
return;
}
const curBounds = win.getBounds();
const sourceDisplay = screen.getDisplayMatching(curBounds);
if (sourceDisplay.id === targetDisplay.id) {
return;
}

const sourceArea = sourceDisplay.workArea;
const targetArea = targetDisplay.workArea;
const nextHeight = Math.min(curBounds.height, targetArea.height);
const nextWidth = Math.min(curBounds.width, targetArea.width);
const maxXOffset = Math.max(0, targetArea.width - nextWidth);
const maxYOffset = Math.max(0, targetArea.height - nextHeight);
const sourceXOffset = curBounds.x - sourceArea.x;
const sourceYOffset = curBounds.y - sourceArea.y;
const nextX = targetArea.x + Math.min(Math.max(sourceXOffset, 0), maxXOffset);
const nextY = targetArea.y + Math.min(Math.max(sourceYOffset, 0), maxYOffset);

win.setBounds({ ...curBounds, x: nextX, y: nextY, width: nextWidth, height: nextHeight });
}

// small delay on fullscreen toggle to ensure that the OS has finished the fullscreen transition on its end
const PreQuakeFullscreenDelayMs = 120;
const PostQuakeFullscreenDelayMs = 80;

// handles a theoretical race condition where the user spams the hotkey before the toggle finishes
let quakeToggleInProgress = false;

async function quakeToggle() {
if (quakeToggleInProgress) {
return;
}
quakeToggleInProgress = true;
try {
const electronHotKey = waveKeyToElectronKey(rawGlobalHotKey);
console.log("registering globalhotkey of ", electronHotKey);
globalShortcut.register(electronHotKey, () => {
const selectedWindow = focusedWaveWindow;
const firstWaveWindow = getAllWaveWindows()[0];
if (focusedWaveWindow) {
selectedWindow.focus();
} else if (firstWaveWindow) {
firstWaveWindow.focus();
// quake mode: toggle visibility of the designated quake window
const window = quakeWindow;
if (window && !window.isDestroyed()) {
if (window.isVisible()) {
window.hide();
} else {
fireAndForget(createNewWaveWindow);
const targetDisplay = getDisplayForQuakeToggle();
// Some environments don't move the window if it's fullscreen, so we have to toggle fullscreen before the move
const wasFullscreen = window.isFullScreen();
if (wasFullscreen) {
window.setFullScreen(false);
await delay(PreQuakeFullscreenDelayMs);
if (window.isDestroyed()) {
return;
}
}
moveWindowToDisplay(window, targetDisplay);
window.show();
if (wasFullscreen) {
await delay(PostQuakeFullscreenDelayMs);
if (window.isDestroyed()) {
return;
}
moveWindowToDisplay(window, targetDisplay);
window.setFullScreen(true);
}
window.focus();
if (window.activeTabView?.webContents) {
window.activeTabView.webContents.focus();
}
}
} else if (window == null) {
// no quake window yet, create one
await createNewWaveWindow();
} else {
// quake window was destroyed, clear it
quakeWindow = null;
await createNewWaveWindow();
}
} finally {
quakeToggleInProgress = false;
}
}

let currentRawGlobalHotKey: string = null;
let currentGlobalHotKey: string = null;

export function registerGlobalHotkey(rawGlobalHotKey: string) {
if (rawGlobalHotKey === currentRawGlobalHotKey) {
return;
}
if (currentGlobalHotKey != null) {
globalShortcut.unregister(currentGlobalHotKey);
currentGlobalHotKey = null;
currentRawGlobalHotKey = null;
}
if (!rawGlobalHotKey) {
return;
}
try {
const electronHotKey = waveKeyToElectronKey(rawGlobalHotKey);
const ok = globalShortcut.register(electronHotKey, () => {
fireAndForget(quakeToggle);
});
currentRawGlobalHotKey = rawGlobalHotKey;
currentGlobalHotKey = electronHotKey;
console.log("registered globalhotkey", rawGlobalHotKey, "=>", electronHotKey, "ok=", ok);
} catch (e) {
console.log("error registering global hotkey: ", e);
console.log("error registering global hotkey", rawGlobalHotKey, ":", e);
}
}

export function initGlobalHotkeyEventSubscription() {
waveEventSubscribeSingle({
eventType: "config",
handler: (event) => {
try {
const hotkey = event?.data?.fullconfig?.settings?.["app:globalhotkey"];
registerGlobalHotkey(hotkey ?? null);
} catch (e) {
console.log("error handling config event for globalhotkey", e);
}
},
});
}
13 changes: 13 additions & 0 deletions emain/emain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,10 @@ import {
createNewWaveWindow,
focusedWaveWindow,
getAllWaveWindows,
getQuakeWindow,
getWaveWindowById,
getWaveWindowByWorkspaceId,
initGlobalHotkeyEventSubscription,
registerGlobalHotkey,
relaunchBrowserWindows,
WaveBrowserWindow,
Expand Down Expand Up @@ -427,6 +429,16 @@ async function appMain() {

electronApp.on("activate", () => {
const allWindows = getAllWaveWindows();
const anyVisible = allWindows.some((w) => !w.isDestroyed() && w.isVisible());
if (anyVisible) {
return;
}
const qw = getQuakeWindow();
if (qw != null && !qw.isDestroyed()) {
qw.show();
qw.focus();
return;
}
if (allWindows.length === 0) {
fireAndForget(createNewWaveWindow);
}
Expand All @@ -445,6 +457,7 @@ async function appMain() {
if (rawGlobalHotKey) {
registerGlobalHotkey(rawGlobalHotKey);
}
initGlobalHotkeyEventSubscription();
}

appMain().catch((e) => {
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.