Plugins - Implemented features

Core - Removed Stylet dependency for #500
This commit is contained in:
SpoinkyNL committed 2020-11-11 23:53:52 +01:00
1 parent b56966c585
commit e812929215
139 files changed
+1635 -1106

No files matched your search

@@ -0,0 +1,37 @@
<UserControl x:Class="Artemis.UI.Screens.Settings.Tabs.Plugins.PluginFeatureView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Artemis.UI.Screens.Settings.Tabs.Plugins"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:s="https://github.com/canton7/Stylet"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance local:PluginFeatureViewModel}">
<Grid Margin="-3 -8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="30" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Icon column -->
<materialDesign:PackIcon Kind="{Binding Icon}" VerticalAlignment="Center" HorizontalAlignment="Center" />
<!-- Display name column -->
<TextBlock Grid.Column="1" Text="{Binding Name}" VerticalAlignment="Center" />
<!-- Enable toggle column -->
<StackPanel Grid.Column="2" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="8"
Visibility="{Binding Enabling, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}, Mode=OneWay}">
<CheckBox Style="{StaticResource MaterialDesignCheckBox}" IsChecked="{Binding IsEnabled}">
Feature enabled
</CheckBox>
</StackPanel>
<StackPanel Grid.Column="2" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="8"
Visibility="{Binding Enabling, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}">
<ProgressBar Style="{StaticResource MaterialDesignCircularProgressBar}" Value="0" IsIndeterminate="True" />
</StackPanel>
</Grid>
</UserControl>
@@ -0,0 +1,160 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.DataModelExpansions;
using Artemis.Core.DeviceProviders;
using Artemis.Core.LayerBrushes;
using Artemis.Core.LayerEffects;
using Artemis.Core.Modules;
using Artemis.Core.Services;
using Artemis.UI.Shared.Services;
using Humanizer;
using MaterialDesignThemes.Wpf;
using Stylet;
namespace Artemis.UI.Screens.Settings.Tabs.Plugins
{
public class PluginFeatureViewModel : Screen
{
private readonly IDialogService _dialogService;
private readonly IPluginManagementService _pluginManagementService;
private readonly ISnackbarMessageQueue _snackbarMessageQueue;
private bool _enabling;
private bool _isEnabled;
public PluginFeatureViewModel(PluginFeature feature,
IDialogService dialogService,
IPluginManagementService pluginManagementService,
ISnackbarMessageQueue snackbarMessageQueue)
{
_dialogService = dialogService;
_pluginManagementService = pluginManagementService;
_snackbarMessageQueue = snackbarMessageQueue;
Feature = feature;
Icon = GetIconKind();
IsEnabled = Feature.IsEnabled;
}
public PluginFeature Feature { get; }
public PackIconKind Icon { get; }
public string Name => Feature.GetType().Name.Humanize();
public Exception LoadException => Feature.LoadException;
public bool Enabling
{
get => _enabling;
set => SetAndNotify(ref _enabling, value);
}
public bool IsEnabled
{
get => _isEnabled;
set => SetAndNotify(ref _isEnabled, value);
}
public void ShowLogsFolder()
{
try
{
Process.Start(Environment.GetEnvironmentVariable("WINDIR") + @"\explorer.exe", Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs"));
}
catch (Exception e)
{
_dialogService.ShowExceptionDialog("Welp, we couldn\'t open the logs folder for you", e);
}
}
protected override void OnInitialActivate()
{
base.OnInitialActivate();
_pluginManagementService.PluginFeatureEnabling += OnFeatureEnabling;
_pluginManagementService.PluginFeatureEnabled += OnFeatureEnableStopped;
_pluginManagementService.PluginFeatureEnableFailed += OnFeatureEnableStopped;
}
protected override void OnClose()
{
base.OnClose();
_pluginManagementService.PluginFeatureEnabling -= OnFeatureEnabling;
_pluginManagementService.PluginFeatureEnabled -= OnFeatureEnableStopped;
_pluginManagementService.PluginFeatureEnableFailed -= OnFeatureEnableStopped;
}
private async Task UpdateEnabled(bool enable)
{
if (IsEnabled == enable)
{
NotifyOfPropertyChange(nameof(IsEnabled));
return;
}
if (enable)
{
Enabling = true;
try
{
await Task.Run(() => _pluginManagementService.EnablePluginFeature(Feature));
}
catch (Exception e)
{
_snackbarMessageQueue.Enqueue($"Failed to enable {Name}\r\n{e.Message}", "VIEW LOGS", ShowLogsFolder);
}
finally
{
Enabling = false;
}
}
else
{
_pluginManagementService.DisablePluginFeature(Feature);
}
}
private PackIconKind GetIconKind()
{
switch (Feature)
{
case BaseDataModelExpansion _:
return PackIconKind.TableAdd;
case DeviceProvider _:
return PackIconKind.Devices;
case ProfileModule _:
return PackIconKind.VectorRectangle;
case Module _:
return PackIconKind.GearBox;
case LayerBrushProvider _:
return PackIconKind.Brush;
case LayerEffectProvider _:
return PackIconKind.AutoAwesome;
}
return PackIconKind.Plugin;
}
#region Event handlers
private void OnFeatureEnabling(object? sender, PluginFeatureEventArgs e)
{
if (e.PluginFeature != Feature) return;
Enabling = true;
}
private void OnFeatureEnableStopped(object? sender, PluginFeatureEventArgs e)
{
if (e.PluginFeature != Feature) return;
Enabling = false;
IsEnabled = e.PluginFeature.IsEnabled;
NotifyOfPropertyChange(nameof(LoadException));
}
#endregion
}
}
@@ -11,7 +11,6 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
{
private readonly IPluginManagementService _pluginManagementService;
private readonly ISettingsVmFactory _settingsVmFactory;
private BindableCollection<PluginSettingsViewModel> _plugins;
public PluginSettingsTabViewModel(IPluginManagementService pluginManagementService, ISettingsVmFactory settingsVmFactory)
{
@@ -29,8 +28,8 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
Items.Clear();
await Task.Delay(200);
List<PluginSettingsViewModel> instances = _pluginManagementService.GetAllPluginInfo().Select(p => _settingsVmFactory.CreatePluginSettingsViewModel(p.Plugin)).ToList();
foreach (PluginSettingsViewModel pluginSettingsViewModel in instances)
List<PluginSettingsViewModel> instances = _pluginManagementService.GetAllPlugins().Select(p => _settingsVmFactory.CreatePluginSettingsViewModel(p)).ToList();
foreach (PluginSettingsViewModel pluginSettingsViewModel in instances.OrderBy(i => i.Plugin.Info.Name))
Items.Add(pluginSettingsViewModel);
});
@@ -6,17 +6,19 @@
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:devices="clr-namespace:Artemis.UI.Screens.Settings.Tabs.Plugins"
xmlns:controls="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared"
d:DataContext="{d:DesignInstance devices:PluginSettingsViewModel}"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<materialDesign:Card Width="400">
<materialDesign:Card Width="800">
<Grid Margin="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="3*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="100" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
@@ -24,45 +26,27 @@
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<materialDesign:PackIcon Kind="{Binding Icon}" Width="48" Height="48" Grid.Row="0" Grid.RowSpan="2" HorizontalAlignment="Center" />
<TextBlock Grid.Column="1" Grid.Row="0" Style="{StaticResource MaterialDesignTextBlock}" Text="{Binding PluginInfo.Name}" />
<Button Grid.Column="2" Grid.Row="0"
Background="#FF4343"
BorderBrush="#FF7474"
Foreground="White"
Height="22"
Padding="4"
Margin="0 -18 0 0"
Visibility="{Binding DisplayLoadFailed, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}"
Style="{StaticResource MaterialDesignRaisedButton}"
ToolTip="Click to see the full error that occured"
Command="{s:Action ShowLoadException}">
<TextBlock FontSize="11">LOAD FAILED</TextBlock>
</Button>
<Button Grid.Column="2" Grid.Row="0"
Height="22"
Padding="4"
Margin="0 -18 0 0"
Visibility="{Binding RequiresRestart, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}"
Style="{StaticResource MaterialDesignRaisedButton}"
ToolTip="Click to restart"
Command="{s:Action Restart}">
<TextBlock FontSize="11">NEEDS RESTART</TextBlock>
</Button>
<controls:ArtemisIcon Icon="{Binding Icon}"
Width="48"
Height="48"
Margin="0 5 0 0"
Grid.Row="0"
Grid.RowSpan="2"
HorizontalAlignment="Center"
VerticalAlignment="Top" />
<TextBlock Grid.Column="1" Grid.Row="0" Style="{StaticResource MaterialDesignBody2TextBlock}" Text="{Binding Plugin.Info.Name}" />
<TextBlock Grid.Column="1"
Grid.Row="1"
Grid.ColumnSpan="2"
TextWrapping="Wrap"
Text="{Binding PluginInfo.Description}"
Text="{Binding Plugin.Info.Description}"
Style="{StaticResource MaterialDesignTextBlock}"
Foreground="{DynamicResource MaterialDesignNavigationItemSubheader}" />
</Grid>
@@ -82,6 +66,23 @@
Visibility="{Binding Enabling, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}">
<ProgressBar Style="{StaticResource MaterialDesignCircularProgressBar}" Value="0" IsIndeterminate="True" />
</StackPanel>
<Border Grid.Column="2" Grid.Row="0" Grid.RowSpan="2" BorderBrush="{StaticResource MaterialDesignDivider}" BorderThickness="1 0 0 0" Margin="10 0 0 0" Padding="10 0 0 0">
<StackPanel>
<TextBlock Margin="10 10 0 5" Style="{StaticResource MaterialDesignBody2TextBlock}">Plugin features</TextBlock>
<ListBox ItemsSource="{Binding Items}"
materialDesign:RippleAssist.IsDisabled="True"
HorizontalContentAlignment="Stretch"
VirtualizingPanel.ScrollUnit="Pixel">
<ListBox.ItemTemplate>
<DataTemplate>
<ContentControl s:View.Model="{Binding}" VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch" IsTabStop="False" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</Border>
</Grid>
</materialDesign:Card>
</UserControl>
@@ -2,64 +2,50 @@
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.DataModelExpansions;
using Artemis.Core.DeviceProviders;
using Artemis.Core.LayerBrushes;
using Artemis.Core.LayerEffects;
using Artemis.Core.Modules;
using Artemis.Core.Services;
using Artemis.UI.Exceptions;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using MaterialDesignThemes.Wpf;
using Ninject;
using Ninject.Parameters;
using Serilog;
using Stylet;
using Module = Artemis.Core.Modules.Module;
namespace Artemis.UI.Screens.Settings.Tabs.Plugins
{
public class PluginSettingsViewModel : PropertyChangedBase
public class PluginSettingsViewModel : Conductor<PluginFeatureViewModel>.Collection.AllActive
{
private readonly IDialogService _dialogService;
private readonly ILogger _logger;
private readonly IPluginManagementService _pluginManagementService;
private readonly ISettingsVmFactory _settingsVmFactory;
private readonly ISnackbarMessageQueue _snackbarMessageQueue;
private readonly IWindowManager _windowManager;
private bool _enabling;
private PluginImplementation _pluginImplementation;
private PluginInfo _pluginInfo;
private Plugin _plugin;
public PluginSettingsViewModel(PluginImplementation pluginImplementation,
ILogger logger,
public PluginSettingsViewModel(Plugin plugin,
ISettingsVmFactory settingsVmFactory,
IWindowManager windowManager,
IDialogService dialogService,
IPluginManagementService pluginManagementService,
ISnackbarMessageQueue snackbarMessageQueue)
{
PluginImplementation = pluginImplementation;
PluginInfo = pluginImplementation.PluginInfo;
Plugin = plugin;
_logger = logger;
_settingsVmFactory = settingsVmFactory;
_windowManager = windowManager;
_dialogService = dialogService;
_pluginManagementService = pluginManagementService;
_snackbarMessageQueue = snackbarMessageQueue;
Icon = PluginUtilities.GetPluginIcon(Plugin, Plugin.Info.Icon);
}
public PluginImplementation PluginImplementation
public Plugin Plugin
{
get => _pluginImplementation;
set => SetAndNotify(ref _pluginImplementation, value);
}
public PluginInfo PluginInfo
{
get => _pluginInfo;
set => SetAndNotify(ref _pluginInfo, value);
get => _plugin;
set => SetAndNotify(ref _plugin, value);
}
public bool Enabling
@@ -68,34 +54,25 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
set => SetAndNotify(ref _enabling, value);
}
public PackIconKind Icon => GetIconKind();
public string Type => PluginImplementation.GetType().BaseType?.Name ?? PluginImplementation.GetType().Name;
public bool CanOpenSettings => IsEnabled && PluginImplementation.ConfigurationDialog != null;
public bool DisplayLoadFailed => !Enabling && PluginInfo.LoadException != null;
public bool RequiresRestart => PluginImplementation.IsEnabled && !PluginInfo.IsEnabled;
public object Icon { get; set; }
public string Type => Plugin.GetType().BaseType?.Name ?? Plugin.GetType().Name;
public bool CanOpenSettings => IsEnabled && Plugin.ConfigurationDialog != null;
public bool IsEnabled
{
get => PluginImplementation.PluginInfo.IsEnabled;
get => Plugin.IsEnabled;
set => Task.Run(() => UpdateEnabled(value));
}
public void OpenSettings()
{
PluginConfigurationDialog configurationViewModel = PluginImplementation.ConfigurationDialog;
PluginConfigurationDialog configurationViewModel = (PluginConfigurationDialog) Plugin.ConfigurationDialog;
if (configurationViewModel == null)
return;
try
{
// Limit to one constructor, there's no need to have more and it complicates things anyway
ConstructorInfo[] constructors = configurationViewModel.Type.GetConstructors();
if (constructors.Length != 1)
throw new ArtemisUIException("Plugin configuration dialogs must have exactly one constructor");
ParameterInfo pluginParameter = constructors.First().GetParameters().First(p => typeof(PluginImplementation).IsAssignableFrom(p.ParameterType));
ConstructorArgument plugin = new ConstructorArgument(pluginParameter.Name, PluginImplementation);
PluginConfigurationViewModel viewModel = (PluginConfigurationViewModel) PluginInfo.Kernel.Get(configurationViewModel.Type, plugin);
PluginConfigurationViewModel viewModel = (PluginConfigurationViewModel) Plugin.Kernel.Get(configurationViewModel.Type);
_windowManager.ShowDialog(new PluginSettingsWindowViewModel(viewModel, Icon));
}
catch (Exception e)
@@ -117,49 +94,22 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
}
}
public void ShowLoadException()
protected override void OnInitialActivate()
{
if (PluginInfo.LoadException == null)
return;
Plugin.FeatureAdded += PluginOnFeatureAdded;
Plugin.FeatureRemoved += PluginOnFeatureRemoved;
foreach (PluginFeature pluginFeature in Plugin.Features)
Items.Add(_settingsVmFactory.CreatePluginFeatureViewModel(pluginFeature));
_dialogService.ShowExceptionDialog("The plugin failed to load: " + PluginInfo.LoadException.Message, PluginInfo.LoadException);
base.OnInitialActivate();
}
public async Task Restart()
protected override void OnClose()
{
_logger.Debug("Restarting for device provider disable {pluginInfo}", PluginImplementation.PluginInfo);
Plugin.FeatureAdded -= PluginOnFeatureAdded;
Plugin.FeatureRemoved -= PluginOnFeatureRemoved;
// Give the logger a chance to write, might not always be enough but oh well
await Task.Delay(500);
Core.Utilities.Shutdown(2, true);
}
private PackIconKind GetIconKind()
{
if (PluginInfo.Icon != null)
{
bool parsedIcon = Enum.TryParse<PackIconKind>(PluginInfo.Icon, true, out PackIconKind iconEnum);
if (parsedIcon)
return iconEnum;
}
switch (PluginImplementation)
{
case BaseDataModelExpansion _:
return PackIconKind.TableAdd;
case DeviceProvider _:
return PackIconKind.Devices;
case ProfileModule _:
return PackIconKind.VectorRectangle;
case Module _:
return PackIconKind.GearBox;
case LayerBrushProvider _:
return PackIconKind.Brush;
case LayerEffectProvider _:
return PackIconKind.AutoAwesome;
}
return PackIconKind.Plugin;
base.OnClose();
}
private async Task UpdateEnabled(bool enable)
@@ -170,24 +120,17 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
return;
}
if (!enable && PluginImplementation is DeviceProvider)
{
await DisableDeviceProvider();
return;
}
if (enable)
{
Enabling = true;
NotifyOfPropertyChange(nameof(DisplayLoadFailed));
try
{
_pluginManagementService.EnablePluginImplementation(PluginImplementation);
await Task.Run(() => _pluginManagementService.EnablePlugin(Plugin));
}
catch (Exception e)
{
_snackbarMessageQueue.Enqueue($"Failed to enable plugin {PluginInfo.Name}\r\n{e.Message}", "VIEW LOGS", ShowLogsFolder);
_snackbarMessageQueue.Enqueue($"Failed to enable plugin {Plugin.Info.Name}\r\n{e.Message}", "VIEW LOGS", ShowLogsFolder);
}
finally
{
@@ -195,44 +138,24 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
}
}
else
_pluginManagementService.DisablePluginImplementation(PluginImplementation);
{
_pluginManagementService.DisablePlugin(Plugin);
}
NotifyOfPropertyChange(nameof(IsEnabled));
NotifyOfPropertyChange(nameof(CanOpenSettings));
NotifyOfPropertyChange(nameof(RequiresRestart));
NotifyOfPropertyChange(nameof(DisplayLoadFailed));
}
private async Task DisableDeviceProvider()
private void PluginOnFeatureRemoved(object? sender, PluginFeatureEventArgs e)
{
bool restart = false;
PluginFeatureViewModel viewModel = Items.FirstOrDefault(i => i.Feature == e.PluginFeature);
if (viewModel != null)
Items.Remove(viewModel);
}
// If any plugin already requires a restart, don't ask the user again
bool restartQueued = _pluginManagementService.GetAllPluginInfo().Any(p => p.Plugin != null && !p.IsEnabled && p.Plugin.IsEnabled);
// If the plugin isn't enabled (load failed), it can be disabled without a restart
if (!restartQueued && PluginImplementation.IsEnabled)
{
restart = await _dialogService.ShowConfirmDialog(
"Disable device provider",
"You are disabling a device provider, Artemis has to restart to \r\nfully disable this type of plugin",
"Restart now",
"Restart later"
);
}
_pluginManagementService.DisablePluginImplementation(PluginImplementation);
if (restart)
{
_logger.Debug("Restarting for device provider disable {pluginInfo}", PluginImplementation.PluginInfo);
// Give the logger a chance to write, might not always be enough but oh well
await Task.Delay(500);
Core.Utilities.Shutdown(2, true);
}
NotifyOfPropertyChange(nameof(IsEnabled));
NotifyOfPropertyChange(nameof(RequiresRestart));
NotifyOfPropertyChange(nameof(DisplayLoadFailed));
private void PluginOnFeatureAdded(object? sender, PluginFeatureEventArgs e)
{
Items.Add(_settingsVmFactory.CreatePluginFeatureViewModel(e.PluginFeature));
}
}
}
@@ -23,7 +23,7 @@
Identifier="PluginSettingsDialog"
DialogTheme="Inherit">
<DockPanel>
<controls:AppBar Type="Dense" Title="{Binding ActiveItem.PluginImplementation.PluginInfo.Name}" DockPanel.Dock="Top" Margin="-18 0 0 0" ShowShadow="False">
<controls:AppBar Type="Dense" Title="{Binding ActiveItem.Plugin.Info.Name}" DockPanel.Dock="Top" Margin="-18 0 0 0" ShowShadow="False">
<controls:AppBar.AppIcon>
<materialDesign:PackIcon Kind="{Binding Icon}" Width="20" Height="28" />
</controls:AppBar.AppIcon>
@@ -1,6 +1,5 @@
using System;
using Artemis.Core;
using MaterialDesignThemes.Wpf;
using Artemis.UI.Shared;
using Stylet;
namespace Artemis.UI.Screens.Settings.Tabs.Plugins
@@ -9,12 +8,14 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
{
private readonly PluginConfigurationViewModel _configurationViewModel;
public PluginSettingsWindowViewModel(PluginConfigurationViewModel configurationViewModel, PackIconKind icon)
public PluginSettingsWindowViewModel(PluginConfigurationViewModel configurationViewModel, object icon)
{
_configurationViewModel = configurationViewModel ?? throw new ArgumentNullException(nameof(configurationViewModel));
Icon = icon;
}
public object Icon { get; }
protected override void OnInitialActivate()
{
ActiveItem = _configurationViewModel;
@@ -23,8 +24,6 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
base.OnInitialActivate();
}
public PackIconKind Icon { get; }
private void ActiveItemOnClosed(object sender, CloseEventArgs e)
{
ActiveItem.Closed -= ActiveItemOnClosed;