From c98725f35530175d25a34b2c64959d25339ad4a2 Mon Sep 17 00:00:00 2001 From: Matthias Konitzny <konitzny@ibr.cs.tu-bs.de> Date: Fri, 16 Sep 2022 12:27:33 +0200 Subject: [PATCH] Implemented system to merge checkpoints. --- src/editor/components/nodedetails.tsx | 27 +++++++++-- src/editor/components/sidepanel.tsx | 8 +++- src/editor/editor.tsx | 68 +++++++++++++++++++++------ 3 files changed, 84 insertions(+), 19 deletions(-) diff --git a/src/editor/components/nodedetails.tsx b/src/editor/components/nodedetails.tsx index 76a830c..70083d9 100644 --- a/src/editor/components/nodedetails.tsx +++ b/src/editor/components/nodedetails.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState } from "react"; import { Node } from "../../common/graph/node"; import { NodeType } from "../../common/graph/nodetype"; import "./nodedetails.css"; @@ -7,14 +7,21 @@ import { NodeDataChangeRequest } from "../editor"; type NodeDetailsProps = { selectedNodes: Node[]; idToObjectType: Map<number, NodeType>; - onNodeDataChange: { (requests: NodeDataChangeRequest[]): void }; + onNodeDataChange: ( + requests: NodeDataChangeRequest[], + createCheckpoint?: boolean + ) => void; + createCheckpoint: (description: string) => void; }; function NodeDetails({ selectedNodes, idToObjectType, onNodeDataChange, + createCheckpoint, }: NodeDetailsProps) { + const [changed, setChanged] = useState(false); + if (selectedNodes.length == 0) { return <div id="nodedetails">No node selected.</div>; } @@ -45,6 +52,10 @@ function NodeDetails({ key: keyof NodeDataChangeRequest, value: ValueType ) { + if (!changed) { + setChanged(true); + } + Object.assign(referenceData, { [key]: value }); onNodeDataChange( @@ -67,12 +78,20 @@ function NodeDetails({ return Object.assign({}, defaults, update, { id: node.id, }); - }) + }), + false ); }; + const handleBlur = () => { + if (changed) { + createCheckpoint(`Modified ${selectedNodes.length} node(s) data.`); + setChanged(false); + } + }; + return ( - <div id="nodedetails"> + <div id="nodedetails" onBlur={handleBlur}> {selectedNodes.length === 1 ? ( <div> <label htmlFor="node-name" hidden> diff --git a/src/editor/components/sidepanel.tsx b/src/editor/components/sidepanel.tsx index a0e1d63..4022b64 100644 --- a/src/editor/components/sidepanel.tsx +++ b/src/editor/components/sidepanel.tsx @@ -18,7 +18,11 @@ interface SidepanelProps { onUndo: () => void; onNodeTypeSelect: (type: NodeType) => void; onSettingsChange: (settings: EditorSettings) => void; - onNodeDataChange: { (requests: NodeDataChangeRequest[]): void }; + onNodeDataChange: ( + requests: NodeDataChangeRequest[], + createCheckpoint?: boolean + ) => void; + createCheckpoint: (description: string) => void; onSave: () => void; selectedNodes: Node[]; settings: EditorSettings; @@ -33,6 +37,7 @@ function Sidepanel({ onSettingsChange, onNodeDataChange, onSave, + createCheckpoint, selectedNodes, settings, }: SidepanelProps) { @@ -52,6 +57,7 @@ function Sidepanel({ selectedNodes={selectedNodes} idToObjectType={graph.idToObjectGroup} onNodeDataChange={onNodeDataChange} + createCheckpoint={createCheckpoint} /> <hr /> <NodeTypesEditor diff --git a/src/editor/editor.tsx b/src/editor/editor.tsx index 976693a..556bfe9 100644 --- a/src/editor/editor.tsx +++ b/src/editor/editor.tsx @@ -90,6 +90,7 @@ export class Editor extends React.PureComponent<any, stateTypes> { this.handleCheckpointRequest = this.handleCheckpointRequest.bind(this); this.handleUndo = this.handleUndo.bind(this); this.handleRedo = this.handleRedo.bind(this); + this.createCheckpoint = this.createCheckpoint.bind(this); document.addEventListener("keydown", (e) => { this.keyPressed(e.key); @@ -232,7 +233,10 @@ export class Editor extends React.PureComponent<any, stateTypes> { this.selectNodes(nodesWithType); } - private handleNodeDataChange(nodeData: NodeDataChangeRequest[]) { + private handleNodeDataChange( + nodeData: NodeDataChangeRequest[], + createCheckpoint = true + ) { if (nodeData.length == 0) { return; } @@ -246,16 +250,25 @@ export class Editor extends React.PureComponent<any, stateTypes> { Object.assign(node, request); } - graph.createCheckpoint(`Modified ${nodeData.length} node(s) data.`); + // Create checkpoint + if (createCheckpoint) { + graph.createCheckpoint(`Modified ${nodeData.length} node(s) data.`); + } // Push shallow copy to state this.setState({ graph: graph }); } - private handleNodeCreation(position?: Coordinate2D): Node { + private handleNodeCreation( + position?: Coordinate2D, + createCheckpoint = true + ): Node { const graph = Object.assign(new DynamicGraph(), this.state.graph); const node = graph.createNode(undefined, position.x, position.y, 0, 0); - graph.createCheckpoint("Created new node."); + + if (createCheckpoint) { + graph.createCheckpoint("Created new node."); + } this.setState({ graph: graph, @@ -263,7 +276,7 @@ export class Editor extends React.PureComponent<any, stateTypes> { return node; } - private handleNodeDeletion(ids: number[]) { + private handleNodeDeletion(ids: number[], createCheckpoint = true) { if (ids.length == 0) { return; } @@ -273,29 +286,42 @@ export class Editor extends React.PureComponent<any, stateTypes> { const selectedNodes = this.state.selectedNodes.filter( (node) => !ids.includes(node.id) ); - graph.createCheckpoint(`Deleted ${ids.length} nodes.`); + + if (createCheckpoint) { + graph.createCheckpoint(`Deleted ${ids.length} nodes.`); + } this.setState({ graph: graph, selectedNodes: selectedNodes }); } - private handleLinkCreation(source: number, target: number): Link { + private handleLinkCreation( + source: number, + target: number, + createCheckpoint = true + ): Link { const graph = Object.assign(new DynamicGraph(), this.state.graph); const link = graph.createLink(source, target); - graph.createCheckpoint( - `Created link between ${graph.node(source).name} and ${ - graph.node(target).name - }.` - ); + + if (createCheckpoint) { + graph.createCheckpoint( + `Created link between ${graph.node(source).name} and ${ + graph.node(target).name + }.` + ); + } this.setState({ graph: graph }); return link; } - private handleLinkDeletion(ids: number[]) { + private handleLinkDeletion(ids: number[], createCheckpoint = true) { const graph = Object.assign(new DynamicGraph(), this.state.graph); ids.forEach((id) => graph.deleteLink(id)); - graph.createCheckpoint(`Deleted ${ids.length} link(s).`); + + if (createCheckpoint) { + graph.createCheckpoint(`Deleted ${ids.length} link(s).`); + } this.setState({ graph: graph }); } @@ -303,8 +329,11 @@ export class Editor extends React.PureComponent<any, stateTypes> { private loadGraphFromCheckpoint(checkpoint: Checkpoint<SimGraphData>) { const graph = new DynamicGraph(); graph.fromSerializedObject(checkpoint.data); + + // Transfer checkpoints to new graph object graph.history.copyCheckpointsFromHistory(this.state.graph.history); + // Restore selected nodes const selectedNodes = this.state.selectedNodes .map((node) => graph.node(node.id)) .filter((node) => node != undefined); @@ -336,6 +365,16 @@ export class Editor extends React.PureComponent<any, stateTypes> { this.setState({ graph: graph }); } + /** + * Creates a new Checkpoint in the graph history + * @param description Checkpoint description. + */ + private createCheckpoint(description: string) { + const graph = Object.assign(new DynamicGraph(), this.state.graph); + graph.createCheckpoint(description); + this.setState({ graph: graph }); + } + render(): React.ReactNode { return ( <div id="ks-editor"> @@ -393,6 +432,7 @@ export class Editor extends React.PureComponent<any, stateTypes> { settings={this.state.settings} onNodeDataChange={this.handleNodeDataChange} onSave={this.saveSpace} + createCheckpoint={this.createCheckpoint} /> </div> )} -- GitLab