better find/replace (with highlighting of all occurrences) + more consistent error visualization
This commit is contained in:
parent
64aab1a6df
commit
8b0726ef01
21 changed files with 244 additions and 139 deletions
|
|
@ -47,7 +47,8 @@ details:has(+ details) {
|
|||
}
|
||||
|
||||
.toolbar input {
|
||||
height: 22px;
|
||||
height: 26px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.toolbar div {
|
||||
vertical-align: bottom;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { emptyState } from "@/statecharts/concrete_syntax";
|
|||
import { ModalOverlay } from "./Overlays/ModalOverlay";
|
||||
import { FindReplace } from "./BottomPanel/FindReplace";
|
||||
import { useCustomMemo } from "@/hooks/useCustomMemo";
|
||||
import { usePersistentState } from "@/hooks/usePersistentState";
|
||||
|
||||
export type EditHistory = {
|
||||
current: VisualEditorState,
|
||||
|
|
@ -51,6 +52,8 @@ export function App() {
|
|||
const [editHistory, setEditHistory] = useState<EditHistory|null>(null);
|
||||
const [modal, setModal] = useState<ReactElement|null>(null);
|
||||
|
||||
const [[findText, replaceText], setFindReplaceText] = usePersistentState("findReplaceTxt", ["", ""]);
|
||||
|
||||
const {commitState, replaceState, onRedo, onUndo, onRotate} = useEditor(setEditHistory);
|
||||
|
||||
const editorState = editHistory && editHistory.current;
|
||||
|
|
@ -67,9 +70,17 @@ export function App() {
|
|||
([prevState, prevConns], [nextState, nextConns]) => {
|
||||
if ((prevState === null) !== (nextState === null)) return false;
|
||||
if ((prevConns === null) !== (nextConns === null)) return false;
|
||||
if (prevConns === null) {
|
||||
return nextConns === null;
|
||||
}
|
||||
if (prevState === null) {
|
||||
return nextState === null;
|
||||
}
|
||||
if (nextConns === null) return false;
|
||||
if (nextState === null) return false;
|
||||
// the following check is much cheaper than re-rendering everything that depends on
|
||||
return connectionsEqual(prevConns!, nextConns!)
|
||||
&& reducedConcreteSyntaxEqual(prevState!, nextState!);
|
||||
return connectionsEqual(prevConns, nextConns)
|
||||
&& reducedConcreteSyntaxEqual(prevState, nextState);
|
||||
});
|
||||
const ast = parsed && parsed[0];
|
||||
|
||||
|
|
@ -161,7 +172,7 @@ export function App() {
|
|||
<div className="stackHorizontal" style={{flexGrow:1, overflow: "auto"}}>
|
||||
|
||||
{/* top-to-bottom: top bar, editor */}
|
||||
<div className="stackVertical" style={{flexGrow:1, overflow: "auto"}}>
|
||||
<div className="stackVertical" style={{flexGrow:1, overflow: "hidden"}}>
|
||||
{/* Top bar */}
|
||||
<div
|
||||
className="shadowBelow"
|
||||
|
|
@ -174,12 +185,12 @@ export function App() {
|
|||
{/* Editor */}
|
||||
<div style={{flexGrow: 1, overflow: "auto"}}>
|
||||
{editorState && conns && syntaxErrors &&
|
||||
<VisualEditor {...{state: editorState, commitState, replaceState, conns, syntaxErrors: allErrors, highlightActive, highlightTransitions, setModal, ...appState}}/>}
|
||||
<VisualEditor {...{state: editorState, commitState, replaceState, conns, syntaxErrors: allErrors, highlightActive, highlightTransitions, setModal, ...appState, findText:findText}}/>}
|
||||
</div>
|
||||
|
||||
{appState.showFindReplace &&
|
||||
<div>
|
||||
<FindReplace setCS={setEditorState} hide={() => setters.setShowFindReplace(false)}/>
|
||||
{editorState && appState.showFindReplace &&
|
||||
<div style={{}}>
|
||||
<FindReplace findText={findText} replaceText={replaceText} setFindReplaceText={setFindReplaceText} cs={editorState} setCS={setEditorState} hide={() => setters.setShowFindReplace(false)}/>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,27 @@
|
|||
.errorStatus {
|
||||
/* background-color: rgb(230,0,0); */
|
||||
background-color: var(--error-color);
|
||||
color: var(--background-color);
|
||||
.statusBar {
|
||||
background-color: var(--statusbar-bg-color);
|
||||
color: var(--statusbar-fg-color);
|
||||
}
|
||||
|
||||
.statusBar.error {
|
||||
background-color: var(--statusbar-error-bg-color);
|
||||
color: var(--statusbar-error-fg-color);
|
||||
}
|
||||
|
||||
.statusBar summary:hover {
|
||||
background-color: color-mix(in srgb, var(--statusbar-bg-color) 60%, var(--background-color));
|
||||
}
|
||||
.statusBar.error summary:hover {
|
||||
background-color: color-mix(in srgb, var(--statusbar-error-bg-color) 70%, var(--text-color));
|
||||
}
|
||||
|
||||
.statusBar a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.greeter {
|
||||
/* border-top: 1px var(--separator-color) solid; */
|
||||
background-color: var(--greeter-bg-color);
|
||||
background-color: var(--statusbar-bg-color);
|
||||
}
|
||||
|
||||
.bottom {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import "./BottomPanel.css";
|
|||
import { PersistentDetailsLocalStorage } from "../Components/PersistentDetails";
|
||||
import { Logo } from "@/App/Logo/Logo";
|
||||
import { AppState } from "../App";
|
||||
import { FindReplace } from "./FindReplace";
|
||||
import { VisualEditorState } from "../VisualEditor/VisualEditor";
|
||||
import { Setters } from "../makePartialSetter";
|
||||
|
||||
|
|
@ -16,7 +15,8 @@ export function BottomPanel(props: {errors: TraceableError[], setEditorState: Di
|
|||
const [greeting, setGreeting] = useState(
|
||||
<div className="greeter" style={{textAlign:'center'}}>
|
||||
<span style={{fontSize: 18, fontStyle: 'italic'}}>
|
||||
Welcome to <Logo/>
|
||||
Welcome to
|
||||
<Logo width={300} height="auto" style={{verticalAlign: 'middle'}}/>
|
||||
</span>
|
||||
</div>);
|
||||
|
||||
|
|
@ -26,23 +26,38 @@ export function BottomPanel(props: {errors: TraceableError[], setEditorState: Di
|
|||
}, 2000);
|
||||
}, []);
|
||||
|
||||
return <div className="toolbar bottom">
|
||||
return <div className="bottom">
|
||||
{greeting}
|
||||
{/* {props.showFindReplace &&
|
||||
<div>
|
||||
<FindReplace setCS={props.setEditorState} hide={() => props.setShowFindReplace(false)}/>
|
||||
</div>
|
||||
} */}
|
||||
<div className={"statusBar" + props.errors.length ? " error" : ""}>
|
||||
<div className={"stackHorizontal statusBar" + (props.errors.length ? " error" : "")}>
|
||||
<div style={{flexGrow:1}}>
|
||||
<PersistentDetailsLocalStorage initiallyOpen={false} localStorageKey="errorsExpanded">
|
||||
<summary>{props.errors.length} errors</summary>
|
||||
<div style={{maxHeight: '25vh', overflow: 'auto'}}>
|
||||
{props.errors.map(({message, shapeUid})=>
|
||||
<div>
|
||||
{shapeUid}: {message}
|
||||
</div>)}
|
||||
</div>
|
||||
</PersistentDetailsLocalStorage>
|
||||
<summary>{props.errors.length} errors</summary>
|
||||
<div style={{maxHeight: '20vh', overflow: 'auto'}}>
|
||||
{props.errors.map(({message, shapeUid})=>
|
||||
<div>
|
||||
{shapeUid}: {message}
|
||||
</div>)}
|
||||
</div>
|
||||
</PersistentDetailsLocalStorage>
|
||||
</div>
|
||||
{/* <div ></div> */}
|
||||
<div>
|
||||
switch to
|
||||
{location.host === "localhost:3000" ?
|
||||
<a href={`https://deemz.org/public/statebuddy/${location.hash}`}>production</a>
|
||||
: <a href={`http://localhost:3000/${location.hash}`}>development</a>
|
||||
}
|
||||
mode
|
||||
</div>
|
||||
|
|
||||
<div>
|
||||
Rev: <a title={"git"} href={`https://deemz.org/git/research/statebuddy/commit/${gitRev}`}>{gitRev.slice(0,8)}</a>
|
||||
</div>
|
||||
</div>
|
||||
{greeting}
|
||||
</div>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,48 +1,82 @@
|
|||
import { Dispatch, useCallback, useEffect } from "react";
|
||||
import { Dispatch, FormEvent, SetStateAction, useCallback } 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';
|
||||
import { useShortcuts } from "@/hooks/useShortcuts";
|
||||
import SwapVertIcon from '@mui/icons-material/SwapVert';
|
||||
|
||||
type FindReplaceProps = {
|
||||
findText: string,
|
||||
replaceText: string,
|
||||
setFindReplaceText: Dispatch<SetStateAction<[string, string]>>,
|
||||
cs: VisualEditorState,
|
||||
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", "");
|
||||
|
||||
export function FindReplace({findText, replaceText, setFindReplaceText, cs, setCS, hide}: FindReplaceProps) {
|
||||
const onReplace = useCallback(() => {
|
||||
setCS(cs => {
|
||||
return {
|
||||
...cs,
|
||||
texts: cs.texts.map(txt => ({
|
||||
...txt,
|
||||
text: txt.text.replaceAll(findTxt, replaceTxt)
|
||||
text: txt.text.replaceAll(findText, replaceText)
|
||||
})),
|
||||
};
|
||||
});
|
||||
}, [findTxt, replaceTxt]);
|
||||
|
||||
useShortcuts([
|
||||
{keys: ["Enter"], action: onReplace},
|
||||
])
|
||||
}, [findText, replaceText, setCS]);
|
||||
|
||||
const onSwap = useCallback(() => {
|
||||
setReplaceTxt(findTxt);
|
||||
setFindText(replaceTxt);
|
||||
}, [findTxt, replaceTxt]);
|
||||
setFindReplaceText(([findText, replaceText]) => [replaceText, findText]);
|
||||
}, [findText, replaceText]);
|
||||
|
||||
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>;
|
||||
const onSubmit = useCallback((e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onReplace();
|
||||
// onSwap();
|
||||
}, [findText, replaceText, onSwap, onReplace]);
|
||||
|
||||
const n = findText === "" ? 0 : cs.texts.reduce((count, txt) => count+(txt.text.indexOf(findText) !== -1 ? 1: 0), 0);
|
||||
|
||||
return <form onSubmit={onSubmit}>
|
||||
<div className="toolbar toolbarGroup" style={{display: 'flex', flexDirection: 'row'}}>
|
||||
<div style={{flexGrow:1, display: 'flex', flexDirection: 'column'}}>
|
||||
<input placeholder="find"
|
||||
title="old text"
|
||||
value={findText}
|
||||
onChange={e => setFindReplaceText(([_, replaceText]) => [e.target.value, replaceText])}
|
||||
style={{flexGrow: 1, minWidth: 20}}/>
|
||||
<br/>
|
||||
<input tabIndex={0} placeholder="replace"
|
||||
title="new text"
|
||||
value={replaceText}
|
||||
onChange={(e => setFindReplaceText(([findText, _]) => [findText, e.target.value]))}
|
||||
style={{flexGrow: 1, minWidth: 20}}/>
|
||||
</div>
|
||||
<div style={{flex: '0 0 content', display: 'flex', justifyItems: 'flex-start', flexDirection: 'column'}}>
|
||||
<div style={{display: 'flex'}}>
|
||||
<button
|
||||
type="button" // <-- prevent form submission on click
|
||||
title="swap find/replace fields"
|
||||
onClick={onSwap}
|
||||
style={{flexGrow: 1}}>
|
||||
<SwapVertIcon fontSize="small"/>
|
||||
</button>
|
||||
<button
|
||||
type="button" // <-- prevent form submission on click
|
||||
title="hide find & replace"
|
||||
onClick={hide}
|
||||
style={{flexGrow: 1 }}>
|
||||
<CloseIcon fontSize="small"/>
|
||||
</button>
|
||||
</div>
|
||||
<input type="submit"
|
||||
disabled={n===0}
|
||||
title="replace all occurrences in model"
|
||||
value={`replace all (${n})`}
|
||||
style={{height: 26}}/>
|
||||
</div>
|
||||
</div>
|
||||
</form>;
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
import { SVGAttributes, SVGProps } from "react";
|
||||
|
||||
// i couldn't find a better way to make the text in the logo adapt to light/dark mode...
|
||||
export function Logo() {
|
||||
return <svg style={{maxWidth: '100%'}} width="424.29" height="105.72" version="1.1" viewBox="0 0 424.29 105.72">
|
||||
export function Logo(props: SVGAttributes<SVGElement>) {
|
||||
return <svg style={{maxWidth: '100%'}} width="424.29" height="105.72" version="1.1" viewBox="0 0 424.29 105.72" {...props} >
|
||||
<style>{`
|
||||
.logoText {
|
||||
fill: var(--text-color);
|
||||
|
|
|
|||
|
|
@ -70,14 +70,15 @@ export const ShowInputEvents = memo(function ShowInputEvents({inputEvents, onRai
|
|||
};
|
||||
});
|
||||
|
||||
const shortcutSpec = raiseHandlers.map((handler, i) => {
|
||||
const n = (i+1)%10;
|
||||
// less painful and more readable than figuring out the equivalent of range(n) in JS:
|
||||
// (btw, useShortcuts must always be called with an array of the same size)
|
||||
useShortcuts([0,1,2,3,4,5,6,7,8,9].map(i => {
|
||||
const n = (i+1) % 10;
|
||||
return {
|
||||
keys: [n.toString()],
|
||||
action: handler,
|
||||
action: raiseHandlers[n] || (() => {}),
|
||||
};
|
||||
});
|
||||
useShortcuts(shortcutSpec);
|
||||
}));
|
||||
|
||||
const KeyInfo = KeyInfoVisible; // always show keyboard shortcuts on input events, we can't expect the user to remember them
|
||||
|
||||
|
|
@ -107,7 +108,6 @@ export const ShowInputEvents = memo(function ShowInputEvents({inputEvents, onRai
|
|||
</div>;
|
||||
})
|
||||
}, (prevProps, nextProps) => {
|
||||
console.log('onRaise changed:', prevProps.onRaise === nextProps.onRaise, prevProps.onRaise, nextProps.onRaise);
|
||||
return prevProps.onRaise === nextProps.onRaise
|
||||
&& prevProps.disabled === nextProps.disabled
|
||||
&& jsonDeepEqual(prevProps.inputEvents, nextProps.inputEvents);
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ export const SideBar = memo(function SideBar({showExecutionTrace, showConnection
|
|||
<button title="see in trace (below)" className={activeProperty === i ? "active" : ""} onClick={() => setActiveProperty(i)}>
|
||||
<VisibilityIcon fontSize="small"/>
|
||||
</button>
|
||||
<input type="text" style={{width:'calc(100% - 90px)'}} value={property} onChange={e => setProperties(properties => properties.toSpliced(i, 1, e.target.value))}/>
|
||||
<input type="text" style={{width:'calc(100% - 90px)'}} value={property} onChange={e => setProperties(properties => properties.toSpliced(i, 1, e.target.value))} placeholder='write MTL property...'/>
|
||||
<button title="delete this property" onClick={() => setProperties(properties => properties.toSpliced(i, 1))}>
|
||||
<DeleteOutlineIcon fontSize="small"/>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -246,11 +246,5 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="toolbarGroup">
|
||||
{location.host === "localhost:3000" ?
|
||||
<a href={`https://deemz.org/public/statebuddy/${location.hash}`}>production</a>
|
||||
: <a href={`http://localhost:3000/${location.hash}`}>development</a>
|
||||
}
|
||||
</div>
|
||||
</div>;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export const ArrowSVG = memo(function(props: { arrow: Arrow; selected: ArrowPart
|
|||
data-parts="start end" />
|
||||
|
||||
{props.error && <text
|
||||
className="error"
|
||||
className="errorHover"
|
||||
x={(start.x + end.x) / 2 + 5}
|
||||
y={(start.y + end.y) / 2}
|
||||
textAnchor="middle"
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export const RountangleSVG = memo(function RountangleSVG(props: {rountangle: Rou
|
|||
<text x={10} y={20} className="uid">{props.rountangle.uid}</text>
|
||||
|
||||
{props.error &&
|
||||
<text className="error" x={10} y={40} data-uid={uid} data-parts="left top right bottom">{props.error}</text>}
|
||||
<text className="errorHover" x={30} y={20} data-uid={uid} data-parts="left top right bottom">{props.error}</text>}
|
||||
|
||||
<RectHelper uid={uid} size={minSize}
|
||||
selected={props.selected}
|
||||
|
|
|
|||
|
|
@ -1,36 +1,46 @@
|
|||
import { TextDialog } from "@/App/Modals/TextDialog";
|
||||
import { TraceableError } from "../../statecharts/parser";
|
||||
import {Text} from "../../statecharts/concrete_syntax";
|
||||
import { Dispatch, memo, ReactElement, SetStateAction } from "react";
|
||||
import { Dispatch, memo, ReactElement, SetStateAction, SVGTextElementAttributes } from "react";
|
||||
import { jsonDeepEqual } from "@/util/util";
|
||||
|
||||
export const TextSVG = memo(function TextSVG(props: {text: Text, error: TraceableError|undefined, selected: boolean, highlight: boolean, onEdit: (text: Text, newText: string) => void, setModal: Dispatch<SetStateAction<ReactElement|null>>}) {
|
||||
const commonProps = {
|
||||
"data-uid": props.text.uid,
|
||||
"data-parts": "text",
|
||||
textAnchor: "middle" as "middle",
|
||||
className: "draggableText"
|
||||
+ (props.selected ? " selected":"")
|
||||
+ (props.highlight ? " highlight":""),
|
||||
style: {whiteSpace: "preserve"},
|
||||
}
|
||||
|
||||
let textNode;
|
||||
if (props.error?.data?.location) {
|
||||
const {start,end} = props.error.data.location;
|
||||
textNode = <><text {...commonProps}>
|
||||
{props.text.text.slice(0, start.offset)}
|
||||
<tspan className="error" data-uid={props.text.uid} data-parts="text">
|
||||
{props.text.text.slice(start.offset, end.offset)}
|
||||
{start.offset === end.offset && <>_</>}
|
||||
export const FragmentedText = function FragmentedText({start, end, text, highlightClassName, uid, parts, ...rest}: {start: number, end: number, text: string, highlightClassName: string, uid: string, parts: string} & SVGTextElementAttributes<SVGTextElement>) {
|
||||
if (start !== -1 && start !== end) {
|
||||
return <text data-uid={uid} data-parts={parts} {...rest}>
|
||||
{text.slice(0, start)}
|
||||
<tspan className={highlightClassName} data-uid={uid} data-parts="text">
|
||||
{text.slice(start, end)}
|
||||
</tspan>
|
||||
{props.text.text.slice(end.offset)}
|
||||
</text>
|
||||
<text className="error errorHover" y={20} textAnchor="middle">{props.error.message}</text></>;
|
||||
{text.slice(end)}
|
||||
</text>;
|
||||
}
|
||||
else {
|
||||
textNode = <text {...commonProps}>{props.text.text}</text>;
|
||||
return <text data-uid={uid} data-parts={parts} {...rest}>
|
||||
{text}
|
||||
</text>
|
||||
}
|
||||
}
|
||||
|
||||
export const TextSVG = memo(function TextSVG(props: {text: Text, error: TraceableError|undefined, selected: boolean, highlight: boolean, onEdit: (text: Text, newText: string) => void, setModal: Dispatch<SetStateAction<ReactElement|null>>, findText: string}) {
|
||||
|
||||
const className = "draggableText"
|
||||
+ (props.selected ? " selected":"")
|
||||
+ (props.highlight ? " highlight":"")
|
||||
+ (props.error ? " error":"");
|
||||
|
||||
const found = props.text.text.indexOf(props.findText);
|
||||
const start = (found >= 0) ? found : -1
|
||||
const end = (found >= 0) ? found + props.findText.length : -1;
|
||||
|
||||
const textNode = <FragmentedText
|
||||
{...{start, end}}
|
||||
text={props.text.text}
|
||||
textAnchor="middle"
|
||||
className={className}
|
||||
highlightClassName="findText"
|
||||
uid={props.text.uid}
|
||||
parts="text"
|
||||
/>;
|
||||
|
||||
return <g
|
||||
key={props.text.uid}
|
||||
|
|
@ -44,6 +54,9 @@ export const TextSVG = memo(function TextSVG(props: {text: Text, error: Traceabl
|
|||
}}>
|
||||
{textNode}
|
||||
<text className="draggableText helper" textAnchor="middle" data-uid={props.text.uid} data-parts="text" style={{whiteSpace: "preserve"}}>{props.text.text}</text>
|
||||
{props.error &&
|
||||
<text className="errorHover" y={-20} textAnchor="middle">{props.error.message}</text>
|
||||
}
|
||||
</g>;
|
||||
}, (prevProps, newProps) => {
|
||||
return jsonDeepEqual(prevProps.text, newProps)
|
||||
|
|
@ -52,4 +65,5 @@ export const TextSVG = memo(function TextSVG(props: {text: Text, error: Traceabl
|
|||
&& prevProps.setModal === newProps.setModal
|
||||
&& prevProps.error === newProps.error
|
||||
&& prevProps.selected === newProps.selected
|
||||
&& prevProps.findText === newProps.findText
|
||||
});
|
||||
|
|
|
|||
|
|
@ -112,10 +112,6 @@ line.selected, circle.selected {
|
|||
stroke-width: 4px;
|
||||
}
|
||||
|
||||
.draggableText.selected, .draggableText.selected:hover {
|
||||
fill: var(--accent-border-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
text.helper {
|
||||
fill: rgba(0,0,0,0);
|
||||
stroke: rgba(0,0,0,0);
|
||||
|
|
@ -126,24 +122,41 @@ text.helper:hover {
|
|||
cursor: grab;
|
||||
}
|
||||
|
||||
.draggableText, .draggableText.highlight {
|
||||
.draggableText {
|
||||
paint-order: stroke;
|
||||
fill: var(--text-color);
|
||||
stroke: var(--background-color);
|
||||
stroke-width: 4px;
|
||||
stroke-linecap: butt;
|
||||
stroke-linejoin: miter;
|
||||
stroke-opacity: 1;
|
||||
fill-opacity:1;
|
||||
text-anchor: middle;
|
||||
white-space: preserve;
|
||||
}
|
||||
|
||||
.draggableText.highlight:not(.selected) {
|
||||
fill: var(--associated-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.draggableText.selected {
|
||||
fill: var(--accent-border-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
.draggableText.highlight:not(.selected) {
|
||||
fill: var(--associated-color);
|
||||
}
|
||||
.draggableText.error:not(.selected) {
|
||||
fill: var(--error-color);
|
||||
}
|
||||
|
||||
text.errorHover {
|
||||
display: none;
|
||||
paint-order: stroke;
|
||||
fill: var(--error-color);
|
||||
stroke: var(--background-color);
|
||||
stroke-width: 4px;
|
||||
font-weight: 600;
|
||||
white-space: preserve;
|
||||
}
|
||||
g:hover > text.errorHover {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
tspan.findText {
|
||||
fill: var(--foundtext-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.highlight:not(.selected):not(text) {
|
||||
|
|
@ -152,7 +165,7 @@ text.helper:hover {
|
|||
fill: none;
|
||||
}
|
||||
|
||||
.arrow.error {
|
||||
.arrow.error:not(.selected) {
|
||||
stroke: var(--error-color);
|
||||
}
|
||||
.arrow.fired {
|
||||
|
|
@ -172,19 +185,13 @@ text.helper:hover {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
text.error, tspan.error {
|
||||
fill: var(--error-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.errorHover {
|
||||
/* .errorHover {
|
||||
display: none;
|
||||
}
|
||||
|
||||
g:hover > .errorHover {
|
||||
display: inline;
|
||||
}
|
||||
} */
|
||||
|
||||
text.uid {
|
||||
fill: var(--separator-color);
|
||||
|
|
|
|||
|
|
@ -52,9 +52,10 @@ type VisualEditorProps = {
|
|||
highlightTransitions: string[],
|
||||
setModal: Dispatch<SetStateAction<ReactElement|null>>,
|
||||
zoom: number;
|
||||
findText: string;
|
||||
};
|
||||
|
||||
export const VisualEditor = memo(function VisualEditor({state, commitState, replaceState, conns, syntaxErrors: errors, insertMode, highlightActive, highlightTransitions, setModal, zoom}: VisualEditorProps) {
|
||||
export const VisualEditor = memo(function VisualEditor({state, commitState, replaceState, conns, syntaxErrors: errors, insertMode, highlightActive, highlightTransitions, setModal, zoom, findText}: VisualEditorProps) {
|
||||
|
||||
// While dragging, the editor is in a temporary state (a state that is not committed to the edit history). If the temporary state is not null, then this state will be what you see.
|
||||
// const [temporaryState, setTemporaryState] = useState<VisualEditorState | null>(null);
|
||||
|
|
@ -199,8 +200,6 @@ export const VisualEditor = memo(function VisualEditor({state, commitState, repl
|
|||
</marker>
|
||||
</defs>
|
||||
|
||||
{(rootErrors.length>0) && <text className="error" x={5} y={20}>{rootErrors.join(' ')}</text>}
|
||||
|
||||
<Rountangles rountangles={state.rountangles} {...{selection, sidesToHighlight, rountanglesToHighlight, errors, highlightActive}}/>
|
||||
<Diamonds diamonds={state.diamonds} {...{selection, sidesToHighlight, rountanglesToHighlight, errors}}/>
|
||||
|
||||
|
|
@ -235,7 +234,9 @@ export const VisualEditor = memo(function VisualEditor({state, commitState, repl
|
|||
}
|
||||
)}
|
||||
|
||||
<Texts texts={state.texts} {...{selection, textsToHighlight, errors, onEditText, setModal}}/>
|
||||
<Texts texts={state.texts} {...{selection, textsToHighlight, errors, onEditText, setModal, findText}}/>
|
||||
|
||||
{(rootErrors.length>0) && <text className="errorHover" x={5} y={20} style={{display:'inline'}}>{rootErrors.join('\n')}</text>}
|
||||
|
||||
{selectionRect}
|
||||
</svg>;
|
||||
|
|
@ -282,7 +283,7 @@ const Diamonds = memo(function Diamonds({diamonds, selection, sidesToHighlight,
|
|||
&& arraysEqual(p.errors, n.errors);
|
||||
});
|
||||
|
||||
const Texts = memo(function Texts({texts, selection, textsToHighlight, errors, onEditText, setModal}: {texts: Text[], selection: Selection, textsToHighlight: {[key: string]: boolean}, errors: TraceableError[], onEditText: (text: Text, newText: string) => void, setModal: Dispatch<SetStateAction<ReactElement|null>>}) {
|
||||
const Texts = memo(function Texts({texts, selection, textsToHighlight, errors, onEditText, setModal, findText}: {texts: Text[], selection: Selection, textsToHighlight: {[key: string]: boolean}, errors: TraceableError[], onEditText: (text: Text, newText: string) => void, setModal: Dispatch<SetStateAction<ReactElement|null>>, findText: string}) {
|
||||
return <>{texts.map(txt => {
|
||||
return <TextSVG
|
||||
key={txt.uid}
|
||||
|
|
@ -292,6 +293,7 @@ const Texts = memo(function Texts({texts, selection, textsToHighlight, errors, o
|
|||
highlight={textsToHighlight.hasOwnProperty(txt.uid)}
|
||||
onEdit={onEditText}
|
||||
setModal={setModal}
|
||||
findText={findText}
|
||||
/>
|
||||
})}</>;
|
||||
}, (p, n) => {
|
||||
|
|
@ -300,6 +302,7 @@ const Texts = memo(function Texts({texts, selection, textsToHighlight, errors, o
|
|||
&& objectsEqual(p.textsToHighlight, n.textsToHighlight)
|
||||
&& arraysEqual(p.errors, n.errors)
|
||||
&& p.onEditText === n.onEditText
|
||||
&& p.setModal === n.setModal;
|
||||
&& p.setModal === n.setModal
|
||||
&& p.findText === n.findText;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -239,7 +239,7 @@ export function useMouse(
|
|||
...t,
|
||||
topLeft: addV2D(t.topLeft, pointerDelta),
|
||||
}
|
||||
}),
|
||||
}).toSorted((a,b) => a.topLeft.y - b.topLeft.y),
|
||||
}));
|
||||
setDragging(true);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -138,8 +138,6 @@ export function useSimulator(ast: Statechart|null, plant: Plant<any, UniversalPl
|
|||
};
|
||||
}, [cE, currentTraceItem, time, appendNewConfig]);
|
||||
|
||||
console.log({onRaise});
|
||||
|
||||
// timer elapse events are triggered by a change of the simulated time (possibly as a scheduled JS event loop timeout)
|
||||
useEffect(() => {
|
||||
// console.log('time effect:', time, currentTraceItem);
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ import { App } from "./App/App";
|
|||
|
||||
const elem = document.getElementById("root")!;
|
||||
const app = (
|
||||
// <StrictMode>
|
||||
<StrictMode>
|
||||
<App />
|
||||
// </StrictMode>
|
||||
</StrictMode>
|
||||
);
|
||||
|
||||
if (import.meta.hot) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Dispatch, SetStateAction, useCallback, useState } from "react";
|
|||
// like useState, but it is persisted in localStorage
|
||||
// important: values must be JSON-(de-)serializable
|
||||
export function usePersistentState<T>(key: string, initial: T): [T, Dispatch<SetStateAction<T>>] {
|
||||
console.log('usePersistentState ---', key);
|
||||
const [state, setState] = useState(() => {
|
||||
const recovered = localStorage.getItem(key);
|
||||
let parsed;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,12 @@ html, body {
|
|||
--light-accent-color: light-dark(rgba(0,0,255,0.2), rgba(78, 186, 248, 0.377));
|
||||
--accent-border-color: light-dark(blue, rgb(64, 185, 255));
|
||||
--accent-opaque-color: light-dark(#ccccff, #305b73);
|
||||
|
||||
--statusbar-bg-color: light-dark(rgb(157, 200, 255), rgb(48, 48, 68));
|
||||
--statusbar-fg-color: light-dark(rgb(0, 0, 0), white);
|
||||
--statusbar-error-bg-color: light-dark(rgb(163, 0, 0), rgb(255, 82, 82));
|
||||
--statusbar-error-fg-color: light-dark(white, black);
|
||||
|
||||
--separator-color: light-dark(lightgrey, rgb(58, 58, 58));
|
||||
--inactive-bg-color: light-dark(#f7f7f7, rgb(29, 29, 29));
|
||||
--inactive-fg-color: light-dark(grey, rgb(70, 70, 70));
|
||||
|
|
@ -40,6 +46,7 @@ html, body {
|
|||
--input-event-hover-bg-color: light-dark(rgb(195, 224, 176), rgb(59, 88, 40));
|
||||
--input-event-active-bg-color: light-dark(rgb(176, 204, 158), rgb(77, 117, 53));
|
||||
--output-event-bg-color: light-dark(rgb(230, 249, 255), rgb(28, 83, 104));
|
||||
--foundtext-color: light-dark(rgb(0, 189, 164), rgb(202, 149, 218));
|
||||
|
||||
background-color: var(--background-color);
|
||||
color: var(--text-color);
|
||||
|
|
@ -50,16 +57,16 @@ input {
|
|||
border: 1px solid var(--separator-color);
|
||||
}
|
||||
|
||||
button {
|
||||
button, input[type="submit"] {
|
||||
background-color: var(--button-bg-color);
|
||||
border: 1px var(--separator-color) solid;
|
||||
}
|
||||
|
||||
button:not(:disabled):hover {
|
||||
button:not(:disabled):hover, input[type="submit"]:not(:disabled):hover {
|
||||
background-color: var(--light-accent-color);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
button:disabled, input[type="submit"]:disabled {
|
||||
background-color: var(--inactive-bg-color);
|
||||
color: var(--inactive-fg-color);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ export function parseStatechart(concreteSyntax: ReducedConcreteSyntax, conns: Co
|
|||
else if (state.initial.length === 0) {
|
||||
errors.push({
|
||||
shapeUid: state.uid,
|
||||
message: "no initial state",
|
||||
message: "needs initial state",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -216,8 +216,8 @@ export function parseStatechart(concreteSyntax: ReducedConcreteSyntax, conns: Co
|
|||
|
||||
// step 3: figure out labels
|
||||
|
||||
const textsSorted = concreteSyntax.texts.toSorted((a,b) => a.topLeft.y - b.topLeft.y);
|
||||
for (const text of textsSorted) {
|
||||
// ASSUMPTION: text is sorted by y-coordinate
|
||||
for (const text of concreteSyntax.texts) {
|
||||
let parsed: ParsedText;
|
||||
try {
|
||||
parsed = cachedParseLabel(text.text); // may throw
|
||||
|
|
@ -226,7 +226,7 @@ export function parseStatechart(concreteSyntax: ReducedConcreteSyntax, conns: Co
|
|||
if (e instanceof SyntaxError) {
|
||||
errors.push({
|
||||
shapeUid: text.uid,
|
||||
message: e.message,
|
||||
message: 'parser: ' + e.message,
|
||||
data: e,
|
||||
});
|
||||
parsed = {
|
||||
|
|
@ -248,7 +248,7 @@ export function parseStatechart(concreteSyntax: ReducedConcreteSyntax, conns: Co
|
|||
// triggers
|
||||
if (parsed.trigger.kind === "event") {
|
||||
if (src.kind === "pseudo") {
|
||||
errors.push({shapeUid: text.uid, message: "pseudo state outgoing transition must not have event trigger"});
|
||||
errors.push({shapeUid: text.uid, message: "cannot have trigger"});
|
||||
}
|
||||
else {
|
||||
const {event} = parsed.trigger;
|
||||
|
|
@ -262,7 +262,7 @@ export function parseStatechart(concreteSyntax: ReducedConcreteSyntax, conns: Co
|
|||
}
|
||||
else if (parsed.trigger.kind === "after") {
|
||||
if (src.kind === "pseudo") {
|
||||
errors.push({shapeUid: text.uid, message: "pseudo state outgoing transition must not have after-trigger"});
|
||||
errors.push({shapeUid: text.uid, message: "cannot have trigger"});
|
||||
}
|
||||
else {
|
||||
src.timers.push(parsed.trigger.durationMs);
|
||||
|
|
@ -274,7 +274,7 @@ export function parseStatechart(concreteSyntax: ReducedConcreteSyntax, conns: Co
|
|||
}
|
||||
else if (parsed.trigger.kind === "triggerless") {
|
||||
if (src.kind !== "pseudo") {
|
||||
errors.push({shapeUid: text.uid, message: "triggerless transitions only allowed on pseudo-states"});
|
||||
errors.push({shapeUid: text.uid, message: "needs trigger"});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -296,7 +296,7 @@ export function parseStatechart(concreteSyntax: ReducedConcreteSyntax, conns: Co
|
|||
else {
|
||||
errors.push({
|
||||
shapeUid: text.uid,
|
||||
message: "states can only have entry/exit triggers",
|
||||
message: "must belong to transition",
|
||||
data: {start: {offset: 0}, end: {offset: text.text.length}},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
3
todo.txt
3
todo.txt
|
|
@ -79,6 +79,9 @@ TODO
|
|||
https://pub.dev/packages/ploeg_tree_layout
|
||||
- local variable scopes
|
||||
|
||||
Breaking changes (after mosis25 is finished):
|
||||
- state names start with # (not with //)
|
||||
|
||||
Publish StateBuddy paper(s):
|
||||
compare CS approach to other tools, not only YAKINDU
|
||||
z
|
||||
Loading…
Add table
Add a link
Reference in a new issue