godot/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs
Ignacio Etcheverry e59ac40712 Mono: Better handling of missing/outdated API assemblies
Remove the old API assembly invalidation system. It's pretty simple since now the editor has a hard dependency on the API assemblies and SCons takes care of prebuilding them.
If we fail to load a project's API assembly because it was either missing or outdated, we just copy the prebuilt assemblies to the project and try again. We also do this when creating the solution and before building, just in case the user removed them from the disk after they were loaded.
This way the API assemblies will be always loaded successfully. If they are not, it's a bug.

Also fixed:

- EditorDef was behaving like GlobalDef in GodotTools.
- NullReferenceException because we can't serialize System.WeakReference yet. Use Godot.WeakRef in the mean time.
2019-07-14 19:17:07 +02:00

492 lines
18 KiB
C#

using Godot;
using GodotTools.Utils;
using System;
using System.Collections.Generic;
using System.IO;
using GodotTools.Internals;
using GodotTools.ProjectEditor;
using static GodotTools.Internals.Globals;
using File = GodotTools.Utils.File;
using Path = System.IO.Path;
using OS = GodotTools.Utils.OS;
namespace GodotTools
{
public class GodotSharpEditor : EditorPlugin, ISerializationListener
{
private EditorSettings editorSettings;
private PopupMenu menuPopup;
private AcceptDialog errorDialog;
private AcceptDialog aboutDialog;
private CheckBox aboutDialogCheckBox;
private ToolButton bottomPanelBtn;
private MonoDevelopInstance monoDevelopInstance;
private MonoDevelopInstance visualStudioForMacInstance;
private WeakRef exportPluginWeak; // TODO Use WeakReference once we have proper serialization
public MonoBottomPanel MonoBottomPanel { get; private set; }
private bool CreateProjectSolution()
{
using (var pr = new EditorProgress("create_csharp_solution", "Generating solution...".TTR(), 2))
{
pr.Step("Generating C# project...".TTR());
string resourceDir = ProjectSettings.GlobalizePath("res://");
string path = resourceDir;
string name = (string) ProjectSettings.GetSetting("application/config/name");
if (name.Empty())
name = "UnnamedProject";
string guid = CSharpProject.GenerateGameProject(path, name);
if (guid.Length > 0)
{
var solution = new DotNetSolution(name)
{
DirectoryPath = path
};
var projectInfo = new DotNetSolution.ProjectInfo
{
Guid = guid,
PathRelativeToSolution = name + ".csproj",
Configs = new List<string> {"Debug", "Release", "Tools"}
};
solution.AddNewProject(name, projectInfo);
try
{
solution.Save();
}
catch (IOException e)
{
ShowErrorDialog("Failed to save solution. Exception message: ".TTR() + e.Message);
return false;
}
// Make sure to update the API assemblies if they happen to be missing. Just in
// case the user decided to delete them at some point after they were loaded.
Internal.UpdateApiAssembliesFromPrebuilt();
pr.Step("Done".TTR());
// Here, after all calls to progress_task_step
CallDeferred(nameof(_RemoveCreateSlnMenuOption));
}
else
{
ShowErrorDialog("Failed to create C# project.".TTR());
}
return true;
}
}
private void _RemoveCreateSlnMenuOption()
{
menuPopup.RemoveItem(menuPopup.GetItemIndex((int) MenuOptions.CreateSln));
bottomPanelBtn.Show();
}
private void _ShowAboutDialog()
{
bool showOnStart = (bool) editorSettings.GetSetting("mono/editor/show_info_on_start");
aboutDialogCheckBox.Pressed = showOnStart;
aboutDialog.PopupCenteredMinsize();
}
private void _ToggleAboutDialogOnStart(bool enabled)
{
bool showOnStart = (bool) editorSettings.GetSetting("mono/editor/show_info_on_start");
if (showOnStart != enabled)
editorSettings.SetSetting("mono/editor/show_info_on_start", enabled);
}
private void _MenuOptionPressed(MenuOptions id)
{
switch (id)
{
case MenuOptions.CreateSln:
CreateProjectSolution();
break;
case MenuOptions.AboutCSharp:
_ShowAboutDialog();
break;
default:
throw new ArgumentOutOfRangeException(nameof(id), id, "Invalid menu option");
}
}
private void _BuildSolutionPressed()
{
if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
{
if (!CreateProjectSolution())
return; // Failed to create solution
}
Instance.MonoBottomPanel.BuildProjectPressed();
}
public override void _Notification(int what)
{
base._Notification(what);
if (what == NotificationReady)
{
bool showInfoDialog = (bool) editorSettings.GetSetting("mono/editor/show_info_on_start");
if (showInfoDialog)
{
aboutDialog.PopupExclusive = true;
_ShowAboutDialog();
// Once shown a first time, it can be seen again via the Mono menu - it doesn't have to be exclusive from that time on.
aboutDialog.PopupExclusive = false;
}
}
}
public enum MenuOptions
{
CreateSln,
AboutCSharp,
}
public enum ExternalEditor
{
None,
VisualStudio, // TODO (Windows-only)
VisualStudioForMac, // Mac-only
MonoDevelop,
VsCode
}
public void ShowErrorDialog(string message, string title = "Error")
{
errorDialog.WindowTitle = title;
errorDialog.DialogText = message;
errorDialog.PopupCenteredMinsize();
}
private static string _vsCodePath = string.Empty;
private static readonly string[] VsCodeNames =
{
"code", "code-oss", "vscode", "vscode-oss", "visual-studio-code", "visual-studio-code-oss"
};
public Error OpenInExternalEditor(Script script, int line, int col)
{
var editor = (ExternalEditor) editorSettings.GetSetting("mono/editor/external_editor");
switch (editor)
{
case ExternalEditor.VsCode:
{
if (_vsCodePath.Empty() || !File.Exists(_vsCodePath))
{
// Try to search it again if it wasn't found last time or if it was removed from its location
_vsCodePath = VsCodeNames.SelectFirstNotNull(OS.PathWhich, orElse: string.Empty);
}
var args = new List<string>();
bool osxAppBundleInstalled = false;
if (OS.IsOSX())
{
// The package path is '/Applications/Visual Studio Code.app'
const string vscodeBundleId = "com.microsoft.VSCode";
osxAppBundleInstalled = Internal.IsOsxAppBundleInstalled(vscodeBundleId);
if (osxAppBundleInstalled)
{
args.Add("-b");
args.Add(vscodeBundleId);
// The reusing of existing windows made by the 'open' command might not choose a wubdiw that is
// editing our folder. It's better to ask for a new window and let VSCode do the window management.
args.Add("-n");
// The open process must wait until the application finishes (which is instant in VSCode's case)
args.Add("--wait-apps");
args.Add("--args");
}
}
var resourcePath = ProjectSettings.GlobalizePath("res://");
args.Add(resourcePath);
string scriptPath = ProjectSettings.GlobalizePath(script.ResourcePath);
if (line >= 0)
{
args.Add("-g");
args.Add($"{scriptPath}:{line + 1}:{col}");
}
else
{
args.Add(scriptPath);
}
string command;
if (OS.IsOSX())
{
if (!osxAppBundleInstalled && _vsCodePath.Empty())
{
GD.PushError("Cannot find code editor: VSCode");
return Error.FileNotFound;
}
command = osxAppBundleInstalled ? "/usr/bin/open" : _vsCodePath;
}
else
{
if (_vsCodePath.Empty())
{
GD.PushError("Cannot find code editor: VSCode");
return Error.FileNotFound;
}
command = _vsCodePath;
}
try
{
OS.RunProcess(command, args);
}
catch (Exception e)
{
GD.PushError($"Error when trying to run code editor: VSCode. Exception message: '{e.Message}'");
}
break;
}
case ExternalEditor.VisualStudioForMac:
goto case ExternalEditor.MonoDevelop;
case ExternalEditor.MonoDevelop:
{
MonoDevelopInstance GetMonoDevelopInstance(string solutionPath)
{
if (OS.IsOSX() && editor == ExternalEditor.VisualStudioForMac)
{
if (visualStudioForMacInstance == null)
visualStudioForMacInstance = new MonoDevelopInstance(solutionPath, MonoDevelopInstance.EditorId.VisualStudioForMac);
return visualStudioForMacInstance;
}
if (monoDevelopInstance == null)
monoDevelopInstance = new MonoDevelopInstance(solutionPath, MonoDevelopInstance.EditorId.MonoDevelop);
return monoDevelopInstance;
}
string scriptPath = ProjectSettings.GlobalizePath(script.ResourcePath);
if (line >= 0)
scriptPath += $";{line + 1};{col}";
GetMonoDevelopInstance(GodotSharpDirs.ProjectSlnPath).Execute(scriptPath);
break;
}
case ExternalEditor.None:
return Error.Unavailable;
default:
throw new ArgumentOutOfRangeException();
}
return Error.Ok;
}
public bool OverridesExternalEditor()
{
return (ExternalEditor) editorSettings.GetSetting("mono/editor/external_editor") != ExternalEditor.None;
}
public override bool Build()
{
return GodotSharpBuilds.EditorBuildCallback();
}
public override void EnablePlugin()
{
base.EnablePlugin();
if (Instance != null)
throw new InvalidOperationException();
Instance = this;
var editorInterface = GetEditorInterface();
var editorBaseControl = editorInterface.GetBaseControl();
editorSettings = editorInterface.GetEditorSettings();
errorDialog = new AcceptDialog();
editorBaseControl.AddChild(errorDialog);
MonoBottomPanel = new MonoBottomPanel();
bottomPanelBtn = AddControlToBottomPanel(MonoBottomPanel, "Mono".TTR());
AddChild(new HotReloadAssemblyWatcher {Name = "HotReloadAssemblyWatcher"});
menuPopup = new PopupMenu();
menuPopup.Hide();
menuPopup.SetAsToplevel(true);
AddToolSubmenuItem("Mono", menuPopup);
// TODO: Remove or edit this info dialog once Mono support is no longer in alpha
{
menuPopup.AddItem("About C# support".TTR(), (int) MenuOptions.AboutCSharp);
aboutDialog = new AcceptDialog();
editorBaseControl.AddChild(aboutDialog);
aboutDialog.WindowTitle = "Important: C# support is not feature-complete";
// We don't use DialogText as the default AcceptDialog Label doesn't play well with the TextureRect and CheckBox
// we'll add. Instead we add containers and a new autowrapped Label inside.
// Main VBoxContainer (icon + label on top, checkbox at bottom)
var aboutVBox = new VBoxContainer();
aboutDialog.AddChild(aboutVBox);
// HBoxContainer for icon + label
var aboutHBox = new HBoxContainer();
aboutVBox.AddChild(aboutHBox);
var aboutIcon = new TextureRect();
aboutIcon.Texture = aboutIcon.GetIcon("NodeWarning", "EditorIcons");
aboutHBox.AddChild(aboutIcon);
var aboutLabel = new Label();
aboutHBox.AddChild(aboutLabel);
aboutLabel.RectMinSize = new Vector2(600, 150) * EditorScale;
aboutLabel.SizeFlagsVertical = (int) Control.SizeFlags.ExpandFill;
aboutLabel.Autowrap = true;
aboutLabel.Text =
"C# support in Godot Engine is in late alpha stage and, while already usable, " +
"it is not meant for use in production.\n\n" +
"Projects can be exported to Linux, macOS and Windows, but not yet to mobile or web platforms. " +
"Bugs and usability issues will be addressed gradually over future releases, " +
"potentially including compatibility breaking changes as new features are implemented for a better overall C# experience.\n\n" +
"If you experience issues with this Mono build, please report them on Godot's issue tracker with details about your system, MSBuild version, IDE, etc.:\n\n" +
" https://github.com/godotengine/godot/issues\n\n" +
"Your critical feedback at this stage will play a great role in shaping the C# support in future releases, so thank you!";
EditorDef("mono/editor/show_info_on_start", true);
// CheckBox in main container
aboutDialogCheckBox = new CheckBox {Text = "Show this warning when starting the editor"};
aboutDialogCheckBox.Connect("toggled", this, nameof(_ToggleAboutDialogOnStart));
aboutVBox.AddChild(aboutDialogCheckBox);
}
if (File.Exists(GodotSharpDirs.ProjectSlnPath) && File.Exists(GodotSharpDirs.ProjectCsProjPath))
{
// Make sure the existing project has Api assembly references configured correctly
CSharpProject.FixApiHintPath(GodotSharpDirs.ProjectCsProjPath);
}
else
{
bottomPanelBtn.Hide();
menuPopup.AddItem("Create C# solution".TTR(), (int) MenuOptions.CreateSln);
}
menuPopup.Connect("id_pressed", this, nameof(_MenuOptionPressed));
var buildButton = new ToolButton
{
Text = "Build",
HintTooltip = "Build solution",
FocusMode = Control.FocusModeEnum.None
};
buildButton.Connect("pressed", this, nameof(_BuildSolutionPressed));
AddControlToContainer(CustomControlContainer.Toolbar, buildButton);
// External editor settings
EditorDef("mono/editor/external_editor", ExternalEditor.None);
string settingsHintStr = "Disabled";
if (OS.IsWindows())
{
settingsHintStr += $",MonoDevelop:{(int) ExternalEditor.MonoDevelop}" +
$",Visual Studio Code:{(int) ExternalEditor.VsCode}";
}
else if (OS.IsOSX())
{
settingsHintStr += $",Visual Studio:{(int) ExternalEditor.VisualStudioForMac}" +
$",MonoDevelop:{(int) ExternalEditor.MonoDevelop}" +
$",Visual Studio Code:{(int) ExternalEditor.VsCode}";
}
else if (OS.IsUnix())
{
settingsHintStr += $",MonoDevelop:{(int) ExternalEditor.MonoDevelop}" +
$",Visual Studio Code:{(int) ExternalEditor.VsCode}";
}
editorSettings.AddPropertyInfo(new Godot.Collections.Dictionary
{
["type"] = Variant.Type.Int,
["name"] = "mono/editor/external_editor",
["hint"] = PropertyHint.Enum,
["hint_string"] = settingsHintStr
});
// Export plugin
var exportPlugin = new GodotSharpExport();
AddExportPlugin(exportPlugin);
exportPluginWeak = WeakRef(exportPlugin);
GodotSharpBuilds.Initialize();
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (exportPluginWeak != null)
{
// We need to dispose our export plugin before the editor destroys EditorSettings.
// Otherwise, if the GC disposes it at a later time, EditorExportPlatformAndroid
// will be freed after EditorSettings already was, and its device polling thread
// will try to access the EditorSettings singleton, resulting in null dereferencing.
(exportPluginWeak.GetRef() as GodotSharpExport)?.Dispose();
exportPluginWeak.Dispose();
}
}
public void OnBeforeSerialize()
{
}
public void OnAfterDeserialize()
{
Instance = this;
}
// Singleton
public static GodotSharpEditor Instance { get; private set; }
private GodotSharpEditor()
{
}
}
}