getting rid of some code duplication
This commit is contained in:
parent
0266675f29
commit
970b9d850e
21 changed files with 325 additions and 302 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -32,3 +32,6 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||||
|
|
||||||
# Finder (MacOS) folder config
|
# Finder (MacOS) folder config
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# When building the app, we include the git rev in the status bar. We do this by calling git and writing the rev to a file, which is then included by the app.
|
||||||
|
src/git-rev.txt
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,12 @@
|
||||||
"module": "src/index.tsx",
|
"module": "src/index.tsx",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun --hot src/index.tsx",
|
"dev": "bun --hot src/index.tsx",
|
||||||
"build": "NODE_ENV=production bun build ./src/index.html --outdir=dist --sourcemap --target=browser --minify --define:process.env.NODE_ENV='\"production\"' --env='BUN_PUBLIC_*'",
|
"build": "git rev-parse HEAD > src/git-rev.txt && NODE_ENV=production bun build ./src/index.html --outdir=dist --sourcemap --target=browser --minify --define:process.env.NODE_ENV='\"production\"' --env='BUN_PUBLIC_*'",
|
||||||
"start": "NODE_ENV=production bun src/index.tsx"
|
"start": "NODE_ENV=production bun src/index.tsx"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/roboto": "^5.2.8",
|
"@fontsource/roboto": "^5.2.8",
|
||||||
"@mui/icons-material": "^7.3.4",
|
"@mui/icons-material": "^7.3.4",
|
||||||
// "argus-wasm": "git+https://deemz.org/git/joeri/argus-wasm.git#a4491b3433d48aa1f941bd5ad37b36f819d3b2ac",
|
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0"
|
"react-dom": "^19.2.0"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -60,26 +60,6 @@ details:has(+ details) {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
|
||||||
background-color: var(--button-bg-color);
|
|
||||||
border: 1px var(--separator-color) solid;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:not(:disabled):hover {
|
|
||||||
background-color: var(--light-accent-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
button:disabled {
|
|
||||||
background-color: var(--inactive-bg-color);
|
|
||||||
color: var(--inactive-fg-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
button.active {
|
|
||||||
border: solid var(--accent-border-color) 1px;
|
|
||||||
background-color: var(--light-accent-color);
|
|
||||||
color: var(--text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modalOuter {
|
.modalOuter {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import "../index.css";
|
import "../index.css";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
|
|
||||||
import { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { Dispatch, ReactElement, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
import { detectConnections } from "@/statecharts/detect_connections";
|
import { detectConnections } from "@/statecharts/detect_connections";
|
||||||
import { parseStatechart } from "../statecharts/parser";
|
import { parseStatechart } from "../statecharts/parser";
|
||||||
|
|
@ -16,7 +16,8 @@ import { useSimulator } from "./hooks/useSimulator";
|
||||||
import { useUrlHashState } from "../hooks/useUrlHashState";
|
import { useUrlHashState } from "../hooks/useUrlHashState";
|
||||||
import { plants } from "./plants";
|
import { plants } from "./plants";
|
||||||
import { emptyState } from "@/statecharts/concrete_syntax";
|
import { emptyState } from "@/statecharts/concrete_syntax";
|
||||||
import { ModalOverlay } from "./Modals/ModalOverlay";
|
import { ModalOverlay } from "./Overlays/ModalOverlay";
|
||||||
|
import { FindReplace } from "./BottomPanel/FindReplace";
|
||||||
|
|
||||||
export type EditHistory = {
|
export type EditHistory = {
|
||||||
current: VisualEditorState,
|
current: VisualEditorState,
|
||||||
|
|
@ -28,13 +29,18 @@ export type AppState = {
|
||||||
showKeys: boolean,
|
showKeys: boolean,
|
||||||
zoom: number,
|
zoom: number,
|
||||||
insertMode: InsertMode,
|
insertMode: InsertMode,
|
||||||
|
showFindReplace: boolean,
|
||||||
|
findText: string,
|
||||||
|
replaceText: string,
|
||||||
} & SideBarState;
|
} & SideBarState;
|
||||||
|
|
||||||
const defaultAppState: AppState = {
|
const defaultAppState: AppState = {
|
||||||
showKeys: true,
|
showKeys: true,
|
||||||
zoom: 1,
|
zoom: 1,
|
||||||
insertMode: 'and',
|
insertMode: 'and',
|
||||||
|
showFindReplace: false,
|
||||||
|
findText: "",
|
||||||
|
replaceText: "",
|
||||||
...defaultSideBarState,
|
...defaultSideBarState,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -143,7 +149,7 @@ export function App() {
|
||||||
return <div style={{
|
return <div style={{
|
||||||
height:'100%',
|
height:'100%',
|
||||||
// doesn't work:
|
// doesn't work:
|
||||||
colorScheme: lightMode!=="auto"?lightMode:undefined,
|
// colorScheme: lightMode !== "auto" ? lightMode : undefined,
|
||||||
}}>
|
}}>
|
||||||
<ModalOverlay modal={modal} setModal={setModal}>
|
<ModalOverlay modal={modal} setModal={setModal}>
|
||||||
{/* top-to-bottom: everything -> bottom panel */}
|
{/* top-to-bottom: everything -> bottom panel */}
|
||||||
|
|
@ -160,7 +166,7 @@ export function App() {
|
||||||
style={{flex: '0 0 content'}}
|
style={{flex: '0 0 content'}}
|
||||||
>
|
>
|
||||||
{editHistory && <TopPanel
|
{editHistory && <TopPanel
|
||||||
{...{onUndo, onRedo, onRotate, setModal, editHistory, ...simulator, ...setters, ...appState}}
|
{...{onUndo, onRedo, onRotate, setModal, editHistory, ...simulator, ...setters, ...appState, setEditorState}}
|
||||||
/>}
|
/>}
|
||||||
</div>
|
</div>
|
||||||
{/* Editor */}
|
{/* Editor */}
|
||||||
|
|
@ -168,6 +174,13 @@ export function App() {
|
||||||
{editorState && conns && syntaxErrors &&
|
{editorState && conns && syntaxErrors &&
|
||||||
<VisualEditor {...{state: editorState, setState: setEditorState, conns, syntaxErrors: allErrors, highlightActive, highlightTransitions, setModal, makeCheckPoint, ...appState}}/>}
|
<VisualEditor {...{state: editorState, setState: setEditorState, conns, syntaxErrors: allErrors, highlightActive, highlightTransitions, setModal, makeCheckPoint, ...appState}}/>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{appState.showFindReplace &&
|
||||||
|
<div>
|
||||||
|
<FindReplace setCS={setEditorState} hide={() => setters.setShowFindReplace(false)}/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right: sidebar */}
|
{/* Right: sidebar */}
|
||||||
|
|
@ -186,7 +199,7 @@ export function App() {
|
||||||
|
|
||||||
{/* Bottom panel */}
|
{/* Bottom panel */}
|
||||||
<div style={{flex: '0 0 content'}}>
|
<div style={{flex: '0 0 content'}}>
|
||||||
{syntaxErrors && <BottomPanel {...{errors: syntaxErrors}}/>}
|
{syntaxErrors && <BottomPanel {...{errors: syntaxErrors, ...appState, setEditorState, ...setters}}/>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalOverlay>
|
</ModalOverlay>
|
||||||
|
|
@ -194,4 +207,3 @@ export function App() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,11 @@
|
||||||
color: var(--background-color);
|
color: var(--background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.greeter {
|
||||||
|
/* border-top: 1px var(--separator-color) solid; */
|
||||||
|
background-color: var(--greeter-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
.bottom {
|
.bottom {
|
||||||
border-top: 1px var(--separator-color) solid;
|
border-top: 1px var(--separator-color) solid;
|
||||||
background-color: var(--bottom-panel-bg-color);
|
background-color: var(--bottom-panel-bg-color);
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,20 @@
|
||||||
import { useEffect, useState } from "react";
|
import { Dispatch, useEffect, useState } from "react";
|
||||||
import { TraceableError } from "../../statecharts/parser";
|
import { TraceableError } from "../../statecharts/parser";
|
||||||
|
|
||||||
import "./BottomPanel.css";
|
import "./BottomPanel.css";
|
||||||
|
|
||||||
import { PersistentDetailsLocalStorage } from "../PersistentDetails";
|
import { PersistentDetailsLocalStorage } from "../Components/PersistentDetails";
|
||||||
import { Logo } from "@/App/Logo/Logo";
|
import { Logo } from "@/App/Logo/Logo";
|
||||||
|
import { AppState } from "../App";
|
||||||
|
import { FindReplace } from "./FindReplace";
|
||||||
|
import { VisualEditorState } from "../VisualEditor/VisualEditor";
|
||||||
|
import { Setters } from "../makePartialSetter";
|
||||||
|
|
||||||
export function BottomPanel(props: {errors: TraceableError[]}) {
|
import gitRev from "@/git-rev.txt";
|
||||||
|
|
||||||
|
export function BottomPanel(props: {errors: TraceableError[], setEditorState: Dispatch<(state: VisualEditorState) => VisualEditorState>} & AppState & Setters<AppState>) {
|
||||||
const [greeting, setGreeting] = useState(
|
const [greeting, setGreeting] = useState(
|
||||||
<div style={{textAlign:'center'}}>
|
<div className="greeter" style={{textAlign:'center'}}>
|
||||||
<span style={{fontSize: 18, fontStyle: 'italic'}}>
|
<span style={{fontSize: 18, fontStyle: 'italic'}}>
|
||||||
Welcome to <Logo/>
|
Welcome to <Logo/>
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -21,9 +27,12 @@ export function BottomPanel(props: {errors: TraceableError[]}) {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return <div className="toolbar bottom">
|
return <div className="toolbar bottom">
|
||||||
{greeting}
|
{/* {props.showFindReplace &&
|
||||||
{props.errors.length > 0 &&
|
<div>
|
||||||
<div className="errorStatus">
|
<FindReplace setCS={props.setEditorState} hide={() => props.setShowFindReplace(false)}/>
|
||||||
|
</div>
|
||||||
|
} */}
|
||||||
|
<div className={"statusBar" + props.errors.length ? " error" : ""}>
|
||||||
<PersistentDetailsLocalStorage initiallyOpen={false} localStorageKey="errorsExpanded">
|
<PersistentDetailsLocalStorage initiallyOpen={false} localStorageKey="errorsExpanded">
|
||||||
<summary>{props.errors.length} errors</summary>
|
<summary>{props.errors.length} errors</summary>
|
||||||
<div style={{maxHeight: '25vh', overflow: 'auto'}}>
|
<div style={{maxHeight: '25vh', overflow: 'auto'}}>
|
||||||
|
|
@ -34,6 +43,6 @@ export function BottomPanel(props: {errors: TraceableError[]}) {
|
||||||
</div>
|
</div>
|
||||||
</PersistentDetailsLocalStorage>
|
</PersistentDetailsLocalStorage>
|
||||||
</div>
|
</div>
|
||||||
}
|
{greeting}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
59
src/App/BottomPanel/FindReplace.tsx
Normal file
59
src/App/BottomPanel/FindReplace.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { Dispatch, useCallback, useEffect } from "react";
|
||||||
|
import { VisualEditorState } from "../VisualEditor/VisualEditor";
|
||||||
|
import { usePersistentState } from "@/hooks/usePersistentState";
|
||||||
|
|
||||||
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
|
import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
|
||||||
|
|
||||||
|
type FindReplaceProps = {
|
||||||
|
setCS: Dispatch<(oldState: VisualEditorState) => VisualEditorState>,
|
||||||
|
// setModal: (modal: null) => void;
|
||||||
|
hide: () => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function FindReplace({setCS, hide}: FindReplaceProps) {
|
||||||
|
const [findTxt, setFindText] = usePersistentState("findTxt", "");
|
||||||
|
const [replaceTxt, setReplaceTxt] = usePersistentState("replaceTxt", "");
|
||||||
|
|
||||||
|
const onReplace = useCallback(() => {
|
||||||
|
setCS(cs => {
|
||||||
|
return {
|
||||||
|
...cs,
|
||||||
|
texts: cs.texts.map(txt => ({
|
||||||
|
...txt,
|
||||||
|
text: txt.text.replaceAll(findTxt, replaceTxt)
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [findTxt, replaceTxt]);
|
||||||
|
|
||||||
|
const onKeyDown = useCallback((e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onReplace();
|
||||||
|
// setModal(null);
|
||||||
|
}
|
||||||
|
}, [onReplace]);
|
||||||
|
|
||||||
|
const onSwap = useCallback(() => {
|
||||||
|
setReplaceTxt(findTxt);
|
||||||
|
setFindText(replaceTxt);
|
||||||
|
}, [findTxt, replaceTxt]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener("keydown", onKeyDown);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", onKeyDown);
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return <div className="toolbar toolbarGroup" style={{display: 'flex'}}>
|
||||||
|
<input placeholder="find" value={findTxt} onChange={e => setFindText(e.target.value)} style={{width:300}}/>
|
||||||
|
<button tabIndex={-1} onClick={onSwap}><SwapHorizIcon fontSize="small"/></button>
|
||||||
|
<input tabIndex={0} placeholder="replace" value={replaceTxt} onChange={(e => setReplaceTxt(e.target.value))} style={{width:300}}/>
|
||||||
|
|
||||||
|
<button onClick={onReplace}>replace all</button>
|
||||||
|
<button onClick={hide} style={{marginLeft: 'auto'}}><CloseIcon fontSize="small"/></button>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
5
src/App/Components/TwoStateButton.tsx
Normal file
5
src/App/Components/TwoStateButton.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { ButtonHTMLAttributes, PropsWithChildren } from "react";
|
||||||
|
|
||||||
|
export function TwoStateButton({active, children, className, ...rest}: PropsWithChildren<{active: boolean} & ButtonHTMLAttributes<HTMLButtonElement>>) {
|
||||||
|
return <button className={(className||"") + (active?" active":"")} {...rest}>{children}</button>
|
||||||
|
}
|
||||||
14
src/App/Overlays/WindowOverlay.tsx
Normal file
14
src/App/Overlays/WindowOverlay.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
// import { Dispatch, PropsWithChildren, ReactElement, SetStateAction } from "react";
|
||||||
|
// import { OverlayWindow } from "../App";
|
||||||
|
|
||||||
|
// export function WindowOverlay(props: PropsWithChildren<{overlayWindows: OverlayWindow[]}>) {
|
||||||
|
|
||||||
|
// return <>
|
||||||
|
// {props.modal && <div
|
||||||
|
// className="modalOuter"
|
||||||
|
// onMouseDown={() => props.setModal(null)}>
|
||||||
|
// </div>}
|
||||||
|
|
||||||
|
// {props.children}
|
||||||
|
// </>;
|
||||||
|
// }
|
||||||
|
|
@ -15,7 +15,7 @@ import { RTHistory } from './RTHistory';
|
||||||
import { BigStepCause, TraceState } from '../hooks/useSimulator';
|
import { BigStepCause, TraceState } from '../hooks/useSimulator';
|
||||||
import { plants, UniversalPlantState } from '../plants';
|
import { plants, UniversalPlantState } from '../plants';
|
||||||
import { TimeMode } from '@/statecharts/time';
|
import { TimeMode } from '@/statecharts/time';
|
||||||
import { PersistentDetails } from '../PersistentDetails';
|
import { PersistentDetails } from '../Components/PersistentDetails';
|
||||||
import "./SideBar.css";
|
import "./SideBar.css";
|
||||||
|
|
||||||
type SavedTraces = [string, BigStepCause[]][];
|
type SavedTraces = [string, BigStepCause[]][];
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { KeyInfoHidden, KeyInfoVisible } from "./KeyInfo";
|
||||||
import { HistoryIcon, PseudoStateIcon, RountangleIcon } from "./Icons";
|
import { HistoryIcon, PseudoStateIcon, RountangleIcon } from "./Icons";
|
||||||
|
|
||||||
import TrendingFlatIcon from '@mui/icons-material/TrendingFlat';
|
import TrendingFlatIcon from '@mui/icons-material/TrendingFlat';
|
||||||
|
import { useShortcuts } from "@/hooks/useShortcuts";
|
||||||
|
|
||||||
export type InsertMode = "and" | "or" | "pseudo" | "shallow" | "deep" | "transition" | "text";
|
export type InsertMode = "and" | "or" | "pseudo" | "shallow" | "deep" | "transition" | "text";
|
||||||
|
|
||||||
|
|
@ -18,45 +19,14 @@ const insertModes: [InsertMode, string, ReactElement, ReactElement][] = [
|
||||||
|
|
||||||
export const InsertModes = memo(function InsertModes({showKeys, insertMode, setInsertMode}: {showKeys: boolean, insertMode: InsertMode, setInsertMode: Dispatch<SetStateAction<InsertMode>>}) {
|
export const InsertModes = memo(function InsertModes({showKeys, insertMode, setInsertMode}: {showKeys: boolean, insertMode: InsertMode, setInsertMode: Dispatch<SetStateAction<InsertMode>>}) {
|
||||||
|
|
||||||
const onKeyDown = useCallback((e: KeyboardEvent) => {
|
useShortcuts([
|
||||||
// @ts-ignore
|
{keys: ["a"], action: () => setInsertMode("and")},
|
||||||
if (["INPUT", "TEXTAREA", "SELECT"].includes(e.target?.tagName)) return;
|
{keys: ["o"], action: () => setInsertMode("or")},
|
||||||
|
{keys: ["p"], action: () => setInsertMode("pseudo")},
|
||||||
if (!e.ctrlKey) {
|
{keys: ["t"], action: () => setInsertMode("transition")},
|
||||||
if (e.key === "a") {
|
{keys: ["x"], action: () => setInsertMode("text")},
|
||||||
e.preventDefault();
|
{keys: ["h"], action: () => setInsertMode(mode => mode === "shallow" ? "deep" : "shallow")},
|
||||||
setInsertMode("and");
|
]);
|
||||||
}
|
|
||||||
if (e.key === "o") {
|
|
||||||
e.preventDefault();
|
|
||||||
setInsertMode("or");
|
|
||||||
}
|
|
||||||
if (e.key === "p") {
|
|
||||||
e.preventDefault();
|
|
||||||
setInsertMode("pseudo");
|
|
||||||
}
|
|
||||||
if (e.key === "t") {
|
|
||||||
e.preventDefault();
|
|
||||||
setInsertMode("transition");
|
|
||||||
}
|
|
||||||
if (e.key === "x") {
|
|
||||||
e.preventDefault();
|
|
||||||
setInsertMode("text");
|
|
||||||
}
|
|
||||||
if (e.key === "h") {
|
|
||||||
e.preventDefault();
|
|
||||||
setInsertMode(oldMode => {
|
|
||||||
if (oldMode === "shallow") return "deep";
|
|
||||||
return "shallow";
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [setInsertMode]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
window.addEventListener("keydown", onKeyDown);
|
|
||||||
() => window.removeEventListener("keydown", onKeyDown);
|
|
||||||
}, [onKeyDown]);
|
|
||||||
|
|
||||||
const KeyInfo = showKeys ? KeyInfoVisible : KeyInfoHidden;
|
const KeyInfo = showKeys ? KeyInfoVisible : KeyInfoHidden;
|
||||||
return <>{insertModes.map(([m, hint, buttonTxt, keyInfo]) => <KeyInfo key={m} keyInfo={keyInfo}>
|
return <>{insertModes.map(([m, hint, buttonTxt, keyInfo]) => <KeyInfo key={m} keyInfo={keyInfo}>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { KeyInfoHidden, KeyInfoVisible } from "./KeyInfo";
|
||||||
import { setRealtime, TimeMode } from "@/statecharts/time";
|
import { setRealtime, TimeMode } from "@/statecharts/time";
|
||||||
|
|
||||||
import SpeedIcon from '@mui/icons-material/Speed';
|
import SpeedIcon from '@mui/icons-material/Speed';
|
||||||
|
import { useShortcuts } from "@/hooks/useShortcuts";
|
||||||
|
|
||||||
export const SpeedControl = memo(function SpeedControl({showKeys, timescale, setTimescale, setTime}: {showKeys: boolean, timescale: number, setTimescale: Dispatch<SetStateAction<number>>, setTime: Dispatch<SetStateAction<TimeMode>>}) {
|
export const SpeedControl = memo(function SpeedControl({showKeys, timescale, setTimescale, setTime}: {showKeys: boolean, timescale: number, setTimescale: Dispatch<SetStateAction<number>>, setTime: Dispatch<SetStateAction<TimeMode>>}) {
|
||||||
|
|
||||||
|
|
@ -31,25 +32,10 @@ export const SpeedControl = memo(function SpeedControl({showKeys, timescale, set
|
||||||
onTimeScaleChange((timescale*2).toString(), Math.round(performance.now()));
|
onTimeScaleChange((timescale*2).toString(), Math.round(performance.now()));
|
||||||
}, [onTimeScaleChange, timescale]);
|
}, [onTimeScaleChange, timescale]);
|
||||||
|
|
||||||
const onKeyDown = useCallback((e: KeyboardEvent) => {
|
useShortcuts([
|
||||||
// @ts-ignore
|
{keys: ["s"], action: onSlower},
|
||||||
if (["INPUT", "TEXTAREA", "SELECT"].includes(e.target?.tagName)) return;
|
{keys: ["f"], action: onFaster},
|
||||||
if (!e.ctrlKey) {
|
]);
|
||||||
if (e.key === "s") {
|
|
||||||
e.preventDefault();
|
|
||||||
onSlower();
|
|
||||||
}
|
|
||||||
if (e.key === "f") {
|
|
||||||
e.preventDefault();
|
|
||||||
onFaster();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [onSlower, onFaster])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
window.addEventListener("keydown", onKeyDown);
|
|
||||||
return () => window.removeEventListener("keydown", onKeyDown);
|
|
||||||
}, [onKeyDown])
|
|
||||||
|
|
||||||
const KeyInfo = showKeys ? KeyInfoVisible : KeyInfoHidden;
|
const KeyInfo = showKeys ? KeyInfoVisible : KeyInfoHidden;
|
||||||
return <>
|
return <>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import { Dispatch, memo, ReactElement, SetStateAction, useCallback, useEffect, useMemo, useState } from "react";
|
import { Dispatch, memo, ReactElement, SetStateAction, useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { TimerElapseEvent, Timers } from "../../statecharts/runtime_types";
|
import { TimerElapseEvent, Timers } from "../../statecharts/runtime_types";
|
||||||
import { getSimTime, setPaused, setRealtime, TimeMode } from "../../statecharts/time";
|
import { getSimTime, setPaused, setRealtime, TimeMode } from "../../statecharts/time";
|
||||||
import { InsertMode } from "./InsertModes";
|
|
||||||
import { About } from "../Modals/About";
|
import { About } from "../Modals/About";
|
||||||
import { EditHistory, LightMode } from "../App";
|
import { AppState, EditHistory, LightMode } from "../App";
|
||||||
import { KeyInfoHidden, KeyInfoVisible } from "./KeyInfo";
|
import { KeyInfoHidden, KeyInfoVisible } from "./KeyInfo";
|
||||||
import { UndoRedoButtons } from "./UndoRedoButtons";
|
import { UndoRedoButtons } from "./UndoRedoButtons";
|
||||||
import { ZoomButtons } from "./ZoomButtons";
|
import { ZoomButtons } from "./ZoomButtons";
|
||||||
|
|
@ -15,6 +14,8 @@ import BrightnessAutoIcon from '@mui/icons-material/BrightnessAuto';
|
||||||
|
|
||||||
import SpeedIcon from '@mui/icons-material/Speed';
|
import SpeedIcon from '@mui/icons-material/Speed';
|
||||||
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
||||||
|
import FindInPageIcon from '@mui/icons-material/FindInPage';
|
||||||
|
import FindInPageOutlinedIcon from '@mui/icons-material/FindInPageOutlined';
|
||||||
|
|
||||||
import AccessAlarmIcon from '@mui/icons-material/AccessAlarm';
|
import AccessAlarmIcon from '@mui/icons-material/AccessAlarm';
|
||||||
import CachedIcon from '@mui/icons-material/Cached';
|
import CachedIcon from '@mui/icons-material/Cached';
|
||||||
|
|
@ -29,10 +30,16 @@ import { usePersistentState } from "@/hooks/usePersistentState";
|
||||||
import { RotateButtons } from "./RotateButtons";
|
import { RotateButtons } from "./RotateButtons";
|
||||||
import { SpeedControl } from "./SpeedControl";
|
import { SpeedControl } from "./SpeedControl";
|
||||||
import { TraceState } from "../hooks/useSimulator";
|
import { TraceState } from "../hooks/useSimulator";
|
||||||
|
import { FindReplace } from "../BottomPanel/FindReplace";
|
||||||
|
import { VisualEditorState } from "../VisualEditor/VisualEditor";
|
||||||
|
import { Setters } from "../makePartialSetter";
|
||||||
|
import { TwoStateButton } from "../Components/TwoStateButton";
|
||||||
|
import { useShortcuts } from "@/hooks/useShortcuts";
|
||||||
|
|
||||||
export type TopPanelProps = {
|
export type TopPanelProps = {
|
||||||
trace: TraceState | null,
|
trace: TraceState | null,
|
||||||
time: TimeMode,
|
time: TimeMode,
|
||||||
|
|
||||||
setTime: Dispatch<SetStateAction<TimeMode>>,
|
setTime: Dispatch<SetStateAction<TimeMode>>,
|
||||||
onUndo: () => void,
|
onUndo: () => void,
|
||||||
onRedo: () => void,
|
onRedo: () => void,
|
||||||
|
|
@ -40,28 +47,32 @@ export type TopPanelProps = {
|
||||||
onInit: () => void,
|
onInit: () => void,
|
||||||
onClear: () => void,
|
onClear: () => void,
|
||||||
onBack: () => void,
|
onBack: () => void,
|
||||||
|
|
||||||
// lightMode: LightMode,
|
// lightMode: LightMode,
|
||||||
// setLightMode: Dispatch<SetStateAction<LightMode>>,
|
// setLightMode: Dispatch<SetStateAction<LightMode>>,
|
||||||
insertMode: InsertMode,
|
// insertMode: InsertMode,
|
||||||
setInsertMode: Dispatch<SetStateAction<InsertMode>>,
|
// setInsertMode: Dispatch<SetStateAction<InsertMode>>,
|
||||||
setModal: Dispatch<SetStateAction<ReactElement|null>>,
|
setModal: Dispatch<SetStateAction<ReactElement|null>>,
|
||||||
zoom: number,
|
// zoom: number,
|
||||||
setZoom: Dispatch<SetStateAction<number>>,
|
// setZoom: Dispatch<SetStateAction<number>>,
|
||||||
showKeys: boolean,
|
// showKeys: boolean,
|
||||||
setShowKeys: Dispatch<SetStateAction<boolean>>,
|
// setShowKeys: Dispatch<SetStateAction<boolean>>,
|
||||||
editHistory: EditHistory,
|
editHistory: EditHistory,
|
||||||
}
|
setEditorState: Dispatch<(oldState: VisualEditorState) => VisualEditorState>,
|
||||||
|
} & AppState & Setters<AppState>
|
||||||
|
|
||||||
const ShortCutShowKeys = <kbd>~</kbd>;
|
const ShortCutShowKeys = <kbd>~</kbd>;
|
||||||
|
|
||||||
export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, onRedo, onRotate, onInit, onClear, onBack, insertMode, setInsertMode, setModal, zoom, setZoom, showKeys, setShowKeys, editHistory}: TopPanelProps) {
|
function toggle(booleanSetter: Dispatch<(state: boolean) => boolean>) {
|
||||||
|
return () => booleanSetter(x => !x);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, onRedo, onRotate, onInit, onClear, onBack, insertMode, setInsertMode, setModal, zoom, setZoom, showKeys, setShowKeys, editHistory, showFindReplace, setShowFindReplace, setEditorState}: TopPanelProps) {
|
||||||
const [displayTime, setDisplayTime] = useState(0);
|
const [displayTime, setDisplayTime] = useState(0);
|
||||||
const [timescale, setTimescale] = usePersistentState("timescale", 1);
|
const [timescale, setTimescale] = usePersistentState("timescale", 1);
|
||||||
|
|
||||||
const config = trace && trace.trace[trace.idx];
|
const config = trace && trace.trace[trace.idx];
|
||||||
|
|
||||||
const KeyInfo = showKeys ? KeyInfoVisible : KeyInfoHidden;
|
|
||||||
|
|
||||||
const updateDisplayedTime = useCallback(() => {
|
const updateDisplayedTime = useCallback(() => {
|
||||||
const now = Math.round(performance.now());
|
const now = Math.round(performance.now());
|
||||||
const timeMs = getSimTime(time, now);
|
const timeMs = getSimTime(time, now);
|
||||||
|
|
@ -69,12 +80,8 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on
|
||||||
}, [time, setDisplayTime]);
|
}, [time, setDisplayTime]);
|
||||||
|
|
||||||
const formattedDisplayTime = useMemo(() => formatTime(displayTime), [displayTime]);
|
const formattedDisplayTime = useMemo(() => formatTime(displayTime), [displayTime]);
|
||||||
|
|
||||||
// const lastSimTime = useMemo(() => time.kind === "realtime" ? time.since.simtime : time.simtime, [time]);
|
|
||||||
|
|
||||||
const lastSimTime = config?.simtime || 0;
|
const lastSimTime = config?.simtime || 0;
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// This has no effect on statechart execution. In between events, the statechart is doing nothing. However, by updating the displayed time, we give the illusion of continuous progress.
|
// This has no effect on statechart execution. In between events, the statechart is doing nothing. However, by updating the displayed time, we give the illusion of continuous progress.
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
|
|
@ -115,54 +122,18 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on
|
||||||
}
|
}
|
||||||
}, [nextTimedTransition, setTime]);
|
}, [nextTimedTransition, setTime]);
|
||||||
|
|
||||||
|
useShortcuts([
|
||||||
|
{keys: ["`"], action: toggle(setShowKeys)},
|
||||||
|
{keys: ["Ctrl", "Shift", "F"], action: toggle(setShowFindReplace)},
|
||||||
|
{keys: ["i"], action: onInit},
|
||||||
|
{keys: ["c"], action: onClear},
|
||||||
|
{keys: ["Tab"], action: config && onSkip || onInit},
|
||||||
|
{keys: ["Backspace"], action: onBack},
|
||||||
|
{keys: ["Shift", "Tab"], action: onBack},
|
||||||
|
{keys: [" "], action: () => config && onChangePaused(time.kind !== "paused", Math.round(performance.now()))},
|
||||||
|
]);
|
||||||
|
|
||||||
console.log({lastSimTime, displayTime, nxt: nextTimedTransition?.[0]});
|
const KeyInfo = showKeys ? KeyInfoVisible : KeyInfoHidden;
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const onKeyDown = (e: KeyboardEvent) => {
|
|
||||||
// don't capture keyboard events when focused on an input element:
|
|
||||||
// @ts-ignore
|
|
||||||
if (["INPUT", "TEXTAREA", "SELECT"].includes(e.target?.tagName)) return;
|
|
||||||
|
|
||||||
if (!e.ctrlKey) {
|
|
||||||
if (e.key === " ") {
|
|
||||||
e.preventDefault();
|
|
||||||
if (config) {
|
|
||||||
onChangePaused(time.kind !== "paused", Math.round(performance.now()));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (e.key === "i") {
|
|
||||||
e.preventDefault();
|
|
||||||
onInit();
|
|
||||||
}
|
|
||||||
if (e.key === "c") {
|
|
||||||
e.preventDefault();
|
|
||||||
onClear();
|
|
||||||
}
|
|
||||||
if (e.key === "Tab") {
|
|
||||||
if (config === null) {
|
|
||||||
onInit();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
onSkip();
|
|
||||||
}
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
if (e.key === "`") {
|
|
||||||
e.preventDefault();
|
|
||||||
setShowKeys(show => !show);
|
|
||||||
}
|
|
||||||
if (e.key === "Backspace") {
|
|
||||||
e.preventDefault();
|
|
||||||
onBack();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener("keydown", onKeyDown);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("keydown", onKeyDown);
|
|
||||||
};
|
|
||||||
}, [config, time, onInit, onChangePaused, setShowKeys, onSkip, onBack, onClear]);
|
|
||||||
|
|
||||||
return <div className="toolbar">
|
return <div className="toolbar">
|
||||||
|
|
||||||
|
|
@ -207,11 +178,26 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on
|
||||||
 
|
 
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* rotate */}
|
||||||
<div className="toolbarGroup">
|
<div className="toolbarGroup">
|
||||||
<RotateButtons selection={editHistory.current.selection} onRotate={onRotate}/>
|
<RotateButtons selection={editHistory.current.selection} onRotate={onRotate}/>
|
||||||
 
|
 
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* find, replace */}
|
||||||
|
<div className="toolbarGroup">
|
||||||
|
<KeyInfo keyInfo={<><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>F</kbd></>}>
|
||||||
|
<TwoStateButton
|
||||||
|
title="show find & replace"
|
||||||
|
active={showFindReplace}
|
||||||
|
onClick={() => setShowFindReplace(x => !x)}
|
||||||
|
>
|
||||||
|
<FindInPageOutlinedIcon fontSize="small"/>
|
||||||
|
</TwoStateButton>
|
||||||
|
</KeyInfo>
|
||||||
|
 
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* execution */}
|
{/* execution */}
|
||||||
<div className="toolbarGroup">
|
<div className="toolbarGroup">
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,27 +3,14 @@ import { KeyInfoHidden, KeyInfoVisible } from "./KeyInfo";
|
||||||
|
|
||||||
import UndoIcon from '@mui/icons-material/Undo';
|
import UndoIcon from '@mui/icons-material/Undo';
|
||||||
import RedoIcon from '@mui/icons-material/Redo';
|
import RedoIcon from '@mui/icons-material/Redo';
|
||||||
|
import { useShortcuts } from "@/hooks/useShortcuts";
|
||||||
|
|
||||||
export const UndoRedoButtons = memo(function UndoRedoButtons({showKeys, onUndo, onRedo, historyLength, futureLength}: {showKeys: boolean, onUndo: () => void, onRedo: () => void, historyLength: number, futureLength: number}) {
|
export const UndoRedoButtons = memo(function UndoRedoButtons({showKeys, onUndo, onRedo, historyLength, futureLength}: {showKeys: boolean, onUndo: () => void, onRedo: () => void, historyLength: number, futureLength: number}) {
|
||||||
|
|
||||||
const onKeyDown = useCallback((e: KeyboardEvent) => {
|
useShortcuts([
|
||||||
if (e.ctrlKey) {
|
{keys: ["Ctrl", "z"], action: onUndo},
|
||||||
// ctrl is down
|
{keys: ["Ctrl", "Shift", "Z"], action: onRedo},
|
||||||
if (e.key === "z") {
|
])
|
||||||
e.preventDefault();
|
|
||||||
onUndo();
|
|
||||||
}
|
|
||||||
if (e.key === "Z") {
|
|
||||||
e.preventDefault();
|
|
||||||
onRedo();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [onUndo, onRedo]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
window.addEventListener("keydown", onKeyDown);
|
|
||||||
return () => window.removeEventListener("keydown", onKeyDown);
|
|
||||||
}, [onKeyDown]);
|
|
||||||
|
|
||||||
const KeyInfo = showKeys ? KeyInfoVisible : KeyInfoHidden;
|
const KeyInfo = showKeys ? KeyInfoVisible : KeyInfoHidden;
|
||||||
return <>
|
return <>
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,20 @@ import { KeyInfoHidden, KeyInfoVisible } from "./KeyInfo";
|
||||||
|
|
||||||
import ZoomInIcon from '@mui/icons-material/ZoomIn';
|
import ZoomInIcon from '@mui/icons-material/ZoomIn';
|
||||||
import ZoomOutIcon from '@mui/icons-material/ZoomOut';
|
import ZoomOutIcon from '@mui/icons-material/ZoomOut';
|
||||||
|
import { useShortcuts } from "@/hooks/useShortcuts";
|
||||||
|
|
||||||
const shortcutZoomIn = <><kbd>Ctrl</kbd>+<kbd>-</kbd></>;
|
const shortcutZoomIn = <><kbd>Ctrl</kbd>+<kbd>-</kbd></>;
|
||||||
const shortcutZoomOut = <><kbd>Ctrl</kbd>+<kbd>+</kbd></>;
|
const shortcutZoomOut = <><kbd>Ctrl</kbd>+<kbd>+</kbd></>;
|
||||||
|
|
||||||
export const ZoomButtons = memo(function ZoomButtons({showKeys, zoom, setZoom}: {showKeys: boolean, zoom: number, setZoom: Dispatch<SetStateAction<number>>}) {
|
export const ZoomButtons = memo(function ZoomButtons({showKeys, zoom, setZoom}: {showKeys: boolean, zoom: number, setZoom: Dispatch<SetStateAction<number>>}) {
|
||||||
|
|
||||||
|
useShortcuts([
|
||||||
|
{keys: ["Ctrl", "+"], action: onZoomIn}, // plus on numerical keypad
|
||||||
|
{keys: ["Ctrl", "Shift", "+"], action: onZoomIn}, // plus on normal keyboard requires Shift key
|
||||||
|
{keys: ["Ctrl", "="], action: onZoomIn}, // most browsers also bind this shortcut so it would be confusing if we also did not override it
|
||||||
|
{keys: ["Ctrl", "-"], action: onZoomOut},
|
||||||
|
]);
|
||||||
|
|
||||||
const KeyInfo = showKeys ? KeyInfoVisible : KeyInfoHidden;
|
const KeyInfo = showKeys ? KeyInfoVisible : KeyInfoHidden;
|
||||||
|
|
||||||
function onZoomIn() {
|
function onZoomIn() {
|
||||||
|
|
@ -19,27 +27,6 @@ export const ZoomButtons = memo(function ZoomButtons({showKeys, zoom, setZoom}:
|
||||||
setZoom(zoom => Math.max(zoom / ZOOM_STEP, ZOOM_MIN));
|
setZoom(zoom => Math.max(zoom / ZOOM_STEP, ZOOM_MIN));
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const onKeyDown = (e: KeyboardEvent) => {
|
|
||||||
if (e.ctrlKey) {
|
|
||||||
if (e.key === "+" || e.key === "=") {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
onZoomIn();
|
|
||||||
}
|
|
||||||
if (e.key === "-") {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
onZoomOut();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener("keydown", onKeyDown);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("keydown", onKeyDown);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<KeyInfo keyInfo={shortcutZoomOut}>
|
<KeyInfo keyInfo={shortcutZoomOut}>
|
||||||
<button title="zoom out" onClick={onZoomOut} disabled={zoom <= ZOOM_MIN}><ZoomOutIcon fontSize="small"/></button>
|
<button title="zoom out" onClick={onZoomOut} disabled={zoom <= ZOOM_MIN}><ZoomOutIcon fontSize="small"/></button>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { MIN_ROUNTANGLE_SIZE } from "../../parameters";
|
||||||
import { InsertMode } from "../../TopPanel/InsertModes";
|
import { InsertMode } from "../../TopPanel/InsertModes";
|
||||||
import { Selecting, SelectingState } from "../Selection";
|
import { Selecting, SelectingState } from "../Selection";
|
||||||
import { Selection, VisualEditorState } from "../VisualEditor";
|
import { Selection, VisualEditorState } from "../VisualEditor";
|
||||||
|
import { useShortcuts } from "@/hooks/useShortcuts";
|
||||||
|
|
||||||
export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoom: number, refSVG: {current: SVGSVGElement|null}, state: VisualEditorState, setState: Dispatch<(v: VisualEditorState) => VisualEditorState>, deleteSelection: () => void) {
|
export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoom: number, refSVG: {current: SVGSVGElement|null}, state: VisualEditorState, setState: Dispatch<(v: VisualEditorState) => VisualEditorState>, deleteSelection: () => void) {
|
||||||
const [dragging, setDragging] = useState(false);
|
const [dragging, setDragging] = useState(false);
|
||||||
|
|
@ -300,49 +301,10 @@ export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoo
|
||||||
}, [dragging, selectingState, refSVG.current]);
|
}, [dragging, selectingState, refSVG.current]);
|
||||||
|
|
||||||
const trackShiftKey = useCallback((e: KeyboardEvent) => {
|
const trackShiftKey = useCallback((e: KeyboardEvent) => {
|
||||||
// @ts-ignore
|
setShiftOrCtrlPressed(e.shiftKey || e.ctrlKey);
|
||||||
if (["INPUT", "TEXTAREA", "SELECT"].includes(e.target?.tagName)) return;
|
|
||||||
|
|
||||||
if (e.shiftKey || e.ctrlKey) {
|
|
||||||
setShiftOrCtrlPressed(true);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
setShiftOrCtrlPressed(false);
|
|
||||||
}
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onKeyDown = useCallback((e: KeyboardEvent) => {
|
const onSelectAll = useCallback(() => {
|
||||||
// don't capture keyboard events when focused on an input element:
|
|
||||||
// @ts-ignore
|
|
||||||
if (["INPUT", "TEXTAREA", "SELECT"].includes(e.target?.tagName)) return;
|
|
||||||
|
|
||||||
if (e.key === "o") {
|
|
||||||
// selected states become OR-states
|
|
||||||
setState(state => ({
|
|
||||||
...state,
|
|
||||||
rountangles: state.rountangles.map(r => state.selection.some(rs => rs.uid === r.uid) ? ({...r, kind: "or"}) : r),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
if (e.key === "a") {
|
|
||||||
// selected states become AND-states
|
|
||||||
setState(state => ({
|
|
||||||
...state,
|
|
||||||
rountangles: state.rountangles.map(r => state.selection.some(rs => rs.uid === r.uid) ? ({...r, kind: "and"}) : r),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
// if (e.key === "p") {
|
|
||||||
// // selected states become pseudo-states
|
|
||||||
// setSelection(selection => {
|
|
||||||
// setState(state => ({
|
|
||||||
// ...state,
|
|
||||||
// rountangles: state.rountangles.map(r => selection.some(rs => rs.uid === r.uid) ? ({...r, kind: "pseudo"}) : r),
|
|
||||||
// }));
|
|
||||||
// return selection;
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
if (e.ctrlKey) {
|
|
||||||
if (e.key === "a") {
|
|
||||||
e.preventDefault();
|
|
||||||
setDragging(false);
|
setDragging(false);
|
||||||
setState(state => ({
|
setState(state => ({
|
||||||
...state,
|
...state,
|
||||||
|
|
@ -353,23 +315,33 @@ export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoo
|
||||||
...state.arrows.flatMap(a => ["start", "end"].map(part => ({uid: a.uid, part}))),
|
...state.arrows.flatMap(a => ["start", "end"].map(part => ({uid: a.uid, part}))),
|
||||||
...state.texts.map(t => ({uid: t.uid, part: "text"})),
|
...state.texts.map(t => ({uid: t.uid, part: "text"})),
|
||||||
...state.history.map(h => ({uid: h.uid, part: "history"})),
|
...state.history.map(h => ({uid: h.uid, part: "history"})),
|
||||||
]
|
],
|
||||||
}))
|
}));
|
||||||
}
|
}, [setState, setDragging]);
|
||||||
}
|
|
||||||
}, [makeCheckPoint, deleteSelection, setState, setDragging]);
|
const convertSelection = useCallback((kind: "or"|"and") => {
|
||||||
|
makeCheckPoint();
|
||||||
|
setState(state => ({
|
||||||
|
...state,
|
||||||
|
rountangles: state.rountangles.map(r => state.selection.some(rs => rs.uid === r.uid) ? ({...r, kind}) : r),
|
||||||
|
}));
|
||||||
|
}, [makeCheckPoint, setState]);
|
||||||
|
|
||||||
|
useShortcuts([
|
||||||
|
{keys: ["o"], action: useCallback(() => convertSelection("or"), [convertSelection])},
|
||||||
|
{keys: ["a"], action: useCallback(() => convertSelection("and"), [convertSelection])},
|
||||||
|
{keys: ["Ctrl", "a"], action: onSelectAll},
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// mousemove and mouseup are global event handlers so they keep working when pointer is outside of browser window
|
// mousemove and mouseup are global event handlers so they keep working when pointer is outside of browser window
|
||||||
window.addEventListener("mouseup", onMouseUp);
|
window.addEventListener("mouseup", onMouseUp);
|
||||||
window.addEventListener("mousemove", onMouseMove);
|
window.addEventListener("mousemove", onMouseMove);
|
||||||
window.addEventListener("keydown", onKeyDown);
|
|
||||||
window.addEventListener("keydown", trackShiftKey);
|
window.addEventListener("keydown", trackShiftKey);
|
||||||
window.addEventListener("keyup", trackShiftKey);
|
window.addEventListener("keyup", trackShiftKey);
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("mousemove", onMouseMove);
|
window.removeEventListener("mousemove", onMouseMove);
|
||||||
window.removeEventListener("mouseup", onMouseUp);
|
window.removeEventListener("mouseup", onMouseUp);
|
||||||
window.removeEventListener("keydown", onKeyDown);
|
|
||||||
window.removeEventListener("keydown", trackShiftKey);
|
window.removeEventListener("keydown", trackShiftKey);
|
||||||
window.removeEventListener("keyup", trackShiftKey);
|
window.removeEventListener("keyup", trackShiftKey);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
26
src/hooks/useShortcuts.ts
Normal file
26
src/hooks/useShortcuts.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
export function useShortcuts(spec: {keys: string[], action: () => void}[]) {
|
||||||
|
for (const {keys, action} of spec) {
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = (e: KeyboardEvent) => {
|
||||||
|
// @ts-ignore: don't steal keyboard events while the user is typing in a text box, etc.
|
||||||
|
if (["INPUT", "TEXTAREA", "SELECT"].includes(e.target?.tagName)) return;
|
||||||
|
|
||||||
|
if (e.ctrlKey !== keys.includes("Ctrl")) return;
|
||||||
|
if (e.shiftKey !== keys.includes("Shift")) return;
|
||||||
|
if (!keys.includes(e.key)) return;
|
||||||
|
const remainingKeys = keys.filter(key => key !== "Ctrl" && key !== "Shift" && key !== e.key);
|
||||||
|
if (remainingKeys.length !== 0) {
|
||||||
|
console.warn("impossible shortcut sequence:", keys.join(' + '));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
action();
|
||||||
|
};
|
||||||
|
window.addEventListener("keydown", handler);
|
||||||
|
return () => window.removeEventListener("keydown", handler);
|
||||||
|
}, [action]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -32,8 +32,9 @@ html, body {
|
||||||
--fired-transition-color: light-dark(rgb(160, 0, 168), rgb(160, 0, 168));
|
--fired-transition-color: light-dark(rgb(160, 0, 168), rgb(160, 0, 168));
|
||||||
--firing-transition-color: light-dark(rgba(255, 128, 9, 1), rgba(255, 128, 9, 1));
|
--firing-transition-color: light-dark(rgba(255, 128, 9, 1), rgba(255, 128, 9, 1));
|
||||||
--associated-color: light-dark(green, rgb(186, 245, 119));
|
--associated-color: light-dark(green, rgb(186, 245, 119));
|
||||||
--bottom-panel-bg-color: light-dark(rgb(255, 249, 235), rgb(24, 40, 70));
|
--greeter-bg-color: light-dark(rgb(255, 249, 235), rgb(24, 40, 70));
|
||||||
--summary-hover-bg-color: light-dark(#eee, #313131);
|
/* --bottom-panel-bg-color: light-dark(rgb(219, 219, 219), rgb(31, 33, 36)); */
|
||||||
|
--summary-hover-bg-color: light-dark(#eee, #2e2f35);
|
||||||
--internal-event-bg-color: light-dark(rgb(255, 218, 252), rgb(99, 27, 94));
|
--internal-event-bg-color: light-dark(rgb(255, 218, 252), rgb(99, 27, 94));
|
||||||
--input-event-bg-color: light-dark(rgb(224, 247, 209), rgb(59, 95, 37));
|
--input-event-bg-color: light-dark(rgb(224, 247, 209), rgb(59, 95, 37));
|
||||||
--input-event-hover-bg-color: light-dark(rgb(195, 224, 176), rgb(59, 88, 40));
|
--input-event-hover-bg-color: light-dark(rgb(195, 224, 176), rgb(59, 88, 40));
|
||||||
|
|
@ -49,6 +50,25 @@ input {
|
||||||
border: 1px solid var(--separator-color);
|
border: 1px solid var(--separator-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: var(--button-bg-color);
|
||||||
|
border: 1px var(--separator-color) solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:not(:disabled):hover {
|
||||||
|
background-color: var(--light-accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
background-color: var(--inactive-bg-color);
|
||||||
|
color: var(--inactive-fg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.active {
|
||||||
|
border: solid var(--accent-border-color) 1px;
|
||||||
|
background-color: var(--light-accent-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
div#root {
|
div#root {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
|
||||||
3
todo.txt
3
todo.txt
|
|
@ -53,9 +53,11 @@ TODO
|
||||||
- hovering over event in side panel should highlight all occurrences of the event in the SC
|
- hovering over event in side panel should highlight all occurrences of the event in the SC
|
||||||
- rename events / variables
|
- rename events / variables
|
||||||
find/replace?
|
find/replace?
|
||||||
|
|
||||||
- hovering over error in bottom panel should highlight that error in the SC
|
- hovering over error in bottom panel should highlight that error in the SC
|
||||||
- highlight selected shapes while making a selection
|
- highlight selected shapes while making a selection
|
||||||
- highlight about-to-fire transitions
|
- highlight about-to-fire transitions
|
||||||
|
|
||||||
- integrate undo-history with browser history (back/forward buttons)
|
- integrate undo-history with browser history (back/forward buttons)
|
||||||
|
|
||||||
- ability to 'freeze' editor (e.g., to show plant SC)
|
- ability to 'freeze' editor (e.g., to show plant SC)
|
||||||
|
|
@ -74,3 +76,4 @@ TODO
|
||||||
|
|
||||||
Publish StateBuddy paper(s):
|
Publish StateBuddy paper(s):
|
||||||
compare CS approach to other tools, not only YAKINDU
|
compare CS approach to other tools, not only YAKINDU
|
||||||
|
z
|
||||||
Loading…
Add table
Add a link
Reference in a new issue