From dedfc3134430ad29cfe34e4fc8c015cf23fd74e1 Mon Sep 17 00:00:00 2001 From: Mike Sawka Date: Wed, 21 Aug 2024 15:49:23 -0700 Subject: [PATCH] implement Cmd-I and restructure block viewmodels (#257) --- frontend/app/appkey.ts | 25 ++++- frontend/app/block/block.tsx | 141 ++++++++++++++---------- frontend/app/store/global.ts | 46 ++++++++ frontend/app/view/cpuplot/cpuplot.tsx | 2 +- frontend/app/view/helpview/helpview.tsx | 2 +- frontend/app/view/term/term.tsx | 2 +- frontend/app/view/waveai/waveai.tsx | 2 +- frontend/app/view/webview/webview.tsx | 46 +++----- frontend/wave.ts | 4 +- 9 files changed, 174 insertions(+), 96 deletions(-) diff --git a/frontend/app/appkey.ts b/frontend/app/appkey.ts index 834d0f51d..ddf0a70da 100644 --- a/frontend/app/appkey.ts +++ b/frontend/app/appkey.ts @@ -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}`; diff --git a/frontend/app/block/block.tsx b/frontend/app/block/block.tsx index 9df89b447..913d173fd 100644 --- a/frontend/app/block/block.tsx +++ b/frontend/app/block/block.tsx @@ -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 -): { 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 = No View; - viewModel = makeDefaultViewModel(blockId); - } else if (blockView === "term") { - const termViewModel = makeTerminalModel(blockId); - viewElem = ; - viewModel = termViewModel; - } else if (blockView === "preview") { - const previewModel = makePreviewModel(blockId); - viewElem = ; - viewModel = previewModel; - } else if (blockView === "plot") { - viewElem = ; - } else if (blockView === "web") { - const webviewModel = makeWebViewModel(blockId); - viewElem = ; - viewModel = webviewModel; - } else if (blockView === "waveai") { - const waveAiModel = makeWaveAiViewModel(blockId); - viewElem = ; - viewModel = waveAiModel; - } else if (blockView === "cpuplot") { - const cpuPlotModel = makeCpuPlotViewModel(blockId); - viewElem = ; - viewModel = cpuPlotModel; - } else if (blockView == "help") { - viewElem = ; - viewModel = makeDefaultViewModel(blockId); + return No View; } - if (viewModel == null) { - viewElem = Invalid View "{blockView}"; - viewModel = makeDefaultViewModel(blockId); + if (blockView === "term") { + return ; } - return { viewElem, viewModel }; + if (blockView === "preview") { + return ; + } + if (blockView === "plot") { + return ; + } + if (blockView === "web") { + return ; + } + if (blockView === "waveai") { + return ; + } + if (blockView === "cpuplot") { + return ; + } + if (blockView == "help") { + return ; + } + return Invalid View "{blockView}"; } 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(WOS.makeORef("block", blockId)); if (!blockData) { return null; } - let { viewModel } = getViewElemAndModel(blockId, blockData?.meta?.view, null); return ( { ); }); -const BlockFull = React.memo(({ blockId, layoutModel }: BlockProps) => { +const BlockFull = React.memo(({ blockId, layoutModel, viewModel }: FullBlockProps) => { + counterInc("render-BlockFull"); const focusElemRef = React.useRef(null); const blockRef = React.useRef(null); const [blockClicked, setBlockClicked] = React.useState(false); - const [blockData, blockDataLoading] = WOS.useWaveObjectValue(WOS.makeORef("block", blockId)); + const [blockData] = WOS.useWaveObjectValue(WOS.makeORef("block", blockId)); const [focusedChild, setFocusedChild] = React.useState(null); const isFocusedAtom = useBlockAtom(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 = Loading...; - } 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 ; + counterInc("render-Block"); + counterInc("render-Block-" + props.blockId.substring(0, 8)); + const [blockData, loading] = WOS.useWaveObjectValue(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 ; + if (props.preview) { + return ; + } + return ; }); export { Block }; diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index a929b45df..0a858e4e6 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -23,6 +23,8 @@ let PLATFORM: NodeJS.Platform = "darwin"; const globalStore = jotai.createStore(); let atoms: GlobalAtomsType; let globalEnvironment: "electron" | "renderer"; +const blockViewModelMap = new Map(); +const Counters = new Map(); type GlobalInitOptions = { platform: NodeJS.Platform; @@ -238,6 +240,13 @@ function useBlockAtom(blockId: string, name: string, makeFn: () => jotai.Atom return atom as jotai.Atom; } +function useBlockDataLoaded(blockId: string): boolean { + const loadedAtom = useBlockAtom(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, }; diff --git a/frontend/app/view/cpuplot/cpuplot.tsx b/frontend/app/view/cpuplot/cpuplot.tsx index e52d3205a..f52c540c2 100644 --- a/frontend/app/view/cpuplot/cpuplot.tsx +++ b/frontend/app/view/cpuplot/cpuplot.tsx @@ -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(); const plotData = jotai.useAtomValue(model.dataAtom); const addPlotData = jotai.useSetAtom(model.addDataAtom); diff --git a/frontend/app/view/helpview/helpview.tsx b/frontend/app/view/helpview/helpview.tsx index f0593b7db..ec84b61fe 100644 --- a/frontend/app/view/helpview/helpview.tsx +++ b/frontend/app/view/helpview/helpview.tsx @@ -168,7 +168,7 @@ Other useful metadata values to override block titles, icons, colors, themes, et `; -function HelpView() { +function HelpView({ blockId }: { blockId: string }) { return ; } diff --git a/frontend/app/view/term/term.tsx b/frontend/app/view/term/term.tsx index 0b145acb5..a8d55e52b 100644 --- a/frontend/app/view/term/term.tsx +++ b/frontend/app/view/term/term.tsx @@ -404,4 +404,4 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => { ); }; -export { TerminalView, makeTerminalModel }; +export { TermViewModel, TerminalView, makeTerminalModel }; diff --git a/frontend/app/view/waveai/waveai.tsx b/frontend/app/view/waveai/waveai.tsx index fea856f2e..1915c7d29 100644 --- a/frontend/app/view/waveai/waveai.tsx +++ b/frontend/app/view/waveai/waveai.tsx @@ -398,7 +398,7 @@ const ChatInput = forwardRef( } ); -const WaveAi = ({ model }: { model: WaveAiModel }) => { +const WaveAi = ({ model }: { model: WaveAiModel; blockId: string }) => { const { messages, sendMessage } = model.useWaveAi(); const waveaiRef = useRef(null); const chatWindowRef = useRef(null); diff --git a/frontend/app/view/webview/webview.tsx b/frontend/app/view/webview/webview.tsx index 5ceae1ac3..630fefdd9 100644 --- a/frontend/app/view/webview/webview.tsx +++ b/frontend/app/view/webview/webview.tsx @@ -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; + 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 ; }); diff --git a/frontend/wave.ts b/frontend/wave.ts index 1e926acca..8f800cc9c 100644 --- a/frontend/wave.ts +++ b/frontend/wave.ts @@ -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)})`;