reorganize block view more

This commit is contained in:
sawka 2024-08-01 15:35:13 -07:00
parent c8cc1de2d4
commit 94d6c14011
5 changed files with 529 additions and 506 deletions

View File

@ -1,15 +1,12 @@
// Copyright 2024, Command Line Inc. // Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { useLongClick } from "@/app/hook/useLongClick"; import { BlockComponentModel, BlockProps } from "@/app/block/blocktypes";
import { PlotView } from "@/app/view/plotview/plotview"; import { PlotView } from "@/app/view/plotview/plotview";
import { PreviewView, makePreviewModel } from "@/app/view/preview/preview"; import { PreviewView, makePreviewModel } from "@/app/view/preview/preview";
import { Button } from "@/element/button";
import { ErrorBoundary } from "@/element/errorboundary"; import { ErrorBoundary } from "@/element/errorboundary";
import { CenteredDiv } from "@/element/quickelems"; import { CenteredDiv } from "@/element/quickelems";
import { ContextMenuModel } from "@/store/contextmenu"; import { atoms, setBlockFocus, useBlockAtom } from "@/store/global";
import { atoms, globalStore, setBlockFocus, useBlockAtom } from "@/store/global";
import * as services from "@/store/services";
import * as WOS from "@/store/wos"; import * as WOS from "@/store/wos";
import * as util from "@/util/util"; import * as util from "@/util/util";
import { CpuPlotView, makeCpuPlotViewModel } from "@/view/cpuplot/cpuplot"; import { CpuPlotView, makeCpuPlotViewModel } from "@/view/cpuplot/cpuplot";
@ -17,511 +14,13 @@ import { HelpView } from "@/view/helpview/helpview";
import { TerminalView, makeTerminalModel } from "@/view/term/term"; import { TerminalView, makeTerminalModel } from "@/view/term/term";
import { WaveAi, makeWaveAiViewModel } from "@/view/waveai/waveai"; import { WaveAi, makeWaveAiViewModel } from "@/view/waveai/waveai";
import { WebView, makeWebViewModel } from "@/view/webview/webview"; import { WebView, makeWebViewModel } from "@/view/webview/webview";
import clsx from "clsx";
import * as jotai from "jotai"; import * as jotai from "jotai";
import * as React from "react"; import * as React from "react";
import { BlockFrame } from "./blockframe";
import { blockViewToIcon, blockViewToName } from "./blockutil";
import { getLayoutStateAtomForTab } from "@/layout/lib/layoutAtom";
import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil";
import { isBlockMagnified } from "@/util/layoututil";
import "./block.less"; import "./block.less";
export interface LayoutComponentModel {
disablePointerEvents: boolean;
onClose?: () => void;
onMagnifyToggle?: () => void;
dragHandleRef?: React.RefObject<HTMLDivElement>;
}
interface BlockProps {
blockId: string;
preview: boolean;
layoutModel: LayoutComponentModel;
}
interface BlockComponentModel {
onClick?: () => void;
onFocusCapture?: React.FocusEventHandler<HTMLDivElement>;
blockRef?: React.RefObject<HTMLDivElement>;
}
interface BlockFrameProps {
blockId: string;
blockModel?: BlockComponentModel;
layoutModel?: LayoutComponentModel;
viewModel?: ViewModel;
preview: boolean;
numBlocksInTab?: number;
children?: React.ReactNode;
}
const colorRegex = /^((#[0-9a-f]{6,8})|([a-z]+))$/;
function processTitleString(titleString: string): React.ReactNode[] {
if (titleString == null) {
return null;
}
const tagRegex = /<(\/)?([a-z]+)(?::([#a-z0-9@-]+))?>/g;
let lastIdx = 0;
let match;
let partsStack = [[]];
while ((match = tagRegex.exec(titleString)) != null) {
const lastPart = partsStack[partsStack.length - 1];
const before = titleString.substring(lastIdx, match.index);
lastPart.push(before);
lastIdx = match.index + match[0].length;
const [_, isClosing, tagName, tagParam] = match;
if (tagName == "icon" && !isClosing) {
if (tagParam == null) {
continue;
}
const iconClass = util.makeIconClass(tagParam, false);
if (iconClass == null) {
continue;
}
lastPart.push(<i key={match.index} className={iconClass} />);
continue;
}
if (tagName == "c" || tagName == "color") {
if (isClosing) {
if (partsStack.length <= 1) {
continue;
}
partsStack.pop();
continue;
}
if (tagParam == null) {
continue;
}
if (!tagParam.match(colorRegex)) {
continue;
}
let children = [];
const rtag = React.createElement("span", { key: match.index, style: { color: tagParam } }, children);
lastPart.push(rtag);
partsStack.push(children);
continue;
}
if (tagName == "i" || tagName == "b") {
if (isClosing) {
if (partsStack.length <= 1) {
continue;
}
partsStack.pop();
continue;
}
let children = [];
const rtag = React.createElement(tagName, { key: match.index }, children);
lastPart.push(rtag);
partsStack.push(children);
continue;
}
}
partsStack[partsStack.length - 1].push(titleString.substring(lastIdx));
return partsStack[0];
}
function getBlockHeaderIcon(blockIcon: string, blockData: Block): React.ReactNode {
let blockIconElem: React.ReactNode = null;
if (util.isBlank(blockIcon)) {
blockIcon = "square";
}
let iconColor = blockData?.meta?.["icon:color"];
if (iconColor && !iconColor.match(colorRegex)) {
iconColor = null;
}
let iconStyle = null;
if (!util.isBlank(iconColor)) {
iconStyle = { color: iconColor };
}
const iconClass = util.makeIconClass(blockIcon, true);
if (iconClass != null) {
blockIconElem = <i key="icon" style={iconStyle} className={clsx(`block-frame-icon`, iconClass)} />;
}
return blockIconElem;
}
function getBlockHeaderText(blockIcon: string, blockData: Block, settings: SettingsConfigType): React.ReactNode {
if (!blockData) {
return "no block data";
}
let blockIdStr = "";
if (settings?.blockheader?.showblockids) {
blockIdStr = ` [${blockData.oid.substring(0, 8)}]`;
}
let blockIconElem = getBlockHeaderIcon(blockIcon, blockData);
if (!util.isBlank(blockData?.meta?.title)) {
try {
const rtn = processTitleString(blockData.meta.title) ?? [];
return [blockIconElem, ...rtn, blockIdStr == "" ? null : blockIdStr];
} catch (e) {
console.error("error processing title", blockData.meta.title, e);
return [blockIconElem, blockData.meta.title + blockIdStr];
}
}
let viewString = blockData?.meta?.view;
if (blockData?.meta?.controller == "cmd") {
viewString = "cmd";
}
return [blockIconElem, viewString + blockIdStr];
}
function handleHeaderContextMenu(
e: React.MouseEvent<HTMLDivElement>,
blockData: Block,
viewModel: ViewModel,
onMagnifyToggle: () => void,
onClose: () => void
) {
e.preventDefault();
e.stopPropagation();
let menu: ContextMenuItem[] = [
{
label: "Magnify Block",
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);
}
const IconButton = React.memo(({ decl, className }: { decl: HeaderIconButton; className?: string }) => {
const buttonRef = React.useRef<HTMLDivElement>(null);
useLongClick(buttonRef, decl.click, decl.longClick);
return (
<div ref={buttonRef} className={clsx(className)} title={decl.title}>
<i className={util.makeIconClass(decl.icon, true)} />
</div>
);
});
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>
);
});
const BlockFrame_Default_Component = ({
blockId,
layoutModel,
viewModel,
blockModel,
preview,
numBlocksInTab,
children,
}: BlockFrameProps) => {
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
const settingsConfig = jotai.useAtomValue(atoms.settingsConfigAtom);
const isFocusedAtom = useBlockAtom<boolean>(blockId, "isFocused", () => {
return jotai.atom((get) => {
const winData = get(atoms.waveWindow);
return winData?.activeblockid === blockId;
});
});
let isFocused = jotai.useAtomValue(isFocusedAtom);
const viewIconUnion = util.useAtomValueSafe(viewModel.viewIcon) ?? blockViewToIcon(blockData?.meta?.view);
const viewName = util.useAtomValueSafe(viewModel.viewName) ?? blockViewToName(blockData?.meta?.view);
const headerTextUnion = util.useAtomValueSafe(viewModel.viewText);
const preIconButton = util.useAtomValueSafe(viewModel.preIconButton);
const endIconButtons = util.useAtomValueSafe(viewModel.endIconButtons);
const customBg = util.useAtomValueSafe(viewModel.blockBg);
const tabId = globalStore.get(atoms.activeTabId);
const tabAtom = WOS.getWaveObjectAtom<Tab>(WOS.makeORef("tab", tabId));
const layoutTreeState = util.useAtomValueSafe(getLayoutStateAtomForTab(tabId, tabAtom));
if (preview) {
isFocused = true;
}
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 viewIconElem: JSX.Element = null;
if (viewIconUnion == null || typeof viewIconUnion === "string") {
const viewIcon = viewIconUnion as string;
viewIconElem = <div className="block-frame-view-icon">{getBlockHeaderIcon(viewIcon, blockData)}</div>;
} else {
viewIconElem = <IconButton decl={viewIconUnion} className="block-frame-view-icon" />;
}
let preIconButtonElem: JSX.Element = null;
if (preIconButton) {
preIconButtonElem = <IconButton decl={preIconButton} className="block-frame-preicon-button" />;
}
const endIconsElem: JSX.Element[] = [];
if (endIconButtons && endIconButtons.length > 0) {
endIconsElem.push(
...endIconButtons.map((button, idx) => (
<IconButton key={idx} decl={button} className="block-frame-endicon-button" />
))
);
}
const settingsDecl: HeaderIconButton = {
elemtype: "iconbutton",
icon: "cog",
title: "Settings",
click: (e) =>
handleHeaderContextMenu(e, blockData, viewModel, layoutModel?.onMagnifyToggle, layoutModel?.onClose),
};
endIconsElem.push(
<IconButton key="settings" decl={settingsDecl} className="block-frame-endicon-button block-frame-settings" />
);
if (isBlockMagnified(layoutTreeState, blockId)) {
const magnifyDecl: HeaderIconButton = {
elemtype: "iconbutton",
icon: "regular@magnifying-glass-minus",
title: "Minimize",
click: layoutModel?.onMagnifyToggle,
};
endIconsElem.push(
<IconButton key="magnify" decl={magnifyDecl} className="block-frame-endicon-button block-frame-magnify" />
);
}
const closeDecl: HeaderIconButton = {
elemtype: "iconbutton",
icon: "xmark-large",
title: "Close",
click: layoutModel?.onClose,
};
endIconsElem.push(
<IconButton key="close" decl={closeDecl} className="block-frame-endicon-button block-frame-default-close" />
);
function renderHeaderElements(headerTextUnion: HeaderElem[]): JSX.Element[] {
const headerTextElems: JSX.Element[] = [];
function renderElement(elem: HeaderElem, key: number): JSX.Element {
if (elem.elemtype == "iconbutton") {
return (
<IconButton
key={key}
decl={elem}
className={clsx("block-frame-header-iconbutton", elem.className)}
/>
);
} else if (elem.elemtype == "input") {
return <Input key={key} decl={elem} className={clsx("block-frame-input", elem.className)} />;
} else if (elem.elemtype == "text") {
return (
<div key={key} className="block-frame-text">
{elem.text}
</div>
);
} else if (elem.elemtype == "textbutton") {
return (
<Button key={key} className={elem.className} onClick={(e) => elem.onClick(e)}>
{elem.text}
</Button>
);
} else if (elem.elemtype == "div") {
return (
<div
key={key}
className={clsx("block-frame-div", elem.className)}
onMouseOver={elem.onMouseOver}
onMouseOut={elem.onMouseOut}
>
{elem.children.map((child, childIdx) => renderElement(child, childIdx))}
</div>
);
}
return null;
}
for (let idx = 0; idx < headerTextUnion.length; idx++) {
const elem = headerTextUnion[idx];
const renderedElement = renderElement(elem, idx);
if (renderedElement) {
headerTextElems.push(renderedElement);
}
}
return headerTextElems;
}
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));
}
function handleDoubleClick() {
layoutModel?.onMagnifyToggle();
}
function handleKeyDown(e: React.KeyboardEvent<HTMLDivElement>) {
const waveEvent = adaptFromReactOrNativeKeyEvent(e);
if (checkKeyPressed(waveEvent, "Cmd:m")) {
e.preventDefault();
layoutModel?.onMagnifyToggle();
return;
}
}
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",
isFocused ? "block-focused" : null,
preview ? "block-preview" : null,
numBlocksInTab == 1 ? "block-no-highlight" : null,
"block-" + blockId
)}
onClick={blockModel?.onClick}
onFocusCapture={blockModel?.onFocusCapture}
ref={blockModel?.blockRef}
style={style}
onKeyDown={handleKeyDown}
>
<div className="block-mask"></div>
<div className="block-frame-default-inner" style={innerStyle}>
<div
className="block-frame-default-header"
ref={layoutModel?.dragHandleRef}
onDoubleClick={handleDoubleClick}
onContextMenu={(e) =>
handleHeaderContextMenu(
e,
blockData,
viewModel,
layoutModel?.onMagnifyToggle,
layoutModel?.onClose
)
}
>
{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>
<div className="block-frame-textelems-wrapper">{headerTextElems}</div>
<div className="block-frame-end-icons">{endIconsElem}</div>
</div>
{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.blockId;
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} />;
});
function blockViewToIcon(view: string): string {
if (view == "term") {
return "terminal";
}
if (view == "preview") {
return "file";
}
if (view == "web") {
return "globe";
}
if (view == "waveai") {
return "sparkles";
}
if (view == "help") {
return "circle-question";
}
return "square";
}
function blockViewToName(view: string): string {
if (util.isBlank(view)) {
return "(No View)";
}
if (view == "term") {
return "Terminal";
}
if (view == "preview") {
return "Preview";
}
if (view == "web") {
return "Web";
}
if (view == "waveai") {
return "WaveAI";
}
if (view == "help") {
return "Help";
}
return view;
}
function getViewElemAndModel( function getViewElemAndModel(
blockId: string, blockId: string,
blockView: string, blockView: string,

View File

@ -0,0 +1,305 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { blockViewToIcon, blockViewToName, getBlockHeaderIcon, IconButton, Input } 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 * as services from "@/app/store/services";
import { getLayoutStateAtomForTab } from "@/layout/lib/layoutAtom";
import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil";
import { isBlockMagnified } from "@/util/layoututil";
import * as util from "@/util/util";
import clsx from "clsx";
import * as jotai from "jotai";
import * as React from "react";
import { BlockFrameProps } from "./blocktypes";
function handleHeaderContextMenu(
e: React.MouseEvent<HTMLDivElement>,
blockData: Block,
viewModel: ViewModel,
onMagnifyToggle: () => void,
onClose: () => void
) {
e.preventDefault();
e.stopPropagation();
let menu: ContextMenuItem[] = [
{
label: "Magnify Block",
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);
}
const BlockFrame_Default_Component = ({
blockId,
layoutModel,
viewModel,
blockModel,
preview,
numBlocksInTab,
children,
}: BlockFrameProps) => {
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
const settingsConfig = jotai.useAtomValue(atoms.settingsConfigAtom);
const isFocusedAtom = useBlockAtom<boolean>(blockId, "isFocused", () => {
return jotai.atom((get) => {
const winData = get(atoms.waveWindow);
return winData?.activeblockid === blockId;
});
});
let isFocused = jotai.useAtomValue(isFocusedAtom);
const viewIconUnion = util.useAtomValueSafe(viewModel.viewIcon) ?? blockViewToIcon(blockData?.meta?.view);
const viewName = util.useAtomValueSafe(viewModel.viewName) ?? blockViewToName(blockData?.meta?.view);
const headerTextUnion = util.useAtomValueSafe(viewModel.viewText);
const preIconButton = util.useAtomValueSafe(viewModel.preIconButton);
const endIconButtons = util.useAtomValueSafe(viewModel.endIconButtons);
const customBg = util.useAtomValueSafe(viewModel.blockBg);
const tabId = globalStore.get(atoms.activeTabId);
const tabAtom = WOS.getWaveObjectAtom<Tab>(WOS.makeORef("tab", tabId));
const layoutTreeState = util.useAtomValueSafe(getLayoutStateAtomForTab(tabId, tabAtom));
if (preview) {
isFocused = true;
}
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 viewIconElem: JSX.Element = null;
if (viewIconUnion == null || typeof viewIconUnion === "string") {
const viewIcon = viewIconUnion as string;
viewIconElem = <div className="block-frame-view-icon">{getBlockHeaderIcon(viewIcon, blockData)}</div>;
} else {
viewIconElem = <IconButton decl={viewIconUnion} className="block-frame-view-icon" />;
}
let preIconButtonElem: JSX.Element = null;
if (preIconButton) {
preIconButtonElem = <IconButton decl={preIconButton} className="block-frame-preicon-button" />;
}
const endIconsElem: JSX.Element[] = [];
if (endIconButtons && endIconButtons.length > 0) {
endIconsElem.push(
...endIconButtons.map((button, idx) => (
<IconButton key={idx} decl={button} className="block-frame-endicon-button" />
))
);
}
const settingsDecl: HeaderIconButton = {
elemtype: "iconbutton",
icon: "cog",
title: "Settings",
click: (e) =>
handleHeaderContextMenu(e, blockData, viewModel, layoutModel?.onMagnifyToggle, layoutModel?.onClose),
};
endIconsElem.push(
<IconButton key="settings" decl={settingsDecl} className="block-frame-endicon-button block-frame-settings" />
);
if (isBlockMagnified(layoutTreeState, blockId)) {
const magnifyDecl: HeaderIconButton = {
elemtype: "iconbutton",
icon: "regular@magnifying-glass-minus",
title: "Minimize",
click: layoutModel?.onMagnifyToggle,
};
endIconsElem.push(
<IconButton key="magnify" decl={magnifyDecl} className="block-frame-endicon-button block-frame-magnify" />
);
}
const closeDecl: HeaderIconButton = {
elemtype: "iconbutton",
icon: "xmark-large",
title: "Close",
click: layoutModel?.onClose,
};
endIconsElem.push(
<IconButton key="close" decl={closeDecl} className="block-frame-endicon-button block-frame-default-close" />
);
function renderHeaderElements(headerTextUnion: HeaderElem[]): JSX.Element[] {
const headerTextElems: JSX.Element[] = [];
function renderElement(elem: HeaderElem, key: number): JSX.Element {
if (elem.elemtype == "iconbutton") {
return (
<IconButton
key={key}
decl={elem}
className={clsx("block-frame-header-iconbutton", elem.className)}
/>
);
} else if (elem.elemtype == "input") {
return <Input key={key} decl={elem} className={clsx("block-frame-input", elem.className)} />;
} else if (elem.elemtype == "text") {
return (
<div key={key} className="block-frame-text">
{elem.text}
</div>
);
} else if (elem.elemtype == "textbutton") {
return (
<Button key={key} className={elem.className} onClick={(e) => elem.onClick(e)}>
{elem.text}
</Button>
);
} else if (elem.elemtype == "div") {
return (
<div
key={key}
className={clsx("block-frame-div", elem.className)}
onMouseOver={elem.onMouseOver}
onMouseOut={elem.onMouseOut}
>
{elem.children.map((child, childIdx) => renderElement(child, childIdx))}
</div>
);
}
return null;
}
for (let idx = 0; idx < headerTextUnion.length; idx++) {
const elem = headerTextUnion[idx];
const renderedElement = renderElement(elem, idx);
if (renderedElement) {
headerTextElems.push(renderedElement);
}
}
return headerTextElems;
}
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));
}
function handleDoubleClick() {
layoutModel?.onMagnifyToggle();
}
function handleKeyDown(e: React.KeyboardEvent<HTMLDivElement>) {
const waveEvent = adaptFromReactOrNativeKeyEvent(e);
if (checkKeyPressed(waveEvent, "Cmd:m")) {
e.preventDefault();
layoutModel?.onMagnifyToggle();
return;
}
}
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",
isFocused ? "block-focused" : null,
preview ? "block-preview" : null,
numBlocksInTab == 1 ? "block-no-highlight" : null,
"block-" + blockId
)}
onClick={blockModel?.onClick}
onFocusCapture={blockModel?.onFocusCapture}
ref={blockModel?.blockRef}
style={style}
onKeyDown={handleKeyDown}
>
<div className="block-mask"></div>
<div className="block-frame-default-inner" style={innerStyle}>
<div
className="block-frame-default-header"
ref={layoutModel?.dragHandleRef}
onDoubleClick={handleDoubleClick}
onContextMenu={(e) =>
handleHeaderContextMenu(
e,
blockData,
viewModel,
layoutModel?.onMagnifyToggle,
layoutModel?.onClose
)
}
>
{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>
<div className="block-frame-textelems-wrapper">{headerTextElems}</div>
<div className="block-frame-end-icons">{endIconsElem}</div>
</div>
{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.blockId;
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 };

View File

@ -0,0 +1,31 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
export interface LayoutComponentModel {
disablePointerEvents: boolean;
onClose?: () => void;
onMagnifyToggle?: () => void;
dragHandleRef?: React.RefObject<HTMLDivElement>;
}
export interface BlockProps {
blockId: string;
preview: boolean;
layoutModel: LayoutComponentModel;
}
export interface BlockComponentModel {
onClick?: () => void;
onFocusCapture?: React.FocusEventHandler<HTMLDivElement>;
blockRef?: React.RefObject<HTMLDivElement>;
}
export interface BlockFrameProps {
blockId: string;
blockModel?: BlockComponentModel;
layoutModel?: LayoutComponentModel;
viewModel?: ViewModel;
preview: boolean;
numBlocksInTab?: number;
children?: React.ReactNode;
}

View File

@ -0,0 +1,187 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { useLongClick } from "@/app/hook/useLongClick";
import * as util from "@/util/util";
import clsx from "clsx";
import * as React from "react";
export const colorRegex = /^((#[0-9a-f]{6,8})|([a-z]+))$/;
export function blockViewToIcon(view: string): string {
if (view == "term") {
return "terminal";
}
if (view == "preview") {
return "file";
}
if (view == "web") {
return "globe";
}
if (view == "waveai") {
return "sparkles";
}
if (view == "help") {
return "circle-question";
}
return "square";
}
export function blockViewToName(view: string): string {
if (util.isBlank(view)) {
return "(No View)";
}
if (view == "term") {
return "Terminal";
}
if (view == "preview") {
return "Preview";
}
if (view == "web") {
return "Web";
}
if (view == "waveai") {
return "WaveAI";
}
if (view == "help") {
return "Help";
}
return view;
}
export function processTitleString(titleString: string): React.ReactNode[] {
if (titleString == null) {
return null;
}
const tagRegex = /<(\/)?([a-z]+)(?::([#a-z0-9@-]+))?>/g;
let lastIdx = 0;
let match;
let partsStack = [[]];
while ((match = tagRegex.exec(titleString)) != null) {
const lastPart = partsStack[partsStack.length - 1];
const before = titleString.substring(lastIdx, match.index);
lastPart.push(before);
lastIdx = match.index + match[0].length;
const [_, isClosing, tagName, tagParam] = match;
if (tagName == "icon" && !isClosing) {
if (tagParam == null) {
continue;
}
const iconClass = util.makeIconClass(tagParam, false);
if (iconClass == null) {
continue;
}
lastPart.push(<i key={match.index} className={iconClass} />);
continue;
}
if (tagName == "c" || tagName == "color") {
if (isClosing) {
if (partsStack.length <= 1) {
continue;
}
partsStack.pop();
continue;
}
if (tagParam == null) {
continue;
}
if (!tagParam.match(colorRegex)) {
continue;
}
let children = [];
const rtag = React.createElement("span", { key: match.index, style: { color: tagParam } }, children);
lastPart.push(rtag);
partsStack.push(children);
continue;
}
if (tagName == "i" || tagName == "b") {
if (isClosing) {
if (partsStack.length <= 1) {
continue;
}
partsStack.pop();
continue;
}
let children = [];
const rtag = React.createElement(tagName, { key: match.index }, children);
lastPart.push(rtag);
partsStack.push(children);
continue;
}
}
partsStack[partsStack.length - 1].push(titleString.substring(lastIdx));
return partsStack[0];
}
export function getBlockHeaderIcon(blockIcon: string, blockData: Block): React.ReactNode {
let blockIconElem: React.ReactNode = null;
if (util.isBlank(blockIcon)) {
blockIcon = "square";
}
let iconColor = blockData?.meta?.["icon:color"];
if (iconColor && !iconColor.match(colorRegex)) {
iconColor = null;
}
let iconStyle = null;
if (!util.isBlank(iconColor)) {
iconStyle = { color: iconColor };
}
const iconClass = util.makeIconClass(blockIcon, true);
if (iconClass != null) {
blockIconElem = <i key="icon" style={iconStyle} className={clsx(`block-frame-icon`, iconClass)} />;
}
return blockIconElem;
}
export function getBlockHeaderText(blockIcon: string, blockData: Block, settings: SettingsConfigType): React.ReactNode {
if (!blockData) {
return "no block data";
}
let blockIdStr = "";
if (settings?.blockheader?.showblockids) {
blockIdStr = ` [${blockData.oid.substring(0, 8)}]`;
}
let blockIconElem = getBlockHeaderIcon(blockIcon, blockData);
if (!util.isBlank(blockData?.meta?.title)) {
try {
const rtn = processTitleString(blockData.meta.title) ?? [];
return [blockIconElem, ...rtn, blockIdStr == "" ? null : blockIdStr];
} catch (e) {
console.error("error processing title", blockData.meta.title, e);
return [blockIconElem, blockData.meta.title + blockIdStr];
}
}
let viewString = blockData?.meta?.view;
if (blockData?.meta?.controller == "cmd") {
viewString = "cmd";
}
return [blockIconElem, viewString + blockIdStr];
}
export const IconButton = React.memo(({ decl, className }: { decl: HeaderIconButton; className?: string }) => {
const buttonRef = React.useRef<HTMLDivElement>(null);
useLongClick(buttonRef, decl.click, decl.longClick);
return (
<div ref={buttonRef} className={clsx(className)} title={decl.title}>
<i className={util.makeIconClass(decl.icon, true)} />
</div>
);
});
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>
);
});

View File

@ -1,7 +1,8 @@
// Copyright 2023, Command Line Inc. // Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { Block, LayoutComponentModel } from "@/app/block/block"; import { Block } from "@/app/block/block";
import { LayoutComponentModel } from "@/app/block/blocktypes";
import { getApi } from "@/store/global"; import { getApi } from "@/store/global";
import * as services from "@/store/services"; import * as services from "@/store/services";
import * as WOS from "@/store/wos"; import * as WOS from "@/store/wos";