diff --git a/src/App/AST.css b/src/App/AST.css index 71ce83a..afc7a09 100644 --- a/src/App/AST.css +++ b/src/App/AST.css @@ -18,7 +18,11 @@ details:open { details > summary:hover { background-color: #eee; - cursor: pointer; + cursor: default; +} + +.errorStatus details > summary:hover { + background-color: rgb(102, 0, 0); } .stateTree > * { @@ -35,9 +39,9 @@ details > summary:hover { } /* if
has no children (besides the obvious child), then hide the marker */ -details:not(:has(:not(summary))) > summary::marker { +/* details:not(:has(:not(summary))) > summary::marker { content: " "; -} +} */ .outputEvent { border: 1px black solid; diff --git a/src/App/App.tsx b/src/App/App.tsx index e937535..67a8a90 100644 --- a/src/App/App.tsx +++ b/src/App/App.tsx @@ -19,6 +19,7 @@ import { getKeyHandler } from "./shortcut_handler"; import { BottomPanel } from "./BottomPanel"; import { emptyState } from "@/statecharts/concrete_syntax"; import { usePersistentState } from "@/util/persistent_state"; +import { PersistentDetails } from "./PersistentDetails"; type EditHistory = { current: VisualEditorState, @@ -191,10 +192,6 @@ export function App() { const highlightTransitions = (rtIdx === undefined) ? [] : rt[rtIdx].firedTransitions; - const [showStateTree, setShowStateTree] = usePersistentState("showStateTree", true); - const [showInputEvents, setShowInputEvents] = usePersistentState("showInputEvents", true); - const [showOutputEvents, setShowOutputEvents] = usePersistentState("showOutputEvents", true); - return <> {/* Modal dialog */} @@ -245,25 +242,22 @@ export function App() { }}> -
setShowStateTree(e.newState === "open")}> + state tree
-
+
-
setShowInputEvents(e.newState === "open")}> + input events -
+
-
setShowOutputEvents(e.newState === "open")}> + output events -
+
 "Welcome to StateBuddy, buddy!"); @@ -18,7 +20,14 @@ export function BottomPanel(props: {errors: TraceableError[]}) { <>{greeting} {props.errors.length > 0 &&
- {props.errors.length>0 && <>{props.errors.length} errors: {props.errors.map(({message})=>message).join(', ')}} -
} + + {props.errors.length} errors + {props.errors.map(({message})=> +
+ {message} +
)} +
+ + } ; } \ No newline at end of file diff --git a/src/App/PersistentDetails.tsx b/src/App/PersistentDetails.tsx new file mode 100644 index 0000000..3d59be4 --- /dev/null +++ b/src/App/PersistentDetails.tsx @@ -0,0 +1,15 @@ +import { usePersistentState } from "@/util/persistent_state" +import { DetailsHTMLAttributes, PropsWithChildren } from "react"; + +type Props = { + localStorageKey: string, + initiallyOpen?: boolean, +} & DetailsHTMLAttributes; + +// A
node that remembers whether it was open or closed by storing that state in localStorage. +export function PersistentDetails({localStorageKey, initiallyOpen, children, ...rest}: PropsWithChildren) { + const [open, setOpen] = usePersistentState(localStorageKey, initiallyOpen); + return
setOpen(e.newState === "open")} {...rest}> + {children} +
; +} diff --git a/src/VisualEditor/VisualEditor.tsx b/src/VisualEditor/VisualEditor.tsx index eef8d45..2a3978d 100644 --- a/src/VisualEditor/VisualEditor.tsx +++ b/src/VisualEditor/VisualEditor.tsx @@ -1,4 +1,4 @@ -import { Dispatch, ReactElement, SetStateAction, useEffect, useMemo, useRef, useState } from "react"; +import { ClipboardEvent, Dispatch, ReactElement, SetStateAction, useEffect, useMemo, useRef, useState } from "react"; import { Statechart } from "../statecharts/abstract_syntax"; import { Arrow, ArrowPart, Diamond, History, Rountangle, RountanglePart, Text } from "../statecharts/concrete_syntax"; @@ -26,11 +26,6 @@ export type VisualEditorState = { selection: Selection; }; - -type DraggingState = { - lastMousePos: Vec2D; -} | null; // null means: not dragging - type SelectingState = Rect2D | null; export type RountangleSelectable = { @@ -82,7 +77,7 @@ type VisualEditorProps = { export function VisualEditor({state, setState, ast, setAST, rt, errors, setErrors, mode, highlightActive, highlightTransitions, setModal, makeCheckPoint}: VisualEditorProps) { - const [dragging, setDragging] = useState(null); + const [dragging, setDragging] = useState(false); // uid's of selected rountangles // const [selection, setSelection] = useState([]); @@ -231,9 +226,7 @@ export function VisualEditor({state, setState, ast, setAST, rt, errors, setError } throw new Error("unreachable, mode=" + mode); // shut up typescript }); - setDragging({ - lastMousePos: currentPointer, - }); + setDragging(true); return; } @@ -257,7 +250,7 @@ export function VisualEditor({state, setState, ast, setAST, rt, errors, setError setSelection(() => [{uid, parts}] as Selection); } else { - setDragging(null); + setDragging(false); setSelectingState({ topLeft: currentPointer, size: {x: 0, y: 0}, @@ -268,15 +261,13 @@ export function VisualEditor({state, setState, ast, setAST, rt, errors, setError } // start dragging - setDragging({ - lastMousePos: currentPointer, - }); + setDragging(true); return; } } // otherwise, just start making a selection - setDragging(null); + setDragging(false); setSelectingState({ topLeft: currentPointer, size: {x: 0, y: 0}, @@ -343,7 +334,7 @@ export function VisualEditor({state, setState, ast, setAST, rt, errors, setError } }), })); - setDragging({lastMousePos: currentPointer}); + setDragging(true); } else if (selectingState) { setSelectingState(ss => { @@ -358,7 +349,7 @@ export function VisualEditor({state, setState, ast, setAST, rt, errors, setError const onMouseUp = (e: {target: any, pageX: number, pageY: number}) => { if (dragging) { - setDragging(null); + setDragging(false); // do not persist sizes smaller than 40x40 setState(state => { return { @@ -416,14 +407,14 @@ export function VisualEditor({state, setState, ast, setAST, rt, errors, setError setSelectingState(null); // no longer making a selection }; - function deleteShapes(selection: Selection) { + function deleteSelection() { setState(state => ({ ...state, - rountangles: state.rountangles.filter(r => !selection.some(rs => rs.uid === r.uid)), - diamonds: state.diamonds.filter(d => !selection.some(ds => ds.uid === d.uid)), - history: state.history.filter(h => !selection.some(hs => hs.uid === h.uid)), - arrows: state.arrows.filter(a => !selection.some(as => as.uid === a.uid)), - texts: state.texts.filter(t => !selection.some(ts => ts.uid === t.uid)), + rountangles: state.rountangles.filter(r => !state.selection.some(rs => rs.uid === r.uid)), + diamonds: state.diamonds.filter(d => !state.selection.some(ds => ds.uid === d.uid)), + history: state.history.filter(h => !state.selection.some(hs => hs.uid === h.uid)), + arrows: state.arrows.filter(a => !state.selection.some(as => as.uid === a.uid)), + texts: state.texts.filter(t => !state.selection.some(ts => ts.uid === t.uid)), selection: [], })); } @@ -431,30 +422,22 @@ export function VisualEditor({state, setState, ast, setAST, rt, errors, setError const onKeyDown = (e: KeyboardEvent) => { if (e.key === "Delete") { // delete selection - if (selection.length > 0) { - makeCheckPoint(); - deleteShapes(selection); - } + makeCheckPoint(); + deleteSelection(); } if (e.key === "o") { // selected states become OR-states - setSelection(selection => { - setState(state => ({ - ...state, - rountangles: state.rountangles.map(r => selection.some(rs => rs.uid === r.uid) ? ({...r, kind: "or"}) : r), - })); - return selection; - }) + setState(state => ({ + ...state, + rountangles: state.rountangles.map(r => state.selection.some(rs => rs.uid === r.uid) ? ({...r, kind: "or"}) : r), + })); } if (e.key === "a") { // selected states become AND-states - setSelection(selection => { - setState(state => ({ - ...state, - rountangles: state.rountangles.map(r => selection.some(rs => rs.uid === r.uid) ? ({...r, kind: "and"}) : r), - })); - return selection; - }); + setState(state => ({ + ...state, + rountangles: state.rountangles.map(r => state.selection.some(rs => rs.uid === r.uid) ? ({...r, kind: "and"}) : r), + })); } // if (e.key === "p") { // // selected states become pseudo-states @@ -469,13 +452,16 @@ export function VisualEditor({state, setState, ast, setAST, rt, errors, setError if (e.ctrlKey) { if (e.key === "a") { e.preventDefault(); - setDragging(null); - // @ts-ignore - setSelection([ + setDragging(false); + setState(state => ({ + ...state, + // @ts-ignore + selection: [ ...state.rountangles.map(r => ({uid: r.uid, parts: ["left", "top", "right", "bottom"]})), ...state.arrows.map(a => ({uid: a.uid, parts: ["start", "end"]})), ...state.texts.map(t => ({uid: t.uid, parts: ["text"]})), - ]); + ] + })) } } }; @@ -630,7 +616,7 @@ export function VisualEditor({state, setState, ast, setAST, rt, errors, setError function onCut(e: ClipboardEvent) { if (selection.length > 0) { copyInternal(selection, e); - deleteShapes(selection); + deleteSelection(); e.preventDefault(); } } @@ -666,16 +652,13 @@ export function VisualEditor({state, setState, ast, setAST, rt, errors, setError const rootErrors = errors.filter(({shapeUid}) => shapeUid === "root").map(({message}) => message); return e.preventDefault()} ref={refSVG} - // @ts-ignore onCopy={onCopy} - // @ts-ignore onPaste={onPaste} - // @ts-ignore onCut={onCut} > diff --git a/src/statecharts/concrete_syntax.ts b/src/statecharts/concrete_syntax.ts index a4b7c7d..9939df9 100644 --- a/src/statecharts/concrete_syntax.ts +++ b/src/statecharts/concrete_syntax.ts @@ -32,7 +32,7 @@ export type RountanglePart = "left" | "top" | "right" | "bottom"; export type ArrowPart = "start" | "end"; export const emptyState: VisualEditorState = { - rountangles: [], texts: [], arrows: [], diamonds: [], history: [], nextID: 0, + rountangles: [], texts: [], arrows: [], diamonds: [], history: [], nextID: 0, selection: [], }; // used to find which rountangle an arrow connects to (src/tgt)