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
This commit is contained in:
1 parent
91da6f0071
commit
2e5c08b2ad
84 files changed
+14250
-11038
No files matched your search
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in new issue
Block a user