diff --git a/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionOperator.cs index dba5e36f8..186644a2a 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionOperator.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionOperator.cs @@ -34,6 +34,36 @@ namespace Artemis.Core.Models.Profile.Conditions /// public abstract string Icon { get; } + /// + /// Gets or sets whether this condition operator supports a right side, defaults to true + /// + public bool SupportsRightSide { get; protected set; } = true; + + public bool SupportsType(Type type) + { + if (type == null) + return true; + return CompatibleTypes.Any(t => t.IsCastableFrom(type)); + } + + /// + /// Creates a binary expression comparing two types + /// + /// The parameter on the left side of the expression + /// The parameter on the right side of the expression + /// + public abstract BinaryExpression CreateExpression(Expression leftSide, Expression rightSide); + + /// + /// Returns an expression that checks the given expression for null + /// + /// + /// + protected Expression CreateNullCheckExpression(Expression expression) + { + return Expression.NotEqual(expression, Expression.Constant(null)); + } + internal void Register(PluginInfo pluginInfo, IDataModelService dataModelService) { if (_registered) @@ -63,20 +93,5 @@ namespace Artemis.Core.Models.Profile.Conditions // Profile editor service will call Unsubscribe _dataModelService.RemoveConditionOperator(this); } - - public bool SupportsType(Type type) - { - if (type == null) - return true; - return CompatibleTypes.Any(t => t.IsCastableFrom(type)); - } - - /// - /// Creates a binary expression comparing two types - /// - /// The parameter on the left side of the expression - /// The parameter on the right side of the expression - /// - public abstract BinaryExpression CreateExpression(Expression leftSide, Expression rightSide); } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionPredicate.cs b/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionPredicate.cs index 7da7e87ca..eeb31bf6c 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionPredicate.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionPredicate.cs @@ -304,7 +304,11 @@ namespace Artemis.Core.Models.Profile.Conditions if (leftSideAccessor.Type.IsValueType && RightStaticValue == null) return; - var rightSideConstant = Expression.Constant(RightStaticValue); + // If the right side value is null, the constant type cannot be inferred and must be provided manually + var rightSideConstant = RightStaticValue != null + ? Expression.Constant(RightStaticValue) + : Expression.Constant(null, leftSideAccessor.Type); + var conditionExpression = Operator.CreateExpression(leftSideAccessor, rightSideConstant); StaticConditionLambda = Expression.Lambda>(conditionExpression, leftSideParameter); diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringContainsConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringContainsConditionOperator.cs new file mode 100644 index 000000000..f82d7e367 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringContainsConditionOperator.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; + +namespace Artemis.Core.Models.Profile.Conditions.Operators +{ + public class StringContainsConditionOperator : DisplayConditionOperator + { + private readonly MethodInfo _toLower; + private readonly MethodInfo _contains; + + public StringContainsConditionOperator() + { + _toLower = typeof(string).GetMethod("ToLower", new Type[] { }); + _contains = typeof(string).GetMethod("Contains", new[] {typeof(string) }); + } + + public override IReadOnlyCollection CompatibleTypes => new List {typeof(string)}; + + public override string Description => "Contains"; + public override string Icon => "Contain"; + + public override BinaryExpression CreateExpression(Expression leftSide, Expression rightSide) + { + return Expression.Equal(Expression.Call(Expression.Call(leftSide, _toLower), _contains, Expression.Call(rightSide, _toLower)), Expression.Constant(true)); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringEndsWithConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringEndsWithConditionOperator.cs new file mode 100644 index 000000000..69bcdc5b3 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringEndsWithConditionOperator.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; + +namespace Artemis.Core.Models.Profile.Conditions.Operators +{ + public class StringEndsWithConditionOperator : DisplayConditionOperator + { + private readonly MethodInfo _toLower; + private readonly MethodInfo _endsWith; + + public StringEndsWithConditionOperator() + { + _toLower = typeof(string).GetMethod("ToLower", new Type[] { }); + _endsWith = typeof(string).GetMethod("EndsWith", new[] { typeof(string) }); + } + + public override IReadOnlyCollection CompatibleTypes => new List { typeof(string) }; + + public override string Description => "Ends with"; + public override string Icon => "ContainEnd"; + + public override BinaryExpression CreateExpression(Expression leftSide, Expression rightSide) + { + return Expression.Equal(Expression.Call(Expression.Call(leftSide, _toLower), _endsWith, Expression.Call(rightSide, _toLower)), Expression.Constant(true)); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringEqualsConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringEqualsConditionOperator.cs new file mode 100644 index 000000000..56fce09f7 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringEqualsConditionOperator.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; + +namespace Artemis.Core.Models.Profile.Conditions.Operators +{ + public class StringEqualsConditionOperator : DisplayConditionOperator + { + private readonly MethodInfo _toLower; + + public StringEqualsConditionOperator() + { + _toLower = typeof(string).GetMethod("ToLower", new Type[] { }); + } + + public override IReadOnlyCollection CompatibleTypes => new List {typeof(string)}; + + public override string Description => "Equals"; + public override string Icon => "Equal"; + + public override BinaryExpression CreateExpression(Expression leftSide, Expression rightSide) + { + return Expression.Equal(Expression.Call(leftSide, _toLower), Expression.Call(rightSide, _toLower)); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNotContainsConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNotContainsConditionOperator.cs new file mode 100644 index 000000000..11ee0144b --- /dev/null +++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNotContainsConditionOperator.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; + +namespace Artemis.Core.Models.Profile.Conditions.Operators +{ + public class StringNotContainsConditionOperator : DisplayConditionOperator + { + private readonly MethodInfo _toLower; + private readonly MethodInfo _contains; + + public StringNotContainsConditionOperator() + { + _toLower = typeof(string).GetMethod("ToLower", new Type[] { }); + _contains = typeof(string).GetMethod("Contains", new[] { typeof(string) }); + } + + public override IReadOnlyCollection CompatibleTypes => new List { typeof(string) }; + + public override string Description => "Does not contain"; + public override string Icon => "FormatStrikethrough"; + + public override BinaryExpression CreateExpression(Expression leftSide, Expression rightSide) + { + return Expression.Equal(Expression.Call(Expression.Call(leftSide, _toLower), _contains, Expression.Call(rightSide, _toLower)), Expression.Constant(false)); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNotEqualConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNotEqualConditionOperator.cs new file mode 100644 index 000000000..7235dd5bc --- /dev/null +++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNotEqualConditionOperator.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; + +namespace Artemis.Core.Models.Profile.Conditions.Operators +{ + public class StringNotEqualConditionOperator : DisplayConditionOperator + { + private readonly MethodInfo _toLower; + + public StringNotEqualConditionOperator() + { + _toLower = typeof(string).GetMethod("ToLower", new Type[] { }); + } + + public override IReadOnlyCollection CompatibleTypes => new List {typeof(string)}; + + public override string Description => "Does not equal"; + public override string Icon => "NotEqualVariant"; + + public override BinaryExpression CreateExpression(Expression leftSide, Expression rightSide) + { + return Expression.NotEqual(Expression.Call(leftSide, _toLower), Expression.Call(rightSide, _toLower)); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNullConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNullConditionOperator.cs new file mode 100644 index 000000000..589b31223 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNullConditionOperator.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; + +namespace Artemis.Core.Models.Profile.Conditions.Operators +{ + public class StringNullConditionOperator : DisplayConditionOperator + { + public StringNullConditionOperator() + { + SupportsRightSide = false; + } + + public override IReadOnlyCollection CompatibleTypes => new List {typeof(string)}; + + public override string Description => "Is null"; + public override string Icon => "Null"; + + public override BinaryExpression CreateExpression(Expression leftSide, Expression rightSide) + { + return Expression.Equal(leftSide, Expression.Constant(null, leftSide.Type)); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringStartsWithConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringStartsWithConditionOperator.cs new file mode 100644 index 000000000..06259737c --- /dev/null +++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringStartsWithConditionOperator.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; + +namespace Artemis.Core.Models.Profile.Conditions.Operators +{ + public class StringStartsWithConditionOperator : DisplayConditionOperator + { + private readonly MethodInfo _toLower; + private readonly MethodInfo _startsWith; + + public StringStartsWithConditionOperator() + { + _toLower = typeof(string).GetMethod("ToLower", new Type[] { }); + _startsWith = typeof(string).GetMethod("StartsWith", new[] { typeof(string) }); + } + + public override IReadOnlyCollection CompatibleTypes => new List { typeof(string) }; + + public override string Description => "Starts with"; + public override string Icon => "ContainStart"; + + public override BinaryExpression CreateExpression(Expression leftSide, Expression rightSide) + { + return Expression.Equal(Expression.Call(Expression.Call(leftSide, _toLower), _startsWith, Expression.Call(rightSide, _toLower)), Expression.Constant(true)); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Surface/ArtemisDevice.cs b/src/Artemis.Core/Models/Surface/ArtemisDevice.cs index 3cb5f73f0..a8b2444c1 100644 --- a/src/Artemis.Core/Models/Surface/ArtemisDevice.cs +++ b/src/Artemis.Core/Models/Surface/ArtemisDevice.cs @@ -134,10 +134,14 @@ namespace Artemis.Core.Models.Surface internal void ApplyToRgbDevice() { - RgbDevice.Location = new Point(DeviceEntity.X, DeviceEntity.Y); RgbDevice.Rotation = DeviceEntity.Rotation; RgbDevice.Scale = DeviceEntity.Scale; + // Workaround for device rotation not applying + if (DeviceEntity.X == 0 && DeviceEntity.Y == 0) + RgbDevice.Location = new Point(1, 1); + RgbDevice.Location = new Point(DeviceEntity.X, DeviceEntity.Y); + CalculateRenderProperties(); OnDeviceUpdated(); } diff --git a/src/Artemis.Core/Services/DataModelService.cs b/src/Artemis.Core/Services/DataModelService.cs index ba67bf455..4c29ecadc 100644 --- a/src/Artemis.Core/Services/DataModelService.cs +++ b/src/Artemis.Core/Services/DataModelService.cs @@ -151,7 +151,21 @@ namespace Artemis.Core.Services { if (type == null) return new List(_registeredConditionOperators); - return _registeredConditionOperators.Where(c => c.CompatibleTypes.Any(t => t.IsCastableFrom(type))).ToList(); + + var candidates = _registeredConditionOperators.Where(c => c.CompatibleTypes.Any(t => t.IsCastableFrom(type))).ToList(); + + // If there are multiple operators with the same description, use the closest match + foreach (var displayConditionOperators in candidates.GroupBy(c => c.Description).Where(g => g.Count() > 1).ToList()) + { + var bestCandidate = displayConditionOperators.OrderByDescending(c => c.CompatibleTypes.Contains(type)).FirstOrDefault(); + foreach (var displayConditionOperator in displayConditionOperators) + { + if (displayConditionOperator != bestCandidate) + candidates.Remove(displayConditionOperator); + } + } + + return candidates; } } @@ -162,12 +176,24 @@ namespace Artemis.Core.Services private void RegisterBuiltInConditionOperators() { + // General usage for any type RegisterConditionOperator(Constants.CorePluginInfo, new EqualsConditionOperator()); RegisterConditionOperator(Constants.CorePluginInfo, new NotEqualConditionOperator()); + + // Numeric operators RegisterConditionOperator(Constants.CorePluginInfo, new LessThanConditionOperator()); RegisterConditionOperator(Constants.CorePluginInfo, new GreaterThanConditionOperator()); RegisterConditionOperator(Constants.CorePluginInfo, new LessThanOrEqualConditionOperator()); RegisterConditionOperator(Constants.CorePluginInfo, new GreaterThanOrEqualConditionOperator()); + + // String operators + RegisterConditionOperator(Constants.CorePluginInfo, new StringEqualsConditionOperator()); + RegisterConditionOperator(Constants.CorePluginInfo, new StringNotEqualConditionOperator()); + RegisterConditionOperator(Constants.CorePluginInfo, new StringContainsConditionOperator()); + RegisterConditionOperator(Constants.CorePluginInfo, new StringNotContainsConditionOperator()); + RegisterConditionOperator(Constants.CorePluginInfo, new StringStartsWithConditionOperator()); + RegisterConditionOperator(Constants.CorePluginInfo, new StringEndsWithConditionOperator()); + RegisterConditionOperator(Constants.CorePluginInfo, new StringNullConditionOperator()); } private void PluginServiceOnPluginEnabled(object sender, PluginEventArgs e) diff --git a/src/Artemis.Storage/Migrations/M4ProfileSegments.cs b/src/Artemis.Storage/Migrations/M4ProfileSegments.cs new file mode 100644 index 000000000..e06761e8d --- /dev/null +++ b/src/Artemis.Storage/Migrations/M4ProfileSegments.cs @@ -0,0 +1,39 @@ +using System; +using System.Linq; +using System.Runtime.InteropServices; +using Artemis.Storage.Entities.Profile; +using Artemis.Storage.Migrations.Interfaces; +using LiteDB; + +namespace Artemis.Storage.Migrations +{ + public class M4ProfileSegmentsMigration : IStorageMigration + { + public int UserVersion => 4; + + public void Apply(LiteRepository repository) + { + var profiles = repository.Query().ToList(); + foreach (var profileEntity in profiles) + { + foreach (var folder in profileEntity.Folders.Where(f => f.MainSegmentLength == TimeSpan.Zero)) + { + if (folder.PropertyEntities.Any(p => p.KeyframeEntities.Any())) + folder.MainSegmentLength = folder.PropertyEntities.Where(p => p.KeyframeEntities.Any()).Max(p => p.KeyframeEntities.Max(k => k.Position)); + if (folder.MainSegmentLength == TimeSpan.Zero) + folder.MainSegmentLength = TimeSpan.FromSeconds(5); + } + + foreach (var layer in profileEntity.Layers.Where(l => l.MainSegmentLength == TimeSpan.Zero)) + { + if (layer.PropertyEntities.Any(p => p.KeyframeEntities.Any())) + layer.MainSegmentLength = layer.PropertyEntities.Where(p => p.KeyframeEntities.Any()).Max(p => p.KeyframeEntities.Max(k => k.Position)); + if (layer.MainSegmentLength == TimeSpan.Zero) + layer.MainSegmentLength = TimeSpan.FromSeconds(5); + } + + repository.Update(profileEntity); + } + } + } +} \ No newline at end of file diff --git a/src/Artemis.Storage/Repositories/PluginRepository.cs b/src/Artemis.Storage/Repositories/PluginRepository.cs index 7d0f79652..d31f33a31 100644 --- a/src/Artemis.Storage/Repositories/PluginRepository.cs +++ b/src/Artemis.Storage/Repositories/PluginRepository.cs @@ -12,7 +12,7 @@ namespace Artemis.Storage.Repositories internal PluginRepository(LiteRepository repository) { _repository = repository; - + _repository.Database.GetCollection().EnsureIndex(s => new {s.Name, s.PluginGuid}, true); } diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/DisplayConditions/DisplayConditionGroupViewModel.cs b/src/Artemis.UI/Screens/Module/ProfileEditor/DisplayConditions/DisplayConditionGroupViewModel.cs index eb06ec9b2..051a601d9 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/DisplayConditions/DisplayConditionGroupViewModel.cs +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/DisplayConditions/DisplayConditionGroupViewModel.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Artemis.Core.Models.Profile.Conditions; using Artemis.UI.Ninject.Factories; using Artemis.UI.Screens.Module.ProfileEditor.DisplayConditions.Abstract; +using Artemis.UI.Shared.Services.Interfaces; using Humanizer; using Stylet; @@ -11,13 +12,15 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.DisplayConditions { public class DisplayConditionGroupViewModel : DisplayConditionViewModel { + private readonly IProfileEditorService _profileEditorService; private readonly IDisplayConditionsVmFactory _displayConditionsVmFactory; private bool _isRootGroup; private bool _isInitialized; - public DisplayConditionGroupViewModel(DisplayConditionGroup displayConditionGroup, DisplayConditionViewModel parent, IDisplayConditionsVmFactory displayConditionsVmFactory) : base( - displayConditionGroup, parent) + public DisplayConditionGroupViewModel(DisplayConditionGroup displayConditionGroup, DisplayConditionViewModel parent, + IProfileEditorService profileEditorService, IDisplayConditionsVmFactory displayConditionsVmFactory) : base(displayConditionGroup, parent) { + _profileEditorService = profileEditorService; _displayConditionsVmFactory = displayConditionsVmFactory; Execute.PostToUIThread(async () => { @@ -55,13 +58,17 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.DisplayConditions DisplayConditionGroup.AddChild(new DisplayConditionPredicate(DisplayConditionGroup, PredicateType.Static)); else if (type == "Dynamic") DisplayConditionGroup.AddChild(new DisplayConditionPredicate(DisplayConditionGroup, PredicateType.Dynamic)); + Update(); + _profileEditorService.UpdateSelectedProfileElement(); } public void AddGroup() { DisplayConditionGroup.AddChild(new DisplayConditionGroup(DisplayConditionGroup)); + Update(); + _profileEditorService.UpdateSelectedProfileElement(); } public override void Update() diff --git a/src/Artemis.UI/Screens/RootViewModel.cs b/src/Artemis.UI/Screens/RootViewModel.cs index 88427dc2e..cdc5c2fe0 100644 --- a/src/Artemis.UI/Screens/RootViewModel.cs +++ b/src/Artemis.UI/Screens/RootViewModel.cs @@ -254,6 +254,7 @@ namespace Artemis.UI.Screens protected override void OnClose() { SidebarViewModel.Dispose(); + // Lets force the GC to run after closing the window so it is obvious to users watching task manager // that closing the UI will decrease the memory footprint of the application. diff --git a/src/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs b/src/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs index 4eb80341c..c45634af6 100644 --- a/src/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs +++ b/src/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs @@ -197,6 +197,9 @@ namespace Artemis.UI.Screens.Sidebar public void Dispose() { + var closeTask = CloseCurrentItem(); + closeTask.Wait(); + _pluginService.PluginEnabled -= PluginServiceOnPluginEnabled; _pluginService.PluginDisabled -= PluginServiceOnPluginDisabled; } diff --git a/src/Plugins/Artemis.Plugins.Modules.General/GeneralModule.cs b/src/Plugins/Artemis.Plugins.Modules.General/GeneralModule.cs index a9e01ffed..cbe76d302 100644 --- a/src/Plugins/Artemis.Plugins.Modules.General/GeneralModule.cs +++ b/src/Plugins/Artemis.Plugins.Modules.General/GeneralModule.cs @@ -38,8 +38,10 @@ namespace Artemis.Plugins.Modules.General public void UpdateCurrentWindow() { var processId = WindowUtilities.GetActiveProcessId(); - if (DataModel.ActiveWindow == null || DataModel.ActiveWindow.Process.Id != processId) + if (DataModel.ActiveWindow == null || DataModel.ActiveWindow.Process.Id != processId) DataModel.ActiveWindow = new WindowDataModel(Process.GetProcessById(processId)); + if (DataModel.ActiveWindow != null && string.IsNullOrWhiteSpace(DataModel.ActiveWindow.WindowTitle)) + DataModel.ActiveWindow.WindowTitle = Process.GetProcessById(WindowUtilities.GetActiveProcessId()).MainWindowTitle; } #endregion