From ceeaa4bf6d18f0e9d2ecc78d112a0921b1f738e3 Mon Sep 17 00:00:00 2001 From: Robert Beekman Date: Thu, 3 Jun 2021 22:34:43 +0200 Subject: [PATCH] Profiles - Reworked profile system Sidebar - Redesigned sidebar with customizable categories Profiles - Added the ability to configure custom profile icons Profiles - Added the ability to activate multiple profiles for modules at once Profiles - Added the ability to create profiles for no modules Profiles - Added the ability to suspend a profile or an entire category Profiles - Added profile activation conditions Profiles - Added file-based importing/exporting Profile editor - Condensed UI, removed tabs Profile editor - Disable condition operators until a left-side is picked --- .../Artemis.Core.csproj.DotSettings | 1 + src/Artemis.Core/Events/ModuleEventArgs.cs | 21 + .../Profiles/ProfileConfigurationEventArgs.cs | 20 + .../JsonConverters/StreamConverter.cs | 45 + .../Abstract/DataModelConditionPredicate.cs | 69 +- .../EventPredicateWrapperDataModel.cs | 2 +- .../Wrappers/ListPredicateWrapperDataModel.cs | 2 +- .../Models/Profile/DataModel/DataModelPath.cs | 6 +- src/Artemis.Core/Models/Profile/Folder.cs | 8 +- src/Artemis.Core/Models/Profile/Layer.cs | 2 + src/Artemis.Core/Models/Profile/Profile.cs | 89 +- .../Models/Profile/ProfileCategory.cs | 244 ++++++ .../Models/Profile/ProfileDescriptor.cs | 41 - .../Models/Profile/ProfileElement.cs | 10 +- .../ProfileConfiguration.cs | 210 +++++ .../ProfileConfigurationExportModel.cs | 30 + .../ProfileConfigurationIcon.cs | 89 ++ .../DataModelExpansions/DataModelExpansion.cs | 53 -- .../Internal/BaseDataModelExpansion.cs | 47 - .../Plugins/DataModelPluginFeature.cs | 13 - .../LayerBrushes/Internal/BaseLayerBrush.cs | 2 +- .../Internal/PropertiesLayerBrush.cs | 4 +- .../Plugins/LayerBrushes/RgbNetLayerBrush.cs | 104 --- .../Attributes/DataModelIgnoreAttribute.cs | 0 .../Attributes/DataModelProperty.cs | 0 .../DataModel.cs | 17 +- .../DynamicChild.cs | 0 src/Artemis.Core/Plugins/Modules/Module.cs | 203 +++-- src/Artemis.Core/Plugins/Modules/ModuleTab.cs | 44 - .../Plugins/Modules/ProfileModule.cs | 329 ------- src/Artemis.Core/Plugins/PluginFeatureInfo.cs | 12 +- src/Artemis.Core/Services/CoreService.cs | 100 +-- .../Services/Interfaces/ICoreService.cs | 9 +- .../Services/Interfaces/IModuleService.cs | 32 +- src/Artemis.Core/Services/ModuleService.cs | 300 +++---- .../Services/Registration/DataModelService.cs | 17 +- .../Storage/Interfaces/IProfileService.cs | 200 +++-- .../Services/Storage/ProfileService.cs | 536 ++++++++---- .../EndPoints/DataModelJsonPluginEndPoint.cs | 27 +- .../WebServer/Interfaces/IWebServerService.cs | 20 - .../Services/WebServer/WebServerService.cs | 18 - src/Artemis.Core/Stores/DataModelStore.cs | 2 +- .../Utilities/CorePluginFeature.cs | 7 +- src/Artemis.Core/Utilities/IntroAnimation.cs | 99 --- .../Entities/Module/ModuleSettingsEntity.cs | 17 - .../Entities/Profile/ProfileCategoryEntity.cs | 17 + .../Profile/ProfileConfigurationEntity.cs | 22 + .../Entities/Profile/ProfileEntity.cs | 4 +- .../Migrations/M0008PluginFeatures.cs | 13 - .../Migrations/M0012ProfileCategories.cs | 48 + .../Interfaces/IModuleRepository.cs | 14 - .../Interfaces/IProfileCategoryRepository.cs | 19 + .../Interfaces/IProfileRepository.cs | 1 - .../Repositories/ModuleRepository.cs | 43 - .../Repositories/ProfileCategoryRepository.cs | 75 ++ .../Repositories/ProfileRepository.cs | 9 - .../Controls/ProfileConfigurationIcon.xaml | 63 ++ .../Controls/ProfileConfigurationIcon.xaml.cs | 30 + .../StreamToBitmapImageConverter.cs | 42 + .../Input/DataModelDynamicView.xaml | 5 + .../Input/DataModelDynamicViewModel.cs | 13 +- .../Events/ProfileConfigurationEventArgs.cs | 32 + .../Events/ProfileEventArgs.cs | 32 - .../Ninject/Factories/ISharedVMFactory.cs | 5 +- .../PropertyInput/PropertyInputViewModel.cs | 4 +- .../Services/DataModelUIService.cs | 52 +- .../Interfaces/IDataModelUIService.cs | 20 +- .../Services/Interfaces/IDialogService.cs | 4 +- .../Interfaces/IProfileEditorService.cs | 44 +- .../Services/ProfileEditorService.cs | 199 ++--- .../Utilities/BindingProxy.cs | 2 +- src/Artemis.UI/Artemis.UI.csproj | 10 +- .../Converters/NullToVisibilityConverter.cs | 41 - .../SolidColorBrushToColorConverter.cs | 29 + .../Events/RequestSelectSidebarItemEvent.cs | 6 +- .../Ninject/Factories/IVMFactory.cs | 40 +- .../Screens/Modules/ModuleRootView.xaml | 26 - .../Screens/Modules/ModuleRootViewModel.cs | 57 -- .../Tabs/ActivationRequirementView.xaml | 44 - .../Tabs/ActivationRequirementsView.xaml | 54 -- .../Tabs/ActivationRequirementsViewModel.cs | 36 - src/Artemis.UI/Screens/News/NewsView.xaml | 20 - src/Artemis.UI/Screens/News/NewsViewModel.cs | 12 - .../DataModelConditionPredicateViewModel.cs | 45 +- .../Abstract/DataModelConditionViewModel.cs | 2 + .../DataModelConditionEventViewModel.cs | 17 +- .../DataModelConditionGroupViewModel.cs | 59 +- .../DataModelConditionListViewModel.cs | 31 +- .../DataModelConditionEventPredicateView.xaml | 1 + ...taModelConditionEventPredicateViewModel.cs | 4 +- ...ataModelConditionGeneralPredicateView.xaml | 1 + ...ModelConditionGeneralPredicateViewModel.cs | 4 +- .../DataModelConditionListPredicateView.xaml | 1 + ...ataModelConditionListPredicateViewModel.cs | 21 +- .../Dialogs/ProfileCreateView.xaml | 30 - .../Dialogs/ProfileCreateViewModel.cs | 40 - .../Dialogs/ProfileEditViewModel.cs | 42 - .../Dialogs/ProfileExportView.xaml | 44 - .../Dialogs/ProfileExportViewModel.cs | 49 -- .../Dialogs/ProfileImportView.xaml | 61 -- .../Dialogs/ProfileImportViewModel.cs | 37 - .../DisplayConditionsViewModel.cs | 33 +- .../ConditionalDataBindingModeViewModel.cs | 21 +- .../DataBindingConditionViewModel.cs | 14 +- .../DataBindings/DataBindingViewModel.cs | 10 +- .../DataBindingModifierView.xaml | 3 +- .../DataBindingModifierViewModel.cs | 10 +- .../DirectDataBindingModeViewModel.cs | 9 +- .../LayerEffects/EffectsViewModel.cs | 2 +- .../LayerProperties/LayerPropertiesView.xaml | 2 +- .../LayerPropertiesViewModel.cs | 8 +- .../Timeline/TimelineKeyframeViewModel.cs | 4 +- .../Timeline/TimelineSegmentViewModel.cs | 14 +- .../Timeline/TimelineViewModel.cs | 14 +- .../LayerProperties/Tree/TreeGroupView.xaml | 2 +- .../Tree/TreeGroupViewModel.cs | 10 +- .../Tree/TreePropertyViewModel.cs | 4 +- .../ProfileEditor/ProfileEditorView.xaml | 132 +-- .../ProfileEditor/ProfileEditorViewModel.cs | 202 +---- .../Dialogs/LayerHintsDialogViewModel.cs | 2 +- .../ProfileTree/ProfileTreeViewModel.cs | 20 +- .../ProfileTree/TreeItem/TreeItemViewModel.cs | 10 +- .../Visualization/ProfileLayerViewModel.cs | 12 +- .../Visualization/ProfileViewModel.cs | 18 +- .../Visualization/Tools/EditToolViewModel.cs | 18 +- .../Tools/SelectionRemoveToolViewModel.cs | 2 +- .../Tools/SelectionToolViewModel.cs | 4 +- src/Artemis.UI/Screens/RootView.xaml | 125 +-- src/Artemis.UI/Screens/RootViewModel.cs | 100 +-- .../Debug/Tabs/DataModelDebugViewModel.cs | 5 +- .../Tabs/DevicePropertiesTabViewModel.cs | 4 +- .../Screens/Settings/SettingsViewModel.cs | 3 - .../Settings/Tabs/About/AboutTabView.xaml | 820 +++++++++--------- .../Tabs/Modules/ModuleOrderModuleView.xaml | 20 - .../Modules/ModuleOrderModuleViewModel.cs | 43 - .../Tabs/Modules/ModuleOrderTabView.xaml | 164 ---- .../Tabs/Modules/ModuleOrderTabViewModel.cs | 97 --- .../Tabs/Plugins/PluginFeatureView.xaml | 2 +- .../Screens/Sidebar/ArtemisSidebar.xaml | 127 +++ .../ModuleActivationRequirementView.xaml | 48 + .../ModuleActivationRequirementViewModel.cs} | 8 +- .../ModuleActivationRequirementsView.xaml | 27 + .../ModuleActivationRequirementsViewModel.cs | 43 + .../Dialogs/ProfileEdit/ProfileEditView.xaml | 304 +++++++ .../ProfileEdit/ProfileEditViewModel.cs | 214 +++++ .../ProfileEdit/ProfileIconViewModel.cs | 16 + .../ProfileEdit/ProfileModuleViewModel.cs | 22 + .../Dialogs/SidebarCategoryCreateView.xaml} | 28 +- .../Dialogs/SidebarCategoryCreateViewModel.cs | 56 ++ .../Dialogs/SidebarCategoryUpdateView.xaml | 44 + .../Dialogs/SidebarCategoryUpdateViewModel.cs | 73 ++ .../Screens/Sidebar/SidebarCategoryView.xaml | 213 +++++ .../Sidebar/SidebarCategoryView.xaml.cs | 21 + .../Sidebar/SidebarCategoryViewModel.cs | 282 ++++++ .../SidebarProfileConfigurationView.xaml | 188 ++++ .../SidebarProfileConfigurationViewModel.cs | 165 ++++ .../Screens/Sidebar/SidebarScreenView.xaml | 15 + .../Screens/Sidebar/SidebarScreenViewModel.cs | 31 + .../Screens/Sidebar/SidebarView.xaml | 191 +++- .../Screens/Sidebar/SidebarView.xaml.cs | 15 - .../Screens/Sidebar/SidebarViewModel.cs | 347 +++----- .../Steps/FinishStepViewModel.cs | 14 - .../Visualization/SurfaceDeviceView.xaml | 2 +- src/Artemis.UI/Screens/TrayView.xaml | 5 - 164 files changed, 5019 insertions(+), 4220 deletions(-) create mode 100644 src/Artemis.Core/Events/ModuleEventArgs.cs create mode 100644 src/Artemis.Core/Events/Profiles/ProfileConfigurationEventArgs.cs create mode 100644 src/Artemis.Core/JsonConverters/StreamConverter.cs create mode 100644 src/Artemis.Core/Models/Profile/ProfileCategory.cs delete mode 100644 src/Artemis.Core/Models/Profile/ProfileDescriptor.cs create mode 100644 src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs create mode 100644 src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationExportModel.cs create mode 100644 src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationIcon.cs delete mode 100644 src/Artemis.Core/Plugins/DataModelExpansions/DataModelExpansion.cs delete mode 100644 src/Artemis.Core/Plugins/DataModelExpansions/Internal/BaseDataModelExpansion.cs delete mode 100644 src/Artemis.Core/Plugins/DataModelPluginFeature.cs delete mode 100644 src/Artemis.Core/Plugins/LayerBrushes/RgbNetLayerBrush.cs rename src/Artemis.Core/Plugins/{DataModelExpansions => Modules}/Attributes/DataModelIgnoreAttribute.cs (100%) rename src/Artemis.Core/Plugins/{DataModelExpansions => Modules}/Attributes/DataModelProperty.cs (100%) rename src/Artemis.Core/Plugins/{DataModelExpansions => Modules}/DataModel.cs (96%) rename src/Artemis.Core/Plugins/{DataModelExpansions => Modules}/DynamicChild.cs (100%) delete mode 100644 src/Artemis.Core/Plugins/Modules/ModuleTab.cs delete mode 100644 src/Artemis.Core/Plugins/Modules/ProfileModule.cs delete mode 100644 src/Artemis.Core/Utilities/IntroAnimation.cs delete mode 100644 src/Artemis.Storage/Entities/Module/ModuleSettingsEntity.cs create mode 100644 src/Artemis.Storage/Entities/Profile/ProfileCategoryEntity.cs create mode 100644 src/Artemis.Storage/Entities/Profile/ProfileConfigurationEntity.cs create mode 100644 src/Artemis.Storage/Migrations/M0012ProfileCategories.cs delete mode 100644 src/Artemis.Storage/Repositories/Interfaces/IModuleRepository.cs create mode 100644 src/Artemis.Storage/Repositories/Interfaces/IProfileCategoryRepository.cs delete mode 100644 src/Artemis.Storage/Repositories/ModuleRepository.cs create mode 100644 src/Artemis.Storage/Repositories/ProfileCategoryRepository.cs create mode 100644 src/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.xaml create mode 100644 src/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.xaml.cs create mode 100644 src/Artemis.UI.Shared/Converters/StreamToBitmapImageConverter.cs create mode 100644 src/Artemis.UI.Shared/Events/ProfileConfigurationEventArgs.cs delete mode 100644 src/Artemis.UI.Shared/Events/ProfileEventArgs.cs delete mode 100644 src/Artemis.UI/Converters/NullToVisibilityConverter.cs create mode 100644 src/Artemis.UI/Converters/SolidColorBrushToColorConverter.cs delete mode 100644 src/Artemis.UI/Screens/Modules/ModuleRootView.xaml delete mode 100644 src/Artemis.UI/Screens/Modules/ModuleRootViewModel.cs delete mode 100644 src/Artemis.UI/Screens/Modules/Tabs/ActivationRequirementView.xaml delete mode 100644 src/Artemis.UI/Screens/Modules/Tabs/ActivationRequirementsView.xaml delete mode 100644 src/Artemis.UI/Screens/Modules/Tabs/ActivationRequirementsViewModel.cs delete mode 100644 src/Artemis.UI/Screens/News/NewsView.xaml delete mode 100644 src/Artemis.UI/Screens/News/NewsViewModel.cs delete mode 100644 src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileCreateView.xaml delete mode 100644 src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileCreateViewModel.cs delete mode 100644 src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileEditViewModel.cs delete mode 100644 src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileExportView.xaml delete mode 100644 src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileExportViewModel.cs delete mode 100644 src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileImportView.xaml delete mode 100644 src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileImportViewModel.cs delete mode 100644 src/Artemis.UI/Screens/Settings/Tabs/Modules/ModuleOrderModuleView.xaml delete mode 100644 src/Artemis.UI/Screens/Settings/Tabs/Modules/ModuleOrderModuleViewModel.cs delete mode 100644 src/Artemis.UI/Screens/Settings/Tabs/Modules/ModuleOrderTabView.xaml delete mode 100644 src/Artemis.UI/Screens/Settings/Tabs/Modules/ModuleOrderTabViewModel.cs create mode 100644 src/Artemis.UI/Screens/Sidebar/ArtemisSidebar.xaml create mode 100644 src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ModuleActivationRequirementView.xaml rename src/Artemis.UI/Screens/{Modules/Tabs/ActivationRequirementViewModel.cs => Sidebar/Dialogs/ProfileEdit/ModuleActivationRequirementViewModel.cs} (88%) create mode 100644 src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ModuleActivationRequirementsView.xaml create mode 100644 src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ModuleActivationRequirementsViewModel.cs create mode 100644 src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ProfileEditView.xaml create mode 100644 src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ProfileEditViewModel.cs create mode 100644 src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ProfileIconViewModel.cs create mode 100644 src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ProfileModuleViewModel.cs rename src/Artemis.UI/Screens/{ProfileEditor/Dialogs/ProfileEditView.xaml => Sidebar/Dialogs/SidebarCategoryCreateView.xaml} (61%) create mode 100644 src/Artemis.UI/Screens/Sidebar/Dialogs/SidebarCategoryCreateViewModel.cs create mode 100644 src/Artemis.UI/Screens/Sidebar/Dialogs/SidebarCategoryUpdateView.xaml create mode 100644 src/Artemis.UI/Screens/Sidebar/Dialogs/SidebarCategoryUpdateViewModel.cs create mode 100644 src/Artemis.UI/Screens/Sidebar/SidebarCategoryView.xaml create mode 100644 src/Artemis.UI/Screens/Sidebar/SidebarCategoryView.xaml.cs create mode 100644 src/Artemis.UI/Screens/Sidebar/SidebarCategoryViewModel.cs create mode 100644 src/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationView.xaml create mode 100644 src/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationViewModel.cs create mode 100644 src/Artemis.UI/Screens/Sidebar/SidebarScreenView.xaml create mode 100644 src/Artemis.UI/Screens/Sidebar/SidebarScreenViewModel.cs delete mode 100644 src/Artemis.UI/Screens/Sidebar/SidebarView.xaml.cs diff --git a/src/Artemis.Core/Artemis.Core.csproj.DotSettings b/src/Artemis.Core/Artemis.Core.csproj.DotSettings index daa70a5ef..461cc1c15 100644 --- a/src/Artemis.Core/Artemis.Core.csproj.DotSettings +++ b/src/Artemis.Core/Artemis.Core.csproj.DotSettings @@ -1,5 +1,6 @@  True + True True True True diff --git a/src/Artemis.Core/Events/ModuleEventArgs.cs b/src/Artemis.Core/Events/ModuleEventArgs.cs new file mode 100644 index 000000000..44db160a3 --- /dev/null +++ b/src/Artemis.Core/Events/ModuleEventArgs.cs @@ -0,0 +1,21 @@ +using System; +using Artemis.Core.Modules; + +namespace Artemis.Core +{ + /// + /// Provides data about module events + /// + public class ModuleEventArgs : EventArgs + { + internal ModuleEventArgs(Module module) + { + Module = module; + } + + /// + /// Gets the module this event is related to + /// + public Module Module { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Events/Profiles/ProfileConfigurationEventArgs.cs b/src/Artemis.Core/Events/Profiles/ProfileConfigurationEventArgs.cs new file mode 100644 index 000000000..4bfa83c26 --- /dev/null +++ b/src/Artemis.Core/Events/Profiles/ProfileConfigurationEventArgs.cs @@ -0,0 +1,20 @@ +using System; + +namespace Artemis.Core +{ + /// + /// Provides data for profile configuration events. + /// + public class ProfileConfigurationEventArgs : EventArgs + { + internal ProfileConfigurationEventArgs(ProfileConfiguration profileConfiguration) + { + ProfileConfiguration = profileConfiguration; + } + + /// + /// Gets the profile configuration this event is related to + /// + public ProfileConfiguration ProfileConfiguration { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/JsonConverters/StreamConverter.cs b/src/Artemis.Core/JsonConverters/StreamConverter.cs new file mode 100644 index 000000000..26e638d0e --- /dev/null +++ b/src/Artemis.Core/JsonConverters/StreamConverter.cs @@ -0,0 +1,45 @@ +using System; +using System.IO; +using Newtonsoft.Json; + +namespace Artemis.Core.JsonConverters +{ + /// + public class StreamConverter : JsonConverter + { + #region Overrides of JsonConverter + + /// + public override void WriteJson(JsonWriter writer, Stream? value, JsonSerializer serializer) + { + if (value == null) + { + writer.WriteNull(); + return; + } + + using MemoryStream memoryStream = new(); + value.Position = 0; + value.CopyTo(memoryStream); + writer.WriteValue(memoryStream.ToArray()); + } + + /// + public override Stream? ReadJson(JsonReader reader, Type objectType, Stream? existingValue, bool hasExistingValue, JsonSerializer serializer) + { + if (reader.Value is not string base64) + return null; + + if (existingValue == null || !hasExistingValue || !existingValue.CanRead) + return new MemoryStream(Convert.FromBase64String(base64)); + + using MemoryStream memoryStream = new(Convert.FromBase64String(base64)); + existingValue.Position = 0; + memoryStream.CopyTo(existingValue); + existingValue.Position = 0; + return existingValue; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/Abstract/DataModelConditionPredicate.cs b/src/Artemis.Core/Models/Profile/Conditions/Abstract/DataModelConditionPredicate.cs index 8424d3876..f836d9157 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/Abstract/DataModelConditionPredicate.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/Abstract/DataModelConditionPredicate.cs @@ -105,37 +105,66 @@ namespace Artemis.Core InitializeRightPath(); // Right side static else if (PredicateType == ProfileRightSideType.Static && Entity.RightStaticValue != null) + { try { - if (LeftPath != null && LeftPath.IsValid) + // If the left path is not valid we cannot reliably set up the right side because the type is unknown + // Because of that wait for it to validate first + if (LeftPath != null && !LeftPath.IsValid) { - // Use the left side type so JSON.NET has a better idea what to do - Type leftSideType = LeftPath.GetPropertyType()!; - object? rightSideValue; - - try - { - rightSideValue = CoreJson.DeserializeObject(Entity.RightStaticValue, leftSideType); - } - // If deserialization fails, use the type's default - catch (JsonSerializationException e) - { - DeserializationLogger.LogPredicateDeserializationFailure(this, e); - rightSideValue = Activator.CreateInstance(leftSideType); - } - - UpdateRightSideStatic(rightSideValue); + LeftPath.PathValidated += InitializeRightSideStatic; + return; } - else + if (LeftPath == null) + return; + + // Use the left side type so JSON.NET has a better idea what to do + Type leftSideType = LeftPath.GetPropertyType()!; + object? rightSideValue; + + try { - // Hope for the best... - UpdateRightSideStatic(CoreJson.DeserializeObject(Entity.RightStaticValue)); + rightSideValue = CoreJson.DeserializeObject(Entity.RightStaticValue, leftSideType); } + // If deserialization fails, use the type's default + catch (JsonSerializationException e) + { + DeserializationLogger.LogPredicateDeserializationFailure(this, e); + rightSideValue = Activator.CreateInstance(leftSideType); + } + + UpdateRightSideStatic(rightSideValue); } catch (JsonReaderException e) { DeserializationLogger.LogPredicateDeserializationFailure(this, e); } + } + } + + private void InitializeRightSideStatic(object? sender, EventArgs args) + { + if (LeftPath == null) + return; + + LeftPath.PathValidated -= InitializeRightSideStatic; + + // Use the left side type so JSON.NET has a better idea what to do + Type leftSideType = LeftPath.GetPropertyType()!; + object? rightSideValue; + + try + { + rightSideValue = CoreJson.DeserializeObject(Entity.RightStaticValue, leftSideType); + } + // If deserialization fails, use the type's default + catch (JsonSerializationException e) + { + DeserializationLogger.LogPredicateDeserializationFailure(this, e); + rightSideValue = Activator.CreateInstance(leftSideType); + } + + UpdateRightSideStatic(rightSideValue); } /// diff --git a/src/Artemis.Core/Models/Profile/Conditions/Wrappers/EventPredicateWrapperDataModel.cs b/src/Artemis.Core/Models/Profile/Conditions/Wrappers/EventPredicateWrapperDataModel.cs index c170b68e5..76e19f3c4 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/Wrappers/EventPredicateWrapperDataModel.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/Wrappers/EventPredicateWrapperDataModel.cs @@ -16,7 +16,7 @@ namespace Artemis.Core { internal EventPredicateWrapperDataModel() { - Feature = Constants.CorePluginFeature; + Module = Constants.CorePluginFeature; } /// diff --git a/src/Artemis.Core/Models/Profile/Conditions/Wrappers/ListPredicateWrapperDataModel.cs b/src/Artemis.Core/Models/Profile/Conditions/Wrappers/ListPredicateWrapperDataModel.cs index aa512f797..f6eafa524 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/Wrappers/ListPredicateWrapperDataModel.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/Wrappers/ListPredicateWrapperDataModel.cs @@ -16,7 +16,7 @@ namespace Artemis.Core { internal ListPredicateWrapperDataModel() { - Feature = Constants.CorePluginFeature; + Module = Constants.CorePluginFeature; } /// diff --git a/src/Artemis.Core/Models/Profile/DataModel/DataModelPath.cs b/src/Artemis.Core/Models/Profile/DataModel/DataModelPath.cs index 9c5953749..c2eb9f1e2 100644 --- a/src/Artemis.Core/Models/Profile/DataModel/DataModelPath.cs +++ b/src/Artemis.Core/Models/Profile/DataModel/DataModelPath.cs @@ -93,7 +93,7 @@ namespace Artemis.Core /// /// Gets the data model ID of the if it is a /// - public string? DataModelId => Target?.Feature.Id; + public string? DataModelId => Target?.Module.Id; /// /// Gets the point-separated path associated with this @@ -327,7 +327,7 @@ namespace Artemis.Core private void DataModelStoreOnDataModelAdded(object? sender, DataModelStoreEvent e) { - if (e.Registration.DataModel.Feature.Id != Entity.DataModelId) + if (e.Registration.DataModel.Module.Id != Entity.DataModelId) return; Target = e.Registration.DataModel; @@ -336,7 +336,7 @@ namespace Artemis.Core private void DataModelStoreOnDataModelRemoved(object? sender, DataModelStoreEvent e) { - if (e.Registration.DataModel.Feature.Id != Entity.DataModelId) + if (e.Registration.DataModel.Module.Id != Entity.DataModelId) return; Target = null; diff --git a/src/Artemis.Core/Models/Profile/Folder.cs b/src/Artemis.Core/Models/Profile/Folder.cs index 16231eed8..8f875aaf6 100644 --- a/src/Artemis.Core/Models/Profile/Folder.cs +++ b/src/Artemis.Core/Models/Profile/Folder.cs @@ -205,13 +205,6 @@ namespace Artemis.Core canvas.SaveLayer(layerPaint); canvas.Translate(Bounds.Left - basePosition.X, Bounds.Top - basePosition.Y); - // If required, apply the opacity override of the module to the root folder - if (IsRootFolder && Profile.Module.OpacityOverride < 1) - { - double multiplier = Easings.SineEaseInOut(Profile.Module.OpacityOverride); - layerPaint.Color = layerPaint.Color.WithAlpha((byte) (layerPaint.Color.Alpha * multiplier)); - } - // No point rendering if the alpha was set to zero by one of the effects if (layerPaint.Color.Alpha == 0) return; @@ -240,6 +233,7 @@ namespace Artemis.Core { Disposed = true; + Disable(); foreach (ProfileElement profileElement in Children) profileElement.Dispose(); diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs index 01754e172..6c1b74da8 100644 --- a/src/Artemis.Core/Models/Profile/Layer.cs +++ b/src/Artemis.Core/Models/Profile/Layer.cs @@ -168,6 +168,8 @@ namespace Artemis.Core /// protected override void Dispose(bool disposing) { + Disable(); + Disposed = true; // Brush first in case it depends on any of the other disposables during it's own disposal diff --git a/src/Artemis.Core/Models/Profile/Profile.cs b/src/Artemis.Core/Models/Profile/Profile.cs index 8bb05cb45..3107d0805 100644 --- a/src/Artemis.Core/Models/Profile/Profile.cs +++ b/src/Artemis.Core/Models/Profile/Profile.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Artemis.Core.Modules; using Artemis.Storage.Entities.Profile; using SkiaSharp; @@ -13,31 +12,15 @@ namespace Artemis.Core public sealed class Profile : ProfileElement { private readonly object _lock = new(); - private bool _isActivated; private bool _isFreshImport; - - internal Profile(ProfileModule module, string name) : base(null!) - { - ProfileEntity = new ProfileEntity(); - EntityId = Guid.NewGuid(); - - Profile = this; - Module = module; - Name = name; - UndoStack = new Stack(); - RedoStack = new Stack(); - - Folder _ = new(this, "Root folder"); - Save(); - } - - internal Profile(ProfileModule module, ProfileEntity profileEntity) : base(null!) + + internal Profile(ProfileConfiguration configuration, ProfileEntity profileEntity) : base(null!) { + Configuration = configuration; Profile = this; ProfileEntity = profileEntity; EntityId = profileEntity.Id; - Module = module; UndoStack = new Stack(); RedoStack = new Stack(); @@ -45,18 +28,9 @@ namespace Artemis.Core } /// - /// Gets the module backing this profile + /// Gets the profile configuration of this profile /// - public ProfileModule Module { get; } - - /// - /// Gets a boolean indicating whether this profile is activated - /// - public bool IsActivated - { - get => _isActivated; - private set => SetAndNotify(ref _isActivated, value); - } + public ProfileConfiguration Configuration { get; } /// /// Gets or sets a boolean indicating whether this profile is freshly imported i.e. no changes have been made to it @@ -87,8 +61,6 @@ namespace Artemis.Core { if (Disposed) throw new ObjectDisposedException("Profile"); - if (!IsActivated) - throw new ArtemisCoreException($"Cannot update inactive profile: {this}"); foreach (ProfileElement profileElement in Children) profileElement.Update(deltaTime); @@ -102,8 +74,6 @@ namespace Artemis.Core { if (Disposed) throw new ObjectDisposedException("Profile"); - if (!IsActivated) - throw new ArtemisCoreException($"Cannot render inactive profile: {this}"); foreach (ProfileElement profileElement in Children) profileElement.Render(canvas, basePosition); @@ -133,7 +103,7 @@ namespace Artemis.Core /// public override string ToString() { - return $"[Profile] {nameof(Name)}: {Name}, {nameof(IsActivated)}: {IsActivated}, {nameof(Module)}: {Module}"; + return $"[Profile] {nameof(Name)}: {Name}"; } /// @@ -149,29 +119,15 @@ namespace Artemis.Core layer.PopulateLeds(devices); } - /// - /// Occurs when the profile has been activated. - /// - public event EventHandler? Activated; - - /// - /// Occurs when the profile is being deactivated. - /// - public event EventHandler? Deactivated; - /// protected override void Dispose(bool disposing) { if (!disposing) return; - OnDeactivating(); - foreach (ProfileElement profileElement in Children) profileElement.Dispose(); ChildrenList.Clear(); - - IsActivated = false; Disposed = true; } @@ -180,7 +136,7 @@ namespace Artemis.Core if (Disposed) throw new ObjectDisposedException("Profile"); - Name = ProfileEntity.Name; + Name = Configuration.Name; IsFreshImport = ProfileEntity.IsFreshImport; lock (ChildrenList) @@ -197,7 +153,9 @@ namespace Artemis.Core Folder _ = new(this, "Root folder"); } else + { AddChild(new Folder(this, this, rootFolder)); + } } } @@ -207,9 +165,7 @@ namespace Artemis.Core throw new ObjectDisposedException("Profile"); ProfileEntity.Id = EntityId; - ProfileEntity.ModuleId = Module.Id; - ProfileEntity.Name = Name; - ProfileEntity.IsActive = IsActivated; + ProfileEntity.Name = Configuration.Name; ProfileEntity.IsFreshImport = IsFreshImport; foreach (ProfileElement profileElement in Children) @@ -221,30 +177,5 @@ namespace Artemis.Core ProfileEntity.Layers.Clear(); ProfileEntity.Layers.AddRange(GetAllLayers().Select(f => f.LayerEntity)); } - - internal void Activate(IEnumerable devices) - { - lock (_lock) - { - if (Disposed) - throw new ObjectDisposedException("Profile"); - if (IsActivated) - return; - - PopulateLeds(devices); - OnActivated(); - IsActivated = true; - } - } - - private void OnActivated() - { - Activated?.Invoke(this, EventArgs.Empty); - } - - private void OnDeactivating() - { - Deactivated?.Invoke(this, EventArgs.Empty); - } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/ProfileCategory.cs b/src/Artemis.Core/Models/Profile/ProfileCategory.cs new file mode 100644 index 000000000..b5d0b85f2 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/ProfileCategory.cs @@ -0,0 +1,244 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Artemis.Storage.Entities.Profile; + +namespace Artemis.Core +{ + public class ProfileCategory : CorePropertyChanged, IStorageModel + { + private readonly List _profileConfigurations = new(); + private bool _isCollapsed; + private bool _isSuspended; + private string _name; + private int _order; + + /// + /// Creates a new instance of the class + /// + /// The name of the category + internal ProfileCategory(string name) + { + _name = name; + Entity = new ProfileCategoryEntity(); + } + + internal ProfileCategory(ProfileCategoryEntity entity) + { + Entity = entity; + Load(); + } + + /// + /// Gets or sets the name of the profile category + /// + public string Name + { + get => _name; + set => SetAndNotify(ref _name, value); + } + + /// + /// The order in which this category appears in the update loop and sidebar + /// + public int Order + { + get => _order; + set => SetAndNotify(ref _order, value); + } + + /// + /// Gets or sets a boolean indicating whether the category is collapsed or not + /// Note: Has no implications other than inside the UI + /// + public bool IsCollapsed + { + get => _isCollapsed; + set => SetAndNotify(ref _isCollapsed, value); + } + + /// + /// Gets or sets a boolean indicating whether this category is suspended, disabling all its profiles + /// + public bool IsSuspended + { + get => _isSuspended; + set => SetAndNotify(ref _isSuspended, value); + } + + /// + /// Gets a read only collection of the profiles inside this category + /// + public ReadOnlyCollection ProfileConfigurations => _profileConfigurations.AsReadOnly(); + + /// + /// Gets the unique ID of this category + /// + public Guid EntityId => Entity.Id; + + internal ProfileCategoryEntity Entity { get; } + + + /// + /// Adds a profile configuration to this category + /// + public void AddProfileConfiguration(ProfileConfiguration configuration, int? targetIndex) + { + // Removing the original will shift every item in the list forwards, keep that in mind with the target index + if (configuration.Category == this && targetIndex != null && targetIndex.Value > _profileConfigurations.IndexOf(configuration)) + targetIndex -= 1; + + configuration.Category.RemoveProfileConfiguration(configuration); + + if (targetIndex != null) + _profileConfigurations.Insert(Math.Clamp(targetIndex.Value, 0, _profileConfigurations.Count), configuration); + else + _profileConfigurations.Add(configuration); + configuration.Category = this; + + for (int index = 0; index < _profileConfigurations.Count; index++) + _profileConfigurations[index].Order = index; + OnProfileConfigurationAdded(new ProfileConfigurationEventArgs(configuration)); + } + + /// + public override string ToString() + { + return $"[ProfileCategory] {Order} {nameof(Name)}: {Name}, {nameof(IsSuspended)}: {IsSuspended}"; + } + + internal void RemoveProfileConfiguration(ProfileConfiguration configuration) + { + if (!_profileConfigurations.Remove(configuration)) return; + + for (int index = 0; index < _profileConfigurations.Count; index++) + _profileConfigurations[index].Order = index; + OnProfileConfigurationRemoved(new ProfileConfigurationEventArgs(configuration)); + } + + #region Implementation of IStorageModel + + /// + public void Load() + { + Name = Entity.Name; + IsCollapsed = Entity.IsCollapsed; + IsSuspended = Entity.IsSuspended; + Order = Entity.Order; + + _profileConfigurations.Clear(); + foreach (ProfileConfigurationEntity entityProfileConfiguration in Entity.ProfileConfigurations) + _profileConfigurations.Add(new ProfileConfiguration(this, entityProfileConfiguration)); + } + + /// + public void Save() + { + Entity.Name = Name; + Entity.IsCollapsed = IsCollapsed; + Entity.IsSuspended = IsSuspended; + Entity.Order = Order; + + Entity.ProfileConfigurations.Clear(); + foreach (ProfileConfiguration profileConfiguration in ProfileConfigurations) + { + profileConfiguration.Save(); + Entity.ProfileConfigurations.Add(profileConfiguration.Entity); + } + } + + #endregion + + #region Events + + /// + /// Occurs when a profile configuration is added to this + /// + public event EventHandler? ProfileConfigurationAdded; + + /// + /// Occurs when a profile configuration is removed from this + /// + public event EventHandler? ProfileConfigurationRemoved; + + /// + /// Invokes the event + /// + protected virtual void OnProfileConfigurationAdded(ProfileConfigurationEventArgs e) + { + ProfileConfigurationAdded?.Invoke(this, e); + } + + /// + /// Invokes the event + /// + protected virtual void OnProfileConfigurationRemoved(ProfileConfigurationEventArgs e) + { + ProfileConfigurationRemoved?.Invoke(this, e); + } + + #endregion + } + + /// + /// Represents a name of one of the default categories + /// + public enum DefaultCategoryName + { + /// + /// The category used by profiles tied to games + /// + Games, + + /// + /// The category used by profiles tied to applications + /// + Applications, + + /// + /// The category used by general profiles + /// + General + } + + /// + /// Represents a type of behaviour when this profile is activated + /// + public enum ActivationBehaviour + { + /// + /// Do nothing to other profiles + /// + None, + + /// + /// Disable all other profiles + /// + DisableOthers, + + /// + /// Disable all other profiles below this one + /// + DisableOthersBelow, + + /// + /// Disable all other profiles above this one + /// + DisableOthersAbove, + + /// + /// Disable all other profiles in the same category + /// + DisableOthersInCategory, + + /// + /// Disable all other profiles below this one in the same category + /// + DisableOthersBelowInCategory, + + /// + /// Disable all other profiles above this one in the same category + /// + DisableOthersAboveInCategory + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/ProfileDescriptor.cs b/src/Artemis.Core/Models/Profile/ProfileDescriptor.cs deleted file mode 100644 index 7e5d776e5..000000000 --- a/src/Artemis.Core/Models/Profile/ProfileDescriptor.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using Artemis.Core.Modules; -using Artemis.Storage.Entities.Profile; - -namespace Artemis.Core -{ - /// - /// Represents a descriptor that describes a profile - /// - public class ProfileDescriptor : CorePropertyChanged - { - internal ProfileDescriptor(ProfileModule profileModule, ProfileEntity profileEntity) - { - ProfileModule = profileModule; - - Id = profileEntity.Id; - Name = profileEntity.Name; - IsLastActiveProfile = profileEntity.IsActive; - } - - /// - /// Gets the module backing the profile - /// - public ProfileModule ProfileModule { get; } - - /// - /// Gets the unique ID of the profile by which it can be loaded from storage - /// - public Guid Id { get; } - - /// - /// Gets the name of the profile - /// - public string Name { get; } - - /// - /// Gets a boolean indicating whether this was the last active profile - /// - public bool IsLastActiveProfile { get; } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/ProfileElement.cs b/src/Artemis.Core/Models/Profile/ProfileElement.cs index 39006fb85..58f005f94 100644 --- a/src/Artemis.Core/Models/Profile/ProfileElement.cs +++ b/src/Artemis.Core/Models/Profile/ProfileElement.cs @@ -11,22 +11,21 @@ namespace Artemis.Core /// public abstract class ProfileElement : CorePropertyChanged, IDisposable { - private bool _suspended; private Guid _entityId; private string? _name; private int _order; private ProfileElement? _parent; private Profile _profile; + private bool _suspended; internal List ChildrenList; - internal bool Disposed; internal ProfileElement(Profile profile) { _profile = profile; ChildrenList = new List(); } - + /// /// Gets the unique ID of this profile element /// @@ -95,6 +94,11 @@ namespace Artemis.Core set => SetAndNotify(ref _suspended, value); } + /// + /// Gets a boolean indicating whether the profile element is disposed + /// + public bool Disposed { get; protected set; } + /// /// Updates the element /// diff --git a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs new file mode 100644 index 000000000..84869736e --- /dev/null +++ b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs @@ -0,0 +1,210 @@ +using System.Collections.Generic; +using System.Linq; +using Artemis.Core.Modules; +using Artemis.Storage.Entities.Profile; + +namespace Artemis.Core +{ + public class ProfileConfiguration : CorePropertyChanged, IStorageModel + { + private ProfileCategory _category; + + private bool _isMissingModule; + private bool _isSuspended; + private Module? _module; + private string _name; + private int _order; + private Profile? _profile; + + internal ProfileConfiguration(ProfileCategory category, string name, string icon) + { + _name = name; + _category = category; + + Entity = new ProfileConfigurationEntity(); + Icon = new ProfileConfigurationIcon(Entity) {MaterialIcon = icon}; + } + + internal ProfileConfiguration(ProfileCategory category, ProfileConfigurationEntity entity) + { + // Will be loaded from the entity + _name = null!; + _category = category; + + Entity = entity; + Icon = new ProfileConfigurationIcon(Entity); + Load(); + } + + /// + /// Gets or sets the name of this profile configuration + /// + public string Name + { + get => _name; + set => SetAndNotify(ref _name, value); + } + + /// + /// The order in which this profile appears in the update loop and sidebar + /// + public int Order + { + get => _order; + set => SetAndNotify(ref _order, value); + } + + /// + /// Gets or sets a boolean indicating whether this profile is suspended, disabling it regardless of the + /// + /// + public bool IsSuspended + { + get => _isSuspended; + set => SetAndNotify(ref _isSuspended, value); + } + + /// + /// Gets a boolean indicating whether this profile configuration is missing any modules + /// + public bool IsMissingModule + { + get => _isMissingModule; + private set => SetAndNotify(ref _isMissingModule, value); + } + + /// + /// Gets or sets the category of this profile configuration + /// + public ProfileCategory Category + { + get => _category; + internal set => SetAndNotify(ref _category, value); + } + + /// + /// Gets the icon configuration + /// + public ProfileConfigurationIcon Icon { get; } + + /// + /// Gets the profile of this profile configuration + /// + public Profile? Profile + { + get => _profile; + internal set => SetAndNotify(ref _profile, value); + } + + /// + /// Gets or sets the behaviour of when this profile is activated + /// + public ActivationBehaviour ActivationBehaviour { get; set; } + + /// + /// Gets the data model condition that must evaluate to for this profile to be activated + /// alongside any activation requirements of the , if set + /// + public DataModelConditionGroup? ActivationCondition { get; set; } + + /// + /// Gets or sets the module this profile uses + /// + public Module? Module + { + get => _module; + set + { + _module = value; + IsMissingModule = false; + } + } + + /// + /// Gets a boolean indicating whether the activation conditions where met during the last call + /// + public bool ActivationConditionMet { get; private set; } + + /// + /// Gets or sets a boolean indicating whether this profile configuration is being edited + /// + public bool IsBeingEdited { get; set; } + + /// + /// Gets the entity used by this profile config + /// + public ProfileConfigurationEntity Entity { get; } + + /// + /// Updates this configurations activation condition status + /// + public void Update() + { + ActivationConditionMet = ActivationCondition == null || ActivationCondition.Evaluate(); + } + + public bool ShouldBeActive(bool includeActivationCondition) + { + if (Category.IsSuspended || IsSuspended || IsMissingModule) + return false; + + if (includeActivationCondition) + return ActivationConditionMet && (Module == null || Module.IsActivated); + return Module == null || Module.IsActivated; + } + + /// + public override string ToString() + { + return $"[ProfileConfiguration] {nameof(Name)}: {Name}"; + } + + internal void LoadModules(List enabledModules) + { + Module = enabledModules.FirstOrDefault(m => m.Id == Entity.ModuleId); + IsMissingModule = Module == null && Entity.ModuleId != null; + } + + #region Implementation of IStorageModel + + /// + public void Load() + { + Name = Entity.Name; + IsSuspended = Entity.IsSuspended; + ActivationBehaviour = (ActivationBehaviour) Entity.ActivationBehaviour; + + Icon.Load(); + + ActivationCondition = Entity.ActivationCondition != null + ? new DataModelConditionGroup(null, Entity.ActivationCondition) + : null; + } + + /// + public void Save() + { + Entity.Name = Name; + Entity.IsSuspended = IsSuspended; + Entity.ActivationBehaviour = (int) ActivationBehaviour; + Entity.ProfileCategoryId = Category.Entity.Id; + + Icon.Save(); + + if (ActivationCondition != null) + { + ActivationCondition.Save(); + Entity.ActivationCondition = ActivationCondition.Entity; + } + else + { + Entity.ActivationCondition = null; + } + + if (!IsMissingModule) + Entity.ModuleId = Module?.Id; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationExportModel.cs b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationExportModel.cs new file mode 100644 index 000000000..c899cdc01 --- /dev/null +++ b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationExportModel.cs @@ -0,0 +1,30 @@ +using System.IO; +using Artemis.Core.JsonConverters; +using Artemis.Storage.Entities.Profile; +using Newtonsoft.Json; + +namespace Artemis.Core +{ + /// + /// A model that can be used to serialize a profile configuration, it's profile and it's icon + /// + public class ProfileConfigurationExportModel + { + /// + /// Gets or sets the storage entity of the profile configuration + /// + public ProfileConfigurationEntity? ProfileConfigurationEntity { get; set; } + + /// + /// Gets or sets the storage entity of the profile + /// + [JsonProperty(Required = Required.Always)] + public ProfileEntity ProfileEntity { get; set; } = null!; + + /// + /// Gets or sets a stream containing the profile image + /// + [JsonConverter(typeof(StreamConverter))] + public Stream? ProfileImage { get; set; } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationIcon.cs b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationIcon.cs new file mode 100644 index 000000000..b188852bf --- /dev/null +++ b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationIcon.cs @@ -0,0 +1,89 @@ +using System.ComponentModel; +using System.IO; +using Artemis.Storage.Entities.Profile; + +namespace Artemis.Core +{ + /// + /// Represents the icon of a + /// + public class ProfileConfigurationIcon : CorePropertyChanged, IStorageModel + { + private readonly ProfileConfigurationEntity _entity; + private Stream? _fileIcon; + private ProfileConfigurationIconType _iconType; + private string? _materialIcon; + + internal ProfileConfigurationIcon(ProfileConfigurationEntity entity) + { + _entity = entity; + } + + /// + /// Gets or sets the type of icon this profile configuration uses + /// + public ProfileConfigurationIconType IconType + { + get => _iconType; + set => SetAndNotify(ref _iconType, value); + } + + /// + /// Gets or sets the icon if it is a Material icon + /// + public string? MaterialIcon + { + get => _materialIcon; + set => SetAndNotify(ref _materialIcon, value); + } + + /// + /// Gets or sets a stream containing the icon if it is bitmap or SVG + /// + /// + public Stream? FileIcon + { + get => _fileIcon; + set => SetAndNotify(ref _fileIcon, value); + } + + #region Implementation of IStorageModel + + /// + public void Load() + { + IconType = (ProfileConfigurationIconType) _entity.IconType; + MaterialIcon = _entity.MaterialIcon; + } + + /// + public void Save() + { + _entity.IconType = (int) IconType; + _entity.MaterialIcon = MaterialIcon; + } + + #endregion + } + + /// + /// Represents a type of profile icon + /// + public enum ProfileConfigurationIconType + { + /// + /// An icon picked from the Material Design Icons collection + /// + [Description("Material Design Icon")] MaterialIcon, + + /// + /// A bitmap image icon + /// + [Description("Bitmap Image")] BitmapImage, + + /// + /// An SVG image icon + /// + [Description("SVG Image")] SvgImage + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/DataModelExpansions/DataModelExpansion.cs b/src/Artemis.Core/Plugins/DataModelExpansions/DataModelExpansion.cs deleted file mode 100644 index a0dd6b998..000000000 --- a/src/Artemis.Core/Plugins/DataModelExpansions/DataModelExpansion.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; - -namespace Artemis.Core.DataModelExpansions -{ - /// - /// Allows you to expand the application-wide datamodel - /// - public abstract class DataModelExpansion : BaseDataModelExpansion where T : DataModel - { - /// - /// The main data model of this data model expansion - /// Note: This default data model is automatically registered upon plugin enable - /// - public T DataModel - { - get => InternalDataModel as T ?? throw new InvalidOperationException("Internal datamodel does not match the type of the data model"); - internal set => InternalDataModel = value; - } - - /// - /// Hide the provided property using a lambda expression, e.g. HideProperty(dm => dm.TimeDataModel.CurrentTimeUTC) - /// - /// A lambda expression pointing to the property to ignore - public void HideProperty(Expression> propertyLambda) - { - PropertyInfo propertyInfo = ReflectionUtilities.GetPropertyInfo(DataModel, propertyLambda); - if (!HiddenPropertiesList.Any(p => p.Equals(propertyInfo))) - HiddenPropertiesList.Add(propertyInfo); - } - - /// - /// Stop hiding the provided property using a lambda expression, e.g. ShowProperty(dm => - /// dm.TimeDataModel.CurrentTimeUTC) - /// - /// A lambda expression pointing to the property to stop ignoring - public void ShowProperty(Expression> propertyLambda) - { - PropertyInfo propertyInfo = ReflectionUtilities.GetPropertyInfo(DataModel, propertyLambda); - HiddenPropertiesList.RemoveAll(p => p.Equals(propertyInfo)); - } - - internal override void InternalEnable() - { - DataModel = Activator.CreateInstance(); - DataModel.Feature = this; - DataModel.DataModelDescription = GetDataModelDescription(); - base.InternalEnable(); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/DataModelExpansions/Internal/BaseDataModelExpansion.cs b/src/Artemis.Core/Plugins/DataModelExpansions/Internal/BaseDataModelExpansion.cs deleted file mode 100644 index 1c3397ef4..000000000 --- a/src/Artemis.Core/Plugins/DataModelExpansions/Internal/BaseDataModelExpansion.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Reflection; - -namespace Artemis.Core.DataModelExpansions -{ - /// - /// For internal use only, to implement your own layer property type, extend - /// instead. - /// - public abstract class BaseDataModelExpansion : DataModelPluginFeature - { - /// - /// Gets a list of all properties ignored at runtime using IgnoreProperty(x => x.y) - /// - protected internal readonly List HiddenPropertiesList = new(); - - /// - /// Gets a list of all properties ignored at runtime using IgnoreProperty(x => x.y) - /// - public ReadOnlyCollection HiddenProperties => HiddenPropertiesList.AsReadOnly(); - - internal DataModel? InternalDataModel { get; set; } - - /// - /// Called each frame when the data model should update - /// - /// Time in seconds since the last update - public abstract void Update(double deltaTime); - - internal void InternalUpdate(double deltaTime) - { - if (InternalDataModel != null) - Update(deltaTime); - } - - /// - /// Override to provide your own data model description. By default this returns a description matching your plugin - /// name and description - /// - /// - public virtual DataModelPropertyAttribute GetDataModelDescription() - { - return new() {Name = Plugin.Info.Name, Description = Plugin.Info.Description}; - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/DataModelPluginFeature.cs b/src/Artemis.Core/Plugins/DataModelPluginFeature.cs deleted file mode 100644 index 099fba242..000000000 --- a/src/Artemis.Core/Plugins/DataModelPluginFeature.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace Artemis.Core -{ - /// - /// Represents an feature of a certain type provided by a plugin with support for data models - /// - public abstract class DataModelPluginFeature : PluginFeature - { - - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerBrushes/Internal/BaseLayerBrush.cs b/src/Artemis.Core/Plugins/LayerBrushes/Internal/BaseLayerBrush.cs index 8f63b16b9..0154529f6 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/Internal/BaseLayerBrush.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/Internal/BaseLayerBrush.cs @@ -6,7 +6,7 @@ using SkiaSharp; namespace Artemis.Core.LayerBrushes { /// - /// For internal use only, please use or or instead + /// For internal use only, please use or or instead /// public abstract class BaseLayerBrush : CorePropertyChanged, IDisposable { diff --git a/src/Artemis.Core/Plugins/LayerBrushes/Internal/PropertiesLayerBrush.cs b/src/Artemis.Core/Plugins/LayerBrushes/Internal/PropertiesLayerBrush.cs index 9184e1c6b..4225b0455 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/Internal/PropertiesLayerBrush.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/Internal/PropertiesLayerBrush.cs @@ -1,11 +1,9 @@ using System; -using System.Collections.Generic; -using System.Linq; namespace Artemis.Core.LayerBrushes { /// - /// For internal use only, please use or or instead + /// For internal use only, please use or or instead /// public abstract class PropertiesLayerBrush : BaseLayerBrush where T : LayerPropertyGroup { diff --git a/src/Artemis.Core/Plugins/LayerBrushes/RgbNetLayerBrush.cs b/src/Artemis.Core/Plugins/LayerBrushes/RgbNetLayerBrush.cs deleted file mode 100644 index 7dd558a16..000000000 --- a/src/Artemis.Core/Plugins/LayerBrushes/RgbNetLayerBrush.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Artemis.Core.Services; -using Ninject; -using RGB.NET.Core; -using SkiaSharp; - -namespace Artemis.Core.LayerBrushes -{ - /// - /// An RGB.NET brush that uses RGB.NET's per-LED rendering engine. - /// Note: This brush type always renders on top of regular brushes - /// - /// - public abstract class RgbNetLayerBrush : PropertiesLayerBrush where T : LayerPropertyGroup - { - /// - /// Creates a new instance of the class - /// - protected RgbNetLayerBrush() - { - BrushType = LayerBrushType.RgbNet; - SupportsTransformation = false; - } - - /// - /// The LED group this layer effect is applied to - /// - public ListLedGroup? LedGroup { get; internal set; } - - /// - /// For internal use only, is public for dependency injection but ignore pl0x - /// - [Inject] - public IRgbService? RgbService { get; set; } - - /// - /// Called when Artemis needs an instance of the RGB.NET effect you are implementing - /// - /// Your RGB.NET effect - public abstract IBrush GetBrush(); - - #region IDisposable - - /// - 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() - { - if (LedGroup == null) - return; - - if (Layer.Parent != null) - LedGroup.ZIndex = Layer.Parent.Children.Count - Layer.Parent.Children.IndexOf(Layer); - else - LedGroup.ZIndex = 1; - - List missingLeds = Layer.Leds.Where(l => !LedGroup.ContainsLed(l.RgbLed)).Select(l => l.RgbLed).ToList(); - List extraLeds = LedGroup.Where(l => Layer.Leds.All(layerLed => layerLed.RgbLed != l)).ToList(); - LedGroup.AddLeds(missingLeds); - LedGroup.RemoveLeds(extraLeds); - LedGroup.Brush = GetBrush(); - } - - internal override void Initialize() - { - if (RgbService == null) - throw new ArtemisCoreException("Cannot initialize RGB.NET layer brush because RgbService is not set"); - - LedGroup = new ListLedGroup(RgbService.Surface); - Layer.RenderPropertiesUpdated += LayerOnRenderPropertiesUpdated; - - InitializeProperties(); - UpdateLedGroup(); - } - - // Not used in this effect type - internal override void InternalRender(SKCanvas canvas, SKRect bounds, SKPaint paint) - { - throw new NotImplementedException("RGB.NET layer effects do not implement InternalRender"); - } - - private void LayerOnRenderPropertiesUpdated(object? sender, EventArgs e) - { - UpdateLedGroup(); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/DataModelExpansions/Attributes/DataModelIgnoreAttribute.cs b/src/Artemis.Core/Plugins/Modules/Attributes/DataModelIgnoreAttribute.cs similarity index 100% rename from src/Artemis.Core/Plugins/DataModelExpansions/Attributes/DataModelIgnoreAttribute.cs rename to src/Artemis.Core/Plugins/Modules/Attributes/DataModelIgnoreAttribute.cs diff --git a/src/Artemis.Core/Plugins/DataModelExpansions/Attributes/DataModelProperty.cs b/src/Artemis.Core/Plugins/Modules/Attributes/DataModelProperty.cs similarity index 100% rename from src/Artemis.Core/Plugins/DataModelExpansions/Attributes/DataModelProperty.cs rename to src/Artemis.Core/Plugins/Modules/Attributes/DataModelProperty.cs diff --git a/src/Artemis.Core/Plugins/DataModelExpansions/DataModel.cs b/src/Artemis.Core/Plugins/Modules/DataModel.cs similarity index 96% rename from src/Artemis.Core/Plugins/DataModelExpansions/DataModel.cs rename to src/Artemis.Core/Plugins/Modules/DataModel.cs index 57f2c8b7e..1321b705a 100644 --- a/src/Artemis.Core/Plugins/DataModelExpansions/DataModel.cs +++ b/src/Artemis.Core/Plugins/Modules/DataModel.cs @@ -7,6 +7,7 @@ using System.Reflection; using Artemis.Core.Modules; using Humanizer; using Newtonsoft.Json; +using Module = Artemis.Core.Modules.Module; namespace Artemis.Core.DataModelExpansions { @@ -23,16 +24,16 @@ namespace Artemis.Core.DataModelExpansions protected DataModel() { // These are both set right after construction to keep the constructor of inherited classes clean - Feature = null!; + Module = null!; DataModelDescription = null!; } /// - /// Gets the plugin feature this data model belongs to + /// Gets the module this data model belongs to /// [JsonIgnore] [DataModelIgnore] - public DataModelPluginFeature Feature { get; internal set; } + public Module Module { get; internal set; } /// /// Gets the describing this data model @@ -59,11 +60,9 @@ namespace Artemis.Core.DataModelExpansions /// public ReadOnlyCollection GetHiddenProperties() { - if (Feature is ProfileModule profileModule) - return profileModule.HiddenProperties; - if (Feature is BaseDataModelExpansion dataModelExpansion) - return dataModelExpansion.HiddenProperties; - + if (Module is Module module) + return module.HiddenProperties; + return new List().AsReadOnly(); } @@ -149,7 +148,7 @@ namespace Artemis.Core.DataModelExpansions attribute.Name ??= key.Humanize(); if (initialValue is DataModel dynamicDataModel) { - dynamicDataModel.Feature = Feature; + dynamicDataModel.Module = Module; dynamicDataModel.DataModelDescription = attribute; } diff --git a/src/Artemis.Core/Plugins/DataModelExpansions/DynamicChild.cs b/src/Artemis.Core/Plugins/Modules/DynamicChild.cs similarity index 100% rename from src/Artemis.Core/Plugins/DataModelExpansions/DynamicChild.cs rename to src/Artemis.Core/Plugins/Modules/DynamicChild.cs diff --git a/src/Artemis.Core/Plugins/Modules/Module.cs b/src/Artemis.Core/Plugins/Modules/Module.cs index 8224cc826..3aa7816e1 100644 --- a/src/Artemis.Core/Plugins/Modules/Module.cs +++ b/src/Artemis.Core/Plugins/Modules/Module.cs @@ -1,21 +1,22 @@ using System; using System.Collections.Generic; -using System.Diagnostics; +using System.Collections.ObjectModel; +using System.IO; using System.Linq; +using System.Linq.Expressions; +using System.Reflection; using Artemis.Core.DataModelExpansions; -using Artemis.Storage.Entities.Module; -using SkiaSharp; namespace Artemis.Core.Modules { /// - /// Allows you to add support for new games/applications while utilizing your own data model + /// Allows you to add new data to the Artemis data model /// public abstract class Module : Module where T : DataModel { /// /// The data model driving this module - /// Note: This default data model is automatically registered upon plugin enable + /// Note: This default data model is automatically registered and instantiated upon plugin enable /// public T DataModel { @@ -24,43 +25,61 @@ namespace Artemis.Core.Modules } /// - /// Gets or sets whether this module must also expand the main data model - /// - /// Note: If expanding the main data model is all you want your plugin to do, create a - /// plugin instead. - /// + /// Hide the provided property using a lambda expression, e.g. HideProperty(dm => dm.TimeDataModel.CurrentTimeUTC) /// - public bool ExpandsDataModel + /// A lambda expression pointing to the property to ignore + public void HideProperty(Expression> propertyLambda) { - get => InternalExpandsMainDataModel; - set => InternalExpandsMainDataModel = value; + PropertyInfo propertyInfo = ReflectionUtilities.GetPropertyInfo(DataModel, propertyLambda); + if (!HiddenPropertiesList.Any(p => p.Equals(propertyInfo))) + HiddenPropertiesList.Add(propertyInfo); } /// - /// Override to provide your own data model description. By default this returns a description matching your plugin - /// name and description + /// Stop hiding the provided property using a lambda expression, e.g. ShowProperty(dm => + /// dm.TimeDataModel.CurrentTimeUTC) /// - /// - public virtual DataModelPropertyAttribute GetDataModelDescription() + /// A lambda expression pointing to the property to stop ignoring + public void ShowProperty(Expression> propertyLambda) { - return new() {Name = Plugin.Info.Name, Description = Plugin.Info.Description}; + PropertyInfo propertyInfo = ReflectionUtilities.GetPropertyInfo(DataModel, propertyLambda); + HiddenPropertiesList.RemoveAll(p => p.Equals(propertyInfo)); } internal override void InternalEnable() { DataModel = Activator.CreateInstance(); - DataModel.Feature = this; + DataModel.Module = this; DataModel.DataModelDescription = GetDataModelDescription(); base.InternalEnable(); } + + internal override void InternalDisable() + { + Deactivate(true); + base.InternalDisable(); + } } /// - /// Allows you to add support for new games/applications + /// For internal use only, please use . /// - public abstract class Module : DataModelPluginFeature + public abstract class Module : PluginFeature { + private readonly List<(DefaultCategoryName, string)> _pendingDefaultProfilePaths = new(); + private readonly List<(DefaultCategoryName, string)> _defaultProfilePaths = new(); + + /// + /// Gets a list of all properties ignored at runtime using IgnoreProperty(x => x.y) + /// + protected internal readonly List HiddenPropertiesList = new(); + + /// + /// Gets a read only collection of default profile paths + /// + public IReadOnlyCollection<(DefaultCategoryName, string)> DefaultProfilePaths => _defaultProfilePaths.AsReadOnly(); + /// /// The modules display name that's shown in the menu /// @@ -107,35 +126,23 @@ namespace Artemis.Core.Modules public ActivationRequirementType ActivationRequirementMode { get; set; } = ActivationRequirementType.Any; /// - /// Gets or sets the default priority category for this module, defaults to - /// + /// Gets or sets a boolean indicating whether this module is always available to profiles or only when profiles + /// specifically target this module. + /// Note: If set to , are not evaluated. /// - public ModulePriorityCategory DefaultPriorityCategory { get; set; } = ModulePriorityCategory.Normal; - - /// - /// Gets the current priority category of this module - /// - public ModulePriorityCategory PriorityCategory { get; internal set; } - - /// - /// Gets the current priority of this module within its priority category - /// - public int Priority { get; internal set; } - - /// - /// A list of custom module tabs that show in the UI - /// - public IEnumerable? ModuleTabs { get; protected set; } + public bool IsAlwaysAvailable { get; set; } /// /// Gets whether updating this module is currently allowed /// public bool IsUpdateAllowed => IsActivated && (UpdateDuringActivationOverride || !IsActivatedOverride); - internal DataModel? InternalDataModel { get; set; } + /// + /// Gets a list of all properties ignored at runtime using IgnoreProperty(x => x.y) + /// + public ReadOnlyCollection HiddenProperties => HiddenPropertiesList.AsReadOnly(); - internal bool InternalExpandsMainDataModel { get; set; } - internal ModuleSettingsEntity? SettingsEntity { get; set; } + internal DataModel? InternalDataModel { get; set; } /// /// Called each frame when the module should update @@ -143,14 +150,6 @@ namespace Artemis.Core.Modules /// Time in seconds since the last update public abstract void Update(double deltaTime); - /// - /// Called each frame when the module should render - /// - /// Time since the last render - /// - /// - public abstract void Render(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo); - /// /// Called when the are met or during an override /// @@ -158,7 +157,9 @@ namespace Artemis.Core.Modules /// If true, the activation was due to an override. This usually means the module was activated /// by the profile editor /// - public abstract void ModuleActivated(bool isOverride); + public virtual void ModuleActivated(bool isOverride) + { + } /// /// Called when the are no longer met or during an override @@ -167,7 +168,9 @@ namespace Artemis.Core.Modules /// If true, the deactivation was due to an override. This usually means the module was deactivated /// by the profile editor /// - public abstract void ModuleDeactivated(bool isOverride); + public virtual void ModuleDeactivated(bool isOverride) + { + } /// /// Evaluates the activation requirements following the and returns the result @@ -185,6 +188,50 @@ namespace Artemis.Core.Modules return false; } + /// + /// Override to provide your own data model description. By default this returns a description matching your plugin + /// name and description + /// + /// + public virtual DataModelPropertyAttribute GetDataModelDescription() + { + return new() {Name = Plugin.Info.Name, Description = Plugin.Info.Description}; + } + + /// + /// Adds a default profile by reading it from the file found at the provided path + /// + /// The category in which to place the default profile + /// A path pointing towards a profile file. May be relative to the plugin directory. + /// + /// if the default profile was added; if it was not because it is + /// already in the list. + /// + protected bool AddDefaultProfile(DefaultCategoryName category, string file) + { + // It can be null if the plugin has not loaded yet in which case Plugin.ResolveRelativePath fails + if (Plugin == null!) + { + if (_pendingDefaultProfilePaths.Contains((category, file))) + return false; + _pendingDefaultProfilePaths.Add((category, file)); + return true; + } + + if (!Path.IsPathRooted(file)) + file = Plugin.ResolveRelativePath(file); + + // Ensure the file exists + if (!File.Exists(file)) + throw new ArtemisPluginFeatureException(this, $"Could not find default profile at {file}."); + + if (_defaultProfilePaths.Contains((category, file))) + return false; + _defaultProfilePaths.Add((category, file)); + + return true; + } + internal virtual void InternalUpdate(double deltaTime) { StartUpdateMeasure(); @@ -193,13 +240,6 @@ namespace Artemis.Core.Modules StopUpdateMeasure(); } - internal virtual void InternalRender(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo) - { - StartRenderMeasure(); - Render(deltaTime, canvas, canvasInfo); - StopRenderMeasure(); - } - internal virtual void Activate(bool isOverride) { if (IsActivated) @@ -220,6 +260,20 @@ namespace Artemis.Core.Modules ModuleDeactivated(isOverride); } + #region Overrides of PluginFeature + + /// + internal override void InternalEnable() + { + foreach ((DefaultCategoryName categoryName, var path) in _pendingDefaultProfilePaths) + AddDefaultProfile(categoryName, path); + _pendingDefaultProfilePaths.Clear(); + + base.InternalEnable(); + } + + #endregion + internal virtual void Reactivate(bool isDeactivateOverride, bool isActivateOverride) { if (!IsActivated) @@ -228,16 +282,6 @@ namespace Artemis.Core.Modules Deactivate(isDeactivateOverride); Activate(isActivateOverride); } - - internal void ApplyToEntity() - { - if (SettingsEntity == null) - SettingsEntity = new ModuleSettingsEntity(); - - SettingsEntity.ModuleId = Id; - SettingsEntity.PriorityCategory = (int) PriorityCategory; - SettingsEntity.Priority = Priority; - } } /// @@ -255,25 +299,4 @@ namespace Artemis.Core.Modules /// All } - - /// - /// Describes the priority category of a module - /// - public enum ModulePriorityCategory - { - /// - /// Indicates a normal render priority - /// - Normal, - - /// - /// Indicates that the module renders for a specific application/game, rendering on top of normal modules - /// - Application, - - /// - /// Indicates that the module renders an overlay, always rendering on top - /// - Overlay - } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Modules/ModuleTab.cs b/src/Artemis.Core/Plugins/Modules/ModuleTab.cs deleted file mode 100644 index 01c11411e..000000000 --- a/src/Artemis.Core/Plugins/Modules/ModuleTab.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; - -namespace Artemis.Core.Modules -{ - /// - public class ModuleTab : ModuleTab where T : IModuleViewModel - { - /// - /// Creates a new instance of the class - /// - /// The title of the tab - public ModuleTab(string title) : base(title) - { - } - - /// - public override Type Type => typeof(T); - } - - /// - /// Describes a UI tab for a specific module - /// - public abstract class ModuleTab - { - /// - /// Creates a new instance of the class - /// - /// The title of the tab - protected ModuleTab(string title) - { - Title = title; - } - - /// - /// The title of the tab - /// - public string Title { get; protected set; } - - /// - /// The type of view model the tab contains - /// - public abstract Type Type { get; } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Modules/ProfileModule.cs b/src/Artemis.Core/Plugins/Modules/ProfileModule.cs deleted file mode 100644 index 081959517..000000000 --- a/src/Artemis.Core/Plugins/Modules/ProfileModule.cs +++ /dev/null @@ -1,329 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.IO; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Threading.Tasks; -using Artemis.Core.DataModelExpansions; -using Artemis.Core.Services; -using Artemis.Storage.Entities.Profile; -using Newtonsoft.Json; -using SkiaSharp; - -namespace Artemis.Core.Modules -{ - /// - /// Allows you to add support for new games/applications while utilizing Artemis' profile engine and your own data - /// model - /// - public abstract class ProfileModule : ProfileModule where T : DataModel - { - /// - /// The data model driving this module - /// Note: This default data model is automatically registered upon plugin enable - /// - public T DataModel - { - get => InternalDataModel as T ?? throw new InvalidOperationException("Internal datamodel does not match the type of the data model"); - internal set => InternalDataModel = value; - } - - /// - /// Gets or sets whether this module must also expand the main data model - /// - /// Note: If expanding the main data model is all you want your plugin to do, create a - /// plugin instead. - /// - /// - public bool ExpandsDataModel - { - get => InternalExpandsMainDataModel; - set => InternalExpandsMainDataModel = value; - } - - /// - /// Override to provide your own data model description. By default this returns a description matching your plugin - /// name and description - /// - /// - public virtual DataModelPropertyAttribute GetDataModelDescription() - { - return new() {Name = Plugin.Info.Name, Description = Plugin.Info.Description}; - } - - /// - /// Hide the provided property using a lambda expression, e.g. HideProperty(dm => dm.TimeDataModel.CurrentTimeUTC) - /// - /// A lambda expression pointing to the property to ignore - public void HideProperty(Expression> propertyLambda) - { - PropertyInfo propertyInfo = ReflectionUtilities.GetPropertyInfo(DataModel, propertyLambda); - if (!HiddenPropertiesList.Any(p => p.Equals(propertyInfo))) - HiddenPropertiesList.Add(propertyInfo); - } - - /// - /// Stop hiding the provided property using a lambda expression, e.g. ShowProperty(dm => - /// dm.TimeDataModel.CurrentTimeUTC) - /// - /// A lambda expression pointing to the property to stop ignoring - public void ShowProperty(Expression> propertyLambda) - { - PropertyInfo propertyInfo = ReflectionUtilities.GetPropertyInfo(DataModel, propertyLambda); - HiddenPropertiesList.RemoveAll(p => p.Equals(propertyInfo)); - } - - internal override void InternalEnable() - { - DataModel = Activator.CreateInstance(); - DataModel.Feature = this; - DataModel.DataModelDescription = GetDataModelDescription(); - base.InternalEnable(); - } - - internal override void InternalDisable() - { - Deactivate(true); - base.InternalDisable(); - } - } - - /// - /// Allows you to add support for new games/applications while utilizing Artemis' profile engine - /// - public abstract class ProfileModule : Module - { - private readonly List _defaultProfilePaths = new(); - private readonly List _pendingDefaultProfilePaths = new(); - private readonly List _defaultProfiles = new(); - private readonly object _lock = new(); - - /// - /// Gets a list of all properties ignored at runtime using IgnoreProperty(x => x.y) - /// - protected internal readonly List HiddenPropertiesList = new(); - - - /// - /// Creates a new instance of the class - /// - protected ProfileModule() - { - OpacityOverride = 1; - } - - /// - /// Gets a list of all properties ignored at runtime using IgnoreProperty(x => x.y) - /// - public ReadOnlyCollection HiddenProperties => HiddenPropertiesList.AsReadOnly(); - - /// - /// Gets the currently active profile - /// - public Profile? ActiveProfile { get; private set; } - - /// - /// Disables updating the profile, rendering does continue - /// - public bool IsProfileUpdatingDisabled { get; set; } - - /// - /// Overrides the opacity of the root folder - /// - public double OpacityOverride { get; set; } - - /// - /// Indicates whether or not a profile change is being animated - /// - public bool AnimatingProfileChange { get; private set; } - - /// - /// Gets a list of default profiles, to add a new default profile use - /// - internal ReadOnlyCollection DefaultProfiles => _defaultProfiles.AsReadOnly(); - - /// - /// Called after the profile has updated - /// - /// Time in seconds since the last update - public virtual void ProfileUpdated(double deltaTime) - { - } - - /// - /// Called after the profile has rendered - /// - /// Time since the last render - /// - /// - public virtual void ProfileRendered(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo) - { - } - - /// - /// Occurs when the has changed - /// - public event EventHandler? ActiveProfileChanged; - - /// - /// Adds a default profile by reading it from the file found at the provided path - /// - /// A path pointing towards a profile file. May be relative to the plugin directory. - /// - /// if the default profile was added; if it was not because it is - /// already in the list. - /// - protected bool AddDefaultProfile(string file) - { - // It can be null if the plugin has not loaded yet... - if (Plugin == null!) - { - if (_pendingDefaultProfilePaths.Contains(file)) - return false; - _pendingDefaultProfilePaths.Add(file); - return true; - } - - if (!Path.IsPathRooted(file)) - file = Plugin.ResolveRelativePath(file); - - if (_defaultProfilePaths.Contains(file)) - return false; - _defaultProfilePaths.Add(file); - - // Ensure the file exists - if (!File.Exists(file)) - throw new ArtemisPluginFeatureException(this, $"Could not find default profile at {file}."); - // Deserialize and make sure that succeeded - ProfileEntity? profileEntity = JsonConvert.DeserializeObject(File.ReadAllText(file), ProfileService.ExportSettings); - if (profileEntity == null) - throw new ArtemisPluginFeatureException(this, $"Failed to deserialize default profile at {file}."); - // Ensure the profile ID is unique - if (_defaultProfiles.Any(d => d.Id == profileEntity.Id)) - throw new ArtemisPluginFeatureException(this, $"Cannot add default profile from {file}, profile ID {profileEntity.Id} already in use."); - - profileEntity.IsFreshImport = true; - profileEntity.IsActive = false; - _defaultProfiles.Add(profileEntity); - - return true; - } - - /// - /// Invokes the event - /// - protected virtual void OnActiveProfileChanged() - { - ActiveProfileChanged?.Invoke(this, EventArgs.Empty); - } - - internal override void InternalEnable() - { - foreach (string pendingDefaultProfile in _pendingDefaultProfilePaths) - AddDefaultProfile(pendingDefaultProfile); - _pendingDefaultProfilePaths.Clear(); - - base.InternalEnable(); - } - - internal override void InternalUpdate(double deltaTime) - { - StartUpdateMeasure(); - if (IsUpdateAllowed) - Update(deltaTime); - - lock (_lock) - { - OpacityOverride = AnimatingProfileChange - ? Math.Max(0, OpacityOverride - 0.1) - : Math.Min(1, OpacityOverride + 0.1); - - // Update the profile - if (!IsProfileUpdatingDisabled) - ActiveProfile?.Update(deltaTime); - } - - ProfileUpdated(deltaTime); - StopUpdateMeasure(); - } - - internal override void InternalRender(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo) - { - StartRenderMeasure(); - Render(deltaTime, canvas, canvasInfo); - - lock (_lock) - { - // Render the profile - ActiveProfile?.Render(canvas, SKPointI.Empty); - } - - ProfileRendered(deltaTime, canvas, canvasInfo); - StopRenderMeasure(); - } - - internal async Task ChangeActiveProfileAnimated(Profile? profile, IEnumerable devices) - { - if (profile != null && profile.Module != this) - throw new ArtemisCoreException($"Cannot activate a profile of module {profile.Module} on a module of plugin {this}."); - if (!IsActivated) - throw new ArtemisCoreException("Cannot activate a profile on a deactivated module"); - - if (profile == ActiveProfile || AnimatingProfileChange) - return; - - AnimatingProfileChange = true; - - while (OpacityOverride > 0) - await Task.Delay(50); - - ChangeActiveProfile(profile, devices); - AnimatingProfileChange = false; - - while (OpacityOverride < 1) - await Task.Delay(50); - } - - internal void ChangeActiveProfile(Profile? profile, IEnumerable devices) - { - if (profile != null && profile.Module != this) - throw new ArtemisCoreException($"Cannot activate a profile of module {profile.Module} on a module of plugin {this}."); - if (!IsActivated) - throw new ArtemisCoreException("Cannot activate a profile on a deactivated module"); - - lock (_lock) - { - if (profile == ActiveProfile) - return; - - ActiveProfile?.Dispose(); - - ActiveProfile = profile; - ActiveProfile?.Activate(devices); - } - - OnActiveProfileChanged(); - } - - internal override void Deactivate(bool isOverride) - { - base.Deactivate(isOverride); - - Profile? profile = ActiveProfile; - ActiveProfile = null; - profile?.Dispose(); - } - - internal override void Reactivate(bool isDeactivateOverride, bool isActivateOverride) - { - if (!IsActivated) - return; - - // Avoid disposing the profile - base.Deactivate(isDeactivateOverride); - Activate(isActivateOverride); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs index 507abacfa..390e7419e 100644 --- a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs +++ b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs @@ -35,14 +35,10 @@ namespace Artemis.Core AlwaysEnabled = attribute?.AlwaysEnabled ?? false; if (Icon != null) return; - if (typeof(BaseDataModelExpansion).IsAssignableFrom(featureType)) - Icon = "TableAdd"; - else if (typeof(DeviceProvider).IsAssignableFrom(featureType)) + if (typeof(DeviceProvider).IsAssignableFrom(featureType)) Icon = "Devices"; - else if (typeof(ProfileModule).IsAssignableFrom(featureType)) - Icon = "VectorRectangle"; else if (typeof(Module).IsAssignableFrom(featureType)) - Icon = "GearBox"; + Icon = "VectorRectangle"; else if (typeof(LayerBrushProvider).IsAssignableFrom(featureType)) Icon = "Brush"; else if (typeof(LayerEffectProvider).IsAssignableFrom(featureType)) @@ -66,10 +62,8 @@ namespace Artemis.Core if (Icon != null) return; Icon = Instance switch { - BaseDataModelExpansion => "TableAdd", DeviceProvider => "Devices", - ProfileModule => "VectorRectangle", - Module => "GearBox", + Module => "VectorRectangle", LayerBrushProvider => "Brush", LayerEffectProvider => "AutoAwesome", _ => "Plugin" diff --git a/src/Artemis.Core/Services/CoreService.cs b/src/Artemis.Core/Services/CoreService.cs index f907bd0e8..b07ba5861 100644 --- a/src/Artemis.Core/Services/CoreService.cs +++ b/src/Artemis.Core/Services/CoreService.cs @@ -29,11 +29,10 @@ namespace Artemis.Core.Services private readonly PluginSetting _loggingLevel; private readonly IPluginManagementService _pluginManagementService; private readonly IProfileService _profileService; + private readonly IModuleService _moduleService; private readonly IRgbService _rgbService; private readonly List _updateExceptions = new(); - private List _dataModelExpansions = new(); private DateTime _lastExceptionLog; - private List _modules = new(); // ReSharper disable UnusedParameter.Local public CoreService(IKernel kernel, @@ -43,8 +42,7 @@ namespace Artemis.Core.Services IPluginManagementService pluginManagementService, IRgbService rgbService, IProfileService profileService, - IModuleService moduleService // injected to ensure module priorities get applied - ) + IModuleService moduleService) { Kernel = kernel; Constants.CorePlugin.Kernel = kernel; @@ -53,18 +51,14 @@ namespace Artemis.Core.Services _pluginManagementService = pluginManagementService; _rgbService = rgbService; _profileService = profileService; + _moduleService = moduleService; _loggingLevel = settingsService.GetSetting("Core.LoggingLevel", LogEventLevel.Debug); _frameStopWatch = new Stopwatch(); StartupArguments = new List(); - - UpdatePluginCache(); - + _rgbService.IsRenderPaused = true; _rgbService.Surface.Updating += SurfaceOnUpdating; _loggingLevel.SettingChanged += (sender, args) => ApplyLoggingLevel(); - - _pluginManagementService.PluginFeatureEnabled += (sender, args) => UpdatePluginCache(); - _pluginManagementService.PluginFeatureDisabled += (sender, args) => UpdatePluginCache(); } // ReSharper restore UnusedParameter.Local @@ -79,12 +73,6 @@ namespace Artemis.Core.Services FrameRendered?.Invoke(this, e); } - private void UpdatePluginCache() - { - _modules = _pluginManagementService.GetFeaturesOfType().Where(p => p.IsEnabled).ToList(); - _dataModelExpansions = _pluginManagementService.GetFeaturesOfType().Where(p => p.IsEnabled).ToList(); - } - private void ApplyLoggingLevel() { string? argument = StartupArguments.FirstOrDefault(a => a.StartsWith("--logging")); @@ -119,68 +107,19 @@ namespace Artemis.Core.Services try { _frameStopWatch.Restart(); - - // Render all active modules + + _moduleService.UpdateActiveModules(args.DeltaTime); SKTexture texture = _rgbService.OpenRender(); - - lock (_dataModelExpansions) - { - // Update all active modules, check Enabled status because it may go false before before the _dataModelExpansions list is updated - foreach (BaseDataModelExpansion dataModelExpansion in _dataModelExpansions.Where(e => e.IsEnabled)) - { - try - { - dataModelExpansion.InternalUpdate(args.DeltaTime); - } - catch (Exception e) - { - _updateExceptions.Add(e); - } - } - } - - List modules; - lock (_modules) - { - modules = _modules.Where(m => m.IsActivated || m.InternalExpandsMainDataModel) - .OrderBy(m => m.PriorityCategory) - .ThenByDescending(m => m.Priority) - .ToList(); - } - - // Update all active modules - foreach (Module module in modules) - { - try - { - module.InternalUpdate(args.DeltaTime); - } - catch (Exception e) - { - _updateExceptions.Add(e); - } - } - SKCanvas canvas = texture.Surface.Canvas; canvas.Save(); if (Math.Abs(texture.RenderScale - 1) > 0.001) canvas.Scale(texture.RenderScale); canvas.Clear(new SKColor(0, 0, 0)); - // While non-activated modules may be updated above if they expand the main data model, they may never render - if (!ModuleRenderingDisabled) + if (!ProfileRenderingDisabled) { - foreach (Module module in modules.Where(m => m.IsActivated)) - { - try - { - module.InternalRender(args.DeltaTime, canvas, texture.ImageInfo); - } - catch (Exception e) - { - _updateExceptions.Add(e); - } - } + _profileService.UpdateProfiles(args.DeltaTime); + _profileService.RenderProfiles(canvas); } OnFrameRendering(new FrameRenderingEventArgs(canvas, args.DeltaTime, _rgbService.Surface)); @@ -228,7 +167,7 @@ namespace Artemis.Core.Services } public TimeSpan FrameTime { get; private set; } - public bool ModuleRenderingDisabled { get; set; } + public bool ProfileRenderingDisabled { get; set; } public List StartupArguments { get; set; } public bool IsElevated { get; set; } @@ -272,25 +211,6 @@ namespace Artemis.Core.Services OnInitialized(); } - public void PlayIntroAnimation() - { - IntroAnimation intro = new(_logger, _profileService, _rgbService.EnabledDevices); - - // Draw a white overlay over the device - void DrawOverlay(object? sender, FrameRenderingEventArgs args) - { - if (intro.AnimationProfile.GetAllLayers().All(l => l.Timeline.IsFinished)) - { - FrameRendering -= DrawOverlay; - intro.AnimationProfile.Dispose(); - } - - intro.Render(args.DeltaTime, args.Canvas); - } - - FrameRendering += DrawOverlay; - } - public event EventHandler? Initialized; public event EventHandler? FrameRendering; public event EventHandler? FrameRendered; diff --git a/src/Artemis.Core/Services/Interfaces/ICoreService.cs b/src/Artemis.Core/Services/Interfaces/ICoreService.cs index 4e1077c0f..7206216cd 100644 --- a/src/Artemis.Core/Services/Interfaces/ICoreService.cs +++ b/src/Artemis.Core/Services/Interfaces/ICoreService.cs @@ -19,9 +19,9 @@ namespace Artemis.Core.Services TimeSpan FrameTime { get; } /// - /// Gets or sets whether modules are rendered each frame by calling their Render method + /// Gets or sets whether profiles are rendered each frame by calling their Render method /// - bool ModuleRenderingDisabled { get; set; } + bool ProfileRenderingDisabled { get; set; } /// /// Gets or sets a list of startup arguments @@ -38,11 +38,6 @@ namespace Artemis.Core.Services /// void Initialize(); - /// - /// Plays the into animation profile defined in Resources/intro-profile.json - /// - void PlayIntroAnimation(); - /// /// Occurs the core has finished initializing /// diff --git a/src/Artemis.Core/Services/Interfaces/IModuleService.cs b/src/Artemis.Core/Services/Interfaces/IModuleService.cs index d7dd93325..0aaaa1a30 100644 --- a/src/Artemis.Core/Services/Interfaces/IModuleService.cs +++ b/src/Artemis.Core/Services/Interfaces/IModuleService.cs @@ -1,5 +1,5 @@ using System; -using System.Threading.Tasks; +using System.Collections.Generic; using Artemis.Core.Modules; namespace Artemis.Core.Services @@ -10,33 +10,29 @@ namespace Artemis.Core.Services public interface IModuleService : IArtemisService { /// - /// Gets the current active module override. If set, all other modules are deactivated and only the - /// is active. + /// Updates all currently active modules /// - Module? ActiveModuleOverride { get; } - - /// - /// Changes the current and deactivates all other modules - /// - /// - Task SetActiveModuleOverride(Module? overrideModule); + /// + void UpdateActiveModules(double deltaTime); /// /// Evaluates every enabled module's activation requirements and activates/deactivates modules accordingly /// - Task UpdateModuleActivation(); + void UpdateModuleActivation(); /// - /// Updates the priority and priority category of the given module + /// Overrides activation on the provided module and restores regular activation to any remaining modules /// - /// The module to update - /// The new priority category of the module - /// The new priority of the module - void UpdateModulePriority(Module module, ModulePriorityCategory category, int priority); + void SetActivationOverride(Module? module); /// - /// Occurs when the priority of a module is updated. + /// Occurs whenever a module is activated /// - event EventHandler? ModulePriorityUpdated; + event EventHandler ModuleActivated; + + /// + /// Occurs whenever a module is deactivated + /// + event EventHandler ModuleDeactivated; } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/ModuleService.cs b/src/Artemis.Core/Services/ModuleService.cs index 22bc8299c..956e69281 100644 --- a/src/Artemis.Core/Services/ModuleService.cs +++ b/src/Artemis.Core/Services/ModuleService.cs @@ -1,103 +1,48 @@ using System; using System.Collections.Generic; -using System.Diagnostics; +using System.IO; using System.Linq; -using System.Threading; -using System.Threading.Tasks; using System.Timers; using Artemis.Core.Modules; -using Artemis.Storage.Entities.Profile; -using Artemis.Storage.Repositories.Interfaces; +using Newtonsoft.Json; using Serilog; -using Timer = System.Timers.Timer; namespace Artemis.Core.Services { internal class ModuleService : IModuleService { - private static readonly SemaphoreSlim ActiveModuleSemaphore = new(1, 1); + private readonly Timer _activationUpdateTimer; private readonly ILogger _logger; - private readonly IModuleRepository _moduleRepository; - private readonly IProfileRepository _profileRepository; - private readonly IPluginManagementService _pluginManagementService; private readonly IProfileService _profileService; + private readonly List _modules; + private readonly object _updateLock = new(); - public ModuleService(ILogger logger, IModuleRepository moduleRepository, IProfileRepository profileRepository, IPluginManagementService pluginManagementService, IProfileService profileService) + private Module? _activationOverride; + + public ModuleService(ILogger logger, IPluginManagementService pluginManagementService, IProfileService profileService) { _logger = logger; - _moduleRepository = moduleRepository; - _profileRepository = profileRepository; - _pluginManagementService = pluginManagementService; _profileService = profileService; - _pluginManagementService.PluginFeatureEnabled += OnPluginFeatureEnabled; - Timer activationUpdateTimer = new(2000); - activationUpdateTimer.Start(); - activationUpdateTimer.Elapsed += ActivationUpdateTimerOnElapsed; + _activationUpdateTimer = new Timer(2000); + _activationUpdateTimer.Start(); + _activationUpdateTimer.Elapsed += ActivationUpdateTimerOnElapsed; - foreach (Module module in _pluginManagementService.GetFeaturesOfType()) - InitialiseOrApplyPriority(module); + pluginManagementService.PluginFeatureEnabled += PluginManagementServiceOnPluginFeatureEnabled; + pluginManagementService.PluginFeatureDisabled += PluginManagementServiceOnPluginFeatureDisabled; + _modules = pluginManagementService.GetFeaturesOfType().ToList(); + foreach (Module module in _modules) + ImportDefaultProfiles(module); } - private async void ActivationUpdateTimerOnElapsed(object sender, ElapsedEventArgs e) + protected virtual void OnModuleActivated(ModuleEventArgs e) { - await UpdateModuleActivation(); + ModuleActivated?.Invoke(this, e); } - private async Task ActivateModule(Module module) + protected virtual void OnModuleDeactivated(ModuleEventArgs e) { - try - { - ProfileModule? profileModule = module as ProfileModule; - - if (profileModule != null && profileModule.DefaultProfiles.Any()) - { - List descriptors = _profileService.GetProfileDescriptors(profileModule); - foreach (ProfileEntity defaultProfile in profileModule.DefaultProfiles) - { - if (descriptors.All(d => d.Id != defaultProfile.Id)) - _profileRepository.Add(defaultProfile); - } - } - - module.Activate(false); - - try - { - // If this is a profile module, activate the last active profile after module activation - if (profileModule != null) - await _profileService.ActivateLastProfileAnimated(profileModule); - } - catch (Exception e) - { - _logger.Warning(e, $"Failed to activate last profile on module {module}"); - } - } - catch (Exception e) - { - _logger.Error(new ArtemisPluginFeatureException(module, "Failed to activate module.", e), "Failed to activate module"); - throw; - } - } - - private async Task DeactivateModule(Module module) - { - try - { - // If this is a profile module, animate profile disable - // module.Deactivate would do the same but without animation - if (module.IsActivated && module is ProfileModule profileModule) - await profileModule.ChangeActiveProfileAnimated(null, Enumerable.Empty()); - - module.Deactivate(false); - } - catch (Exception e) - { - _logger.Error(new ArtemisPluginFeatureException( - module, "Failed to deactivate module and last profile.", e), "Failed to deactivate module and last profile" - ); - throw; - } + ModuleDeactivated?.Invoke(this, e); } private void OverrideActivate(Module module) @@ -110,21 +55,15 @@ namespace Artemis.Core.Services // If activating while it should be deactivated, its an override bool shouldBeActivated = module.EvaluateActivationRequirements(); module.Activate(!shouldBeActivated); - - // If this is a profile module, activate the last active profile after module activation - if (module is ProfileModule profileModule) - _profileService.ActivateLastProfile(profileModule); } catch (Exception e) { - _logger.Error(new ArtemisPluginFeatureException( - module, "Failed to activate module and last profile.", e), "Failed to activate module and last profile" - ); + _logger.Error(new ArtemisPluginFeatureException(module, "Failed to activate module.", e), "Failed to activate module"); throw; } } - private void OverrideDeactivate(Module module, bool clearingOverride) + private void OverrideDeactivate(Module module) { try { @@ -134,163 +73,132 @@ namespace Artemis.Core.Services // If deactivating while it should be activated, its an override bool shouldBeActivated = module.EvaluateActivationRequirements(); // No need to deactivate if it is not in an overridden state - if (shouldBeActivated && !module.IsActivatedOverride && !clearingOverride) + if (shouldBeActivated && !module.IsActivatedOverride) return; module.Deactivate(true); } catch (Exception e) { - _logger.Error(new ArtemisPluginFeatureException( - module, "Failed to deactivate module and last profile.", e), "Failed to deactivate module and last profile" - ); + _logger.Error(new ArtemisPluginFeatureException(module, "Failed to deactivate module.", e), "Failed to deactivate module"); throw; } } - private void OnPluginFeatureEnabled(object? sender, PluginFeatureEventArgs e) + private void ActivationUpdateTimerOnElapsed(object sender, ElapsedEventArgs e) { - if (e.PluginFeature is Module module) - InitialiseOrApplyPriority(module); + UpdateModuleActivation(); } - private void InitialiseOrApplyPriority(Module module) + private void PluginManagementServiceOnPluginFeatureEnabled(object? sender, PluginFeatureEventArgs e) { - ModulePriorityCategory category = module.DefaultPriorityCategory; - int priority = 1; - - module.SettingsEntity = _moduleRepository.GetByModuleId(module.Id); - if (module.SettingsEntity != null) + lock (_updateLock) { - category = (ModulePriorityCategory) module.SettingsEntity.PriorityCategory; - priority = module.SettingsEntity.Priority; + if (e.PluginFeature is Module module && !_modules.Contains(module)) + { + ImportDefaultProfiles(module); + _modules.Add(module); + } } - - UpdateModulePriority(module, category, priority); } - public Module? ActiveModuleOverride { get; private set; } + private void PluginManagementServiceOnPluginFeatureDisabled(object? sender, PluginFeatureEventArgs e) + { + lock (_updateLock) + { + if (e.PluginFeature is Module module) + _modules.Remove(module); + } + } - public async Task SetActiveModuleOverride(Module? overrideModule) + private void ImportDefaultProfiles(Module module) { try { - await ActiveModuleSemaphore.WaitAsync(); - - if (ActiveModuleOverride == overrideModule) - return; - - if (overrideModule != null) + List profileConfigurations = _profileService.ProfileCategories.SelectMany(c => c.ProfileConfigurations).ToList(); + foreach ((DefaultCategoryName categoryName, string profilePath) in module.DefaultProfilePaths) { - OverrideActivate(overrideModule); - _logger.Information($"Setting active module override to {overrideModule.DisplayName}"); - } - else - { - _logger.Information("Clearing active module override"); - } + ProfileConfigurationExportModel? profileConfigurationExportModel = JsonConvert.DeserializeObject(File.ReadAllText(profilePath), IProfileService.ExportSettings); + if (profileConfigurationExportModel?.ProfileEntity == null) + throw new ArtemisCoreException($"Default profile at path {profilePath} contains no valid profile data"); + if (profileConfigurations.Any(p => p.Entity.ProfileId == profileConfigurationExportModel.ProfileEntity.Id)) + continue; - // Always deactivate all other modules whenever override is called - List modules = _pluginManagementService.GetFeaturesOfType().ToList(); - foreach (Module module in modules.Where(m => m != overrideModule)) - OverrideDeactivate(module, overrideModule != null); + ProfileCategory category = _profileService.ProfileCategories.FirstOrDefault(c => c.Name == categoryName.ToString()) ?? + _profileService.CreateProfileCategory(categoryName.ToString()); - ActiveModuleOverride = overrideModule; + _profileService.ImportProfile(category, profileConfigurationExportModel, false, true, null); + } } - finally + catch (Exception e) { - ActiveModuleSemaphore.Release(); + _logger.Warning(e, "Failed to import default profiles for module {module}", module); } } - public async Task UpdateModuleActivation() + public void UpdateModuleActivation() { - if (ActiveModuleSemaphore.CurrentCount == 0) - return; - - try + lock (_updateLock) { - await ActiveModuleSemaphore.WaitAsync(); - - if (ActiveModuleOverride != null) + try { - // The conditions of the active module override may be matched, in that case reactivate as a non-override - // the principle is different for this service but not for the module - bool shouldBeActivated = ActiveModuleOverride.EvaluateActivationRequirements(); - if (shouldBeActivated && ActiveModuleOverride.IsActivatedOverride) - ActiveModuleOverride.Reactivate(true, false); - else if (!shouldBeActivated && !ActiveModuleOverride.IsActivatedOverride) ActiveModuleOverride.Reactivate(false, true); - - return; - } - - Stopwatch stopwatch = new(); - stopwatch.Start(); - - List modules = _pluginManagementService.GetFeaturesOfType().ToList(); - List tasks = new(); - foreach (Module module in modules) - { - lock (module) + _activationUpdateTimer.Elapsed -= ActivationUpdateTimerOnElapsed; + foreach (Module module in _modules) { - bool shouldBeActivated = module.EvaluateActivationRequirements() && module.IsEnabled; + if (module.IsActivatedOverride) + continue; + + if (module.IsAlwaysAvailable) + { + module.Activate(false); + continue; + } + + module.Profiler.StartMeasurement("EvaluateActivationRequirements"); + bool shouldBeActivated = module.IsEnabled && module.EvaluateActivationRequirements(); + module.Profiler.StopMeasurement("EvaluateActivationRequirements"); + if (shouldBeActivated && !module.IsActivated) - tasks.Add(ActivateModule(module)); + { + module.Activate(false); + OnModuleActivated(new ModuleEventArgs(module)); + } else if (!shouldBeActivated && module.IsActivated) - tasks.Add(DeactivateModule(module)); + { + module.Deactivate(false); + OnModuleDeactivated(new ModuleEventArgs(module)); + } } } - - await Task.WhenAll(tasks); - - stopwatch.Stop(); - if (stopwatch.ElapsedMilliseconds > 100 && !tasks.Any()) - _logger.Warning("Activation requirements evaluation took too long: {moduleCount} module(s) in {elapsed}", modules.Count, stopwatch.Elapsed); - } - finally - { - ActiveModuleSemaphore.Release(); - } - } - - public void UpdateModulePriority(Module module, ModulePriorityCategory category, int priority) - { - if (module.PriorityCategory == category && module.Priority == priority) - return; - - List modules = _pluginManagementService - .GetFeaturesOfType() - .Where(m => m.PriorityCategory == category) - .OrderBy(m => m.Priority) - .ToList(); - - if (modules.Contains(module)) - modules.Remove(module); - - priority = Math.Min(modules.Count, Math.Max(0, priority)); - modules.Insert(priority, module); - - module.PriorityCategory = category; - for (int index = 0; index < modules.Count; index++) - { - Module categoryModule = modules[index]; - categoryModule.Priority = index; - - // Don't save modules whose priority hasn't been initialized yet - if (categoryModule == module || categoryModule.SettingsEntity != null) + finally { - categoryModule.ApplyToEntity(); - _moduleRepository.Save(categoryModule.SettingsEntity); + _activationUpdateTimer.Elapsed += ActivationUpdateTimerOnElapsed; } } - - ModulePriorityUpdated?.Invoke(this, EventArgs.Empty); } - #region Events + public void SetActivationOverride(Module? module) + { + lock (_updateLock) + { + if (_activationOverride != null) + OverrideDeactivate(_activationOverride); + _activationOverride = module; + if (_activationOverride != null) + OverrideActivate(_activationOverride); + } + } - public event EventHandler? ModulePriorityUpdated; + public void UpdateActiveModules(double deltaTime) + { + lock (_updateLock) + { + foreach (Module module in _modules) + module.InternalUpdate(deltaTime); + } + } - #endregion + public event EventHandler? ModuleActivated; + public event EventHandler? ModuleDeactivated; } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/Registration/DataModelService.cs b/src/Artemis.Core/Services/Registration/DataModelService.cs index dc4fa5787..90e1c4739 100644 --- a/src/Artemis.Core/Services/Registration/DataModelService.cs +++ b/src/Artemis.Core/Services/Registration/DataModelService.cs @@ -13,8 +13,6 @@ namespace Artemis.Core.Services // Add data models of already loaded plugins foreach (Module module in pluginManagementService.GetFeaturesOfType().Where(p => p.IsEnabled)) AddModuleDataModel(module); - foreach (BaseDataModelExpansion dataModelExpansion in pluginManagementService.GetFeaturesOfType().Where(p => p.IsEnabled)) - AddDataModelExpansionDataModel(dataModelExpansion); // Add data models of new plugins when they get enabled pluginManagementService.PluginFeatureEnabled += OnPluginFeatureEnabled; @@ -54,8 +52,6 @@ namespace Artemis.Core.Services { if (e.PluginFeature is Module module) AddModuleDataModel(module); - else if (e.PluginFeature is BaseDataModelExpansion dataModelExpansion) - AddDataModelExpansionDataModel(dataModelExpansion); } private void AddModuleDataModel(Module module) @@ -66,19 +62,8 @@ namespace Artemis.Core.Services if (module.InternalDataModel.DataModelDescription == null) throw new ArtemisPluginFeatureException(module, "Module overrides GetDataModelDescription but returned null"); - module.InternalDataModel.IsExpansion = module.InternalExpandsMainDataModel; + module.InternalDataModel.IsExpansion = module.IsAlwaysAvailable; RegisterDataModel(module.InternalDataModel); } - - private void AddDataModelExpansionDataModel(BaseDataModelExpansion dataModelExpansion) - { - if (dataModelExpansion.InternalDataModel == null) - throw new ArtemisCoreException("Cannot add data model expansion that is not enabled"); - if (dataModelExpansion.InternalDataModel.DataModelDescription == null) - throw new ArtemisPluginFeatureException(dataModelExpansion, "Data model expansion overrides GetDataModelDescription but returned null"); - - dataModelExpansion.InternalDataModel.IsExpansion = true; - RegisterDataModel(dataModelExpansion.InternalDataModel); - } } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs b/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs index 9a11fbf1c..84fa8ebae 100644 --- a/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs +++ b/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Artemis.Core.Modules; +using System.Collections.ObjectModel; +using Newtonsoft.Json; +using SkiaSharp; namespace Artemis.Core.Services { @@ -10,122 +10,144 @@ namespace Artemis.Core.Services public interface IProfileService : IArtemisService { /// - /// Creates a new profile for the given module and returns a descriptor pointing to it + /// Gets the JSON serializer settings used to create profile mementos /// - /// The profile module to create the profile for - /// The name of the new profile - /// - ProfileDescriptor CreateProfileDescriptor(ProfileModule module, string name); + public static JsonSerializerSettings MementoSettings { get; } = new() {TypeNameHandling = TypeNameHandling.All}; /// - /// Gets a descriptor for each profile stored for the given + /// Gets the JSON serializer settings used to import/export profiles /// - /// The module to return profile descriptors for - /// - List GetProfileDescriptors(ProfileModule module); + public static JsonSerializerSettings ExportSettings { get; } = new() {TypeNameHandling = TypeNameHandling.All, Formatting = Formatting.Indented}; + + /// + /// Gets a read only collection containing all the profile categories + /// + ReadOnlyCollection ProfileCategories { get; } + + /// + /// Gets a read only collection containing all the profile configurations + /// + ReadOnlyCollection ProfileConfigurations { get; } + + /// + /// Gets or sets a boolean indicating whether rendering should only be done for profiles being edited + /// + bool RenderForEditor { get; set; } + + /// + /// Activates the profile of the given with the currently active surface + /// + /// The profile configuration of the profile to activate + Profile ActivateProfile(ProfileConfiguration profileConfiguration); + + /// + /// Deactivates the profile of the given with the currently active surface + /// + /// The profile configuration of the profile to activate + void DeactivateProfile(ProfileConfiguration profileConfiguration); + + /// + /// Permanently deletes the profile of the given + /// + /// The profile configuration of the profile to delete + void DeleteProfile(ProfileConfiguration profileConfiguration); + + /// + /// Saves the provided and it's s but not the + /// s themselves + /// + /// The profile category to update + void SaveProfileCategory(ProfileCategory profileCategory); + + /// + /// Creates a new profile category and saves it to persistent storage + /// + /// The name of the new profile category, must be unique + /// The newly created profile category + ProfileCategory CreateProfileCategory(string name); + + /// + /// Permanently deletes the provided profile category + /// + void DeleteProfileCategory(ProfileCategory profileCategory); + + /// + /// Creates a new profile configuration and adds it to the provided + /// + /// The profile category to add the profile to + /// The name of the new profile configuration + /// The icon of the new profile configuration + /// The newly created profile configuration + ProfileConfiguration CreateProfileConfiguration(ProfileCategory category, string name, string icon); + + /// + /// Removes the provided profile configuration from the + /// + /// + void RemoveProfileConfiguration(ProfileConfiguration profileConfiguration); + + /// + /// Loads the icon of this profile configuration if needed and puts it into ProfileConfiguration.Icon.FileIcon + /// + void LoadProfileConfigurationIcon(ProfileConfiguration profileConfiguration); + + /// + /// Saves the current icon of this profile + /// + void SaveProfileConfigurationIcon(ProfileConfiguration profileConfiguration); /// /// Writes the profile to persistent storage /// /// /// - void UpdateProfile(Profile profile, bool includeChildren); + void SaveProfile(Profile profile, bool includeChildren); /// - /// Disposes and permanently deletes the provided profile - /// - /// The profile to delete - void DeleteProfile(Profile profile); - - /// - /// Permanently deletes the profile described by the provided profile descriptor - /// - /// The descriptor pointing to the profile to delete - void DeleteProfile(ProfileDescriptor profileDescriptor); - - /// - /// Activates the last profile of the given profile module - /// - /// - void ActivateLastProfile(ProfileModule profileModule); - - /// - /// Reloads the currently active profile on the provided profile module - /// - void ReloadProfile(ProfileModule module); - - /// - /// Asynchronously activates the last profile of the given profile module using a fade animation - /// - /// - /// - Task ActivateLastProfileAnimated(ProfileModule profileModule); - - /// - /// Activates the profile described in the given with the currently active surface - /// - /// The descriptor describing the profile to activate - Profile ActivateProfile(ProfileDescriptor profileDescriptor); - - /// - /// Asynchronously activates the profile described in the given with the currently - /// active surface using a fade animation - /// - /// The descriptor describing the profile to activate - Task ActivateProfileAnimated(ProfileDescriptor profileDescriptor); - - /// - /// Clears the active profile on the given - /// - /// The profile module to deactivate the active profile on - void ClearActiveProfile(ProfileModule module); - - /// - /// Asynchronously clears the active profile on the given using a fade animation - /// - /// The profile module to deactivate the active profile on - Task ClearActiveProfileAnimated(ProfileModule module); - - /// - /// Attempts to restore the profile to the state it had before the last call. + /// Attempts to restore the profile to the state it had before the last call. /// /// - bool UndoUpdateProfile(Profile profile); + bool UndoSaveProfile(Profile profile); /// - /// Attempts to restore the profile to the state it had before the last call. + /// Attempts to restore the profile to the state it had before the last call. /// /// - bool RedoUpdateProfile(Profile profile); + bool RedoSaveProfile(Profile profile); /// - /// Prepares the profile for rendering. You should not need to call this, it is exposed for some niche usage in the - /// core + /// Exports the profile described in the given into an export model /// - /// - void InstantiateProfile(Profile profile); + /// The profile configuration of the profile to export + /// The resulting export model + ProfileConfigurationExportModel ExportProfile(ProfileConfiguration profileConfiguration); /// - /// [Placeholder] Exports the profile described in the given in a JSON format + /// Imports the provided base64 encoded GZIPed JSON as a profile configuration /// - /// The descriptor of the profile to export - /// The resulting JSON - string ExportProfile(ProfileDescriptor profileDescriptor); - - /// - /// [Placeholder] Imports the provided base64 encoded GZIPed JSON as a profile for the given - /// - /// - /// The content of the profile as JSON - /// The module to import the profile in to + /// The in which to import the profile + /// The model containing the profile to import + /// Whether or not to give the profile a new GUID, making it unique + /// Whether or not to mark the profile as a fresh import, causing it to be adapted until any changes are made to it /// Text to add after the name of the profile (separated by a dash) - /// - ProfileDescriptor ImportProfile(string json, ProfileModule profileModule, string nameAffix = "imported"); + /// The resulting profile configuration + ProfileConfiguration ImportProfile(ProfileCategory category, ProfileConfigurationExportModel exportModel, bool makeUnique = true, bool markAsFreshImport = true, string? nameAffix = "imported"); /// /// Adapts a given profile to the currently active devices /// /// The profile to adapt void AdaptProfile(Profile profile); + + /// + /// Updates all currently active profiles + /// + void UpdateProfiles(double deltaTime); + + /// + /// Renders all currently active profiles + /// + /// + void RenderProfiles(SKCanvas canvas); } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/Storage/ProfileService.cs b/src/Artemis.Core/Services/Storage/ProfileService.cs index e75eec263..fe887ddf2 100644 --- a/src/Artemis.Core/Services/Storage/ProfileService.cs +++ b/src/Artemis.Core/Services/Storage/ProfileService.cs @@ -1,12 +1,13 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; -using System.Threading.Tasks; using Artemis.Core.Modules; using Artemis.Storage.Entities.Profile; using Artemis.Storage.Repositories.Interfaces; using Newtonsoft.Json; using Serilog; +using SkiaSharp; namespace Artemis.Core.Services { @@ -14,48 +15,38 @@ namespace Artemis.Core.Services { private readonly ILogger _logger; private readonly IPluginManagementService _pluginManagementService; - private readonly IRgbService _rgbService; + private readonly List _profileCategories; + private readonly IProfileCategoryRepository _profileCategoryRepository; private readonly IProfileRepository _profileRepository; + private readonly IRgbService _rgbService; + + private readonly List _updateExceptions = new(); + private DateTime _lastUpdateExceptionLog; + private readonly List _renderExceptions = new(); + private DateTime _lastRenderExceptionLog; public ProfileService(ILogger logger, - IPluginManagementService pluginManagementService, IRgbService rgbService, + // TODO: Move these two IConditionOperatorService conditionOperatorService, IDataBindingService dataBindingService, + IProfileCategoryRepository profileCategoryRepository, + IPluginManagementService pluginManagementService, IProfileRepository profileRepository) { _logger = logger; - _pluginManagementService = pluginManagementService; _rgbService = rgbService; + _profileCategoryRepository = profileCategoryRepository; + _pluginManagementService = pluginManagementService; _profileRepository = profileRepository; + _profileCategories = new List(_profileCategoryRepository.GetAll().Select(c => new ProfileCategory(c)).OrderBy(c => c.Order)); _rgbService.LedsChanged += RgbServiceOnLedsChanged; - } + _pluginManagementService.PluginFeatureEnabled += PluginManagementServiceOnPluginFeatureToggled; + _pluginManagementService.PluginFeatureDisabled += PluginManagementServiceOnPluginFeatureToggled; - public static JsonSerializerSettings MementoSettings { get; set; } = new() {TypeNameHandling = TypeNameHandling.All}; - public static JsonSerializerSettings ExportSettings { get; set; } = new() {TypeNameHandling = TypeNameHandling.All, Formatting = Formatting.Indented}; - - public ProfileDescriptor? GetLastActiveProfile(ProfileModule module) - { - List moduleProfiles = _profileRepository.GetByModuleId(module.Id); - if (!moduleProfiles.Any()) - return CreateProfileDescriptor(module, "Default"); - - ProfileEntity? profileEntity = moduleProfiles.FirstOrDefault(p => p.IsActive) ?? moduleProfiles.FirstOrDefault(); - return profileEntity == null ? null : new ProfileDescriptor(module, profileEntity); - } - - private void SaveActiveProfile(ProfileModule module) - { - if (module.ActiveProfile == null) - return; - - List profileEntities = _profileRepository.GetByModuleId(module.Id); - foreach (ProfileEntity profileEntity in profileEntities) - { - profileEntity.IsActive = module.ActiveProfile.EntityId == profileEntity.Id; - _profileRepository.Save(profileEntity); - } + if (!_profileCategories.Any()) + CreateDefaultProfileCategories(); } /// @@ -63,168 +54,324 @@ namespace Artemis.Core.Services /// private void ActiveProfilesPopulateLeds() { - List profileModules = _pluginManagementService.GetFeaturesOfType(); - foreach (ProfileModule profileModule in profileModules) + foreach (ProfileConfiguration profileConfiguration in ProfileConfigurations) { - // Avoid race condition, make the check here - if (profileModule.ActiveProfile == null) - continue; - - profileModule.ActiveProfile.PopulateLeds(_rgbService.EnabledDevices); - if (profileModule.ActiveProfile.IsFreshImport) + if (profileConfiguration.Profile == null) continue; + profileConfiguration.Profile.PopulateLeds(_rgbService.EnabledDevices); + + if (!profileConfiguration.Profile.IsFreshImport) continue; + _logger.Debug("Profile is a fresh import, adapting to surface - {profile}", profileConfiguration.Profile); + AdaptProfile(profileConfiguration.Profile); + } + } + + private void UpdateModules() + { + lock (_profileRepository) + { + List modules = _pluginManagementService.GetFeaturesOfType(); + foreach (ProfileCategory profileCategory in _profileCategories) + foreach (ProfileConfiguration profileConfiguration in profileCategory.ProfileConfigurations) + profileConfiguration.LoadModules(modules); + } + } + + private void RgbServiceOnLedsChanged(object? sender, EventArgs e) + { + ActiveProfilesPopulateLeds(); + } + + private void PluginManagementServiceOnPluginFeatureToggled(object? sender, PluginFeatureEventArgs e) + { + if (e.PluginFeature is Module) + UpdateModules(); + } + + public bool RenderForEditor { get; set; } + + public void UpdateProfiles(double deltaTime) + { + lock (_profileCategories) + { + // Iterate the children in reverse because the first category must be rendered last to end up on top + for (int i = _profileCategories.Count - 1; i > -1; i--) { - _logger.Debug("Profile is a fresh import, adapting to surface - {profile}", profileModule.ActiveProfile); - AdaptProfile(profileModule.ActiveProfile); + ProfileCategory profileCategory = _profileCategories[i]; + for (int j = profileCategory.ProfileConfigurations.Count - 1; j > -1; j--) + { + ProfileConfiguration profileConfiguration = profileCategory.ProfileConfigurations[j]; + // Profiles being edited are updated at their own leisure + if (profileConfiguration.IsBeingEdited) + continue; + + bool shouldBeActive = profileConfiguration.ShouldBeActive(false); + if (shouldBeActive) + { + profileConfiguration.Update(); + shouldBeActive = profileConfiguration.ActivationConditionMet; + } + + try + { + // Make sure the profile is active or inactive according to the parameters above + if (shouldBeActive && profileConfiguration.Profile == null) + ActivateProfile(profileConfiguration); + else if (!shouldBeActive && profileConfiguration.Profile != null) + DeactivateProfile(profileConfiguration); + + profileConfiguration.Profile?.Update(deltaTime); + } + catch (Exception e) + { + _updateExceptions.Add(e); + } + } + } + + LogProfileUpdateExceptions(); + } + } + + public void RenderProfiles(SKCanvas canvas) + { + lock (_profileCategories) + { + // Iterate the children in reverse because the first category must be rendered last to end up on top + for (int i = _profileCategories.Count - 1; i > -1; i--) + { + ProfileCategory profileCategory = _profileCategories[i]; + for (int j = profileCategory.ProfileConfigurations.Count - 1; j > -1; j--) + { + try + { + ProfileConfiguration profileConfiguration = profileCategory.ProfileConfigurations[j]; + if (RenderForEditor) + { + if (profileConfiguration.IsBeingEdited) + profileConfiguration.Profile?.Render(canvas, SKPointI.Empty); + } + else + { + // Ensure all criteria are met before rendering + if (!profileConfiguration.IsSuspended && !profileConfiguration.IsMissingModule && profileConfiguration.ActivationConditionMet) + profileConfiguration.Profile?.Render(canvas, SKPointI.Empty); + } + } + catch (Exception e) + { + _renderExceptions.Add(e); + } + } + } + + LogProfileRenderExceptions(); + } + } + + private void CreateDefaultProfileCategories() + { + foreach (DefaultCategoryName defaultCategoryName in Enum.GetValues()) + CreateProfileCategory(defaultCategoryName.ToString()); + } + + private void LogProfileUpdateExceptions() + { + // Only log update exceptions every 10 seconds to avoid spamming the logs + if (DateTime.Now - _lastUpdateExceptionLog < TimeSpan.FromSeconds(10)) + return; + _lastUpdateExceptionLog = DateTime.Now; + + if (!_updateExceptions.Any()) + return; + + // Group by stack trace, that should gather up duplicate exceptions + foreach (IGrouping exceptions in _updateExceptions.GroupBy(e => e.StackTrace)) + _logger.Warning(exceptions.First(), "Exception was thrown {count} times during profile update in the last 10 seconds", exceptions.Count()); + + // When logging is finished start with a fresh slate + _updateExceptions.Clear(); + } + + private void LogProfileRenderExceptions() + { + // Only log update exceptions every 10 seconds to avoid spamming the logs + if (DateTime.Now - _lastRenderExceptionLog < TimeSpan.FromSeconds(10)) + return; + _lastRenderExceptionLog = DateTime.Now; + + if (!_renderExceptions.Any()) + return; + + // Group by stack trace, that should gather up duplicate exceptions + foreach (IGrouping exceptions in _renderExceptions.GroupBy(e => e.StackTrace)) + _logger.Warning(exceptions.First(), "Exception was thrown {count} times during profile render in the last 10 seconds", exceptions.Count()); + + // When logging is finished start with a fresh slate + _renderExceptions.Clear(); + } + + public ReadOnlyCollection ProfileCategories + { + get + { + lock (_profileRepository) + { + return _profileCategories.AsReadOnly(); } } } - public List GetProfileDescriptors(ProfileModule module) + public ReadOnlyCollection ProfileConfigurations { - List profileEntities = _profileRepository.GetByModuleId(module.Id); - return profileEntities.Select(e => new ProfileDescriptor(module, e)).ToList(); + get + { + lock (_profileRepository) + { + return _profileCategories.SelectMany(c => c.ProfileConfigurations).ToList().AsReadOnly(); + } + } } - public ProfileDescriptor CreateProfileDescriptor(ProfileModule module, string name) + public void LoadProfileConfigurationIcon(ProfileConfiguration profileConfiguration) { - ProfileEntity profileEntity = new() {Id = Guid.NewGuid(), Name = name, ModuleId = module.Id}; - _profileRepository.Add(profileEntity); + if (profileConfiguration.Icon.IconType == ProfileConfigurationIconType.MaterialIcon) + return; + if (profileConfiguration.Icon.FileIcon != null) + return; - return new ProfileDescriptor(module, profileEntity); + profileConfiguration.Icon.FileIcon = _profileCategoryRepository.GetProfileIconStream(profileConfiguration.Entity.FileIconId); } - public void ActivateLastProfile(ProfileModule profileModule) + public void SaveProfileConfigurationIcon(ProfileConfiguration profileConfiguration) { - ProfileDescriptor? activeProfile = GetLastActiveProfile(profileModule); - if (activeProfile != null) - ActivateProfile(activeProfile); + if (profileConfiguration.Icon.IconType == ProfileConfigurationIconType.MaterialIcon) + return; + + if (profileConfiguration.Icon.FileIcon != null) + { + profileConfiguration.Icon.FileIcon.Position = 0; + _profileCategoryRepository.SaveProfileIconStream(profileConfiguration.Entity, profileConfiguration.Icon.FileIcon); + } } - public async Task ActivateLastProfileAnimated(ProfileModule profileModule) + public Profile ActivateProfile(ProfileConfiguration profileConfiguration) { - ProfileDescriptor? activeProfile = GetLastActiveProfile(profileModule); - if (activeProfile != null) - await ActivateProfileAnimated(activeProfile); - } + if (profileConfiguration.Profile != null) + return profileConfiguration.Profile; - public Profile ActivateProfile(ProfileDescriptor profileDescriptor) - { - if (profileDescriptor.ProfileModule.ActiveProfile?.EntityId == profileDescriptor.Id) - return profileDescriptor.ProfileModule.ActiveProfile; - - ProfileEntity profileEntity = _profileRepository.Get(profileDescriptor.Id); + ProfileEntity profileEntity = _profileRepository.Get(profileConfiguration.Entity.ProfileId); if (profileEntity == null) - throw new ArtemisCoreException($"Cannot find profile named: {profileDescriptor.Name} ID: {profileDescriptor.Id}"); + throw new ArtemisCoreException($"Cannot find profile named: {profileConfiguration.Name} ID: {profileConfiguration.Entity.ProfileId}"); - Profile profile = new(profileDescriptor.ProfileModule, profileEntity); - InstantiateProfile(profile); + Profile profile = new(profileConfiguration, profileEntity); + profile.PopulateLeds(_rgbService.EnabledDevices); - profileDescriptor.ProfileModule.ChangeActiveProfile(profile, _rgbService.EnabledDevices); if (profile.IsFreshImport) { _logger.Debug("Profile is a fresh import, adapting to surface - {profile}", profile); AdaptProfile(profile); } - SaveActiveProfile(profileDescriptor.ProfileModule); - + profileConfiguration.Profile = profile; return profile; } - public void ReloadProfile(ProfileModule module) + public void DeactivateProfile(ProfileConfiguration profileConfiguration) { - if (module.ActiveProfile == null) + if (profileConfiguration.IsBeingEdited) + throw new ArtemisCoreException("Cannot disable a profile that is being edited, that's rude"); + if (profileConfiguration.Profile == null) return; - ProfileEntity entity = _profileRepository.Get(module.ActiveProfile.EntityId); - Profile profile = new(module, entity); - InstantiateProfile(profile); - - module.ChangeActiveProfile(null, _rgbService.EnabledDevices); - module.ChangeActiveProfile(profile, _rgbService.EnabledDevices); + Profile profile = profileConfiguration.Profile; + profileConfiguration.Profile = null; + profile.Dispose(); } - public async Task ActivateProfileAnimated(ProfileDescriptor profileDescriptor) + public void DeleteProfile(ProfileConfiguration profileConfiguration) { - if (profileDescriptor.ProfileModule.ActiveProfile?.EntityId == profileDescriptor.Id) - return profileDescriptor.ProfileModule.ActiveProfile; + DeactivateProfile(profileConfiguration); - ProfileEntity profileEntity = _profileRepository.Get(profileDescriptor.Id); - if (profileEntity == null) - throw new ArtemisCoreException($"Cannot find profile named: {profileDescriptor.Name} ID: {profileDescriptor.Id}"); - - Profile profile = new(profileDescriptor.ProfileModule, profileEntity); - InstantiateProfile(profile); - - void ActivatingRgbServiceOnLedsChanged(object? sender, EventArgs e) - { - profile.PopulateLeds(_rgbService.EnabledDevices); - } - - void ActivatingProfilePluginToggle(object? sender, PluginEventArgs e) - { - if (!profile.Disposed) - InstantiateProfile(profile); - } - - // This could happen during activation so subscribe to it - _pluginManagementService.PluginEnabled += ActivatingProfilePluginToggle; - _pluginManagementService.PluginDisabled += ActivatingProfilePluginToggle; - _rgbService.LedsChanged += ActivatingRgbServiceOnLedsChanged; - - await profileDescriptor.ProfileModule.ChangeActiveProfileAnimated(profile, _rgbService.EnabledDevices); - if (profile.IsFreshImport) - { - _logger.Debug("Profile is a fresh import, adapting to surface - {profile}", profile); - AdaptProfile(profile); - } - - SaveActiveProfile(profileDescriptor.ProfileModule); - - _pluginManagementService.PluginEnabled -= ActivatingProfilePluginToggle; - _pluginManagementService.PluginDisabled -= ActivatingProfilePluginToggle; - _rgbService.LedsChanged -= ActivatingRgbServiceOnLedsChanged; - - return profile; - } - - - public void ClearActiveProfile(ProfileModule module) - { - module.ChangeActiveProfile(null, _rgbService.EnabledDevices); - SaveActiveProfile(module); - } - - public async Task ClearActiveProfileAnimated(ProfileModule module) - { - await module.ChangeActiveProfileAnimated(null, _rgbService.EnabledDevices); - } - - public void DeleteProfile(Profile profile) - { - _logger.Debug("Removing profile " + profile); - - // If the given profile is currently active, disable it first (this also disposes it) - if (profile.Module.ActiveProfile == profile) - ClearActiveProfile(profile.Module); - else - profile.Dispose(); - - _profileRepository.Remove(profile.ProfileEntity); - } - - public void DeleteProfile(ProfileDescriptor profileDescriptor) - { - ProfileEntity profileEntity = _profileRepository.Get(profileDescriptor.Id); + ProfileEntity profileEntity = _profileRepository.Get(profileConfiguration.Entity.ProfileId); if (profileEntity == null) return; + + profileConfiguration.Category.RemoveProfileConfiguration(profileConfiguration); _profileRepository.Remove(profileEntity); + SaveProfileCategory(profileConfiguration.Category); } - public void UpdateProfile(Profile profile, bool includeChildren) + public ProfileCategory CreateProfileCategory(string name) { - string memento = JsonConvert.SerializeObject(profile.ProfileEntity, MementoSettings); + lock (_profileRepository) + { + ProfileCategory profileCategory = new(name); + _profileCategories.Add(profileCategory); + SaveProfileCategory(profileCategory); + return profileCategory; + } + } + public void DeleteProfileCategory(ProfileCategory profileCategory) + { + List profileConfigurations = profileCategory.ProfileConfigurations.ToList(); + foreach (ProfileConfiguration profileConfiguration in profileConfigurations) + RemoveProfileConfiguration(profileConfiguration); + + lock (_profileRepository) + { + _profileCategories.Remove(profileCategory); + _profileCategoryRepository.Remove(profileCategory.Entity); + } + } + + /// + /// Creates a new profile configuration and adds it to the provided + /// + /// The profile category to add the profile to + /// The name of the new profile configuration + /// The icon of the new profile configuration + /// The newly created profile configuration + public ProfileConfiguration CreateProfileConfiguration(ProfileCategory category, string name, string icon) + { + ProfileConfiguration configuration = new(category, name, icon); + ProfileEntity entity = new(); + _profileRepository.Add(entity); + + configuration.Entity.ProfileId = entity.Id; + category.AddProfileConfiguration(configuration, 0); + return configuration; + } + + /// + /// Removes the provided profile configuration from the + /// + /// + public void RemoveProfileConfiguration(ProfileConfiguration profileConfiguration) + { + profileConfiguration.Category.RemoveProfileConfiguration(profileConfiguration); + + DeactivateProfile(profileConfiguration); + SaveProfileCategory(profileConfiguration.Category); + ProfileEntity profileEntity = _profileRepository.Get(profileConfiguration.Entity.ProfileId); + if (profileEntity != null) + _profileRepository.Remove(profileEntity); + } + + public void SaveProfileCategory(ProfileCategory profileCategory) + { + profileCategory.Save(); + _profileCategoryRepository.Save(profileCategory.Entity); + + lock (_profileCategories) + { + _profileCategories.Sort((a, b) => a.Order - b.Order); + } + } + + public void SaveProfile(Profile profile, bool includeChildren) + { + string memento = JsonConvert.SerializeObject(profile.ProfileEntity, IProfileService.MementoSettings); profile.Save(); if (includeChildren) { @@ -235,7 +382,7 @@ namespace Artemis.Core.Services } // If there are no changes, don't bother saving - string updatedMemento = JsonConvert.SerializeObject(profile.ProfileEntity, MementoSettings); + string updatedMemento = JsonConvert.SerializeObject(profile.ProfileEntity, IProfileService.MementoSettings); if (memento.Equals(updatedMemento)) { _logger.Debug("Updating profile - Skipping save, no changes"); @@ -253,7 +400,7 @@ namespace Artemis.Core.Services _profileRepository.Save(profile.ProfileEntity); } - public bool UndoUpdateProfile(Profile profile) + public bool UndoSaveProfile(Profile profile) { // Keep the profile from being rendered by locking it lock (profile) @@ -265,20 +412,20 @@ namespace Artemis.Core.Services } string top = profile.UndoStack.Pop(); - string memento = JsonConvert.SerializeObject(profile.ProfileEntity, MementoSettings); + string memento = JsonConvert.SerializeObject(profile.ProfileEntity, IProfileService.MementoSettings); profile.RedoStack.Push(memento); - profile.ProfileEntity = JsonConvert.DeserializeObject(top, MementoSettings) + profile.ProfileEntity = JsonConvert.DeserializeObject(top, IProfileService.MementoSettings) ?? throw new InvalidOperationException("Failed to deserialize memento"); profile.Load(); - InstantiateProfile(profile); + profile.PopulateLeds(_rgbService.EnabledDevices); } _logger.Debug("Undo profile update - Success"); return true; } - public bool RedoUpdateProfile(Profile profile) + public bool RedoSaveProfile(Profile profile) { // Keep the profile from being rendered by locking it lock (profile) @@ -290,53 +437,81 @@ namespace Artemis.Core.Services } string top = profile.RedoStack.Pop(); - string memento = JsonConvert.SerializeObject(profile.ProfileEntity, MementoSettings); + string memento = JsonConvert.SerializeObject(profile.ProfileEntity, IProfileService.MementoSettings); profile.UndoStack.Push(memento); - profile.ProfileEntity = JsonConvert.DeserializeObject(top, MementoSettings) + profile.ProfileEntity = JsonConvert.DeserializeObject(top, IProfileService.MementoSettings) ?? throw new InvalidOperationException("Failed to deserialize memento"); profile.Load(); - InstantiateProfile(profile); + profile.PopulateLeds(_rgbService.EnabledDevices); _logger.Debug("Redo profile update - Success"); return true; } } - public void InstantiateProfile(Profile profile) + public ProfileConfigurationExportModel ExportProfile(ProfileConfiguration profileConfiguration) { - profile.PopulateLeds(_rgbService.EnabledDevices); + // The profile may not be active and in that case lets activate it real quick + Profile profile = profileConfiguration.Profile ?? ActivateProfile(profileConfiguration); + + return new ProfileConfigurationExportModel + { + ProfileConfigurationEntity = profileConfiguration.Entity, + ProfileEntity = profile.ProfileEntity, + ProfileImage = profileConfiguration.Icon.FileIcon + }; } - public string ExportProfile(ProfileDescriptor profileDescriptor) + public ProfileConfiguration ImportProfile(ProfileCategory category, ProfileConfigurationExportModel exportModel, bool makeUnique, bool markAsFreshImport, string? nameAffix) { - ProfileEntity profileEntity = _profileRepository.Get(profileDescriptor.Id); - if (profileEntity == null) - throw new ArtemisCoreException($"Cannot find profile named: {profileDescriptor.Name} ID: {profileDescriptor.Id}"); + if (exportModel.ProfileEntity == null) + throw new ArtemisCoreException("Cannot import a profile without any data"); - return JsonConvert.SerializeObject(profileEntity, ExportSettings); - } - - public ProfileDescriptor ImportProfile(string json, ProfileModule profileModule, string nameAffix) - { - ProfileEntity? profileEntity = JsonConvert.DeserializeObject(json, ExportSettings); - if (profileEntity == null) - throw new ArtemisCoreException("Failed to import profile but JSON.NET threw no error :("); + // Create a copy of the entity because we'll be using it from now on + ProfileEntity profileEntity = JsonConvert.DeserializeObject( + JsonConvert.SerializeObject(exportModel.ProfileEntity, IProfileService.ExportSettings), IProfileService.ExportSettings + )!; // Assign a new GUID to make sure it is unique in case of a previous import of the same content - profileEntity.UpdateGuid(Guid.NewGuid()); - profileEntity.Name = $"{profileEntity.Name} - {nameAffix}"; - profileEntity.IsFreshImport = true; - profileEntity.IsActive = false; + if (makeUnique) + profileEntity.UpdateGuid(Guid.NewGuid()); + + if (nameAffix != null) + profileEntity.Name = $"{profileEntity.Name} - {nameAffix}"; + if (markAsFreshImport) + profileEntity.IsFreshImport = true; _profileRepository.Add(profileEntity); - return new ProfileDescriptor(profileModule, profileEntity); + + ProfileConfiguration profileConfiguration; + if (exportModel.ProfileConfigurationEntity != null) + { + // A new GUID will be given on save + exportModel.ProfileConfigurationEntity.FileIconId = Guid.Empty; + profileConfiguration = new ProfileConfiguration(category, exportModel.ProfileConfigurationEntity); + if (nameAffix != null) + profileConfiguration.Name = $"{profileConfiguration.Name} - {nameAffix}"; + } + else + { + profileConfiguration = new ProfileConfiguration(category, exportModel.ProfileEntity!.Name, "Import"); + } + + if (exportModel.ProfileImage != null) + profileConfiguration.Icon.FileIcon = exportModel.ProfileImage; + + profileConfiguration.Entity.ProfileId = profileEntity.Id; + category.AddProfileConfiguration(profileConfiguration, 0); + SaveProfileCategory(category); + + return profileConfiguration; } /// public void AdaptProfile(Profile profile) { - string memento = JsonConvert.SerializeObject(profile.ProfileEntity, MementoSettings); + string memento = JsonConvert.SerializeObject(profile.ProfileEntity, IProfileService.MementoSettings); List devices = _rgbService.EnabledDevices.ToList(); foreach (Layer layer in profile.GetAllLayers()) @@ -355,14 +530,5 @@ namespace Artemis.Core.Services _profileRepository.Save(profile.ProfileEntity); } - - #region Event handlers - - private void RgbServiceOnLedsChanged(object? sender, EventArgs e) - { - ActiveProfilesPopulateLeds(); - } - - #endregion } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/WebServer/EndPoints/DataModelJsonPluginEndPoint.cs b/src/Artemis.Core/Services/WebServer/EndPoints/DataModelJsonPluginEndPoint.cs index 052c9c5a3..d0e5e206b 100644 --- a/src/Artemis.Core/Services/WebServer/EndPoints/DataModelJsonPluginEndPoint.cs +++ b/src/Artemis.Core/Services/WebServer/EndPoints/DataModelJsonPluginEndPoint.cs @@ -15,17 +15,7 @@ namespace Artemis.Core.Services /// public class DataModelJsonPluginEndPoint : PluginEndPoint where T : DataModel { - private readonly ProfileModule? _profileModule; - private readonly Module? _module; - private readonly DataModelExpansion? _dataModelExpansion; - - internal DataModelJsonPluginEndPoint(ProfileModule profileModule, string name, PluginsModule pluginsModule) : base(profileModule, name, pluginsModule) - { - _profileModule = profileModule ?? throw new ArgumentNullException(nameof(profileModule)); - - ThrowOnFail = true; - Accepts = MimeType.Json; - } + private readonly Module _module; internal DataModelJsonPluginEndPoint(Module module, string name, PluginsModule pluginsModule) : base(module, name, pluginsModule) { @@ -35,14 +25,6 @@ namespace Artemis.Core.Services Accepts = MimeType.Json; } - internal DataModelJsonPluginEndPoint(DataModelExpansion dataModelExpansion, string name, PluginsModule pluginsModule) : base(dataModelExpansion, name, pluginsModule) - { - _dataModelExpansion = dataModelExpansion ?? throw new ArgumentNullException(nameof(dataModelExpansion)); - - ThrowOnFail = true; - Accepts = MimeType.Json; - } - /// /// Whether or not the end point should throw an exception if deserializing the received JSON fails. /// If set to malformed JSON is silently ignored; if set to malformed @@ -63,12 +45,7 @@ namespace Artemis.Core.Services using TextReader reader = context.OpenRequestText(); try { - if (_profileModule != null) - JsonConvert.PopulateObject(await reader.ReadToEndAsync(), _profileModule.DataModel); - else if (_module != null) - JsonConvert.PopulateObject(await reader.ReadToEndAsync(), _module.DataModel); - else - JsonConvert.PopulateObject(await reader.ReadToEndAsync(), _dataModelExpansion!.DataModel); + JsonConvert.PopulateObject(await reader.ReadToEndAsync(), _module.DataModel); } catch (JsonException) { diff --git a/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs b/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs index 8a20910db..22051d7fd 100644 --- a/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs +++ b/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs @@ -54,26 +54,6 @@ namespace Artemis.Core.Services /// The resulting end point DataModelJsonPluginEndPoint AddDataModelJsonEndPoint(Module module, string endPointName) where T : DataModel; - /// - /// Adds a new endpoint that directly maps received JSON to the data model of the provided - /// . - /// - /// The data model type of the module - /// The module whose datamodel to apply the received JSON to - /// The name of the end point, must be unique - /// The resulting end point - DataModelJsonPluginEndPoint AddDataModelJsonEndPoint(ProfileModule profileModule, string endPointName) where T : DataModel; - - /// - /// Adds a new endpoint that directly maps received JSON to the data model of the provided - /// . - /// - /// The data model type of the module - /// The data model expansion whose datamodel to apply the received JSON to - /// The name of the end point, must be unique - /// The resulting end point - DataModelJsonPluginEndPoint AddDataModelJsonEndPoint(DataModelExpansion dataModelExpansion, string endPointName) where T : DataModel; - /// /// Adds a new endpoint for the given plugin feature receiving an a . /// diff --git a/src/Artemis.Core/Services/WebServer/WebServerService.cs b/src/Artemis.Core/Services/WebServer/WebServerService.cs index a310fca63..4c9272d91 100644 --- a/src/Artemis.Core/Services/WebServer/WebServerService.cs +++ b/src/Artemis.Core/Services/WebServer/WebServerService.cs @@ -142,24 +142,6 @@ namespace Artemis.Core.Services return endPoint; } - public DataModelJsonPluginEndPoint AddDataModelJsonEndPoint(ProfileModule profileModule, string endPointName) where T : DataModel - { - if (profileModule == null) throw new ArgumentNullException(nameof(profileModule)); - if (endPointName == null) throw new ArgumentNullException(nameof(endPointName)); - DataModelJsonPluginEndPoint endPoint = new(profileModule, endPointName, PluginsModule); - PluginsModule.AddPluginEndPoint(endPoint); - return endPoint; - } - - public DataModelJsonPluginEndPoint AddDataModelJsonEndPoint(DataModelExpansion dataModelExpansion, string endPointName) where T : DataModel - { - if (dataModelExpansion == null) throw new ArgumentNullException(nameof(dataModelExpansion)); - if (endPointName == null) throw new ArgumentNullException(nameof(endPointName)); - DataModelJsonPluginEndPoint endPoint = new(dataModelExpansion, endPointName, PluginsModule); - PluginsModule.AddPluginEndPoint(endPoint); - return endPoint; - } - private void HandleDataModelRequest(Module module, T value) where T : DataModel { } diff --git a/src/Artemis.Core/Stores/DataModelStore.cs b/src/Artemis.Core/Stores/DataModelStore.cs index e44598908..336dac6f5 100644 --- a/src/Artemis.Core/Stores/DataModelStore.cs +++ b/src/Artemis.Core/Stores/DataModelStore.cs @@ -17,7 +17,7 @@ namespace Artemis.Core if (Registrations.Any(r => r.DataModel == dataModel)) throw new ArtemisCoreException($"Data model store already contains data model '{dataModel.DataModelDescription}'"); - registration = new DataModelRegistration(dataModel, dataModel.Feature) {IsInStore = true}; + registration = new DataModelRegistration(dataModel, dataModel.Module) {IsInStore = true}; Registrations.Add(registration); } diff --git a/src/Artemis.Core/Utilities/CorePluginFeature.cs b/src/Artemis.Core/Utilities/CorePluginFeature.cs index 66dcb40ff..6e7857389 100644 --- a/src/Artemis.Core/Utilities/CorePluginFeature.cs +++ b/src/Artemis.Core/Utilities/CorePluginFeature.cs @@ -1,11 +1,12 @@ using Artemis.Core.LayerEffects; +using Artemis.Core.Modules; namespace Artemis.Core { /// /// An empty data model plugin feature used by /// - internal class CorePluginFeature : DataModelPluginFeature + internal class CorePluginFeature : Module { public CorePluginFeature() { @@ -20,6 +21,10 @@ namespace Artemis.Core public override void Disable() { } + + public override void Update(double deltaTime) + { + } } internal class EffectPlaceholderPlugin : LayerEffectProvider diff --git a/src/Artemis.Core/Utilities/IntroAnimation.cs b/src/Artemis.Core/Utilities/IntroAnimation.cs deleted file mode 100644 index 9d295e499..000000000 --- a/src/Artemis.Core/Utilities/IntroAnimation.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using Artemis.Core.Modules; -using Artemis.Core.Services; -using Artemis.Storage.Entities.Profile; -using Serilog; -using SkiaSharp; - -namespace Artemis.Core -{ - internal class IntroAnimation - { - private readonly ILogger _logger; - private readonly IProfileService _profileService; - private readonly IEnumerable _devices; - - public IntroAnimation(ILogger logger, IProfileService profileService, IEnumerable devices) - { - _logger = logger; - _profileService = profileService; - _devices = devices; - - AnimationProfile = CreateIntroProfile(); - } - - public Profile AnimationProfile { get; set; } - - public void Render(double deltaTime, SKCanvas canvas) - { - AnimationProfile.Update(deltaTime); - AnimationProfile.Render(canvas, SKPointI.Empty); - } - - private Profile CreateIntroProfile() - { - try - { - // Load the intro profile from JSON into a ProfileEntity - string json = File.ReadAllText(Path.Combine(Constants.ApplicationFolder, "Resources", "intro-profile.json")); - ProfileEntity profileEntity = CoreJson.DeserializeObject(json)!; - // Inject every LED on the surface into each layer - foreach (LayerEntity profileEntityLayer in profileEntity.Layers) - profileEntityLayer.Leds.AddRange(_devices.SelectMany(d => d.Leds).Select(l => new LedEntity - { - DeviceIdentifier = l.Device.Identifier, - LedName = l.RgbLed.Id.ToString() - })); - - Profile profile = new(new DummyModule(), profileEntity); - profile.Activate(_devices); - - _profileService.InstantiateProfile(profile); - return profile; - } - catch (Exception e) - { - _logger.Warning(e, "Failed to load intro profile"); - } - - return new Profile(new DummyModule(), "Intro"); - } - } - - internal class DummyModule : ProfileModule - { - public override void Enable() - { - throw new NotImplementedException(); - } - - public override void Disable() - { - throw new NotImplementedException(); - } - - public override void Update(double deltaTime) - { - throw new NotImplementedException(); - } - - public override void Render(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo) - { - throw new NotImplementedException(); - } - - public override void ModuleActivated(bool isOverride) - { - throw new NotImplementedException(); - } - - public override void ModuleDeactivated(bool isOverride) - { - throw new NotImplementedException(); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Module/ModuleSettingsEntity.cs b/src/Artemis.Storage/Entities/Module/ModuleSettingsEntity.cs deleted file mode 100644 index 3cddf93b7..000000000 --- a/src/Artemis.Storage/Entities/Module/ModuleSettingsEntity.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace Artemis.Storage.Entities.Module -{ - public class ModuleSettingsEntity - { - public ModuleSettingsEntity() - { - Id = Guid.NewGuid(); - } - - public Guid Id { get; set; } - public string ModuleId { get; set; } - public int PriorityCategory { get; set; } - public int Priority { get; set; } - } -} \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/ProfileCategoryEntity.cs b/src/Artemis.Storage/Entities/Profile/ProfileCategoryEntity.cs new file mode 100644 index 000000000..b69ceb875 --- /dev/null +++ b/src/Artemis.Storage/Entities/Profile/ProfileCategoryEntity.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; + +namespace Artemis.Storage.Entities.Profile +{ + public class ProfileCategoryEntity + { + public Guid Id { get; set; } + + public string Name { get; set; } + public bool IsCollapsed { get; set; } + public bool IsSuspended { get; set; } + public int Order { get; set; } + + public List ProfileConfigurations { get; set; } = new(); + } +} \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/ProfileConfigurationEntity.cs b/src/Artemis.Storage/Entities/Profile/ProfileConfigurationEntity.cs new file mode 100644 index 000000000..9f7fcf0f6 --- /dev/null +++ b/src/Artemis.Storage/Entities/Profile/ProfileConfigurationEntity.cs @@ -0,0 +1,22 @@ +using System; +using Artemis.Storage.Entities.Profile.Conditions; + +namespace Artemis.Storage.Entities.Profile +{ + public class ProfileConfigurationEntity + { + public string Name { get; set; } + public string MaterialIcon { get; set; } + public Guid FileIconId { get; set; } + public int IconType { get; set; } + + public bool IsSuspended { get; set; } + public int ActivationBehaviour { get; set; } + public DataModelConditionGroupEntity ActivationCondition { get; set; } + + public string ModuleId { get; set; } + + public Guid ProfileCategoryId { get; set; } + public Guid ProfileId { get; set; } + } +} \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/ProfileEntity.cs b/src/Artemis.Storage/Entities/Profile/ProfileEntity.cs index 8fac49757..cea7e2439 100644 --- a/src/Artemis.Storage/Entities/Profile/ProfileEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/ProfileEntity.cs @@ -13,10 +13,8 @@ namespace Artemis.Storage.Entities.Profile } public Guid Id { get; set; } - public string ModuleId { get; set; } - + public string Name { get; set; } - public bool IsActive { get; set; } public bool IsFreshImport { get; set; } public List Folders { get; set; } diff --git a/src/Artemis.Storage/Migrations/M0008PluginFeatures.cs b/src/Artemis.Storage/Migrations/M0008PluginFeatures.cs index 5dc088531..03b4b9c23 100644 --- a/src/Artemis.Storage/Migrations/M0008PluginFeatures.cs +++ b/src/Artemis.Storage/Migrations/M0008PluginFeatures.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using Artemis.Storage.Entities.Module; using Artemis.Storage.Migrations.Interfaces; using LiteDB; @@ -87,18 +86,6 @@ namespace Artemis.Storage.Migrations // Remove the default brush the user selected, this will make the UI pick a new one repository.Database.Execute("DELETE PluginSettingEntity WHERE $.Name = \"ProfileEditor.DefaultLayerBrushDescriptor\""); - // Module settings - repository.Database.GetCollection().DropIndex("PluginGuid"); - ILiteCollection modules = repository.Database.GetCollection("ModuleSettingsEntity"); - foreach (BsonDocument bsonDocument in modules.FindAll()) - { - if (ReplaceIfFound(bsonDocument, "PluginGuid", "ModuleId", pluginMap)) - modules.Update(bsonDocument); - else if (bsonDocument.ContainsKey("PluginGuid")) - modules.Delete(bsonDocument["_id"]); - } - repository.Database.GetCollection().EnsureIndex(s => s.ModuleId, true); - // Profiles ILiteCollection collection = repository.Database.GetCollection("ProfileEntity"); foreach (BsonDocument bsonDocument in collection.FindAll()) diff --git a/src/Artemis.Storage/Migrations/M0012ProfileCategories.cs b/src/Artemis.Storage/Migrations/M0012ProfileCategories.cs new file mode 100644 index 000000000..9cfe2068f --- /dev/null +++ b/src/Artemis.Storage/Migrations/M0012ProfileCategories.cs @@ -0,0 +1,48 @@ +using System.Linq; +using Artemis.Storage.Entities.Profile; +using Artemis.Storage.Migrations.Interfaces; +using LiteDB; + +namespace Artemis.Storage.Migrations +{ + public class M0012ProfileCategories : IStorageMigration + { + public int UserVersion => 12; + + public void Apply(LiteRepository repository) + { + ILiteCollection profileCategories = repository.Database.GetCollection(); + profileCategories.EnsureIndex(s => s.Name, true); + ProfileCategoryEntity? profileCategoryEntity = profileCategories.Find(c => c.Name == "Converted").FirstOrDefault(); + if (profileCategoryEntity == null) + { + profileCategoryEntity = new ProfileCategoryEntity {Name = "Imported"}; + profileCategories.Insert(profileCategoryEntity); + } + + ILiteCollection collection = repository.Database.GetCollection("ProfileEntity"); + foreach (BsonDocument bsonDocument in collection.FindAll()) + { + // Profiles with a ModuleId have not been converted + if (bsonDocument.ContainsKey("ModuleId")) + { + string moduleId = bsonDocument["ModuleId"].AsString; + bsonDocument.Remove("ModuleId"); + + ProfileConfigurationEntity profileConfiguration = new() + { + Name = bsonDocument["Name"].AsString, + MaterialIcon = "ApplicationImport", + ModuleId = moduleId, + ProfileId = bsonDocument["_id"].AsGuid + }; + + profileCategoryEntity.ProfileConfigurations.Add(profileConfiguration); + collection.Update(bsonDocument); + } + } + + profileCategories.Update(profileCategoryEntity); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Storage/Repositories/Interfaces/IModuleRepository.cs b/src/Artemis.Storage/Repositories/Interfaces/IModuleRepository.cs deleted file mode 100644 index 918efb361..000000000 --- a/src/Artemis.Storage/Repositories/Interfaces/IModuleRepository.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; -using Artemis.Storage.Entities.Module; - -namespace Artemis.Storage.Repositories.Interfaces -{ - public interface IModuleRepository : IRepository - { - void Add(ModuleSettingsEntity moduleSettingsEntity); - ModuleSettingsEntity GetByModuleId(string moduleId); - List GetAll(); - List GetByCategory(int category); - void Save(ModuleSettingsEntity moduleSettingsEntity); - } -} \ No newline at end of file diff --git a/src/Artemis.Storage/Repositories/Interfaces/IProfileCategoryRepository.cs b/src/Artemis.Storage/Repositories/Interfaces/IProfileCategoryRepository.cs new file mode 100644 index 000000000..b6a27dc73 --- /dev/null +++ b/src/Artemis.Storage/Repositories/Interfaces/IProfileCategoryRepository.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Artemis.Storage.Entities.Profile; + +namespace Artemis.Storage.Repositories.Interfaces +{ + public interface IProfileCategoryRepository : IRepository + { + void Add(ProfileCategoryEntity profileCategoryEntity); + void Remove(ProfileCategoryEntity profileCategoryEntity); + List GetAll(); + ProfileCategoryEntity Get(Guid id); + Stream GetProfileIconStream(Guid id); + void SaveProfileIconStream(ProfileConfigurationEntity profileConfigurationEntity, Stream stream); + ProfileCategoryEntity IsUnique(string name, Guid? id); + void Save(ProfileCategoryEntity profileCategoryEntity); + } +} \ No newline at end of file diff --git a/src/Artemis.Storage/Repositories/Interfaces/IProfileRepository.cs b/src/Artemis.Storage/Repositories/Interfaces/IProfileRepository.cs index a42c74966..1d8dcd7cf 100644 --- a/src/Artemis.Storage/Repositories/Interfaces/IProfileRepository.cs +++ b/src/Artemis.Storage/Repositories/Interfaces/IProfileRepository.cs @@ -10,7 +10,6 @@ namespace Artemis.Storage.Repositories.Interfaces void Remove(ProfileEntity profileEntity); List GetAll(); ProfileEntity Get(Guid id); - List GetByModuleId(string moduleId); void Save(ProfileEntity profileEntity); } } \ No newline at end of file diff --git a/src/Artemis.Storage/Repositories/ModuleRepository.cs b/src/Artemis.Storage/Repositories/ModuleRepository.cs deleted file mode 100644 index aae79d974..000000000 --- a/src/Artemis.Storage/Repositories/ModuleRepository.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Collections.Generic; -using Artemis.Storage.Entities.Module; -using Artemis.Storage.Repositories.Interfaces; -using LiteDB; - -namespace Artemis.Storage.Repositories -{ - internal class ModuleRepository : IModuleRepository - { - private readonly LiteRepository _repository; - - public ModuleRepository(LiteRepository repository) - { - _repository = repository; - _repository.Database.GetCollection().EnsureIndex(s => s.ModuleId, true); - } - - public void Add(ModuleSettingsEntity moduleSettingsEntity) - { - _repository.Insert(moduleSettingsEntity); - } - - public ModuleSettingsEntity GetByModuleId(string moduleId) - { - return _repository.FirstOrDefault(s => s.ModuleId == moduleId); - } - - public List GetAll() - { - return _repository.Query().ToList(); - } - - public List GetByCategory(int category) - { - return _repository.Query().Where(s => s.PriorityCategory == category).ToList(); - } - - public void Save(ModuleSettingsEntity moduleSettingsEntity) - { - _repository.Upsert(moduleSettingsEntity); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Storage/Repositories/ProfileCategoryRepository.cs b/src/Artemis.Storage/Repositories/ProfileCategoryRepository.cs new file mode 100644 index 000000000..8c8de1110 --- /dev/null +++ b/src/Artemis.Storage/Repositories/ProfileCategoryRepository.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Artemis.Storage.Entities.Profile; +using Artemis.Storage.Repositories.Interfaces; +using LiteDB; + +namespace Artemis.Storage.Repositories +{ + internal class ProfileCategoryRepository : IProfileCategoryRepository + { + private readonly LiteRepository _repository; + private readonly ILiteStorage _profileIcons; + + public ProfileCategoryRepository(LiteRepository repository) + { + _repository = repository; + _repository.Database.GetCollection().EnsureIndex(s => s.Name, true); + _profileIcons = _repository.Database.GetStorage("profileIcons"); + } + + public void Add(ProfileCategoryEntity profileCategoryEntity) + { + _repository.Insert(profileCategoryEntity); + } + + public void Remove(ProfileCategoryEntity profileCategoryEntity) + { + _repository.Delete(profileCategoryEntity.Id); + } + + public List GetAll() + { + return _repository.Query().ToList(); + } + + public ProfileCategoryEntity Get(Guid id) + { + return _repository.FirstOrDefault(p => p.Id == id); + } + + public ProfileCategoryEntity IsUnique(string name, Guid? id) + { + if (id == null) + return _repository.FirstOrDefault(p => p.Name == name); + return _repository.FirstOrDefault(p => p.Name == name && p.Id != id.Value); + } + + public void Save(ProfileCategoryEntity profileCategoryEntity) + { + _repository.Upsert(profileCategoryEntity); + } + + public Stream GetProfileIconStream(Guid id) + { + if (!_profileIcons.Exists(id)) + return null; + + MemoryStream stream = new(); + _profileIcons.Download(id, stream); + return stream; + } + + public void SaveProfileIconStream(ProfileConfigurationEntity profileConfigurationEntity, Stream stream) + { + if (profileConfigurationEntity.FileIconId == Guid.Empty) + profileConfigurationEntity.FileIconId = Guid.NewGuid(); + + if (stream == null && _profileIcons.Exists(profileConfigurationEntity.FileIconId)) + _profileIcons.Delete(profileConfigurationEntity.FileIconId); + + _profileIcons.Upload(profileConfigurationEntity.FileIconId, "image", stream); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Storage/Repositories/ProfileRepository.cs b/src/Artemis.Storage/Repositories/ProfileRepository.cs index 1fdc88868..c6014a6b5 100644 --- a/src/Artemis.Storage/Repositories/ProfileRepository.cs +++ b/src/Artemis.Storage/Repositories/ProfileRepository.cs @@ -36,15 +36,6 @@ namespace Artemis.Storage.Repositories return _repository.FirstOrDefault(p => p.Id == id); } - public List GetByModuleId(string moduleId) - { - return _repository.Query() - .Include(p => p.Folders) - .Include(p => p.Layers) - .Where(s => s.ModuleId == moduleId) - .ToList(); - } - public void Save(ProfileEntity profileEntity) { _repository.Upsert(profileEntity); diff --git a/src/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.xaml b/src/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.xaml new file mode 100644 index 000000000..b7a60b9c8 --- /dev/null +++ b/src/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.xaml @@ -0,0 +1,63 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.xaml.cs b/src/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.xaml.cs new file mode 100644 index 000000000..d98061e91 --- /dev/null +++ b/src/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.xaml.cs @@ -0,0 +1,30 @@ +using System.Windows; +using System.Windows.Controls; +using MaterialDesignThemes.Wpf; + +namespace Artemis.UI.Shared + +{ + /// + /// Interaction logic for ProfileConfigurationIcon.xaml + /// + public partial class ProfileConfigurationIcon : UserControl + { + /// + /// Gets or sets the + /// + public static readonly DependencyProperty ConfigurationIconProperty = + DependencyProperty.Register(nameof(ConfigurationIcon), typeof(Core.ProfileConfigurationIcon), typeof(ProfileConfigurationIcon)); + + public ProfileConfigurationIcon() + { + InitializeComponent(); + } + + public Core.ProfileConfigurationIcon ConfigurationIcon + { + get => (Core.ProfileConfigurationIcon) GetValue(ConfigurationIconProperty); + set => SetValue(ConfigurationIconProperty, value); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Converters/StreamToBitmapImageConverter.cs b/src/Artemis.UI.Shared/Converters/StreamToBitmapImageConverter.cs new file mode 100644 index 000000000..6e8be2be0 --- /dev/null +++ b/src/Artemis.UI.Shared/Converters/StreamToBitmapImageConverter.cs @@ -0,0 +1,42 @@ +using System; +using System.Globalization; +using System.IO; +using System.Windows.Data; +using System.Windows.Media.Imaging; + +namespace Artemis.UI.Shared +{ + /// + /// + /// Converts into . + /// + [ValueConversion(typeof(Stream), typeof(BitmapImage))] + public class StreamToBitmapImageConverter : IValueConverter + { + /// + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is not Stream stream) + return null; + + stream.Position = 0; + + BitmapImage selectedBitmap = new(); + selectedBitmap.BeginInit(); + selectedBitmap.StreamSource = stream; + selectedBitmap.CacheOption = BitmapCacheOption.OnLoad; + selectedBitmap.EndInit(); + selectedBitmap.Freeze(); + + stream.Position = 0; + return selectedBitmap; + } + + + /// + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return Binding.DoNothing; + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicView.xaml b/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicView.xaml index 1f516180f..6ebda995f 100644 --- a/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicView.xaml +++ b/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicView.xaml @@ -77,6 +77,11 @@ + + + + + diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicViewModel.cs b/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicViewModel.cs index 12292fc6d..fa4ad2f99 100644 --- a/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicViewModel.cs +++ b/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicViewModel.cs @@ -19,8 +19,8 @@ namespace Artemis.UI.Shared.Input /// public class DataModelDynamicViewModel : PropertyChangedBase, IDisposable { + private readonly List _modules; private readonly IDataModelUIService _dataModelUIService; - private readonly Module _module; private readonly Timer _updateTimer; private SolidColorBrush _buttonBrush = new(Color.FromRgb(171, 71, 188)); private DataModelPath? _dataModelPath; @@ -31,9 +31,9 @@ namespace Artemis.UI.Shared.Input private bool _isEnabled = true; private string _placeholder = "Select a property"; - internal DataModelDynamicViewModel(Module module, ISettingsService settingsService, IDataModelUIService dataModelUIService) + internal DataModelDynamicViewModel(List modules, ISettingsService settingsService, IDataModelUIService dataModelUIService) { - _module = module; + _modules = modules; _dataModelUIService = dataModelUIService; _updateTimer = new Timer(500); @@ -107,6 +107,11 @@ namespace Artemis.UI.Shared.Input /// public BindableCollection ExtraDataModelViewModels { get; } + /// + /// Gets a boolean indicating whether there are any modules providing data models + /// + public bool HasNoModules => (DataModelViewModel == null || !DataModelViewModel.Children.Any()) && !HasExtraDataModels; + /// /// Gets a boolean indicating whether there are any extra data models /// @@ -233,7 +238,7 @@ namespace Artemis.UI.Shared.Input private void Initialize() { // Get the data models - DataModelViewModel = _dataModelUIService.GetPluginDataModelVisualization(_module, true); + DataModelViewModel = _dataModelUIService.GetPluginDataModelVisualization(_modules, true); if (DataModelViewModel != null) DataModelViewModel.UpdateRequested += DataModelOnUpdateRequested; ExtraDataModelViewModels.CollectionChanged += ExtraDataModelViewModelsOnCollectionChanged; diff --git a/src/Artemis.UI.Shared/Events/ProfileConfigurationEventArgs.cs b/src/Artemis.UI.Shared/Events/ProfileConfigurationEventArgs.cs new file mode 100644 index 000000000..3ec9ac748 --- /dev/null +++ b/src/Artemis.UI.Shared/Events/ProfileConfigurationEventArgs.cs @@ -0,0 +1,32 @@ +using System; +using Artemis.Core; + +namespace Artemis.UI.Shared +{ + /// + /// Provides data on profile related events raised by the profile editor + /// + public class ProfileConfigurationEventArgs : EventArgs + { + internal ProfileConfigurationEventArgs(ProfileConfiguration? profileConfiguration) + { + ProfileConfiguration = profileConfiguration; + } + + internal ProfileConfigurationEventArgs(ProfileConfiguration? profileConfiguration, ProfileConfiguration? previousProfileConfiguration) + { + ProfileConfiguration = profileConfiguration; + PreviousProfileConfiguration = previousProfileConfiguration; + } + + /// + /// Gets the profile the event was raised for + /// + public ProfileConfiguration? ProfileConfiguration { get; } + + /// + /// If applicable, the previous active profile before the event was raised + /// + public ProfileConfiguration? PreviousProfileConfiguration { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Events/ProfileEventArgs.cs b/src/Artemis.UI.Shared/Events/ProfileEventArgs.cs deleted file mode 100644 index 95fe4aba3..000000000 --- a/src/Artemis.UI.Shared/Events/ProfileEventArgs.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using Artemis.Core; - -namespace Artemis.UI.Shared -{ - /// - /// Provides data on profile related events raised by the profile editor - /// - public class ProfileEventArgs : EventArgs - { - internal ProfileEventArgs(Profile? profile) - { - Profile = profile; - } - - internal ProfileEventArgs(Profile? profile, Profile? previousProfile) - { - Profile = profile; - PreviousProfile = previousProfile; - } - - /// - /// Gets the profile the event was raised for - /// - public Profile? Profile { get; } - - /// - /// If applicable, the previous active profile before the event was raised - /// - public Profile? PreviousProfile { get; } - } -} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Ninject/Factories/ISharedVMFactory.cs b/src/Artemis.UI.Shared/Ninject/Factories/ISharedVMFactory.cs index ab9cb2927..ee1c48316 100644 --- a/src/Artemis.UI.Shared/Ninject/Factories/ISharedVMFactory.cs +++ b/src/Artemis.UI.Shared/Ninject/Factories/ISharedVMFactory.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Artemis.Core.DataModelExpansions; using Artemis.Core.Modules; using Artemis.UI.Shared.Input; @@ -20,9 +21,9 @@ namespace Artemis.UI.Shared /// /// Creates a new instance of the class /// - /// The module to associate the dynamic view model with + /// The modules to associate the dynamic view model with /// A new instance of the class - DataModelDynamicViewModel DataModelDynamicViewModel(Module module); + DataModelDynamicViewModel DataModelDynamicViewModel(List modules); /// /// Creates a new instance of the class diff --git a/src/Artemis.UI.Shared/PropertyInput/PropertyInputViewModel.cs b/src/Artemis.UI.Shared/PropertyInput/PropertyInputViewModel.cs index 1f15dbd53..a5e043888 100644 --- a/src/Artemis.UI.Shared/PropertyInput/PropertyInputViewModel.cs +++ b/src/Artemis.UI.Shared/PropertyInput/PropertyInputViewModel.cs @@ -142,7 +142,7 @@ namespace Artemis.UI.Shared if (InputDragging) ProfileEditorService.UpdateProfilePreview(); else - ProfileEditorService.UpdateSelectedProfileElement(); + ProfileEditorService.SaveSelectedProfileElement(); } } @@ -186,7 +186,7 @@ namespace Artemis.UI.Shared public void InputDragEnded(object sender, EventArgs e) { InputDragging = false; - ProfileEditorService.UpdateSelectedProfileElement(); + ProfileEditorService.SaveSelectedProfileElement(); } private void LayerPropertyOnUpdated(object? sender, EventArgs e) diff --git a/src/Artemis.UI.Shared/Services/DataModelUIService.cs b/src/Artemis.UI.Shared/Services/DataModelUIService.cs index 729468223..6066de626 100644 --- a/src/Artemis.UI.Shared/Services/DataModelUIService.cs +++ b/src/Artemis.UI.Shared/Services/DataModelUIService.cs @@ -35,7 +35,7 @@ namespace Artemis.UI.Shared.Services public DataModelPropertiesViewModel GetMainDataModelVisualization() { DataModelPropertiesViewModel viewModel = new(null, null, null); - foreach (DataModel dataModelExpansion in _dataModelService.GetDataModels().OrderBy(d => d.DataModelDescription.Name)) + foreach (DataModel dataModelExpansion in _dataModelService.GetDataModels().Where(d => d.IsExpansion).OrderBy(d => d.DataModelDescription.Name)) viewModel.Children.Add(new DataModelPropertiesViewModel(dataModelExpansion, viewModel, new DataModelPath(dataModelExpansion))); // Update to populate children @@ -47,7 +47,7 @@ namespace Artemis.UI.Shared.Services public void UpdateModules(DataModelPropertiesViewModel mainDataModelVisualization) { List disabledChildren = mainDataModelVisualization.Children - .Where(d => d.DataModel != null && !d.DataModel.Feature.IsEnabled) + .Where(d => d.DataModel != null && !d.DataModel.Module.IsEnabled) .ToList(); foreach (DataModelVisualizationViewModel child in disabledChildren) mainDataModelVisualization.Children.Remove(child); @@ -65,34 +65,33 @@ namespace Artemis.UI.Shared.Services mainDataModelVisualization.Update(this, null); } - public DataModelPropertiesViewModel? GetPluginDataModelVisualization(PluginFeature pluginFeature, bool includeMainDataModel) + public DataModelPropertiesViewModel? GetPluginDataModelVisualization(List modules, bool includeMainDataModel) { + DataModelPropertiesViewModel root; + // This will contain any modules that are always available if (includeMainDataModel) + root = GetMainDataModelVisualization(); + else { - DataModelPropertiesViewModel mainDataModel = GetMainDataModelVisualization(); - - // If the main data model already includes the plugin data model we're done - if (mainDataModel.Children.Any(c => c.DataModel?.Feature == pluginFeature)) - return mainDataModel; - // Otherwise get just the plugin data model and add it - DataModelPropertiesViewModel? pluginDataModel = GetPluginDataModelVisualization(pluginFeature, false); - if (pluginDataModel != null) - mainDataModel.Children.Add(pluginDataModel); - - return mainDataModel; + root = new DataModelPropertiesViewModel(null, null, null); + root.UpdateRequested += (sender, args) => root.Update(this, null); } - DataModel? dataModel = _dataModelService.GetPluginDataModel(pluginFeature); - if (dataModel == null) + foreach (Module module in modules) + { + DataModel? dataModel = _dataModelService.GetPluginDataModel(module); + if (dataModel == null) + continue; + + root.Children.Add(new DataModelPropertiesViewModel(dataModel, root, new DataModelPath(dataModel))); + } + + if (!root.Children.Any()) return null; - DataModelPropertiesViewModel viewModel = new(null, null, null); - viewModel.Children.Add(new DataModelPropertiesViewModel(dataModel, viewModel, new DataModelPath(dataModel))); - // Update to populate children - viewModel.Update(this, null); - viewModel.UpdateRequested += (sender, args) => viewModel.Update(this, null); - return viewModel; + root.Update(this, null); + return root; } public DataModelVisualizationRegistration RegisterDataModelInput(Plugin plugin, IReadOnlyCollection? compatibleConversionTypes = null) where T : DataModelInputViewModel @@ -226,9 +225,14 @@ namespace Artemis.UI.Shared.Services } } - public DataModelDynamicViewModel GetDynamicSelectionViewModel(Module module) + public DataModelDynamicViewModel GetDynamicSelectionViewModel(Module? module) { - return _dataModelVmFactory.DataModelDynamicViewModel(module); + return _dataModelVmFactory.DataModelDynamicViewModel(module == null ? new List() : new List {module}); + } + + public DataModelDynamicViewModel GetDynamicSelectionViewModel(List modules) + { + return _dataModelVmFactory.DataModelDynamicViewModel(modules); } public DataModelStaticViewModel GetStaticInputViewModel(Type targetType, DataModelPropertyAttribute targetDescription) diff --git a/src/Artemis.UI.Shared/Services/Interfaces/IDataModelUIService.cs b/src/Artemis.UI.Shared/Services/Interfaces/IDataModelUIService.cs index 18ba8dd42..5da6a381b 100644 --- a/src/Artemis.UI.Shared/Services/Interfaces/IDataModelUIService.cs +++ b/src/Artemis.UI.Shared/Services/Interfaces/IDataModelUIService.cs @@ -34,10 +34,13 @@ namespace Artemis.UI.Shared.Services /// /// Creates a data model visualization view model for the data model of the provided plugin feature /// - /// The plugin feature to create hte data model visualization view model for - /// Whether or not also to include the main data model + /// The modules to create the data model visualization view model for + /// + /// Whether or not also to include the main data model (and therefore any modules marked + /// as ) + /// /// A data model visualization view model containing the data model of the provided feature - DataModelPropertiesViewModel? GetPluginDataModelVisualization(PluginFeature pluginFeature, bool includeMainDataModel); + DataModelPropertiesViewModel? GetPluginDataModelVisualization(List modules, bool includeMainDataModel); /// /// Updates the children of the provided main data model visualization, removing disabled children and adding newly @@ -105,9 +108,16 @@ namespace Artemis.UI.Shared.Services /// /// Creates a view model that allows selecting a value from the data model /// - /// + /// An extra non-always active module to include + /// + DataModelDynamicViewModel GetDynamicSelectionViewModel(Module? module); + + /// + /// Creates a view model that allows selecting a value from the data model + /// + /// A list of extra extra non-always active modules to include /// A view model that allows selecting a value from the data model - DataModelDynamicViewModel GetDynamicSelectionViewModel(Module module); + DataModelDynamicViewModel GetDynamicSelectionViewModel(List modules); /// /// Creates a view model that allows entering a value matching the target data model type diff --git a/src/Artemis.UI.Shared/Services/Interfaces/IDialogService.cs b/src/Artemis.UI.Shared/Services/Interfaces/IDialogService.cs index 2a9fb5d4f..1b94080eb 100644 --- a/src/Artemis.UI.Shared/Services/Interfaces/IDialogService.cs +++ b/src/Artemis.UI.Shared/Services/Interfaces/IDialogService.cs @@ -19,7 +19,7 @@ namespace Artemis.UI.Shared.Services /// The text of the confirm button, defaults to "Confirm" /// The text of the cancel button, defaults to "Cancel" /// A task that resolves to true if confirmed and false if cancelled - Task ShowConfirmDialog(string header, string text, string confirmText = "Confirm", string cancelText = "Cancel"); + Task ShowConfirmDialog(string header, string text, string confirmText = "Confirm", string? cancelText = "Cancel"); /// /// Shows a confirm dialog on the dialog host provided in identifier. @@ -33,7 +33,7 @@ namespace Artemis.UI.Shared.Services /// The text of the confirm button, defaults to "Confirm" /// The text of the cancel button, defaults to "Cancel" /// A task that resolves to true if confirmed and false if cancelled - Task ShowConfirmDialogAt(string identifier, string header, string text, string confirmText = "Confirm", string cancelText = "Cancel"); + Task ShowConfirmDialogAt(string identifier, string header, string text, string confirmText = "Confirm", string? cancelText = "Cancel"); /// /// Shows a dialog by initializing a view model implementing diff --git a/src/Artemis.UI.Shared/Services/Interfaces/IProfileEditorService.cs b/src/Artemis.UI.Shared/Services/Interfaces/IProfileEditorService.cs index 2b17579b2..0184583d9 100644 --- a/src/Artemis.UI.Shared/Services/Interfaces/IProfileEditorService.cs +++ b/src/Artemis.UI.Shared/Services/Interfaces/IProfileEditorService.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Windows; using Artemis.Core; -using Artemis.Core.Modules; namespace Artemis.UI.Shared.Services { @@ -12,8 +11,15 @@ namespace Artemis.UI.Shared.Services /// public interface IProfileEditorService : IArtemisSharedUIService { + /// + /// Gets the currently selected profile configuration + /// if the editor is closed + /// + ProfileConfiguration? SelectedProfileConfiguration { get; } + /// /// Gets the currently selected profile + /// if the editor is closed, always equal to . /// Profile? SelectedProfile { get; } @@ -48,15 +54,15 @@ namespace Artemis.UI.Shared.Services bool Playing { get; set; } /// - /// Changes the selected profile + /// Changes the selected profile by its /// - /// The profile to select - void ChangeSelectedProfile(Profile? profile); + /// The profile configuration of the profile to select + void ChangeSelectedProfileConfiguration(ProfileConfiguration? profileConfiguration); /// - /// Updates the selected profile and saves it to persistent storage + /// Saves the of the selected to persistent storage /// - void UpdateSelectedProfile(); + void SaveSelectedProfileConfiguration(); /// /// Changes the selected profile element @@ -65,9 +71,9 @@ namespace Artemis.UI.Shared.Services void ChangeSelectedProfileElement(RenderProfileElement? profileElement); /// - /// Updates the selected profile element and saves the profile it is contained in to persistent storage + /// Saves the currently selected to persistent storage /// - void UpdateSelectedProfileElement(); + void SaveSelectedProfileElement(); /// /// Changes the selected data binding property @@ -81,22 +87,16 @@ namespace Artemis.UI.Shared.Services void UpdateProfilePreview(); /// - /// Restores the profile to the last call + /// Restores the profile to the last call /// /// if undo was successful, otherwise - bool UndoUpdateProfile(); + bool UndoSaveProfile(); /// - /// Restores the profile to the last call + /// Restores the profile to the last call /// /// if redo was successful, otherwise - bool RedoUpdateProfile(); - - /// - /// Gets the current module the profile editor is initialized for - /// - /// The current module the profile editor is initialized for - ProfileModule? GetCurrentModule(); + bool RedoSaveProfile(); /// /// Registers a new property input view model used in the profile editor for the generic type defined in @@ -182,22 +182,22 @@ namespace Artemis.UI.Shared.Services /// /// Occurs when a new profile is selected /// - event EventHandler ProfileSelected; + event EventHandler SelectedProfileChanged; /// /// Occurs then the currently selected profile is updated /// - event EventHandler SelectedProfileUpdated; + event EventHandler SelectedProfileSaved; /// /// Occurs when a new profile element is selected /// - event EventHandler ProfileElementSelected; + event EventHandler SelectedProfileElementChanged; /// /// Occurs when the currently selected profile element is updated /// - event EventHandler SelectedProfileElementUpdated; + event EventHandler SelectedProfileElementSaved; /// /// Occurs when the currently selected data binding layer property is changed diff --git a/src/Artemis.UI.Shared/Services/ProfileEditorService.cs b/src/Artemis.UI.Shared/Services/ProfileEditorService.cs index 8f1c97548..ed1a2d68f 100644 --- a/src/Artemis.UI.Shared/Services/ProfileEditorService.cs +++ b/src/Artemis.UI.Shared/Services/ProfileEditorService.cs @@ -24,70 +24,25 @@ namespace Artemis.UI.Shared.Services private readonly IProfileService _profileService; private readonly List _registeredPropertyEditors; private readonly IRgbService _rgbService; + private readonly IModuleService _moduleService; private readonly object _selectedProfileElementLock = new(); private readonly object _selectedProfileLock = new(); private TimeSpan _currentTime; private bool _doTick; private int _pixelsPerSecond; - public ProfileEditorService(IKernel kernel, ILogger logger, IProfileService profileService, ICoreService coreService, IRgbService rgbService) + public ProfileEditorService(IKernel kernel, ILogger logger, IProfileService profileService, ICoreService coreService, IRgbService rgbService, IModuleService moduleService) { _kernel = kernel; _logger = logger; _profileService = profileService; _rgbService = rgbService; + _moduleService = moduleService; _registeredPropertyEditors = new List(); coreService.FrameRendered += CoreServiceOnFrameRendered; PixelsPerSecond = 100; } - public event EventHandler? CurrentTimelineChanged; - - protected virtual void OnSelectedProfileChanged(ProfileEventArgs e) - { - ProfileSelected?.Invoke(this, e); - } - - protected virtual void OnSelectedProfileUpdated(ProfileEventArgs e) - { - SelectedProfileUpdated?.Invoke(this, e); - } - - protected virtual void OnSelectedProfileElementChanged(RenderProfileElementEventArgs e) - { - ProfileElementSelected?.Invoke(this, e); - } - - protected virtual void OnSelectedProfileElementUpdated(RenderProfileElementEventArgs e) - { - SelectedProfileElementUpdated?.Invoke(this, e); - } - - protected virtual void OnCurrentTimeChanged() - { - CurrentTimeChanged?.Invoke(this, EventArgs.Empty); - } - - protected virtual void OnCurrentTimelineChanged() - { - CurrentTimelineChanged?.Invoke(this, EventArgs.Empty); - } - - protected virtual void OnPixelsPerSecondChanged() - { - PixelsPerSecondChanged?.Invoke(this, EventArgs.Empty); - } - - protected virtual void OnProfilePreviewUpdated() - { - ProfilePreviewUpdated?.Invoke(this, EventArgs.Empty); - } - - protected virtual void OnSelectedDataBindingChanged() - { - SelectedDataBindingChanged?.Invoke(this, EventArgs.Empty); - } - private void CoreServiceOnFrameRendered(object? sender, FrameRenderedEventArgs e) { if (!_doTick) return; @@ -101,7 +56,7 @@ namespace Artemis.UI.Shared.Services return; // Trigger a profile change - OnSelectedProfileChanged(new ProfileEventArgs(SelectedProfile, SelectedProfile)); + OnSelectedProfileChanged(new ProfileConfigurationEventArgs(SelectedProfileConfiguration, SelectedProfileConfiguration)); // Trigger a selected element change RenderProfileElement? previousSelectedProfileElement = SelectedProfileElement; if (SelectedProfileElement is Folder folder) @@ -148,16 +103,11 @@ namespace Artemis.UI.Shared.Services } } - private void SelectedProfileOnDeactivated(object? sender, EventArgs e) - { - // Execute.PostToUIThread(() => ChangeSelectedProfile(null)); - ChangeSelectedProfile(null); - } - public ReadOnlyCollection RegisteredPropertyEditors => _registeredPropertyEditors.AsReadOnly(); public bool Playing { get; set; } - public Profile? SelectedProfile { get; private set; } + public ProfileConfiguration? SelectedProfileConfiguration { get; private set; } + public Profile? SelectedProfile => SelectedProfileConfiguration?.Profile; public RenderProfileElement? SelectedProfileElement { get; private set; } public ILayerProperty? SelectedDataBinding { get; private set; } @@ -183,43 +133,54 @@ namespace Artemis.UI.Shared.Services } } - public void ChangeSelectedProfile(Profile? profile) + public void ChangeSelectedProfileConfiguration(ProfileConfiguration? profileConfiguration) { lock (_selectedProfileLock) { - if (SelectedProfile == profile) + if (SelectedProfileConfiguration == profileConfiguration) return; - if (profile != null && !profile.IsActivated) - throw new ArtemisSharedUIException("Cannot change the selected profile to an inactive profile"); + if (profileConfiguration?.Profile != null && profileConfiguration.Profile.Disposed) + throw new ArtemisSharedUIException("Cannot select a disposed profile"); - _logger.Verbose("ChangeSelectedProfile {profile}", profile); + _logger.Verbose("ChangeSelectedProfileConfiguration {profile}", profileConfiguration); ChangeSelectedProfileElement(null); + ProfileConfigurationEventArgs profileConfigurationElementEvent = new(profileConfiguration, SelectedProfileConfiguration); - ProfileEventArgs profileElementEvent = new(profile, SelectedProfile); + // No need to deactivate the profile, if needed it will be deactivated next update + if (SelectedProfileConfiguration != null) + SelectedProfileConfiguration.IsBeingEdited = false; - // Ensure there is never a deactivated profile as the selected profile - if (SelectedProfile != null) - SelectedProfile.Deactivated -= SelectedProfileOnDeactivated; - SelectedProfile = profile; - if (SelectedProfile != null) - SelectedProfile.Deactivated += SelectedProfileOnDeactivated; + // The new profile may need activation + SelectedProfileConfiguration = profileConfiguration; + if (SelectedProfileConfiguration != null) + { + SelectedProfileConfiguration.IsBeingEdited = true; + _moduleService.SetActivationOverride(SelectedProfileConfiguration.Module); + _profileService.ActivateProfile(SelectedProfileConfiguration); + _profileService.RenderForEditor = true; + } + else + { + _moduleService.SetActivationOverride(null); + _profileService.RenderForEditor = false; + } - OnSelectedProfileChanged(profileElementEvent); + OnSelectedProfileChanged(profileConfigurationElementEvent); UpdateProfilePreview(); } } - public void UpdateSelectedProfile() + public void SaveSelectedProfileConfiguration() { lock (_selectedProfileLock) { - _logger.Verbose("UpdateSelectedProfile {profile}", SelectedProfile); + _logger.Verbose("SaveSelectedProfileConfiguration {profile}", SelectedProfile); if (SelectedProfile == null) return; - _profileService.UpdateProfile(SelectedProfile, true); - OnSelectedProfileUpdated(new ProfileEventArgs(SelectedProfile)); + _profileService.SaveProfile(SelectedProfile, true); + OnSelectedProfileUpdated(new ProfileConfigurationEventArgs(SelectedProfileConfiguration)); UpdateProfilePreview(); } } @@ -240,15 +201,15 @@ namespace Artemis.UI.Shared.Services } } - public void UpdateSelectedProfileElement() + public void SaveSelectedProfileElement() { lock (_selectedProfileElementLock) { - _logger.Verbose("UpdateSelectedProfileElement {profile}", SelectedProfileElement); + _logger.Verbose("SaveSelectedProfileElement {profile}", SelectedProfileElement); if (SelectedProfile == null) return; - _profileService.UpdateProfile(SelectedProfile, true); + _profileService.SaveProfile(SelectedProfile, true); OnSelectedProfileElementUpdated(new RenderProfileElementEventArgs(SelectedProfileElement)); UpdateProfilePreview(); } @@ -267,12 +228,12 @@ namespace Artemis.UI.Shared.Services Tick(); } - public bool UndoUpdateProfile() + public bool UndoSaveProfile() { if (SelectedProfile == null) return false; - bool undid = _profileService.UndoUpdateProfile(SelectedProfile); + bool undid = _profileService.UndoSaveProfile(SelectedProfile); if (!undid) return false; @@ -280,12 +241,12 @@ namespace Artemis.UI.Shared.Services return true; } - public bool RedoUpdateProfile() + public bool RedoSaveProfile() { if (SelectedProfile == null) return false; - bool redid = _profileService.RedoUpdateProfile(SelectedProfile); + bool redid = _profileService.RedoSaveProfile(SelectedProfile); if (!redid) return false; @@ -319,8 +280,11 @@ namespace Artemis.UI.Shared.Services if (existing != null) { if (existing.Plugin != plugin) + { throw new ArtemisSharedUIException($"Cannot register property editor for type {supportedType.Name} because an editor was already " + $"registered by {existing.Plugin}"); + } + return existing; } @@ -364,8 +328,10 @@ namespace Artemis.UI.Shared.Services if (snapToCurrentTime) // Snap to the current time + { if (Math.Abs(time.TotalMilliseconds - CurrentTime.TotalMilliseconds) < tolerance.TotalMilliseconds) return CurrentTime; + } if (snapTimes != null) { @@ -401,13 +367,9 @@ namespace Artemis.UI.Shared.Services viewModelType = registration.ViewModelType.MakeGenericType(layerProperty.GetType().GenericTypeArguments); } else if (registration != null) - { viewModelType = registration.ViewModelType; - } else - { return null; - } if (viewModelType == null) return null; @@ -419,11 +381,6 @@ namespace Artemis.UI.Shared.Services return (PropertyInputViewModel) kernel.Get(viewModelType, parameter); } - public ProfileModule? GetCurrentModule() - { - return SelectedProfile?.Module; - } - public List GetLedsInRectangle(Rect rect) { return _rgbService.EnabledDevices @@ -432,15 +389,6 @@ namespace Artemis.UI.Shared.Services .ToList(); } - public event EventHandler? ProfileSelected; - public event EventHandler? SelectedProfileUpdated; - public event EventHandler? ProfileElementSelected; - public event EventHandler? SelectedProfileElementUpdated; - public event EventHandler? SelectedDataBindingChanged; - public event EventHandler? CurrentTimeChanged; - public event EventHandler? PixelsPerSecondChanged; - public event EventHandler? ProfilePreviewUpdated; - #region Copy/paste public ProfileElement? DuplicateProfileElement(ProfileElement profileElement) @@ -512,7 +460,7 @@ namespace Artemis.UI.Shared.Services if (pasted != null) { target.Profile.PopulateLeds(_rgbService.EnabledDevices); - UpdateSelectedProfile(); + SaveSelectedProfileConfiguration(); ChangeSelectedProfileElement(pasted); } @@ -520,5 +468,58 @@ namespace Artemis.UI.Shared.Services } #endregion + + #region Events + + public event EventHandler? SelectedProfileChanged; + public event EventHandler? SelectedProfileSaved; + public event EventHandler? SelectedProfileElementChanged; + public event EventHandler? SelectedProfileElementSaved; + public event EventHandler? SelectedDataBindingChanged; + public event EventHandler? CurrentTimeChanged; + public event EventHandler? PixelsPerSecondChanged; + public event EventHandler? ProfilePreviewUpdated; + + protected virtual void OnSelectedProfileChanged(ProfileConfigurationEventArgs e) + { + SelectedProfileChanged?.Invoke(this, e); + } + + protected virtual void OnSelectedProfileUpdated(ProfileConfigurationEventArgs e) + { + SelectedProfileSaved?.Invoke(this, e); + } + + protected virtual void OnSelectedProfileElementChanged(RenderProfileElementEventArgs e) + { + SelectedProfileElementChanged?.Invoke(this, e); + } + + protected virtual void OnSelectedProfileElementUpdated(RenderProfileElementEventArgs e) + { + SelectedProfileElementSaved?.Invoke(this, e); + } + + protected virtual void OnCurrentTimeChanged() + { + CurrentTimeChanged?.Invoke(this, EventArgs.Empty); + } + + protected virtual void OnPixelsPerSecondChanged() + { + PixelsPerSecondChanged?.Invoke(this, EventArgs.Empty); + } + + protected virtual void OnProfilePreviewUpdated() + { + ProfilePreviewUpdated?.Invoke(this, EventArgs.Empty); + } + + protected virtual void OnSelectedDataBindingChanged() + { + SelectedDataBindingChanged?.Invoke(this, EventArgs.Empty); + } + + #endregion } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Utilities/BindingProxy.cs b/src/Artemis.UI.Shared/Utilities/BindingProxy.cs index df793d3a7..a80d70192 100644 --- a/src/Artemis.UI.Shared/Utilities/BindingProxy.cs +++ b/src/Artemis.UI.Shared/Utilities/BindingProxy.cs @@ -2,7 +2,7 @@ namespace Artemis.UI.Shared { - internal class BindingProxy : Freezable + public class BindingProxy : Freezable { // Using a DependencyProperty as the backing store for Data. This enables animation, styling, binding, etc... public static readonly DependencyProperty DataProperty = diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj index 5ae4e77bb..2dfd8751c 100644 --- a/src/Artemis.UI/Artemis.UI.csproj +++ b/src/Artemis.UI/Artemis.UI.csproj @@ -369,11 +369,15 @@ $(DefaultXamlRuntime) - - $(DefaultXamlRuntime) - $(DefaultXamlRuntime) + + $(DefaultXamlRuntime) + Designer + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Converters/NullToVisibilityConverter.cs b/src/Artemis.UI/Converters/NullToVisibilityConverter.cs deleted file mode 100644 index 36c31ae09..000000000 --- a/src/Artemis.UI/Converters/NullToVisibilityConverter.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Globalization; -using System.Windows; -using System.Windows.Data; - -namespace Artemis.UI.Converters -{ - public class NullToVisibilityConverter : IValueConverter - { - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) - { - Parameters direction; - if (parameter == null) - direction = Parameters.Normal; - else - direction = (Parameters) Enum.Parse(typeof(Parameters), (string) parameter); - - if (direction == Parameters.Normal) - { - if (value == null) - return Visibility.Collapsed; - return Visibility.Visible; - } - - if (value == null) - return Visibility.Visible; - return Visibility.Collapsed; - } - - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) - { - throw new NotImplementedException(); - } - - private enum Parameters - { - Normal, - Inverted - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Converters/SolidColorBrushToColorConverter.cs b/src/Artemis.UI/Converters/SolidColorBrushToColorConverter.cs new file mode 100644 index 000000000..1a6c96040 --- /dev/null +++ b/src/Artemis.UI/Converters/SolidColorBrushToColorConverter.cs @@ -0,0 +1,29 @@ +using System; +using System.Globalization; +using System.Windows.Data; +using System.Windows.Media; + +namespace Artemis.UI.Converters +{ + /// + /// + /// Converts into . + /// + [ValueConversion(typeof(SolidColorBrush), typeof(Color))] + public class SolidColorBrushToColorConverter : IValueConverter + { + /// + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is SolidColorBrush brush) + return brush.Color; + return Colors.Transparent; + } + + /// + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return Binding.DoNothing; + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Events/RequestSelectSidebarItemEvent.cs b/src/Artemis.UI/Events/RequestSelectSidebarItemEvent.cs index f4c1e6bf4..3241ac8f8 100644 --- a/src/Artemis.UI/Events/RequestSelectSidebarItemEvent.cs +++ b/src/Artemis.UI/Events/RequestSelectSidebarItemEvent.cs @@ -2,11 +2,11 @@ { public class RequestSelectSidebarItemEvent { - public RequestSelectSidebarItemEvent(string label) + public RequestSelectSidebarItemEvent(string displayName) { - Label = label; + DisplayName = displayName; } - public string Label { get; } + public string DisplayName { get; } } } \ No newline at end of file diff --git a/src/Artemis.UI/Ninject/Factories/IVMFactory.cs b/src/Artemis.UI/Ninject/Factories/IVMFactory.cs index 2f8a72426..1bf08637d 100644 --- a/src/Artemis.UI/Ninject/Factories/IVMFactory.cs +++ b/src/Artemis.UI/Ninject/Factories/IVMFactory.cs @@ -1,9 +1,7 @@ -using Artemis.Core; +using System.Collections.Generic; +using Artemis.Core; using Artemis.Core.Modules; -using Artemis.UI.Screens.Modules; -using Artemis.UI.Screens.Modules.Tabs; using Artemis.UI.Screens.Plugins; -using Artemis.UI.Screens.ProfileEditor; using Artemis.UI.Screens.ProfileEditor.Conditions; using Artemis.UI.Screens.ProfileEditor.LayerProperties; using Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings; @@ -17,12 +15,13 @@ using Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs.AdaptionHints; using Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem; using Artemis.UI.Screens.ProfileEditor.Visualization; using Artemis.UI.Screens.ProfileEditor.Visualization.Tools; -using Artemis.UI.Screens.Settings.Debug; using Artemis.UI.Screens.Settings.Device; using Artemis.UI.Screens.Settings.Device.Tabs; using Artemis.UI.Screens.Settings.Tabs.Devices; using Artemis.UI.Screens.Settings.Tabs.Plugins; using Artemis.UI.Screens.Shared; +using Artemis.UI.Screens.Sidebar; +using Artemis.UI.Screens.Sidebar.Dialogs.ProfileEdit; using Stylet; namespace Artemis.UI.Ninject.Factories @@ -31,14 +30,6 @@ namespace Artemis.UI.Ninject.Factories { } - public interface IModuleVmFactory : IVmFactory - { - ModuleRootViewModel CreateModuleRootViewModel(Module module); - ProfileEditorViewModel CreateProfileEditorViewModel(ProfileModule module); - ActivationRequirementsViewModel CreateActivationRequirementsViewModel(Module module); - ActivationRequirementViewModel CreateActivationRequirementViewModel(IModuleActivationRequirement activationRequirement); - } - public interface ISettingsVmFactory : IVmFactory { PluginSettingsViewModel CreatePluginSettingsViewModel(Plugin plugin); @@ -84,12 +75,12 @@ namespace Artemis.UI.Ninject.Factories public interface IDataModelConditionsVmFactory : IVmFactory { - DataModelConditionGroupViewModel DataModelConditionGroupViewModel(DataModelConditionGroup dataModelConditionGroup, ConditionGroupType groupType); - DataModelConditionListViewModel DataModelConditionListViewModel(DataModelConditionList dataModelConditionList); - DataModelConditionEventViewModel DataModelConditionEventViewModel(DataModelConditionEvent dataModelConditionEvent); - DataModelConditionGeneralPredicateViewModel DataModelConditionGeneralPredicateViewModel(DataModelConditionGeneralPredicate dataModelConditionGeneralPredicate); - DataModelConditionListPredicateViewModel DataModelConditionListPredicateViewModel(DataModelConditionListPredicate dataModelConditionListPredicate); - DataModelConditionEventPredicateViewModel DataModelConditionEventPredicateViewModel(DataModelConditionEventPredicate dataModelConditionEventPredicate); + DataModelConditionGroupViewModel DataModelConditionGroupViewModel(DataModelConditionGroup dataModelConditionGroup, ConditionGroupType groupType, List modules); + DataModelConditionListViewModel DataModelConditionListViewModel(DataModelConditionList dataModelConditionList, List modules); + DataModelConditionEventViewModel DataModelConditionEventViewModel(DataModelConditionEvent dataModelConditionEvent, List modules); + DataModelConditionGeneralPredicateViewModel DataModelConditionGeneralPredicateViewModel(DataModelConditionGeneralPredicate dataModelConditionGeneralPredicate, List modules); + DataModelConditionListPredicateViewModel DataModelConditionListPredicateViewModel(DataModelConditionListPredicate dataModelConditionListPredicate, List modules); + DataModelConditionEventPredicateViewModel DataModelConditionEventPredicateViewModel(DataModelConditionEventPredicate dataModelConditionEventPredicate, List modules); } public interface ILayerPropertyVmFactory : IVmFactory @@ -111,8 +102,15 @@ namespace Artemis.UI.Ninject.Factories PluginPrerequisiteViewModel PluginPrerequisiteViewModel(PluginPrerequisite pluginPrerequisite, bool uninstall); } + public interface ISidebarVmFactory : IVmFactory + { + SidebarCategoryViewModel SidebarCategoryViewModel(ProfileCategory profileCategory); + SidebarProfileConfigurationViewModel SidebarProfileConfigurationViewModel(ProfileConfiguration profileConfiguration); + ModuleActivationRequirementViewModel ModuleActivationRequirementViewModel(IModuleActivationRequirement activationRequirement); + } + // TODO: Move these two - public interface IDataBindingsVmFactory + public interface IDataBindingsVmFactory { IDataBindingViewModel DataBindingViewModel(IDataBindingRegistration registration); DirectDataBindingModeViewModel DirectDataBindingModeViewModel(DirectDataBinding directDataBinding); @@ -121,7 +119,7 @@ namespace Artemis.UI.Ninject.Factories DataBindingConditionViewModel DataBindingConditionViewModel(DataBindingCondition dataBindingCondition); } - public interface IPropertyVmFactory + public interface IPropertyVmFactory { ITreePropertyViewModel TreePropertyViewModel(ILayerProperty layerProperty, LayerPropertyViewModel layerPropertyViewModel); ITimelinePropertyViewModel TimelinePropertyViewModel(ILayerProperty layerProperty, LayerPropertyViewModel layerPropertyViewModel); diff --git a/src/Artemis.UI/Screens/Modules/ModuleRootView.xaml b/src/Artemis.UI/Screens/Modules/ModuleRootView.xaml deleted file mode 100644 index 0278e9576..000000000 --- a/src/Artemis.UI/Screens/Modules/ModuleRootView.xaml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Modules/ModuleRootViewModel.cs b/src/Artemis.UI/Screens/Modules/ModuleRootViewModel.cs deleted file mode 100644 index fba336e81..000000000 --- a/src/Artemis.UI/Screens/Modules/ModuleRootViewModel.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Artemis.Core.Modules; -using Artemis.UI.Ninject.Factories; -using Artemis.UI.Shared.Modules; -using Ninject; -using Ninject.Parameters; -using Stylet; - -namespace Artemis.UI.Screens.Modules -{ - public class ModuleRootViewModel : Conductor.Collection.OneActive - { - private readonly IModuleVmFactory _moduleVmFactory; - - public ModuleRootViewModel(Module module, IModuleVmFactory moduleVmFactory) - { - DisplayName = module?.DisplayName; - Module = module; - - _moduleVmFactory = moduleVmFactory; - } - - public Module Module { get; } - - protected override void OnInitialActivate() - { - AddTabs(); - base.OnInitialActivate(); - } - - private void AddTabs() - { - // Create the profile editor and module VMs - if (Module is ProfileModule profileModule) - Items.Add(_moduleVmFactory.CreateProfileEditorViewModel(profileModule)); - - if (Module.ActivationRequirements.Any()) - Items.Add(_moduleVmFactory.CreateActivationRequirementsViewModel(Module)); - - if (Module.ModuleTabs != null) - { - List moduleTabs = new(Module.ModuleTabs); - foreach (ModuleTab moduleTab in moduleTabs.Where(m => m != null)) - { - ConstructorArgument module = new("module", Module); - ConstructorArgument displayName = new("displayName", DisplayName); - - ModuleViewModel viewModel = (ModuleViewModel) Module.Plugin.Kernel.Get(moduleTab.Type, module, displayName); - Items.Add(viewModel); - } - } - - ActiveItem = Items.FirstOrDefault(); - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Modules/Tabs/ActivationRequirementView.xaml b/src/Artemis.UI/Screens/Modules/Tabs/ActivationRequirementView.xaml deleted file mode 100644 index d77c7da0d..000000000 --- a/src/Artemis.UI/Screens/Modules/Tabs/ActivationRequirementView.xaml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Modules/Tabs/ActivationRequirementsView.xaml b/src/Artemis.UI/Screens/Modules/Tabs/ActivationRequirementsView.xaml deleted file mode 100644 index 10086633c..000000000 --- a/src/Artemis.UI/Screens/Modules/Tabs/ActivationRequirementsView.xaml +++ /dev/null @@ -1,54 +0,0 @@ - - - - - Activation requirements - - This module has built-in activation requirements and won't activate until - . - These requirements allow the module creator to decide when the module is activated and you cannot override them. - - - - Note: While you have the profile editor open the module is always activated and any other modules are deactivated. - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Modules/Tabs/ActivationRequirementsViewModel.cs b/src/Artemis.UI/Screens/Modules/Tabs/ActivationRequirementsViewModel.cs deleted file mode 100644 index eb68da1e4..000000000 --- a/src/Artemis.UI/Screens/Modules/Tabs/ActivationRequirementsViewModel.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Linq; -using Artemis.Core.Modules; -using Artemis.UI.Ninject.Factories; -using Stylet; - -namespace Artemis.UI.Screens.Modules.Tabs -{ - public class ActivationRequirementsViewModel : Conductor.Collection.AllActive - { - private readonly IModuleVmFactory _moduleVmFactory; - - public ActivationRequirementsViewModel(Module module, IModuleVmFactory moduleVmFactory) - { - _moduleVmFactory = moduleVmFactory; - - DisplayName = "ACTIVATION REQUIREMENTS"; - Module = module; - - ActivationType = Module.ActivationRequirementMode == ActivationRequirementType.All - ? "all requirements are met" - : "any requirement is met"; - } - - public Module Module { get; } - - public string ActivationType { get; set; } - - protected override void OnActivate() - { - Items.Clear(); - Items.AddRange(Module.ActivationRequirements.Select(_moduleVmFactory.CreateActivationRequirementViewModel)); - - base.OnActivate(); - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/News/NewsView.xaml b/src/Artemis.UI/Screens/News/NewsView.xaml deleted file mode 100644 index 5b248fef1..000000000 --- a/src/Artemis.UI/Screens/News/NewsView.xaml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - News is not yet implemented - - - The news page will keep you up-to-date with the latest developments in the Artemis community. - You'll find the latest patch notes here and see featured workshop contributions. - - - - \ No newline at end of file diff --git a/src/Artemis.UI/Screens/News/NewsViewModel.cs b/src/Artemis.UI/Screens/News/NewsViewModel.cs deleted file mode 100644 index d93080fd3..000000000 --- a/src/Artemis.UI/Screens/News/NewsViewModel.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Stylet; - -namespace Artemis.UI.Screens.News -{ - public class NewsViewModel : Screen, IMainScreenViewModel - { - public NewsViewModel() - { - DisplayName = "News"; - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Abstract/DataModelConditionPredicateViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Abstract/DataModelConditionPredicateViewModel.cs index a38c17ddb..9995cd02f 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Abstract/DataModelConditionPredicateViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Abstract/DataModelConditionPredicateViewModel.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Windows; using System.Windows.Media; using Artemis.Core; +using Artemis.Core.Modules; using Artemis.Core.Services; using Artemis.UI.Shared; using Artemis.UI.Shared.Input; @@ -16,6 +17,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions.Abstract { private readonly IConditionOperatorService _conditionOperatorService; private readonly IDataModelUIService _dataModelUIService; + private readonly List _modules; private readonly IProfileEditorService _profileEditorService; private DataModelStaticViewModel _rightSideInputViewModel; private DataModelDynamicViewModel _rightSideSelectionViewModel; @@ -25,11 +27,13 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions.Abstract protected DataModelConditionPredicateViewModel( DataModelConditionPredicate dataModelConditionPredicate, + List modules, IProfileEditorService profileEditorService, IDataModelUIService dataModelUIService, IConditionOperatorService conditionOperatorService, ISettingsService settingsService) : base(dataModelConditionPredicate) { + _modules = modules; _profileEditorService = profileEditorService; _dataModelUIService = dataModelUIService; _conditionOperatorService = conditionOperatorService; @@ -44,6 +48,8 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions.Abstract public DataModelConditionPredicate DataModelConditionPredicate => (DataModelConditionPredicate) Model; public PluginSetting ShowDataModelValues { get; } + public bool CanSelectOperator => DataModelConditionPredicate.LeftPath is {IsValid: true}; + public BaseConditionOperator SelectedOperator { get => _selectedOperator; @@ -75,12 +81,12 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions.Abstract public override void Delete() { base.Delete(); - _profileEditorService.UpdateSelectedProfileElement(); + _profileEditorService.SaveSelectedProfileElement(); } public virtual void Initialize() { - LeftSideSelectionViewModel = _dataModelUIService.GetDynamicSelectionViewModel(_profileEditorService.GetCurrentModule()); + LeftSideSelectionViewModel = _dataModelUIService.GetDynamicSelectionViewModel(_modules); LeftSideSelectionViewModel.PropertySelected += LeftSideOnPropertySelected; if (LeftSideColor != null) LeftSideSelectionViewModel.ButtonBrush = LeftSideColor; @@ -107,10 +113,11 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions.Abstract else if (!Operators.Contains(DataModelConditionPredicate.Operator)) DataModelConditionPredicate.UpdateOperator(Operators.FirstOrDefault(o => o.Description == DataModelConditionPredicate.Operator.Description) ?? Operators.FirstOrDefault()); + NotifyOfPropertyChange(nameof(CanSelectOperator)); SelectedOperator = DataModelConditionPredicate.Operator; // Without a selected operator or one that supports a right side, leave the right side input empty - if (SelectedOperator == null || SelectedOperator.RightSideType == null) + if (SelectedOperator?.RightSideType == null) { DisposeRightSideStaticViewModel(); DisposeRightSideDynamicViewModel(); @@ -132,7 +139,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions.Abstract DisposeRightSideDynamicViewModel(); if (RightSideInputViewModel == null) CreateRightSideInputViewModel(); - + Type preferredType = DataModelConditionPredicate.GetPreferredRightSideType(); if (preferredType != null && RightSideInputViewModel.TargetType != preferredType) RightSideInputViewModel.UpdateTargetType(preferredType); @@ -149,7 +156,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions.Abstract return; DataModelConditionPredicate.UpdateLeftSide(LeftSideSelectionViewModel.DataModelPath); - _profileEditorService.UpdateSelectedProfileElement(); + _profileEditorService.SaveSelectedProfileElement(); SelectedOperator = DataModelConditionPredicate.Operator; Update(); @@ -158,7 +165,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions.Abstract public void ApplyRightSideDynamic() { DataModelConditionPredicate.UpdateRightSideDynamic(RightSideSelectionViewModel.DataModelPath); - _profileEditorService.UpdateSelectedProfileElement(); + _profileEditorService.SaveSelectedProfileElement(); Update(); } @@ -166,7 +173,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions.Abstract public void ApplyRightSideStatic(object value) { DataModelConditionPredicate.UpdateRightSideStatic(value); - _profileEditorService.UpdateSelectedProfileElement(); + _profileEditorService.SaveSelectedProfileElement(); Update(); } @@ -174,7 +181,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions.Abstract public void ApplyOperator() { DataModelConditionPredicate.UpdateOperator(SelectedOperator); - _profileEditorService.UpdateSelectedProfileElement(); + _profileEditorService.SaveSelectedProfileElement(); Update(); } @@ -196,6 +203,26 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions.Abstract ApplyOperator(); } + public override void UpdateModules() + { + if (LeftSideSelectionViewModel != null) + { + LeftSideSelectionViewModel.PropertySelected -= LeftSideOnPropertySelected; + LeftSideSelectionViewModel.Dispose(); + LeftSideSelectionViewModel = null; + } + DisposeRightSideStaticViewModel(); + DisposeRightSideDynamicViewModel(); + + // If the modules changed the paths may no longer be valid if they targeted a module no longer available, in that case clear the path + if (DataModelConditionPredicate.LeftPath?.Target != null && !DataModelConditionPredicate.LeftPath.Target.IsExpansion && !_modules.Contains(DataModelConditionPredicate.LeftPath.Target.Module)) + DataModelConditionPredicate.UpdateLeftSide(null); + if (DataModelConditionPredicate.RightPath?.Target != null && !DataModelConditionPredicate.RightPath.Target.IsExpansion && !_modules.Contains(DataModelConditionPredicate.RightPath.Target.Module)) + DataModelConditionPredicate.UpdateRightSideDynamic(null); + + Initialize(); + } + #region IDisposable protected virtual void Dispose(bool disposing) @@ -227,7 +254,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions.Abstract private void CreateRightSideSelectionViewModel() { - RightSideSelectionViewModel = _dataModelUIService.GetDynamicSelectionViewModel(_profileEditorService.GetCurrentModule()); + RightSideSelectionViewModel = _dataModelUIService.GetDynamicSelectionViewModel(_modules); RightSideSelectionViewModel.ButtonBrush = (SolidColorBrush) Application.Current.FindResource("PrimaryHueMidBrush"); RightSideSelectionViewModel.DisplaySwitchButton = true; RightSideSelectionViewModel.PropertySelected += RightSideOnPropertySelected; diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Abstract/DataModelConditionViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Abstract/DataModelConditionViewModel.cs index b2f94a711..9f7db7a2d 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Abstract/DataModelConditionViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Abstract/DataModelConditionViewModel.cs @@ -63,5 +63,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions.Abstract groupViewModel.ConvertToPredicate(this); return true; } + + public abstract void UpdateModules(); } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionEventViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionEventViewModel.cs index ab685f689..d50edbbc9 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionEventViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionEventViewModel.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Windows.Media; using Artemis.Core; +using Artemis.Core.Modules; using Artemis.UI.Ninject.Factories; using Artemis.UI.Screens.ProfileEditor.Conditions.Abstract; using Artemis.UI.Shared; @@ -14,15 +15,18 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions { private readonly IDataModelConditionsVmFactory _dataModelConditionsVmFactory; private readonly IDataModelUIService _dataModelUIService; + private readonly List _modules; private readonly IProfileEditorService _profileEditorService; private DateTime _lastTrigger; private string _triggerPastParticiple; public DataModelConditionEventViewModel(DataModelConditionEvent dataModelConditionEvent, + List modules, IProfileEditorService profileEditorService, IDataModelUIService dataModelUIService, IDataModelConditionsVmFactory dataModelConditionsVmFactory) : base(dataModelConditionEvent) { + _modules = modules; _profileEditorService = profileEditorService; _dataModelUIService = dataModelUIService; _dataModelConditionsVmFactory = dataModelConditionsVmFactory; @@ -46,7 +50,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions public void Initialize() { - LeftSideSelectionViewModel = _dataModelUIService.GetDynamicSelectionViewModel(_profileEditorService.GetCurrentModule()); + LeftSideSelectionViewModel = _dataModelUIService.GetDynamicSelectionViewModel(_modules); LeftSideSelectionViewModel.PropertySelected += LeftSideSelectionViewModelOnPropertySelected; LeftSideSelectionViewModel.LoadEventChildren = false; @@ -82,7 +86,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions if (!(childModel is DataModelConditionGroup dataModelConditionGroup)) continue; - DataModelConditionGroupViewModel viewModel = _dataModelConditionsVmFactory.DataModelConditionGroupViewModel(dataModelConditionGroup, ConditionGroupType.Event); + DataModelConditionGroupViewModel viewModel = _dataModelConditionsVmFactory.DataModelConditionGroupViewModel(dataModelConditionGroup, ConditionGroupType.Event, _modules); viewModel.IsRootGroup = true; viewModels.Add(viewModel); } @@ -103,11 +107,18 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions public void ApplyEvent() { DataModelConditionEvent.UpdateEvent(LeftSideSelectionViewModel.DataModelPath); - _profileEditorService.UpdateSelectedProfileElement(); + _profileEditorService.SaveSelectedProfileElement(); Update(); } + public override void UpdateModules() + { + LeftSideSelectionViewModel.Dispose(); + LeftSideSelectionViewModel.PropertySelected -= LeftSideSelectionViewModelOnPropertySelected; + Initialize(); + } + protected override void OnInitialActivate() { Initialize(); diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionGroupViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionGroupViewModel.cs index de5822a2c..c649a9e16 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionGroupViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionGroupViewModel.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Linq; using Artemis.Core; +using Artemis.Core.Modules; +using Artemis.Core.Services; using Artemis.UI.Extensions; using Artemis.UI.Ninject.Factories; using Artemis.UI.Screens.ProfileEditor.Conditions.Abstract; @@ -14,17 +16,23 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions public class DataModelConditionGroupViewModel : DataModelConditionViewModel { private readonly IDataModelConditionsVmFactory _dataModelConditionsVmFactory; + private readonly List _modules; + private readonly ICoreService _coreService; private readonly IProfileEditorService _profileEditorService; private bool _isEventGroup; private bool _isRootGroup; public DataModelConditionGroupViewModel(DataModelConditionGroup dataModelConditionGroup, ConditionGroupType groupType, + List modules, + ICoreService coreService, IProfileEditorService profileEditorService, IDataModelConditionsVmFactory dataModelConditionsVmFactory) : base(dataModelConditionGroup) { GroupType = groupType; + _modules = modules; + _coreService = coreService; _profileEditorService = profileEditorService; _dataModelConditionsVmFactory = dataModelConditionsVmFactory; @@ -66,7 +74,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions DataModelConditionGroup.BooleanOperator = enumValue; NotifyOfPropertyChange(nameof(SelectedBooleanOperator)); - _profileEditorService.UpdateSelectedProfileElement(); + _profileEditorService.SaveSelectedProfileElement(); } public void AddCondition() @@ -87,7 +95,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions } Update(); - _profileEditorService.UpdateSelectedProfileElement(); + _profileEditorService.SaveSelectedProfileElement(); } public void AddEventCondition() @@ -104,7 +112,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions DataModelConditionGroup.AddChild(new DataModelConditionEvent(DataModelConditionGroup), index); Update(); - _profileEditorService.UpdateSelectedProfileElement(); + _profileEditorService.SaveSelectedProfileElement(); } public void AddGroup() @@ -112,7 +120,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions DataModelConditionGroup.AddChild(new DataModelConditionGroup(DataModelConditionGroup)); Update(); - _profileEditorService.UpdateSelectedProfileElement(); + _profileEditorService.SaveSelectedProfileElement(); } public override void Update() @@ -131,22 +139,22 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions switch (childModel) { case DataModelConditionGroup dataModelConditionGroup: - Items.Add(_dataModelConditionsVmFactory.DataModelConditionGroupViewModel(dataModelConditionGroup, GroupType)); + Items.Add(_dataModelConditionsVmFactory.DataModelConditionGroupViewModel(dataModelConditionGroup, GroupType, _modules)); break; case DataModelConditionList dataModelConditionList: - Items.Add(_dataModelConditionsVmFactory.DataModelConditionListViewModel(dataModelConditionList)); + Items.Add(_dataModelConditionsVmFactory.DataModelConditionListViewModel(dataModelConditionList, _modules)); break; case DataModelConditionEvent dataModelConditionEvent: - Items.Add(_dataModelConditionsVmFactory.DataModelConditionEventViewModel(dataModelConditionEvent)); + Items.Add(_dataModelConditionsVmFactory.DataModelConditionEventViewModel(dataModelConditionEvent, _modules)); break; case DataModelConditionGeneralPredicate dataModelConditionGeneralPredicate: - Items.Add(_dataModelConditionsVmFactory.DataModelConditionGeneralPredicateViewModel(dataModelConditionGeneralPredicate)); + Items.Add(_dataModelConditionsVmFactory.DataModelConditionGeneralPredicateViewModel(dataModelConditionGeneralPredicate, _modules)); break; case DataModelConditionListPredicate dataModelConditionListPredicate: - Items.Add(_dataModelConditionsVmFactory.DataModelConditionListPredicateViewModel(dataModelConditionListPredicate)); + Items.Add(_dataModelConditionsVmFactory.DataModelConditionListPredicateViewModel(dataModelConditionListPredicate, _modules)); break; case DataModelConditionEventPredicate dataModelConditionEventPredicate: - Items.Add(_dataModelConditionsVmFactory.DataModelConditionEventPredicateViewModel(dataModelConditionEventPredicate)); + Items.Add(_dataModelConditionsVmFactory.DataModelConditionEventPredicateViewModel(dataModelConditionEventPredicate, _modules)); break; } } @@ -173,6 +181,12 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions dataModelConditionViewModel.Evaluate(); } + public override void UpdateModules() + { + foreach (DataModelConditionViewModel dataModelConditionViewModel in Items) + dataModelConditionViewModel.UpdateModules(); + } + public void ConvertToConditionList(DataModelConditionViewModel predicateViewModel) { // Store the old index and remove the old predicate @@ -203,8 +217,33 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions Update(); } + private void CoreServiceOnFrameRendered(object? sender, FrameRenderedEventArgs e) + { + if (IsRootGroup) + Evaluate(); + } + public event EventHandler Updated; + #region Overrides of Screen + + /// + protected override void OnInitialActivate() + { + base.OnInitialActivate(); + Update(); + _coreService.FrameRendered += CoreServiceOnFrameRendered; + } + + /// + protected override void OnClose() + { + _coreService.FrameRendered -= CoreServiceOnFrameRendered; + base.OnClose(); + } + + #endregion + protected virtual void OnUpdated() { Updated?.Invoke(this, EventArgs.Empty); diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionListViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionListViewModel.cs index 746955bba..c4e244b66 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionListViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionListViewModel.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Windows.Forms.VisualStyles; using System.Windows.Media; using Artemis.Core; +using Artemis.Core.Modules; using Artemis.UI.Ninject.Factories; using Artemis.UI.Screens.ProfileEditor.Conditions.Abstract; using Artemis.UI.Shared; @@ -16,14 +16,16 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions { private readonly IDataModelConditionsVmFactory _dataModelConditionsVmFactory; private readonly IDataModelUIService _dataModelUIService; + private readonly List _modules; private readonly IProfileEditorService _profileEditorService; - public DataModelConditionListViewModel( - DataModelConditionList dataModelConditionList, + public DataModelConditionListViewModel(DataModelConditionList dataModelConditionList, + List modules, IProfileEditorService profileEditorService, IDataModelUIService dataModelUIService, IDataModelConditionsVmFactory dataModelConditionsVmFactory) : base(dataModelConditionList) { + _modules = modules; _profileEditorService = profileEditorService; _dataModelUIService = dataModelUIService; _dataModelConditionsVmFactory = dataModelConditionsVmFactory; @@ -39,7 +41,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions DataModelConditionList.ListOperator = enumValue; NotifyOfPropertyChange(nameof(SelectedListOperator)); - _profileEditorService.UpdateSelectedProfileElement(); + _profileEditorService.SaveSelectedProfileElement(); } public void AddCondition() @@ -47,7 +49,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions DataModelConditionList.AddChild(new DataModelConditionGeneralPredicate(DataModelConditionList, ProfileRightSideType.Dynamic)); Update(); - _profileEditorService.UpdateSelectedProfileElement(); + _profileEditorService.SaveSelectedProfileElement(); } public void AddGroup() @@ -55,25 +57,32 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions DataModelConditionList.AddChild(new DataModelConditionGroup(DataModelConditionList)); Update(); - _profileEditorService.UpdateSelectedProfileElement(); + _profileEditorService.SaveSelectedProfileElement(); } public override void Evaluate() { IsConditionMet = DataModelConditionList.Evaluate(); - foreach (DataModelConditionViewModel dataModelConditionViewModel in Items) + foreach (DataModelConditionViewModel dataModelConditionViewModel in Items) dataModelConditionViewModel.Evaluate(); } public override void Delete() { base.Delete(); - _profileEditorService.UpdateSelectedProfileElement(); + _profileEditorService.SaveSelectedProfileElement(); + } + + /// + public override void UpdateModules() + { + foreach (DataModelConditionViewModel dataModelConditionViewModel in Items) + dataModelConditionViewModel.UpdateModules(); } public void Initialize() { - LeftSideSelectionViewModel = _dataModelUIService.GetDynamicSelectionViewModel(_profileEditorService.GetCurrentModule()); + LeftSideSelectionViewModel = _dataModelUIService.GetDynamicSelectionViewModel(_modules); LeftSideSelectionViewModel.PropertySelected += LeftSideSelectionViewModelOnPropertySelected; IReadOnlyCollection editors = _dataModelUIService.RegisteredDataModelEditors; @@ -96,7 +105,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions return; DataModelConditionList.UpdateList(LeftSideSelectionViewModel.DataModelPath); - _profileEditorService.UpdateSelectedProfileElement(); + _profileEditorService.SaveSelectedProfileElement(); Update(); } @@ -120,7 +129,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions if (!(childModel is DataModelConditionGroup dataModelConditionGroup)) continue; - DataModelConditionGroupViewModel viewModel = _dataModelConditionsVmFactory.DataModelConditionGroupViewModel(dataModelConditionGroup, ConditionGroupType.List); + DataModelConditionGroupViewModel viewModel = _dataModelConditionsVmFactory.DataModelConditionGroupViewModel(dataModelConditionGroup, ConditionGroupType.List, _modules); viewModel.IsRootGroup = true; viewModels.Add(viewModel); } diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionEventPredicateView.xaml b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionEventPredicateView.xaml index 3fb1a1bd9..a90fc0b13 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionEventPredicateView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionEventPredicateView.xaml @@ -55,6 +55,7 @@ Background="#7B7B7B" BorderBrush="#7B7B7B" Content="{Binding SelectedOperator.Description}" + IsEnabled="{Binding CanSelectOperator}" Click="PropertyButton_OnClick"> diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionEventPredicateViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionEventPredicateViewModel.cs index 185ec12c0..90fb5702c 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionEventPredicateViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionEventPredicateViewModel.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Windows.Media; using Artemis.Core; +using Artemis.Core.Modules; using Artemis.Core.Services; using Artemis.UI.Extensions; using Artemis.UI.Screens.ProfileEditor.Conditions.Abstract; @@ -16,11 +17,12 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions private readonly IDataModelUIService _dataModelUIService; public DataModelConditionEventPredicateViewModel(DataModelConditionEventPredicate dataModelConditionEventPredicate, + List modules, IProfileEditorService profileEditorService, IDataModelUIService dataModelUIService, IConditionOperatorService conditionOperatorService, ISettingsService settingsService) - : base(dataModelConditionEventPredicate, profileEditorService, dataModelUIService, conditionOperatorService, settingsService) + : base(dataModelConditionEventPredicate, modules, profileEditorService, dataModelUIService, conditionOperatorService, settingsService) { _dataModelUIService = dataModelUIService; diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionGeneralPredicateView.xaml b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionGeneralPredicateView.xaml index 6bd65d3e7..eab1e69d3 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionGeneralPredicateView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionGeneralPredicateView.xaml @@ -56,6 +56,7 @@ Background="#7B7B7B" BorderBrush="#7B7B7B" Content="{Binding SelectedOperator.Description}" + IsEnabled="{Binding CanSelectOperator}" Click="PropertyButton_OnClick"> diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionGeneralPredicateViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionGeneralPredicateViewModel.cs index 96c2382b0..f02494b7f 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionGeneralPredicateViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionGeneralPredicateViewModel.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using Artemis.Core; +using Artemis.Core.Modules; using Artemis.Core.Services; using Artemis.UI.Screens.ProfileEditor.Conditions.Abstract; using Artemis.UI.Shared; @@ -14,11 +15,12 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions private readonly IDataModelUIService _dataModelUIService; public DataModelConditionGeneralPredicateViewModel(DataModelConditionGeneralPredicate dataModelConditionGeneralPredicate, + List modules, IProfileEditorService profileEditorService, IDataModelUIService dataModelUIService, IConditionOperatorService conditionOperatorService, ISettingsService settingsService) - : base(dataModelConditionGeneralPredicate, profileEditorService, dataModelUIService, conditionOperatorService, settingsService) + : base(dataModelConditionGeneralPredicate, modules, profileEditorService, dataModelUIService, conditionOperatorService, settingsService) { _dataModelUIService = dataModelUIService; } diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionListPredicateView.xaml b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionListPredicateView.xaml index b22c824d3..5a273a099 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionListPredicateView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionListPredicateView.xaml @@ -55,6 +55,7 @@ Background="#7B7B7B" BorderBrush="#7B7B7B" Content="{Binding SelectedOperator.Description}" + IsEnabled="{Binding CanSelectOperator}" Click="PropertyButton_OnClick"> diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionListPredicateViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionListPredicateViewModel.cs index 2ca951ee8..1a9f947b5 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionListPredicateViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionListPredicateViewModel.cs @@ -3,8 +3,8 @@ using System.Collections.Generic; using System.Linq; using System.Windows.Media; using Artemis.Core; +using Artemis.Core.Modules; using Artemis.Core.Services; -using Artemis.UI.Extensions; using Artemis.UI.Screens.ProfileEditor.Conditions.Abstract; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; @@ -16,11 +16,12 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions private readonly IDataModelUIService _dataModelUIService; public DataModelConditionListPredicateViewModel(DataModelConditionListPredicate dataModelConditionListPredicate, + List modules, IProfileEditorService profileEditorService, IDataModelUIService dataModelUIService, IConditionOperatorService conditionOperatorService, ISettingsService settingsService) - : base(dataModelConditionListPredicate, profileEditorService, dataModelUIService, conditionOperatorService, settingsService) + : base(dataModelConditionListPredicate, modules, profileEditorService, dataModelUIService, conditionOperatorService, settingsService) { _dataModelUIService = dataModelUIService; @@ -44,6 +45,17 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions } } + public override void Evaluate() + { + throw new NotImplementedException(); + } + + public override void UpdateModules() + { + foreach (DataModelConditionViewModel dataModelConditionViewModel in Items) + dataModelConditionViewModel.UpdateModules(); + } + protected override void OnInitialActivate() { base.OnInitialActivate(); @@ -81,10 +93,5 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions return wrapper.CreateViewModel(_dataModelUIService, new DataModelUpdateConfiguration(true)); } - - public override void Evaluate() - { - throw new NotImplementedException(); - } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileCreateView.xaml b/src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileCreateView.xaml deleted file mode 100644 index 092967c07..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileCreateView.xaml +++ /dev/null @@ -1,30 +0,0 @@ - - - - Add a new profile - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileCreateViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileCreateViewModel.cs deleted file mode 100644 index 75be9f097..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileCreateViewModel.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Threading.Tasks; -using Artemis.UI.Shared.Services; -using FluentValidation; -using Stylet; - -namespace Artemis.UI.Screens.ProfileEditor.Dialogs -{ - public class ProfileCreateViewModel : DialogViewModelBase - { - private string _profileName; - - public ProfileCreateViewModel(IModelValidator validator) : base(validator) - { - } - - public string ProfileName - { - get => _profileName; - set => SetAndNotify(ref _profileName, value); - } - - public async Task Accept() - { - await ValidateAsync(); - - if (HasErrors) - return; - - Session.Close(ProfileName); - } - } - - public class ProfileCreateViewModelValidator : AbstractValidator - { - public ProfileCreateViewModelValidator() - { - RuleFor(m => m.ProfileName).NotEmpty().WithMessage("Profile name may not be empty"); - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileEditViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileEditViewModel.cs deleted file mode 100644 index 5fc6693d9..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileEditViewModel.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Threading.Tasks; -using Artemis.Core; -using Artemis.UI.Shared.Services; -using FluentValidation; -using Stylet; - -namespace Artemis.UI.Screens.ProfileEditor.Dialogs -{ - public class ProfileEditViewModel : DialogViewModelBase - { - private string _profileName; - - public ProfileEditViewModel(IModelValidator validator, Profile profile) : base(validator) - { - ProfileName = profile.Name; - } - - public string ProfileName - { - get => _profileName; - set => SetAndNotify(ref _profileName, value); - } - - public async Task Accept() - { - await ValidateAsync(); - - if (HasErrors) - return; - - Session.Close(ProfileName); - } - } - - public class ProfileEditViewModelValidator : AbstractValidator - { - public ProfileEditViewModelValidator() - { - RuleFor(m => m.ProfileName).NotEmpty().WithMessage("Profile name may not be empty"); - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileExportView.xaml b/src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileExportView.xaml deleted file mode 100644 index 4db9dc873..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileExportView.xaml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - Export current profile - - - - It looks like you have not set up any profile adaption hints. This means Artemis can't do much to make your profile look good on a different surface other than try finding the same LEDs as you have. - - To configure adaption hints, right-click on a layer and choose View Adaption Hints. - - To learn more about profile adaption, check out - - this wiki article - . - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileExportViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileExportViewModel.cs deleted file mode 100644 index 7b6758ee9..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileExportViewModel.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Windows; -using System.Windows.Navigation; -using Artemis.Core; -using Artemis.Core.Services; -using Artemis.UI.Shared.Services; - -namespace Artemis.UI.Screens.ProfileEditor.Dialogs -{ - public class ProfileExportViewModel : DialogViewModelBase - { - private readonly IProfileService _profileService; - private readonly IMessageService _messageService; - - public ProfileExportViewModel(ProfileDescriptor profileDescriptor, IProfileService profileService, IMessageService messageService) - { - ProfileDescriptor = profileDescriptor; - - _profileService = profileService; - _messageService = messageService; - } - - public ProfileDescriptor ProfileDescriptor { get; } - - #region Overrides of Screen - - /// - protected override void OnActivate() - { - // TODO: If the profile has hints on all layers, call Accept - base.OnActivate(); - } - - #endregion - - public void OpenHyperlink(object sender, RequestNavigateEventArgs e) - { - Core.Utilities.OpenUrl(e.Uri.AbsoluteUri); - } - - public void Accept() - { - string encoded = _profileService.ExportProfile(ProfileDescriptor); - Clipboard.SetText(encoded); - _messageService.ShowMessage("Profile contents exported to clipboard."); - - Session.Close(); - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileImportView.xaml b/src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileImportView.xaml deleted file mode 100644 index 049e78217..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileImportView.xaml +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - Import profile to current module - - - - Please note that importing profiles like this is placeholder functionality. The idea is that this will eventually happen via the workshop. - - - - The workshop will include tools to make profiles convert easily and look good on different layouts. - That means right now when you import this profile unless you have the exact same setup as - the person who exported it, you'll have to select LEDs for each layer in the profile. - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileImportViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileImportViewModel.cs deleted file mode 100644 index a6ba778cc..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileImportViewModel.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Artemis.Core; -using Artemis.Core.Modules; -using Artemis.Core.Services; -using Artemis.UI.Shared.Services; - -namespace Artemis.UI.Screens.ProfileEditor.Dialogs -{ - public class ProfileImportViewModel : DialogViewModelBase - { - private readonly IProfileService _profileService; - private readonly IMessageService _messageService; - private string _profileJson; - - public ProfileImportViewModel(ProfileModule profileModule, IProfileService profileService, IMessageService messageService) - { - ProfileModule = profileModule; - - _profileService = profileService; - _messageService = messageService; - } - - public ProfileModule ProfileModule { get; } - - public string ProfileJson - { - get => _profileJson; - set => SetAndNotify(ref _profileJson, value); - } - - public void Accept() - { - ProfileDescriptor descriptor = _profileService.ImportProfile(ProfileJson, ProfileModule); - _messageService.ShowMessage("Profile imported."); - Session.Close(descriptor); - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs index b203639b2..1ac940b11 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; using System.ComponentModel; using System.Linq; using Artemis.Core; +using Artemis.Core.Modules; using Artemis.Core.Services; using Artemis.UI.Ninject.Factories; using Artemis.UI.Screens.ProfileEditor.Conditions; @@ -14,17 +16,15 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions public class DisplayConditionsViewModel : Conductor, IProfileEditorPanelViewModel { private readonly IDataModelConditionsVmFactory _dataModelConditionsVmFactory; - private readonly ICoreService _coreService; private readonly IProfileEditorService _profileEditorService; private RenderProfileElement _renderProfileElement; private bool _displayStartHint; private bool _isEventCondition; - public DisplayConditionsViewModel(IProfileEditorService profileEditorService, IDataModelConditionsVmFactory dataModelConditionsVmFactory, ICoreService coreService) + public DisplayConditionsViewModel(IProfileEditorService profileEditorService, IDataModelConditionsVmFactory dataModelConditionsVmFactory) { _profileEditorService = profileEditorService; _dataModelConditionsVmFactory = dataModelConditionsVmFactory; - _coreService = coreService; } public bool DisplayStartHint @@ -59,7 +59,7 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions TimelinePlayMode playMode = value ? TimelinePlayMode.Repeat : TimelinePlayMode.Once; if (RenderProfileElement == null || RenderProfileElement?.Timeline.PlayMode == playMode) return; RenderProfileElement.Timeline.PlayMode = playMode; - _profileEditorService.UpdateSelectedProfileElement(); + _profileEditorService.SaveSelectedProfileElement(); } } @@ -71,7 +71,7 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions TimelineStopMode stopMode = value ? TimelineStopMode.Finish : TimelineStopMode.SkipToEnd; if (RenderProfileElement == null || RenderProfileElement?.Timeline.StopMode == stopMode) return; RenderProfileElement.Timeline.StopMode = stopMode; - _profileEditorService.UpdateSelectedProfileElement(); + _profileEditorService.SaveSelectedProfileElement(); } } @@ -82,7 +82,7 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions { if (RenderProfileElement == null || RenderProfileElement?.Timeline.EventOverlapMode == value) return; RenderProfileElement.Timeline.EventOverlapMode = value; - _profileEditorService.UpdateSelectedProfileElement(); + _profileEditorService.SaveSelectedProfileElement(); } } @@ -90,19 +90,17 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions protected override void OnInitialActivate() { - _profileEditorService.ProfileElementSelected += ProfileEditorServiceOnProfileElementSelected; - _coreService.FrameRendered += CoreServiceOnFrameRendered; + _profileEditorService.SelectedProfileElementChanged += SelectedProfileEditorServiceOnSelectedProfileElementChanged; base.OnInitialActivate(); } protected override void OnClose() { - _profileEditorService.ProfileElementSelected -= ProfileEditorServiceOnProfileElementSelected; - _coreService.FrameRendered -= CoreServiceOnFrameRendered; + _profileEditorService.SelectedProfileElementChanged -= SelectedProfileEditorServiceOnSelectedProfileElementChanged; base.OnClose(); } - private void ProfileEditorServiceOnProfileElementSelected(object sender, RenderProfileElementEventArgs e) + private void SelectedProfileEditorServiceOnSelectedProfileElementChanged(object sender, RenderProfileElementEventArgs e) { if (RenderProfileElement != null) { @@ -127,9 +125,11 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions if (e.RenderProfileElement.DisplayCondition == null) e.RenderProfileElement.DisplayCondition = new DataModelConditionGroup(null); - ActiveItem = _dataModelConditionsVmFactory.DataModelConditionGroupViewModel(e.RenderProfileElement.DisplayCondition, ConditionGroupType.General); + List modules = new(); + if (_profileEditorService.SelectedProfileConfiguration?.Module != null) + modules.Add(_profileEditorService.SelectedProfileConfiguration.Module); + ActiveItem = _dataModelConditionsVmFactory.DataModelConditionGroupViewModel(e.RenderProfileElement.DisplayCondition, ConditionGroupType.General, modules); ActiveItem.IsRootGroup = true; - ActiveItem.Update(); DisplayStartHint = !RenderProfileElement.DisplayCondition.Children.Any(); IsEventCondition = RenderProfileElement.DisplayCondition.Children.Any(c => c is DataModelConditionEvent); @@ -146,11 +146,6 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions NotifyOfPropertyChange(nameof(EventOverlapMode)); } - private void CoreServiceOnFrameRendered(object sender, FrameRenderedEventArgs e) - { - ActiveItem?.Evaluate(); - } - private void DisplayConditionOnChildrenModified(object sender, EventArgs e) { DisplayStartHint = !RenderProfileElement.DisplayCondition.Children.Any(); @@ -159,7 +154,7 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions public void EventTriggerModeSelected() { - _profileEditorService.UpdateSelectedProfileElement(); + _profileEditorService.SaveSelectedProfileElement(); } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/ConditionalDataBindingModeViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/ConditionalDataBindingModeViewModel.cs index f7cfd0071..0b6d1eeef 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/ConditionalDataBindingModeViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/ConditionalDataBindingModeViewModel.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using Artemis.Core; -using Artemis.Core.Services; using Artemis.UI.Extensions; using Artemis.UI.Ninject.Factories; using Artemis.UI.Shared.Services; @@ -15,16 +14,13 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.Conditio IDataBindingModeViewModel { private readonly IDataBindingsVmFactory _dataBindingsVmFactory; - private readonly ICoreService _coreService; private readonly IProfileEditorService _profileEditorService; private bool _updating; public ConditionalDataBindingModeViewModel(ConditionalDataBinding conditionalDataBinding, - ICoreService coreService, IProfileEditorService profileEditorService, IDataBindingsVmFactory dataBindingsVmFactory) { - _coreService = coreService; _profileEditorService = profileEditorService; _dataBindingsVmFactory = dataBindingsVmFactory; @@ -41,7 +37,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.Conditio DataBindingConditionViewModel viewModel = Items.First(c => c.DataBindingCondition == condition); viewModel.ActiveItem.AddCondition(); - _profileEditorService.UpdateSelectedProfileElement(); + _profileEditorService.SaveSelectedProfileElement(); } public void RemoveCondition(DataBindingCondition dataBindingCondition) @@ -52,22 +48,9 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.Conditio protected override void OnInitialActivate() { Initialize(); - _coreService.FrameRendered += CoreServiceOnFrameRendered; base.OnInitialActivate(); } - protected override void OnClose() - { - _coreService.FrameRendered -= CoreServiceOnFrameRendered; - base.OnClose(); - } - - private void CoreServiceOnFrameRendered(object sender, FrameRenderedEventArgs e) - { - foreach (DataBindingConditionViewModel dataBindingConditionViewModel in Items) - dataBindingConditionViewModel.Evaluate(); - } - private void UpdateItems() { _updating = true; @@ -111,7 +94,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.Conditio ConditionalDataBinding.ApplyOrder(); - _profileEditorService.UpdateSelectedProfileElement(); + _profileEditorService.SaveSelectedProfileElement(); } private void ConditionalDataBindingOnConditionsUpdated(object sender, EventArgs e) diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/DataBindingConditionViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/DataBindingConditionViewModel.cs index ff8df07a0..e9f620f21 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/DataBindingConditionViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/DataBindingConditionViewModel.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.Linq; using Artemis.Core; +using Artemis.Core.Modules; using Artemis.UI.Ninject.Factories; using Artemis.UI.Screens.ProfileEditor.Conditions; using Artemis.UI.Shared; @@ -34,7 +36,10 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.Conditio protected override void OnInitialActivate() { base.OnInitialActivate(); - ActiveItem = _dataModelConditionsVmFactory.DataModelConditionGroupViewModel(DataBindingCondition.Condition, ConditionGroupType.General); + List modules = new(); + if (_profileEditorService.SelectedProfileConfiguration?.Module != null) + modules.Add(_profileEditorService.SelectedProfileConfiguration.Module); + ActiveItem = _dataModelConditionsVmFactory.DataModelConditionGroupViewModel(DataBindingCondition.Condition, ConditionGroupType.General, modules); ActiveItem.IsRootGroup = true; ActiveItem.Update(); @@ -54,12 +59,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.Conditio private void ValueViewModelOnValueUpdated(object sender, DataModelInputStaticEventArgs e) { DataBindingCondition.Value = (TProperty) Convert.ChangeType(e.Value, typeof(TProperty)); - _profileEditorService.UpdateSelectedProfileElement(); - } - - public void Evaluate() - { - ActiveItem?.Evaluate(); + _profileEditorService.SaveSelectedProfileElement(); } public void AddCondition() diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingViewModel.cs index 2127e82c8..49b3040e9 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingViewModel.cs @@ -178,7 +178,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings Registration.DataBinding.EasingFunction = SelectedEasingViewModel?.EasingFunction ?? Easings.Functions.Linear; } - _profileEditorService.UpdateSelectedProfileElement(); + _profileEditorService.SaveSelectedProfileElement(); Update(); } @@ -198,7 +198,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings } CreateDataBindingModeModeViewModel(); - _profileEditorService.UpdateSelectedProfileElement(); + _profileEditorService.SaveSelectedProfileElement(); } private void UpdateTestResult() @@ -243,7 +243,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings return; Registration.LayerProperty.EnableDataBinding(Registration); - _profileEditorService.UpdateSelectedProfileElement(); + _profileEditorService.SaveSelectedProfileElement(); } private void RemoveDataBinding() @@ -255,7 +255,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings Registration.LayerProperty.DisableDataBinding(Registration.DataBinding); Update(); - _profileEditorService.UpdateSelectedProfileElement(); + _profileEditorService.SaveSelectedProfileElement(); } public void CopyDataBinding() @@ -282,7 +282,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings Update(); - _profileEditorService.UpdateSelectedProfileElement(); + _profileEditorService.SaveSelectedProfileElement(); } private void OnFrameRendered(object sender, FrameRenderedEventArgs e) diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/DataBindingModifierView.xaml b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/DataBindingModifierView.xaml index e1264a5b6..4d0635574 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/DataBindingModifierView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/DataBindingModifierView.xaml @@ -9,6 +9,7 @@ xmlns:utilities="clr-namespace:Artemis.UI.Utilities" xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.DirectDataBinding" xmlns:modifierTypes="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.DirectDataBinding.ModifierTypes" + xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800"> @@ -16,7 +17,7 @@ - + diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/DataBindingModifierViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/DataBindingModifierViewModel.cs index 8b943a6b6..f6811db6f 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/DataBindingModifierViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/DataBindingModifierViewModel.cs @@ -71,19 +71,19 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.DirectDa public void Delete() { Modifier.DirectDataBinding.RemoveModifier(Modifier); - _profileEditorService.UpdateSelectedProfileElement(); + _profileEditorService.SaveSelectedProfileElement(); } private void ParameterSelectionViewModelOnPropertySelected(object sender, DataModelInputDynamicEventArgs e) { Modifier.UpdateParameterDynamic(e.DataModelPath); - _profileEditorService.UpdateSelectedProfileElement(); + _profileEditorService.SaveSelectedProfileElement(); } private void StaticInputViewModelOnValueUpdated(object sender, DataModelInputStaticEventArgs e) { Modifier.UpdateParameterStatic(e.Value); - _profileEditorService.UpdateSelectedProfileElement(); + _profileEditorService.SaveSelectedProfileElement(); } private void Update() @@ -100,7 +100,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.DirectDa else if (Modifier.ParameterType == ProfileRightSideType.Dynamic) { DisposeStaticInputViewModel(); - DynamicSelectionViewModel = _dataModelUIService.GetDynamicSelectionViewModel(_profileEditorService.GetCurrentModule()); + DynamicSelectionViewModel = _dataModelUIService.GetDynamicSelectionViewModel(_profileEditorService.SelectedProfileConfiguration.Module); if (DynamicSelectionViewModel != null) { DynamicSelectionViewModel.DisplaySwitchButton = true; @@ -149,7 +149,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.DirectDa return; Modifier.UpdateModifierType(modifierTypeViewModel.ModifierType); - _profileEditorService.UpdateSelectedProfileElement(); + _profileEditorService.SaveSelectedProfileElement(); Update(); } diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/DirectDataBindingModeViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/DirectDataBindingModeViewModel.cs index d7c9272b6..40479a351 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/DirectDataBindingModeViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/DirectDataBindingModeViewModel.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using Artemis.Core; +using Artemis.Core.Modules; using Artemis.UI.Extensions; using Artemis.UI.Ninject.Factories; using Artemis.UI.Shared; @@ -87,7 +88,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.DirectDa private void Initialize() { DirectDataBinding.ModifiersUpdated += DirectDataBindingOnModifiersUpdated; - TargetSelectionViewModel = _dataModelUIService.GetDynamicSelectionViewModel(_profileEditorService.GetCurrentModule()); + TargetSelectionViewModel = _dataModelUIService.GetDynamicSelectionViewModel(_profileEditorService.SelectedProfileConfiguration.Module); TargetSelectionViewModel.PropertySelected += TargetSelectionViewModelOnPropertySelected; ModifierViewModels.CollectionChanged += ModifierViewModelsOnCollectionChanged; Update(); @@ -106,7 +107,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.DirectDa DirectDataBinding.ApplyOrder(); - _profileEditorService.UpdateSelectedProfileElement(); + _profileEditorService.SaveSelectedProfileElement(); } #region Target @@ -116,7 +117,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.DirectDa DirectDataBinding.UpdateSource(e.DataModelPath); Update(); - _profileEditorService.UpdateSelectedProfileElement(); + _profileEditorService.SaveSelectedProfileElement(); } #endregion @@ -126,7 +127,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.DirectDa public void AddModifier() { DirectDataBinding.AddModifier(ProfileRightSideType.Dynamic); - _profileEditorService.UpdateSelectedProfileElement(); + _profileEditorService.SaveSelectedProfileElement(); } private void UpdateModifierViewModels() diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerEffects/EffectsViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerEffects/EffectsViewModel.cs index c3e43e965..c3087f8e9 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerEffects/EffectsViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerEffects/EffectsViewModel.cs @@ -70,7 +70,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.LayerEffects { await Task.Delay(500); renderElement.AddLayerEffect(SelectedLayerEffectDescriptor); - _profileEditorService.UpdateSelectedProfileElement(); + _profileEditorService.SaveSelectedProfileElement(); }); } } diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesView.xaml b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesView.xaml index a22dfb527..10ca02db6 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesView.xaml @@ -18,7 +18,7 @@ d:DataContext="{d:DesignInstance {x:Type layerProperties:LayerPropertiesViewModel}}" behaviors:InputBindingBehavior.PropagateInputBindingsToWindow="True"> - + - - - + d:DesignHeight="640" d:DesignWidth="1200" + d:DataContext="{d:DesignInstance screens:RootViewModel}"> + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + - - - - - - - + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/RootViewModel.cs b/src/Artemis.UI/Screens/RootViewModel.cs index 97e19b281..c29653c02 100644 --- a/src/Artemis.UI/Screens/RootViewModel.cs +++ b/src/Artemis.UI/Screens/RootViewModel.cs @@ -8,8 +8,6 @@ using System.Windows.Input; using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Events; -using Artemis.UI.Screens.Modules; -using Artemis.UI.Screens.Settings.Tabs.General; using Artemis.UI.Screens.Sidebar; using Artemis.UI.Screens.StartupWizard; using Artemis.UI.Services; @@ -23,20 +21,18 @@ using Constants = Artemis.Core.Constants; namespace Artemis.UI.Screens { - public sealed class RootViewModel : Conductor, IDisposable + public sealed class RootViewModel : Conductor, IDisposable { private readonly IRegistrationService _builtInRegistrationService; - private readonly IMessageService _messageService; private readonly ICoreService _coreService; - private readonly IWindowManager _windowManager; private readonly IDebugService _debugService; - private readonly IKernel _kernel; private readonly IEventAggregator _eventAggregator; - private readonly ISettingsService _settingsService; private readonly Timer _frameTimeUpdateTimer; - private readonly SidebarViewModel _sidebarViewModel; + private readonly IKernel _kernel; + private readonly IMessageService _messageService; + private readonly ISettingsService _settingsService; + private readonly IWindowManager _windowManager; private readonly PluginSetting _windowSize; - private bool _activeItemReady; private string _frameTime; private bool _lostFocus; private ISnackbarMessageQueue _mainMessageQueue; @@ -62,38 +58,24 @@ namespace Artemis.UI.Screens _debugService = debugService; _builtInRegistrationService = builtInRegistrationService; _messageService = messageService; - _sidebarViewModel = sidebarViewModel; _frameTimeUpdateTimer = new Timer(500); - _windowSize = _settingsService.GetSetting("UI.RootWindowSize"); - _sidebarViewModel.ConductWith(this); - ActiveItem = sidebarViewModel.SelectedItem; - ActiveItemReady = true; - PinSidebar = _settingsService.GetSetting("UI.PinSidebar", false); + SidebarViewModel = sidebarViewModel; + SidebarViewModel.ConductWith(this); AssemblyInformationalVersionAttribute versionAttribute = typeof(RootViewModel).Assembly.GetCustomAttribute(); WindowTitle = $"Artemis {versionAttribute?.InformationalVersion} build {Constants.BuildInfo.BuildNumberDisplay}"; } - public PluginSetting PinSidebar { get; } - - // Just a litte trick to get the non-active variant completely removed from XAML (that should probably be done in the view) - public SidebarViewModel PinnedSidebarViewModel => PinSidebar.Value ? _sidebarViewModel : null; - public SidebarViewModel DockedSidebarViewModel => PinSidebar.Value ? null : _sidebarViewModel; + public SidebarViewModel SidebarViewModel { get; } public ISnackbarMessageQueue MainMessageQueue { get => _mainMessageQueue; set => SetAndNotify(ref _mainMessageQueue, value); } - - public bool ActiveItemReady - { - get => _activeItemReady; - set => SetAndNotify(ref _activeItemReady, value); - } - + public string WindowTitle { get => _windowTitle; @@ -150,52 +132,15 @@ namespace Artemis.UI.Screens _eventAggregator.Publish(new MainWindowMouseEvent(sender, false, e)); } - private void UpdateSidebarPinState() - { - _sidebarViewModel.IsSidebarOpen = true; - - NotifyOfPropertyChange(nameof(PinnedSidebarViewModel)); - NotifyOfPropertyChange(nameof(DockedSidebarViewModel)); - } - private void UpdateFrameTime() { FrameTime = $"Frame time: {_coreService.FrameTime.TotalMilliseconds:F2} ms"; } - private void SidebarViewModelOnPropertyChanged(object sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(_sidebarViewModel.SelectedItem) && ActiveItem != _sidebarViewModel.SelectedItem) - { - // Unless the sidebar is pinned, close it upon selecting an item - if (!PinSidebar.Value) - _sidebarViewModel.IsSidebarOpen = false; - - ActiveItemReady = false; - - // Allow the menu to close, it's slower but feels more responsive, funny how that works right - Execute.PostToUIThread(async () => - { - await Task.Delay(400); - ActiveItem = _sidebarViewModel.SelectedItem; - await Task.Delay(200); - ActiveItemReady = true; - }); - - } - } - private void OnFrameTimeUpdateTimerOnElapsed(object sender, ElapsedEventArgs args) { UpdateFrameTime(); } - - private void PinSidebarOnSettingChanged(object sender, EventArgs e) - { - UpdateSidebarPinState(); - } - - #region IDisposable /// public void Dispose() @@ -203,7 +148,15 @@ namespace Artemis.UI.Screens _frameTimeUpdateTimer?.Dispose(); } - #endregion + private void SidebarViewModelOnSelectedScreenChanged(object? sender, EventArgs e) + { + ActiveItem = SidebarViewModel.SelectedScreen; + } + + private void ShowSetupWizard() + { + _windowManager.ShowDialog(_kernel.Get()); + } #region Overrides of Screen @@ -228,14 +181,14 @@ namespace Artemis.UI.Screens MainMessageQueue = _messageService.MainMessageQueue; UpdateFrameTime(); + SidebarViewModel.SelectedScreenChanged += SidebarViewModelOnSelectedScreenChanged; + ActiveItem = SidebarViewModel.SelectedScreen; + _builtInRegistrationService.RegisterBuiltInDataModelDisplays(); _builtInRegistrationService.RegisterBuiltInDataModelInputs(); _builtInRegistrationService.RegisterBuiltInPropertyEditors(); _frameTimeUpdateTimer.Elapsed += OnFrameTimeUpdateTimerOnElapsed; - _sidebarViewModel.PropertyChanged += SidebarViewModelOnPropertyChanged; - PinSidebar.SettingChanged += PinSidebarOnSettingChanged; - _frameTimeUpdateTimer.Start(); _window = (MaterialWindow) View; @@ -247,11 +200,6 @@ namespace Artemis.UI.Screens base.OnInitialActivate(); } - private void ShowSetupWizard() - { - _windowManager.ShowDialog(_kernel.Get()); - } - protected override void OnClose() { // Ensure no element with focus can leak, if we don't do this the root VM is retained by Window.EffectiveValues @@ -261,15 +209,13 @@ namespace Artemis.UI.Screens MainMessageQueue = null; _frameTimeUpdateTimer.Stop(); + SidebarViewModel.SelectedScreenChanged -= SidebarViewModelOnSelectedScreenChanged; + _windowSize.Value ??= new WindowSize(); _windowSize.Value.ApplyFromWindow(_window); _windowSize.Save(); _frameTimeUpdateTimer.Elapsed -= OnFrameTimeUpdateTimerOnElapsed; - _sidebarViewModel.PropertyChanged -= SidebarViewModelOnPropertyChanged; - PinSidebar.SettingChanged -= PinSidebarOnSettingChanged; - - _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/Settings/Debug/Tabs/DataModelDebugViewModel.cs b/src/Artemis.UI/Screens/Settings/Debug/Tabs/DataModelDebugViewModel.cs index ae0364bb4..37c056fe8 100644 --- a/src/Artemis.UI/Screens/Settings/Debug/Tabs/DataModelDebugViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Debug/Tabs/DataModelDebugViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Timers; using Artemis.Core; @@ -116,7 +117,7 @@ namespace Artemis.UI.Screens.Settings.Debug.Tabs private void OnPluginFeatureToggled(object sender, PluginFeatureEventArgs e) { - if (e.PluginFeature is DataModelPluginFeature) + if (e.PluginFeature is Module) PopulateModules(); } @@ -134,7 +135,7 @@ namespace Artemis.UI.Screens.Settings.Debug.Tabs private void GetDataModel() { MainDataModel = SelectedModule != null - ? _dataModelUIService.GetPluginDataModelVisualization(SelectedModule, false) + ? _dataModelUIService.GetPluginDataModelVisualization(new List() {SelectedModule}, false) : _dataModelUIService.GetMainDataModelVisualization(); } diff --git a/src/Artemis.UI/Screens/Settings/Device/Tabs/DevicePropertiesTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Device/Tabs/DevicePropertiesTabViewModel.cs index 4af2c8313..af4f44aa4 100644 --- a/src/Artemis.UI/Screens/Settings/Device/Tabs/DevicePropertiesTabViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Device/Tabs/DevicePropertiesTabViewModel.cs @@ -184,7 +184,7 @@ namespace Artemis.UI.Screens.Settings.Device.Tabs if (HasErrors) return; - _coreService.ModuleRenderingDisabled = true; + _coreService.ProfileRenderingDisabled = true; await Task.Delay(100); Device.X = X; @@ -200,7 +200,7 @@ namespace Artemis.UI.Screens.Settings.Device.Tabs _rgbService.SaveDevice(Device); - _coreService.ModuleRenderingDisabled = false; + _coreService.ProfileRenderingDisabled = false; } public void Reset() diff --git a/src/Artemis.UI/Screens/Settings/SettingsViewModel.cs b/src/Artemis.UI/Screens/Settings/SettingsViewModel.cs index 78ddb1866..9c0c2a5d6 100644 --- a/src/Artemis.UI/Screens/Settings/SettingsViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/SettingsViewModel.cs @@ -1,7 +1,6 @@ using Artemis.UI.Screens.Settings.Tabs.About; using Artemis.UI.Screens.Settings.Tabs.Devices; using Artemis.UI.Screens.Settings.Tabs.General; -using Artemis.UI.Screens.Settings.Tabs.Modules; using Artemis.UI.Screens.Settings.Tabs.Plugins; using Stylet; @@ -11,7 +10,6 @@ namespace Artemis.UI.Screens.Settings { public SettingsViewModel( GeneralSettingsTabViewModel generalSettingsTabViewModel, - ModuleOrderTabViewModel moduleOrderTabViewModel, PluginSettingsTabViewModel pluginSettingsTabViewModel, DeviceSettingsTabViewModel deviceSettingsTabViewModel, AboutTabViewModel aboutTabViewModel) @@ -19,7 +17,6 @@ namespace Artemis.UI.Screens.Settings DisplayName = "Settings"; Items.Add(generalSettingsTabViewModel); - Items.Add(moduleOrderTabViewModel); Items.Add(pluginSettingsTabViewModel); Items.Add(deviceSettingsTabViewModel); Items.Add(aboutTabViewModel); diff --git a/src/Artemis.UI/Screens/Settings/Tabs/About/AboutTabView.xaml b/src/Artemis.UI/Screens/Settings/Tabs/About/AboutTabView.xaml index eb7427a8d..6167eafe1 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/About/AboutTabView.xaml +++ b/src/Artemis.UI/Screens/Settings/Tabs/About/AboutTabView.xaml @@ -11,406 +11,426 @@ mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800" d:DataContext="{d:DesignInstance local:AboutTabViewModel}"> - - - - - - - - - - - - - - - Artemis 2 - + + + + + + + + + + + + + + + Artemis 2 + - - - - + + + + + + + + + + PolyForm Noncommercial License 1.0.0 + + + + + + + + + + + + + + + + + + + + + + + Robert 'Spoinky' Beekman + + + Project owner, main contributor + + + + + + + + + + + + + + + + + + + + + + + + + Darth Affe + + + + RGB.NET + + developer, main contributor + + + + + + + + + + + + + + + + + + + + + + + + + Diogo 'DrMeteor' Trindade + + + Main contributor + + + + + + + + + + + + + + + + + + + + + + + + + Kai Werling + + + Graphics design + + + + + + + + + + + + Special thanks + + + + - The various people creating PRs to Artemis.Plugins and the main repository + - All the people on Discord providing feedback and testing + + + + + + External libraries + + + + - Ben.Demystifier + + https://github.com/benaadams/Ben.Demystifier + + + - EmbedIO + + https://unosquare.github.io/embedio/ + + + - FluentValidation + + https://fluentvalidation.net/ + + + - Furl.Http + + https://flurl.dev/ + + + - gong-wpf-dragdrop + + https://github.com/punker76/gong-wpf-dragdrop + + + - Hardcodet.NotifyIcon.Wpf.NetCore + + https://github.com/HavenDV/H.NotifyIcon.WPF + + + - Humanizer + + https://github.com/Humanizr/Humanizer + + + - LiteDB + + https://www.litedb.org/ + + + - MaterialDesignThemes + + https://github.com/MaterialDesignInXAML/MaterialDesignInXamlToolkit + + + - MaterialDesignExtensions + + https://spiegelp.github.io/MaterialDesignExtensions/ + + + - McMaster.NETCore.Plugins + + https://github.com/natemcmaster/DotNetCorePlugins + + + - Newtonsoft.Json + + https://www.newtonsoft.com/json + + + - Ninject + + http://www.ninject.org/ + + + - Ookii.Dialogs.Wpf + + https://github.com/ookii-dialogs/ookii-dialogs-wpf + + + - RawInput.Sharp + + https://github.com/mfakane/rawinput-sharp + + + - RGB.NET + + https://github.com/DarthAffe/RGB.NET + + + - Serilog + + https://serilog.net/ + + + - SkiaSharp + + https://github.com/mono/SkiaSharp + + + - Stylet + + https://github.com/canton7/Stylet + + + - Unclassified.NetRevisionTask + + https://unclassified.software/en/apps/netrevisiontask + + + + - - - - - PolyForm Noncommercial License 1.0.0 - - - - - - - - - - - - - - - - - - - - - - - Robert 'Spoinky' Beekman - - - Project owner, main contributor - - - - - - - - - - - - - - - - - - - - - - - - - Darth Affe - - - - RGB.NET - developer, main contributor - - - - - - - - - - - - - - - - - - - - - - - - - Diogo 'DrMeteor' Trindade - - - Main contributor - - - - - - - - - - - - - - - - - - - - - - - - - Kai Werling - - - Graphics design - - - - - - - - - - - - Special thanks - - - - - The various people creating PRs to Artemis.Plugins and the main repository - - All the people on Discord providing feedback and testing - - - - - - External libraries - - - - - Ben.Demystifier - - https://github.com/benaadams/Ben.Demystifier - - - EmbedIO - - https://unosquare.github.io/embedio/ - - - FluentValidation - - https://fluentvalidation.net/ - - - Furl.Http - - https://flurl.dev/ - - - gong-wpf-dragdrop - - https://github.com/punker76/gong-wpf-dragdrop - - - Hardcodet.NotifyIcon.Wpf.NetCore - - https://github.com/HavenDV/H.NotifyIcon.WPF - - - Humanizer - - https://github.com/Humanizr/Humanizer - - - LiteDB - - https://www.litedb.org/ - - - MaterialDesignThemes - - https://github.com/MaterialDesignInXAML/MaterialDesignInXamlToolkit - - - MaterialDesignExtensions - - https://spiegelp.github.io/MaterialDesignExtensions/ - - - McMaster.NETCore.Plugins - - https://github.com/natemcmaster/DotNetCorePlugins - - - Newtonsoft.Json - - https://www.newtonsoft.com/json - - - Ninject - - http://www.ninject.org/ - - - Ookii.Dialogs.Wpf - - https://github.com/ookii-dialogs/ookii-dialogs-wpf - - - RawInput.Sharp - - https://github.com/mfakane/rawinput-sharp - - - RGB.NET - - https://github.com/DarthAffe/RGB.NET - - - Serilog - - https://serilog.net/ - - - SkiaSharp - - https://github.com/mono/SkiaSharp - - - Stylet - - https://github.com/canton7/Stylet - - - Unclassified.NetRevisionTask - - https://unclassified.software/en/apps/netrevisiontask - - - - - - + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Tabs/Modules/ModuleOrderModuleView.xaml b/src/Artemis.UI/Screens/Settings/Tabs/Modules/ModuleOrderModuleView.xaml deleted file mode 100644 index 6594c23f3..000000000 --- a/src/Artemis.UI/Screens/Settings/Tabs/Modules/ModuleOrderModuleView.xaml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Tabs/Modules/ModuleOrderModuleViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/Modules/ModuleOrderModuleViewModel.cs deleted file mode 100644 index 983c4bf30..000000000 --- a/src/Artemis.UI/Screens/Settings/Tabs/Modules/ModuleOrderModuleViewModel.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using Artemis.Core.Modules; -using Stylet; - -namespace Artemis.UI.Screens.Settings.Tabs.Modules -{ - public class ModuleOrderModuleViewModel : PropertyChangedBase - { - private string _priority; - - public ModuleOrderModuleViewModel(Module module) - { - Module = module; - Update(); - } - - public Module Module { get; } - - public string Priority - { - get => _priority; - set => SetAndNotify(ref _priority, value); - } - - public void Update() - { - switch (Module.PriorityCategory) - { - case ModulePriorityCategory.Normal: - Priority = "3." + (Module.Priority + 1); - break; - case ModulePriorityCategory.Application: - Priority = "2." + (Module.Priority + 1); - break; - case ModulePriorityCategory.Overlay: - Priority = "1." + (Module.Priority + 1); - break; - default: - throw new ArgumentOutOfRangeException(); - } - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Tabs/Modules/ModuleOrderTabView.xaml b/src/Artemis.UI/Screens/Settings/Tabs/Modules/ModuleOrderTabView.xaml deleted file mode 100644 index 9e015f13f..000000000 --- a/src/Artemis.UI/Screens/Settings/Tabs/Modules/ModuleOrderTabView.xaml +++ /dev/null @@ -1,164 +0,0 @@ - - - - - - - - - - - Module priority - - Drag and drop the modules below to change their rendering priority. - - Like in the profile editor, the modules at the top render over modules at the bottom - - The categories serve as a starting point for new modules, you may freely move modules between the categories - - - - - - - - 1 - Overlays - - Modules that should always render on top - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 2 - Applications/games - - Modules that are related to specific applications or games - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 3 - Normal - - Regular modules that are always active in the background - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Tabs/Modules/ModuleOrderTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/Modules/ModuleOrderTabViewModel.cs deleted file mode 100644 index 5ba0b72e4..000000000 --- a/src/Artemis.UI/Screens/Settings/Tabs/Modules/ModuleOrderTabViewModel.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Artemis.Core.Modules; -using Artemis.Core.Services; -using GongSolutions.Wpf.DragDrop; -using Stylet; - -namespace Artemis.UI.Screens.Settings.Tabs.Modules -{ - public class ModuleOrderTabViewModel : Screen, IDropTarget - { - private readonly IModuleService _moduleService; - private readonly IPluginManagementService _pluginManagementService; - private readonly DefaultDropHandler _defaultDropHandler; - private List _modules; - - public ModuleOrderTabViewModel(IPluginManagementService pluginManagementService, IModuleService moduleService) - { - DisplayName = "MODULE PRIORITY"; - - _pluginManagementService = pluginManagementService; - _moduleService = moduleService; - _defaultDropHandler = new DefaultDropHandler(); - - NormalModules = new BindableCollection(); - ApplicationModules = new BindableCollection(); - OverlayModules = new BindableCollection(); - } - - protected override void OnActivate() - { - base.OnActivate(); - Update(); - } - - protected override void OnDeactivate() - { - base.OnDeactivate(); - _modules = null; - } - - public BindableCollection NormalModules { get; set; } - public BindableCollection ApplicationModules { get; set; } - public BindableCollection OverlayModules { get; set; } - - public void DragOver(IDropInfo dropInfo) - { - _defaultDropHandler.DragOver(dropInfo); - } - - public void Drop(IDropInfo dropInfo) - { - if (dropInfo.TargetItem == dropInfo.Data) - return; - - if (!(dropInfo.Data is ModuleOrderModuleViewModel viewModel)) - return; - if (!(dropInfo.TargetCollection is BindableCollection targetCollection)) - return; - - int insertIndex = dropInfo.InsertIndex; - - ModulePriorityCategory category; - if (targetCollection == NormalModules) - category = ModulePriorityCategory.Normal; - else if (targetCollection == ApplicationModules) - category = ModulePriorityCategory.Application; - else - category = ModulePriorityCategory.Overlay; - - // If moving down, take the removal of ourselves into consideration with the insert index - if (targetCollection.Contains(viewModel) && targetCollection.IndexOf(viewModel) < insertIndex) - insertIndex--; - - _moduleService.UpdateModulePriority(viewModel.Module, category, insertIndex); - - Update(); - } - - public void Update() - { - if (_modules == null) - _modules = _pluginManagementService.GetFeaturesOfType().Select(m => new ModuleOrderModuleViewModel(m)).ToList(); - NormalModules.Clear(); - NormalModules.AddRange(_modules.Where(m => m.Module.PriorityCategory == ModulePriorityCategory.Normal).OrderBy(m => m.Module.Priority)); - - ApplicationModules.Clear(); - ApplicationModules.AddRange(_modules.Where(m => m.Module.PriorityCategory == ModulePriorityCategory.Application).OrderBy(m => m.Module.Priority)); - - OverlayModules.Clear(); - OverlayModules.AddRange(_modules.Where(m => m.Module.PriorityCategory == ModulePriorityCategory.Overlay).OrderBy(m => m.Module.Priority)); - - foreach (ModuleOrderModuleViewModel moduleOrderModuleViewModel in _modules) - moduleOrderModuleViewModel.Update(); - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginFeatureView.xaml b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginFeatureView.xaml index 92b177023..bd8688b08 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginFeatureView.xaml +++ b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginFeatureView.xaml @@ -17,7 +17,7 @@ - + diff --git a/src/Artemis.UI/Screens/Sidebar/ArtemisSidebar.xaml b/src/Artemis.UI/Screens/Sidebar/ArtemisSidebar.xaml new file mode 100644 index 000000000..b417b269f --- /dev/null +++ b/src/Artemis.UI/Screens/Sidebar/ArtemisSidebar.xaml @@ -0,0 +1,127 @@ + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ModuleActivationRequirementView.xaml b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ModuleActivationRequirementView.xaml new file mode 100644 index 000000000..1048573a7 --- /dev/null +++ b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ModuleActivationRequirementView.xaml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Artemis.UI/Screens/Modules/Tabs/ActivationRequirementViewModel.cs b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ModuleActivationRequirementViewModel.cs similarity index 88% rename from src/Artemis.UI/Screens/Modules/Tabs/ActivationRequirementViewModel.cs rename to src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ModuleActivationRequirementViewModel.cs index 628f3d4a2..a565fa221 100644 --- a/src/Artemis.UI/Screens/Modules/Tabs/ActivationRequirementViewModel.cs +++ b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ModuleActivationRequirementViewModel.cs @@ -4,16 +4,16 @@ using Artemis.Core.Modules; using Humanizer; using Stylet; -namespace Artemis.UI.Screens.Modules.Tabs +namespace Artemis.UI.Screens.Sidebar.Dialogs.ProfileEdit { - public sealed class ActivationRequirementViewModel : Screen, IDisposable + public sealed class ModuleActivationRequirementViewModel : Screen, IDisposable { private readonly IModuleActivationRequirement _activationRequirement; private readonly Timer _updateTimer; private string _requirementDescription; private bool _requirementMet; - public ActivationRequirementViewModel(IModuleActivationRequirement activationRequirement) + public ModuleActivationRequirementViewModel(IModuleActivationRequirement activationRequirement) { _activationRequirement = activationRequirement; _updateTimer = new Timer(500); @@ -63,7 +63,7 @@ namespace Artemis.UI.Screens.Modules.Tabs } #region IDisposable - + /// public void Dispose() { diff --git a/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ModuleActivationRequirementsView.xaml b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ModuleActivationRequirementsView.xaml new file mode 100644 index 000000000..60d16503e --- /dev/null +++ b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ModuleActivationRequirementsView.xaml @@ -0,0 +1,27 @@ + + + + This module has built-in activation requirements and your profile won't activate until + . + These requirements allow the module creator to decide when the data is available to your profile you cannot override them. + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ModuleActivationRequirementsViewModel.cs b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ModuleActivationRequirementsViewModel.cs new file mode 100644 index 000000000..49a82f1d5 --- /dev/null +++ b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ModuleActivationRequirementsViewModel.cs @@ -0,0 +1,43 @@ +using System.Linq; +using Artemis.Core.Modules; +using Artemis.UI.Ninject.Factories; +using Stylet; + +namespace Artemis.UI.Screens.Sidebar.Dialogs.ProfileEdit +{ + public class ModuleActivationRequirementsViewModel : Conductor.Collection.AllActive + { + private readonly ISidebarVmFactory _sidebarVmFactory; + private Module _module; + private string _activationType; + + public ModuleActivationRequirementsViewModel(ISidebarVmFactory sidebarVmFactory) + { + _sidebarVmFactory = sidebarVmFactory; + } + + public Module Module + { + get => _module; + set => SetAndNotify(ref _module, value); + } + + public string ActivationType + { + get => _activationType; + set => SetAndNotify(ref _activationType, value); + } + + public void SetModule(Module module) + { + Module = module; + ActivationType = Module != null && Module.ActivationRequirementMode == ActivationRequirementType.All + ? "all requirements are met" + : "any requirement is met"; + + Items.Clear(); + if (Module != null) + Items.AddRange(Module.ActivationRequirements.Select(_sidebarVmFactory.ModuleActivationRequirementViewModel)); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ProfileEditView.xaml b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ProfileEditView.xaml new file mode 100644 index 000000000..b298b261b --- /dev/null +++ b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ProfileEditView.xaml @@ -0,0 +1,304 @@ + + + + + + + + + + + + + + + + + + + + + + + Add a new profile + + + | Properties + + + + + + General + + + + + + + + + No usable modules found + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Keybinds + + You may set up keybinds to activate/deactivate the profile + + + + + Keybinds are not yet implemented + + + + + + + + + + + + + + + + + Activation conditions + + If you only want this profile to be active under certain conditions, configure those conditions below + + + + + + + + + + + + + + + + AND + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ProfileEditViewModel.cs b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ProfileEditViewModel.cs new file mode 100644 index 000000000..a54e44440 --- /dev/null +++ b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ProfileEditViewModel.cs @@ -0,0 +1,214 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using Artemis.Core; +using Artemis.Core.Modules; +using Artemis.Core.Services; +using Artemis.UI.Ninject.Factories; +using Artemis.UI.Screens.ProfileEditor.Conditions; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services; +using FluentValidation; +using MaterialDesignThemes.Wpf; +using Ookii.Dialogs.Wpf; +using Stylet; + +namespace Artemis.UI.Screens.Sidebar.Dialogs.ProfileEdit +{ + public class ProfileEditViewModel : DialogViewModelBase + { + private readonly DataModelConditionGroup _dataModelConditionGroup; + private readonly List _modules; + private readonly IProfileService _profileService; + private bool _changedImage; + private bool _initializing; + private string _profileName; + private ProfileIconViewModel _selectedIcon; + private ProfileConfigurationIconType _selectedIconType; + private Stream _selectedImage; + private ProfileModuleViewModel _selectedModule; + + public ProfileEditViewModel(ProfileConfiguration profileConfiguration, bool isNew, + IProfileService profileService, + IPluginManagementService pluginManagementService, + ISidebarVmFactory sidebarVmFactory, + IDataModelConditionsVmFactory dataModelConditionsVmFactory, + IModelValidator validator) : base(validator) + { + ProfileConfiguration = profileConfiguration; + IsNew = isNew; + + _profileService = profileService; + _dataModelConditionGroup = ProfileConfiguration.ActivationCondition ?? new DataModelConditionGroup(null); + _modules = ProfileConfiguration.Module != null ? new List {ProfileConfiguration.Module} : new List(); + + IconTypes = new BindableCollection(EnumUtilities.GetAllValuesAndDescriptions(typeof(ProfileConfigurationIconType))); + Icons = new BindableCollection(); + Modules = new BindableCollection( + pluginManagementService.GetFeaturesOfType().Where(m => !m.IsAlwaysAvailable).Select(m => new ProfileModuleViewModel(m)) + ); + Initializing = true; + ActivationConditionViewModel = dataModelConditionsVmFactory.DataModelConditionGroupViewModel(_dataModelConditionGroup, ConditionGroupType.General, _modules); + ActivationConditionViewModel.ConductWith(this); + ActivationConditionViewModel.IsRootGroup = true; + ModuleActivationRequirementsViewModel = new ModuleActivationRequirementsViewModel(sidebarVmFactory); + ModuleActivationRequirementsViewModel.ConductWith(this); + ModuleActivationRequirementsViewModel.SetModule(ProfileConfiguration.Module); + + _profileName = ProfileConfiguration.Name; + _selectedModule = Modules.FirstOrDefault(m => m.Module == ProfileConfiguration.Module); + _selectedIconType = ProfileConfiguration.Icon.IconType; + _selectedImage = ProfileConfiguration.Icon.FileIcon; + + Task.Run(() => + { + Icons.AddRange(Enum.GetValues() + .GroupBy(i => i).Select(g => g.First()).Select(i => new ProfileIconViewModel(i)) + .OrderBy(i => i.IconName) + .ToList()); + if (IsNew) + SelectedIcon = Icons[new Random().Next(0, Icons.Count - 1)]; + else + SelectedIcon = Icons.FirstOrDefault(i => i.Icon.ToString() == ProfileConfiguration.Icon.MaterialIcon); + Initializing = false; + }); + } + + public ProfileConfiguration ProfileConfiguration { get; } + public bool IsNew { get; } + public BindableCollection IconTypes { get; } + public BindableCollection Icons { get; } + public BindableCollection Modules { get; } + public bool HasUsableModules => Modules.Any(); + + public bool Initializing + { + get => _initializing; + set => SetAndNotify(ref _initializing, value); + } + + public string ProfileName + { + get => _profileName; + set => SetAndNotify(ref _profileName, value); + } + + public ProfileConfigurationIconType SelectedIconType + { + get => _selectedIconType; + set + { + if (!SetAndNotify(ref _selectedIconType, value)) return; + SelectedImage = null; + } + } + + public Stream SelectedImage + { + get => _selectedImage; + set => SetAndNotify(ref _selectedImage, value); + } + + public ProfileIconViewModel SelectedIcon + { + get => _selectedIcon; + set => SetAndNotify(ref _selectedIcon, value); + } + + public ProfileModuleViewModel SelectedModule + { + get => _selectedModule; + set + { + if (!SetAndNotify(ref _selectedModule, value)) return; + _modules.Clear(); + if (value != null) + _modules.Add(value.Module); + + ActivationConditionViewModel.UpdateModules(); + ModuleActivationRequirementsViewModel.SetModule(value?.Module); + } + } + + public DataModelConditionGroupViewModel ActivationConditionViewModel { get; } + public ModuleActivationRequirementsViewModel ModuleActivationRequirementsViewModel { get; } + + public void Delete() + { + Session.Close(nameof(Delete)); + } + + public async Task Accept() + { + await ValidateAsync(); + + if (HasErrors) + return; + + ProfileConfiguration.Name = ProfileName; + ProfileConfiguration.Icon.IconType = SelectedIconType; + ProfileConfiguration.Icon.MaterialIcon = SelectedIcon?.Icon.ToString(); + ProfileConfiguration.Icon.FileIcon = SelectedImage; + + ProfileConfiguration.Module = SelectedModule?.Module; + + if (_dataModelConditionGroup.Children.Any()) + ProfileConfiguration.ActivationCondition = _dataModelConditionGroup; + + if (_changedImage) + { + ProfileConfiguration.Icon.FileIcon = SelectedImage; + _profileService.SaveProfileConfigurationIcon(ProfileConfiguration); + } + + _profileService.SaveProfileCategory(ProfileConfiguration.Category); + + Session.Close(nameof(Accept)); + } + + public void SelectBitmapFile() + { + VistaOpenFileDialog dialog = new() + { + Filter = "All Graphics Types|*.bmp;*.jpg;*.jpeg;*.png;*.tif;*.tiff|BMP |*.bmp|GIF|*.gif|JPG|*.jpg;*.jpeg|PNG|*.png|TIFF|*.tif;*.tiff", + Title = "Select profile icon" + }; + bool? result = dialog.ShowDialog(); + if (result != true) + return; + + _changedImage = true; + + // TODO: Scale down to 100x100-ish + SelectedImage = File.OpenRead(dialog.FileName); + } + + public void SelectSvgFile() + { + VistaOpenFileDialog dialog = new() + { + Filter = "Scalable Vector Graphics|*.svg", + Title = "Select profile icon" + }; + bool? result = dialog.ShowDialog(); + if (result != true) + return; + + _changedImage = true; + SelectedImage = File.OpenRead(dialog.FileName); + } + } + + public class ProfileEditViewModelValidator : AbstractValidator + { + public ProfileEditViewModelValidator() + { + RuleFor(m => m.ProfileName).NotEmpty().WithMessage("Profile name may not be empty"); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ProfileIconViewModel.cs b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ProfileIconViewModel.cs new file mode 100644 index 000000000..404643bea --- /dev/null +++ b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ProfileIconViewModel.cs @@ -0,0 +1,16 @@ +using MaterialDesignThemes.Wpf; + +namespace Artemis.UI.Screens.Sidebar.Dialogs.ProfileEdit +{ + public class ProfileIconViewModel + { + public ProfileIconViewModel(PackIconKind icon) + { + Icon = icon; + IconName = icon.ToString(); + } + + public PackIconKind Icon { get; } + public string IconName { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ProfileModuleViewModel.cs b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ProfileModuleViewModel.cs new file mode 100644 index 000000000..5f923d22c --- /dev/null +++ b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ProfileModuleViewModel.cs @@ -0,0 +1,22 @@ +using Artemis.Core.Modules; +using Stylet; + +namespace Artemis.UI.Screens.Sidebar.Dialogs.ProfileEdit +{ + public class ProfileModuleViewModel : PropertyChangedBase + { + public ProfileModuleViewModel(Module module) + { + Module = module; + Icon = module.DisplayIcon; + Name = module.DisplayName; + Description = module.Info.Description; + } + + public string Icon { get; } + public string Name { get; } + public string Description { get; } + + public Module Module { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileEditView.xaml b/src/Artemis.UI/Screens/Sidebar/Dialogs/SidebarCategoryCreateView.xaml similarity index 61% rename from src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileEditView.xaml rename to src/Artemis.UI/Screens/Sidebar/Dialogs/SidebarCategoryCreateView.xaml index ffef5e22d..7fef085cf 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileEditView.xaml +++ b/src/Artemis.UI/Screens/Sidebar/Dialogs/SidebarCategoryCreateView.xaml @@ -1,30 +1,30 @@ - + xmlns:s="https://github.com/canton7/Stylet" + mc:Ignorable="d" + d:DesignHeight="450" d:DesignWidth="280" + Width="300"> - Edit profile + Add a new category - + Text="{Binding CategoryName, UpdateSourceTrigger=PropertyChanged}" /> - - - \ No newline at end of file + diff --git a/src/Artemis.UI/Screens/Sidebar/Dialogs/SidebarCategoryCreateViewModel.cs b/src/Artemis.UI/Screens/Sidebar/Dialogs/SidebarCategoryCreateViewModel.cs new file mode 100644 index 000000000..84f646dff --- /dev/null +++ b/src/Artemis.UI/Screens/Sidebar/Dialogs/SidebarCategoryCreateViewModel.cs @@ -0,0 +1,56 @@ +using System.Threading.Tasks; +using Artemis.Core.Services; +using Artemis.Storage.Repositories.Interfaces; +using Artemis.UI.Screens.ProfileEditor.Dialogs; +using Artemis.UI.Shared.Services; +using FluentValidation; +using Stylet; + +namespace Artemis.UI.Screens.Sidebar.Dialogs +{ + public class SidebarCategoryCreateViewModel : DialogViewModelBase + { + private readonly IProfileService _profileService; + private string _categoryName; + + public SidebarCategoryCreateViewModel(IProfileService profileService, IModelValidator validator) : base(validator) + { + _profileService = profileService; + } + + public string CategoryName + { + get => _categoryName; + set => SetAndNotify(ref _categoryName, value); + } + + public async Task Accept() + { + await ValidateAsync(); + + if (HasErrors) + return; + + Session.Close(_profileService.CreateProfileCategory(CategoryName)); + } + } + + public class SidebarCategoryCreateViewModelValidator : AbstractValidator + { + private readonly IProfileCategoryRepository _profileCategoryRepository; + + public SidebarCategoryCreateViewModelValidator(IProfileCategoryRepository profileCategoryRepository) + { + _profileCategoryRepository = profileCategoryRepository; + + RuleFor(m => m.CategoryName) + .NotEmpty().WithMessage("Category name may not be empty") + .Must(BeUniqueCategory).WithMessage("Category name already taken"); + } + + private bool BeUniqueCategory(SidebarCategoryCreateViewModel viewModel, string categoryName) + { + return _profileCategoryRepository.IsUnique(categoryName, null) == null; + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Sidebar/Dialogs/SidebarCategoryUpdateView.xaml b/src/Artemis.UI/Screens/Sidebar/Dialogs/SidebarCategoryUpdateView.xaml new file mode 100644 index 000000000..ccefd4908 --- /dev/null +++ b/src/Artemis.UI/Screens/Sidebar/Dialogs/SidebarCategoryUpdateView.xaml @@ -0,0 +1,44 @@ + + + + Update category + + + + + + + + + + + + + + + + + + diff --git a/src/Artemis.UI/Screens/Sidebar/Dialogs/SidebarCategoryUpdateViewModel.cs b/src/Artemis.UI/Screens/Sidebar/Dialogs/SidebarCategoryUpdateViewModel.cs new file mode 100644 index 000000000..87884d426 --- /dev/null +++ b/src/Artemis.UI/Screens/Sidebar/Dialogs/SidebarCategoryUpdateViewModel.cs @@ -0,0 +1,73 @@ +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.Storage.Repositories.Interfaces; +using Artemis.UI.Shared.Services; +using FluentValidation; +using Stylet; +using WinRT.Interop; + +namespace Artemis.UI.Screens.Sidebar.Dialogs +{ + public class SidebarCategoryUpdateViewModel : DialogViewModelBase + { + private readonly IDialogService _dialogService; + private readonly IProfileService _profileService; + private string _categoryName; + + public SidebarCategoryUpdateViewModel(ProfileCategory profileCategory, IProfileService profileService, IDialogService dialogService, + IModelValidator validator) : base(validator) + { + _categoryName = profileCategory.Name; + _profileService = profileService; + _dialogService = dialogService; + + ProfileCategory = profileCategory; + } + + public ProfileCategory ProfileCategory { get; } + + public string CategoryName + { + get => _categoryName; + set => SetAndNotify(ref _categoryName, value); + } + + public async Task Accept() + { + await ValidateAsync(); + + if (HasErrors) + return; + + ProfileCategory.Name = CategoryName; + _profileService.SaveProfileCategory(ProfileCategory); + + Session.Close(nameof(Accept)); + } + + public void Delete() + { + Session.Close(nameof(Delete)); + } + } + + public class SidebarCategoryUpdateViewModelValidator : AbstractValidator + { + private readonly IProfileCategoryRepository _profileCategoryRepository; + + public SidebarCategoryUpdateViewModelValidator(IProfileCategoryRepository profileCategoryRepository) + { + _profileCategoryRepository = profileCategoryRepository; + + RuleFor(m => m.CategoryName) + .NotEmpty().WithMessage("Category name may not be empty") + .Must(BeUniqueCategory).WithMessage("Category name already taken"); + } + + private bool BeUniqueCategory(SidebarCategoryUpdateViewModel viewModel, string categoryName) + { + return _profileCategoryRepository.IsUnique(categoryName, viewModel.ProfileCategory.EntityId) == null; + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Sidebar/SidebarCategoryView.xaml b/src/Artemis.UI/Screens/Sidebar/SidebarCategoryView.xaml new file mode 100644 index 000000000..4b6bd073d --- /dev/null +++ b/src/Artemis.UI/Screens/Sidebar/SidebarCategoryView.xaml @@ -0,0 +1,213 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationViewModel.cs b/src/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationViewModel.cs new file mode 100644 index 000000000..a571dc088 --- /dev/null +++ b/src/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationViewModel.cs @@ -0,0 +1,165 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.UI.Events; +using Artemis.UI.Screens.Sidebar.Dialogs.ProfileEdit; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services; +using Newtonsoft.Json; +using Ookii.Dialogs.Wpf; +using Stylet; + +namespace Artemis.UI.Screens.Sidebar +{ + public class SidebarProfileConfigurationViewModel : Screen + { + private readonly IDialogService _dialogService; + private readonly IEventAggregator _eventAggregator; + private readonly IProfileService _profileService; + + public SidebarProfileConfigurationViewModel(ProfileConfiguration profileConfiguration, + IProfileService profileService, + IDialogService dialogService, + IEventAggregator eventAggregator) + { + _profileService = profileService; + _dialogService = dialogService; + _eventAggregator = eventAggregator; + ProfileConfiguration = profileConfiguration; + } + + public ProfileConfiguration ProfileConfiguration { get; } + + public bool IsProfileActive => ProfileConfiguration.Profile != null; + + public bool IsSuspended + { + get => ProfileConfiguration.IsSuspended; + set + { + ProfileConfiguration.IsSuspended = value; + _profileService.SaveProfileCategory(ProfileConfiguration.Category); + } + } + + public async Task ViewProperties() + { + object result = await _dialogService.ShowDialog(new Dictionary + { + {"profileConfiguration", ProfileConfiguration}, + {"isNew", false} + }); + + if (result is nameof(ProfileEditViewModel.Delete)) + await Delete(); + } + + public void SuspendAbove(string action) + { + foreach (ProfileConfiguration profileConfiguration in ProfileConfiguration.Category.ProfileConfigurations.OrderBy(p => p.Order).TakeWhile(c => c != ProfileConfiguration)) + { + if (profileConfiguration != ProfileConfiguration) + profileConfiguration.IsSuspended = action == "suspend"; + } + } + + public void SuspendBelow(string action) + { + foreach (ProfileConfiguration profileConfiguration in ProfileConfiguration.Category.ProfileConfigurations.OrderBy(p => p.Order).SkipWhile(c => c != ProfileConfiguration)) + { + if (profileConfiguration != ProfileConfiguration) + profileConfiguration.IsSuspended = action == "suspend"; + } + } + + public async Task Export() + { + string filename = new string(ProfileConfiguration.Name.Select(ch => Path.GetInvalidFileNameChars().Contains(ch) ? '_' : ch).ToArray()) + ".json"; + VistaSaveFileDialog dialog = new() + { + Filter = "Artemis Profile|*.json", + Title = "Export Artemis profile", + FileName = filename + }; + bool? result = dialog.ShowDialog(); + if (result != true) + return; + + ProfileConfigurationExportModel profileConfigurationExportModel = _profileService.ExportProfile(ProfileConfiguration); + string json = JsonConvert.SerializeObject(profileConfigurationExportModel, IProfileService.ExportSettings); + + if (!dialog.FileName.EndsWith(".json")) + dialog.FileName += ".json"; + await File.WriteAllTextAsync(dialog.FileName, json); + } + + public void Duplicate() + { + ProfileConfigurationExportModel export = _profileService.ExportProfile(ProfileConfiguration); + _profileService.ImportProfile(ProfileConfiguration.Category, export, true, false, "copy"); + } + + public void Copy() + { + JsonClipboard.SetObject(_profileService.ExportProfile(ProfileConfiguration)); + } + + public void Paste() + { + ProfileConfigurationExportModel profileConfiguration = JsonClipboard.GetData(); + if (profileConfiguration == null) + return; + + _profileService.ImportProfile(ProfileConfiguration.Category, profileConfiguration, true, false, "copy"); + } + + public async Task Delete() + { + if (await _dialogService.ShowConfirmDialog("Delete profile", "Are you sure you want to delete this profile?\r\nThis cannot be undone.")) + { + // Close the editor first by heading to Home if the profile is being edited + if (ProfileConfiguration.IsBeingEdited) + _eventAggregator.Publish(new RequestSelectSidebarItemEvent("Home")); + + _profileService.RemoveProfileConfiguration(ProfileConfiguration); + } + } + + #region Overrides of Screen + + /// + protected override void OnActivate() + { + _profileService.LoadProfileConfigurationIcon(ProfileConfiguration); + ProfileConfiguration.PropertyChanged += ProfileConfigurationOnPropertyChanged; + NotifyOfPropertyChange(nameof(IsProfileActive)); + + base.OnActivate(); + } + + /// + protected override void OnDeactivate() + { + ProfileConfiguration.PropertyChanged -= ProfileConfigurationOnPropertyChanged; + base.OnDeactivate(); + } + + #endregion + + #region Event handlers + + private void ProfileConfigurationOnPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(ProfileConfiguration.Profile)) + NotifyOfPropertyChange(nameof(IsProfileActive)); + if (e.PropertyName == nameof(ProfileConfiguration.IsSuspended)) + NotifyOfPropertyChange(nameof(IsSuspended)); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Sidebar/SidebarScreenView.xaml b/src/Artemis.UI/Screens/Sidebar/SidebarScreenView.xaml new file mode 100644 index 000000000..164918db6 --- /dev/null +++ b/src/Artemis.UI/Screens/Sidebar/SidebarScreenView.xaml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Sidebar/SidebarScreenViewModel.cs b/src/Artemis.UI/Screens/Sidebar/SidebarScreenViewModel.cs new file mode 100644 index 000000000..7b1c969f1 --- /dev/null +++ b/src/Artemis.UI/Screens/Sidebar/SidebarScreenViewModel.cs @@ -0,0 +1,31 @@ +using MaterialDesignThemes.Wpf; +using Ninject; +using Stylet; + +namespace Artemis.UI.Screens.Sidebar +{ + public class SidebarScreenViewModel : SidebarScreenViewModel where T : Screen + { + public SidebarScreenViewModel(PackIconKind icon, string displayName) : base(icon, displayName) + { + } + + public override Screen CreateInstance(IKernel kernel) + { + return kernel.Get(); + } + } + + public abstract class SidebarScreenViewModel + { + protected SidebarScreenViewModel(PackIconKind icon, string displayName) + { + Icon = icon; + DisplayName = displayName; + } + + public abstract Screen CreateInstance(IKernel kernel); + public PackIconKind Icon { get; } + public string DisplayName { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Sidebar/SidebarView.xaml b/src/Artemis.UI/Screens/Sidebar/SidebarView.xaml index e5829e573..6bccc544d 100644 --- a/src/Artemis.UI/Screens/Sidebar/SidebarView.xaml +++ b/src/Artemis.UI/Screens/Sidebar/SidebarView.xaml @@ -4,56 +4,163 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:sidebar="clr-namespace:Artemis.UI.Screens.Sidebar" - xmlns:controls="clr-namespace:MaterialDesignExtensions.Controls;assembly=MaterialDesignExtensions" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:s="https://github.com/canton7/Stylet" + xmlns:svgc="http://sharpvectors.codeplex.com/svgc/" + xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared" + xmlns:dd="urn:gong-wpf-dragdrop" + xmlns:converters="clr-namespace:Artemis.UI.Converters" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800" d:DataContext="{d:DesignInstance sidebar:SidebarViewModel}"> - - - - - - - + + + + + + + + + + + + + + + + + - + + + + + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + Artemis 2 + + - + + + + + + + + + - - + + + +