From dbc56553b3f016379bc44890abcfd39243b8f9f5 Mon Sep 17 00:00:00 2001 From: cloudiees <73859729+cloudiees@users.noreply.github.com> Date: Sat, 20 Sep 2025 21:48:09 -0500 Subject: [PATCH] Added keybinds/shortcuts --- src/components/Dock.tsx | 58 +++++++++++++++++++ src/components/Editor.tsx | 113 ++++++++++++++++++++++---------------- 2 files changed, 123 insertions(+), 48 deletions(-) diff --git a/src/components/Dock.tsx b/src/components/Dock.tsx index e3a6e69..8ec14bf 100644 --- a/src/components/Dock.tsx +++ b/src/components/Dock.tsx @@ -17,6 +17,7 @@ import { saveFSMAtom, } from "../lib/backend"; import { useAtomValue, useSetAtom } from "jotai"; +import { useEffect } from "react"; const Dock = () => { const DockIconSize = 24; @@ -35,6 +36,63 @@ const Dock = () => { const setSaveFSM = useSetAtom(saveFSMAtom); const saveFSM = useAtomValue(saveFSMAtom); + // Keybind Handling + useEffect(() => { + // Setting keybind for saving fsm + const ctrls = (e: KeyboardEvent) => (e.ctrlKey || e.metaKey) && e.key == "s"; + + // Prevents browser from doing default command for whatever the keybind above is + const ignore = (e: KeyboardEvent) => { + if (ctrls(e)) { + e.preventDefault(); + } + } + + // Put this in a different handler because this only works if ignore is on keydown and this command is on keyup, and quite frankly keyup feels terrible to use for the other keybinds + const handleSave = (e: KeyboardEvent) => { + if (ctrls(e)) setSaveFSM(true); + } + + // Assigning keybinds to the tools + const handleKeybinds = (e: KeyboardEvent) => { + if (e.key == "1") { + if (currentState == "grab") setCurrentState("nil"); + else setCurrentState("grab"); + } + else if (e.key == "2") { + if (currentState == "select") setCurrentState("nil"); + else setCurrentState("select"); + } + else if (e.key == "3") { + if (currentState == "create") setCurrentState("nil"); + else setCurrentState("create"); + + } + else if (e.key == "4") { + if (currentState == "delete") setCurrentState("nil"); + else setCurrentState("delete"); + } + else if (e.key == "5" && currSelected != "nil") setCurrentState("settings"); + else if (e.key == "6") { + if (currentState == "connect") setCurrentState("nil"); + else setCurrentState("connect"); + } + else if (e.key == "Escape") { // Deselect everything + setSaveFSM(false); + setCurrentState("nil"); + } + } + + window.addEventListener("keydown", handleKeybinds); + window.addEventListener("keyup", handleSave); + window.addEventListener("keydown", ignore); + return () => { + window.removeEventListener("keydown", handleKeybinds); + window.removeEventListener("keyup", handleSave); + window.removeEventListener("keydown", ignore) + } + }, [setCurrentState, currSelected, setSaveFSM, currentState]); + return (
diff --git a/src/components/Editor.tsx b/src/components/Editor.tsx index 6072d70..2444624 100644 --- a/src/components/Editor.tsx +++ b/src/components/Editor.tsx @@ -11,7 +11,7 @@ import { } from "../lib/backend"; import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { Nodes } from "../lib/backend"; -import { useRef, useState, useEffect } from "react"; +import { useRef, useState, useEffect, useCallback } from "react"; import clsx from "clsx"; import { Check, X } from "lucide-react"; @@ -46,6 +46,55 @@ const Editor = () => { const [startState, setStartState] = useAtom(start_state); + // Deletes node based off node id + const deleteNode = useCallback((id) => { + const clickedGroup = layerRef.current.findOne(`#g${id}`); + clickedGroup.destroy(); // Delete the Node + + transitions.forEach((tr) => { + let tre = null; + let trText = null; + if (tr && (tr.from == id || tr.to == id)) { + tre = layerRef.current.findOne(`#tr${tr.id}`); + tre.destroy(); // Delete the arrow + + trText = layerRef.current.findOne(`#trtext${tr.id}`); + trText.destroy(); // Also delete the Label of the transition + + // Delete the transition for the other node participating in the state + if (tr.from == id) { + const aliveNodeTransitions = nodeList[tr.to].transitions; + + for (let i = 0; i < aliveNodeTransitions.length; i++) { + if (aliveNodeTransitions[i].trId == tr.id) { + nodeList[tr.to].transitions.splice(i, 1); + } + } + } else { + const aliveNodeTransitions = nodeList[tr.from].transitions; + + for (let i = 0; i < aliveNodeTransitions.length; i++) { + if (aliveNodeTransitions[i].trId == tr.id) { + nodeList[tr.from].transitions.splice(i, 1); + } + } + } + + transitions[tr.id] = undefined; // remove the arrow entry from the array + } + }); + + updateTransitions(transitions); + + // Update the nodeList store + nodeList[id] = undefined; + updateNodeList(nodeList); + + // If the deleted Node is the one currently selected + // Then deselect it + if (currSelected == id) setCurrSelected("nil"); + }, [currSelected, nodeList, setCurrSelected, transitions, updateNodeList, updateTransitions]); + // Every time a state's controls are changed(size), it's transition arrows should also be updated useEffect(() => { if (recentStateControlSaved == "nil") return; @@ -81,6 +130,19 @@ const Editor = () => { } }, [recentStateControlSaved]); + // Handles delete keybind + useEffect(() => { + const handleKeybinds = (e: KeyboardEvent) => { + if (e.key == "Delete" && currSelected != "nil" && currentEditorState != "settings") { + deleteNode(currSelected); + } + } + window.addEventListener("keyup", handleKeybinds); + return () => { + window.removeEventListener("keyup", handleKeybinds); + } + },[currSelected, setCurrSelected, deleteNode, currentEditorState]); + // Handle Creating Nodes by clicking function handleEditorClick(e: any) { // Return if not in create mode @@ -117,52 +179,7 @@ const Editor = () => { const clickedNode = layerRef.current.findOne(`#${id}`); if (currentEditorState == "delete") { - const clickedGroup = layerRef.current.findOne(`#g${id}`); - clickedGroup.destroy(); // Delete the Node - - transitions.forEach((tr) => { - let tre = null; - let trText = null; - if (tr && (tr.from == id || tr.to == id)) { - tre = layerRef.current.findOne(`#tr${tr.id}`); - tre.destroy(); // Delete the arrow - - trText = layerRef.current.findOne(`#trtext${tr.id}`); - trText.destroy(); // Also delete the Label of the transition - - // Delete the transition for the other node participating in the state - if (tr.from == id) { - const aliveNodeTransitions = nodeList[tr.to].transitions; - - for (let i = 0; i < aliveNodeTransitions.length; i++) { - if (aliveNodeTransitions[i].trId == tr.id) { - nodeList[tr.to].transitions.splice(i, 1); - } - } - } else { - const aliveNodeTransitions = nodeList[tr.from].transitions; - - for (let i = 0; i < aliveNodeTransitions.length; i++) { - if (aliveNodeTransitions[i].trId == tr.id) { - nodeList[tr.from].transitions.splice(i, 1); - } - } - } - - transitions[tr.id] = undefined; // remove the arrow entry from the array - } - }); - - updateTransitions(transitions); - - // Update the nodeList store - nodeList[id] = undefined; - updateNodeList(nodeList); - - // If the deleted Node is the one currently selected - // Then deselect it - if (currSelected == id) setCurrSelected("nil"); - + deleteNode(id); return; } @@ -521,7 +538,7 @@ const Editor = () => { // Update location of text trText.x( transitions[trNameEditor[2]].points[2] - - 3 * trNameEditor[1].length + 3 * trNameEditor[1].length ); trText.y(transitions[trNameEditor[2]].points[3] - 20);