less code duplication
This commit is contained in:
parent
970b9d850e
commit
1bd801ce5d
6 changed files with 47 additions and 69 deletions
|
|
@ -4,6 +4,7 @@ import { usePersistentState } from "@/hooks/usePersistentState";
|
||||||
|
|
||||||
import CloseIcon from '@mui/icons-material/Close';
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
|
import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
|
||||||
|
import { useShortcuts } from "@/hooks/useShortcuts";
|
||||||
|
|
||||||
type FindReplaceProps = {
|
type FindReplaceProps = {
|
||||||
setCS: Dispatch<(oldState: VisualEditorState) => VisualEditorState>,
|
setCS: Dispatch<(oldState: VisualEditorState) => VisualEditorState>,
|
||||||
|
|
@ -27,27 +28,15 @@ export function FindReplace({setCS, hide}: FindReplaceProps) {
|
||||||
});
|
});
|
||||||
}, [findTxt, replaceTxt]);
|
}, [findTxt, replaceTxt]);
|
||||||
|
|
||||||
const onKeyDown = useCallback((e: KeyboardEvent) => {
|
useShortcuts([
|
||||||
if (e.key === "Enter") {
|
{keys: ["Enter"], action: onReplace},
|
||||||
e.preventDefault();
|
])
|
||||||
e.stopPropagation();
|
|
||||||
onReplace();
|
|
||||||
// setModal(null);
|
|
||||||
}
|
|
||||||
}, [onReplace]);
|
|
||||||
|
|
||||||
const onSwap = useCallback(() => {
|
const onSwap = useCallback(() => {
|
||||||
setReplaceTxt(findTxt);
|
setReplaceTxt(findTxt);
|
||||||
setFindText(replaceTxt);
|
setFindText(replaceTxt);
|
||||||
}, [findTxt, replaceTxt]);
|
}, [findTxt, replaceTxt]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
window.addEventListener("keydown", onKeyDown);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("keydown", onKeyDown);
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return <div className="toolbar toolbarGroup" style={{display: 'flex'}}>
|
return <div className="toolbar toolbarGroup" style={{display: 'flex'}}>
|
||||||
<input placeholder="find" value={findTxt} onChange={e => setFindText(e.target.value)} style={{width:300}}/>
|
<input placeholder="find" value={findTxt} onChange={e => setFindText(e.target.value)} style={{width:300}}/>
|
||||||
<button tabIndex={-1} onClick={onSwap}><SwapHorizIcon fontSize="small"/></button>
|
<button tabIndex={-1} onClick={onSwap}><SwapHorizIcon fontSize="small"/></button>
|
||||||
|
|
|
||||||
|
|
@ -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 { cachedParseLabel } from "@/statecharts/parser";
|
||||||
|
import { useShortcuts } from "@/hooks/useShortcuts";
|
||||||
|
|
||||||
export function TextDialog(props: {setModal: Dispatch<SetStateAction<ReactElement|null>>, text: string, done: (newText: string|undefined) => void}) {
|
export function TextDialog(props: {setModal: Dispatch<SetStateAction<ReactElement|null>>, text: string, done: (newText: string|undefined) => void}) {
|
||||||
const [text, setText] = useState(props.text);
|
const [text, setText] = useState(props.text);
|
||||||
|
|
||||||
function onKeyDown(e: KeyboardEvent) {
|
useShortcuts([
|
||||||
if (e.key === "Enter") {
|
{keys: ["Enter"], action: useCallback(() => {
|
||||||
if (!e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
props.done(text);
|
props.done(text);
|
||||||
props.setModal(null);
|
props.setModal(null);
|
||||||
}
|
}, [text, props.done, props.setModal])},
|
||||||
}
|
{keys: ["Escape"], action: useCallback(() => {
|
||||||
if (e.key === "Escape") {
|
props.setModal(null);
|
||||||
props.setModal(null);
|
}, [props.setModal])},
|
||||||
e.stopPropagation();
|
], false);
|
||||||
}
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
|
|
||||||
let parseError = "";
|
let parseError = "";
|
||||||
try {
|
try {
|
||||||
|
|
@ -28,7 +24,7 @@ export function TextDialog(props: {setModal: Dispatch<SetStateAction<ReactElemen
|
||||||
parseError = e.message;
|
parseError = e.message;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div onKeyDown={onKeyDown} style={{padding: 4}}>
|
return <div style={{padding: 4}}>
|
||||||
{/* Text label:<br/> */}
|
{/* Text label:<br/> */}
|
||||||
<textarea autoFocus style={{fontFamily: 'Roboto', width: 400, height: 60}} onChange={e=>setText(e.target.value)} value={text} onFocus={e => e.target.select()}/>
|
<textarea autoFocus style={{fontFamily: 'Roboto', width: 400, height: 60}} onChange={e=>setText(e.target.value)} value={text} onFocus={e => e.target.select()}/>
|
||||||
<br/>
|
<br/>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { usePersistentState } from "../../hooks/usePersistentState";
|
||||||
import { ConcreteState, stateDescription, Transition, UnstableState } from "../../statecharts/abstract_syntax";
|
import { ConcreteState, stateDescription, Transition, UnstableState } from "../../statecharts/abstract_syntax";
|
||||||
import { Action, EventTrigger, Expression } from "../../statecharts/label_ast";
|
import { Action, EventTrigger, Expression } from "../../statecharts/label_ast";
|
||||||
import { KeyInfoHidden, KeyInfoVisible } from "../TopPanel/KeyInfo";
|
import { KeyInfoHidden, KeyInfoVisible } from "../TopPanel/KeyInfo";
|
||||||
|
import { useShortcuts } from '@/hooks/useShortcuts';
|
||||||
|
|
||||||
export function ShowTransition(props: {transition: Transition}) {
|
export function ShowTransition(props: {transition: Transition}) {
|
||||||
return <>➝ {stateDescription(props.transition.tgt)}</>;
|
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}) => {
|
const raiseHandlers = inputEvents.map(({event}) => {
|
||||||
return () => {
|
return () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|
@ -67,23 +68,16 @@ export function ShowInputEvents({inputEvents, onRaise, disabled, showKeys}: {inp
|
||||||
onRaise(event, paramParsed);
|
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;
|
const shortcutSpec = raiseHandlers.map((handler, i) => {
|
||||||
if (raiseHandlers[n] !== undefined) {
|
const n = (i+1)%10;
|
||||||
raiseHandlers[n]();
|
return {
|
||||||
e.stopPropagation();
|
keys: [n.toString()],
|
||||||
e.preventDefault();
|
action: handler,
|
||||||
}
|
};
|
||||||
}
|
});
|
||||||
useEffect(() => {
|
useShortcuts(shortcutSpec);
|
||||||
window.addEventListener("keydown", onKeyDown);
|
|
||||||
return () => window.removeEventListener("keydown", onKeyDown);
|
|
||||||
}, [raiseHandlers]);
|
|
||||||
// const KeyInfo = showKeys ? KeyInfoVisible : KeyInfoHidden;
|
|
||||||
const KeyInfo = KeyInfoVisible; // always show keyboard shortcuts on input events, we can't expect the user to remember them
|
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", {});
|
const [inputParams, setInputParams] = usePersistentState<{[eventName:string]: string}>("inputParams", {});
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { Arrow, Diamond, Rountangle, Text, History } from "@/statecharts/concret
|
||||||
import { ClipboardEvent, Dispatch, SetStateAction, useCallback, useEffect } from "react";
|
import { ClipboardEvent, Dispatch, SetStateAction, useCallback, useEffect } from "react";
|
||||||
import { Selection, VisualEditorState } from "../VisualEditor";
|
import { Selection, VisualEditorState } from "../VisualEditor";
|
||||||
import { addV2D } from "@/util/geometry";
|
import { addV2D } from "@/util/geometry";
|
||||||
|
import { useShortcuts } from "@/hooks/useShortcuts";
|
||||||
|
|
||||||
// const offset = {x: 40, y: 40};
|
// const offset = {x: 40, y: 40};
|
||||||
const offset = {x: 0, y: 0};
|
const offset = {x: 0, y: 0};
|
||||||
|
|
@ -12,6 +13,7 @@ export function useCopyPaste(makeCheckPoint: () => void, state: VisualEditorStat
|
||||||
if (data) {
|
if (data) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(data);
|
const parsed = JSON.parse(data);
|
||||||
|
makeCheckPoint();
|
||||||
setState(state => {
|
setState(state => {
|
||||||
try {
|
try {
|
||||||
let nextID = state.nextID;
|
let nextID = state.nextID;
|
||||||
|
|
@ -49,7 +51,6 @@ export function useCopyPaste(makeCheckPoint: () => void, state: VisualEditorStat
|
||||||
...copiedTexts.map(t => ({uid: t.uid, parts: ["text"]})),
|
...copiedTexts.map(t => ({uid: t.uid, parts: ["text"]})),
|
||||||
...copiedHistories.map(h => ({uid: h.uid, parts: ["history"]})),
|
...copiedHistories.map(h => ({uid: h.uid, parts: ["history"]})),
|
||||||
];
|
];
|
||||||
makeCheckPoint();
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
rountangles: [...state.rountangles, ...copiedRountangles],
|
rountangles: [...state.rountangles, ...copiedRountangles],
|
||||||
|
|
@ -72,7 +73,7 @@ export function useCopyPaste(makeCheckPoint: () => void, state: VisualEditorStat
|
||||||
}
|
}
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
}, [setState]);
|
}, [makeCheckPoint, setState]);
|
||||||
|
|
||||||
const copyInternal = useCallback((state: VisualEditorState, selection: Selection, e: ClipboardEvent) => {
|
const copyInternal = useCallback((state: VisualEditorState, selection: Selection, e: ClipboardEvent) => {
|
||||||
const uidsToCopy = new Set(selection.map(shape => shape.uid));
|
const uidsToCopy = new Set(selection.map(shape => shape.uid));
|
||||||
|
|
@ -106,6 +107,7 @@ export function useCopyPaste(makeCheckPoint: () => void, state: VisualEditorStat
|
||||||
}, [state, selection]);
|
}, [state, selection]);
|
||||||
|
|
||||||
const deleteSelection = useCallback(() => {
|
const deleteSelection = useCallback(() => {
|
||||||
|
makeCheckPoint();
|
||||||
setState(state => ({
|
setState(state => ({
|
||||||
...state,
|
...state,
|
||||||
rountangles: state.rountangles.filter(r => !state.selection.some(rs => rs.uid === r.uid)),
|
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)),
|
texts: state.texts.filter(t => !state.selection.some(ts => ts.uid === t.uid)),
|
||||||
selection: [],
|
selection: [],
|
||||||
}));
|
}));
|
||||||
}, [setState]);
|
}, [makeCheckPoint, setState]);
|
||||||
|
|
||||||
const onKeyDown = (e: KeyboardEvent) => {
|
useShortcuts([
|
||||||
// @ts-ignore
|
{keys: ["Delete"], action: deleteSelection},
|
||||||
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);
|
|
||||||
})
|
|
||||||
|
|
||||||
return {onCopy, onPaste, onCut, deleteSelection};
|
return {onCopy, onPaste, onCut, deleteSelection};
|
||||||
}
|
}
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
import { useEffect } from "react";
|
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) {
|
for (const {keys, action} of spec) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (e: KeyboardEvent) => {
|
const handler = (e: KeyboardEvent) => {
|
||||||
// @ts-ignore: don't steal keyboard events while the user is typing in a text box, etc.
|
if (ignoreInputs) {
|
||||||
if (["INPUT", "TEXTAREA", "SELECT"].includes(e.target?.tagName)) return;
|
// @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.ctrlKey !== keys.includes("Ctrl")) return;
|
||||||
if (e.shiftKey !== keys.includes("Shift")) return;
|
if (e.shiftKey !== keys.includes("Shift")) return;
|
||||||
|
|
|
||||||
7
todo.txt
7
todo.txt
|
|
@ -31,7 +31,8 @@
|
||||||
TODO
|
TODO
|
||||||
|
|
||||||
- bugs
|
- 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:
|
- maybe support:
|
||||||
- explicit order of:
|
- explicit order of:
|
||||||
|
|
@ -64,6 +65,10 @@ TODO
|
||||||
- show insert mode also next to cursor
|
- show insert mode also next to cursor
|
||||||
- plot plant signals
|
- 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:
|
- performance:
|
||||||
maybe try this for rendering the execution trace:
|
maybe try this for rendering the execution trace:
|
||||||
https://legacy.reactjs.org/docs/optimizing-performance.html#virtualize-long-lists
|
https://legacy.reactjs.org/docs/optimizing-performance.html#virtualize-long-lists
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue