Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
using System;
using System.Collections.Generic;
using System.Linq;
using Godot;
using GodotExt;
using JetBrains.Annotations;
using OpenScadGraphEditor.History;
using OpenScadGraphEditor.Library;
using OpenScadGraphEditor.Library.External;
using OpenScadGraphEditor.Library.IO;
using OpenScadGraphEditor.Nodes;
using OpenScadGraphEditor.Nodes.Reroute;
using OpenScadGraphEditor.Refactorings;
using OpenScadGraphEditor.Utils;
using OpenScadGraphEditor.Widgets;
using OpenScadGraphEditor.Widgets.AddDialog;
using OpenScadGraphEditor.Widgets.CommentEditingDialog;
using OpenScadGraphEditor.Widgets.DocumentationDialog;
using OpenScadGraphEditor.Widgets.HelpDialog;
using OpenScadGraphEditor.Widgets.IconButton;
using OpenScadGraphEditor.Widgets.ImportDialog;
using OpenScadGraphEditor.Widgets.InvokableRefactorDialog;
using OpenScadGraphEditor.Widgets.LogConsole;
using OpenScadGraphEditor.Widgets.NodeColorDialog;
using OpenScadGraphEditor.Widgets.ProjectTree;
using OpenScadGraphEditor.Widgets.SettingsDialog;
using OpenScadGraphEditor.Widgets.StylusDebugDialog;
using OpenScadGraphEditor.Widgets.UsageDialog;
using OpenScadGraphEditor.Widgets.VariableRefactorDialog;
using Serilog;
namespace OpenScadGraphEditor
{
[UsedImplicitly]
public class GraphEditor : Control, ICanPerformRefactorings
{
// TODO: this class gets _really_ big.
private AddDialog _addDialog;
private QuickActionsPopup _quickActionsPopup;
private Control _editingInterface;
private TextEdit _textEdit;
private FileDialog _fileDialog;
private ProjectTree _projectTree;
private ImportDialog _importDialog;
private IconButton _undoButton;
private IconButton _redoButton;
private string _currentFile;
private bool _dirty;
private Button _forceSaveButton;
private Label _fileNameLabel;
private TabContainer _tabContainer;
private ScadProject _currentProject;
private HistoryStack _currentHistoryStack;
private BuiltInLibrary _rootResolver;
private InvokableRefactorDialog _invokableRefactorDialog;
private VariableRefactorDialog _variableRefactorDialog;
private NodeColorDialog _nodeColorDialog;
private CommentEditingDialog _commentEditingDialog;
private StylusDebugDialog _stylusDebugDialog;
private SettingsDialog _settingsDialog;
private HelpDialog _helpDialog;
private UsageDialog _usageDialog;
private DocumentationDialog _documentationDialog;
private readonly List<IAddDialogEntry> _addDialogEntries = new List<IAddDialogEntry>();
private readonly List<IAddDialogEntryFactory> _addDialogEntryFactories =
typeof(IAddDialogEntryFactory)
.GetImplementors()
.CreateInstances<IAddDialogEntryFactory>()
.ToList();
private ScadGraph _copyBuffer;
private Button _consoleButton;
private Button _usageButton;
private LogConsole _logConsole;
private bool _codeChanged;
private bool _refactoringInProgress;
private readonly Dictionary<string, ScadGraphEdit> _openEditors = new Dictionary<string, ScadGraphEdit>();
private readonly Configuration _configuration = new Configuration();
private IconButton _openOpenScadButton;
public override void _Ready()
{
_logConsole = this.WithName<LogConsole>("LogConsole");
var logFile = OS.GetUserDataDir() + "/log.txt";
OS.SetWindowTitle($"OpenSCAD Graph Editor ({AppVersion.Version})");
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console()
.WriteTo.File(logFile, rollingInterval: RollingInterval.Day, retainedFileCountLimit: 5)
.WriteTo.Sink(_logConsole)
.CreateLogger();
GD.Print("OpenSCAD log file is: " + logFile);
Log.Information("Initializing OpenScad Graph Editor");
OS.LowProcessorUsageMode = true;
_logConsole.OpenLogFileRequested += () => OS.ShellOpen(logFile);
_configuration.Load();
// scale all themes to editor scale
var percent = _configuration.GetEditorScalePercent();
foreach (var theme in Resources.AllThemes)
{
theme.Scale(percent);
}
_rootResolver = new BuiltInLibrary();
_tabContainer = this.WithName<TabContainer>("TabContainer");
_tabContainer
.Connect("tab_changed")
.To(this, nameof(OnTabChanged));
_addDialog = this.WithName<AddDialog>("AddDialog");
_quickActionsPopup = this.WithName<QuickActionsPopup>("QuickActionsPopup");
_importDialog = this.WithName<ImportDialog>("ImportDialog");
_importDialog.OnNewImportRequested += OnNewImportRequested;
_usageDialog = this.WithName<UsageDialog>("UsageDialog");
_usageDialog.NodeHighlightRequested += OnNodeHighlightRequested;
_settingsDialog = this.WithName<SettingsDialog>("SettingsDialog");
_stylusDebugDialog = this.WithName<StylusDebugDialog>("StylusDebugDialog");
_consoleButton = this.WithName<Button>("ConsoleButton");
_consoleButton
.Connect("pressed")
.To(this, nameof(OnConsoleButtonToggled));
_usageButton = this.WithName<Button>("UsageButton");
_usageButton
.Connect("pressed")
.To(this, nameof(OnUsageButtonToggled));
_helpDialog = this.WithName<HelpDialog>("HelpDialog");
_commentEditingDialog = this.WithName<CommentEditingDialog>("CommentEditingDialog");
_commentEditingDialog.CommentAndTitleChanged += OnCommentAndTitleChanged;
_nodeColorDialog = this.WithName<NodeColorDialog>("NodeColorDialog");
_nodeColorDialog.ColorSelected +=
(graph, node, color) => PerformRefactorings("Change node color",
new ToggleModifierRefactoring(graph, node, ScadNodeModifier.Color, true, color));
_nodeColorDialog.ColorCleared +=
(graph, node) => PerformRefactorings("Clear node color",
new ToggleModifierRefactoring(graph, node, ScadNodeModifier.Color, false));
_invokableRefactorDialog = this.WithName<InvokableRefactorDialog>("InvokableRefactorDialog");
_invokableRefactorDialog.InvokableModificationRequested +=
(description, refactorings) => PerformRefactorings($"Change {description.Name}", refactorings);
_invokableRefactorDialog.InvokableCreationRequested +=
// create the invokable, then open it's graph in a new tab.
(description, refactorings) => PerformRefactorings($"Create {description.Name}", refactorings,
() => Open(_currentProject.FindDefiningGraph(description)));
_documentationDialog = this.WithName<DocumentationDialog>("DocumentationDialog");
_documentationDialog.RefactoringRequested += (refactoring) => PerformRefactorings("Edit documentation", refactoring);
_variableRefactorDialog = this.WithName<VariableRefactorDialog>("VariableRefactorDialog");
_variableRefactorDialog.RefactoringsRequested +=
(refactorings) => PerformRefactorings("Rename variable", refactorings);
_editingInterface = this.WithName<Control>("EditingInterface");
_textEdit = this.WithName<TextEdit>("TextEdit");
_fileDialog = this.WithName<FileDialog>("FileDialog");
_fileNameLabel = this.WithName<Label>("FileNameLabel");
_forceSaveButton = this.WithName<Button>("ForceSaveButton");
_forceSaveButton.Connect("pressed")
.To(this, nameof(SaveChanges));
_projectTree = this.WithName<ProjectTree>("ProjectTree");
_projectTree.ItemActivated += Open;
_projectTree.ItemContextMenuRequested += OnItemContextMenuRequested;
_undoButton = this.WithName<IconButton>("UndoButton");
_undoButton.ButtonPressed += Undo;
_redoButton = this.WithName<IconButton>("RedoButton");
_redoButton.ButtonPressed += Redo;
this.WithName<IconButton>("AddExternalReferenceButton")
.ButtonPressed += OnImportScadFile;
this.WithName<Button>("NewButton")
.Connect("pressed")
.To(this, nameof(OnNewButtonPressed));
this.WithName<Button>("PreviewButton")
.Connect("toggled")
.To(this, nameof(OnPreviewToggled));
this.WithName<Button>("SettingsButton")
.Connect("pressed")
.To(this, nameof(OnSettingsButtonPressed));
_openOpenScadButton = this.WithName<IconButton>("OpenOpenScadButton");
_openOpenScadButton
.ButtonPressed += OnOpenOpenScadButtonPressed;
var openButton = this.WithName<OpenButton>("OpenButton");
openButton.OpenFileRequested += OnOpenFile;
openButton.OpenFileDialogRequested += OnOpenFileDialogRequested;
openButton.Init(_configuration);
this.WithName<Button>("SaveAsButton")
.Connect("pressed")
.To(this, nameof(OnSaveAsPressed));
this.WithName<IconButton>("AddModuleButton")
.ButtonPressed += OnAddModulePressed;
this.WithName<IconButton>("AddFunctionButton")
.ButtonPressed += OnAddFunctionPressed;
this.WithName<IconButton>("AddVariableButton")
.ButtonPressed += OnAddVariablePressed;
MarkDirty(true);
this.WithName<Timer>("Timer")
.Connect("timeout")
.To(this, nameof(SaveChanges));
_copyBuffer = new ScadGraph();
OnNewButtonPressed();
NotificationService.ShowNotification("Welcome to OpenScad Graph Editor");
}
private void OnOpenOpenScadButtonPressed()
{
var path = _configuration.GetOpenScadPath();
if (path.Empty())
{
NotificationService.ShowNotification("OpenSCAD path not set. Please set it in the settings dialog.");
return;
}
NotificationService.ShowNotification("Opening OpenSCAD. Please wait a few seconds for it to open.");
OS.Execute(path, new[] {"--viewall", _currentFile + ".scad"}, blocking: false);
}
private void OnSettingsButtonPressed()
{
_settingsDialog.Open(_configuration);
}
private void OnConsoleButtonToggled()
{
if (!_logConsole.Visible)
{
_logConsole.Open();
_usageDialog.Hide();
}
else
{
_logConsole.Hide();
}
}
private void OnUsageButtonToggled()
{
if (!_usageDialog.Visible)
{
_usageDialog.Show();
_logConsole.Hide();
}
else
{
_usageDialog.Hide();
}
}
private void OnTabChanged(int index)
{
var editor = _tabContainer.GetChild<ScadGraphEdit>(index);
if (editor.Graph != null)
{
// re-paint
editor.Render(editor.Graph);
}
}
private void OnNodeHighlightRequested(UsagePointInformation information)
{
var graph = _currentProject.AllDeclaredInvokables.FirstOrDefault(it =>
it.Description.Id == information.GraphId);
if (graph != null)
{
var node = graph.GetAllNodes().FirstOrDefault(it => it.Id == information.NodeId);
if (node != null)
{
var edit = Open(graph);
edit.FocusNode(node);
return;
}
}
NotificationService.ShowNotification("This usage no longer exists");
}
private void OnCommentAndTitleChanged(RequestContext context, string title, string text)
{
var refactorings = new List<Refactoring>();
var stepName = "Change comment";
if (title.Empty() && text.Empty())
{
stepName = "Remove comment";
}
if (!context.TryGetNode(out var node))
{
if (title.Empty() && text.Empty())
{
// nothing to do
return;
}
stepName = "Create comment";
// create a new node
node = NodeFactory.Build<Comment>();
node.Offset = context.Position;
refactorings.Add(new AddNodeRefactoring(context.Source, node));
}
// update the comment
refactorings.Add(new ChangeCommentRefactoring(context.Source, node, title, text));
PerformRefactorings(stepName, refactorings);
}
private void OnNewImportRequested(string path, IncludeMode includeMode)
{
OnRefactoringRequested($"Add reference to {path}", new AddExternalReferenceRefactoring(path, includeMode));
}
private void OnItemContextMenuRequested(ProjectTreeEntry entry, Vector2 mousePosition)
{
var actions = new List<QuickAction>();
var deleteTitle = $"Delete {entry.Title}";
if (entry is ScadInvokableTreeEntry invokableTreeEntry)
{
var invokableDescription = invokableTreeEntry.Description;
if (_currentProject.IsDefinedInThisProject(invokableDescription))
{
if (!(invokableDescription is MainModuleDescription))
{
actions.Add(new QuickAction($"Refactor {invokableDescription.Name}",
() => _invokableRefactorDialog.Open(invokableDescription, _currentProject)
)
);
actions.Add(new QuickAction($"Edit documentation of {invokableDescription.Name}",
() => _documentationDialog.Open(invokableDescription)
)
);
actions.Add(new QuickAction(deleteTitle,
() => OnRefactoringRequested(deleteTitle,
new DeleteInvokableRefactoring(invokableDescription))));
}
}
if (!(invokableDescription is MainModuleDescription) && !(invokableDescription.IsBuiltin))
{
actions.Add(new QuickAction($"Find usages of {invokableDescription.Name}",
() => FindAndShowUsages(invokableDescription)));
}
}
if (entry is ScadVariableTreeEntry scadVariableListEntry)
{
if (_currentProject.IsDefinedInThisProject(scadVariableListEntry.Description))
{
actions.Add(new QuickAction($"Rename {scadVariableListEntry.Description.Name}",
() => _variableRefactorDialog.Open(scadVariableListEntry.Description, _currentProject)
)
);
actions.Add(new QuickAction(deleteTitle,
() => OnRefactoringRequested(
deleteTitle, new DeleteVariableRefactoring(scadVariableListEntry.Description))));
}
actions.Add(new QuickAction($"Find usages of {scadVariableListEntry.Description.Name}",
() => FindAndShowUsages(scadVariableListEntry.Description)));
}
if (entry is ExternalReferenceTreeEntry externalReferenceTreeEntry &&
!externalReferenceTreeEntry.Description.IsTransitive)
{
var removeReferenceTitle = $"Remove reference to {entry.Title}";
actions.Add(new QuickAction(removeReferenceTitle,
() => OnRefactoringRequested(
removeReferenceTitle,
new DeleteExternalReferenceRefactoring(externalReferenceTreeEntry.Description))));
var refreshReferenceTitle = $"Refresh reference to {entry.Title}";
actions.Add(new QuickAction(refreshReferenceTitle,
() => OnRefactoringRequested(
refreshReferenceTitle,
new RefreshExternalReferenceRefactoring(externalReferenceTreeEntry.Description))));
}
_quickActionsPopup.Open(mousePosition, actions);
}
private void Open(ProjectTreeEntry entry)
{
if (entry is ScadInvokableTreeEntry invokableTreeEntry
&& _currentProject.IsDefinedInThisProject(invokableTreeEntry.Description))
{
Open(_currentProject.FindDefiningGraph(invokableTreeEntry.Description));
}
}
private void Clear()
{
_currentProject?.Discard();
_currentHistoryStack = null;
_currentFile = null;
_fileNameLabel.Text = "<not yet saved to a file>";
_tabContainer.GetChildNodes<ScadGraphEdit>().ForAll(it => it.RemoveAndFree());
_openEditors.Clear();
_projectTree.Clear();
_openOpenScadButton.Disabled = true;
_openOpenScadButton.HintTooltip = "(disabled) save project to enable";
}
private void RefreshControls()
{
_undoButton.Disabled = !_currentHistoryStack.CanUndo(out var undoDescription);
_undoButton.HintTooltip = $"Undo : {undoDescription}";
_redoButton.Disabled = !_currentHistoryStack.CanRedo(out var redoDescription);
_redoButton.HintTooltip = $"Redo : {redoDescription}";
_projectTree.Setup(new List<ProjectTreeEntry>() {new RootProjectTreeEntry(_currentProject)});
// Re-Build the list of entries for the add dialog.
_addDialogEntries.Clear();
_addDialogEntries.AddRange(
_addDialogEntryFactories.SelectMany(it => it.BuildEntries(_currentProject, this))
);
// add an entry for creating a comment
// TODO: fix this
/* _addDialogEntries.Add(
new SingleNodeBasedEntry(
Resources.ScadBuiltinIcon,
NodeFactory.Build<Comment>,
AddComment
)
); */
}
private void AddComment(RequestContext context, SingleNodeBasedEntry entry)
{
_commentEditingDialog.Open(RequestContext.ForPosition(context.Source, context.Position));
}
private void Undo()
{
GdAssert.That(_currentHistoryStack.CanUndo(out _), "Cannot undo");
var editorState = _currentHistoryStack.Undo(_currentProject);
RestoreEditorState(editorState);
MarkDirty(true);
}
private void Redo()
{
GdAssert.That(_currentHistoryStack.CanRedo(out _), "Cannot redo");
var editorState = _currentHistoryStack.Redo(_currentProject);
RestoreEditorState(editorState);
MarkDirty(true);
}
private void OnImportScadFile()
{
_importDialog.OpenForNewImport(_currentFile);
}
private void AddNode(RequestContext context, ScadNode node)
{
node.Offset = context.Position;
context.TryGetNodeAndPort(out var otherNode, out var otherPort);
PerformRefactorings("Add node",
new AddNodeRefactoring(context.Source, node, otherNode, otherPort));
}
private ScadGraphEdit Open(ScadGraph toOpen)
{
// check if it is already open
if (_openEditors.TryGetValue(toOpen.Description.Id, out var existingEditor))
{
_tabContainer.CurrentTab = existingEditor.GetIndex();
existingEditor.Render(toOpen);
return existingEditor;
}
// if not, open a new tab
var editor = Prefabs.New<ScadGraphEdit>();
editor.RefactoringsRequested += PerformRefactorings;
editor.NodePopupRequested += OnNodeContextMenuRequested;
editor.ItemDataDropped += OnItemDataDropped;
editor.AddDialogRequested += OnAddDialogRequested;
editor.CopyRequested += OnCopyRequested;
editor.PasteRequested += OnPasteRequested;
editor.CutRequested += OnCutRequested;
editor.EditCommentRequested += OnEditCommentRequested;
editor.HelpRequested += OnHelpRequested;
editor.Name = toOpen.Description.Id;
editor.MoveToNewParent(_tabContainer);
editor.Render(toOpen);
_openEditors[toOpen.Description.Id] = editor;
_tabContainer.CurrentTab = _tabContainer.GetChildCount() - 1;
_tabContainer.SetTabTitle(_tabContainer.CurrentTab, editor.Graph.Description.NodeNameOrFallback);
editor.FocusEntryPoint();
return editor;
}
private void OnHelpRequested(RequestContext obj)
{
if (obj.TryGetNode(out var node))
{
_helpDialog.Open(obj.Source, node);
}
}
private void Close(ScadGraphEdit editor)
{
_openEditors.Remove(editor.Graph.Description.Id);
editor.RemoveAndFree();
}
private void OnCutRequested(ScadGraphEdit source, List<ScadNode> selection)
{
// first copy the nodes
OnCopyRequested(source, selection);
// then run refactorings to delete them
var deleteRefactorings = selection
// only delete nodes which can be deleted
.Where(it => !(it is ICannotBeDeleted))
// will implicitly also delete the connections.
.Select(it => new DeleteNodeRefactoring(source.Graph, it))
.ToList();
PerformRefactorings("Cut nodes", deleteRefactorings);
}
/// <summary>
/// Called when the user wants to edit the comment of a node.
/// </summary>
private void OnEditCommentRequested(RequestContext requestContext)
{
var hasNode = requestContext.TryGetNode(out var node);
GdAssert.That(hasNode, "No node found");
if (hasNode && node is Comment comment)
{
_commentEditingDialog.Open(requestContext, comment.CommentTitle, comment.CommentDescription);
}
else
{
var hasComment = node.TryGetComment(out var existingComment);
_commentEditingDialog.Open(requestContext, hasComment ? existingComment : "", showDescription: false );
}
}
/// <summary>
/// Copies the given nodes from the source graph into a copy buffer.
/// </summary>
private ScadGraph MakeCopyBuffer(ScadGraph source, IEnumerable<ScadNode> selection)
{
var result = new ScadGraph();
// remove all nodes which cannot be deleted (which are usually nodes that are built in so they cannot be copied either).
// also make it a HashSet so we don't have duplicates and have quicker lookups
var sanitizedSet = selection
.Where(it => !(it is ICannotBeDeleted))
.ToHashSet();
var idMapping = new Dictionary<string, string>();
// make copies of all the nodes and put them into the copy buffer
foreach (var node in sanitizedSet)
{
var copy = NodeFactory.Duplicate(node, _currentProject);
// make note of the id mapping, because we need this to resolve connections
// and bound nodes later.
idMapping[node.Id] = copy.Id;
result.AddNode(copy);
}
// correct the id mappings of any bound nodes
foreach (var node in result.GetAllNodes().Where(it => it is IAmBoundToOtherNode))
{
var partnerId = ((IAmBoundToOtherNode) node).OtherNodeId;
// find out which id the partner has in the copy
var partnerCopyId = idMapping[partnerId];
// and set the partner id to the copy id
((IAmBoundToOtherNode) node).OtherNodeId = partnerCopyId;
}
// copy all connections which are between the selected nodes
foreach (var connection in source.GetAllConnections())
{
if (sanitizedSet.Contains(connection.From) && sanitizedSet.Contains(connection.To))
{
result.AddConnection(idMapping[connection.From.Id], connection.FromPort,
idMapping[connection.To.Id], connection.ToPort);
}
}
return result;
}
private void OnCopyRequested(ScadGraphEdit source, List<ScadNode> selection)
{
// when we make a copy we need to take special care about bound nodes
// each bound node needs to have its partner within the copy even if it was not originally selected.
// so we look up all bound nodes in the selection and if its partner is not in the
// selection we silently add it.
var correctedSelection = new List<ScadNode>(selection);
var boundNodes = selection.Where(it => it is IAmBoundToOtherNode).ToList();
foreach(var boundNode in boundNodes)
{
var boundTo = (IAmBoundToOtherNode)boundNode;
var partner = source.Graph.ById(boundTo.OtherNodeId);
if (!selection.Contains(partner))
{
correctedSelection.Add(partner);
}
}
// now we can be sure that we have no bound nodes without their partner in the corrected selection.
_copyBuffer = MakeCopyBuffer(source.Graph, correctedSelection);
}
private void OnPasteRequested(ScadGraphEdit target, Vector2 position)
{
// now make another copy from the copy buffer and paste that. The reason we do this is because
// nodes need unique Ids so for each pasting we need to make a new copy.
var copy = MakeCopyBuffer(_copyBuffer, _copyBuffer.GetAllNodes());
// now the clipboard may contain nodes that are not allowed in the given target graph. So we need
// to filter these out here and also delete all connections to these nodes.
var disallowedNodes = copy.GetAllNodes()
.Where(it => !target.Graph.Description.CanUse(it))
.ToList();
// delete all connections to the disallowed nodes
copy.GetAllConnections()
.Where(it => disallowedNodes.Any(it.InvolvesNode))
.ToList() // avoid concurrent modification
.ForAll(it => copy.RemoveConnection(it));
// and the nodes themselves
disallowedNodes.ForAll(it => copy.RemoveNode(it));
var scadNodes = copy.GetAllNodes().ToList();
if (scadNodes.Count == 0)
{
return; // nothing to paste
}
// we now need to normalize the position of the nodes so they are pasted in the correct position
// we do this by finding the bounding box of the nodes and then offsetting them by the difference between the position
// of the bounding box and the position of the node that is closes to top left
// we start with a rectangle that is a point that simply has the position of the first node
var boundingBox = new Rect2(scadNodes[0].Offset, Vector2.Zero);
// now expand this rectangle so it contains all the points of all the offsets of the nodes
boundingBox = scadNodes.Aggregate(boundingBox, (current, node) => current.Expand(node.Offset));
// now we calculate the offset of the bounding box position and the desired position
var offset = position - boundingBox.Position;
// and offset every node by this
scadNodes.ForAll(it => it.Offset += offset);
// now run the refactorings to add the given nodes and connections
var refactorings = new List<Refactoring>();
foreach (var node in scadNodes)
{
refactorings.Add(new AddNodeRefactoring(target.Graph, node));
}
foreach (var connection in copy.GetAllConnections())
{
refactorings.Add(
new AddConnectionRefactoring(
new ScadConnection(target.Graph, connection.From, connection.FromPort, connection.To,
connection.ToPort
)
)
);
}
PerformRefactorings("Paste nodes", refactorings, () => target.SelectNodes(scadNodes));
}
private void OnAddFunctionPressed()
{
_invokableRefactorDialog.OpenForNewFunction(_currentProject);
}
private void OnAddVariablePressed()
{
_variableRefactorDialog.OpenForNewVariable(_currentProject);
}
private void OnAddModulePressed()
{
_invokableRefactorDialog.OpenForNewModule(_currentProject);
}
private void OnRefactoringRequested(string humanReadableDescription, Refactoring refactoring)
{
PerformRefactorings(humanReadableDescription, refactoring);
}
public void PerformRefactorings(string description, params Refactoring[] refactorings)
{
PerformRefactorings(description, (IEnumerable<Refactoring>) refactorings);
}
public void PerformRefactorings(string description, IEnumerable<Refactoring> refactorings,
params Action[] after)
{
try
{
GdAssert.That(!_refactoringInProgress,
"Cannot run a refactoring while a refactoring is running. Probably some UI throws events when internal state is updated.");
_refactoringInProgress = true;
Log.Debug(">> Refactorings start");
var top = _currentHistoryStack.Peek();
var currentEditorState = GetEditorState();
// the editor state could be different from how it was when we finished the previous refactoring (e.g tab changed or
// the editor was moved). we therefore patch the new editor state onto the top of the stack.
// so we can return to that state once we undo this refactoring. This way we fix a problem that
// when you open a new tab, and move a node there, then undo that the new tab is closed.
// it is important to patch this _before_ we do the refactoring otherwise we may refer to
// data that is no longer there after the refactoring.
top.PatchEditorState(currentEditorState);
var context = new RefactoringContext(_currentProject);
context.PerformRefactorings(refactorings);
// close all tabs referring to graphs which are no longer in the project
_tabContainer.GetChildNodes<ScadGraphEdit>()
.Where(it => !_currentProject.IsDefinedInThisProject(it.Graph.Description))
.ToList()
.ForAll(Close);
if (_tabContainer.CurrentTab >= _tabContainer.GetTabCount())
{
// open the left most tab
_tabContainer.CurrentTab = 0;
}
for (var i = 0; i < _tabContainer.GetTabCount();i++)
{
var graphEdit = (ScadGraphEdit) _tabContainer.GetTabControl(i);
// update the graph renderings.
graphEdit.Render(graphEdit.Graph);
// and update the tab title as it might have changed.
_tabContainer.SetTabTitle(i, graphEdit.Graph.Description.NodeNameOrFallback);
}
// important, the snapshot must be made _after_ the changes.
_currentHistoryStack.AddSnapshot(description, _currentProject, GetEditorState());
RefreshControls();
MarkDirty(true);
_refactoringInProgress = false;
Log.Debug("<< Refactorings end");
after.ForAll(it => it());
}
catch (Exception e)
{
NotificationService.ShowError("Unexpected exception occurred. Running undo to restore a known working state. See console for details.");
Log.Error(e, "Unexpected exception");
Undo();
}
}
private void OnNewButtonPressed()
{
Clear();
_currentProject = new ScadProject(_rootResolver);
Open(_currentProject.MainModule);
// important, this needs to be done after the main module is opened, so we get correct editor state
_currentHistoryStack = new HistoryStack(_currentProject, GetEditorState());
RefreshControls();
}
private EditorState GetEditorState()
{
// create a list of currently open tabs
// order is important here, so we get the child order from the _tabContainer rather than using
// _openEditors
var tabs = _tabContainer.GetChildNodes<ScadGraphEdit>().Select(it => new EditorOpenTab(it.Graph.Description.Id, it.ScrollOffset)).ToList();
return new EditorState(tabs, _tabContainer.CurrentTab);
}
private void RestoreEditorState(EditorState editorState)
{
// close all open tabs
foreach (var tab in _openEditors.Values.ToList())
{
Close(tab);
}
// open all tabs that were open back then
foreach (var openTab in editorState.OpenTabs)
{
var invokableId = openTab.InvokableId;
var invokable = _currentProject.AllDeclaredInvokables.First(it => it.Description.Id == invokableId);
var editor = Open(invokable);
editor.ScrollOffset = openTab.ScrollOffset;
}
// and finally select the tab that was selected
_tabContainer.CurrentTab = editorState.CurrentTabIndex;
RefreshControls();
}
private void OnAddDialogRequested(RequestContext context)
{
// when shift is pressed this means we want to have a reroute node.
if (Input.IsKeyPressed((int) KeyList.Shift))
{
var rerouteNode = NodeFactory.Build<RerouteNode>();
// when also CTRL is down, we make the reroute node a wireless node
if (Input.IsKeyPressed((int) KeyList.Control))
{
rerouteNode.IsWireless = true;
}
AddNode(context, rerouteNode);
return;
}
// otherwise let the user choose a node.
_addDialog.Open(_addDialogEntries, context);
}
private void OnItemDataDropped(ScadGraphEdit graph, ProjectTreeEntry entry, Vector2 mousePosition,
Vector2 virtualPosition)
{
// did we drag an invokable from the list to the graph?
if (entry is ScadInvokableTreeEntry invokableListEntry)
{
// then add a new node calling the invokable.
var description = invokableListEntry.Description;
ScadNode node;
if (description is FunctionDescription functionDescription)
{
node = NodeFactory.Build<FunctionInvocation>(functionDescription);
}
else if (description is ModuleDescription moduleDescription)
{
node = NodeFactory.Build<ModuleInvocation>(moduleDescription);
}
else
{
throw new InvalidOperationException("Unknown invokable type.");
}
// ensure we don't drag in stuff that isn't usable by the graph in question.
if (!graph.Graph.Description.CanUse(node))
{
return;
}
node.Offset = virtualPosition;
OnRefactoringRequested("Add node", new AddNodeRefactoring(graph.Graph, node));
}
// did we drag a variable from the list to the graph?
if (entry is ScadVariableTreeEntry variableListEntry)
{
// in case of a variable we can either _get_ or _set_ the variable
// so we will need to open a popup menu to let the user choose.
var getNode = NodeFactory.Build<GetVariable>(variableListEntry.Description);
getNode.Offset = virtualPosition;
var setNode = NodeFactory.Build<SetVariable>(variableListEntry.Description);
setNode.Offset = virtualPosition;
var actions = new List<QuickAction>
{
new QuickAction($"Get {variableListEntry.Description.Name}",
() => OnRefactoringRequested("Add node", new AddNodeRefactoring(graph.Graph, getNode))),
new QuickAction($"Set {variableListEntry.Description.Name}",
() => OnRefactoringRequested("Add node", new AddNodeRefactoring(graph.Graph, setNode)))
};
_quickActionsPopup.Open(mousePosition, actions);
}
}
private void OnNodeContextMenuRequested(RequestContext requestContext)
{
var graph = requestContext.Source;
var position = requestContext.Position;
requestContext.TryGetNode(out var node);
// build a list of quick actions that include all refactorings that would apply to the selected node
var actions = UserSelectableNodeRefactoring
.GetApplicable(graph, node)
.OrderBy(it => it.Order)
.GroupBy(it => it.Group)
.Select(it =>
{
if (it.Count() > 1)
{
// group into submenu
return new QuickAction(it.Key, it.Select(refactoring =>
new QuickAction(refactoring.Title,
() => OnRefactoringRequested(refactoring.Title, refactoring))).ToList());
}
// just return the item
var refactoring = it.First();
return new QuickAction(refactoring.Title, () => OnRefactoringRequested(refactoring.Title, refactoring));
});
if (node is Comment comment)
{
actions = actions.Append(
new QuickAction("Edit comment",
() => _commentEditingDialog.Open(requestContext, comment.CommentTitle,
comment.CommentDescription))
);
}
else
{
var hasComment = node.TryGetComment(out var existingComment);
actions = actions.Append(
new QuickAction(hasComment ? "Edit comment" : "Add comment",
() => _commentEditingDialog.Open(requestContext, title: hasComment ? existingComment : "",
showDescription: false)
)
);
if (hasComment)
{
actions = actions.Append(
new QuickAction("Remove comment",
() => OnRefactoringRequested("Remove comment",
new ChangeCommentRefactoring(graph, node, "")))
);
}
}
if (node is RerouteNode rerouteNode)
{
var text = rerouteNode.IsWireless ? "Make wired" : "Make wireless";
actions = actions.Append(
new QuickAction(text,
() => OnRefactoringRequested(text, new ToggleWirelessRefactoring(graph, rerouteNode)))
);
}
// if the node references some invokable, add an action to open the refactor dialog for this invokable.
// and one to go to the definition. Only do this if the invokable is part of this project (and not built-in or included).
if (node is IReferToAnInvokable iReferToAnInvokable)
{
var name = iReferToAnInvokable.InvokableDescription.Name;
if (_currentProject.IsDefinedInThisProject(iReferToAnInvokable.InvokableDescription))
{
// if the node isn't actually the entry point, add an action to go to the entrypoint
if (!(node is EntryPoint))
{
actions = actions.Append(
new QuickAction($"Go to definition of {name}",
() => Open(_currentProject.FindDefiningGraph(iReferToAnInvokable.InvokableDescription))
)
);
}
actions = actions.Append(
new QuickAction($"Refactor {name}",
() => _invokableRefactorDialog.Open(iReferToAnInvokable.InvokableDescription, _currentProject)
)
);
actions = actions.Append(
new QuickAction($"Edit documentation of {name}",
() => _documentationDialog.Open(iReferToAnInvokable.InvokableDescription)
)
);
}
if (!iReferToAnInvokable.InvokableDescription.IsBuiltin)
{
actions = actions.Append(
new QuickAction($"Find usages of {name}",
() => FindAndShowUsages(iReferToAnInvokable.InvokableDescription)
)
);
}
}
if (node is ICanHaveModifier)
{
actions = actions.Append(new QuickAction("Debugging aids"));
var currentModifiers = node.GetModifiers();
var hasDebug = currentModifiers.HasFlag(ScadNodeModifier.Debug);
actions = actions.Append(
new QuickAction("Debug subtree",
() => OnRefactoringRequested("Toggle: Debug subtree",
new ToggleModifierRefactoring(graph, node, ScadNodeModifier.Debug, !hasDebug)), true,
hasDebug));
var hasRoot = currentModifiers.HasFlag(ScadNodeModifier.Root);
actions = actions.Append(
new QuickAction("Make node root",
() => OnRefactoringRequested("Toggle: Make node root",
new ToggleModifierRefactoring(graph, node, ScadNodeModifier.Root, !hasRoot)), true,
hasRoot));
var hasBackground = currentModifiers.HasFlag(ScadNodeModifier.Background);
actions = actions.Append(
new QuickAction("Background subtree",
() => OnRefactoringRequested("Toggle: Background subtree",
new ToggleModifierRefactoring(graph, node, ScadNodeModifier.Background, !hasBackground)),
true, hasBackground));
var hasDisable = currentModifiers.HasFlag(ScadNodeModifier.Disable);
actions = actions.Append(
new QuickAction("Disable subtree",
() => OnRefactoringRequested("Toggle: Disable subtree",
new ToggleModifierRefactoring(graph, node, ScadNodeModifier.Disable, !hasDisable)), true,
hasDisable));
actions = actions.Append(
new QuickAction("Set color",
() => _nodeColorDialog.Open(graph, node)));
if (currentModifiers.HasFlag(ScadNodeModifier.Color))
{
actions = actions.Append(
new QuickAction("Clear color",
() => OnRefactoringRequested("Remove color",
new ToggleModifierRefactoring(graph, node, ScadNodeModifier.Color, false))));
}
}
_quickActionsPopup.Open(position, actions);
}
private void OnOpenFileDialogRequested()
{
_fileDialog.Mode = FileDialog.ModeEnum.OpenFile;
_fileDialog.PopupCentered();
_fileDialog.Connect("file_selected")
.WithFlags(ConnectFlags.Oneshot)
.To(this, nameof(OnOpenFile));
}
private void OnOpenFile(string filename)
{
var file = new File();
if (!file.FileExists(filename))
{
return;
}
var savedProject = ResourceLoader.Load<SavedProject>(filename, "", noCache: true);
if (savedProject == null)
{
NotificationService.ShowError("Cannot load file " + filename);
return;
}
// workaround for https://github.com/godotengine/godot/issues/59686
// as the "noCache" flag currently isn't working properly
savedProject.ResourcePath = "";
Clear();
_currentFile = filename;
_configuration.AddRecentFile(filename);
_fileNameLabel.Text = filename;
_openOpenScadButton.Disabled = false;
_openOpenScadButton.HintTooltip = "";
_currentProject = new ScadProject(_rootResolver);
try
{
_currentProject.Load(savedProject, _currentFile);
}
catch (Exception e)
{
NotificationService.ShowError("Error when loading file. See log for details.");
Log.Error(e, "Exception when loading file {File}", _currentFile);
// don't load a half-broken file
OnNewButtonPressed();
return;
}
Open(_currentProject.MainModule);
// again, important, must be done after the main module is opened
_currentHistoryStack = new HistoryStack(_currentProject, GetEditorState());
RefreshControls();
RenderScadOutput();
}
private void OnSaveAsPressed()
{
_fileDialog.Mode = FileDialog.ModeEnum.SaveFile;
_fileDialog.PopupCentered();
_fileDialog.Connect("file_selected")
.WithFlags(ConnectFlags.Oneshot)
.To(this, nameof(OnSaveFileSelected));
}
private void OnSaveFileSelected(string filename)
{
_currentFile = filename;
_configuration.AddRecentFile(filename);
_fileNameLabel.Text = filename;
_openOpenScadButton.Disabled = false;
_openOpenScadButton.HintTooltip = "";
MarkDirty(true);
}
private void OnPreviewToggled(bool preview)
{
_editingInterface.Visible = !preview;
_textEdit.Visible = preview;
}
private void MarkDirty(bool codeChange)
{
_dirty = true;
_codeChanged = codeChange;
if (_codeChanged)
{
_forceSaveButton.Text = "[.!.]";
}
else
{
_forceSaveButton.Text = "[...]";
}
}
private void SaveChanges()
{
if (!_dirty)
{
return;
}
if (_codeChanged)
{
RenderScadOutput();
}
if (_currentFile != null)
{
// save resource
var savedProject = _currentProject.Save();
var error = ResourceSaver.Save(_currentFile, savedProject);
if (error != Error.Ok)
{
NotificationService.ShowNotification("Cannot save project, see log for details");
Log.Warning("Unable to save project to {File} -> Error {Error}", _currentFile, error);
}
else
{
Log.Information("Saved project");
}
}
_forceSaveButton.Text = "[OK]";
_dirty = false;
}
private void RenderScadOutput()
{
var rendered = _currentProject.Render();
_textEdit.Text = rendered;
if (_currentFile != null)
{
// save rendered output
var file = new File();
var outputPath = _currentFile + ".scad";
var error = file.Open(outputPath, File.ModeFlags.Write);
if (error == Error.Ok)
{
file.StoreString(rendered);
file.Close();
}
else
{
NotificationService.ShowError("Cannot save SCAD file, see log for details");
Log.Warning("Unable to save SCAD output file to {File} -> Error {Error}", outputPath, error);
}
}
}
public override void _Input(InputEvent evt)
{
if (evt.IsUndo() && _currentHistoryStack.CanUndo(out _))
{
Undo();
}
if (evt.IsRedo() && _currentHistoryStack.CanRedo(out _))
{
Redo();
}
if (evt.IsStylusDebug())
{
_stylusDebugDialog.Show();
}
}
private void FindAndShowUsages(VariableDescription variableDescription)
{
var usagePoints = _currentProject.FindAllReferencingNodes(variableDescription)
.Select(it => new UsagePointInformation($"{it.Node.NodeTitle} in ({it.Graph.Description.Name})",
it.Graph.Description.Id, it.Node.Id))
.ToList();
if (usagePoints.Count == 0)
{
NotificationService.ShowNotification("No usages found.");
return;
}
_usageDialog.Open($"Usages of variable {variableDescription.Name}", usagePoints);
_usageButton.Show();
}
private void FindAndShowUsages(InvokableDescription invokableDescription)
{
var usagePoints = _currentProject.FindAllReferencingNodes(invokableDescription)
.Select(it => new UsagePointInformation($"{it.Node.NodeTitle} in ({it.Graph.Description.Name})",
it.Graph.Description.Id, it.Node.Id))
.ToList();
if (usagePoints.Count == 0)
{
NotificationService.ShowNotification("No usages found.");
return;
}
var type = invokableDescription is FunctionDescription ? "function" : "module";
_usageDialog.Open($"Usages of {type} {invokableDescription.Name}", usagePoints);
_usageButton.Show();
}
public override void _Notification(int what)
{
if (what == NotificationWmAbout)
{
// OSX about menu.
NotificationService.ShowNotification($"This is OpenSCAD Graph Editor ({AppVersion.Version}");
}
}
}
}be0a8da09This file is pinned to the selected release. Download the original to open it in your design tool.