const ShortCutShowKeys = ~;
-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 [timescale, setTimescale] = usePersistentState("timescale", 1);
const config = trace && trace.trace[trace.idx];
- const KeyInfo = showKeys ? KeyInfoVisible : KeyInfoHidden;
-
const updateDisplayedTime = useCallback(() => {
const now = Math.round(performance.now());
const timeMs = getSimTime(time, now);
@@ -69,12 +80,8 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on
}, [time, setDisplayTime]);
const formattedDisplayTime = useMemo(() => formatTime(displayTime), [displayTime]);
-
- // const lastSimTime = useMemo(() => time.kind === "realtime" ? time.since.simtime : time.simtime, [time]);
-
const lastSimTime = config?.simtime || 0;
-
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.
const interval = setInterval(() => {
@@ -115,54 +122,18 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on
}
}, [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]});
-
- 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]);
+ const KeyInfo = showKeys ? KeyInfoVisible : KeyInfoHidden;
return
@@ -207,11 +178,26 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on
+ {/* rotate */}
+ {/* find, replace */}
+
+ Ctrl+Shift+F>}>
+ setShowFindReplace(x => !x)}
+ >
+
+
+
+
+
+
{/* execution */}
diff --git a/src/App/TopPanel/UndoRedoButtons.tsx b/src/App/TopPanel/UndoRedoButtons.tsx
index 0a9d0e5..c03a71d 100644
--- a/src/App/TopPanel/UndoRedoButtons.tsx
+++ b/src/App/TopPanel/UndoRedoButtons.tsx
@@ -3,27 +3,14 @@ import { KeyInfoHidden, KeyInfoVisible } from "./KeyInfo";
import UndoIcon from '@mui/icons-material/Undo';
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}) {
- const onKeyDown = useCallback((e: KeyboardEvent) => {
- if (e.ctrlKey) {
- // ctrl is down
- 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]);
+ useShortcuts([
+ {keys: ["Ctrl", "z"], action: onUndo},
+ {keys: ["Ctrl", "Shift", "Z"], action: onRedo},
+ ])
const KeyInfo = showKeys ? KeyInfoVisible : KeyInfoHidden;
return <>
diff --git a/src/App/TopPanel/ZoomButtons.tsx b/src/App/TopPanel/ZoomButtons.tsx
index 77ae440..718c311 100644
--- a/src/App/TopPanel/ZoomButtons.tsx
+++ b/src/App/TopPanel/ZoomButtons.tsx
@@ -4,12 +4,20 @@ import { KeyInfoHidden, KeyInfoVisible } from "./KeyInfo";
import ZoomInIcon from '@mui/icons-material/ZoomIn';
import ZoomOutIcon from '@mui/icons-material/ZoomOut';
+import { useShortcuts } from "@/hooks/useShortcuts";
const shortcutZoomIn = <>Ctrl+->;
const shortcutZoomOut = <>Ctrl++>;
export const ZoomButtons = memo(function ZoomButtons({showKeys, zoom, setZoom}: {showKeys: boolean, zoom: number, setZoom: Dispatch>}) {
+ 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;
function onZoomIn() {
@@ -19,27 +27,6 @@ export const ZoomButtons = memo(function ZoomButtons({showKeys, zoom, setZoom}:
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 <>
diff --git a/src/App/VisualEditor/hooks/useMouse.tsx b/src/App/VisualEditor/hooks/useMouse.tsx
index 5dd1596..733910f 100644
--- a/src/App/VisualEditor/hooks/useMouse.tsx
+++ b/src/App/VisualEditor/hooks/useMouse.tsx
@@ -6,6 +6,7 @@ import { MIN_ROUNTANGLE_SIZE } from "../../parameters";
import { InsertMode } from "../../TopPanel/InsertModes";
import { Selecting, SelectingState } from "../Selection";
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) {
const [dragging, setDragging] = useState(false);
@@ -300,76 +301,47 @@ export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoo
}, [dragging, selectingState, refSVG.current]);
const trackShiftKey = useCallback((e: KeyboardEvent) => {
- // @ts-ignore
- if (["INPUT", "TEXTAREA", "SELECT"].includes(e.target?.tagName)) return;
-
- if (e.shiftKey || e.ctrlKey) {
- setShiftOrCtrlPressed(true);
- }
- else {
- setShiftOrCtrlPressed(false);
- }
+ setShiftOrCtrlPressed(e.shiftKey || e.ctrlKey);
}, []);
- const onKeyDown = useCallback((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 onSelectAll = useCallback(() => {
+ setDragging(false);
+ setState(state => ({
+ ...state,
+ // @ts-ignore
+ selection: [
+ ...state.rountangles.flatMap(r => ["left", "top", "right", "bottom"].map(part => ({uid: r.uid, part}))),
+ ...state.diamonds.flatMap(d => ["left", "top", "right", "bottom"].map(part => ({uid: d.uid, part}))),
+ ...state.arrows.flatMap(a => ["start", "end"].map(part => ({uid: a.uid, part}))),
+ ...state.texts.map(t => ({uid: t.uid, part: "text"})),
+ ...state.history.map(h => ({uid: h.uid, part: "history"})),
+ ],
+ }));
+ }, [setState, setDragging]);
- 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);
- setState(state => ({
- ...state,
- // @ts-ignore
- selection: [
- ...state.rountangles.flatMap(r => ["left", "top", "right", "bottom"].map(part => ({uid: r.uid, part}))),
- ...state.diamonds.flatMap(d => ["left", "top", "right", "bottom"].map(part => ({uid: d.uid, part}))),
- ...state.arrows.flatMap(a => ["start", "end"].map(part => ({uid: a.uid, part}))),
- ...state.texts.map(t => ({uid: t.uid, part: "text"})),
- ...state.history.map(h => ({uid: h.uid, part: "history"})),
- ]
- }))
- }
- }
- }, [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(() => {
// mousemove and mouseup are global event handlers so they keep working when pointer is outside of browser window
window.addEventListener("mouseup", onMouseUp);
window.addEventListener("mousemove", onMouseMove);
- window.addEventListener("keydown", onKeyDown);
window.addEventListener("keydown", trackShiftKey);
window.addEventListener("keyup", trackShiftKey);
return () => {
window.removeEventListener("mousemove", onMouseMove);
window.removeEventListener("mouseup", onMouseUp);
- window.removeEventListener("keydown", onKeyDown);
window.removeEventListener("keydown", trackShiftKey);
window.removeEventListener("keyup", trackShiftKey);
};
diff --git a/src/hooks/useShortcuts.ts b/src/hooks/useShortcuts.ts
new file mode 100644
index 0000000..13cb4e2
--- /dev/null
+++ b/src/hooks/useShortcuts.ts
@@ -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]);
+ }
+}
diff --git a/src/index.css b/src/index.css
index f0f2852..78b28ca 100644
--- a/src/index.css
+++ b/src/index.css
@@ -32,8 +32,9 @@ html, body {
--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));
--associated-color: light-dark(green, rgb(186, 245, 119));
- --bottom-panel-bg-color: light-dark(rgb(255, 249, 235), rgb(24, 40, 70));
- --summary-hover-bg-color: light-dark(#eee, #313131);
+ --greeter-bg-color: light-dark(rgb(255, 249, 235), rgb(24, 40, 70));
+ /* --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));
--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));
@@ -49,6 +50,25 @@ input {
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 {
height: 100%;
diff --git a/todo.txt b/todo.txt
index 5035608..6044e5c 100644
--- a/todo.txt
+++ b/todo.txt
@@ -53,9 +53,11 @@ TODO
- hovering over event in side panel should highlight all occurrences of the event in the SC
- rename events / variables
find/replace?
+
- hovering over error in bottom panel should highlight that error in the SC
- highlight selected shapes while making a selection
- highlight about-to-fire transitions
+
- integrate undo-history with browser history (back/forward buttons)
- ability to 'freeze' editor (e.g., to show plant SC)
@@ -74,3 +76,4 @@ TODO
Publish StateBuddy paper(s):
compare CS approach to other tools, not only YAKINDU
+z
\ No newline at end of file