mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-02 18:39:05 +01:00
implement Cmd-I and restructure block viewmodels (#257)
This commit is contained in:
parent
7df91de2e5
commit
dedfc31344
@ -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}`;
|
||||
|
@ -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 };
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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" />;
|
||||
}
|
||||
|
||||
|
@ -404,4 +404,4 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export { TerminalView, makeTerminalModel };
|
||||
export { TermViewModel, TerminalView, makeTerminalModel };
|
||||
|
@ -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);
|
||||
|
@ -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>;
|
||||
});
|
||||
|
||||
|
@ -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)})`;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user