toolbar button to undo last step
This commit is contained in:
parent
59d5e9913a
commit
5e60b3dc95
8 changed files with 99 additions and 31 deletions
|
|
@ -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>;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
 
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
 
|
 
|
||||||
|
|
@ -221,7 +237,7 @@ export function TopPanel({rt, time, setTime, onInit, onClear, onRaise, ast, mode
|
||||||
|
|
||||||
 
|
 
|
||||||
|
|
||||||
<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>
|
||||||
|
|
||||||
|
 
|
||||||
|
<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>
|
||||||
|
|
||||||
 
|
 
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 => {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
@ -38,4 +43,4 @@ input {
|
||||||
|
|
||||||
label {
|
label {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue