ability to display keyboard shortcuts in top bar

This commit is contained in:
Joeri Exelmans 2025-10-19 13:53:46 +02:00
parent 373e26dc1b
commit 9ce55e0264
5 changed files with 136 additions and 24 deletions

View file

@ -35,18 +35,26 @@ summary {
text-align: "right";
}
.toolbar > * {
.toolbar * {
vertical-align: middle;
height: 26px;
}
.toolbar > input {
.toolbar *:not(label) {
/* vertical-align: bottom; */
}
.toolbar input {
height: 20px;
}
.toolbar div {
vertical-align: bottom;
}
.toolbar button {
height: 26px;
}
button.active {
border: solid blue 2px;
background-color: rgba(0,0,255,0.2);
color: black;
}
}

16
src/App/KeyInfo.tsx Normal file
View file

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

View file

@ -11,9 +11,12 @@ import SkipNextIcon from '@mui/icons-material/SkipNext';
import TrendingFlatIcon from '@mui/icons-material/TrendingFlat';
import AccessAlarmIcon from '@mui/icons-material/AccessAlarm';
import StopIcon from '@mui/icons-material/Stop';
import UndoIcon from '@mui/icons-material/Undo';
import RedoIcon from '@mui/icons-material/Redo';
import { formatTime } from "./util";
import { InsertMode } from "../VisualEditor/VisualEditor";
import { KeyInfoHidden, KeyInfoVisible } from "./KeyInfo";
export type TopPanelProps = {
rt?: BigStep,
@ -61,6 +64,9 @@ function HistoryIcon(props: {kind: "shallow"|"deep"}) {
export function TopPanel({rt, time, setTime, onInit, onClear, onRaise, ast, mode, setMode}: TopPanelProps) {
const [displayTime, setDisplayTime] = useState("0.000");
const [timescale, setTimescale] = useState(1);
const [showKeys, setShowKeys] = useState(true);
const KeyInfo = showKeys ? KeyInfoVisible : KeyInfoHidden;
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
@ -68,12 +74,25 @@ export function TopPanel({rt, time, setTime, onInit, onClear, onRaise, ast, mode
e.preventDefault();
onChangePaused(time.kind !== "paused", performance.now());
};
if (e.key === "i") {
e.preventDefault();
onInit();
}
};
window.addEventListener("keydown", onKeyDown);
return () => {
window.removeEventListener("keydown", onKeyDown);
};
}, [time]);
}, [time, onInit]);
useEffect(() => {
setTimeout(() => localStorage.setItem("showKeys", showKeys?"1":"0"), 100);
}, [showKeys])
useEffect(() => {
const show = localStorage.getItem("showKeys") || "1";
setShowKeys(show==="1")
}, [])
function updateDisplayedTime() {
const now = performance.now();
@ -126,53 +145,71 @@ export function TopPanel({rt, time, setTime, onInit, onClear, onRaise, ast, mode
return <>
<div className="toolbar">
<div style={{display:'inline-block'}}>
<div style={{display:'inline-block'}}>
{([
["and", "AND-states", <RountangleIcon kind="and"/>],
["or", "OR-states", <RountangleIcon kind="or"/>],
["pseudo", "pseudo-states", <PseudoStateIcon/>],
["shallow", "shallow history", <HistoryIcon kind="shallow"/>],
["deep", "deep history", <HistoryIcon kind="deep"/>],
["transition", "transitions", <TrendingFlatIcon fontSize="small"/>],
["text", "text", <>&nbsp;T&nbsp;</>],
] as [InsertMode, string, ReactElement][]).map(([m, hint, buttonTxt]) =>
["and", "AND-states", <RountangleIcon kind="and"/>, <kbd>A</kbd>],
["or", "OR-states", <RountangleIcon kind="or"/>, <kbd>O</kbd>],
["pseudo", "pseudo-states", <PseudoStateIcon/>, <kbd>P</kbd>],
["shallow", "shallow history", <HistoryIcon kind="shallow"/>, <kbd>H</kbd>],
["deep", "deep history", <HistoryIcon kind="deep"/>, <></>],
["transition", "transitions", <TrendingFlatIcon fontSize="small"/>, <kbd>T</kbd>],
["text", "text", <>&nbsp;T&nbsp;</>, <kbd>X</kbd>],
] as [InsertMode, string, ReactElement, ReactElement][]).map(([m, hint, buttonTxt, keyInfo]) =>
<KeyInfo keyInfo={keyInfo}>
<button
title={"insert "+hint}
disabled={mode===m}
className={mode===m ? "active":""}
onClick={() => setMode(m)}
>{buttonTxt}</button>)}
>{buttonTxt}</button></KeyInfo>)}
</div>
&emsp;
<div style={{display:'inline-block'}}>
<KeyInfo keyInfo={<kbd>I</kbd>}>
<button title="(re)initialize simulation" onClick={onInit} ><PlayArrowIcon fontSize="small"/><CachedIcon fontSize="small"/></button>
</KeyInfo>
<button title="clear the simulation" onClick={onClear} disabled={!rt}><StopIcon fontSize="small"/></button>
&emsp;
<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>
{/* <ToggleButtonGroup value={time.kind} exclusive onChange={(_,newValue) => onChangePaused(newValue==="paused", performance.now())} size="small">
<ToggleButton disableRipple value="paused" disabled={!rt}><PauseIcon/></ToggleButton>
<ToggleButton disableRipple value="realtime" disabled={!rt}><PlayArrowIcon/></ToggleButton>
</ToggleButtonGroup> */}
<KeyInfo keyInfo={<kbd>Space</kbd>}>
<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>
</KeyInfo>
&emsp;
<label htmlFor="number-timescale">timescale</label>&nbsp;
<KeyInfo keyInfo={<kbd>S</kbd>}>
<button title="slower" onClick={() => onTimeScaleChange((timescale/2).toString(), performance.now())}>÷2</button>
</KeyInfo>
<input title="controls how fast the simulation should run in real time mode - larger than 1 means: faster than wall-clock time" id="number-timescale" value={timescale.toFixed(3)} style={{width:40}} readOnly onChange={e => onTimeScaleChange(e.target.value, performance.now())}/>
<KeyInfo keyInfo={<kbd>F</kbd>}>
<button title="faster" onClick={() => onTimeScaleChange((timescale*2).toString(), performance.now())}>×2</button>
</KeyInfo>
&emsp;
<KeyInfo>
<label htmlFor="time">time (s)</label>&nbsp;
<input title="the current simulated time" id="time" disabled={!rt} value={displayTime} readOnly={true} className="readonlyTextBox" />
</KeyInfo>
&emsp;
<KeyInfo>
<label htmlFor="next-timeout">next (s)</label>&nbsp;
<input title="next point in simulated time where a timed transition may fire" id="next-timeout" disabled={!rt} value={nextTimedTransition ? formatTime(nextTimedTransition[0]) : '+inf'} readOnly={true} className="readonlyTextBox"/>
</KeyInfo>
<KeyInfo keyInfo={<kbd>Tab</kbd>}>
<button title="advance time just enough for the next timer to elapse" disabled={nextTimedTransition===undefined} onClick={() => {
const now = performance.now();
setTime(time => {
@ -184,9 +221,15 @@ export function TopPanel({rt, time, setTime, onInit, onClear, onRaise, ast, mode
}
});
}}><SkipNextIcon fontSize="small"/><AccessAlarmIcon fontSize="small"/></button>
</KeyInfo>
</div>
&emsp;
</div>
<div style={{display:'inline-block'}}>
{ast.inputEvents &&
<>
{ast.inputEvents.map(({event, paramName}) =>
@ -207,9 +250,20 @@ export function TopPanel({rt, time, setTime, onInit, onClear, onRaise, ast, mode
}}>
<BoltIcon fontSize="small"/>
{event}
</button>{paramName && <><input id={`input-${event}-param`} style={{width: 20}} placeholder={paramName}/></>}&nbsp;</>)}
</button>
{paramName && <><input id={`input-${event}-param`} style={{width: 20}} placeholder={paramName}/></>}
&nbsp;</>)}
</>
}
&emsp;
<div style={{display:"inline-block"}}>
<input id="checkbox-keys" type="checkbox" checked={showKeys} onChange={e => setShowKeys(e.target.checked)}></input>
<label for="checkbox-keys">shortcuts</label>
</div>
</div>
</div></>;
}

View file

@ -18,5 +18,11 @@ export function getKeyHandler(setMode: Dispatch<SetStateAction<InsertMode>>) {
if (e.key === "x") {
setMode("text");
}
if (e.key === "h") {
setMode(oldMode => {
if (oldMode === "shallow") return "deep";
return "shallow";
})
}
}
}

View file

@ -7,3 +7,31 @@ html, body {
div#root {
height: 100%;
}
kbd {
display: inline-block;
padding: .12em .3em;
padding-bottom: 3px;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: .9em;
line-height: 1;
border: 0.8px solid #aaa;
border-radius: 4px;
background: linear-gradient(#ebebeb, #fff);
box-shadow: inset 0 -2px 0 #aaa;
vertical-align: middle;
user-select: none;
}
kbd:active { transform: translateY(1px); }
input {
/* border: solid blue 2px; */
accent-color: rgba(0,0,255,0.2);
/* accent-color: blue; */
}
::selection {
background-color: rgba(0,0,255,0.2);
}