Unified node model to pass data from layout to blocks (#259)

This adds a new NodeModel, which can be passed from the TileLayout to
contained blocks. It contains all the layout data that the block should
care about, including focus status, whether a drag operation is
underway, whether the node is magnified, etc.

This also adds a focus stack for the layout, which will let the focus
switch to the last-focused node when the currently-focused one is
closed.

This also addresses a regression in the resize handles that caused them
to be offset from the cursor when dragged.

---------

Co-authored-by: sawka <mike.sawka@gmail.com>
This commit is contained in:
Evan Simkowitz
2024-08-26 11:56:00 -07:00
committed by GitHub
co-authored by sawka
parent 8e5a4a457c
commit 164afeeb66
19 changed files with 596 additions and 573 deletions
+27 -125
View File
@@ -1,14 +1,19 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { atoms, createBlock, getViewModel, globalStore, setBlockFocus, WOS } from "@/app/store/global";
import { deleteLayoutModelForTab, getLayoutModelForTab } from "@/layout/index";
import { atoms, createBlock, globalStore, WOS } from "@/app/store/global";
import {
deleteLayoutModelForTab,
getLayoutModelForActiveTab,
getLayoutModelForTab,
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 transformRegexp = /translate3d\(\s*([0-9.]+)px\s*,\s*([0-9.]+)px,\s*0\)/;
function setControlShift() {
globalStore.set(simpleControlShiftAtom, true);
@@ -38,99 +43,21 @@ function genericClose(tabId: string) {
deleteLayoutModelForTab(tabId);
return;
}
// close block
const activeBlockId = globalStore.get(atoms.waveWindow)?.activeblockid;
if (activeBlockId == null) {
return;
}
const layoutModel = getLayoutModelForTab(tabAtom);
const curBlockLeafId = layoutModel.getNodeByBlockId(activeBlockId)?.id;
layoutModel.closeNodeById(curBlockLeafId);
layoutModel.closeFocusedNode();
}
function switchBlockIdx(index: number) {
const tabId = globalStore.get(atoms.activeTabId);
const tabAtom = WOS.getWaveObjectAtom<Tab>(WOS.makeORef("tab", tabId));
const layoutModel = getLayoutModelForTab(tabAtom);
function switchBlockByBlockNum(index: number) {
const layoutModel = getLayoutModelForActiveTab();
if (!layoutModel) {
return;
}
const leafsOrdered = globalStore.get(layoutModel.leafsOrdered);
const newLeafIdx = index - 1;
if (newLeafIdx < 0 || newLeafIdx >= leafsOrdered.length) {
return;
}
const leaf = leafsOrdered[newLeafIdx];
if (leaf?.data?.blockId == null) {
return;
}
setBlockFocus(leaf.data.blockId);
layoutModel.switchNodeFocusByBlockNum(index);
}
function getCenter(dimensions: Dimensions): Point {
return {
x: dimensions.left + dimensions.width / 2,
y: dimensions.top + dimensions.height / 2,
};
}
function findBlockAtPoint(m: Map<string, Dimensions>, p: Point): string {
for (const [blockId, dimension] of m.entries()) {
if (
p.x >= dimension.left &&
p.x <= dimension.left + dimension.width &&
p.y >= dimension.top &&
p.y <= dimension.top + dimension.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 layoutModel = getLayoutModelForTab(tabAtom);
const curBlockId = globalStore.get(atoms.waveWindow)?.activeblockid;
const addlProps = globalStore.get(layoutModel.additionalProps);
const blockPositions: Map<string, Dimensions> = new Map();
const leafsOrdered = globalStore.get(layoutModel.leafsOrdered);
for (const leaf of leafsOrdered) {
const pos = addlProps[leaf.id]?.rect;
if (pos) {
blockPositions.set(leaf.data.blockId, pos);
}
}
const curBlockPos = blockPositions.get(curBlockId);
if (!curBlockPos) {
return;
}
blockPositions.delete(curBlockId);
const boundingRect = layoutModel.displayContainerRef?.current.getBoundingClientRect();
if (!boundingRect) {
return;
}
const maxX = boundingRect.left + boundingRect.width;
const maxY = boundingRect.top + boundingRect.height;
const moveAmount = 10;
const curPoint = getCenter(curBlockPos);
while (true) {
console.log("nextPoint", curPoint, curBlockPos);
curPoint.x += offsetX * moveAmount;
curPoint.y += offsetY * moveAmount;
if (curPoint.x < 0 || curPoint.x > maxX || curPoint.y < 0 || curPoint.y > maxY) {
return;
}
const blockId = findBlockAtPoint(blockPositions, curPoint);
if (blockId != null) {
setBlockFocus(blockId);
return;
}
}
function switchBlockInDirection(tabId: string, direction: NavigateDirection) {
const layoutModel = getLayoutModelForTabById(tabId);
layoutModel.switchNodeFocusInDirection(direction);
}
function switchTabAbs(index: number) {
@@ -158,7 +85,6 @@ function switchTab(offset: number) {
}
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);
services.ObjectService.SetActiveTab(newActiveTabId);
}
@@ -174,17 +100,17 @@ function appHandleKeyUp(event: KeyboardEvent) {
}
}
async function handleCmdT() {
async function handleCmdN() {
const termBlockDef: BlockDef = {
meta: {
view: "term",
controller: "shell",
},
};
const tabId = globalStore.get(atoms.activeTabId);
const win = globalStore.get(atoms.waveWindow);
if (win?.activeblockid != null) {
const blockAtom = WOS.getWaveObjectAtom<Block>(WOS.makeORef("block", win.activeblockid));
const layoutModel = getLayoutModelForActiveTab();
const focusedNode = globalStore.get(layoutModel.focusedNode);
if (focusedNode != null) {
const blockAtom = WOS.getWaveObjectAtom<Block>(WOS.makeORef("block", focusedNode.data?.blockId));
const blockData = globalStore.get(blockAtom);
if (blockData?.meta?.view == "term") {
if (blockData?.meta?.["cmd:cwd"] != null) {
@@ -195,27 +121,7 @@ async function handleCmdT() {
termBlockDef.meta.connection = blockData.meta.connection;
}
}
const newBlockId = await createBlock(termBlockDef);
setBlockFocus(newBlockId);
}
function handleCmdI() {
const waveWindow = globalStore.get(atoms.waveWindow);
if (waveWindow == null) {
return;
}
let activeBlockId = waveWindow.activeblockid;
if (activeBlockId == null) {
// get the first block
const tabData = globalStore.get(atoms.tabAtom);
const firstBlockId = tabData.blockids?.length == 0 ? null : tabData.blockids[0];
if (firstBlockId == null) {
return;
}
activeBlockId = firstBlockId;
}
const viewModel = getViewModel(activeBlockId);
viewModel?.giveFocus?.();
await createBlock(termBlockDef);
}
function appHandleKeyDown(waveEvent: WaveKeyboardEvent): boolean {
@@ -241,11 +147,7 @@ function appHandleKeyDown(waveEvent: WaveKeyboardEvent): boolean {
return true;
}
if (keyutil.checkKeyPressed(waveEvent, "Cmd:n")) {
handleCmdT();
return true;
}
if (keyutil.checkKeyPressed(waveEvent, "Cmd:i")) {
handleCmdI();
handleCmdN();
return true;
}
if (keyutil.checkKeyPressed(waveEvent, "Cmd:t")) {
@@ -265,24 +167,24 @@ function appHandleKeyDown(waveEvent: WaveKeyboardEvent): boolean {
keyutil.checkKeyPressed(waveEvent, `Ctrl:Shift:c{Digit${idx}}`) ||
keyutil.checkKeyPressed(waveEvent, `Ctrl:Shift:c{Numpad${idx}}`)
) {
switchBlockIdx(idx);
switchBlockByBlockNum(idx);
return true;
}
}
if (keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:ArrowUp")) {
switchBlock(tabId, 0, -1);
switchBlockInDirection(tabId, NavigateDirection.Up);
return true;
}
if (keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:ArrowDown")) {
switchBlock(tabId, 0, 1);
switchBlockInDirection(tabId, NavigateDirection.Down);
return true;
}
if (keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:ArrowLeft")) {
switchBlock(tabId, -1, 0);
switchBlockInDirection(tabId, NavigateDirection.Left);
return true;
}
if (keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:ArrowRight")) {
switchBlock(tabId, 1, 0);
switchBlockInDirection(tabId, NavigateDirection.Right);
return true;
}
if (keyutil.checkKeyPressed(waveEvent, "Cmd:w")) {
+41 -46
View File
@@ -1,20 +1,13 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { BlockComponentModel, BlockProps, LayoutComponentModel } from "@/app/block/blocktypes";
import { BlockComponentModel, BlockProps } from "@/app/block/blocktypes";
import { PlotView } from "@/app/view/plotview/plotview";
import { PreviewModel, PreviewView, makePreviewModel } from "@/app/view/preview/preview";
import { ErrorBoundary } from "@/element/errorboundary";
import { CenteredDiv } from "@/element/quickelems";
import {
atoms,
counterInc,
getViewModel,
registerViewModel,
setBlockFocus,
unregisterViewModel,
useBlockAtom,
} from "@/store/global";
import { NodeModel } from "@/layout/index";
import { counterInc, getViewModel, registerViewModel, unregisterViewModel } from "@/store/global";
import * as WOS from "@/store/wos";
import * as util from "@/util/util";
import { CpuPlotView, CpuPlotViewModel, makeCpuPlotViewModel } from "@/view/cpuplot/cpuplot";
@@ -24,15 +17,13 @@ import { WaveAi, WaveAiModel, makeWaveAiViewModel } from "@/view/waveai/waveai";
import { WebView, WebViewModel, makeWebViewModel } from "@/view/webview/webview";
import * as jotai from "jotai";
import * as React from "react";
import "./block.less";
import { BlockFrame } from "./blockframe";
import { blockViewToIcon, blockViewToName } from "./blockutil";
import "./block.less";
type FullBlockProps = {
blockId: string;
preview: boolean;
layoutModel: LayoutComponentModel;
nodeModel: NodeModel;
viewModel: ViewModel;
};
@@ -105,16 +96,15 @@ function makeDefaultViewModel(blockId: string, viewType: string): ViewModel {
return viewModel;
}
const BlockPreview = React.memo(({ blockId, layoutModel, viewModel }: FullBlockProps) => {
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
const BlockPreview = React.memo(({ nodeModel, viewModel }: FullBlockProps) => {
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", nodeModel.blockId));
if (!blockData) {
return null;
}
return (
<BlockFrame
key={blockId}
blockId={blockId}
layoutModel={layoutModel}
key={nodeModel.blockId}
nodeModel={nodeModel}
preview={true}
blockModel={null}
viewModel={viewModel}
@@ -122,20 +112,16 @@ const BlockPreview = React.memo(({ blockId, layoutModel, viewModel }: FullBlockP
);
});
const BlockFull = React.memo(({ blockId, layoutModel, viewModel }: FullBlockProps) => {
const BlockFull = React.memo(({ nodeModel, viewModel }: FullBlockProps) => {
counterInc("render-BlockFull");
const focusElemRef = React.useRef<HTMLInputElement>(null);
const blockRef = React.useRef<HTMLDivElement>(null);
const [blockClicked, setBlockClicked] = React.useState(false);
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", nodeModel.blockId));
const [focusedChild, setFocusedChild] = React.useState(null);
const isFocusedAtom = useBlockAtom<boolean>(blockId, "isFocused", () => {
return jotai.atom((get) => {
const winData = get(atoms.waveWindow);
return winData.activeblockid === blockId;
});
});
const isFocused = jotai.useAtomValue(isFocusedAtom);
const isFocused = jotai.useAtomValue(nodeModel.isFocused);
const disablePointerEvents = jotai.useAtomValue(nodeModel.disablePointerEvents);
const addlProps = jotai.useAtomValue(nodeModel.additionalProps);
React.useLayoutEffect(() => {
setBlockClicked(isFocused);
@@ -150,24 +136,24 @@ const BlockFull = React.memo(({ blockId, layoutModel, viewModel }: FullBlockProp
if (!focusWithin) {
setFocusTarget();
}
setBlockFocus(blockId);
nodeModel.focusNode();
}, [blockClicked]);
React.useLayoutEffect(() => {
if (focusedChild == null) {
return;
}
setBlockFocus(blockId);
}, [focusedChild, blockId]);
nodeModel.focusNode();
}, [focusedChild]);
// treat the block as clicked on creation
const setBlockClickedTrue = React.useCallback(() => {
setBlockClicked(true);
}, []);
let viewElem = React.useMemo(
() => getViewElem(blockId, blockData?.meta?.view, viewModel),
[blockId, blockData?.meta?.view, viewModel]
const viewElem = React.useMemo(
() => getViewElem(nodeModel.blockId, blockData?.meta?.view, viewModel),
[nodeModel.blockId, blockData?.meta?.view, viewModel]
);
const determineFocusedChild = React.useCallback(
@@ -193,20 +179,29 @@ const BlockFull = React.memo(({ blockId, layoutModel, viewModel }: FullBlockProp
return (
<BlockFrame
key={blockId}
blockId={blockId}
layoutModel={layoutModel}
key={nodeModel.blockId}
nodeModel={nodeModel}
preview={false}
blockModel={blockModel}
viewModel={viewModel}
>
<div key="focuselem" className="block-focuselem">
<input type="text" value="" ref={focusElemRef} id={`${blockId}-dummy-focus`} onChange={() => {}} />
<input
type="text"
value=""
ref={focusElemRef}
id={`${nodeModel.blockId}-dummy-focus`}
onChange={() => {}}
/>
</div>
<div
key="content"
className="block-content"
style={{ pointerEvents: layoutModel?.disablePointerEvents ? "none" : undefined }}
style={{
pointerEvents: disablePointerEvents ? "none" : undefined,
width: addlProps?.transform?.width,
height: addlProps?.transform?.height,
}}
>
<ErrorBoundary>
<React.Suspense fallback={<CenteredDiv>Loading...</CenteredDiv>}>{viewElem}</React.Suspense>
@@ -218,19 +213,19 @@ const BlockFull = React.memo(({ blockId, layoutModel, viewModel }: FullBlockProp
const Block = React.memo((props: BlockProps) => {
counterInc("render-Block");
counterInc("render-Block-" + props.blockId.substring(0, 8));
const [blockData, loading] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", props.blockId));
let viewModel = getViewModel(props.blockId);
counterInc("render-Block-" + props.nodeModel.blockId.substring(0, 8));
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.blockId, blockData?.meta?.view);
registerViewModel(props.blockId, viewModel);
viewModel = makeViewModel(props.nodeModel.blockId, blockData?.meta?.view);
registerViewModel(props.nodeModel.blockId, viewModel);
}
React.useEffect(() => {
return () => {
unregisterViewModel(props.blockId);
unregisterViewModel(props.nodeModel.blockId);
};
}, []);
if (loading || util.isBlank(props.blockId) || blockData == null) {
if (loading || util.isBlank(props.nodeModel.blockId) || blockData == null) {
return null;
}
if (props.preview) {
+68 -74
View File
@@ -11,21 +11,22 @@ import {
} from "@/app/block/blockutil";
import { Button } from "@/app/element/button";
import { ContextMenuModel } from "@/app/store/contextmenu";
import { atoms, globalStore, useBlockAtom, WOS } from "@/app/store/global";
import { atoms, globalStore, WOS } from "@/app/store/global";
import * as services from "@/app/store/services";
import { MagnifyIcon } from "@/element/magnify";
import { useLayoutModel } from "@/layout/index";
import { NodeModel } from "@/layout/index";
import { checkKeyPressed, keydownWrapper } from "@/util/keyutil";
import * as util from "@/util/util";
import clsx from "clsx";
import * as jotai from "jotai";
import * as React from "react";
import { BlockFrameProps, LayoutComponentModel } from "./blocktypes";
import { BlockFrameProps } from "./blocktypes";
function handleHeaderContextMenu(
e: React.MouseEvent<HTMLDivElement>,
blockData: Block,
viewModel: ViewModel,
magnified: boolean,
onMagnifyToggle: () => void,
onClose: () => void
) {
@@ -33,7 +34,7 @@ function handleHeaderContextMenu(
e.stopPropagation();
let menu: ContextMenuItem[] = [
{
label: "Magnify Block",
label: magnified ? "Un-magnify Block" : "Magnify Block",
click: () => {
onMagnifyToggle();
},
@@ -78,20 +79,27 @@ function getViewIconElem(viewIconUnion: string | HeaderIconButton, blockData: Bl
}
}
const OptMagnifyButton = React.memo(({ layoutCompModel }: { layoutCompModel: LayoutComponentModel }) => {
const magnifyDecl: HeaderIconButton = {
elemtype: "iconbutton",
icon: <MagnifyIcon enabled={layoutCompModel?.isMagnified} />,
title: layoutCompModel?.isMagnified ? "Minimize" : "Magnify",
click: layoutCompModel?.onMagnifyToggle,
};
return <IconButton key="magnify" decl={magnifyDecl} className="block-frame-magnify" />;
});
const OptMagnifyButton = React.memo(
({ magnified, toggleMagnify }: { magnified: boolean; toggleMagnify: () => void }) => {
const magnifyDecl: HeaderIconButton = {
elemtype: "iconbutton",
icon: <MagnifyIcon enabled={magnified} />,
title: magnified ? "Minimize" : "Magnify",
click: toggleMagnify,
};
return <IconButton key="magnify" decl={magnifyDecl} className="block-frame-magnify" />;
}
);
function computeEndIcons(blockData: Block, viewModel: ViewModel, layoutModel: LayoutComponentModel): JSX.Element[] {
function computeEndIcons(
viewModel: ViewModel,
magnified: boolean,
toggleMagnify: () => void,
onClose: () => void,
onContextMenu: (e: React.MouseEvent<HTMLDivElement>) => void
): JSX.Element[] {
const endIconsElem: JSX.Element[] = [];
const endIconButtons = util.useAtomValueSafe(viewModel.endIconButtons);
if (endIconButtons && endIconButtons.length > 0) {
endIconsElem.push(...endIconButtons.map((button, idx) => <IconButton key={idx} decl={button} />));
}
@@ -99,30 +107,44 @@ function computeEndIcons(blockData: Block, viewModel: ViewModel, layoutModel: La
elemtype: "iconbutton",
icon: "cog",
title: "Settings",
click: (e) =>
handleHeaderContextMenu(e, blockData, viewModel, layoutModel?.onMagnifyToggle, layoutModel?.onClose),
click: onContextMenu,
};
endIconsElem.push(<IconButton key="settings" decl={settingsDecl} className="block-frame-settings" />);
endIconsElem.push(<OptMagnifyButton key="unmagnify" layoutCompModel={layoutModel} />);
endIconsElem.push(<OptMagnifyButton key="unmagnify" magnified={magnified} toggleMagnify={toggleMagnify} />);
const closeDecl: HeaderIconButton = {
elemtype: "iconbutton",
icon: "xmark-large",
title: "Close",
click: layoutModel?.onClose,
click: onClose,
};
endIconsElem.push(<IconButton key="close" decl={closeDecl} className="block-frame-default-close" />);
return endIconsElem;
}
const BlockFrame_Header = ({ blockId, layoutModel, viewModel }: BlockFrameProps) => {
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
const BlockFrame_Header = ({ nodeModel, viewModel, preview }: BlockFrameProps) => {
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", nodeModel.blockId));
const viewName = util.useAtomValueSafe(viewModel.viewName) ?? blockViewToName(blockData?.meta?.view);
const settingsConfig = jotai.useAtomValue(atoms.settingsConfigAtom);
const viewIconUnion = util.useAtomValueSafe(viewModel.viewIcon) ?? blockViewToIcon(blockData?.meta?.view);
const preIconButton = util.useAtomValueSafe(viewModel.preIconButton);
const headerTextUnion = util.useAtomValueSafe(viewModel.viewText);
const magnified = jotai.useAtomValue(nodeModel.isMagnified);
const dragHandleRef = preview ? null : nodeModel.dragHandleRef;
const endIconsElem = computeEndIcons(blockData, viewModel, layoutModel);
const onContextMenu = React.useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
handleHeaderContextMenu(e, blockData, viewModel, magnified, nodeModel.toggleMagnify, nodeModel.onClose);
},
[magnified]
);
const endIconsElem = computeEndIcons(
viewModel,
magnified,
nodeModel.toggleMagnify,
nodeModel.onClose,
onContextMenu
);
const viewIconElem = getViewIconElem(viewIconUnion, blockData);
let preIconButtonElem: JSX.Element = null;
if (preIconButton) {
@@ -139,23 +161,17 @@ const BlockFrame_Header = ({ blockId, layoutModel, viewModel }: BlockFrameProps)
);
}
} else if (Array.isArray(headerTextUnion)) {
headerTextElems.push(...renderHeaderElements(headerTextUnion));
headerTextElems.push(...renderHeaderElements(headerTextUnion, preview));
}
return (
<div
className="block-frame-default-header"
ref={layoutModel?.dragHandleRef}
onContextMenu={(e) =>
handleHeaderContextMenu(e, blockData, viewModel, layoutModel?.onMagnifyToggle, layoutModel?.onClose)
}
>
<div className="block-frame-default-header" ref={dragHandleRef} onContextMenu={onContextMenu}>
{preIconButtonElem}
<div className="block-frame-default-header-iconview">
{viewIconElem}
<div className="block-frame-view-type">{viewName}</div>
{settingsConfig?.blockheader?.showblockids && (
<div className="block-frame-blockid">[{blockId.substring(0, 8)}]</div>
<div className="block-frame-blockid">[{nodeModel.blockId.substring(0, 8)}]</div>
)}
</div>
<div className="block-frame-textelems-wrapper">{headerTextElems}</div>
@@ -164,11 +180,11 @@ const BlockFrame_Header = ({ blockId, layoutModel, viewModel }: BlockFrameProps)
);
};
const HeaderTextElem = React.memo(({ elem }: { elem: HeaderElem }) => {
const HeaderTextElem = React.memo(({ elem, preview }: { elem: HeaderElem; preview: boolean }) => {
if (elem.elemtype == "iconbutton") {
return <IconButton decl={elem} className={clsx("block-frame-header-iconbutton", elem.className)} />;
} else if (elem.elemtype == "input") {
return <Input decl={elem} className={clsx("block-frame-input", elem.className)} />;
return <Input decl={elem} className={clsx("block-frame-input", elem.className)} preview={preview} />;
} else if (elem.elemtype == "text") {
return <div className="block-frame-text">{elem.text}</div>;
} else if (elem.elemtype == "textbutton") {
@@ -187,7 +203,7 @@ const HeaderTextElem = React.memo(({ elem }: { elem: HeaderElem }) => {
onMouseOut={elem.onMouseOut}
>
{elem.children.map((child, childIdx) => (
<HeaderTextElem elem={child} key={childIdx} />
<HeaderTextElem elem={child} key={childIdx} preview={preview} />
))}
</div>
);
@@ -195,11 +211,11 @@ const HeaderTextElem = React.memo(({ elem }: { elem: HeaderElem }) => {
return null;
});
function renderHeaderElements(headerTextUnion: HeaderElem[]): JSX.Element[] {
function renderHeaderElements(headerTextUnion: HeaderElem[], preview: boolean): JSX.Element[] {
const headerTextElems: JSX.Element[] = [];
for (let idx = 0; idx < headerTextUnion.length; idx++) {
const elem = headerTextUnion[idx];
const renderedElement = <HeaderTextElem elem={elem} key={idx} />;
const renderedElement = <HeaderTextElem elem={elem} key={idx} preview={preview} />;
if (renderedElement) {
headerTextElems.push(renderedElement);
}
@@ -207,18 +223,11 @@ function renderHeaderElements(headerTextUnion: HeaderElem[]): JSX.Element[] {
return headerTextElems;
}
function BlockNum({ blockId }: { blockId: string }) {
const tabId = jotai.useAtomValue(atoms.activeTabId);
const tabAtom = WOS.getWaveObjectAtom<Tab>(WOS.makeORef("tab", tabId));
const layoutModel = useLayoutModel(tabAtom);
const leafsOrdered = jotai.useAtomValue(layoutModel.leafsOrdered);
const index = React.useMemo(() => leafsOrdered.findIndex((leaf) => leaf.data?.blockId == blockId), [leafsOrdered]);
return index !== -1 ? index + 1 : null;
}
const BlockMask = ({ blockId, preview, isFocused }: { blockId: string; preview: boolean; isFocused: boolean }) => {
const BlockMask = ({ nodeModel }: { nodeModel: NodeModel }) => {
const isFocused = jotai.useAtomValue(nodeModel.isFocused);
const blockNum = jotai.useAtomValue(nodeModel.blockNum);
const isLayoutMode = jotai.useAtomValue(atoms.controlShiftDelayAtom);
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", nodeModel.blockId));
const style: React.CSSProperties = {};
if (!isFocused && blockData?.meta?.["frame:bordercolor"]) {
@@ -231,9 +240,7 @@ const BlockMask = ({ blockId, preview, isFocused }: { blockId: string; preview:
if (isLayoutMode) {
innerElem = (
<div className="block-mask-inner">
<div className="bignum">
<BlockNum blockId={blockId} />
</div>
<div className="bignum">{blockNum}</div>
</div>
);
}
@@ -245,27 +252,17 @@ const BlockMask = ({ blockId, preview, isFocused }: { blockId: string; preview:
};
const BlockFrame_Default_Component = (props: BlockFrameProps) => {
const { blockId, layoutModel, viewModel, blockModel, preview, numBlocksInTab, children } = props;
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
const isFocusedAtom = useBlockAtom<boolean>(blockId, "isFocused", () => {
return jotai.atom((get) => {
const winData = get(atoms.waveWindow);
return winData?.activeblockid === blockId;
});
});
const { nodeModel, viewModel, blockModel, preview, numBlocksInTab, children } = props;
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", nodeModel.blockId));
const isFocused = jotai.useAtomValue(nodeModel.isFocused);
const viewIconUnion = util.useAtomValueSafe(viewModel.viewIcon) ?? blockViewToIcon(blockData?.meta?.view);
const customBg = util.useAtomValueSafe(viewModel.blockBg);
let isFocused = jotai.useAtomValue(isFocusedAtom);
if (preview) {
isFocused = true;
}
const viewIconElem = getViewIconElem(viewIconUnion, blockData);
function handleKeyDown(waveEvent: WaveKeyboardEvent): boolean {
if (checkKeyPressed(waveEvent, "Cmd:m")) {
layoutModel?.onMagnifyToggle();
nodeModel.toggleMagnify();
return true;
}
if (viewModel?.keyDownHandler) {
@@ -286,20 +283,17 @@ const BlockFrame_Default_Component = (props: BlockFrameProps) => {
const previewElem = <div className="block-frame-preview">{viewIconElem}</div>;
return (
<div
className={clsx(
"block",
"block-frame-default",
isFocused ? "block-focused" : null,
preview ? "block-preview" : null,
numBlocksInTab == 1 ? "block-no-highlight" : null,
"block-" + blockId
)}
className={clsx("block", "block-frame-default", "block-" + nodeModel.blockId, {
"block-focused": isFocused || preview,
"block-preview": preview,
"block-no-highlight": numBlocksInTab === 1,
})}
onClick={blockModel?.onClick}
onFocusCapture={blockModel?.onFocusCapture}
ref={blockModel?.blockRef}
onKeyDown={keydownWrapper(handleKeyDown)}
>
<BlockMask blockId={blockId} preview={preview} isFocused={isFocused} />
<BlockMask nodeModel={nodeModel} />
<div className="block-frame-default-inner" style={innerStyle}>
<BlockFrame_Header {...props} />
{preview ? previewElem : children}
@@ -311,7 +305,7 @@ const BlockFrame_Default_Component = (props: BlockFrameProps) => {
const BlockFrame_Default = React.memo(BlockFrame_Default_Component) as typeof BlockFrame_Default_Component;
const BlockFrame = React.memo((props: BlockFrameProps) => {
const blockId = props.blockId;
const blockId = props.nodeModel.blockId;
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
const tabData = jotai.useAtomValue(atoms.tabAtom);
+3 -12
View File
@@ -1,18 +1,10 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
export interface LayoutComponentModel {
disablePointerEvents: boolean;
onClose?: () => void;
onMagnifyToggle?: () => void;
isMagnified: boolean;
dragHandleRef?: React.RefObject<HTMLDivElement>;
}
import { NodeModel } from "@/layout/index";
export interface BlockProps {
blockId: string;
preview: boolean;
layoutModel: LayoutComponentModel;
nodeModel: NodeModel;
}
export interface BlockComponentModel {
@@ -22,9 +14,8 @@ export interface BlockComponentModel {
}
export interface BlockFrameProps {
blockId: string;
blockModel?: BlockComponentModel;
layoutModel?: LayoutComponentModel;
nodeModel?: NodeModel;
viewModel?: ViewModel;
preview: boolean;
numBlocksInTab?: number;
+23 -17
View File
@@ -196,20 +196,26 @@ export const ConnectionButton = React.memo(({ decl }: { decl: ConnectionButton }
);
});
export const Input = React.memo(({ decl, className }: { decl: HeaderInput; className: string }) => {
const { value, ref, isDisabled, onChange, onKeyDown, onFocus, onBlur } = decl;
return (
<div className="input-wrapper">
<input
ref={ref}
disabled={isDisabled}
className={className}
value={value}
onChange={(e) => onChange(e)}
onKeyDown={(e) => onKeyDown(e)}
onFocus={(e) => onFocus(e)}
onBlur={(e) => onBlur(e)}
/>
</div>
);
});
export const Input = React.memo(
({ decl, className, preview }: { decl: HeaderInput; className: string; preview: boolean }) => {
const { value, ref, isDisabled, onChange, onKeyDown, onFocus, onBlur } = decl;
return (
<div className="input-wrapper">
<input
ref={
!preview
? ref
: undefined /* don't wire up the input field if the preview block is being rendered */
}
disabled={isDisabled}
className={className}
value={value}
onChange={(e) => onChange(e)}
onKeyDown={(e) => onKeyDown(e)}
onFocus={(e) => onFocus(e)}
onBlur={(e) => onBlur(e)}
/>
</div>
);
}
);
+11 -20
View File
@@ -3,6 +3,7 @@
import { handleIncomingRpcMessage, sendRawRpcMessage } from "@/app/store/wshrpc";
import {
getLayoutModelForActiveTab,
getLayoutModelForTabById,
LayoutTreeActionType,
LayoutTreeInsertNodeAction,
@@ -12,7 +13,7 @@ import {
import { getWebServerEndpoint, getWSServerEndpoint } from "@/util/endpoints";
import { fetch } from "@/util/fetchutil";
import * as util from "@/util/util";
import { produce } from "immer";
import { fireAndForget } from "@/util/util";
import * as jotai from "jotai";
import * as rxjs from "rxjs";
import { modalsModel } from "./modalmodel";
@@ -60,7 +61,6 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) {
const isFullScreenAtom = jotai.atom(false) as jotai.PrimitiveAtom<boolean>;
try {
getApi().onFullScreenChange((isFullScreen) => {
console.log("fullscreen change", isFullScreen);
globalStore.set(isFullScreenAtom, isFullScreen);
});
} catch (_) {
@@ -118,7 +118,6 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) {
try {
globalStore.set(updaterStatusAtom, getApi().getUpdaterStatus());
getApi().onUpdaterStatusChange((status) => {
console.log("updater status change", status);
globalStore.set(updaterStatusAtom, status);
});
} catch (_) {
@@ -336,7 +335,7 @@ function handleWaveEvent(event: WaveEvent) {
function handleWSEventMessage(msg: WSEventType) {
if (msg.eventtype == null) {
console.log("unsupported event", msg);
console.warn("unsupported WSEvent", msg);
return;
}
if (msg.eventtype == "config") {
@@ -380,7 +379,7 @@ function handleWSEventMessage(msg: WSEventType) {
case LayoutTreeActionType.DeleteNode: {
const leaf = layoutModel?.getNodeByBlockId(layoutAction.blockid);
if (leaf) {
layoutModel.closeNode(leaf);
fireAndForget(() => layoutModel.closeNode(leaf.id));
} else {
console.error(
"Cannot apply eventbus layout action DeleteNode, could not find leaf node with blockId",
@@ -406,7 +405,7 @@ function handleWSEventMessage(msg: WSEventType) {
break;
}
default:
console.log("unsupported layout action", layoutAction);
console.warn("unsupported layout action", layoutAction);
break;
}
return;
@@ -459,12 +458,13 @@ function getApi(): ElectronApi {
return (window as any).api;
}
async function createBlock(blockDef: BlockDef): Promise<string> {
async function createBlock(blockDef: BlockDef, magnified = false): Promise<string> {
const rtOpts: RuntimeOpts = { termsize: { rows: 25, cols: 80 } };
const blockId = await services.ObjectService.CreateBlock(blockDef, rtOpts);
const insertNodeAction: LayoutTreeInsertNodeAction = {
type: LayoutTreeActionType.InsertNode,
node: newLayoutNode(undefined, undefined, undefined, { blockId }),
magnified,
};
const activeTabId = globalStore.get(atoms.uiContext).activetabid;
const layoutModel = getLayoutModelForTabById(activeTabId);
@@ -503,18 +503,9 @@ async function fetchWaveFile(
return { data: new Uint8Array(data), fileInfo };
}
function setBlockFocus(blockId: string) {
let winData = globalStore.get(atoms.waveWindow);
if (winData == null) {
return;
}
if (winData.activeblockid === blockId) {
return;
}
winData = produce(winData, (draft) => {
draft.activeblockid = blockId;
});
WOS.setObjectValue(winData, globalStore.set, true);
function setNodeFocus(nodeId: string) {
const layoutModel = getLayoutModelForActiveTab();
layoutModel.focusNode(nodeId);
}
const objectIdWeakMap = new WeakMap();
@@ -635,7 +626,7 @@ export {
PLATFORM,
registerViewModel,
sendWSCommand,
setBlockFocus,
setNodeFocus,
setPlatform,
subscribeToConnEvents,
unregisterViewModel,
+6 -28
View File
@@ -2,9 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
import { Block } from "@/app/block/block";
import { LayoutComponentModel } from "@/app/block/blocktypes";
import { CenteredDiv } from "@/element/quickelems";
import { ContentRenderer, TileLayout } from "@/layout/index";
import { ContentRenderer, NodeModel, PreviewRenderer, TileLayout } from "@/layout/index";
import { getApi } from "@/store/global";
import * as services from "@/store/services";
import * as WOS from "@/store/wos";
@@ -21,34 +20,13 @@ const TabContent = React.memo(({ tabId }: { tabId: string }) => {
const tabData = useAtomValue(tabAtom);
const tileLayoutContents = useMemo(() => {
const renderBlock: ContentRenderer = (
blockData: TabLayoutData,
ready: boolean,
isMagnified: boolean,
disablePointerEvents: boolean,
onMagnifyToggle: () => void,
onClose: () => void,
dragHandleRef: React.RefObject<HTMLDivElement>
) => {
if (!blockData.blockId || !ready) {
return null;
}
const layoutModel: LayoutComponentModel = {
disablePointerEvents,
onClose,
onMagnifyToggle,
dragHandleRef,
isMagnified,
};
return (
<Block key={blockData.blockId} blockId={blockData.blockId} layoutModel={layoutModel} preview={false} />
);
const renderBlock: ContentRenderer = (nodeModel: NodeModel) => {
return <Block key={nodeModel.blockId} nodeModel={nodeModel} preview={false} />;
};
function renderPreview(tabData: TabLayoutData) {
if (!tabData) return;
return <Block key={tabData.blockId} blockId={tabData.blockId} layoutModel={null} preview={true} />;
}
const renderPreview: PreviewRenderer = (nodeModel: NodeModel) => {
return <Block key={nodeModel.blockId} nodeModel={nodeModel} preview={true} />;
};
function onNodeDelete(data: TabLayoutData) {
return services.ObjectService.DeleteBlock(data.blockId);
+1 -38
View File
@@ -17,7 +17,6 @@ import * as services from "@/store/services";
import * as keyutil from "@/util/keyutil";
import * as util from "@/util/util";
import clsx from "clsx";
import { produce } from "immer";
import * as jotai from "jotai";
import "public/xterm.css";
import * as React from "react";
@@ -105,17 +104,6 @@ const testVDom: VDomElem = {
],
};
function setBlockFocus(blockId: string) {
let winData = globalStore.get(atoms.waveWindow);
if (winData == null) {
return;
}
winData = produce(winData, (draft) => {
draft.activeblockid = blockId;
});
WOS.setObjectValue(winData, globalStore.set, true);
}
class TermViewModel {
viewType: string;
connected: boolean;
@@ -256,17 +244,10 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
const htmlElemFocusRef = React.useRef<HTMLInputElement>(null);
model.htmlElemFocusRef = htmlElemFocusRef;
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
const isFocusedAtom = useBlockAtom<boolean>(blockId, "isFocused", () => {
return jotai.atom((get) => {
const winData = get(atoms.waveWindow);
return winData?.activeblockid === blockId;
});
});
const termSettingsAtom = useSettingsAtom<TerminalConfigType>("term", (settings: SettingsConfigType) => {
return settings?.term;
});
const termSettings = jotai.useAtomValue(termSettingsAtom);
const isFocused = jotai.useAtomValue(isFocusedAtom);
React.useEffect(() => {
function handleTerminalKeydown(event: KeyboardEvent): boolean {
@@ -323,9 +304,6 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
);
(window as any).term = termWrap;
termRef.current = termWrap;
termWrap.addFocusListener(() => {
setBlockFocus(blockId);
});
const rszObs = new ResizeObserver(() => {
termWrap.handleResize_debounced();
});
@@ -358,16 +336,6 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
termMode = "term";
}
// set initial focus
React.useEffect(() => {
if (isFocused && termMode == "term") {
termRef.current?.terminal.focus();
}
if (isFocused && termMode == "html") {
htmlElemFocusRef.current?.focus();
}
}, []);
// set intitial controller status, and then subscribe for updates
React.useEffect(() => {
function updateShellProcStatus(status: string) {
@@ -455,11 +423,7 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
);
return (
<div
className={clsx("view-term", "term-mode-" + termMode, isFocused ? "is-focused" : null)}
onKeyDown={handleKeyDown}
ref={viewRef}
>
<div className={clsx("view-term", "term-mode-" + termMode)} onKeyDown={handleKeyDown} ref={viewRef}>
{typeAhead[blockId] && (
<TypeAheadModal
anchor={viewRef}
@@ -483,7 +447,6 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
if (htmlElemFocusRef.current != null) {
htmlElemFocusRef.current.focus();
}
setBlockFocus(blockId);
}}
>
<div key="htmlElemFocus" className="term-htmlelem-focus">