diff --git a/src/Artemis.Core/Plugins/PluginFeatureAttribute.cs b/src/Artemis.Core/Plugins/PluginFeatureAttribute.cs index 40863ce25..e761b7a3e 100644 --- a/src/Artemis.Core/Plugins/PluginFeatureAttribute.cs +++ b/src/Artemis.Core/Plugins/PluginFeatureAttribute.cs @@ -26,6 +26,7 @@ namespace Artemis.Core /// /// Marks the feature to always be enabled as long as the plugin is enabled + /// Note: always if this is the plugin's only feature /// public bool AlwaysEnabled { get; set; } } diff --git a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs index 62012d9a4..c0350c26f 100644 --- a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs +++ b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs @@ -122,10 +122,11 @@ namespace Artemis.Core } /// - /// Marks the feature to always be enabled as long as the plugin is enabled and cannot be disabled + /// Marks the feature to always be enabled as long as the plugin is enabled and cannot be disabled. + /// Note: always if this is the plugin's only feature /// [JsonProperty] - public bool AlwaysEnabled { get; } + public bool AlwaysEnabled { get; internal set; } /// /// Gets a boolean indicating whether the feature is enabled in persistent storage diff --git a/src/Artemis.Core/Plugins/PluginInfo.cs b/src/Artemis.Core/Plugins/PluginInfo.cs index 2486544cc..e0850b3a0 100644 --- a/src/Artemis.Core/Plugins/PluginInfo.cs +++ b/src/Artemis.Core/Plugins/PluginInfo.cs @@ -24,7 +24,7 @@ namespace Artemis.Core private Plugin _plugin = null!; private Version _version = null!; private bool _requiresAdmin; - + private PluginPlatform? _platforms; internal PluginInfo() { @@ -142,7 +142,17 @@ namespace Artemis.Core get => _requiresAdmin; internal set => SetAndNotify(ref _requiresAdmin, value); } - + + /// + /// Gets + /// + [JsonProperty] + public PluginPlatform? Platforms + { + get => _platforms; + internal set => _platforms = value; + } + /// /// Gets the plugin this info is associated with /// @@ -164,6 +174,11 @@ namespace Artemis.Core return Icon.Contains('.') ? Plugin.ResolveRelativePath(Icon) : Icon; } } + + /// + /// Gets a boolean indicating whether this plugin is compatible with the current operating system + /// + public bool IsCompatible => Platforms.MatchesCurrentOperatingSystem(); internal string PreferredPluginDirectory => $"{Main.Split(".dll")[0].Replace("/", "").Replace("\\", "")}-{Guid.ToString().Substring(0, 8)}"; diff --git a/src/Artemis.Core/Plugins/PluginPlatform.cs b/src/Artemis.Core/Plugins/PluginPlatform.cs index 5d44e7943..fa07d2646 100644 --- a/src/Artemis.Core/Plugins/PluginPlatform.cs +++ b/src/Artemis.Core/Plugins/PluginPlatform.cs @@ -1,4 +1,5 @@ using System; +using Newtonsoft.Json; namespace Artemis.Core; @@ -6,15 +7,36 @@ namespace Artemis.Core; /// Specifies OS platforms a plugin may support. /// [Flags] +[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] public enum PluginPlatform { /// The Windows platform. - Windows = 0, + Windows = 1, /// The Linux platform. - Linux = 1, + Linux = 2, /// The OSX platform. // ReSharper disable once InconsistentNaming - OSX = 2 + OSX = 4 +} + +internal static class PluginPlatformExtensions +{ + /// + /// Determines whether the provided platform matches the current operating system. + /// + internal static bool MatchesCurrentOperatingSystem(this PluginPlatform? platform) + { + if (platform == null) + return true; + + if (OperatingSystem.IsWindows()) + return platform.Value.HasFlag(PluginPlatform.Windows); + if (OperatingSystem.IsLinux()) + return platform.Value.HasFlag(PluginPlatform.Linux); + if (OperatingSystem.IsMacOS()) + return platform.Value.HasFlag(PluginPlatform.OSX); + return false; + } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs b/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs index edb408093..f4bc30b79 100644 --- a/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs +++ b/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs @@ -101,16 +101,7 @@ namespace Artemis.Core /// public bool AppliesToPlatform() { - if (Platform == null) - return true; - - if (OperatingSystem.IsWindows()) - return Platform.Value.HasFlag(PluginPlatform.Windows); - if (OperatingSystem.IsLinux()) - return Platform.Value.HasFlag(PluginPlatform.Linux); - if (OperatingSystem.IsMacOS()) - return Platform.Value.HasFlag(PluginPlatform.OSX); - return false; + return Platform.MatchesCurrentOperatingSystem(); } /// diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs index 4623bd948..48e403954 100644 --- a/src/Artemis.Core/Services/PluginManagementService.cs +++ b/src/Artemis.Core/Services/PluginManagementService.cs @@ -260,7 +260,7 @@ namespace Artemis.Core.Services } } - foreach (Plugin plugin in _plugins.Where(p => p.Entity.IsEnabled)) + foreach (Plugin plugin in _plugins.Where(p => p.Info.IsCompatible && p.Entity.IsEnabled)) { try { @@ -364,7 +364,13 @@ namespace Artemis.Core.Services // Load the enabled state and if not found, default to true PluginFeatureEntity featureEntity = plugin.Entity.Features.FirstOrDefault(i => i.Type == featureType.FullName) ?? new PluginFeatureEntity {IsEnabled = true, Type = featureType.FullName!}; - plugin.AddFeature(new PluginFeatureInfo(plugin, featureType, featureEntity, (PluginFeatureAttribute?) Attribute.GetCustomAttribute(featureType, typeof(PluginFeatureAttribute)))); + PluginFeatureInfo feature = new(plugin, featureType, featureEntity, (PluginFeatureAttribute?) Attribute.GetCustomAttribute(featureType, typeof(PluginFeatureAttribute))); + + // If the plugin only has a single feature, it should always be enabled + if (featureTypes.Count == 1) + feature.AlwaysEnabled = true; + + plugin.AddFeature(feature); } if (!featureTypes.Any()) @@ -390,6 +396,9 @@ namespace Artemis.Core.Services public void EnablePlugin(Plugin plugin, bool saveState, bool ignorePluginLock) { + if (!plugin.Info.IsCompatible) + throw new ArtemisPluginException(plugin, $"This plugin only supports the following operating system(s): {plugin.Info.Platforms}"); + if (plugin.Assembly == null) throw new ArtemisPluginException(plugin, "Cannot enable a plugin that hasn't successfully been loaded"); @@ -446,7 +455,18 @@ namespace Artemis.Core.Services // Activate features after they are all loaded foreach (PluginFeatureInfo pluginFeature in plugin.Features.Where(f => f.Instance != null && (f.EnabledInStorage || f.AlwaysEnabled))) - EnablePluginFeature(pluginFeature.Instance!, false, !ignorePluginLock); + { + try + { + EnablePluginFeature(pluginFeature.Instance!, false, !ignorePluginLock); + } + catch (Exception) + { + if (pluginFeature.AlwaysEnabled) + DisablePlugin(plugin, false); + throw; + } + } if (saveState) { diff --git a/src/Artemis.UI.Linux/App.axaml.cs b/src/Artemis.UI.Linux/App.axaml.cs index 95c65051e..143078616 100644 --- a/src/Artemis.UI.Linux/App.axaml.cs +++ b/src/Artemis.UI.Linux/App.axaml.cs @@ -1,6 +1,7 @@ using Artemis.Core.Services; using Artemis.UI.Linux.Providers.Input; using Avalonia; +using Avalonia.Controls; using Avalonia.Markup.Xaml; using Avalonia.Threading; using ReactiveUI; @@ -26,6 +27,9 @@ namespace Artemis.UI.Linux public override void OnFrameworkInitializationCompleted() { + if (Design.IsDesignMode) + return; + ArtemisBootstrapper.Initialize(); if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) _applicationStateManager = new ApplicationStateManager(_kernel!, desktop.Args); diff --git a/src/Artemis.UI.MacOS/App.axaml.cs b/src/Artemis.UI.MacOS/App.axaml.cs index 4993d81c9..42a066f61 100644 --- a/src/Artemis.UI.MacOS/App.axaml.cs +++ b/src/Artemis.UI.MacOS/App.axaml.cs @@ -1,4 +1,5 @@ using Avalonia; +using Avalonia.Controls; using Avalonia.Markup.Xaml; using Avalonia.Threading; using Ninject; @@ -20,6 +21,9 @@ namespace Artemis.UI.MacOS public override void OnFrameworkInitializationCompleted() { + if (Design.IsDesignMode) + return; + ArtemisBootstrapper.Initialize(); } } diff --git a/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj b/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj index 88ec4b2c3..a034b7833 100644 --- a/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj +++ b/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj @@ -3,8 +3,8 @@ Library net6.0 enable - - + + bin\ x64 x64 @@ -15,20 +15,20 @@ - - - - - - - - - - - + + + + + + + + + + + - + diff --git a/src/Artemis.UI/ReactiveCoreWindow.cs b/src/Artemis.UI.Shared/ReactiveCoreWindow.cs similarity index 98% rename from src/Artemis.UI/ReactiveCoreWindow.cs rename to src/Artemis.UI.Shared/ReactiveCoreWindow.cs index c8325b036..9c82b0afd 100644 --- a/src/Artemis.UI/ReactiveCoreWindow.cs +++ b/src/Artemis.UI.Shared/ReactiveCoreWindow.cs @@ -4,7 +4,7 @@ using Avalonia.Controls; using FluentAvalonia.UI.Controls; using ReactiveUI; -namespace Artemis.UI +namespace Artemis.UI.Shared { /// /// A ReactiveUI that implements the interface and will diff --git a/src/Artemis.UI.Shared/Services/Builders/OpenFolderDialogBuilder.cs b/src/Artemis.UI.Shared/Services/Builders/OpenFolderDialogBuilder.cs new file mode 100644 index 000000000..47846d170 --- /dev/null +++ b/src/Artemis.UI.Shared/Services/Builders/OpenFolderDialogBuilder.cs @@ -0,0 +1,54 @@ +using System.Threading.Tasks; +using Avalonia.Controls; + +namespace Artemis.UI.Shared.Services.Builders; + +/// +/// Represents a builder that can create a . +/// +public class OpenFolderDialogBuilder +{ + private readonly OpenFolderDialog _openFolderDialog; + private readonly Window _parent; + + /// + /// Creates a new instance of the class. + /// + /// The parent window that will host the dialog. + internal OpenFolderDialogBuilder(Window parent) + { + _parent = parent; + _openFolderDialog = new OpenFolderDialog(); + } + + + /// + /// Set the title of the dialog + /// + public OpenFolderDialogBuilder WithTitle(string? title) + { + _openFolderDialog.Title = title; + return this; + } + + /// + /// Set the initial directory of the dialog + /// + public OpenFolderDialogBuilder WithDirectory(string? directory) + { + _openFolderDialog.Directory = directory; + return this; + } + + /// + /// Asynchronously shows the folder dialog. + /// + /// + /// A task that on completion returns an array containing the full path to the selected + /// folder, or null if the dialog was canceled. + /// + public async Task ShowAsync() + { + return await _openFolderDialog.ShowAsync(_parent); + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Services/Interfaces/IWindowService.cs b/src/Artemis.UI.Shared/Services/Interfaces/IWindowService.cs index 06efe5882..f10d6c2fc 100644 --- a/src/Artemis.UI.Shared/Services/Interfaces/IWindowService.cs +++ b/src/Artemis.UI.Shared/Services/Interfaces/IWindowService.cs @@ -22,7 +22,7 @@ namespace Artemis.UI.Shared.Services /// Given a ViewModel, show its corresponding View as a window /// /// ViewModel to show the View for - void ShowWindow(object viewModel); + Window ShowWindow(object viewModel); /// /// Shows a dialog displaying the given exception @@ -61,6 +61,12 @@ namespace Artemis.UI.Shared.Services /// Task ShowConfirmContentDialog(string title, string message, string confirm = "Confirm", string? cancel = "Cancel"); + /// + /// Creates an open folder dialog, use the fluent API to configure it + /// + /// The builder that can be used to configure the dialog + OpenFolderDialogBuilder CreateOpenFolderDialog(); + /// /// Creates an open file dialog, use the fluent API to configure it /// diff --git a/src/Artemis.UI.Shared/Services/Window/WindowService.cs b/src/Artemis.UI.Shared/Services/Window/WindowService.cs index 7621d84c0..5348f2574 100644 --- a/src/Artemis.UI.Shared/Services/Window/WindowService.cs +++ b/src/Artemis.UI.Shared/Services/Window/WindowService.cs @@ -30,7 +30,7 @@ namespace Artemis.UI.Shared.Services return viewModel; } - public void ShowWindow(object viewModel) + public Window ShowWindow(object viewModel) { Window? parent = GetCurrentWindow(); @@ -49,6 +49,8 @@ namespace Artemis.UI.Shared.Services window.Show(parent); else window.Show(); + + return window; } public async Task ShowDialogAsync(params (string name, object? value)[] parameters) where TViewModel : DialogViewModelBase @@ -86,7 +88,6 @@ namespace Artemis.UI.Shared.Services Window window = (Window) Activator.CreateInstance(type)!; window.DataContext = viewModel; viewModel.CloseRequested += (_, args) => window.Close(args.Result); - viewModel.CancelRequested += (_, _) => window.Close(); return await window.ShowDialog(parent); } @@ -118,6 +119,14 @@ namespace Artemis.UI.Shared.Services throw new ArtemisSharedUIException("Can't show a content dialog without any windows being shown."); return new ContentDialogBuilder(_kernel, currentWindow); } + + public OpenFolderDialogBuilder CreateOpenFolderDialog() + { + Window? currentWindow = GetCurrentWindow(); + if (currentWindow == null) + throw new ArtemisSharedUIException("Can't show an open folder dialog without any windows being shown."); + return new OpenFolderDialogBuilder(currentWindow); + } public OpenFileDialogBuilder CreateOpenFileDialog() { diff --git a/src/Artemis.UI.Shared/Styles/Notifications.axaml b/src/Artemis.UI.Shared/Styles/Notifications.axaml index bfe195ede..0e9553ca2 100644 --- a/src/Artemis.UI.Shared/Styles/Notifications.axaml +++ b/src/Artemis.UI.Shared/Styles/Notifications.axaml @@ -19,7 +19,7 @@ - + diff --git a/src/Artemis.UI.Shared/ViewModelBase.cs b/src/Artemis.UI.Shared/ViewModelBase.cs index 7f7a0dca0..24a4d2a11 100644 --- a/src/Artemis.UI.Shared/ViewModelBase.cs +++ b/src/Artemis.UI.Shared/ViewModelBase.cs @@ -51,17 +51,8 @@ public abstract class DialogViewModelBase : ValidatableViewModelBase { CloseRequested?.Invoke(this, new DialogClosedEventArgs(result)); } - - /// - /// Closes the dialog without a result - /// - public void Cancel() - { - CancelRequested?.Invoke(this, EventArgs.Empty); - } - + internal event EventHandler>? CloseRequested; - internal event EventHandler? CancelRequested; } /// diff --git a/src/Artemis.UI.Windows/App.axaml.cs b/src/Artemis.UI.Windows/App.axaml.cs index 94834a5ac..1f2e213d2 100644 --- a/src/Artemis.UI.Windows/App.axaml.cs +++ b/src/Artemis.UI.Windows/App.axaml.cs @@ -2,6 +2,7 @@ using Artemis.Core.Services; using Artemis.UI.Windows.Ninject; using Artemis.UI.Windows.Providers.Input; using Avalonia; +using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using Avalonia.Threading; @@ -22,9 +23,9 @@ namespace Artemis.UI.Windows public override void OnFrameworkInitializationCompleted() { - if (ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop) + if (ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop || Design.IsDesignMode) return; - + ArtemisBootstrapper.Initialize(); _applicationStateManager = new ApplicationStateManager(_kernel!, desktop.Args); RegisterProviders(_kernel!); diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj index 92065dd99..081646711 100644 --- a/src/Artemis.UI/Artemis.UI.csproj +++ b/src/Artemis.UI/Artemis.UI.csproj @@ -8,44 +8,44 @@ x64 - + - - - + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - + + + - - + + - - + + diff --git a/src/Artemis.UI/MainWindow.axaml.cs b/src/Artemis.UI/MainWindow.axaml.cs index 77be8f698..666d7cdb0 100644 --- a/src/Artemis.UI/MainWindow.axaml.cs +++ b/src/Artemis.UI/MainWindow.axaml.cs @@ -1,5 +1,6 @@ using System; using Artemis.UI.Screens.Root; +using Artemis.UI.Shared; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Shapes; diff --git a/src/Artemis.UI/Screens/Debugger/DebugView.axaml.cs b/src/Artemis.UI/Screens/Debugger/DebugView.axaml.cs index 0709047e5..7b03c0a46 100644 --- a/src/Artemis.UI/Screens/Debugger/DebugView.axaml.cs +++ b/src/Artemis.UI/Screens/Debugger/DebugView.axaml.cs @@ -1,6 +1,7 @@ using System; using System.Reactive.Disposables; using System.Reactive.Linq; +using Artemis.UI.Shared; using Artemis.UI.Shared.Events; using Avalonia; using Avalonia.Controls; diff --git a/src/Artemis.UI/Screens/Plugins/PluginFeatureView.axaml b/src/Artemis.UI/Screens/Plugins/PluginFeatureView.axaml index 10138d3fa..c39c97888 100644 --- a/src/Artemis.UI/Screens/Plugins/PluginFeatureView.axaml +++ b/src/Artemis.UI/Screens/Plugins/PluginFeatureView.axaml @@ -4,8 +4,10 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared" + xmlns:plugins="clr-namespace:Artemis.UI.Screens.Plugins" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Screens.Plugins.PluginFeatureView"> + x:Class="Artemis.UI.Screens.Plugins.PluginFeatureView" + x:DataType="plugins:PluginFeatureViewModel"> @@ -24,14 +26,15 @@ + IsVisible="{CompiledBinding LoadException, Converter={x:Static ObjectConverters.IsNull}}" /> - - + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabViewModel.cs index 1be16315b..e8c28e4c0 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabViewModel.cs @@ -90,7 +90,7 @@ namespace Artemis.UI.Screens.Settings // Enable it via the VM to enable the prerequisite dialog PluginSettingsViewModel? pluginViewModel = Plugins.FirstOrDefault(i => i.Plugin == plugin); if (pluginViewModel is {IsEnabled: false}) - pluginViewModel.IsEnabled = true; + await pluginViewModel.UpdateEnabled(true); _notificationService.CreateNotification() .WithTitle("Plugin imported") diff --git a/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditView.axaml.cs b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditView.axaml.cs index fc40de8ce..3bd22e95c 100644 --- a/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditView.axaml.cs +++ b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditView.axaml.cs @@ -1,5 +1,6 @@ using System; using System.Reactive.Disposables; +using Artemis.UI.Shared; using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; diff --git a/src/Artemis.UI/Screens/VisualScripting/NodeScriptWindowView.axaml.cs b/src/Artemis.UI/Screens/VisualScripting/NodeScriptWindowView.axaml.cs index 29e10a9a0..b539e42a8 100644 --- a/src/Artemis.UI/Screens/VisualScripting/NodeScriptWindowView.axaml.cs +++ b/src/Artemis.UI/Screens/VisualScripting/NodeScriptWindowView.axaml.cs @@ -1,3 +1,4 @@ +using Artemis.UI.Shared; using Avalonia; using Avalonia.Markup.Xaml;