reimplement the editor use lastest antd and other libs, feature of this commit:
1. file manger 2. highlight for click input or output 3. selector of subtress 4. autocomplete for input or output
No files matched your search
@@ -0,0 +1,7 @@
|
||||
**/*.js
|
||||
**/*.d.ts
|
||||
electron/
|
||||
common/
|
||||
.eslintrc.cjs
|
||||
playwright.config.ts
|
||||
vite.config.ts
|
||||
@@ -0,0 +1,40 @@
|
||||
module.exports = {
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
|
||||
// TODO
|
||||
// "plugin:@typescript-eslint/recommended-type-checked",
|
||||
],
|
||||
plugins: ["@typescript-eslint", "@stylistic/js", "react-refresh"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
rules: {
|
||||
"@typescript-eslint/no-namespace": "off",
|
||||
"@typescript-eslint/no-shadow": "error",
|
||||
"react-refresh/only-export-components": "warn",
|
||||
|
||||
// TODO:
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||
"@typescript-eslint/no-unsafe-argument": "off",
|
||||
"@typescript-eslint/no-floating-promises": "off",
|
||||
|
||||
// https://eslint.style/rules/js/lines-between-class-members
|
||||
"@stylistic/js/lines-between-class-members": [
|
||||
"error",
|
||||
{
|
||||
enforce: [
|
||||
{ blankLine: "always", prev: "method", next: "method" },
|
||||
{ blankLine: "always", prev: "field", next: "method" },
|
||||
{ blankLine: "always", prev: "method", next: "field" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
parserOptions: {
|
||||
project: true,
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
root: true,
|
||||
};
|
||||
@@ -1,4 +1,32 @@
|
||||
node_modules/
|
||||
dist/
|
||||
build/
|
||||
settings.json
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
dist-electron
|
||||
release
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/.debug.env
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
#lockfile
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
@@ -1,3 +1 @@
|
||||
# registry=https://registry.npm.taobao.org/
|
||||
# ELECTRON_MIRROR=https://registry.npmmirror.com/-/binary/electron/
|
||||
# ELECTRON_BUILDER_BINARIES_MIRROR=https://registry.npmmirror.com/-/binary/electron-builder-binaries/
|
||||
shamefully-hoist=true
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"endOfLine": "lf",
|
||||
"semi": true,
|
||||
"tabWidth": 4,
|
||||
"tabWidth": 2,
|
||||
"printWidth": 100,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { createRequire } from 'node:module'
|
||||
import { spawn } from 'node:child_process'
|
||||
|
||||
const pkg = createRequire(import.meta.url)('../package.json')
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
// write .debug.env
|
||||
const envContent = Object.entries(pkg.debug.env).map(([key, val]) => `${key}=${val}`)
|
||||
fs.writeFileSync(path.join(__dirname, '.debug.env'), envContent.join('\n'))
|
||||
|
||||
// bootstrap
|
||||
spawn(
|
||||
// TODO: terminate `npm run dev` when Debug exits.
|
||||
process.platform === 'win32' ? 'npm.cmd' : 'npm',
|
||||
['run', 'dev'],
|
||||
{
|
||||
stdio: 'inherit',
|
||||
env: Object.assign(process.env, { VSCODE_DEBUG: 'true' }),
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
"mrmlnc.vscode-json5"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"compounds": [
|
||||
{
|
||||
"name": "Debug App",
|
||||
"preLaunchTask": "Before Debug",
|
||||
"configurations": ["Debug Main Process", "Debug Renderer Process"],
|
||||
"presentation": {
|
||||
"hidden": false,
|
||||
"group": "",
|
||||
"order": 1
|
||||
},
|
||||
"stopAll": true
|
||||
}
|
||||
],
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug Main Process",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
|
||||
"windows": {
|
||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd"
|
||||
},
|
||||
"runtimeArgs": ["--no-sandbox", "--remote-debugging-port=9229", "."],
|
||||
"envFile": "${workspaceFolder}/.vscode/.debug.env",
|
||||
"console": "integratedTerminal"
|
||||
},
|
||||
{
|
||||
"name": "Debug Renderer Process",
|
||||
"port": 9229,
|
||||
"request": "attach",
|
||||
"type": "chrome",
|
||||
"timeout": 60000,
|
||||
"skipFiles": [
|
||||
"<node_internals>/**",
|
||||
"${workspaceRoot}/node_modules/**",
|
||||
"${workspaceRoot}/dist-electron/**",
|
||||
// Skip files in host(VITE_DEV_SERVER_URL)
|
||||
"http://127.0.0.1:7777/**"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"typescript.tsc.autoDetect": "off",
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": [
|
||||
"/*electron-builder.json5",
|
||||
"/*electron-builder.json"
|
||||
],
|
||||
"url": "https://json.schemastore.org/electron-builder"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Before Debug",
|
||||
"type": "shell",
|
||||
"command": "node .vscode/.debug.script.mjs",
|
||||
"isBackground": true,
|
||||
"problemMatcher": {
|
||||
"owner": "typescript",
|
||||
"fileLocation": "relative",
|
||||
"pattern": {
|
||||
// TODO: correct "regexp"
|
||||
"regexp": "^([a-zA-Z]\\:\/?([\\w\\-]\/?)+\\.\\w+):(\\d+):(\\d+): (ERROR|WARNING)\\: (.*)$",
|
||||
"file": 1,
|
||||
"line": 3,
|
||||
"column": 4,
|
||||
"code": 5,
|
||||
"message": 6
|
||||
},
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": "^.*VITE v.* ready in \\d* ms.*$",
|
||||
"endsPattern": "^.*\\[startup\\] Electron App.*$"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 13 KiB |
@@ -1,46 +0,0 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { BehaviorNodeModel, BehaviorTreeModel } from './BehaviorTreeModel';
|
||||
|
||||
export default (scriptPath: string, dirPath: string) => {
|
||||
console.log("run script", scriptPath);
|
||||
const str = fs.readFileSync(scriptPath, "utf8");
|
||||
const script = eval(str);
|
||||
const batchExec = (filePath: string) => {
|
||||
const files = fs.readdirSync(filePath);
|
||||
files.forEach((filename) => {
|
||||
var filedir = path.join(filePath, filename);
|
||||
const stats = fs.statSync(filedir);
|
||||
if (stats.isFile() && filename.match(/.\.json$/)) {
|
||||
const str = fs.readFileSync(filedir, "utf8");
|
||||
let tree: BehaviorTreeModel = JSON.parse(str);
|
||||
if (script.processTree) {
|
||||
tree = script.processTree(tree);
|
||||
}
|
||||
if (tree && script.processNode) {
|
||||
const processNode = (node: BehaviorNodeModel) => {
|
||||
script.processNode(node, tree);
|
||||
if (node.children) {
|
||||
for (let child of node.children) {
|
||||
processNode(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
processNode(tree.root);
|
||||
}
|
||||
if (tree) {
|
||||
fs.writeFileSync(
|
||||
filedir,
|
||||
JSON.stringify(tree)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
if (stats.isDirectory()) {
|
||||
batchExec(filedir);
|
||||
}
|
||||
});
|
||||
}
|
||||
batchExec(dirPath);
|
||||
console.log("run script", scriptPath, "done!");
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import { TreeGraphData } from "@antv/g6/lib/types";
|
||||
|
||||
export interface ArgsOptional {
|
||||
name: string;
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
export interface ArgsDefType {
|
||||
name: string;
|
||||
type: string;
|
||||
desc: string;
|
||||
default: string;
|
||||
options: ArgsOptional[];
|
||||
}
|
||||
export interface BehaviorNodeTypeModel {
|
||||
name: string;
|
||||
type?: string;
|
||||
desc?: string;
|
||||
args?: ArgsDefType[];
|
||||
input?: string[];
|
||||
output?: string[];
|
||||
doc?: string;
|
||||
}
|
||||
|
||||
export interface BehaviorNodeModel {
|
||||
id: number;
|
||||
name: string;
|
||||
desc?: string;
|
||||
args?: { [key: string]: any };
|
||||
input?: string[];
|
||||
output?: string[];
|
||||
children?: BehaviorNodeModel[];
|
||||
debug?: boolean;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
export interface BehaviorTreeModel {
|
||||
name: string;
|
||||
desc?: string;
|
||||
root: BehaviorNodeModel;
|
||||
}
|
||||
|
||||
export interface GraphNodeModel extends TreeGraphData {
|
||||
name: string;
|
||||
desc?: string;
|
||||
args?: { [key: string]: any };
|
||||
input?: string[];
|
||||
output?: string[];
|
||||
children?: GraphNodeModel[];
|
||||
conf: BehaviorNodeTypeModel;
|
||||
debug?: boolean;
|
||||
parent?: string;
|
||||
path?: string;
|
||||
|
||||
size?: number[];
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
export default {
|
||||
OPEN_FILE: "OPEN_FILE",
|
||||
OPEN_DIR: "OPEN_DIR",
|
||||
RELOAD_SERVER: "RELOAD_SERVER",
|
||||
BATCH_EXEC: "BATCH_EXEC",
|
||||
CREATE_TREE: "CREATE_TREE",
|
||||
CREATE_NODE: "CREATE_NODE",
|
||||
DELETE_NODE: "DELETE_NODE",
|
||||
COPY_NODE: "COPY_NODE",
|
||||
PASTE_NODE: "PASTE_NODE",
|
||||
SAVE_ALL: "SAVE_ALL",
|
||||
BUILD: "BUILD",
|
||||
SAVE: "SAVE",
|
||||
SAVE_AS_SUBTREE: "SAVE_AS_SUBTREE",
|
||||
UNDO: "UNDO",
|
||||
REDO: "REDO",
|
||||
};
|
||||
@@ -1,203 +0,0 @@
|
||||
import { message } from "antd";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import Settings from "../main-process/Settings";
|
||||
import { BehaviorNodeModel, BehaviorTreeModel, GraphNodeModel } from "./BehaviorTreeModel";
|
||||
|
||||
interface INodeDataLike {
|
||||
path?: string;
|
||||
id: string | number;
|
||||
}
|
||||
|
||||
export const isSubtree = (nodeData: INodeDataLike) => {
|
||||
return nodeData.path && nodeData.id.toString() !== "1";
|
||||
};
|
||||
|
||||
const parsingStack: string[] = [];
|
||||
|
||||
export const cloneNodeData = (nodeData: GraphNodeModel) => {
|
||||
const newData: BehaviorNodeModel = {
|
||||
id: Number(nodeData.id),
|
||||
name: nodeData.name,
|
||||
desc: nodeData.desc,
|
||||
path: nodeData.path,
|
||||
};
|
||||
if (nodeData.input) {
|
||||
newData.input = [];
|
||||
for (let v of nodeData.input) {
|
||||
newData.input.push(v || "");
|
||||
}
|
||||
}
|
||||
if (nodeData.output) {
|
||||
newData.output = [];
|
||||
for (let v of nodeData.output) {
|
||||
newData.output.push(v || "");
|
||||
}
|
||||
}
|
||||
if (nodeData.args) {
|
||||
newData.args = {};
|
||||
for (let k in nodeData.args) {
|
||||
let v = nodeData.args[k];
|
||||
newData.args[k] = v;
|
||||
}
|
||||
}
|
||||
if (nodeData.children && !isSubtree(nodeData)) {
|
||||
newData.children = [];
|
||||
for (let child of nodeData.children) {
|
||||
newData.children.push(cloneNodeData(child));
|
||||
}
|
||||
}
|
||||
return newData;
|
||||
};
|
||||
|
||||
export const refreshNodeId = (nodeData: GraphNodeModel, id?: number) => {
|
||||
if (!id) {
|
||||
id = 1;
|
||||
}
|
||||
nodeData.id = (id++).toString();
|
||||
if (nodeData.children) {
|
||||
nodeData.children.forEach((child) => {
|
||||
child.parent = nodeData.id;
|
||||
id = refreshNodeId(child, id);
|
||||
});
|
||||
}
|
||||
return id;
|
||||
};
|
||||
|
||||
export const calcTreeNodeSize = (treeNode: GraphNodeModel) => {
|
||||
var height = 40 + 2;
|
||||
const updateHeight = (obj: any) => {
|
||||
if (Array.isArray(obj) || (obj && Object.keys(obj).length > 0)) {
|
||||
const { str, line } = toBreakWord(`参数:${JSON.stringify(obj)}`, 35);
|
||||
height += 20 * line;
|
||||
}
|
||||
};
|
||||
if (treeNode.path) {
|
||||
height += 20;
|
||||
}
|
||||
updateHeight(treeNode.args);
|
||||
updateHeight(treeNode.input);
|
||||
updateHeight(treeNode.output);
|
||||
return [200, height];
|
||||
};
|
||||
|
||||
export const createTreeData = (bNode: BehaviorNodeModel, settings: Settings, parent?: string) => {
|
||||
let treeData: GraphNodeModel = {
|
||||
id: bNode.id.toString(),
|
||||
name: bNode.name,
|
||||
desc: bNode.desc,
|
||||
args: bNode.args,
|
||||
input: bNode.input,
|
||||
output: bNode.output,
|
||||
debug: bNode.debug,
|
||||
conf: settings.getNodeConf(bNode.name),
|
||||
parent: parent,
|
||||
};
|
||||
|
||||
treeData.size = calcTreeNodeSize(treeData);
|
||||
|
||||
if (!parent) {
|
||||
parsingStack.length = 0;
|
||||
}
|
||||
|
||||
if (bNode.children) {
|
||||
treeData.children = [];
|
||||
bNode.children.forEach((child) => {
|
||||
treeData.children.push(createTreeData(child, settings, treeData.id));
|
||||
});
|
||||
} else if (bNode.path) {
|
||||
if (parsingStack.indexOf(bNode.path) >= 0) {
|
||||
treeData.path = bNode.path;
|
||||
treeData.size = calcTreeNodeSize(treeData);
|
||||
message.error(`循环引用节点:${bNode.path}`, 4);
|
||||
return treeData;
|
||||
}
|
||||
parsingStack.push(bNode.path);
|
||||
try {
|
||||
const subtreePath = settings.workdir + "/" + bNode.path;
|
||||
const str = fs.readFileSync(subtreePath, "utf8");
|
||||
treeData = createTreeData(JSON.parse(str).root, settings, treeData.id);
|
||||
treeData.path = bNode.path;
|
||||
treeData.size = calcTreeNodeSize(treeData);
|
||||
} catch (error) {
|
||||
message.error(`解析子树失败:${bNode.path}`);
|
||||
console.log("parse subtree:", error);
|
||||
}
|
||||
parsingStack.pop();
|
||||
}
|
||||
calcTreeNodeSize(treeData);
|
||||
return treeData;
|
||||
};
|
||||
|
||||
export const createBuildData = (path: string, settings: Settings): BehaviorNodeModel | null => {
|
||||
try {
|
||||
path = settings.workdir + "/" + path;
|
||||
const str = fs.readFileSync(path, "utf8");
|
||||
let treeModel = JSON.parse(str);
|
||||
const data = createTreeData(treeModel.root, settings);
|
||||
refreshNodeId(data);
|
||||
treeModel.root = createFileData(data, true);
|
||||
return treeModel as BehaviorNodeModel;
|
||||
} catch (error) {
|
||||
console.log("build error:", path, error);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const createFileData = (gNode: GraphNodeModel, includeSubtree?: boolean) => {
|
||||
const nodeData: BehaviorNodeModel = {
|
||||
id: Number(gNode.id),
|
||||
name: gNode.name,
|
||||
desc: gNode.desc || undefined,
|
||||
args: gNode.args || undefined,
|
||||
input: gNode.input || undefined,
|
||||
output: gNode.output || undefined,
|
||||
debug: gNode.debug,
|
||||
path: gNode.path,
|
||||
};
|
||||
if (gNode.children && (includeSubtree || !isSubtree(gNode))) {
|
||||
nodeData.children = [];
|
||||
gNode.children.forEach((child) => {
|
||||
nodeData.children.push(createFileData(child, includeSubtree));
|
||||
});
|
||||
}
|
||||
return nodeData;
|
||||
};
|
||||
|
||||
export const getRemoteSettings = () => {
|
||||
return require("@electron/remote").getGlobal("settings") as Settings;
|
||||
};
|
||||
|
||||
export const fileName2treeName = (filename: string) => {
|
||||
return path.basename(filename).slice(0, -5);
|
||||
};
|
||||
|
||||
export const createNewTree = (filename: string) => {
|
||||
const tree: BehaviorTreeModel = {
|
||||
name: fileName2treeName(filename),
|
||||
root: {
|
||||
id: 1,
|
||||
name: "Sequence",
|
||||
desc: "新建行为树",
|
||||
},
|
||||
};
|
||||
return tree;
|
||||
};
|
||||
|
||||
export const toBreakWord = (str: string, len: number, char = "\n") => {
|
||||
var strTemp = "";
|
||||
var line = 1;
|
||||
if (str.length <= len) {
|
||||
return { str, line };
|
||||
}
|
||||
while (str.length > len) {
|
||||
strTemp += str.substr(0, len) + char;
|
||||
str = str.substr(len, str.length);
|
||||
line++;
|
||||
}
|
||||
strTemp += str;
|
||||
return {
|
||||
str: strTemp,
|
||||
line,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* @see https://www.electron.build/configuration/configuration
|
||||
*/
|
||||
{
|
||||
appId: "behavior3editor",
|
||||
productName: "Behavior3 Editor",
|
||||
asar: true,
|
||||
directories: {
|
||||
output: "release/${version}",
|
||||
},
|
||||
files: ["dist-electron", "dist"],
|
||||
mac: {
|
||||
artifactName: "behavior3editor_${version}.${ext}",
|
||||
target: ["dmg", "zip"],
|
||||
},
|
||||
win: {
|
||||
target: [
|
||||
{
|
||||
target: "nsis",
|
||||
arch: ["x64"],
|
||||
// arch: ["x64", "ia32"],
|
||||
},
|
||||
],
|
||||
artifactName: "behavior3editor_${version}.${ext}",
|
||||
},
|
||||
nsis: {
|
||||
oneClick: false,
|
||||
perMachine: false,
|
||||
allowToChangeInstallationDirectory: true,
|
||||
deleteAppDataOnUninstall: false,
|
||||
},
|
||||
publish: {
|
||||
provider: "generic",
|
||||
channel: "latest",
|
||||
url: "https://github.com/electron-vite/electron-vite-react/releases/download/v0.9.9/",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/// <reference types="vite-electron-plugin/electron-env" />
|
||||
|
||||
declare namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
VSCODE_DEBUG?: 'true'
|
||||
DIST_ELECTRON: string
|
||||
DIST: string
|
||||
/** /dist/ or /public/ */
|
||||
PUBLIC: string
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
import { app, BrowserWindow, shell, ipcMain } from "electron";
|
||||
import { release } from "node:os";
|
||||
import path, { join } from "node:path";
|
||||
import { update } from "./update";
|
||||
|
||||
import * as fs from "fs";
|
||||
|
||||
// The built directory structure
|
||||
//
|
||||
// ├─┬ dist-electron
|
||||
// │ ├─┬ main
|
||||
// │ │ └── index.js > Electron-Main
|
||||
// │ └─┬ preload
|
||||
// │ └── index.js > Preload-Scripts
|
||||
// ├─┬ dist
|
||||
// │ └── index.html > Electron-Renderer
|
||||
//
|
||||
process.env.DIST_ELECTRON = join(__dirname, "../");
|
||||
process.env.DIST = join(process.env.DIST_ELECTRON, "../dist");
|
||||
process.env.PUBLIC = process.env.VITE_DEV_SERVER_URL
|
||||
? join(process.env.DIST_ELECTRON, "../public")
|
||||
: process.env.DIST;
|
||||
|
||||
// Disable GPU Acceleration for Windows 7
|
||||
if (release().startsWith("6.1")) app.disableHardwareAcceleration();
|
||||
|
||||
// Set application name for Windows 10+ notifications
|
||||
if (process.platform === "win32") app.setAppUserModelId(app.getName());
|
||||
|
||||
if (!app.requestSingleInstanceLock()) {
|
||||
app.quit();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Remove electron security warnings
|
||||
// This warning only shows in development mode
|
||||
// Read more on https://www.electronjs.org/docs/latest/tutorial/security
|
||||
// process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'
|
||||
|
||||
interface Workspace {
|
||||
projectPath?: string;
|
||||
window: BrowserWindow;
|
||||
}
|
||||
|
||||
const preload = "../preload/index.js";
|
||||
const url = process.env.VITE_DEV_SERVER_URL;
|
||||
const indexHtml = join(process.env.DIST, "index.html");
|
||||
const windows: Workspace[] = [];
|
||||
|
||||
async function createWindow(projectPath?: string) {
|
||||
const win = new BrowserWindow({
|
||||
title: "Behaviour3 Editor",
|
||||
frame: false,
|
||||
width: 1280,
|
||||
height: 800,
|
||||
minHeight: 600,
|
||||
minWidth: 800,
|
||||
closable: true,
|
||||
minimizable: true,
|
||||
maximizable: true,
|
||||
titleBarStyle: "hidden",
|
||||
titleBarOverlay:
|
||||
process.platform === "darwin"
|
||||
? true
|
||||
: { color: "#0d1117", height: 35, symbolColor: "#7d8590" },
|
||||
backgroundColor: "#0d1117",
|
||||
trafficLightPosition: { x: 10, y: 10 },
|
||||
icon: join(process.env.PUBLIC, "favicon.ico"),
|
||||
webPreferences: {
|
||||
preload,
|
||||
// Warning: Enable nodeIntegration and disable contextIsolation is not secure in production
|
||||
// Consider using contextBridge.exposeInMainWorld
|
||||
// Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
},
|
||||
});
|
||||
|
||||
const workspace = { projectPath, window: win };
|
||||
windows.push(workspace);
|
||||
|
||||
win.maximizable = true;
|
||||
|
||||
if (process.env.VITE_DEV_SERVER_URL) {
|
||||
// electron-vite-vue#298
|
||||
win.loadURL(url);
|
||||
// Open devTool if the app is not packaged
|
||||
win.webContents.openDevTools();
|
||||
} else {
|
||||
win.maximize();
|
||||
win.loadFile(indexHtml);
|
||||
}
|
||||
|
||||
win.webContents.on("did-finish-load", () => {
|
||||
win.webContents.setZoomFactor(1);
|
||||
if (workspace.projectPath) {
|
||||
win?.webContents.send("open-project", workspace.projectPath);
|
||||
}
|
||||
|
||||
const nextWin = BrowserWindow.getAllWindows().at(-1);
|
||||
if (nextWin) {
|
||||
nextWin.focus();
|
||||
nextWin.webContents.send("refresh-app-men");
|
||||
}
|
||||
});
|
||||
|
||||
win.on("closed", () => {
|
||||
const index = windows.findIndex((w) => w.window === win);
|
||||
windows.splice(index, 1);
|
||||
});
|
||||
|
||||
// Make all links open with the browser, not with the application
|
||||
win.webContents.setWindowOpenHandler(({ url }) => {
|
||||
if (url.startsWith("https:")) shell.openExternal(url);
|
||||
return { action: "deny" };
|
||||
});
|
||||
|
||||
require("@electron/remote/main").enable(win.webContents);
|
||||
|
||||
// Apply electron-updater
|
||||
// update(win);
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
require("@electron/remote/main").initialize();
|
||||
createWindow();
|
||||
});
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") app.quit();
|
||||
});
|
||||
|
||||
app.on("second-instance", () => {
|
||||
createWindow();
|
||||
});
|
||||
|
||||
app.on("activate", () => {
|
||||
const allWindows = BrowserWindow.getAllWindows();
|
||||
|
||||
if (allWindows.length) {
|
||||
allWindows[0].focus();
|
||||
} else {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
|
||||
// New window example arg: new windows url
|
||||
ipcMain.handle("open-win", (event, arg: string | undefined) => {
|
||||
if (arg) {
|
||||
let workspace = windows.find((v) => v.projectPath === arg);
|
||||
if (workspace) {
|
||||
workspace.window.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
workspace = windows.find((v) => v.window.webContents.id === event.sender.id);
|
||||
if (!workspace.projectPath) {
|
||||
workspace.projectPath = arg;
|
||||
workspace.window.webContents.send("open-project", arg);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
createWindow(arg);
|
||||
});
|
||||
|
||||
ipcMain.handle("trashItem", (_, arg: string) => {
|
||||
arg = arg.replace(/\//g, path.sep);
|
||||
shell.trashItem(arg).catch((e) => console.error(e));
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
import { app, ipcMain } from 'electron'
|
||||
import {
|
||||
type ProgressInfo,
|
||||
type UpdateDownloadedEvent,
|
||||
autoUpdater
|
||||
} from 'electron-updater'
|
||||
|
||||
export function update(win: Electron.BrowserWindow) {
|
||||
|
||||
// When set to false, the update download will be triggered through the API
|
||||
autoUpdater.autoDownload = false
|
||||
autoUpdater.disableWebInstaller = false
|
||||
autoUpdater.allowDowngrade = false
|
||||
|
||||
// start check
|
||||
autoUpdater.on('checking-for-update', function () { })
|
||||
// update available
|
||||
autoUpdater.on('update-available', (arg) => {
|
||||
win.webContents.send('update-can-available', { update: true, version: app.getVersion(), newVersion: arg?.version })
|
||||
})
|
||||
// update not available
|
||||
autoUpdater.on('update-not-available', (arg) => {
|
||||
win.webContents.send('update-can-available', { update: false, version: app.getVersion(), newVersion: arg?.version })
|
||||
})
|
||||
|
||||
// Checking for updates
|
||||
ipcMain.handle('check-update', async () => {
|
||||
if (!app.isPackaged) {
|
||||
const error = new Error('The update feature is only available after the package.')
|
||||
return { message: error.message, error }
|
||||
}
|
||||
|
||||
try {
|
||||
return await autoUpdater.checkForUpdatesAndNotify()
|
||||
} catch (error) {
|
||||
return { message: 'Network error', error }
|
||||
}
|
||||
})
|
||||
|
||||
// Start downloading and feedback on progress
|
||||
ipcMain.handle('start-download', (event) => {
|
||||
startDownload(
|
||||
(error, progressInfo) => {
|
||||
if (error) {
|
||||
// feedback download error message
|
||||
event.sender.send('update-error', { message: error.message, error })
|
||||
} else {
|
||||
// feedback update progress message
|
||||
event.sender.send('download-progress', progressInfo)
|
||||
}
|
||||
},
|
||||
() => {
|
||||
// feedback update downloaded message
|
||||
event.sender.send('update-downloaded')
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
// Install now
|
||||
ipcMain.handle('quit-and-install', () => {
|
||||
autoUpdater.quitAndInstall(false, true)
|
||||
})
|
||||
}
|
||||
|
||||
function startDownload(
|
||||
callback: (error: Error | null, info: ProgressInfo) => void,
|
||||
complete: (event: UpdateDownloadedEvent) => void,
|
||||
) {
|
||||
autoUpdater.on('download-progress', info => callback(null, info))
|
||||
autoUpdater.on('error', error => callback(error, null))
|
||||
autoUpdater.on('update-downloaded', complete)
|
||||
autoUpdater.downloadUpdate()
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
function domReady(condition: DocumentReadyState[] = ["complete", "interactive"]) {
|
||||
return new Promise((resolve) => {
|
||||
if (condition.includes(document.readyState)) {
|
||||
resolve(true);
|
||||
} else {
|
||||
document.addEventListener("readystatechange", () => {
|
||||
if (condition.includes(document.readyState)) {
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const safeDOM = {
|
||||
append(parent: HTMLElement, child: HTMLElement) {
|
||||
if (!Array.from(parent.children).find((e) => e === child)) {
|
||||
return parent.appendChild(child);
|
||||
}
|
||||
},
|
||||
remove(parent: HTMLElement, child: HTMLElement) {
|
||||
if (Array.from(parent.children).find((e) => e === child)) {
|
||||
return parent.removeChild(child);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* https://tobiasahlin.com/spinkit
|
||||
* https://connoratherton.com/loaders
|
||||
* https://projects.lukehaas.me/css-loaders
|
||||
* https://matejkustec.github.io/SpinThatShit
|
||||
*/
|
||||
function useLoading() {
|
||||
const className = `loaders-css__square-spin`;
|
||||
const styleContent = `
|
||||
@keyframes square-spin {
|
||||
25% { transform: perspective(100px) rotateX(180deg) rotateY(0); }
|
||||
50% { transform: perspective(100px) rotateX(180deg) rotateY(180deg); }
|
||||
75% { transform: perspective(100px) rotateX(0) rotateY(180deg); }
|
||||
100% { transform: perspective(100px) rotateX(0) rotateY(0); }
|
||||
}
|
||||
.${className} > div {
|
||||
animation-fill-mode: both;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: #fff;
|
||||
animation: square-spin 3s 0s cubic-bezier(0.09, 0.57, 0.49, 0.9) infinite;
|
||||
}
|
||||
.app-loading-wrap {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #282c34;
|
||||
z-index: 9;
|
||||
}
|
||||
`;
|
||||
const oStyle = document.createElement("style");
|
||||
const oDiv = document.createElement("div");
|
||||
|
||||
oStyle.id = "app-loading-style";
|
||||
oStyle.innerHTML = styleContent;
|
||||
oDiv.className = "app-loading-wrap";
|
||||
oDiv.innerHTML = `<div class="${className}"><div></div></div>`;
|
||||
|
||||
return {
|
||||
appendLoading() {
|
||||
safeDOM.append(document.head, oStyle);
|
||||
safeDOM.append(document.body, oDiv);
|
||||
},
|
||||
removeLoading() {
|
||||
safeDOM.remove(document.head, oStyle);
|
||||
safeDOM.remove(document.body, oDiv);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
const { appendLoading, removeLoading } = useLoading();
|
||||
domReady().then(appendLoading);
|
||||
|
||||
window.onmessage = (ev) => {
|
||||
ev.data.payload === "removeLoading" && removeLoading();
|
||||
};
|
||||
|
||||
setTimeout(removeLoading, 4999);
|
||||
@@ -1,14 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>行为树编辑器</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="./dist/bundle.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<!-- <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" /> -->
|
||||
<title>Behavior3 Editor</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 草鞋没号
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,474 +0,0 @@
|
||||
import {
|
||||
Menu,
|
||||
app,
|
||||
dialog,
|
||||
BrowserWindow,
|
||||
MenuItem,
|
||||
WebContents,
|
||||
MenuItemConstructorOptions,
|
||||
} from "electron";
|
||||
import * as fs from "fs";
|
||||
import * as Utils from "../common/Utils";
|
||||
import MainEventType from "../common/MainEventType";
|
||||
import { MainProcess } from "./MainProcess";
|
||||
import Settings from "./Settings";
|
||||
|
||||
const packageConf = require("../package.json");
|
||||
|
||||
export default class AppMenu {
|
||||
private mainProcess: MainProcess;
|
||||
private mainWindow: BrowserWindow;
|
||||
private webContents: WebContents;
|
||||
private settings: Settings;
|
||||
private buildDir?: string;
|
||||
|
||||
constructor(mainProcess: MainProcess) {
|
||||
this.mainProcess = mainProcess;
|
||||
this.mainWindow = mainProcess.mainWindow;
|
||||
this.webContents = mainProcess.mainWindow.webContents;
|
||||
this.settings = mainProcess.settings;
|
||||
}
|
||||
|
||||
createMenu() {
|
||||
const menu: Menu = new Menu();
|
||||
menu.append(this.createFileMenu());
|
||||
menu.append(this.createEditMenu());
|
||||
menu.append(this.createNodeMenu());
|
||||
menu.append(this.createWorkspaceMenu());
|
||||
menu.append(this.createToolsMenu());
|
||||
return menu;
|
||||
}
|
||||
|
||||
private createFileMenu() {
|
||||
const fileItems: MenuItemConstructorOptions[] = [];
|
||||
for (let path of this.settings.recentFiles) {
|
||||
fileItems.push({
|
||||
label: path,
|
||||
click: () => {
|
||||
console.log("open recent file", path);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return new MenuItem({
|
||||
label: "行为树",
|
||||
submenu: [
|
||||
{
|
||||
label: "新建",
|
||||
accelerator: "CmdOrCtrl+n",
|
||||
click: () => {
|
||||
(async () => {
|
||||
const res = await dialog.showSaveDialog({
|
||||
properties: ["showOverwriteConfirmation"],
|
||||
filters: [{ name: "Json", extensions: ["json"] }],
|
||||
});
|
||||
if (!res.canceled) {
|
||||
const path = res.filePath;
|
||||
fs.writeFileSync(
|
||||
path,
|
||||
JSON.stringify(Utils.createNewTree(path), null, 2)
|
||||
);
|
||||
this.webContents.send(MainEventType.CREATE_TREE, path);
|
||||
}
|
||||
})();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "打开文件",
|
||||
accelerator: "CmdOrCtrl+O",
|
||||
click: () => {
|
||||
(async () => {
|
||||
const res = await dialog.showOpenDialog({
|
||||
properties: ["openFile"],
|
||||
filters: [{ name: "Json", extensions: ["json"] }],
|
||||
});
|
||||
if (res.filePaths.length > 0) {
|
||||
const path = res.filePaths[0];
|
||||
if (this.settings.recentFiles.indexOf(path) < 0) {
|
||||
this.settings.recentFiles.unshift(path);
|
||||
this.settings.save();
|
||||
this.mainProcess.rebuildMenu();
|
||||
}
|
||||
this.webContents.send(MainEventType.OPEN_FILE, path);
|
||||
}
|
||||
})();
|
||||
},
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "最近打开",
|
||||
submenu: fileItems,
|
||||
},
|
||||
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "保存",
|
||||
accelerator: "CmdOrCtrl+s",
|
||||
click: () => {
|
||||
this.webContents.send(MainEventType.SAVE);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "全部保存",
|
||||
accelerator: "CmdOrCtrl+shift+s",
|
||||
click: () => {
|
||||
this.webContents.send(MainEventType.SAVE_ALL);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "保存为子树",
|
||||
click: async () => {
|
||||
const res = await dialog.showSaveDialog({
|
||||
defaultPath: this.settings.workdir,
|
||||
properties: ["showOverwriteConfirmation"],
|
||||
filters: [{ name: "Json", extensions: ["json"] }],
|
||||
});
|
||||
if (!res.canceled) {
|
||||
const path = res.filePath;
|
||||
this.webContents.send(MainEventType.SAVE_AS_SUBTREE, path);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "合并导出Json",
|
||||
click: () => {
|
||||
(async () => {
|
||||
const res = await dialog.showSaveDialog({
|
||||
properties: ["showOverwriteConfirmation"],
|
||||
filters: [{ name: "Json", extensions: ["json"] }],
|
||||
});
|
||||
if (res.filePath) {
|
||||
const curWorkspace = this.settings.curWorkspace;
|
||||
curWorkspace.writeAllTrees(res.filePath, (err) => {
|
||||
const msg = err ? err : "导出成功";
|
||||
dialog.showMessageBox({
|
||||
type: "info",
|
||||
buttons: ["ok"],
|
||||
message: msg,
|
||||
});
|
||||
});
|
||||
}
|
||||
})();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "构建",
|
||||
accelerator: "CmdOrCtrl+b",
|
||||
click: async () => {
|
||||
if (!this.buildDir) {
|
||||
const res = await dialog.showOpenDialog({
|
||||
properties: ["openDirectory"],
|
||||
});
|
||||
if (res.canceled) {
|
||||
return;
|
||||
}
|
||||
this.buildDir = res.filePaths[0];
|
||||
}
|
||||
this.webContents.send(MainEventType.BUILD, this.buildDir);
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
private createEditMenu() {
|
||||
return new MenuItem({
|
||||
label: "编辑",
|
||||
submenu: [
|
||||
{
|
||||
label: "撤销",
|
||||
accelerator: "CmdOrCtrl+z",
|
||||
role: "undo",
|
||||
},
|
||||
{
|
||||
label: "恢复",
|
||||
accelerator: process.platform === "darwin" ? "cmd+shift+z" : "ctrl+y",
|
||||
role: "redo",
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "新建节点",
|
||||
accelerator: process.platform === "darwin" ? "enter" : "insert",
|
||||
click: () => {
|
||||
this.webContents.send(MainEventType.CREATE_NODE, "unknow");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "删除节点",
|
||||
accelerator: process.platform === "darwin" ? "backspace" : "delete",
|
||||
click: () => {
|
||||
this.webContents.send(MainEventType.DELETE_NODE);
|
||||
},
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "复制节点",
|
||||
role: "copy",
|
||||
},
|
||||
{
|
||||
label: "粘贴节点",
|
||||
role: "paste",
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
private createWorkspaceMenu() {
|
||||
const openWorkspace = (path: string) => {
|
||||
const curWorkspace = this.settings.curWorkspace;
|
||||
curWorkspace.setFilepath(path);
|
||||
curWorkspace.load();
|
||||
this.settings.pushRecentWorkspace(path);
|
||||
this.mainProcess.rebuildMenu();
|
||||
this.webContents.send(
|
||||
MainEventType.OPEN_DIR,
|
||||
curWorkspace.getWorkdir(),
|
||||
curWorkspace.getFilepath()
|
||||
);
|
||||
};
|
||||
const saveToNewPath = () => {
|
||||
(async () => {
|
||||
const res = await dialog.showSaveDialog({
|
||||
properties: ["showOverwriteConfirmation"],
|
||||
filters: [{ name: "Json", extensions: ["json"] }],
|
||||
});
|
||||
if (!res.canceled) {
|
||||
this.settings.curWorkspace.setFilepath(res.filePath);
|
||||
this.settings.curWorkspace.save();
|
||||
openWorkspace(res.filePath);
|
||||
}
|
||||
})();
|
||||
};
|
||||
const recentItems: MenuItemConstructorOptions[] = [];
|
||||
for (let path of this.settings.recentWorkspaces) {
|
||||
recentItems.push({
|
||||
label: path,
|
||||
click: () => {
|
||||
console.log("open recent workspace", path);
|
||||
openWorkspace(path);
|
||||
},
|
||||
});
|
||||
}
|
||||
return new MenuItem({
|
||||
label: "工作区",
|
||||
submenu: [
|
||||
{
|
||||
label: "打开",
|
||||
click: () => {
|
||||
(async () => {
|
||||
const res = await dialog.showOpenDialog({
|
||||
properties: ["openFile"],
|
||||
filters: [{ name: "Json", extensions: ["json"] }],
|
||||
});
|
||||
if (res.filePaths.length > 0) {
|
||||
openWorkspace(res.filePaths[0]);
|
||||
}
|
||||
})();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "保存",
|
||||
click: () => {
|
||||
if (this.settings.curWorkspace.getFilepath()) {
|
||||
this.settings.curWorkspace.save();
|
||||
} else {
|
||||
saveToNewPath();
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "另存为",
|
||||
click: () => {
|
||||
saveToNewPath();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "刷新",
|
||||
accelerator: "F5",
|
||||
click: () => {
|
||||
console.log("reload workspace");
|
||||
openWorkspace(this.settings.curWorkspace.getFilepath());
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "最近打开",
|
||||
submenu: recentItems,
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "打开目录",
|
||||
accelerator: "CmdOrCtrl+Shift+O",
|
||||
click: () => {
|
||||
if (!this.settings.curWorkspace.getNodeConfPath()) {
|
||||
dialog.showErrorBox("警告", "请先指定节点定义配置!");
|
||||
return;
|
||||
}
|
||||
(async () => {
|
||||
const res = await dialog.showOpenDialog({
|
||||
properties: ["openDirectory"],
|
||||
});
|
||||
if (res.filePaths.length > 0) {
|
||||
const curWorkspace = this.settings.curWorkspace;
|
||||
const path = res.filePaths[0];
|
||||
curWorkspace.setWorkdir(path);
|
||||
curWorkspace.save();
|
||||
this.webContents.send(
|
||||
MainEventType.OPEN_DIR,
|
||||
path,
|
||||
curWorkspace.getFilepath()
|
||||
);
|
||||
}
|
||||
})();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "节点定义",
|
||||
submenu: [
|
||||
{
|
||||
label: "选择文件",
|
||||
click: () => {
|
||||
(async () => {
|
||||
const res = await dialog.showOpenDialog({
|
||||
properties: ["openFile"],
|
||||
filters: [{ name: "Json", extensions: ["json"] }],
|
||||
});
|
||||
if (res.filePaths.length > 0) {
|
||||
const nodeConfigPath = res.filePaths[0];
|
||||
this.settings.curWorkspace.setNodeConfPath(nodeConfigPath);
|
||||
this.mainProcess.rebuildMenu();
|
||||
}
|
||||
})();
|
||||
},
|
||||
},
|
||||
{ type: "separator" },
|
||||
{ label: this.settings.nodeConfPath },
|
||||
],
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "关闭",
|
||||
click: () => {
|
||||
app.quit();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
private createToolsMenu() {
|
||||
const serverItems: MenuItemConstructorOptions[] = [];
|
||||
const curServerName = this.settings.serverName;
|
||||
for (let model of this.settings.curWorkspace.getServers()) {
|
||||
serverItems.push({
|
||||
label: `${model.name} ${model.host}`,
|
||||
type: "checkbox",
|
||||
checked: curServerName == model.name,
|
||||
click: () => {
|
||||
this.settings.serverName = model.name;
|
||||
this.mainProcess.rebuildMenu();
|
||||
},
|
||||
});
|
||||
}
|
||||
return new MenuItem({
|
||||
label: "开发工具",
|
||||
submenu: [
|
||||
{
|
||||
label: "热更",
|
||||
accelerator: "CmdOrCtrl+R",
|
||||
click: () => {
|
||||
this.webContents.send(MainEventType.RELOAD_SERVER);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "联调服务器",
|
||||
submenu: serverItems,
|
||||
},
|
||||
{
|
||||
label: "批处理脚本",
|
||||
click: () => {
|
||||
(async () => {
|
||||
const res = await dialog.showOpenDialog({
|
||||
properties: ["openFile"],
|
||||
filters: [{ name: "Javascript", extensions: ["js"] }],
|
||||
});
|
||||
if (res.filePaths.length > 0) {
|
||||
this.webContents.send(MainEventType.BATCH_EXEC, res.filePaths[0]);
|
||||
}
|
||||
})();
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
label: "打开控制台",
|
||||
accelerator: "CmdOrCtrl+Shift+I",
|
||||
click: (_, browserWindow) => {
|
||||
browserWindow.webContents.toggleDevTools();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "重载编辑器",
|
||||
click: (_, browserWindow) => {
|
||||
browserWindow.reload();
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
label: `当前版本:${packageConf.version}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
private createNodeMenu() {
|
||||
const classifyItems: MenuItemConstructorOptions[] = [];
|
||||
const map: { [key: string]: MenuItemConstructorOptions } = {};
|
||||
for (let t of this.settings.nodeClassify) {
|
||||
let item: MenuItemConstructorOptions = {
|
||||
id: t.classify,
|
||||
label: `${t.classify}(${t.desc})`,
|
||||
submenu: [],
|
||||
};
|
||||
classifyItems.push(item);
|
||||
map[t.classify] = item;
|
||||
}
|
||||
const other: MenuItemConstructorOptions = {
|
||||
id: "other",
|
||||
label: "其它",
|
||||
submenu: [],
|
||||
};
|
||||
var hasOther = false;
|
||||
|
||||
for (let node of this.settings.nodeConfig) {
|
||||
const item: MenuItemConstructorOptions = {
|
||||
label: `${node.name}(${node.desc})`,
|
||||
click: () => {
|
||||
console.log("create node", node.name);
|
||||
this.webContents.send(MainEventType.CREATE_NODE, node.name);
|
||||
},
|
||||
};
|
||||
let typeItem = map[node.type];
|
||||
if (!typeItem) {
|
||||
typeItem = other;
|
||||
hasOther = true;
|
||||
}
|
||||
if (typeItem.submenu instanceof Menu) {
|
||||
console.log("typeItem.submenu error", typeItem);
|
||||
} else {
|
||||
(typeItem.submenu as MenuItemConstructorOptions[]).push(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasOther) {
|
||||
classifyItems.push(other);
|
||||
}
|
||||
|
||||
return new MenuItem({
|
||||
label: "新建节点",
|
||||
submenu: classifyItems,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import { app, BrowserWindow, Menu, MenuItem, dialog, nativeTheme } from "electron";
|
||||
import AppMenu from "./AppMenu";
|
||||
import Settings from "./Settings";
|
||||
import MainEventType from "../common/MainEventType";
|
||||
import electronLocalshortcut from "electron-localshortcut";
|
||||
|
||||
// 一些暴露给render-process的全局变量
|
||||
export interface Global {
|
||||
settings: Settings;
|
||||
}
|
||||
declare var global: Global;
|
||||
|
||||
export class MainProcess {
|
||||
mainWindow: BrowserWindow;
|
||||
appMenu: AppMenu;
|
||||
settings: Settings;
|
||||
constructor() {
|
||||
nativeTheme.themeSource = "dark";
|
||||
app.on("ready", () => {
|
||||
this.createWindow();
|
||||
this.registerShortcut("CmdOrCtrl+C", () => {
|
||||
this.mainWindow.webContents.send(MainEventType.COPY_NODE);
|
||||
});
|
||||
this.registerShortcut("CmdOrCtrl+V", () => {
|
||||
this.mainWindow.webContents.send(MainEventType.PASTE_NODE);
|
||||
});
|
||||
this.registerShortcut("CmdOrCtrl+Z", () => {
|
||||
this.mainWindow.webContents.send(MainEventType.UNDO);
|
||||
});
|
||||
this.registerShortcut(process.platform === "darwin" ? "Cmd+Shift+Z" : "ctrl+y", () => {
|
||||
this.mainWindow.webContents.send(MainEventType.REDO);
|
||||
});
|
||||
});
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
app.on("activate", () => {
|
||||
if (this.mainWindow === null) {
|
||||
this.createWindow();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
registerShortcut(accelerator: string | string[], callback: () => void) {
|
||||
electronLocalshortcut.register(this.mainWindow, accelerator, callback);
|
||||
}
|
||||
|
||||
createWindow() {
|
||||
this.settings = new Settings();
|
||||
global.settings = this.settings;
|
||||
|
||||
this.mainWindow = new BrowserWindow({
|
||||
width: 1280,
|
||||
height: 800,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
},
|
||||
// fullscreenable:false,
|
||||
// maximizable:false
|
||||
});
|
||||
|
||||
require("@electron/remote/main").initialize();
|
||||
require("@electron/remote/main").enable(this.mainWindow.webContents);
|
||||
this.mainWindow.maximize();
|
||||
// mainWindow.webContents.openDevTools();
|
||||
this.mainWindow.loadFile("index.html");
|
||||
this.mainWindow.on("closed", function () {
|
||||
this.mainWindow = null;
|
||||
});
|
||||
this.appMenu = new AppMenu(this);
|
||||
this.rebuildMenu();
|
||||
}
|
||||
|
||||
rebuildMenu() {
|
||||
Menu.setApplicationMenu(this.appMenu.createMenu());
|
||||
}
|
||||
}
|
||||
|
||||
export default new MainProcess();
|
||||
@@ -1,168 +0,0 @@
|
||||
import * as fs from "fs";
|
||||
import Workspace from "./Workspace";
|
||||
import { app } from "electron";
|
||||
|
||||
export interface BehaviorNodeClassify {
|
||||
classify: string;
|
||||
desc?: string;
|
||||
}
|
||||
|
||||
export interface SettingsModel {
|
||||
recentWorkspaces?: string[];
|
||||
recentFiles?: string[];
|
||||
serverName?: string;
|
||||
nodeClassify?: BehaviorNodeClassify[];
|
||||
treesDesc?: { [name: string]: string };
|
||||
}
|
||||
|
||||
const settingPath = app.getPath("userData") + "/settings.json";
|
||||
|
||||
console.log("setting path:", settingPath);
|
||||
|
||||
const sampleNodeClassify: BehaviorNodeClassify[] = [
|
||||
{ classify: "Composite", desc: "复合节点" },
|
||||
{ classify: "Decorator", desc: "修饰节点" },
|
||||
{ classify: "Condition", desc: "条件节点" },
|
||||
{ classify: "Action", desc: "行为节点" },
|
||||
];
|
||||
|
||||
export default class Settings {
|
||||
private model: SettingsModel;
|
||||
private dirty: boolean = false;
|
||||
curWorkspace?: Workspace;
|
||||
|
||||
constructor() {
|
||||
this.load();
|
||||
|
||||
setInterval(() => {
|
||||
if (this.dirty) {
|
||||
fs.writeFileSync(settingPath, JSON.stringify(this.model, null, 2));
|
||||
this.dirty = false;
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
get nodeConfPath() {
|
||||
return this.curWorkspace.getNodeConfPath();
|
||||
}
|
||||
get recentWorkspaces() {
|
||||
return this.model.recentWorkspaces;
|
||||
}
|
||||
get recentFiles() {
|
||||
return this.model.recentFiles;
|
||||
}
|
||||
get nodeConfig() {
|
||||
return this.curWorkspace.nodeConf;
|
||||
}
|
||||
get nodeClassify() {
|
||||
return this.model.nodeClassify;
|
||||
}
|
||||
get serverModel() {
|
||||
const servers = this.curWorkspace.getServers();
|
||||
if (!servers) {
|
||||
return;
|
||||
}
|
||||
for (let server of servers) {
|
||||
if (server.name == this.model.serverName) {
|
||||
return server;
|
||||
}
|
||||
}
|
||||
return servers[0];
|
||||
}
|
||||
get serverName() {
|
||||
const servers = this.curWorkspace.getServers();
|
||||
if (!servers) {
|
||||
return "";
|
||||
}
|
||||
for (let server of servers) {
|
||||
if (server.name == this.model.serverName) {
|
||||
return this.model.serverName;
|
||||
}
|
||||
}
|
||||
if (servers[0]) {
|
||||
return servers[0].name;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
set serverName(name: string) {
|
||||
this.model.serverName = name;
|
||||
this.save();
|
||||
}
|
||||
|
||||
set(config: SettingsModel) {
|
||||
this.model = {
|
||||
...this.model,
|
||||
...config,
|
||||
};
|
||||
this.save();
|
||||
}
|
||||
|
||||
load() {
|
||||
if (fs.existsSync(settingPath)) {
|
||||
const str = fs.readFileSync(settingPath, "utf8");
|
||||
this.model = JSON.parse(str);
|
||||
if (!this.model.treesDesc) {
|
||||
this.model.treesDesc = {};
|
||||
this.save();
|
||||
}
|
||||
} else {
|
||||
this.model = {
|
||||
recentWorkspaces: ["sample/workspace.json"],
|
||||
recentFiles: [],
|
||||
nodeClassify: sampleNodeClassify,
|
||||
treesDesc: {},
|
||||
};
|
||||
this.save();
|
||||
}
|
||||
this.curWorkspace = new Workspace(this.model.recentWorkspaces[0]);
|
||||
this.curWorkspace.load();
|
||||
}
|
||||
|
||||
save() {
|
||||
this.dirty = true;
|
||||
}
|
||||
|
||||
get workdir() {
|
||||
return this.curWorkspace.getWorkdir();
|
||||
}
|
||||
|
||||
getNodeConf(name: string) {
|
||||
return this.curWorkspace.getNodeConf(name);
|
||||
}
|
||||
|
||||
hasNodeConf(name: string) {
|
||||
return this.curWorkspace.hasNodeConf(name);
|
||||
}
|
||||
|
||||
pushRecentWorkspace(path: string) {
|
||||
var list = this.model.recentWorkspaces;
|
||||
if (list.indexOf(path) >= 0) {
|
||||
list = list.filter((value) => value !== path);
|
||||
}
|
||||
list.unshift(path);
|
||||
while (list.length > 10) {
|
||||
list.pop();
|
||||
}
|
||||
this.model.recentWorkspaces = list;
|
||||
this.save();
|
||||
}
|
||||
|
||||
getTreeDesc(name: string) {
|
||||
const key = this.curWorkspace.getFilepath() + " " + name;
|
||||
var desc = this.model.treesDesc[key];
|
||||
if (!desc) {
|
||||
const str = fs.readFileSync(name, "utf8");
|
||||
const tree = JSON.parse(str);
|
||||
desc = tree.desc ? tree.desc : "";
|
||||
this.model.treesDesc[key] = desc;
|
||||
this.save();
|
||||
}
|
||||
return desc;
|
||||
}
|
||||
|
||||
setTreeDesc(name: string, desc?: string) {
|
||||
const key = this.curWorkspace.getFilepath() + " " + name;
|
||||
this.model.treesDesc[key] = desc;
|
||||
}
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
import * as fs from "fs";
|
||||
import { BehaviorNodeTypeModel } from "../common/BehaviorTreeModel";
|
||||
import * as glob from "glob";
|
||||
import * as path from "path";
|
||||
|
||||
export interface ServerModel {
|
||||
name: string;
|
||||
host: string;
|
||||
}
|
||||
|
||||
export interface WorkspaceModel {
|
||||
isRelative?: boolean;
|
||||
nodeConfPath: string;
|
||||
workdir: string;
|
||||
servers?: ServerModel[];
|
||||
}
|
||||
|
||||
const unknowNodeType: BehaviorNodeTypeModel = {
|
||||
name: "unknow",
|
||||
desc: "新建节点",
|
||||
type: "Action",
|
||||
};
|
||||
|
||||
export default class Workspace {
|
||||
private filepath: string;
|
||||
private nodeConfPath: string;
|
||||
private workdir: string;
|
||||
private model: WorkspaceModel;
|
||||
|
||||
private name2conf: { [name: string]: BehaviorNodeTypeModel } = {};
|
||||
private types: BehaviorNodeTypeModel[] = [];
|
||||
|
||||
constructor(filepath: string) {
|
||||
this.filepath = filepath;
|
||||
}
|
||||
|
||||
load() {
|
||||
if (!this.filepath) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const str = fs.readFileSync(this.filepath, "utf8");
|
||||
const model = JSON.parse(str) as WorkspaceModel;
|
||||
if (model.isRelative) {
|
||||
const root = path.dirname(this.filepath);
|
||||
this.nodeConfPath = path.join(root, model.nodeConfPath);
|
||||
this.workdir = path.join(root, model.workdir);
|
||||
} else {
|
||||
this.nodeConfPath = model.nodeConfPath;
|
||||
this.workdir = model.workdir;
|
||||
}
|
||||
this.model = model;
|
||||
|
||||
this.initNodeConf();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
private initNodeConf() {
|
||||
if (!this.nodeConfPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const types: BehaviorNodeTypeModel[] = JSON.parse(
|
||||
fs.readFileSync(this.nodeConfPath, "utf8")
|
||||
);
|
||||
this.name2conf = {};
|
||||
types.forEach((t) => {
|
||||
this.name2conf[t.name] = t;
|
||||
});
|
||||
this.types = types;
|
||||
}
|
||||
|
||||
getNodeConfPath() {
|
||||
return this.nodeConfPath;
|
||||
}
|
||||
setNodeConfPath(path: string) {
|
||||
this.nodeConfPath = path;
|
||||
this.initNodeConf();
|
||||
}
|
||||
getWorkdir(): string {
|
||||
return this.workdir;
|
||||
}
|
||||
setWorkdir(workdir: string) {
|
||||
this.workdir = workdir;
|
||||
}
|
||||
getServers() {
|
||||
if (this.model && this.model.servers) {
|
||||
return this.model.servers;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
getModel() {
|
||||
return this.model;
|
||||
}
|
||||
|
||||
writeAllTrees(outFilePath: string, cb?: (err: string) => void) {
|
||||
if (!fs.existsSync(this.getWorkdir())) {
|
||||
cb && cb("请先打开工作区");
|
||||
return;
|
||||
}
|
||||
glob.glob(path.join(this.getWorkdir(), "**/*.json"), function (err, files) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
cb && cb(err.message);
|
||||
return;
|
||||
}
|
||||
|
||||
let result = new Array();
|
||||
files.forEach((file) => {
|
||||
const data = fs.readFileSync(file, "utf8");
|
||||
result.push(JSON.parse(data));
|
||||
});
|
||||
|
||||
fs.writeFileSync(outFilePath, JSON.stringify(result, null, 2));
|
||||
cb && cb(null);
|
||||
});
|
||||
}
|
||||
|
||||
save(filepath?: string) {
|
||||
if (filepath) {
|
||||
this.filepath = filepath;
|
||||
}
|
||||
if (!this.filepath) {
|
||||
return;
|
||||
}
|
||||
fs.writeFileSync(
|
||||
this.filepath,
|
||||
JSON.stringify(
|
||||
{
|
||||
nodeConfPath: this.nodeConfPath,
|
||||
workdir: this.workdir,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
get nodeConf() {
|
||||
return this.types;
|
||||
}
|
||||
|
||||
getFilepath() {
|
||||
return this.filepath;
|
||||
}
|
||||
|
||||
setFilepath(filepath: string) {
|
||||
this.filepath = filepath;
|
||||
}
|
||||
|
||||
getNodeConf(name: string) {
|
||||
return this.name2conf[name] || unknowNodeType;
|
||||
}
|
||||
|
||||
hasNodeConf(name: string) {
|
||||
return !!this.name2conf[name];
|
||||
}
|
||||
}
|
||||
@@ -1,69 +1,86 @@
|
||||
{
|
||||
"name": "behavior3editor",
|
||||
"version": "0.2.3",
|
||||
"version": "1.0.0",
|
||||
"main": "dist-electron/main/index.js",
|
||||
"description": "行为树编辑器",
|
||||
"main": "./dist/main.js",
|
||||
"author": "zhandouxiaojiji & codetypes",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"debug": {
|
||||
"env": {
|
||||
"VITE_DEV_SERVER_URL": "http://127.0.0.1:7777/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"prestart": "webpack --config webpack.config.js",
|
||||
"start": "electron .",
|
||||
"build": "electron-packager ./ behavior3editor --platform=win32 --arch=x64 --out=./build --overwrite",
|
||||
"build-mac": "electron-packager ./ behavior3editor --platform=darwin --arch=universal --out=./build --overwrite",
|
||||
"dist-mac": "npm run prestart && electron-builder --dir && electron-builder --mac",
|
||||
"dist": "npm run prestart && electron-builder --dir && electron-builder --win --x64"
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build && electron-builder",
|
||||
"tsc-check:": "tsc --noEmit",
|
||||
"postinstall": "copy-files-from-to"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+ssh://git@github.com/zhandouxiaojiji/behavior3editor.git"
|
||||
},
|
||||
"author": "zhandouxiaojiji",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/zhandouxiaojiji/behavior3editor/issues"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.example.app",
|
||||
"directories": {
|
||||
"output": "build"
|
||||
}
|
||||
},
|
||||
|
||||
"homepage": "https://github.com/zhandouxiaojiji/behavior3editor#readme",
|
||||
"devDependencies": {
|
||||
"@types/electron-localshortcut": "^3.1.3",
|
||||
"@types/glob": "^8.1.0",
|
||||
"@types/node": "^20.11.25",
|
||||
"@types/rimraf": "^3.0.2",
|
||||
"copy-webpack-plugin": "^12.0.2",
|
||||
"css-loader": "^6.10.0",
|
||||
"electron": "^29.1.1",
|
||||
"electron-builder": "^24.13.3",
|
||||
"electron-localshortcut": "^3.2.1",
|
||||
"electron-packager": "^17.1.2",
|
||||
"file-loader": "^6.2.0",
|
||||
"less": "^4.2.0",
|
||||
"less-loader": "^12.2.0",
|
||||
"node-fetch": "^3.3.2",
|
||||
"source-map-loader": "^5.0.0",
|
||||
"style-loader": "^3.3.4",
|
||||
"ts-loader": "^9.5.1",
|
||||
"typescript": "^5.3.3",
|
||||
"webpack": "^5.90.3",
|
||||
"webpack-cli": "^5.1.4"
|
||||
"@stylistic/eslint-plugin-js": "^1.7.0",
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@typescript-eslint/eslint-plugin": "^7.3.1",
|
||||
"@typescript-eslint/parser": "^7.3.1",
|
||||
"@vitejs/plugin-react": "^3.1.0",
|
||||
"copy-files-from-to": "^3.9.1",
|
||||
"electron": "^23.2.0",
|
||||
"electron-builder": "^23.6.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.6",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"sass": "^1.60.0",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.1.4",
|
||||
"vite-electron-plugin": "^0.8.2",
|
||||
"vite-plugin-electron": "^0.11.1",
|
||||
"vite-plugin-electron-renderer": "^0.13.12"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.3.0",
|
||||
"@antv/g6": "^3.8.5",
|
||||
"@ant-design/icons": "^5.3.3",
|
||||
"@antv/g6": "^4.8.24",
|
||||
"@electron/remote": "^2.1.2",
|
||||
"@types/react": "^16.9.51",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"antd": "^4.6.6",
|
||||
"lodash": "^4.17.20",
|
||||
"react": "^16.13.1",
|
||||
"react-confirm-alert": "^2.6.2",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-markdown": "^5.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"shineout": "^1.6.1",
|
||||
"sync-request": "^6.1.0"
|
||||
}
|
||||
"@types/electron-localshortcut": "^3.1.3",
|
||||
"@types/node": "^20.11.27",
|
||||
"ahooks": "^3.7.10",
|
||||
"antd": "^5.16.0",
|
||||
"electron-localshortcut": "^3.2.1",
|
||||
"electron-updater": "^5.3.0",
|
||||
"i18next": "^23.10.1",
|
||||
"i18next-browser-languagedetector": "^7.2.0",
|
||||
"i18next-http-backend": "^2.5.0",
|
||||
"json-stable-stringify": "^1.1.1",
|
||||
"react-hotkeys-hook": "^4.5.0",
|
||||
"react-i18next": "^14.1.0",
|
||||
"react-icons": "^5.0.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-merge-refs": "^2.1.1",
|
||||
"ts-key-enum": "^2.0.12",
|
||||
"use-force-update": "^1.0.11",
|
||||
"usehooks-ts": "^3.0.1",
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"copyFilesSettings": {
|
||||
"whenFileExists": "overwrite"
|
||||
},
|
||||
"copyFiles": [
|
||||
{
|
||||
"from": "pre-commit",
|
||||
"to": ".git/hooks/pre-commit"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/bin/sh
|
||||
echo -e "\nValidating Javascript:\n"
|
||||
# Check for eslint
|
||||
which tsc &> /dev/null
|
||||
if [[ "$?" == 1 ]]; then
|
||||
echo -e "\t\033[41mPlease install tsc\033[0m"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tsc --noEmit
|
||||
|
||||
if [[ "$?" == 0 ]]; then
|
||||
echo -e "\033[42mCOMMIT SUCCEEDED\033[0m\n"
|
||||
exit 0
|
||||
else
|
||||
echo -e "\033[41mCOMMIT FAILED:\033[0m Your commit contains files that should pass ESLint but do not. Please fix the ESLint errors and try again.\n"
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,55 @@
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="none" x="0px" y="0px" width="128px" height="128px" viewBox="0 0 128 128">
|
||||
<defs>
|
||||
<g id="Layer1_0_FILL">
|
||||
<path fill="#2F3242" stroke="none" d="
|
||||
M 128 64
|
||||
Q 128 37.55 109.25 18.75 90.5 0 64 0 37.5 0 18.75 18.75 0 37.5 0 64 0 90.5 18.75 109.25 37.5 128 64 128 90.5 128 109.25 109.25 128 90.5 128 64 Z"/>
|
||||
</g>
|
||||
|
||||
<g id="Layer0_0_FILL">
|
||||
<path fill="#A2ECFB" stroke="none" d="
|
||||
M 21.05 10.95
|
||||
Q 21.7 10.95 22.2 10.5 22.65 10.05 22.65 9.4
|
||||
L 22.65 4.7
|
||||
Q 22.65 4.05 22.2 3.6 21.7 3.15 21.05 3.15
|
||||
L 16.4 3.15
|
||||
Q 15.75 3.15 15.25 3.6 14.8 4.05 14.8 4.7
|
||||
L 14.8 6.25 14.05 6.25
|
||||
Q 12.75 6.25 11.8 7.15 10.9 8.1 10.9 9.4
|
||||
L 10.9 11.75 7.8 11.75 7.8 10.95
|
||||
Q 7.8 10.3 7.35 9.85 6.9 9.4 6.25 9.4
|
||||
L 3.1 9.4
|
||||
Q 2.45 9.4 2 9.85 1.55 10.3 1.55 10.95
|
||||
L 1.55 14.1
|
||||
Q 1.55 14.75 2 15.2 2.45 15.65 3.1 15.65
|
||||
L 6.25 15.65
|
||||
Q 6.9 15.65 7.35 15.2 7.8 14.75 7.8 14.1
|
||||
L 7.8 13.3 10.9 13.3 10.9 15.65
|
||||
Q 10.9 16.95 11.8 17.85 12.75 18.75 14.05 18.75
|
||||
L 14.8 18.75 14.8 20.35
|
||||
Q 14.8 21 15.25 21.45 15.75 21.9 16.4 21.9
|
||||
L 21.05 21.9
|
||||
Q 21.7 21.9 22.2 21.45 22.65 21 22.65 20.35
|
||||
L 22.65 15.65
|
||||
Q 22.65 15 22.2 14.55 21.7 14.1 21.05 14.1
|
||||
L 16.4 14.1
|
||||
Q 15.75 14.1 15.25 14.55 14.8 15 14.8 15.65
|
||||
L 14.8 17.2 14.05 17.2
|
||||
Q 13.4 17.2 12.95 16.75 12.5 16.3 12.5 15.65
|
||||
L 12.5 9.4
|
||||
Q 12.5 8.75 12.95 8.3 13.4 7.85 14.05 7.85
|
||||
L 14.8 7.85 14.8 9.4
|
||||
Q 14.8 10.05 15.25 10.5 15.75 10.95 16.4 10.95
|
||||
L 21.05 10.95 Z"/>
|
||||
</g>
|
||||
</defs>
|
||||
|
||||
<g transform="matrix( 1, 0, 0, 1, 0,0) ">
|
||||
<use xlink:href="#Layer1_0_FILL"/>
|
||||
</g>
|
||||
|
||||
<g transform="matrix( 3.75091552734375, 0, 0, 3.75091552734375, 12.6,15.75) ">
|
||||
<use xlink:href="#Layer0_0_FILL"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 614 B After Width: | Height: | Size: 614 B |
|
Before Width: | Height: | Size: 857 B After Width: | Height: | Size: 857 B |
|
Before Width: | Height: | Size: 812 B After Width: | Height: | Size: 812 B |
|
Before Width: | Height: | Size: 833 B After Width: | Height: | Size: 833 B |
|
Before Width: | Height: | Size: 772 B After Width: | Height: | Size: 772 B |
|
Before Width: | Height: | Size: 663 B After Width: | Height: | Size: 663 B |
|
Before Width: | Height: | Size: 694 B After Width: | Height: | Size: 694 B |
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"about": "About Behavior3",
|
||||
"actuaSize": "Actual Size",
|
||||
"build": "Build",
|
||||
"batch": "Batch Script",
|
||||
"buildCompleted": "Build completed",
|
||||
"cancel": "Cancel",
|
||||
"copy": "Copy",
|
||||
"createProject": "Create Project",
|
||||
"delete": "Delete",
|
||||
"deleteNode": "Delete Node",
|
||||
"devTools": "Toggle Developer Tools",
|
||||
"donotSave": "Don't Save",
|
||||
"duplicate": "Duplicate",
|
||||
"editSubtree": "Edit Subtree",
|
||||
"forceReload": "Force Reload",
|
||||
"fullscreen": "Toggle Full Screen",
|
||||
"hide": "Hide Behavior3",
|
||||
"hideOthers": "Hide Others",
|
||||
"insertNode": "Insert Node",
|
||||
"language": "Language",
|
||||
"moveToTrash": "Move to Trash",
|
||||
"newFile": "New File...",
|
||||
"newFolder": "New Folder...",
|
||||
"newWindow": "New Window",
|
||||
"open": "Open...",
|
||||
"openProject": "Open Project",
|
||||
"paste": "Paste",
|
||||
"quit": "Quit Behavior3",
|
||||
"recent": "Recent",
|
||||
"redo": "Redo",
|
||||
"reload": "Reload",
|
||||
"rename": "Rename...",
|
||||
"replace": "Replace",
|
||||
"reportIssue": "Report Issue",
|
||||
"revealFileOnMac": "Reveal in Finder",
|
||||
"revealFileOnWindows": "Reveal in File Explorer",
|
||||
"save": "Save",
|
||||
"saveAll": "Save All",
|
||||
"saveAs": "Save As...",
|
||||
"saveAsSubtree": "Save As Subtree",
|
||||
"searchFile": "Search File",
|
||||
"services": "Services",
|
||||
"start": "Start",
|
||||
"undo": "Undo",
|
||||
"unhide": "Show All",
|
||||
"zoomIn": "Zoom In",
|
||||
"zoomOut": "Zoom Out",
|
||||
|
||||
"menu.edit": "Edit",
|
||||
"menu.file": "File",
|
||||
"menu.view": "View",
|
||||
"menu.window": "Window",
|
||||
"menu.help": "Help",
|
||||
|
||||
"workspace.saveAllOnClose": "Do you want to save the changes to the following {{count}} files?",
|
||||
"workspace.saveOnClose": "Do you want to save the changes you made to '{{name}}'?",
|
||||
|
||||
"explorer.deleteFile": "Are you sure you want delete '{{name}}'?",
|
||||
"explorer.deleteFolder": "Are you sure you want delete '{{name}}' and its contents?",
|
||||
"explorer.replaceFile": "Are you sure you want replace '{{name}}'?",
|
||||
"explorer.restoreFileInfo": "You can restore this file from the Trash.",
|
||||
"explorer.title": "Explorer",
|
||||
|
||||
"tree.overview": "Overview",
|
||||
"tree.name": "Tree name",
|
||||
"tree.desc": "Tree desc",
|
||||
|
||||
"regnode.input": "Input: ",
|
||||
"regnode.output": "Output: ",
|
||||
"regnode.args": "Args: ",
|
||||
"regnode.mark": "Mark: ",
|
||||
"regnode.subtree": "Subtree: ",
|
||||
|
||||
"node.args": "Const arguments",
|
||||
"node.codePlaceholder": "a.b * 3 > 2",
|
||||
"node.debug": "Debug",
|
||||
"node.deleteRootNodeDenied": "Unable to delete the root node",
|
||||
"node.editSubtreeDenied": "Unable to edit the subtree node",
|
||||
"node.desc": "Node desc",
|
||||
"node.fileRequired": "{{field}} field is required",
|
||||
"node.id": "Node id",
|
||||
"node.inputVariable": "Input variables",
|
||||
"node.name": "Node name",
|
||||
"node.noNodeSelected": "No node selected",
|
||||
"node.outputVariable": "Output variables",
|
||||
"node.pasteDataError": "Paste data error",
|
||||
"node.rootNodeAsSubtree": "Root node cann't be save as subtree",
|
||||
"node.subtree": "Subtree",
|
||||
"node.subtreePathError": "You should save the subtree inside the current workspace",
|
||||
|
||||
"node.unknown.desc": "New node",
|
||||
"node.alwaysFail.desc": "Always return failure",
|
||||
"node.alwaysFail.doc": "+ Only one child node, multiple child nodes will be executed first\n+ If any child node fails, return failure",
|
||||
"node.alwaysSuccess.desc": "Always return success",
|
||||
"node.alwaysSuccess.doc": "+ Only one child node, multiple child nodes will be executed first\n+ If any child node fails, return success",
|
||||
"node.check.arg1.desc": "Expression",
|
||||
"node.check.desc": "Evaluate expression",
|
||||
"node.check.doc": "+ Do simple calculation, return success or failure",
|
||||
"node.clear.desc": "Clear variable",
|
||||
"node.clear.output1": "Variable name",
|
||||
"node.forEach.desc": "Iterate array",
|
||||
"node.forEach.input1": "Array",
|
||||
"node.forEach.output1": "Variable name",
|
||||
"node.forEach.doc": "+ Before executing each child node, set the current variable to the output\n+ Execute all child nodes\n+ Return success or running",
|
||||
"node.getTime.desc": "Get current time",
|
||||
"node.getTime.output1": "Current time",
|
||||
"node.isNull.desc": "Check variable is null",
|
||||
"node.isNull.input1": "Variable",
|
||||
"node.log.desc": "Print log",
|
||||
"node.log.arg1.desc": "Log content",
|
||||
"node.loop.desc": "Loop execute",
|
||||
"node.loop.arg1.desc": "Loop times",
|
||||
"node.loop.input1": "Loop times",
|
||||
"node.not.desc": "Invert",
|
||||
"node.not.doc": "+ Invert the return value",
|
||||
"node.notNull.desc": "Check variable is not null",
|
||||
"node.notNull.input1": "Variable",
|
||||
"node.once.desc": "Execute once",
|
||||
"node.once.doc": "+ Execute all child nodes\n+ Return success or running",
|
||||
"node.parallel.desc": "Parallel execute",
|
||||
"node.parallel.doc": "+ Execute all child nodes\n+ Return success",
|
||||
"node.selector.desc": "Selector execute",
|
||||
"node.selector.doc": "+ Execute all child nodes\n+ Return success or failure",
|
||||
"node.sequence.desc": "Sequence execute",
|
||||
"node.sequence.doc": "+ Execute all child nodes\n+ Return success or failure",
|
||||
"node.wait.desc": "Wait",
|
||||
"node.wait.arg1.desc": "Wait time"
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"about": "关于 Behavior3",
|
||||
"actualSize": "实际大小",
|
||||
"build": "构建",
|
||||
"batch": "批处理脚本",
|
||||
"buildCompleted": "构建完成",
|
||||
"cancel": "取消",
|
||||
"copy": "复制",
|
||||
"createProject": "新建项目",
|
||||
"delete": "删除",
|
||||
"deleteNode": "删除节点",
|
||||
"devTools": "切换开发者工具",
|
||||
"donotSave": "不保存",
|
||||
"duplicate": "复制",
|
||||
"editSubtree": "编辑子树",
|
||||
"forceReload": "强制重新加载",
|
||||
"fullscreen": "切换全屏",
|
||||
"hide": "隐藏 Behavior3",
|
||||
"hideOthers": "隐藏其它",
|
||||
"insertNode": "插入节点",
|
||||
"language": "语言",
|
||||
"moveToTrash": "移至回收站",
|
||||
"newFile": "新建行为树...",
|
||||
"newFolder": "新建目录...",
|
||||
"newWindow": "新建窗口",
|
||||
"open": "打开...",
|
||||
"openProject": "打开项目",
|
||||
"paste": "粘贴",
|
||||
"quit": "退出 Behavior3",
|
||||
"recent": "最近打开",
|
||||
"redo": "重做",
|
||||
"reload": "重新加载",
|
||||
"rename": "重命名...",
|
||||
"replace": "替换",
|
||||
"reportIssue": "问题反馈",
|
||||
"revealFileOnMac": "在 Finder 中显示",
|
||||
"revealFileOnWindows": "在文件管理器中打开",
|
||||
"save": "保存",
|
||||
"saveAll": "保存全部",
|
||||
"saveAs": "另存为...",
|
||||
"saveAsSubtree": "另存为子树",
|
||||
"searchFile": "搜索文件",
|
||||
"services": "服务",
|
||||
"start": "开始",
|
||||
"undo": "撤销",
|
||||
"unhide": "显示所有",
|
||||
"zoomIn": "放大",
|
||||
"zoomOut": "缩小",
|
||||
|
||||
"menu.file": "文件",
|
||||
"menu.edit": "编辑",
|
||||
"menu.view": "视图",
|
||||
"menu.window": "窗口",
|
||||
"menu.help": "帮助",
|
||||
|
||||
"workspace.saveAllOnClose": "你需要保存对以下 {{count}} 个文件的修改吗?",
|
||||
"workspace.saveOnClose": "你需要保存对文件 {{name}} 的修改吗?",
|
||||
|
||||
"explorer.deleteFile": "你确定要删除文件 '{{name}}'?",
|
||||
"explorer.deleteFolder": "你确定要删除目录 '{{name}}'?",
|
||||
"explorer.replaceFile": "你确定要替换文件 '{{name}}'?",
|
||||
"explorer.restoreFileInfo": "你可以从回收站恢复此文件。",
|
||||
"explorer.title": "资源浏览",
|
||||
|
||||
"tree.overview": "概况",
|
||||
"tree.name": "行为树",
|
||||
"tree.desc": "说明",
|
||||
|
||||
"regnode.input": "输入:",
|
||||
"regnode.output": "输出:",
|
||||
"regnode.args": "参数:",
|
||||
"regnode.mark": "备注:",
|
||||
"regnode.subtree": "子树:",
|
||||
|
||||
"node.args": "常量参数",
|
||||
"node.codePlaceholder": "a.b * 3 > 2",
|
||||
"node.debug": "调试",
|
||||
"node.deleteRootNodeDenied": "不能删除根节点",
|
||||
"node.editSubtreeDenied": "不能编辑子树节点",
|
||||
"node.desc": "节点说明",
|
||||
"node.fileRequired": "字段<b>{{field}}</b>必填",
|
||||
"node.id": "节点标识",
|
||||
"node.inputVariable": "输入变量",
|
||||
"node.name": "节点名称",
|
||||
"node.noNodeSelected": "未选择任务节点",
|
||||
"node.outputVariable": "输出变量",
|
||||
"node.pasteDataError": "粘贴数据出错",
|
||||
"node.rootNodeAsSubtree": "根节点不能保存为子树",
|
||||
"node.subtree": "子树",
|
||||
"node.subtreePathError": "子树必须保存在当前工作区的目录内",
|
||||
|
||||
"node.unknown.desc": "新建节点",
|
||||
"node.alwaysFail.desc": "始终返回失败",
|
||||
"node.alwaysFail.doc": "+ 只能有一个子节点,多个仅执行第一个\n+ 不管子节点是否成功都返回失败",
|
||||
"node.alwaysSuccess.desc": "始终返回成功",
|
||||
"node.alwaysSuccess.doc": "+ 只能有一个子节点,多个仅执行第一个\n+ 不管子节点是否成功都返回成功",
|
||||
"node.check.arg1.desc": "表达式",
|
||||
"node.check.desc": "对表达式进行求值",
|
||||
"node.check.doc": "+ 做简单数值公式判定,返回成功或失败",
|
||||
"node.clear.desc": "清除变量",
|
||||
"node.clear.output1": "清除的变量名",
|
||||
"node.forEach.desc": "遍历数组",
|
||||
"node.forEach.input1": "数组",
|
||||
"node.forEach.output1": "变量名",
|
||||
"node.forEach.doc": "+ 每次执行子节点前会设置当前遍历到的变量\n+ 会执行所有子节点\n+ 永远返回成功/正在运行",
|
||||
"node.getTime.desc": "获取当前时间",
|
||||
"node.getTime.output1": "当前时间",
|
||||
"node.isNull.desc": "判断变量是否为空",
|
||||
"node.isNull.input1": "判断的变量",
|
||||
"node.log.desc": "打印日志",
|
||||
"node.log.arg1.desc": "日志内容",
|
||||
"node.loop.desc": "循环执行",
|
||||
"node.loop.arg1.desc": "循环次数",
|
||||
"node.loop.input1": "循环次数",
|
||||
"node.not.desc": "取反",
|
||||
"node.not.doc": "+ 将子节点的返回值取反",
|
||||
"node.notNull.desc": "判断变量是否存在",
|
||||
"node.notNull.input1": "判断的变量",
|
||||
"node.once.desc": "只执行一次",
|
||||
"node.once.doc": "+ 可以接多个子节点,子节点默认全部执行\n+ 被打断后该节点后的子节点依旧不会执行\n+ 该节点执行后永远返回成功",
|
||||
"node.parallel.desc": "并行执行",
|
||||
"node.parallel.doc": "+ 执行所有子节点并返回成功",
|
||||
"node.selector.desc": "选择执行",
|
||||
"node.selector.doc": "+ 一直往下执行,有子节点返回成功则返回成功,若全部节点返回失败则返回失败\n+ 子节点是或 (OR) 的关系",
|
||||
"node.sequence.desc": "顺序执行",
|
||||
"node.sequence.doc": "+ 一直往下执行,只有当所有子节点都返回成功, 才返回成功\n+ 子节点是与(AND)的关系",
|
||||
"node.wait.desc": "等待",
|
||||
"node.wait.arg1.desc": "等待时间"
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="none" x="0px" y="0px" width="128px" height="128px" viewBox="0 0 128 128">
|
||||
<defs>
|
||||
<g id="Layer1_0_FILL">
|
||||
<path fill="#2F3242" stroke="none" d="
|
||||
M 128 64
|
||||
Q 128 37.55 109.25 18.75 90.5 0 64 0 37.5 0 18.75 18.75 0 37.5 0 64 0 90.5 18.75 109.25 37.5 128 64 128 90.5 128 109.25 109.25 128 90.5 128 64 Z"/>
|
||||
</g>
|
||||
|
||||
<g id="Layer0_0_FILL">
|
||||
<path fill="#A2ECFB" stroke="none" d="
|
||||
M 21.05 10.95
|
||||
Q 21.7 10.95 22.2 10.5 22.65 10.05 22.65 9.4
|
||||
L 22.65 4.7
|
||||
Q 22.65 4.05 22.2 3.6 21.7 3.15 21.05 3.15
|
||||
L 16.4 3.15
|
||||
Q 15.75 3.15 15.25 3.6 14.8 4.05 14.8 4.7
|
||||
L 14.8 6.25 14.05 6.25
|
||||
Q 12.75 6.25 11.8 7.15 10.9 8.1 10.9 9.4
|
||||
L 10.9 11.75 7.8 11.75 7.8 10.95
|
||||
Q 7.8 10.3 7.35 9.85 6.9 9.4 6.25 9.4
|
||||
L 3.1 9.4
|
||||
Q 2.45 9.4 2 9.85 1.55 10.3 1.55 10.95
|
||||
L 1.55 14.1
|
||||
Q 1.55 14.75 2 15.2 2.45 15.65 3.1 15.65
|
||||
L 6.25 15.65
|
||||
Q 6.9 15.65 7.35 15.2 7.8 14.75 7.8 14.1
|
||||
L 7.8 13.3 10.9 13.3 10.9 15.65
|
||||
Q 10.9 16.95 11.8 17.85 12.75 18.75 14.05 18.75
|
||||
L 14.8 18.75 14.8 20.35
|
||||
Q 14.8 21 15.25 21.45 15.75 21.9 16.4 21.9
|
||||
L 21.05 21.9
|
||||
Q 21.7 21.9 22.2 21.45 22.65 21 22.65 20.35
|
||||
L 22.65 15.65
|
||||
Q 22.65 15 22.2 14.55 21.7 14.1 21.05 14.1
|
||||
L 16.4 14.1
|
||||
Q 15.75 14.1 15.25 14.55 14.8 15 14.8 15.65
|
||||
L 14.8 17.2 14.05 17.2
|
||||
Q 13.4 17.2 12.95 16.75 12.5 16.3 12.5 15.65
|
||||
L 12.5 9.4
|
||||
Q 12.5 8.75 12.95 8.3 13.4 7.85 14.05 7.85
|
||||
L 14.8 7.85 14.8 9.4
|
||||
Q 14.8 10.05 15.25 10.5 15.75 10.95 16.4 10.95
|
||||
L 21.05 10.95 Z"/>
|
||||
</g>
|
||||
</defs>
|
||||
|
||||
<g transform="matrix( 1, 0, 0, 1, 0,0) ">
|
||||
<use xlink:href="#Layer1_0_FILL"/>
|
||||
</g>
|
||||
|
||||
<g transform="matrix( 3.75091552734375, 0, 0, 3.75091552734375, 12.6,15.75) ">
|
||||
<use xlink:href="#Layer0_0_FILL"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -1,28 +0,0 @@
|
||||
.editor {
|
||||
width : 100%;
|
||||
height : 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.editorBd {
|
||||
width : 100%;
|
||||
height : 100%;
|
||||
display: flex;
|
||||
|
||||
}
|
||||
|
||||
.editorContent {
|
||||
/* min-width: 500px; */
|
||||
height : 100%;
|
||||
}
|
||||
|
||||
.editorSidebar {
|
||||
/* min-width: 500px; */
|
||||
height : 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.mind {
|
||||
width : 100%;
|
||||
height: 800px;
|
||||
}
|
||||
@@ -1,349 +0,0 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Card,
|
||||
Divider,
|
||||
Form,
|
||||
Input,
|
||||
AutoComplete,
|
||||
Select,
|
||||
Switch,
|
||||
InputNumber,
|
||||
notification,
|
||||
message,
|
||||
} from "antd";
|
||||
import { INode } from "@antv/g6/lib/interface/item";
|
||||
import {
|
||||
BehaviorNodeModel,
|
||||
BehaviorNodeTypeModel,
|
||||
ArgsDefType,
|
||||
} from "../../common/BehaviorTreeModel";
|
||||
import Settings from "../../main-process/Settings";
|
||||
import { FormInstance } from "antd/lib/form";
|
||||
import Markdown from "react-markdown";
|
||||
|
||||
const { Item } = Form;
|
||||
const { Option } = Select;
|
||||
|
||||
interface NodePanelProps {
|
||||
isSubtreeNode: boolean;
|
||||
model: BehaviorNodeModel;
|
||||
settings: Settings;
|
||||
updateNode: (id: string, forceUpdate: boolean) => void;
|
||||
pushUndoStack: () => void;
|
||||
}
|
||||
|
||||
interface NodePanelState {}
|
||||
|
||||
export default class NodePanel extends React.Component<NodePanelProps> {
|
||||
formRef = React.createRef<FormInstance>();
|
||||
|
||||
componentDidUpdate() {
|
||||
this.formRef.current.resetFields();
|
||||
this.formRef.current.setFieldsValue(this.getInitialValues());
|
||||
}
|
||||
|
||||
getInitialValues() {
|
||||
const { model } = this.props;
|
||||
const initialValues: any = {
|
||||
name: model.name,
|
||||
desc: model.desc,
|
||||
debug: model.debug,
|
||||
path: model.path,
|
||||
customArgs: model.args ? JSON.stringify(model.args, null, " ") : "",
|
||||
};
|
||||
if (model.args) {
|
||||
for (let k in model.args) {
|
||||
initialValues[`args.${k}`] = model.args[k];
|
||||
}
|
||||
}
|
||||
if (model.input) {
|
||||
model.input.forEach((v, i) => {
|
||||
initialValues[`input.${i}`] = v;
|
||||
});
|
||||
}
|
||||
if (model.output) {
|
||||
model.output.forEach((v, i) => {
|
||||
initialValues[`output.${i}`] = v;
|
||||
});
|
||||
}
|
||||
return initialValues;
|
||||
}
|
||||
|
||||
onFinish = (values: any) => {
|
||||
console.log("Success:", values);
|
||||
const { updateNode, pushUndoStack, model, settings } = this.props;
|
||||
const conf = settings.getNodeConf(values.name);
|
||||
if (!conf) {
|
||||
notification.warn({ message: `节点${values.name}未定义` });
|
||||
return;
|
||||
}
|
||||
var args: any = {};
|
||||
if (values.customArgs) {
|
||||
try {
|
||||
args = JSON.parse(values.customArgs);
|
||||
} catch (e) {
|
||||
message.warn(`您输入的自定义参数不符合json格式${values.customArgs}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
pushUndoStack();
|
||||
|
||||
var forceUpdate = false;
|
||||
if (model.name != values.name) {
|
||||
model.name = values.name;
|
||||
forceUpdate = true;
|
||||
}
|
||||
model.desc = values.desc;
|
||||
model.debug = values.debug;
|
||||
|
||||
if (model.path !== values.path) {
|
||||
model.path = values.path;
|
||||
forceUpdate = true;
|
||||
}
|
||||
|
||||
if (conf.args) {
|
||||
conf.args.forEach((e) => {
|
||||
const k = "args." + e.name;
|
||||
if (e.type.indexOf("number") > 0) {
|
||||
args[e.name] = Number(values[k]);
|
||||
} else {
|
||||
args[e.name] = values[k];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
model.args = args;
|
||||
this.formRef.current.setFieldsValue({
|
||||
customArgs: model.args ? JSON.stringify(model.args, null, " ") : "",
|
||||
});
|
||||
|
||||
if (conf.input) {
|
||||
model.input = [];
|
||||
conf.input.forEach((e, i) => {
|
||||
model.input.push(values["input." + i] || "");
|
||||
});
|
||||
} else {
|
||||
model.input = null;
|
||||
}
|
||||
|
||||
if (conf.output) {
|
||||
model.output = [];
|
||||
conf.output.forEach((e, i) => {
|
||||
model.output.push(values["output." + i] || "");
|
||||
});
|
||||
} else {
|
||||
model.output = null;
|
||||
}
|
||||
|
||||
if (forceUpdate) {
|
||||
this.forceUpdate();
|
||||
}
|
||||
updateNode(model.id.toString(), forceUpdate);
|
||||
};
|
||||
|
||||
onFinishFailed = (errorInfo: any) => {
|
||||
console.log("Failed:", errorInfo);
|
||||
};
|
||||
|
||||
handleSubmit = () => {
|
||||
console.log("handleSubmit");
|
||||
this.formRef.current.submit();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { model, settings, isSubtreeNode } = this.props;
|
||||
const conf = settings.getNodeConf(model.name);
|
||||
const title = conf.desc;
|
||||
|
||||
const options: any = [];
|
||||
settings.nodeConfig.map((e) => {
|
||||
options.push({ label: `${e.name}(${e.desc})`, value: e.name });
|
||||
});
|
||||
|
||||
const layout = {
|
||||
labelCol: { span: 6 },
|
||||
wrapperCol: { span: 18 },
|
||||
};
|
||||
return (
|
||||
<Card title={title} style={{ height: window.screen.height - 100, overflow: "auto" }}>
|
||||
<Form
|
||||
{...layout}
|
||||
name="basic"
|
||||
onFinish={this.onFinish}
|
||||
initialValues={this.getInitialValues()}
|
||||
ref={this.formRef}
|
||||
>
|
||||
<Item label="节点id">
|
||||
<Input value={model.id} disabled={true} />
|
||||
</Item>
|
||||
<Item label="节点名称" name="name">
|
||||
<AutoComplete
|
||||
disabled={isSubtreeNode}
|
||||
options={options}
|
||||
onBlur={this.handleSubmit}
|
||||
filterOption={(inputValue: string, option: any) => {
|
||||
return (
|
||||
option.label.toUpperCase().indexOf(inputValue.toUpperCase()) !==
|
||||
-1
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Item>
|
||||
<Item label="节点说明" name="desc">
|
||||
<Input disabled={isSubtreeNode} onBlur={this.handleSubmit} />
|
||||
</Item>
|
||||
<Item label="调试开关" name="debug" valuePropName="checked">
|
||||
<Switch disabled={isSubtreeNode} onChange={this.handleSubmit} />
|
||||
</Item>
|
||||
<Item label="子树" name="path">
|
||||
<Input disabled={isSubtreeNode && !model.path} onBlur={this.handleSubmit} />
|
||||
</Item>
|
||||
<Markdown source={conf.doc} />
|
||||
{this.renderInputs(conf, isSubtreeNode)}
|
||||
{this.renderArgs(conf, isSubtreeNode)}
|
||||
{this.renderOutputs(conf, isSubtreeNode)}
|
||||
</Form>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
renderArgs(conf: BehaviorNodeTypeModel, isSubtreeNode: boolean) {
|
||||
if (!conf || !conf.args || conf.args.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 普通参数
|
||||
const normalArgs = (e: ArgsDefType) => {
|
||||
const required = e.type.indexOf("?") == -1;
|
||||
if (e.type.indexOf("string") >= 0) {
|
||||
return <Input disabled={isSubtreeNode} onBlur={this.handleSubmit} />;
|
||||
} else if (e.type.indexOf("int") >= 0) {
|
||||
return (
|
||||
<InputNumber
|
||||
disabled={isSubtreeNode}
|
||||
precision={0}
|
||||
style={{ width: "100%" }}
|
||||
onBlur={this.handleSubmit}
|
||||
/>
|
||||
);
|
||||
} else if (e.type.indexOf("float") >= 0) {
|
||||
return (
|
||||
<InputNumber
|
||||
disabled={isSubtreeNode}
|
||||
style={{ width: "100%" }}
|
||||
onBlur={this.handleSubmit}
|
||||
/>
|
||||
);
|
||||
} else if (e.type.indexOf("boolean") >= 0) {
|
||||
return <Switch disabled={isSubtreeNode} onChange={this.handleSubmit} />;
|
||||
} else if (e.type.indexOf("code") >= 0) {
|
||||
return (
|
||||
<Input
|
||||
disabled={isSubtreeNode}
|
||||
onBlur={this.handleSubmit}
|
||||
placeholder={"表达式"}
|
||||
/>
|
||||
);
|
||||
} else if (e.type.indexOf("enum") >= 0) {
|
||||
return (
|
||||
<Select
|
||||
disabled={isSubtreeNode}
|
||||
style={{ width: 120 }}
|
||||
onChange={this.handleSubmit}
|
||||
>
|
||||
{e.options.map((e) => {
|
||||
return (
|
||||
<Option key={e.name} value={e.value}>
|
||||
{e.name}
|
||||
</Option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// 自定义参数
|
||||
const customArgs = () => {
|
||||
return (
|
||||
<Item name="customArgs" label="自定义" key="customArgs">
|
||||
<Input.TextArea
|
||||
disabled={isSubtreeNode}
|
||||
onBlur={this.handleSubmit}
|
||||
style={{ minHeight: 100 }}
|
||||
/>
|
||||
</Item>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Divider orientation="left">
|
||||
<h3>常量参数</h3>
|
||||
</Divider>
|
||||
{conf &&
|
||||
conf.args &&
|
||||
conf.args.map((e, i: number) => {
|
||||
const required = e.type.indexOf("?") == -1;
|
||||
return (
|
||||
<Item
|
||||
initialValue={e.default}
|
||||
name={`args.${e.name}`}
|
||||
label={e.desc}
|
||||
key={`args.${e.name}`}
|
||||
valuePropName={
|
||||
e.type.indexOf("boolean") >= 0 ? "checked" : undefined
|
||||
}
|
||||
rules={[{ required, message: `${e.desc}(${e.name})为必填字段` }]}
|
||||
>
|
||||
{normalArgs(e)}
|
||||
</Item>
|
||||
);
|
||||
})}
|
||||
{customArgs()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderInputs(conf: BehaviorNodeTypeModel, isSubtreeNode: boolean) {
|
||||
if (!conf.input || !conf.input || conf.input.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Divider orientation="left">
|
||||
<h3>输入变量</h3>
|
||||
</Divider>
|
||||
{conf.input.map((e, i) => {
|
||||
return (
|
||||
<Item label={e} name={`input.${i}`} key={`input.${i}`}>
|
||||
<Input disabled={isSubtreeNode} onBlur={this.handleSubmit} />
|
||||
</Item>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderOutputs(conf: BehaviorNodeTypeModel, isSubtreeNode: boolean) {
|
||||
if (!conf.output || !conf.output || conf.output.length == 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<Divider orientation="left">
|
||||
<h3>输出变量</h3>
|
||||
</Divider>
|
||||
{conf.output.map((e, i) => {
|
||||
return (
|
||||
<Item label={e} name={`output.${i}`} key={`output.${i}`}>
|
||||
<Input disabled={isSubtreeNode} onBlur={this.handleSubmit} />
|
||||
</Item>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
import React from "react";
|
||||
import { Button, Card, Form, Input } from "antd";
|
||||
import { BehaviorTreeModel } from "../../common/BehaviorTreeModel";
|
||||
import { FormInstance } from "antd/lib/form";
|
||||
|
||||
const { Item } = Form;
|
||||
|
||||
interface TreePanelProps {
|
||||
model?: BehaviorTreeModel;
|
||||
onRenameTree: (name: string) => void;
|
||||
onRemoveTree: () => void;
|
||||
onChangeTreeDesc: (desc: string) => void;
|
||||
}
|
||||
|
||||
interface TreePanelState {
|
||||
}
|
||||
|
||||
const inlineFormItemLayout = {
|
||||
labelCol: {
|
||||
sm: { span: 4 },
|
||||
},
|
||||
wrapperCol: {
|
||||
sm: { span: 12 },
|
||||
},
|
||||
};
|
||||
|
||||
export default class TreePanel extends React.Component<TreePanelProps, TreePanelState> {
|
||||
name: string = '';
|
||||
desc: string = '';
|
||||
formRef = React.createRef<FormInstance>();
|
||||
|
||||
componentDidMount() {
|
||||
const { model } = this.props;
|
||||
this.name = model.name;
|
||||
this.desc = model.desc;
|
||||
this.formRef.current.resetFields();
|
||||
this.formRef.current.setFieldsValue({
|
||||
name: model.name,
|
||||
desc: model.desc,
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const { model } = this.props;
|
||||
if (!model) {
|
||||
return <div />;
|
||||
}
|
||||
return (
|
||||
<Card title="概况" style={{ height: "100%" }}>
|
||||
<Form
|
||||
{...inlineFormItemLayout}
|
||||
ref={this.formRef}
|
||||
>
|
||||
<Item label="行为树" name="name">
|
||||
<Input disabled onBlur={this.handleSubmit} />
|
||||
</Item>
|
||||
<Item label="说明" name="desc">
|
||||
<Input onBlur={this.handleSubmit} />
|
||||
</Item>
|
||||
{/* <Item>
|
||||
<Button type='ghost' onClick={this.handleRemoveTree}>删除</Button>
|
||||
</Item> */}
|
||||
</Form>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
handleSubmit = () => {
|
||||
const { model, onRenameTree, onChangeTreeDesc } = this.props;
|
||||
const name = this.formRef.current.getFieldValue("name");
|
||||
const desc = this.formRef.current.getFieldValue("desc");
|
||||
if (name != this.name) {
|
||||
console.log("change name", name);
|
||||
this.name = name;
|
||||
model.name = name;
|
||||
onRenameTree(name);
|
||||
}
|
||||
if (desc != this.desc) {
|
||||
console.log("change desc", desc);
|
||||
this.desc = desc;
|
||||
model.desc = desc;
|
||||
onChangeTreeDesc(desc);
|
||||
}
|
||||
}
|
||||
|
||||
handleRemoveTree = () => {
|
||||
this.props.onRemoveTree();
|
||||
}
|
||||
}
|
||||
@@ -1,690 +0,0 @@
|
||||
import G6, { TreeGraph } from "@antv/g6";
|
||||
import { G6GraphEvent } from "@antv/g6/lib/interface/behavior";
|
||||
import { Col, Row, message } from "antd";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as React from "react";
|
||||
import {
|
||||
BehaviorNodeModel,
|
||||
BehaviorTreeModel,
|
||||
GraphNodeModel,
|
||||
} from "../../common/BehaviorTreeModel";
|
||||
import * as Utils from "../../common/Utils";
|
||||
import Settings from "../../main-process/Settings";
|
||||
import NodePanel from "./NodePanel";
|
||||
import TreePanel from "./TreePanel";
|
||||
|
||||
import { Item, Matrix } from "@antv/g6/lib/types";
|
||||
import { clipboard } from "electron";
|
||||
import "./Editor.css";
|
||||
|
||||
export interface EditorProps {
|
||||
filepath: string;
|
||||
onChangeSaveState: (unsave: boolean) => void;
|
||||
onOpenSubtree: (path: string) => void;
|
||||
}
|
||||
|
||||
interface EditorState {
|
||||
curNodeId?: string;
|
||||
blockNodeSelectChange?: boolean;
|
||||
viewportMatrix?: Matrix;
|
||||
}
|
||||
|
||||
export default class Editor extends React.Component<EditorProps, EditorState> {
|
||||
private ref: React.RefObject<any>;
|
||||
state: EditorState = {};
|
||||
isInEditor: boolean = false;
|
||||
|
||||
private graph: TreeGraph;
|
||||
private dragSrcId: string;
|
||||
private dragDstId: string;
|
||||
private autoId: number;
|
||||
private undoStack: BehaviorNodeModel[] = [];
|
||||
private redoStack: BehaviorNodeModel[] = [];
|
||||
private treeModel: BehaviorTreeModel;
|
||||
private settings: Settings;
|
||||
private data: GraphNodeModel;
|
||||
private unsave: boolean = false;
|
||||
|
||||
constructor(props: EditorProps) {
|
||||
super(props);
|
||||
this.ref = React.createRef();
|
||||
|
||||
this.settings = Utils.getRemoteSettings();
|
||||
const str = fs.readFileSync(this.props.filepath, "utf8");
|
||||
this.treeModel = JSON.parse(str);
|
||||
this.data = Utils.createTreeData(this.treeModel.root, this.settings);
|
||||
this.autoId = Utils.refreshNodeId(this.data);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps: EditorProps, nextState: EditorState) {
|
||||
return (
|
||||
this.props.filepath != nextProps.filepath || this.state.curNodeId != nextState.curNodeId
|
||||
);
|
||||
}
|
||||
|
||||
setItemState(item: string | Item, state: string, value: string | boolean) {
|
||||
if (typeof item === "string") {
|
||||
item = this.graph.findById(item);
|
||||
}
|
||||
if (item) {
|
||||
this.graph.setItemState(item, state, value);
|
||||
}
|
||||
}
|
||||
|
||||
findParent(node: GraphNodeModel) {
|
||||
if (node.parent) {
|
||||
return this.graph.findDataById(node.parent) as GraphNodeModel;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
findSubtree(node?: GraphNodeModel): GraphNodeModel | null {
|
||||
if (node?.path) {
|
||||
return node;
|
||||
} else if (node?.parent) {
|
||||
return this.findSubtree(this.findParent(node));
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
isSubtreeNode(node?: GraphNodeModel): boolean {
|
||||
if (node?.path) {
|
||||
return true;
|
||||
} else if (node?.parent) {
|
||||
return this.isSubtreeNode(this.findParent(node));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
isAncestor(ancestor: GraphNodeModel, node: GraphNodeModel): boolean {
|
||||
if (ancestor.id === node.parent) {
|
||||
return true;
|
||||
} else if (node.parent) {
|
||||
return this.isAncestor(
|
||||
ancestor,
|
||||
this.graph.findDataById(node.parent) as GraphNodeModel
|
||||
);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const graph = new TreeGraph({
|
||||
container: this.ref.current,
|
||||
width: window.screen.width * 0.66,
|
||||
height: window.screen.height,
|
||||
animate: false,
|
||||
maxZoom: 2,
|
||||
// fitCenter: true,
|
||||
modes: {
|
||||
default: ["drag-canvas", "zoom-canvas", "click-select", "hover"],
|
||||
},
|
||||
defaultEdge: {
|
||||
type: "cubic-horizontal",
|
||||
style: {
|
||||
stroke: "#A3B1BF",
|
||||
},
|
||||
},
|
||||
defaultNode: {
|
||||
type: "TreeNode",
|
||||
},
|
||||
layout: {
|
||||
type: "compactBox",
|
||||
direction: "LR",
|
||||
getHGap: () => 50,
|
||||
getWidth: (d: GraphNodeModel) => {
|
||||
return 150;
|
||||
},
|
||||
getHeight: (d: GraphNodeModel) => {
|
||||
if (d.size) {
|
||||
return d.size[1];
|
||||
} else {
|
||||
return 50;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
graph.on("viewportchange", (data: any) => {
|
||||
if (data.action == "translate" || data.action == "zoom") {
|
||||
this.state.viewportMatrix = data.matrix;
|
||||
}
|
||||
});
|
||||
|
||||
graph.on("contextmenu", (e: G6GraphEvent) => {
|
||||
require("@electron/remote").Menu.getApplicationMenu().popup();
|
||||
});
|
||||
|
||||
graph.on("canvas:mouseover", () => {
|
||||
this.isInEditor = true;
|
||||
});
|
||||
|
||||
graph.on("canvas:mouseleave", () => {
|
||||
this.isInEditor = false;
|
||||
});
|
||||
|
||||
graph.on("node:click", (e: G6GraphEvent) => {
|
||||
if (e.shape.cfg.name === "collapse-icon") {
|
||||
const item = e.item;
|
||||
this.onSelectNode(item.getID());
|
||||
const data = item.getModel();
|
||||
if ((data as unknown as GraphNodeModel).children?.length > 0) {
|
||||
data.collapsed = !data.collapsed;
|
||||
graph.setItemState(item, "collapsed", data.collapsed as boolean);
|
||||
e.shape.attr("symbol", data.collapsed ? G6.Marker.expand : G6.Marker.collapse);
|
||||
this.graph.layout();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
graph.on("node:dblclick", (e: G6GraphEvent) => {
|
||||
const data = this.findSubtree(e.item.getModel() as unknown as GraphNodeModel);
|
||||
if (data) {
|
||||
this.props.onOpenSubtree(data.path);
|
||||
}
|
||||
});
|
||||
|
||||
graph.on("node:mouseenter", (e: G6GraphEvent) => {
|
||||
const { item } = e;
|
||||
if (item.hasState("selected")) {
|
||||
return;
|
||||
}
|
||||
this.setItemState(item, "hover", true);
|
||||
});
|
||||
|
||||
graph.on("node:mouseleave", (e: G6GraphEvent) => {
|
||||
const { item } = e;
|
||||
if (item.hasState("selected")) {
|
||||
return;
|
||||
}
|
||||
this.setItemState(item, "hover", false);
|
||||
});
|
||||
|
||||
graph.on("nodeselectchange", (e: G6GraphEvent) => {
|
||||
if (this.state.blockNodeSelectChange) {
|
||||
// ** 重置选中效果
|
||||
this.onSelectNode(this.state.curNodeId);
|
||||
return;
|
||||
}
|
||||
if (e.target) {
|
||||
this.onSelectNode(e.target.getID());
|
||||
} else {
|
||||
this.onSelectNode(null);
|
||||
}
|
||||
});
|
||||
|
||||
const clearDragDstState = () => {
|
||||
if (this.dragDstId) {
|
||||
this.setItemState(this.dragDstId, "dragRight", false);
|
||||
this.setItemState(this.dragDstId, "dragDown", false);
|
||||
this.setItemState(this.dragDstId, "dragUp", false);
|
||||
this.dragDstId = null;
|
||||
}
|
||||
};
|
||||
|
||||
const clearDragSrcState = () => {
|
||||
if (this.dragSrcId) {
|
||||
this.setItemState(this.dragSrcId, "dragSrc", false);
|
||||
this.dragSrcId = null;
|
||||
}
|
||||
};
|
||||
|
||||
graph.on("node:dragstart", (e: G6GraphEvent) => {
|
||||
this.dragSrcId = e.item.getID();
|
||||
this.setItemState(this.dragSrcId, "dragSrc", true);
|
||||
});
|
||||
graph.on("node:dragend", (e: G6GraphEvent) => {
|
||||
if (this.dragSrcId) {
|
||||
this.setItemState(this.dragSrcId, "dragSrc", false);
|
||||
this.dragSrcId = null;
|
||||
}
|
||||
});
|
||||
|
||||
graph.on("node:dragover", (e: G6GraphEvent) => {
|
||||
const dstNodeId = e.item.getID();
|
||||
if (dstNodeId == this.dragSrcId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.dragDstId) {
|
||||
this.setItemState(this.dragDstId, "dragRight", false);
|
||||
this.setItemState(this.dragDstId, "dragDown", false);
|
||||
this.setItemState(this.dragDstId, "dragUp", false);
|
||||
}
|
||||
|
||||
const box = e.item.getBBox();
|
||||
if (e.x > box.minX + box.width * 0.6) {
|
||||
this.setItemState(dstNodeId, "dragRight", true);
|
||||
} else if (e.y > box.minY + box.height * 0.5) {
|
||||
this.setItemState(dstNodeId, "dragDown", true);
|
||||
} else {
|
||||
this.setItemState(dstNodeId, "dragUp", true);
|
||||
}
|
||||
this.dragDstId = dstNodeId;
|
||||
});
|
||||
|
||||
graph.on("node:dragleave", (e: G6GraphEvent) => {
|
||||
clearDragDstState();
|
||||
});
|
||||
|
||||
graph.on("node:drop", (e: G6GraphEvent) => {
|
||||
const srcNodeId = this.dragSrcId;
|
||||
const dstNode = e.item;
|
||||
|
||||
var dragDir;
|
||||
if (dstNode.hasState("dragRight")) {
|
||||
dragDir = "dragRight";
|
||||
} else if (dstNode.hasState("dragDown")) {
|
||||
dragDir = "dragDown";
|
||||
} else if (dstNode.hasState("dragUp")) {
|
||||
dragDir = "dragUp";
|
||||
}
|
||||
|
||||
clearDragSrcState();
|
||||
clearDragDstState();
|
||||
|
||||
if (!srcNodeId) {
|
||||
console.log("no drag src");
|
||||
return;
|
||||
}
|
||||
|
||||
if (srcNodeId == dstNode.getID()) {
|
||||
console.log("drop same node");
|
||||
return;
|
||||
}
|
||||
|
||||
const rootData = graph.findDataById("1");
|
||||
const srcData = graph.findDataById(srcNodeId) as GraphNodeModel;
|
||||
const srcParent = this.findParent(srcData);
|
||||
const dstData = graph.findDataById(dstNode.getID()) as GraphNodeModel;
|
||||
const dstParent = this.findParent(dstData);
|
||||
if (!srcParent) {
|
||||
console.log("no parent!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isAncestor(srcData, dstData)) {
|
||||
// 不能将父节点拖到自已的子孙节点
|
||||
console.log("cannot move to child");
|
||||
return;
|
||||
}
|
||||
|
||||
const removeSrc = () => {
|
||||
this.pushUndoStack();
|
||||
srcParent.children = srcParent.children.filter((e) => e.id != srcData.id);
|
||||
};
|
||||
console.log("dstNode", dstNode);
|
||||
if (dragDir == "dragRight") {
|
||||
removeSrc();
|
||||
if (!dstData.children) {
|
||||
dstData.children = [];
|
||||
}
|
||||
srcData.parent = dstData.id;
|
||||
dstData.children.push(srcData);
|
||||
} else if (dragDir == "dragUp") {
|
||||
if (!dstParent) {
|
||||
return;
|
||||
}
|
||||
removeSrc();
|
||||
const idx = dstParent.children.findIndex((e) => e.id == dstData.id);
|
||||
srcData.parent = dstParent.id;
|
||||
dstParent.children.splice(idx, 0, srcData);
|
||||
} else if (dragDir == "dragDown") {
|
||||
if (!dstParent) {
|
||||
return;
|
||||
}
|
||||
removeSrc();
|
||||
const idx = dstParent.children.findIndex((e) => e.id == dstData.id);
|
||||
srcData.parent = dstParent.id;
|
||||
dstParent.children.splice(idx + 1, 0, srcData);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log("cur data", graph.findDataById('1'));
|
||||
this.changeWithoutAnim();
|
||||
});
|
||||
|
||||
graph.data(this.data);
|
||||
graph.render();
|
||||
graph.fitCenter();
|
||||
graph.set("animate", true);
|
||||
|
||||
this.graph = graph;
|
||||
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* remember the last matrix that triggered by Translate or Zoom action , and restore that matrix where the graph is reconstruct.
|
||||
*/
|
||||
restoreViewport() {
|
||||
if (this.state.viewportMatrix) {
|
||||
this.graph.getGroup().setMatrix(this.state.viewportMatrix);
|
||||
}
|
||||
}
|
||||
|
||||
onSelectNode(curNodeId: string | null) {
|
||||
const graph = this.graph;
|
||||
|
||||
if (this.state.curNodeId) {
|
||||
this.setItemState(this.state.curNodeId, "selected", false);
|
||||
this.setItemState(this.state.curNodeId, "hover", false);
|
||||
}
|
||||
|
||||
this.setState({ curNodeId });
|
||||
if (this.state.curNodeId) {
|
||||
this.setItemState(this.state.curNodeId, "selected", true);
|
||||
}
|
||||
}
|
||||
|
||||
createNode(name: string) {
|
||||
if (!this.isInEditor) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("editor create node", name);
|
||||
const { curNodeId } = this.state;
|
||||
if (!curNodeId) {
|
||||
message.warn("未选中节点");
|
||||
return;
|
||||
}
|
||||
this.pushUndoStack();
|
||||
const curNodeData = this.graph.findDataById(curNodeId);
|
||||
const newNodeData: BehaviorNodeModel = {
|
||||
id: this.autoId++,
|
||||
name: name,
|
||||
};
|
||||
if (!curNodeData.children) {
|
||||
curNodeData.children = [];
|
||||
}
|
||||
curNodeData.children.push(Utils.createTreeData(newNodeData, this.settings, curNodeId));
|
||||
this.changeWithoutAnim();
|
||||
}
|
||||
|
||||
deleteNode() {
|
||||
console.log("editor delete node");
|
||||
const { curNodeId } = this.state;
|
||||
if (!curNodeId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (curNodeId === "1") {
|
||||
message.warn("根节点不能删除!");
|
||||
return;
|
||||
}
|
||||
|
||||
this.onSelectNode(null);
|
||||
this.pushUndoStack();
|
||||
const parentData = this.findParent(this.graph.findDataById(curNodeId) as GraphNodeModel);
|
||||
parentData.children = parentData.children.filter((e) => e.id != curNodeId);
|
||||
this.changeWithoutAnim();
|
||||
}
|
||||
|
||||
changeWithoutAnim() {
|
||||
this.graph.set("animate", false);
|
||||
this.graph.changeData();
|
||||
this.graph.layout();
|
||||
this.graph.set("animate", true);
|
||||
|
||||
this.props.onChangeSaveState(true);
|
||||
this.unsave = true;
|
||||
}
|
||||
|
||||
save() {
|
||||
if (!this.unsave) {
|
||||
return;
|
||||
}
|
||||
const { filepath } = this.props;
|
||||
const data = this.graph.findDataById("1") as GraphNodeModel;
|
||||
this.autoId = Utils.refreshNodeId(data);
|
||||
const root = Utils.createFileData(data);
|
||||
const treeModel = {
|
||||
name: path.basename(filepath).slice(0, -5),
|
||||
root,
|
||||
desc: this.treeModel.desc,
|
||||
} as BehaviorTreeModel;
|
||||
fs.writeFileSync(filepath, JSON.stringify(treeModel, null, 2));
|
||||
this.props.onChangeSaveState(false);
|
||||
this.unsave = false;
|
||||
|
||||
const treeData = Utils.createTreeData(root, this.settings);
|
||||
this.autoId = Utils.refreshNodeId(treeData);
|
||||
|
||||
this.graph.set("animate", false);
|
||||
this.graph.changeData(treeData);
|
||||
this.graph.layout();
|
||||
this.restoreViewport();
|
||||
this.graph.set("animate", true);
|
||||
}
|
||||
|
||||
saveAsSubtree(subpath: string) {
|
||||
const { curNodeId } = this.state;
|
||||
if (!curNodeId) {
|
||||
message.warn("未选择一个节点");
|
||||
return;
|
||||
} else if (curNodeId == "1") {
|
||||
message.warn("根节点不能保存为子树");
|
||||
return;
|
||||
}
|
||||
const workdir = this.settings.workdir;
|
||||
if (subpath.indexOf(workdir) == -1) {
|
||||
message.warn("请保存在工作区中");
|
||||
return;
|
||||
}
|
||||
const data = this.graph.findDataById(curNodeId) as GraphNodeModel;
|
||||
const subroot = Utils.createFileData(data);
|
||||
const subtreeModel = {
|
||||
name: path.basename(subpath).slice(0, -5),
|
||||
root: subroot,
|
||||
desc: data.desc,
|
||||
} as BehaviorTreeModel;
|
||||
fs.writeFileSync(subpath, JSON.stringify(subtreeModel, null, 2));
|
||||
this.pushUndoStack();
|
||||
data.path = subpath.substring(workdir.length + 1);
|
||||
this.unsave = true;
|
||||
this.save();
|
||||
}
|
||||
|
||||
reload() {
|
||||
if (!this.unsave) {
|
||||
const data = this.graph.findDataById("1") as GraphNodeModel;
|
||||
this.autoId = Utils.refreshNodeId(data);
|
||||
const root = Utils.createFileData(data);
|
||||
const treeData = Utils.createTreeData(root, this.settings);
|
||||
this.autoId = Utils.refreshNodeId(treeData);
|
||||
this.graph.set("animate", false);
|
||||
this.graph.changeData(treeData);
|
||||
this.graph.layout();
|
||||
this.restoreViewport();
|
||||
this.graph.set("animate", true);
|
||||
}
|
||||
}
|
||||
|
||||
getTreeModel() {
|
||||
const { filepath } = this.props;
|
||||
const data = this.graph.findDataById("1") as GraphNodeModel;
|
||||
this.autoId = Utils.refreshNodeId(data);
|
||||
const root = Utils.createFileData(data);
|
||||
return {
|
||||
name: path.basename(filepath).slice(0, -5),
|
||||
root,
|
||||
desc: this.treeModel.desc,
|
||||
} as BehaviorTreeModel;
|
||||
}
|
||||
|
||||
copyNode() {
|
||||
if (!this.isInEditor) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("editor copy node");
|
||||
const { curNodeId } = this.state;
|
||||
if (!curNodeId) {
|
||||
return;
|
||||
}
|
||||
const data = this.graph.findDataById(curNodeId) as GraphNodeModel;
|
||||
if (data) {
|
||||
const str = JSON.stringify(Utils.cloneNodeData(data), null, 2);
|
||||
console.log("copy:", str);
|
||||
clipboard.writeText(str);
|
||||
}
|
||||
}
|
||||
|
||||
pasteNode() {
|
||||
if (!this.isInEditor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { curNodeId } = this.state;
|
||||
if (!curNodeId) {
|
||||
message.warn("未选中节点");
|
||||
return;
|
||||
}
|
||||
|
||||
const curNodeData = this.graph.findDataById(curNodeId);
|
||||
try {
|
||||
const str = clipboard.readText();
|
||||
if (!str || str == "") {
|
||||
return;
|
||||
}
|
||||
console.log("paste:", str);
|
||||
const data = Utils.createTreeData(JSON.parse(str), this.settings, curNodeId);
|
||||
this.autoId = Utils.refreshNodeId(data, this.autoId);
|
||||
this.onSelectNode(null);
|
||||
if (!curNodeData.children) {
|
||||
curNodeData.children = [];
|
||||
}
|
||||
this.pushUndoStack();
|
||||
curNodeData.children.push(data);
|
||||
// this.autoId = Utils.refreshNodeId(this.graph.findDataById("1") as GraphNodeModel);
|
||||
this.changeWithoutAnim();
|
||||
} catch (error) {
|
||||
// message.error("粘贴数据有误");
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
useStackData(data: BehaviorNodeModel) {
|
||||
this.graph.set("animate", false);
|
||||
this.data = Utils.createTreeData(data, this.settings);
|
||||
this.autoId = Utils.refreshNodeId(this.data);
|
||||
this.graph.changeData(this.data);
|
||||
this.graph.layout();
|
||||
this.restoreViewport();
|
||||
this.graph.set("animate", true);
|
||||
|
||||
this.props.onChangeSaveState(true);
|
||||
this.unsave = true;
|
||||
}
|
||||
|
||||
pushUndoStack(keepRedo?: boolean) {
|
||||
this.undoStack.push(Utils.cloneNodeData(this.graph.findDataById("1") as GraphNodeModel));
|
||||
console.log("push undo", this.undoStack);
|
||||
if (!keepRedo) {
|
||||
this.redoStack = [];
|
||||
}
|
||||
}
|
||||
|
||||
pushRedoStack() {
|
||||
this.redoStack.push(Utils.cloneNodeData(this.graph.findDataById("1") as GraphNodeModel));
|
||||
console.log("push redo", this.redoStack);
|
||||
}
|
||||
|
||||
undo() {
|
||||
if (this.undoStack.length == 0) {
|
||||
return;
|
||||
}
|
||||
const data = this.undoStack.pop();
|
||||
this.pushRedoStack();
|
||||
this.useStackData(data);
|
||||
}
|
||||
|
||||
redo() {
|
||||
if (this.redoStack.length == 0) {
|
||||
return;
|
||||
}
|
||||
const data = this.redoStack.pop();
|
||||
this.pushUndoStack(true);
|
||||
this.useStackData(data);
|
||||
}
|
||||
|
||||
changeTreeDesc(desc: string) {
|
||||
this.treeModel.desc = desc;
|
||||
this.settings.setTreeDesc(this.props.filepath, desc);
|
||||
this.unsave = true;
|
||||
this.save();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { curNodeId } = this.state;
|
||||
console.log("render tree", curNodeId);
|
||||
var curNode: any;
|
||||
if (curNodeId) {
|
||||
curNode = this.graph.findDataById(curNodeId);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="editor">
|
||||
<Row className="editorBd">
|
||||
<Col
|
||||
span={18}
|
||||
className="editorContent"
|
||||
ref={this.ref}
|
||||
onMouseDownCapture={(event) => {
|
||||
this.state.blockNodeSelectChange = false;
|
||||
}}
|
||||
/>
|
||||
<Col
|
||||
span={6}
|
||||
className="editorSidebar"
|
||||
onMouseDownCapture={(event) => {
|
||||
this.state.blockNodeSelectChange = true;
|
||||
}}
|
||||
>
|
||||
{curNode ? (
|
||||
<NodePanel
|
||||
model={curNode}
|
||||
isSubtreeNode={this.isSubtreeNode(curNode)}
|
||||
settings={this.settings}
|
||||
updateNode={(id, forceUpdate) => {
|
||||
if (forceUpdate) {
|
||||
const data: GraphNodeModel = this.graph.findDataById(
|
||||
id
|
||||
) as GraphNodeModel;
|
||||
data.conf = this.settings.getNodeConf(data.name);
|
||||
data.size = Utils.calcTreeNodeSize(data);
|
||||
this.changeWithoutAnim();
|
||||
this.save();
|
||||
return;
|
||||
}
|
||||
const item = this.graph.findById(id);
|
||||
item.draw();
|
||||
this.props.onChangeSaveState(true);
|
||||
this.unsave = true;
|
||||
}}
|
||||
pushUndoStack={() => {
|
||||
this.pushUndoStack();
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<TreePanel
|
||||
model={this.treeModel}
|
||||
onRenameTree={(name: string) => {}}
|
||||
onRemoveTree={() => {}}
|
||||
onChangeTreeDesc={(desc) => {
|
||||
this.changeTreeDesc(desc);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,432 +0,0 @@
|
||||
import React, { ChangeEvent, Component } from "react";
|
||||
import { Input } from "antd";
|
||||
import { Tree } from "shineout";
|
||||
import { FolderOutlined, FileOutlined, FolderOpenOutlined } from "@ant-design/icons";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { ipcRenderer } from "electron";
|
||||
import MainEventType from "../common/MainEventType";
|
||||
import Settings from "../main-process/Settings";
|
||||
import * as Utils from "../common/Utils";
|
||||
|
||||
const { Search } = Input;
|
||||
const DirectoryTree = Tree;
|
||||
|
||||
|
||||
class FileDataNode {
|
||||
name: string; //display name
|
||||
desc: string; //describe
|
||||
filepath: string; //file full path
|
||||
isFolder: boolean;
|
||||
|
||||
settings: Settings;
|
||||
|
||||
parent: FileDataNode;
|
||||
visible: boolean = true;
|
||||
children: FileDataNode[];
|
||||
|
||||
public constructor(init?: Partial<FileDataNode>, settings?: Settings) {
|
||||
Object.assign(this, init);
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
get id() {
|
||||
return this.filepath;
|
||||
}
|
||||
|
||||
get text() {
|
||||
if (!this.desc) {
|
||||
return this.name;
|
||||
}
|
||||
return `${this.name}(${this.desc})`;
|
||||
}
|
||||
|
||||
get path() {
|
||||
return this.filepath;
|
||||
}
|
||||
|
||||
public getRenderData() {
|
||||
if (!this.visible) {
|
||||
return null;
|
||||
}
|
||||
let ret = new FileDataNode({
|
||||
name: this.name,
|
||||
desc: this.isFolder ? undefined : this.settings.getTreeDesc(this.filepath),
|
||||
filepath: this.filepath,
|
||||
isFolder: this.isFolder,
|
||||
children: new Array(),
|
||||
}, this.settings);
|
||||
|
||||
for (const child of this.children) {
|
||||
const data = child.getRenderData();
|
||||
if (data) {
|
||||
ret.children.push(data);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
loadChilds(recursive?: boolean) {
|
||||
const folder = this.path;
|
||||
if (folder == "" || !fs.existsSync(folder)) {
|
||||
return [];
|
||||
}
|
||||
const files = fs.readdirSync(folder).filter((f) => {
|
||||
return f.endsWith(".json");
|
||||
});
|
||||
const list: FileDataNode[] = [];
|
||||
files.forEach((filename) => {
|
||||
const fullPath = path.join(folder, filename);
|
||||
const stat = fs.statSync(fullPath);
|
||||
const isFolder = stat.isDirectory();
|
||||
const name = isFolder ? filename : filename.slice(0, -5);
|
||||
const node = new FileDataNode({
|
||||
name: name,
|
||||
desc: isFolder ? undefined : this.settings.getTreeDesc(fullPath),
|
||||
filepath: fullPath,
|
||||
isFolder: isFolder,
|
||||
parent: this,
|
||||
}, this.settings);
|
||||
|
||||
(node.children =
|
||||
isFolder && recursive ? (node.loadChilds(recursive) as FileDataNode[]) : []),
|
||||
list.push(node);
|
||||
});
|
||||
list.sort((a, b) => {
|
||||
const av = a.isFolder ? 100 : 0;
|
||||
const bv = b.isFolder ? 100 : 0;
|
||||
return bv - av;
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
addChild(filePath: string) {
|
||||
const isDir = fs.statSync(filePath).isDirectory();
|
||||
const newNode = new FileDataNode({
|
||||
name: path.basename(filePath),
|
||||
filepath: filePath,
|
||||
isFolder: isDir,
|
||||
parent: this,
|
||||
children: [],
|
||||
}, this.settings);
|
||||
this.children = [newNode, ...this.children];
|
||||
}
|
||||
|
||||
removeFromParent() {
|
||||
const parent = this.parent;
|
||||
if (parent) {
|
||||
const index = parent.children.indexOf(this);
|
||||
parent.children.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
expandSelf(recursive: boolean = true) {
|
||||
if (this.isFolder) {
|
||||
this.children = this.loadChilds(recursive);
|
||||
}
|
||||
}
|
||||
|
||||
findChild(id: string): FileDataNode {
|
||||
for (const child of this.children) {
|
||||
if (child.id == id) {
|
||||
return child;
|
||||
} else {
|
||||
const ret = child.findChild(id);
|
||||
if (ret) {
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getList(): FileDataNode[] {
|
||||
let ret = new Array<FileDataNode>(this);
|
||||
for (const child of this.children) {
|
||||
const childList = child.getList();
|
||||
ret.push(...childList);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
setVisible(keyWord: string) {
|
||||
let ret = false;
|
||||
for (let i = this.children.length - 1; i >= 0; i--) {
|
||||
const child = this.children[i];
|
||||
if (!child.isFolder) {
|
||||
const keyStr = child.text;
|
||||
child.visible = keyStr.includes(keyWord);
|
||||
} else {
|
||||
child.visible = child.setVisible(keyWord);
|
||||
}
|
||||
ret = child.visible || ret;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
// name:string;
|
||||
// path: string;
|
||||
// isFolder : boolean;
|
||||
// isEditing?: boolean;
|
||||
// children?: NodeData[];
|
||||
}
|
||||
|
||||
const NodeActions: { [x: string]: string } = {
|
||||
["create"]: "New Tree",
|
||||
["createFolder"]: "New Folder",
|
||||
["rename"]: "Rename",
|
||||
["delete"]: "Delete",
|
||||
["reveal_in_explorer"]: "Reveal In File Explorer",
|
||||
};
|
||||
|
||||
interface ExplorerNodeProps {
|
||||
visible: boolean;
|
||||
title: string;
|
||||
selected: boolean;
|
||||
expended: boolean;
|
||||
isLeaf: boolean;
|
||||
searchKey: string;
|
||||
}
|
||||
class ExplorerNode extends Component<ExplorerNodeProps> {
|
||||
shouldComponentUpdate(nextProps: ExplorerNodeProps) {
|
||||
return JSON.stringify(this.props) != JSON.stringify(nextProps);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { title, visible, selected, expended, isLeaf, searchKey } = this.props;
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const index = title.indexOf(searchKey);
|
||||
const beforeStr = title.substr(0, index);
|
||||
const afterStr = title.substr(index + searchKey.length);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={selected ? "explorer-node-selected" : "explorer-node"}
|
||||
>
|
||||
{!isLeaf ? (expended ? <FolderOpenOutlined /> : <FolderOutlined />) : <FileOutlined />}
|
||||
{isLeaf && index > -1 ? (
|
||||
<span className="explorer-node-title">
|
||||
{beforeStr}
|
||||
<span className="explorer-node-search-value">{searchKey}</span>
|
||||
{afterStr}
|
||||
</span>
|
||||
) : (
|
||||
<span className="explorer-node-title">{title}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export interface ExplorerProps {
|
||||
workdir: string;
|
||||
onOpenTree: (path: string) => void;
|
||||
onDeleteTree: (path: string) => void;
|
||||
}
|
||||
|
||||
|
||||
interface ExplorerState {
|
||||
root: FileDataNode;
|
||||
searchKey: string;
|
||||
selectedKey: string;
|
||||
defaultExpandedKeys: string[];
|
||||
expandedKeys: string[];
|
||||
autoExpandParent: boolean;
|
||||
rightClickNode: {
|
||||
pageX: number;
|
||||
pageY: number;
|
||||
key: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default class Explorer extends Component<ExplorerProps> {
|
||||
state: ExplorerState = {
|
||||
root: this.getRootNode(this.props.workdir),
|
||||
searchKey: "",
|
||||
selectedKey: "",
|
||||
rightClickNode: null,
|
||||
expandedKeys: [],
|
||||
defaultExpandedKeys: [],
|
||||
autoExpandParent: true,
|
||||
};
|
||||
|
||||
curWorkdir: string = "";
|
||||
settings: Settings;
|
||||
|
||||
shouldComponentUpdate(nextProps: ExplorerProps) {
|
||||
const shouldUpdate = this.curWorkdir != nextProps.workdir;
|
||||
this.curWorkdir = nextProps.workdir;
|
||||
return shouldUpdate;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.settings = Utils.getRemoteSettings();
|
||||
ipcRenderer.on(MainEventType.CREATE_TREE, (event: any, path: string) => {
|
||||
console.log("on Create tree", path);
|
||||
this.props.onOpenTree(path);
|
||||
this.state.root.expandSelf();
|
||||
this.forceUpdate();
|
||||
});
|
||||
|
||||
ipcRenderer.on(MainEventType.OPEN_DIR, (event: any, workdir: any, workspace: string) => {
|
||||
console.log("prop on open workspace", workspace);
|
||||
this.updateRoot();
|
||||
this.forceUpdate();
|
||||
});
|
||||
|
||||
this.updateRoot();
|
||||
}
|
||||
|
||||
updateRoot() {
|
||||
var workdir = this.props.workdir;
|
||||
if (!workdir || workdir === "") {
|
||||
return;
|
||||
}
|
||||
const root = this.getRootNode(workdir);
|
||||
root.expandSelf()
|
||||
this.setState({
|
||||
root: root,
|
||||
expandedKeys: [root.id],
|
||||
defaultExpandedKeys: [root.id],
|
||||
});
|
||||
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
getRootNode(workdir: string) {
|
||||
if (workdir && workdir !== "") {
|
||||
return new FileDataNode({
|
||||
name: path.basename(workdir),
|
||||
filepath: workdir,
|
||||
isFolder: true,
|
||||
parent: null,
|
||||
children: []
|
||||
}, this.settings);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// renderContextMeun(selectedNode: FileDataNode) {
|
||||
// const actions = Object.keys(NodeActions);
|
||||
// return (
|
||||
// <Menu>
|
||||
// {actions.map((action) => {
|
||||
// const displayName: string = NodeActions[action];
|
||||
// return (
|
||||
// <Menu.Item
|
||||
// key={action}
|
||||
// onClick={(info) => {
|
||||
// //this.handleContextMenu({ node: selectedNode, action:{action} });
|
||||
// }}
|
||||
// >{displayName}
|
||||
// </Menu.Item>
|
||||
// );
|
||||
// })}
|
||||
// </Menu>
|
||||
// );
|
||||
// }
|
||||
|
||||
handleOnSearch(value: string) {
|
||||
const expandedKeys = this.state.root
|
||||
.getList()
|
||||
.map((item) => {
|
||||
if (item.isFolder) return null;
|
||||
const title = item.text;
|
||||
if (title.includes(value) && item.parent) {
|
||||
return item.parent.id;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((item, i, self) => item && self.indexOf(item) === i);
|
||||
|
||||
const root = this.state.root;
|
||||
root.setVisible(value || "");
|
||||
if (value && value.length > 0) {
|
||||
this.setState({
|
||||
root: root,
|
||||
searchKey: value,
|
||||
expandedKeys: expandedKeys,
|
||||
autoExpandParent: true,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
root: root,
|
||||
searchKey: "",
|
||||
expandedKeys: this.state.defaultExpandedKeys,
|
||||
autoExpandParent: false,
|
||||
});
|
||||
}
|
||||
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
selectNode(id: string) {
|
||||
this.setState({
|
||||
selectedKey: id
|
||||
});
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
renderItem(node: FileDataNode) {
|
||||
return (<ExplorerNode
|
||||
visible={node.visible}
|
||||
title={node.text}
|
||||
selected={this.state.selectedKey == node.id}
|
||||
expended={this.state.expandedKeys.includes(node.id)}
|
||||
isLeaf={!node.isFolder}
|
||||
searchKey={this.state.searchKey}
|
||||
/>);
|
||||
}
|
||||
|
||||
render() {
|
||||
console.log("render Explorer");
|
||||
const { onOpenTree, onDeleteTree, workdir } = this.props;
|
||||
|
||||
const root = this.state.root;
|
||||
if (!workdir || workdir === "" || !root) {
|
||||
return `请打开workspace.json文件`;
|
||||
}
|
||||
|
||||
const nodes = [root.getRenderData()];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Search
|
||||
allowClear
|
||||
placeholder="Search"
|
||||
// onChange={(e) => {
|
||||
// const value = e.target.value?.toLowerCase();
|
||||
// this.handleOnSearch(value);
|
||||
// }}
|
||||
onSearch={(value, event) => {
|
||||
this.handleOnSearch(value?.toLowerCase());
|
||||
}}
|
||||
/>
|
||||
|
||||
<DirectoryTree
|
||||
keygen="id"
|
||||
data={nodes}
|
||||
line={false}
|
||||
doubleClickExpand
|
||||
defaultExpanded={[root.id]}
|
||||
expanded={this.state.expandedKeys}
|
||||
value={[this.state.selectedKey]}
|
||||
onExpand={(expanded) => {
|
||||
this.setState({ expandedKeys: expanded })
|
||||
this.forceUpdate();
|
||||
}}
|
||||
renderItem={this.renderItem.bind(this)}
|
||||
onClick={(node: FileDataNode) => {
|
||||
if (!node.isFolder) {
|
||||
onOpenTree(node.path);
|
||||
}
|
||||
}}
|
||||
></DirectoryTree>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,355 +0,0 @@
|
||||
import G6 from "@antv/g6";
|
||||
import { toBreakWord } from "../common/Utils";
|
||||
import Settings from "../main-process/Settings";
|
||||
import { BehaviorNodeTypeModel } from "../common/BehaviorTreeModel";
|
||||
|
||||
const NODE_COLORS: any = {
|
||||
["Composite"]: "rgb(91,237,32)",
|
||||
["Decorator"]: "rgb(218,167,16)",
|
||||
["Condition"]: "rgb(228,20,139)",
|
||||
["Action"]: "rgb(91,143,249)",
|
||||
["Other"]: "rgb(112,112,112)",
|
||||
["Error"]: "rgb(255,0,0)",
|
||||
};
|
||||
|
||||
const SELECTED_LINE_WIDTH = 4;
|
||||
|
||||
export default function (settings: Settings) {
|
||||
const collapseStyle: any = {
|
||||
"collapse-icon": {
|
||||
// symbol: G6.Marker.expand,
|
||||
fill: "blue",
|
||||
},
|
||||
};
|
||||
G6.registerNode(
|
||||
"TreeNode",
|
||||
{
|
||||
options: {
|
||||
type: "rect",
|
||||
anchorPoints: [
|
||||
[0, 0.5],
|
||||
[1, 0.5],
|
||||
],
|
||||
labelCfg: {
|
||||
style: {
|
||||
fill: "blue",
|
||||
fontSize: 10,
|
||||
},
|
||||
},
|
||||
style: {
|
||||
fill: "white",
|
||||
},
|
||||
stateStyles: {
|
||||
selected: {
|
||||
"main-box": {
|
||||
stroke: "yellow",
|
||||
lineWidth: SELECTED_LINE_WIDTH,
|
||||
},
|
||||
"name-bg-selected": {
|
||||
fill: "yellow",
|
||||
},
|
||||
},
|
||||
hover: {
|
||||
stroke: "white",
|
||||
lineWidth: 3,
|
||||
},
|
||||
dragSrc: {
|
||||
fill: "gray",
|
||||
},
|
||||
dragRight: {
|
||||
"drag-right": {
|
||||
fillOpacity: 0.7,
|
||||
},
|
||||
},
|
||||
dragUp: {
|
||||
"drag-up": {
|
||||
fillOpacity: 0.7,
|
||||
},
|
||||
},
|
||||
dragDown: {
|
||||
"drag-down": {
|
||||
fillOpacity: 0.7,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
draw(cfg, group) {
|
||||
const nodeConf = settings.getNodeConf(cfg.name as string);
|
||||
let classify = nodeConf.type || "Other";
|
||||
if (
|
||||
!settings.hasNodeConf(cfg.name as string) ||
|
||||
(cfg.path && (!cfg.children || (cfg.children as []).length == 0))
|
||||
) {
|
||||
classify = "Error";
|
||||
}
|
||||
const color = NODE_COLORS[classify] || "Other";
|
||||
var size = cfg.size ? (cfg.size as number[]) : [150, 40];
|
||||
const w = size[0];
|
||||
const h = size[1];
|
||||
const r = 4;
|
||||
const shape = group.addShape("rect", {
|
||||
attrs: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: w,
|
||||
height: h,
|
||||
stroke: color,
|
||||
lineWidth: 2,
|
||||
fill: "white",
|
||||
radius: r,
|
||||
},
|
||||
name: "main-box",
|
||||
draggable: true,
|
||||
});
|
||||
|
||||
// is subtree
|
||||
if (cfg.path && cfg.id !== "1") {
|
||||
group.addShape("rect", {
|
||||
attrs: {
|
||||
x: -10,
|
||||
y: -10,
|
||||
width: w + 20,
|
||||
height: h + 20,
|
||||
stroke: "white",
|
||||
lineWidth: 2.5,
|
||||
lineDash: [5, 5],
|
||||
radius: [r, r, r, r],
|
||||
},
|
||||
name: "subtree",
|
||||
draggable: true,
|
||||
});
|
||||
}
|
||||
|
||||
// name bg
|
||||
group.addShape("rect", {
|
||||
attrs: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: w,
|
||||
height: 20,
|
||||
fill: color,
|
||||
radius: [r, r, 0, 0],
|
||||
},
|
||||
name: "name-bg-selected",
|
||||
draggable: true,
|
||||
});
|
||||
group.addShape("rect", {
|
||||
attrs: {
|
||||
x: SELECTED_LINE_WIDTH / 2,
|
||||
y: SELECTED_LINE_WIDTH / 2,
|
||||
width: w - 5,
|
||||
height: 20 - SELECTED_LINE_WIDTH / 2,
|
||||
fill: color,
|
||||
radius: [r, r, 0, 0],
|
||||
},
|
||||
name: "name-bg",
|
||||
draggable: true,
|
||||
});
|
||||
|
||||
// id text
|
||||
group.addShape("text", {
|
||||
attrs: {
|
||||
textBaseline: "top",
|
||||
x: -3,
|
||||
y: h / 2 - 8,
|
||||
fontSize: 20,
|
||||
lineHeight: 20,
|
||||
text: cfg.id,
|
||||
textAlign: "right",
|
||||
fill: "white",
|
||||
stroke: "black",
|
||||
lineWidth: 2,
|
||||
},
|
||||
name: "id-text",
|
||||
});
|
||||
|
||||
// icon
|
||||
var img = `./static/icons/${classify}.svg`;
|
||||
group.addShape("image", {
|
||||
attrs: {
|
||||
x: 5,
|
||||
y: 2,
|
||||
height: 14,
|
||||
width: 14,
|
||||
img,
|
||||
},
|
||||
name: "node-icon",
|
||||
});
|
||||
|
||||
// debug
|
||||
if (cfg.debug) {
|
||||
group.addShape("image", {
|
||||
attrs: {
|
||||
x: 182,
|
||||
y: 2,
|
||||
height: 14,
|
||||
width: 14,
|
||||
img: `./static/icons/Debug.svg`,
|
||||
},
|
||||
name: "node-icon",
|
||||
});
|
||||
}
|
||||
|
||||
// name text
|
||||
group.addShape("text", {
|
||||
attrs: {
|
||||
textBaseline: "top",
|
||||
x: 22,
|
||||
y: 3,
|
||||
fontWeight: 800,
|
||||
lineHeight: 20,
|
||||
text: cfg.name,
|
||||
fill: "black",
|
||||
},
|
||||
name: "name-text",
|
||||
});
|
||||
|
||||
var x = 5;
|
||||
var y = 24;
|
||||
// desc text
|
||||
const desc = cfg.desc || nodeConf.desc;
|
||||
if (desc) {
|
||||
group.addShape("text", {
|
||||
attrs: {
|
||||
textBaseline: "top",
|
||||
x,
|
||||
y,
|
||||
lineHeight: 20,
|
||||
fontWeight: 800,
|
||||
text: `备注:${desc}`,
|
||||
fill: "black",
|
||||
},
|
||||
name: "desc-text",
|
||||
});
|
||||
}
|
||||
|
||||
const args: any = cfg.args;
|
||||
if (nodeConf.args && args && Object.keys(args).length > 0) {
|
||||
const { str, line } = toBreakWord(`参数:${JSON.stringify(args)}`, 34);
|
||||
group.addShape("text", {
|
||||
attrs: {
|
||||
textBaseline: "top",
|
||||
x,
|
||||
y: y + 20,
|
||||
w,
|
||||
lineHeight: 20,
|
||||
text: str,
|
||||
fill: "black",
|
||||
},
|
||||
name: "args-text",
|
||||
});
|
||||
y += 20 * line;
|
||||
}
|
||||
|
||||
const input: [] = cfg.input ? (cfg.input as []) : [];
|
||||
if (nodeConf.input && input.length > 0) {
|
||||
const { str, line } = toBreakWord(`输入:${JSON.stringify(input)}`, 35);
|
||||
group.addShape("text", {
|
||||
attrs: {
|
||||
textBaseline: "top",
|
||||
x,
|
||||
y: y + 20,
|
||||
lineHeight: 20,
|
||||
text: str,
|
||||
fill: "black",
|
||||
},
|
||||
name: "input-text",
|
||||
});
|
||||
y += 20 * line;
|
||||
}
|
||||
|
||||
const output: [] = cfg.output ? (cfg.output as []) : [];
|
||||
if (nodeConf.output && output.length > 0) {
|
||||
const { str, line } = toBreakWord(`输出:${JSON.stringify(output)}`, 35);
|
||||
group.addShape("text", {
|
||||
attrs: {
|
||||
textBaseline: "top",
|
||||
x,
|
||||
y: y + 20,
|
||||
lineHeight: 20,
|
||||
text: str,
|
||||
fill: "black",
|
||||
},
|
||||
name: "output-text",
|
||||
});
|
||||
y += 20 * line;
|
||||
}
|
||||
|
||||
if (cfg.path) {
|
||||
group.addShape("text", {
|
||||
attrs: {
|
||||
textBaseline: "top",
|
||||
x,
|
||||
y: y + 20,
|
||||
lineHeight: 20,
|
||||
text: `子树:${cfg.path}`,
|
||||
fill: "black",
|
||||
},
|
||||
name: "subtree-text",
|
||||
});
|
||||
y += 20;
|
||||
}
|
||||
|
||||
group.addShape("rect", {
|
||||
name: "drag-up",
|
||||
attrs: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: w,
|
||||
height: h / 2,
|
||||
fill: "blue",
|
||||
fillOpacity: 0,
|
||||
radius: [r, r, 0, 0],
|
||||
},
|
||||
draggable: true,
|
||||
});
|
||||
|
||||
group.addShape("rect", {
|
||||
name: "drag-down",
|
||||
attrs: {
|
||||
x: 0,
|
||||
y: h / 2,
|
||||
width: w,
|
||||
height: h / 2,
|
||||
fill: "blue",
|
||||
fillOpacity: 0,
|
||||
radius: [0, 0, r, r],
|
||||
},
|
||||
draggable: true,
|
||||
});
|
||||
|
||||
group.addShape("rect", {
|
||||
name: "drag-right",
|
||||
attrs: {
|
||||
x: w / 2,
|
||||
y: 0,
|
||||
width: w / 2,
|
||||
height: h,
|
||||
fill: "blue",
|
||||
fillOpacity: 0,
|
||||
radius: [0, r, r, 0],
|
||||
},
|
||||
draggable: true,
|
||||
});
|
||||
|
||||
if (Array.isArray(cfg.children) && cfg.children.length > 0) {
|
||||
group.addShape("marker", {
|
||||
attrs: {
|
||||
x: w,
|
||||
y: h / 2,
|
||||
r: 6,
|
||||
symbol: G6.Marker.collapse,
|
||||
stroke: "#666",
|
||||
lineWidth: 1,
|
||||
fill: "#fff",
|
||||
},
|
||||
name: "collapse-icon",
|
||||
});
|
||||
}
|
||||
|
||||
return shape;
|
||||
},
|
||||
},
|
||||
"single-node"
|
||||
);
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
import { Tabs, message } from "antd";
|
||||
import { ipcRenderer } from "electron";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import React, { Component } from "react";
|
||||
import * as Utils from "../common/Utils";
|
||||
import Editor from "./Editor";
|
||||
|
||||
import "antd/dist/antd.dark.css";
|
||||
import { BehaviorTreeModel } from "../common/BehaviorTreeModel";
|
||||
import MainEventType from "../common/MainEventType";
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
interface TreeTabsProps {
|
||||
onTabSelected: (path: string) => void;
|
||||
}
|
||||
|
||||
interface TreeInfo {
|
||||
filepath: string;
|
||||
unsave?: boolean;
|
||||
}
|
||||
|
||||
interface TreeTabsState {
|
||||
trees: TreeInfo[];
|
||||
curTree?: string;
|
||||
}
|
||||
|
||||
export default class TreeTabs extends Component<TreeTabsProps, TreeTabsState> {
|
||||
state: TreeTabsState = {
|
||||
trees: [],
|
||||
};
|
||||
|
||||
editors: { [path: string]: Editor } = {};
|
||||
|
||||
componentDidMount() {
|
||||
ipcRenderer.on(MainEventType.CREATE_NODE, (event: any, name: any) => {
|
||||
const editor = this.getCurEditor();
|
||||
editor?.createNode(name);
|
||||
});
|
||||
|
||||
ipcRenderer.on(MainEventType.DELETE_NODE, () => {
|
||||
const editor = this.getCurEditor();
|
||||
editor?.deleteNode();
|
||||
});
|
||||
|
||||
ipcRenderer.on(MainEventType.COPY_NODE, () => {
|
||||
const editor = this.getCurEditor();
|
||||
editor?.copyNode();
|
||||
});
|
||||
|
||||
ipcRenderer.on(MainEventType.PASTE_NODE, () => {
|
||||
const editor = this.getCurEditor();
|
||||
editor?.pasteNode();
|
||||
});
|
||||
|
||||
ipcRenderer.on(MainEventType.SAVE, (event: any) => {
|
||||
const { curTree: curPath } = this.state;
|
||||
if (!curPath) {
|
||||
return;
|
||||
}
|
||||
const editor = this.editors[curPath];
|
||||
editor.save();
|
||||
message.success("已保存");
|
||||
});
|
||||
|
||||
ipcRenderer.on(MainEventType.SAVE_AS_SUBTREE, (event: any, path: string) => {
|
||||
const { curTree: curPath } = this.state;
|
||||
if (!curPath) {
|
||||
return;
|
||||
}
|
||||
const editor = this.editors[curPath];
|
||||
editor.saveAsSubtree(path);
|
||||
});
|
||||
|
||||
ipcRenderer.on(MainEventType.SAVE_ALL, (event: any) => {
|
||||
for (let k in this.editors) {
|
||||
let editor = this.editors[k];
|
||||
editor.save();
|
||||
}
|
||||
message.success("已保存所有行为树");
|
||||
});
|
||||
|
||||
ipcRenderer.on(MainEventType.BUILD, async (event, buildDir: string) => {
|
||||
for (let k in this.editors) {
|
||||
let editor = this.editors[k];
|
||||
editor.save();
|
||||
}
|
||||
|
||||
const settings = Utils.getRemoteSettings();
|
||||
const files = fs.readdirSync(settings.workdir).filter((f) => {
|
||||
return f.endsWith(".json");
|
||||
});
|
||||
for (const path of files) {
|
||||
const buildpath = buildDir + "/" + path;
|
||||
console.log("build:", buildpath);
|
||||
const treeModel = Utils.createBuildData(path, settings);
|
||||
fs.writeFileSync(buildpath, JSON.stringify(treeModel, null, 2));
|
||||
}
|
||||
message.success("已构建所有行为树");
|
||||
});
|
||||
|
||||
ipcRenderer.on(MainEventType.UNDO, () => {
|
||||
const editor = this.getCurEditor();
|
||||
if (editor?.isInEditor) {
|
||||
editor.undo();
|
||||
}
|
||||
});
|
||||
|
||||
ipcRenderer.on(MainEventType.REDO, () => {
|
||||
const editor = this.getCurEditor();
|
||||
if (editor?.isInEditor) {
|
||||
editor.redo();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
setTimeout(() => {
|
||||
this.props.onTabSelected(this.state.curTree);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
getOpenTreesModel() {
|
||||
const trees: BehaviorTreeModel[] = [];
|
||||
for (let k in this.editors) {
|
||||
let editor = this.editors[k];
|
||||
if (editor) {
|
||||
editor.save();
|
||||
trees.push(editor.getTreeModel());
|
||||
}
|
||||
}
|
||||
return trees;
|
||||
}
|
||||
|
||||
getCurEditor() {
|
||||
const { curTree: curPath } = this.state;
|
||||
if (!curPath) {
|
||||
return;
|
||||
}
|
||||
return this.editors[curPath];
|
||||
}
|
||||
|
||||
openFile(path: string) {
|
||||
if (!this.state.trees.find((e) => e.filepath == path)) {
|
||||
const trees = this.state.trees;
|
||||
trees.push({ filepath: path });
|
||||
this.setState({ trees, curTree: path });
|
||||
} else {
|
||||
this.setState({ curTree: path });
|
||||
}
|
||||
}
|
||||
|
||||
closeFile(path: string) {
|
||||
let trees = this.state.trees;
|
||||
trees = trees.filter((tree) => tree.filepath != path);
|
||||
const length = trees.length;
|
||||
this.setState({ trees: trees, curTree: length > 0 ? trees[0].filepath : null });
|
||||
if (length > 0) {
|
||||
const editor = this.editors[trees[0].filepath];
|
||||
editor?.reload();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { trees, curTree } = this.state;
|
||||
console.log("render tabs", trees);
|
||||
if (!curTree) {
|
||||
return <div />;
|
||||
}
|
||||
this.editors = {};
|
||||
return (
|
||||
<Tabs
|
||||
hideAdd
|
||||
className="tabs"
|
||||
type="editable-card"
|
||||
defaultActiveKey={curTree}
|
||||
activeKey={curTree}
|
||||
onChange={(activeKey) => {
|
||||
const editor = this.editors[activeKey];
|
||||
editor?.reload();
|
||||
this.setState({ curTree: activeKey });
|
||||
}}
|
||||
onEdit={(targetKey, action) => {
|
||||
if (action == "remove") {
|
||||
this.closeFile(targetKey as string);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{trees.map((tree) => {
|
||||
return (
|
||||
<TabPane
|
||||
tab={`${path.basename(tree.filepath)}${tree.unsave ? "*" : ""}`}
|
||||
key={tree.filepath}
|
||||
>
|
||||
<Editor
|
||||
filepath={tree.filepath}
|
||||
onChangeSaveState={(unsave) => {
|
||||
if (tree.unsave != unsave) {
|
||||
tree.unsave = unsave;
|
||||
this.forceUpdate();
|
||||
}
|
||||
}}
|
||||
onOpenSubtree={(path) => {
|
||||
const settings = Utils.getRemoteSettings();
|
||||
this.openFile(settings.workdir + "/" + path);
|
||||
}}
|
||||
ref={(ref) => {
|
||||
this.editors[tree.filepath] = ref;
|
||||
}}
|
||||
/>
|
||||
</TabPane>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
body {
|
||||
/* background-color: #242424; */
|
||||
background-color: rgb(20, 20, 20);
|
||||
padding : 0px;
|
||||
}
|
||||
|
||||
.body {
|
||||
width : 100%;
|
||||
height : 100%;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.ant-layout {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.sider {
|
||||
height : 100%;
|
||||
overflow-y : auto;
|
||||
overflow-x : hidden;
|
||||
transition : all 0s;
|
||||
}
|
||||
|
||||
.sider:hover {
|
||||
overflow-y : auto;
|
||||
}
|
||||
|
||||
#sizer-hint-bar {
|
||||
position : absolute;
|
||||
width : 10px;
|
||||
height : 100vh;
|
||||
background : #66ccff;
|
||||
top : 0;
|
||||
left : 245px;
|
||||
opacity : 0;
|
||||
transition : opacity 0.3s;
|
||||
cursor : col-resize;
|
||||
z-index : 10;
|
||||
}
|
||||
|
||||
.content {
|
||||
width : 100%;
|
||||
height : 100%;
|
||||
display: flex;
|
||||
/* background-color: #242424; */
|
||||
background-color: rgb(20, 20, 20);
|
||||
|
||||
}
|
||||
|
||||
.tabs {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
background-color: rgb(20, 20, 20);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
height : 50px;
|
||||
background-color: #818181;
|
||||
}
|
||||
|
||||
|
||||
[data-theme="dark"] .site-tree-search-value {
|
||||
color: #d84a1b;
|
||||
}
|
||||
|
||||
|
||||
.explorer-node {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: 20px;
|
||||
white-space: nowrap;
|
||||
list-style: none;
|
||||
outline: 0;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.explorer-node-selected {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: 20px;
|
||||
white-space: nowrap;
|
||||
list-style: none;
|
||||
outline: 0;
|
||||
color: #ffffff;
|
||||
background-color: #818181;
|
||||
}
|
||||
|
||||
.explorer-node-title {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.explorer-node-search-value {
|
||||
color: #f50;
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
import * as ReactDOM from "react-dom";
|
||||
import SyncRequest from "sync-request";
|
||||
import React, { Component } from "react";
|
||||
import { Layout, message, Tabs } from "antd";
|
||||
import * as Utils from "../common/Utils";
|
||||
import MainEventType from "../common/MainEventType";
|
||||
import Settings from "../main-process/Settings";
|
||||
import { ipcRenderer } from "electron";
|
||||
import "antd/dist/antd.dark.css";
|
||||
import "./index.css";
|
||||
import RegisterNode from "./RegisterNode";
|
||||
import TreeTabs from "./TreeTabs";
|
||||
import Explorer from "./Explorer";
|
||||
import BatchExec from "../common/BatchExec";
|
||||
|
||||
const { Sider, Content } = Layout;
|
||||
|
||||
interface MainState {
|
||||
workdir: string;
|
||||
workspace: string;
|
||||
}
|
||||
|
||||
export default class Main extends Component {
|
||||
state: MainState = {
|
||||
workdir: "",
|
||||
workspace: "",
|
||||
};
|
||||
|
||||
settings: Settings;
|
||||
tabs: TreeTabs;
|
||||
explorer: Explorer;
|
||||
session = 0;
|
||||
|
||||
componentDidMount() {
|
||||
this.updateSettings();
|
||||
RegisterNode(this.settings);
|
||||
|
||||
ipcRenderer.on(MainEventType.OPEN_FILE, (event: any, path: any) => {
|
||||
this.setState({ curPath: path });
|
||||
});
|
||||
|
||||
ipcRenderer.on(MainEventType.OPEN_DIR, (event: any, workdir: any, workspace: string) => {
|
||||
console.log("on open workspace", workspace);
|
||||
document.title = workspace;
|
||||
this.setState({ workdir, workspace });
|
||||
});
|
||||
|
||||
ipcRenderer.on(MainEventType.RELOAD_SERVER, () => {
|
||||
const serverModel = this.settings.serverModel;
|
||||
if (!serverModel) {
|
||||
message.warning("没有配置服务器");
|
||||
return;
|
||||
}
|
||||
SyncRequest("POST", serverModel.host, {
|
||||
body: JSON.stringify({
|
||||
cmd: "btree.reload",
|
||||
data: {
|
||||
trees: this.tabs.getOpenTreesModel(),
|
||||
},
|
||||
session: this.session++,
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
});
|
||||
message.success("服务器已更新");
|
||||
});
|
||||
|
||||
ipcRenderer.on(MainEventType.BATCH_EXEC, (event: any, path: string) => {
|
||||
BatchExec(path, this.state.workdir);
|
||||
});
|
||||
|
||||
console.log("workdir", this.settings.curWorkspace.getWorkdir());
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
workdir: this.settings.curWorkspace.getWorkdir(),
|
||||
workspace: this.settings.curWorkspace.getFilepath(),
|
||||
});
|
||||
}, 50);
|
||||
|
||||
this.setReSizer();
|
||||
}
|
||||
|
||||
setReSizer() {
|
||||
let startX: number;
|
||||
let startWidth: number;
|
||||
|
||||
const sider: any = document.getElementsByClassName("sider")[0];
|
||||
const reSizerParent: any = document.getElementsByClassName("ant-layout-sider-children")[0];
|
||||
const reSizer: any = document.getElementById("sizer-hint-bar");
|
||||
const content: any = document.getElementsByClassName("ant-layout-content content")[0];
|
||||
|
||||
function initDrag(e: DragEvent) {
|
||||
startX = e.clientX;
|
||||
startWidth = parseInt(document.defaultView.getComputedStyle(reSizerParent).width, 10);
|
||||
document.documentElement.addEventListener("mousemove", doDrag, false);
|
||||
document.documentElement.addEventListener("mouseup", stopDrag, false);
|
||||
}
|
||||
|
||||
function doDrag(e: DragEvent) {
|
||||
const calcWidth = startWidth + e.clientX - startX;
|
||||
setWidth(calcWidth);
|
||||
}
|
||||
|
||||
function setWidth(width: number) {
|
||||
const maxValue = window.innerWidth * 0.8;
|
||||
const minValue = 100;
|
||||
if (width > maxValue) {
|
||||
width = maxValue;
|
||||
} else if (width < minValue) {
|
||||
width = minValue;
|
||||
}
|
||||
reSizerParent.style.width = width + "px";
|
||||
sider.style.width = width + "px";
|
||||
sider.style.maxWidth = width + "px";
|
||||
sider.style.minWidth = "0px";
|
||||
sider.style.flex = "";
|
||||
reSizer.style.left = width - 5 + "px";
|
||||
content.style.width = window.innerWidth - width + "px";
|
||||
}
|
||||
|
||||
function stopDrag(e: DragEvent) {
|
||||
document.documentElement.removeEventListener("mousemove", doDrag, false);
|
||||
document.documentElement.removeEventListener("mouseup", stopDrag, false);
|
||||
}
|
||||
|
||||
reSizer.addEventListener(
|
||||
"mouseover",
|
||||
function () {
|
||||
reSizer.style.opacity = "1";
|
||||
reSizer.addEventListener("mousedown", initDrag, false);
|
||||
reSizer.addEventListener(
|
||||
"mouseout",
|
||||
function init() {
|
||||
reSizer.style.opacity = "0";
|
||||
},
|
||||
false
|
||||
);
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
window.addEventListener("resize", function () {
|
||||
const currentWidth = parseInt(
|
||||
document.defaultView.getComputedStyle(reSizerParent).width,
|
||||
10
|
||||
);
|
||||
setWidth(currentWidth);
|
||||
});
|
||||
|
||||
setWidth(250);
|
||||
}
|
||||
|
||||
updateSettings() {
|
||||
this.settings = Utils.getRemoteSettings();
|
||||
}
|
||||
|
||||
render() {
|
||||
console.log("render main");
|
||||
const { workdir, workspace } = this.state;
|
||||
document.title = `行为树编辑器 - ${workspace}`;
|
||||
return (
|
||||
<Layout className="body" style={{ flexDirection: "column" }}>
|
||||
<Sider className="sider" width={250}>
|
||||
{workdir !== "" ? (
|
||||
<Explorer
|
||||
ref={(ref) => {
|
||||
this.explorer = ref;
|
||||
}}
|
||||
workdir={workdir}
|
||||
onOpenTree={(path) => {
|
||||
this.tabs.openFile(path);
|
||||
}}
|
||||
onDeleteTree={(path) => {
|
||||
this.tabs.closeFile(path);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
"Please Open Workspace"
|
||||
)}
|
||||
</Sider>
|
||||
<div id="sizer-hint-bar" />
|
||||
<Content className="content">
|
||||
<TreeTabs
|
||||
ref={(ref) => {
|
||||
this.tabs = ref;
|
||||
}}
|
||||
onTabSelected={(path) => {
|
||||
this.explorer.selectNode(path);
|
||||
}}
|
||||
/>
|
||||
</Content>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.render(<Main />, document.getElementById("root") as HTMLElement);
|
||||
@@ -0,0 +1,236 @@
|
||||
[
|
||||
{
|
||||
"name": "AlwaysFail",
|
||||
"type": "Decorator",
|
||||
"doc": "+ 只能有一个子节点,多个仅执行第一个\n+ 不管子节点是否成功都返回失败\n",
|
||||
"desc": "始终返回失败"
|
||||
},
|
||||
{
|
||||
"name": "AlwaysSuccess",
|
||||
"type": "Decorator",
|
||||
"doc": "+ 只能有一个子节点,多个仅执行第一个\n+ 不管子节点是否成功都返回成功\n",
|
||||
"desc": "始终返回成功"
|
||||
},
|
||||
{
|
||||
"name": "Attack",
|
||||
"type": "Action",
|
||||
"input": [
|
||||
"{目标}"
|
||||
],
|
||||
"desc": "攻击"
|
||||
},
|
||||
{
|
||||
"name": "Cmp",
|
||||
"type": "Condition",
|
||||
"doc": "+ 若值为空,返回失败\n+ 非整数类型可能会报错\n",
|
||||
"input": [
|
||||
"值(int)"
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "value",
|
||||
"desc": "值",
|
||||
"type": "code?"
|
||||
},
|
||||
{
|
||||
"name": "gt",
|
||||
"desc": ">",
|
||||
"type": "int?"
|
||||
},
|
||||
{
|
||||
"name": "ge",
|
||||
"desc": ">=",
|
||||
"type": "int?"
|
||||
},
|
||||
{
|
||||
"name": "eq",
|
||||
"desc": "==",
|
||||
"type": "int?"
|
||||
},
|
||||
{
|
||||
"name": "le",
|
||||
"desc": "<=",
|
||||
"type": "int?"
|
||||
},
|
||||
{
|
||||
"name": "lt",
|
||||
"desc": "<",
|
||||
"type": "int?"
|
||||
}
|
||||
],
|
||||
"desc": "比较值大小"
|
||||
},
|
||||
{
|
||||
"name": "FindEnemy",
|
||||
"output": [
|
||||
"目标单位"
|
||||
],
|
||||
"type": "Condition",
|
||||
"doc": "+ 没找到返回失败\n",
|
||||
"args": [
|
||||
{
|
||||
"name": "x",
|
||||
"desc": "x",
|
||||
"type": "int?"
|
||||
},
|
||||
{
|
||||
"name": "y",
|
||||
"desc": "y",
|
||||
"type": "int?"
|
||||
},
|
||||
{
|
||||
"name": "w",
|
||||
"desc": "宽",
|
||||
"type": "int?"
|
||||
},
|
||||
{
|
||||
"name": "h",
|
||||
"desc": "高",
|
||||
"type": "int?"
|
||||
},
|
||||
{
|
||||
"name": "count",
|
||||
"desc": "查找上限",
|
||||
"type": "string?"
|
||||
}
|
||||
],
|
||||
"desc": "查找敌人"
|
||||
},
|
||||
{
|
||||
"name": "GetHp",
|
||||
"output": [
|
||||
"生命值"
|
||||
],
|
||||
"type": "Action",
|
||||
"desc": "获取生命值"
|
||||
},
|
||||
{
|
||||
"name": "Idle",
|
||||
"type": "Action",
|
||||
"desc": "待机"
|
||||
},
|
||||
{
|
||||
"name": "Log",
|
||||
"type": "Action",
|
||||
"args": [
|
||||
{
|
||||
"name": "str",
|
||||
"desc": "日志",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"desc": "打印日志"
|
||||
},
|
||||
{
|
||||
"name": "MoveToPos",
|
||||
"type": "Action",
|
||||
"args": [
|
||||
{
|
||||
"name": "x",
|
||||
"desc": "x",
|
||||
"type": "int"
|
||||
},
|
||||
{
|
||||
"name": "y",
|
||||
"desc": "y",
|
||||
"type": "int"
|
||||
}
|
||||
],
|
||||
"desc": "移动到坐标"
|
||||
},
|
||||
{
|
||||
"name": "MoveToTarget",
|
||||
"type": "Action",
|
||||
"input": [
|
||||
"{目标}"
|
||||
],
|
||||
"desc": "移动到目标"
|
||||
},
|
||||
{
|
||||
"name": "Not",
|
||||
"type": "Decorator",
|
||||
"doc": "+ 将子节点的返回值取反\n",
|
||||
"desc": "取反"
|
||||
},
|
||||
{
|
||||
"name": "Parallel",
|
||||
"type": "Composite",
|
||||
"doc": "执行所有子节点并返回成功\n",
|
||||
"desc": "并行执行"
|
||||
},
|
||||
{
|
||||
"name": "Selector",
|
||||
"type": "Composite",
|
||||
"doc": "+ 一直往下执行,有子节点返回成功则返回成功,若全部节点返回失败则返回失败\n+ 子节点是或的关系\n",
|
||||
"desc": "选择执行"
|
||||
},
|
||||
{
|
||||
"name": "Sequence",
|
||||
"type": "Composite",
|
||||
"doc": "+ 一直往下执行,有子节点返回成功则返回成功,若全部节点返回失败则返回失败\n+ 子节点是或的关系\n",
|
||||
"desc": "顺序执行"
|
||||
},
|
||||
{
|
||||
"name": "Wait",
|
||||
"type": "Action",
|
||||
"args": [
|
||||
{
|
||||
"name": "time",
|
||||
"desc": "时间/tick",
|
||||
"type": "int"
|
||||
}
|
||||
],
|
||||
"desc": "等待"
|
||||
},
|
||||
{
|
||||
"name": "TestB3",
|
||||
"type": "Action",
|
||||
"args": [
|
||||
{
|
||||
"name": "time",
|
||||
"desc": "时间",
|
||||
"type": "int"
|
||||
},
|
||||
{
|
||||
"name": "random",
|
||||
"desc": "随机",
|
||||
"type": "float?"
|
||||
},
|
||||
{
|
||||
"name": "open",
|
||||
"desc": "打开来",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"name": "check",
|
||||
"desc": "检查",
|
||||
"type": "code"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"desc": "检查",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"desc": "状态",
|
||||
"type": "enum",
|
||||
"options": [
|
||||
{
|
||||
"name": "成功",
|
||||
"value": "SUCCESS"
|
||||
},
|
||||
{
|
||||
"name": "失败",
|
||||
"value": "FAILURE"
|
||||
},
|
||||
{
|
||||
"name": "运行中",
|
||||
"value": "RUNNING"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"desc": "等待"
|
||||
}
|
||||
]
|
||||
@@ -1 +0,0 @@
|
||||
[{"name":"AlwaysFail","type":"Decorator","doc":"+ 只能有一个子节点,多个仅执行第一个\n+ 不管子节点是否成功都返回失败\n","desc":"始终返回失败"},{"name":"AlwaysSuccess","type":"Decorator","doc":"+ 只能有一个子节点,多个仅执行第一个\n+ 不管子节点是否成功都返回成功\n","desc":"始终返回成功"},{"name":"Attack","type":"Action","input":["{目标}"],"desc":"攻击"},{"name":"Cmp","type":"Condition","doc":"+ 若值为空,返回失败\n+ 非整数类型可能会报错\n","input":["值(int)"],"args":[{"name":"value","desc":"值","type":"code?"},{"name":"gt","desc":">","type":"int?"},{"name":"ge","desc":">=","type":"int?"},{"name":"eq","desc":"==","type":"int?"},{"name":"le","desc":"<=","type":"int?"},{"name":"lt","desc":"<","type":"int?"}],"desc":"比较值大小"},{"name":"FindEnemy","output":["目标单位"],"type":"Condition","doc":"+ 没找到返回失败\n","args":[{"name":"x","desc":"x","type":"int?"},{"name":"y","desc":"y","type":"int?"},{"name":"w","desc":"宽","type":"int?"},{"name":"h","desc":"高","type":"int?"},{"name":"count","desc":"查找上限","type":"string?"}],"desc":"查找敌人"},{"name":"GetHp","output":["生命值"],"type":"Action","desc":"获取生命值"},{"name":"Idle","type":"Action","desc":"待机"},{"name":"Log","type":"Action","args":[{"name":"str","desc":"日志","type":"string"}],"desc":"打印日志"},{"name":"MoveToPos","type":"Action","args":[{"name":"x","desc":"x","type":"int"},{"name":"y","desc":"y","type":"int"}],"desc":"移动到坐标"},{"name":"MoveToTarget","type":"Action","input":["{目标}"],"desc":"移动到目标"},{"name":"Not","type":"Decorator","doc":"+ 将子节点的返回值取反\n","desc":"取反"},{"name":"Parallel","type":"Composite","doc":"执行所有子节点并返回成功\n","desc":"并行执行"},{"name":"Selector","type":"Composite","doc":"+ 一直往下执行,有子节点返回成功则返回成功,若全部节点返回失败则返回失败\n+ 子节点是或的关系\n","desc":"选择执行"},{"name":"Sequence","type":"Composite","doc":"+ 一直往下执行,有子节点返回成功则返回成功,若全部节点返回失败则返回失败\n+ 子节点是或的关系\n","desc":"顺序执行"},{"name":"Wait","type":"Action","args":[{"name":"time","desc":"时间/tick","type":"int"}],"desc":"等待"}]
|
||||
@@ -24,13 +24,12 @@
|
||||
"id": 4,
|
||||
"name": "Sequence",
|
||||
"desc": "新建行为树",
|
||||
"args": {},
|
||||
"path": "subtree2.json"
|
||||
"path": "workdir/subtree2.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"id": 7,
|
||||
"name": "Cmp",
|
||||
"args": {
|
||||
"gt": 50,
|
||||
@@ -41,24 +40,23 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "Log",
|
||||
"id": 8,
|
||||
"name": "TestB3",
|
||||
"desc": "攻击",
|
||||
"args": {
|
||||
"str": "Attack!"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"id": 9,
|
||||
"name": "Sequence",
|
||||
"desc": "新建行为树",
|
||||
"path": "subtree1.json"
|
||||
"path": "workdir/subtree1.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"id": 12,
|
||||
"name": "Log",
|
||||
"desc": "逃跑",
|
||||
"args": {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "subtree1",
|
||||
"root": {
|
||||
"id": 1,
|
||||
"name": "Sequence",
|
||||
"desc": "新建行为树",
|
||||
"children": [
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Log",
|
||||
"args": {
|
||||
"str": "hello"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Wait",
|
||||
"args": {
|
||||
"time": 1
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": 4,
|
||||
"name": "unknow"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"desc": "子树1"
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "subtree2",
|
||||
"root": {
|
||||
"id": 1,
|
||||
"name": "Sequence",
|
||||
"desc": "新建行为树",
|
||||
"children": [
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Cmp",
|
||||
"args": {},
|
||||
"input": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Attack",
|
||||
"input": [
|
||||
"in"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"desc": "子树2"
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "subtree",
|
||||
"name": "subtree1",
|
||||
"root": {
|
||||
"id": 1,
|
||||
"name": "Sequence",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="216" height="216" viewBox="0 0 216 216"><path fill="#80bd01" d="M104.6 180.7c-2 0-3.9-.5-5.7-1.5l-18.1-10.7c-2.7-1.5-1.4-2-.5-2.4 3.6-1.2 4.3-1.5 8.2-3.7.4-.2.9-.1 1.3.1l13.9 8.2c.5.3 1.2.3 1.7 0l54.1-31.2c.5-.3.8-.9.8-1.5V75.7c0-.6-.3-1.2-.8-1.5l-54-31.2c-.5-.3-1.2-.3-1.7 0l-54 31.2c-.5.3-.9.9-.9 1.5v62.4c0 .6.3 1.2.9 1.4l14.8 8.6c8 4 13-.7 13-5.5V81c0-.9.7-1.6 1.6-1.6h6.9c.9 0 1.6.7 1.6 1.6v61.6c0 10.7-5.8 16.9-16 16.9-3.1 0-5.6 0-12.5-3.4L44.8 148c-3.5-2-5.7-5.8-5.7-9.9V75.7c0-4.1 2.2-7.8 5.7-9.9l54.1-31.2c3.4-1.9 8-1.9 11.4 0l54.1 31.2c3.5 2 5.7 5.8 5.7 9.9v62.4c0 4.1-2.2 7.8-5.7 9.9l-54.1 31.2c-1.8 1-3.7 1.5-5.7 1.5zm43.6-61.5c0-11.7-7.9-14.8-24.5-17-16.8-2.2-18.5-3.4-18.5-7.3 0-3.2 1.4-7.6 13.9-7.6 11.1 0 15.2 2.4 16.9 9.9.1.7.8 1.2 1.5 1.2h7c.4 0 .8-.2 1.1-.5.3-.3.5-.8.4-1.2-1.1-12.9-9.7-18.9-27-18.9-15.4 0-24.6 6.5-24.6 17.4 0 11.8 9.1 15.1 23.9 16.6 17.7 1.7 19.1 4.3 19.1 7.8 0 6-4.8 8.6-16.2 8.6-14.3 0-17.5-3.6-18.5-10.7-.1-.8-.8-1.3-1.6-1.3h-7c-.9 0-1.6.7-1.6 1.6 0 9.1 5 20 28.6 20 17.3-.1 27.1-6.8 27.1-18.6zM172 55.9V57h3v8h1.2v-8h3.1v-1.1H172zm8.4 9.1h1.2v-7.6l2.6 7.6h1.2l2.6-7.6V65h1.2v-9h-1.7l-2.6 7.6-2.6-7.6h-1.8v9z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,936 @@
|
||||
import {
|
||||
EditEvent,
|
||||
EditNode,
|
||||
EditTree,
|
||||
EditorStore,
|
||||
useWorkspace,
|
||||
} from "@/contexts/workspace-context";
|
||||
import { NodeModel, TreeGraphData, TreeModel } from "@/misc/b3type";
|
||||
import * as b3util from "@/misc/b3util";
|
||||
import { message } from "@/misc/hooks";
|
||||
import { Hotkey, isHotkeyPressed, useHotkeys } from "@/misc/keys";
|
||||
import Path from "@/misc/path";
|
||||
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 { clipboard } from "electron";
|
||||
import * as fs from "fs";
|
||||
import React, { FC, useCallback, useEffect, useMemo, useRef } 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 "./register-node";
|
||||
|
||||
export interface EditorProps extends React.HTMLAttributes<HTMLElement> {
|
||||
data: EditorStore;
|
||||
onUpdate: () => void;
|
||||
}
|
||||
|
||||
export const Editor: FC<EditorProps> = ({ onUpdate: updateState, data: editor, ...props }) => {
|
||||
const workspace = {
|
||||
editing: useWorkspace((state) => state.editing),
|
||||
onEditingNode: useWorkspace((state) => state.onEditingNode),
|
||||
onEditingTree: useWorkspace((state) => state.onEditingTree),
|
||||
open: useWorkspace((state) => state.open),
|
||||
workdir: useWorkspace((state) => state.workdir),
|
||||
};
|
||||
const graphRef = useRef(null);
|
||||
const sizeRef = useRef(null);
|
||||
const editorSize = useSize(sizeRef);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onChange = () => {
|
||||
if (!editor.unsave) {
|
||||
editor.unsave = true;
|
||||
updateState();
|
||||
}
|
||||
};
|
||||
|
||||
const findParent = (node: TreeGraphData) => {
|
||||
if (node.parent) {
|
||||
return findDataById(node.parent);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const findHightlight = (node: TreeGraphData, highlight: string[], changed?: TreeGraphData[]) => {
|
||||
changed ||= [];
|
||||
if (node.highlightInput || node.highlightOutput) {
|
||||
node.highlightInput = false;
|
||||
node.highlightOutput = false;
|
||||
changed.push(node);
|
||||
}
|
||||
|
||||
if (node.input) {
|
||||
for (const v of node.input) {
|
||||
if (highlight.includes(v)) {
|
||||
node.highlightInput = true;
|
||||
changed.push(node);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (node.output) {
|
||||
for (const v of node.output) {
|
||||
if (highlight.includes(v)) {
|
||||
node.highlightOutput = true;
|
||||
changed.push(node);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
node.children?.forEach((child: TreeGraphData) => findHightlight(child, highlight, changed));
|
||||
|
||||
return changed;
|
||||
};
|
||||
|
||||
const findDataById = (id: string) => {
|
||||
return editor.graph.findDataById(id) as TreeGraphData;
|
||||
};
|
||||
|
||||
const isAncestor = (ancestor: TreeGraphData, node: TreeGraphData): boolean => {
|
||||
if (ancestor.id === node.parent) {
|
||||
return true;
|
||||
} else if (node.parent) {
|
||||
return isAncestor(ancestor, findDataById(node.parent));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const isSubtreeNode = (node: TreeGraphData | null): boolean => {
|
||||
if (node?.path) {
|
||||
return true;
|
||||
} else if (node?.parent) {
|
||||
return isSubtreeNode(findParent(node));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const findSubtree = (node: TreeGraphData | null): TreeGraphData | null => {
|
||||
if (node?.path) {
|
||||
return node;
|
||||
} else if (node?.parent) {
|
||||
return findSubtree(findParent(node));
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const reload = () => {
|
||||
const rootData = findDataById("1");
|
||||
const rootNode = b3util.createNode(rootData);
|
||||
editor.data = b3util.createTreeData(rootNode);
|
||||
editor.autoId = b3util.refreshTreeDataId(editor.data);
|
||||
editor.graph.changeData(editor.data);
|
||||
editor.graph.layout();
|
||||
restoreViewport();
|
||||
};
|
||||
|
||||
const checkSubtree = () => {
|
||||
if (editor.graph) {
|
||||
const data = findDataById("1");
|
||||
if (b3util.isSubtreeUpdated(data)) {
|
||||
reload();
|
||||
pushHistory();
|
||||
onChange();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const clearDragDstState = () => {
|
||||
if (editor.dragDstId) {
|
||||
setItemState(editor.dragDstId, "dragRight", false);
|
||||
setItemState(editor.dragDstId, "dragDown", false);
|
||||
setItemState(editor.dragDstId, "dragUp", false);
|
||||
editor.dragDstId = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const clearDragSrcState = () => {
|
||||
if (editor.dragSrcId) {
|
||||
setItemState(editor.dragSrcId, "dragSrc", false);
|
||||
editor.dragSrcId = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const setItemState = (item: string | Item, state: string, value: string | boolean) => {
|
||||
if (typeof item === "string") {
|
||||
item = editor.graph.findById(item);
|
||||
}
|
||||
if (item) {
|
||||
editor.graph.setItemState(item, state, value);
|
||||
}
|
||||
};
|
||||
|
||||
const updateNode = (editNode: EditNode) => {
|
||||
const newNode = editNode.data;
|
||||
const data = findDataById(editNode.data.id.toString());
|
||||
const oldNode = b3util.createNode(data, false);
|
||||
if (b3util.isNodeEqual(oldNode, newNode)) {
|
||||
return;
|
||||
}
|
||||
b3util.copyFromNode(data, newNode);
|
||||
data.size = b3util.calcTreeDataSize(data);
|
||||
if (oldNode.path !== newNode.path) {
|
||||
reload();
|
||||
selectNode(null);
|
||||
selectNode(data.id);
|
||||
} else {
|
||||
const item = editor.graph.findById(data.id);
|
||||
item.draw();
|
||||
}
|
||||
pushHistory();
|
||||
onChange();
|
||||
};
|
||||
|
||||
const updateTree = (editTree: EditTree) => {
|
||||
if (editor.desc !== editTree.data.desc) {
|
||||
editor.desc = editTree.data.desc || "";
|
||||
onChange();
|
||||
}
|
||||
};
|
||||
|
||||
const selectNode = (id: string | null) => {
|
||||
if (id && id === editor.selectedId) {
|
||||
setItemState(editor.selectedId, "selected", true);
|
||||
setItemState(editor.selectedId, "hover", false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (editor.selectedId) {
|
||||
setItemState(editor.selectedId, "selected", false);
|
||||
setItemState(editor.selectedId, "hover", false);
|
||||
}
|
||||
|
||||
editor.selectedId = id;
|
||||
|
||||
if (editor.selectedId) {
|
||||
const data = findDataById(editor.selectedId);
|
||||
workspace.onEditingTree(null);
|
||||
workspace.onEditingNode({
|
||||
data: b3util.createNode(data, false),
|
||||
editable: !isSubtreeNode(data),
|
||||
});
|
||||
setItemState(editor.selectedId, "selected", true);
|
||||
} else {
|
||||
workspace.onEditingTree({
|
||||
data: {
|
||||
name: editor.name,
|
||||
desc: editor.desc,
|
||||
root: null!,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const restoreViewport = () => {
|
||||
if (editor.graphMatrix) {
|
||||
editor.graph.getGroup().setMatrix(editor.graphMatrix);
|
||||
}
|
||||
};
|
||||
|
||||
const pushHistory = () => {
|
||||
editor.historyStack.length = ++editor.historyIndex;
|
||||
editor.historyStack.push(b3util.createNode(findDataById("1")));
|
||||
};
|
||||
|
||||
const copyNode = () => {
|
||||
if (editor.selectedId) {
|
||||
const data = findDataById(editor.selectedId);
|
||||
if (data) {
|
||||
const str = JSON.stringify(b3util.createNode(data), null, 2);
|
||||
clipboard.writeText(str);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const pasteNode = () => {
|
||||
if (!editor.selectedId) {
|
||||
message.warning(t("node.noNodeSelected"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSubtreeNode(findDataById(editor.selectedId))) {
|
||||
message.warning(t("node.editSubtreeDenied"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const str = clipboard.readText();
|
||||
if (!str || str == "") {
|
||||
return;
|
||||
}
|
||||
const curNodeData = findDataById(editor.selectedId);
|
||||
const data = b3util.createTreeData(JSON.parse(str), editor.selectedId);
|
||||
editor.autoId = b3util.refreshTreeDataId(data, editor.autoId);
|
||||
selectNode(null);
|
||||
curNodeData.children ||= [];
|
||||
curNodeData.children.push(data);
|
||||
updateGrahp();
|
||||
pushHistory();
|
||||
onChange();
|
||||
} catch (error) {
|
||||
message.error(t("node.pasteDataError"));
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const replaceNode = () => {
|
||||
if (!editor.selectedId) {
|
||||
message.warning(t("node.noNodeSelected"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSubtreeNode(findDataById(editor.selectedId))) {
|
||||
message.warning(t("node.editSubtreeDenied"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const str = clipboard.readText();
|
||||
if (!str || str == "") {
|
||||
return;
|
||||
}
|
||||
const curNodeData = findDataById(editor.selectedId);
|
||||
if (curNodeData.parent) {
|
||||
const parentData = findDataById(curNodeData.parent);
|
||||
const idx = parentData.children!.indexOf(curNodeData);
|
||||
const data = b3util.createTreeData(JSON.parse(str), editor.selectedId);
|
||||
editor.autoId = b3util.refreshTreeDataId(data, editor.autoId);
|
||||
parentData.children![idx] = data;
|
||||
updateGrahp();
|
||||
} else {
|
||||
editor.data = b3util.createTreeData(JSON.parse(str), editor.selectedId);
|
||||
editor.autoId = b3util.refreshTreeDataId(editor.data);
|
||||
editor.graph.changeData(editor.data);
|
||||
editor.graph.render();
|
||||
editor.graph.layout();
|
||||
}
|
||||
selectNode(null);
|
||||
pushHistory();
|
||||
onChange();
|
||||
} catch (error) {
|
||||
message.error(t("node.pasteDataError"));
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const createNode = () => {
|
||||
if (!editor.selectedId) {
|
||||
message.warning(t("node.noNodeSelected"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSubtreeNode(findDataById(editor.selectedId))) {
|
||||
message.warning(t("node.editSubtreeDenied"));
|
||||
return;
|
||||
}
|
||||
|
||||
const curNodeData = findDataById(editor.selectedId);
|
||||
const newNodeData: NodeModel = {
|
||||
id: editor.autoId++,
|
||||
name: "unknow",
|
||||
};
|
||||
curNodeData.children ||= [];
|
||||
curNodeData.children.push(b3util.createTreeData(newNodeData, editor.selectedId));
|
||||
updateGrahp();
|
||||
pushHistory();
|
||||
onChange();
|
||||
};
|
||||
|
||||
const deleteNode = () => {
|
||||
if (!editor.selectedId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (editor.selectedId === "1") {
|
||||
message.warning(t("node.deleteRootNodeDenied"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSubtreeNode(findDataById(editor.selectedId))) {
|
||||
message.warning(t("node.editSubtreeDenied"));
|
||||
return;
|
||||
}
|
||||
|
||||
const parentData = findParent(findDataById(editor.selectedId))!;
|
||||
parentData.children = parentData.children!.filter((e) => e.id != editor.selectedId);
|
||||
selectNode(null);
|
||||
updateGrahp();
|
||||
pushHistory();
|
||||
onChange();
|
||||
};
|
||||
|
||||
const useStackData = (data: NodeModel) => {
|
||||
editor.data = b3util.createTreeData(data);
|
||||
editor.autoId = b3util.refreshTreeDataId(editor.data);
|
||||
editor.graph.changeData(editor.data);
|
||||
editor.graph.layout();
|
||||
restoreViewport();
|
||||
onChange();
|
||||
};
|
||||
|
||||
const updateGrahp = () => {
|
||||
editor.graph.changeData();
|
||||
editor.graph.layout();
|
||||
};
|
||||
|
||||
const undo = () => {
|
||||
if (editor.historyIndex > 0) {
|
||||
const data = editor.historyStack[--editor.historyIndex];
|
||||
useStackData(data);
|
||||
}
|
||||
};
|
||||
|
||||
const redo = () => {
|
||||
if (editor.historyIndex < editor.historyStack.length - 1) {
|
||||
const data = editor.historyStack[++editor.historyIndex];
|
||||
useStackData(data);
|
||||
}
|
||||
};
|
||||
|
||||
const rename = (newPath: string) => {
|
||||
editor.path = newPath;
|
||||
editor.size.width = 0;
|
||||
editor.size.height = 0;
|
||||
};
|
||||
|
||||
const save = () => {
|
||||
if (editor.unsave || !fs.existsSync(editor.path)) {
|
||||
const path = editor.path;
|
||||
const data = findDataById("1");
|
||||
editor.autoId = b3util.refreshTreeDataId(data);
|
||||
const root = b3util.createFileData(data);
|
||||
const treeModel = {
|
||||
name: Path.basenameWithoutExt(path),
|
||||
root,
|
||||
desc: editor.desc,
|
||||
} as TreeModel;
|
||||
fs.writeFileSync(path, JSON.stringify(treeModel, null, 2));
|
||||
editor.unsave = false;
|
||||
|
||||
editor.data = b3util.createTreeData(root);
|
||||
editor.autoId = b3util.refreshTreeDataId(editor.data);
|
||||
editor.graph.changeData(editor.data);
|
||||
editor.graph.layout();
|
||||
restoreViewport();
|
||||
|
||||
updateState();
|
||||
}
|
||||
};
|
||||
|
||||
const editSubtree = () => {
|
||||
if (!editor.selectedId) {
|
||||
message.warning(t("node.noNodeSelected"));
|
||||
return;
|
||||
}
|
||||
const d1 = findDataById(editor.selectedId);
|
||||
const data = findSubtree(findDataById(editor.selectedId));
|
||||
if (data?.path) {
|
||||
const path = `${workspace.workdir}/${data.path}`;
|
||||
workspace.open(path);
|
||||
}
|
||||
};
|
||||
|
||||
const saveAsSubtree = () => {
|
||||
if (!editor.selectedId) {
|
||||
message.warning(t("node.noNodeSelected"));
|
||||
return;
|
||||
}
|
||||
if (editor.selectedId === "1") {
|
||||
message.warning(t("node.saveAsSubtree"));
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
const subpath = dialog.showSaveDialogSync({
|
||||
defaultPath: workspace.workdir,
|
||||
properties: ["showOverwriteConfirmation"],
|
||||
filters: [{ name: "Json", extensions: ["json"] }],
|
||||
});
|
||||
if (!subpath) {
|
||||
return;
|
||||
}
|
||||
if (subpath.indexOf(workspace.workdir) == -1) {
|
||||
message.warning(t("node.subtreePathError"));
|
||||
return;
|
||||
}
|
||||
const data = findDataById(editor.selectedId!);
|
||||
const subroot = b3util.createFileData(data);
|
||||
const subtreeModel = {
|
||||
name: Path.basename(subpath).slice(0, -5),
|
||||
root: subroot,
|
||||
desc: data.desc,
|
||||
} as TreeModel;
|
||||
fs.writeFileSync(subpath, JSON.stringify(subtreeModel, null, 2));
|
||||
data.path = subpath.substring(workspace.workdir.length + 1);
|
||||
reload();
|
||||
pushHistory();
|
||||
onChange();
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const destroyGraph = () => {
|
||||
if (editor.graph) {
|
||||
editor.graph.destroy();
|
||||
editor.graph = undefined!;
|
||||
}
|
||||
};
|
||||
|
||||
const createGraph = (ref: React.MutableRefObject<null>) => {
|
||||
editor.graph = new TreeGraph({
|
||||
container: ref.current!,
|
||||
animate: false,
|
||||
maxZoom: 2,
|
||||
minZoom: 0.4,
|
||||
modes: {
|
||||
default: [
|
||||
"drag-canvas",
|
||||
"zoom-canvas",
|
||||
"click-select",
|
||||
"hover",
|
||||
{
|
||||
type: "collapse-expand",
|
||||
trigger: "dblclick",
|
||||
onChange: (item, collapsed) => {
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
selectNode(item.getID());
|
||||
const data = item.getModel();
|
||||
data.collapsed = collapsed;
|
||||
setItemState(item, "collapsed", data.collapsed as boolean);
|
||||
const icon = data.collapsed ? G6.Marker.expand : G6.Marker.collapse;
|
||||
const marker = item
|
||||
.get("group")
|
||||
.find((ele: any) => ele.get("name") === "collapse-icon");
|
||||
editor.graph.refresh();
|
||||
marker.attr("symbol", icon);
|
||||
return true;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
defaultEdge: {
|
||||
type: "cubic-horizontal",
|
||||
size: 2.5,
|
||||
style: {
|
||||
stroke: "#A3B1BF",
|
||||
},
|
||||
},
|
||||
defaultNode: {
|
||||
type: "TreeNode",
|
||||
},
|
||||
layout: {
|
||||
type: "compactBox",
|
||||
direction: "LR",
|
||||
getHGap: () => 60,
|
||||
getWidth: (d: TreeGraphData) => {
|
||||
return 150;
|
||||
},
|
||||
getHeight: (d: TreeGraphData) => {
|
||||
if (d.size) {
|
||||
return d.size[1];
|
||||
} else {
|
||||
return 50;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
editor.graph.on("viewportchange", (data: any) => {
|
||||
if (data.action == "translate" || data.action == "zoom") {
|
||||
editor.graphMatrix = data.matrix;
|
||||
}
|
||||
});
|
||||
|
||||
editor.graph.on("node:click", (e: G6GraphEvent) => {
|
||||
const highlight: string[] = [];
|
||||
if (e.shape.cfg.name === "input-text") {
|
||||
const data = findDataById(e.item.getID());
|
||||
data.input?.forEach((v) => v && highlight.push(v));
|
||||
} else if (e.shape.cfg.name === "output-text") {
|
||||
const data = findDataById(e.item.getID());
|
||||
data.output?.forEach((v) => v && highlight.push(v));
|
||||
}
|
||||
const changed = findHightlight(findDataById("1"), highlight);
|
||||
changed.forEach((v) => {
|
||||
const item = editor.graph.findById(v.id);
|
||||
item.draw();
|
||||
});
|
||||
});
|
||||
|
||||
editor.graph.on("node:contextmenu", (e: G6GraphEvent) => {
|
||||
selectNode(e.item.getID());
|
||||
});
|
||||
|
||||
editor.graph.on("node:mouseenter", (e: G6GraphEvent) => {
|
||||
if (!e.item.hasState("selected")) {
|
||||
setItemState(e.item, "hover", true);
|
||||
}
|
||||
});
|
||||
|
||||
editor.graph.on("node:mouseleave", (e: G6GraphEvent) => {
|
||||
if (!e.item.hasState("selected")) {
|
||||
setItemState(e.item, "hover", false);
|
||||
}
|
||||
});
|
||||
|
||||
editor.graph.on("nodeselectchange", (e: G6GraphEvent) => {
|
||||
if (e.target) {
|
||||
selectNode((e.target as unknown as Item).getID());
|
||||
} else {
|
||||
selectNode(null);
|
||||
}
|
||||
});
|
||||
|
||||
editor.graph.on("node:dragstart", (e: G6GraphEvent) => {
|
||||
editor.dragSrcId = e.item.getID();
|
||||
setItemState(editor.dragSrcId, "dragSrc", true);
|
||||
});
|
||||
|
||||
editor.graph.on("node:dragend", (e: G6GraphEvent) => {
|
||||
if (editor.dragSrcId) {
|
||||
setItemState(editor.dragSrcId, "dragSrc", false);
|
||||
editor.dragSrcId = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
editor.graph.on("node:dragover", (e: G6GraphEvent) => {
|
||||
const dstNodeId = e.item.getID();
|
||||
if (dstNodeId === editor.dragSrcId) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearDragDstState();
|
||||
|
||||
const box = e.item.getBBox();
|
||||
if (e.x > box.minX + box.width * 0.6) {
|
||||
setItemState(dstNodeId, "dragRight", true);
|
||||
} else if (e.y > box.minY + box.height * 0.5) {
|
||||
setItemState(dstNodeId, "dragDown", true);
|
||||
} else {
|
||||
setItemState(dstNodeId, "dragUp", true);
|
||||
}
|
||||
editor.dragDstId = dstNodeId;
|
||||
});
|
||||
|
||||
editor.graph.on("node:dragleave", (e: G6GraphEvent) => {
|
||||
clearDragDstState();
|
||||
});
|
||||
|
||||
editor.graph.on("node:drop", (e: G6GraphEvent) => {
|
||||
const srcNodeId = editor.dragSrcId;
|
||||
const dstNode = e.item;
|
||||
|
||||
let dragDir;
|
||||
if (dstNode.hasState("dragRight")) {
|
||||
dragDir = "dragRight";
|
||||
} else if (dstNode.hasState("dragDown")) {
|
||||
dragDir = "dragDown";
|
||||
} else if (dstNode.hasState("dragUp")) {
|
||||
dragDir = "dragUp";
|
||||
}
|
||||
|
||||
clearDragSrcState();
|
||||
clearDragDstState();
|
||||
|
||||
if (!srcNodeId) {
|
||||
console.log("no drag src");
|
||||
return;
|
||||
}
|
||||
|
||||
if (srcNodeId == dstNode.getID()) {
|
||||
console.log("drop same node");
|
||||
return;
|
||||
}
|
||||
|
||||
const srcData = findDataById(srcNodeId);
|
||||
const srcParent = findParent(srcData);
|
||||
const dstData = findDataById(dstNode.getID());
|
||||
const dstParent = findParent(dstData);
|
||||
if (!srcParent) {
|
||||
console.log("no parent!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (isAncestor(srcData, dstData)) {
|
||||
console.log("cannot move to child");
|
||||
return;
|
||||
}
|
||||
|
||||
const removeSrc = () => {
|
||||
srcParent.children = srcParent.children!.filter((value) => value.id != srcData.id);
|
||||
};
|
||||
|
||||
if (dragDir == "dragRight") {
|
||||
removeSrc();
|
||||
if (!dstData.children) {
|
||||
dstData.children = [];
|
||||
}
|
||||
srcData.parent = dstData.id;
|
||||
dstData.children.push(srcData);
|
||||
} else if (dragDir == "dragUp") {
|
||||
if (!dstParent) {
|
||||
return;
|
||||
}
|
||||
removeSrc();
|
||||
const idx = dstParent.children!.findIndex((value) => value.id == dstData.id);
|
||||
srcData.parent = dstParent.id;
|
||||
dstParent.children!.splice(idx, 0, srcData);
|
||||
} else if (dragDir == "dragDown") {
|
||||
if (!dstParent) {
|
||||
return;
|
||||
}
|
||||
removeSrc();
|
||||
const idx = dstParent.children!.findIndex((value) => value.id == dstData.id);
|
||||
srcData.parent = dstParent.id;
|
||||
dstParent.children!.splice(idx + 1, 0, srcData);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
updateGrahp();
|
||||
pushHistory();
|
||||
onChange();
|
||||
});
|
||||
|
||||
editor.graph.data(editor.data);
|
||||
editor.graph.render();
|
||||
if (editor.graphMatrix) {
|
||||
restoreViewport();
|
||||
} else {
|
||||
editor.graph.fitCenter();
|
||||
}
|
||||
};
|
||||
|
||||
const keysRef = useHotkeys<HTMLDivElement>(
|
||||
[
|
||||
Hotkey.Copy,
|
||||
Hotkey.Paste,
|
||||
Hotkey.Save,
|
||||
Hotkey.Replace,
|
||||
Hotkey.Undo,
|
||||
Hotkey.Redo,
|
||||
Hotkey.Insert,
|
||||
Hotkey.Delete,
|
||||
Hotkey.Enter,
|
||||
Hotkey.Backspace,
|
||||
],
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
if (isHotkeyPressed(Hotkey.Copy)) {
|
||||
copyNode();
|
||||
} else if (isHotkeyPressed(Hotkey.Replace)) {
|
||||
replaceNode();
|
||||
} else if (isHotkeyPressed(Hotkey.Paste)) {
|
||||
pasteNode();
|
||||
} else if (isHotkeyPressed(Hotkey.Redo)) {
|
||||
redo();
|
||||
} else if (isHotkeyPressed(Hotkey.Undo)) {
|
||||
undo();
|
||||
} else if (isHotkeyPressed(Hotkey.Insert) || isHotkeyPressed(Hotkey.Enter)) {
|
||||
createNode();
|
||||
} else if (isHotkeyPressed(Hotkey.Delete) || isHotkeyPressed(Hotkey.Backspace)) {
|
||||
deleteNode();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!editorSize || (editorSize.width === 0 && editorSize.height === 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (editor.size.width !== editorSize.width || editor.size.height !== editorSize.height) {
|
||||
destroyGraph();
|
||||
}
|
||||
|
||||
if (!editor.graph) {
|
||||
createGraph(graphRef);
|
||||
editor.size.width = editorSize.width;
|
||||
editor.size.height = editorSize.height;
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace.editing === editor) {
|
||||
checkSubtree();
|
||||
}
|
||||
}, [workspace.editing]);
|
||||
|
||||
const menuItems = useMemo(() => {
|
||||
const MenuItem: FC<FlexProps> = (itemProps) => {
|
||||
return (
|
||||
<Flex
|
||||
gap="50px"
|
||||
style={{ minWidth: "200px", justifyContent: "space-between", alignItems: "center" }}
|
||||
{...itemProps}
|
||||
></Flex>
|
||||
);
|
||||
};
|
||||
const isMac = process.platform === "darwin";
|
||||
const arr: MenuProps["items"] = [
|
||||
{
|
||||
label: (
|
||||
<MenuItem>
|
||||
<div>{t("copy")}</div>
|
||||
<div>{isMac ? "⌘ C" : "Ctrl+C"}</div>
|
||||
</MenuItem>
|
||||
),
|
||||
key: "copy",
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<MenuItem>
|
||||
<div>{t("paste")}</div>
|
||||
<div>{isMac ? "⌘ V" : "Ctrl+V"}</div>
|
||||
</MenuItem>
|
||||
),
|
||||
key: "paste",
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<MenuItem>
|
||||
<div>{t("replace")}</div>
|
||||
<div>{isMac ? "⇧ ⌘ V" : "Ctrl+Shift+V"} </div>
|
||||
</MenuItem>
|
||||
),
|
||||
key: "replace",
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<MenuItem>
|
||||
<div>{t("insertNode")}</div>
|
||||
<div>{isMac ? <IoMdReturnLeft /> : "Enter"}</div>
|
||||
</MenuItem>
|
||||
),
|
||||
key: "insert",
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<MenuItem>
|
||||
<div>{t("deleteNode")}</div>
|
||||
<div>{isMac ? <FiDelete /> : "Backspace"}</div>
|
||||
</MenuItem>
|
||||
),
|
||||
key: "delete",
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<MenuItem>
|
||||
<div>{t("editSubtree")}</div>
|
||||
<div></div>
|
||||
</MenuItem>
|
||||
),
|
||||
key: "editSubtree",
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<MenuItem>
|
||||
<div>{t("saveAsSubtree")}</div>
|
||||
<div></div>
|
||||
</MenuItem>
|
||||
),
|
||||
key: "saveAsSubtree",
|
||||
},
|
||||
];
|
||||
return arr;
|
||||
}, [t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editor.graph) {
|
||||
editor.graph.changeData(editor.data);
|
||||
editor.graph.layout();
|
||||
restoreViewport();
|
||||
}
|
||||
}, [t]);
|
||||
|
||||
type MenuInfo = Parameters<Exclude<MenuProps["onClick"], undefined>>[0];
|
||||
const onClick = useCallback((info: MenuInfo) => {
|
||||
editor.dispatch?.(info.key as EditEvent);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className="b3-editor"
|
||||
ref={mergeRefs([sizeRef, keysRef])}
|
||||
tabIndex={-1}
|
||||
style={{ maxWidth: "inherit", maxHeight: "inherit" }}
|
||||
>
|
||||
<Dropdown menu={{ items: menuItems, onClick }} trigger={["contextMenu"]}>
|
||||
<div tabIndex={-1} style={{ width: "100%", height: "100%" }} ref={graphRef} />
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,738 @@
|
||||
import { FileModel, useWorkspace } from "@/contexts/workspace-context";
|
||||
import * as b3util from "@/misc/b3util";
|
||||
import { modal } from "@/misc/hooks";
|
||||
import { Hotkey, isHotkeyPressed, isMacos, useHotkeys } from "@/misc/keys";
|
||||
import Path from "@/misc/path";
|
||||
import { DownOutlined } from "@ant-design/icons";
|
||||
import { Button, Dropdown, Flex, FlexProps, Input, MenuProps, Space, Tree } from "antd";
|
||||
import { ItemType } from "antd/es/menu/hooks/useItems";
|
||||
import { ipcRenderer, shell } from "electron";
|
||||
import * as fs from "fs";
|
||||
import React, { FC, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FaExclamationTriangle, FaSwatchbook } from "react-icons/fa";
|
||||
import { FiCommand, FiDelete } from "react-icons/fi";
|
||||
import { IoMdReturnLeft } from "react-icons/io";
|
||||
|
||||
const { DirectoryTree } = Tree;
|
||||
|
||||
type MenuInfo = Parameters<Exclude<MenuProps["onClick"], undefined>>[0];
|
||||
type MenuEvent =
|
||||
| "open"
|
||||
| "newFile"
|
||||
| "newFolder"
|
||||
| "revealFile"
|
||||
| "rename"
|
||||
| "delete"
|
||||
| "paste"
|
||||
| "copy"
|
||||
| "move"
|
||||
| "duplicate";
|
||||
|
||||
const findFile = (path: string | undefined, node: FileModel): FileModel | undefined => {
|
||||
if (!path) {
|
||||
return;
|
||||
} else if (node.path === path) {
|
||||
return node;
|
||||
}
|
||||
if (node.children) {
|
||||
for (const child of node.children) {
|
||||
const ret = findFile(path, child);
|
||||
if (ret) {
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const findParent = (node: FileModel, parent?: FileModel): FileModel | undefined => {
|
||||
if (parent && parent.children) {
|
||||
if (parent.children?.indexOf(node) >= 0) {
|
||||
return parent;
|
||||
}
|
||||
for (const child of parent.children) {
|
||||
const v = findParent(node, child);
|
||||
if (v) {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const resolveKeys = (path: string, node: FileModel, keys: React.Key[]) => {
|
||||
if (node.path === path) {
|
||||
return true;
|
||||
}
|
||||
if (node.children) {
|
||||
keys.push(node.path);
|
||||
for (const child of node.children) {
|
||||
if (resolveKeys(path, child, keys)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
keys.pop();
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// rename file
|
||||
const renameFile = (oldPath: string, newPath: string) => {
|
||||
const workspace = useWorkspace.getState();
|
||||
if (fs.existsSync(newPath)) {
|
||||
return false;
|
||||
} else if (oldPath !== newPath) {
|
||||
try {
|
||||
fs.renameSync(oldPath, newPath);
|
||||
const isDirectory = fs.statSync(newPath).isDirectory();
|
||||
for (const editor of workspace.editors) {
|
||||
if (isDirectory) {
|
||||
if (editor.path.startsWith(oldPath)) {
|
||||
editor.dispatch?.("rename", editor.path.replace(oldPath, newPath));
|
||||
}
|
||||
} else {
|
||||
if (editor.path === oldPath) {
|
||||
editor.dispatch?.("rename", newPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const Explorer: FC = () => {
|
||||
const workspace = {
|
||||
close: useWorkspace((state) => state.close),
|
||||
editing: useWorkspace((state) => state.editing),
|
||||
editors: useWorkspace((state) => state.editors),
|
||||
open: useWorkspace((state) => state.open),
|
||||
fileTree: useWorkspace((state) => state.fileTree),
|
||||
};
|
||||
const { t } = useTranslation();
|
||||
const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
|
||||
const [expandedKeys, setExpandedKeys] = useState<React.Key[]>(
|
||||
workspace.fileTree?.path ? [workspace.fileTree.path] : []
|
||||
);
|
||||
const [copyFile, setCopyFile] = useState("");
|
||||
const [newName, setNewName] = useState<string | null>(null);
|
||||
const [contextMenu, setContextMenu] = useState<ItemType[]>([]);
|
||||
|
||||
if (workspace.fileTree) {
|
||||
workspace.fileTree.icon = <FaSwatchbook style={{ marginTop: "5px" }} />;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace.editing) {
|
||||
const keys: React.Key[] = [];
|
||||
resolveKeys(workspace.editing.path, workspace.fileTree!, keys);
|
||||
for (const k of expandedKeys) {
|
||||
if (keys.indexOf(k) === -1) {
|
||||
keys.push(k);
|
||||
}
|
||||
}
|
||||
setExpandedKeys(keys);
|
||||
setSelectedKeys([workspace.editing.path]);
|
||||
}
|
||||
}, [workspace.editing]);
|
||||
|
||||
const keysRef = useHotkeys<HTMLDivElement>(
|
||||
[
|
||||
Hotkey.F2,
|
||||
Hotkey.Enter,
|
||||
Hotkey.Copy,
|
||||
Hotkey.Paste,
|
||||
Hotkey.Delete,
|
||||
Hotkey.MacDelete,
|
||||
Hotkey.Escape,
|
||||
Hotkey.Duplicate,
|
||||
],
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
const node = findFile(selectedKeys[0], workspace.fileTree!);
|
||||
if (isHotkeyPressed(Hotkey.F2) || (isMacos && isHotkeyPressed(Hotkey.Enter))) {
|
||||
if (node && node !== workspace.fileTree) {
|
||||
node.editing = true;
|
||||
setNewName("");
|
||||
}
|
||||
} else if (isHotkeyPressed(Hotkey.Delete) || isHotkeyPressed(Hotkey.MacDelete)) {
|
||||
if (node && node !== workspace.fileTree) {
|
||||
dispatch("delete", node);
|
||||
}
|
||||
} else if (isHotkeyPressed(Hotkey.Escape)) {
|
||||
if (node) {
|
||||
node.editing = false;
|
||||
setNewName(null);
|
||||
}
|
||||
} else if (isHotkeyPressed(Hotkey.Duplicate)) {
|
||||
if (node && node.isLeaf) {
|
||||
dispatch("duplicate", node);
|
||||
}
|
||||
} else if (isHotkeyPressed(Hotkey.Copy)) {
|
||||
if (node && node.isLeaf) {
|
||||
dispatch("copy", node);
|
||||
}
|
||||
} else if (isHotkeyPressed(Hotkey.Paste)) {
|
||||
if (node) {
|
||||
dispatch("paste", node);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const submitRename = (node: FileModel) => {
|
||||
if (!newName) {
|
||||
if (fs.existsSync(node.path)) {
|
||||
node.editing = false;
|
||||
} else {
|
||||
const parent = findParent(node, workspace.fileTree);
|
||||
if (parent && parent.children) {
|
||||
parent.children = parent.children.filter((v) => v !== node);
|
||||
}
|
||||
}
|
||||
setNewName(null);
|
||||
return;
|
||||
}
|
||||
if (node.path.endsWith("/:")) {
|
||||
node.path = Path.dirname(node.path) + "/" + newName.replace(/[^\w. _-]+/g, "");
|
||||
node.title = Path.basename(node.path);
|
||||
fs.mkdirSync(node.path);
|
||||
} else if (node.path.endsWith("/.json")) {
|
||||
node.path = Path.dirname(node.path) + "/" + newName.replace(/[^\w. _-]+/g, "") + ".json";
|
||||
node.title = Path.basename(node.path);
|
||||
fs.writeFileSync(node.path, JSON.stringify(b3util.createNewTree(node.title), null, 2));
|
||||
workspace.open(node.path);
|
||||
} else {
|
||||
const newpath = Path.dirname(node.path) + "/" + newName + Path.extname(node.path);
|
||||
if (renameFile(node.path, newpath)) {
|
||||
setSelectedKeys([newpath]);
|
||||
}
|
||||
}
|
||||
node.editing = false;
|
||||
setNewName(null);
|
||||
};
|
||||
|
||||
const dispatch = (event: MenuEvent, node: FileModel, dest?: FileModel) => {
|
||||
switch (event) {
|
||||
case "open": {
|
||||
workspace.open(node.path);
|
||||
break;
|
||||
}
|
||||
case "newFolder": {
|
||||
const folderNode: FileModel = {
|
||||
path: node.path + "/:",
|
||||
title: "",
|
||||
children: [],
|
||||
editing: true,
|
||||
};
|
||||
node.children?.unshift(folderNode);
|
||||
setNewName("");
|
||||
if (expandedKeys.indexOf(node.path) === -1) {
|
||||
setExpandedKeys([node.path, ...expandedKeys]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "newFile": {
|
||||
const folderNode: FileModel = {
|
||||
path: node.path + "/.json",
|
||||
title: "",
|
||||
isLeaf: true,
|
||||
editing: true,
|
||||
};
|
||||
node.children?.unshift(folderNode);
|
||||
setNewName("");
|
||||
if (expandedKeys.indexOf(node.path) === -1) {
|
||||
setExpandedKeys([node.path, ...expandedKeys]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "paste": {
|
||||
if (copyFile) {
|
||||
let folder = node.path;
|
||||
if (node.isLeaf) {
|
||||
folder = Path.dirname(node.path);
|
||||
}
|
||||
const newPath = folder + "/" + Path.basename(copyFile);
|
||||
if (fs.existsSync(newPath)) {
|
||||
if (node.path === newPath) {
|
||||
dispatch("duplicate", node);
|
||||
} else {
|
||||
const alert = modal.confirm({
|
||||
centered: true,
|
||||
content: (
|
||||
<Flex vertical gap="middle">
|
||||
<div>
|
||||
<FaExclamationTriangle style={{ fontSize: "60px", color: "#FADB14" }} />
|
||||
</div>
|
||||
<div>{t("explorer.replaceFile", { name: Path.basename(copyFile) })}</div>
|
||||
</Flex>
|
||||
),
|
||||
footer: (
|
||||
<Flex vertical gap="middle" style={{ paddingTop: "30px" }}>
|
||||
<Flex vertical gap="6px">
|
||||
<Button
|
||||
danger
|
||||
onClick={() => {
|
||||
workspace.close(newPath);
|
||||
renameFile(node.path, newPath);
|
||||
alert.destroy();
|
||||
}}
|
||||
>
|
||||
{t("replace")}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
alert.destroy();
|
||||
}}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
fs.copyFileSync(copyFile, newPath);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "copy": {
|
||||
setCopyFile(node.path);
|
||||
break;
|
||||
}
|
||||
case "duplicate": {
|
||||
for (let i = 1; ; i++) {
|
||||
const dupName = Path.basenameWithoutExt(node.path) + " " + i + ".json";
|
||||
const dupPath = Path.dirname(node.path) + "/" + dupName;
|
||||
if (!fs.existsSync(dupPath)) {
|
||||
fs.copyFileSync(node.path, dupPath);
|
||||
setSelectedKeys([dupPath]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "delete": {
|
||||
if (node === workspace.fileTree) {
|
||||
return;
|
||||
}
|
||||
if (node.isLeaf) {
|
||||
const alert = modal.confirm({
|
||||
centered: true,
|
||||
content: (
|
||||
<Flex vertical gap="middle">
|
||||
<div>
|
||||
<FaExclamationTriangle style={{ fontSize: "60px", color: "#FADB14" }} />
|
||||
</div>
|
||||
<div>{t("explorer.deleteFile", { name: node.title })}</div>
|
||||
</Flex>
|
||||
),
|
||||
footer: (
|
||||
<Flex vertical gap="middle" style={{ paddingTop: "30px" }}>
|
||||
<Flex vertical gap="6px">
|
||||
<Button
|
||||
onClick={() => {
|
||||
alert.destroy();
|
||||
if (node.path === workspace.editing?.path) {
|
||||
workspace.close(node.path);
|
||||
}
|
||||
ipcRenderer.invoke("trashItem", node.path);
|
||||
}}
|
||||
>
|
||||
{t("moveToTrash")}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
alert.destroy();
|
||||
}}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</Flex>
|
||||
<div style={{ fontSize: "11px", textAlign: "center" }}>
|
||||
{t("explorer.restoreFileInfo")}
|
||||
</div>
|
||||
</Flex>
|
||||
),
|
||||
});
|
||||
} else {
|
||||
const alert = modal.confirm({
|
||||
centered: true,
|
||||
content: (
|
||||
<Flex vertical gap="middle">
|
||||
<div>
|
||||
<FaExclamationTriangle style={{ fontSize: "60px", color: "#FADB14" }} />
|
||||
</div>
|
||||
<div>{t("explorer.deleteFolder", { name: node.title })}</div>
|
||||
</Flex>
|
||||
),
|
||||
footer: (
|
||||
<Flex vertical gap="middle" style={{ paddingTop: "30px" }}>
|
||||
<Flex vertical gap="6px">
|
||||
<Button
|
||||
onClick={() => {
|
||||
workspace.editors.forEach((editor) => {
|
||||
if (editor.path.startsWith(node.path + "/")) {
|
||||
workspace.close(editor.path);
|
||||
}
|
||||
});
|
||||
ipcRenderer.invoke("trashItem", node.path);
|
||||
alert.destroy();
|
||||
}}
|
||||
>
|
||||
{t("moveToTrash")}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
alert.destroy();
|
||||
}}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</Flex>
|
||||
<div style={{ fontSize: "11px", textAlign: "center" }}>
|
||||
{t("explorer.restoreFileInfo")}
|
||||
</div>
|
||||
</Flex>
|
||||
),
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "move": {
|
||||
try {
|
||||
const newPath = dest!.path + "/" + Path.basename(node.path);
|
||||
const doMove = () => {
|
||||
fs.renameSync(node.path, newPath);
|
||||
for (const editor of workspace.editors) {
|
||||
if (editor.path.startsWith(node.path)) {
|
||||
editor.dispatch?.("rename", dest!.path + "/" + Path.basename(editor.path));
|
||||
}
|
||||
console.log("editor move", editor.path === newPath, editor.path, newPath);
|
||||
if (editor.path.startsWith(newPath)) {
|
||||
console.log("editor reload", editor.path === newPath, editor.path, newPath);
|
||||
editor.dispatch?.("reload");
|
||||
}
|
||||
}
|
||||
};
|
||||
if (fs.existsSync(newPath)) {
|
||||
const alert = modal.confirm({
|
||||
centered: true,
|
||||
content: (
|
||||
<Flex vertical gap="middle">
|
||||
<div>
|
||||
<FaExclamationTriangle style={{ fontSize: "60px", color: "#FADB14" }} />
|
||||
</div>
|
||||
<div>{t("explorer.replaceFile", { name: Path.basename(node.path) })}</div>
|
||||
</Flex>
|
||||
),
|
||||
footer: (
|
||||
<Flex vertical gap="middle" style={{ paddingTop: "30px" }}>
|
||||
<Flex vertical gap="6px">
|
||||
<Button
|
||||
danger
|
||||
onClick={() => {
|
||||
console.log("close file", newPath);
|
||||
workspace.close(node.path);
|
||||
doMove();
|
||||
alert.destroy();
|
||||
}}
|
||||
>
|
||||
{t("replace")}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
alert.destroy();
|
||||
}}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
),
|
||||
});
|
||||
} else {
|
||||
doMove();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("move file:", error);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "revealFile":
|
||||
shell.showItemInFolder(node.path.replaceAll("/", Path.sep));
|
||||
break;
|
||||
case "rename": {
|
||||
node.editing = true;
|
||||
setNewName("");
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// context menu
|
||||
const fileContextMenu = useMemo(() => {
|
||||
const MenuItem: FC<FlexProps> = (itemProps) => {
|
||||
return (
|
||||
<Flex
|
||||
gap="50px"
|
||||
style={{ minWidth: "200px", justifyContent: "space-between", alignItems: "center" }}
|
||||
{...itemProps}
|
||||
></Flex>
|
||||
);
|
||||
};
|
||||
const isMac = process.platform === "darwin";
|
||||
const arr: MenuProps["items"] = [
|
||||
{
|
||||
label: (
|
||||
<MenuItem>
|
||||
<div>{t("open")}</div>
|
||||
</MenuItem>
|
||||
),
|
||||
key: "open",
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<MenuItem>
|
||||
<div>{t("copy")}</div>
|
||||
<div>{isMac ? "⌘ C" : "Ctrl+C"}</div>
|
||||
</MenuItem>
|
||||
),
|
||||
key: "copy",
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<MenuItem>
|
||||
<div>{t("duplicate")}</div>
|
||||
<div>{isMac ? "⌘ D" : "Ctrl+D"}</div>
|
||||
</MenuItem>
|
||||
),
|
||||
key: "duplicate",
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<MenuItem>
|
||||
<div>{isMac ? t("revealFileOnMac") : t("revealFileOnWindows")}</div>
|
||||
</MenuItem>
|
||||
),
|
||||
key: "revealFile",
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<MenuItem>
|
||||
<div>{t("rename")}</div>
|
||||
{isMac && <IoMdReturnLeft />}
|
||||
{!isMac && <div>F2</div>}
|
||||
</MenuItem>
|
||||
),
|
||||
key: "rename",
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<MenuItem>
|
||||
<div>{t("delete")}</div>
|
||||
{isMac && (
|
||||
<Space size={6}>
|
||||
<FiCommand />
|
||||
<FiDelete />
|
||||
</Space>
|
||||
)}
|
||||
</MenuItem>
|
||||
),
|
||||
key: "delete",
|
||||
},
|
||||
];
|
||||
return arr;
|
||||
}, [t]);
|
||||
|
||||
const directoryContextMenu = useMemo(() => {
|
||||
const MenuItem: FC<FlexProps> = (itemProps) => {
|
||||
return (
|
||||
<Flex
|
||||
gap="50px"
|
||||
style={{ minWidth: "200px", justifyContent: "space-between", alignItems: "center" }}
|
||||
{...itemProps}
|
||||
></Flex>
|
||||
);
|
||||
};
|
||||
const isMac = process.platform === "darwin";
|
||||
const arr: MenuProps["items"] = [
|
||||
{
|
||||
label: (
|
||||
<MenuItem>
|
||||
<div>{t("newFile")}</div>
|
||||
</MenuItem>
|
||||
),
|
||||
key: "newFile",
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<MenuItem>
|
||||
<div>{t("newFolder")}</div>
|
||||
</MenuItem>
|
||||
),
|
||||
key: "newFolder",
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<MenuItem>
|
||||
<div>{isMac ? t("revealFileOnMac") : t("revealFileOnWindows")}</div>
|
||||
</MenuItem>
|
||||
),
|
||||
key: "revealFile",
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<MenuItem>
|
||||
<div style={{ color: copyFile ? "inherit" : "gray" }}>{t("paste")}</div>
|
||||
<div style={{ color: copyFile ? "inherit" : "gray" }}>{isMac ? "⌘ V" : "Ctrl+V"}</div>
|
||||
</MenuItem>
|
||||
),
|
||||
key: "paste",
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<MenuItem>
|
||||
<div>{t("rename")}</div>
|
||||
{isMac && <IoMdReturnLeft />}
|
||||
{!isMac && <div>F2</div>}
|
||||
</MenuItem>
|
||||
),
|
||||
key: "rename",
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<MenuItem>
|
||||
<div>{t("delete")}</div>
|
||||
{isMac && (
|
||||
<Space size={6}>
|
||||
<FiCommand />
|
||||
<FiDelete />
|
||||
</Space>
|
||||
)}
|
||||
</MenuItem>
|
||||
),
|
||||
key: "delete",
|
||||
},
|
||||
];
|
||||
return arr;
|
||||
}, [t, copyFile]);
|
||||
|
||||
const onClick = (info: MenuInfo) => {
|
||||
const node = findFile(selectedKeys[0], workspace.fileTree!) ?? workspace.fileTree;
|
||||
if (node) {
|
||||
dispatch(info.key as MenuEvent, node);
|
||||
}
|
||||
};
|
||||
|
||||
if (!workspace.fileTree) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={keysRef}
|
||||
tabIndex={-1}
|
||||
style={{ height: "100%" }}
|
||||
onContextMenuCapture={() => {
|
||||
setContextMenu(directoryContextMenu);
|
||||
}}
|
||||
onContextMenu={(e) => {
|
||||
if (workspace.fileTree) {
|
||||
// setSelectedKeys([workspace.fileTree.path]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div style={{ padding: "12px 24px" }}>
|
||||
<span style={{ fontSize: "18px", fontWeight: "600" }}>{t("explorer.title")}</span>
|
||||
</div>
|
||||
<Flex vertical style={{ overflow: "auto", height: "100%" }}>
|
||||
<Dropdown menu={{ items: contextMenu, onClick }} trigger={["contextMenu"]}>
|
||||
<div>
|
||||
<DirectoryTree
|
||||
tabIndex={-1}
|
||||
treeData={workspace.fileTree ? [workspace.fileTree] : []}
|
||||
fieldNames={{ key: "path" }}
|
||||
onExpand={(keys) => {
|
||||
setExpandedKeys(keys);
|
||||
}}
|
||||
onRightClick={(info) => {
|
||||
if (info.node.isLeaf) {
|
||||
setContextMenu(fileContextMenu);
|
||||
}
|
||||
setSelectedKeys([info.node.path]);
|
||||
}}
|
||||
onSelect={(_, info) => {
|
||||
const node = info.selectedNodes.at(0);
|
||||
if (node) {
|
||||
if (!node.children) {
|
||||
workspace.open(node.path);
|
||||
}
|
||||
setSelectedKeys([node.path]);
|
||||
}
|
||||
if (node && !node.children) {
|
||||
workspace.open(node.path);
|
||||
}
|
||||
}}
|
||||
onDrop={(info) => {
|
||||
dispatch("move", info.dragNode, info.node);
|
||||
}}
|
||||
titleRender={(node) => {
|
||||
if (node.editing) {
|
||||
return (
|
||||
<div style={{ display: "inline-flex" }}>
|
||||
<Input
|
||||
defaultValue={Path.basenameWithoutExt(node.title)}
|
||||
autoFocus
|
||||
style={{ padding: "0px 0px", borderRadius: "2px" }}
|
||||
onFocus={(e) => e.target.select()}
|
||||
onChange={(e) => setNewName(e.target.value)}
|
||||
onPressEnter={() => {
|
||||
if (newName) {
|
||||
submitRename(node);
|
||||
}
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyUp={(e) => {
|
||||
if (e.code === Hotkey.Escape) {
|
||||
if (!fs.existsSync(node.path)) {
|
||||
const parent = findParent(node, workspace.fileTree);
|
||||
if (parent && parent.children) {
|
||||
parent.children = parent.children.filter((v) => v !== node);
|
||||
}
|
||||
}
|
||||
node.editing = false;
|
||||
setNewName(null);
|
||||
}
|
||||
}}
|
||||
onBlur={() => submitRename(node)}
|
||||
></Input>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <span key={node.path}>{node.title}</span>;
|
||||
}
|
||||
}}
|
||||
allowDrop={(options) => {
|
||||
return !options.dropNode.isLeaf;
|
||||
}}
|
||||
defaultExpandedKeys={[workspace.fileTree.path]}
|
||||
draggable={newName !== null ? false : { icon: false }}
|
||||
expandedKeys={expandedKeys}
|
||||
switcherIcon={<DownOutlined />}
|
||||
selectedKeys={selectedKeys}
|
||||
/>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,359 @@
|
||||
import { EditNode, EditTree, FileModel, useWorkspace } from "@/contexts/workspace-context";
|
||||
import { NodeModel, TreeGraphData, TreeModel } from "@/misc/b3type";
|
||||
import { Hotkey } from "@/misc/keys";
|
||||
import Path from "@/misc/path";
|
||||
import { AutoComplete, Divider, Flex, Form, Input, InputNumber, Select, Switch } from "antd";
|
||||
import { DefaultOptionType } from "antd/es/select";
|
||||
import { FC, useEffect, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Markdown from "react-markdown";
|
||||
|
||||
interface OptionType extends DefaultOptionType {}
|
||||
|
||||
export const Inspector: FC = () => {
|
||||
const workspace = {
|
||||
editing: useWorkspace((state) => state.editing),
|
||||
editingNode: useWorkspace((state) => state.editingNode),
|
||||
editingTree: useWorkspace((state) => state.editingTree),
|
||||
getNodeDef: useWorkspace((state) => state.getNodeDef),
|
||||
nodeDefs: useWorkspace((state) => state.nodeDefs),
|
||||
onEditingNode: useWorkspace((state) => state.onEditingNode),
|
||||
allFiles: useWorkspace((state) => state.allFiles),
|
||||
fileTree: useWorkspace((state) => state.fileTree),
|
||||
};
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
|
||||
// set form values
|
||||
useEffect(() => {
|
||||
if (workspace.editingTree) {
|
||||
const data = workspace.editingTree.data;
|
||||
form.resetFields();
|
||||
form.setFieldValue("name", data.name);
|
||||
form.setFieldValue("desc", data.desc);
|
||||
} else if (workspace.editingNode) {
|
||||
const data = workspace.editingNode.data;
|
||||
const def = workspace.getNodeDef(workspace.editingNode.data.name);
|
||||
form.resetFields();
|
||||
form.setFieldValue("id", data.id);
|
||||
form.setFieldValue("name", data.name);
|
||||
form.setFieldValue("desc", data.desc);
|
||||
form.setFieldValue("debug", data.debug);
|
||||
form.setFieldValue("path", data.path);
|
||||
def.args?.forEach((v) => {
|
||||
form.setFieldValue(`args.${v.name}`, data.args?.[v.name]);
|
||||
});
|
||||
def.input?.forEach((_, i) => {
|
||||
form.setFieldValue(`input.${i}`, data.input?.[i]);
|
||||
});
|
||||
def.output?.forEach((_, i) => {
|
||||
form.setFieldValue(`output.${i}`, data.output?.[i]);
|
||||
});
|
||||
}
|
||||
}, [workspace.editingNode, workspace.editingTree]);
|
||||
|
||||
// auto complete for node
|
||||
const nodeOptions = useMemo(() => {
|
||||
const options: OptionType[] = [];
|
||||
workspace.nodeDefs.forEach((e) => {
|
||||
options.push({ label: `${e.name}(${e.desc})`, value: e.name });
|
||||
});
|
||||
return options;
|
||||
}, [workspace.nodeDefs]);
|
||||
|
||||
// auto complete for input and output
|
||||
const inoutVarOptions = useMemo(() => {
|
||||
const options: OptionType[] = [];
|
||||
const collect = (node?: TreeGraphData) => {
|
||||
if (node) {
|
||||
node.input?.forEach((v, i) => {
|
||||
const desc = node.def.input?.[i] ?? "<unknown>";
|
||||
if (v && !options.find((option) => option.value !== v)) {
|
||||
options.push({ label: `${v}(${desc})`, value: v });
|
||||
}
|
||||
});
|
||||
node.output?.forEach((v, i) => {
|
||||
const desc = node.def.output?.[i] ?? "<unknown>";
|
||||
if (v && !options.find((option) => option.value !== v)) {
|
||||
options.push({ label: `${v}(${desc})`, value: v });
|
||||
}
|
||||
});
|
||||
node.children?.forEach((child) => collect(child));
|
||||
}
|
||||
};
|
||||
collect(workspace.editing?.data);
|
||||
return options;
|
||||
}, [workspace.editing]);
|
||||
|
||||
// auto complete for subtree
|
||||
const subtreeOptions = useMemo(() => {
|
||||
const options: OptionType[] = [];
|
||||
workspace.allFiles.forEach((path) => {
|
||||
const value = Path.relative(workspace.fileTree!.path, path);
|
||||
const desc = ""; //fileNode.desc ? `(${fileNode.desc})` : "";
|
||||
options.push({
|
||||
label: `${value}${desc}`,
|
||||
value: value,
|
||||
});
|
||||
});
|
||||
return options;
|
||||
}, [workspace.allFiles, workspace.fileTree]);
|
||||
|
||||
if (workspace.editingTree) {
|
||||
const finish = (values: any) => {
|
||||
const data = {} as TreeModel;
|
||||
data.name = values.name;
|
||||
data.desc = values.desc || undefined;
|
||||
workspace.editing?.dispatch?.("updateTree", {
|
||||
data: {
|
||||
name: values.name,
|
||||
desc: values.desc || undefined,
|
||||
},
|
||||
} as EditTree);
|
||||
};
|
||||
return (
|
||||
<Flex className="b3-inspector" vertical style={{ height: "100%" }}>
|
||||
<div style={{ padding: "12px 24px" }}>
|
||||
<span style={{ fontSize: "18px", fontWeight: "600" }}>{t("tree.overview")}</span>
|
||||
</div>
|
||||
<div style={{ overflow: "auto", height: "100%", padding: "24px" }}>
|
||||
<Form form={form} labelCol={{ span: 8 }} onFinish={finish}>
|
||||
<Form.Item name="name" label={t("tree.name")}>
|
||||
<Input disabled={true} />
|
||||
</Form.Item>
|
||||
<Form.Item name="desc" label={t("tree.desc")}>
|
||||
<Input onBlur={form.submit} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
</Flex>
|
||||
);
|
||||
} else if (workspace.editingNode) {
|
||||
const editingNode = workspace.editingNode;
|
||||
const def = workspace.getNodeDef(editingNode.data.name);
|
||||
const disabled = !editingNode.editable;
|
||||
|
||||
// update value
|
||||
const finish = (values: any) => {
|
||||
const data = {} as NodeModel;
|
||||
data.id = editingNode.data.id;
|
||||
data.name = values.name;
|
||||
data.debug = values.debug || undefined;
|
||||
data.desc = values.desc || undefined;
|
||||
data.path = values.path || undefined;
|
||||
|
||||
def.args?.forEach((arg) => {
|
||||
const v = values[`args.${arg.name}`];
|
||||
if (v !== null && v !== undefined && v !== "") {
|
||||
data.args ||= {};
|
||||
data.args[arg.name] = v;
|
||||
}
|
||||
});
|
||||
|
||||
def.input?.forEach((_, i) => {
|
||||
const v = values[`input.${i}`];
|
||||
data.input ||= [];
|
||||
data.input.push(v ?? "");
|
||||
});
|
||||
|
||||
def.output?.forEach((_, i) => {
|
||||
const v = values[`output.${i}`];
|
||||
data.output ||= [];
|
||||
data.output!.push(v ?? "");
|
||||
});
|
||||
workspace.editing?.dispatch?.("updateNode", {
|
||||
data: data,
|
||||
} as EditNode);
|
||||
};
|
||||
|
||||
// change node def
|
||||
const changeNodeDef = (newname: string) => {
|
||||
if (editingNode.data.name !== newname) {
|
||||
editingNode.data = {
|
||||
id: editingNode.data.id,
|
||||
name: workspace.nodeDefs.get(newname)?.name ?? newname,
|
||||
desc: editingNode.data.desc,
|
||||
debug: editingNode.data.debug,
|
||||
};
|
||||
workspace.onEditingNode(editingNode);
|
||||
form.submit();
|
||||
} else {
|
||||
form.submit();
|
||||
}
|
||||
};
|
||||
|
||||
const changeSubtree = () => {
|
||||
if (form.getFieldValue("path") !== editingNode.data.path) {
|
||||
finish(form.getFieldsValue());
|
||||
} else {
|
||||
form.submit();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex className="b3-inspector" vertical style={{ height: "100%" }}>
|
||||
<div style={{ padding: "12px 24px" }}>
|
||||
<span style={{ fontSize: "18px", fontWeight: "600" }}>{def.desc}</span>
|
||||
</div>
|
||||
<div style={{ overflow: "auto", height: "100%", padding: "24px" }}>
|
||||
<Form
|
||||
form={form}
|
||||
wrapperCol={{ span: "auto" }}
|
||||
labelCol={{ span: "auto" }}
|
||||
onFinish={finish}
|
||||
>
|
||||
<Form.Item
|
||||
name="id"
|
||||
label={
|
||||
<div style={{ minWidth: "80px", justifyContent: "flex-end" }}>{t("node.id")}</div>
|
||||
}
|
||||
>
|
||||
<Input disabled={true} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("node.name")} name="name">
|
||||
<AutoComplete
|
||||
disabled={disabled}
|
||||
options={nodeOptions}
|
||||
onBlur={() => changeNodeDef(form.getFieldValue("name"))}
|
||||
onSelect={changeNodeDef}
|
||||
onInputKeyDown={(e) => e.code === Hotkey.Escape && e.preventDefault()}
|
||||
filterOption={(inputValue: string, option?: OptionType) => {
|
||||
const label = option!.label as string;
|
||||
return label.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1;
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="desc" label={t("node.desc")}>
|
||||
<Input disabled={disabled} onBlur={form.submit} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("node.debug")} name="debug" valuePropName="checked">
|
||||
<Switch disabled={disabled && !editingNode.data.path} onChange={form.submit} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("node.subtree")} name="path">
|
||||
<AutoComplete
|
||||
disabled={disabled && !editingNode.data.path}
|
||||
options={subtreeOptions}
|
||||
onBlur={changeSubtree}
|
||||
onInputKeyDown={(e) => e.code === Hotkey.Escape && e.preventDefault()}
|
||||
filterOption={(inputValue: string, option?: OptionType) => {
|
||||
const label = option!.label as string;
|
||||
return label.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1;
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Markdown>{def.doc}</Markdown>
|
||||
{def.input?.length && (
|
||||
<>
|
||||
<Divider orientation="left">
|
||||
<h4>{t("node.inputVariable")}</h4>
|
||||
</Divider>
|
||||
{def.input.map((v, i) => {
|
||||
const required = v.indexOf("?") == -1;
|
||||
const desc = v.replace("?", "");
|
||||
return (
|
||||
<Form.Item
|
||||
label={desc}
|
||||
name={`input.${i}`}
|
||||
key={`input.${i}`}
|
||||
rules={[{ required, message: t("node.fileRequired", { field: desc }) }]}
|
||||
>
|
||||
<AutoComplete
|
||||
disabled={disabled}
|
||||
options={inoutVarOptions}
|
||||
onBlur={form.submit}
|
||||
onInputKeyDown={(e) => e.code === Hotkey.Escape && e.preventDefault()}
|
||||
filterOption={(inputValue: string, option?: OptionType) => {
|
||||
const label = option!.label as string;
|
||||
return label.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1;
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
{def.args?.length && (
|
||||
<>
|
||||
<Divider orientation="left">
|
||||
<h4>{t("node.args")}</h4>
|
||||
</Divider>
|
||||
{def.args.map((v) => {
|
||||
const required = v.type.indexOf("?") == -1;
|
||||
return (
|
||||
<Form.Item
|
||||
initialValue={v.default}
|
||||
name={`args.${v.name}`}
|
||||
label={v.desc}
|
||||
key={`args.${v.name}`}
|
||||
valuePropName={v.type.indexOf("boolean") >= 0 ? "checked" : undefined}
|
||||
rules={[{ required, message: t("node.fileRequired", { field: v.desc }) }]}
|
||||
>
|
||||
{v.type.indexOf("string") >= 0 && (
|
||||
<Input disabled={disabled} onBlur={form.submit} />
|
||||
)}
|
||||
{v.type.indexOf("int") >= 0 && (
|
||||
<InputNumber disabled={disabled} onBlur={form.submit} precision={0} />
|
||||
)}
|
||||
{v.type.indexOf("float") >= 0 && (
|
||||
<InputNumber disabled={disabled} onBlur={form.submit} />
|
||||
)}
|
||||
{v.type.indexOf("boolean") >= 0 && (
|
||||
<Switch disabled={disabled} onChange={form.submit} />
|
||||
)}
|
||||
{v.type.indexOf("code") >= 0 && (
|
||||
<Input onBlur={form.submit} placeholder={t("node.codePlaceholder")} />
|
||||
)}
|
||||
{v.type.indexOf("enum") >= 0 && (
|
||||
<Select disabled={disabled} onBlur={form.submit} onChange={form.submit}>
|
||||
{v.options?.map((value) => {
|
||||
return (
|
||||
<Select.Option key={value.name} value={value.value}>
|
||||
{value.name}
|
||||
</Select.Option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
)}
|
||||
</Form.Item>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
{def.output?.length && (
|
||||
<>
|
||||
<Divider orientation="left">
|
||||
<h4>{t("node.outputVariable")}</h4>
|
||||
</Divider>
|
||||
{def.output.map((v, i) => {
|
||||
const required = v.indexOf("?") == -1;
|
||||
const desc = v.replace("?", "");
|
||||
return (
|
||||
<Form.Item
|
||||
label={desc}
|
||||
name={`output.${i}`}
|
||||
key={`output.${i}`}
|
||||
rules={[{ required, message: t("node.fileRequired", { field: desc }) }]}
|
||||
>
|
||||
<AutoComplete
|
||||
disabled={disabled}
|
||||
options={inoutVarOptions}
|
||||
onBlur={form.submit}
|
||||
onInputKeyDown={(e) => e.code === Hotkey.Escape && e.preventDefault()}
|
||||
filterOption={(inputValue: string, option?: OptionType) => {
|
||||
const label = option!.label as string;
|
||||
return label.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1;
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
</Flex>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,457 @@
|
||||
import { useSetting } from "@/contexts/setting-context";
|
||||
import { useWorkspace } from "@/contexts/workspace-context";
|
||||
import * as b3util from "@/misc/b3util";
|
||||
import i18n from "@/misc/i18n";
|
||||
import { Hotkey, sendInputEvent } from "@/misc/keys";
|
||||
import { CheckOutlined } from "@ant-design/icons";
|
||||
import { Menu as AppMenu, BrowserWindow, app, dialog } from "@electron/remote";
|
||||
import { Button, DropDownProps, Dropdown, Flex, FlexProps, LayoutProps, Space } from "antd";
|
||||
import { ItemType } from "antd/es/menu/hooks/useItems";
|
||||
import { ipcRenderer } from "electron";
|
||||
import { MenuItemConstructorOptions } from "electron/renderer";
|
||||
import * as fs from "fs";
|
||||
import { FC, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { PiTreeStructureFill } from "react-icons/pi";
|
||||
|
||||
const MenuItemLabel: FC<FlexProps> = (itemProps) => {
|
||||
return (
|
||||
<Flex
|
||||
gap="50px"
|
||||
style={{ minWidth: "160px", justifyContent: "space-between", alignItems: "center" }}
|
||||
{...itemProps}
|
||||
></Flex>
|
||||
);
|
||||
};
|
||||
|
||||
const getFocusedWebContents = () => {
|
||||
return BrowserWindow.getFocusedWindow()?.webContents;
|
||||
};
|
||||
|
||||
ipcRenderer.on("open-project", (_, dir) => {
|
||||
useWorkspace.getState().init(dir);
|
||||
});
|
||||
|
||||
ipcRenderer.on("refresh-app-men", () => {
|
||||
// trigger refresh
|
||||
useSetting.getState().load();
|
||||
});
|
||||
|
||||
export const Menu: FC<LayoutProps> = () => {
|
||||
const { t } = useTranslation();
|
||||
const [trigger, setTrigger] = useState<DropDownProps["trigger"]>(["click"]);
|
||||
const workspace = {
|
||||
allFiles: useWorkspace((state) => state.allFiles),
|
||||
batchProject: useWorkspace((state) => state.batchProject),
|
||||
buildProject: useWorkspace((state) => state.buildProject),
|
||||
createProject: useWorkspace((state) => state.createProject),
|
||||
editing: useWorkspace((state) => state.editing),
|
||||
editors: useWorkspace((state) => state.editors),
|
||||
init: useWorkspace((state) => state.init),
|
||||
open: useWorkspace((state) => state.open),
|
||||
openProject: useWorkspace((state) => state.openProject),
|
||||
path: useWorkspace((state) => state.path),
|
||||
save: useWorkspace((state) => state.save),
|
||||
saveAll: useWorkspace((state) => state.saveAll),
|
||||
workdir: useWorkspace((state) => state.workdir),
|
||||
};
|
||||
const enabled = !!workspace.workdir;
|
||||
const homedir = app.getPath("home");
|
||||
const settings = {
|
||||
recent: useSetting((state) => state.recent),
|
||||
};
|
||||
|
||||
const isMac = process.platform === "darwin";
|
||||
const menuTemplate: MenuItemConstructorOptions[] = useMemo(() => {
|
||||
const recentWorkspaces: MenuItemConstructorOptions[] = settings.recent.map((path, i) => ({
|
||||
id: "menu.file.recent" + i,
|
||||
label: path.replace(homedir, "~"),
|
||||
click: () => workspace.openProject(path),
|
||||
}));
|
||||
return [
|
||||
// { role: 'appMenu' }
|
||||
...(isMac
|
||||
? [
|
||||
{
|
||||
id: "menu.app",
|
||||
label: app.name,
|
||||
submenu: [
|
||||
{ label: t("about"), role: "about" },
|
||||
{ type: "separator" },
|
||||
{ label: t("services"), role: "services" },
|
||||
{ type: "separator" },
|
||||
{ label: t("hide"), role: "hide" },
|
||||
{ label: t("hideOthers"), role: "hideOthers" },
|
||||
{ label: t("unhide"), role: "unhide" },
|
||||
{ type: "separator" },
|
||||
{ label: t("quit"), role: "quit" },
|
||||
],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: "menu.file",
|
||||
label: t("menu.file"),
|
||||
submenu: [
|
||||
{
|
||||
id: "menu.file.newFile",
|
||||
label: t("newFile"),
|
||||
accelerator: "CmdOrCtrl+N",
|
||||
enabled: enabled,
|
||||
click: () => {
|
||||
console.log("newFile");
|
||||
const path = dialog.showSaveDialogSync({
|
||||
properties: ["showOverwriteConfirmation"],
|
||||
filters: [{ name: "Json", extensions: ["json"] }],
|
||||
});
|
||||
if (path) {
|
||||
fs.writeFileSync(path, JSON.stringify(b3util.createNewTree(path), null, 2));
|
||||
workspace.open(path);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "menu.file.newWindow",
|
||||
label: t("newWindow"),
|
||||
click: () => {
|
||||
ipcRenderer.invoke("open-win");
|
||||
},
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
id: "menu.file.openProject",
|
||||
label: t("openProject"),
|
||||
click: () => workspace.openProject(),
|
||||
},
|
||||
{
|
||||
id: "menu.file.createProject",
|
||||
label: t("createProject"),
|
||||
click: () => workspace.createProject(),
|
||||
},
|
||||
{ id: "menu.file.recent", label: t("recent"), submenu: recentWorkspaces },
|
||||
{ type: "separator" },
|
||||
{
|
||||
id: "menu.file.save",
|
||||
label: t("save"),
|
||||
accelerator: "CmdOrCtrl+S",
|
||||
enabled: enabled,
|
||||
click: () => {
|
||||
console.log("save1");
|
||||
workspace.save();
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "menu.file.saveAs",
|
||||
label: t("saveAs"),
|
||||
accelerator: "Shift+CmdOrCtrl+S",
|
||||
enabled: enabled,
|
||||
},
|
||||
{
|
||||
id: "menu.file.saveAll",
|
||||
label: t("saveAll"),
|
||||
accelerator: "Alt+CmdOrCtrl+S",
|
||||
enabled: enabled,
|
||||
click: () => workspace.saveAll(),
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
id: "menu.file.build",
|
||||
label: t("build"),
|
||||
accelerator: "CmdOrCtrl+B",
|
||||
enabled: enabled,
|
||||
click: () => {
|
||||
workspace.buildProject();
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "menu.file.batch",
|
||||
label: t("batch"),
|
||||
enabled: enabled,
|
||||
click: () => workspace.batchProject(),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "menu.edit",
|
||||
label: t("menu.edit"),
|
||||
submenu: [
|
||||
{
|
||||
id: "menu.edit.undo",
|
||||
label: t("undo"),
|
||||
enabled: enabled,
|
||||
accelerator: Hotkey.Undo,
|
||||
click: () => {
|
||||
sendInputEvent(Hotkey.Undo);
|
||||
getFocusedWebContents()?.undo();
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "menu.edit.redo",
|
||||
label: t("redo"),
|
||||
enabled: enabled,
|
||||
accelerator: Hotkey.Redo,
|
||||
click: () => {
|
||||
sendInputEvent(Hotkey.Redo);
|
||||
getFocusedWebContents()?.redo();
|
||||
},
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
id: "menu.edit.copy",
|
||||
label: t("copy"),
|
||||
enabled: enabled,
|
||||
accelerator: Hotkey.Copy,
|
||||
click: () => {
|
||||
sendInputEvent(Hotkey.Copy);
|
||||
getFocusedWebContents()?.copy();
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "menu.edit.paste",
|
||||
label: t("paste"),
|
||||
enabled: enabled,
|
||||
accelerator: Hotkey.Paste,
|
||||
click: () => {
|
||||
sendInputEvent(Hotkey.Paste);
|
||||
getFocusedWebContents()?.paste();
|
||||
},
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
id: "menu.edit.insertNode",
|
||||
label: t("insertNode"),
|
||||
enabled: enabled,
|
||||
click: () => {
|
||||
workspace.editing?.dispatch?.("insert");
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "menu.edit.deleteNode",
|
||||
label: t("deleteNode"),
|
||||
enabled: enabled,
|
||||
click: () => {
|
||||
workspace.editing?.dispatch?.("delete");
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// { role: 'viewMenu' }
|
||||
{
|
||||
id: "menu.view",
|
||||
label: t("menu.view"),
|
||||
submenu: [
|
||||
{
|
||||
id: "menu.view.reload",
|
||||
label: t("reload"),
|
||||
accelerator: "CmdOrCtrl+R",
|
||||
click: () => {
|
||||
const path = workspace.path;
|
||||
getFocusedWebContents()?.reload();
|
||||
workspace.init(path);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "menu.view.devTools",
|
||||
label: t("devTools"),
|
||||
accelerator: "Alt+CmdOrCtrl+I",
|
||||
click: () => {
|
||||
getFocusedWebContents()?.openDevTools();
|
||||
},
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
id: "menu.view.actualSize",
|
||||
label: t("actualSize"),
|
||||
click: () => {
|
||||
const webContents = getFocusedWebContents();
|
||||
if (webContents) {
|
||||
webContents.setZoomFactor(1);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "menu.view.zoomIn",
|
||||
label: t("zoomIn"),
|
||||
role: "zoomIn",
|
||||
click: () => {
|
||||
const webContents = getFocusedWebContents();
|
||||
if (webContents) {
|
||||
webContents.setZoomFactor(webContents.getZoomFactor() + 0.1);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "menu.view.zoomOut",
|
||||
label: t("zoomOut"),
|
||||
click: () => {
|
||||
const webContents = getFocusedWebContents();
|
||||
if (webContents) {
|
||||
webContents.setZoomFactor(webContents.getZoomFactor() - 0.1);
|
||||
}
|
||||
},
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
id: "menu.view.fullscreen",
|
||||
label: t("fullscreen"),
|
||||
click: () => {
|
||||
const window = BrowserWindow.getFocusedWindow();
|
||||
if (window) {
|
||||
window.setFullScreen(!window.isFullScreen());
|
||||
}
|
||||
},
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
id: "menu.view.language",
|
||||
label: t("language"),
|
||||
submenu: [
|
||||
{
|
||||
id: "menu.view.language.english",
|
||||
label: "English",
|
||||
type: "radio",
|
||||
checked: i18n.language?.startsWith("en"),
|
||||
click: () => {
|
||||
i18n.changeLanguage("en");
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "menu.view.language.chinese",
|
||||
label: "简体中文",
|
||||
type: "radio",
|
||||
checked: i18n.language?.startsWith("zh"),
|
||||
click: () => {
|
||||
i18n.changeLanguage("zh");
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "menu.help",
|
||||
label: t("menu.help"),
|
||||
submenu: [
|
||||
{
|
||||
id: "menu.help.about",
|
||||
label: t("reportIssue"),
|
||||
click: () => {
|
||||
window.open("https://github.com/zhandouxiaojiji/behavior3editor");
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// {
|
||||
// id: "menu.window",
|
||||
// label: t("menu.window"),
|
||||
// submenu: [
|
||||
// { id: "menu.window.minimize", role: "minimize" },
|
||||
// { id: "menu.window.zoom", role: "zoom" },
|
||||
// ...(isMac
|
||||
// ? [
|
||||
// { type: "separator" },
|
||||
// { id: "menu.window.front", role: "front" },
|
||||
// { type: "separator" },
|
||||
// { id: "menu.window.window", role: "window" },
|
||||
// ]
|
||||
// : [{ id: "menu.window.close", role: "close" }]),
|
||||
// ],
|
||||
// },
|
||||
] as MenuItemConstructorOptions[];
|
||||
}, [t, workspace.workdir, settings.recent, workspace.editing]);
|
||||
|
||||
if (isMac) {
|
||||
const menu = AppMenu.buildFromTemplate(menuTemplate);
|
||||
AppMenu.setApplicationMenu(menu);
|
||||
}
|
||||
|
||||
if (!process.env.VITE_DEV_SERVER_URL && isMac) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex className="b3-menu" vertical={false} align="center" style={{ paddingLeft: "10px" }}>
|
||||
<PiTreeStructureFill size="25px" style={{ paddingRight: "5px" }} />
|
||||
<Space size={2} className="b3-no-drag-region">
|
||||
{menuTemplate.map((item, index) => {
|
||||
const items = item.submenu as MenuItemConstructorOptions[];
|
||||
if (item.label === app.name) {
|
||||
return undefined;
|
||||
}
|
||||
const createItem = (option: MenuItemConstructorOptions): ItemType => {
|
||||
if (option.type === "separator") {
|
||||
return {
|
||||
type: "divider",
|
||||
};
|
||||
}
|
||||
const keys = (option.accelerator?.split("+") ?? []).map((key) => {
|
||||
const lowerKey = key.toLowerCase();
|
||||
if (lowerKey.indexOf("alt") !== -1) {
|
||||
return isMac ? "⌥" : "Alt";
|
||||
} else if (
|
||||
lowerKey.indexOf("cmdorctrl") !== -1 ||
|
||||
lowerKey.indexOf("ctrl") !== -1 ||
|
||||
lowerKey.indexOf("meta") !== -1
|
||||
) {
|
||||
return isMac ? "⌘" : "Ctrl";
|
||||
} else if (lowerKey.indexOf("shift") !== -1) {
|
||||
return isMac ? "⇧" : "Shift";
|
||||
} else {
|
||||
return key.toUpperCase().replace("=", "+");
|
||||
}
|
||||
});
|
||||
const ctrl = keys.indexOf("ctrl");
|
||||
if (ctrl >= 0) {
|
||||
keys.splice(ctrl, 1);
|
||||
keys.unshift("Ctrl");
|
||||
}
|
||||
const subItems = option.submenu as MenuItemConstructorOptions[] | undefined;
|
||||
return {
|
||||
label: (
|
||||
<MenuItemLabel>
|
||||
<div>
|
||||
{option.type === "radio" && option.checked && (
|
||||
<CheckOutlined style={{ paddingRight: "5px" }} />
|
||||
)}
|
||||
{option.type === "radio" && !option.checked && (
|
||||
<div
|
||||
style={{
|
||||
display: "inline-block",
|
||||
width: "20px",
|
||||
}}
|
||||
></div>
|
||||
)}
|
||||
{option.label}
|
||||
</div>
|
||||
<div>{keys.join(isMac ? " " : "+")}</div>
|
||||
</MenuItemLabel>
|
||||
),
|
||||
key: option.id ?? "",
|
||||
disabled: option.enabled === false,
|
||||
onClick: () => {
|
||||
option.click?.(null!, null!, null!);
|
||||
},
|
||||
children: subItems?.map((subOption) => createItem(subOption)),
|
||||
};
|
||||
};
|
||||
return (
|
||||
<Dropdown
|
||||
trigger={trigger}
|
||||
key={`menu${index}`}
|
||||
dropdownRender={(node) => {
|
||||
return <div className="b3-app-menu">{node}</div>;
|
||||
}}
|
||||
menu={{
|
||||
items: items.map((value) => createItem(value)),
|
||||
}}
|
||||
>
|
||||
<Button type="text" size="small" onClick={() => setTrigger(["hover"])}>
|
||||
{item.label}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
);
|
||||
})}
|
||||
</Space>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,412 @@
|
||||
import { useWorkspace } from "@/contexts/workspace-context";
|
||||
import { toBreakWord } from "@/misc/b3util";
|
||||
import i18n from "@/misc/i18n";
|
||||
import G6 from "@antv/g6";
|
||||
|
||||
const NODE_COLORS: any = {
|
||||
["Composite"]: "rgb(91,237,32)",
|
||||
["Decorator"]: "rgb(218,167,16)",
|
||||
["Condition"]: "rgb(228,20,139)",
|
||||
["Action"]: "rgb(91,143,249)",
|
||||
["Other"]: "rgb(112,112,112)",
|
||||
["Error"]: "rgb(255,0,0)",
|
||||
};
|
||||
|
||||
const SELECTED_LINE_WIDTH = 4;
|
||||
|
||||
G6.registerNode(
|
||||
"TreeNode",
|
||||
{
|
||||
options: {
|
||||
type: "rect",
|
||||
anchorPoints: [
|
||||
[0, 0.5],
|
||||
[1, 0.5],
|
||||
],
|
||||
labelCfg: {
|
||||
style: {
|
||||
fill: "blue",
|
||||
fontSize: 10,
|
||||
},
|
||||
},
|
||||
style: {
|
||||
fill: "white",
|
||||
},
|
||||
stateStyles: {
|
||||
selected: {
|
||||
"main-box": {
|
||||
stroke: "yellow",
|
||||
lineWidth: SELECTED_LINE_WIDTH,
|
||||
},
|
||||
"name-bg-selected": {
|
||||
fill: "yellow",
|
||||
},
|
||||
},
|
||||
hover: {
|
||||
stroke: "white",
|
||||
lineWidth: 3,
|
||||
},
|
||||
dragSrc: {
|
||||
fill: "gray",
|
||||
},
|
||||
dragRight: {
|
||||
"drag-right": {
|
||||
fillOpacity: 1,
|
||||
strokeOpacity: 1,
|
||||
},
|
||||
},
|
||||
dragUp: {
|
||||
"drag-up": {
|
||||
fillOpacity: 1,
|
||||
strokeOpacity: 1,
|
||||
},
|
||||
},
|
||||
dragDown: {
|
||||
"drag-down": {
|
||||
fillOpacity: 1,
|
||||
strokeOpacity: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
draw(cfg, group) {
|
||||
const workspace = useWorkspace.getState();
|
||||
const nodeDef = workspace.getNodeDef(cfg.name as string);
|
||||
let classify = nodeDef.type || "Other";
|
||||
let color = nodeDef.color || NODE_COLORS[classify] || NODE_COLORS["Other"];
|
||||
if (
|
||||
!workspace.hasNodeDef(cfg.name as string) ||
|
||||
(cfg.path && (!cfg.children || (cfg.children as []).length == 0))
|
||||
) {
|
||||
classify = "Error";
|
||||
color = NODE_COLORS[classify];
|
||||
}
|
||||
const size = cfg.size ? (cfg.size as number[]) : [150, 40];
|
||||
const w = size[0];
|
||||
const h = size[1];
|
||||
const r = 4;
|
||||
|
||||
type ShapeCfg = Parameters<typeof group.addShape>[1];
|
||||
const addShape = (type: string, shapeCfg: ShapeCfg, clickEnabled: boolean = false) => {
|
||||
shapeCfg.draggable = clickEnabled;
|
||||
shapeCfg.capture = clickEnabled;
|
||||
return group.addShape(type, shapeCfg);
|
||||
};
|
||||
|
||||
const shape = addShape(
|
||||
"rect",
|
||||
{
|
||||
attrs: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: w,
|
||||
height: h,
|
||||
stroke: color,
|
||||
lineWidth: 2,
|
||||
fill: "white",
|
||||
radius: r,
|
||||
},
|
||||
name: "main-box",
|
||||
draggable: true,
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
// is subtree
|
||||
if (cfg.path && cfg.id !== "1") {
|
||||
addShape("rect", {
|
||||
attrs: {
|
||||
x: -10,
|
||||
y: -10,
|
||||
width: w + 20,
|
||||
height: h + 20,
|
||||
stroke: "white",
|
||||
lineWidth: 2.5,
|
||||
lineDash: [5, 5],
|
||||
radius: [r, r, r, r],
|
||||
},
|
||||
name: "subtree",
|
||||
draggable: true,
|
||||
});
|
||||
}
|
||||
|
||||
// name bg
|
||||
addShape("rect", {
|
||||
attrs: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: w,
|
||||
height: 20,
|
||||
fill: color,
|
||||
radius: [r, r, 0, 0],
|
||||
},
|
||||
name: "name-bg-selected",
|
||||
draggable: true,
|
||||
});
|
||||
addShape("rect", {
|
||||
attrs: {
|
||||
x: SELECTED_LINE_WIDTH / 2,
|
||||
y: SELECTED_LINE_WIDTH / 2,
|
||||
width: w - 4,
|
||||
height: 20 - SELECTED_LINE_WIDTH / 2,
|
||||
fill: color,
|
||||
radius: [r, r, 0, 0],
|
||||
},
|
||||
name: "name-bg",
|
||||
draggable: true,
|
||||
});
|
||||
|
||||
// id text
|
||||
addShape("text", {
|
||||
attrs: {
|
||||
textBaseline: "top",
|
||||
x: -3,
|
||||
y: h / 2 - 8,
|
||||
fontSize: 20,
|
||||
lineHeight: 20,
|
||||
text: cfg.id,
|
||||
textAlign: "right",
|
||||
fill: "white",
|
||||
stroke: "black",
|
||||
lineWidth: 2,
|
||||
},
|
||||
name: "id-text",
|
||||
});
|
||||
|
||||
// icon
|
||||
const img = `./icons/${classify}.svg`;
|
||||
addShape("image", {
|
||||
attrs: {
|
||||
x: 5,
|
||||
y: 2,
|
||||
height: 14,
|
||||
width: 14,
|
||||
img,
|
||||
},
|
||||
name: "node-icon",
|
||||
});
|
||||
|
||||
// debug
|
||||
if (cfg.debug) {
|
||||
addShape("image", {
|
||||
attrs: {
|
||||
x: 182,
|
||||
y: 2,
|
||||
height: 14,
|
||||
width: 14,
|
||||
img: `./icons/Debug.svg`,
|
||||
},
|
||||
name: "node-icon",
|
||||
});
|
||||
}
|
||||
|
||||
// name text
|
||||
addShape("text", {
|
||||
attrs: {
|
||||
textBaseline: "top",
|
||||
x: 22,
|
||||
y: 3,
|
||||
fontWeight: 800,
|
||||
lineHeight: 20,
|
||||
text: cfg.name,
|
||||
fill: "black",
|
||||
},
|
||||
name: "name-text",
|
||||
});
|
||||
|
||||
const x = 5;
|
||||
let y = 24;
|
||||
// desc text
|
||||
const desc = cfg.desc || nodeDef.desc;
|
||||
if (desc) {
|
||||
addShape("text", {
|
||||
attrs: {
|
||||
textBaseline: "top",
|
||||
x,
|
||||
y,
|
||||
lineHeight: 20,
|
||||
fontWeight: 800,
|
||||
text: `${i18n.t("regnode.mark")}${desc}`,
|
||||
fill: "black",
|
||||
},
|
||||
name: "desc-text",
|
||||
});
|
||||
}
|
||||
|
||||
const args: any = cfg.args;
|
||||
if (nodeDef.args && args && Object.keys(args).length > 0) {
|
||||
const { str, line } = toBreakWord(`${i18n.t("regnode.args")}${JSON.stringify(args)}`, 34);
|
||||
addShape("text", {
|
||||
attrs: {
|
||||
textBaseline: "top",
|
||||
x,
|
||||
y: y + 20,
|
||||
w,
|
||||
lineHeight: 20,
|
||||
text: str,
|
||||
fill: "black",
|
||||
},
|
||||
name: "args-text",
|
||||
});
|
||||
y += 20 * line;
|
||||
}
|
||||
|
||||
const input: [] = cfg.input ? (cfg.input as []) : [];
|
||||
if (nodeDef.input && input.length > 0) {
|
||||
const { str, line } = toBreakWord(`${i18n.t("regnode.input")}${JSON.stringify(input)}`, 35);
|
||||
if (cfg.highlightInput) {
|
||||
addShape("rect", {
|
||||
attrs: {
|
||||
x: x - 2,
|
||||
y: y + 17,
|
||||
width: w - 6,
|
||||
height: 18,
|
||||
fill: "#0d1117",
|
||||
radius: [r, r, r, r],
|
||||
},
|
||||
name: "input-text-bg",
|
||||
});
|
||||
}
|
||||
addShape(
|
||||
"text",
|
||||
{
|
||||
attrs: {
|
||||
textBaseline: "top",
|
||||
x,
|
||||
y: y + 20,
|
||||
lineHeight: 20,
|
||||
text: str,
|
||||
fill: cfg.highlightInput ? "white" : "black",
|
||||
fontWeight: cfg.highlightInput ? "bolder" : undefined,
|
||||
},
|
||||
name: "input-text",
|
||||
},
|
||||
true
|
||||
);
|
||||
y += 20 * line;
|
||||
}
|
||||
|
||||
const output: [] = cfg.output ? (cfg.output as []) : [];
|
||||
if (nodeDef.output && output.length > 0) {
|
||||
const { str, line } = toBreakWord(
|
||||
`${i18n.t("regnode.output")}${JSON.stringify(output)}`,
|
||||
35
|
||||
);
|
||||
if (cfg.highlightOutput) {
|
||||
addShape("rect", {
|
||||
attrs: {
|
||||
x: x - 2,
|
||||
y: y + 17,
|
||||
width: w - 6,
|
||||
height: 18,
|
||||
fill: "#0d1117",
|
||||
radius: [r, r, r, r],
|
||||
},
|
||||
name: "output-text-bg",
|
||||
});
|
||||
}
|
||||
addShape(
|
||||
"text",
|
||||
{
|
||||
attrs: {
|
||||
textBaseline: "top",
|
||||
x,
|
||||
y: y + 20,
|
||||
lineHeight: 20,
|
||||
text: str,
|
||||
fill: cfg.highlightOutput ? "white" : "black",
|
||||
fontWeight: cfg.highlightOutput ? "bolder" : undefined,
|
||||
},
|
||||
name: "output-text",
|
||||
},
|
||||
true
|
||||
);
|
||||
y += 20 * line;
|
||||
}
|
||||
|
||||
if (cfg.path) {
|
||||
addShape("text", {
|
||||
attrs: {
|
||||
textBaseline: "top",
|
||||
x,
|
||||
y: y + 20,
|
||||
lineHeight: 20,
|
||||
text: `${i18n.t("regnode.subtree")}${cfg.path}`,
|
||||
fill: "black",
|
||||
},
|
||||
name: "subtree-text",
|
||||
});
|
||||
y += 20;
|
||||
}
|
||||
|
||||
addShape("rect", {
|
||||
name: "drag-up",
|
||||
attrs: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: w,
|
||||
height: h / 2,
|
||||
lineWidth: 2,
|
||||
stroke: "#1668dc",
|
||||
strokeOpacity: 0,
|
||||
fill: "#1668dc",
|
||||
fillOpacity: 0,
|
||||
radius: [r, r, 0, 0],
|
||||
},
|
||||
draggable: true,
|
||||
});
|
||||
|
||||
addShape("rect", {
|
||||
name: "drag-down",
|
||||
attrs: {
|
||||
x: 0,
|
||||
y: h / 2,
|
||||
width: w,
|
||||
height: h / 2,
|
||||
lineWidth: 2,
|
||||
stroke: "#1668dc",
|
||||
strokeOpacity: 0,
|
||||
fill: "#1668dc",
|
||||
fillOpacity: 0,
|
||||
radius: [0, 0, r, r],
|
||||
},
|
||||
draggable: true,
|
||||
});
|
||||
|
||||
addShape("rect", {
|
||||
name: "drag-right",
|
||||
attrs: {
|
||||
x: w / 2,
|
||||
y: 0,
|
||||
width: w / 2,
|
||||
height: h,
|
||||
lineWidth: 2,
|
||||
stroke: "#1668dc",
|
||||
strokeOpacity: 0,
|
||||
fill: "#1668dc",
|
||||
fillOpacity: 0,
|
||||
radius: [0, r, r, 0],
|
||||
},
|
||||
draggable: true,
|
||||
});
|
||||
|
||||
if (Array.isArray(cfg.children) && cfg.children.length > 0) {
|
||||
addShape("marker", {
|
||||
attrs: {
|
||||
x: w,
|
||||
y: h / 2,
|
||||
r: 6,
|
||||
symbol: G6.Marker.collapse,
|
||||
stroke: "#666",
|
||||
lineWidth: 1,
|
||||
fill: "#fff",
|
||||
},
|
||||
name: "collapse-icon",
|
||||
});
|
||||
}
|
||||
|
||||
return shape;
|
||||
},
|
||||
},
|
||||
"single-node"
|
||||
);
|
||||
@@ -0,0 +1,11 @@
|
||||
import { useSetting } from "@/contexts/setting-context";
|
||||
import { message, setGlobalHooks } from "@/misc/hooks";
|
||||
import { FC } from "react";
|
||||
|
||||
export const Setup: FC = () => {
|
||||
if (!message) {
|
||||
useSetting().load();
|
||||
}
|
||||
setGlobalHooks();
|
||||
return <div></div>;
|
||||
};
|
||||
@@ -0,0 +1,117 @@
|
||||
import { useWorkspace } from "@/contexts/workspace-context";
|
||||
import { Hotkey } from "@/misc/keys";
|
||||
import Path from "@/misc/path";
|
||||
import { SearchOutlined } from "@ant-design/icons";
|
||||
import { Button, Flex, LayoutProps, Select } from "antd";
|
||||
import React, { FC, useMemo } from "react";
|
||||
import { Menu } from "./menu";
|
||||
|
||||
interface OptionType {
|
||||
label: string | React.ReactNode;
|
||||
value: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export const TitleBar: FC<LayoutProps> = () => {
|
||||
const workspace = {
|
||||
allFiles: useWorkspace((state) => state.allFiles),
|
||||
fileTree: useWorkspace((state) => state.fileTree),
|
||||
isShowingSearch: useWorkspace((state) => state.isShowingSearch),
|
||||
onShowingSearch: useWorkspace((state) => state.onShowingSearch),
|
||||
open: useWorkspace((state) => state.open),
|
||||
name: useWorkspace((state) => state.path),
|
||||
};
|
||||
const searchOptions = useMemo(() => {
|
||||
const options: OptionType[] = [];
|
||||
workspace.allFiles.forEach((path) => {
|
||||
const value = Path.relative(workspace.fileTree!.path, path);
|
||||
options.push({
|
||||
label: (
|
||||
<div>
|
||||
{Path.basename(value)}
|
||||
<span> </span>
|
||||
<span style={{ color: "gray", fontSize: "12px" }}>{Path.dirname(value)}</span>
|
||||
</div>
|
||||
),
|
||||
value: value,
|
||||
path: path,
|
||||
});
|
||||
});
|
||||
return options;
|
||||
}, [workspace.allFiles, workspace.fileTree]);
|
||||
return (
|
||||
<Flex className="b3-titlebar" style={{ width: "100%", height: 35, alignItems: "center" }}>
|
||||
<div
|
||||
className="b3-drag-region"
|
||||
style={{
|
||||
display: "block",
|
||||
height: "35px",
|
||||
width: "100%",
|
||||
left: 0,
|
||||
top: 0,
|
||||
position: "absolute",
|
||||
}}
|
||||
/>
|
||||
<div style={{ width: "100%" }}>{process.platform !== "darwin" && <Menu />}</div>
|
||||
<Flex style={{ width: "100%", alignItems: "center", justifyContent: "center" }}>
|
||||
<Button
|
||||
className="b3-no-drag-region"
|
||||
onClick={() => {
|
||||
workspace.onShowingSearch(true);
|
||||
}}
|
||||
style={{
|
||||
width: "400px",
|
||||
height: "25px",
|
||||
border: "1px solid #30363d",
|
||||
borderRadius: "6px",
|
||||
padding: "0px",
|
||||
cursor: "pointer",
|
||||
color: "#7d8590",
|
||||
boxShadow: "none",
|
||||
}}
|
||||
icon={<SearchOutlined style={{ color: "#7d8590" }} />}
|
||||
>
|
||||
{Path.basename(workspace.name)}
|
||||
</Button>
|
||||
{workspace.isShowingSearch && (
|
||||
<Select
|
||||
showSearch
|
||||
autoFocus
|
||||
defaultOpen
|
||||
dropdownStyle={{
|
||||
top: "5px",
|
||||
paddingTop: "40px",
|
||||
zIndex: 998,
|
||||
display: "inline-block",
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.code === Hotkey.Escape) {
|
||||
workspace.onShowingSearch(false);
|
||||
}
|
||||
}}
|
||||
suffixIcon={<SearchOutlined />}
|
||||
className="b3-search b3-no-drag-region"
|
||||
style={{ width: "400px", top: "5px", position: "absolute", zIndex: 999 }}
|
||||
placeholder=""
|
||||
optionFilterProp="value"
|
||||
onBlur={() => workspace.onShowingSearch(false)}
|
||||
onChange={(_, option) => {
|
||||
if (!(option instanceof Array)) {
|
||||
workspace.onShowingSearch(false);
|
||||
workspace.open(option.path);
|
||||
}
|
||||
}}
|
||||
filterOption={(input, option) => (option?.value ?? "").includes(input)}
|
||||
filterSort={(optionA, optionB) =>
|
||||
(optionA?.value ?? "")
|
||||
.toLowerCase()
|
||||
.localeCompare((optionB?.value ?? "").toLowerCase())
|
||||
}
|
||||
options={searchOptions}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
<div style={{ width: "100%" }}>{process.platform === "darwin" && <Menu />}</div>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,428 @@
|
||||
import { useSetting } from "@/contexts/setting-context";
|
||||
import { EditorStore, useWorkspace } from "@/contexts/workspace-context";
|
||||
import { modal } from "@/misc/hooks";
|
||||
import { Hotkey, isHotkeyPressed, setInputFocus, useHotkeys } from "@/misc/keys";
|
||||
import Path from "@/misc/path";
|
||||
import { app } from "@electron/remote";
|
||||
import { Button, Flex, Layout, Space, Tabs, Tag, Tooltip } from "antd";
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BiAddToQueue } from "react-icons/bi";
|
||||
import { FaExclamationTriangle, FaRegFolderOpen } from "react-icons/fa";
|
||||
import { PiTreeStructureFill } from "react-icons/pi";
|
||||
import useForceUpdate from "use-force-update";
|
||||
import { useWindowSize } from "usehooks-ts";
|
||||
import { Editor } from "./editor";
|
||||
import { Explorer } from "./explorer";
|
||||
import { Inspector } from "./inspector";
|
||||
import { TitleBar } from "./titlebar";
|
||||
|
||||
const { Header, Content, Sider } = Layout;
|
||||
|
||||
export const Workspace: FC = () => {
|
||||
const workspace = {
|
||||
buildProject: useWorkspace((state) => state.buildProject),
|
||||
close: useWorkspace((state) => state.close),
|
||||
createProject: useWorkspace((state) => state.createProject),
|
||||
edit: useWorkspace((state) => state.edit),
|
||||
editing: useWorkspace((state) => state.editing),
|
||||
editors: useWorkspace((state) => state.editors),
|
||||
fileTree: useWorkspace((state) => state.fileTree),
|
||||
find: useWorkspace((state) => state.find),
|
||||
init: useWorkspace((state) => state.init),
|
||||
isShowingSearch: useWorkspace((state) => state.isShowingSearch),
|
||||
onShowingSearch: useWorkspace((state) => state.onShowingSearch),
|
||||
openProject: useWorkspace((state) => state.openProject),
|
||||
save: useWorkspace((state) => state.save),
|
||||
};
|
||||
const settings = {
|
||||
recent: useSetting((state) => state.recent),
|
||||
};
|
||||
const [isShowingSave, setShowingSave] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const forceUpdate = useForceUpdate();
|
||||
const isMac = process.platform === "darwin";
|
||||
const { width = 0, height = 0 } = useWindowSize();
|
||||
|
||||
const keysRef = useHotkeys<HTMLDivElement>(
|
||||
[Hotkey.Save, Hotkey.CloseEditor, Hotkey.Search, Hotkey.Build],
|
||||
(event) => {
|
||||
if (isHotkeyPressed(Hotkey.Save)) {
|
||||
event.preventDefault();
|
||||
workspace.save();
|
||||
} else if (isHotkeyPressed(Hotkey.CloseEditor)) {
|
||||
if (workspace.editing) {
|
||||
event.preventDefault();
|
||||
if (workspace.editing.unsave) {
|
||||
showSaveDialog(workspace.editing);
|
||||
} else {
|
||||
workspace.close(workspace.editing.path);
|
||||
}
|
||||
}
|
||||
} else if (isHotkeyPressed(Hotkey.Search)) {
|
||||
event.preventDefault();
|
||||
workspace.onShowingSearch(true);
|
||||
} else if (isHotkeyPressed(Hotkey.Build)) {
|
||||
event.preventDefault();
|
||||
workspace.buildProject();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!workspace.isShowingSearch && !isShowingSave) {
|
||||
keysRef.current?.focus();
|
||||
}
|
||||
}, [workspace.isShowingSearch]);
|
||||
|
||||
const showSaveDialog = (editor: EditorStore) => {
|
||||
if (isShowingSave) {
|
||||
return;
|
||||
}
|
||||
setShowingSave(true);
|
||||
const alert = modal.confirm({
|
||||
centered: true,
|
||||
content: (
|
||||
<Flex vertical gap="middle">
|
||||
<div>
|
||||
<FaExclamationTriangle style={{ fontSize: "60px", color: "#FADB14" }} />
|
||||
</div>
|
||||
<div>{t("workspace.saveOnClose", { name: Path.basename(editor.path) })}</div>
|
||||
</Flex>
|
||||
),
|
||||
footer: (
|
||||
<Flex vertical gap="middle" style={{ paddingTop: "30px" }}>
|
||||
<Flex vertical gap="6px">
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
editor.dispatch?.("save");
|
||||
workspace.close(editor.path);
|
||||
alert.destroy();
|
||||
keysRef.current?.focus();
|
||||
setShowingSave(false);
|
||||
}}
|
||||
>
|
||||
{t("save")}
|
||||
</Button>
|
||||
<Button
|
||||
danger
|
||||
onClick={() => {
|
||||
workspace.close(editor.path);
|
||||
alert.destroy();
|
||||
keysRef.current?.focus();
|
||||
setShowingSave(false);
|
||||
}}
|
||||
>
|
||||
{t("donotSave")}
|
||||
</Button>
|
||||
</Flex>
|
||||
<Button
|
||||
onClick={() => {
|
||||
alert.destroy();
|
||||
keysRef.current?.focus();
|
||||
setShowingSave(false);
|
||||
}}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</Flex>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
const showSaveAllDialog = (unsaves: EditorStore[]) => {
|
||||
if (isShowingSave) {
|
||||
return;
|
||||
}
|
||||
setShowingSave(true);
|
||||
const alert = modal.confirm({
|
||||
centered: true,
|
||||
content: (
|
||||
<Flex vertical gap="middle">
|
||||
<div>
|
||||
<FaExclamationTriangle style={{ fontSize: "60px", color: "#FADB14" }} />
|
||||
</div>
|
||||
{unsaves.length == 1 && (
|
||||
<div>{t("workspace.saveOnClose", { name: Path.basename(unsaves[0].path) })}</div>
|
||||
)}
|
||||
{unsaves.length > 1 && (
|
||||
<>
|
||||
<div>{t("workspace.saveAllOnClose", { count: unsaves.length })}</div>
|
||||
<Flex vertical style={{ fontSize: "12px" }}>
|
||||
{unsaves.map((editor) => (
|
||||
<div>{Path.basename(editor.path)}</div>
|
||||
))}
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
),
|
||||
footer: (
|
||||
<Flex vertical gap="middle" style={{ paddingTop: "30px" }}>
|
||||
<Flex vertical gap="6px">
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
unsaves.forEach((editor) => editor.dispatch?.("save"));
|
||||
alert.destroy();
|
||||
window.close();
|
||||
setShowingSave(false);
|
||||
}}
|
||||
>
|
||||
{unsaves.length > 1 ? t("saveAll") : t("save")}
|
||||
</Button>
|
||||
<Button
|
||||
danger
|
||||
onClick={() => {
|
||||
workspace.editors.length = 0;
|
||||
alert.destroy();
|
||||
window.close();
|
||||
setShowingSave(false);
|
||||
}}
|
||||
>
|
||||
{t("donotSave")}
|
||||
</Button>
|
||||
</Flex>
|
||||
<Button
|
||||
onClick={() => {
|
||||
alert.destroy();
|
||||
setShowingSave(false);
|
||||
}}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</Flex>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
window.onbeforeunload = (e) => {
|
||||
const unsaves = workspace.editors.filter((editor) => editor.unsave);
|
||||
if (unsaves.length) {
|
||||
showSaveAllDialog(unsaves);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout
|
||||
className="b3-workspace"
|
||||
tabIndex={-1}
|
||||
ref={keysRef}
|
||||
style={{ width: width, height: height }}
|
||||
>
|
||||
<Header
|
||||
style={{
|
||||
padding: "0px",
|
||||
height: "fit-content",
|
||||
borderBottom: `1px solid var(--b3-color-border)`,
|
||||
}}
|
||||
>
|
||||
<TitleBar />
|
||||
</Header>
|
||||
<Layout
|
||||
hasSider
|
||||
style={{ overflow: "hidden" }}
|
||||
onFocus={(e) => {
|
||||
setInputFocus(e.target);
|
||||
}}
|
||||
>
|
||||
{workspace.fileTree && (
|
||||
<Sider
|
||||
width={300}
|
||||
style={{
|
||||
height: "100%",
|
||||
borderRight: `1px solid var(--b3-color-border)`,
|
||||
}}
|
||||
>
|
||||
<Explorer />
|
||||
</Sider>
|
||||
)}
|
||||
<Content>
|
||||
{!workspace.fileTree && (
|
||||
<Flex vertical align="center" style={{ height: "100%" }}>
|
||||
<Flex
|
||||
vertical
|
||||
style={{
|
||||
fontSize: "15px",
|
||||
width: "fit-content",
|
||||
height: "100%",
|
||||
paddingTop: "50px",
|
||||
paddingBottom: "50px",
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
align="center"
|
||||
gap="5px"
|
||||
style={{ fontSize: "40px", fontWeight: "600", marginBottom: "10px" }}
|
||||
>
|
||||
<PiTreeStructureFill size="50px" />
|
||||
<div>Behavior3 Editor</div>
|
||||
</Flex>
|
||||
<Flex vertical style={{ paddingLeft: "55px", paddingBottom: "15px" }}>
|
||||
<div style={{ fontSize: "22px", fontWeight: "500" }}>{t("start")}</div>
|
||||
<Flex
|
||||
align="center"
|
||||
gap="5px"
|
||||
style={{
|
||||
width: "fit-content",
|
||||
fontWeight: "500",
|
||||
cursor: "pointer",
|
||||
color: "var(--ant-color-primary)",
|
||||
}}
|
||||
onClick={() => workspace.createProject()}
|
||||
>
|
||||
<BiAddToQueue size="20px" />
|
||||
{t("createProject")}
|
||||
</Flex>
|
||||
<Flex
|
||||
align="center"
|
||||
gap="5px"
|
||||
style={{
|
||||
fontWeight: "500",
|
||||
width: "fit-content",
|
||||
cursor: "pointer",
|
||||
color: "var(--ant-color-primary)",
|
||||
}}
|
||||
onClick={() => workspace.openProject()}
|
||||
>
|
||||
<FaRegFolderOpen size="19px" />
|
||||
{t("openProject")}
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex vertical style={{ paddingLeft: "55px" }}>
|
||||
<div style={{ fontSize: "22px", fontWeight: "500" }}>{t("recent")}</div>
|
||||
<div style={{ overflow: "auto", height: "100%" }}>
|
||||
{settings.recent.map((path) => {
|
||||
const homedir = app.getPath("home");
|
||||
return (
|
||||
<div key={path}>
|
||||
<span
|
||||
onClick={() => workspace.openProject(path)}
|
||||
style={{
|
||||
color: "var(--ant-color-primary)",
|
||||
fontWeight: "500",
|
||||
cursor: "pointer",
|
||||
marginRight: "15px",
|
||||
}}
|
||||
>
|
||||
{Path.basename(path)}
|
||||
</span>
|
||||
<span>{Path.dirname(path).replace(homedir, "~")}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
{workspace.editors.length == 0 && (
|
||||
<Flex vertical align="center" justify="center" style={{ height: "100%" }}>
|
||||
<Flex
|
||||
vertical
|
||||
gap="10px"
|
||||
style={{
|
||||
color: "gray",
|
||||
fontSize: "15px",
|
||||
width: "fit-content",
|
||||
paddingTop: "50px",
|
||||
marginRight: "150px",
|
||||
paddingBottom: "50px",
|
||||
}}
|
||||
>
|
||||
{[
|
||||
{ label: t("searchFile"), hotkeys: isMac ? "⌘ P" : "Ctrl + P" },
|
||||
{ label: t("build"), hotkeys: isMac ? "⌘ B" : "Ctrl + B" },
|
||||
{ label: t("insertNode"), hotkeys: "Enter" },
|
||||
{ label: t("deleteNode"), hotkeys: "Backspace" },
|
||||
].map((v) => (
|
||||
<Flex
|
||||
key={v.label}
|
||||
gap="20px"
|
||||
style={{
|
||||
minWidth: "200px",
|
||||
}}
|
||||
>
|
||||
<span style={{ width: "150px", textAlign: "end" }}>{v.label}</span>
|
||||
<Space size={5}>
|
||||
{v.hotkeys.split(" ").map((key, index) => (
|
||||
<div key={index}>
|
||||
{key === "+" && <div>+</div>}
|
||||
{key !== "+" && (
|
||||
<Tag style={{ color: "gray", fontSize: "14px", marginRight: 0 }}>
|
||||
{key}
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
{workspace.editors.length > 0 && (
|
||||
<Tabs
|
||||
hideAdd
|
||||
type="editable-card"
|
||||
activeKey={workspace.editing?.path}
|
||||
onEdit={(activeKey, action) => {
|
||||
if (action === "remove") {
|
||||
const path = activeKey as string;
|
||||
const editor = workspace.find(path);
|
||||
if (editor && editor.unsave) {
|
||||
showSaveDialog(editor);
|
||||
} else {
|
||||
workspace.close(path);
|
||||
}
|
||||
}
|
||||
}}
|
||||
onChange={(activeKey) => {
|
||||
workspace.edit(activeKey);
|
||||
}}
|
||||
items={workspace.editors.map((v) => {
|
||||
return {
|
||||
label: (
|
||||
<Tooltip
|
||||
arrow={false}
|
||||
placement="bottom"
|
||||
mouseEnterDelay={1}
|
||||
// mouseLeaveDelay={100}
|
||||
color="#010409"
|
||||
overlayStyle={{ userSelect: "none", WebkitUserSelect: "none" }}
|
||||
autoAdjustOverflow={true}
|
||||
overlayInnerStyle={{
|
||||
width: "fit-content",
|
||||
border: "1px solid var(--b3-color-border)",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
title={<div style={{ width: "max-content" }}>{v.path}</div>}
|
||||
>
|
||||
{`${Path.basename(v.path)}${v.unsave ? "*" : ""}`}
|
||||
</Tooltip>
|
||||
),
|
||||
key: v.path,
|
||||
children: <Editor data={v} onUpdate={forceUpdate} />,
|
||||
};
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</Content>
|
||||
{workspace.editors.length > 0 && (
|
||||
<Sider
|
||||
className="b3-inspector-wrapper"
|
||||
width={340}
|
||||
style={{
|
||||
height: "100%",
|
||||
borderLeft: `1px solid var(--b3-color-border)`,
|
||||
}}
|
||||
>
|
||||
<Inspector />
|
||||
</Sider>
|
||||
)}
|
||||
</Layout>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import { readJson, writeJson } from "@/misc/json";
|
||||
import { app } from "@electron/remote";
|
||||
import { create } from "zustand";
|
||||
import * as fs from "fs";
|
||||
|
||||
const settingPath = app.getPath("userData") + "/settings.json";
|
||||
|
||||
export type SettingModel = {
|
||||
recent: string[];
|
||||
};
|
||||
|
||||
export type SettingStore = {
|
||||
recent: string[];
|
||||
load: () => void;
|
||||
save: () => void;
|
||||
appendRecent: (path: string) => void;
|
||||
removeRecent: (path: string) => void;
|
||||
};
|
||||
|
||||
export const useSetting = create<SettingStore>((set, get) => ({
|
||||
recent: [],
|
||||
load: () => {
|
||||
try {
|
||||
if (fs.existsSync(settingPath)) {
|
||||
const settings = readJson(settingPath) as SettingModel;
|
||||
if (!(settings.recent instanceof Array)) {
|
||||
settings.recent = [];
|
||||
}
|
||||
|
||||
set({ recent: settings.recent });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
save: () => {
|
||||
const settings = get();
|
||||
writeJson(settingPath, {
|
||||
recent: settings.recent,
|
||||
} as SettingModel);
|
||||
},
|
||||
appendRecent: (path: string) => {
|
||||
const recent = get().recent.filter((v) => v !== path);
|
||||
recent.unshift(path);
|
||||
set({ recent });
|
||||
get().save();
|
||||
},
|
||||
removeRecent: (path: string) => {
|
||||
const recent = get().recent.filter((v) => v !== path);
|
||||
set({ recent });
|
||||
get().save();
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,450 @@
|
||||
import { NodeDef, NodeModel, TreeGraphData, TreeModel, unknownNodeDef } from "@/misc/b3type";
|
||||
import * as b3util from "@/misc/b3util";
|
||||
import { message } from "@/misc/hooks";
|
||||
import i18n from "@/misc/i18n";
|
||||
import { readJson } from "@/misc/json";
|
||||
import Path from "@/misc/path";
|
||||
import { Matrix, TreeGraph } from "@antv/g6";
|
||||
import { BrowserWindow, dialog } from "@electron/remote";
|
||||
import * as fs from "fs";
|
||||
import React from "react";
|
||||
import { create } from "zustand";
|
||||
import { useSetting } from "./setting-context";
|
||||
import { ipcRenderer } from "electron";
|
||||
|
||||
let buildDir: string | undefined;
|
||||
|
||||
interface BatchScript {
|
||||
processTree?(tree: TreeModel): TreeModel;
|
||||
processNode?(node: NodeModel, tree: TreeModel): NodeModel;
|
||||
}
|
||||
|
||||
export type EditEvent =
|
||||
| "save"
|
||||
| "copy"
|
||||
| "paste"
|
||||
| "replace"
|
||||
| "delete"
|
||||
| "insert"
|
||||
| "undo"
|
||||
| "redo"
|
||||
| "reload"
|
||||
| "rename"
|
||||
| "updateTree"
|
||||
| "updateNode"
|
||||
| "editSubtree"
|
||||
| "saveAsSubtree";
|
||||
|
||||
export class EditorStore {
|
||||
path: string;
|
||||
data: TreeGraphData;
|
||||
desc: string;
|
||||
name: string;
|
||||
|
||||
autoId: number = 1;
|
||||
dragSrcId?: string;
|
||||
dragDstId?: string;
|
||||
unsave: boolean = false;
|
||||
historyStack: NodeModel[] = [];
|
||||
historyIndex: number = 0;
|
||||
selectedId?: string | null;
|
||||
|
||||
size = { width: 0, height: 0 };
|
||||
graphMatrix?: Matrix;
|
||||
graph!: TreeGraph;
|
||||
|
||||
dispatch?: (event: EditEvent, data?: unknown) => void;
|
||||
|
||||
constructor(path: string) {
|
||||
this.path = path;
|
||||
|
||||
const file = readJson(path);
|
||||
this.data = b3util.createTreeData(file.root);
|
||||
this.desc = file.desc;
|
||||
this.name = file.name || path.slice(0, -5);
|
||||
this.autoId = b3util.refreshTreeDataId(this.data);
|
||||
this.historyStack.push(b3util.createNode(this.data));
|
||||
this.historyIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
export type FileModel = {
|
||||
path: string;
|
||||
title: string;
|
||||
icon?: React.ReactNode;
|
||||
desc?: string;
|
||||
isLeaf?: boolean;
|
||||
children?: FileModel[];
|
||||
editing?: boolean;
|
||||
};
|
||||
|
||||
export type EditNode = {
|
||||
data: NodeModel;
|
||||
editable: boolean;
|
||||
};
|
||||
|
||||
export type EditTree = {
|
||||
data: TreeModel;
|
||||
};
|
||||
|
||||
export type WorkspaceStore = {
|
||||
init: (project: string) => void;
|
||||
createProject: () => void;
|
||||
openProject: (project?: string) => void;
|
||||
batchProject: () => void;
|
||||
buildProject: () => void;
|
||||
|
||||
workdir: string;
|
||||
path: string;
|
||||
|
||||
allFiles: string[];
|
||||
fileTree?: FileModel;
|
||||
editors: EditorStore[];
|
||||
editing?: EditorStore;
|
||||
|
||||
isShowingSearch: boolean;
|
||||
onShowingSearch: (isShowingSearch: boolean) => void;
|
||||
|
||||
open: (path: string) => void;
|
||||
edit: (path: string) => void;
|
||||
close: (path: string) => void;
|
||||
find: (path: string) => EditorStore | undefined;
|
||||
|
||||
save: () => void;
|
||||
saveAs: () => void;
|
||||
saveAll: () => void;
|
||||
|
||||
watch(): void;
|
||||
loadTrees: () => void;
|
||||
loadNodeDefs: () => void;
|
||||
|
||||
// edit node
|
||||
editingNode?: EditNode | null;
|
||||
onEditingNode: (node: EditNode) => void;
|
||||
|
||||
// edit tree
|
||||
editingTree?: EditTree | null;
|
||||
onEditingTree: (tree: EditTree | null) => void;
|
||||
|
||||
// node setting
|
||||
nodeDefs: Map<string, NodeDef>;
|
||||
getNodeDef: (name: string) => NodeDef;
|
||||
hasNodeDef: (name: string) => boolean;
|
||||
};
|
||||
|
||||
const loadFileTree = (workdir: string, filename: string) => {
|
||||
const fullpath = fs.realpathSync(`${workdir}/${filename}`);
|
||||
if (!fs.existsSync(fullpath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stat = fs.statSync(fullpath);
|
||||
if (!(stat.isDirectory() || fullpath.endsWith(".json"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data: FileModel = {
|
||||
path: fullpath.replaceAll(Path.sep, "/"),
|
||||
title: Path.basename(filename),
|
||||
};
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
data.children = [];
|
||||
const files = fs.readdirSync(data.path);
|
||||
files.forEach((v) => {
|
||||
const child = loadFileTree(workdir, `${filename}/${v}`);
|
||||
if (child) {
|
||||
data.children?.push(child);
|
||||
}
|
||||
});
|
||||
data.children.sort((a, b) => {
|
||||
if ((a.children && b.children) || (!a.children && !b.children)) {
|
||||
return a.title.localeCompare(b.title);
|
||||
} else {
|
||||
return a.children ? -1 : 1;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
data.isLeaf = true;
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const saveFile = (editor?: EditorStore) => {
|
||||
if (editor?.unsave) {
|
||||
editor.dispatch?.("save");
|
||||
}
|
||||
};
|
||||
|
||||
export const useWorkspace = create<WorkspaceStore>((set, get) => ({
|
||||
allFiles: [],
|
||||
fileTree: undefined,
|
||||
editors: [],
|
||||
workdir: "",
|
||||
path: "",
|
||||
|
||||
init: (path) => {
|
||||
const workspace = get();
|
||||
if (!workspace.workdir) {
|
||||
try {
|
||||
workspace.workdir = Path.dirname(path).replaceAll(Path.sep, "/");
|
||||
workspace.path = path;
|
||||
workspace.loadTrees();
|
||||
workspace.loadNodeDefs();
|
||||
workspace.watch();
|
||||
useSetting.getState().appendRecent(path);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
useSetting.getState().removeRecent(path);
|
||||
message.error(`load workspace error: ${path}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
createProject: () => {
|
||||
const path = dialog.showSaveDialogSync({
|
||||
properties: ["showOverwriteConfirmation", "createDirectory"],
|
||||
filters: [{ name: "Behavior3 Workspace", extensions: ["b3-workspace"] }],
|
||||
});
|
||||
if (path) {
|
||||
const workspace = get();
|
||||
b3util.createProject(path);
|
||||
workspace.init(path);
|
||||
}
|
||||
},
|
||||
|
||||
openProject: (project?: string) => {
|
||||
if (project) {
|
||||
ipcRenderer.invoke("open-win", project);
|
||||
} else {
|
||||
const win = BrowserWindow.getFocusedWindow();
|
||||
if (win) {
|
||||
const path = dialog.showOpenDialogSync(win, {
|
||||
filters: [{ name: "workspace", extensions: ["b3-workspace"] }],
|
||||
});
|
||||
if (path?.length) {
|
||||
ipcRenderer.invoke("open-win", path[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
buildProject: () => {
|
||||
const workspace = get();
|
||||
if (!buildDir && workspace.path) {
|
||||
buildDir = dialog.showOpenDialogSync({
|
||||
properties: ["openDirectory", "createDirectory"],
|
||||
})?.[0];
|
||||
if (buildDir) {
|
||||
for (const editor of workspace.editors) {
|
||||
editor.dispatch?.("save");
|
||||
}
|
||||
try {
|
||||
for (const path of workspace.allFiles) {
|
||||
const buildpath = buildDir + "/" + Path.relative(workspace.workdir, path);
|
||||
console.log("build:", buildpath);
|
||||
const treeModel = b3util.createBuildData(path);
|
||||
fs.mkdirSync(Path.dirname(buildpath), { recursive: true });
|
||||
fs.writeFileSync(buildpath, JSON.stringify(treeModel, null, 2));
|
||||
}
|
||||
message.success(i18n.t("buildCompleted"));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
batchProject: () => {
|
||||
const workspace = get();
|
||||
const scriptPath = dialog.showOpenDialogSync({
|
||||
properties: ["openFile"],
|
||||
defaultPath: workspace.workdir,
|
||||
filters: [{ name: "Javascript", extensions: ["js"] }],
|
||||
})?.[0];
|
||||
if (scriptPath) {
|
||||
try {
|
||||
console.log("run script", scriptPath);
|
||||
const str = fs.readFileSync(scriptPath, "utf8");
|
||||
const batch = eval(str) as BatchScript;
|
||||
workspace.allFiles.forEach((path) => {
|
||||
const treeStr = fs.readFileSync(path, "utf8");
|
||||
let tree: TreeModel | undefined = JSON.parse(treeStr);
|
||||
if (batch.processTree && tree) {
|
||||
tree = batch.processTree(tree);
|
||||
}
|
||||
if (tree && batch.processNode) {
|
||||
const processNode = (node: NodeModel) => {
|
||||
batch.processNode?.(node, tree!);
|
||||
node.children?.forEach((child) => processNode(child));
|
||||
};
|
||||
processNode(tree.root);
|
||||
}
|
||||
if (tree) {
|
||||
fs.writeFileSync(path, JSON.stringify(tree));
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
isShowingSearch: false,
|
||||
onShowingSearch: (isShowingSearch) => {
|
||||
set({ isShowingSearch });
|
||||
},
|
||||
|
||||
open: (path) => {
|
||||
const workspace = get();
|
||||
let editor = workspace.editors.find((v) => v.path === path);
|
||||
if (!editor) {
|
||||
try {
|
||||
editor = new EditorStore(path);
|
||||
workspace.editors.push(editor);
|
||||
set({ editors: workspace.editors });
|
||||
workspace.edit(editor.path);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
message.error(`invalid file: ${path}`);
|
||||
}
|
||||
} else if (workspace.editing !== editor) {
|
||||
workspace.edit(editor.path);
|
||||
}
|
||||
},
|
||||
|
||||
edit: (path) => {
|
||||
const workspace = get();
|
||||
const editor = workspace.editors.find((v) => v.path === path);
|
||||
set({ editing: editor, editingNode: null });
|
||||
if (editor) {
|
||||
set({
|
||||
editingTree: {
|
||||
data: {
|
||||
name: editor.name,
|
||||
desc: editor.desc,
|
||||
root: null!,
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
set({ editingNode: null, editingTree: null });
|
||||
}
|
||||
},
|
||||
|
||||
close: (path) => {
|
||||
const workspace = get();
|
||||
const idx = workspace.editors.findIndex((v) => v.path === path);
|
||||
const editors = workspace.editors.filter((v) => v.path !== path);
|
||||
let editting = workspace.editing;
|
||||
if (editors.length && path === editting?.path) {
|
||||
editting = editors[idx === editors.length ? idx - 1 : idx];
|
||||
set({
|
||||
editingTree: {
|
||||
data: {
|
||||
name: editting.name,
|
||||
desc: editting.desc,
|
||||
root: null!,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
if (editors.length === 0) {
|
||||
editting = undefined;
|
||||
set({ editingNode: undefined });
|
||||
}
|
||||
set({ editing: editting, editors: editors });
|
||||
},
|
||||
|
||||
find: (path) => {
|
||||
const workspace = get();
|
||||
return workspace.editors.find((v) => v.path === path);
|
||||
},
|
||||
|
||||
save: () => {
|
||||
const workspace = get();
|
||||
saveFile(workspace.editing);
|
||||
},
|
||||
|
||||
saveAs: () => {},
|
||||
|
||||
saveAll: () => {
|
||||
const workspace = get();
|
||||
for (const editor of workspace.editors) {
|
||||
saveFile(editor);
|
||||
}
|
||||
},
|
||||
|
||||
watch: () => {
|
||||
try {
|
||||
const workspace = get();
|
||||
let hasEvent = false;
|
||||
fs.watch(workspace.workdir, { recursive: true }, (event, filename) => {
|
||||
console.log("fileanme", event, filename);
|
||||
if (event === "rename") {
|
||||
if (!hasEvent) {
|
||||
setTimeout(() => {
|
||||
workspace.loadTrees();
|
||||
hasEvent = false;
|
||||
}, 200);
|
||||
hasEvent = true;
|
||||
}
|
||||
}
|
||||
if (event === "change" && filename === "node-config.b3-setting") {
|
||||
workspace.loadNodeDefs();
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
|
||||
loadTrees: () => {
|
||||
const workspace = get();
|
||||
const data = loadFileTree(workspace.workdir, ".")!;
|
||||
data.title = Path.basename(workspace.workdir);
|
||||
set({ fileTree: data });
|
||||
|
||||
const allFiles: string[] = [];
|
||||
const collect = (fileNode?: FileModel) => {
|
||||
if (fileNode?.isLeaf) {
|
||||
allFiles.push(fileNode.path);
|
||||
}
|
||||
fileNode?.children?.forEach((child) => collect(child));
|
||||
};
|
||||
collect(data);
|
||||
set({ allFiles });
|
||||
},
|
||||
|
||||
loadNodeDefs: () => {
|
||||
const workspace = get();
|
||||
const nodeDefData = readJson(`${workspace.workdir}/node-config.b3-setting`) as NodeDef[];
|
||||
for (const v of nodeDefData) {
|
||||
workspace.nodeDefs.set(v.name, v);
|
||||
}
|
||||
set({ nodeDefs: workspace.nodeDefs });
|
||||
workspace.editing?.dispatch?.("reload");
|
||||
},
|
||||
|
||||
// node edit
|
||||
onEditingNode: (node) => {
|
||||
set({ editingNode: node });
|
||||
},
|
||||
|
||||
// tree edit
|
||||
onEditingTree: (tree) => {
|
||||
set({ editingTree: tree });
|
||||
},
|
||||
|
||||
// node def
|
||||
nodeDefs: new Map(),
|
||||
getNodeDef: (name) => {
|
||||
const workspace = get();
|
||||
unknownNodeDef.desc = i18n.t("node.unknown.desc");
|
||||
return workspace.nodeDefs.get(name) || unknownNodeDef;
|
||||
},
|
||||
hasNodeDef: (name) => {
|
||||
const workspace = get();
|
||||
return workspace.nodeDefs.has(name);
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,159 @@
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: only dark;
|
||||
|
||||
background-color: #0d1117;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
|
||||
--b3-color-border: #30363d;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.b3-drag-region {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
.b3-no-drag-region {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.b3-search .ant-select-selector {
|
||||
border: 1px solid var(--b3-color-border) !important;
|
||||
}
|
||||
|
||||
.b3-search .ant-select-selector:hover,
|
||||
.b3-search .ant-select-selector:focus-within {
|
||||
border: 1px solid var(--ant-color-primary) !important;
|
||||
}
|
||||
|
||||
.ant-input-number {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ant-tabs-nav {
|
||||
background-color: #010409;
|
||||
}
|
||||
|
||||
.ant-tabs,
|
||||
.ant-tabs-content-holder,
|
||||
.ant-tabs-content,
|
||||
.ant-tabs-tabpane,
|
||||
.b3-editor {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ant-tabs-tab {
|
||||
border-radius: 0 !important;
|
||||
margin-left: 0 !important;
|
||||
border-left-width: 0 !important;
|
||||
border-top: 2px solid #010409 !important;
|
||||
}
|
||||
|
||||
.ant-tabs-tab.ant-tabs-tab-active {
|
||||
border-top: 2px solid #f78166 !important;
|
||||
border-bottom: 1px solid #0d1117 !important;
|
||||
background-color: #0d1117 !important;
|
||||
}
|
||||
|
||||
.ant-tree,
|
||||
.ant-tree-treenode-draggable {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
.ant-tree .ant-tree-node-content-wrapper {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
.ant-tree-switcher.ant-tree-switcher-noop,
|
||||
.ant-tree-switcher.ant-tree-switcher_open,
|
||||
.ant-tree-switcher.ant-tree-switcher_close {
|
||||
width: 20px !important;
|
||||
text-align: end !important;
|
||||
background-color: unset !important;
|
||||
}
|
||||
|
||||
.ant-tree-indent .ant-tree-indent-unit {
|
||||
width: 12px !important;
|
||||
}
|
||||
|
||||
.b3-inspector .ant-form-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.b3-inspector .ant-form-item-label {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.ant-form-item .ant-form-item-explain {
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.ant-divider-horizontal.ant-divider-with-text {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.ant-modal-root,
|
||||
.b3-workspace {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.ant-modal {
|
||||
margin-bottom: 100px;
|
||||
width: 270px !important;
|
||||
}
|
||||
|
||||
.ant-modal-content {
|
||||
border-radius: 6px !important;
|
||||
padding-left: 20px !important;
|
||||
padding-right: 20px !important;
|
||||
}
|
||||
|
||||
.ant-modal-confirm-body .anticon-exclamation-circle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ant-modal-confirm-paragraph {
|
||||
display: contents !important;
|
||||
text-align: center !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ant-modal-confirm-btns {
|
||||
margin-top: 30px !important;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.ant-modal-confirm-btns .ant-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.b3-app-menu .ant-dropdown-trigger,
|
||||
.b3-app-menu .ant-dropdown-menu-item,
|
||||
.b3-app-menu .ant-dropdown-menu-submenu-title {
|
||||
height: 23px;
|
||||
padding-left: 10px !important;
|
||||
padding-right: 10px !important;
|
||||
font-size: 13px !important;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
.b3-app-menu .ant-dropdown-menu-submenu-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { App, ConfigProvider } from "antd";
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { Setup } from "./components/setup";
|
||||
import { Workspace } from "./components/workspace";
|
||||
import "./misc/i18n";
|
||||
import { themeConfig } from "./misc/theme";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<ConfigProvider theme={themeConfig}>
|
||||
<App>
|
||||
<Setup />
|
||||
<Workspace />
|
||||
</App>
|
||||
</ConfigProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
postMessage({ payload: "removeLoading" }, "*");
|
||||
@@ -0,0 +1,67 @@
|
||||
import { TreeGraphData as G6TreeGraphData } from "@antv/g6";
|
||||
|
||||
export interface NodeArgOption {
|
||||
name: string;
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
export interface NodeArg {
|
||||
name: string;
|
||||
type: string;
|
||||
desc: string;
|
||||
default?: string;
|
||||
options?: NodeArgOption[];
|
||||
}
|
||||
export interface NodeDef {
|
||||
name: string;
|
||||
type?: string;
|
||||
desc?: string;
|
||||
args?: NodeArg[];
|
||||
input?: string[];
|
||||
output?: string[];
|
||||
doc?: string;
|
||||
color?: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export interface NodeModel {
|
||||
id: number;
|
||||
name: string;
|
||||
desc?: string;
|
||||
args?: { [key: string]: any };
|
||||
input?: string[];
|
||||
output?: string[];
|
||||
children?: NodeModel[];
|
||||
debug?: boolean;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
export interface TreeModel {
|
||||
name: string;
|
||||
desc?: string;
|
||||
root: NodeModel;
|
||||
}
|
||||
|
||||
export interface TreeGraphData extends G6TreeGraphData {
|
||||
name: string;
|
||||
desc?: string;
|
||||
args?: { [key: string]: any };
|
||||
input?: string[];
|
||||
output?: string[];
|
||||
children?: TreeGraphData[];
|
||||
def: NodeDef;
|
||||
debug?: boolean;
|
||||
parent?: string;
|
||||
path?: string;
|
||||
lastModified?: number;
|
||||
|
||||
size?: number[];
|
||||
highlightInput?: boolean;
|
||||
highlightOutput?: boolean;
|
||||
}
|
||||
|
||||
export const unknownNodeDef: NodeDef = {
|
||||
name: "unknown",
|
||||
desc: "新建节点",
|
||||
type: "Action",
|
||||
};
|
||||
@@ -0,0 +1,439 @@
|
||||
import { useWorkspace } from "@/contexts/workspace-context";
|
||||
import { NodeModel, TreeGraphData, TreeModel } from "@/misc/b3type";
|
||||
import * as fs from "fs";
|
||||
import { message } from "./hooks";
|
||||
import Path from "./path";
|
||||
import i18n from "./i18n";
|
||||
|
||||
export const isSubtreeRoot = (data: TreeGraphData) => {
|
||||
return data.path && data.id.toString() !== "1";
|
||||
};
|
||||
|
||||
export const isSubtreeUpdated = (data: TreeGraphData) => {
|
||||
if (data.path) {
|
||||
try {
|
||||
const subtreePath = useWorkspace.getState().workdir + "/" + data.path;
|
||||
if (fs.statSync(subtreePath).mtimeMs !== data.lastModified) {
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (data.children) {
|
||||
for (const child of data.children) {
|
||||
if (isSubtreeUpdated(child)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const isNodeEqual = (node1: NodeModel, node2: NodeModel) => {
|
||||
if (
|
||||
node1.name === node2.name &&
|
||||
node1.desc === node2.desc &&
|
||||
node1.path === node2.path &&
|
||||
node1.debug === node2.debug
|
||||
) {
|
||||
const def = useWorkspace.getState().getNodeDef(node1.name);
|
||||
|
||||
for (const arg of def.args ?? []) {
|
||||
if (node1.args?.[arg.name] !== node2.args?.[arg.name]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (def.input?.length) {
|
||||
for (let i = 0; i < def.input.length; i++) {
|
||||
if (node1.input?.[i] !== node2.input?.[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (def.output?.length) {
|
||||
for (let i = 0; i < def.output.length; i++) {
|
||||
if (node1.output?.[i] !== node2.output?.[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const copyFromNode = (data: TreeGraphData, node: NodeModel) => {
|
||||
data.name = node.name;
|
||||
data.debug = node.debug;
|
||||
data.desc = node.desc;
|
||||
data.path = node.path;
|
||||
data.args = node.args;
|
||||
data.input = node.input;
|
||||
data.output = node.output;
|
||||
};
|
||||
|
||||
const parsingStack: string[] = [];
|
||||
|
||||
export const createNode = (data: TreeGraphData, includeChildren: boolean = true) => {
|
||||
const node: NodeModel = {
|
||||
id: Number(data.id),
|
||||
name: data.name,
|
||||
desc: data.desc,
|
||||
path: data.path,
|
||||
debug: data.debug,
|
||||
};
|
||||
if (data.input) {
|
||||
node.input = [];
|
||||
for (const v of data.input) {
|
||||
node.input.push(v ?? "");
|
||||
}
|
||||
}
|
||||
if (data.output) {
|
||||
node.output = [];
|
||||
for (const v of data.output) {
|
||||
node.output.push(v ?? "");
|
||||
}
|
||||
}
|
||||
if (data.args) {
|
||||
node.args = {};
|
||||
for (const k in data.args) {
|
||||
const v = data.args[k];
|
||||
if (v !== null && v !== undefined) {
|
||||
node.args[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (data.children && !isSubtreeRoot(data) && includeChildren) {
|
||||
node.children = [];
|
||||
for (const child of data.children) {
|
||||
node.children.push(createNode(child));
|
||||
}
|
||||
}
|
||||
return node;
|
||||
};
|
||||
|
||||
export const refreshTreeDataId = (data: TreeGraphData, id?: number) => {
|
||||
if (!id) {
|
||||
id = 1;
|
||||
}
|
||||
data.id = (id++).toString();
|
||||
if (data.children) {
|
||||
data.children.forEach((child) => {
|
||||
child.parent = data.id;
|
||||
id = refreshTreeDataId(child, id);
|
||||
});
|
||||
}
|
||||
return id;
|
||||
};
|
||||
|
||||
export const calcTreeDataSize = (data: TreeGraphData) => {
|
||||
let height = 40 + 2;
|
||||
const updateHeight = (obj: any) => {
|
||||
if (Array.isArray(obj) || (obj && Object.keys(obj).length > 0)) {
|
||||
const { str, line } = toBreakWord(`参数:${JSON.stringify(obj)}`, 35);
|
||||
height += 20 * line;
|
||||
}
|
||||
};
|
||||
if (data.path) {
|
||||
height += 20;
|
||||
}
|
||||
updateHeight(data.args);
|
||||
updateHeight(data.input);
|
||||
updateHeight(data.output);
|
||||
return [200, height];
|
||||
};
|
||||
|
||||
export const createTreeData = (node: NodeModel, parent?: string) => {
|
||||
const workspace = useWorkspace.getState();
|
||||
let treeData: TreeGraphData = {
|
||||
id: node.id.toFixed(),
|
||||
name: node.name,
|
||||
desc: node.desc,
|
||||
args: node.args,
|
||||
input: node.input,
|
||||
output: node.output,
|
||||
debug: node.debug,
|
||||
def: workspace.getNodeDef(node.name),
|
||||
parent: parent,
|
||||
};
|
||||
|
||||
treeData.size = calcTreeDataSize(treeData);
|
||||
|
||||
if (!parent) {
|
||||
parsingStack.length = 0;
|
||||
}
|
||||
|
||||
if (node.children) {
|
||||
treeData.children = [];
|
||||
node.children.forEach((child) => {
|
||||
treeData.children!.push(createTreeData(child, treeData.id));
|
||||
});
|
||||
} else if (node.path) {
|
||||
if (parsingStack.indexOf(node.path) >= 0) {
|
||||
treeData.path = node.path;
|
||||
treeData.size = calcTreeDataSize(treeData);
|
||||
message.error(`循环引用节点:${node.path}`, 4);
|
||||
return treeData;
|
||||
}
|
||||
parsingStack.push(node.path);
|
||||
try {
|
||||
const subtreePath = workspace.workdir + "/" + node.path;
|
||||
const str = fs.readFileSync(subtreePath, "utf8");
|
||||
treeData = createTreeData(JSON.parse(str).root, treeData.id);
|
||||
treeData.lastModified = fs.statSync(subtreePath).mtimeMs;
|
||||
treeData.path = node.path;
|
||||
treeData.debug = node.debug;
|
||||
treeData.size = calcTreeDataSize(treeData);
|
||||
} catch (error) {
|
||||
message.error(`解析子树失败:${node.path}`);
|
||||
console.log("parse subtree:", error);
|
||||
}
|
||||
parsingStack.pop();
|
||||
}
|
||||
calcTreeDataSize(treeData);
|
||||
return treeData;
|
||||
};
|
||||
|
||||
export const createBuildData = (path: string) => {
|
||||
try {
|
||||
const str = fs.readFileSync(path, "utf8");
|
||||
const treeModel = JSON.parse(str);
|
||||
const data = createTreeData(treeModel.root);
|
||||
refreshTreeDataId(data);
|
||||
treeModel.root = createFileData(data, true);
|
||||
return treeModel as NodeModel;
|
||||
} catch (error) {
|
||||
console.log("build error:", path, error);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const createFileData = (data: TreeGraphData, includeSubtree?: boolean) => {
|
||||
const nodeData: NodeModel = {
|
||||
id: Number(data.id),
|
||||
name: data.name,
|
||||
desc: data.desc || undefined,
|
||||
args: data.args || undefined,
|
||||
input: data.input || undefined,
|
||||
output: data.output || undefined,
|
||||
debug: data.debug || undefined,
|
||||
path: data.path || undefined,
|
||||
};
|
||||
if (data.children?.length && (includeSubtree || !isSubtreeRoot(data))) {
|
||||
nodeData.children = [];
|
||||
data.children.forEach((child) => {
|
||||
nodeData.children!.push(createFileData(child, includeSubtree));
|
||||
});
|
||||
}
|
||||
return nodeData;
|
||||
};
|
||||
|
||||
export const createNewTree = (filename: string) => {
|
||||
const tree: TreeModel = {
|
||||
name: Path.basenameWithoutExt(filename),
|
||||
root: {
|
||||
id: 1,
|
||||
name: "Sequence",
|
||||
},
|
||||
};
|
||||
return tree;
|
||||
};
|
||||
|
||||
export const toBreakWord = (str: string, len: number, char = "\n") => {
|
||||
let strTemp = "";
|
||||
let line = 1;
|
||||
if (str.length <= len) {
|
||||
return { str, line };
|
||||
}
|
||||
while (str.length > len) {
|
||||
strTemp += str.slice(0, len) + char;
|
||||
str = str.slice(len, str.length);
|
||||
line++;
|
||||
}
|
||||
strTemp += str;
|
||||
return {
|
||||
str: strTemp,
|
||||
line,
|
||||
};
|
||||
};
|
||||
|
||||
export const createProject = (path: string) => {
|
||||
fs.writeFileSync(
|
||||
Path.dirname(path) + "/node-config.b3-setting",
|
||||
JSON.stringify(
|
||||
[
|
||||
{
|
||||
name: "AlwaysFail",
|
||||
type: "Decorator",
|
||||
desc: i18n.t("node.alwaysFail.desc"),
|
||||
doc: i18n.t("node.alwaysFail.doc"),
|
||||
},
|
||||
{
|
||||
name: "AlwaysSuccess",
|
||||
type: "Decorator",
|
||||
desc: i18n.t("node.alwaysSuccess.desc"),
|
||||
doc: i18n.t("node.alwaysSuccess.doc"),
|
||||
},
|
||||
{
|
||||
name: "Check",
|
||||
type: "Condition",
|
||||
desc: i18n.t("node.check.desc"),
|
||||
args: [
|
||||
{
|
||||
name: "value",
|
||||
type: "code",
|
||||
desc: i18n.t("node.check.arg1.desc"),
|
||||
},
|
||||
],
|
||||
doc: i18n.t("node.check.doc"),
|
||||
},
|
||||
{
|
||||
name: "Clear",
|
||||
type: "Action",
|
||||
desc: i18n.t("node.clear.desc"),
|
||||
output: [i18n.t("node.clear.output1")],
|
||||
},
|
||||
{
|
||||
name: "ForEach",
|
||||
type: "Composite",
|
||||
desc: i18n.t("node.forEach.desc"),
|
||||
input: [i18n.t("node.forEach.input1")],
|
||||
output: [i18n.t("node.forEach.output1")],
|
||||
doc: i18n.t("node.forEach.doc"),
|
||||
},
|
||||
{
|
||||
name: "GetTime",
|
||||
type: "Action",
|
||||
desc: i18n.t("node.getTime.desc"),
|
||||
output: [i18n.t("node.getTime.output1")],
|
||||
},
|
||||
{
|
||||
name: "IsNull",
|
||||
type: "Condition",
|
||||
desc: i18n.t("node.isNull.desc"),
|
||||
input: [i18n.t("node.isNull.input1")],
|
||||
},
|
||||
{
|
||||
name: "Log",
|
||||
type: "Action",
|
||||
desc: i18n.t("node.log.desc"),
|
||||
args: [
|
||||
{
|
||||
name: "message",
|
||||
type: "string",
|
||||
desc: i18n.t("node.log.arg1.desc"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Loop",
|
||||
type: "Composite",
|
||||
desc: i18n.t("node.loop.desc"),
|
||||
args: [
|
||||
{
|
||||
name: "count",
|
||||
type: "int?",
|
||||
desc: i18n.t("node.loop.arg1.desc"),
|
||||
},
|
||||
],
|
||||
input: [i18n.t("node.loop.input1") + "?"],
|
||||
},
|
||||
{
|
||||
name: "Not",
|
||||
type: "Decorator",
|
||||
desc: i18n.t("node.not.desc"),
|
||||
doc: i18n.t("node.not.doc"),
|
||||
},
|
||||
{
|
||||
name: "NotNull",
|
||||
type: "Condition",
|
||||
desc: i18n.t("node.notNull.desc"),
|
||||
input: [i18n.t("node.notNull.input")],
|
||||
},
|
||||
{
|
||||
name: "Once",
|
||||
type: "Composite",
|
||||
desc: i18n.t("node.once.desc"),
|
||||
doc: i18n.t("node.once.doc"),
|
||||
},
|
||||
{
|
||||
name: "Parallel",
|
||||
type: "Composite",
|
||||
desc: i18n.t("node.parallel.desc"),
|
||||
doc: i18n.t("node.parallel.doc"),
|
||||
},
|
||||
{
|
||||
name: "Selector",
|
||||
type: "Composite",
|
||||
desc: i18n.t("node.selector.desc"),
|
||||
doc: i18n.t("node.selector.doc"),
|
||||
},
|
||||
{
|
||||
name: "Sequence",
|
||||
type: "Composite",
|
||||
desc: i18n.t("node.sequence.desc"),
|
||||
doc: i18n.t("node.sequence.doc"),
|
||||
},
|
||||
{
|
||||
name: "Wait",
|
||||
type: "Action",
|
||||
desc: i18n.t("node.wait.desc"),
|
||||
args: [
|
||||
{
|
||||
name: "time",
|
||||
type: "float",
|
||||
desc: i18n.t("node.wait.arg1.desc"),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
fs.writeFileSync(
|
||||
Path.dirname(path) + "/example.json",
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "example",
|
||||
root: {
|
||||
id: 1,
|
||||
name: "Sequence",
|
||||
children: [
|
||||
{
|
||||
id: 2,
|
||||
name: "Log",
|
||||
args: {
|
||||
str: "hello",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Wait",
|
||||
args: {
|
||||
time: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path,
|
||||
JSON.stringify(
|
||||
{
|
||||
nodeConf: "node-config.b3-setting",
|
||||
metadata: [],
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import { App } from "antd";
|
||||
import type { MessageInstance } from "antd/es/message/interface";
|
||||
import type { ModalStaticFunctions } from "antd/es/modal/confirm";
|
||||
import type { NotificationInstance } from "antd/es/notification/interface";
|
||||
|
||||
let message: MessageInstance;
|
||||
let notification: NotificationInstance;
|
||||
let modal: Omit<ModalStaticFunctions, "warn">;
|
||||
|
||||
const setGlobalHooks = () => {
|
||||
const staticFunction = App.useApp();
|
||||
message = staticFunction.message;
|
||||
modal = staticFunction.modal;
|
||||
notification = staticFunction.notification;
|
||||
};
|
||||
|
||||
export { message, modal, notification, setGlobalHooks };
|
||||
@@ -0,0 +1,45 @@
|
||||
import dayjs from "dayjs";
|
||||
import i18n from "i18next";
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
import HttpApi from "i18next-http-backend";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
|
||||
declare module "i18next" {
|
||||
interface CustomTypeOptions {
|
||||
returnNull: false;
|
||||
}
|
||||
}
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.use(HttpApi)
|
||||
.init(
|
||||
{
|
||||
debug: import.meta.env.NODE_ENV === "development",
|
||||
returnNull: false,
|
||||
fallbackLng: "en",
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
react: {
|
||||
useSuspense: false,
|
||||
},
|
||||
load: "languageOnly",
|
||||
detection: {
|
||||
caches: ["localStorage", "sessionStorage", "cookie"],
|
||||
},
|
||||
backend: {
|
||||
loadPath: `locales/{{lng}}.json`,
|
||||
},
|
||||
},
|
||||
function (err, t) {
|
||||
dayjs.locale(i18n.language);
|
||||
}
|
||||
);
|
||||
|
||||
i18n.on("languageChanged", (lng) => {
|
||||
dayjs.locale(lng);
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
@@ -0,0 +1,11 @@
|
||||
import * as fs from "fs";
|
||||
|
||||
export const readJson = (path: string) => {
|
||||
const str = fs.readFileSync(path, "utf-8");
|
||||
return JSON.parse(str);
|
||||
};
|
||||
|
||||
export const writeJson = (path: string, data: any) => {
|
||||
const str = JSON.stringify(data, undefined, 2);
|
||||
fs.writeFileSync(path, str, "utf-8");
|
||||
};
|
||||
@@ -0,0 +1,105 @@
|
||||
import { BrowserWindow } from "@electron/remote";
|
||||
import { MutableRefObject } from "react";
|
||||
import {
|
||||
HotkeyCallback,
|
||||
Keys,
|
||||
isHotkeyPressed as _isHotkeyPressed,
|
||||
useHotkeys as _useHotkeys,
|
||||
} from "react-hotkeys-hook";
|
||||
import { OptionsOrDependencyArray, RefType } from "react-hotkeys-hook/dist/types";
|
||||
import { Key } from "ts-key-enum";
|
||||
|
||||
export const isMacos = process.platform === "darwin";
|
||||
|
||||
let forceTarget: HTMLElement | undefined;
|
||||
|
||||
const hotkey = (key: string) => {
|
||||
if (key.indexOf("ctrl") >= 0 && isMacos) {
|
||||
key = key.replace("ctrl", Key.Meta);
|
||||
}
|
||||
return key.toLowerCase();
|
||||
};
|
||||
|
||||
export const isHotkeyPressed = (key: string) => {
|
||||
if (!key) {
|
||||
return false;
|
||||
} else if (key.indexOf("ctrl") >= 0 && isMacos) {
|
||||
key = key.replace("ctrl", Key.Meta);
|
||||
}
|
||||
return _isHotkeyPressed(key.toLowerCase(), "+");
|
||||
};
|
||||
|
||||
export function useHotkeys<T extends HTMLElement>(
|
||||
keys: Keys,
|
||||
callback: HotkeyCallback,
|
||||
options?: OptionsOrDependencyArray,
|
||||
dependencies?: OptionsOrDependencyArray
|
||||
): MutableRefObject<RefType<T>> {
|
||||
if (keys instanceof Array) {
|
||||
keys = keys.filter((v) => !!v);
|
||||
}
|
||||
return _useHotkeys(keys, callback, options, dependencies);
|
||||
}
|
||||
|
||||
export const Hotkey = {
|
||||
Backspace: Key.Backspace,
|
||||
Build: hotkey("ctrl+b"),
|
||||
CloseEditor: hotkey("ctrl+w"),
|
||||
Copy: hotkey("ctrl+c"),
|
||||
Delete: Key.Delete,
|
||||
Duplicate: hotkey("ctrl+d"),
|
||||
Enter: Key.Enter,
|
||||
Escape: Key.Escape,
|
||||
F2: Key.F2,
|
||||
Insert: Key.Insert,
|
||||
MacDelete: isMacos ? hotkey("ctrl+backspace") : "",
|
||||
Paste: hotkey("ctrl+v"),
|
||||
Redo: isMacos ? hotkey("shift+ctrl+z") : hotkey("ctrl+y"),
|
||||
Replace: hotkey("shift+ctrl+v"),
|
||||
Save: hotkey("ctrl+s"),
|
||||
Search: hotkey("ctrl+p"),
|
||||
Undo: hotkey("ctrl+z"),
|
||||
};
|
||||
|
||||
export const setInputFocus = (target: HTMLElement) => {
|
||||
forceTarget = target;
|
||||
};
|
||||
|
||||
export const sendInputEvent = (key: string) => {
|
||||
const webContents = BrowserWindow.getFocusedWindow()?.webContents;
|
||||
if (webContents) {
|
||||
let keyCode: string | undefined;
|
||||
const modifiers: Electron.InputEvent["modifiers"] = [];
|
||||
key.split("+").forEach((v) => {
|
||||
if (v.indexOf("ctrl") >= 0) {
|
||||
modifiers.push("ctrl");
|
||||
} else if (v.indexOf("shift") >= 0) {
|
||||
modifiers.push("shift");
|
||||
} else if (v.indexOf("alt") >= 0) {
|
||||
modifiers.push("alt");
|
||||
} else if (v.indexOf("meta") >= 0) {
|
||||
modifiers.push("meta");
|
||||
} else {
|
||||
keyCode = v;
|
||||
}
|
||||
});
|
||||
if (!keyCode) {
|
||||
return;
|
||||
}
|
||||
forceTarget?.focus();
|
||||
modifiers.forEach((mod) => {
|
||||
webContents.sendInputEvent({
|
||||
type: "keyDown",
|
||||
keyCode: mod,
|
||||
});
|
||||
});
|
||||
webContents.sendInputEvent({ type: "keyDown", keyCode: keyCode, modifiers: modifiers });
|
||||
webContents.sendInputEvent({ type: "keyUp", keyCode: keyCode, modifiers: modifiers });
|
||||
modifiers.forEach((mod) => {
|
||||
webContents.sendInputEvent({
|
||||
type: "keyUp",
|
||||
keyCode: mod,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import path from "path";
|
||||
|
||||
const Path = path;
|
||||
|
||||
declare module "path" {
|
||||
interface PlatformPath {
|
||||
basenameWithoutExt(path: string): string;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
path.basenameWithoutExt = (path: string) => {
|
||||
return Path.basename(path, Path.extname(path));
|
||||
};
|
||||
|
||||
export default Path;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { ThemeConfig, theme } from "antd";
|
||||
import "../index.scss";
|
||||
|
||||
export const themeConfig: ThemeConfig = {
|
||||
cssVar: true,
|
||||
algorithm: theme.darkAlgorithm,
|
||||
token: {
|
||||
colorBgBase: "#0d1117",
|
||||
colorBgContainer: "#0d1117",
|
||||
colorBgElevated: "#161b22",
|
||||
colorBorderSecondary: "#30363d",
|
||||
borderRadius: 4,
|
||||
},
|
||||
components: {
|
||||
Tree: {
|
||||
// directoryNodeSelectedBg: "#010409",
|
||||
borderRadius: 0,
|
||||
colorBgContainer: "#010409",
|
||||
},
|
||||
Tabs: {
|
||||
horizontalMargin: "0",
|
||||
},
|
||||
Layout: {
|
||||
headerBg: "#0d1117",
|
||||
siderBg: "#010409",
|
||||
},
|
||||
Dropdown: {
|
||||
motionDurationMid: "0.1s",
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -1,21 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/",
|
||||
"sourceMap": true,
|
||||
"esModuleInterop": true,
|
||||
"noImplicitAny": true,
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"jsx": "react"
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"./common/**/*",
|
||||
"./main-process/**/*",
|
||||
"./render-process/**/*",
|
||||
"./fs-tree-view/**/*",
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts", "package.json", "electron"]
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { rmSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import electron from "vite-electron-plugin";
|
||||
import { customStart, loadViteEnv } from "vite-electron-plugin/plugin";
|
||||
import renderer from "vite-plugin-electron-renderer";
|
||||
import pkg from "./package.json";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(({ command }) => {
|
||||
rmSync("dist-electron", { recursive: true, force: true });
|
||||
|
||||
const sourcemap = true; //command === "serve" || !!process.env.VSCODE_DEBUG;
|
||||
|
||||
return {
|
||||
build: {
|
||||
sourcemap: true,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.join(__dirname, "src"),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
react(),
|
||||
electron({
|
||||
include: ["electron"],
|
||||
transformOptions: {
|
||||
sourcemap,
|
||||
},
|
||||
plugins: [
|
||||
...(!!process.env.VSCODE_DEBUG
|
||||
? [
|
||||
// Will start Electron via VSCode Debug
|
||||
customStart(() =>
|
||||
console.log(/* For `.vscode/.debug.script.mjs` */ "[startup] Electron App")
|
||||
),
|
||||
]
|
||||
: []),
|
||||
// Allow use `import.meta.env.VITE_SOME_KEY` in Electron-Main
|
||||
loadViteEnv(),
|
||||
],
|
||||
}),
|
||||
// Use Node.js API in the Renderer-process
|
||||
renderer(),
|
||||
],
|
||||
server: !!process.env.VSCODE_DEBUG
|
||||
? (() => {
|
||||
const url = new URL(pkg.debug.env.VITE_DEV_SERVER_URL);
|
||||
return {
|
||||
host: url.hostname,
|
||||
port: +url.port,
|
||||
};
|
||||
})()
|
||||
: undefined,
|
||||
clearScreen: false,
|
||||
};
|
||||
});
|
||||
@@ -1,52 +0,0 @@
|
||||
const CopyPlugin = require("copy-webpack-plugin");
|
||||
|
||||
module.exports = {
|
||||
mode: "development",
|
||||
entry: {
|
||||
"bundle": ["./render-process/index.tsx"],
|
||||
"main": ["./main-process/MainProcess.tsx"]
|
||||
},
|
||||
output: {
|
||||
filename: "[name].js",
|
||||
path: __dirname + "/dist"
|
||||
},
|
||||
|
||||
devtool: "source-map",
|
||||
|
||||
resolve: {
|
||||
extensions: [".ts", ".tsx", ".js", ".json"]
|
||||
},
|
||||
|
||||
module: {
|
||||
rules: [
|
||||
{ test: /\.tsx?$/, loader: "ts-loader" },
|
||||
{ test: /\.(jpe?g|png|gif|svg)$/i, loader: "file-loader" },
|
||||
{ enforce: "pre", test: /\.js$/, loader: "source-map-loader" },
|
||||
{
|
||||
test: /\.less$/,
|
||||
include: /node_modules/,
|
||||
use: ['style-loader',{ loader: 'css-loader' },{ loader: 'less-loader' }]
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: ['style-loader', { loader: 'css-loader' }]
|
||||
},
|
||||
{//https://github.com/ashtuchkin/iconv-lite/issues/205
|
||||
test: /node_modules[\/\\](iconv-lite)[\/\\].+/,
|
||||
resolve: {
|
||||
aliasFields: ['main']
|
||||
}
|
||||
},
|
||||
|
||||
]
|
||||
},
|
||||
|
||||
plugins: [
|
||||
],
|
||||
|
||||
target: "electron-renderer",
|
||||
|
||||
externals: [{
|
||||
'electron-reload': 'require("electron-reload")'
|
||||
}]
|
||||
};
|
||||