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);