errors in bottom panel are collapsible
This commit is contained in:
parent
297905a4af
commit
60a7d12857
6 changed files with 74 additions and 69 deletions
|
|
@ -18,7 +18,11 @@ details:open {
|
||||||
|
|
||||||
details > summary:hover {
|
details > summary:hover {
|
||||||
background-color: #eee;
|
background-color: #eee;
|
||||||
cursor: pointer;
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.errorStatus details > summary:hover {
|
||||||
|
background-color: rgb(102, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stateTree > * {
|
.stateTree > * {
|
||||||
|
|
@ -35,9 +39,9 @@ details > summary:hover {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* if <details> has no children (besides the obvious <summary> child), then hide the marker */
|
/* if <details> has no children (besides the obvious <summary> child), then hide the marker */
|
||||||
details:not(:has(:not(summary))) > summary::marker {
|
/* details:not(:has(:not(summary))) > summary::marker {
|
||||||
content: " ";
|
content: " ";
|
||||||
}
|
} */
|
||||||
|
|
||||||
.outputEvent {
|
.outputEvent {
|
||||||
border: 1px black solid;
|
border: 1px black solid;
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import { getKeyHandler } from "./shortcut_handler";
|
||||||
import { BottomPanel } from "./BottomPanel";
|
import { BottomPanel } from "./BottomPanel";
|
||||||
import { emptyState } from "@/statecharts/concrete_syntax";
|
import { emptyState } from "@/statecharts/concrete_syntax";
|
||||||
import { usePersistentState } from "@/util/persistent_state";
|
import { usePersistentState } from "@/util/persistent_state";
|
||||||
|
import { PersistentDetails } from "./PersistentDetails";
|
||||||
|
|
||||||
type EditHistory = {
|
type EditHistory = {
|
||||||
current: VisualEditorState,
|
current: VisualEditorState,
|
||||||
|
|
@ -191,10 +192,6 @@ export function App() {
|
||||||
|
|
||||||
const highlightTransitions = (rtIdx === undefined) ? [] : rt[rtIdx].firedTransitions;
|
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 <>
|
return <>
|
||||||
|
|
||||||
{/* Modal dialog */}
|
{/* Modal dialog */}
|
||||||
|
|
@ -245,25 +242,22 @@ export function App() {
|
||||||
}}>
|
}}>
|
||||||
<Stack sx={{height:'100%'}}>
|
<Stack sx={{height:'100%'}}>
|
||||||
<Box className="onTop" sx={{flex: '0 0 content', backgroundColor: ''}}>
|
<Box className="onTop" sx={{flex: '0 0 content', backgroundColor: ''}}>
|
||||||
<details open={showStateTree}
|
<PersistentDetails localStorageKey="showStateTree" initiallyOpen={true}>
|
||||||
onToggle={e => setShowStateTree(e.newState === "open")}>
|
|
||||||
<summary>state tree</summary>
|
<summary>state tree</summary>
|
||||||
<ul>
|
<ul>
|
||||||
<ShowAST {...{...ast, rt: rt.at(rtIdx!), highlightActive}}/>
|
<ShowAST {...{...ast, rt: rt.at(rtIdx!), highlightActive}}/>
|
||||||
</ul>
|
</ul>
|
||||||
</details>
|
</PersistentDetails>
|
||||||
<hr/>
|
<hr/>
|
||||||
<details open={showInputEvents}
|
<PersistentDetails localStorageKey="showInputEvents" initiallyOpen={true}>
|
||||||
onToggle={e => setShowInputEvents(e.newState === "open")}>
|
|
||||||
<summary>input events</summary>
|
<summary>input events</summary>
|
||||||
<ShowInputEvents inputEvents={ast.inputEvents} onRaise={onRaise} disabled={rtIdx===undefined}/>
|
<ShowInputEvents inputEvents={ast.inputEvents} onRaise={onRaise} disabled={rtIdx===undefined}/>
|
||||||
</details>
|
</PersistentDetails>
|
||||||
<hr/>
|
<hr/>
|
||||||
<details open={showOutputEvents}
|
<PersistentDetails localStorageKey="showOutputEvents" initiallyOpen={true}>
|
||||||
onToggle={e => setShowOutputEvents(e.newState === "open")}>
|
|
||||||
<summary>output events</summary>
|
<summary>output events</summary>
|
||||||
<ShowOutputEvents outputEvents={ast.outputEvents}/>
|
<ShowOutputEvents outputEvents={ast.outputEvents}/>
|
||||||
</details>
|
</PersistentDetails>
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
flexGrow:1,
|
flexGrow:1,
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import { TraceableError } from "../statecharts/parser";
|
||||||
import "./BottomPanel.css";
|
import "./BottomPanel.css";
|
||||||
|
|
||||||
import head from "../head.svg" ;
|
import head from "../head.svg" ;
|
||||||
|
import { usePersistentState } from "@/util/persistent_state";
|
||||||
|
import { PersistentDetails } from "./PersistentDetails";
|
||||||
|
|
||||||
export function BottomPanel(props: {errors: TraceableError[]}) {
|
export function BottomPanel(props: {errors: TraceableError[]}) {
|
||||||
const [greeting, setGreeting] = useState(<><b><img src={head} style={{transform: "scaleX(-1)"}}/> "Welcome to StateBuddy, buddy!"</b></>);
|
const [greeting, setGreeting] = useState(<><b><img src={head} style={{transform: "scaleX(-1)"}}/> "Welcome to StateBuddy, buddy!"</b></>);
|
||||||
|
|
@ -18,7 +20,14 @@ export function BottomPanel(props: {errors: TraceableError[]}) {
|
||||||
<>{greeting}</>
|
<>{greeting}</>
|
||||||
{props.errors.length > 0 &&
|
{props.errors.length > 0 &&
|
||||||
<div className="errorStatus">
|
<div className="errorStatus">
|
||||||
{props.errors.length>0 && <>{props.errors.length} errors: {props.errors.map(({message})=>message).join(', ')}</>}
|
<PersistentDetails initiallyOpen={false} localStorageKey="errorsExpanded">
|
||||||
</div>}
|
<summary>{props.errors.length} errors</summary>
|
||||||
|
{props.errors.map(({message})=>
|
||||||
|
<div>
|
||||||
|
{message}
|
||||||
|
</div>)}
|
||||||
|
</PersistentDetails>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
15
src/App/PersistentDetails.tsx
Normal file
15
src/App/PersistentDetails.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { usePersistentState } from "@/util/persistent_state"
|
||||||
|
import { DetailsHTMLAttributes, PropsWithChildren } from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
localStorageKey: string,
|
||||||
|
initiallyOpen?: boolean,
|
||||||
|
} & DetailsHTMLAttributes<HTMLDetailsElement>;
|
||||||
|
|
||||||
|
// A <details> node that remembers whether it was open or closed by storing that state in localStorage.
|
||||||
|
export function PersistentDetails({localStorageKey, initiallyOpen, children, ...rest}: PropsWithChildren<Props>) {
|
||||||
|
const [open, setOpen] = usePersistentState(localStorageKey, initiallyOpen);
|
||||||
|
return <details open={open} onToggle={e => setOpen(e.newState === "open")} {...rest}>
|
||||||
|
{children}
|
||||||
|
</details>;
|
||||||
|
}
|
||||||
|
|
@ -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 { Statechart } from "../statecharts/abstract_syntax";
|
||||||
import { Arrow, ArrowPart, Diamond, History, Rountangle, RountanglePart, Text } from "../statecharts/concrete_syntax";
|
import { Arrow, ArrowPart, Diamond, History, Rountangle, RountanglePart, Text } from "../statecharts/concrete_syntax";
|
||||||
|
|
@ -26,11 +26,6 @@ export type VisualEditorState = {
|
||||||
selection: Selection;
|
selection: Selection;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
type DraggingState = {
|
|
||||||
lastMousePos: Vec2D;
|
|
||||||
} | null; // null means: not dragging
|
|
||||||
|
|
||||||
type SelectingState = Rect2D | null;
|
type SelectingState = Rect2D | null;
|
||||||
|
|
||||||
export type RountangleSelectable = {
|
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) {
|
export function VisualEditor({state, setState, ast, setAST, rt, errors, setErrors, mode, highlightActive, highlightTransitions, setModal, makeCheckPoint}: VisualEditorProps) {
|
||||||
|
|
||||||
const [dragging, setDragging] = useState<DraggingState>(null);
|
const [dragging, setDragging] = useState(false);
|
||||||
|
|
||||||
// uid's of selected rountangles
|
// uid's of selected rountangles
|
||||||
// const [selection, setSelection] = useState<Selection>([]);
|
// const [selection, setSelection] = useState<Selection>([]);
|
||||||
|
|
@ -231,9 +226,7 @@ export function VisualEditor({state, setState, ast, setAST, rt, errors, setError
|
||||||
}
|
}
|
||||||
throw new Error("unreachable, mode=" + mode); // shut up typescript
|
throw new Error("unreachable, mode=" + mode); // shut up typescript
|
||||||
});
|
});
|
||||||
setDragging({
|
setDragging(true);
|
||||||
lastMousePos: currentPointer,
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -257,7 +250,7 @@ export function VisualEditor({state, setState, ast, setAST, rt, errors, setError
|
||||||
setSelection(() => [{uid, parts}] as Selection);
|
setSelection(() => [{uid, parts}] as Selection);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
setDragging(null);
|
setDragging(false);
|
||||||
setSelectingState({
|
setSelectingState({
|
||||||
topLeft: currentPointer,
|
topLeft: currentPointer,
|
||||||
size: {x: 0, y: 0},
|
size: {x: 0, y: 0},
|
||||||
|
|
@ -268,15 +261,13 @@ export function VisualEditor({state, setState, ast, setAST, rt, errors, setError
|
||||||
}
|
}
|
||||||
|
|
||||||
// start dragging
|
// start dragging
|
||||||
setDragging({
|
setDragging(true);
|
||||||
lastMousePos: currentPointer,
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// otherwise, just start making a selection
|
// otherwise, just start making a selection
|
||||||
setDragging(null);
|
setDragging(false);
|
||||||
setSelectingState({
|
setSelectingState({
|
||||||
topLeft: currentPointer,
|
topLeft: currentPointer,
|
||||||
size: {x: 0, y: 0},
|
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) {
|
else if (selectingState) {
|
||||||
setSelectingState(ss => {
|
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}) => {
|
const onMouseUp = (e: {target: any, pageX: number, pageY: number}) => {
|
||||||
if (dragging) {
|
if (dragging) {
|
||||||
setDragging(null);
|
setDragging(false);
|
||||||
// do not persist sizes smaller than 40x40
|
// do not persist sizes smaller than 40x40
|
||||||
setState(state => {
|
setState(state => {
|
||||||
return {
|
return {
|
||||||
|
|
@ -416,14 +407,14 @@ export function VisualEditor({state, setState, ast, setAST, rt, errors, setError
|
||||||
setSelectingState(null); // no longer making a selection
|
setSelectingState(null); // no longer making a selection
|
||||||
};
|
};
|
||||||
|
|
||||||
function deleteShapes(selection: Selection) {
|
function deleteSelection() {
|
||||||
setState(state => ({
|
setState(state => ({
|
||||||
...state,
|
...state,
|
||||||
rountangles: state.rountangles.filter(r => !selection.some(rs => rs.uid === r.uid)),
|
rountangles: state.rountangles.filter(r => !state.selection.some(rs => rs.uid === r.uid)),
|
||||||
diamonds: state.diamonds.filter(d => !selection.some(ds => ds.uid === d.uid)),
|
diamonds: state.diamonds.filter(d => !state.selection.some(ds => ds.uid === d.uid)),
|
||||||
history: state.history.filter(h => !selection.some(hs => hs.uid === h.uid)),
|
history: state.history.filter(h => !state.selection.some(hs => hs.uid === h.uid)),
|
||||||
arrows: state.arrows.filter(a => !selection.some(as => as.uid === a.uid)),
|
arrows: state.arrows.filter(a => !state.selection.some(as => as.uid === a.uid)),
|
||||||
texts: state.texts.filter(t => !selection.some(ts => ts.uid === t.uid)),
|
texts: state.texts.filter(t => !state.selection.some(ts => ts.uid === t.uid)),
|
||||||
selection: [],
|
selection: [],
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
@ -431,30 +422,22 @@ export function VisualEditor({state, setState, ast, setAST, rt, errors, setError
|
||||||
const onKeyDown = (e: KeyboardEvent) => {
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
if (e.key === "Delete") {
|
if (e.key === "Delete") {
|
||||||
// delete selection
|
// delete selection
|
||||||
if (selection.length > 0) {
|
|
||||||
makeCheckPoint();
|
makeCheckPoint();
|
||||||
deleteShapes(selection);
|
deleteSelection();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (e.key === "o") {
|
if (e.key === "o") {
|
||||||
// selected states become OR-states
|
// selected states become OR-states
|
||||||
setSelection(selection => {
|
|
||||||
setState(state => ({
|
setState(state => ({
|
||||||
...state,
|
...state,
|
||||||
rountangles: state.rountangles.map(r => selection.some(rs => rs.uid === r.uid) ? ({...r, kind: "or"}) : r),
|
rountangles: state.rountangles.map(r => state.selection.some(rs => rs.uid === r.uid) ? ({...r, kind: "or"}) : r),
|
||||||
}));
|
}));
|
||||||
return selection;
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
if (e.key === "a") {
|
if (e.key === "a") {
|
||||||
// selected states become AND-states
|
// selected states become AND-states
|
||||||
setSelection(selection => {
|
|
||||||
setState(state => ({
|
setState(state => ({
|
||||||
...state,
|
...state,
|
||||||
rountangles: state.rountangles.map(r => selection.some(rs => rs.uid === r.uid) ? ({...r, kind: "and"}) : r),
|
rountangles: state.rountangles.map(r => state.selection.some(rs => rs.uid === r.uid) ? ({...r, kind: "and"}) : r),
|
||||||
}));
|
}));
|
||||||
return selection;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
// if (e.key === "p") {
|
// if (e.key === "p") {
|
||||||
// // selected states become pseudo-states
|
// // selected states become pseudo-states
|
||||||
|
|
@ -469,13 +452,16 @@ export function VisualEditor({state, setState, ast, setAST, rt, errors, setError
|
||||||
if (e.ctrlKey) {
|
if (e.ctrlKey) {
|
||||||
if (e.key === "a") {
|
if (e.key === "a") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setDragging(null);
|
setDragging(false);
|
||||||
|
setState(state => ({
|
||||||
|
...state,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
setSelection([
|
selection: [
|
||||||
...state.rountangles.map(r => ({uid: r.uid, parts: ["left", "top", "right", "bottom"]})),
|
...state.rountangles.map(r => ({uid: r.uid, parts: ["left", "top", "right", "bottom"]})),
|
||||||
...state.arrows.map(a => ({uid: a.uid, parts: ["start", "end"]})),
|
...state.arrows.map(a => ({uid: a.uid, parts: ["start", "end"]})),
|
||||||
...state.texts.map(t => ({uid: t.uid, parts: ["text"]})),
|
...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) {
|
function onCut(e: ClipboardEvent) {
|
||||||
if (selection.length > 0) {
|
if (selection.length > 0) {
|
||||||
copyInternal(selection, e);
|
copyInternal(selection, e);
|
||||||
deleteShapes(selection);
|
deleteSelection();
|
||||||
e.preventDefault();
|
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);
|
const rootErrors = errors.filter(({shapeUid}) => shapeUid === "root").map(({message}) => message);
|
||||||
|
|
||||||
return <svg width="4000px" height="4000px"
|
return <svg width="4000px" height="4000px"
|
||||||
className={"svgCanvas"+(active.has("root")?" active":"")+(dragging!==null?" dragging":"")}
|
className={"svgCanvas"+(active.has("root")?" active":"")+(dragging ? " dragging" : "")}
|
||||||
onMouseDown={onMouseDown}
|
onMouseDown={onMouseDown}
|
||||||
onContextMenu={e => e.preventDefault()}
|
onContextMenu={e => e.preventDefault()}
|
||||||
ref={refSVG}
|
ref={refSVG}
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
onCopy={onCopy}
|
onCopy={onCopy}
|
||||||
// @ts-ignore
|
|
||||||
onPaste={onPaste}
|
onPaste={onPaste}
|
||||||
// @ts-ignore
|
|
||||||
onCut={onCut}
|
onCut={onCut}
|
||||||
>
|
>
|
||||||
<defs>
|
<defs>
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ export type RountanglePart = "left" | "top" | "right" | "bottom";
|
||||||
export type ArrowPart = "start" | "end";
|
export type ArrowPart = "start" | "end";
|
||||||
|
|
||||||
export const emptyState: VisualEditorState = {
|
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)
|
// used to find which rountangle an arrow connects to (src/tgt)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue