better UI
This commit is contained in:
parent
44fb8726ca
commit
1f9379df7f
16 changed files with 440 additions and 248 deletions
|
|
@ -1,22 +1,42 @@
|
|||
details.active {
|
||||
/* background-color: rgba(128, 72, 0, 0.855);
|
||||
color: white; */
|
||||
border: rgb(192, 125, 0);
|
||||
background-color:rgb(255, 251, 244);
|
||||
filter: drop-shadow( 0px 0px 3px rgba(192, 125, 0, 0.856));
|
||||
}
|
||||
|
||||
details {
|
||||
border: 1px black solid;
|
||||
/* border-radius: 5px; */
|
||||
background-color: white;
|
||||
details > summary {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
/* these two rules add a bit of padding to an opened <details> node */
|
||||
details:open > summary {
|
||||
margin-bottom: 4px;
|
||||
padding-right: 2px;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
color: black;
|
||||
width: fit-content;
|
||||
border-radius: 10px;
|
||||
}
|
||||
details:open {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
details > summary:hover {
|
||||
background-color: #eee;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.stateTree > * {
|
||||
padding-left: 10px;
|
||||
/* border: 1px black solid; */
|
||||
background-color: white;
|
||||
/* margin-bottom: 4px; */
|
||||
/* padding-right: 2px; */
|
||||
/* padding-top: 2px; */
|
||||
/* padding-bottom: 2px; */
|
||||
/* color: black; */
|
||||
/* width: fit-content; */
|
||||
/* border-radius: 10px; */
|
||||
}
|
||||
|
||||
/* if <details> has no children (besides the obvious <summary> child), then hide the marker */
|
||||
details:not(:has(:not(summary))) > summary::marker {
|
||||
content: " ";
|
||||
}
|
||||
|
||||
.outputEvent {
|
||||
|
|
@ -29,6 +49,25 @@ details {
|
|||
display: inline-block;
|
||||
}
|
||||
|
||||
.inputEvent {
|
||||
border: 1px black solid;
|
||||
border-radius: 6px;
|
||||
margin-left: 4px;
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
background-color: rgb(224, 247, 209);
|
||||
display: inline-block;
|
||||
}
|
||||
.inputEvent * {
|
||||
vertical-align: middle;
|
||||
}
|
||||
button.inputEvent:hover:not(:disabled) {
|
||||
background-color: rgb(195, 224, 176);
|
||||
}
|
||||
button.inputEvent:active:not(:disabled) {
|
||||
background-color: rgb(176, 204, 158);
|
||||
}
|
||||
|
||||
.activeState {
|
||||
border: rgb(192, 125, 0);
|
||||
background-color:rgb(255, 251, 244);
|
||||
|
|
@ -46,6 +85,33 @@ hr {
|
|||
border: 0;
|
||||
border-top: 1px solid #ccc;
|
||||
margin: 0;
|
||||
margin-bottom: -3px;
|
||||
margin-bottom: -1px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: circle;
|
||||
margin-block-start: 0;
|
||||
margin-block-end: 0;
|
||||
padding-inline-start: 24px;
|
||||
/* list-style-position: ; */
|
||||
}
|
||||
|
||||
.insetParent {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.insetChild {
|
||||
position: absolute;
|
||||
box-shadow: inset 0 10px 10px -10px rgba(0, 0, 0, 0.75);
|
||||
height: 20px;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
|
||||
.onTop {
|
||||
box-shadow: 0 -10px 10px 10px rgba(0, 0, 0, 0.75);
|
||||
z-index: 1;
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { Dispatch, ReactElement, SetStateAction } from "react";
|
||||
import logo from "../../artwork/logo.svg";
|
||||
|
||||
export function About(props: {setModal}) {
|
||||
export function About(props: {setModal: Dispatch<SetStateAction<ReactElement|null>>}) {
|
||||
return <div style={{maxWidth: '500px', padding: 4}}>
|
||||
<p><img src={logo} style={{maxWidth:'100%'}}/></p>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
details {
|
||||
/* details {
|
||||
padding-left: 20;
|
||||
/* margin-left: 30; */
|
||||
}
|
||||
summary {
|
||||
margin-left: -20;
|
||||
}
|
||||
} */
|
||||
|
||||
.runtimeState {
|
||||
padding-left: 4px;
|
||||
|
|
@ -39,10 +38,6 @@ summary {
|
|||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.toolbar *:not(label) {
|
||||
/* vertical-align: bottom; */
|
||||
}
|
||||
|
||||
.toolbar input {
|
||||
height: 20px;
|
||||
}
|
||||
|
|
@ -67,15 +62,11 @@ button.active {
|
|||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
text-align: center;
|
||||
|
||||
background-color: rgba(200,200,200,0.7);
|
||||
/* backdrop-filter: blur(2px) */
|
||||
}
|
||||
|
||||
.modalInner {
|
||||
|
|
@ -86,3 +77,19 @@ button.active {
|
|||
max-height: 100vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.line {
|
||||
border-bottom: solid 1px #000;
|
||||
height: 10px;
|
||||
line-height: 20px;
|
||||
text-align: left;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.line .content {
|
||||
background-color: #FFF;
|
||||
display: inline;
|
||||
padding: 0 10px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
179
src/App/App.tsx
179
src/App/App.tsx
|
|
@ -1,4 +1,4 @@
|
|||
import { ReactElement, useEffect, useRef, useState } from "react";
|
||||
import { Dispatch, ReactElement, SetStateAction, useEffect, useRef, useState } from "react";
|
||||
|
||||
import { emptyStatechart, Statechart } from "../statecharts/abstract_syntax";
|
||||
import { handleInputEvent, initialize } from "../statecharts/interpreter";
|
||||
|
|
@ -9,26 +9,73 @@ import { getSimTime, getWallClkDelay, TimeMode } from "../statecharts/time";
|
|||
import "../index.css";
|
||||
import "./App.css";
|
||||
|
||||
import { Box, Stack } from "@mui/material";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import { TopPanel } from "./TopPanel";
|
||||
import { RTHistory } from "./RTHistory";
|
||||
import { ShowAST, ShowOutputEvents } from "./ShowAST";
|
||||
import { ShowAST, ShowInputEvents, ShowOutputEvents } from "./ShowAST";
|
||||
import { TraceableError } from "../statecharts/parser";
|
||||
import { getKeyHandler } from "./shortcut_handler";
|
||||
import { BottomPanel } from "./BottomPanel";
|
||||
import { emptyState, VisualEditorState } from "@/statecharts/concrete_syntax";
|
||||
import { usePersistentState } from "@/util/persistent_state";
|
||||
|
||||
type EditHistory = {
|
||||
current: VisualEditorState,
|
||||
history: VisualEditorState[],
|
||||
future: VisualEditorState[],
|
||||
}
|
||||
|
||||
export function App() {
|
||||
const [mode, setMode] = useState<InsertMode>("and");
|
||||
const [historyState, setHistoryState] = useState<EditHistory>({current: emptyState, history: [], future: []});
|
||||
const [ast, setAST] = useState<Statechart>(emptyStatechart);
|
||||
const [errors, setErrors] = useState<TraceableError[]>([]);
|
||||
const [rt, setRT] = useState<BigStep[]>([]);
|
||||
const [rtIdx, setRTIdx] = useState<number|undefined>();
|
||||
const [time, setTime] = useState<TimeMode>({kind: "paused", simtime: 0});
|
||||
|
||||
const [modal, setModal] = useState<ReactElement|null>(null);
|
||||
|
||||
const editorState = historyState.current;
|
||||
const setEditorState = (cb: (value: VisualEditorState) => VisualEditorState) => {
|
||||
setHistoryState(historyState => ({...historyState, current: cb(historyState.current)}));
|
||||
}
|
||||
|
||||
const refRightSideBar = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
||||
function makeCheckPoint() {
|
||||
setHistoryState(historyState => ({
|
||||
...historyState,
|
||||
history: [...historyState.history, historyState.current],
|
||||
future: [],
|
||||
}));
|
||||
}
|
||||
function onUndo() {
|
||||
setHistoryState(historyState => {
|
||||
if (historyState.history.length === 0) {
|
||||
return historyState; // no change
|
||||
}
|
||||
return {
|
||||
current: historyState.history.at(-1)!,
|
||||
history: historyState.history.slice(0,-1),
|
||||
future: [...historyState.future, historyState.current],
|
||||
}
|
||||
})
|
||||
}
|
||||
function onRedo() {
|
||||
setHistoryState(historyState => {
|
||||
if (historyState.future.length === 0) {
|
||||
return historyState; // no change
|
||||
}
|
||||
return {
|
||||
current: historyState.future.at(-1)!,
|
||||
history: [...historyState.history, historyState.current],
|
||||
future: historyState.future.slice(0,-1),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onInit() {
|
||||
const config = initialize(ast);
|
||||
setRT([{inputEvent: null, simtime: 0, ...config}]);
|
||||
|
|
@ -140,13 +187,16 @@ export function App() {
|
|||
// return state && state.parent?.kind !== "and";
|
||||
// })) || new Set();
|
||||
|
||||
const highlightActive = (rtIdx === undefined) ? new Set() : rt[rtIdx].mode;
|
||||
const highlightActive: Set<string> = (rtIdx === undefined) ? new Set() : rt[rtIdx].mode;
|
||||
|
||||
const highlightTransitions = (rtIdx === undefined) ? [] : rt[rtIdx].firedTransitions;
|
||||
|
||||
console.log(ast);
|
||||
const [showStateTree, setShowStateTree] = usePersistentState("showStateTree", true);
|
||||
const [showInputEvents, setShowInputEvents] = usePersistentState("showInputEvents", true);
|
||||
const [showOutputEvents, setShowOutputEvents] = usePersistentState("showOutputEvents", true);
|
||||
|
||||
return <>
|
||||
|
||||
{/* Modal dialog */}
|
||||
{modal && <div
|
||||
className="modalOuter"
|
||||
|
|
@ -157,56 +207,83 @@ export function App() {
|
|||
</span>
|
||||
</div>
|
||||
</div>}
|
||||
<Stack sx={{height:'100vh'}}>
|
||||
{/* Top bar */}
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
borderBottom: 1,
|
||||
borderColor: "divider",
|
||||
alignItems: 'center',
|
||||
flex: '0 0 content',
|
||||
}}>
|
||||
<TopPanel
|
||||
rt={rtIdx === undefined ? undefined : rt[rtIdx]}
|
||||
{...{rtIdx, ast, time, setTime, onInit, onClear, onRaise, onBack, mode, setMode, setModal}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Everything below the top bar */}
|
||||
<Stack direction="row" sx={{
|
||||
overflow: 'auto',
|
||||
}}>
|
||||
<Stack sx={{height:'100%'}}>
|
||||
<Stack direction="row" sx={{flexGrow:1, overflow: "auto"}}>
|
||||
|
||||
{/* main */}
|
||||
<Box sx={{
|
||||
flexGrow:1,
|
||||
overflow:'auto',
|
||||
}}>
|
||||
<VisualEditor {...{ast, setAST, rt: rt.at(rtIdx!), setRT, errors, setErrors, mode, highlightActive, highlightTransitions, setModal}}/>
|
||||
{/* Left: top bar and main editor */}
|
||||
<Box sx={{flexGrow:1, overflow: "auto"}}>
|
||||
<Stack sx={{height:'100%'}}>
|
||||
{/* Top bar */}
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
borderBottom: 1,
|
||||
borderColor: "divider",
|
||||
alignItems: 'center',
|
||||
flex: '0 0 content',
|
||||
}}>
|
||||
<TopPanel
|
||||
rt={rtIdx === undefined ? undefined : rt[rtIdx]}
|
||||
{...{rtIdx, ast, time, setTime, onUndo, onRedo, onInit, onClear, onRaise, onBack, mode, setMode, setModal}}
|
||||
/>
|
||||
</Box>
|
||||
{/* Below the top bar: Editor */}
|
||||
<Box sx={{flexGrow:1, overflow: "auto"}}>
|
||||
<VisualEditor {...{state: editorState, setState: setEditorState, ast, setAST, rt: rt.at(rtIdx!), setRT, errors, setErrors, mode, highlightActive, highlightTransitions, setModal, makeCheckPoint}}/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* right sidebar */}
|
||||
<Box
|
||||
sx={{
|
||||
borderLeft: 1,
|
||||
borderColor: "divider",
|
||||
flex: '0 0 content',
|
||||
overflowY: "auto",
|
||||
overflowX: "visible",
|
||||
maxWidth: 'min(300px, 30vw)',
|
||||
}}>
|
||||
<ShowAST {...{...ast, rt: rt.at(rtIdx!), highlightActive}}/>
|
||||
<ShowOutputEvents outputEvents={ast.outputEvents}/>
|
||||
<br/>
|
||||
<div ref={refRightSideBar}>
|
||||
<RTHistory {...{ast, rt, rtIdx, setTime, setRTIdx, refRightSideBar}}/>
|
||||
</div>
|
||||
</Box>
|
||||
{/* Right: sidebar */}
|
||||
<Box sx={{
|
||||
borderLeft: 1,
|
||||
borderColor: "divider",
|
||||
flex: '0 0 content',
|
||||
overflowY: "auto",
|
||||
overflowX: "visible",
|
||||
maxWidth: 'min(300px, 30vw)',
|
||||
}}>
|
||||
<Stack sx={{height:'100%'}}>
|
||||
<Box className="onTop" sx={{flex: '0 0 content', backgroundColor: ''}}>
|
||||
<details open={showStateTree}
|
||||
onToggle={e => setShowStateTree(e.newState === "open")}>
|
||||
<summary>state tree</summary>
|
||||
<ul>
|
||||
<ShowAST {...{...ast, rt: rt.at(rtIdx!), highlightActive}}/>
|
||||
</ul>
|
||||
</details>
|
||||
<hr/>
|
||||
<details open={showInputEvents}
|
||||
onToggle={e => setShowInputEvents(e.newState === "open")}>
|
||||
<summary>input events</summary>
|
||||
<ShowInputEvents inputEvents={ast.inputEvents} onRaise={onRaise} disabled={rtIdx===undefined}/>
|
||||
</details>
|
||||
<hr/>
|
||||
<details open={showOutputEvents}
|
||||
onToggle={e => setShowOutputEvents(e.newState === "open")}>
|
||||
<summary>output events</summary>
|
||||
<ShowOutputEvents outputEvents={ast.outputEvents}/>
|
||||
</details>
|
||||
</Box>
|
||||
<Box sx={{
|
||||
flexGrow:1,
|
||||
overflow:'auto',
|
||||
minHeight: '75%', // <-- allows us to always scroll down the sidebar far enough such that the execution history is enough in view
|
||||
}}>
|
||||
<Box sx={{ height: '100%'}}>
|
||||
<div ref={refRightSideBar}>
|
||||
<RTHistory {...{ast, rt, rtIdx, setTime, setRTIdx, refRightSideBar}}/>
|
||||
</div>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
|
||||
</Stack>
|
||||
<Box sx={{
|
||||
flex: '0 0 content',
|
||||
}}>
|
||||
|
||||
{/* Bottom panel */}
|
||||
<Box sx={{flex: '0 0 content'}}>
|
||||
<BottomPanel {...{errors}}/>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@ import "./BottomPanel.css";
|
|||
import head from "../head.svg" ;
|
||||
|
||||
export function BottomPanel(props: {errors: TraceableError[]}) {
|
||||
const [greeting, setGreeting] = useState(<><b><img src={head}/> "Welcome to StateBuddy, buddy!"</b></>);
|
||||
const [greeting, setGreeting] = useState(<><b><img src={head} style={{transform: "scaleX(-1)"}}/> "Welcome to StateBuddy, buddy!"</b></>);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setGreeting("");
|
||||
setGreeting(<></>);
|
||||
}, 2000);
|
||||
}, []);
|
||||
|
||||
|
|
|
|||
29
src/App/Icons.tsx
Normal file
29
src/App/Icons.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
|
||||
export function RountangleIcon(props: { kind: string; }) {
|
||||
return <svg width={20} height={20}>
|
||||
<rect rx={7} ry={7}
|
||||
x={1} y={1}
|
||||
width={18} height={18}
|
||||
className={`rountangle ${props.kind}`}
|
||||
style={{ ...(props.kind === "or" ? { strokeDasharray: '3 2' } : {}), strokeWidth: 1.2 }} />
|
||||
</svg>;
|
||||
}
|
||||
|
||||
export function PseudoStateIcon(props: {}) {
|
||||
const w = 20, h = 20;
|
||||
return <svg width={w} height={h}>
|
||||
<polygon
|
||||
points={`
|
||||
${w / 2} ${1},
|
||||
${w - 1} ${h / 2},
|
||||
${w / 2} ${h - 1},
|
||||
${1} ${h / 2},
|
||||
`} fill="white" stroke="black" strokeWidth={1.2} />
|
||||
</svg>;
|
||||
}
|
||||
|
||||
export function HistoryIcon(props: { kind: "shallow" | "deep"; }) {
|
||||
const w = 20, h = 20;
|
||||
const text = props.kind === "shallow" ? "H" : "H*";
|
||||
return <svg width={w} height={h}><circle cx={w / 2} cy={h / 2} r={Math.min(w, h) / 2 - 1} fill="white" stroke="black" /><text x={w / 2} y={h / 2 + 4} textAnchor="middle" fontSize={11} fontWeight={400}>{text}</text></svg>;
|
||||
}
|
||||
|
|
@ -23,8 +23,12 @@ export function RTHistory({rt, rtIdx, ast, setRTIdx, setTime, refRightSideBar}:
|
|||
{rt.map((r, idx) => <>
|
||||
<hr/>
|
||||
<div className={"runtimeState"+(idx===rtIdx?" active":"")} onClick={() => gotoRt(idx, r.simtime)}>
|
||||
<div>{formatTime(r.simtime)}, {r.inputEvent || "<init>"}</div>
|
||||
<ShowMode mode={r.mode} statechart={ast}/>
|
||||
<div>
|
||||
{formatTime(r.simtime)}
|
||||
 
|
||||
<div className="inputEvent">{r.inputEvent || "<init>"}</div>
|
||||
</div>
|
||||
<ShowMode mode={r.mode.difference(rt[idx-1]?.mode || new Set())} statechart={ast}/>
|
||||
<ShowEnvironment environment={r.environment}/>
|
||||
{r.outputEvents.length>0 && <>^
|
||||
{r.outputEvents.map((e:RaisedEvent) => <span className="outputEvent">{e.name}</span>)}
|
||||
|
|
@ -49,5 +53,7 @@ function ShowMode(props: {mode: Mode, statechart: Statechart}) {
|
|||
}
|
||||
|
||||
function getActiveLeafs(mode: Mode, sc: Statechart) {
|
||||
return new Set([...mode].filter(uid => sc.uid2State.get(uid)?.children?.length === 0));
|
||||
return new Set([...mode].filter(uid =>
|
||||
sc.uid2State.get(uid)?.children?.length === 0
|
||||
));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { ConcreteState, PseudoState, stateDescription, Transition } from "../statecharts/abstract_syntax";
|
||||
import { Action, Expression } from "../statecharts/label_ast";
|
||||
import { Action, EventTrigger, Expression } from "../statecharts/label_ast";
|
||||
import { RT_Statechart } from "../statecharts/runtime_types";
|
||||
|
||||
import "./AST.css";
|
||||
|
|
@ -34,12 +34,22 @@ export function ShowAction(props: {action: Action}) {
|
|||
|
||||
export function ShowAST(props: {root: ConcreteState | PseudoState, transitions: Map<string, Transition[]>, rt: RT_Statechart | undefined, highlightActive: Set<string>}) {
|
||||
const description = stateDescription(props.root);
|
||||
const outgoing = props.transitions.get(props.root.uid) || [];
|
||||
// const outgoing = props.transitions.get(props.root.uid) || [];
|
||||
|
||||
return <details open={true} className={props.highlightActive.has(props.root.uid) ? "active" : ""}>
|
||||
return <li>{props.root.kind}: {description}
|
||||
{props.root.kind !== "pseudo" && props.root.children.length>0 &&
|
||||
<ul>
|
||||
{props.root.children.map(child =>
|
||||
<ShowAST key={child.uid} root={child} transitions={props.transitions} rt={props.rt} highlightActive={props.highlightActive} />
|
||||
)}
|
||||
</ul>
|
||||
}
|
||||
</li>;
|
||||
|
||||
return <details open={true} className={"stateTree" + (props.highlightActive.has(props.root.uid) ? " active" : "")}>
|
||||
<summary>{props.root.kind}: {description}</summary>
|
||||
|
||||
{props.root.kind !== "pseudo" && props.root.entryActions.length>0 &&
|
||||
{/* {props.root.kind !== "pseudo" && props.root.entryActions.length>0 &&
|
||||
props.root.entryActions.map(action =>
|
||||
<div> entry / <ShowAction action={action}/></div>
|
||||
)
|
||||
|
|
@ -48,23 +58,56 @@ export function ShowAST(props: {root: ConcreteState | PseudoState, transitions:
|
|||
props.root.exitActions.map(action =>
|
||||
<div> exit / <ShowAction action={action}/></div>
|
||||
)
|
||||
}
|
||||
} */}
|
||||
|
||||
{props.root.kind !== "pseudo" && props.root.children.length>0 &&
|
||||
props.root.children.map(child =>
|
||||
<ShowAST root={child} transitions={props.transitions} rt={props.rt} highlightActive={props.highlightActive} />
|
||||
<ShowAST key={child.uid} root={child} transitions={props.transitions} rt={props.rt} highlightActive={props.highlightActive} />
|
||||
)
|
||||
}
|
||||
{outgoing.length>0 &&
|
||||
{/* {outgoing.length>0 &&
|
||||
outgoing.map(transition => <> <ShowTransition transition={transition}/><br/></>)
|
||||
}
|
||||
</details>
|
||||
} */}
|
||||
</details>;
|
||||
}
|
||||
|
||||
import BoltIcon from '@mui/icons-material/Bolt';
|
||||
|
||||
export function ShowInputEvents({inputEvents, onRaise, disabled}: {inputEvents: EventTrigger[], onRaise: (e: string, p: any) => void, disabled: boolean}) {
|
||||
return inputEvents.map(({event, paramName}) =>
|
||||
<div key={event+'/'+paramName} className="toolbarGroup">
|
||||
<button
|
||||
className="inputEvent"
|
||||
title={`raise this input event`}
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
// @ts-ignore
|
||||
const param = document.getElementById(`input-${event}-param`)?.value;
|
||||
let paramParsed;
|
||||
try {
|
||||
if (param) {
|
||||
paramParsed = JSON.parse(param); // may throw
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
alert("invalid json");
|
||||
return;
|
||||
}
|
||||
onRaise(event, paramParsed);
|
||||
}}>
|
||||
<BoltIcon fontSize="small"/>
|
||||
{event}
|
||||
</button>
|
||||
{paramName &&
|
||||
<><input id={`input-${event}-param`} style={{width: 20}} placeholder={paramName}/></>
|
||||
}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ShowOutputEvents(props: {outputEvents: Set<string>}) {
|
||||
return <div style={{whiteSpace: 'wrap'}}>
|
||||
out:
|
||||
{[...props.outputEvents].map(eventName => {
|
||||
return <><span className="outputEvent">{eventName}</span> </>;
|
||||
})}
|
||||
</div>;
|
||||
return [...props.outputEvents].map(eventName => {
|
||||
return <><div className="outputEvent">{eventName}</div> </>;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Dispatch, ReactElement, SetStateAction, useState } from "react";
|
||||
import { Dispatch, ReactElement, SetStateAction, useState, KeyboardEvent } from "react";
|
||||
|
||||
import { parse as parseLabel } from "../statecharts/label_parser";
|
||||
|
||||
|
|
@ -23,6 +23,7 @@ export function TextDialog(props: {setModal: Dispatch<SetStateAction<ReactElemen
|
|||
try {
|
||||
const parsed = parseLabel(text);
|
||||
} catch (e) {
|
||||
// @ts-ignore
|
||||
error = e.message;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,13 +20,16 @@ import { formatTime } from "./util";
|
|||
import { InsertMode } from "../VisualEditor/VisualEditor";
|
||||
import { KeyInfoHidden, KeyInfoVisible } from "./KeyInfo";
|
||||
import { About } from "./About";
|
||||
import { Stack } from "@mui/material";
|
||||
import { usePersistentState } from "@/util/persistent_state";
|
||||
import { RountangleIcon, PseudoStateIcon, HistoryIcon } from "./Icons";
|
||||
|
||||
export type TopPanelProps = {
|
||||
rt?: BigStep,
|
||||
rtIdx?: number,
|
||||
time: TimeMode,
|
||||
setTime: Dispatch<SetStateAction<TimeMode>>,
|
||||
onUndo: () => void,
|
||||
onRedo: () => void,
|
||||
onInit: () => void,
|
||||
onClear: () => void,
|
||||
onRaise: (e: string, p: any) => void,
|
||||
|
|
@ -34,44 +37,13 @@ export type TopPanelProps = {
|
|||
ast: Statechart,
|
||||
mode: InsertMode,
|
||||
setMode: Dispatch<SetStateAction<InsertMode>>,
|
||||
setModal: Dispatch<SetStateAction<ReactElement>>,
|
||||
setModal: Dispatch<SetStateAction<ReactElement|null>>,
|
||||
}
|
||||
|
||||
function RountangleIcon(props: {kind: string}) {
|
||||
return <svg width={20} height={20}>
|
||||
<rect rx={7} ry={7}
|
||||
x={1} y={1}
|
||||
width={18} height={18}
|
||||
className={`rountangle ${props.kind}`}
|
||||
style={{...(props.kind === "or" ? {strokeDasharray: '3 2'}: {}), strokeWidth: 1.2}}
|
||||
/>
|
||||
</svg>;
|
||||
}
|
||||
|
||||
function PseudoStateIcon(props: {}) {
|
||||
const w=20, h=20;
|
||||
return <svg width={w} height={h}>
|
||||
<polygon
|
||||
points={`
|
||||
${w/2} ${1},
|
||||
${w-1} ${h/2},
|
||||
${w/2} ${h-1},
|
||||
${1} ${h/2},
|
||||
`} fill="white" stroke="black" strokeWidth={1.2}/>
|
||||
</svg>;
|
||||
}
|
||||
|
||||
function HistoryIcon(props: {kind: "shallow"|"deep"}) {
|
||||
const w=20, h=20;
|
||||
const text = props.kind === "shallow" ? "H" : "H*";
|
||||
return <svg width={w} height={h}><circle cx={w/2} cy={h/2} r={Math.min(w,h)/2-1} fill="white" stroke="black"/><text x={w/2} y={h/2+4} textAnchor="middle" fontSize={11} fontWeight={400}>{text}</text></svg>;
|
||||
}
|
||||
|
||||
|
||||
export function TopPanel({rt, rtIdx, time, setTime, onInit, onClear, onRaise, onBack, ast, mode, setMode, setModal}: TopPanelProps) {
|
||||
export function TopPanel({rt, rtIdx, time, setTime, onUndo, onRedo, onInit, onClear, onRaise, onBack, ast, mode, setMode, setModal}: TopPanelProps) {
|
||||
const [displayTime, setDisplayTime] = useState("0.000");
|
||||
const [timescale, setTimescale] = useState(1);
|
||||
const [showKeys, setShowKeys] = useState(true);
|
||||
const [showKeys, setShowKeys] = usePersistentState("shortcuts", true);
|
||||
|
||||
const KeyInfo = showKeys ? KeyInfoVisible : KeyInfoHidden;
|
||||
|
||||
|
|
@ -92,8 +64,13 @@ export function TopPanel({rt, rtIdx, time, setTime, onInit, onClear, onRaise, on
|
|||
onClear();
|
||||
}
|
||||
if (e.key === "Tab") {
|
||||
if (rtIdx === undefined) {
|
||||
onInit();
|
||||
}
|
||||
else {
|
||||
onSkip();
|
||||
}
|
||||
e.preventDefault();
|
||||
onSkip();
|
||||
}
|
||||
if (e.key === "s") {
|
||||
e.preventDefault();
|
||||
|
|
@ -112,6 +89,17 @@ export function TopPanel({rt, rtIdx, time, setTime, onInit, onClear, onRaise, on
|
|||
onBack();
|
||||
}
|
||||
}
|
||||
else {
|
||||
// ctrl is down
|
||||
if (e.key === "z") {
|
||||
e.preventDefault();
|
||||
onUndo();
|
||||
}
|
||||
if (e.key === "Z") {
|
||||
e.preventDefault();
|
||||
onRedo();
|
||||
}
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
return () => {
|
||||
|
|
@ -119,15 +107,6 @@ export function TopPanel({rt, rtIdx, time, setTime, onInit, onClear, onRaise, on
|
|||
};
|
||||
}, [time, onInit, timescale]);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => localStorage.setItem("showKeys", showKeys?"1":"0"), 100);
|
||||
}, [showKeys])
|
||||
|
||||
useEffect(() => {
|
||||
const show = localStorage.getItem("showKeys") || "1";
|
||||
setShowKeys(show==="1")
|
||||
}, [])
|
||||
|
||||
function updateDisplayedTime() {
|
||||
const now = Math.round(performance.now());
|
||||
const timeMs = getSimTime(time, now);
|
||||
|
|
@ -214,10 +193,10 @@ export function TopPanel({rt, rtIdx, time, setTime, onInit, onClear, onRaise, on
|
|||
{/* undo / redo */}
|
||||
<div className="toolbarGroup">
|
||||
<KeyInfo keyInfo={<><kbd>Ctrl</kbd>+<kbd>Z</kbd></>}>
|
||||
<button title="undo"><UndoIcon fontSize="small"/></button>
|
||||
<button title="undo" onClick={onUndo}><UndoIcon fontSize="small"/></button>
|
||||
</KeyInfo>
|
||||
<KeyInfo keyInfo={<><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>Z</kbd></>}>
|
||||
<button title="redo"><RedoIcon fontSize="small"/></button>
|
||||
<button title="redo" onClick={onRedo}><RedoIcon fontSize="small"/></button>
|
||||
</KeyInfo>
|
||||
 
|
||||
</div>
|
||||
|
|
@ -233,7 +212,7 @@ export function TopPanel({rt, rtIdx, time, setTime, onInit, onClear, onRaise, on
|
|||
["transition", "transitions", <TrendingFlatIcon fontSize="small"/>, <kbd>T</kbd>],
|
||||
["text", "text", <> T </>, <kbd>X</kbd>],
|
||||
] as [InsertMode, string, ReactElement, ReactElement][]).map(([m, hint, buttonTxt, keyInfo]) =>
|
||||
<KeyInfo keyInfo={keyInfo}>
|
||||
<KeyInfo key={m} keyInfo={keyInfo}>
|
||||
<button
|
||||
title={"insert "+hint}
|
||||
disabled={mode===m}
|
||||
|
|
@ -294,33 +273,42 @@ export function TopPanel({rt, rtIdx, time, setTime, onInit, onClear, onRaise, on
|
|||
</div>
|
||||
|
||||
{/* input events */}
|
||||
<div className="toolbarGroup">
|
||||
{/* <div className="toolbarGroup">
|
||||
{ast.inputEvents &&
|
||||
<>
|
||||
{ast.inputEvents.map(({event, paramName}) =>
|
||||
<div className="toolbarGroup"><button title={`raise input event '${event}'`} disabled={!rt} onClick={() => {
|
||||
// @ts-ignore
|
||||
const param = document.getElementById(`input-${event}-param`)?.value;
|
||||
let paramParsed;
|
||||
try {
|
||||
if (param) {
|
||||
paramParsed = JSON.parse(param); // may throw
|
||||
<div key={event+'/'+paramName} className="toolbarGroup">
|
||||
<button
|
||||
className="inputEvent"
|
||||
title={`raise this input event`}
|
||||
disabled={!rt}
|
||||
onClick={() => {
|
||||
// @ts-ignore
|
||||
const param = document.getElementById(`input-${event}-param`)?.value;
|
||||
let paramParsed;
|
||||
try {
|
||||
if (param) {
|
||||
paramParsed = JSON.parse(param); // may throw
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
alert("invalid json");
|
||||
return;
|
||||
}
|
||||
onRaise(event, paramParsed);
|
||||
}}>
|
||||
<BoltIcon fontSize="small"/>
|
||||
{event}
|
||||
</button>
|
||||
{paramName &&
|
||||
<><input id={`input-${event}-param`} style={{width: 20}} placeholder={paramName}/></>
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
alert("invalid json");
|
||||
return;
|
||||
}
|
||||
onRaise(event, paramParsed);
|
||||
}}>
|
||||
<BoltIcon fontSize="small"/>
|
||||
{event}
|
||||
</button>
|
||||
{paramName && <><input id={`input-${event}-param`} style={{width: 20}} placeholder={paramName}/></>}
|
||||
</div>)}
|
||||
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
</div>;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue