1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2026-01-02 10:43:31 +00:00

Merge branch 'development'

This commit is contained in:
Robert 2021-03-11 23:22:31 +01:00
commit 7f66ccaa4d
54 changed files with 1021 additions and 557 deletions

View File

@ -46,7 +46,14 @@ namespace Artemis.Core
/// </summary> /// </summary>
public static readonly BuildInfo BuildInfo = File.Exists(Path.Combine(ApplicationFolder, "buildinfo.json")) public static readonly BuildInfo BuildInfo = File.Exists(Path.Combine(ApplicationFolder, "buildinfo.json"))
? JsonConvert.DeserializeObject<BuildInfo>(File.ReadAllText(Path.Combine(ApplicationFolder, "buildinfo.json"))) ? JsonConvert.DeserializeObject<BuildInfo>(File.ReadAllText(Path.Combine(ApplicationFolder, "buildinfo.json")))
: new BuildInfo(); : new BuildInfo
{
IsLocalBuild = true,
BuildId = 1337,
BuildNumber = 1337,
SourceBranch = "local",
SourceVersion = "local"
};
/// <summary> /// <summary>
/// The plugin used by core components of Artemis /// The plugin used by core components of Artemis

View File

@ -17,4 +17,20 @@ namespace Artemis.Core
/// </summary> /// </summary>
public PluginFeature PluginFeature { get; } public PluginFeature PluginFeature { get; }
} }
/// <summary>
/// Provides data about plugin feature info related events
/// </summary>
public class PluginFeatureInfoEventArgs : EventArgs
{
internal PluginFeatureInfoEventArgs(PluginFeatureInfo pluginFeatureInfo)
{
PluginFeatureInfo = pluginFeatureInfo;
}
/// <summary>
/// Gets the plugin feature this event is related to
/// </summary>
public PluginFeatureInfo PluginFeatureInfo { get; }
}
} }

View File

@ -38,6 +38,9 @@ namespace Artemis.Core
/// </summary> /// </summary>
public DataModelPath? EventPath { get; private set; } public DataModelPath? EventPath { get; private set; }
/// <summary>
/// Gets the last time the event this condition is applied to was triggered
/// </summary>
public DateTime LastTrigger { get; private set; } public DateTime LastTrigger { get; private set; }
/// <summary> /// <summary>
@ -67,7 +70,6 @@ namespace Artemis.Core
// If there are no children, we always evaluate to true whenever the event triggered // If there are no children, we always evaluate to true whenever the event triggered
return true; return true;
} }
/// <summary> /// <summary>

View File

@ -225,7 +225,7 @@ namespace Artemis.Core
{ {
LedEntity ledEntity = new() LedEntity ledEntity = new()
{ {
DeviceIdentifier = artemisLed.Device.RgbDevice.GetDeviceIdentifier(), DeviceIdentifier = artemisLed.Device.Identifier,
LedName = artemisLed.RgbLed.Id.ToString() LedName = artemisLed.RgbLed.Id.ToString()
}; };
LayerEntity.Leds.Add(ledEntity); LayerEntity.Leds.Add(ledEntity);
@ -578,7 +578,7 @@ namespace Artemis.Core
List<ArtemisLed> availableLeds = devices.SelectMany(d => d.Leds).ToList(); List<ArtemisLed> availableLeds = devices.SelectMany(d => d.Leds).ToList();
foreach (LedEntity ledEntity in LayerEntity.Leds) foreach (LedEntity ledEntity in LayerEntity.Leds)
{ {
ArtemisLed? match = availableLeds.FirstOrDefault(a => a.Device.RgbDevice.GetDeviceIdentifier() == ledEntity.DeviceIdentifier && ArtemisLed? match = availableLeds.FirstOrDefault(a => a.Device.Identifier == ledEntity.DeviceIdentifier &&
a.RgbLed.Id.ToString() == ledEntity.LedName); a.RgbLed.Id.ToString() == ledEntity.LedName);
if (match != null) if (match != null)
leds.Add(match); leds.Add(match);

View File

@ -21,6 +21,7 @@ namespace Artemis.Core
internal ArtemisDevice(IRGBDevice rgbDevice, DeviceProvider deviceProvider) internal ArtemisDevice(IRGBDevice rgbDevice, DeviceProvider deviceProvider)
{ {
Identifier = rgbDevice.GetDeviceIdentifier();
DeviceEntity = new DeviceEntity(); DeviceEntity = new DeviceEntity();
RgbDevice = rgbDevice; RgbDevice = rgbDevice;
DeviceProvider = deviceProvider; DeviceProvider = deviceProvider;
@ -45,6 +46,7 @@ namespace Artemis.Core
internal ArtemisDevice(IRGBDevice rgbDevice, DeviceProvider deviceProvider, DeviceEntity deviceEntity) internal ArtemisDevice(IRGBDevice rgbDevice, DeviceProvider deviceProvider, DeviceEntity deviceEntity)
{ {
Identifier = rgbDevice.GetDeviceIdentifier();
DeviceEntity = deviceEntity; DeviceEntity = deviceEntity;
RgbDevice = rgbDevice; RgbDevice = rgbDevice;
DeviceProvider = deviceProvider; DeviceProvider = deviceProvider;
@ -59,6 +61,11 @@ namespace Artemis.Core
ApplyKeyboardLayout(); ApplyKeyboardLayout();
} }
/// <summary>
/// Gets the (hopefully unique and persistent) ID of this device
/// </summary>
public string Identifier { get; }
/// <summary> /// <summary>
/// Gets the rectangle covering the device /// Gets the rectangle covering the device
/// </summary> /// </summary>
@ -336,7 +343,7 @@ namespace Artemis.Core
internal void ApplyToEntity() internal void ApplyToEntity()
{ {
// Other properties are computed // Other properties are computed
DeviceEntity.Id = RgbDevice.GetDeviceIdentifier(); DeviceEntity.Id = Identifier;
DeviceEntity.InputIdentifiers.Clear(); DeviceEntity.InputIdentifiers.Clear();
foreach (ArtemisDeviceInputIdentifier identifier in InputIdentifiers) foreach (ArtemisDeviceInputIdentifier identifier in InputIdentifiers)

View File

@ -46,6 +46,9 @@ namespace Artemis.Core
private set => SetAndNotify(ref _absoluteRectangle, value); private set => SetAndNotify(ref _absoluteRectangle, value);
} }
/// <summary>
/// Gets the layout applied to this LED
/// </summary>
public ArtemisLedLayout? Layout { get; internal set; } public ArtemisLedLayout? Layout { get; internal set; }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -5,6 +5,9 @@ using RGB.NET.Layout;
namespace Artemis.Core namespace Artemis.Core
{ {
/// <summary>
/// Represents a LED layout decorated with extra Artemis-specific data
/// </summary>
public class ArtemisLedLayout public class ArtemisLedLayout
{ {
internal ArtemisLedLayout(ArtemisLayout deviceLayout, ILedLayout led) internal ArtemisLedLayout(ArtemisLayout deviceLayout, ILedLayout led)

View File

@ -29,6 +29,9 @@ namespace Artemis.Core.LayerBrushes
/// </summary> /// </summary>
public ListLedGroup? LedGroup { get; internal set; } public ListLedGroup? LedGroup { get; internal set; }
/// <summary>
/// For internal use only, is public for dependency injection but ignore pl0x
/// </summary>
[Inject] [Inject]
public IRgbService? RgbService { get; set; } public IRgbService? RgbService { get; set; }
@ -38,6 +41,26 @@ namespace Artemis.Core.LayerBrushes
/// <returns>Your RGB.NET effect</returns> /// <returns>Your RGB.NET effect</returns>
public abstract IBrush GetBrush(); public abstract IBrush GetBrush();
#region IDisposable
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (RgbService == null)
throw new ArtemisCoreException("Cannot dispose RGB.NET layer brush because RgbService is not set");
Layer.RenderPropertiesUpdated -= LayerOnRenderPropertiesUpdated;
LedGroup?.Detach();
LedGroup = null;
}
base.Dispose(disposing);
}
#endregion
internal void UpdateLedGroup() internal void UpdateLedGroup()
{ {
if (LedGroup == null) if (LedGroup == null)
@ -67,26 +90,6 @@ namespace Artemis.Core.LayerBrushes
UpdateLedGroup(); UpdateLedGroup();
} }
#region IDisposable
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (RgbService == null)
throw new ArtemisCoreException("Cannot dispose RGB.NET layer brush because RgbService is not set");
Layer.RenderPropertiesUpdated -= LayerOnRenderPropertiesUpdated;
LedGroup?.Detach();
LedGroup = null;
}
base.Dispose(disposing);
}
#endregion
// Not used in this effect type // Not used in this effect type
internal override void InternalRender(SKCanvas canvas, SKRect bounds, SKPaint paint) internal override void InternalRender(SKCanvas canvas, SKRect bounds, SKPaint paint)
{ {

View File

@ -16,7 +16,7 @@ namespace Artemis.Core
/// </summary> /// </summary>
public class Plugin : CorePropertyChanged, IDisposable public class Plugin : CorePropertyChanged, IDisposable
{ {
private readonly List<PluginFeature> _features; private readonly List<PluginFeatureInfo> _features;
private bool _isEnabled; private bool _isEnabled;
@ -26,7 +26,7 @@ namespace Artemis.Core
Directory = directory; Directory = directory;
Entity = pluginEntity ?? new PluginEntity {Id = Guid, IsEnabled = true}; Entity = pluginEntity ?? new PluginEntity {Id = Guid, IsEnabled = true};
_features = new List<PluginFeature>(); _features = new List<PluginFeatureInfo>();
} }
/// <summary> /// <summary>
@ -61,7 +61,7 @@ namespace Artemis.Core
/// <summary> /// <summary>
/// Gets a read-only collection of all features this plugin provides /// Gets a read-only collection of all features this plugin provides
/// </summary> /// </summary>
public ReadOnlyCollection<PluginFeature> Features => _features.AsReadOnly(); public ReadOnlyCollection<PluginFeatureInfo> Features => _features.AsReadOnly();
/// <summary> /// <summary>
/// The assembly the plugin code lives in /// The assembly the plugin code lives in
@ -107,7 +107,7 @@ namespace Artemis.Core
/// <returns>If found, the instance of the feature</returns> /// <returns>If found, the instance of the feature</returns>
public T? GetFeature<T>() where T : PluginFeature public T? GetFeature<T>() where T : PluginFeature
{ {
return _features.FirstOrDefault(i => i is T) as T; return _features.FirstOrDefault(i => i.Instance is T) as T;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -122,23 +122,24 @@ namespace Artemis.Core
Entity.IsEnabled = IsEnabled; Entity.IsEnabled = IsEnabled;
} }
internal void AddFeature(PluginFeature feature) internal void AddFeature(PluginFeatureInfo featureInfo)
{ {
feature.Plugin = this; if (featureInfo.Plugin != this)
_features.Add(feature); throw new ArtemisCoreException("Feature is not associated with this plugin");
_features.Add(featureInfo);
OnFeatureAdded(new PluginFeatureEventArgs(feature)); OnFeatureAdded(new PluginFeatureInfoEventArgs(featureInfo));
} }
internal void RemoveFeature(PluginFeature feature) internal void RemoveFeature(PluginFeatureInfo featureInfo)
{ {
if (feature.IsEnabled) if (featureInfo.Instance != null && featureInfo.Instance.IsEnabled)
throw new ArtemisCoreException("Cannot remove an enabled feature from a plugin"); throw new ArtemisCoreException("Cannot remove an enabled feature from a plugin");
_features.Remove(feature); _features.Remove(featureInfo);
feature.Dispose(); featureInfo.Instance?.Dispose();
OnFeatureRemoved(new PluginFeatureEventArgs(feature)); OnFeatureRemoved(new PluginFeatureInfoEventArgs(featureInfo));
} }
internal void SetEnabled(bool enable) internal void SetEnabled(bool enable)
@ -146,7 +147,7 @@ namespace Artemis.Core
if (IsEnabled == enable) if (IsEnabled == enable)
return; return;
if (!enable && Features.Any(e => e.IsEnabled)) if (!enable && Features.Any(e => e.Instance != null && e.Instance.IsEnabled))
throw new ArtemisCoreException("Cannot disable this plugin because it still has enabled features"); throw new ArtemisCoreException("Cannot disable this plugin because it still has enabled features");
IsEnabled = enable; IsEnabled = enable;
@ -176,8 +177,8 @@ namespace Artemis.Core
{ {
if (disposing) if (disposing)
{ {
foreach (PluginFeature feature in Features) foreach (PluginFeatureInfo feature in Features)
feature.Dispose(); feature.Instance?.Dispose();
SetEnabled(false); SetEnabled(false);
Kernel?.Dispose(); Kernel?.Dispose();
@ -214,12 +215,12 @@ namespace Artemis.Core
/// <summary> /// <summary>
/// Occurs when an feature is loaded and added to the plugin /// Occurs when an feature is loaded and added to the plugin
/// </summary> /// </summary>
public event EventHandler<PluginFeatureEventArgs>? FeatureAdded; public event EventHandler<PluginFeatureInfoEventArgs>? FeatureAdded;
/// <summary> /// <summary>
/// Occurs when an feature is disabled and removed from the plugin /// Occurs when an feature is disabled and removed from the plugin
/// </summary> /// </summary>
public event EventHandler<PluginFeatureEventArgs>? FeatureRemoved; public event EventHandler<PluginFeatureInfoEventArgs>? FeatureRemoved;
/// <summary> /// <summary>
/// Invokes the Enabled event /// Invokes the Enabled event
@ -240,7 +241,7 @@ namespace Artemis.Core
/// <summary> /// <summary>
/// Invokes the FeatureAdded event /// Invokes the FeatureAdded event
/// </summary> /// </summary>
protected virtual void OnFeatureAdded(PluginFeatureEventArgs e) protected virtual void OnFeatureAdded(PluginFeatureInfoEventArgs e)
{ {
FeatureAdded?.Invoke(this, e); FeatureAdded?.Invoke(this, e);
} }
@ -248,7 +249,7 @@ namespace Artemis.Core
/// <summary> /// <summary>
/// Invokes the FeatureRemoved event /// Invokes the FeatureRemoved event
/// </summary> /// </summary>
protected virtual void OnFeatureRemoved(PluginFeatureEventArgs e) protected virtual void OnFeatureRemoved(PluginFeatureInfoEventArgs e)
{ {
FeatureRemoved?.Invoke(this, e); FeatureRemoved?.Invoke(this, e);
} }

View File

@ -1,4 +1,5 @@
using Artemis.Core.DataModelExpansions; using System;
using Artemis.Core.DataModelExpansions;
using Artemis.Core.DeviceProviders; using Artemis.Core.DeviceProviders;
using Artemis.Core.LayerBrushes; using Artemis.Core.LayerBrushes;
using Artemis.Core.LayerEffects; using Artemis.Core.LayerEffects;
@ -16,22 +17,48 @@ namespace Artemis.Core
{ {
private string? _description; private string? _description;
private string? _icon; private string? _icon;
private PluginFeature? _instance;
private string _name = null!; private string _name = null!;
private PluginFeature _pluginFeature = null!;
internal PluginFeatureInfo() internal PluginFeatureInfo(Plugin plugin, Type featureType, PluginFeatureAttribute? attribute)
{ {
Plugin = plugin ?? throw new ArgumentNullException(nameof(plugin));
FeatureType = featureType ?? throw new ArgumentNullException(nameof(featureType));
Name = attribute?.Name ?? featureType.Name.Humanize(LetterCasing.Title);
Description = attribute?.Description;
Icon = attribute?.Icon;
if (Icon != null) return;
if (typeof(BaseDataModelExpansion).IsAssignableFrom(featureType))
Icon = "TableAdd";
else if (typeof(DeviceProvider).IsAssignableFrom(featureType))
Icon = "Devices";
else if (typeof(ProfileModule).IsAssignableFrom(featureType))
Icon = "VectorRectangle";
else if (typeof(Module).IsAssignableFrom(featureType))
Icon = "GearBox";
else if (typeof(LayerBrushProvider).IsAssignableFrom(featureType))
Icon = "Brush";
else if (typeof(LayerEffectProvider).IsAssignableFrom(featureType))
Icon = "AutoAwesome";
else
Icon = "Plugin";
} }
internal PluginFeatureInfo(PluginFeature instance, PluginFeatureAttribute? attribute) internal PluginFeatureInfo(Plugin plugin, PluginFeatureAttribute? attribute, PluginFeature instance)
{ {
if (instance == null) throw new ArgumentNullException(nameof(instance));
Plugin = plugin ?? throw new ArgumentNullException(nameof(plugin));
FeatureType = instance.GetType();
Name = attribute?.Name ?? instance.GetType().Name.Humanize(LetterCasing.Title); Name = attribute?.Name ?? instance.GetType().Name.Humanize(LetterCasing.Title);
Description = attribute?.Description; Description = attribute?.Description;
Icon = attribute?.Icon; Icon = attribute?.Icon;
PluginFeature = instance; Instance = instance;
if (Icon != null) return; if (Icon != null) return;
Icon = PluginFeature switch Icon = Instance switch
{ {
BaseDataModelExpansion => "TableAdd", BaseDataModelExpansion => "TableAdd",
DeviceProvider => "Devices", DeviceProvider => "Devices",
@ -43,6 +70,16 @@ namespace Artemis.Core
}; };
} }
/// <summary>
/// Gets the plugin this feature info is associated with
/// </summary>
public Plugin Plugin { get; }
/// <summary>
/// Gets the type of the feature
/// </summary>
public Type FeatureType { get; }
/// <summary> /// <summary>
/// The name of the plugin /// The name of the plugin
/// </summary> /// </summary>
@ -75,18 +112,18 @@ namespace Artemis.Core
} }
/// <summary> /// <summary>
/// Gets the plugin this info is associated with /// Gets the feature this info is associated with
/// </summary> /// </summary>
public PluginFeature PluginFeature public PluginFeature? Instance
{ {
get => _pluginFeature; get => _instance;
internal set => SetAndNotify(ref _pluginFeature, value); internal set => SetAndNotify(ref _instance, value);
} }
/// <inheritdoc /> /// <inheritdoc />
public override string ToString() public override string ToString()
{ {
return PluginFeature.Id; return Instance?.Id ?? "Uninitialized feature";
} }
} }
} }

View File

@ -38,5 +38,10 @@ namespace Artemis.Core.Services.Core
/// </summary> /// </summary>
[JsonProperty] [JsonProperty]
public string SourceVersion { get; internal set; } = null!; public string SourceVersion { get; internal set; } = null!;
/// <summary>
/// Gets a boolean indicating whether the current build is a local build
/// </summary>
public bool IsLocalBuild { get; internal set; }
} }
} }

View File

@ -71,7 +71,7 @@ namespace Artemis.Core.Services
Plugin ImportPlugin(string fileName); Plugin ImportPlugin(string fileName);
/// <summary> /// <summary>
/// Unloads and permanently removes the provided plugin /// Unloads and permanently removes the provided plugin
/// </summary> /// </summary>
/// <param name="plugin">The plugin to remove</param> /// <param name="plugin">The plugin to remove</param>
void RemovePlugin(Plugin plugin); void RemovePlugin(Plugin plugin);
@ -127,6 +127,13 @@ namespace Artemis.Core.Services
/// <returns></returns> /// <returns></returns>
DeviceProvider GetDeviceProviderByDevice(IRGBDevice device); DeviceProvider GetDeviceProviderByDevice(IRGBDevice device);
/// <summary>
/// Queues an action for the provided plugin for the next time Artemis starts, before plugins are loaded
/// </summary>
/// <param name="plugin">The plugin to queue the action for</param>
/// <param name="pluginAction">The action to take</param>
void QueuePluginAction(Plugin plugin, PluginManagementAction pluginAction);
#region Events #region Events
/// <summary> /// <summary>

View File

@ -52,7 +52,7 @@ namespace Artemis.Core.Services
bool IsRenderPaused { get; set; } bool IsRenderPaused { get; set; }
/// <summary> /// <summary>
/// Recreates the Texture to use the given <see cref="SKBitmap"/> /// Recreates the Texture to use the given <see cref="SKBitmap" />
/// </summary> /// </summary>
/// <param name="bitmap"></param> /// <param name="bitmap"></param>
void UpdateTexture(SKBitmap bitmap); void UpdateTexture(SKBitmap bitmap);
@ -119,7 +119,16 @@ namespace Artemis.Core.Services
/// </summary> /// </summary>
void SaveDevices(); void SaveDevices();
/// <summary>
/// Enables the provided device
/// </summary>
/// <param name="device">The device to enable</param>
void EnableDevice(ArtemisDevice device); void EnableDevice(ArtemisDevice device);
/// <summary>
/// Disables the provided device
/// </summary>
/// <param name="device">The device to disable</param>
void DisableDevice(ArtemisDevice device); void DisableDevice(ArtemisDevice device);
/// <summary> /// <summary>
@ -133,7 +142,7 @@ namespace Artemis.Core.Services
event EventHandler<DeviceEventArgs> DeviceRemoved; event EventHandler<DeviceEventArgs> DeviceRemoved;
/// <summary> /// <summary>
/// Occurs when the surface has had modifications to its LED collection /// Occurs when the surface has had modifications to its LED collection
/// </summary> /// </summary>
event EventHandler LedsChanged; event EventHandler LedsChanged;
} }

View File

@ -67,7 +67,7 @@ namespace Artemis.Core.Services
// If this is a profile module, animate profile disable // If this is a profile module, animate profile disable
// module.Deactivate would do the same but without animation // module.Deactivate would do the same but without animation
if (module.IsActivated && module is ProfileModule profileModule) if (module.IsActivated && module is ProfileModule profileModule)
await profileModule.ChangeActiveProfileAnimated(null, null); await profileModule.ChangeActiveProfileAnimated(null, Enumerable.Empty<ArtemisDevice>());
module.Deactivate(false); module.Deactivate(false);
} }
@ -210,6 +210,7 @@ namespace Artemis.Core.Services
List<Module> modules = _pluginManagementService.GetFeaturesOfType<Module>().ToList(); List<Module> modules = _pluginManagementService.GetFeaturesOfType<Module>().ToList();
List<Task> tasks = new(); List<Task> tasks = new();
foreach (Module module in modules) foreach (Module module in modules)
{
lock (module) lock (module)
{ {
bool shouldBeActivated = module.EvaluateActivationRequirements() && module.IsEnabled; bool shouldBeActivated = module.EvaluateActivationRequirements() && module.IsEnabled;
@ -218,6 +219,7 @@ namespace Artemis.Core.Services
else if (!shouldBeActivated && module.IsActivated) else if (!shouldBeActivated && module.IsActivated)
tasks.Add(DeactivateModule(module)); tasks.Add(DeactivateModule(module));
} }
}
await Task.WhenAll(tasks); await Task.WhenAll(tasks);

View File

@ -5,7 +5,6 @@ using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Runtime.Loader;
using Artemis.Core.DeviceProviders; using Artemis.Core.DeviceProviders;
using Artemis.Core.Ninject; using Artemis.Core.Ninject;
using Artemis.Storage.Entities.Plugins; using Artemis.Storage.Entities.Plugins;
@ -28,6 +27,7 @@ namespace Artemis.Core.Services
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IPluginRepository _pluginRepository; private readonly IPluginRepository _pluginRepository;
private readonly List<Plugin> _plugins; private readonly List<Plugin> _plugins;
private bool _isElevated;
public PluginManagementService(IKernel kernel, ILogger logger, IPluginRepository pluginRepository) public PluginManagementService(IKernel kernel, ILogger logger, IPluginRepository pluginRepository)
{ {
@ -35,6 +35,8 @@ namespace Artemis.Core.Services
_logger = logger; _logger = logger;
_pluginRepository = pluginRepository; _pluginRepository = pluginRepository;
_plugins = new List<Plugin>(); _plugins = new List<Plugin>();
ProcessQueuedActions();
} }
private void CopyBuiltInPlugin(FileInfo zipFileInfo, ZipArchive zipArchive) private void CopyBuiltInPlugin(FileInfo zipFileInfo, ZipArchive zipArchive)
@ -130,7 +132,11 @@ namespace Artemis.Core.Services
{ {
lock (_plugins) lock (_plugins)
{ {
return _plugins.Where(p => p.IsEnabled).SelectMany(p => p.Features.Where(i => i.IsEnabled && i is T)).Cast<T>().ToList(); return _plugins.Where(p => p.IsEnabled)
.SelectMany(p => p.Features.Where(f => f.Instance != null && f.Instance.IsEnabled && f.Instance is T))
.Select(f => f.Instance)
.Cast<T>()
.ToList();
} }
} }
@ -178,6 +184,7 @@ namespace Artemis.Core.Services
if (LoadingPlugins) if (LoadingPlugins)
throw new ArtemisCoreException("Cannot load plugins while a previous load hasn't been completed yet."); throw new ArtemisCoreException("Cannot load plugins while a previous load hasn't been completed yet.");
_isElevated = isElevated;
LoadingPlugins = true; LoadingPlugins = true;
// Unload all currently loaded plugins first // Unload all currently loaded plugins first
@ -186,6 +193,7 @@ namespace Artemis.Core.Services
// Load the plugin assemblies into the plugin context // Load the plugin assemblies into the plugin context
DirectoryInfo pluginDirectory = new(Path.Combine(Constants.DataFolder, "plugins")); DirectoryInfo pluginDirectory = new(Path.Combine(Constants.DataFolder, "plugins"));
foreach (DirectoryInfo subDirectory in pluginDirectory.EnumerateDirectories()) foreach (DirectoryInfo subDirectory in pluginDirectory.EnumerateDirectories())
{
try try
{ {
LoadPlugin(subDirectory); LoadPlugin(subDirectory);
@ -194,11 +202,12 @@ namespace Artemis.Core.Services
{ {
_logger.Warning(new ArtemisPluginException("Failed to load plugin", e), "Plugin exception"); _logger.Warning(new ArtemisPluginException("Failed to load plugin", e), "Plugin exception");
} }
}
// ReSharper disable InconsistentlySynchronizedField - It's read-only, idc // ReSharper disable InconsistentlySynchronizedField - It's read-only, idc
_logger.Debug("Loaded {count} plugin(s)", _plugins.Count); _logger.Debug("Loaded {count} plugin(s)", _plugins.Count);
bool adminRequired = _plugins.Any(p => p.Entity.IsEnabled && p.Info.RequiresAdmin); bool adminRequired = _plugins.Any(p => p.Info.RequiresAdmin && p.Entity.IsEnabled && p.Entity.Features.Any(f => f.IsEnabled));
if (!isElevated && adminRequired) if (!isElevated && adminRequired)
{ {
_logger.Information("Restarting because one or more plugins requires elevation"); _logger.Information("Restarting because one or more plugins requires elevation");
@ -218,7 +227,7 @@ namespace Artemis.Core.Services
foreach (Plugin plugin in _plugins.Where(p => p.Entity.IsEnabled)) foreach (Plugin plugin in _plugins.Where(p => p.Entity.IsEnabled))
EnablePlugin(plugin, false, ignorePluginLock); EnablePlugin(plugin, false, ignorePluginLock);
_logger.Debug("Enabled {count} plugin(s)", _plugins.Where(p => p.IsEnabled).Sum(p => p.Features.Count(f => f.IsEnabled))); _logger.Debug("Enabled {count} plugin(s)", _plugins.Count(p => p.IsEnabled));
// ReSharper restore InconsistentlySynchronizedField // ReSharper restore InconsistentlySynchronizedField
LoadingPlugins = false; LoadingPlugins = false;
@ -289,6 +298,28 @@ namespace Artemis.Core.Services
throw new ArtemisPluginException(plugin, "Failed to load the plugins assembly", e); throw new ArtemisPluginException(plugin, "Failed to load the plugins assembly", e);
} }
// Get the Plugin feature from the main assembly and if there is only one, instantiate it
List<Type> featureTypes;
try
{
featureTypes = plugin.Assembly.GetTypes().Where(t => typeof(PluginFeature).IsAssignableFrom(t)).ToList();
}
catch (ReflectionTypeLoadException e)
{
throw new ArtemisPluginException(
plugin,
"Failed to initialize the plugin assembly",
// ReSharper disable once RedundantEnumerableCastCall - Casting from nullable to non-nullable here
new AggregateException(e.LoaderExceptions.Where(le => le != null).Cast<Exception>().ToArray())
);
}
foreach (Type featureType in featureTypes)
plugin.AddFeature(new PluginFeatureInfo(plugin, featureType, (PluginFeatureAttribute?) Attribute.GetCustomAttribute(featureType, typeof(PluginFeatureAttribute))));
if (!featureTypes.Any())
_logger.Warning("Plugin {plugin} contains no features", plugin);
List<Type> bootstrappers = plugin.Assembly.GetTypes().Where(t => typeof(IPluginBootstrapper).IsAssignableFrom(t)).ToList(); List<Type> bootstrappers = plugin.Assembly.GetTypes().Where(t => typeof(IPluginBootstrapper).IsAssignableFrom(t)).ToList();
if (bootstrappers.Count > 1) if (bootstrappers.Count > 1)
_logger.Warning($"{plugin} has more than one bootstrapper, only initializing {bootstrappers.First().FullName}"); _logger.Warning($"{plugin} has more than one bootstrapper, only initializing {bootstrappers.First().FullName}");
@ -309,66 +340,64 @@ namespace Artemis.Core.Services
if (plugin.Assembly == null) if (plugin.Assembly == null)
throw new ArtemisPluginException(plugin, "Cannot enable a plugin that hasn't successfully been loaded"); throw new ArtemisPluginException(plugin, "Cannot enable a plugin that hasn't successfully been loaded");
if (plugin.Info.RequiresAdmin && plugin.Entity.Features.Any(f => f.IsEnabled) && !_isElevated)
{
if (!saveState)
throw new ArtemisCoreException("Cannot enable a plugin that requires elevation without saving it's state.");
plugin.Entity.IsEnabled = true;
SavePlugin(plugin);
_logger.Information("Restarting because a newly enabled plugin requires elevation");
Utilities.Restart(true, TimeSpan.FromMilliseconds(500));
return;
}
// Create the Ninject child kernel and load the module // Create the Ninject child kernel and load the module
plugin.Kernel = new ChildKernel(_kernel, new PluginModule(plugin)); plugin.Kernel = new ChildKernel(_kernel, new PluginModule(plugin));
OnPluginEnabling(new PluginEventArgs(plugin)); OnPluginEnabling(new PluginEventArgs(plugin));
plugin.SetEnabled(true); plugin.SetEnabled(true);
// Get the Plugin feature from the main assembly and if there is only one, instantiate it // Create instances of each feature
List<Type> featureTypes;
try
{
featureTypes = plugin.Assembly.GetTypes().Where(t => typeof(PluginFeature).IsAssignableFrom(t)).ToList();
}
catch (ReflectionTypeLoadException e)
{
throw new ArtemisPluginException(
plugin,
"Failed to initialize the plugin assembly",
// ReSharper disable once RedundantEnumerableCastCall - Casting from nullable to non-nullable here
new AggregateException(e.LoaderExceptions.Where(le => le != null).Cast<Exception>().ToArray())
);
}
if (!featureTypes.Any())
_logger.Warning("Plugin {plugin} contains no features", plugin);
// Create instances of each feature and add them to the plugin
// Construction should be simple and not contain any logic so failure at this point means the entire plugin fails // Construction should be simple and not contain any logic so failure at this point means the entire plugin fails
foreach (Type featureType in featureTypes) foreach (PluginFeatureInfo featureInfo in plugin.Features)
{
try try
{ {
plugin.Kernel.Bind(featureType).ToSelf().InSingletonScope(); plugin.Kernel.Bind(featureInfo.FeatureType).ToSelf().InSingletonScope();
// Include Plugin as a parameter for the PluginSettingsProvider // Include Plugin as a parameter for the PluginSettingsProvider
IParameter[] parameters = {new Parameter("Plugin", plugin, false)}; IParameter[] parameters = {new Parameter("Plugin", plugin, false)};
PluginFeature instance = (PluginFeature) plugin.Kernel.Get(featureType, parameters); PluginFeature instance = (PluginFeature) plugin.Kernel.Get(featureInfo.FeatureType, parameters);
// Get the PluginFeature attribute which contains extra info on the feature // Get the PluginFeature attribute which contains extra info on the feature
PluginFeatureAttribute? pluginFeatureAttribute = (PluginFeatureAttribute?) Attribute.GetCustomAttribute(featureType, typeof(PluginFeatureAttribute)); featureInfo.Instance = instance;
instance.Info = new PluginFeatureInfo(instance, pluginFeatureAttribute); instance.Info = featureInfo;
plugin.AddFeature(instance); instance.Plugin = plugin;
// Load the enabled state and if not found, default to true // Load the enabled state and if not found, default to true
instance.Entity = plugin.Entity.Features.FirstOrDefault(i => i.Type == featureType.FullName) ?? instance.Entity = plugin.Entity.Features.FirstOrDefault(i => i.Type == featureInfo.FeatureType.FullName) ??
new PluginFeatureEntity {IsEnabled = plugin.Info.AutoEnableFeatures, Type = featureType.FullName!}; new PluginFeatureEntity {IsEnabled = plugin.Info.AutoEnableFeatures, Type = featureInfo.FeatureType.FullName!};
} }
catch (Exception e) catch (Exception e)
{ {
_logger.Warning(new ArtemisPluginException(plugin, "Failed to instantiate feature", e), "Failed to instantiate feature", plugin); _logger.Warning(new ArtemisPluginException(plugin, "Failed to instantiate feature", e), "Failed to instantiate feature", plugin);
} }
}
// Activate plugins after they are all loaded // Activate features after they are all loaded
foreach (PluginFeature pluginFeature in plugin.Features.Where(i => i.Entity.IsEnabled)) foreach (PluginFeatureInfo pluginFeature in plugin.Features.Where(f => f.Instance != null && f.Instance.Entity.IsEnabled))
{
try try
{ {
EnablePluginFeature(pluginFeature, false, !ignorePluginLock); EnablePluginFeature(pluginFeature.Instance!, false, !ignorePluginLock);
} }
catch (Exception) catch (Exception)
{ {
// ignored, logged in EnablePluginFeature // ignored, logged in EnablePluginFeature
} }
}
if (saveState) if (saveState)
{ {
@ -406,12 +435,10 @@ namespace Artemis.Core.Services
if (!plugin.IsEnabled) if (!plugin.IsEnabled)
return; return;
while (plugin.Features.Any()) foreach (PluginFeatureInfo pluginFeatureInfo in plugin.Features)
{ {
PluginFeature feature = plugin.Features[0]; if (pluginFeatureInfo.Instance != null && pluginFeatureInfo.Instance.IsEnabled)
if (feature.IsEnabled) DisablePluginFeature(pluginFeatureInfo.Instance, false);
DisablePluginFeature(feature, false);
plugin.RemoveFeature(feature);
} }
plugin.SetEnabled(false); plugin.SetEnabled(false);
@ -450,7 +477,6 @@ namespace Artemis.Core.Services
Plugin? existing = _plugins.FirstOrDefault(p => p.Guid == pluginInfo.Guid); Plugin? existing = _plugins.FirstOrDefault(p => p.Guid == pluginInfo.Guid);
if (existing != null) if (existing != null)
{
try try
{ {
RemovePlugin(existing); RemovePlugin(existing);
@ -459,7 +485,6 @@ namespace Artemis.Core.Services
{ {
throw new ArtemisPluginException("A plugin with the same GUID is already loaded, failed to remove old version", e); throw new ArtemisPluginException("A plugin with the same GUID is already loaded, failed to remove old version", e);
} }
}
string targetDirectory = pluginInfo.Main.Split(".dll")[0].Replace("/", "").Replace("\\", ""); string targetDirectory = pluginInfo.Main.Split(".dll")[0].Replace("/", "").Replace("\\", "");
string uniqueTargetDirectory = targetDirectory; string uniqueTargetDirectory = targetDirectory;
@ -515,6 +540,21 @@ namespace Artemis.Core.Services
_logger.Verbose("Enabling plugin feature {feature} - {plugin}", pluginFeature, pluginFeature.Plugin); _logger.Verbose("Enabling plugin feature {feature} - {plugin}", pluginFeature, pluginFeature.Plugin);
OnPluginFeatureEnabling(new PluginFeatureEventArgs(pluginFeature)); OnPluginFeatureEnabling(new PluginFeatureEventArgs(pluginFeature));
if (pluginFeature.Plugin.Info.RequiresAdmin && !_isElevated)
{
if (!saveState)
throw new ArtemisCoreException("Cannot enable a feature that requires elevation without saving it's state.");
pluginFeature.Entity.IsEnabled = true;
pluginFeature.Plugin.Entity.IsEnabled = true;
SavePlugin(pluginFeature.Plugin);
_logger.Information("Restarting because a newly enabled feature requires elevation");
Utilities.Restart(true, TimeSpan.FromMilliseconds(500));
return;
}
try try
{ {
pluginFeature.SetEnabled(true, isAutoEnable); pluginFeature.SetEnabled(true, isAutoEnable);
@ -578,13 +618,43 @@ namespace Artemis.Core.Services
#endregion #endregion
#region Queued actions
public void QueuePluginAction(Plugin plugin, PluginManagementAction pluginAction)
{
List<PluginQueuedActionEntity> existing = _pluginRepository.GetQueuedActions(plugin.Guid);
if (existing.Any(e => pluginAction == PluginManagementAction.Delete && e is PluginQueuedDeleteEntity))
return;
if (pluginAction == PluginManagementAction.Delete)
_pluginRepository.AddQueuedAction(new PluginQueuedDeleteEntity {PluginGuid = plugin.Guid, Directory = plugin.Directory.FullName});
}
private void ProcessQueuedActions()
{
foreach (PluginQueuedActionEntity pluginQueuedActionEntity in _pluginRepository.GetQueuedActions())
{
if (pluginQueuedActionEntity is PluginQueuedDeleteEntity deleteAction)
{
if (Directory.Exists(deleteAction.Directory))
Directory.Delete(deleteAction.Directory, true);
}
_pluginRepository.RemoveQueuedAction(pluginQueuedActionEntity);
}
}
#endregion
#region Storage #region Storage
private void SavePlugin(Plugin plugin) private void SavePlugin(Plugin plugin)
{ {
foreach (PluginFeature pluginFeature in plugin.Features) foreach (PluginFeatureInfo featureInfo in plugin.Features.Where(i => i.Instance != null))
if (plugin.Entity.Features.All(i => i.Type != pluginFeature.GetType().FullName)) {
plugin.Entity.Features.Add(pluginFeature.Entity); if (plugin.Entity.Features.All(i => i.Type != featureInfo.FeatureType.FullName))
plugin.Entity.Features.Add(featureInfo.Instance!.Entity);
}
_pluginRepository.SavePlugin(plugin.Entity); _pluginRepository.SavePlugin(plugin.Entity);
} }
@ -663,4 +733,20 @@ namespace Artemis.Core.Services
#endregion #endregion
} }
/// <summary>
/// Represents a type of plugin management action
/// </summary>
public enum PluginManagementAction
{
/// <summary>
/// A plugin management action that removes a plugin
/// </summary>
Delete,
// /// <summary>
// /// A plugin management action that updates a plugin
// /// </summary>
// Update
}
} }

View File

@ -239,6 +239,7 @@ namespace Artemis.Core.Services
public void ApplyDeviceLayout(ArtemisDevice device, ArtemisLayout layout, bool createMissingLeds, bool removeExessiveLeds) public void ApplyDeviceLayout(ArtemisDevice device, ArtemisLayout layout, bool createMissingLeds, bool removeExessiveLeds)
{ {
device.ApplyLayout(layout, createMissingLeds, removeExessiveLeds); device.ApplyLayout(layout, createMissingLeds, removeExessiveLeds);
UpdateLedGroup();
} }
public ArtemisDevice? GetDevice(IRGBDevice rgbDevice) public ArtemisDevice? GetDevice(IRGBDevice rgbDevice)

View File

@ -146,7 +146,8 @@ namespace Artemis.Core.Services
void ActivatingProfilePluginToggle(object? sender, PluginEventArgs e) void ActivatingProfilePluginToggle(object? sender, PluginEventArgs e)
{ {
InstantiateProfile(profile); if (!profile.Disposed)
InstantiateProfile(profile);
} }
// This could happen during activation so subscribe to it // This could happen during activation so subscribe to it

View File

@ -61,7 +61,7 @@ namespace Artemis.Core.Services
server.StateChanged += (s, e) => _logger.Verbose("WebServer new state - {state}", e.NewState); server.StateChanged += (s, e) => _logger.Verbose("WebServer new state - {state}", e.NewState);
// Store the URL in a webserver.txt file so that remote applications can find it // Store the URL in a webserver.txt file so that remote applications can find it
File.WriteAllText(Path.Combine(Constants.DataFolder, "webserver.txt"), url); File.WriteAllText(Path.Combine(Constants.DataFolder, "webserver.txt"), $"http://localhost:{_webServerPortSetting.Value}/");
return server; return server;
} }

View File

@ -9,7 +9,7 @@ namespace Artemis.Core
{ {
public CorePluginFeature() public CorePluginFeature()
{ {
Constants.CorePlugin.AddFeature(this); Constants.CorePlugin.AddFeature(new PluginFeatureInfo(Constants.CorePlugin, null, this));
IsEnabled = true; IsEnabled = true;
} }

View File

@ -44,7 +44,7 @@ namespace Artemis.Core
foreach (LayerEntity profileEntityLayer in profileEntity.Layers) foreach (LayerEntity profileEntityLayer in profileEntity.Layers)
profileEntityLayer.Leds.AddRange(_devices.SelectMany(d => d.Leds).Select(l => new LedEntity profileEntityLayer.Leds.AddRange(_devices.SelectMany(d => d.Leds).Select(l => new LedEntity
{ {
DeviceIdentifier = l.Device.RgbDevice.GetDeviceIdentifier(), DeviceIdentifier = l.Device.Identifier,
LedName = l.RgbLed.Id.ToString() LedName = l.RgbLed.Id.ToString()
})); }));

View File

@ -13,4 +13,21 @@ namespace Artemis.Storage.Entities.Plugins
public string Name { get; set; } public string Name { get; set; }
public string Value { get; set; } public string Value { get; set; }
} }
/// <summary>
/// Represents a queued action for a plugin
/// </summary>
public abstract class PluginQueuedActionEntity
{
public Guid Id { get; set; }
public Guid PluginGuid { get; set; }
}
/// <summary>
/// Represents a queued delete action for a plugin
/// </summary>
public class PluginQueuedDeleteEntity : PluginQueuedActionEntity
{
public string Directory { get; set; }
}
} }

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using Artemis.Storage.Entities.Plugins; using Artemis.Storage.Entities.Plugins;
namespace Artemis.Storage.Repositories.Interfaces namespace Artemis.Storage.Repositories.Interfaces
@ -8,9 +9,15 @@ namespace Artemis.Storage.Repositories.Interfaces
void AddPlugin(PluginEntity pluginEntity); void AddPlugin(PluginEntity pluginEntity);
PluginEntity GetPluginByGuid(Guid pluginGuid); PluginEntity GetPluginByGuid(Guid pluginGuid);
void SavePlugin(PluginEntity pluginEntity); void SavePlugin(PluginEntity pluginEntity);
void AddSetting(PluginSettingEntity pluginSettingEntity); void AddSetting(PluginSettingEntity pluginSettingEntity);
PluginSettingEntity GetSettingByGuid(Guid pluginGuid); PluginSettingEntity GetSettingByGuid(Guid pluginGuid);
PluginSettingEntity GetSettingByNameAndGuid(string name, Guid pluginGuid); PluginSettingEntity GetSettingByNameAndGuid(string name, Guid pluginGuid);
void SaveSetting(PluginSettingEntity pluginSettingEntity); void SaveSetting(PluginSettingEntity pluginSettingEntity);
void AddQueuedAction(PluginQueuedActionEntity pluginQueuedActionEntity);
List<PluginQueuedActionEntity> GetQueuedActions();
List<PluginQueuedActionEntity> GetQueuedActions(Guid pluginGuid);
void RemoveQueuedAction(PluginQueuedActionEntity pluginQueuedActionEntity);
} }
} }

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using Artemis.Storage.Entities.Plugins; using Artemis.Storage.Entities.Plugins;
using Artemis.Storage.Repositories.Interfaces; using Artemis.Storage.Repositories.Interfaces;
using LiteDB; using LiteDB;
@ -14,6 +15,7 @@ namespace Artemis.Storage.Repositories
_repository = repository; _repository = repository;
_repository.Database.GetCollection<PluginSettingEntity>().EnsureIndex(s => new {s.Name, s.PluginGuid}, true); _repository.Database.GetCollection<PluginSettingEntity>().EnsureIndex(s => new {s.Name, s.PluginGuid}, true);
_repository.Database.GetCollection<PluginQueuedActionEntity>().EnsureIndex(s => s.PluginGuid);
} }
public void AddPlugin(PluginEntity pluginEntity) public void AddPlugin(PluginEntity pluginEntity)
@ -51,5 +53,24 @@ namespace Artemis.Storage.Repositories
{ {
_repository.Upsert(pluginSettingEntity); _repository.Upsert(pluginSettingEntity);
} }
public List<PluginQueuedActionEntity> GetQueuedActions()
{
return _repository.Query<PluginQueuedActionEntity>().ToList();
}
public List<PluginQueuedActionEntity> GetQueuedActions(Guid pluginGuid)
{
return _repository.Query<PluginQueuedActionEntity>().Where(q => q.PluginGuid == pluginGuid).ToList();
}
public void AddQueuedAction(PluginQueuedActionEntity pluginQueuedActionEntity)
{
_repository.Upsert(pluginQueuedActionEntity);
}
public void RemoveQueuedAction(PluginQueuedActionEntity pluginQueuedActionEntity)
{
_repository.Delete<PluginQueuedActionEntity>(pluginQueuedActionEntity.Id);
}
} }
} }

View File

@ -51,7 +51,7 @@ namespace Artemis.UI.Shared
byte g = Led.RgbLed.Color.GetG(); byte g = Led.RgbLed.Color.GetG();
byte b = Led.RgbLed.Color.GetB(); byte b = Led.RgbLed.Color.GetB();
_renderColor.A = (byte)(isDimmed ? 100 : 255); _renderColor.A = (byte) (isDimmed ? 100 : 255);
_renderColor.A = isDimmed ? Dimmed : NonDimmed; _renderColor.A = isDimmed ? Dimmed : NonDimmed;
_renderColor.R = r; _renderColor.R = r;
_renderColor.G = g; _renderColor.G = g;
@ -135,6 +135,8 @@ namespace Artemis.UI.Shared
{ {
try try
{ {
double width = Led.RgbLed.Size.Width - deflateAmount;
double height = Led.RgbLed.Size.Height - deflateAmount;
// DisplayGeometry = Geometry.Parse(Led.RgbLed.ShapeData); // DisplayGeometry = Geometry.Parse(Led.RgbLed.ShapeData);
DisplayGeometry = Geometry.Combine( DisplayGeometry = Geometry.Combine(
Geometry.Empty, Geometry.Empty,
@ -144,11 +146,27 @@ namespace Artemis.UI.Shared
{ {
Children = new TransformCollection Children = new TransformCollection
{ {
new ScaleTransform(Led.RgbLed.Size.Width - deflateAmount, Led.RgbLed.Size.Height - deflateAmount), new ScaleTransform(width, height),
new TranslateTransform(deflateAmount / 2, deflateAmount / 2) new TranslateTransform(deflateAmount / 2, deflateAmount / 2)
} }
} }
); );
if (DisplayGeometry.Bounds.Width > width)
{
DisplayGeometry = Geometry.Combine(Geometry.Empty, DisplayGeometry, GeometryCombineMode.Union, new TransformGroup
{
Children = new TransformCollection {new ScaleTransform(width / DisplayGeometry.Bounds.Width, 1)}
});
}
if (DisplayGeometry.Bounds.Height > height)
{
DisplayGeometry = Geometry.Combine(Geometry.Empty, DisplayGeometry, GeometryCombineMode.Union, new TransformGroup
{
Children = new TransformCollection {new ScaleTransform(1, height / DisplayGeometry.Bounds.Height)}
});
}
} }
catch (Exception) catch (Exception)
{ {

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Linq; using System.Linq;
using System.Timers; using System.Timers;
@ -139,7 +140,11 @@ namespace Artemis.UI.Shared.Input
set set
{ {
if (!SetAndNotify(ref _isDataModelViewModelOpen, value)) return; if (!SetAndNotify(ref _isDataModelViewModelOpen, value)) return;
if (value) UpdateDataModelVisualization(); if (value)
{
UpdateDataModelVisualization();
OpenSelectedValue(DataModelViewModel);
}
} }
} }
@ -303,6 +308,21 @@ namespace Artemis.UI.Shared.Input
extraDataModelViewModel.Update(_dataModelUIService, new DataModelUpdateConfiguration(LoadEventChildren)); extraDataModelViewModel.Update(_dataModelUIService, new DataModelUpdateConfiguration(LoadEventChildren));
} }
private void OpenSelectedValue(DataModelVisualizationViewModel dataModelPropertiesViewModel)
{
if (DataModelPath == null)
return;
if (dataModelPropertiesViewModel.Children.Any(c => c.DataModelPath != null && DataModelPath.Path.StartsWith(c.DataModelPath.Path)))
{
dataModelPropertiesViewModel.IsVisualizationExpanded = true;
foreach (DataModelVisualizationViewModel dataModelVisualizationViewModel in dataModelPropertiesViewModel.Children)
{
OpenSelectedValue(dataModelVisualizationViewModel);
}
}
}
#endregion #endregion
#region Events #region Events

View File

@ -334,9 +334,6 @@
</Compile> </Compile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Update="buildinfo.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Properties\Settings.settings"> <None Update="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator> <Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput> <LastGenOutput>Settings.Designer.cs</LastGenOutput>

View File

@ -39,7 +39,7 @@ namespace Artemis.UI.Ninject.Factories
public interface ISettingsVmFactory : IVmFactory public interface ISettingsVmFactory : IVmFactory
{ {
PluginSettingsViewModel CreatePluginSettingsViewModel(Plugin plugin); PluginSettingsViewModel CreatePluginSettingsViewModel(Plugin plugin);
PluginFeatureViewModel CreatePluginFeatureViewModel(PluginFeature feature); PluginFeatureViewModel CreatePluginFeatureViewModel(PluginFeatureInfo pluginFeatureInfo, bool showShield);
DeviceSettingsViewModel CreateDeviceSettingsViewModel(ArtemisDevice device); DeviceSettingsViewModel CreateDeviceSettingsViewModel(ArtemisDevice device);
} }

View File

@ -136,7 +136,7 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions
RenderProfileElement.DisplayCondition.ChildRemoved += DisplayConditionOnChildrenModified; RenderProfileElement.DisplayCondition.ChildRemoved += DisplayConditionOnChildrenModified;
} }
private void CoreServiceOnFrameRendered(object? sender, FrameRenderedEventArgs e) private void CoreServiceOnFrameRendered(object sender, FrameRenderedEventArgs e)
{ {
ActiveItem?.Evaluate(); ActiveItem?.Evaluate();
} }

View File

@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Artemis.Core; using Artemis.Core;
using Artemis.UI.Ninject.Factories; using Artemis.UI.Ninject.Factories;
@ -14,7 +13,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings
{ {
private readonly IDataBindingsVmFactory _dataBindingsVmFactory; private readonly IDataBindingsVmFactory _dataBindingsVmFactory;
private readonly IProfileEditorService _profileEditorService; private readonly IProfileEditorService _profileEditorService;
private ILayerProperty? _selectedDataBinding; private ILayerProperty _selectedDataBinding;
private int _selectedItemIndex; private int _selectedItemIndex;
private bool _updating; private bool _updating;

View File

@ -14,8 +14,8 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization
public class ProfileLayerViewModel : CanvasViewModel public class ProfileLayerViewModel : CanvasViewModel
{ {
private readonly ILayerEditorService _layerEditorService; private readonly ILayerEditorService _layerEditorService;
private readonly IProfileEditorService _profileEditorService;
private readonly PanZoomViewModel _panZoomViewModel; private readonly PanZoomViewModel _panZoomViewModel;
private readonly IProfileEditorService _profileEditorService;
private bool _isSelected; private bool _isSelected;
private Geometry _shapeGeometry; private Geometry _shapeGeometry;
private Rect _viewportRectangle; private Rect _viewportRectangle;

View File

@ -11,6 +11,7 @@
d:DesignHeight="510.9" d:DesignWidth="800" d:DesignHeight="510.9" d:DesignWidth="800"
d:DataContext="{d:DesignInstance {x:Type visualization:ProfileViewModel}}"> d:DataContext="{d:DesignInstance {x:Type visualization:ProfileViewModel}}">
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
@ -23,16 +24,16 @@
<StackPanel Orientation="Vertical" /> <StackPanel Orientation="Vertical" />
</ItemsPanelTemplate> </ItemsPanelTemplate>
</ListBox.ItemsPanel> </ListBox.ItemsPanel>
<ListBoxItem ToolTip="Pan over different parts of the surface"> <ListBoxItem ToolTip="Pan over different parts of the surface - Ctrl">
<materialDesign:PackIcon Kind="HandLeft" /> <materialDesign:PackIcon Kind="HandLeft" />
</ListBoxItem> </ListBoxItem>
<ListBoxItem ToolTip="Transform layer shape (hold SHIFT for incremental changes and X/Y snapping)" IsEnabled="{Binding CanSelectEditTool}"> <ListBoxItem ToolTip="Transform layer shape (hold SHIFT for incremental changes and X/Y snapping) - Ctrl+T" IsEnabled="{Binding CanSelectEditTool}">
<materialDesign:PackIcon Kind="TransitConnectionVariant" /> <materialDesign:PackIcon Kind="TransitConnectionVariant" />
</ListBoxItem> </ListBoxItem>
<ListBoxItem ToolTip="Change layer selection (hold SHIFT to add to existing selection)"> <ListBoxItem ToolTip="Change layer selection (hold SHIFT to add to existing selection) - Ctrl+Q">
<materialDesign:PackIcon Kind="SelectionDrag" /> <materialDesign:PackIcon Kind="SelectionDrag" />
</ListBoxItem> </ListBoxItem>
<ListBoxItem ToolTip="Remove from layer selection"> <ListBoxItem ToolTip="Remove from layer selection - Ctrl+W">
<materialDesign:PackIcon Kind="SelectOff" /> <materialDesign:PackIcon Kind="SelectOff" />
</ListBoxItem> </ListBoxItem>
</ListBox> </ListBox>
@ -133,9 +134,9 @@
<materialDesign:Card Padding="8"> <materialDesign:Card Padding="8">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<CheckBox Style="{StaticResource MaterialDesignCheckBox}" <CheckBox Style="{StaticResource MaterialDesignCheckBox}"
IsChecked="{Binding HighlightSelectedLayer.Value}" IsChecked="{Binding FocusSelectedLayer.Value}"
ToolTip="If selected, dims all LEDs that are not part of the selected layer"> ToolTip="If selected, dims all LEDs that are not part of the selected layer">
Highlight LEDs of selected layer Focus selected layer
</CheckBox> </CheckBox>
<CheckBox Style="{StaticResource MaterialDesignCheckBox}" <CheckBox Style="{StaticResource MaterialDesignCheckBox}"
Margin="10 0 0 0" Margin="10 0 0 0"

View File

@ -15,7 +15,7 @@ using Stylet;
namespace Artemis.UI.Screens.ProfileEditor.Visualization namespace Artemis.UI.Screens.ProfileEditor.Visualization
{ {
public class ProfileViewModel : Conductor<CanvasViewModel>.Collection.AllActive, IProfileEditorPanelViewModel, IHandle<MainWindowFocusChangedEvent>, IHandle<MainWindowKeyEvent> public class ProfileViewModel : Conductor<CanvasViewModel>.Collection.AllActive, IProfileEditorPanelViewModel, IHandle<MainWindowKeyEvent>
{ {
private readonly ICoreService _coreService; private readonly ICoreService _coreService;
private readonly IProfileEditorService _profileEditorService; private readonly IProfileEditorService _profileEditorService;
@ -31,7 +31,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization
private bool _canSelectEditTool; private bool _canSelectEditTool;
private BindableCollection<ArtemisDevice> _devices; private BindableCollection<ArtemisDevice> _devices;
private BindableCollection<ArtemisLed> _highlightedLeds; private BindableCollection<ArtemisLed> _highlightedLeds;
private PluginSetting<bool> _highlightSelectedLayer; private PluginSetting<bool> _focusSelectedLayer;
private DateTime _lastUpdate; private DateTime _lastUpdate;
private PanZoomViewModel _panZoomViewModel; private PanZoomViewModel _panZoomViewModel;
private Layer _previousSelectedLayer; private Layer _previousSelectedLayer;
@ -55,7 +55,6 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization
eventAggregator.Subscribe(this); eventAggregator.Subscribe(this);
} }
public bool CanSelectEditTool public bool CanSelectEditTool
{ {
get => _canSelectEditTool; get => _canSelectEditTool;
@ -86,10 +85,10 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization
set => SetAndNotify(ref _alwaysApplyDataBindings, value); set => SetAndNotify(ref _alwaysApplyDataBindings, value);
} }
public PluginSetting<bool> HighlightSelectedLayer public PluginSetting<bool> FocusSelectedLayer
{ {
get => _highlightSelectedLayer; get => _focusSelectedLayer;
set => SetAndNotify(ref _highlightSelectedLayer, value); set => SetAndNotify(ref _focusSelectedLayer, value);
} }
public VisualizationToolViewModel ActiveToolViewModel public VisualizationToolViewModel ActiveToolViewModel
@ -146,12 +145,12 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization
ApplyActiveProfile(); ApplyActiveProfile();
AlwaysApplyDataBindings = _settingsService.GetSetting("ProfileEditor.AlwaysApplyDataBindings", true); AlwaysApplyDataBindings = _settingsService.GetSetting("ProfileEditor.AlwaysApplyDataBindings", true);
HighlightSelectedLayer = _settingsService.GetSetting("ProfileEditor.HighlightSelectedLayer", true); FocusSelectedLayer = _settingsService.GetSetting("ProfileEditor.FocusSelectedLayer", true);
_lastUpdate = DateTime.Now; _lastUpdate = DateTime.Now;
_coreService.FrameRendered += OnFrameRendered; _coreService.FrameRendered += OnFrameRendered;
HighlightSelectedLayer.SettingChanged += HighlightSelectedLayerOnSettingChanged; FocusSelectedLayer.SettingChanged += HighlightSelectedLayerOnSettingChanged;
_rgbService.DeviceAdded += RgbServiceOnDevicesModified; _rgbService.DeviceAdded += RgbServiceOnDevicesModified;
_rgbService.DeviceRemoved += RgbServiceOnDevicesModified; _rgbService.DeviceRemoved += RgbServiceOnDevicesModified;
_profileEditorService.ProfileSelected += OnProfileSelected; _profileEditorService.ProfileSelected += OnProfileSelected;
@ -164,7 +163,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization
protected override void OnClose() protected override void OnClose()
{ {
_coreService.FrameRendered -= OnFrameRendered; _coreService.FrameRendered -= OnFrameRendered;
HighlightSelectedLayer.SettingChanged -= HighlightSelectedLayerOnSettingChanged; FocusSelectedLayer.SettingChanged -= HighlightSelectedLayerOnSettingChanged;
_rgbService.DeviceAdded -= RgbServiceOnDevicesModified; _rgbService.DeviceAdded -= RgbServiceOnDevicesModified;
_rgbService.DeviceRemoved -= RgbServiceOnDevicesModified; _rgbService.DeviceRemoved -= RgbServiceOnDevicesModified;
_profileEditorService.ProfileSelected -= OnProfileSelected; _profileEditorService.ProfileSelected -= OnProfileSelected;
@ -174,7 +173,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization
_previousSelectedLayer.LayerBrushUpdated -= SelectedLayerOnLayerBrushUpdated; _previousSelectedLayer.LayerBrushUpdated -= SelectedLayerOnLayerBrushUpdated;
AlwaysApplyDataBindings.Save(); AlwaysApplyDataBindings.Save();
HighlightSelectedLayer.Save(); FocusSelectedLayer.Save();
base.OnClose(); base.OnClose();
} }
@ -191,8 +190,10 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization
// Add new layers missing a VM // Add new layers missing a VM
foreach (Layer layer in layers) foreach (Layer layer in layers)
{
if (layerViewModels.All(vm => vm.Layer != layer)) if (layerViewModels.All(vm => vm.Layer != layer))
Items.Add(_profileLayerVmFactory.Create(layer, PanZoomViewModel)); Items.Add(_profileLayerVmFactory.Create(layer, PanZoomViewModel));
}
// Remove layers that no longer exist // Remove layers that no longer exist
IEnumerable<ProfileLayerViewModel> toRemove = layerViewModels.Where(vm => !layers.Contains(vm.Layer)); IEnumerable<ProfileLayerViewModel> toRemove = layerViewModels.Where(vm => !layers.Contains(vm.Layer));
@ -209,7 +210,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization
private void UpdateLedsDimStatus() private void UpdateLedsDimStatus()
{ {
HighlightedLeds.Clear(); HighlightedLeds.Clear();
if (HighlightSelectedLayer.Value && _profileEditorService.SelectedProfileElement is Layer layer) if (FocusSelectedLayer.Value && _profileEditorService.SelectedProfileElement is Layer layer)
HighlightedLeds.AddRange(layer.Leds); HighlightedLeds.AddRange(layer.Leds);
} }
@ -234,6 +235,9 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization
private void ActivateToolByIndex(int value) private void ActivateToolByIndex(int value)
{ {
if (value == 1 && !CanSelectEditTool)
return;
switch (value) switch (value)
{ {
case 0: case 0:
@ -274,12 +278,14 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization
{ {
((IInputElement) sender).CaptureMouse(); ((IInputElement) sender).CaptureMouse();
ActiveToolViewModel?.MouseDown(sender, e); ActiveToolViewModel?.MouseDown(sender, e);
e.Handled = false;
} }
public void CanvasMouseUp(object sender, MouseButtonEventArgs e) public void CanvasMouseUp(object sender, MouseButtonEventArgs e)
{ {
((IInputElement) sender).ReleaseMouseCapture(); ((IInputElement) sender).ReleaseMouseCapture();
ActiveToolViewModel?.MouseUp(sender, e); ActiveToolViewModel?.MouseUp(sender, e);
e.Handled = false;
} }
public void CanvasMouseMove(object sender, MouseEventArgs e) public void CanvasMouseMove(object sender, MouseEventArgs e)
@ -382,24 +388,6 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization
ActivateToolByIndex(2); ActivateToolByIndex(2);
} }
public void Handle(MainWindowFocusChangedEvent message)
{
// if (PauseRenderingOnFocusLoss == null || ScreenState != ScreenState.Active)
// return;
//
// try
// {
// if (PauseRenderingOnFocusLoss.Value && !message.IsFocused)
// _updateTrigger.Stop();
// else if (PauseRenderingOnFocusLoss.Value && message.IsFocused)
// _updateTrigger.Start();
// }
// catch (NullReferenceException)
// {
// // TODO: Remove when fixed in RGB.NET, or avoid double stopping
// }
}
public void Handle(MainWindowKeyEvent message) public void Handle(MainWindowKeyEvent message)
{ {
if (message.KeyDown) if (message.KeyDown)
@ -412,6 +400,17 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization
} }
ActiveToolViewModel?.KeyDown(message.EventArgs); ActiveToolViewModel?.KeyDown(message.EventArgs);
// If T is pressed while Ctrl is down, that makes it Ctrl+T > swap to transformation tool on Ctrl release
if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.LeftCtrl))
{
if (message.EventArgs.Key == Key.T)
_previousTool = 1;
else if (message.EventArgs.Key == Key.Q)
_previousTool = 2;
else if (message.EventArgs.Key == Key.W)
_previousTool = 3;
}
} }
else else
{ {

View File

@ -24,110 +24,124 @@
<mde:MaterialWindow.InputBindings> <mde:MaterialWindow.InputBindings>
<KeyBinding Command="{s:Action ClearSelection}" Key="Escape" /> <KeyBinding Command="{s:Action ClearSelection}" Key="Escape" />
</mde:MaterialWindow.InputBindings> </mde:MaterialWindow.InputBindings>
<DockPanel>
<mde:AppBar Type="Dense"
Title="{Binding Device.RgbDevice.DeviceInfo.Model}"
ShowShadow="True"
DockPanel.Dock="Top"
Margin="-18 0 0 0">
<mde:AppBar.AppIcon>
<materialDesign:PackIcon Kind="HammerWrench" Width="20" Height="28" />
</mde:AppBar.AppIcon>
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal"> <materialDesign:DialogHost IsTabStop="False"
<Button Style="{StaticResource MaterialDesignIconForegroundButton}" ToolTip="Identify" Command="{s:Action IdentifyDevice}"> Focusable="False"
<materialDesign:PackIcon Kind="AlarmLight" /> Identifier="DeviceDialog"
</Button> DialogTheme="Inherit"
<materialDesign:PopupBox PlacementMode="BottomAndAlignRightEdges" StaysOpen="False"> SnackbarMessageQueue="{Binding DeviceMessageQueue}">
<StackPanel> <DockPanel>
<Button Command="{s:Action OpenPluginDirectory}"> <mde:AppBar Type="Dense"
<StackPanel Orientation="Horizontal"> Title="{Binding Device.RgbDevice.DeviceInfo.Model}"
<materialDesign:PackIcon Kind="Plugin" Margin="0 0 10 0 " VerticalAlignment="Center" /> ShowShadow="True"
<TextBlock VerticalAlignment="Center">Open plugin directory</TextBlock> DockPanel.Dock="Top"
</StackPanel> Margin="-18 0 0 0">
</Button> <mde:AppBar.AppIcon>
<Button Command="{s:Action OpenImageDirectory}"> <materialDesign:PackIcon Kind="HammerWrench" Width="20" Height="28" />
<StackPanel Orientation="Horizontal"> </mde:AppBar.AppIcon>
<materialDesign:PackIcon Kind="Image" Margin="0 0 10 0 " VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center">Open layout image directory</TextBlock>
</StackPanel>
</Button>
<Separator />
<Button Command="{s:Action ReloadLayout}">
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="Reload" Margin="0 0 10 0 " VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center">Reload layout</TextBlock>
</StackPanel>
</Button>
<Button Command="{s:Action ExportLayout}">
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="Xml" Margin="0 0 10 0 " VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center">Export layout</TextBlock>
</StackPanel>
</Button>
</StackPanel>
</materialDesign:PopupBox>
</StackPanel>
</mde:AppBar>
<Grid> <StackPanel DockPanel.Dock="Right" Orientation="Horizontal">
<Grid.ColumnDefinitions> <Button Style="{StaticResource MaterialDesignIconForegroundButton}" ToolTip="Identify" Command="{s:Action IdentifyDevice}">
<ColumnDefinition Width="*" /> <materialDesign:PackIcon Kind="AlarmLight" />
<ColumnDefinition Width="0" /> </Button>
<ColumnDefinition Width="*" /> <materialDesign:PopupBox PlacementMode="BottomAndAlignRightEdges" StaysOpen="False">
</Grid.ColumnDefinitions> <StackPanel>
<Button Command="{s:Action OpenPluginDirectory}">
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="Plugin" Margin="0 0 10 0 " VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center">Open plugin directory</TextBlock>
</StackPanel>
</Button>
<Button Command="{s:Action OpenImageDirectory}">
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="Image" Margin="0 0 10 0 " VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center">Open layout image directory</TextBlock>
</StackPanel>
</Button>
<Separator />
<Button Command="{s:Action ReloadLayout}">
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="Reload" Margin="0 0 10 0 " VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center">Reload layout</TextBlock>
</StackPanel>
</Button>
<Button Command="{s:Action ExportLayout}">
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="Xml" Margin="0 0 10 0 " VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center">Export layout</TextBlock>
</StackPanel>
</Button>
</StackPanel>
</materialDesign:PopupBox>
</StackPanel>
</mde:AppBar>
<Grid Name="DeviceDisplayGrid" Grid.Column="0"> <Grid>
<Grid.Background> <Grid.ColumnDefinitions>
<VisualBrush TileMode="Tile" Stretch="Uniform" Viewport="0 0 25 25" ViewportUnits="Absolute"> <ColumnDefinition Width="*" />
<VisualBrush.Visual> <ColumnDefinition Width="0" />
<Grid Width="20" Height="20"> <ColumnDefinition Width="*" />
<Grid.RowDefinitions> </Grid.ColumnDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Rectangle Grid.Row="0" Grid.Column="0" Fill="Black" Opacity="0.15" />
<Rectangle Grid.Row="0" Grid.Column="1" />
<Rectangle Grid.Row="1" Grid.Column="0" />
<Rectangle Grid.Row="1" Grid.Column="1" Fill="Black" Opacity="0.15" />
</Grid>
</VisualBrush.Visual>
</VisualBrush>
</Grid.Background>
<shared:DeviceVisualizer Device="{Binding Device}" <Grid Name="DeviceDisplayGrid" Grid.Column="0">
HighlightedLeds="{Binding SelectedLeds}" <Grid.Background>
HorizontalAlignment="Center" <VisualBrush TileMode="Tile" Stretch="Uniform" Viewport="0 0 25 25" ViewportUnits="Absolute">
VerticalAlignment="Center" <VisualBrush.Visual>
ShowColors="True" <Grid Width="20" Height="20">
Margin="0 0 100 0" /> <Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Rectangle Grid.Row="0" Grid.Column="0" Fill="Black" Opacity="0.15" />
<Rectangle Grid.Row="0" Grid.Column="1" />
<Rectangle Grid.Row="1" Grid.Column="0" />
<Rectangle Grid.Row="1" Grid.Column="1" Fill="Black" Opacity="0.15" />
</Grid>
</VisualBrush.Visual>
</VisualBrush>
</Grid.Background>
<shared:DeviceVisualizer Device="{Binding Device}"
HighlightedLeds="{Binding SelectedLeds}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
ShowColors="True"
Margin="0 0 100 0" />
</Grid>
<GridSplitter Grid.Column="1" Width="15" Margin="-15 0 0 0" Background="Transparent" HorizontalAlignment="Stretch" Panel.ZIndex="3" />
<materialDesign:Card Grid.Column="2"
materialDesign:ShadowAssist.ShadowDepth="Depth3"
Background="{DynamicResource MaterialDesignPaper}">
<Grid>
<TabControl
Style="{StaticResource MaterialDesignTabControl}"
ItemsSource="{Binding Items}"
SelectedItem="{Binding ActiveItem}"
DisplayMemberPath="DisplayName">
<TabControl.ContentTemplate>
<DataTemplate>
<ContentControl s:View.Model="{Binding IsAsync=True}"
VerticalContentAlignment="Stretch"
HorizontalContentAlignment="Stretch"
IsTabStop="False"
TextElement.Foreground="{DynamicResource MaterialDesignBody}" />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
<materialDesign:Snackbar x:Name="DeviceSnackbar"
MessageQueue="{Binding DeviceMessageQueue}"
materialDesign:SnackbarMessage.InlineActionButtonMaxHeight="80"
materialDesign:SnackbarMessage.ContentMaxHeight="200" />
</Grid>
</materialDesign:Card>
</Grid> </Grid>
</DockPanel>
<GridSplitter Grid.Column="1" Width="15" Margin="-15 0 0 0" Background="Transparent" HorizontalAlignment="Stretch" Panel.ZIndex="3" /> </materialDesign:DialogHost>
<materialDesign:Card Grid.Column="2"
materialDesign:ShadowAssist.ShadowDepth="Depth3"
Background="{DynamicResource MaterialDesignPaper}">
<TabControl
Style="{StaticResource MaterialDesignTabControl}"
ItemsSource="{Binding Items}"
SelectedItem="{Binding ActiveItem}"
DisplayMemberPath="DisplayName">
<TabControl.ContentTemplate>
<DataTemplate>
<ContentControl s:View.Model="{Binding IsAsync=True}"
VerticalContentAlignment="Stretch"
HorizontalContentAlignment="Stretch"
IsTabStop="False"
TextElement.Foreground="{DynamicResource MaterialDesignBody}" />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</materialDesign:Card>
</Grid>
</DockPanel>
</mde:MaterialWindow> </mde:MaterialWindow>

View File

@ -9,6 +9,7 @@ using Artemis.Core.Services;
using Artemis.UI.Ninject.Factories; using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.Shared; using Artemis.UI.Screens.Shared;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
using MaterialDesignThemes.Wpf;
using Ookii.Dialogs.Wpf; using Ookii.Dialogs.Wpf;
using RGB.NET.Layout; using RGB.NET.Layout;
using Stylet; using Stylet;
@ -21,6 +22,7 @@ namespace Artemis.UI.Screens.Settings.Device
private readonly IDialogService _dialogService; private readonly IDialogService _dialogService;
private readonly IRgbService _rgbService; private readonly IRgbService _rgbService;
private ArtemisLed _selectedLed; private ArtemisLed _selectedLed;
private SnackbarMessageQueue _deviceMessageQueue;
public DeviceDialogViewModel(ArtemisDevice device, IDeviceService deviceService, IRgbService rgbService, IDialogService dialogService, IDeviceDebugVmFactory factory) public DeviceDialogViewModel(ArtemisDevice device, IDeviceService deviceService, IRgbService rgbService, IDialogService dialogService, IDeviceDebugVmFactory factory)
{ {
@ -38,9 +40,28 @@ namespace Artemis.UI.Screens.Settings.Device
DisplayName = $"{device.RgbDevice.DeviceInfo.Model} | Artemis"; DisplayName = $"{device.RgbDevice.DeviceInfo.Model} | Artemis";
} }
protected override void OnInitialActivate()
{
DeviceMessageQueue = new SnackbarMessageQueue(TimeSpan.FromSeconds(5));
Device.DeviceUpdated += DeviceOnDeviceUpdated;
base.OnInitialActivate();
}
protected override void OnClose()
{
Device.DeviceUpdated -= DeviceOnDeviceUpdated;
base.OnClose();
}
public ArtemisDevice Device { get; } public ArtemisDevice Device { get; }
public PanZoomViewModel PanZoomViewModel { get; } public PanZoomViewModel PanZoomViewModel { get; }
public SnackbarMessageQueue DeviceMessageQueue
{
get => _deviceMessageQueue;
set => SetAndNotify(ref _deviceMessageQueue, value);
}
public ArtemisLed SelectedLed public ArtemisLed SelectedLed
{ {
get => _selectedLed; get => _selectedLed;
@ -50,7 +71,10 @@ namespace Artemis.UI.Screens.Settings.Device
NotifyOfPropertyChange(nameof(SelectedLeds)); NotifyOfPropertyChange(nameof(SelectedLeds));
} }
} }
public List<ArtemisLed> SelectedLeds => SelectedLed != null ? new List<ArtemisLed> { SelectedLed } : null;
public bool CanExportLayout => Device.Layout?.IsValid ?? false;
public List<ArtemisLed> SelectedLeds => SelectedLed != null ? new List<ArtemisLed> {SelectedLed} : null;
public bool CanOpenImageDirectory => Device.Layout?.Image != null; public bool CanOpenImageDirectory => Device.Layout?.Image != null;
@ -165,5 +189,14 @@ namespace Artemis.UI.Screens.Settings.Device
#endregion #endregion
// ReSharper restore UnusedMember.Global // ReSharper restore UnusedMember.Global
#region Event handlers
private void DeviceOnDeviceUpdated(object? sender, EventArgs e)
{
NotifyOfPropertyChange(nameof(CanExportLayout));
}
#endregion
} }
} }

View File

@ -91,15 +91,21 @@
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBlock Style="{StaticResource MaterialDesignTextBlock}">Layout file path</TextBlock> <TextBlock Style="{StaticResource MaterialDesignTextBlock}">Default layout file path</TextBlock>
<Button Grid.Column="1" Style="{StaticResource MaterialDesignIconForegroundButton}" ToolTip="Copy path to clipboard" Width="24" Height="24"> <Button Grid.Column="1"
Style="{StaticResource MaterialDesignIconForegroundButton}"
ToolTip="Copy path to clipboard"
Width="24"
Height="24"
Command="{s:Action CopyToClipboard}"
CommandParameter="{Binding DefaultLayoutPath}">
<materialDesign:PackIcon Kind="ContentCopy" Width="18" Height="18" /> <materialDesign:PackIcon Kind="ContentCopy" Width="18" Height="18" />
</Button> </Button>
</Grid> </Grid>
<TextBlock Style="{StaticResource MaterialDesignTextBlock}" <TextBlock Style="{StaticResource MaterialDesignTextBlock}"
Foreground="{DynamicResource MaterialDesignNavigationItemSubheader}" Foreground="{DynamicResource MaterialDesignNavigationItemSubheader}"
TextWrapping="Wrap" TextWrapping="Wrap"
Text="{Binding Device.Layout.FilePath}" /> Text="{Binding DefaultLayoutPath}" />
<Separator Style="{StaticResource MaterialDesignSeparator}" Margin="0 5" /> <Separator Style="{StaticResource MaterialDesignSeparator}" Margin="0 5" />
<Grid> <Grid>
@ -108,14 +114,19 @@
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBlock Style="{StaticResource MaterialDesignTextBlock}">Image file path</TextBlock> <TextBlock Style="{StaticResource MaterialDesignTextBlock}">Image file path</TextBlock>
<Button Grid.Column="1" Style="{StaticResource MaterialDesignIconForegroundButton}" ToolTip="Copy path to clipboard" Width="24" Height="24"> <Button Grid.Column="1" Style="{StaticResource MaterialDesignIconForegroundButton}"
ToolTip="Copy path to clipboard"
Width="24"
Height="24"
Command="{s:Action CopyToClipboard}"
CommandParameter="{Binding Device.Layout.Image.LocalPath}">
<materialDesign:PackIcon Kind="ContentCopy" Width="18" Height="18" /> <materialDesign:PackIcon Kind="ContentCopy" Width="18" Height="18" />
</Button> </Button>
</Grid> </Grid>
<TextBlock Style="{StaticResource MaterialDesignTextBlock}" <TextBlock Style="{StaticResource MaterialDesignTextBlock}"
Foreground="{DynamicResource MaterialDesignNavigationItemSubheader}" Foreground="{DynamicResource MaterialDesignNavigationItemSubheader}"
TextWrapping="Wrap" TextWrapping="Wrap"
Text="{Binding Device.Layout.Image}" /> Text="{Binding Device.Layout.Image.LocalPath}" />
</StackPanel> </StackPanel>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@ -1,4 +1,5 @@
using Artemis.Core; using System.Windows;
using Artemis.Core;
using RGB.NET.Core; using RGB.NET.Core;
using Stylet; using Stylet;
@ -6,6 +7,8 @@ namespace Artemis.UI.Screens.Settings.Device.Tabs
{ {
public class DeviceInfoTabViewModel : Screen public class DeviceInfoTabViewModel : Screen
{ {
private string _defaultLayoutPath;
public DeviceInfoTabViewModel(ArtemisDevice device) public DeviceInfoTabViewModel(ArtemisDevice device)
{ {
Device = device; Device = device;
@ -14,5 +17,23 @@ namespace Artemis.UI.Screens.Settings.Device.Tabs
public bool IsKeyboard => Device.RgbDevice.DeviceInfo.DeviceType == RGBDeviceType.Keyboard; public bool IsKeyboard => Device.RgbDevice.DeviceInfo.DeviceType == RGBDeviceType.Keyboard;
public ArtemisDevice Device { get; } public ArtemisDevice Device { get; }
public string DefaultLayoutPath
{
get => _defaultLayoutPath;
set => SetAndNotify(ref _defaultLayoutPath, value);
}
public void CopyToClipboard(string content)
{
Clipboard.SetText(content);
((DeviceDialogViewModel) Parent).DeviceMessageQueue.Enqueue("Copied path to clipboard.");
}
protected override void OnInitialActivate()
{
DefaultLayoutPath = Device.DeviceProvider.LoadLayout(Device).FilePath;
base.OnInitialActivate();
}
} }
} }

View File

@ -174,20 +174,31 @@
</StackPanel> </StackPanel>
</materialDesign:HintAssist.Hint> </materialDesign:HintAssist.Hint>
</TextBox> </TextBox>
<Button Style="{StaticResource MaterialDesignRaisedButton}" Margin="0 8 8 0" Command="{s:Action SelectPhysicalLayout}">
SELECT PHYSICAL LAYOUT (PLACEHOLDER)
</Button>
</StackPanel> </StackPanel>
<!-- Buttons --> <!-- Buttons -->
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Right"> <Grid Grid.Row="1" >
<Button Style="{StaticResource MaterialDesignOutlinedButton}" IsCancel="True" Margin="0 8 8 0" Command="{s:Action Reset}"> <Grid.ColumnDefinitions>
RESET <ColumnDefinition Width="Auto"/>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Button Grid.Column="0"
Style="{StaticResource MaterialDesignOutlinedButton}"
Margin="0 8 8 0"
Command="{s:Action SelectPhysicalLayout}"
ToolTip="Restart device setup, allowing you to select a new physical and logical layout">
RESTART SETUP
</Button> </Button>
<Button Style="{StaticResource MaterialDesignRaisedButton}" IsDefault="True" Margin="0 8 8 0" Command="{s:Action Apply}">
APPLY <StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right">
</Button> <Button Style="{StaticResource MaterialDesignOutlinedButton}" IsCancel="True" Margin="0 8 8 0" Command="{s:Action Reset}">
</StackPanel> RESET
</Button>
<Button Style="{StaticResource MaterialDesignRaisedButton}" IsDefault="True" Margin="0 8 8 0" Command="{s:Action Apply}">
APPLY
</Button>
</StackPanel>
</Grid>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@ -15,7 +15,6 @@ namespace Artemis.UI.Screens.Settings.Device.Tabs
public class DevicePropertiesTabViewModel : Screen public class DevicePropertiesTabViewModel : Screen
{ {
private readonly ICoreService _coreService; private readonly ICoreService _coreService;
private readonly IMessageService _messageService;
private readonly IDialogService _dialogService; private readonly IDialogService _dialogService;
private readonly IRgbService _rgbService; private readonly IRgbService _rgbService;
private float _blueScale; private float _blueScale;
@ -34,13 +33,11 @@ namespace Artemis.UI.Screens.Settings.Device.Tabs
public DevicePropertiesTabViewModel(ArtemisDevice device, public DevicePropertiesTabViewModel(ArtemisDevice device,
ICoreService coreService, ICoreService coreService,
IRgbService rgbService, IRgbService rgbService,
IMessageService messageService,
IDialogService dialogService, IDialogService dialogService,
IModelValidator<DevicePropertiesTabViewModel> validator) : base(validator) IModelValidator<DevicePropertiesTabViewModel> validator) : base(validator)
{ {
_coreService = coreService; _coreService = coreService;
_rgbService = rgbService; _rgbService = rgbService;
_messageService = messageService;
_dialogService = dialogService; _dialogService = dialogService;
Device = device; Device = device;
@ -115,7 +112,7 @@ namespace Artemis.UI.Screens.Settings.Device.Tabs
if (e.OriginalSource is Button) if (e.OriginalSource is Button)
{ {
Device.CustomLayoutPath = null; Device.CustomLayoutPath = null;
_messageService.ShowMessage("Cleared imported layout"); ((DeviceDialogViewModel) Parent).DeviceMessageQueue.Enqueue("Cleared imported layout.");
return; return;
} }
@ -126,13 +123,13 @@ namespace Artemis.UI.Screens.Settings.Device.Tabs
if (result == true) if (result == true)
{ {
Device.CustomLayoutPath = dialog.FileName; Device.CustomLayoutPath = dialog.FileName;
_messageService.ShowMessage($"Imported layout from {dialog.FileName}"); ((DeviceDialogViewModel) Parent).DeviceMessageQueue.Enqueue($"Imported layout from {dialog.FileName}.");
} }
} }
public async Task SelectPhysicalLayout() public async Task SelectPhysicalLayout()
{ {
await _dialogService.ShowDialog<DeviceLayoutDialogViewModel>(new Dictionary<string, object> {{"device", Device}}); await _dialogService.ShowDialogAt<DeviceLayoutDialogViewModel>("DeviceDialog", new Dictionary<string, object> {{"device", Device}});
} }
public async Task Apply() public async Task Apply()
@ -151,6 +148,7 @@ namespace Artemis.UI.Screens.Settings.Device.Tabs
Device.RedScale = RedScale / 100f; Device.RedScale = RedScale / 100f;
Device.GreenScale = GreenScale / 100f; Device.GreenScale = GreenScale / 100f;
Device.BlueScale = BlueScale / 100f; Device.BlueScale = BlueScale / 100f;
_rgbService.SaveDevice(Device);
_coreService.ModuleRenderingDisabled = false; _coreService.ModuleRenderingDisabled = false;
} }
@ -195,7 +193,8 @@ namespace Artemis.UI.Screens.Settings.Device.Tabs
private void DeviceOnPropertyChanged(object sender, PropertyChangedEventArgs e) private void DeviceOnPropertyChanged(object sender, PropertyChangedEventArgs e)
{ {
if (e.PropertyName == nameof(Device.CustomLayoutPath)) _rgbService.ApplyBestDeviceLayout(Device); if (e.PropertyName == nameof(Device.CustomLayoutPath))
_rgbService.ApplyBestDeviceLayout(Device);
} }
private void OnFrameRendering(object sender, FrameRenderingEventArgs e) private void OnFrameRendering(object sender, FrameRenderingEventArgs e)

View File

@ -32,13 +32,20 @@
<ProgressBar IsIndeterminate="True" /> <ProgressBar IsIndeterminate="True" />
</StackPanel> </StackPanel>
<ItemsControl ItemsSource="{Binding Changes}" Visibility="{Binding RetrievingChanges, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}, Mode=OneWay}"> <Grid Visibility="{Binding RetrievingChanges, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}, Mode=OneWay}">
<ItemsControl.ItemTemplate> <ItemsControl ItemsSource="{Binding Changes}" Visibility="{Binding HasChanges, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}">
<DataTemplate> <ItemsControl.ItemTemplate>
<TextBlock Text="{Binding}" Style="{StaticResource MaterialDesignSubtitle1TextBlock}" Foreground="{DynamicResource MaterialDesignBodyLight}"></TextBlock> <DataTemplate>
</DataTemplate> <TextBlock Text="{Binding}" Style="{StaticResource MaterialDesignSubtitle1TextBlock}" Foreground="{DynamicResource MaterialDesignBodyLight}"></TextBlock>
</ItemsControl.ItemTemplate> </DataTemplate>
</ItemsControl> </ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Visibility="{Binding HasChanges, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}, Mode=OneWay}"
Style="{StaticResource MaterialDesignSubtitle1TextBlock}"
Foreground="{DynamicResource MaterialDesignBodyLight}">
Couldn't retrieve changes, sorry :(
</TextBlock>
</Grid>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0 8 0 0"> <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0 8 0 0">
<Button Style="{StaticResource MaterialDesignFlatButton}" <Button Style="{StaticResource MaterialDesignFlatButton}"

View File

@ -1,28 +1,42 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Artemis.Core; using Artemis.Core;
using Artemis.UI.Services; using Artemis.UI.Services;
using Artemis.UI.Services.Models.UpdateService; using Artemis.UI.Services.Models.UpdateService;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
using Newtonsoft.Json.Linq;
using Stylet; using Stylet;
namespace Artemis.UI.Screens.Settings.Dialogs namespace Artemis.UI.Screens.Settings.Dialogs
{ {
public class UpdateDialogViewModel : DialogViewModelBase public class UpdateDialogViewModel : DialogViewModelBase
{ {
private readonly DevOpsBuild _buildInfo;
private readonly IDialogService _dialogService; private readonly IDialogService _dialogService;
// Based on https://docs.microsoft.com/en-us/azure/devops/pipelines/repos/github?view=azure-devops&tabs=yaml#skipping-ci-for-individual-commits
private readonly string[] _excludedCommitMessages =
{
"[skip ci]",
"[ci skip]",
"skip-checks: true",
"skip-checks:true",
"[skip azurepipelines]",
"[azurepipelines skip]",
"[skip azpipelines]",
"[azpipelines skip]",
"[skip azp]",
"[azp skip]",
"***NO_CI***"
};
private readonly IMessageService _messageService; private readonly IMessageService _messageService;
private readonly IUpdateService _updateService; private readonly IUpdateService _updateService;
private bool _canUpdate = true; private bool _canUpdate = true;
private bool _hasChanges;
private bool _retrievingChanges; private bool _retrievingChanges;
public UpdateDialogViewModel(DevOpsBuild buildInfo, IUpdateService updateService, IDialogService dialogService, IMessageService messageService) public UpdateDialogViewModel(DevOpsBuild buildInfo, IUpdateService updateService, IDialogService dialogService, IMessageService messageService)
{ {
_buildInfo = buildInfo;
_updateService = updateService; _updateService = updateService;
_dialogService = dialogService; _dialogService = dialogService;
_messageService = messageService; _messageService = messageService;
@ -44,12 +58,37 @@ namespace Artemis.UI.Screens.Settings.Dialogs
set => SetAndNotify(ref _retrievingChanges, value); set => SetAndNotify(ref _retrievingChanges, value);
} }
public bool HasChanges
{
get => _hasChanges;
set => SetAndNotify(ref _hasChanges, value);
}
public bool CanUpdate public bool CanUpdate
{ {
get => _canUpdate; get => _canUpdate;
set => SetAndNotify(ref _canUpdate, value); set => SetAndNotify(ref _canUpdate, value);
} }
public async Task Update()
{
try
{
CanUpdate = false;
await _updateService.ApplyUpdate();
}
catch (Exception e)
{
_dialogService.ShowExceptionDialog("An exception occurred while applying the update", e);
}
finally
{
CanUpdate = true;
}
Session.Close(true);
}
private async Task GetBuildChanges() private async Task GetBuildChanges()
{ {
try try
@ -70,9 +109,10 @@ namespace Artemis.UI.Screens.Settings.Dialogs
Changes.AddRange(difference.Commits.Where(c => c.Parents.Count == 1) Changes.AddRange(difference.Commits.Where(c => c.Parents.Count == 1)
.SelectMany(c => c.Commit.Message.Split("\n")) .SelectMany(c => c.Commit.Message.Split("\n"))
.Select(m => m.Trim()) .Select(m => m.Trim())
.Where(m => !string.IsNullOrWhiteSpace(m)) .Where(m => !string.IsNullOrWhiteSpace(m) && !_excludedCommitMessages.Contains(m))
.OrderBy(m => m) .OrderBy(m => m)
); );
HasChanges = Changes.Any();
} }
} }
catch (Exception e) catch (Exception e)
@ -84,24 +124,5 @@ namespace Artemis.UI.Screens.Settings.Dialogs
RetrievingChanges = false; RetrievingChanges = false;
} }
} }
public async Task Update()
{
try
{
CanUpdate = false;
await _updateService.ApplyUpdate();
}
catch (Exception e)
{
_dialogService.ShowExceptionDialog("An exception occurred while applying the update", e);
}
finally
{
CanUpdate = true;
}
Session.Close();
}
} }
} }

View File

@ -3,17 +3,20 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection.Metadata;
using System.Security.Principal; using System.Security.Principal;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Xml.Linq; using System.Xml.Linq;
using Artemis.Core; using Artemis.Core;
using Artemis.Core.LayerBrushes; using Artemis.Core.LayerBrushes;
using Artemis.Core.Services; using Artemis.Core.Services;
using Artemis.Core.Services.Core;
using Artemis.UI.Properties; using Artemis.UI.Properties;
using Artemis.UI.Screens.StartupWizard; using Artemis.UI.Screens.StartupWizard;
using Artemis.UI.Services; using Artemis.UI.Services;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
using Artemis.UI.Utilities;
using Ninject; using Ninject;
using Serilog.Events; using Serilog.Events;
using Stylet; using Stylet;
@ -70,7 +73,7 @@ namespace Artemis.UI.Screens.Settings.Tabs.General
_defaultLayerBrushDescriptor = _settingsService.GetSetting("ProfileEditor.DefaultLayerBrushDescriptor", new LayerBrushReference _defaultLayerBrushDescriptor = _settingsService.GetSetting("ProfileEditor.DefaultLayerBrushDescriptor", new LayerBrushReference
{ {
LayerBrushProviderId = "Artemis.Plugins.LayerBrushes.Color.ColorBrushProvider-92a9d6ba", LayerBrushProviderId = "Artemis.Plugins.LayerBrushes.Color.ColorBrushProvider-92a9d6ba",
BrushType = "ColorBrush" BrushType = "SolidBrush"
}); });
WebServerPortSetting = _settingsService.GetSetting("WebServer.Port", 9696); WebServerPortSetting = _settingsService.GetSetting("WebServer.Port", 9696);
@ -274,10 +277,20 @@ namespace Artemis.UI.Screens.Settings.Tabs.General
return; return;
CanOfferUpdatesIfFound = false; CanOfferUpdatesIfFound = false;
bool updateFound = await _updateService.OfferUpdateIfFound(); try
if (!updateFound) {
_messageService.ShowMessage("You are already running the latest Artemis build. (☞゚ヮ゚)☞"); bool updateFound = await _updateService.OfferUpdateIfFound();
CanOfferUpdatesIfFound = true; if (!updateFound)
_messageService.ShowMessage("You are already running the latest Artemis build. (☞゚ヮ゚)☞");
}
catch (Exception exception)
{
_messageService.ShowMessage($"Failed to check for updates: {exception.Message}");
}
finally
{
CanOfferUpdatesIfFound = true;
}
} }
protected override void OnInitialActivate() protected override void OnInitialActivate()
@ -296,7 +309,8 @@ namespace Artemis.UI.Screens.Settings.Tabs.General
if (File.Exists(autoRunFile)) if (File.Exists(autoRunFile))
File.Delete(autoRunFile); File.Delete(autoRunFile);
// TODO: Don't do anything if running a development build, only auto-run release builds if (Constants.BuildInfo.IsLocalBuild)
return;
// Create or remove the task if necessary // Create or remove the task if necessary
try try
@ -304,26 +318,13 @@ namespace Artemis.UI.Screens.Settings.Tabs.General
bool taskCreated = false; bool taskCreated = false;
if (!recreate) if (!recreate)
{ {
Process schtasks = new() taskCreated = SettingsUtilities.IsAutoRunTaskCreated();
{
StartInfo =
{
WindowStyle = ProcessWindowStyle.Hidden,
UseShellExecute = true,
FileName = Path.Combine(Environment.SystemDirectory, "schtasks.exe"),
Arguments = "/TN \"Artemis 2 autorun\""
}
};
schtasks.Start();
schtasks.WaitForExit();
taskCreated = schtasks.ExitCode == 0;
} }
if (StartWithWindows && !taskCreated) if (StartWithWindows && !taskCreated)
CreateAutoRunTask(); SettingsUtilities.CreateAutoRunTask(TimeSpan.FromSeconds(AutoRunDelay));
else if (!StartWithWindows && taskCreated) else if (!StartWithWindows && taskCreated)
RemoveAutoRunTask(); SettingsUtilities.RemoveAutoRunTask();
} }
catch (Exception e) catch (Exception e)
{ {
@ -331,71 +332,10 @@ namespace Artemis.UI.Screens.Settings.Tabs.General
throw; throw;
} }
} }
private void CreateAutoRunTask()
{
XDocument document = XDocument.Parse(Resources.artemis_autorun);
XElement task = document.Descendants().First();
task.Descendants().First(d => d.Name.LocalName == "RegistrationInfo").Descendants().First(d => d.Name.LocalName == "Date")
.SetValue(DateTime.Now);
task.Descendants().First(d => d.Name.LocalName == "RegistrationInfo").Descendants().First(d => d.Name.LocalName == "Author")
.SetValue(WindowsIdentity.GetCurrent().Name);
task.Descendants().First(d => d.Name.LocalName == "Triggers").Descendants().First(d => d.Name.LocalName == "LogonTrigger").Descendants().First(d => d.Name.LocalName == "Delay")
.SetValue(TimeSpan.FromSeconds(AutoRunDelay));
task.Descendants().First(d => d.Name.LocalName == "Principals").Descendants().First(d => d.Name.LocalName == "Principal").Descendants().First(d => d.Name.LocalName == "UserId")
.SetValue(WindowsIdentity.GetCurrent().User.Value);
task.Descendants().First(d => d.Name.LocalName == "Actions").Descendants().First(d => d.Name.LocalName == "Exec").Descendants().First(d => d.Name.LocalName == "WorkingDirectory")
.SetValue(Constants.ApplicationFolder);
task.Descendants().First(d => d.Name.LocalName == "Actions").Descendants().First(d => d.Name.LocalName == "Exec").Descendants().First(d => d.Name.LocalName == "Command")
.SetValue("\"" + Constants.ExecutablePath + "\"");
string xmlPath = Path.GetTempFileName();
using (Stream fileStream = new FileStream(xmlPath, FileMode.Create))
{
document.Save(fileStream);
}
Process schtasks = new()
{
StartInfo =
{
WindowStyle = ProcessWindowStyle.Hidden,
UseShellExecute = true,
Verb = "runas",
FileName = Path.Combine(Environment.SystemDirectory, "schtasks.exe"),
Arguments = $"/Create /XML \"{xmlPath}\" /tn \"Artemis 2 autorun\" /F"
}
};
schtasks.Start();
schtasks.WaitForExit();
File.Delete(xmlPath);
}
private void RemoveAutoRunTask()
{
Process schtasks = new()
{
StartInfo =
{
WindowStyle = ProcessWindowStyle.Hidden,
UseShellExecute = true,
Verb = "runas",
FileName = Path.Combine(Environment.SystemDirectory, "schtasks.exe"),
Arguments = "/Delete /TN \"Artemis 2 autorun\" /f"
}
};
schtasks.Start();
schtasks.WaitForExit();
}
} }
public enum ApplicationColorScheme public enum ApplicationColorScheme
{ {
Light, Light,

View File

@ -23,7 +23,7 @@
<!-- Icon column --> <!-- Icon column -->
<shared:ArtemisIcon Grid.Column="0" <shared:ArtemisIcon Grid.Column="0"
Icon="{Binding Feature.Info.Icon}" Icon="{Binding FeatureInfo.Icon}"
Width="20" Width="20"
VerticalAlignment="Center" VerticalAlignment="Center"
HorizontalAlignment="Center" HorizontalAlignment="Center"
@ -42,12 +42,21 @@
</Button> </Button>
<!-- Display name column --> <!-- Display name column -->
<TextBlock Grid.Column="1" Text="{Binding Feature.Info.Name}" Style="{StaticResource MaterialDesignTextBlock}" VerticalAlignment="Center" ToolTip="{Binding Feature.Info.Description}" /> <TextBlock Grid.Column="1" Text="{Binding FeatureInfo.Name}" Style="{StaticResource MaterialDesignTextBlock}" VerticalAlignment="Center" ToolTip="{Binding FeatureInfo.Description}" />
<!-- Enable toggle column --> <!-- Enable toggle column -->
<StackPanel Grid.Column="2" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="8" <StackPanel Grid.Column="2"
Visibility="{Binding Enabling, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}, Mode=OneWay, FallbackValue=Collapsed}"> HorizontalAlignment="Right"
<CheckBox Style="{StaticResource MaterialDesignCheckBox}" IsChecked="{Binding IsEnabled}"> VerticalAlignment="Center"
Margin="8"
Visibility="{Binding Enabling, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}, Mode=OneWay, FallbackValue=Collapsed}"
Orientation="Horizontal">
<materialDesign:PackIcon Kind="ShieldHalfFull"
ToolTip="Plugin requires admin rights"
VerticalAlignment="Center"
Margin="0 0 5 0"
Visibility="{Binding ShowShield, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}" />
<CheckBox Style="{StaticResource MaterialDesignCheckBox}" IsChecked="{Binding IsEnabled}" IsEnabled="{Binding FeatureInfo.Plugin.IsEnabled}">
Feature enabled Feature enabled
</CheckBox> </CheckBox>
</StackPanel> </StackPanel>

View File

@ -11,25 +11,32 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
{ {
public class PluginFeatureViewModel : Screen public class PluginFeatureViewModel : Screen
{ {
private readonly ICoreService _coreService;
private readonly IDialogService _dialogService; private readonly IDialogService _dialogService;
private readonly IPluginManagementService _pluginManagementService; private readonly IPluginManagementService _pluginManagementService;
private bool _enabling; private bool _enabling;
private readonly IMessageService _messageService; private readonly IMessageService _messageService;
public PluginFeatureViewModel(PluginFeature feature, public PluginFeatureViewModel(PluginFeatureInfo pluginFeatureInfo,
bool showShield,
ICoreService coreService,
IDialogService dialogService, IDialogService dialogService,
IPluginManagementService pluginManagementService, IPluginManagementService pluginManagementService,
IMessageService messageService) IMessageService messageService)
{ {
_coreService = coreService;
_dialogService = dialogService; _dialogService = dialogService;
_pluginManagementService = pluginManagementService; _pluginManagementService = pluginManagementService;
_messageService = messageService; _messageService = messageService;
Feature = feature; FeatureInfo = pluginFeatureInfo;
ShowShield = FeatureInfo.Plugin.Info.RequiresAdmin && showShield;
} }
public PluginFeature Feature { get; } public PluginFeatureInfo FeatureInfo { get; }
public Exception LoadException => Feature.LoadException; public Exception LoadException => FeatureInfo.Instance?.LoadException;
public bool ShowShield { get; }
public bool Enabling public bool Enabling
{ {
@ -39,7 +46,7 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
public bool IsEnabled public bool IsEnabled
{ {
get => Feature.IsEnabled; get => FeatureInfo.Instance != null && FeatureInfo.Instance.IsEnabled;
set => Task.Run(() => UpdateEnabled(value)); set => Task.Run(() => UpdateEnabled(value));
} }
@ -60,7 +67,7 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
if (LoadException == null) if (LoadException == null)
return; return;
_dialogService.ShowExceptionDialog("Feature failed to enable", Feature.LoadException); _dialogService.ShowExceptionDialog("Feature failed to enable", LoadException);
} }
protected override void OnInitialActivate() protected override void OnInitialActivate()
@ -83,7 +90,7 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
private async Task UpdateEnabled(bool enable) private async Task UpdateEnabled(bool enable)
{ {
if (IsEnabled == enable) if (IsEnabled == enable || FeatureInfo.Instance == null)
{ {
NotifyOfPropertyChange(nameof(IsEnabled)); NotifyOfPropertyChange(nameof(IsEnabled));
return; return;
@ -95,11 +102,21 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
try try
{ {
await Task.Run(() => _pluginManagementService.EnablePluginFeature(Feature, true)); if (FeatureInfo.Plugin.Info.RequiresAdmin && !_coreService.IsElevated)
{
bool confirmed = await _dialogService.ShowConfirmDialog("Enable feature", "The plugin of this feature requires admin rights, are you sure you want to enable it?");
if (!confirmed)
{
NotifyOfPropertyChange(nameof(IsEnabled));
return;
}
}
await Task.Run(() => _pluginManagementService.EnablePluginFeature(FeatureInfo.Instance, true));
} }
catch (Exception e) catch (Exception e)
{ {
_messageService.ShowMessage($"Failed to enable {Feature.Info.Name}\r\n{e.Message}", "VIEW LOGS", ShowLogsFolder); _messageService.ShowMessage($"Failed to enable {FeatureInfo.Name}\r\n{e.Message}", "VIEW LOGS", ShowLogsFolder);
} }
finally finally
{ {
@ -108,7 +125,7 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
} }
else else
{ {
_pluginManagementService.DisablePluginFeature(Feature, true); _pluginManagementService.DisablePluginFeature(FeatureInfo.Instance, true);
NotifyOfPropertyChange(nameof(IsEnabled)); NotifyOfPropertyChange(nameof(IsEnabled));
} }
} }
@ -117,13 +134,13 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
private void OnFeatureEnabling(object sender, PluginFeatureEventArgs e) private void OnFeatureEnabling(object sender, PluginFeatureEventArgs e)
{ {
if (e.PluginFeature != Feature) return; if (e.PluginFeature != FeatureInfo.Instance) return;
Enabling = true; Enabling = true;
} }
private void OnFeatureEnableStopped(object sender, PluginFeatureEventArgs e) private void OnFeatureEnableStopped(object sender, PluginFeatureEventArgs e)
{ {
if (e.PluginFeature != Feature) return; if (e.PluginFeature != FeatureInfo.Instance) return;
Enabling = false; Enabling = false;
NotifyOfPropertyChange(nameof(IsEnabled)); NotifyOfPropertyChange(nameof(IsEnabled));

View File

@ -116,21 +116,11 @@
<RowDefinition Height="*" /> <RowDefinition Height="*" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<TextBlock Margin="10 10 0 5" Style="{StaticResource MaterialDesignBody2TextBlock}">Plugin features</TextBlock> <TextBlock Margin="10 10 0 5" Style="{StaticResource MaterialDesignBody2TextBlock}">Plugin features</TextBlock>
<TextBlock Grid.Row="1"
HorizontalAlignment="Center"
Margin="0 30 0 0"
Style="{StaticResource MaterialDesignTextBlock}"
Foreground="{DynamicResource MaterialDesignNavigationItemSubheader}"
Visibility="{Binding IsEnabled, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}, Mode=OneWay}">
Enable the plugin to view its features
</TextBlock>
<ListBox Grid.Row="1" <ListBox Grid.Row="1"
MaxHeight="135" MaxHeight="135"
ItemsSource="{Binding Items}" ItemsSource="{Binding Items}"
HorizontalContentAlignment="Stretch" HorizontalContentAlignment="Stretch"
VirtualizingPanel.ScrollUnit="Pixel" VirtualizingPanel.ScrollUnit="Pixel">
Visibility="{Binding IsEnabled, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}">
<b:Interaction.Behaviors> <b:Interaction.Behaviors>
<shared:ScrollParentWhenAtMax /> <shared:ScrollParentWhenAtMax />
</b:Interaction.Behaviors> </b:Interaction.Behaviors>

View File

@ -117,22 +117,12 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
protected override void OnInitialActivate() protected override void OnInitialActivate()
{ {
Plugin.FeatureAdded += PluginOnFeatureAdded; foreach (PluginFeatureInfo pluginFeatureInfo in Plugin.Features)
Plugin.FeatureRemoved += PluginOnFeatureRemoved; Items.Add(_settingsVmFactory.CreatePluginFeatureViewModel(pluginFeatureInfo, false));
foreach (PluginFeature pluginFeature in Plugin.Features)
Items.Add(_settingsVmFactory.CreatePluginFeatureViewModel(pluginFeature));
base.OnInitialActivate(); base.OnInitialActivate();
} }
protected override void OnClose()
{
Plugin.FeatureAdded -= PluginOnFeatureAdded;
Plugin.FeatureRemoved -= PluginOnFeatureRemoved;
base.OnClose();
}
private async Task UpdateEnabled(bool enable) private async Task UpdateEnabled(bool enable)
{ {
if (IsEnabled == enable) if (IsEnabled == enable)
@ -178,17 +168,5 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
NotifyOfPropertyChange(nameof(IsEnabled)); NotifyOfPropertyChange(nameof(IsEnabled));
NotifyOfPropertyChange(nameof(CanOpenSettings)); NotifyOfPropertyChange(nameof(CanOpenSettings));
} }
private void PluginOnFeatureRemoved(object sender, PluginFeatureEventArgs e)
{
PluginFeatureViewModel viewModel = Items.FirstOrDefault(i => i.Feature == e.PluginFeature);
if (viewModel != null)
Items.Remove(viewModel);
}
private void PluginOnFeatureAdded(object sender, PluginFeatureEventArgs e)
{
Items.Add(_settingsVmFactory.CreatePluginFeatureViewModel(e.PluginFeature));
}
} }
} }

View File

@ -5,6 +5,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mde="https://spiegelp.github.io/MaterialDesignExtensions/winfx/xaml" xmlns:mde="https://spiegelp.github.io/MaterialDesignExtensions/winfx/xaml"
xmlns:s="https://github.com/canton7/Stylet" xmlns:s="https://github.com/canton7/Stylet"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d" mc:Ignorable="d"
Width="800" Width="800"
Height="600" Height="600"
@ -17,47 +18,58 @@
FontFamily="pack://application:,,,/MaterialDesignThemes.Wpf;component/Resources/Roboto/#Roboto" FontFamily="pack://application:,,,/MaterialDesignThemes.Wpf;component/Resources/Roboto/#Roboto"
UseLayoutRounding="True" UseLayoutRounding="True"
d:DesignHeight="450" d:DesignWidth="800"> d:DesignHeight="450" d:DesignWidth="800">
<materialDesign:DialogHost IsTabStop="False"
<mde:Stepper IsLinear="True" Layout="Horizontal" ActiveStepChanged="{s:Action ActiveStepChanged}" CancelNavigation="{s:Action SkipOrFinishWizard}" Margin="15"> Focusable="False"
<mde:Step> Identifier="RootDialog"
<mde:Step.Header> DialogTheme="Inherit"
<mde:StepTitleHeader FirstLevelTitle="Welcome" /> SnackbarMessageQueue="{Binding MainMessageQueue}">
</mde:Step.Header> <Grid>
<mde:Step.Content> <mde:Stepper IsLinear="True" Layout="Horizontal" ActiveStepChanged="{s:Action ActiveStepChanged}" CancelNavigation="{s:Action SkipOrFinishWizard}" Margin="15">
<ContentControl s:View.Model="{Binding ActiveItem}" /> <mde:Step>
</mde:Step.Content> <mde:Step.Header>
</mde:Step> <mde:StepTitleHeader FirstLevelTitle="Welcome" />
<mde:Step> </mde:Step.Header>
<mde:Step.Header> <mde:Step.Content>
<mde:StepTitleHeader FirstLevelTitle="Devices" SecondLevelTitle="Pick your brands" /> <ContentControl s:View.Model="{Binding ActiveItem}" />
</mde:Step.Header> </mde:Step.Content>
<mde:Step.Content> </mde:Step>
<ContentControl s:View.Model="{Binding ActiveItem}" /> <mde:Step>
</mde:Step.Content> <mde:Step.Header>
</mde:Step> <mde:StepTitleHeader FirstLevelTitle="Devices" SecondLevelTitle="Pick your brands" />
<mde:Step> </mde:Step.Header>
<mde:Step.Header> <mde:Step.Content>
<mde:StepTitleHeader FirstLevelTitle="Desktop layout" SecondLevelTitle="Map your surface" /> <ContentControl s:View.Model="{Binding ActiveItem}" />
</mde:Step.Header> </mde:Step.Content>
<mde:Step.Content> </mde:Step>
<ContentControl s:View.Model="{Binding ActiveItem}" /> <mde:Step>
</mde:Step.Content> <mde:Step.Header>
</mde:Step> <mde:StepTitleHeader FirstLevelTitle="Desktop layout" SecondLevelTitle="Map your surface" />
<mde:Step> </mde:Step.Header>
<mde:Step.Header> <mde:Step.Content>
<mde:StepTitleHeader FirstLevelTitle="Settings" SecondLevelTitle="Set your preferences" /> <ContentControl s:View.Model="{Binding ActiveItem}" />
</mde:Step.Header> </mde:Step.Content>
<mde:Step.Content> </mde:Step>
<ContentControl s:View.Model="{Binding ActiveItem}" /> <mde:Step>
</mde:Step.Content> <mde:Step.Header>
</mde:Step> <mde:StepTitleHeader FirstLevelTitle="Settings" SecondLevelTitle="Set your preferences" />
<mde:Step> </mde:Step.Header>
<mde:Step.Header> <mde:Step.Content>
<mde:StepTitleHeader FirstLevelTitle="Finish" /> <ContentControl s:View.Model="{Binding ActiveItem}" />
</mde:Step.Header> </mde:Step.Content>
<mde:Step.Content> </mde:Step>
<ContentControl s:View.Model="{Binding ActiveItem}" /> <mde:Step>
</mde:Step.Content> <mde:Step.Header>
</mde:Step> <mde:StepTitleHeader FirstLevelTitle="Finish" />
</mde:Stepper> </mde:Step.Header>
<mde:Step.Content>
<ContentControl s:View.Model="{Binding ActiveItem}" />
</mde:Step.Content>
</mde:Step>
</mde:Stepper>
<materialDesign:Snackbar x:Name="MainSnackbar"
MessageQueue="{Binding MainMessageQueue}"
materialDesign:SnackbarMessage.InlineActionButtonMaxHeight="80"
materialDesign:SnackbarMessage.ContentMaxHeight="200" />
</Grid>
</materialDesign:DialogHost>
</mde:MaterialWindow> </mde:MaterialWindow>

View File

@ -2,18 +2,22 @@
using Artemis.Core; using Artemis.Core;
using Artemis.Core.Services; using Artemis.Core.Services;
using Artemis.UI.Screens.StartupWizard.Steps; using Artemis.UI.Screens.StartupWizard.Steps;
using Artemis.UI.Shared.Services;
using MaterialDesignExtensions.Controllers; using MaterialDesignExtensions.Controllers;
using MaterialDesignExtensions.Controls; using MaterialDesignExtensions.Controls;
using MaterialDesignThemes.Wpf;
using Stylet; using Stylet;
namespace Artemis.UI.Screens.StartupWizard namespace Artemis.UI.Screens.StartupWizard
{ {
public class StartupWizardViewModel : Conductor<Screen>.Collection.OneActive public class StartupWizardViewModel : Conductor<Screen>.Collection.OneActive
{ {
private readonly IMessageService _messageService;
private readonly ISettingsService _settingsService; private readonly ISettingsService _settingsService;
private StepperController _stepperController; private StepperController _stepperController;
public StartupWizardViewModel(ISettingsService settingsService, public StartupWizardViewModel(ISettingsService settingsService,
IMessageService messageService,
WelcomeStepViewModel welcome, WelcomeStepViewModel welcome,
DevicesStepViewModel devices, DevicesStepViewModel devices,
LayoutStepViewModel layout, LayoutStepViewModel layout,
@ -21,6 +25,7 @@ namespace Artemis.UI.Screens.StartupWizard
FinishStepViewModel finish) FinishStepViewModel finish)
{ {
_settingsService = settingsService; _settingsService = settingsService;
_messageService = messageService;
Items.Add(welcome); Items.Add(welcome);
Items.Add(devices); Items.Add(devices);
Items.Add(layout); Items.Add(layout);
@ -30,6 +35,8 @@ namespace Artemis.UI.Screens.StartupWizard
ActiveItem = Items.First(); ActiveItem = Items.First();
} }
public ISnackbarMessageQueue MainMessageQueue { get; set; }
public void ActiveStepChanged(object sender, ActiveStepChangedEventArgs e) public void ActiveStepChanged(object sender, ActiveStepChangedEventArgs e)
{ {
Stepper stepper = (Stepper) sender; Stepper stepper = (Stepper) sender;
@ -40,22 +47,23 @@ namespace Artemis.UI.Screens.StartupWizard
} }
public void SkipOrFinishWizard() public void SkipOrFinishWizard()
{
RequestClose();
}
protected override void OnClose()
{ {
PluginSetting<bool> setting = _settingsService.GetSetting("UI.SetupWizardCompleted", false); PluginSetting<bool> setting = _settingsService.GetSetting("UI.SetupWizardCompleted", false);
setting.Value = true; setting.Value = true;
setting.Save(); setting.Save();
base.OnClose(); RequestClose();
} }
public void Continue() public void Continue()
{ {
_stepperController.Continue(); _stepperController.Continue();
} }
protected override void OnInitialActivate()
{
MainMessageQueue = _messageService.MainMessageQueue;
base.OnInitialActivate();
}
} }
} }

View File

@ -16,7 +16,7 @@
</Grid.RowDefinitions> </Grid.RowDefinitions>
<TextBlock Grid.Row="0" Style="{StaticResource MaterialDesignBody1TextBlock}" TextWrapping="Wrap"> <TextBlock Grid.Row="0" Style="{StaticResource MaterialDesignBody1TextBlock}" TextWrapping="Wrap">
EnabledDevices are supported through the use of device providers. <LineBreak /> Devices are supported through the use of device providers. <LineBreak />
In the list below you can enable device providers for each brand you own by checking <Run Text="Feature enabled" FontWeight="Bold" />. In the list below you can enable device providers for each brand you own by checking <Run Text="Feature enabled" FontWeight="Bold" />.
</TextBlock> </TextBlock>

View File

@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Artemis.Core;
using Artemis.Core.DeviceProviders; using Artemis.Core.DeviceProviders;
using Artemis.Core.Services; using Artemis.Core.Services;
using Artemis.UI.Ninject.Factories; using Artemis.UI.Ninject.Factories;
@ -27,11 +28,10 @@ namespace Artemis.UI.Screens.StartupWizard.Steps
Items.Clear(); Items.Clear();
// _pluginManagementService.GetFeaturesOfType<>() will only give us enabled features so lets get all of them this way // _pluginManagementService.GetFeaturesOfType<>() will only give us enabled features so lets get all of them this way
IEnumerable<DeviceProvider> features = _pluginManagementService.GetAllPlugins() IEnumerable<PluginFeatureInfo> features = _pluginManagementService.GetAllPlugins()
.SelectMany(p => p.Features.Where(f => f is DeviceProvider)) .SelectMany(p => p.Features.Where(f => typeof(DeviceProvider).IsAssignableFrom(f.FeatureType)))
.Cast<DeviceProvider>()
.OrderBy(d => d.GetType().Name); .OrderBy(d => d.GetType().Name);
Items.AddRange(features.Select(d => _settingsVmFactory.CreatePluginFeatureViewModel(d))); Items.AddRange(features.Select(d => _settingsVmFactory.CreatePluginFeatureViewModel(d, true)));
base.OnActivate(); base.OnActivate();
} }

View File

@ -7,6 +7,7 @@ using Artemis.UI.Screens.Settings.Tabs.General;
using Artemis.UI.Screens.Settings.Tabs.Plugins; using Artemis.UI.Screens.Settings.Tabs.Plugins;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
using Artemis.UI.Utilities;
using Stylet; using Stylet;
namespace Artemis.UI.Screens.StartupWizard.Steps namespace Artemis.UI.Screens.StartupWizard.Steps
@ -34,7 +35,7 @@ namespace Artemis.UI.Screens.StartupWizard.Steps
_settingsService.GetSetting("UI.AutoRun", false).Value = value; _settingsService.GetSetting("UI.AutoRun", false).Value = value;
_settingsService.GetSetting("UI.AutoRun", false).Save(); _settingsService.GetSetting("UI.AutoRun", false).Save();
NotifyOfPropertyChange(nameof(StartWithWindows)); NotifyOfPropertyChange(nameof(StartWithWindows));
Task.Run(ApplyAutorun); Task.Run(() => ApplyAutorun(false));
} }
} }
@ -59,23 +60,35 @@ namespace Artemis.UI.Screens.StartupWizard.Steps
} }
} }
private void ApplyAutorun() private void ApplyAutorun(bool recreate)
{ {
if (!StartWithWindows)
StartMinimized = false;
// Remove the old auto-run method of placing a shortcut in shell:startup
string autoRunFile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Startup), "Artemis.lnk");
if (File.Exists(autoRunFile))
File.Delete(autoRunFile);
// Local builds shouldn't auto-run, this is just annoying
// if (Constants.BuildInfo.IsLocalBuild)
// return;
// Create or remove the task if necessary
try try
{ {
string autoRunFile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Startup), "Artemis.lnk"); bool taskCreated = false;
string executableFile = Constants.ExecutablePath; if (!recreate)
taskCreated = SettingsUtilities.IsAutoRunTaskCreated();
if (File.Exists(autoRunFile)) if (StartWithWindows && !taskCreated)
File.Delete(autoRunFile); SettingsUtilities.CreateAutoRunTask(TimeSpan.FromSeconds(15));
if (StartWithWindows) else if (!StartWithWindows && taskCreated)
ShortcutUtilities.Create(autoRunFile, executableFile, "--autorun", new FileInfo(executableFile).DirectoryName, "Artemis", "", ""); SettingsUtilities.RemoveAutoRunTask();
else
StartMinimized = false;
} }
catch (Exception e) catch (Exception e)
{ {
_dialogService.ShowExceptionDialog("An exception occured while trying to apply the auto run setting", e); Execute.PostToUIThread(() => _dialogService.ShowExceptionDialog("An exception occured while trying to apply the auto run setting", e));
throw; throw;
} }
} }

View File

@ -59,7 +59,7 @@ namespace Artemis.UI.Services
#region Event handlers #region Event handlers
private async void WindowServiceOnMainWindowOpened(object? sender, EventArgs e) private async void WindowServiceOnMainWindowOpened(object sender, EventArgs e)
{ {
List<ArtemisDevice> devices = _rgbService.Devices.Where(device => DeviceNeedsLayout(device) && !_ignoredDevices.Contains(device)).ToList(); List<ArtemisDevice> devices = _rgbService.Devices.Where(device => DeviceNeedsLayout(device) && !_ignoredDevices.Contains(device)).ToList();
foreach (ArtemisDevice artemisDevice in devices) foreach (ArtemisDevice artemisDevice in devices)

View File

@ -7,6 +7,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Timers;
using Artemis.Core; using Artemis.Core;
using Artemis.Core.Services; using Artemis.Core.Services;
using Artemis.UI.Exceptions; using Artemis.UI.Exceptions;
@ -23,7 +24,9 @@ namespace Artemis.UI.Services
{ {
public class UpdateService : IUpdateService public class UpdateService : IUpdateService
{ {
private const double UpdateCheckInterval = 3600000; // once per hour
private const string ApiUrl = "https://dev.azure.com/artemis-rgb/Artemis/_apis/"; private const string ApiUrl = "https://dev.azure.com/artemis-rgb/Artemis/_apis/";
private readonly PluginSetting<bool> _autoInstallUpdates; private readonly PluginSetting<bool> _autoInstallUpdates;
private readonly PluginSetting<bool> _checkForUpdates; private readonly PluginSetting<bool> _checkForUpdates;
private readonly IDialogService _dialogService; private readonly IDialogService _dialogService;
@ -41,13 +44,20 @@ namespace Artemis.UI.Services
_checkForUpdates = settingsService.GetSetting("UI.CheckForUpdates", true); _checkForUpdates = settingsService.GetSetting("UI.CheckForUpdates", true);
_autoInstallUpdates = settingsService.GetSetting("UI.AutoInstallUpdates", false); _autoInstallUpdates = settingsService.GetSetting("UI.AutoInstallUpdates", false);
_checkForUpdates.SettingChanged += CheckForUpdatesOnSettingChanged; _checkForUpdates.SettingChanged += CheckForUpdatesOnSettingChanged;
Timer timer = new(UpdateCheckInterval);
timer.Elapsed += async (_, _) => await AutoUpdate();
timer.Start();
} }
public bool SuspendAutoUpdate { get; set; }
private async Task OfferUpdate(DevOpsBuild buildInfo) private async Task OfferUpdate(DevOpsBuild buildInfo)
{ {
await _dialogService.ShowDialog<UpdateDialogViewModel>(new Dictionary<string, object> {{"buildInfo", buildInfo}}); object result = await _dialogService.ShowDialog<UpdateDialogViewModel>(new Dictionary<string, object> {{"buildInfo", buildInfo}});
if (result is bool resultBool && resultBool == false)
SuspendAutoUpdate = true;
} }
private async Task UpdateInstaller() private async Task UpdateInstaller()
@ -73,10 +83,20 @@ namespace Artemis.UI.Services
public async Task<bool> AutoUpdate() public async Task<bool> AutoUpdate()
{ {
if (!_checkForUpdates.Value) if (Constants.BuildInfo.IsLocalBuild)
return false;
if (!_checkForUpdates.Value || SuspendAutoUpdate)
return false; return false;
return await OfferUpdateIfFound(); try
{
return await OfferUpdateIfFound();
}
catch (Exception e)
{
_logger.Warning(e, "Auto update failed");
return false;
}
} }
public async Task<bool> OfferUpdateIfFound() public async Task<bool> OfferUpdateIfFound()
@ -186,13 +206,13 @@ namespace Artemis.UI.Services
catch (Exception e) catch (Exception e)
{ {
_logger.Warning(e, "GetBuildInfo: Failed to retrieve build info JSON"); _logger.Warning(e, "GetBuildInfo: Failed to retrieve build info JSON");
return null; throw;
} }
} }
catch (FlurlHttpException e) catch (FlurlHttpException e)
{ {
_logger.Warning("GetBuildInfo: Getting build info, request returned {statusCode}", e.StatusCode); _logger.Warning("GetBuildInfo: Getting build info, request returned {statusCode}", e.StatusCode);
return null; throw;
} }
} }
@ -208,16 +228,16 @@ namespace Artemis.UI.Services
#region Event handlers #region Event handlers
private void CheckForUpdatesOnSettingChanged(object sender, EventArgs e) private async void CheckForUpdatesOnSettingChanged(object sender, EventArgs e)
{ {
// Run an auto-update as soon as the setting gets changed to enabled // Run an auto-update as soon as the setting gets changed to enabled
if (_checkForUpdates.Value) if (_checkForUpdates.Value)
AutoUpdate(); await AutoUpdate();
} }
private void WindowServiceOnMainWindowOpened(object sender, EventArgs e) private async void WindowServiceOnMainWindowOpened(object sender, EventArgs e)
{ {
_logger.Information("Main window opened!"); await AutoUpdate();
} }
#endregion #endregion
@ -225,6 +245,8 @@ namespace Artemis.UI.Services
public interface IUpdateService : IArtemisUIService public interface IUpdateService : IArtemisUIService
{ {
bool SuspendAutoUpdate { get; set; }
/// <summary> /// <summary>
/// If auto-update is enabled this will offer updates if found /// If auto-update is enabled this will offer updates if found
/// </summary> /// </summary>

View File

@ -0,0 +1,95 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Security.Principal;
using System.Xml.Linq;
using Artemis.Core;
using Artemis.UI.Properties;
namespace Artemis.UI.Utilities
{
public static class SettingsUtilities
{
public static bool IsAutoRunTaskCreated()
{
Process schtasks = new()
{
StartInfo =
{
WindowStyle = ProcessWindowStyle.Hidden,
UseShellExecute = true,
FileName = Path.Combine(Environment.SystemDirectory, "schtasks.exe"),
Arguments = "/TN \"Artemis 2 autorun\""
}
};
schtasks.Start();
schtasks.WaitForExit();
return schtasks.ExitCode == 0;
}
public static void CreateAutoRunTask(TimeSpan autoRunDelay)
{
XDocument document = XDocument.Parse(Resources.artemis_autorun);
XElement task = document.Descendants().First();
task.Descendants().First(d => d.Name.LocalName == "RegistrationInfo").Descendants().First(d => d.Name.LocalName == "Date")
.SetValue(DateTime.Now);
task.Descendants().First(d => d.Name.LocalName == "RegistrationInfo").Descendants().First(d => d.Name.LocalName == "Author")
.SetValue(WindowsIdentity.GetCurrent().Name);
task.Descendants().First(d => d.Name.LocalName == "Triggers").Descendants().First(d => d.Name.LocalName == "LogonTrigger").Descendants().First(d => d.Name.LocalName == "Delay")
.SetValue(autoRunDelay);
task.Descendants().First(d => d.Name.LocalName == "Principals").Descendants().First(d => d.Name.LocalName == "Principal").Descendants().First(d => d.Name.LocalName == "UserId")
.SetValue(WindowsIdentity.GetCurrent().User.Value);
task.Descendants().First(d => d.Name.LocalName == "Actions").Descendants().First(d => d.Name.LocalName == "Exec").Descendants().First(d => d.Name.LocalName == "WorkingDirectory")
.SetValue(Constants.ApplicationFolder);
task.Descendants().First(d => d.Name.LocalName == "Actions").Descendants().First(d => d.Name.LocalName == "Exec").Descendants().First(d => d.Name.LocalName == "Command")
.SetValue("\"" + Constants.ExecutablePath + "\"");
string xmlPath = Path.GetTempFileName();
using (Stream fileStream = new FileStream(xmlPath, FileMode.Create))
{
document.Save(fileStream);
}
Process schtasks = new()
{
StartInfo =
{
WindowStyle = ProcessWindowStyle.Hidden,
UseShellExecute = true,
Verb = "runas",
FileName = Path.Combine(Environment.SystemDirectory, "schtasks.exe"),
Arguments = $"/Create /XML \"{xmlPath}\" /tn \"Artemis 2 autorun\" /F"
}
};
schtasks.Start();
schtasks.WaitForExit();
File.Delete(xmlPath);
}
public static void RemoveAutoRunTask()
{
Process schtasks = new()
{
StartInfo =
{
WindowStyle = ProcessWindowStyle.Hidden,
UseShellExecute = true,
Verb = "runas",
FileName = Path.Combine(Environment.SystemDirectory, "schtasks.exe"),
Arguments = "/Delete /TN \"Artemis 2 autorun\" /f"
}
};
schtasks.Start();
schtasks.WaitForExit();
}
}
}

View File

@ -1,6 +0,0 @@
{
"BuildId": 442,
"BuildNumber": 20210130.3,
"SourceBranch": "refs/heads/master",
"SourceVersion": "b29ab064ae46d93141f31afaa8ee3ea9063c6263"
}