toolbar button to undo last step

This commit is contained in:
Joeri Exelmans 2025-10-19 15:11:51 +02:00
parent 59d5e9913a
commit 5e60b3dc95
8 changed files with 99 additions and 31 deletions

View file

@ -56,6 +56,28 @@ export function App() {
scrollDownSidebar(); scrollDownSidebar();
} }
function onBack() {
setTime(() => {
if (rtIdx !== undefined) {
if (rtIdx > 0)
return {
kind: "paused",
simtime: rt[rtIdx-1].simtime,
}
}
return { kind: "paused", simtime: 0 };
});
setRTIdx(rtIdx => {
if (rtIdx !== undefined) {
if (rtIdx > 0)
return rtIdx - 1;
else
return 0;
}
else return undefined;
})
}
function scrollDownSidebar() { function scrollDownSidebar() {
if (refRightSideBar.current) { if (refRightSideBar.current) {
const el = refRightSideBar.current; const el = refRightSideBar.current;
@ -128,15 +150,21 @@ export function App() {
borderBottom: 1, borderBottom: 1,
borderColor: "divider", borderColor: "divider",
alignItems: 'center', alignItems: 'center',
flex: '0 0 content',
}}> }}>
<TopPanel <TopPanel
rt={rtIdx === undefined ? undefined : rt[rtIdx]} rt={rtIdx === undefined ? undefined : rt[rtIdx]}
{...{ast, time, setTime, onInit, onClear, onRaise, mode, setMode}} {...{rtIdx, ast, time, setTime, onInit, onClear, onRaise, onBack, mode, setMode}}
/> />
</Box> </Box>
<Stack direction="row" sx={{height:'calc(100vh - 64px)'}}> <Stack direction="row" sx={{
overflow: 'auto',
}}>
{/* main */} {/* main */}
<Box sx={{flexGrow:1, overflow:'auto'}}> <Box sx={{
flexGrow:1,
overflow:'auto',
}}>
<VisualEditor {...{ast, setAST, rt: rt.at(rtIdx!), setRT, errors, setErrors, mode, highlightActive, highlightTransitions}}/> <VisualEditor {...{ast, setAST, rt: rt.at(rtIdx!), setRT, errors, setErrors, mode, highlightActive, highlightTransitions}}/>
</Box> </Box>
{/* right sidebar */} {/* right sidebar */}
@ -145,7 +173,6 @@ export function App() {
borderLeft: 1, borderLeft: 1,
borderColor: "divider", borderColor: "divider",
flex: '0 0 content', flex: '0 0 content',
height: 'calc(100vh-32px)',
overflow: "auto", overflow: "auto",
}}> }}>
<ShowAST {...{...ast, rt: rt.at(rtIdx!), highlightActive}}/> <ShowAST {...{...ast, rt: rt.at(rtIdx!), highlightActive}}/>
@ -155,7 +182,9 @@ export function App() {
</div> </div>
</Box> </Box>
</Stack> </Stack>
<Box> <Box sx={{
flex: '0 0 content',
}}>
<BottomPanel {...{errors}}/> <BottomPanel {...{errors}}/>
</Box> </Box>
</Stack>; </Stack>;

View file

@ -1,3 +1,9 @@
.errorStatus { .errorStatus {
color: rgb(230,0,0); /* background-color: rgb(230,0,0); */
background-color: var(--error-color);
color: white;
}
.bottom {
background-color: lightyellow;
} }

View file

@ -1,10 +1,22 @@
import { useEffect, useState } from "react";
import { TraceableError } from "../statecharts/parser"; import { TraceableError } from "../statecharts/parser";
import "./BottomPanel.css"; import "./BottomPanel.css";
export function BottomPanel(props: {errors: TraceableError[]}) { export function BottomPanel(props: {errors: TraceableError[]}) {
return <div className="toolbar"> const [greeting, setGreeting] = useState("Welcome to StateBuddy, buddy!");
<div className="errorStatus">{
props.errors.length>0 && <>{props.errors.length} errors {props.errors.map(({message})=>message).join(',')}</>}</div> useEffect(() => {
setTimeout(() => {
setGreeting("");
}, 2000);
}, []);
return <div className="toolbar bottom">
<>{greeting}</>
{props.errors.length > 0 &&
<div className="errorStatus">
{props.errors.length>0 && <>{props.errors.length} errors: {props.errors.map(({message})=>message).join(', ')}</>}
</div>}
</div>; </div>;
} }

View file

@ -2,10 +2,10 @@ import { Stack } from "@mui/material";
export function KeyInfoVisible(props: {keyInfo, children}) { export function KeyInfoVisible(props: {keyInfo, children}) {
return <Stack style={{display: "inline-block"}}> return <Stack style={{display: "inline-block"}}>
<div style={{fontSize:11, height: 16, textAlign:"center"}}> <div style={{fontSize:11, height: 18, textAlign:"center", paddingLeft: 3, paddingRight: 3}}>
{props.keyInfo} {props.keyInfo}
</div> </div>
<div> <div style={{textAlign:"center"}}>
{props.children} {props.children}
</div> </div>
</Stack> </Stack>

View file

@ -8,7 +8,7 @@ import PauseIcon from '@mui/icons-material/Pause';
import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import BoltIcon from '@mui/icons-material/Bolt'; import BoltIcon from '@mui/icons-material/Bolt';
import SkipNextIcon from '@mui/icons-material/SkipNext'; import SkipNextIcon from '@mui/icons-material/SkipNext';
import TrendingFlatIcon from '@mui/icons-material/TrendingFlat'; import SkipPreviousIcon from '@mui/icons-material/SkipPrevious';import TrendingFlatIcon from '@mui/icons-material/TrendingFlat';
import AccessAlarmIcon from '@mui/icons-material/AccessAlarm'; import AccessAlarmIcon from '@mui/icons-material/AccessAlarm';
import StopIcon from '@mui/icons-material/Stop'; import StopIcon from '@mui/icons-material/Stop';
import UndoIcon from '@mui/icons-material/Undo'; import UndoIcon from '@mui/icons-material/Undo';
@ -20,11 +20,13 @@ import { KeyInfoHidden, KeyInfoVisible } from "./KeyInfo";
export type TopPanelProps = { export type TopPanelProps = {
rt?: BigStep, rt?: BigStep,
rtIdx?: number,
time: TimeMode, time: TimeMode,
setTime: Dispatch<SetStateAction<TimeMode>>, setTime: Dispatch<SetStateAction<TimeMode>>,
onInit: () => void, onInit: () => void,
onClear: () => void, onClear: () => void,
onRaise: (e: string, p: any) => void, onRaise: (e: string, p: any) => void,
onBack: () => void,
ast: Statechart, ast: Statechart,
mode: InsertMode, mode: InsertMode,
setMode: Dispatch<SetStateAction<InsertMode>>, setMode: Dispatch<SetStateAction<InsertMode>>,
@ -61,7 +63,7 @@ function HistoryIcon(props: {kind: "shallow"|"deep"}) {
} }
export function TopPanel({rt, time, setTime, onInit, onClear, onRaise, ast, mode, setMode}: TopPanelProps) { export function TopPanel({rt, rtIdx, time, setTime, onInit, onClear, onRaise, onBack, ast, mode, setMode}: TopPanelProps) {
const [displayTime, setDisplayTime] = useState("0.000"); const [displayTime, setDisplayTime] = useState("0.000");
const [timescale, setTimescale] = useState(1); const [timescale, setTimescale] = useState(1);
const [showKeys, setShowKeys] = useState(true); const [showKeys, setShowKeys] = useState(true);
@ -99,6 +101,10 @@ export function TopPanel({rt, time, setTime, onInit, onClear, onRaise, ast, mode
e.preventDefault(); e.preventDefault();
setShowKeys(show => !show); setShowKeys(show => !show);
} }
if (e.key === "Backspace") {
e.preventDefault();
onBack();
}
}; };
window.addEventListener("keydown", onKeyDown); window.addEventListener("keydown", onKeyDown);
return () => { return () => {
@ -186,9 +192,20 @@ export function TopPanel({rt, time, setTime, onInit, onClear, onRaise, ast, mode
return <> return <>
<div className="toolbar"> <div className="toolbar">
<div style={{display:'inline-block'}}>
<div style={{display:'inline-block'}}> <div style={{display:'inline-block'}}>
<div style={{display:'inline-block'}}>
<KeyInfo keyInfo={<><kbd>Ctrl</kbd>+<kbd>Z</kbd></>}>
<button title="undo"><UndoIcon fontSize="small"/></button>
</KeyInfo>
<KeyInfo keyInfo={<><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>Z</kbd></>}>
<button title="redo"><RedoIcon fontSize="small"/></button>
</KeyInfo>
</div>
&emsp;
<div style={{display:'inline-block'}}>
{([ {([
["and", "AND-states", <RountangleIcon kind="and"/>, <kbd>A</kbd>], ["and", "AND-states", <RountangleIcon kind="and"/>, <kbd>A</kbd>],
["or", "OR-states", <RountangleIcon kind="or"/>, <kbd>O</kbd>], ["or", "OR-states", <RountangleIcon kind="or"/>, <kbd>O</kbd>],
@ -205,7 +222,6 @@ export function TopPanel({rt, time, setTime, onInit, onClear, onRaise, ast, mode
className={mode===m ? "active":""} className={mode===m ? "active":""}
onClick={() => setMode(m)} onClick={() => setMode(m)}
>{buttonTxt}</button></KeyInfo>)} >{buttonTxt}</button></KeyInfo>)}
</div> </div>
&emsp; &emsp;
@ -221,7 +237,7 @@ export function TopPanel({rt, time, setTime, onInit, onClear, onRaise, ast, mode
&emsp; &emsp;
<KeyInfo keyInfo={<kbd>Space</kbd>}> <KeyInfo keyInfo={<><kbd>Space</kbd> toggles</>}>
<button title="pause the simulation" disabled={!rt || time.kind==="paused"} className={(rt && time.kind==="paused") ? "active":""} onClick={() => onChangePaused(true, performance.now())}><PauseIcon fontSize="small"/></button> <button title="pause the simulation" disabled={!rt || time.kind==="paused"} className={(rt && time.kind==="paused") ? "active":""} onClick={() => onChangePaused(true, performance.now())}><PauseIcon fontSize="small"/></button>
<button title="run the simulation in real time" disabled={!rt || time.kind==="realtime"} className={(rt && time.kind==="realtime") ? "active":""} onClick={() => onChangePaused(false, performance.now())}><PlayArrowIcon fontSize="small"/></button> <button title="run the simulation in real time" disabled={!rt || time.kind==="realtime"} className={(rt && time.kind==="realtime") ? "active":""} onClick={() => onChangePaused(false, performance.now())}><PlayArrowIcon fontSize="small"/></button>
</KeyInfo> </KeyInfo>
@ -253,6 +269,12 @@ export function TopPanel({rt, time, setTime, onInit, onClear, onRaise, ast, mode
<button title="advance time just enough for the next timer to elapse" disabled={nextTimedTransition===undefined} onClick={onSkip}><SkipNextIcon fontSize="small"/><AccessAlarmIcon fontSize="small"/></button> <button title="advance time just enough for the next timer to elapse" disabled={nextTimedTransition===undefined} onClick={onSkip}><SkipNextIcon fontSize="small"/><AccessAlarmIcon fontSize="small"/></button>
</KeyInfo> </KeyInfo>
&emsp;
<KeyInfo keyInfo={<kbd>Backspace</kbd>}>
<button title="undo last step (go back in time)"
disabled={rtIdx===undefined || rtIdx===0} onClick={onBack}><SkipPreviousIcon fontSize="small"/></button>
</KeyInfo>
</div> </div>
&emsp; &emsp;
@ -291,7 +313,7 @@ export function TopPanel({rt, time, setTime, onInit, onClear, onRaise, ast, mode
<div style={{display:"inline-block"}}> <div style={{display:"inline-block"}}>
<KeyInfo keyInfo={<kbd>~</kbd>}> <KeyInfo keyInfo={<kbd>~</kbd>}>
<input id="checkbox-keys" type="checkbox" checked={showKeys} onChange={e => setShowKeys(e.target.checked)}></input> <input id="checkbox-keys" type="checkbox" checked={showKeys} onChange={e => setShowKeys(e.target.checked)}></input>
<label for="checkbox-keys">shortcuts</label> <label for="checkbox-keys">see shortcuts</label>
</KeyInfo> </KeyInfo>
</div> </div>

View file

@ -39,19 +39,12 @@
/* fill: rgba(0, 0, 255, 0.2); */ /* fill: rgba(0, 0, 255, 0.2); */
} }
.rountangle.error { .rountangle.error {
stroke: rgb(230,0,0); stroke: var(--error-color);
} }
.rountangle.active { .rountangle.active {
/* fill: rgb(255, 140, 0); */
/* fill-opacity: 0.2; */
/* stroke: rgb(100, 149, 237); */
/* stroke: */
stroke: rgb(192, 125, 0); stroke: rgb(192, 125, 0);
fill:rgb(255, 251, 244); fill:rgb(255, 251, 244);
/* fill: lightgrey; */ filter: drop-shadow( 0px 0px 3px rgba(192, 125, 0, 0.85));
/* color: white; */
filter: drop-shadow( 0px 0px 3px rgba(192, 125, 0, 0.856));
/* stroke-width: 3px; */
} }
.selected:hover:not(:active) { .selected:hover:not(:active) {
@ -166,7 +159,7 @@ text.helper:hover {
} }
.arrow.error { .arrow.error {
stroke: rgb(230,0,0); stroke: var(--error-color);
} }
.arrow.fired { .arrow.fired {
stroke: rgb(192, 125, 0); stroke: rgb(192, 125, 0);
@ -174,7 +167,7 @@ text.helper:hover {
} }
text.error, tspan.error { text.error, tspan.error {
fill: rgb(230,0,0); fill: var(--error-color);
font-weight: 600; font-weight: 600;
} }

View file

@ -314,7 +314,8 @@ export function VisualEditor({ast, setAST, rt, errors, setErrors, mode, highligh
const onMouseMove = (e: {pageX: number, pageY: number}) => { const onMouseMove = (e: {pageX: number, pageY: number}) => {
const currentPointer = getCurrentPointer(e); const currentPointer = getCurrentPointer(e);
if (dragging) { if (dragging) {
const pointerDelta = subtractV2D(currentPointer, dragging.lastMousePos); // const pointerDelta = subtractV2D(currentPointer, dragging.lastMousePos);
const pointerDelta = {x: e.movementX, y: e.movementY};
setState(state => ({ setState(state => ({
...state, ...state,
rountangles: state.rountangles.map(r => { rountangles: state.rountangles.map(r => {

View file

@ -4,6 +4,11 @@ html, body {
font-family: Roboto, sans-serif; font-family: Roboto, sans-serif;
} }
body {
/* --error-color: darkred; */
--error-color: rgb(163, 0, 0);
}
div#root { div#root {
height: 100%; height: 100%;
} }
@ -18,7 +23,7 @@ kbd {
border: 0.8px solid #aaa; border: 0.8px solid #aaa;
border-radius: 4px; border-radius: 4px;
background: linear-gradient(#ebebeb, #fff); background: linear-gradient(#ebebeb, #fff);
box-shadow: inset 0 -2px 0 #aaa; box-shadow: inset 0 -1.5px 0 #aaa;
vertical-align: middle; vertical-align: middle;
user-select: none; user-select: none;
} }