diff --git a/emain/emain.ts b/emain/emain.ts index 2ca78a04..0568383a 100644 --- a/emain/emain.ts +++ b/emain/emain.ts @@ -612,6 +612,32 @@ function makeAppMenu() { role: "quit", }, ]; + const viewMenu: Electron.MenuItemConstructorOptions[] = [ + { + role: "forceReload", + }, + { + role: "toggleDevTools", + }, + { + type: "separator", + }, + { + role: "resetZoom", + }, + { + role: "zoomIn", + }, + { + role: "zoomOut", + }, + { + type: "separator", + }, + { + role: "togglefullscreen", + }, + ]; const menuTemplate: Electron.MenuItemConstructorOptions[] = [ { role: "appMenu", @@ -626,6 +652,7 @@ function makeAppMenu() { }, { role: "viewMenu", + submenu: viewMenu, }, { role: "windowMenu", diff --git a/frontend/app/block/block.tsx b/frontend/app/block/block.tsx index 136f2542..cfef875e 100644 --- a/frontend/app/block/block.tsx +++ b/frontend/app/block/block.tsx @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 import { useLongClick } from "@/app/hook/useLongClick"; -import { CodeEditor } from "@/app/view/codeeditor/codeeditor"; import { Button } from "@/element/button"; import { ErrorBoundary } from "@/element/errorboundary"; import { CenteredDiv } from "@/element/quickelems"; @@ -331,11 +330,7 @@ const BlockFrame_Default_Component = ({ ); } else if (elem.elemtype == "textbutton") { return ( - ); @@ -509,8 +504,6 @@ function getViewElemAndModel( viewModel = previewModel; } else if (blockView === "plot") { viewElem = ; - } else if (blockView === "codeedit") { - viewElem = ; } else if (blockView === "web") { const webviewModel = makeWebViewModel(blockId); viewElem = ; diff --git a/frontend/app/element/button.less b/frontend/app/element/button.less index fba32d4e..7186472d 100644 --- a/frontend/app/element/button.less +++ b/frontend/app/element/button.less @@ -15,7 +15,7 @@ line-height: 1.5; white-space: nowrap; user-select: none; - -webkit-user-select: none; + font-weight: 500; color: var(--main-text-color); @@ -140,7 +140,12 @@ border-radius: 4px; } -.vertical-padding-3 { - padding-top: 3px; - padding-bottom: 3px; +.vertical-padding-2 { + padding-top: 2px; + padding-bottom: 2px; +} + +.horizontal-padding-10 { + padding-left: 10px; + padding-right: 10px; } diff --git a/frontend/app/tab/tab.less b/frontend/app/tab/tab.less index 8a5501d2..b0810f6e 100644 --- a/frontend/app/tab/tab.less +++ b/frontend/app/tab/tab.less @@ -31,8 +31,8 @@ rgba(255, 255, 255, 0.1); box-shadow: inset 0 1px 0 0 hsla(0, 0%, 100%, 0.05), - 0 0 0 0.5px hsla(0, 0%, 100%, 0.35), - inset 0 -1px 0 0 rgba(0, 0, 0, 0.2); + 0 0 0 0.5px hsla(0, 0%, 100%, 0.25), + inset 0 0 0 0 rgba(0, 0, 0, 0.2); } .name { diff --git a/frontend/app/view/codeeditor/codeeditor.tsx b/frontend/app/view/codeeditor/codeeditor.tsx index caf5c493..4d2a03e9 100644 --- a/frontend/app/view/codeeditor/codeeditor.tsx +++ b/frontend/app/view/codeeditor/codeeditor.tsx @@ -1,18 +1,17 @@ // Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { globalStore } from "@/store/global"; +import { useHeight } from "@/app/hook/useHeight"; import loader from "@monaco-editor/loader"; import { Editor, Monaco } from "@monaco-editor/react"; -import * as jotai from "jotai"; import type * as MonacoTypes from "monaco-editor/esm/vs/editor/editor.api"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef } from "react"; +import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil"; import "./codeeditor.less"; // there is a global monaco variable (TODO get the correct TS type) declare var monaco: Monaco; -let monacoLoadedAtom = jotai.atom(false); function loadMonaco() { loader.config({ paths: { vs: "monaco" } }); @@ -35,8 +34,6 @@ function loadMonaco() { "editor.background": "#fefefe", }, }); - globalStore.set(monacoLoadedAtom, true); - console.log("monaco loaded", monaco); }) .catch((e) => { console.error("error loading monaco", e); @@ -63,47 +60,72 @@ function defaultEditorOptions(): MonacoTypes.editor.IEditorOptions { return opts; } -interface CodeEditProps { - readonly?: boolean; +interface CodeEditorProps { + parentRef: React.MutableRefObject; text: string; - language?: string; filename: string; + readonly: boolean; + language?: string; onChange?: (text: string) => void; + onSave?: () => void; + onCancel?: () => void; + onEdit?: () => void; } -export function CodeEditor({ readonly = false, text, language, filename, onChange }: CodeEditProps) { - const [divDims, setDivDims] = useState(null); - const monacoLoaded = jotai.useAtomValue(monacoLoadedAtom); - - const monacoRef = useRef(null); +export function CodeEditor({ + readonly = false, + parentRef, + text, + language, + filename, + onChange, + onSave, + onCancel, + onEdit, +}: CodeEditorProps) { const divRef = useRef(null); - const monacoLoadedRef = useRef(null); + const parentHeight = useHeight(parentRef); const theme = "wave-theme-dark"; useEffect(() => { - if (!divRef.current) { - return; + function handleKeyDown(e: KeyboardEvent) { + const waveEvent = adaptFromReactOrNativeKeyEvent(e); + if (onSave) { + if (checkKeyPressed(waveEvent, "Cmd:s")) { + e.preventDefault(); + onSave(); + return; + } + } + if (onCancel) { + if (checkKeyPressed(waveEvent, "Cmd:r")) { + e.preventDefault(); + onCancel(); + return; + } + } + if (onEdit) { + if (checkKeyPressed(waveEvent, "Cmd:e")) { + e.preventDefault(); + onEdit(); + return; + } + } } - const height = divRef.current.clientHeight; - const width = divRef.current.clientWidth; - setDivDims({ height, width }); - }, []); - useEffect(() => { - if (monacoLoadedRef.current === null) { - monacoLoadedRef.current = monacoLoaded; - } - }, [monacoLoaded]); + const currentParentRef = parentRef.current; + currentParentRef.addEventListener("keydown", handleKeyDown); - function handleEditorMount(editor: MonacoTypes.editor.IStandaloneCodeEditor) { - monacoRef.current = editor; - const monacoModel = editor.getModel(); - //monaco.editor.setModelLanguage(monacoModel, "text/markdown"); - } + return () => { + currentParentRef.removeEventListener("keydown", handleKeyDown); + }; + }, [onSave, onCancel, onEdit]); function handleEditorChange(text: string, ev: MonacoTypes.editor.IModelContentChangedEvent) { - onChange(text); + if (onChange) { + onChange(text); + } } const editorOpts = defaultEditorOptions(); @@ -112,18 +134,15 @@ export function CodeEditor({ readonly = false, text, language, filename, onChang return (
- {divDims != null && monacoLoaded ? ( - - ) : null} +
); diff --git a/frontend/app/view/preview.tsx b/frontend/app/view/preview.tsx index c854c72c..bb57f457 100644 --- a/frontend/app/view/preview.tsx +++ b/frontend/app/view/preview.tsx @@ -115,13 +115,13 @@ export class PreviewModel implements ViewModel { { elemtype: "textbutton", text: "Save", - className: "primary warning", + className: "primary warning border-radius-4 vertical-padding-2 horizontal-padding-10", onClick: this.handleFileSave.bind(this), }, { elemtype: "textbutton", text: "Cancel", - className: "secondary", + className: "secondary border-radius-4 vertical-padding-2 horizontal-padding-10", onClick: () => this.toggleCodeEditorReadOnly(true), } ); @@ -129,7 +129,7 @@ export class PreviewModel implements ViewModel { viewTextChildren.push({ elemtype: "textbutton", text: "Edit", - className: "secondary", + className: "secondary border-radius-4 vertical-padding-2 horizontal-padding-10", onClick: () => this.toggleCodeEditorReadOnly(false), }); } @@ -388,17 +388,21 @@ function StreamingPreview({ fileInfo }: { fileInfo: FileInfo }) { } function CodeEditPreview({ + parentRef, contentAtom, filename, readonly, isCeViewAtom, newFileContentAtom, + model, }: { + parentRef: React.MutableRefObject; contentAtom: jotai.Atom>; filename: string; readonly: boolean; isCeViewAtom: jotai.PrimitiveAtom; newFileContentAtom: jotai.PrimitiveAtom; + model: PreviewModel; }) { const fileContent = jotai.useAtomValue(contentAtom); const setIsCeView = jotai.useSetAtom(isCeViewAtom); @@ -413,10 +417,14 @@ function CodeEditPreview({ return ( setNewFileContent(text)} + onSave={() => model.handleFileSave()} + onCancel={() => model.toggleCodeEditorReadOnly(true)} + onEdit={() => model.toggleCodeEditorReadOnly(false)} /> ); } @@ -519,10 +527,12 @@ function PreviewView({ blockId, model }: { blockId: string; model: PreviewModel view = ( ); } else if (mimeType === "directory") { @@ -553,4 +563,4 @@ function PreviewView({ blockId, model }: { blockId: string; model: PreviewModel ); } -export { PreviewView, makePreviewModel }; +export { makePreviewModel, PreviewView };