diff --git a/src/App/BottomPanel/FindReplace.tsx b/src/App/BottomPanel/FindReplace.tsx
index 818dbe2..4ea9326 100644
--- a/src/App/BottomPanel/FindReplace.tsx
+++ b/src/App/BottomPanel/FindReplace.tsx
@@ -4,6 +4,7 @@ import { usePersistentState } from "@/hooks/usePersistentState";
import CloseIcon from '@mui/icons-material/Close';
import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
+import { useShortcuts } from "@/hooks/useShortcuts";
type FindReplaceProps = {
setCS: Dispatch<(oldState: VisualEditorState) => VisualEditorState>,
@@ -27,27 +28,15 @@ export function FindReplace({setCS, hide}: FindReplaceProps) {
});
}, [findTxt, replaceTxt]);
- const onKeyDown = useCallback((e: KeyboardEvent) => {
- if (e.key === "Enter") {
- e.preventDefault();
- e.stopPropagation();
- onReplace();
- // setModal(null);
- }
- }, [onReplace]);
+ useShortcuts([
+ {keys: ["Enter"], action: onReplace},
+ ])
const onSwap = useCallback(() => {
setReplaceTxt(findTxt);
setFindText(replaceTxt);
}, [findTxt, replaceTxt]);
- useEffect(() => {
- window.addEventListener("keydown", onKeyDown);
- return () => {
- window.removeEventListener("keydown", onKeyDown);
- }
- }, [])
-
return
setFindText(e.target.value)} style={{width:300}}/>
diff --git a/src/App/Modals/TextDialog.tsx b/src/App/Modals/TextDialog.tsx
index 6ab731b..ead2abc 100644
--- a/src/App/Modals/TextDialog.tsx
+++ b/src/App/Modals/TextDialog.tsx
@@ -1,24 +1,20 @@
-import { Dispatch, ReactElement, SetStateAction, useState, KeyboardEvent, useEffect, useRef } from "react";
+import { Dispatch, ReactElement, SetStateAction, useState, useCallback } from "react";
import { cachedParseLabel } from "@/statecharts/parser";
+import { useShortcuts } from "@/hooks/useShortcuts";
export function TextDialog(props: {setModal: Dispatch
>, text: string, done: (newText: string|undefined) => void}) {
const [text, setText] = useState(props.text);
- function onKeyDown(e: KeyboardEvent) {
- if (e.key === "Enter") {
- if (!e.shiftKey) {
- e.preventDefault();
+ useShortcuts([
+ {keys: ["Enter"], action: useCallback(() => {
props.done(text);
props.setModal(null);
- }
- }
- if (e.key === "Escape") {
- props.setModal(null);
- e.stopPropagation();
- }
- e.stopPropagation();
- }
+ }, [text, props.done, props.setModal])},
+ {keys: ["Escape"], action: useCallback(() => {
+ props.setModal(null);
+ }, [props.setModal])},
+ ], false);
let parseError = "";
try {
@@ -28,7 +24,7 @@ export function TextDialog(props: {setModal: Dispatch
+ return
{/* Text label:
*/}
;
-}
\ No newline at end of file
+}
diff --git a/src/App/SideBar/ShowAST.tsx b/src/App/SideBar/ShowAST.tsx
index 6c240ad..37e7cb9 100644
--- a/src/App/SideBar/ShowAST.tsx
+++ b/src/App/SideBar/ShowAST.tsx
@@ -4,6 +4,7 @@ import { usePersistentState } from "../../hooks/usePersistentState";
import { ConcreteState, stateDescription, Transition, UnstableState } from "../../statecharts/abstract_syntax";
import { Action, EventTrigger, Expression } from "../../statecharts/label_ast";
import { KeyInfoHidden, KeyInfoVisible } from "../TopPanel/KeyInfo";
+import { useShortcuts } from '@/hooks/useShortcuts';
export function ShowTransition(props: {transition: Transition}) {
return <>➝ {stateDescription(props.transition.tgt)}>;
@@ -49,7 +50,7 @@ export const ShowAST = memo(function ShowASTx(props: {root: ConcreteState | Unst
});
-export function ShowInputEvents({inputEvents, onRaise, disabled, showKeys}: {inputEvents: EventTrigger[], onRaise: (e: string, p: any) => void, disabled: boolean, showKeys: boolean}) {
+export function ShowInputEvents({inputEvents, onRaise, disabled}: {inputEvents: EventTrigger[], onRaise: (e: string, p: any) => void, disabled: boolean}) {
const raiseHandlers = inputEvents.map(({event}) => {
return () => {
// @ts-ignore
@@ -67,23 +68,16 @@ export function ShowInputEvents({inputEvents, onRaise, disabled, showKeys}: {inp
onRaise(event, paramParsed);
};
});
- 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;
- const n = (parseInt(e.key)+9) % 10;
- if (raiseHandlers[n] !== undefined) {
- raiseHandlers[n]();
- e.stopPropagation();
- e.preventDefault();
- }
- }
- useEffect(() => {
- window.addEventListener("keydown", onKeyDown);
- return () => window.removeEventListener("keydown", onKeyDown);
- }, [raiseHandlers]);
- // const KeyInfo = showKeys ? KeyInfoVisible : KeyInfoHidden;
+ const shortcutSpec = raiseHandlers.map((handler, i) => {
+ const n = (i+1)%10;
+ return {
+ keys: [n.toString()],
+ action: handler,
+ };
+ });
+ useShortcuts(shortcutSpec);
+
const KeyInfo = KeyInfoVisible; // always show keyboard shortcuts on input events, we can't expect the user to remember them
const [inputParams, setInputParams] = usePersistentState<{[eventName:string]: string}>("inputParams", {});
diff --git a/src/App/VisualEditor/hooks/useCopyPaste.ts b/src/App/VisualEditor/hooks/useCopyPaste.ts
index 21e8f9b..1cc01e8 100644
--- a/src/App/VisualEditor/hooks/useCopyPaste.ts
+++ b/src/App/VisualEditor/hooks/useCopyPaste.ts
@@ -2,6 +2,7 @@ import { Arrow, Diamond, Rountangle, Text, History } from "@/statecharts/concret
import { ClipboardEvent, Dispatch, SetStateAction, useCallback, useEffect } from "react";
import { Selection, VisualEditorState } from "../VisualEditor";
import { addV2D } from "@/util/geometry";
+import { useShortcuts } from "@/hooks/useShortcuts";
// const offset = {x: 40, y: 40};
const offset = {x: 0, y: 0};
@@ -12,6 +13,7 @@ export function useCopyPaste(makeCheckPoint: () => void, state: VisualEditorStat
if (data) {
try {
const parsed = JSON.parse(data);
+ makeCheckPoint();
setState(state => {
try {
let nextID = state.nextID;
@@ -49,7 +51,6 @@ export function useCopyPaste(makeCheckPoint: () => void, state: VisualEditorStat
...copiedTexts.map(t => ({uid: t.uid, parts: ["text"]})),
...copiedHistories.map(h => ({uid: h.uid, parts: ["history"]})),
];
- makeCheckPoint();
return {
...state,
rountangles: [...state.rountangles, ...copiedRountangles],
@@ -72,7 +73,7 @@ export function useCopyPaste(makeCheckPoint: () => void, state: VisualEditorStat
}
e.preventDefault();
}
- }, [setState]);
+ }, [makeCheckPoint, setState]);
const copyInternal = useCallback((state: VisualEditorState, selection: Selection, e: ClipboardEvent) => {
const uidsToCopy = new Set(selection.map(shape => shape.uid));
@@ -106,6 +107,7 @@ export function useCopyPaste(makeCheckPoint: () => void, state: VisualEditorStat
}, [state, selection]);
const deleteSelection = useCallback(() => {
+ makeCheckPoint();
setState(state => ({
...state,
rountangles: state.rountangles.filter(r => !state.selection.some(rs => rs.uid === r.uid)),
@@ -115,23 +117,11 @@ export function useCopyPaste(makeCheckPoint: () => void, state: VisualEditorStat
texts: state.texts.filter(t => !state.selection.some(ts => ts.uid === t.uid)),
selection: [],
}));
- }, [setState]);
+ }, [makeCheckPoint, setState]);
- const onKeyDown = (e: KeyboardEvent) => {
- // @ts-ignore
- if (["INPUT", "TEXTAREA", "SELECT"].includes(e.target?.tagName)) return;
- if (e.key === "Delete") {
- // delete selection
- makeCheckPoint();
- deleteSelection();
- e.preventDefault();
- }
- }
-
- useEffect(() => {
- window.addEventListener("keydown", onKeyDown);
- return () => window.removeEventListener("keydown", onKeyDown);
- })
+ useShortcuts([
+ {keys: ["Delete"], action: deleteSelection},
+ ])
return {onCopy, onPaste, onCut, deleteSelection};
}
\ No newline at end of file
diff --git a/src/hooks/useShortcuts.ts b/src/hooks/useShortcuts.ts
index 13cb4e2..cafb6db 100644
--- a/src/hooks/useShortcuts.ts
+++ b/src/hooks/useShortcuts.ts
@@ -1,11 +1,15 @@
import { useEffect } from "react";
-export function useShortcuts(spec: {keys: string[], action: () => void}[]) {
+export function useShortcuts(spec: {keys: string[], action: () => void}[], ignoreInputs = true) {
+ // I don't know if this is efficient, but I decided to just register one event listener for every shortcut, rather than generating one big event listener for all shortcuts.
+ // The benefit is that we don't have to memoize anything: useEffect will only be called if the action updated, and React allows calling useEffect for every item in a list as long as the list doesn't change.
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 (ignoreInputs) {
+ // @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;
diff --git a/todo.txt b/todo.txt
index 6044e5c..e0f1cd1 100644
--- a/todo.txt
+++ b/todo.txt
@@ -31,7 +31,8 @@
TODO
- bugs
- editing SC <-> Plant connections at runtime doesn't seem to work
+ (*) editing SC <-> Plant connections at runtime doesn't seem to work
+ (*) non-determinism error highlights only one of enabled transitions
- maybe support:
- explicit order of:
@@ -64,6 +65,10 @@ TODO
- show insert mode also next to cursor
- plot plant signals
+ - show error when states partially overlap?
+ useful when accidentally pasting the same data multiple times
+ (otherwise, you can't see that you have multiple states)
+
- performance:
maybe try this for rendering the execution trace:
https://legacy.reactjs.org/docs/optimizing-performance.html#virtualize-long-lists