diff --git a/behavior3editor.code-workspace b/behavior3editor.code-workspace index 5c6e990..38d027f 100644 --- a/behavior3editor.code-workspace +++ b/behavior3editor.code-workspace @@ -1,14 +1,11 @@ { - "folders": [ - { - "path": "." - } - ], - "settings": { - "editor.formatOnSave": true, - "editor.defaultFormatter": "esbenp.prettier-vscode", - "[json]": { - "editor.formatOnSave": false - } + "folders": [ + { + "path": "." } + ], + "settings": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode" + } } diff --git a/package-lock.json b/package-lock.json index 7590aa9..9bf5649 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "behavior3editor", - "version": "1.1.2", + "version": "1.1.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "behavior3editor", - "version": "1.1.2", + "version": "1.1.3", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 411038a..f0b5159 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "behavior3editor", - "version": "1.1.2", + "version": "1.1.3", "main": "dist-electron/main/index.js", "description": "行为树编辑器", "author": "zhandouxiaojiji & codetypes", diff --git a/sample/node-config.b3-setting b/sample/node-config.b3-setting index ce58d1f..cab684b 100644 --- a/sample/node-config.b3-setting +++ b/sample/node-config.b3-setting @@ -124,7 +124,7 @@ }, { "name": "MoveToPos", - "type": "Action", + "type": "Action (Client)", "args": [ { "name": "x", @@ -141,7 +141,7 @@ }, { "name": "MoveToTarget", - "type": "Action", + "type": "Action (Server)", "input": [ "{目标}" ], diff --git a/src/components/editor.tsx b/src/components/editor.tsx index 93dfbd7..12d971f 100644 --- a/src/components/editor.tsx +++ b/src/components/editor.tsx @@ -10,17 +10,19 @@ import * as b3util from "@/misc/b3util"; import { message } from "@/misc/hooks"; import { Hotkey, isHotkeyPressed, isMacos, useHotkeys } from "@/misc/keys"; import Path from "@/misc/path"; +import { ArrowDownOutlined, ArrowUpOutlined, CloseOutlined } from "@ant-design/icons"; import G6, { G6GraphEvent, Item, TreeGraph } from "@antv/g6"; import { dialog } from "@electron/remote"; import { useSize } from "ahooks"; -import { Dropdown, Flex, FlexProps, MenuProps } from "antd"; +import { Button, Dropdown, Flex, FlexProps, Input, MenuProps } from "antd"; import { clipboard } from "electron"; import * as fs from "fs"; -import React, { FC, useCallback, useEffect, useMemo, useRef } from "react"; +import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { FiDelete } from "react-icons/fi"; import { IoMdReturnLeft } from "react-icons/io"; import { mergeRefs } from "react-merge-refs"; +import { useDebounceCallback } from "usehooks-ts"; import "./register-node"; export interface EditorProps extends React.HTMLAttributes { @@ -36,11 +38,27 @@ export const Editor: FC = ({ onUpdate: updateState, data: editor, . open: useWorkspace((state) => state.open), workdir: useWorkspace((state) => state.workdir), }; + const [results, setResults] = useState([]); + const [resultIndex, setResultIndex] = useState(0); + const [showingSearch, setShowingSearch] = useState(false); const graphRef = useRef(null); const sizeRef = useRef(null); const editorSize = useSize(sizeRef); const { t } = useTranslation(); + const onSearchChange = useDebounceCallback((filter: string) => { + const ids = filterNodes(filter, findDataById("1")); + setResults(ids); + setResultIndex(0); + if (ids.length > 0) { + editor.graph.focusItem(ids[0]); + selectNode(ids[0]); + } + if (filter.length == 0) { + restoreViewport(); + } + }, 200); + const onChange = () => { if (!editor.unsave) { editor.unsave = true; @@ -93,6 +111,17 @@ export const Editor: FC = ({ onUpdate: updateState, data: editor, . return editor.graph.findDataById(id) as TreeGraphData; }; + const filterNodes = (str: string, node: TreeGraphData | null, ids?: string[]): string[] => { + ids ||= []; + if (node && str) { + if (node.name.toLocaleLowerCase().indexOf(str) >= 0) { + ids.push(node.id); + } + node.children?.forEach((child: TreeGraphData) => filterNodes(str, child, ids)); + } + return ids; + }; + const isAncestor = (ancestor: TreeGraphData, node: TreeGraphData): boolean => { if (ancestor.id === node.parent) { return true; @@ -782,68 +811,70 @@ export const Editor: FC = ({ onUpdate: updateState, data: editor, . } ); - if (!editor.dispatch) { - editor.dispatch = (event: EditEvent, data: unknown) => { - switch (event) { - case "copy": { - copyNode(); - break; - } - case "paste": { - pasteNode(); - break; - } - case "delete": { - deleteNode(); - break; - } - case "insert": { - createNode(); - break; - } - case "replace": { - replaceNode(); - break; - } - case "save": { - save(); - break; - } - case "undo": { - undo(); - break; - } - case "redo": { - redo(); - break; - } - case "reload": { - reload(); - break; - } - case "rename": { - rename(data as string); - break; - } - case "updateTree": { - updateTree(data as EditTree); - break; - } - case "updateNode": { - updateNode(data as EditNode); - break; - } - case "editSubtree": { - editSubtree(); - break; - } - case "saveAsSubtree": { - saveAsSubtree(); - break; - } + editor.dispatch = (event: EditEvent, data: unknown) => { + switch (event) { + case "copy": { + copyNode(); + break; } - }; - } + case "paste": { + pasteNode(); + break; + } + case "delete": { + deleteNode(); + break; + } + case "insert": { + createNode(); + break; + } + case "replace": { + replaceNode(); + break; + } + case "save": { + save(); + break; + } + case "undo": { + undo(); + break; + } + case "redo": { + redo(); + break; + } + case "reload": { + reload(); + break; + } + case "rename": { + rename(data as string); + break; + } + case "updateTree": { + updateTree(data as EditTree); + break; + } + case "updateNode": { + updateNode(data as EditNode); + break; + } + case "searchNode": { + setShowingSearch(true); + break; + } + case "editSubtree": { + editSubtree(); + break; + } + case "saveAsSubtree": { + saveAsSubtree(); + break; + } + } + }; useEffect(() => { if (!editorSize || (editorSize.width === 0 && editorSize.height === 0)) { @@ -956,7 +987,7 @@ export const Editor: FC = ({ onUpdate: updateState, data: editor, . type MenuInfo = Parameters>[0]; const onClick = useCallback((info: MenuInfo) => { - editor.dispatch?.(info.key as EditEvent); + editor.dispatch(info.key as EditEvent); }, []); return ( @@ -967,6 +998,76 @@ export const Editor: FC = ({ onUpdate: updateState, data: editor, . tabIndex={-1} style={{ maxWidth: "inherit", maxHeight: "inherit" }} > + {showingSearch && ( + + onSearchChange(e.currentTarget.value)} + /> +
+ {results.length ? `${resultIndex + 1}/${results.length}` : ""} +
+