waveterm/frontend/app/block/blockframe.tsx

321 lines
12 KiB
TypeScript
Raw Normal View History

2024-08-02 00:35:13 +02:00
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import {
blockViewToIcon,
blockViewToName,
ConnectionButton,
getBlockHeaderIcon,
IconButton,
Input,
} from "@/app/block/blockutil";
2024-08-02 00:35:13 +02:00
import { Button } from "@/app/element/button";
import { ContextMenuModel } from "@/app/store/contextmenu";
import { atoms, globalStore, WOS } from "@/app/store/global";
2024-08-02 00:35:13 +02:00
import * as services from "@/app/store/services";
import { MagnifyIcon } from "@/element/magnify";
import { NodeModel } from "@/layout/index";
2024-08-20 03:41:47 +02:00
import { checkKeyPressed, keydownWrapper } from "@/util/keyutil";
2024-08-02 00:35:13 +02:00
import * as util from "@/util/util";
import clsx from "clsx";
import * as jotai from "jotai";
import * as React from "react";
import { BlockFrameProps } from "./blocktypes";
2024-08-02 00:35:13 +02:00
function handleHeaderContextMenu(
e: React.MouseEvent<HTMLDivElement>,
blockData: Block,
viewModel: ViewModel,
magnified: boolean,
2024-08-02 00:35:13 +02:00
onMagnifyToggle: () => void,
onClose: () => void
) {
e.preventDefault();
e.stopPropagation();
let menu: ContextMenuItem[] = [
{
label: magnified ? "Un-magnify Block" : "Magnify Block",
2024-08-02 00:35:13 +02:00
click: () => {
onMagnifyToggle();
},
},
{
label: "Move to New Window",
click: () => {
const currentTabId = globalStore.get(atoms.activeTabId);
try {
services.WindowService.MoveBlockToNewWindow(currentTabId, blockData.oid);
} catch (e) {
console.error("error moving block to new window", e);
}
},
},
{ type: "separator" },
{
label: "Copy BlockId",
click: () => {
navigator.clipboard.writeText(blockData.oid);
},
},
];
const extraItems = viewModel?.getSettingsMenuItems?.();
if (extraItems && extraItems.length > 0) menu.push({ type: "separator" }, ...extraItems);
menu.push(
{ type: "separator" },
{
label: "Close Block",
click: onClose,
}
);
ContextMenuModel.showContextMenu(menu, e);
}
2024-08-02 00:51:38 +02:00
function getViewIconElem(viewIconUnion: string | HeaderIconButton, blockData: Block): JSX.Element {
2024-08-02 00:35:13 +02:00
if (viewIconUnion == null || typeof viewIconUnion === "string") {
const viewIcon = viewIconUnion as string;
2024-08-02 00:51:38 +02:00
return <div className="block-frame-view-icon">{getBlockHeaderIcon(viewIcon, blockData)}</div>;
2024-08-02 00:35:13 +02:00
} else {
2024-08-02 00:51:38 +02:00
return <IconButton decl={viewIconUnion} className="block-frame-view-icon" />;
2024-08-02 00:35:13 +02:00
}
2024-08-02 00:51:38 +02:00
}
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(
viewModel: ViewModel,
magnified: boolean,
toggleMagnify: () => void,
onClose: () => void,
onContextMenu: (e: React.MouseEvent<HTMLDivElement>) => void
): JSX.Element[] {
2024-08-02 00:35:13 +02:00
const endIconsElem: JSX.Element[] = [];
2024-08-02 00:51:38 +02:00
const endIconButtons = util.useAtomValueSafe(viewModel.endIconButtons);
2024-08-02 00:35:13 +02:00
if (endIconButtons && endIconButtons.length > 0) {
endIconsElem.push(...endIconButtons.map((button, idx) => <IconButton key={idx} decl={button} />));
2024-08-02 00:35:13 +02:00
}
const settingsDecl: HeaderIconButton = {
elemtype: "iconbutton",
icon: "cog",
title: "Settings",
click: onContextMenu,
2024-08-02 00:35:13 +02:00
};
endIconsElem.push(<IconButton key="settings" decl={settingsDecl} className="block-frame-settings" />);
endIconsElem.push(<OptMagnifyButton key="unmagnify" magnified={magnified} toggleMagnify={toggleMagnify} />);
2024-08-02 00:35:13 +02:00
const closeDecl: HeaderIconButton = {
elemtype: "iconbutton",
icon: "xmark-large",
title: "Close",
click: onClose,
2024-08-02 00:35:13 +02:00
};
endIconsElem.push(<IconButton key="close" decl={closeDecl} className="block-frame-default-close" />);
2024-08-02 00:51:38 +02:00
return endIconsElem;
}
2024-08-02 00:35:13 +02:00
const BlockFrame_Header = ({ nodeModel, viewModel, preview }: BlockFrameProps) => {
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", nodeModel.blockId));
2024-08-02 00:51:38 +02:00
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 onContextMenu = React.useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
handleHeaderContextMenu(e, blockData, viewModel, magnified, nodeModel.toggleMagnify, nodeModel.onClose);
},
[magnified]
);
2024-08-02 00:35:13 +02:00
const endIconsElem = computeEndIcons(
viewModel,
magnified,
nodeModel.toggleMagnify,
nodeModel.onClose,
onContextMenu
);
2024-08-02 00:51:38 +02:00
const viewIconElem = getViewIconElem(viewIconUnion, blockData);
let preIconButtonElem: JSX.Element = null;
if (preIconButton) {
preIconButtonElem = <IconButton decl={preIconButton} className="block-frame-preicon-button" />;
}
2024-08-02 00:35:13 +02:00
const headerTextElems: JSX.Element[] = [];
if (typeof headerTextUnion === "string") {
if (!util.isBlank(headerTextUnion)) {
headerTextElems.push(
<div key="text" className="block-frame-text">
{headerTextUnion}
</div>
);
}
} else if (Array.isArray(headerTextUnion)) {
headerTextElems.push(...renderHeaderElements(headerTextUnion, preview));
2024-08-02 00:35:13 +02:00
}
2024-08-02 00:51:38 +02:00
return (
<div className="block-frame-default-header" ref={dragHandleRef} onContextMenu={onContextMenu}>
2024-08-02 00:51:38 +02:00
{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">[{nodeModel.blockId.substring(0, 8)}]</div>
2024-08-02 00:51:38 +02:00
)}
</div>
<div className="block-frame-textelems-wrapper">{headerTextElems}</div>
<div className="block-frame-end-icons">{endIconsElem}</div>
</div>
);
};
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)} preview={preview} />;
} else if (elem.elemtype == "text") {
return <div className="block-frame-text">{elem.text}</div>;
} else if (elem.elemtype == "textbutton") {
return (
<Button className={elem.className} onClick={(e) => elem.onClick(e)}>
{elem.text}
</Button>
);
} else if (elem.elemtype == "connectionbutton") {
return <ConnectionButton decl={elem} />;
} else if (elem.elemtype == "div") {
return (
<div
className={clsx("block-frame-div", elem.className)}
onMouseOver={elem.onMouseOver}
onMouseOut={elem.onMouseOut}
>
{elem.children.map((child, childIdx) => (
<HeaderTextElem elem={child} key={childIdx} preview={preview} />
))}
</div>
);
2024-08-02 00:51:38 +02:00
}
return null;
});
2024-08-02 00:51:38 +02:00
function renderHeaderElements(headerTextUnion: HeaderElem[], preview: boolean): JSX.Element[] {
const headerTextElems: JSX.Element[] = [];
2024-08-02 00:51:38 +02:00
for (let idx = 0; idx < headerTextUnion.length; idx++) {
const elem = headerTextUnion[idx];
const renderedElement = <HeaderTextElem elem={elem} key={idx} preview={preview} />;
2024-08-02 00:51:38 +02:00
if (renderedElement) {
headerTextElems.push(renderedElement);
}
2024-08-02 00:35:13 +02:00
}
2024-08-02 00:51:38 +02:00
return headerTextElems;
}
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", nodeModel.blockId));
const style: React.CSSProperties = {};
if (!isFocused && blockData?.meta?.["frame:bordercolor"]) {
style.borderColor = blockData.meta["frame:bordercolor"];
}
if (isFocused && blockData?.meta?.["frame:bordercolor:focused"]) {
style.borderColor = blockData.meta["frame:bordercolor:focused"];
}
let innerElem = null;
if (isLayoutMode) {
innerElem = (
<div className="block-mask-inner">
<div className="bignum">{blockNum}</div>
</div>
);
}
return (
<div className={clsx("block-mask", { "is-layoutmode": isLayoutMode })} style={style}>
{innerElem}
</div>
);
};
2024-08-02 00:51:38 +02:00
const BlockFrame_Default_Component = (props: BlockFrameProps) => {
const { nodeModel, viewModel, blockModel, preview, numBlocksInTab, children } = props;
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", nodeModel.blockId));
const isFocused = jotai.useAtomValue(nodeModel.isFocused);
2024-08-02 00:51:38 +02:00
const viewIconUnion = util.useAtomValueSafe(viewModel.viewIcon) ?? blockViewToIcon(blockData?.meta?.view);
const customBg = util.useAtomValueSafe(viewModel.blockBg);
2024-08-02 00:51:38 +02:00
const viewIconElem = getViewIconElem(viewIconUnion, blockData);
2024-08-20 03:41:47 +02:00
function handleKeyDown(waveEvent: WaveKeyboardEvent): boolean {
2024-08-02 00:35:13 +02:00
if (checkKeyPressed(waveEvent, "Cmd:m")) {
nodeModel.toggleMagnify();
2024-08-20 03:41:47 +02:00
return true;
2024-08-02 00:35:13 +02:00
}
2024-08-20 03:41:47 +02:00
if (viewModel?.keyDownHandler) {
return viewModel.keyDownHandler(waveEvent);
}
return false;
2024-08-02 00:35:13 +02:00
}
const innerStyle: React.CSSProperties = {};
if (!preview && customBg?.bg != null) {
innerStyle.background = customBg.bg;
if (customBg["bg:opacity"] != null) {
innerStyle.opacity = customBg["bg:opacity"];
}
if (customBg["bg:blendmode"] != null) {
innerStyle.backgroundBlendMode = customBg["bg:blendmode"];
}
}
const previewElem = <div className="block-frame-preview">{viewIconElem}</div>;
return (
<div
className={clsx("block", "block-frame-default", "block-" + nodeModel.blockId, {
"block-focused": isFocused || preview,
"block-preview": preview,
"block-no-highlight": numBlocksInTab === 1,
})}
2024-08-02 00:35:13 +02:00
onClick={blockModel?.onClick}
onFocusCapture={blockModel?.onFocusCapture}
ref={blockModel?.blockRef}
2024-08-20 03:41:47 +02:00
onKeyDown={keydownWrapper(handleKeyDown)}
2024-08-02 00:35:13 +02:00
>
<BlockMask nodeModel={nodeModel} />
2024-08-02 00:35:13 +02:00
<div className="block-frame-default-inner" style={innerStyle}>
2024-08-02 00:51:38 +02:00
<BlockFrame_Header {...props} />
2024-08-02 00:35:13 +02:00
{preview ? previewElem : children}
</div>
</div>
);
};
const BlockFrame_Default = React.memo(BlockFrame_Default_Component) as typeof BlockFrame_Default_Component;
const BlockFrame = React.memo((props: BlockFrameProps) => {
const blockId = props.nodeModel.blockId;
2024-08-02 00:35:13 +02:00
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
const tabData = jotai.useAtomValue(atoms.tabAtom);
if (!blockId || !blockData) {
return null;
}
let FrameElem = BlockFrame_Default;
const numBlocks = tabData?.blockids?.length ?? 0;
return <FrameElem {...props} numBlocksInTab={numBlocks} />;
});
export { BlockFrame };