mirror of
https://github.com/wavetermdev/backup.git
synced 2026-08-05 13:57:07 -07:00
integrate part of keyutil, and implement tab and block movement with keyboard (#70)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as keyutil from "@/util/keyutil";
|
||||
import * as electron from "electron";
|
||||
import fs from "fs";
|
||||
import * as child_process from "node:child_process";
|
||||
@@ -37,6 +38,7 @@ let unameArch: string = process.arch;
|
||||
if (unameArch == "x64") {
|
||||
unameArch = "amd64";
|
||||
}
|
||||
keyutil.setKeyUtilPlatform(unamePlatform);
|
||||
|
||||
function getBaseHostPort(): string {
|
||||
if (isDev) {
|
||||
@@ -386,6 +388,10 @@ electron.ipcMain.on("isDevServer", (event) => {
|
||||
event.returnValue = isDevServer;
|
||||
});
|
||||
|
||||
electron.ipcMain.on("getPlatform", (event) => {
|
||||
event.returnValue = unamePlatform;
|
||||
});
|
||||
|
||||
electron.ipcMain.on("getCursorPoint", (event) => {
|
||||
const window = electron.BrowserWindow.fromWebContents(event.sender);
|
||||
const screenPoint = electron.screen.getCursorScreenPoint();
|
||||
|
||||
@@ -6,6 +6,7 @@ let { contextBridge, ipcRenderer } = require("electron");
|
||||
contextBridge.exposeInMainWorld("api", {
|
||||
isDev: () => ipcRenderer.sendSync("isDev"),
|
||||
isDevServer: () => ipcRenderer.sendSync("isDevServer"),
|
||||
getPlatform: () => ipcRenderer.sendSync("getPlatform"),
|
||||
getCursorPoint: () => ipcRenderer.sendSync("getCursorPoint"),
|
||||
openNewWindow: () => ipcRenderer.send("openNewWindow"),
|
||||
contextEditMenu: (position, opts) => ipcRenderer.send("context-editmenu", position, opts),
|
||||
|
||||
+186
-3
@@ -2,11 +2,14 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { Workspace } from "@/app/workspace/workspace";
|
||||
import { atoms, getApi, globalStore } from "@/store/global";
|
||||
import { getLayoutStateAtomForTab, globalLayoutTransformsMap } from "@/faraday/lib/layoutAtom";
|
||||
import type { LayoutTreeState } from "@/faraday/lib/model";
|
||||
import { WOS, atoms, getApi, globalStore, setBlockFocus } from "@/store/global";
|
||||
import * as services from "@/store/services";
|
||||
import * as keyutil from "@/util/keyutil";
|
||||
import * as util from "@/util/util";
|
||||
import * as jotai from "jotai";
|
||||
import { Provider } from "jotai";
|
||||
|
||||
import * as React from "react";
|
||||
import { DndProvider } from "react-dnd";
|
||||
import { HTML5Backend } from "react-dnd-html5-backend";
|
||||
import { CenteredDiv } from "./element/quickelems";
|
||||
@@ -15,6 +18,7 @@ import "overlayscrollbars/overlayscrollbars.css";
|
||||
import "./app.less";
|
||||
|
||||
const App = () => {
|
||||
let Provider = jotai.Provider;
|
||||
return (
|
||||
<Provider store={globalStore}>
|
||||
<AppInner />
|
||||
@@ -45,9 +49,145 @@ function handleContextMenu(e: React.MouseEvent<HTMLDivElement>) {
|
||||
}
|
||||
}
|
||||
|
||||
function switchTab(offset: number) {
|
||||
console.log("switch tab!", offset);
|
||||
const ws = globalStore.get(atoms.workspace);
|
||||
const activeTabId = globalStore.get(atoms.tabAtom).oid;
|
||||
let tabIdx = -1;
|
||||
for (let i = 0; i < ws.tabids.length; i++) {
|
||||
if (ws.tabids[i] == activeTabId) {
|
||||
tabIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (tabIdx == -1) {
|
||||
return;
|
||||
}
|
||||
tabIdx = (tabIdx + offset) % ws.tabids.length;
|
||||
const newActiveTabId = ws.tabids[tabIdx];
|
||||
services.ObjectService.SetActiveTab(newActiveTabId);
|
||||
}
|
||||
|
||||
function findLeafIdFromBlockId(layoutTree: LayoutTreeState<TabLayoutData>, blockId: string): string {
|
||||
if (layoutTree?.leafs == null) {
|
||||
return null;
|
||||
}
|
||||
for (let leaf of layoutTree.leafs) {
|
||||
if (leaf.data.blockId == blockId) {
|
||||
return leaf.id;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
var transformRegexp = /translate\(\s*([0-9.]+)px\s*,\s*([0-9.]+)px\)/;
|
||||
|
||||
function parseFloatFromCSS(s: string | number): number {
|
||||
if (typeof s == "number") {
|
||||
return s;
|
||||
}
|
||||
return parseFloat(s);
|
||||
}
|
||||
|
||||
function readBoundsFromTransform(fullTransform: React.CSSProperties): Bounds {
|
||||
const transformProp = fullTransform.transform;
|
||||
if (transformProp == null || fullTransform.width == null || fullTransform.height == null) {
|
||||
return null;
|
||||
}
|
||||
const m = transformRegexp.exec(transformProp);
|
||||
if (m == null) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
x: parseFloat(m[1]),
|
||||
y: parseFloat(m[2]),
|
||||
width: parseFloatFromCSS(fullTransform.width),
|
||||
height: parseFloatFromCSS(fullTransform.height),
|
||||
};
|
||||
}
|
||||
|
||||
function boundsMapMaxX(m: Map<string, Bounds>): number {
|
||||
let max = 0;
|
||||
for (let p of m.values()) {
|
||||
if (p.x + p.width > max) {
|
||||
max = p.x + p.width;
|
||||
}
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
function boundsMapMaxY(m: Map<string, Bounds>): number {
|
||||
let max = 0;
|
||||
for (let p of m.values()) {
|
||||
if (p.y + p.height > max) {
|
||||
max = p.y + p.height;
|
||||
}
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
function findBlockAtPoint(m: Map<string, Bounds>, p: Point): string {
|
||||
for (let [blockId, bounds] of m.entries()) {
|
||||
if (p.x >= bounds.x && p.x <= bounds.x + bounds.width && p.y >= bounds.y && p.y <= bounds.y + bounds.height) {
|
||||
return blockId;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function switchBlock(tabId: string, offsetX: number, offsetY: number) {
|
||||
console.log("switch block", offsetX, offsetY);
|
||||
if (offsetY == 0 && offsetX == 0) {
|
||||
return;
|
||||
}
|
||||
const tabAtom = WOS.getWaveObjectAtom<Tab>(WOS.makeORef("tab", tabId));
|
||||
const transforms = globalLayoutTransformsMap.get(tabId);
|
||||
if (transforms == null) {
|
||||
return;
|
||||
}
|
||||
const layoutTreeState = globalStore.get(getLayoutStateAtomForTab(tabId, tabAtom));
|
||||
const curBlockId = globalStore.get(atoms.waveWindow).activeblockid;
|
||||
const curBlockLeafId = findLeafIdFromBlockId(layoutTreeState, curBlockId);
|
||||
if (curBlockLeafId == null) {
|
||||
return;
|
||||
}
|
||||
const blockPos = readBoundsFromTransform(transforms[curBlockLeafId]);
|
||||
if (blockPos == null) {
|
||||
return;
|
||||
}
|
||||
var blockPositions: Map<string, Bounds> = new Map();
|
||||
for (let leaf of layoutTreeState.leafs) {
|
||||
if (leaf.id == curBlockLeafId) {
|
||||
continue;
|
||||
}
|
||||
const pos = readBoundsFromTransform(transforms[leaf.id]);
|
||||
if (pos != null) {
|
||||
blockPositions.set(leaf.data.blockId, pos);
|
||||
}
|
||||
}
|
||||
const maxX = boundsMapMaxX(blockPositions);
|
||||
const maxY = boundsMapMaxY(blockPositions);
|
||||
const moveAmount = 10;
|
||||
let curX = blockPos.x + 1;
|
||||
let curY = blockPos.y + 1;
|
||||
while (true) {
|
||||
curX += offsetX * moveAmount;
|
||||
curY += offsetY * moveAmount;
|
||||
if (curX < 0 || curX > maxX || curY < 0 || curY > maxY) {
|
||||
return;
|
||||
}
|
||||
const blockId = findBlockAtPoint(blockPositions, { x: curX, y: curY });
|
||||
if (blockId != null) {
|
||||
setBlockFocus(blockId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const AppInner = () => {
|
||||
const client = jotai.useAtomValue(atoms.client);
|
||||
const windowData = jotai.useAtomValue(atoms.waveWindow);
|
||||
const tabId = jotai.useAtomValue(atoms.activeTabId);
|
||||
if (client == null || windowData == null) {
|
||||
return (
|
||||
<div className="mainapp">
|
||||
@@ -56,6 +196,49 @@ const AppInner = () => {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function handleKeyDown(ev: KeyboardEvent) {
|
||||
let waveEvent = keyutil.adaptFromReactOrNativeKeyEvent(ev);
|
||||
const rtn = handleKeyDownInternal(waveEvent);
|
||||
if (rtn) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
}
|
||||
}
|
||||
function handleKeyDownInternal(waveEvent: WaveKeyboardEvent): boolean {
|
||||
// global key handler for now (refactor later)
|
||||
if (keyutil.checkKeyPressed(waveEvent, "Cmd:]")) {
|
||||
switchTab(1);
|
||||
return true;
|
||||
}
|
||||
if (keyutil.checkKeyPressed(waveEvent, "Cmd:[")) {
|
||||
switchTab(-1);
|
||||
return true;
|
||||
}
|
||||
if (keyutil.checkKeyPressed(waveEvent, "Cmd:ArrowUp")) {
|
||||
switchBlock(tabId, 0, -1);
|
||||
return true;
|
||||
}
|
||||
if (keyutil.checkKeyPressed(waveEvent, "Cmd:ArrowDown")) {
|
||||
switchBlock(tabId, 0, 1);
|
||||
return true;
|
||||
}
|
||||
if (keyutil.checkKeyPressed(waveEvent, "Cmd:ArrowLeft")) {
|
||||
switchBlock(tabId, -1, 0);
|
||||
return true;
|
||||
}
|
||||
if (keyutil.checkKeyPressed(waveEvent, "Cmd:ArrowRight")) {
|
||||
switchBlock(tabId, 1, 0);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
React.useEffect(() => {
|
||||
const staticKeyDownHandler = handleKeyDown;
|
||||
document.addEventListener("keydown", staticKeyDownHandler);
|
||||
return () => {
|
||||
document.removeEventListener("keydown", staticKeyDownHandler);
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<div className="mainapp" onContextMenu={handleContextMenu}>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
|
||||
@@ -8,10 +8,9 @@ import { TerminalView } from "@/app/view/term/term";
|
||||
import { ErrorBoundary } from "@/element/errorboundary";
|
||||
import { CenteredDiv } from "@/element/quickelems";
|
||||
import { ContextMenuModel } from "@/store/contextmenu";
|
||||
import { atoms, globalStore, useBlockAtom } from "@/store/global";
|
||||
import { atoms, setBlockFocus, useBlockAtom } from "@/store/global";
|
||||
import * as WOS from "@/store/wos";
|
||||
import clsx from "clsx";
|
||||
import { produce } from "immer";
|
||||
import * as jotai from "jotai";
|
||||
import * as React from "react";
|
||||
|
||||
@@ -208,17 +207,6 @@ const BlockFrame = (props: BlockFrameProps) => {
|
||||
return <BlockFrame_Tech {...props} />;
|
||||
};
|
||||
|
||||
function setBlockFocus(blockId: string) {
|
||||
let winData = globalStore.get(atoms.waveWindow);
|
||||
if (winData.activeblockid === blockId) {
|
||||
return;
|
||||
}
|
||||
winData = produce(winData, (draft) => {
|
||||
draft.activeblockid = blockId;
|
||||
});
|
||||
WOS.setObjectValue(winData, globalStore.set, true);
|
||||
}
|
||||
|
||||
const Block = ({ blockId, onClose, dragHandleRef }: BlockProps) => {
|
||||
let blockElem: JSX.Element = null;
|
||||
const focusElemRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { LayoutTreeActionType, LayoutTreeInsertNodeAction, newLayoutNode } from
|
||||
import { getLayoutStateAtomForTab } from "@/faraday/lib/layoutAtom";
|
||||
import { layoutTreeStateReducer } from "@/faraday/lib/layoutState";
|
||||
|
||||
import { produce } from "immer";
|
||||
import * as jotai from "jotai";
|
||||
import * as rxjs from "rxjs";
|
||||
import * as services from "./services";
|
||||
@@ -64,6 +65,13 @@ const tabAtom: jotai.Atom<Tab> = jotai.atom((get) => {
|
||||
}
|
||||
return WOS.getObjectValue(WOS.makeORef("tab", windowData.activetabid), get);
|
||||
});
|
||||
const activeTabIdAtom: jotai.Atom<string> = jotai.atom((get) => {
|
||||
const windowData = get(windowDataAtom);
|
||||
if (windowData == null) {
|
||||
return null;
|
||||
}
|
||||
return windowData.activetabid;
|
||||
});
|
||||
|
||||
const atoms = {
|
||||
// initialized in wave.ts (will not be null inside of application)
|
||||
@@ -75,6 +83,7 @@ const atoms = {
|
||||
workspace: workspaceAtom,
|
||||
settingsConfigAtom: settingsConfigAtom,
|
||||
tabAtom: tabAtom,
|
||||
activeTabId: activeTabIdAtom,
|
||||
};
|
||||
|
||||
// key is "eventType" or "eventType|oref"
|
||||
@@ -285,6 +294,17 @@ async function fetchWaveFile(
|
||||
return { data: new Uint8Array(data), fileInfo };
|
||||
}
|
||||
|
||||
function setBlockFocus(blockId: string) {
|
||||
let winData = globalStore.get(atoms.waveWindow);
|
||||
if (winData.activeblockid === blockId) {
|
||||
return;
|
||||
}
|
||||
winData = produce(winData, (draft) => {
|
||||
draft.activeblockid = blockId;
|
||||
});
|
||||
WOS.setObjectValue(winData, globalStore.set, true);
|
||||
}
|
||||
|
||||
export {
|
||||
WOS,
|
||||
atoms,
|
||||
@@ -299,6 +319,7 @@ export {
|
||||
globalWS,
|
||||
initWS,
|
||||
sendWSCommand,
|
||||
setBlockFocus,
|
||||
useBlockAtom,
|
||||
useBlockCache,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Button } from "@/element/button";
|
||||
import { ContextMenuModel } from "@/store/contextmenu";
|
||||
import * as services from "@/store/services";
|
||||
import * as WOS from "@/store/wos";
|
||||
import { clsx } from "clsx";
|
||||
@@ -12,7 +13,7 @@ interface TabProps {
|
||||
isBeforeActive: boolean;
|
||||
isDragging: boolean;
|
||||
onSelect: () => void;
|
||||
onClose: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
onClose: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
|
||||
onDragStart: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||
onLoaded: () => void;
|
||||
}
|
||||
@@ -41,8 +42,10 @@ const Tab = forwardRef<HTMLDivElement, TabProps>(
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleDoubleClick = (event) => {
|
||||
event.stopPropagation();
|
||||
const handleDoubleClick = (event?: React.MouseEvent<any, any>) => {
|
||||
if (event != null) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
setIsEditable(true);
|
||||
editableTimeoutRef.current = setTimeout(() => {
|
||||
if (editableRef.current) {
|
||||
@@ -102,12 +105,33 @@ const Tab = forwardRef<HTMLDivElement, TabProps>(
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
function handleContextMenu(e: React.MouseEvent<HTMLElement>) {
|
||||
let menu: ContextMenuItem[] = [];
|
||||
menu.push({
|
||||
label: "Edit Name",
|
||||
click: () => {
|
||||
handleDoubleClick(null);
|
||||
},
|
||||
});
|
||||
menu.push({
|
||||
type: "separator",
|
||||
});
|
||||
menu.push({
|
||||
label: "Close",
|
||||
click: () => {
|
||||
onClose(e);
|
||||
},
|
||||
});
|
||||
ContextMenuModel.showContextMenu(menu, e);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={clsx("tab", { active, isDragging, "before-active": isBeforeActive })}
|
||||
onMouseDown={onDragStart}
|
||||
onClick={onSelect}
|
||||
onContextMenu={handleContextMenu}
|
||||
data-tab-id={id}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -425,7 +425,7 @@ const TabBar = ({ workspace }: TabBarProps) => {
|
||||
}, 30);
|
||||
};
|
||||
|
||||
const handleCloseTab = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>, tabId: string) => {
|
||||
const handleCloseTab = (event: React.MouseEvent<HTMLElement, MouseEvent>, tabId: string) => {
|
||||
event.stopPropagation();
|
||||
services.WindowService.CloseTab(tabId);
|
||||
deleteLayoutStateAtomForTab(tabId);
|
||||
|
||||
@@ -64,6 +64,7 @@ const TabContent = ({ tabId }: { tabId: string }) => {
|
||||
<div className="tabcontent">
|
||||
<TileLayout
|
||||
key={tabId}
|
||||
tabId={tabId}
|
||||
renderContent={renderBlock}
|
||||
renderPreview={renderPreview}
|
||||
layoutTreeStateAtom={layoutStateAtom}
|
||||
|
||||
@@ -188,7 +188,7 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
|
||||
return false;
|
||||
}
|
||||
const b64data = btoa(asciiVal);
|
||||
const inputCmd: BlockInputCommand = { command: "controller:input", inputdata64: b64data };
|
||||
const inputCmd: BlockInputCommand = { command: "controller:input", inputdata64: b64data, blockid: blockId };
|
||||
services.BlockService.SendCommand(blockId, inputCmd);
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -98,7 +98,11 @@ function TermSticker({ sticker, config }: { sticker: StickerType; config: Sticke
|
||||
console.log("clickHandler", sticker.clickcmd, sticker.clickblockdef);
|
||||
if (sticker.clickcmd) {
|
||||
const b64data = btoa(sticker.clickcmd);
|
||||
const inputCmd: BlockInputCommand = { command: "controller:input", inputdata64: b64data };
|
||||
const inputCmd: BlockInputCommand = {
|
||||
command: "controller:input",
|
||||
inputdata64: b64data,
|
||||
blockid: config.blockId,
|
||||
};
|
||||
services.BlockService.SendCommand(config.blockId, inputCmd);
|
||||
}
|
||||
if (sticker.clickblockdef) {
|
||||
|
||||
@@ -19,7 +19,7 @@ import React, {
|
||||
import { DropTargetMonitor, useDrag, useDragLayer, useDrop } from "react-dnd";
|
||||
import { debounce, throttle } from "throttle-debounce";
|
||||
import { useDevicePixelRatio } from "use-device-pixel-ratio";
|
||||
import { useLayoutTreeStateReducerAtom } from "./layoutAtom";
|
||||
import { globalLayoutTransformsMap, useLayoutTreeStateReducerAtom } from "./layoutAtom";
|
||||
import { findNode } from "./layoutNode";
|
||||
import {
|
||||
ContentRenderer,
|
||||
@@ -60,6 +60,11 @@ export interface TileLayoutProps<T> {
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* tabId this TileLayout is associated with
|
||||
*/
|
||||
tabId: string;
|
||||
|
||||
/**
|
||||
* A callback for getting the cursor point in reference to the current window. This removes Electron as a runtime dependency, allowing for better integration with Storybook.
|
||||
* @returns The cursor position relative to the current window.
|
||||
@@ -72,6 +77,7 @@ const DragPreviewHeight = 300;
|
||||
|
||||
export const TileLayout = <T,>({
|
||||
layoutTreeStateAtom,
|
||||
tabId,
|
||||
className,
|
||||
renderContent,
|
||||
renderPreview,
|
||||
@@ -117,7 +123,12 @@ export const TileLayout = <T,>({
|
||||
[nodeRefs, setNodeRefs]
|
||||
);
|
||||
const [overlayTransform, setOverlayTransform] = useState<CSSProperties>();
|
||||
const [layoutLeafTransforms, setLayoutLeafTransforms] = useState<Record<string, CSSProperties>>({});
|
||||
const [layoutLeafTransforms, setLayoutLeafTransformsRaw] = useState<Record<string, CSSProperties>>({});
|
||||
|
||||
const setLayoutLeafTransforms = (transforms: Record<string, CSSProperties>) => {
|
||||
globalLayoutTransformsMap.set(tabId, transforms);
|
||||
setLayoutLeafTransformsRaw(transforms);
|
||||
};
|
||||
|
||||
const { activeDrag, dragClientOffset } = useDragLayer((monitor) => ({
|
||||
activeDrag: monitor.isDragging(),
|
||||
|
||||
@@ -14,6 +14,9 @@ import {
|
||||
WritableLayoutTreeStateAtom,
|
||||
} from "./model.js";
|
||||
|
||||
// map from tabId => layout transforms (sizes and positions of the nodes)
|
||||
let globalLayoutTransformsMap = new Map<string, Record<string, React.CSSProperties>>();
|
||||
|
||||
/**
|
||||
* Creates a new layout tree state wrapped as an atom.
|
||||
* @param rootNode The root node for the tree.
|
||||
@@ -126,3 +129,5 @@ export function deleteLayoutStateAtomForTab(tabId: string) {
|
||||
tabLayoutAtomCache.delete(tabId);
|
||||
}
|
||||
}
|
||||
|
||||
export { globalLayoutTransformsMap };
|
||||
|
||||
Vendored
+64
@@ -11,6 +11,13 @@ declare global {
|
||||
onlyPaste?: boolean;
|
||||
};
|
||||
|
||||
type Bounds = {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
type ElectronApi = {
|
||||
/**
|
||||
* Determines whether the current app instance is a development build.
|
||||
@@ -28,6 +35,8 @@ declare global {
|
||||
*/
|
||||
getCursorPoint: () => Electron.Point;
|
||||
|
||||
getPlatform: () => NodeJS.Platform;
|
||||
|
||||
contextEditMenu: (position: { x: number; y: number }, opts: ContextMenuOpts) => void;
|
||||
showContextMenu: (menu: ElectronContextMenuItem[], position: { x: number; y: number }) => void;
|
||||
onContextMenuClick: (callback: (id: string) => void) => void;
|
||||
@@ -49,6 +58,61 @@ declare global {
|
||||
submenu?: ContextMenuItem[];
|
||||
};
|
||||
|
||||
type KeyPressDecl = {
|
||||
mods: {
|
||||
Cmd?: boolean;
|
||||
Option?: boolean;
|
||||
Shift?: boolean;
|
||||
Ctrl?: boolean;
|
||||
Alt?: boolean;
|
||||
Meta?: boolean;
|
||||
};
|
||||
key: string;
|
||||
keyType: string;
|
||||
};
|
||||
|
||||
interface WaveKeyboardEvent {
|
||||
type: string;
|
||||
/**
|
||||
* Equivalent to KeyboardEvent.key.
|
||||
*/
|
||||
key: string;
|
||||
/**
|
||||
* Equivalent to KeyboardEvent.code.
|
||||
*/
|
||||
code: string;
|
||||
/**
|
||||
* Equivalent to KeyboardEvent.shiftKey.
|
||||
*/
|
||||
shift: boolean;
|
||||
/**
|
||||
* Equivalent to KeyboardEvent.controlKey.
|
||||
*/
|
||||
control: boolean;
|
||||
/**
|
||||
* Equivalent to KeyboardEvent.altKey.
|
||||
*/
|
||||
alt: boolean;
|
||||
/**
|
||||
* Equivalent to KeyboardEvent.metaKey.
|
||||
*/
|
||||
meta: boolean;
|
||||
/**
|
||||
* cmd is special, on mac it is meta, on windows it is alt
|
||||
*/
|
||||
cmd: boolean;
|
||||
/**
|
||||
* option is special, on mac it is alt, on windows it is meta
|
||||
*/
|
||||
option: boolean;
|
||||
|
||||
repeat: boolean;
|
||||
/**
|
||||
* Equivalent to KeyboardEvent.location.
|
||||
*/
|
||||
location: number;
|
||||
}
|
||||
|
||||
type SubjectWithRef<T> = rxjs.Subject<T> & { refCount: number; release: () => void };
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
const KeyTypeCodeRegex = /c{(.*)}/;
|
||||
const KeyTypeKey = "key";
|
||||
const KeyTypeCode = "code";
|
||||
|
||||
let PLATFORM: NodeJS.Platform = "darwin";
|
||||
const PlatformMacOS = "darwin";
|
||||
|
||||
function setKeyUtilPlatform(platform: NodeJS.Platform) {
|
||||
PLATFORM = platform;
|
||||
}
|
||||
|
||||
function parseKey(key: string): { key: string; type: string } {
|
||||
let regexMatch = key.match(KeyTypeCodeRegex);
|
||||
if (regexMatch != null && regexMatch.length > 1) {
|
||||
let code = regexMatch[1];
|
||||
return { key: code, type: KeyTypeCode };
|
||||
} else if (regexMatch != null) {
|
||||
console.log("error: regexMatch is not null yet there is no captured group: ", regexMatch, key);
|
||||
}
|
||||
return { key: key, type: KeyTypeKey };
|
||||
}
|
||||
|
||||
function parseKeyDescription(keyDescription: string): KeyPressDecl {
|
||||
let rtn = { key: "", mods: {} } as KeyPressDecl;
|
||||
let keys = keyDescription.replace(/[()]/g, "").split(":");
|
||||
for (let key of keys) {
|
||||
if (key == "Cmd") {
|
||||
rtn.mods.Cmd = true;
|
||||
} else if (key == "Shift") {
|
||||
rtn.mods.Shift = true;
|
||||
} else if (key == "Ctrl") {
|
||||
rtn.mods.Ctrl = true;
|
||||
} else if (key == "Option") {
|
||||
rtn.mods.Option = true;
|
||||
} else if (key == "Alt") {
|
||||
rtn.mods.Alt = true;
|
||||
} else if (key == "Meta") {
|
||||
rtn.mods.Meta = true;
|
||||
} else {
|
||||
let { key: parsedKey, type: keyType } = parseKey(key);
|
||||
rtn.key = parsedKey;
|
||||
rtn.keyType = keyType;
|
||||
if (rtn.keyType == KeyTypeKey && key.length == 1) {
|
||||
// check for if key is upper case
|
||||
// TODO what about unicode upper case?
|
||||
if (/[A-Z]/.test(key.charAt(0))) {
|
||||
// this key is an upper case A - Z - we should apply the shift key, even if it wasn't specified
|
||||
rtn.mods.Shift = true;
|
||||
} else if (key == " ") {
|
||||
rtn.key = "Space";
|
||||
// we allow " " and "Space" to be mapped to Space key
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return rtn;
|
||||
}
|
||||
|
||||
function notMod(keyPressMod: boolean, eventMod: boolean) {
|
||||
return (keyPressMod && !eventMod) || (eventMod && !keyPressMod);
|
||||
}
|
||||
|
||||
function checkKeyPressed(event: WaveKeyboardEvent, keyDescription: string): boolean {
|
||||
let keyPress = parseKeyDescription(keyDescription);
|
||||
if (!keyPress.mods.Alt && notMod(keyPress.mods.Option, event.option)) {
|
||||
return false;
|
||||
}
|
||||
if (!keyPress.mods.Meta && notMod(keyPress.mods.Cmd, event.cmd)) {
|
||||
return false;
|
||||
}
|
||||
if (notMod(keyPress.mods.Shift, event.shift)) {
|
||||
return false;
|
||||
}
|
||||
if (notMod(keyPress.mods.Ctrl, event.control)) {
|
||||
return false;
|
||||
}
|
||||
if (keyPress.mods.Alt && !event.alt) {
|
||||
return false;
|
||||
}
|
||||
if (keyPress.mods.Meta && !event.meta) {
|
||||
return false;
|
||||
}
|
||||
let eventKey = "";
|
||||
let descKey = keyPress.key;
|
||||
if (keyPress.keyType == KeyTypeCode) {
|
||||
eventKey = event.code;
|
||||
}
|
||||
if (keyPress.keyType == KeyTypeKey) {
|
||||
eventKey = event.key;
|
||||
if (eventKey.length == 1 && /[A-Z]/.test(eventKey.charAt(0))) {
|
||||
// key is upper case A-Z, this means shift is applied, we want to allow
|
||||
// "Shift:e" as well as "Shift:E" or "E"
|
||||
eventKey = eventKey.toLocaleLowerCase();
|
||||
descKey = descKey.toLocaleLowerCase();
|
||||
} else if (eventKey == " ") {
|
||||
eventKey = "Space";
|
||||
// a space key is shown as " ", we want users to be able to set space key as "Space" or " ", whichever they prefer
|
||||
}
|
||||
}
|
||||
if (descKey != eventKey) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function adaptFromReactOrNativeKeyEvent(event: React.KeyboardEvent | KeyboardEvent): WaveKeyboardEvent {
|
||||
let rtn: WaveKeyboardEvent = {} as WaveKeyboardEvent;
|
||||
rtn.control = event.ctrlKey;
|
||||
rtn.shift = event.shiftKey;
|
||||
rtn.cmd = PLATFORM == PlatformMacOS ? event.metaKey : event.altKey;
|
||||
rtn.option = PLATFORM == PlatformMacOS ? event.altKey : event.metaKey;
|
||||
rtn.meta = event.metaKey;
|
||||
rtn.alt = event.altKey;
|
||||
rtn.code = event.code;
|
||||
rtn.key = event.key;
|
||||
rtn.location = event.location;
|
||||
rtn.type = event.type;
|
||||
rtn.repeat = event.repeat;
|
||||
return rtn;
|
||||
}
|
||||
|
||||
function adaptFromElectronKeyEvent(event: any): WaveKeyboardEvent {
|
||||
let rtn: WaveKeyboardEvent = {} as WaveKeyboardEvent;
|
||||
rtn.type = event.type;
|
||||
rtn.control = event.control;
|
||||
rtn.cmd = PLATFORM == PlatformMacOS ? event.meta : event.alt;
|
||||
rtn.option = PLATFORM == PlatformMacOS ? event.alt : event.meta;
|
||||
rtn.meta = event.meta;
|
||||
rtn.alt = event.alt;
|
||||
rtn.shift = event.shift;
|
||||
rtn.repeat = event.isAutoRepeat;
|
||||
rtn.location = event.location;
|
||||
rtn.code = event.code;
|
||||
rtn.key = event.key;
|
||||
return rtn;
|
||||
}
|
||||
|
||||
export {
|
||||
adaptFromElectronKeyEvent,
|
||||
adaptFromReactOrNativeKeyEvent,
|
||||
checkKeyPressed,
|
||||
parseKeyDescription,
|
||||
setKeyUtilPlatform,
|
||||
};
|
||||
+4
-1
@@ -1,9 +1,10 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { atoms, globalStore, globalWS, initWS } from "@/store/global";
|
||||
import { atoms, getApi, globalStore, globalWS, initWS } from "@/store/global";
|
||||
import * as services from "@/store/services";
|
||||
import * as WOS from "@/store/wos";
|
||||
import * as keyutil from "@/util/keyutil";
|
||||
import * as React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { App } from "./app/app";
|
||||
@@ -16,6 +17,8 @@ let clientId = urlParams.get("clientid");
|
||||
console.log("Wave Starting");
|
||||
console.log("clientid", clientId, "windowid", windowId);
|
||||
|
||||
keyutil.setKeyUtilPlatform(getApi().getPlatform());
|
||||
|
||||
loadFonts();
|
||||
initWS();
|
||||
(window as any).globalWS = globalWS;
|
||||
|
||||
Reference in New Issue
Block a user