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 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 <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>
|
||||
|
|
|
|||
|
|
@ -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<SetStateAction<ReactElement|null>>, 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<SetStateAction<ReactElemen
|
|||
parseError = e.message;
|
||||
}
|
||||
|
||||
return <div onKeyDown={onKeyDown} style={{padding: 4}}>
|
||||
return <div style={{padding: 4}}>
|
||||
{/* 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()}/>
|
||||
<br/>
|
||||
|
|
@ -39,4 +35,4 @@ export function TextDialog(props: {setModal: Dispatch<SetStateAction<ReactElemen
|
|||
{/* <br/> */}
|
||||
{/* (Tip: <kbd>Shift</kbd>+<kbd>Enter</kbd> to insert newline.) */}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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", {});
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
7
todo.txt
7
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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue