new focus system part 1 (#290)

This commit is contained in:
Mike Sawka 2024-08-29 16:06:15 -07:00 committed by GitHub
parent 637eaa4206
commit a104a6e446
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 442 additions and 94 deletions

View File

@ -16,6 +16,7 @@ import { initGlobal } from "../frontend/app/store/global";
import * as services from "../frontend/app/store/services";
import { WSServerEndpointVarName, WebServerEndpointVarName, getWebServerEndpoint } from "../frontend/util/endpoints";
import { fetch } from "../frontend/util/fetchutil";
import * as keyutil from "../frontend/util/keyutil";
import { fireAndForget } from "../frontend/util/util";
import { AuthKey, AuthKeyEnv, configureAuthKeyRequestInjection } from "./authkey";
import { getAppMenu } from "./menu";
@ -54,6 +55,9 @@ let globalIsRelaunching = false;
let wasActive = true;
let wasInFg = true;
let webviewFocusId: number = null; // set to the getWebContentsId of the webview that has focus (null if not focused)
let webviewKeys: string[] = []; // the keys to trap when webview has focus
let waveSrvProc: child_process.ChildProcessWithoutNullStreams | null = null;
const waveHome = getWaveHomeDir();
@ -104,6 +108,42 @@ function getWindowForEvent(event: Electron.IpcMainEvent): Electron.BrowserWindow
return electron.BrowserWindow.fromId(windowId);
}
function setCtrlShift(wc: Electron.WebContents, state: boolean) {
wc.send("control-shift-state-update", state);
}
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;
}
}
function handleCtrlShiftFocus(sender: Electron.WebContents, focused: boolean) {
if (!focused) {
setCtrlShift(sender, false);
}
}
function runWaveSrv(): Promise<boolean> {
let pResolve: (value: boolean) => void;
let pReject: (reason?: any) => void;
@ -371,6 +411,9 @@ function createBrowserWindow(clientId: string, waveWindow: WaveWindow, fullConfi
});
});
win.webContents.on("before-input-event", (e, input) => {
const waveEvent = keyutil.adaptFromElectronKeyEvent(input);
// console.log("WIN bie", waveEvent.type, waveEvent.code);
handleCtrlShiftState(win.webContents, waveEvent);
if (win.isFocused()) {
wasActive = true;
}
@ -392,6 +435,9 @@ function createBrowserWindow(clientId: string, waveWindow: WaveWindow, fullConfi
console.log("focus", waveWindow.oid);
services.ClientService.FocusWindow(waveWindow.oid);
});
win.on("blur", () => {
handleCtrlShiftFocus(win.webContents, false);
});
win.on("enter-full-screen", async () => {
win.webContents.send("fullscreen-change", true);
});
@ -553,6 +599,51 @@ electron.ipcMain.on("get-env", (event, varName) => {
event.returnValue = process.env[varName] ?? null;
});
const hasBeforeInputRegisteredMap = new Map<number, boolean>();
electron.ipcMain.on("webview-focus", (event: Electron.IpcMainEvent, focusedId: number) => {
webviewFocusId = focusedId;
console.log("webview-focus", focusedId);
if (focusedId == null) {
return;
}
const parentWc = event.sender;
const webviewWc = electron.webContents.fromId(focusedId);
if (webviewWc == null) {
webviewFocusId = null;
return;
}
if (!hasBeforeInputRegisteredMap.get(focusedId)) {
hasBeforeInputRegisteredMap.set(focusedId, true);
webviewWc.on("before-input-event", (e, input) => {
let waveEvent = keyutil.adaptFromElectronKeyEvent(input);
// console.log(`WEB ${focusedId}`, waveEvent.type, waveEvent.code);
handleCtrlShiftState(parentWc, waveEvent);
if (webviewFocusId != focusedId) {
return;
}
if (input.type != "keyDown") {
return;
}
for (let keyDesc of webviewKeys) {
if (keyutil.checkKeyPressed(waveEvent, keyDesc)) {
e.preventDefault();
parentWc.send("reinject-key", waveEvent);
console.log("webview reinject-key", keyDesc);
return;
}
}
});
webviewWc.on("destroyed", () => {
hasBeforeInputRegisteredMap.delete(focusedId);
});
}
});
electron.ipcMain.on("register-global-webview-keys", (event, keys: string[]) => {
webviewKeys = keys ?? [];
});
if (unamePlatform !== "darwin") {
const fac = new FastAverageColor();

View File

@ -28,6 +28,11 @@ contextBridge.exposeInMainWorld("api", {
installAppUpdate: () => ipcRenderer.send("install-app-update"),
onMenuItemAbout: (callback) => ipcRenderer.on("menu-item-about", callback),
updateWindowControlsOverlay: (rect) => ipcRenderer.send("update-window-controls-overlay", rect),
onReinjectKey: (callback) => ipcRenderer.on("reinject-key", (_event, waveEvent) => callback(waveEvent)),
setWebviewFocus: (focused: number) => ipcRenderer.send("webview-focus", focused),
registerGlobalWebviewKeys: (keys) => ipcRenderer.send("register-global-webview-keys", keys),
onControlShiftStateUpdate: (callback) =>
ipcRenderer.on("control-shift-state-update", (_event, state) => callback(state)),
});
// Custom event for "new-window"

View File

@ -1,18 +1,20 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { appHandleKeyDown, appHandleKeyUp } from "@/app/appkey";
import { useWaveObjectValue } from "@/app/store/wos";
import { Workspace } from "@/app/workspace/workspace";
import { ContextMenuModel } from "@/store/contextmenu";
import { PLATFORM, WOS, atoms, getApi, globalStore, useSettingsPrefixAtom } from "@/store/global";
import { appHandleKeyDown } from "@/store/keymodel";
import { getWebServerEndpoint } from "@/util/endpoints";
import { getElemAsStr } from "@/util/focusutil";
import * as keyutil from "@/util/keyutil";
import * as util from "@/util/util";
import useResizeObserver from "@react-hook/resize-observer";
import clsx from "clsx";
import Color from "color";
import * as csstree from "css-tree";
import debug from "debug";
import * as jotai from "jotai";
import "overlayscrollbars/overlayscrollbars.css";
import * as React from "react";
@ -22,6 +24,9 @@ import { debounce } from "throttle-debounce";
import "./app.less";
import { CenteredDiv } from "./element/quickelems";
const dlog = debug("wave:app");
const focusLog = debug("wave:focus");
const App = () => {
let Provider = jotai.Provider;
return (
@ -104,6 +109,40 @@ function AppSettingsUpdater() {
return null;
}
function appFocusIn(e: FocusEvent) {
focusLog("focusin", getElemAsStr(e.target), "<=", getElemAsStr(e.relatedTarget));
}
function appFocusOut(e: FocusEvent) {
focusLog("focusout", getElemAsStr(e.target), "=>", getElemAsStr(e.relatedTarget));
}
function appSelectionChange(e: Event) {
const selection = document.getSelection();
focusLog("selectionchange", getElemAsStr(selection.anchorNode));
}
function AppFocusHandler() {
React.useEffect(() => {
document.addEventListener("focusin", appFocusIn);
document.addEventListener("focusout", appFocusOut);
document.addEventListener("selectionchange", appSelectionChange);
const ivId = setInterval(() => {
const activeElement = document.activeElement;
if (activeElement instanceof HTMLElement) {
focusLog("activeElement", getElemAsStr(activeElement));
}
}, 2000);
return () => {
document.removeEventListener("focusin", appFocusIn);
document.removeEventListener("focusout", appFocusOut);
document.removeEventListener("selectionchange", appSelectionChange);
clearInterval(ivId);
};
});
return null;
}
function encodeFileURL(file: string) {
const webEndpoint = getWebServerEndpoint();
return webEndpoint + `/wave/stream-file?path=${encodeURIComponent(file)}&no404=1`;
@ -202,12 +241,9 @@ const AppKeyHandlers = () => {
React.useEffect(() => {
const staticKeyDownHandler = keyutil.keydownWrapper(appHandleKeyDown);
document.addEventListener("keydown", staticKeyDownHandler);
const savedKeyUpHandler = appHandleKeyUp;
document.addEventListener("keyup", savedKeyUpHandler);
return () => {
document.removeEventListener("keydown", staticKeyDownHandler);
document.removeEventListener("keyup", savedKeyUpHandler);
};
}, []);
return null;
@ -247,6 +283,7 @@ const AppInner = () => {
>
<AppBackground />
<AppKeyHandlers />
<AppFocusHandler />
<AppSettingsUpdater />
<DndProvider backend={HTML5Backend}>
<Workspace />

View File

@ -27,7 +27,7 @@ type FullBlockProps = {
viewModel: ViewModel;
};
function makeViewModel(blockId: string, blockView: string): ViewModel {
function makeViewModel(blockId: string, blockView: string, nodeModel: NodeModel): ViewModel {
if (blockView === "term") {
return makeTerminalModel(blockId);
}
@ -35,7 +35,7 @@ function makeViewModel(blockId: string, blockView: string): ViewModel {
return makePreviewModel(blockId);
}
if (blockView === "web") {
return makeWebViewModel(blockId);
return makeWebViewModel(blockId, nodeModel);
}
if (blockView === "waveai") {
return makeWaveAiViewModel(blockId);
@ -250,7 +250,7 @@ const Block = React.memo((props: BlockProps) => {
const [blockData, loading] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", props.nodeModel.blockId));
let viewModel = getViewModel(props.nodeModel.blockId);
if (viewModel == null || viewModel.viewType != blockData?.meta?.view) {
viewModel = makeViewModel(props.nodeModel.blockId, blockData?.meta?.view);
viewModel = makeViewModel(props.nodeModel.blockId, blockData?.meta?.view, props.nodeModel);
registerViewModel(props.nodeModel.blockId, viewModel);
}
React.useEffect(() => {

View File

@ -315,6 +315,7 @@ const BlockFrame_Default_Component = (props: BlockFrameProps) => {
"block-preview": preview,
"block-no-highlight": numBlocksInTab === 1,
})}
data-blockid={nodeModel.blockId}
onClick={blockModel?.onClick}
onFocusCapture={blockModel?.onFocusCapture}
ref={blockModel?.blockRef}

View File

@ -1,9 +1,10 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { atoms } from "@/store/global";
import { atoms, globalStore } from "@/store/global";
import { modalsModel } from "@/store/modalmodel";
import * as jotai from "jotai";
import { useEffect } from "react";
import { getModalComponent } from "./modalregistry";
import { TosModal } from "./tos";
@ -20,6 +21,9 @@ const ModalsRenderer = () => {
if (!clientData.tosagreed) {
rtn.push(<TosModal key={TosModal.displayName} />);
}
useEffect(() => {
globalStore.set(atoms.modalOpen, rtn.length > 0);
});
return <>{rtn}</>;
};

View File

@ -126,6 +126,7 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) {
}
const reducedMotionPreferenceAtom = jotai.atom((get) => get(settingsAtom)?.["window:reducedmotion"]);
const typeAheadModalAtom = jotai.atom({});
const modalOpen = jotai.atom(false);
atoms = {
// initialized in wave.ts (will not be null inside of application)
windowId: windowIdAtom,
@ -143,6 +144,7 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) {
updaterStatusAtom,
reducedMotionPreferenceAtom,
typeAheadModalAtom,
modalOpen,
};
}

View File

@ -1,7 +1,8 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { atoms, createBlock, globalStore, WOS } from "@/app/store/global";
import { atoms, createBlock, getApi, getViewModel, globalStore, WOS } from "@/app/store/global";
import * as services from "@/app/store/services";
import {
deleteLayoutModelForTab,
getLayoutModelForActiveTab,
@ -9,11 +10,15 @@ import {
getLayoutModelForTabById,
NavigateDirection,
} from "@/layout/index";
import * as services from "@/store/services";
import * as keyutil from "@/util/keyutil";
import * as jotai from "jotai";
const simpleControlShiftAtom = jotai.atom(false);
const globalKeyMap = new Map<string, (waveEvent: WaveKeyboardEvent) => boolean>();
function getSimpleControlShiftAtom() {
return simpleControlShiftAtom;
}
function setControlShift() {
globalStore.set(simpleControlShiftAtom, true);
@ -30,6 +35,22 @@ function unsetControlShift() {
globalStore.set(atoms.controlShiftDelayAtom, false);
}
function shouldDispatchToBlock(): boolean {
if (globalStore.get(atoms.modalOpen)) {
return false;
}
const activeElem = document.activeElement;
if (activeElem != null && activeElem instanceof HTMLElement) {
if (activeElem.tagName == "INPUT" || activeElem.tagName == "TEXTAREA") {
return false;
}
if (activeElem.contentEditable == "true") {
return false;
}
}
return true;
}
function genericClose(tabId: string) {
const tabORef = WOS.makeORef("tab", tabId);
const tabAtom = WOS.getWaveObjectAtom<Tab>(tabORef);
@ -88,18 +109,6 @@ function switchTab(offset: number) {
services.ObjectService.SetActiveTab(newActiveTabId);
}
function appHandleKeyUp(event: KeyboardEvent) {
const waveEvent = keyutil.adaptFromReactOrNativeKeyEvent(event);
if (waveEvent.key === "Control" || waveEvent.key === "Shift") {
unsetControlShift();
}
if (waveEvent.key == "Meta") {
if (waveEvent.control && waveEvent.shift) {
setControlShift();
}
}
}
async function handleCmdN() {
const termBlockDef: BlockDef = {
meta: {
@ -125,74 +134,130 @@ async function handleCmdN() {
}
function appHandleKeyDown(waveEvent: WaveKeyboardEvent): boolean {
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
return handleGlobalWaveKeyboardEvents(waveEvent);
}
function registerControlShiftStateUpdateHandler() {
getApi().onControlShiftStateUpdate((state: boolean) => {
if (state) {
setControlShift();
} else {
// Unset if Meta is pressed
unsetControlShift();
}
return false;
}
const tabId = globalStore.get(atoms.activeTabId);
});
}
// global key handler for now (refactor later)
if (keyutil.checkKeyPressed(waveEvent, "Cmd:]") || keyutil.checkKeyPressed(waveEvent, "Shift:Cmd:]")) {
function registerElectronReinjectKeyHandler() {
getApi().onReinjectKey((event: WaveKeyboardEvent) => {
console.log("reinject key event", event);
const handled = handleGlobalWaveKeyboardEvents(event);
if (handled) {
return;
}
const layoutModel = getLayoutModelForActiveTab();
const focusedNode = globalStore.get(layoutModel.focusedNode);
const blockId = focusedNode?.data?.blockId;
if (blockId != null && shouldDispatchToBlock()) {
const viewModel = getViewModel(blockId);
viewModel?.keyDownHandler?.(event);
}
});
}
function registerGlobalKeys() {
globalKeyMap.set("Cmd:]", () => {
switchTab(1);
return true;
}
if (keyutil.checkKeyPressed(waveEvent, "Cmd:[") || keyutil.checkKeyPressed(waveEvent, "Shift:Cmd:[")) {
});
globalKeyMap.set("Shift:Cmd:]", () => {
switchTab(1);
return true;
});
globalKeyMap.set("Cmd:[", () => {
switchTab(-1);
return true;
}
if (keyutil.checkKeyPressed(waveEvent, "Cmd:n")) {
});
globalKeyMap.set("Shift:Cmd:[", () => {
switchTab(-1);
return true;
});
globalKeyMap.set("Cmd:n", () => {
handleCmdN();
return true;
}
if (keyutil.checkKeyPressed(waveEvent, "Cmd:t")) {
});
globalKeyMap.set("Cmd:i", () => {
// TODO
return true;
});
globalKeyMap.set("Cmd:t", () => {
const workspace = globalStore.get(atoms.workspace);
const newTabName = `T${workspace.tabids.length + 1}`;
services.ObjectService.AddTabToWorkspace(newTabName, true);
return true;
}
for (let idx = 1; idx <= 9; idx++) {
if (keyutil.checkKeyPressed(waveEvent, `Cmd:${idx}`)) {
switchTabAbs(idx);
return true;
}
}
for (let idx = 1; idx <= 9; idx++) {
if (
keyutil.checkKeyPressed(waveEvent, `Ctrl:Shift:c{Digit${idx}}`) ||
keyutil.checkKeyPressed(waveEvent, `Ctrl:Shift:c{Numpad${idx}}`)
) {
switchBlockByBlockNum(idx);
return true;
}
}
if (keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:ArrowUp")) {
switchBlockInDirection(tabId, NavigateDirection.Up);
return true;
}
if (keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:ArrowDown")) {
switchBlockInDirection(tabId, NavigateDirection.Down);
return true;
}
if (keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:ArrowLeft")) {
switchBlockInDirection(tabId, NavigateDirection.Left);
return true;
}
if (keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:ArrowRight")) {
switchBlockInDirection(tabId, NavigateDirection.Right);
return true;
}
if (keyutil.checkKeyPressed(waveEvent, "Cmd:w")) {
// close block, if no more blocks, close tab
});
globalKeyMap.set("Cmd:w", () => {
const tabId = globalStore.get(atoms.activeTabId);
genericClose(tabId);
return true;
});
globalKeyMap.set("Ctrl:Shift:ArrowUp", () => {
const tabId = globalStore.get(atoms.activeTabId);
switchBlockInDirection(tabId, NavigateDirection.Up);
return true;
});
globalKeyMap.set("Ctrl:Shift:ArrowDown", () => {
const tabId = globalStore.get(atoms.activeTabId);
switchBlockInDirection(tabId, NavigateDirection.Down);
return true;
});
globalKeyMap.set("Ctrl:Shift:ArrowLeft", () => {
const tabId = globalStore.get(atoms.activeTabId);
switchBlockInDirection(tabId, NavigateDirection.Left);
return true;
});
globalKeyMap.set("Ctrl:Shift:ArrowRight", () => {
const tabId = globalStore.get(atoms.activeTabId);
switchBlockInDirection(tabId, NavigateDirection.Right);
return true;
});
for (let idx = 1; idx <= 9; idx++) {
globalKeyMap.set(`Cmd:${idx}`, () => {
switchTabAbs(idx);
return true;
});
globalKeyMap.set(`Ctrl:Shift:c{Digit${idx}}`, () => {
switchBlockByBlockNum(idx);
return true;
});
globalKeyMap.set(`Ctrl:Shift:c{Numpad${idx}}`, () => {
switchBlockByBlockNum(idx);
return true;
});
}
return false;
const allKeys = Array.from(globalKeyMap.keys());
// special case keys, handled by web view
allKeys.push("Cmd:l", "Cmd:r", "Cmd:ArrowRight", "Cmd:ArrowLeft");
getApi().registerGlobalWebviewKeys(allKeys);
}
export { appHandleKeyDown, appHandleKeyUp };
// these keyboard events happen *anywhere*, even if you have focus in an input or somewhere else.
function handleGlobalWaveKeyboardEvents(waveEvent: WaveKeyboardEvent): boolean {
for (const key of globalKeyMap.keys()) {
if (keyutil.checkKeyPressed(waveEvent, key)) {
const handler = globalKeyMap.get(key);
if (handler == null) {
return false;
}
return handler(waveEvent);
}
}
}
export {
appHandleKeyDown,
getSimpleControlShiftAtom,
registerControlShiftStateUpdateHandler,
registerElectronReinjectKeyHandler,
registerGlobalKeys,
unsetControlShift,
};

View File

@ -123,7 +123,7 @@ class WshServerType {
}
// command "setconfig" [call]
SetConfigCommand(data: MetaMapType, opts?: RpcOpts): Promise<void> {
SetConfigCommand(data: MetaType, opts?: RpcOpts): Promise<void> {
return WOS.wshServerRpcHelper_call("setconfig", data, opts);
}

View File

@ -1,9 +1,12 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { WOS, globalStore, openLink } from "@/store/global";
import { getApi, openLink } from "@/app/store/global";
import { getSimpleControlShiftAtom } from "@/app/store/keymodel";
import { NodeModel } from "@/layout/index";
import { WOS, globalStore } from "@/store/global";
import * as services from "@/store/services";
import { checkKeyPressed } from "@/util/keyutil";
import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil";
import { fireAndForget } from "@/util/util";
import clsx from "clsx";
import { WebviewTag } from "electron";
@ -27,8 +30,10 @@ export class WebViewModel implements ViewModel {
refreshIcon: jotai.PrimitiveAtom<string>;
webviewRef: React.RefObject<WebviewTag>;
urlInputRef: React.RefObject<HTMLInputElement>;
nodeModel: NodeModel;
constructor(blockId: string) {
constructor(blockId: string, nodeModel: NodeModel) {
this.nodeModel = nodeModel;
this.viewType = "web";
this.blockId = blockId;
this.blockAtom = WOS.getWaveObjectAtom<Block>(`block:${blockId}`);
@ -131,15 +136,19 @@ export class WebViewModel implements ViewModel {
}
}
handleBack(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
e.preventDefault();
e.stopPropagation();
handleBack(e?: React.MouseEvent<HTMLDivElement, MouseEvent>) {
if (e) {
e.preventDefault();
e.stopPropagation();
}
this.webviewRef.current?.goBack();
}
handleForward(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
e.preventDefault();
e.stopPropagation();
handleForward(e?: React.MouseEvent<HTMLDivElement, MouseEvent>) {
if (e) {
e.preventDefault();
e.stopPropagation();
}
this.webviewRef.current?.goForward();
}
@ -165,10 +174,15 @@ export class WebViewModel implements ViewModel {
}
handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
if (event.key === "Enter") {
const waveEvent = adaptFromReactOrNativeKeyEvent(event);
if (checkKeyPressed(waveEvent, "Enter")) {
const url = globalStore.get(this.url);
this.loadUrl(url);
this.urlInputRef.current?.blur();
return;
}
if (checkKeyPressed(waveEvent, "Escape")) {
this.webviewRef.current?.focus();
}
}
@ -268,10 +282,23 @@ export class WebViewModel implements ViewModel {
}
giveFocus(): boolean {
if (this.urlInputRef.current) {
this.urlInputRef.current.focus({ preventScroll: true });
return true;
const ctrlShiftState = globalStore.get(getSimpleControlShiftAtom());
if (ctrlShiftState) {
// this is really weird, we don't get keyup events from webview
const unsubFn = globalStore.sub(getSimpleControlShiftAtom(), () => {
const state = globalStore.get(getSimpleControlShiftAtom());
if (!state) {
unsubFn();
const isStillFocused = globalStore.get(this.nodeModel.isFocused);
if (isStillFocused) {
this.webviewRef.current?.focus();
}
}
});
return false;
}
this.webviewRef.current?.focus();
return true;
}
keyDownHandler(e: WaveKeyboardEvent): boolean {
@ -284,12 +311,20 @@ export class WebViewModel implements ViewModel {
this.webviewRef?.current?.reload();
return true;
}
if (checkKeyPressed(e, "Cmd:ArrowLeft")) {
this.handleBack(null);
return true;
}
if (checkKeyPressed(e, "Cmd:ArrowRight")) {
this.handleForward(null);
return true;
}
return false;
}
}
function makeWebViewModel(blockId: string): WebViewModel {
const webviewModel = new WebViewModel(blockId);
function makeWebViewModel(blockId: string, nodeModel: NodeModel): WebViewModel {
const webviewModel = new WebViewModel(blockId, nodeModel);
return webviewModel;
}
@ -342,6 +377,13 @@ const WebView = memo(({ model }: WebViewProps) => {
console.error(`Failed to load ${e.validatedURL}: ${e.errorDescription}`);
}
};
const webviewFocus = () => {
getApi().setWebviewFocus(webview.getWebContentsId());
model.nodeModel.focusNode();
};
const webviewBlur = () => {
getApi().setWebviewFocus(null);
};
webview.addEventListener("did-navigate-in-page", navigateListener);
webview.addEventListener("did-navigate", navigateListener);
@ -350,6 +392,9 @@ const WebView = memo(({ model }: WebViewProps) => {
webview.addEventListener("new-window", newWindowHandler);
webview.addEventListener("did-fail-load", failLoadHandler);
webview.addEventListener("focus", webviewFocus);
webview.addEventListener("blur", webviewBlur);
// Clean up event listeners on component unmount
return () => {
webview.removeEventListener("did-navigate", navigateListener);
@ -358,6 +403,8 @@ const WebView = memo(({ model }: WebViewProps) => {
webview.removeEventListener("did-fail-load", failLoadHandler);
webview.removeEventListener("did-start-loading", startLoadingHandler);
webview.removeEventListener("did-stop-loading", stopLoadingHandler);
webview.addEventListener("focus", webviewFocus);
webview.addEventListener("blur", webviewBlur);
};
}
}, []);

View File

@ -23,6 +23,7 @@
padding-top: 4px;
padding-bottom: 4px;
margin-left: -4px;
user-select: none;
.widget {
display: flex;

View File

@ -21,6 +21,7 @@ declare global {
reducedMotionPreferenceAtom: jotai.Atom<boolean>;
updaterStatusAtom: jotai.PrimitiveAtom<UpdaterStatus>;
typeAheadModalAtom: jotai.PrimitiveAtom<TypeAheadModalType>;
modalOpen: jotai.PrimitiveAtom<boolean>;
};
type WritableWaveObjectAtom<T extends WaveObj> = jotai.WritableAtom<T, [value: T], void>;
@ -65,6 +66,10 @@ declare global {
installAppUpdate: () => void;
onMenuItemAbout: (callback: () => void) => void;
updateWindowControlsOverlay: (rect: Dimensions) => void;
onReinjectKey: (callback: (waveEvent: WaveKeyboardEvent) => void) => void;
setWebviewFocus: (focusedId: number) => void; // focusedId si the getWebContentsId of the webview
registerGlobalWebviewKeys: (keys: string[]) => void;
onControlShiftStateUpdate: (callback: (state: boolean) => void) => void;
};
type ElectronContextMenuItem = {
@ -97,7 +102,7 @@ declare global {
};
interface WaveKeyboardEvent {
type: string;
type: "keydown" | "keyup" | "keypress" | "unknown";
/**
* Equivalent to KeyboardEvent.key.
*/

View File

@ -0,0 +1,70 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0s
import * as util from "./util";
export function findBlockId(element: HTMLElement): string | null {
let current: HTMLElement = element;
while (current) {
if (current.hasAttribute("data-blockid")) {
return current.getAttribute("data-blockid");
}
current = current.parentElement;
}
return null;
}
export function getElemAsStr(elem: EventTarget) {
if (elem == null) {
return "null";
}
if (!(elem instanceof HTMLElement)) {
if (elem instanceof Text) {
elem = elem.parentElement;
}
if (!(elem instanceof HTMLElement)) {
return "unknown";
}
}
const blockId = findBlockId(elem);
let rtn = elem.tagName.toLowerCase();
if (!util.isBlank(elem.id)) {
rtn += "#" + elem.id;
}
if (!util.isBlank(elem.className)) {
rtn += "." + elem.className;
}
if (blockId != null) {
rtn += ` [${blockId.substring(0, 8)}]`;
}
return rtn;
}
export function hasSelection() {
const sel = document.getSelection();
return sel && sel.rangeCount > 0 && !sel.isCollapsed;
}
export function focusedBlockId(): string {
const focused = document.activeElement;
if (focused instanceof HTMLElement) {
const blockId = findBlockId(focused);
if (blockId) {
return blockId;
}
}
const sel = document.getSelection();
if (sel && sel.anchorNode) {
let anchor = sel.anchorNode;
if (anchor instanceof Text) {
anchor = anchor.parentElement;
}
if (anchor instanceof HTMLElement) {
const blockId = findBlockId(anchor);
if (blockId) {
return blockId;
}
}
}
return null;
}

View File

@ -139,14 +139,24 @@ function adaptFromReactOrNativeKeyEvent(event: React.KeyboardEvent | KeyboardEve
rtn.code = event.code;
rtn.key = event.key;
rtn.location = event.location;
rtn.type = event.type;
if (event.type == "keydown" || event.type == "keyup" || event.type == "keypress") {
rtn.type = event.type;
} else {
rtn.type = "unknown";
}
rtn.repeat = event.repeat;
return rtn;
}
function adaptFromElectronKeyEvent(event: any): WaveKeyboardEvent {
let rtn: WaveKeyboardEvent = {} as WaveKeyboardEvent;
rtn.type = event.type;
if (event.type == "keyUp") {
rtn.type = "keyup";
} else if (event.type == "keyDown") {
rtn.type = "keydown";
} else {
rtn.type = "unknown";
}
rtn.control = event.control;
rtn.cmd = PLATFORM == PlatformMacOS ? event.meta : event.alt;
rtn.option = PLATFORM == PlatformMacOS ? event.alt : event.meta;

View File

@ -1,6 +1,11 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import {
registerControlShiftStateUpdateHandler,
registerElectronReinjectKeyHandler,
registerGlobalKeys,
} from "@/app/store/keymodel";
import { WshServer } from "@/app/store/wshserver";
import {
atoms,
@ -57,6 +62,9 @@ document.addEventListener("DOMContentLoaded", async () => {
initWS();
await loadConnStatus();
subscribeToConnEvents();
registerGlobalKeys();
registerElectronReinjectKeyHandler();
registerControlShiftStateUpdateHandler();
const fullConfig = await services.FileService.GetFullConfig();
console.log("fullconfig", fullConfig);
globalStore.set(atoms.fullConfigAtom, fullConfig);

View File

@ -426,7 +426,8 @@ func GenerateWshServerMethod_ResponseStream(methodDecl *wshrpc.WshRpcMethodDecl,
}
genRespType := fmt.Sprintf("AsyncGenerator<%s, void, boolean>", respType)
if methodDecl.CommandDataType != nil {
sb.WriteString(fmt.Sprintf(" %s(data: %s, opts?: RpcOpts): %s {\n", methodDecl.MethodName, methodDecl.CommandDataType.Name(), genRespType))
cmdDataTsName, _ := TypeToTSType(methodDecl.CommandDataType, tsTypesMap)
sb.WriteString(fmt.Sprintf(" %s(data: %s, opts?: RpcOpts): %s {\n", methodDecl.MethodName, cmdDataTsName, genRespType))
} else {
sb.WriteString(fmt.Sprintf(" %s(opts?: RpcOpts): %s {\n", methodDecl.MethodName, genRespType))
}
@ -448,7 +449,8 @@ func GenerateWshServerMethod_Call(methodDecl *wshrpc.WshRpcMethodDecl, tsTypesMa
dataName = "data"
}
if methodDecl.CommandDataType != nil {
sb.WriteString(fmt.Sprintf(" %s(data: %s, opts?: RpcOpts): %s {\n", methodDecl.MethodName, methodDecl.CommandDataType.Name(), rtnType))
cmdDataTsName, _ := TypeToTSType(methodDecl.CommandDataType, tsTypesMap)
sb.WriteString(fmt.Sprintf(" %s(data: %s, opts?: RpcOpts): %s {\n", methodDecl.MethodName, cmdDataTsName, rtnType))
} else {
sb.WriteString(fmt.Sprintf(" %s(opts?: RpcOpts): %s {\n", methodDecl.MethodName, rtnType))
}