2024-05-13 23:40:18 +02:00
|
|
|
// Copyright 2024, Command Line Inc.
|
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
2024-08-15 03:40:41 +02:00
|
|
|
import { useWaveObjectValue } from "@/app/store/wos";
|
2024-05-28 21:12:28 +02:00
|
|
|
import { Workspace } from "@/app/workspace/workspace";
|
2024-08-16 20:40:10 +02:00
|
|
|
import { deleteLayoutModelForTab, getLayoutModelForTab } from "@/layout/index";
|
2024-06-25 02:44:31 +02:00
|
|
|
import { ContextMenuModel } from "@/store/contextmenu";
|
2024-08-19 23:16:09 +02:00
|
|
|
import { PLATFORM, WOS, atoms, getApi, globalStore, setBlockFocus } from "@/store/global";
|
2024-06-21 21:32:38 +02:00
|
|
|
import * as services from "@/store/services";
|
2024-08-15 03:40:41 +02:00
|
|
|
import { getWebServerEndpoint } from "@/util/endpoints";
|
2024-06-21 21:32:38 +02:00
|
|
|
import * as keyutil from "@/util/keyutil";
|
2024-06-20 00:42:19 +02:00
|
|
|
import * as util from "@/util/util";
|
2024-08-19 23:16:09 +02:00
|
|
|
import useResizeObserver from "@react-hook/resize-observer";
|
2024-08-15 03:40:41 +02:00
|
|
|
import clsx from "clsx";
|
|
|
|
import Color from "color";
|
2024-07-29 20:55:10 +02:00
|
|
|
import * as csstree from "css-tree";
|
2024-05-10 05:24:24 +02:00
|
|
|
import * as jotai from "jotai";
|
2024-08-15 03:40:41 +02:00
|
|
|
import "overlayscrollbars/overlayscrollbars.css";
|
2024-06-21 21:32:38 +02:00
|
|
|
import * as React from "react";
|
2024-06-04 22:05:44 +02:00
|
|
|
import { DndProvider } from "react-dnd";
|
|
|
|
import { HTML5Backend } from "react-dnd-html5-backend";
|
2024-08-19 23:16:09 +02:00
|
|
|
import { debounce } from "throttle-debounce";
|
2024-06-21 05:04:00 +02:00
|
|
|
import "./app.less";
|
2024-08-15 03:40:41 +02:00
|
|
|
import { CenteredDiv } from "./element/quickelems";
|
2024-06-21 05:04:00 +02:00
|
|
|
|
2024-05-10 05:24:24 +02:00
|
|
|
const App = () => {
|
2024-06-21 21:32:38 +02:00
|
|
|
let Provider = jotai.Provider;
|
2024-05-10 05:24:24 +02:00
|
|
|
return (
|
2024-05-14 08:45:41 +02:00
|
|
|
<Provider store={globalStore}>
|
2024-05-10 05:24:24 +02:00
|
|
|
<AppInner />
|
|
|
|
</Provider>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2024-06-25 02:44:31 +02:00
|
|
|
function isContentEditableBeingEdited() {
|
|
|
|
const activeElement = document.activeElement;
|
|
|
|
return (
|
|
|
|
activeElement &&
|
|
|
|
activeElement.getAttribute("contenteditable") !== null &&
|
|
|
|
activeElement.getAttribute("contenteditable") !== "false"
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function canEnablePaste() {
|
|
|
|
const activeElement = document.activeElement;
|
|
|
|
return activeElement.tagName === "INPUT" || activeElement.tagName === "TEXTAREA" || isContentEditableBeingEdited();
|
|
|
|
}
|
|
|
|
|
|
|
|
function canEnableCopy() {
|
|
|
|
const sel = window.getSelection();
|
|
|
|
return !util.isBlank(sel?.toString());
|
|
|
|
}
|
|
|
|
|
|
|
|
function canEnableCut() {
|
|
|
|
const sel = window.getSelection();
|
|
|
|
if (document.activeElement?.classList.contains("xterm-helper-textarea")) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return !util.isBlank(sel?.toString()) && canEnablePaste();
|
|
|
|
}
|
|
|
|
|
2024-06-20 00:42:19 +02:00
|
|
|
function handleContextMenu(e: React.MouseEvent<HTMLDivElement>) {
|
2024-06-25 02:44:31 +02:00
|
|
|
e.preventDefault();
|
|
|
|
const canPaste = canEnablePaste();
|
|
|
|
const canCopy = canEnableCopy();
|
|
|
|
const canCut = canEnableCut();
|
|
|
|
if (!canPaste && !canCopy && !canCut) {
|
|
|
|
return;
|
2024-06-20 00:42:19 +02:00
|
|
|
}
|
2024-06-25 02:44:31 +02:00
|
|
|
let menu: ContextMenuItem[] = [];
|
|
|
|
if (canCut) {
|
|
|
|
menu.push({ label: "Cut", role: "cut" });
|
2024-06-20 00:42:19 +02:00
|
|
|
}
|
2024-06-25 02:44:31 +02:00
|
|
|
if (canCopy) {
|
|
|
|
menu.push({ label: "Copy", role: "copy" });
|
2024-06-20 00:42:19 +02:00
|
|
|
}
|
2024-06-25 02:44:31 +02:00
|
|
|
if (canPaste) {
|
|
|
|
menu.push({ label: "Paste", role: "paste" });
|
2024-06-20 00:42:19 +02:00
|
|
|
}
|
2024-06-25 02:44:31 +02:00
|
|
|
ContextMenuModel.showContextMenu(menu, e);
|
2024-06-20 00:42:19 +02:00
|
|
|
}
|
|
|
|
|
2024-08-03 01:50:11 +02:00
|
|
|
function switchTabAbs(index: number) {
|
|
|
|
const ws = globalStore.get(atoms.workspace);
|
|
|
|
const newTabIdx = index - 1;
|
|
|
|
if (newTabIdx < 0 || newTabIdx >= ws.tabids.length) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const newActiveTabId = ws.tabids[newTabIdx];
|
|
|
|
services.ObjectService.SetActiveTab(newActiveTabId);
|
|
|
|
}
|
|
|
|
|
2024-06-21 21:32:38 +02:00
|
|
|
function switchTab(offset: number) {
|
|
|
|
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;
|
|
|
|
}
|
2024-07-09 23:31:16 +02:00
|
|
|
const newTabIdx = (tabIdx + offset + ws.tabids.length) % ws.tabids.length;
|
|
|
|
const newActiveTabId = ws.tabids[newTabIdx];
|
|
|
|
console.log("switching tabs", tabIdx, newTabIdx, activeTabId, newActiveTabId, ws.tabids);
|
2024-06-21 21:32:38 +02:00
|
|
|
services.ObjectService.SetActiveTab(newActiveTabId);
|
|
|
|
}
|
|
|
|
|
2024-07-22 22:33:10 +02:00
|
|
|
const transformRegexp = /translate3d\(\s*([0-9.]+)px\s*,\s*([0-9.]+)px,\s*0\)/;
|
2024-06-21 21:32:38 +02:00
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2024-08-03 01:50:11 +02:00
|
|
|
function switchBlockIdx(index: number) {
|
|
|
|
const tabId = globalStore.get(atoms.activeTabId);
|
|
|
|
const tabAtom = WOS.getWaveObjectAtom<Tab>(WOS.makeORef("tab", tabId));
|
2024-08-15 03:40:41 +02:00
|
|
|
const layoutModel = getLayoutModelForTab(tabAtom);
|
|
|
|
if (layoutModel?.leafs == null) {
|
2024-08-03 01:50:11 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
const newLeafIdx = index - 1;
|
2024-08-15 03:40:41 +02:00
|
|
|
if (newLeafIdx < 0 || newLeafIdx >= layoutModel.leafs.length) {
|
2024-08-03 01:50:11 +02:00
|
|
|
return;
|
|
|
|
}
|
2024-08-15 03:40:41 +02:00
|
|
|
const leaf = layoutModel.leafs[newLeafIdx];
|
2024-08-03 01:50:11 +02:00
|
|
|
if (leaf?.data?.blockId == null) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
setBlockFocus(leaf.data.blockId);
|
|
|
|
}
|
|
|
|
|
2024-06-21 21:32:38 +02:00
|
|
|
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));
|
2024-08-15 03:40:41 +02:00
|
|
|
const layoutModel = getLayoutModelForTab(tabAtom);
|
2024-07-03 23:31:02 +02:00
|
|
|
const curBlockId = globalStore.get(atoms.waveWindow)?.activeblockid;
|
2024-08-16 20:40:10 +02:00
|
|
|
const addlProps = globalStore.get(layoutModel.additionalProps);
|
|
|
|
const blockPositions: Map<string, Bounds> = new Map();
|
2024-08-15 03:40:41 +02:00
|
|
|
for (const leaf of layoutModel.leafs) {
|
2024-08-16 20:40:10 +02:00
|
|
|
const pos = readBoundsFromTransform(addlProps[leaf.id]?.transform);
|
|
|
|
if (pos) {
|
2024-06-21 21:32:38 +02:00
|
|
|
blockPositions.set(leaf.data.blockId, pos);
|
|
|
|
}
|
|
|
|
}
|
2024-08-16 20:40:10 +02:00
|
|
|
const curBlockPos = blockPositions.get(curBlockId);
|
|
|
|
if (!curBlockPos) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
blockPositions.delete(curBlockId);
|
2024-06-21 21:32:38 +02:00
|
|
|
const maxX = boundsMapMaxX(blockPositions);
|
|
|
|
const maxY = boundsMapMaxY(blockPositions);
|
|
|
|
const moveAmount = 10;
|
2024-08-16 20:40:10 +02:00
|
|
|
let curX = curBlockPos.x + 1;
|
|
|
|
let curY = curBlockPos.y + 1;
|
2024-06-21 21:32:38 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-26 22:30:11 +02:00
|
|
|
function AppSettingsUpdater() {
|
|
|
|
const settings = jotai.useAtomValue(atoms.settingsConfigAtom);
|
|
|
|
React.useEffect(() => {
|
2024-07-27 01:18:53 +02:00
|
|
|
const isTransparentOrBlur = (settings?.window?.transparent || settings?.window?.blur) ?? false;
|
|
|
|
const opacity = util.boundNumber(settings?.window?.opacity ?? 0.8, 0, 1);
|
2024-07-26 22:30:11 +02:00
|
|
|
let baseBgColor = settings?.window?.bgcolor;
|
|
|
|
console.log("window settings", settings.window);
|
2024-07-27 01:18:53 +02:00
|
|
|
if (isTransparentOrBlur) {
|
2024-07-26 22:30:11 +02:00
|
|
|
document.body.classList.add("is-transparent");
|
|
|
|
const rootStyles = getComputedStyle(document.documentElement);
|
|
|
|
if (baseBgColor == null) {
|
|
|
|
baseBgColor = rootStyles.getPropertyValue("--main-bg-color").trim();
|
|
|
|
}
|
|
|
|
const color = new Color(baseBgColor);
|
|
|
|
const rgbaColor = color.alpha(opacity).string();
|
|
|
|
document.body.style.backgroundColor = rgbaColor;
|
|
|
|
} else {
|
|
|
|
document.body.classList.remove("is-transparent");
|
|
|
|
document.body.style.opacity = null;
|
|
|
|
}
|
|
|
|
}, [settings?.window]);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2024-07-29 20:55:10 +02:00
|
|
|
function encodeFileURL(file: string) {
|
|
|
|
const webEndpoint = getWebServerEndpoint();
|
|
|
|
return webEndpoint + `/wave/stream-file?path=${encodeURIComponent(file)}&no404=1`;
|
|
|
|
}
|
|
|
|
|
|
|
|
(window as any).csstree = csstree;
|
|
|
|
|
|
|
|
function processBackgroundUrls(cssText: string): string {
|
|
|
|
if (util.isBlank(cssText)) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
cssText = cssText.trim();
|
|
|
|
if (cssText.endsWith(";")) {
|
|
|
|
cssText = cssText.slice(0, -1);
|
|
|
|
}
|
|
|
|
const attrRe = /^background(-image):\s*/;
|
|
|
|
cssText = cssText.replace(attrRe, "");
|
|
|
|
const ast = csstree.parse("background: " + cssText, {
|
|
|
|
context: "declaration",
|
|
|
|
});
|
|
|
|
let hasJSUrl = false;
|
|
|
|
csstree.walk(ast, {
|
|
|
|
visit: "Url",
|
|
|
|
enter(node) {
|
|
|
|
const originalUrl = node.value.trim();
|
|
|
|
if (originalUrl.startsWith("javascript:")) {
|
|
|
|
hasJSUrl = true;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const newUrl = encodeFileURL(originalUrl);
|
|
|
|
node.value = newUrl;
|
|
|
|
},
|
|
|
|
});
|
|
|
|
if (hasJSUrl) {
|
|
|
|
console.log("invalid background, contains a 'javascript' protocol url which is not allowed");
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
const rtnStyle = csstree.generate(ast);
|
|
|
|
if (rtnStyle == null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return rtnStyle.replace(/^background:\s*/, "");
|
|
|
|
}
|
|
|
|
|
|
|
|
function AppBackground() {
|
2024-08-19 23:16:09 +02:00
|
|
|
const bgRef = React.useRef<HTMLDivElement>(null);
|
2024-07-29 20:55:10 +02:00
|
|
|
const tabId = jotai.useAtomValue(atoms.activeTabId);
|
|
|
|
const [tabData] = useWaveObjectValue<Tab>(WOS.makeORef("tab", tabId));
|
|
|
|
const bgAttr = tabData?.meta?.bg;
|
|
|
|
const style: React.CSSProperties = {};
|
|
|
|
if (!util.isBlank(bgAttr)) {
|
|
|
|
try {
|
|
|
|
const processedBg = processBackgroundUrls(bgAttr);
|
|
|
|
if (!util.isBlank(processedBg)) {
|
|
|
|
const opacity = util.boundNumber(tabData?.meta?.["bg:opacity"], 0, 1) ?? 0.5;
|
|
|
|
style.opacity = opacity;
|
|
|
|
style.background = processedBg;
|
|
|
|
const blendMode = tabData?.meta?.["bg:blendmode"];
|
|
|
|
if (!util.isBlank(blendMode)) {
|
|
|
|
style.backgroundBlendMode = blendMode;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
console.error("error processing background", e);
|
|
|
|
}
|
|
|
|
}
|
2024-08-19 23:16:09 +02:00
|
|
|
const getAvgColor = React.useCallback(
|
|
|
|
debounce(10, () => {
|
|
|
|
if (
|
|
|
|
bgRef.current &&
|
|
|
|
PLATFORM !== "darwin" &&
|
|
|
|
bgRef.current &&
|
|
|
|
"windowControlsOverlay" in window.navigator
|
|
|
|
) {
|
|
|
|
const titlebarRect: Dimensions = (window.navigator.windowControlsOverlay as any).getTitlebarAreaRect();
|
|
|
|
const bgRect = bgRef.current.getBoundingClientRect();
|
|
|
|
if (titlebarRect && bgRect) {
|
|
|
|
const windowControlsLeft = titlebarRect.width - titlebarRect.height;
|
|
|
|
const windowControlsRect: Dimensions = {
|
|
|
|
top: titlebarRect.top,
|
|
|
|
left: windowControlsLeft,
|
|
|
|
height: titlebarRect.height,
|
|
|
|
width: bgRect.width - bgRect.left - windowControlsLeft,
|
|
|
|
};
|
|
|
|
getApi().updateWindowControlsOverlay(windowControlsRect);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}),
|
|
|
|
[bgRef, style]
|
|
|
|
);
|
2024-08-19 23:28:01 +02:00
|
|
|
React.useLayoutEffect(getAvgColor, [getAvgColor]);
|
2024-08-19 23:16:09 +02:00
|
|
|
useResizeObserver(bgRef, getAvgColor);
|
|
|
|
|
|
|
|
return <div ref={bgRef} className="app-background" style={style} />;
|
2024-07-29 20:55:10 +02:00
|
|
|
}
|
|
|
|
|
2024-08-01 04:22:15 +02:00
|
|
|
function genericClose(tabId: string) {
|
|
|
|
const tabORef = WOS.makeORef("tab", tabId);
|
|
|
|
const tabAtom = WOS.getWaveObjectAtom<Tab>(tabORef);
|
|
|
|
const tabData = globalStore.get(tabAtom);
|
|
|
|
if (tabData == null) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (tabData.blockids == null || tabData.blockids.length == 0) {
|
|
|
|
// close tab
|
|
|
|
services.WindowService.CloseTab(tabId);
|
2024-08-15 03:40:41 +02:00
|
|
|
deleteLayoutModelForTab(tabId);
|
2024-08-01 04:22:15 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
// close block
|
|
|
|
const activeBlockId = globalStore.get(atoms.waveWindow)?.activeblockid;
|
|
|
|
if (activeBlockId == null) {
|
|
|
|
return;
|
|
|
|
}
|
2024-08-15 03:40:41 +02:00
|
|
|
const layoutModel = getLayoutModelForTab(tabAtom);
|
2024-08-16 20:40:10 +02:00
|
|
|
const curBlockLeafId = layoutModel.getNodeByBlockId(activeBlockId)?.id;
|
|
|
|
layoutModel.closeNodeById(curBlockLeafId);
|
2024-08-01 04:22:15 +02:00
|
|
|
}
|
|
|
|
|
2024-08-14 23:38:02 +02:00
|
|
|
const simpleControlShiftAtom = jotai.atom(false);
|
2024-08-03 00:39:22 +02:00
|
|
|
|
|
|
|
const AppKeyHandlers = () => {
|
2024-06-21 21:32:38 +02:00
|
|
|
const tabId = jotai.useAtomValue(atoms.activeTabId);
|
2024-08-03 00:39:22 +02:00
|
|
|
|
2024-08-14 23:38:02 +02:00
|
|
|
function setControlShift() {
|
|
|
|
globalStore.set(simpleControlShiftAtom, true);
|
|
|
|
setTimeout(() => {
|
|
|
|
const simpleState = globalStore.get(simpleControlShiftAtom);
|
|
|
|
if (simpleState) {
|
|
|
|
globalStore.set(atoms.controlShiftDelayAtom, true);
|
|
|
|
}
|
|
|
|
}, 400);
|
|
|
|
}
|
|
|
|
|
|
|
|
function unsetControlShift() {
|
|
|
|
globalStore.set(simpleControlShiftAtom, false);
|
|
|
|
globalStore.set(atoms.controlShiftDelayAtom, false);
|
|
|
|
}
|
|
|
|
|
2024-08-03 00:39:22 +02:00
|
|
|
function handleKeyUp(event: KeyboardEvent) {
|
|
|
|
const waveEvent = keyutil.adaptFromReactOrNativeKeyEvent(event);
|
2024-08-14 23:38:02 +02:00
|
|
|
if (waveEvent.key === "Control" || waveEvent.key === "Shift") {
|
|
|
|
unsetControlShift();
|
|
|
|
}
|
|
|
|
if (waveEvent.key == "Meta") {
|
|
|
|
if (waveEvent.control && waveEvent.shift) {
|
|
|
|
setControlShift();
|
|
|
|
}
|
2024-08-03 00:39:22 +02:00
|
|
|
}
|
2024-05-24 23:08:24 +02:00
|
|
|
}
|
2024-06-29 02:53:35 +02:00
|
|
|
|
|
|
|
function handleKeyDown(waveEvent: WaveKeyboardEvent): boolean {
|
2024-08-14 23:38:02 +02:00
|
|
|
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
|
|
|
|
setControlShift();
|
|
|
|
} else {
|
|
|
|
// Unset if Meta is pressed
|
|
|
|
unsetControlShift();
|
|
|
|
}
|
2024-08-03 00:39:22 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2024-06-21 21:32:38 +02:00
|
|
|
// global key handler for now (refactor later)
|
2024-07-30 21:33:28 +02:00
|
|
|
if (keyutil.checkKeyPressed(waveEvent, "Cmd:]") || keyutil.checkKeyPressed(waveEvent, "Shift:Cmd:]")) {
|
2024-06-21 21:32:38 +02:00
|
|
|
switchTab(1);
|
|
|
|
return true;
|
|
|
|
}
|
2024-07-30 21:33:28 +02:00
|
|
|
if (keyutil.checkKeyPressed(waveEvent, "Cmd:[") || keyutil.checkKeyPressed(waveEvent, "Shift:Cmd:[")) {
|
2024-06-21 21:32:38 +02:00
|
|
|
switchTab(-1);
|
|
|
|
return true;
|
|
|
|
}
|
2024-08-03 01:50:11 +02:00
|
|
|
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}}`)
|
|
|
|
) {
|
|
|
|
switchBlockIdx(idx);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
2024-08-19 21:24:08 +02:00
|
|
|
if (keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:ArrowUp")) {
|
2024-06-21 21:32:38 +02:00
|
|
|
switchBlock(tabId, 0, -1);
|
|
|
|
return true;
|
|
|
|
}
|
2024-08-19 21:24:08 +02:00
|
|
|
if (keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:ArrowDown")) {
|
2024-06-21 21:32:38 +02:00
|
|
|
switchBlock(tabId, 0, 1);
|
|
|
|
return true;
|
|
|
|
}
|
2024-08-19 21:24:08 +02:00
|
|
|
if (keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:ArrowLeft")) {
|
2024-06-21 21:32:38 +02:00
|
|
|
switchBlock(tabId, -1, 0);
|
|
|
|
return true;
|
|
|
|
}
|
2024-08-19 21:24:08 +02:00
|
|
|
if (keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:ArrowRight")) {
|
2024-06-21 21:32:38 +02:00
|
|
|
switchBlock(tabId, 1, 0);
|
|
|
|
return true;
|
|
|
|
}
|
2024-08-01 04:22:15 +02:00
|
|
|
if (keyutil.checkKeyPressed(waveEvent, "Cmd:w")) {
|
|
|
|
// close block, if no more blocks, close tab
|
|
|
|
genericClose(tabId);
|
|
|
|
return true;
|
|
|
|
}
|
2024-06-21 21:32:38 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
React.useEffect(() => {
|
2024-06-29 02:53:35 +02:00
|
|
|
const staticKeyDownHandler = keyutil.keydownWrapper(handleKeyDown);
|
2024-06-21 21:32:38 +02:00
|
|
|
document.addEventListener("keydown", staticKeyDownHandler);
|
2024-08-03 00:39:22 +02:00
|
|
|
const savedKeyUpHandler = handleKeyUp;
|
|
|
|
document.addEventListener("keyup", savedKeyUpHandler);
|
|
|
|
|
2024-06-21 21:32:38 +02:00
|
|
|
return () => {
|
|
|
|
document.removeEventListener("keydown", staticKeyDownHandler);
|
2024-08-03 00:39:22 +02:00
|
|
|
document.removeEventListener("keyup", savedKeyUpHandler);
|
2024-06-21 21:32:38 +02:00
|
|
|
};
|
|
|
|
}, []);
|
2024-08-03 00:39:22 +02:00
|
|
|
return null;
|
|
|
|
};
|
|
|
|
|
|
|
|
const AppInner = () => {
|
2024-08-15 06:47:09 +02:00
|
|
|
const [prefersReducedMotion, setPrefersReducedMotion] = React.useState(false);
|
|
|
|
const prefersReducedMotionSetting = jotai.useAtomValue(atoms.reducedMotionPreferenceAtom);
|
2024-08-03 00:39:22 +02:00
|
|
|
const client = jotai.useAtomValue(atoms.client);
|
|
|
|
const windowData = jotai.useAtomValue(atoms.waveWindow);
|
2024-08-15 06:47:09 +02:00
|
|
|
const isFullScreen = jotai.useAtomValue(atoms.isFullScreen);
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
const reducedMotionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
|
|
setPrefersReducedMotion(!reducedMotionQuery || reducedMotionQuery.matches);
|
|
|
|
reducedMotionQuery.addEventListener("change", () => {
|
|
|
|
setPrefersReducedMotion(reducedMotionQuery.matches);
|
|
|
|
});
|
|
|
|
}, []);
|
|
|
|
|
2024-08-03 00:39:22 +02:00
|
|
|
if (client == null || windowData == null) {
|
|
|
|
return (
|
|
|
|
<div className="mainapp">
|
|
|
|
<AppBackground />
|
|
|
|
<CenteredDiv>invalid configuration, client or window was not loaded</CenteredDiv>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
2024-07-22 22:33:10 +02:00
|
|
|
|
2024-05-10 05:24:24 +02:00
|
|
|
return (
|
2024-08-15 06:47:09 +02:00
|
|
|
<div
|
|
|
|
className={clsx("mainapp", PLATFORM, {
|
|
|
|
fullscreen: isFullScreen,
|
|
|
|
"prefers-reduced-motion": prefersReducedMotion || prefersReducedMotionSetting,
|
|
|
|
})}
|
|
|
|
onContextMenu={handleContextMenu}
|
|
|
|
>
|
2024-07-29 20:55:10 +02:00
|
|
|
<AppBackground />
|
2024-08-03 00:39:22 +02:00
|
|
|
<AppKeyHandlers />
|
2024-07-26 22:30:11 +02:00
|
|
|
<AppSettingsUpdater />
|
2024-06-04 22:05:44 +02:00
|
|
|
<DndProvider backend={HTML5Backend}>
|
|
|
|
<Workspace />
|
|
|
|
</DndProvider>
|
2024-05-10 05:24:24 +02:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
export { App };
|