add ctrl/command + g to jump node by id

This commit is contained in:
George Shi committed 2024-04-30 09:58:06 +08:00
1 parent 7878e58ab5
commit d32c457990
6 files changed
+148 -77

No files matched your search

+1
View File
@@ -19,6 +19,7 @@
"hide": "Hide Behavior3", "hide": "Hide Behavior3",
"hideOthers": "Hide Others", "hideOthers": "Hide Others",
"insertNode": "Insert Node", "insertNode": "Insert Node",
"jumpNode": "Jump Node",
"language": "Language", "language": "Language",
"moveToTrash": "Move to Trash", "moveToTrash": "Move to Trash",
"newFile": "New File...", "newFile": "New File...",
+1
View File
@@ -19,6 +19,7 @@
"hide": "隐藏 Behavior3", "hide": "隐藏 Behavior3",
"hideOthers": "隐藏其它", "hideOthers": "隐藏其它",
"insertNode": "插入节点", "insertNode": "插入节点",
"jumpNode": "跳转节点",
"language": "语言", "language": "语言",
"moveToTrash": "移至回收站", "moveToTrash": "移至回收站",
"newFile": "新建行为树...", "newFile": "新建行为树...",
+120 -59
View File
@@ -15,10 +15,10 @@ import { ArrowDownOutlined, ArrowUpOutlined, CloseOutlined } from "@ant-design/i
import G6, { G6GraphEvent, Item, TreeGraph } from "@antv/g6"; import G6, { G6GraphEvent, Item, TreeGraph } from "@antv/g6";
import { dialog } from "@electron/remote"; import { dialog } from "@electron/remote";
import { useSize } from "ahooks"; import { useSize } from "ahooks";
import { Button, Dropdown, Flex, FlexProps, Input, MenuProps } from "antd"; import { Button, Dropdown, Flex, FlexProps, Input, InputRef, MenuProps } from "antd";
import { clipboard } from "electron"; import { clipboard } from "electron";
import * as fs from "fs"; import * as fs from "fs";
import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from "react"; import React, { FC, KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { FiDelete } from "react-icons/fi"; import { FiDelete } from "react-icons/fi";
import { IoMdReturnLeft } from "react-icons/io"; import { IoMdReturnLeft } from "react-icons/io";
@@ -40,6 +40,8 @@ interface FilterOption {
filterStr: string; filterStr: string;
filterCase: boolean; filterCase: boolean;
filterFocus: boolean; filterFocus: boolean;
filterType: string;
placeholder: string;
} }
const createMenu = () => { const createMenu = () => {
@@ -131,6 +133,7 @@ export const Editor: FC<EditorProps> = ({ onUpdate: updateState, data: editor, .
workdir: useWorkspace((state) => state.workdir), workdir: useWorkspace((state) => state.workdir),
}; };
const searchIptRef = useRef<InputRef>(null);
const graphRef = useRef(null); const graphRef = useRef(null);
const sizeRef = useRef(null); const sizeRef = useRef(null);
const editorSize = useSize(sizeRef); const editorSize = useSize(sizeRef);
@@ -144,6 +147,8 @@ export const Editor: FC<EditorProps> = ({ onUpdate: updateState, data: editor, .
filterStr: "", filterStr: "",
filterCase: false, filterCase: false,
filterFocus: false, filterFocus: false,
filterType: "",
placeholder: ""
}); });
const onSearchChange = (option: FilterOption) => { const onSearchChange = (option: FilterOption) => {
@@ -231,32 +236,38 @@ export const Editor: FC<EditorProps> = ({ onUpdate: updateState, data: editor, .
node.highlightGray = option.filterFocus; node.highlightGray = option.filterFocus;
if (option.filterStr) { if (option.filterStr) {
let found = false; let found = false;
if (includeString(node.name, option) || includeString(node.desc || node.def.desc, option)) { if (option.filterType === "nodeId") {
found = true; if (option.filterStr === node.id) {
} else if (node.input) { found = true
for (const str of node.input) {
if (includeString(str, option)) {
found = true;
break;
}
} }
} else if (node.args) { } else {
for (const str in node.args) { if (includeString(node.name, option) || includeString(node.desc || node.def.desc, option)) {
if (includeString(str, option)) {
found = true;
break;
}
}
} else if (node.output) {
for (const str of node.output) {
if (includeString(str, option)) {
found = true;
break;
}
}
} else if (node.path) {
if (includeString(node.path, option)) {
found = true; found = true;
} else if (node.input) {
for (const str of node.input) {
if (includeString(str, option)) {
found = true;
break;
}
}
} else if (node.args) {
for (const str in node.args) {
if (includeString(str, option)) {
found = true;
break;
}
}
} else if (node.output) {
for (const str of node.output) {
if (includeString(str, option)) {
found = true;
break;
}
}
} else if (node.path) {
if (includeString(node.path, option)) {
found = true;
}
} }
} }
if (found) { if (found) {
@@ -1008,7 +1019,11 @@ export const Editor: FC<EditorProps> = ({ onUpdate: updateState, data: editor, .
break; break;
} }
case "searchNode": { case "searchNode": {
setShowingSearch(true); searchByType("");
break;
}
case "jumpNode": {
searchByType("nodeId");
break; break;
} }
case "editSubtree": { case "editSubtree": {
@@ -1075,6 +1090,42 @@ export const Editor: FC<EditorProps> = ({ onUpdate: updateState, data: editor, .
} }
}; };
const searchByType = (type: string) => {
let placeholder = ""
const filterType = type
// todo multiple parameter format judgment
switch (type) {
case "nodeId":
placeholder = t("jumpNode")
break
default:
placeholder = t("searchNode")
break
}
if (!showingSearch) {
setFilterOption({ ...filterOption, placeholder, filterType })
setShowingSearch(true)
return
}
if (filterOption.filterType === type) return searchIptRef.current?.focus()
setShowingSearch(false)
setTimeout(() => {
setShowingSearch(true)
setFilterOption({ ...filterOption, placeholder, filterType })
searchIptRef.current?.focus()
}, 50)
}
const handleKeyDown = (e: KeyboardEvent) => {
if (e.code === Hotkey.Enter) {
nextResult()
} else if ((e.ctrlKey || e.metaKey) && e.code === "KeyF") {
searchByType("")
} else if ((e.ctrlKey || e.metaKey) && e.code === "KeyG") {
searchByType("nodeId")
}
}
return ( return (
<div <div
{...props} {...props}
@@ -1104,6 +1155,8 @@ export const Editor: FC<EditorProps> = ({ onUpdate: updateState, data: editor, .
}} }}
> >
<Input <Input
ref={searchIptRef}
placeholder={ filterOption.placeholder }
autoFocus autoFocus
size="small" size="small"
style={{ style={{
@@ -1119,24 +1172,26 @@ export const Editor: FC<EditorProps> = ({ onUpdate: updateState, data: editor, .
index: 0, index: 0,
}) })
} }
onKeyDown={(e) => e.code === Hotkey.Enter && nextResult()} onKeyDown={ handleKeyDown }
suffix={ suffix={
<Flex gap="2px" style={{ alignItems: "center" }}> <Flex gap="2px" style={{ alignItems: "center" }}>
<Button {filterOption.filterType !== "nodeId" && (
type="text" <Button
size="small" type="text"
className={mergeClassNames( size="small"
"b3-editor-button-filter", className={mergeClassNames(
filterOption.filterCase && "b3-editor-button-filter-selected" "b3-editor-button-filter",
)} filterOption.filterCase && "b3-editor-button-filter-selected"
icon={<VscCaseSensitive style={{ width: "18px", height: "18px" }} />} )}
onClick={() => icon={<VscCaseSensitive style={{ width: "18px", height: "18px" }} />}
onSearchChange({ onClick={() =>
...filterOption, onSearchChange({
filterCase: !filterOption.filterCase, ...filterOption,
}) filterCase: !filterOption.filterCase,
} })
/> }
/>
)}
<Button <Button
type="text" type="text"
size="small" size="small"
@@ -1160,22 +1215,26 @@ export const Editor: FC<EditorProps> = ({ onUpdate: updateState, data: editor, .
? `${filterOption.index + 1}/${filterOption.results.length}` ? `${filterOption.index + 1}/${filterOption.results.length}`
: ""} : ""}
</div> </div>
<Button {filterOption.filterType !== "nodeId" && (
icon={<ArrowDownOutlined />} <Button
type="text" icon={<ArrowDownOutlined />}
size="small" type="text"
style={{ width: "30px" }} size="small"
disabled={filterOption.results.length == 0} style={{ width: "30px" }}
onClick={nextResult} disabled={filterOption.results.length == 0}
/> onClick={nextResult}
<Button />
icon={<ArrowUpOutlined />} )}
type="text" {filterOption.filterType !== "nodeId" && (
size="small" <Button
style={{ width: "30px" }} icon={<ArrowUpOutlined />}
disabled={filterOption.results.length == 0} type="text"
onClick={prevResult} size="small"
/> style={{ width: "30px" }}
disabled={filterOption.results.length == 0}
onClick={prevResult}
/>
)}
<Button <Button
icon={<CloseOutlined />} icon={<CloseOutlined />}
type="text" type="text"
@@ -1189,6 +1248,8 @@ export const Editor: FC<EditorProps> = ({ onUpdate: updateState, data: editor, .
filterCase: false, filterCase: false,
filterFocus: false, filterFocus: false,
filterStr: "", filterStr: "",
filterType: "",
placeholder: ""
}); });
keysRef.current?.focus(); keysRef.current?.focus();
}} }}
+5 -1
View File
@@ -44,7 +44,7 @@ export const Workspace: FC = () => {
const { width = 0, height = 0 } = useWindowSize(); const { width = 0, height = 0 } = useWindowSize();
const keysRef = useHotkeys<HTMLDivElement>( const keysRef = useHotkeys<HTMLDivElement>(
[Hotkey.Save, Hotkey.CloseEditor, Hotkey.SearchTree, Hotkey.Build, Hotkey.SearchNode], [Hotkey.Save, Hotkey.CloseEditor, Hotkey.SearchTree, Hotkey.Build, Hotkey.SearchNode, Hotkey.JumpNode],
(event) => { (event) => {
if (isHotkeyPressed(Hotkey.Save)) { if (isHotkeyPressed(Hotkey.Save)) {
event.preventDefault(); event.preventDefault();
@@ -67,6 +67,9 @@ export const Workspace: FC = () => {
} else if (isHotkeyPressed(Hotkey.SearchNode)) { } else if (isHotkeyPressed(Hotkey.SearchNode)) {
event.preventDefault(); event.preventDefault();
workspace.editing?.dispatch("searchNode"); workspace.editing?.dispatch("searchNode");
} else if (isHotkeyPressed(Hotkey.JumpNode)) {
event.preventDefault()
workspace.editing?.dispatch("jumpNode")
} }
} }
); );
@@ -338,6 +341,7 @@ export const Workspace: FC = () => {
{ label: t("searchFile"), hotkeys: isMacos ? "⌘ P" : "Ctrl + P" }, { label: t("searchFile"), hotkeys: isMacos ? "⌘ P" : "Ctrl + P" },
{ label: t("build"), hotkeys: isMacos ? "⌘ B" : "Ctrl + B" }, { label: t("build"), hotkeys: isMacos ? "⌘ B" : "Ctrl + B" },
{ label: t("searchNode"), hotkeys: isMacos ? "⌘ F" : "Ctrl + F" }, { label: t("searchNode"), hotkeys: isMacos ? "⌘ F" : "Ctrl + F" },
{ label: t("jumpNode"), hotkeys: isMacos ? "⌘ G" : "Ctrl + G" },
{ label: t("insertNode"), hotkeys: "Enter" }, { label: t("insertNode"), hotkeys: "Enter" },
{ label: t("deleteNode"), hotkeys: "Backspace" }, { label: t("deleteNode"), hotkeys: "Backspace" },
].map((v) => ( ].map((v) => (
+19 -16
View File
@@ -16,6 +16,7 @@ let buildDir: string | undefined;
interface BatchScript { interface BatchScript {
processTree?(tree: TreeModel): TreeModel; processTree?(tree: TreeModel): TreeModel;
processNode?(node: NodeModel, tree: TreeModel): NodeModel; processNode?(node: NodeModel, tree: TreeModel): NodeModel;
} }
@@ -26,6 +27,7 @@ export type EditEvent =
| "replace" | "replace"
| "delete" | "delete"
| "insert" | "insert"
| "jumpNode"
| "undo" | "undo"
| "redo" | "redo"
| "reload" | "reload"
@@ -143,13 +145,13 @@ export type WorkspaceStore = {
}; };
const loadFileTree = (workdir: string, filename: string) => { const loadFileTree = (workdir: string, filename: string) => {
const fullpath = fs.realpathSync(`${workdir}/${filename}`); const fullpath = fs.realpathSync(`${ workdir }/${ filename }`);
if (!fs.existsSync(fullpath)) { if (!fs.existsSync(fullpath)) {
return; return;
} }
const stat = fs.statSync(fullpath); const stat = fs.statSync(fullpath);
if (!(stat.isDirectory() || fullpath.endsWith(".json"))) { if (!( stat.isDirectory() || fullpath.endsWith(".json") )) {
return; return;
} }
@@ -162,13 +164,13 @@ const loadFileTree = (workdir: string, filename: string) => {
data.children = []; data.children = [];
const files = fs.readdirSync(data.path); const files = fs.readdirSync(data.path);
files.forEach((v) => { files.forEach((v) => {
const child = loadFileTree(workdir, `${filename}/${v}`); const child = loadFileTree(workdir, `${ filename }/${ v }`);
if (child) { if (child) {
data.children?.push(child); data.children?.push(child);
} }
}); });
data.children.sort((a, b) => { data.children.sort((a, b) => {
if ((a.children && b.children) || (!a.children && !b.children)) { if (( a.children && b.children ) || ( !a.children && !b.children )) {
return a.title.localeCompare(b.title); return a.title.localeCompare(b.title);
} else { } else {
return a.children ? -1 : 1; return a.children ? -1 : 1;
@@ -186,7 +188,7 @@ const saveFile = (editor?: EditorStore) => {
} }
}; };
export const useWorkspace = create<WorkspaceStore>((set, get) => ({ export const useWorkspace = create<WorkspaceStore>((set, get) => ( {
allFiles: [], allFiles: [],
fileTree: undefined, fileTree: undefined,
editors: [], editors: [],
@@ -208,15 +210,15 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
if (!fs.existsSync(path)) { if (!fs.existsSync(path)) {
useSetting.getState().removeRecent(path); useSetting.getState().removeRecent(path);
} }
message.error(`load workspace error: ${path}`); message.error(`load workspace error: ${ path }`);
} }
} }
}, },
createProject: () => { createProject: () => {
const path = dialog.showSaveDialogSync({ const path = dialog.showSaveDialogSync({
properties: ["showOverwriteConfirmation", "createDirectory"], properties: [ "showOverwriteConfirmation", "createDirectory" ],
filters: [{ name: "Behavior3 Workspace", extensions: ["b3-workspace"] }], filters: [ { name: "Behavior3 Workspace", extensions: [ "b3-workspace" ] } ],
}); });
if (path) { if (path) {
const workspace = get(); const workspace = get();
@@ -232,7 +234,7 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
const win = BrowserWindow.getFocusedWindow(); const win = BrowserWindow.getFocusedWindow();
if (win) { if (win) {
const path = dialog.showOpenDialogSync(win, { const path = dialog.showOpenDialogSync(win, {
filters: [{ name: "workspace", extensions: ["b3-workspace"] }], filters: [ { name: "workspace", extensions: [ "b3-workspace" ] } ],
}); });
if (path?.length) { if (path?.length) {
ipcRenderer.invoke("open-win", path[0]); ipcRenderer.invoke("open-win", path[0]);
@@ -246,7 +248,7 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
if (workspace.path) if (workspace.path)
if (!buildDir) { if (!buildDir) {
buildDir = dialog.showOpenDialogSync({ buildDir = dialog.showOpenDialogSync({
properties: ["openDirectory", "createDirectory"], properties: [ "openDirectory", "createDirectory" ],
})?.[0]; })?.[0];
} }
if (buildDir) { if (buildDir) {
@@ -279,9 +281,9 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
batchProject: () => { batchProject: () => {
const workspace = get(); const workspace = get();
const scriptPath = dialog.showOpenDialogSync({ const scriptPath = dialog.showOpenDialogSync({
properties: ["openFile"], properties: [ "openFile" ],
defaultPath: workspace.workdir, defaultPath: workspace.workdir,
filters: [{ name: "Javascript", extensions: ["js"] }], filters: [ { name: "Javascript", extensions: [ "js" ] } ],
})?.[0]; })?.[0];
if (scriptPath) { if (scriptPath) {
try { try {
@@ -327,7 +329,7 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
workspace.edit(editor.path); workspace.edit(editor.path);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
message.error(`invalid file: ${path}`); message.error(`invalid file: ${ path }`);
} }
} else if (workspace.editing !== editor) { } else if (workspace.editing !== editor) {
workspace.edit(editor.path); workspace.edit(editor.path);
@@ -387,7 +389,8 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
saveFile(workspace.editing); saveFile(workspace.editing);
}, },
saveAs: () => {}, saveAs: () => {
},
saveAll: () => { saveAll: () => {
const workspace = get(); const workspace = get();
@@ -442,7 +445,7 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
loadNodeDefs: () => { loadNodeDefs: () => {
const workspace = get(); const workspace = get();
const nodeDefData = readJson(`${workspace.workdir}/node-config.b3-setting`) as NodeDef[]; const nodeDefData = readJson(`${ workspace.workdir }/node-config.b3-setting`) as NodeDef[];
const nodeDefs: Map<string, NodeDef> = new Map(); const nodeDefs: Map<string, NodeDef> = new Map();
for (const v of nodeDefData) { for (const v of nodeDefData) {
nodeDefs.set(v.name, v); nodeDefs.set(v.name, v);
@@ -476,4 +479,4 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
const workspace = get(); const workspace = get();
return workspace.nodeDefs.has(name); return workspace.nodeDefs.has(name);
}, },
})); } ));
+2 -1
View File
@@ -33,7 +33,7 @@ export function useHotkeys<T extends HTMLElement>(
keys: Keys, keys: Keys,
callback: HotkeyCallback, callback: HotkeyCallback,
options?: OptionsOrDependencyArray, options?: OptionsOrDependencyArray,
dependencies?: OptionsOrDependencyArray dependencies?: OptionsOrDependencyArray,
): MutableRefObject<RefType<T>> { ): MutableRefObject<RefType<T>> {
if (keys instanceof Array) { if (keys instanceof Array) {
keys = keys.filter((v) => !!v); keys = keys.filter((v) => !!v);
@@ -52,6 +52,7 @@ export const Hotkey = {
Escape: Key.Escape, Escape: Key.Escape,
F2: Key.F2, F2: Key.F2,
Insert: Key.Insert, Insert: Key.Insert,
JumpNode: hotkey("ctrl+g"),
MacDelete: isMacos ? hotkey("ctrl+backspace") : "", MacDelete: isMacos ? hotkey("ctrl+backspace") : "",
Paste: hotkey("ctrl+v"), Paste: hotkey("ctrl+v"),
Redo: isMacos ? hotkey("shift+ctrl+z") : hotkey("ctrl+y"), Redo: isMacos ? hotkey("shift+ctrl+z") : hotkey("ctrl+y"),