mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +00:00
Plugin info - Implemented property changed
Plugins - Improved enable failure detection Plugins UI - Show an indicator on plugins that failed to enable Plugins UI - Show a progress indicator on plugins that are enabling UI - Added reusable Snackbar (not the Dutch kind with kroketten)
This commit is contained in:
parent
28bcfcc95a
commit
a47eedf1c2
@ -52,13 +52,30 @@ namespace Artemis.Core.Plugins.Abstract
|
||||
if (enable && !Enabled)
|
||||
{
|
||||
Enabled = true;
|
||||
EnablePlugin();
|
||||
PluginInfo.Enabled = true;
|
||||
|
||||
// If enable failed, put it back in a disabled state
|
||||
try
|
||||
{
|
||||
EnablePlugin();
|
||||
}
|
||||
catch
|
||||
{
|
||||
Enabled = false;
|
||||
PluginInfo.Enabled = false;
|
||||
throw;
|
||||
}
|
||||
|
||||
OnPluginEnabled();
|
||||
}
|
||||
else if (!enable && Enabled)
|
||||
{
|
||||
Enabled = false;
|
||||
PluginInfo.Enabled = false;
|
||||
|
||||
// Even if disable failed, still leave it in a disabled state to avoid more issues
|
||||
DisablePlugin();
|
||||
|
||||
OnPluginDisabled();
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,11 +5,24 @@ using Artemis.Core.Plugins.Abstract;
|
||||
using Artemis.Storage.Entities.Plugins;
|
||||
using McMaster.NETCore.Plugins;
|
||||
using Newtonsoft.Json;
|
||||
using Stylet;
|
||||
|
||||
namespace Artemis.Core.Plugins.Models
|
||||
{
|
||||
public class PluginInfo
|
||||
[JsonObject(MemberSerialization.OptIn)]
|
||||
public class PluginInfo : PropertyChangedBase
|
||||
{
|
||||
private Guid _guid;
|
||||
private string _name;
|
||||
private string _description;
|
||||
private string _icon;
|
||||
private Version _version;
|
||||
private string _main;
|
||||
private DirectoryInfo _directory;
|
||||
private Plugin _instance;
|
||||
private bool _enabled;
|
||||
private bool _lastEnableSuccessful;
|
||||
|
||||
internal PluginInfo()
|
||||
{
|
||||
}
|
||||
@ -18,77 +31,125 @@ namespace Artemis.Core.Plugins.Models
|
||||
/// The plugins GUID
|
||||
/// </summary>
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
public Guid Guid { get; internal set; }
|
||||
public Guid Guid
|
||||
{
|
||||
get => _guid;
|
||||
internal set => SetAndNotify(ref _guid, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The name of the plugin
|
||||
/// </summary>
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
public string Name { get; internal set; }
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
internal set => SetAndNotify(ref _name, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A short description of the plugin
|
||||
/// </summary>
|
||||
public string Description { get; set; }
|
||||
[JsonProperty]
|
||||
public string Description
|
||||
{
|
||||
get => _description;
|
||||
set => SetAndNotify(ref _description, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The plugins display icon that's shown in the settings see <see href="https://materialdesignicons.com" /> for
|
||||
/// available
|
||||
/// icons
|
||||
/// </summary>
|
||||
public string Icon { get; set; }
|
||||
[JsonProperty]
|
||||
public string Icon
|
||||
{
|
||||
get => _icon;
|
||||
set => SetAndNotify(ref _icon, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The version of the plugin
|
||||
/// </summary>
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
public Version Version { get; internal set; }
|
||||
public Version Version
|
||||
{
|
||||
get => _version;
|
||||
internal set => SetAndNotify(ref _version, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The main entry DLL, should contain a class implementing Plugin
|
||||
/// </summary>
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
public string Main { get; internal set; }
|
||||
public string Main
|
||||
{
|
||||
get => _main;
|
||||
internal set => SetAndNotify(ref _main, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The plugins root directory
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public DirectoryInfo Directory { get; internal set; }
|
||||
public DirectoryInfo Directory
|
||||
{
|
||||
get => _directory;
|
||||
internal set => SetAndNotify(ref _directory, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A reference to the type implementing Plugin, available after successful load
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public Plugin Instance { get; internal set; }
|
||||
public Plugin Instance
|
||||
{
|
||||
get => _instance;
|
||||
internal set => SetAndNotify(ref _instance, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the user enabled the plugin or not
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public bool Enabled { get; internal set; }
|
||||
public bool Enabled
|
||||
{
|
||||
get => _enabled;
|
||||
internal set => SetAndNotify(ref _enabled, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the last time the plugin loaded, it loaded correctly
|
||||
/// </summary>
|
||||
public bool LastEnableSuccessful
|
||||
{
|
||||
get => _lastEnableSuccessful;
|
||||
internal set => SetAndNotify(ref _lastEnableSuccessful, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The PluginLoader backing this plugin
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
internal PluginLoader PluginLoader { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The assembly the plugin code lives in
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
internal Assembly Assembly { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The entity representing the plugin
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
internal PluginEntity PluginEntity { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{nameof(Guid)}: {Guid}, {nameof(Name)}: {Name}, {nameof(Version)}: {Version}";
|
||||
return $"{Name} v{Version} - {Guid}";
|
||||
}
|
||||
|
||||
internal void ApplyToEntity()
|
||||
{
|
||||
PluginEntity.Id = Guid;
|
||||
PluginEntity.IsEnabled = Enabled;
|
||||
PluginEntity.LastEnableSuccessful = LastEnableSuccessful;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -137,8 +137,6 @@ namespace Artemis.Core.Services
|
||||
var pluginInfo = JsonConvert.DeserializeObject<PluginInfo>(File.ReadAllText(metadataFile));
|
||||
pluginInfo.Directory = subDirectory;
|
||||
|
||||
_logger.Debug("Loading plugin {pluginInfo}", pluginInfo);
|
||||
OnPluginLoading(new PluginEventArgs(pluginInfo));
|
||||
LoadPlugin(pluginInfo);
|
||||
}
|
||||
catch (Exception e)
|
||||
@ -150,38 +148,22 @@ namespace Artemis.Core.Services
|
||||
// Activate plugins after they are all loaded
|
||||
foreach (var pluginInfo in _plugins.Where(p => p.Enabled))
|
||||
{
|
||||
if (!pluginInfo.PluginEntity.LastEnableSuccessful)
|
||||
if (!pluginInfo.LastEnableSuccessful)
|
||||
{
|
||||
pluginInfo.Enabled = false;
|
||||
_logger.Warning("Plugin failed to load last time, disabling it now to avoid instability. Plugin info: {pluginInfo}", pluginInfo);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Mark this as false until the plugin enabled successfully and save it in case the plugin drags us down into a crash
|
||||
pluginInfo.PluginEntity.LastEnableSuccessful = false;
|
||||
_pluginRepository.SavePlugin(pluginInfo.PluginEntity);
|
||||
|
||||
var threwException = false;
|
||||
try
|
||||
{
|
||||
_logger.Debug("Enabling plugin {pluginInfo}", pluginInfo);
|
||||
pluginInfo.Instance.SetEnabled(true);
|
||||
EnablePlugin(pluginInfo.Instance);
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (Exception)
|
||||
{
|
||||
_logger.Warning(new ArtemisPluginException(pluginInfo, "Failed to enable plugin", e), "Plugin exception");
|
||||
pluginInfo.Enabled = false;
|
||||
threwException = true;
|
||||
// ignored, logged in EnablePlugin
|
||||
}
|
||||
|
||||
// We got this far so the plugin enabled and we didn't crash horribly, yay
|
||||
if (!threwException)
|
||||
{
|
||||
pluginInfo.PluginEntity.LastEnableSuccessful = true;
|
||||
_pluginRepository.SavePlugin(pluginInfo.PluginEntity);
|
||||
}
|
||||
|
||||
OnPluginEnabled(new PluginEventArgs(pluginInfo));
|
||||
|
||||
}
|
||||
|
||||
LoadingPlugins = false;
|
||||
@ -213,16 +195,20 @@ namespace Artemis.Core.Services
|
||||
{
|
||||
lock (_plugins)
|
||||
{
|
||||
_logger.Debug("Loading plugin {pluginInfo}", pluginInfo);
|
||||
OnPluginLoading(new PluginEventArgs(pluginInfo));
|
||||
|
||||
// Unload the plugin first if it is already loaded
|
||||
if (_plugins.Contains(pluginInfo))
|
||||
UnloadPlugin(pluginInfo);
|
||||
|
||||
var pluginEntity = _pluginRepository.GetPluginByGuid(pluginInfo.Guid);
|
||||
if (pluginEntity == null)
|
||||
pluginEntity = new PluginEntity {PluginGuid = pluginInfo.Guid, IsEnabled = true, LastEnableSuccessful = true};
|
||||
pluginEntity = new PluginEntity {Id = pluginInfo.Guid, IsEnabled = true, LastEnableSuccessful = true};
|
||||
|
||||
pluginInfo.PluginEntity = pluginEntity;
|
||||
pluginInfo.Enabled = pluginEntity.IsEnabled;
|
||||
pluginInfo.LastEnableSuccessful = pluginEntity.LastEnableSuccessful;
|
||||
|
||||
var mainFile = Path.Combine(pluginInfo.Directory.FullName, pluginInfo.Main);
|
||||
if (!File.Exists(mainFile))
|
||||
@ -310,28 +296,37 @@ namespace Artemis.Core.Services
|
||||
|
||||
public void EnablePlugin(Plugin plugin)
|
||||
{
|
||||
plugin.PluginInfo.Enabled = true;
|
||||
plugin.PluginInfo.PluginEntity.IsEnabled = true;
|
||||
plugin.PluginInfo.PluginEntity.LastEnableSuccessful = false;
|
||||
_pluginRepository.SavePlugin(plugin.PluginInfo.PluginEntity);
|
||||
lock (_plugins)
|
||||
{
|
||||
_logger.Debug("Enabling plugin {pluginInfo}", plugin.PluginInfo);
|
||||
|
||||
var threwException = false;
|
||||
try
|
||||
{
|
||||
plugin.SetEnabled(true);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Warning(new ArtemisPluginException(plugin.PluginInfo, "Failed to enable plugin", e), "Plugin exception");
|
||||
plugin.PluginInfo.Enabled = false;
|
||||
threwException = true;
|
||||
}
|
||||
plugin.PluginInfo.LastEnableSuccessful = false;
|
||||
plugin.PluginInfo.ApplyToEntity();
|
||||
|
||||
// We got this far so the plugin enabled and we didn't crash horribly, yay
|
||||
if (!threwException)
|
||||
{
|
||||
plugin.PluginInfo.PluginEntity.LastEnableSuccessful = true;
|
||||
_pluginRepository.SavePlugin(plugin.PluginInfo.PluginEntity);
|
||||
|
||||
try
|
||||
{
|
||||
plugin.SetEnabled(true);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Warning(new ArtemisPluginException(plugin.PluginInfo, "Exception during SetEnabled(true)", e), "Failed to enable plugin");
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// We got this far so the plugin enabled and we didn't crash horribly, yay
|
||||
if (plugin.PluginInfo.Enabled)
|
||||
{
|
||||
plugin.PluginInfo.LastEnableSuccessful = true;
|
||||
plugin.PluginInfo.ApplyToEntity();
|
||||
|
||||
_pluginRepository.SavePlugin(plugin.PluginInfo.PluginEntity);
|
||||
|
||||
_logger.Debug("Successfully enabled plugin {pluginInfo}", plugin.PluginInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OnPluginEnabled(new PluginEventArgs(plugin.PluginInfo));
|
||||
@ -339,18 +334,29 @@ namespace Artemis.Core.Services
|
||||
|
||||
public void DisablePlugin(Plugin plugin)
|
||||
{
|
||||
plugin.PluginInfo.Enabled = false;
|
||||
plugin.PluginInfo.PluginEntity.IsEnabled = false;
|
||||
_pluginRepository.SavePlugin(plugin.PluginInfo.PluginEntity);
|
||||
|
||||
// Device providers cannot be disabled at runtime, restart the application
|
||||
if (plugin is DeviceProvider)
|
||||
lock (_plugins)
|
||||
{
|
||||
CurrentProcessUtilities.Shutdown(2, true);
|
||||
return;
|
||||
}
|
||||
_logger.Debug("Disabling plugin {pluginInfo}", plugin.PluginInfo);
|
||||
|
||||
plugin.SetEnabled(false);
|
||||
// Device providers cannot be disabled at runtime, restart the application
|
||||
if (plugin is DeviceProvider)
|
||||
{
|
||||
// Don't call SetEnabled(false) but simply update enabled state and save it
|
||||
plugin.PluginInfo.Enabled = false;
|
||||
plugin.PluginInfo.ApplyToEntity();
|
||||
_pluginRepository.SavePlugin(plugin.PluginInfo.PluginEntity);
|
||||
|
||||
_logger.Debug("Shutting down for device provider disable {pluginInfo}", plugin.PluginInfo);
|
||||
CurrentProcessUtilities.Shutdown(2, true);
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.SetEnabled(false);
|
||||
plugin.PluginInfo.ApplyToEntity();
|
||||
_pluginRepository.SavePlugin(plugin.PluginInfo.PluginEntity);
|
||||
|
||||
_logger.Debug("Successfully disabled plugin {pluginInfo}", plugin.PluginInfo);
|
||||
}
|
||||
|
||||
OnPluginDisabled(new PluginEventArgs(plugin.PluginInfo));
|
||||
}
|
||||
|
||||
@ -8,7 +8,6 @@ namespace Artemis.Storage.Entities.Plugins
|
||||
public class PluginEntity
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid PluginGuid { get; set; }
|
||||
|
||||
public bool IsEnabled { get; set; }
|
||||
public bool LastEnableSuccessful { get; set; }
|
||||
|
||||
@ -3,7 +3,7 @@ using LiteDB;
|
||||
|
||||
namespace Artemis.Storage.Migrations
|
||||
{
|
||||
public class AttributeBasedPropertiesMigration : IStorageMigration
|
||||
public class M1AttributeBasedPropertiesMigration : IStorageMigration
|
||||
{
|
||||
public int UserVersion => 1;
|
||||
|
||||
@ -4,7 +4,7 @@ using LiteDB;
|
||||
|
||||
namespace Artemis.Storage.Migrations
|
||||
{
|
||||
public class ProfileEntitiesEnabledMigration : IStorageMigration
|
||||
public class M2ProfileEntitiesEnabledMigration : IStorageMigration
|
||||
{
|
||||
public int UserVersion => 2;
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
using Artemis.Storage.Migrations.Interfaces;
|
||||
using LiteDB;
|
||||
|
||||
namespace Artemis.Storage.Migrations
|
||||
{
|
||||
public class M3PluginEntitiesIndexChangesMigration : IStorageMigration
|
||||
{
|
||||
public int UserVersion => 3;
|
||||
|
||||
public void Apply(LiteRepository repository)
|
||||
{
|
||||
if (repository.Database.CollectionExists("PluginEntity"))
|
||||
repository.Database.DropCollection("PluginEntity");
|
||||
if (repository.Database.CollectionExists("PluginSettingEntity"))
|
||||
repository.Database.DropCollection("PluginSettingEntity");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -13,9 +13,7 @@ namespace Artemis.Storage.Repositories
|
||||
{
|
||||
_repository = repository;
|
||||
|
||||
_repository.Database.GetCollection<PluginEntity>().EnsureIndex(s => s.PluginGuid);
|
||||
_repository.Database.GetCollection<PluginSettingEntity>().EnsureIndex(s => s.Name);
|
||||
_repository.Database.GetCollection<PluginSettingEntity>().EnsureIndex(s => s.PluginGuid);
|
||||
_repository.Database.GetCollection<PluginSettingEntity>().EnsureIndex(s => new {s.Name, s.PluginGuid}, true);
|
||||
}
|
||||
|
||||
public void AddPlugin(PluginEntity pluginEntity)
|
||||
@ -25,12 +23,13 @@ namespace Artemis.Storage.Repositories
|
||||
|
||||
public PluginEntity GetPluginByGuid(Guid pluginGuid)
|
||||
{
|
||||
return _repository.FirstOrDefault<PluginEntity>(p => p.PluginGuid == pluginGuid);
|
||||
return _repository.FirstOrDefault<PluginEntity>(p => p.Id == pluginGuid);
|
||||
}
|
||||
|
||||
public void SavePlugin(PluginEntity pluginEntity)
|
||||
{
|
||||
_repository.Upsert(pluginEntity);
|
||||
_repository.Database.Checkpoint();
|
||||
}
|
||||
|
||||
public void AddSetting(PluginSettingEntity pluginSettingEntity)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using Artemis.UI.Shared.Ninject.Factories;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using MaterialDesignThemes.Wpf;
|
||||
using Ninject.Extensions.Conventions;
|
||||
using Ninject.Modules;
|
||||
|
||||
@ -32,6 +33,8 @@ namespace Artemis.UI.Shared.Ninject
|
||||
.BindAllInterfaces()
|
||||
.Configure(c => c.InSingletonScope());
|
||||
});
|
||||
|
||||
Kernel.Bind<ISnackbarMessageQueue>().ToConstant(new SnackbarMessageQueue()).InSingletonScope();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -70,6 +70,10 @@
|
||||
<Setter Property="IsTabStop" Value="False" />
|
||||
<Setter Property="Focusable" Value="False" />
|
||||
</Style>
|
||||
<Style TargetType="GridSplitter" BasedOn="{StaticResource MaterialDesignGridSplitter}">
|
||||
<Setter Property="IsTabStop" Value="False" />
|
||||
<Setter Property="Focusable" Value="False" />
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
@ -13,6 +13,7 @@ using Artemis.UI.Screens.Module.ProfileEditor.ProfileTree.TreeItem;
|
||||
using Artemis.UI.Screens.Module.ProfileEditor.Visualization;
|
||||
using Artemis.UI.Screens.Settings.Debug;
|
||||
using Artemis.UI.Screens.Settings.Tabs.Devices;
|
||||
using Artemis.UI.Screens.Settings.Tabs.Plugins;
|
||||
using Stylet;
|
||||
|
||||
namespace Artemis.UI.Ninject.Factories
|
||||
@ -26,6 +27,11 @@ namespace Artemis.UI.Ninject.Factories
|
||||
ModuleRootViewModel Create(Module module);
|
||||
}
|
||||
|
||||
public interface IPluginSettingsVmFactory : IVmFactory
|
||||
{
|
||||
PluginSettingsViewModel Create(Plugin plugin);
|
||||
}
|
||||
|
||||
public interface IDeviceSettingsVmFactory : IVmFactory
|
||||
{
|
||||
DeviceSettingsViewModel Create(ArtemisDevice device);
|
||||
|
||||
@ -66,8 +66,9 @@
|
||||
<RowDefinition Height="28" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="1.5*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Left side -->
|
||||
@ -157,8 +158,11 @@
|
||||
</materialDesign:DialogHost>
|
||||
</Grid>
|
||||
|
||||
<!-- Resize -->
|
||||
<GridSplitter Grid.Row="0" Grid.RowSpan="2" Grid.Column="1" Width="15" HorizontalAlignment="Stretch" Cursor="SizeWE" Margin="-15 0" Background="Transparent" />
|
||||
|
||||
<!-- Right side -->
|
||||
<Grid Grid.Row="0" Grid.Column="1">
|
||||
<Grid Grid.Row="0" Grid.Column="2">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="56" />
|
||||
<RowDefinition Height="*" />
|
||||
@ -214,72 +218,124 @@
|
||||
|
||||
<!-- Bottom row, a bit hacky but has a ZIndex of 2 to cut off the time caret that overlaps the entire timeline -->
|
||||
<Grid Grid.Row="1"
|
||||
Grid.ColumnSpan="2"
|
||||
Grid.Column="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Panel.ZIndex="2"
|
||||
Background="{DynamicResource MaterialDesignCardBackground}">
|
||||
<!-- Selected layer controls -->
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid Grid.Column="0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Grid.Column="0"
|
||||
Orientation="Horizontal"
|
||||
Margin="6"
|
||||
Visibility="{Binding SelectedLayer, Converter={StaticResource NullToVisibilityConverter}}">
|
||||
<materialDesign:PackIcon Kind="Layers" Width="16" />
|
||||
<materialDesign:PackIcon Kind="{Binding SelectedLayer.LayerBrush.Descriptor.Icon}"
|
||||
Width="16"
|
||||
Margin="5 0 0 0"
|
||||
ToolTip="{Binding SelectedLayer.LayerBrush.Descriptor.DisplayName, Mode=OneWay}"
|
||||
Background="Transparent"
|
||||
Visibility="{Binding SelectedLayer.LayerBrush, Converter={StaticResource NullToVisibilityConverter}}" />
|
||||
<TextBlock Text="{Binding SelectedLayer.Name}" Margin="5 0 0 0" />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="0"
|
||||
Orientation="Horizontal"
|
||||
Margin="6"
|
||||
Visibility="{Binding SelectedFolder, Converter={StaticResource NullToVisibilityConverter}}">
|
||||
<materialDesign:PackIcon Kind="Folder" Width="16" />
|
||||
<TextBlock Text="{Binding SelectedFolder.Name}" Margin="5 0 0 0" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1"
|
||||
Style="{StaticResource MaterialDesignFlatMidBgButton}"
|
||||
Margin="0 -2"
|
||||
Padding="10 0"
|
||||
Height="20"
|
||||
Width="110"
|
||||
ToolTip="Select an effect to add"
|
||||
VerticalAlignment="Center"
|
||||
Visibility="{Binding PropertyTreeVisible, Converter={x:Static s:BoolToVisibilityConverter.Instance}}"
|
||||
Command="{x:Static materialDesign:Transitioner.MoveLastCommand}"
|
||||
CommandTarget="{Binding ElementName=TransitionCommandAnchor}">
|
||||
<TextBlock FontSize="10">
|
||||
ADD EFFECT
|
||||
</TextBlock>
|
||||
</Button>
|
||||
<Button Grid.Column="1"
|
||||
Style="{StaticResource MaterialDesignFlatMidBgButton}"
|
||||
Margin="0 -2"
|
||||
Padding="10 0"
|
||||
Height="20"
|
||||
Width="110"
|
||||
ToolTip="Show the layer/folder properties"
|
||||
VerticalAlignment="Center"
|
||||
Visibility="{Binding PropertyTreeVisible, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}}"
|
||||
Command="{x:Static materialDesign:Transitioner.MoveFirstCommand}"
|
||||
CommandTarget="{Binding ElementName=TransitionCommandAnchor}">
|
||||
<TextBlock FontSize="10">
|
||||
SHOW PROPERTIES
|
||||
</TextBlock>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<StackPanel Grid.Column="0"
|
||||
Orientation="Horizontal"
|
||||
Margin="6"
|
||||
Visibility="{Binding SelectedLayer, Converter={StaticResource NullToVisibilityConverter}}">
|
||||
<materialDesign:PackIcon Kind="Layers" Width="16" />
|
||||
<materialDesign:PackIcon Kind="{Binding SelectedLayer.LayerBrush.Descriptor.Icon}"
|
||||
Width="16"
|
||||
Margin="5 0 0 0"
|
||||
ToolTip="{Binding SelectedLayer.LayerBrush.Descriptor.DisplayName, Mode=OneWay}"
|
||||
Background="Transparent"
|
||||
Visibility="{Binding SelectedLayer.LayerBrush, Converter={StaticResource NullToVisibilityConverter}}" />
|
||||
<TextBlock Text="{Binding SelectedLayer.Name}" Margin="5 0 0 0" />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="0"
|
||||
Orientation="Horizontal"
|
||||
Margin="6"
|
||||
Visibility="{Binding SelectedFolder, Converter={StaticResource NullToVisibilityConverter}}">
|
||||
<materialDesign:PackIcon Kind="Folder" Width="16" />
|
||||
<TextBlock Text="{Binding SelectedFolder.Name}" Margin="5 0 0 0" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1"
|
||||
Style="{StaticResource MaterialDesignFlatMidBgButton}"
|
||||
Margin="0 -2 5 -2"
|
||||
Padding="10 0"
|
||||
Height="20"
|
||||
Width="110"
|
||||
ToolTip="Select an effect to add"
|
||||
VerticalAlignment="Center"
|
||||
Visibility="{Binding PropertyTreeVisible, Converter={x:Static s:BoolToVisibilityConverter.Instance}}"
|
||||
Command="{x:Static materialDesign:Transitioner.MoveLastCommand}"
|
||||
CommandTarget="{Binding ElementName=TransitionCommandAnchor}">
|
||||
<TextBlock FontSize="11">
|
||||
ADD EFFECT
|
||||
</TextBlock>
|
||||
</Button>
|
||||
<Button Grid.Column="1"
|
||||
Style="{StaticResource MaterialDesignFlatMidBgButton}"
|
||||
Margin="0 -2 5 -2"
|
||||
Padding="10 0"
|
||||
Height="20"
|
||||
Width="110"
|
||||
ToolTip="Show the layer/folder properties"
|
||||
VerticalAlignment="Center"
|
||||
Visibility="{Binding PropertyTreeVisible, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}}"
|
||||
Command="{x:Static materialDesign:Transitioner.MoveFirstCommand}"
|
||||
CommandTarget="{Binding ElementName=TransitionCommandAnchor}">
|
||||
<TextBlock FontSize="11">
|
||||
SHOW PROPERTIES
|
||||
</TextBlock>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="1"
|
||||
Grid.Column="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
Panel.ZIndex="2"
|
||||
Background="{DynamicResource MaterialDesignCardBackground}">
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ListBox Style="{StaticResource MaterialDesignToolToggleListBox}" SelectedIndex="0" Height="20" Margin="5 0 0 0">
|
||||
<ListBoxItem Padding="10 0">
|
||||
<ListBoxItem.ToolTip>
|
||||
<ToolTip Placement="Top" VerticalOffset="-5">
|
||||
<StackPanel>
|
||||
<TextBlock>Select the <Run FontWeight="Bold">enter</Run> timeline</TextBlock>
|
||||
<TextBlock>Played when the folder/layer starts displaying (condition is met)</TextBlock>
|
||||
</StackPanel>
|
||||
</ToolTip>
|
||||
</ListBoxItem.ToolTip>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<materialDesign:PackIcon Kind="RayStart" Width="20" Height="20" Margin="0 -4" />
|
||||
<TextBlock Margin="5 0 0 0" FontSize="11">ENTER</TextBlock>
|
||||
</StackPanel>
|
||||
</ListBoxItem>
|
||||
<ListBoxItem Padding="10 0">
|
||||
<ListBoxItem.ToolTip>
|
||||
<ToolTip Placement="Top" VerticalOffset="-5">
|
||||
<StackPanel>
|
||||
<TextBlock>Select the <Run FontWeight="Bold">main</Run> timeline</TextBlock>
|
||||
<TextBlock>Played after the enter timeline finishes, either on repeat or once</TextBlock>
|
||||
</StackPanel>
|
||||
</ToolTip>
|
||||
</ListBoxItem.ToolTip>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<materialDesign:PackIcon Kind="RayStartEnd" Width="20" Height="20" Margin="0 -4" />
|
||||
<TextBlock Margin="5 0 0 0" FontSize="11">MAIN</TextBlock>
|
||||
</StackPanel>
|
||||
</ListBoxItem>
|
||||
<ListBoxItem Padding="10 0">
|
||||
<ListBoxItem.ToolTip>
|
||||
<ToolTip Placement="Top" VerticalOffset="-5">
|
||||
<StackPanel>
|
||||
<TextBlock>Select the <Run FontWeight="Bold">exit</Run> timeline</TextBlock>
|
||||
<TextBlock>Played when the folder/layer stops displaying (conditon no longer met)</TextBlock>
|
||||
</StackPanel>
|
||||
</ToolTip>
|
||||
|
||||
</ListBoxItem.ToolTip>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<materialDesign:PackIcon Kind="RayEnd" Width="20" Height="20" Margin="0 -4" />
|
||||
<TextBlock Margin="5 0 0 0" FontSize="11">EXIT</TextBlock>
|
||||
</StackPanel>
|
||||
</ListBoxItem>
|
||||
</ListBox>
|
||||
|
||||
<!-- Zoom control -->
|
||||
<Slider Grid.Column="1"
|
||||
@ -290,6 +346,7 @@
|
||||
Maximum="350"
|
||||
Value="{Binding ProfileEditorService.PixelsPerSecond}"
|
||||
Width="319" />
|
||||
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@ -47,27 +47,30 @@
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</mde:MaterialWindow.Resources>
|
||||
<materialDesign:DialogHost Identifier="RootDialog" DialogTheme="Inherit">
|
||||
<materialDesign:DrawerHost IsLeftDrawerOpen="{Binding IsSidebarVisible}">
|
||||
<materialDesign:DrawerHost.LeftDrawerContent>
|
||||
<ContentControl s:View.Model="{Binding SidebarViewModel}" Width="220" ClipToBounds="False" />
|
||||
</materialDesign:DrawerHost.LeftDrawerContent>
|
||||
<DockPanel>
|
||||
<mde:AppBar Type="Dense"
|
||||
IsNavigationDrawerOpen="{Binding IsSidebarVisible, Mode=TwoWay}"
|
||||
Title="{Binding ActiveItem.DisplayName}"
|
||||
ShowNavigationDrawerButton="True"
|
||||
DockPanel.Dock="Top">
|
||||
<StackPanel>
|
||||
<!-- Bug: materialDesign:RippleAssist.RippleOnTop doesn't look as nice but otherwise it doesn't work at all, not sure why -->
|
||||
<Button Style="{StaticResource MaterialDesignIconForegroundButton}" ToolTip="Open debugger" Command="{s:Action ShowDebugger}"
|
||||
materialDesign:RippleAssist.RippleOnTop="True">
|
||||
<materialDesign:PackIcon Kind="Matrix" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</mde:AppBar>
|
||||
<ContentControl s:View.Model="{Binding ActiveItem}" Style="{StaticResource InitializingFade}" />
|
||||
</DockPanel>
|
||||
</materialDesign:DrawerHost>
|
||||
<materialDesign:DialogHost Identifier="RootDialog" DialogTheme="Inherit" SnackbarMessageQueue="{Binding MainMessageQueue}">
|
||||
<Grid>
|
||||
<materialDesign:DrawerHost IsLeftDrawerOpen="{Binding IsSidebarVisible}">
|
||||
<materialDesign:DrawerHost.LeftDrawerContent>
|
||||
<ContentControl s:View.Model="{Binding SidebarViewModel}" Width="220" ClipToBounds="False" />
|
||||
</materialDesign:DrawerHost.LeftDrawerContent>
|
||||
<DockPanel>
|
||||
<mde:AppBar Type="Dense"
|
||||
IsNavigationDrawerOpen="{Binding IsSidebarVisible, Mode=TwoWay}"
|
||||
Title="{Binding ActiveItem.DisplayName}"
|
||||
ShowNavigationDrawerButton="True"
|
||||
DockPanel.Dock="Top">
|
||||
<StackPanel>
|
||||
<!-- Bug: materialDesign:RippleAssist.RippleOnTop doesn't look as nice but otherwise it doesn't work at all, not sure why -->
|
||||
<Button Style="{StaticResource MaterialDesignIconForegroundButton}" ToolTip="Open debugger" Command="{s:Action ShowDebugger}"
|
||||
materialDesign:RippleAssist.RippleOnTop="True">
|
||||
<materialDesign:PackIcon Kind="Matrix" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</mde:AppBar>
|
||||
<ContentControl s:View.Model="{Binding ActiveItem}" Style="{StaticResource InitializingFade}" />
|
||||
</DockPanel>
|
||||
</materialDesign:DrawerHost>
|
||||
<materialDesign:Snackbar x:Name="MainSnackbar" MessageQueue="{Binding MainMessageQueue}" />
|
||||
</Grid>
|
||||
</materialDesign:DialogHost>
|
||||
</mde:MaterialWindow>
|
||||
@ -27,10 +27,11 @@ namespace Artemis.UI.Screens
|
||||
private readonly Timer _titleUpdateTimer;
|
||||
private bool _lostFocus;
|
||||
|
||||
public RootViewModel(IEventAggregator eventAggregator, SidebarViewModel sidebarViewModel, ISettingsService settingsService, ICoreService coreService,
|
||||
IDebugService debugService)
|
||||
public RootViewModel(IEventAggregator eventAggregator, SidebarViewModel sidebarViewModel, ISettingsService settingsService, ICoreService coreService,
|
||||
IDebugService debugService, ISnackbarMessageQueue snackbarMessageQueue)
|
||||
{
|
||||
SidebarViewModel = sidebarViewModel;
|
||||
MainMessageQueue = snackbarMessageQueue;
|
||||
_eventAggregator = eventAggregator;
|
||||
_coreService = coreService;
|
||||
_debugService = debugService;
|
||||
@ -49,11 +50,11 @@ namespace Artemis.UI.Screens
|
||||
}
|
||||
|
||||
public SidebarViewModel SidebarViewModel { get; }
|
||||
public ISnackbarMessageQueue MainMessageQueue { get; set; }
|
||||
public bool IsSidebarVisible { get; set; }
|
||||
public bool ActiveItemReady { get; set; }
|
||||
|
||||
public string WindowTitle { get; set; }
|
||||
|
||||
|
||||
public void WindowDeactivated()
|
||||
{
|
||||
var windowState = ((Window) View).WindowState;
|
||||
|
||||
@ -27,20 +27,20 @@ namespace Artemis.UI.Screens.Settings
|
||||
private readonly IDialogService _dialogService;
|
||||
private readonly IPluginService _pluginService;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly IPluginSettingsVmFactory _pluginSettingsVmFactory;
|
||||
private readonly ISurfaceService _surfaceService;
|
||||
private readonly IWindowManager _windowManager;
|
||||
|
||||
public SettingsViewModel(ISurfaceService surfaceService, IPluginService pluginService, IDialogService dialogService, IWindowManager windowManager,
|
||||
IDebugService debugService, ISettingsService settingsService, IDeviceSettingsVmFactory deviceSettingsVmFactory)
|
||||
public SettingsViewModel(ISurfaceService surfaceService, IPluginService pluginService, IDialogService dialogService, IDebugService debugService,
|
||||
ISettingsService settingsService, IPluginSettingsVmFactory pluginSettingsVmFactory, IDeviceSettingsVmFactory deviceSettingsVmFactory)
|
||||
{
|
||||
DisplayName = "Settings";
|
||||
|
||||
_surfaceService = surfaceService;
|
||||
_pluginService = pluginService;
|
||||
_dialogService = dialogService;
|
||||
_windowManager = windowManager;
|
||||
_debugService = debugService;
|
||||
_settingsService = settingsService;
|
||||
_pluginSettingsVmFactory = pluginSettingsVmFactory;
|
||||
_deviceSettingsVmFactory = deviceSettingsVmFactory;
|
||||
|
||||
DeviceSettingsViewModels = new BindableCollection<DeviceSettingsViewModel>();
|
||||
@ -189,7 +189,7 @@ namespace Artemis.UI.Screens.Settings
|
||||
DeviceSettingsViewModels.AddRange(_surfaceService.ActiveSurface.Devices.Select(d => _deviceSettingsVmFactory.Create(d)));
|
||||
|
||||
Plugins.Clear();
|
||||
Plugins.AddRange(_pluginService.GetAllPluginInfo().Select(p => new PluginSettingsViewModel(p.Instance, _windowManager, _dialogService, _pluginService)));
|
||||
Plugins.AddRange(_pluginService.GetAllPluginInfo().Select(p => _pluginSettingsVmFactory.Create(p.Instance)));
|
||||
|
||||
base.OnInitialActivate();
|
||||
}
|
||||
|
||||
@ -36,6 +36,7 @@
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="80" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
@ -43,9 +44,16 @@
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<materialDesign:PackIcon Kind="{Binding Icon}" Width="48" Height="48" Grid.Row="0" Grid.RowSpan="2" HorizontalAlignment="Center" />
|
||||
<TextBlock Style="{StaticResource MaterialDesignTextBlock}" Text="{Binding PluginInfo.Name}" Grid.Column="1" Grid.Row="0" />
|
||||
|
||||
<TextBlock Grid.Column="1" Grid.Row="0" Style="{StaticResource MaterialDesignTextBlock}" Text="{Binding PluginInfo.Name}" />
|
||||
<materialDesign:Card Grid.Column="2" Grid.Row="0" Background="#FF4343" Height="22" Padding="4" Margin="0 -18 0 0"
|
||||
Visibility="{Binding PluginInfo.LastEnableSuccessful, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}, Mode=OneWay}">
|
||||
LOAD FAILED
|
||||
</materialDesign:Card>
|
||||
|
||||
<TextBlock Grid.Column="1"
|
||||
Grid.Row="1"
|
||||
Grid.ColumnSpan="2"
|
||||
TextWrapping="Wrap"
|
||||
Text="{Binding PluginInfo.Description}"
|
||||
Style="{StaticResource MaterialDesignTextBlock}"
|
||||
@ -56,15 +64,17 @@
|
||||
<Button Style="{StaticResource MaterialDesignOutlinedButton}" ToolTip="MaterialDesignOutlinedButton" Margin="4" s:View.ActionTarget="{Binding}" Command="{s:Action OpenSettings}">
|
||||
SETTINGS
|
||||
</Button>
|
||||
<!-- <Button Style="{StaticResource MaterialDesignOutlinedButton}" ToolTip="MaterialDesignOutlinedButton" Margin="4"> -->
|
||||
<!-- SETTINGS -->
|
||||
<!-- </Button> -->
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Row="1" Grid.Column="1" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="8">
|
||||
<StackPanel Grid.Row="1" Grid.Column="1" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="8"
|
||||
Visibility="{Binding Enabling, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}, Mode=OneWay}">
|
||||
<CheckBox Style="{StaticResource MaterialDesignCheckBox}" IsChecked="{Binding IsEnabled}">
|
||||
Plugin enabled
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Row="1" Grid.Column="1" 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>
|
||||
</materialDesign:Card>
|
||||
</UserControl>
|
||||
@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core.Plugins.Abstract;
|
||||
using Artemis.Core.Plugins.Models;
|
||||
@ -13,9 +15,11 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
|
||||
{
|
||||
private readonly IDialogService _dialogService;
|
||||
private readonly IPluginService _pluginService;
|
||||
private readonly ISnackbarMessageQueue _snackbarMessageQueue;
|
||||
private readonly IWindowManager _windowManager;
|
||||
|
||||
public PluginSettingsViewModel(Plugin plugin, IWindowManager windowManager, IDialogService dialogService, IPluginService pluginService)
|
||||
public PluginSettingsViewModel(Plugin plugin, IWindowManager windowManager, IDialogService dialogService, IPluginService pluginService,
|
||||
ISnackbarMessageQueue snackbarMessageQueue)
|
||||
{
|
||||
Plugin = plugin;
|
||||
PluginInfo = plugin.PluginInfo;
|
||||
@ -23,6 +27,7 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
|
||||
_windowManager = windowManager;
|
||||
_dialogService = dialogService;
|
||||
_pluginService = pluginService;
|
||||
_snackbarMessageQueue = snackbarMessageQueue;
|
||||
}
|
||||
|
||||
public Plugin Plugin { get; set; }
|
||||
@ -40,6 +45,8 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
|
||||
|
||||
public bool CanOpenSettings => IsEnabled && Plugin.HasConfigurationViewModel;
|
||||
|
||||
public bool Enabling { get; set; }
|
||||
|
||||
public async Task OpenSettings()
|
||||
{
|
||||
try
|
||||
@ -55,6 +62,18 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ShowLogsFolder()
|
||||
{
|
||||
try
|
||||
{
|
||||
Process.Start(Environment.GetEnvironmentVariable("WINDIR") + @"\explorer.exe", Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs"));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
await _dialogService.ShowExceptionDialog("Welp, we couldn\'t open the logs folder for you", e);
|
||||
}
|
||||
}
|
||||
|
||||
private PackIconKind GetIconKind()
|
||||
{
|
||||
if (PluginInfo.Icon != null)
|
||||
@ -105,7 +124,23 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
|
||||
}
|
||||
|
||||
if (enable)
|
||||
_pluginService.EnablePlugin(Plugin);
|
||||
{
|
||||
Enabling = true;
|
||||
try
|
||||
{
|
||||
_pluginService.EnablePlugin(Plugin);
|
||||
_snackbarMessageQueue.Enqueue($"Enabled plugin {PluginInfo.Name}");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
_snackbarMessageQueue.Enqueue($"Failed to enable plugin {PluginInfo.Name}", "VIEW LOGS", async () => await ShowLogsFolder());
|
||||
}
|
||||
finally
|
||||
{
|
||||
Enabling = false;
|
||||
NotifyOfPropertyChange(() => IsEnabled);
|
||||
}
|
||||
}
|
||||
else
|
||||
_pluginService.DisablePlugin(Plugin);
|
||||
|
||||
|
||||
@ -244,4 +244,5 @@
|
||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EAlwaysTreatStructAsNotReorderableMigration/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=leds/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=leds/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=snackbar/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
Loading…
x
Reference in New Issue
Block a user