implement Cmd-I and restructure block viewmodels (#257)

This commit is contained in:
Mike Sawka 2024-08-21 15:49:23 -07:00 committed by GitHub
parent 7df91de2e5
commit dedfc31344
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 174 additions and 96 deletions

View File

@ -1,7 +1,7 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { atoms, createBlock, globalStore, setBlockFocus, WOS } from "@/app/store/global";
import { atoms, createBlock, getViewModel, globalStore, setBlockFocus, WOS } from "@/app/store/global";
import { deleteLayoutModelForTab, getLayoutModelForTab } from "@/layout/index";
import * as services from "@/store/services";
import * as keyutil from "@/util/keyutil";
@ -225,6 +225,25 @@ async function handleCmdT() {
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?.();
}
function appHandleKeyDown(waveEvent: WaveKeyboardEvent): boolean {
if (waveEvent.key === "Control" || waveEvent.key === "Shift" || waveEvent.key === "Meta") {
if (waveEvent.control && waveEvent.shift && !waveEvent.meta) {
@ -251,6 +270,10 @@ function appHandleKeyDown(waveEvent: WaveKeyboardEvent): boolean {
handleCmdT();
return true;
}
if (keyutil.checkKeyPressed(waveEvent, "Cmd:i")) {
handleCmdI();
return true;
}
if (keyutil.checkKeyPressed(waveEvent, "Cmd:t")) {
const workspace = globalStore.get(atoms.workspace);
const newTabName = `T${workspace.tabids.length + 1}`;

View File

@ -1,19 +1,19 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { BlockComponentModel, BlockProps } from "@/app/block/blocktypes";
import { BlockComponentModel, BlockProps, LayoutComponentModel } from "@/app/block/blocktypes";
import { PlotView } from "@/app/view/plotview/plotview";
import { PreviewView, makePreviewModel } from "@/app/view/preview/preview";
import { PreviewModel, PreviewView, makePreviewModel } from "@/app/view/preview/preview";
import { ErrorBoundary } from "@/element/errorboundary";
import { CenteredDiv } from "@/element/quickelems";
import { atoms, setBlockFocus, useBlockAtom } from "@/store/global";
import { atoms, counterInc, registerViewModel, setBlockFocus, unregisterViewModel, useBlockAtom } from "@/store/global";
import * as WOS from "@/store/wos";
import * as util from "@/util/util";
import { CpuPlotView, makeCpuPlotViewModel } from "@/view/cpuplot/cpuplot";
import { CpuPlotView, CpuPlotViewModel, makeCpuPlotViewModel } from "@/view/cpuplot/cpuplot";
import { HelpView } from "@/view/helpview/helpview";
import { TerminalView, makeTerminalModel } from "@/view/term/term";
import { WaveAi, makeWaveAiViewModel } from "@/view/waveai/waveai";
import { WebView, makeWebViewModel } from "@/view/webview/webview";
import { TermViewModel, TerminalView, makeTerminalModel } from "@/view/term/term";
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 { BlockFrame } from "./blockframe";
@ -21,47 +21,58 @@ import { blockViewToIcon, blockViewToName } from "./blockutil";
import "./block.less";
function getViewElemAndModel(
blockId: string,
blockView: string,
blockRef: React.RefObject<HTMLDivElement>
): { viewModel: ViewModel; viewElem: JSX.Element } {
let viewElem: JSX.Element = null;
let viewModel: ViewModel = null;
type FullBlockProps = {
blockId: string;
preview: boolean;
layoutModel: LayoutComponentModel;
viewModel: ViewModel;
};
function makeViewModel(blockId: string, blockView: string): ViewModel {
if (blockView === "term") {
return makeTerminalModel(blockId);
}
if (blockView === "preview") {
return makePreviewModel(blockId);
}
if (blockView === "web") {
return makeWebViewModel(blockId);
}
if (blockView === "waveai") {
return makeWaveAiViewModel(blockId);
}
if (blockView === "cpuplot") {
return makeCpuPlotViewModel(blockId);
}
return makeDefaultViewModel(blockId);
}
function getViewElem(blockId: string, blockView: string, viewModel: ViewModel): JSX.Element {
if (util.isBlank(blockView)) {
viewElem = <CenteredDiv>No View</CenteredDiv>;
viewModel = makeDefaultViewModel(blockId);
} else if (blockView === "term") {
const termViewModel = makeTerminalModel(blockId);
viewElem = <TerminalView key={blockId} blockId={blockId} model={termViewModel} />;
viewModel = termViewModel;
} else if (blockView === "preview") {
const previewModel = makePreviewModel(blockId);
viewElem = <PreviewView key={blockId} blockId={blockId} model={previewModel} />;
viewModel = previewModel;
} else if (blockView === "plot") {
viewElem = <PlotView key={blockId} />;
} else if (blockView === "web") {
const webviewModel = makeWebViewModel(blockId);
viewElem = <WebView key={blockId} parentRef={blockRef} model={webviewModel} />;
viewModel = webviewModel;
} else if (blockView === "waveai") {
const waveAiModel = makeWaveAiViewModel(blockId);
viewElem = <WaveAi key={blockId} model={waveAiModel} />;
viewModel = waveAiModel;
} else if (blockView === "cpuplot") {
const cpuPlotModel = makeCpuPlotViewModel(blockId);
viewElem = <CpuPlotView key={blockId} model={cpuPlotModel} />;
viewModel = cpuPlotModel;
} else if (blockView == "help") {
viewElem = <HelpView key={blockId} />;
viewModel = makeDefaultViewModel(blockId);
return <CenteredDiv>No View</CenteredDiv>;
}
if (viewModel == null) {
viewElem = <CenteredDiv>Invalid View "{blockView}"</CenteredDiv>;
viewModel = makeDefaultViewModel(blockId);
if (blockView === "term") {
return <TerminalView key={blockId} blockId={blockId} model={viewModel as TermViewModel} />;
}
return { viewElem, viewModel };
if (blockView === "preview") {
return <PreviewView key={blockId} blockId={blockId} model={viewModel as PreviewModel} />;
}
if (blockView === "plot") {
return <PlotView key={blockId} />;
}
if (blockView === "web") {
return <WebView key={blockId} blockId={blockId} model={viewModel as WebViewModel} />;
}
if (blockView === "waveai") {
return <WaveAi key={blockId} blockId={blockId} model={viewModel as WaveAiModel} />;
}
if (blockView === "cpuplot") {
return <CpuPlotView key={blockId} blockId={blockId} model={viewModel as CpuPlotViewModel} />;
}
if (blockView == "help") {
return <HelpView key={blockId} blockId={blockId} />;
}
return <CenteredDiv>Invalid View "{blockView}"</CenteredDiv>;
}
function makeDefaultViewModel(blockId: string): ViewModel {
@ -85,12 +96,11 @@ function makeDefaultViewModel(blockId: string): ViewModel {
return viewModel;
}
const BlockPreview = React.memo(({ blockId, layoutModel }: BlockProps) => {
const BlockPreview = React.memo(({ blockId, layoutModel, viewModel }: FullBlockProps) => {
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
if (!blockData) {
return null;
}
let { viewModel } = getViewElemAndModel(blockId, blockData?.meta?.view, null);
return (
<BlockFrame
key={blockId}
@ -103,11 +113,12 @@ const BlockPreview = React.memo(({ blockId, layoutModel }: BlockProps) => {
);
});
const BlockFull = React.memo(({ blockId, layoutModel }: BlockProps) => {
const BlockFull = React.memo(({ blockId, layoutModel, 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, blockDataLoading] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
const [focusedChild, setFocusedChild] = React.useState(null);
const isFocusedAtom = useBlockAtom<boolean>(blockId, "isFocused", () => {
return jotai.atom((get) => {
@ -145,9 +156,9 @@ const BlockFull = React.memo(({ blockId, layoutModel }: BlockProps) => {
setBlockClicked(true);
}, []);
let { viewElem, viewModel } = React.useMemo(
() => getViewElemAndModel(blockId, blockData?.meta?.view, blockRef),
[blockId, blockData?.meta?.view, blockRef]
let viewElem = React.useMemo(
() => getViewElem(blockId, blockData?.meta?.view, viewModel),
[blockId, blockData?.meta?.view, viewModel]
);
const determineFocusedChild = React.useCallback(
@ -165,11 +176,6 @@ const BlockFull = React.memo(({ blockId, layoutModel }: BlockProps) => {
focusElemRef.current?.focus({ preventScroll: true });
}, []);
if (!blockId || !blockData) return null;
if (blockDataLoading) {
viewElem = <CenteredDiv>Loading...</CenteredDiv>;
}
const blockModel: BlockComponentModel = {
onClick: setBlockClickedTrue,
onFocusCapture: determineFocusedChild,
@ -202,10 +208,25 @@ const BlockFull = React.memo(({ blockId, layoutModel }: BlockProps) => {
});
const Block = React.memo((props: BlockProps) => {
if (props.preview) {
return <BlockPreview {...props} />;
counterInc("render-Block");
counterInc("render-Block-" + props.blockId.substring(0, 8));
const [blockData, loading] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", props.blockId));
const viewModel = makeViewModel(props.blockId, blockData?.meta?.view);
React.useEffect(() => {
registerViewModel(props.blockId, viewModel);
}, [blockData?.meta?.view]);
React.useEffect(() => {
return () => {
unregisterViewModel(props.blockId);
};
}, []);
if (loading || util.isBlank(props.blockId) || blockData == null) {
return null;
}
return <BlockFull {...props} />;
if (props.preview) {
return <BlockPreview {...props} viewModel={viewModel} />;
}
return <BlockFull {...props} viewModel={viewModel} />;
});
export { Block };

View File

@ -23,6 +23,8 @@ let PLATFORM: NodeJS.Platform = "darwin";
const globalStore = jotai.createStore();
let atoms: GlobalAtomsType;
let globalEnvironment: "electron" | "renderer";
const blockViewModelMap = new Map<string, ViewModel>();
const Counters = new Map<string, number>();
type GlobalInitOptions = {
platform: NodeJS.Platform;
@ -238,6 +240,13 @@ function useBlockAtom<T>(blockId: string, name: string, makeFn: () => jotai.Atom
return atom as jotai.Atom<T>;
}
function useBlockDataLoaded(blockId: string): boolean {
const loadedAtom = useBlockAtom<boolean>(blockId, "block-loaded", () => {
return WOS.getWaveObjectLoadingAtom(WOS.makeORef("block", blockId));
});
return jotai.useAtomValue(loadedAtom);
}
let globalWS: WSControl = null;
function handleWSEventMessage(msg: WSEventType) {
@ -455,8 +464,41 @@ async function openLink(uri: string) {
}
}
function registerViewModel(blockId: string, viewModel: ViewModel) {
blockViewModelMap.set(blockId, viewModel);
}
function unregisterViewModel(blockId: string) {
blockViewModelMap.delete(blockId);
}
function getViewModel(blockId: string): ViewModel {
return blockViewModelMap.get(blockId);
}
function countersClear() {
Counters.clear();
}
function counterInc(name: string, incAmt: number = 1) {
let count = Counters.get(name) ?? 0;
count += incAmt;
Counters.set(name, count);
}
function countersPrint() {
let outStr = "";
for (const [name, count] of Counters.entries()) {
outStr += `${name}: ${count}\n`;
}
console.log(outStr);
}
export {
atoms,
counterInc,
countersClear,
countersPrint,
createBlock,
fetchWaveFile,
getApi,
@ -464,6 +506,7 @@ export {
getEventSubject,
getFileSubject,
getObjectId,
getViewModel,
globalStore,
globalWS,
initGlobal,
@ -471,11 +514,14 @@ export {
isDev,
openLink,
PLATFORM,
registerViewModel,
sendWSCommand,
setBlockFocus,
setPlatform,
unregisterViewModel,
useBlockAtom,
useBlockCache,
useBlockDataLoaded,
useSettingsAtom,
WOS,
};

View File

@ -71,7 +71,7 @@ function makeCpuPlotViewModel(blockId: string): CpuPlotViewModel {
return cpuPlotViewModel;
}
function CpuPlotView({ model }: { model: CpuPlotViewModel }) {
function CpuPlotView({ model }: { model: CpuPlotViewModel; blockId: string }) {
const containerRef = React.useRef<HTMLInputElement>();
const plotData = jotai.useAtomValue(model.dataAtom);
const addPlotData = jotai.useSetAtom(model.addDataAtom);

View File

@ -168,7 +168,7 @@ Other useful metadata values to override block titles, icons, colors, themes, et
`;
function HelpView() {
function HelpView({ blockId }: { blockId: string }) {
return <Markdown text={helpText} className="help-view" />;
}

View File

@ -404,4 +404,4 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
);
};
export { TerminalView, makeTerminalModel };
export { TermViewModel, TerminalView, makeTerminalModel };

View File

@ -398,7 +398,7 @@ const ChatInput = forwardRef<HTMLTextAreaElement, ChatInputProps>(
}
);
const WaveAi = ({ model }: { model: WaveAiModel }) => {
const WaveAi = ({ model }: { model: WaveAiModel; blockId: string }) => {
const { messages, sendMessage } = model.useWaveAi();
const waveaiRef = useRef<HTMLDivElement>(null);
const chatWindowRef = useRef<HTMLDivElement>(null);

View File

@ -9,6 +9,7 @@ import { WebviewTag } from "electron";
import * as jotai from "jotai";
import React, { memo, useEffect } from "react";
import { checkKeyPressed } from "@/util/keyutil";
import "./webview.less";
export class WebViewModel implements ViewModel {
@ -307,6 +308,19 @@ export class WebViewModel implements ViewModel {
return true;
}
}
keyDownHandler(e: WaveKeyboardEvent): boolean {
if (checkKeyPressed(e, "Cmd:l")) {
this.urlInputRef?.current?.focus();
this.urlInputRef?.current?.select();
return true;
}
if (checkKeyPressed(e, "Cmd:r")) {
this.webviewRef?.current?.reload();
return true;
}
return false;
}
}
function makeWebViewModel(blockId: string): WebViewModel {
@ -315,11 +329,11 @@ function makeWebViewModel(blockId: string): WebViewModel {
}
interface WebViewProps {
parentRef: React.RefObject<HTMLDivElement>;
blockId: string;
model: WebViewModel;
}
const WebView = memo(({ parentRef, model }: WebViewProps) => {
const WebView = memo(({ model }: WebViewProps) => {
const url = model.getUrl();
const blockData = jotai.useAtomValue(model.blockAtom);
const metaUrl = blockData?.meta?.url;
@ -386,34 +400,6 @@ const WebView = memo(({ parentRef, model }: WebViewProps) => {
}
}, []);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === "l") {
e.preventDefault();
if (model.urlInputRef) {
model.urlInputRef.current.focus();
model.urlInputRef.current.select();
}
} else if ((e.ctrlKey || e.metaKey) && e.key === "r") {
e.preventDefault();
if (model.webviewRef.current) {
model.webviewRef.current.reload();
}
}
};
const parentElement = parentRef.current;
if (parentElement) {
parentElement.addEventListener("keydown", handleKeyDown);
}
return () => {
if (parentElement) {
parentElement.removeEventListener("keydown", handleKeyDown);
}
};
}, [parentRef]);
return <webview id="webview" className="webview" ref={model.webviewRef} src={url}></webview>;
});

View File

@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import { WshServer } from "@/app/store/wshserver";
import { atoms, getApi, globalStore, globalWS, initGlobal, initWS } from "@/store/global";
import { atoms, countersClear, countersPrint, getApi, globalStore, globalWS, initGlobal, initWS } from "@/store/global";
import * as services from "@/store/services";
import * as WOS from "@/store/wos";
import * as keyutil from "@/util/keyutil";
@ -30,6 +30,8 @@ loadFonts();
(window as any).globalAtoms = atoms;
(window as any).WshServer = WshServer;
(window as any).isFullScreen = false;
(window as any).countersPrint = countersPrint;
(window as any).countersClear = countersClear;
document.title = `The Next Wave (${windowId.substring(0, 8)})`;