private void HandleCanvasContextClick(object sender, PointerGraphicalElementEventArgs e) { IList <TestableEditorElements.MenuOption> options = new List <TestableEditorElements.MenuOption>(); options.Add(new TestableEditorElements.MenuItem(new GUIContent("Add step"), false, () => { IStep step = EntityFactory.CreateStep("New Step"); step.StepMetadata.Position = e.PointerPosition; AddStepWithUndo(step); })); if (SystemClipboard.IsStepInClipboard()) { options.Add(new TestableEditorElements.MenuItem(new GUIContent("Paste step"), false, () => { Paste(e.PointerPosition); })); } else { options.Add(new TestableEditorElements.DisabledMenuItem(new GUIContent("Paste step"))); } TestableEditorElements.DisplayContextMenu(options); }
private void OnGUI() { if (Event.current.type == EventType.ExecuteCommand && Event.current.commandName == "Wait") { TestableEditorElements.StopPlayback(); Event.current.Use(); } if (Event.current.type != EventType.ExecuteCommand) { return; } if (Event.current.commandName != "Finished") { return; } if (Finished != null) { Finished(this, new FinishedEventArgs(window)); } window.Close(); Close(); }
/// <summary> /// Sets a new course in the training workflow editor window after asking to save the current one, if needed. /// </summary> /// <param name="course">New course to set.</param> /// <returns>True if the course is set, false if the user cancels the operation.</returns> public bool SetTrainingCourseWithUserConfirmation(ICourse course) { if (activeCourse != null && IsDirty) { if (IsDirty) { int userConfirmation = TestableEditorElements.DisplayDialogComplex("Unsaved changes detected.", "Do you want to save the changes to a current training course?", "Save", "Cancel", "Discard"); if (userConfirmation == 0) { SaveTraining(); if (activeCourse.Data.Name.Equals(course.Data.Name)) { return(true); } } else if (userConfirmation == 1) { return(false); } } } SetTrainingCourse(course); return(true); }
/// <summary> /// Save the training to given path. /// </summary> public static bool SaveTrainingCourseToFile(ICourse course) { try { if (course == null) { throw new NullReferenceException("The training course is not saved because it doesn't exist."); } string path = GetTrainingPath(course); string directory = Path.GetDirectoryName(path); if (string.IsNullOrEmpty(directory) == false && Directory.Exists(directory) == false) { Directory.CreateDirectory(directory); } string serialized = JsonTrainingSerializer.Serialize(course); File.WriteAllText(path, serialized); // Check if saved as asset. If true, import it. TryReloadAssetByFullPath(path); return(true); } catch (Exception e) { TestableEditorElements.DisplayDialog("Error while saving the training course!", e.ToString(), "Close"); logger.Error(e); return(false); } }
public void LoadTrainingCourseFromFile(string path) { if (string.IsNullOrEmpty(path) || File.Exists(path) == false) { return; } ICourse course = SaveManager.LoadTrainingCourseFromFile(path); string filename = Path.GetFileNameWithoutExtension(path); if (course.Data.Name.Equals(filename) == false) { bool userConfirmation = TestableEditorElements.DisplayDialog("Course name does not match filename.", string.Format("The training course name (\"{0}\") does not match the filename (\"{1}\"). To be able to load the training course, it must be renamed to \"{1}\".", course.Data.Name, filename), "Rename Course", "Cancel"); if (userConfirmation == false) { return; } course.Data.Name = filename; SaveManager.SaveTrainingCourseToFile(course); } SetTrainingCourseWithUserConfirmation(course); IsDirty = false; }
/// <inheritdoc /> public override Rect Draw(Rect rect, object currentValue, Action <object> changeValueCallback, GUIContent label) { EditorGUI.BeginDisabledGroup(EditorConfigurator.Instance.AllowedMenuItemsSettings.GetBehaviorMenuOptions().Any() == false); if (EditorDrawingHelper.DrawAddButton(ref rect, "Add Behavior")) { IList <TestableEditorElements.MenuOption> options = ConvertFromConfigurationOptionsToGenericMenuOptions(EditorConfigurator.Instance.BehaviorsMenuContent.ToList(), currentValue, changeValueCallback); TestableEditorElements.DisplayContextMenu(options); } EditorGUI.EndDisabledGroup(); if (EditorDrawingHelper.DrawHelpButton(ref rect)) { Application.OpenURL("https://developers.innoactive.de/documentation/creator/latest/articles/innoactive-creator/default-behaviors.html"); } if (EditorConfigurator.Instance.AllowedMenuItemsSettings.GetBehaviorMenuOptions().Any() == false) { rect.y += rect.height + EditorDrawingHelper.VerticalSpacing; rect.width -= EditorDrawingHelper.IndentationWidth; EditorGUI.HelpBox(rect, "Your project does not contain any Behaviors. Either create one or import an Innoactive Creator Component.", MessageType.Error); rect.height += rect.height + EditorDrawingHelper.VerticalSpacing; } return(rect); }
/// <inheritdoc /> public override Rect Draw(Rect rect, object currentValue, Action <object> changeValueCallback, GUIContent label) { if (EditorDrawingHelper.DrawAddButton(ref rect, "Add Behavior")) { IList <TestableEditorElements.MenuOption> options = ConvertFromConfigurationOptionsToGenericMenuOptions(EditorConfigurator.Instance.BehaviorsMenuContent.ToList(), currentValue, changeValueCallback); TestableEditorElements.DisplayContextMenu(options); } return(rect); }
/// <summary> /// Start sending <paramref name="recordedActions"/> to the <see cref="window"/> and invoke <paramref name="finishedCallback"/> when done. /// </summary> public static void StartPlayback(EditorWindow window, IList <UserAction> recordedActions) { foreach (UserAction action in recordedActions) { TestableEditorElements.StartPlayback(action.PrepickedSelections); window.RepaintImmediately(); window.SendEvent(action.Event); TestableEditorElements.StopPlayback(); } }
private void OnDestroy() { if (IsRecording) { TestableEditorElements.Panic(); if (recordedWindow != null) { recordedWindow.Close(); } } }
/// <summary> /// Starts recording given <paramref name="test"/>. /// </summary> public void StartRecording(IEditorImguiTest test) { IsRecording = true; recordedWindow = test.BaseGiven(); this.test = test; userActions.Clear(); TestableEditorElements.StartRecording(); }
private void Abort() { IsRecording = false; TestableEditorElements.Panic(); if (recordedWindow != null) { recordedWindow.Close(); } Close(); }
public void Teardown() { if (result != null) { result.Close(); } if (TestableEditorElements.Mode == TestableEditorElements.DisplayMode.Playback) { TestableEditorElements.StopPlayback(); } AdditionalTeardown(); }
private void SaveAndTerminate() { JsonSerializer serializer = JsonSerializer.Create(serializerSettings); List <string> lastPrepickedSelections = TestableEditorElements.StopRecording(); if (userActions.Any()) { userActions.Last().PrepickedSelections = lastPrepickedSelections; } string serialized = JArray.FromObject(userActions, serializer).ToString(); Directory.CreateDirectory(Path.GetDirectoryName(test.PathToRecordedActions)); StreamWriter file = null; try { file = File.CreateText(test.PathToRecordedActions); file.Write(serialized); } finally { IsRecording = false; if (file != null) { file.Close(); } if (recordedWindow != null) { recordedWindow.Close(); } AssetDatabase.ImportAsset(test.PathToRecordedActions); Close(); } }
private void HandleCanvasContextClick(object sender, PointerGraphicalElementEventArgs e) { TestableEditorElements.DisplayContextMenu(new List <TestableEditorElements.MenuOption> { new TestableEditorElements.MenuItem(new GUIContent("Add step"), false, () => { IStep step = new Step("New Step"); step.StepMetadata.Position = e.PointerPosition; RevertableChangesHandler.Do(new TrainingCommand(() => { AddStep(step); chapter.ChapterMetadata.LastSelectedStep = step; }, () => { DeleteStep(step); } )); }) }); }
private static void PreserveTrainingState() { try { if (TrainingWindow.IsOpen == false) { return; } TrainingWindow.GetWindow().MakeTemporarySave(); } catch (Exception e) { logger.Error(e); if (EditorApplication.isPlaying == false && EditorApplication.isPlayingOrWillChangePlaymode) { EditorApplication.isPlaying = false; TestableEditorElements.DisplayDialog("Error while serializing the training!", e.ToString(), "Close"); } } }
/// <summary> /// Start sending <paramref name="recordedActions"/> to the <see cref="window"/> and invoke <paramref name="finishedCallback"/> when done. /// </summary> public static void StartPlayback(EditorWindow window, IList <UserAction> recordedActions, FinishedHandler finishedCallback) { foreach (EditorWindowTestPlayer testPlayer in Resources.FindObjectsOfTypeAll <EditorWindowTestPlayer>()) { testPlayer.Close(); } IsPlaying = true; EditorWindowTestPlayer player = CreateInstance <EditorWindowTestPlayer>(); player.ShowUtility(); player.Finished += finishedCallback; player.window = window; foreach (UserAction action in recordedActions) { TestableEditorElements.StartPlayback(action.PrepickedSelections); player.window.SendEvent(action.Event); player.SendEvent(EditorGUIUtility.CommandEvent("Wait")); } player.SendEvent(EditorGUIUtility.CommandEvent("Finished")); }
private void OnGUI() { try { if (Event.current.type == EventType.ExecuteCommand && Event.current.commandName == "Abort") { Abort(); return; } if (Event.current.type == EventType.ExecuteCommand && Event.current.commandName == "SaveAndTerminate") { SaveAndTerminate(); return; } Rect newPos = recordedWindow.position; minSize = newPos.size; maxSize = newPos.size; position = newPos; MethodInfo onGui = recordedWindow.GetType().GetMethod("OnGUI", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); if (Event.current.type != EventType.Used) { if (ShouldRecordEvent()) { if (userActions.Any()) { userActions.Last().PrepickedSelections = TestableEditorElements.StopRecording(); TestableEditorElements.StartRecording(); } Event toRecord = JsonConvert.DeserializeObject <Event>(JsonConvert.SerializeObject(Event.current, serializerSettings), serializerSettings); UserAction userActionToRecord = new UserAction { Event = toRecord }; if (ShouldReplaceLastUserAction()) { userActions[userActions.Count - 1] = userActionToRecord; } else { userActions.Add(userActionToRecord); } } onGui.Invoke(recordedWindow, new object[0]); } Focus(); } catch { IsRecording = false; TestableEditorElements.Panic(); if (recordedWindow != null) { recordedWindow.Close(); } Close(); throw; } }
private StepNode CreateNewStepNode(IStep step) { StepNode node = new StepNode(Graphics, CurrentChapter, step); node.GraphicalEventHandler.ContextClick += (sender, args) => { TestableEditorElements.DisplayContextMenu(new List <TestableEditorElements.MenuOption> { new TestableEditorElements.MenuItem(new GUIContent("Copy"), false, () => { CopyStep(step); }), new TestableEditorElements.MenuItem(new GUIContent("Cut"), false, () => { CutStep(step, node); }), new TestableEditorElements.MenuItem(new GUIContent("Delete"), false, () => { DeleteStepWithUndo(step, node); }) }); }; node.GraphicalEventHandler.PointerDown += (sender, args) => { UserSelectStepNode(node); }; node.RelativePositionChanged += (sender, args) => { node.Step.StepMetadata.Position = node.Position; }; node.GraphicalEventHandler.PointerUp += (sender, args) => { Graphics.CalculateBoundingBox(); }; // ReSharper disable once ImplicitlyCapturedClosure node.GraphicalEventHandler.PointerDown += (sender, args) => UserSelectStepNode(node); node.CreateTransitionButton.GraphicalEventHandler.PointerClick += (sender, args) => { ITransition transition = EntityFactory.CreateTransition(); RevertableChangesHandler.Do(new CourseCommand( () => { step.Data.Transitions.Data.Transitions.Add(transition); MarkToRefresh(); }, () => { step.Data.Transitions.Data.Transitions.Remove(transition); MarkToRefresh(); } )); }; if (CurrentChapter.ChapterMetadata.LastSelectedStep == step) { SelectStepNode(node); } SetupNode(node, position => node.Step.StepMetadata.Position = position); return(node); }
private void OnGUI() { if (course == null || focusedWindow != this) { Close(); instance.IsClosed = true; } GUI.SetNextControlName(textFieldIdentifier.ToString()); newName = EditorGUILayout.TextField(newName); newName = newName.Trim(); if (isFocusSet == false) { isFocusSet = true; EditorGUI.FocusTextInControl(textFieldIdentifier.ToString()); } if ((Event.current.keyCode == KeyCode.Return || Event.current.keyCode == KeyCode.KeypadEnter)) { if (CourseAssetUtils.CanRename(course, newName, out string error) == false) { if (string.IsNullOrEmpty(error) == false && string.IsNullOrEmpty(error) == false) { TestableEditorElements.DisplayDialog("Cannot rename the course", error, "OK"); } } else { string oldName = course.Data.Name; RevertableChangesHandler.Do(new CourseCommand( () => { if (CourseAssetUtils.CanRename(course, newName, out string errorMessage) == false) { if (string.IsNullOrEmpty(errorMessage) == false) { TestableEditorElements.DisplayDialog("Cannot rename the course", errorMessage, "OK"); } RevertableChangesHandler.FlushStack(); } else { CourseAssetManager.RenameCourse(course, newName); } }, () => { if (CourseAssetUtils.CanRename(course, newName, out string errorMessage) == false) { if (string.IsNullOrEmpty(errorMessage) == false) { TestableEditorElements.DisplayDialog("Cannot rename the course", errorMessage, "OK"); } RevertableChangesHandler.FlushStack(); } else { CourseAssetManager.RenameCourse(course, oldName); } } )); } Close(); instance.IsClosed = true; Event.current.Use(); } else if (Event.current.keyCode == KeyCode.Escape) { Close(); instance.IsClosed = true; Event.current.Use(); } }
private EntryNode CreateEntryNode(IChapter chapter) { EntryNode entryNode = new EntryNode(Graphics); entryNode.IsDragging = false; ExitJoint joint = new ExitJoint(Graphics, entryNode) { RelativePosition = new Vector2(entryNode.LocalBoundingBox.xMax, entryNode.LocalBoundingBox.center.y), }; entryNode.ExitJoints.Add(joint); entryNode.Position = chapter.ChapterMetadata.EntryNodePosition; entryNode.RelativePositionChanged += (sender, args) => { chapter.ChapterMetadata.EntryNodePosition = entryNode.Position; }; entryNode.GraphicalEventHandler.PointerUp += (sender, args) => { entryNode.IsDragging = false; Graphics.CalculateBoundingBox(); }; entryNode.GraphicalEventHandler.PointerDown += (sender, args) => { entryNode.IsDragging = true; Graphics.CalculateBoundingBox(); }; entryNode.GraphicalEventHandler.ContextClick += (sender, args) => { if (chapter.Data.FirstStep == null) { return; } TestableEditorElements.DisplayContextMenu(new List <TestableEditorElements.MenuOption> { new TestableEditorElements.MenuItem(new GUIContent("Delete transition"), false, () => { IStep firstStep = chapter.Data.FirstStep; RevertableChangesHandler.Do(new CourseCommand(() => { chapter.Data.FirstStep = null; MarkToRefresh(); }, () => { chapter.Data.FirstStep = firstStep; MarkToRefresh(); } )); }) }); }; joint.GraphicalEventHandler.PointerDrag += (sender, args) => { joint.DragDelta = args.PointerPosition - joint.Position; }; joint.GraphicalEventHandler.PointerUp += (sender, args) => { joint.DragDelta = Vector2.zero; IStep oldStep = chapter.Data.FirstStep; if (TryGetStepForTransitionDrag(args.PointerPosition, out IStep target) == false) { return; } RevertableChangesHandler.Do(new CourseCommand(() => { chapter.Data.FirstStep = target; MarkToRefresh(); }, () => { chapter.Data.FirstStep = oldStep; MarkToRefresh(); } )); joint.DragDelta = Vector2.zero; }; SetupNode(entryNode, position => chapter.ChapterMetadata.EntryNodePosition = position); return(entryNode); }
private StepNode CreateNewStepNode(IStep step) { StepNode node = new StepNode(Graphics, step); node.GraphicalEventHandler.ContextClick += (sender, args) => { TestableEditorElements.DisplayContextMenu(new List <TestableEditorElements.MenuOption> { new TestableEditorElements.MenuItem(new GUIContent("Delete step"), false, () => { IList <ITransition> incomingTransitions = chapter.Data.Steps.SelectMany(s => s.Data.Transitions.Data.Transitions).Where(transition => transition.Data.TargetStep == step).ToList(); bool wasFirstStep = step == chapter.Data.FirstStep; RevertableChangesHandler.Do(new TrainingCommand( () => { foreach (ITransition transition in incomingTransitions) { transition.Data.TargetStep = null; } DeleteStep(step); if (wasFirstStep) { chapter.Data.FirstStep = null; } }, () => { AddStep(step); if (wasFirstStep) { chapter.Data.FirstStep = step; } foreach (ITransition transition in incomingTransitions) { transition.Data.TargetStep = step; } UserSelectStepNode(node); } )); }) }); }; node.GraphicalEventHandler.PointerDown += (sender, args) => { UserSelectStepNode(node); }; node.RelativePositionChanged += (sender, args) => { node.Step.StepMetadata.Position = node.Position; }; node.GraphicalEventHandler.PointerUp += (sender, args) => { Graphics.CalculateBoundingBox(); }; // ReSharper disable once ImplicitlyCapturedClosure node.GraphicalEventHandler.PointerDown += (sender, args) => UserSelectStepNode(node); node.CreateTransitionButton.GraphicalEventHandler.PointerClick += (sender, args) => { ITransition transition = new Transition(); RevertableChangesHandler.Do(new TrainingCommand( () => { step.Data.Transitions.Data.Transitions.Add(transition); MarkToRefresh(); }, () => { step.Data.Transitions.Data.Transitions.Remove(transition); MarkToRefresh(); } )); }; if (chapter.ChapterMetadata.LastSelectedStep == step) { SelectStepNode(node); } SetupNode(node, position => node.Step.StepMetadata.Position = position); return(node); }
private void SetupTransitions(IChapter chapter, EntryNode entryNode, IDictionary <IStep, StepNode> stepNodes) { if (chapter.Data.FirstStep != null) { CreateNewTransition(entryNode.ExitJoints.First(), stepNodes[chapter.Data.FirstStep].EntryJoints.First()); } foreach (IStep step in stepNodes.Keys) { foreach (ITransition transition in step.Data.Transitions.Data.Transitions) { ExitJoint joint = stepNodes[step].AddExitJoint(); if (transition.Data.TargetStep != null) { StepNode target = stepNodes[transition.Data.TargetStep]; CreateNewTransition(joint, target.EntryJoints.First()); } IStep closuredStep = step; ITransition closuredTransition = transition; int transitionIndex = step.Data.Transitions.Data.Transitions.IndexOf(closuredTransition); joint.GraphicalEventHandler.PointerDrag += (sender, args) => { joint.DragDelta = args.PointerPosition - joint.Position; }; joint.GraphicalEventHandler.PointerUp += (sender, args) => { joint.DragDelta = Vector2.zero; IStep oldStep = closuredTransition.Data.TargetStep; if (TryGetStepForTransitionDrag(args.PointerPosition, out IStep targetStep) == false) { return; } RevertableChangesHandler.Do(new CourseCommand(() => { closuredTransition.Data.TargetStep = targetStep; SelectStepNode(stepNodes[closuredStep]); MarkToRefresh(); }, () => { closuredTransition.Data.TargetStep = oldStep; SelectStepNode(stepNodes[closuredStep]); MarkToRefresh(); } )); }; joint.GraphicalEventHandler.ContextClick += (sender, args) => { TestableEditorElements.DisplayContextMenu(new List <TestableEditorElements.MenuOption> { new TestableEditorElements.MenuItem(new GUIContent("Delete transition"), false, () => { bool isLast = closuredStep.Data.Transitions.Data.Transitions.Count == 1; RevertableChangesHandler.Do(new CourseCommand(() => { closuredStep.Data.Transitions.Data.Transitions.Remove(closuredTransition); if (isLast) { closuredStep.Data.Transitions.Data.Transitions.Add(EntityFactory.CreateTransition()); } MarkToRefresh(); }, () => { if (isLast) { closuredStep.Data.Transitions.Data.Transitions.RemoveAt(0); } closuredStep.Data.Transitions.Data.Transitions.Insert(transitionIndex, closuredTransition); MarkToRefresh(); } )); }) }); }; } } }
private void SetupTransitions(IChapter chapter, EntryNode entryNode, IDictionary <IStep, StepNode> stepNodes) { if (chapter.Data.FirstStep != null) { CreateNewTransition(entryNode.ExitJoints.First(), stepNodes[chapter.Data.FirstStep].EntryJoints.First()); } foreach (IStep step in stepNodes.Keys) { foreach (ITransition transition in step.Data.Transitions.Data.Transitions) { ExitJoint joint = stepNodes[step].AddExitJoint(); if (transition.Data.TargetStep != null) { StepNode target = stepNodes[transition.Data.TargetStep]; CreateNewTransition(joint, target.EntryJoints.First()); } IStep closuredStep = step; ITransition closuredTransition = transition; int transitionIndex = step.Data.Transitions.Data.Transitions.IndexOf(closuredTransition); joint.GraphicalEventHandler.PointerDrag += (sender, args) => { joint.DragDelta = args.PointerPosition - joint.Position; }; joint.GraphicalEventHandler.PointerUp += (sender, args) => { GraphicalElement elementUnderCursor = Graphics.GetGraphicalElementWithHandlerAtPoint(args.PointerPosition).FirstOrDefault(); EntryJoint endJoint = elementUnderCursor as EntryJoint; if (endJoint == null) { joint.DragDelta = Vector2.zero; if (elementUnderCursor != null) { return; } } StepNode endJointStepNode = endJoint == null ? null : endJoint.Parent as StepNode; IStep targetStep = null; IStep oldStep = closuredTransition.Data.TargetStep; if (endJointStepNode != null) { targetStep = endJointStepNode.Step; } RevertableChangesHandler.Do(new TrainingCommand(() => { closuredTransition.Data.TargetStep = targetStep; SelectStepNode(stepNodes[closuredStep]); MarkToRefresh(); }, () => { closuredTransition.Data.TargetStep = oldStep; SelectStepNode(stepNodes[closuredStep]); MarkToRefresh(); } )); joint.DragDelta = Vector2.zero; }; joint.GraphicalEventHandler.ContextClick += (sender, args) => { TestableEditorElements.DisplayContextMenu(new List <TestableEditorElements.MenuOption> { new TestableEditorElements.MenuItem(new GUIContent("Delete transition"), false, () => { RevertableChangesHandler.Do(new TrainingCommand(() => { closuredStep.Data.Transitions.Data.Transitions.Remove(closuredTransition); MarkToRefresh(); }, () => { closuredStep.Data.Transitions.Data.Transitions.Insert(transitionIndex, closuredTransition); MarkToRefresh(); } )); }) }); }; } } }
private EntryNode CreateEntryNode(IChapter chapter) { EntryNode entryNode = new EntryNode(Graphics); ExitJoint joint = new ExitJoint(Graphics, entryNode) { RelativePosition = new Vector2(entryNode.LocalBoundingBox.xMax, entryNode.LocalBoundingBox.center.y), }; entryNode.ExitJoints.Add(joint); entryNode.Position = chapter.ChapterMetadata.EntryNodePosition; entryNode.RelativePositionChanged += (sender, args) => { chapter.ChapterMetadata.EntryNodePosition = entryNode.Position; }; entryNode.GraphicalEventHandler.PointerUp += (sender, args) => { Graphics.CalculateBoundingBox(); }; entryNode.GraphicalEventHandler.ContextClick += (sender, args) => { if (chapter.Data.FirstStep == null) { return; } TestableEditorElements.DisplayContextMenu(new List <TestableEditorElements.MenuOption> { new TestableEditorElements.MenuItem(new GUIContent("Delete transition"), false, () => { IStep firstStep = chapter.Data.FirstStep; RevertableChangesHandler.Do(new TrainingCommand(() => { chapter.Data.FirstStep = null; MarkToRefresh(); }, () => { chapter.Data.FirstStep = firstStep; MarkToRefresh(); } )); }) }); }; joint.GraphicalEventHandler.PointerDrag += (sender, args) => { joint.DragDelta = args.PointerPosition - joint.Position; }; joint.GraphicalEventHandler.PointerUp += (sender, args) => { EntryJoint endJoint = Graphics.GetGraphicalElementWithHandlerAtPoint(args.PointerPosition).FirstOrDefault() as EntryJoint; if (endJoint == null) { joint.DragDelta = Vector2.zero; return; } StepNode endJointStepNode = endJoint.Parent as StepNode; IStep targetStep = null; IStep oldStep = chapter.Data.FirstStep; if (endJointStepNode != null) { targetStep = endJointStepNode.Step; } RevertableChangesHandler.Do(new TrainingCommand(() => { chapter.Data.FirstStep = targetStep; MarkToRefresh(); }, () => { chapter.Data.FirstStep = oldStep; MarkToRefresh(); } )); joint.DragDelta = Vector2.zero; }; SetupNode(entryNode, position => chapter.ChapterMetadata.EntryNodePosition = position); return(entryNode); }