From 8d9272f47c51dcbbcab1f48b46bb208a0d2e2750 Mon Sep 17 00:00:00 2001 From: Robert Beekman Date: Wed, 6 Jul 2016 20:17:46 +0200 Subject: [PATCH] Layer refactor (#118) * Expanded Trace logging for Overwatch and RL, updated The Witcher 3 mod for 1.22 * Events WIP * Moved some views around, added event views * Implemented events * Some improvements to Overwatch parsing.. some. * Improved Overwatch parsing to work well with events * Fixed ultimate ready false positive during ultimate usage * Added debug window which currently shows Razer SDK keyboard output * Expanded Overwatch data model (todo: sticky values to avoid false-positives) * Added sticky values to Overwatch datamodel, should resolve most flickering/false positives * Layer refactor WIP * Moved layer types, animations, conditions and events to interfaces. (WIP) * Code cleanup, moved all profile related models to their own folder/namespace * Finished most of the profile refactoring * More profile refactoring, app compiles again * Switched from XML to JSON for profiles (refactor broke existing profiles anyway) * Made animation event expiration generic and fixed all serialization issues I've come across so far * Cleaned up settings, rigged basic LayerEditorView(Model) to refactored models * Rigged most layer type viewmodels back up to the new models * Fixed most view(models). Added animations to mice and headset. Replaced serialization-based cloning with NClone Fixed some rendering issues that came up with refactoring * Added Current Time to WindowsProfile * Cloning fixes Replaced glitchy cloning package with a simple serialization clone (that package looked so good :c) Removed serialization cloning from render process * Expanded Trace logging for Overwatch and RL, updated The Witcher 3 mod for 1.22 * Events WIP * Moved some views around, added event views * Implemented events * Some improvements to Overwatch parsing.. some. * Improved Overwatch parsing to work well with events * Fixed ultimate ready false positive during ultimate usage * Added debug window which currently shows Razer SDK keyboard output * Expanded Overwatch data model (todo: sticky values to avoid false-positives) * Added sticky values to Overwatch datamodel, should resolve most flickering/false positives * Layer refactor WIP * Moved layer types, animations, conditions and events to interfaces. (WIP) * Code cleanup, moved all profile related models to their own folder/namespace * Finished most of the profile refactoring * More profile refactoring, app compiles again * Switched from XML to JSON for profiles (refactor broke existing profiles anyway) * Made animation event expiration generic and fixed all serialization issues I've come across so far * Cleaned up settings, rigged basic LayerEditorView(Model) to refactored models * Rigged most layer type viewmodels back up to the new models * Fixed most view(models). Added animations to mice and headset. Replaced serialization-based cloning with NClone Fixed some rendering issues that came up with refactoring * Added Current Time to WindowsProfile * Cloning fixes Replaced glitchy cloning package with a simple serialization clone (that package looked so good :c) Removed serialization cloning from render process --- Artemis/Artemis/App.config | 64 +-- Artemis/Artemis/App.xaml | 2 + Artemis/Artemis/App.xaml.cs | 5 +- Artemis/Artemis/Artemis.csproj | 98 +++- Artemis/Artemis/ArtemisBootstrapper.cs | 7 +- Artemis/Artemis/DAL/ProfileProvider.cs | 63 +-- .../Corsair/CorsairHeadsets.cs | 34 +- .../DeviceProviders/Corsair/CorsairMice.cs | 30 +- .../Artemis/DeviceProviders/DeviceProvider.cs | 12 +- .../DeviceProviders/KeyboardProvider.cs | 3 +- Artemis/Artemis/Events/ChangeBitmap.cs | 19 - .../Artemis/Events/RazerColorArrayChanged.cs | 14 + .../ILayerEditorVmFactory.cs | 2 +- .../InjectionModules/ArtemisModules.cs | 30 + .../Artemis/InjectionModules/BaseModules.cs | 1 + Artemis/Artemis/Managers/LoopManager.cs | 67 +-- Artemis/Artemis/Models/EffectModel.cs | 70 ++- Artemis/Artemis/Models/OverlayModel.cs | 3 +- .../Properties/FolderPropertiesModel.cs | 12 - .../Properties/HeadsetPropertiesModel.cs | 12 - .../Properties/KeyboardPropertiesModel.cs | 74 --- .../Properties/LayerPropertiesModel.cs | 63 --- .../Properties/MousePropertiesModel.cs | 12 - .../AudioVisualizer/AudioVisualization.cs | 28 + .../AudioVisualizer/AudioVisualizerModel.cs | 30 +- .../Artemis/Modules/Effects/Bubbles/Bubble.cs | 33 +- .../Modules/Effects/Bubbles/Bubbles.cs | 28 + .../Modules/Effects/Bubbles/Bubbles.settings | 4 +- .../Modules/Effects/Bubbles/BubblesModel.cs | 71 ++- .../Effects/Bubbles/BubblesSettings.cs | 2 +- .../Modules/Effects/Bubbles/BubblesView.xaml | 2 +- .../Effects/Bubbles/BubblesView.xaml.cs | 2 +- .../Effects/Bubbles/BubblesViewModel.cs | 4 +- .../ProfilePreview/ProfilePreviewModel.cs | 40 +- .../Modules/Effects/TypeWave/TypeWaveModel.cs | 30 +- .../WindowsProfile/WindowsProfileDataModel.cs | 10 + .../WindowsProfile/WindowsProfileModel.cs | 23 +- .../CounterStrike/CounterStrike.Designer.cs | 108 +--- .../CounterStrike/CounterStrike.settings | 25 +- .../Games/CounterStrike/CounterStrikeModel.cs | 4 +- .../CounterStrike/CounterStrikeSettings.cs | 39 +- .../Modules/Games/Dota2/Dota2.Designer.cs | 120 ---- .../Modules/Games/Dota2/Dota2.settings | 34 +- .../Artemis/Modules/Games/Dota2/Dota2Model.cs | 4 +- .../Modules/Games/Dota2/Dota2Settings.cs | 59 +- .../Games/Overwatch/OverwatchDataModel.cs | 7 +- .../Modules/Games/Overwatch/OverwatchModel.cs | 225 +++++--- .../Games/RocketLeague/RocketLeagueModel.cs | 13 +- .../Games/TheDivision/TheDivisionModel.cs | 4 +- .../Modules/Games/Witcher3/Witcher3Model.cs | 7 +- .../VolumeDisplay/VolumeDisplayModel.cs | 20 +- .../Layers/Animations/GrowAnimation.cs | 61 ++ .../Layers/Animations/NoneAnimation.cs | 24 + .../Layers/Animations/PulseAnimation.cs | 50 ++ .../Layers/Animations/SlideDownAnimation.cs | 51 ++ .../Layers/Animations/SlideLeftAnimation.cs | 52 ++ .../Layers/Animations/SlideRightAnimation.cs | 51 ++ .../Layers/Animations/SlideUpAnimation.cs | 51 ++ .../Layers/Conditions/DataModelCondition.cs | 15 + .../Layers/Conditions/EventCondition.cs | 21 + .../Layers/Interfaces/ILayerAnimation.cs | 13 + .../Layers/Interfaces/ILayerCondition.cs | 10 + .../Profiles/Layers/Interfaces/ILayerType.cs | 57 ++ .../Layers/Models}/DynamicPropertiesModel.cs | 33 +- .../Layers/Models/EventPropertiesModel.cs | 49 ++ .../Models/KeyboardEventPropertiesModel.cs | 47 ++ .../Layers/Models}/LayerConditionModel.cs | 114 ++-- .../Layers/Models}/LayerModel.cs | 533 +++++++++--------- .../Layers/Models/LayerPropertiesModel.cs | 69 +++ .../Layers/Models/SimplePropertiesModel.cs | 12 + .../Layers/Types/Folder/FolderType.cs | 53 ++ .../Layers/Types/Headset/HeadsetType.cs | 87 +++ .../Types/Keyboard/KeyboardPropertiesModel.cs | 19 + .../Layers/Types/Keyboard/KeyboardType.cs | 100 ++++ .../Types/KeyboardGif/KeyboardGifType.cs | 93 +++ .../Profiles/Layers/Types/Mouse/MouseType.cs | 88 +++ .../{Models => }/Profiles/ProfileModel.cs | 422 +++++++------- .../Resources/Witcher3/Witcher3Artemis.zip | Bin 91536 -> 73648 bytes Artemis/Artemis/Settings/General.settings | 4 +- Artemis/Artemis/Settings/GeneralSettings.cs | 1 - Artemis/Artemis/Styles/ColorBox.xaml | 5 +- Artemis/Artemis/Utilities/ColorHelpers.cs | 2 +- .../Utilities/Converters/JsonConverters.cs | 43 ++ .../{ => Converters}/ValueConverters.cs | 143 ++--- .../Utilities/DataReaders/MmfReader.cs | 17 +- Artemis/Artemis/Utilities/GeneralHelpers.cs | 35 +- Artemis/Artemis/Utilities/GifImage.cs | 31 +- Artemis/Artemis/Utilities/ImageUtilities.cs | 19 + .../Utilities/Layers/AnimationUpdater.cs | 46 -- Artemis/Artemis/Utilities/Layers/Drawer.cs | 161 ------ Artemis/Artemis/Utilities/StickyValue.cs | 6 +- Artemis/Artemis/ViewModels/DebugViewModel.cs | 58 ++ .../Flyouts/FlyoutSettingsViewModel.cs | 19 +- .../Events/EventPropertiesViewModel.cs | 42 ++ .../Profiles/LayerConditionViewModel.cs | 2 +- .../LayerDynamicPropertiesViewModel.cs | 27 +- .../Profiles/LayerEditorViewModel.cs | 155 ++--- .../Layers/FolderPropertiesViewModel.cs | 16 + .../Layers/HeadsetPropertiesViewModel.cs | 44 ++ .../Layers/KeyboardPropertiesViewModel.cs | 83 +++ .../Layers/LayerPropertiesViewModel.cs | 47 ++ .../Layers/MousePropertiesViewModel.cs | 48 ++ .../Profiles/ProfileEditorViewModel.cs | 37 +- .../ViewModels/Profiles/ProfileViewModel.cs | 11 +- .../Properties/FolderPropertiesViewModel.cs | 33 -- .../Properties/HeadsetPropertiesViewModel.cs | 49 -- .../Properties/KeyboardPropertiesViewModel.cs | 99 ---- .../Properties/LayerPropertiesViewModel.cs | 18 - .../Properties/MousePropertiesViewModel.cs | 49 -- Artemis/Artemis/ViewModels/ShellViewModel.cs | 3 +- .../Artemis/ViewModels/SystemTrayViewModel.cs | 7 +- Artemis/Artemis/Views/DebugView.xaml | 29 + Artemis/Artemis/Views/DebugView.xaml.cs | 15 + .../Views/Flyouts/FlyoutSettingsView.xaml | 26 +- .../Profiles/Events/EventPropertiesView.xaml | 66 +++ .../Events/EventPropertiesView.xaml.cs | 15 + .../Profiles/LayerDynamicPropertiesView.xaml | 7 +- .../Views/Profiles/LayerEditorView.xaml | 107 ++-- .../FolderPropertiesView.xaml | 2 +- .../FolderPropertiesView.xaml.cs | 2 +- .../HeadsetPropertiesView.xaml | 2 +- .../HeadsetPropertiesView.xaml.cs | 2 +- .../KeyboardPropertiesView.xaml | 29 +- .../KeyboardPropertiesView.xaml.cs | 2 +- .../Profiles/Layers/MousePropertiesView.xaml | 67 +++ .../MousePropertiesView.xaml.cs | 2 +- .../Views/Profiles/ProfileEditorView.xaml | 3 +- .../Properties/MousePropertiesView.xaml | 34 -- 128 files changed, 3254 insertions(+), 2364 deletions(-) delete mode 100644 Artemis/Artemis/Events/ChangeBitmap.cs create mode 100644 Artemis/Artemis/Events/RazerColorArrayChanged.cs delete mode 100644 Artemis/Artemis/Models/Profiles/Properties/FolderPropertiesModel.cs delete mode 100644 Artemis/Artemis/Models/Profiles/Properties/HeadsetPropertiesModel.cs delete mode 100644 Artemis/Artemis/Models/Profiles/Properties/KeyboardPropertiesModel.cs delete mode 100644 Artemis/Artemis/Models/Profiles/Properties/LayerPropertiesModel.cs delete mode 100644 Artemis/Artemis/Models/Profiles/Properties/MousePropertiesModel.cs create mode 100644 Artemis/Artemis/Modules/Effects/AudioVisualizer/AudioVisualization.cs create mode 100644 Artemis/Artemis/Modules/Effects/Bubbles/Bubbles.cs create mode 100644 Artemis/Artemis/Profiles/Layers/Animations/GrowAnimation.cs create mode 100644 Artemis/Artemis/Profiles/Layers/Animations/NoneAnimation.cs create mode 100644 Artemis/Artemis/Profiles/Layers/Animations/PulseAnimation.cs create mode 100644 Artemis/Artemis/Profiles/Layers/Animations/SlideDownAnimation.cs create mode 100644 Artemis/Artemis/Profiles/Layers/Animations/SlideLeftAnimation.cs create mode 100644 Artemis/Artemis/Profiles/Layers/Animations/SlideRightAnimation.cs create mode 100644 Artemis/Artemis/Profiles/Layers/Animations/SlideUpAnimation.cs create mode 100644 Artemis/Artemis/Profiles/Layers/Conditions/DataModelCondition.cs create mode 100644 Artemis/Artemis/Profiles/Layers/Conditions/EventCondition.cs create mode 100644 Artemis/Artemis/Profiles/Layers/Interfaces/ILayerAnimation.cs create mode 100644 Artemis/Artemis/Profiles/Layers/Interfaces/ILayerCondition.cs create mode 100644 Artemis/Artemis/Profiles/Layers/Interfaces/ILayerType.cs rename Artemis/Artemis/{Models/Profiles/Properties => Profiles/Layers/Models}/DynamicPropertiesModel.cs (76%) create mode 100644 Artemis/Artemis/Profiles/Layers/Models/EventPropertiesModel.cs create mode 100644 Artemis/Artemis/Profiles/Layers/Models/KeyboardEventPropertiesModel.cs rename Artemis/Artemis/{Models/Profiles => Profiles/Layers/Models}/LayerConditionModel.cs (85%) rename Artemis/Artemis/{Models/Profiles => Profiles/Layers/Models}/LayerModel.cs (50%) create mode 100644 Artemis/Artemis/Profiles/Layers/Models/LayerPropertiesModel.cs create mode 100644 Artemis/Artemis/Profiles/Layers/Models/SimplePropertiesModel.cs create mode 100644 Artemis/Artemis/Profiles/Layers/Types/Folder/FolderType.cs create mode 100644 Artemis/Artemis/Profiles/Layers/Types/Headset/HeadsetType.cs create mode 100644 Artemis/Artemis/Profiles/Layers/Types/Keyboard/KeyboardPropertiesModel.cs create mode 100644 Artemis/Artemis/Profiles/Layers/Types/Keyboard/KeyboardType.cs create mode 100644 Artemis/Artemis/Profiles/Layers/Types/KeyboardGif/KeyboardGifType.cs create mode 100644 Artemis/Artemis/Profiles/Layers/Types/Mouse/MouseType.cs rename Artemis/Artemis/{Models => }/Profiles/ProfileModel.cs (61%) create mode 100644 Artemis/Artemis/Utilities/Converters/JsonConverters.cs rename Artemis/Artemis/Utilities/{ => Converters}/ValueConverters.cs (78%) delete mode 100644 Artemis/Artemis/Utilities/Layers/AnimationUpdater.cs delete mode 100644 Artemis/Artemis/Utilities/Layers/Drawer.cs create mode 100644 Artemis/Artemis/ViewModels/DebugViewModel.cs create mode 100644 Artemis/Artemis/ViewModels/Profiles/Events/EventPropertiesViewModel.cs create mode 100644 Artemis/Artemis/ViewModels/Profiles/Layers/FolderPropertiesViewModel.cs create mode 100644 Artemis/Artemis/ViewModels/Profiles/Layers/HeadsetPropertiesViewModel.cs create mode 100644 Artemis/Artemis/ViewModels/Profiles/Layers/KeyboardPropertiesViewModel.cs create mode 100644 Artemis/Artemis/ViewModels/Profiles/Layers/LayerPropertiesViewModel.cs create mode 100644 Artemis/Artemis/ViewModels/Profiles/Layers/MousePropertiesViewModel.cs delete mode 100644 Artemis/Artemis/ViewModels/Profiles/Properties/FolderPropertiesViewModel.cs delete mode 100644 Artemis/Artemis/ViewModels/Profiles/Properties/HeadsetPropertiesViewModel.cs delete mode 100644 Artemis/Artemis/ViewModels/Profiles/Properties/KeyboardPropertiesViewModel.cs delete mode 100644 Artemis/Artemis/ViewModels/Profiles/Properties/LayerPropertiesViewModel.cs delete mode 100644 Artemis/Artemis/ViewModels/Profiles/Properties/MousePropertiesViewModel.cs create mode 100644 Artemis/Artemis/Views/DebugView.xaml create mode 100644 Artemis/Artemis/Views/DebugView.xaml.cs create mode 100644 Artemis/Artemis/Views/Profiles/Events/EventPropertiesView.xaml create mode 100644 Artemis/Artemis/Views/Profiles/Events/EventPropertiesView.xaml.cs rename Artemis/Artemis/Views/Profiles/{Properties => Layers}/FolderPropertiesView.xaml (90%) rename Artemis/Artemis/Views/Profiles/{Properties => Layers}/FolderPropertiesView.xaml.cs (87%) rename Artemis/Artemis/Views/Profiles/{Properties => Layers}/HeadsetPropertiesView.xaml (95%) rename Artemis/Artemis/Views/Profiles/{Properties => Layers}/HeadsetPropertiesView.xaml.cs (87%) rename Artemis/Artemis/Views/Profiles/{Properties => Layers}/KeyboardPropertiesView.xaml (76%) rename Artemis/Artemis/Views/Profiles/{Properties => Layers}/KeyboardPropertiesView.xaml.cs (87%) create mode 100644 Artemis/Artemis/Views/Profiles/Layers/MousePropertiesView.xaml rename Artemis/Artemis/Views/Profiles/{Properties => Layers}/MousePropertiesView.xaml.cs (87%) delete mode 100644 Artemis/Artemis/Views/Profiles/Properties/MousePropertiesView.xaml diff --git a/Artemis/Artemis/App.config b/Artemis/Artemis/App.config index 6ad4f906d..7f7bc23d9 100644 --- a/Artemis/Artemis/App.config +++ b/Artemis/Artemis/App.config @@ -52,38 +52,11 @@ True - - - - - True - - - True - - - True - - - True - - - True - - - #FFFF0000 - - - #FF0000FF - - + Default - - #FF00FF00 - - - #FF6A5ACD + + @@ -145,33 +118,12 @@ True - - - - - True - - - #FFFF2900 - - - #FF26F600 - - - True - - - True - - - True - - - True - Default + + + @@ -344,6 +296,10 @@ + + + + diff --git a/Artemis/Artemis/App.xaml b/Artemis/Artemis/App.xaml index 7acc3be4c..e13bd017f 100644 --- a/Artemis/Artemis/App.xaml +++ b/Artemis/Artemis/App.xaml @@ -2,6 +2,7 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:artemis="clr-namespace:Artemis" + xmlns:converters="clr-namespace:Artemis.Utilities.Converters" DispatcherUnhandledException="Application_DispatcherUnhandledException" ShutdownMode="OnExplicitShutdown"> @@ -21,6 +22,7 @@ + \ No newline at end of file diff --git a/Artemis/Artemis/App.xaml.cs b/Artemis/Artemis/App.xaml.cs index 5ca4b1c47..008f21c59 100644 --- a/Artemis/Artemis/App.xaml.cs +++ b/Artemis/Artemis/App.xaml.cs @@ -2,6 +2,7 @@ using System.Security.Principal; using System.Windows; using System.Windows.Threading; +using Artemis.Utilities; using NLog; using WpfExceptionViewer; @@ -14,8 +15,8 @@ namespace Artemis { public App() { - //if (!IsRunAsAdministrator()) - // GeneralHelpers.RunAsAdministrator(); + if (!IsRunAsAdministrator()) + GeneralHelpers.RunAsAdministrator(); InitializeComponent(); } diff --git a/Artemis/Artemis/Artemis.csproj b/Artemis/Artemis/Artemis.csproj index 83e85ee8a..909e2e886 100644 --- a/Artemis/Artemis/Artemis.csproj +++ b/Artemis/Artemis/Artemis.csproj @@ -38,8 +38,8 @@ https://github.com/SpoinkyNL/Artemis/wiki/Frequently-Asked-Questions-%28FAQ%29 Artemis Artemis - 2 - 1.1.3.2 + 3 + 1.1.3.3 false true true @@ -276,9 +276,9 @@ + - @@ -301,15 +301,14 @@ - - - - - - - - - + + + + + + + + AudioVisualization.settings True @@ -420,6 +419,26 @@ + + + + + + + + + + + + + + + + + + + + True @@ -442,6 +461,8 @@ + + @@ -450,8 +471,6 @@ - - @@ -467,11 +486,11 @@ - + @@ -482,21 +501,25 @@ + - + - - - - + + + + + + DebugView.xaml + EffectsView.xaml @@ -524,13 +547,16 @@ Witcher3View.xaml - + + EventPropertiesView.xaml + + FolderPropertiesView.xaml - + HeadsetPropertiesView.xaml - + KeyboardPropertiesView.xaml @@ -542,7 +568,7 @@ LayerEditorView.xaml - + MousePropertiesView.xaml @@ -660,7 +686,9 @@ - + + Designer + @@ -695,6 +723,10 @@ Designer MSBuild:Compile + + Designer + MSBuild:Compile + Designer MSBuild:Compile @@ -731,15 +763,19 @@ Designer MSBuild:Compile - + Designer MSBuild:Compile - + Designer MSBuild:Compile - + + Designer + MSBuild:Compile + + Designer MSBuild:Compile @@ -755,7 +791,7 @@ MSBuild:Compile Designer - + Designer MSBuild:Compile @@ -802,7 +838,9 @@ false - + + + diff --git a/Artemis/Artemis/ArtemisBootstrapper.cs b/Artemis/Artemis/ArtemisBootstrapper.cs index f4137ab37..b4376a51d 100644 --- a/Artemis/Artemis/ArtemisBootstrapper.cs +++ b/Artemis/Artemis/ArtemisBootstrapper.cs @@ -5,9 +5,11 @@ using System.Windows; using System.Windows.Controls; using System.Windows.Forms; using Artemis.InjectionModules; +using Artemis.Settings; using Artemis.Utilities; using Artemis.ViewModels; using Caliburn.Micro; +using Newtonsoft.Json; using Ninject; using Application = System.Windows.Application; using MessageBox = System.Windows.Forms.MessageBox; @@ -22,7 +24,7 @@ namespace Artemis public ArtemisBootstrapper() { // Start logging before anything else - Logging.SetupLogging(Settings.General.Default.LogLevel); + Logging.SetupLogging(General.Default.LogLevel); CheckDuplicateInstances(); Initialize(); @@ -77,6 +79,7 @@ namespace Artemis protected override void Configure() { + JsonConvert.DefaultSettings = () => new JsonSerializerSettings {TypeNameHandling = TypeNameHandling.Auto}; _kernel = new StandardKernel(new BaseModules(), new ArtemisModules(), new ManagerModules()); _kernel.Bind().To().InSingletonScope(); _kernel.Bind().To().InSingletonScope(); @@ -114,7 +117,7 @@ namespace Artemis private void CheckDuplicateInstances() { bool aIsNewInstance; - Mutex = new Mutex(true, "ArtemisMutex", out aIsNewInstance); + Mutex = new Mutex(true, "ArtemisMutex2", out aIsNewInstance); if (aIsNewInstance) return; diff --git a/Artemis/Artemis/DAL/ProfileProvider.cs b/Artemis/Artemis/DAL/ProfileProvider.cs index 0c764b95e..3babec07f 100644 --- a/Artemis/Artemis/DAL/ProfileProvider.cs +++ b/Artemis/Artemis/DAL/ProfileProvider.cs @@ -4,13 +4,13 @@ using System.IO; using System.IO.Compression; using System.Linq; using System.Reflection; -using System.Xml.Serialization; using Artemis.DeviceProviders; using Artemis.Models; -using Artemis.Models.Profiles; -using Artemis.Models.Profiles.Properties; +using Artemis.Profiles; +using Artemis.Profiles.Layers.Types.Keyboard; using Artemis.Properties; using Artemis.Utilities; +using Newtonsoft.Json; using NLog; namespace Artemis.DAL @@ -65,14 +65,8 @@ namespace Artemis.DAL if (!Directory.Exists(path)) Directory.CreateDirectory(path); - var serializer = new XmlSerializer(typeof(ProfileModel)); - - // Could use a StreamWriter but should serializing fail this method doesn't ruin the existing XML file - using (var xml = new StringWriter()) - { - serializer.Serialize(xml, prof); - File.WriteAllText(path + $@"\{prof.Name}.xml", xml.ToString()); - } + var json = JsonConvert.SerializeObject(prof, Formatting.Indented); + File.WriteAllText(path + $@"\{prof.Name}.json", json); } private static List ReadProfiles() @@ -82,22 +76,18 @@ namespace Artemis.DAL var profiles = new List(); // Create the directory structure - var profilePaths = Directory.GetFiles(ProfileFolder, "*.xml", SearchOption.AllDirectories); + var profilePaths = Directory.GetFiles(ProfileFolder, "*.json", SearchOption.AllDirectories); // Parse the JSON files into objects and add them if they are valid - var deserializer = new XmlSerializer(typeof(ProfileModel)); foreach (var path in profilePaths) { try { - using (var file = new StreamReader(path)) - { - var prof = (ProfileModel) deserializer.Deserialize(file); - if (prof.GameName?.Length > 1 && prof.KeyboardSlug?.Length > 1 && prof.Name?.Length > 1) - profiles.Add(prof); - } + var prof = LoadProfileIfValid(path); + if (prof != null) + profiles.Add(prof); } - catch (InvalidOperationException e) + catch (Exception e) { Logger.Error("Failed to load profile: {0} - {1}", path, e.InnerException.Message); } @@ -144,7 +134,6 @@ namespace Artemis.DAL ((KeyboardPropertiesModel) gifLayer.Properties).GifFile = gifPath; AddOrUpdate(demoProfile); } - } /// @@ -166,18 +155,17 @@ namespace Artemis.DAL /// The loaded profile, or null if invalid public static ProfileModel LoadProfileIfValid(string path) { + // TODO: What exception on load failure? try { - var deserializer = new XmlSerializer(typeof(ProfileModel)); - using (var file = new StreamReader(path)) - { - var prof = (ProfileModel) deserializer.Deserialize(file); - if (!(prof.GameName?.Length > 1) || !(prof.KeyboardSlug?.Length > 1) || !(prof.Name?.Length > 1)) - return null; - return prof; - } + var prof = JsonConvert.DeserializeObject(File.ReadAllText(path)); + if (prof == null) + return null; + if (prof.GameName.Length < 1 || prof.KeyboardSlug.Length < 1 || prof.Name.Length < 1) + return null; + return prof; } - catch (InvalidOperationException) + catch (Exception) { return null; } @@ -186,15 +174,12 @@ namespace Artemis.DAL /// /// Exports the given profile to the provided path in XML /// - /// The profile to export + /// The profile to export /// The path to save the profile to - public static void ExportProfile(ProfileModel selectedProfile, string path) + public static void ExportProfile(ProfileModel prof, string path) { - var serializer = new XmlSerializer(typeof(ProfileModel)); - using (var file = new StreamWriter(path)) - { - serializer.Serialize(file, selectedProfile); - } + var json = JsonConvert.SerializeObject(prof); + File.WriteAllText(path, json); } /// @@ -208,7 +193,7 @@ namespace Artemis.DAL return; // Remove the old file - var path = ProfileFolder + $@"\{profile.KeyboardSlug}\{profile.GameName}\{profile.Name}.xml"; + var path = ProfileFolder + $@"\{profile.KeyboardSlug}\{profile.GameName}\{profile.Name}.json"; if (File.Exists(path)) File.Delete(path); @@ -220,7 +205,7 @@ namespace Artemis.DAL public static void DeleteProfile(ProfileModel profile) { // Remove the file - var path = ProfileFolder + $@"\{profile.KeyboardSlug}\{profile.GameName}\{profile.Name}.xml"; + var path = ProfileFolder + $@"\{profile.KeyboardSlug}\{profile.GameName}\{profile.Name}.json"; if (File.Exists(path)) File.Delete(path); } diff --git a/Artemis/Artemis/DeviceProviders/Corsair/CorsairHeadsets.cs b/Artemis/Artemis/DeviceProviders/Corsair/CorsairHeadsets.cs index fdedd3c49..db45cd5ab 100644 --- a/Artemis/Artemis/DeviceProviders/Corsair/CorsairHeadsets.cs +++ b/Artemis/Artemis/DeviceProviders/Corsair/CorsairHeadsets.cs @@ -1,8 +1,7 @@ -using System.Linq; +using System; +using System.Drawing; +using System.Linq; using System.Threading; -using System.Windows; -using System.Windows.Media; -using Artemis.Utilities; using CUE.NET; using CUE.NET.Devices.Generic.Enums; using Ninject.Extensions.Logging; @@ -35,33 +34,30 @@ namespace Artemis.DeviceProviders.Corsair CueSDK.Reinitialize(); } - public override void UpdateDevice(Brush brush) + public override void UpdateDevice(Bitmap bitmap) { - if (!CanUse || brush == null) + if (!CanUse || bitmap == null) return; + if (bitmap.Width != bitmap.Height) + throw new ArgumentException("Bitmap must be a perfect square"); var leds = CueSDK.HeadsetSDK.Leds.Count(); - var rect = new Rect(new Size(leds*20, leds*20)); - - var visual = new DrawingVisual(); - using (var c = visual.RenderOpen()) - c.DrawRectangle(brush, null, rect); - - using (var img = ImageUtilities.DrawinVisualToBitmap(visual, rect)) + var step = (double) bitmap.Width/leds; + using (bitmap) { - var ledIndex = 0; // Color each LED according to one of the pixels foreach (var corsairLed in CueSDK.HeadsetSDK.Leds) { - corsairLed.Color = ledIndex == 0 - ? img.GetPixel(0, 0) - : img.GetPixel((ledIndex + 1)*20 - 1, (ledIndex + 1)*20 - 1); + if (ledIndex == 0) + corsairLed.Color = bitmap.GetPixel(0, 0); + else + corsairLed.Color = bitmap.GetPixel((int) ((ledIndex + 1)*step - 1), + (int) ((ledIndex + 1)*step - 1)); ledIndex++; } } - // Flush is required for headset to work reliably on CUE2 for some reason - CueSDK.HeadsetSDK.Update(true); + CueSDK.HeadsetSDK.Update(); } private static bool CanInitializeSdk() diff --git a/Artemis/Artemis/DeviceProviders/Corsair/CorsairMice.cs b/Artemis/Artemis/DeviceProviders/Corsair/CorsairMice.cs index 3e08f941c..4fc70ce84 100644 --- a/Artemis/Artemis/DeviceProviders/Corsair/CorsairMice.cs +++ b/Artemis/Artemis/DeviceProviders/Corsair/CorsairMice.cs @@ -1,8 +1,7 @@ -using System.Linq; +using System; +using System.Drawing; +using System.Linq; using System.Threading; -using System.Windows; -using System.Windows.Media; -using Artemis.Utilities; using CUE.NET; using CUE.NET.Devices.Generic.Enums; using Ninject.Extensions.Logging; @@ -35,27 +34,26 @@ namespace Artemis.DeviceProviders.Corsair CueSDK.Reinitialize(); } - public override void UpdateDevice(Brush brush) + public override void UpdateDevice(Bitmap bitmap) { - if (!CanUse || brush == null) + if (!CanUse || bitmap == null) return; + if (bitmap.Width != bitmap.Height) + throw new ArgumentException("Bitmap must be a perfect square"); var leds = CueSDK.MouseSDK.Leds.Count(); - var rect = new Rect(new Size(leds*20, leds*20)); - - var visual = new DrawingVisual(); - using (var c = visual.RenderOpen()) - c.DrawRectangle(brush, null, rect); - - using (var img = ImageUtilities.DrawinVisualToBitmap(visual, rect)) + var step = (double) bitmap.Width/leds; + using (bitmap) { var ledIndex = 0; // Color each LED according to one of the pixels foreach (var corsairLed in CueSDK.MouseSDK.Leds) { - corsairLed.Color = ledIndex == 0 - ? img.GetPixel(0, 0) - : img.GetPixel((ledIndex + 1)*20 - 1, (ledIndex + 1)*20 - 1); + if (ledIndex == 0) + corsairLed.Color = bitmap.GetPixel(0, 0); + else + corsairLed.Color = bitmap.GetPixel((int) ((ledIndex + 1)*step - 1), + (int) ((ledIndex + 1)*step - 1)); ledIndex++; } } diff --git a/Artemis/Artemis/DeviceProviders/DeviceProvider.cs b/Artemis/Artemis/DeviceProviders/DeviceProvider.cs index c244eb73a..f68bc3dbf 100644 --- a/Artemis/Artemis/DeviceProviders/DeviceProvider.cs +++ b/Artemis/Artemis/DeviceProviders/DeviceProvider.cs @@ -1,5 +1,5 @@ -using System.Threading.Tasks; -using System.Windows.Media; +using System.Drawing; +using System.Threading.Tasks; namespace Artemis.DeviceProviders { @@ -16,10 +16,10 @@ namespace Artemis.DeviceProviders public bool CanUse { get; set; } /// - /// Updates a non-keyboard to take the colours of the provided brush + /// Updates a non-keyboard to take the colours found in the provided bitmap /// - /// - public abstract void UpdateDevice(Brush brush); + /// + public abstract void UpdateDevice(Bitmap bitmap); /// /// Tries to enable the device and updates CanUse accordingly @@ -32,7 +32,7 @@ namespace Artemis.DeviceProviders public abstract void Disable(); /// - /// Tries to enable the device and updates CanUse accordingly asynchronously + /// Tries to enable the device and updates CanUse accordingly asynchronously /// /// public Task TryEnableAsync() diff --git a/Artemis/Artemis/DeviceProviders/KeyboardProvider.cs b/Artemis/Artemis/DeviceProviders/KeyboardProvider.cs index ab0984904..887f4be74 100644 --- a/Artemis/Artemis/DeviceProviders/KeyboardProvider.cs +++ b/Artemis/Artemis/DeviceProviders/KeyboardProvider.cs @@ -4,7 +4,6 @@ using System.Threading; using System.Threading.Tasks; using System.Windows; using MahApps.Metro.Controls.Dialogs; -using Brush = System.Windows.Media.Brush; using Size = System.Windows.Size; namespace Artemis.DeviceProviders @@ -87,7 +86,7 @@ namespace Artemis.DeviceProviders return Task.Run(() => Enable()); } - public override void UpdateDevice(Brush brush) + public override void UpdateDevice(Bitmap bitmap) { throw new NotImplementedException("KeyboardProvider doesn't implement UpdateDevice, use DrawBitmap instead."); } diff --git a/Artemis/Artemis/Events/ChangeBitmap.cs b/Artemis/Artemis/Events/ChangeBitmap.cs deleted file mode 100644 index 4f72e82b8..000000000 --- a/Artemis/Artemis/Events/ChangeBitmap.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Drawing; - -namespace Artemis.Events -{ - public class ChangeBitmap - { - public ChangeBitmap(Bitmap bitmap) - { - Bitmap = bitmap; - } - - public Bitmap Bitmap { get; private set; } - - public void ChangeTextMessage(Bitmap bitmap) - { - Bitmap = bitmap; - } - } -} \ No newline at end of file diff --git a/Artemis/Artemis/Events/RazerColorArrayChanged.cs b/Artemis/Artemis/Events/RazerColorArrayChanged.cs new file mode 100644 index 000000000..23d1ae192 --- /dev/null +++ b/Artemis/Artemis/Events/RazerColorArrayChanged.cs @@ -0,0 +1,14 @@ +using System.Windows.Media; + +namespace Artemis.Events +{ + public class RazerColorArrayChanged + { + public RazerColorArrayChanged(Color[,] colors) + { + Colors = colors; + } + + public Color[,] Colors { get; set; } + } +} \ No newline at end of file diff --git a/Artemis/Artemis/InjectionFactories/ILayerEditorVmFactory.cs b/Artemis/Artemis/InjectionFactories/ILayerEditorVmFactory.cs index a33a3416b..0f3e0609f 100644 --- a/Artemis/Artemis/InjectionFactories/ILayerEditorVmFactory.cs +++ b/Artemis/Artemis/InjectionFactories/ILayerEditorVmFactory.cs @@ -1,5 +1,5 @@ using Artemis.Models.Interfaces; -using Artemis.Models.Profiles; +using Artemis.Profiles.Layers.Models; using Artemis.ViewModels.Profiles; namespace Artemis.InjectionFactories diff --git a/Artemis/Artemis/InjectionModules/ArtemisModules.cs b/Artemis/Artemis/InjectionModules/ArtemisModules.cs index 2ec4602f1..4aa41678d 100644 --- a/Artemis/Artemis/InjectionModules/ArtemisModules.cs +++ b/Artemis/Artemis/InjectionModules/ArtemisModules.cs @@ -13,6 +13,14 @@ using Artemis.Modules.Games.RocketLeague; using Artemis.Modules.Games.TheDivision; using Artemis.Modules.Games.Witcher3; using Artemis.Modules.Overlays.VolumeDisplay; +using Artemis.Profiles.Layers.Animations; +using Artemis.Profiles.Layers.Conditions; +using Artemis.Profiles.Layers.Interfaces; +using Artemis.Profiles.Layers.Types.Folder; +using Artemis.Profiles.Layers.Types.Headset; +using Artemis.Profiles.Layers.Types.Keyboard; +using Artemis.Profiles.Layers.Types.KeyboardGif; +using Artemis.Profiles.Layers.Types.Mouse; using Artemis.ViewModels.Abstract; using Ninject.Modules; @@ -55,6 +63,28 @@ namespace Artemis.InjectionModules Bind().To().InSingletonScope(); #endregion + + #region Layers + + // Animations + Bind().To(); + Bind().To(); + Bind().To(); + Bind().To(); + Bind().To(); + Bind().To(); + Bind().To(); + // Conditions + Bind().To(); + Bind().To(); + // Types + Bind().To(); + Bind().To(); + Bind().To(); + Bind().To(); + Bind().To(); + + #endregion } } } \ No newline at end of file diff --git a/Artemis/Artemis/InjectionModules/BaseModules.cs b/Artemis/Artemis/InjectionModules/BaseModules.cs index 05805579a..0c5a73936 100644 --- a/Artemis/Artemis/InjectionModules/BaseModules.cs +++ b/Artemis/Artemis/InjectionModules/BaseModules.cs @@ -19,6 +19,7 @@ namespace Artemis.InjectionModules Bind().ToFactory(); Bind().ToFactory(); Bind().ToSelf(); + Bind().ToSelf().InSingletonScope(); Bind().To().InSingletonScope(); Bind().To().InSingletonScope(); diff --git a/Artemis/Artemis/Managers/LoopManager.cs b/Artemis/Artemis/Managers/LoopManager.cs index 73aa62ab3..32abd3fba 100644 --- a/Artemis/Artemis/Managers/LoopManager.cs +++ b/Artemis/Artemis/Managers/LoopManager.cs @@ -6,7 +6,6 @@ using System.Timers; using Artemis.Events; using Caliburn.Micro; using Ninject.Extensions.Logging; -using Brush = System.Windows.Media.Brush; namespace Artemis.Managers { @@ -21,7 +20,8 @@ namespace Artemis.Managers private readonly Timer _loopTimer; private Bitmap _keyboardBitmap; - public LoopManager(IEventAggregator events, ILogger logger, EffectManager effectManager, DeviceManager deviceManager) + public LoopManager(IEventAggregator events, ILogger logger, EffectManager effectManager, + DeviceManager deviceManager) { events.Subscribe(this); _logger = logger; @@ -48,6 +48,18 @@ namespace Artemis.Managers _keyboardBitmap?.Dispose(); } + public void Handle(ActiveEffectChanged message) + { + if (_deviceManager.ActiveKeyboard != null && _effectManager.ActiveEffect != null) + _keyboardBitmap = _deviceManager.ActiveKeyboard.KeyboardBitmap(_effectManager.ActiveEffect.KeyboardScale); + } + + public void Handle(ActiveKeyboardChanged message) + { + if (_deviceManager.ActiveKeyboard != null && _effectManager.ActiveEffect != null) + _keyboardBitmap = _deviceManager.ActiveKeyboard.KeyboardBitmap(_effectManager.ActiveEffect.KeyboardScale); + } + public Task StartAsync() { return Task.Run(() => Start()); @@ -132,50 +144,31 @@ namespace Artemis.Managers renderEffect.Update(); // Get ActiveEffect's bitmap - Brush mouseBrush = null; - Brush headsetBrush = null; + Bitmap mouseBitmap = null; + Bitmap headsetBitmap = null; var mice = _deviceManager.MiceProviders.Where(m => m.CanUse).ToList(); var headsets = _deviceManager.HeadsetProviders.Where(m => m.CanUse).ToList(); - using (Graphics keyboardGraphics = Graphics.FromImage(_keyboardBitmap)) + if (renderEffect.Initialized) + renderEffect.Render(_keyboardBitmap, out mouseBitmap, out headsetBitmap, mice.Any(), headsets.Any()); + + // Draw enabled overlays on top of the renderEffect + foreach (var overlayModel in _effectManager.EnabledOverlays) { - // Fill the bitmap's background with black to avoid trailing colors on some keyboards - keyboardGraphics.Clear(Color.Black); - - if (renderEffect.Initialized) - renderEffect.Render(keyboardGraphics, out mouseBrush, out headsetBrush, mice.Any(), - headsets.Any()); - - // Draw enabled overlays on top of the renderEffect - foreach (var overlayModel in _effectManager.EnabledOverlays) - { - overlayModel.Update(); - overlayModel.RenderOverlay(keyboardGraphics, ref mouseBrush, ref headsetBrush, mice.Any(), - headsets.Any()); - } - - // Update mice and headsets - foreach (var mouse in mice) - mouse.UpdateDevice(mouseBrush); - foreach (var headset in headsets) - headset.UpdateDevice(headsetBrush); + overlayModel.Update(); + overlayModel.RenderOverlay(_keyboardBitmap, ref mouseBitmap, ref headsetBitmap, mice.Any(), + headsets.Any()); } + // Update mice and headsets + foreach (var mouse in mice) + mouse.UpdateDevice(mouseBitmap); + foreach (var headset in headsets) + headset.UpdateDevice(headsetBitmap); + // Update the keyboard _deviceManager.ActiveKeyboard?.DrawBitmap(_keyboardBitmap); } } - - public void Handle(ActiveKeyboardChanged message) - { - if (_deviceManager.ActiveKeyboard != null &&_effectManager.ActiveEffect != null) - _keyboardBitmap = _deviceManager.ActiveKeyboard.KeyboardBitmap(_effectManager.ActiveEffect.KeyboardScale); - } - - public void Handle(ActiveEffectChanged message) - { - if (_deviceManager.ActiveKeyboard != null && _effectManager.ActiveEffect != null) - _keyboardBitmap = _deviceManager.ActiveKeyboard.KeyboardBitmap(_effectManager.ActiveEffect.KeyboardScale); - } } } \ No newline at end of file diff --git a/Artemis/Artemis/Models/EffectModel.cs b/Artemis/Artemis/Models/EffectModel.cs index 962d4cf90..67423ef6d 100644 --- a/Artemis/Artemis/Models/EffectModel.cs +++ b/Artemis/Artemis/Models/EffectModel.cs @@ -2,12 +2,14 @@ using System.Collections.Generic; using System.Drawing; using System.Linq; +using System.Windows; using Artemis.Managers; using Artemis.Models.Interfaces; -using Artemis.Models.Profiles; +using Artemis.Profiles; +using Artemis.Profiles.Layers.Models; +using Artemis.Profiles.Layers.Types.Headset; +using Artemis.Profiles.Layers.Types.Mouse; using Newtonsoft.Json; -using NLog; -using Brush = System.Windows.Media.Brush; namespace Artemis.Models { @@ -15,12 +17,7 @@ namespace Artemis.Models { public delegate void SettingsUpdateHandler(EffectSettings settings); - public bool Initialized { get; set; } - public MainManager MainManager { get; set; } - public string Name { get; set; } - public int KeyboardScale { get; set; } = 4; - - private DateTime _lastTrace; + protected DateTime LastTrace; protected EffectModel(MainManager mainManager, IDataModel dataModel) { @@ -28,6 +25,11 @@ namespace Artemis.Models DataModel = dataModel; } + public bool Initialized { get; set; } + public MainManager MainManager { get; set; } + public string Name { get; set; } + public int KeyboardScale { get; set; } = 4; + // Used by profile system public IDataModel DataModel { get; set; } public ProfileModel Profile { get; set; } @@ -41,7 +43,8 @@ namespace Artemis.Models public abstract void Update(); // Called after every update - public virtual void Render(Graphics keyboard, out Brush mouse, out Brush headset, bool renderMice, bool renderHeadsets) + public virtual void Render(Bitmap keyboard, out Bitmap mouse, out Bitmap headset, bool renderMice, + bool renderHeadsets) { mouse = null; headset = null; @@ -52,24 +55,41 @@ namespace Artemis.Models // Get all enabled layers who's conditions are met var renderLayers = GetRenderLayers(renderMice, renderHeadsets); - // Trace debugging - if (DateTime.Now.AddSeconds(-2) > _lastTrace) + // Render the keyboard layer-by-layer + var keyboardRect = MainManager.DeviceManager.ActiveKeyboard.KeyboardRectangle(KeyboardScale); + using (var g = Graphics.FromImage(keyboard)) { - _lastTrace = DateTime.Now; - MainManager.Logger.Trace("Effect datamodel as JSON: \r\n{0}", - JsonConvert.SerializeObject(DataModel, Formatting.Indented)); - MainManager.Logger.Trace("Effect {0} has to render {1} layers", Name, renderLayers.Count); - foreach (var renderLayer in renderLayers) - MainManager.Logger.Trace(" Layer name: {0}, layer type: {1}", renderLayer.Name, - renderLayer.LayerType); + // Fill the bitmap's background with black to avoid trailing colors on some keyboards + g.Clear(Color.Black); + Profile.DrawLayers(g, renderLayers.Where(rl => rl.MustDraw()), DataModel, keyboardRect, false, true); } - // Render the keyboard layer-by-layer - Profile.DrawProfile(keyboard, renderLayers, DataModel, MainManager.DeviceManager.ActiveKeyboard.KeyboardRectangle(KeyboardScale), false, true); - // Render the first enabled mouse (will default to null if renderMice was false) - mouse = Profile.GenerateBrush(renderLayers.LastOrDefault(l => l.LayerType == LayerType.Mouse), DataModel); - // Render the first enabled headset (will default to null if renderHeadsets was false) - headset = Profile.GenerateBrush(renderLayers.LastOrDefault(l => l.LayerType == LayerType.Headset), DataModel); + // Render the mouse layer-by-layer + var smallRect = new Rect(0, 0, 40, 40); + mouse = new Bitmap(40, 40); + using (var g = Graphics.FromImage(mouse)) + { + Profile.DrawLayers(g, renderLayers.Where(rl => rl.LayerType is MouseType), DataModel, smallRect, + false, true); + } + + // Render the headset layer-by-layer + headset = new Bitmap(40, 40); + using (var g = Graphics.FromImage(headset)) + { + Profile.DrawLayers(g, renderLayers.Where(rl => rl.LayerType is HeadsetType), DataModel, smallRect, + false, true); + } + + // Trace debugging + if (DateTime.Now.AddSeconds(-2) <= LastTrace) + return; + LastTrace = DateTime.Now; + MainManager.Logger.Trace("Effect datamodel as JSON: \r\n{0}", + JsonConvert.SerializeObject(DataModel, Formatting.Indented)); + MainManager.Logger.Trace("Effect {0} has to render {1} layers", Name, renderLayers.Count); + foreach (var renderLayer in renderLayers) + MainManager.Logger.Trace("- Layer name: {0}, layer type: {1}", renderLayer.Name, renderLayer.LayerType); } public abstract List GetRenderLayers(bool renderMice, bool renderHeadsets); diff --git a/Artemis/Artemis/Models/OverlayModel.cs b/Artemis/Artemis/Models/OverlayModel.cs index 5dac1f475..8d4ac2335 100644 --- a/Artemis/Artemis/Models/OverlayModel.cs +++ b/Artemis/Artemis/Models/OverlayModel.cs @@ -1,6 +1,5 @@ using System.Drawing; using Artemis.Managers; -using Brush = System.Windows.Media.Brush; namespace Artemis.Models { @@ -29,7 +28,7 @@ namespace Artemis.Models } } - public abstract void RenderOverlay(Graphics keyboard, ref Brush mouse, ref Brush headset, bool renderMice, + public abstract void RenderOverlay(Bitmap keyboard, ref Bitmap mouse, ref Bitmap headset, bool renderMice, bool renderHeadsets); } } \ No newline at end of file diff --git a/Artemis/Artemis/Models/Profiles/Properties/FolderPropertiesModel.cs b/Artemis/Artemis/Models/Profiles/Properties/FolderPropertiesModel.cs deleted file mode 100644 index dc36491c4..000000000 --- a/Artemis/Artemis/Models/Profiles/Properties/FolderPropertiesModel.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Artemis.Models.Interfaces; - -namespace Artemis.Models.Profiles.Properties -{ - public class FolderPropertiesModel : LayerPropertiesModel - { - public override AppliedProperties GetAppliedProperties(IDataModel dataModel, bool ignoreDynamic = false) - { - return new AppliedProperties(); - } - } -} \ No newline at end of file diff --git a/Artemis/Artemis/Models/Profiles/Properties/HeadsetPropertiesModel.cs b/Artemis/Artemis/Models/Profiles/Properties/HeadsetPropertiesModel.cs deleted file mode 100644 index c44226e99..000000000 --- a/Artemis/Artemis/Models/Profiles/Properties/HeadsetPropertiesModel.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Artemis.Models.Interfaces; - -namespace Artemis.Models.Profiles.Properties -{ - public class HeadsetPropertiesModel : LayerPropertiesModel - { - public override AppliedProperties GetAppliedProperties(IDataModel dataModel, bool ignoreDynamic = false) - { - return new AppliedProperties {Brush = Brush}; - } - } -} \ No newline at end of file diff --git a/Artemis/Artemis/Models/Profiles/Properties/KeyboardPropertiesModel.cs b/Artemis/Artemis/Models/Profiles/Properties/KeyboardPropertiesModel.cs deleted file mode 100644 index 197fdccae..000000000 --- a/Artemis/Artemis/Models/Profiles/Properties/KeyboardPropertiesModel.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Windows; -using System.Xml.Serialization; -using Artemis.Models.Interfaces; - -namespace Artemis.Models.Profiles.Properties -{ - public class KeyboardPropertiesModel : LayerPropertiesModel - { - public KeyboardPropertiesModel() - { - DynamicProperties = new List(); - } - - public double X { get; set; } - public double Y { get; set; } - public double Width { get; set; } - public double Height { get; set; } - public double Opacity { get; set; } - public bool Contain { get; set; } - public LayerAnimation Animation { get; set; } - public double AnimationSpeed { get; set; } - public string GifFile { get; set; } - public List DynamicProperties { get; set; } - - [XmlIgnore] - public double AnimationProgress { get; set; } - - public Rect GetRect(int scale = 4) - { - return new Rect(X*scale, Y*scale, Width*scale, Height*scale); - } - - public override AppliedProperties GetAppliedProperties(IDataModel dataModel, bool ignoreDynamic = false) - { - var applied = new AppliedProperties - { - X = X, - Y = Y, - Width = Width, - Height = Height, - Opacity = Opacity, - Brush = Brush.CloneCurrentValue() - }; - - if (ignoreDynamic) - return applied; - - foreach (var dynamicProperty in DynamicProperties) - dynamicProperty.ApplyProperty(dataModel, ref applied); - - if (Math.Abs(applied.Opacity - 1) > 0.001) - { - applied.Brush = Brush.CloneCurrentValue(); - applied.Brush.Opacity = applied.Opacity; - } - - return applied; - } - } - - public enum LayerAnimation - { - [Description("None")] None, - [Description("Slide left")] SlideLeft, - [Description("Slide right")] SlideRight, - [Description("Slide up")] SlideUp, - [Description("Slide down")] SlideDown, - [Description("Grow")] Grow, - [Description("Pulse")] Pulse - } -} \ No newline at end of file diff --git a/Artemis/Artemis/Models/Profiles/Properties/LayerPropertiesModel.cs b/Artemis/Artemis/Models/Profiles/Properties/LayerPropertiesModel.cs deleted file mode 100644 index 0cd18e38c..000000000 --- a/Artemis/Artemis/Models/Profiles/Properties/LayerPropertiesModel.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Collections.Generic; -using System.Windows.Media; -using System.Xml.Serialization; -using Artemis.Models.Interfaces; - -namespace Artemis.Models.Profiles.Properties -{ - [XmlInclude(typeof(SolidColorBrush))] - [XmlInclude(typeof(LinearGradientBrush))] - [XmlInclude(typeof(RadialGradientBrush))] - [XmlInclude(typeof(MatrixTransform))] - [XmlInclude(typeof(KeyboardPropertiesModel))] - [XmlInclude(typeof(MousePropertiesModel))] - [XmlInclude(typeof(HeadsetPropertiesModel))] - [XmlInclude(typeof(FolderPropertiesModel))] - public abstract class LayerPropertiesModel - { - private Brush _brush; - - protected LayerPropertiesModel() - { - Conditions = new List(); - } - - public List Conditions { get; set; } - - public Brush Brush - { - get { return _brush; } - set - { - if (value == null) - { - _brush = null; - return; - } - - if (value.IsFrozen) - { - _brush = value; - return; - } - - // Clone the brush off of the UI thread and freeze it - var cloned = value.Dispatcher.Invoke(value.CloneCurrentValue); - cloned.Freeze(); - _brush = cloned; - } - } - - public abstract AppliedProperties GetAppliedProperties(IDataModel dataModel, bool ignoreDynamic = false); - } - - public struct AppliedProperties - { - public double X { get; set; } - public double Y { get; set; } - public double Width { get; set; } - public double Height { get; set; } - public double Opacity { get; set; } - public Brush Brush { get; set; } - } -} \ No newline at end of file diff --git a/Artemis/Artemis/Models/Profiles/Properties/MousePropertiesModel.cs b/Artemis/Artemis/Models/Profiles/Properties/MousePropertiesModel.cs deleted file mode 100644 index 2bebe3c5b..000000000 --- a/Artemis/Artemis/Models/Profiles/Properties/MousePropertiesModel.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Artemis.Models.Interfaces; - -namespace Artemis.Models.Profiles.Properties -{ - public class MousePropertiesModel : LayerPropertiesModel - { - public override AppliedProperties GetAppliedProperties(IDataModel dataModel, bool ignoreDynamic = false) - { - return new AppliedProperties {Brush = Brush}; - } - } -} \ No newline at end of file diff --git a/Artemis/Artemis/Modules/Effects/AudioVisualizer/AudioVisualization.cs b/Artemis/Artemis/Modules/Effects/AudioVisualizer/AudioVisualization.cs new file mode 100644 index 000000000..a3db59c01 --- /dev/null +++ b/Artemis/Artemis/Modules/Effects/AudioVisualizer/AudioVisualization.cs @@ -0,0 +1,28 @@ +namespace Artemis.Modules.Effects.AudioVisualizer { + + + // This class allows you to handle specific events on the settings class: + // The SettingChanging event is raised before a setting's value is changed. + // The PropertyChanged event is raised after a setting's value is changed. + // The SettingsLoaded event is raised after the setting values are loaded. + // The SettingsSaving event is raised before the setting values are saved. + internal sealed partial class AudioVisualization { + + public AudioVisualization() { + // // To add event handlers for saving and changing settings, uncomment the lines below: + // + // this.SettingChanging += this.SettingChangingEventHandler; + // + // this.SettingsSaving += this.SettingsSavingEventHandler; + // + } + + private void SettingChangingEventHandler(object sender, System.Configuration.SettingChangingEventArgs e) { + // Add code to handle the SettingChangingEvent event here. + } + + private void SettingsSavingEventHandler(object sender, System.ComponentModel.CancelEventArgs e) { + // Add code to handle the SettingsSaving event here. + } + } +} diff --git a/Artemis/Artemis/Modules/Effects/AudioVisualizer/AudioVisualizerModel.cs b/Artemis/Artemis/Modules/Effects/AudioVisualizer/AudioVisualizerModel.cs index dc215cfe4..aa103501f 100644 --- a/Artemis/Artemis/Modules/Effects/AudioVisualizer/AudioVisualizerModel.cs +++ b/Artemis/Artemis/Modules/Effects/AudioVisualizer/AudioVisualizerModel.cs @@ -5,13 +5,12 @@ using System.Drawing.Drawing2D; using System.Linq; using Artemis.Managers; using Artemis.Models; -using Artemis.Models.Profiles; using Artemis.Modules.Effects.AudioVisualizer.Utilities; +using Artemis.Profiles.Layers.Models; using Artemis.Utilities; using Artemis.Utilities.Keyboard; using NAudio.CoreAudioApi; using NAudio.Wave; -using Brush = System.Windows.Media.Brush; namespace Artemis.Modules.Effects.AudioVisualizer { @@ -78,7 +77,7 @@ namespace Artemis.Modules.Effects.AudioVisualizer ColorHelpers.ToDrawingColor(Settings.BottomColor) }, LinearGradientMode.Vertical) - { ContainedBrush = false, Height = 0 }); + {ContainedBrush = false, Height = 0}); } _sensitivity = Settings.Sensitivity; _fromBottom = Settings.FromBottom; @@ -118,22 +117,22 @@ namespace Artemis.Modules.Effects.AudioVisualizer if (SpectrumData.Count - 1 < i || SpectrumData[i] == 0) height = 0; else - height = (int)Math.Round(SpectrumData[i] / 2.55); + height = (int) Math.Round(SpectrumData[i]/2.55); // Apply Sensitivity setting - height = height * _sensitivity; + height = height*_sensitivity; var keyboardHeight = - (int)Math.Round(MainManager.DeviceManager.ActiveKeyboard.Height / 100.00 * height * KeyboardScale); + (int) Math.Round(MainManager.DeviceManager.ActiveKeyboard.Height/100.00*height*KeyboardScale); if (keyboardHeight > SoundRectangles[i].Height) SoundRectangles[i].Height = keyboardHeight; else SoundRectangles[i].Height = SoundRectangles[i].Height - Settings.FadeSpeed; // Apply Bars setting - SoundRectangles[i].X = i * KeyboardScale; + SoundRectangles[i].X = i*KeyboardScale; SoundRectangles[i].Width = KeyboardScale; if (_fromBottom) - SoundRectangles[i].Y = MainManager.DeviceManager.ActiveKeyboard.Height * KeyboardScale - + SoundRectangles[i].Y = MainManager.DeviceManager.ActiveKeyboard.Height*KeyboardScale - SoundRectangles[i].Height; } _generating = false; @@ -164,7 +163,7 @@ namespace Artemis.Modules.Effects.AudioVisualizer for (x = 0; x < Lines; x++) { float peak = 0; - var b1 = (int)Math.Pow(2, x * 10.0 / (Lines - 1)); + var b1 = (int) Math.Pow(2, x*10.0/(Lines - 1)); if (b1 > 2047) b1 = 2047; if (b1 <= b0) @@ -174,12 +173,12 @@ namespace Artemis.Modules.Effects.AudioVisualizer if (peak < e.Result[1 + b0].X) peak = e.Result[1 + b0].X; } - var y = (int)(Math.Sqrt(peak) * 3 * 255 - 4); + var y = (int) (Math.Sqrt(peak)*3*255 - 4); if (y > 255) y = 255; if (y < 0) y = 0; - SpectrumData.Add((byte)y); + SpectrumData.Add((byte) y); } } @@ -188,7 +187,7 @@ namespace Artemis.Modules.Effects.AudioVisualizer return null; } - public override void Render(Graphics keyboard, out Brush mouse, out Brush headset, bool renderMice, + public override void Render(Bitmap keyboard, out Bitmap mouse, out Bitmap headset, bool renderMice, bool renderHeadsets) { mouse = null; @@ -200,8 +199,11 @@ namespace Artemis.Modules.Effects.AudioVisualizer // Lock the _spectrumData array while busy with it _generating = true; - foreach (var soundRectangle in SoundRectangles) - soundRectangle.Draw(keyboard); + using (var g = Graphics.FromImage(keyboard)) + { + foreach (var soundRectangle in SoundRectangles) + soundRectangle.Draw(g); + } _generating = false; } diff --git a/Artemis/Artemis/Modules/Effects/Bubbles/Bubble.cs b/Artemis/Artemis/Modules/Effects/Bubbles/Bubble.cs index 535e03428..3e9755968 100644 --- a/Artemis/Artemis/Modules/Effects/Bubbles/Bubble.cs +++ b/Artemis/Artemis/Modules/Effects/Bubbles/Bubble.cs @@ -6,11 +6,24 @@ namespace Artemis.Modules.Effects.Bubbles { public class Bubble { + #region Constructors + + public Bubble(Color color, int radius, Point position, Vector direction) + { + Color = color; + Radius = radius; + Position = position; + Direction = direction; + } + + #endregion + #region Properties & Fields private Brush _brush; private Color _color; + public Color Color { get { return _color; } @@ -27,27 +40,15 @@ namespace Artemis.Modules.Effects.Bubbles #endregion - #region Constructors - - public Bubble(Color color, int radius, Point position, Vector direction) - { - this.Color = color; - this.Radius = radius; - this.Position = position; - this.Direction = direction; - } - - #endregion - #region Methods public void CheckCollision(Rect border) { if (Position.X - Radius < border.X || Position.X + Radius > border.X + border.Width) - Direction = new Vector(Direction.X * -1, Direction.Y); + Direction = new Vector(Direction.X*-1, Direction.Y); if (Position.Y - Radius < border.Y || Position.Y + Radius > border.Y + border.Height) - Direction = new Vector(Direction.X, Direction.Y * -1); + Direction = new Vector(Direction.X, Direction.Y*-1); } public void Move() @@ -57,9 +58,9 @@ namespace Artemis.Modules.Effects.Bubbles public void Draw(Graphics g) { - g.FillEllipse(_brush, (float)Position.X - Radius, (float)Position.Y - Radius, Radius * 2, Radius * 2); + g.FillEllipse(_brush, (float) Position.X - Radius, (float) Position.Y - Radius, Radius*2, Radius*2); } #endregion } -} +} \ No newline at end of file diff --git a/Artemis/Artemis/Modules/Effects/Bubbles/Bubbles.cs b/Artemis/Artemis/Modules/Effects/Bubbles/Bubbles.cs new file mode 100644 index 000000000..bf183261a --- /dev/null +++ b/Artemis/Artemis/Modules/Effects/Bubbles/Bubbles.cs @@ -0,0 +1,28 @@ +namespace Artemis.Modules.Effects.Bubbles { + + + // This class allows you to handle specific events on the settings class: + // The SettingChanging event is raised before a setting's value is changed. + // The PropertyChanged event is raised after a setting's value is changed. + // The SettingsLoaded event is raised after the setting values are loaded. + // The SettingsSaving event is raised before the setting values are saved. + internal sealed partial class Bubbles { + + public Bubbles() { + // // To add event handlers for saving and changing settings, uncomment the lines below: + // + // this.SettingChanging += this.SettingChangingEventHandler; + // + // this.SettingsSaving += this.SettingsSavingEventHandler; + // + } + + private void SettingChangingEventHandler(object sender, System.Configuration.SettingChangingEventArgs e) { + // Add code to handle the SettingChangingEvent event here. + } + + private void SettingsSavingEventHandler(object sender, System.ComponentModel.CancelEventArgs e) { + // Add code to handle the SettingsSaving event here. + } + } +} diff --git a/Artemis/Artemis/Modules/Effects/Bubbles/Bubbles.settings b/Artemis/Artemis/Modules/Effects/Bubbles/Bubbles.settings index 98fa04416..748dada51 100644 --- a/Artemis/Artemis/Modules/Effects/Bubbles/Bubbles.settings +++ b/Artemis/Artemis/Modules/Effects/Bubbles/Bubbles.settings @@ -1,5 +1,7 @@  - + + diff --git a/Artemis/Artemis/Modules/Effects/Bubbles/BubblesModel.cs b/Artemis/Artemis/Modules/Effects/Bubbles/BubblesModel.cs index 66ecd0ea2..0ec3ecb48 100644 --- a/Artemis/Artemis/Modules/Effects/Bubbles/BubblesModel.cs +++ b/Artemis/Artemis/Modules/Effects/Bubbles/BubblesModel.cs @@ -4,24 +4,14 @@ using System.Drawing; using System.Windows; using Artemis.Managers; using Artemis.Models; -using Artemis.Models.Profiles; +using Artemis.Profiles.Layers.Models; using Artemis.Utilities; -using Brush = System.Windows.Media.Brush; +using Point = System.Windows.Point; namespace Artemis.Modules.Effects.Bubbles { public class BubblesModel : EffectModel { - #region Properties & Fields - - private static readonly Random _random = new Random(); - - private readonly List _bubbles = new List(); - - public BubblesSettings Settings { get; } - - #endregion - #region Constructors public BubblesModel(MainManager mainManager, BubblesSettings settings) @@ -34,27 +24,43 @@ namespace Artemis.Modules.Effects.Bubbles #endregion + #region Properties & Fields + + private static readonly Random _random = new Random(); + + private readonly List _bubbles = new List(); + + public BubblesSettings Settings { get; } + + #endregion + #region Methods public override void Enable() { KeyboardScale = Settings.Smoothness; - Rect rect = MainManager.DeviceManager.ActiveKeyboard.KeyboardRectangle(KeyboardScale); + var rect = MainManager.DeviceManager.ActiveKeyboard.KeyboardRectangle(KeyboardScale); - double scaleFactor = Settings.Smoothness / 25.0; + var scaleFactor = Settings.Smoothness/25.0; - for (int i = 0; i < Settings.BubbleCount; i++) + for (var i = 0; i < Settings.BubbleCount; i++) { - Color color = Settings.IsRandomColors ? ColorHelpers.GetRandomRainbowColor() : ColorHelpers.ToDrawingColor(Settings.BubbleColor); + var color = Settings.IsRandomColors + ? ColorHelpers.GetRandomRainbowColor() + : ColorHelpers.ToDrawingColor(Settings.BubbleColor); // -Settings.MoveSpeed because we want to spawn at least one move away from borders - double initialPositionX = ((rect.Width - (Settings.BubbleSize * scaleFactor * 2) - Settings.MoveSpeed * scaleFactor) * _random.NextDouble()) + Settings.BubbleSize * scaleFactor; - double initialPositionY = ((rect.Height - (Settings.BubbleSize * scaleFactor * 2) - Settings.MoveSpeed * scaleFactor) * _random.NextDouble()) + Settings.BubbleSize * scaleFactor; - double initialDirectionX = (Settings.MoveSpeed * scaleFactor * _random.NextDouble()) * (_random.Next(1) == 0 ? -1 : 1); - double initialDirectionY = (Settings.MoveSpeed * scaleFactor - Math.Abs(initialDirectionX)) * (_random.Next(1) == 0 ? -1 : 1); + var initialPositionX = (rect.Width - Settings.BubbleSize*scaleFactor*2 - Settings.MoveSpeed*scaleFactor)* + _random.NextDouble() + Settings.BubbleSize*scaleFactor; + var initialPositionY = (rect.Height - Settings.BubbleSize*scaleFactor*2 - Settings.MoveSpeed*scaleFactor)* + _random.NextDouble() + Settings.BubbleSize*scaleFactor; + var initialDirectionX = Settings.MoveSpeed*scaleFactor*_random.NextDouble()* + (_random.Next(1) == 0 ? -1 : 1); + var initialDirectionY = (Settings.MoveSpeed*scaleFactor - Math.Abs(initialDirectionX))* + (_random.Next(1) == 0 ? -1 : 1); - _bubbles.Add(new Bubble(color, (int)Math.Round(Settings.BubbleSize * scaleFactor), - new System.Windows.Point(initialPositionX, initialPositionY), new Vector(initialDirectionX, initialDirectionY))); + _bubbles.Add(new Bubble(color, (int) Math.Round(Settings.BubbleSize*scaleFactor), + new Point(initialPositionX, initialPositionY), new Vector(initialDirectionX, initialDirectionY))); } Initialized = true; @@ -68,24 +74,31 @@ namespace Artemis.Modules.Effects.Bubbles public override void Update() { - Rect keyboardRectangle = MainManager.DeviceManager.ActiveKeyboard.KeyboardRectangle(KeyboardScale); - foreach (Bubble bubble in _bubbles) + var keyboardRectangle = MainManager.DeviceManager.ActiveKeyboard.KeyboardRectangle(KeyboardScale); + foreach (var bubble in _bubbles) { if (Settings.IsShiftColors) - bubble.Color = ColorHelpers.ShiftColor(bubble.Color, Settings.IsRandomColors ? (int)Math.Round(Settings.ShiftColorSpeed * _random.NextDouble()) : Settings.ShiftColorSpeed); + bubble.Color = ColorHelpers.ShiftColor(bubble.Color, + Settings.IsRandomColors + ? (int) Math.Round(Settings.ShiftColorSpeed*_random.NextDouble()) + : Settings.ShiftColorSpeed); bubble.CheckCollision(keyboardRectangle); bubble.Move(); } } - public override void Render(Graphics keyboard, out Brush mouse, out Brush headset, bool renderMice, bool renderHeadsets) + public override void Render(Bitmap keyboard, out Bitmap mouse, out Bitmap headset, bool renderMice, + bool renderHeadsets) { mouse = null; headset = null; - foreach (Bubble bubble in _bubbles) - bubble.Draw(keyboard); + using (var g = Graphics.FromImage(keyboard)) + { + foreach (var bubble in _bubbles) + bubble.Draw(g); + } } public override List GetRenderLayers(bool renderMice, bool renderHeadsets) @@ -95,4 +108,4 @@ namespace Artemis.Modules.Effects.Bubbles #endregion } -} +} \ No newline at end of file diff --git a/Artemis/Artemis/Modules/Effects/Bubbles/BubblesSettings.cs b/Artemis/Artemis/Modules/Effects/Bubbles/BubblesSettings.cs index 149daa6d1..a6f264c67 100644 --- a/Artemis/Artemis/Modules/Effects/Bubbles/BubblesSettings.cs +++ b/Artemis/Artemis/Modules/Effects/Bubbles/BubblesSettings.cs @@ -57,4 +57,4 @@ namespace Artemis.Modules.Effects.Bubbles Smoothness = 25; } } -} +} \ No newline at end of file diff --git a/Artemis/Artemis/Modules/Effects/Bubbles/BubblesView.xaml b/Artemis/Artemis/Modules/Effects/Bubbles/BubblesView.xaml index 9c40b7792..cb79d8462 100644 --- a/Artemis/Artemis/Modules/Effects/Bubbles/BubblesView.xaml +++ b/Artemis/Artemis/Modules/Effects/Bubbles/BubblesView.xaml @@ -120,7 +120,7 @@ HorizontalAlignment="Right" Width="110" TickPlacement="None" TickFrequency="1" Value="{Binding Path=EffectSettings.MoveSpeed, Mode=TwoWay}" Minimum="1" Maximum="15" SmallChange="10" IsSnapToTickEnabled="True" /> - + diff --git a/Artemis/Artemis/Modules/Effects/Bubbles/BubblesView.xaml.cs b/Artemis/Artemis/Modules/Effects/Bubbles/BubblesView.xaml.cs index 2392befb1..3a9bf3cca 100644 --- a/Artemis/Artemis/Modules/Effects/Bubbles/BubblesView.xaml.cs +++ b/Artemis/Artemis/Modules/Effects/Bubbles/BubblesView.xaml.cs @@ -9,4 +9,4 @@ namespace Artemis.Modules.Effects.Bubbles InitializeComponent(); } } -} +} \ No newline at end of file diff --git a/Artemis/Artemis/Modules/Effects/Bubbles/BubblesViewModel.cs b/Artemis/Artemis/Modules/Effects/Bubbles/BubblesViewModel.cs index c24f95272..9c2803a0e 100644 --- a/Artemis/Artemis/Modules/Effects/Bubbles/BubblesViewModel.cs +++ b/Artemis/Artemis/Modules/Effects/Bubbles/BubblesViewModel.cs @@ -14,7 +14,7 @@ namespace Artemis.Modules.Effects.Bubbles events.Subscribe(this); MainManager.EffectManager.EffectModels.Add(EffectModel); - EffectSettings = ((BubblesModel)EffectModel).Settings; + EffectSettings = ((BubblesModel) EffectModel).Settings; } public void Handle(ActiveEffectChanged message) @@ -22,4 +22,4 @@ namespace Artemis.Modules.Effects.Bubbles NotifyOfPropertyChange(() => EffectEnabled); } } -} +} \ No newline at end of file diff --git a/Artemis/Artemis/Modules/Effects/ProfilePreview/ProfilePreviewModel.cs b/Artemis/Artemis/Modules/Effects/ProfilePreview/ProfilePreviewModel.cs index aa0819ab9..f66a8b7ad 100644 --- a/Artemis/Artemis/Modules/Effects/ProfilePreview/ProfilePreviewModel.cs +++ b/Artemis/Artemis/Modules/Effects/ProfilePreview/ProfilePreviewModel.cs @@ -1,11 +1,13 @@ using System.Collections.Generic; using System.Drawing; using System.Linq; +using System.Windows; using Artemis.Managers; using Artemis.Models; using Artemis.Models.Interfaces; -using Artemis.Models.Profiles; -using Brush = System.Windows.Media.Brush; +using Artemis.Profiles.Layers.Models; +using Artemis.Profiles.Layers.Types.Headset; +using Artemis.Profiles.Layers.Types.Mouse; namespace Artemis.Modules.Effects.ProfilePreview { @@ -32,10 +34,10 @@ namespace Artemis.Modules.Effects.ProfilePreview public override List GetRenderLayers(bool renderMice, bool renderHeadsets) { - return Profile.GetRenderLayers(DataModel, renderMice, renderHeadsets, true); + return Profile.GetRenderLayers(DataModel, renderMice, renderHeadsets, true); } - public override void Render(Graphics keyboard, out Brush mouse, out Brush headset, bool renderMice, + public override void Render(Bitmap keyboard, out Bitmap mouse, out Bitmap headset, bool renderMice, bool renderHeadsets) { mouse = null; @@ -48,12 +50,30 @@ namespace Artemis.Modules.Effects.ProfilePreview var renderLayers = GetRenderLayers(renderMice, renderHeadsets); // Render the keyboard layer-by-layer - Profile?.DrawProfile(keyboard, renderLayers, DataModel, MainManager.DeviceManager.ActiveKeyboard.KeyboardRectangle(KeyboardScale), true, true); - // Render the first enabled mouse (will default to null if renderMice was false) - mouse = Profile?.GenerateBrush(renderLayers.LastOrDefault(l => l.LayerType == LayerType.Mouse), DataModel); - // Render the first enabled headset (will default to null if renderHeadsets was false) - headset = Profile?.GenerateBrush(renderLayers.LastOrDefault(l => l.LayerType == LayerType.Headset), - DataModel); + var keyboardRect = MainManager.DeviceManager.ActiveKeyboard.KeyboardRectangle(KeyboardScale); + using (var g = Graphics.FromImage(keyboard)) + { + // Fill the bitmap's background with black to avoid trailing colors on some keyboards + g.Clear(Color.Black); + Profile.DrawLayers(g, renderLayers.Where(rl => rl.MustDraw()), DataModel, keyboardRect, true, true); + } + + // Render the mouse layer-by-layer + var smallRect = new Rect(0, 0, 40, 40); + mouse = new Bitmap(40, 40); + using (var g = Graphics.FromImage(mouse)) + { + Profile.DrawLayers(g, renderLayers.Where(rl => rl.LayerType is MouseType), DataModel, smallRect, + true, true); + } + + // Render the headset layer-by-layer + headset = new Bitmap(40, 40); + using (var g = Graphics.FromImage(headset)) + { + Profile.DrawLayers(g, renderLayers.Where(rl => rl.LayerType is HeadsetType), DataModel, smallRect, + true, true); + } } } diff --git a/Artemis/Artemis/Modules/Effects/TypeWave/TypeWaveModel.cs b/Artemis/Artemis/Modules/Effects/TypeWave/TypeWaveModel.cs index 366012c8f..8020ac68d 100644 --- a/Artemis/Artemis/Modules/Effects/TypeWave/TypeWaveModel.cs +++ b/Artemis/Artemis/Modules/Effects/TypeWave/TypeWaveModel.cs @@ -7,9 +7,8 @@ using Artemis.DeviceProviders.Corsair; using Artemis.DeviceProviders.Logitech.Utilities; using Artemis.Managers; using Artemis.Models; -using Artemis.Models.Profiles; +using Artemis.Profiles.Layers.Models; using Artemis.Utilities; -using Brush = System.Windows.Media.Brush; namespace Artemis.Modules.Effects.TypeWave { @@ -46,8 +45,8 @@ namespace Artemis.Modules.Effects.TypeWave return; _waves.Add(Settings.IsRandomColors - ? new Wave(new Point(keyMatch.PosX * KeyboardScale, keyMatch.PosY * KeyboardScale), 0, _randomColor) - : new Wave(new Point(keyMatch.PosX * KeyboardScale, keyMatch.PosY * KeyboardScale), 0, + ? new Wave(new Point(keyMatch.PosX*KeyboardScale, keyMatch.PosY*KeyboardScale), 0, _randomColor) + : new Wave(new Point(keyMatch.PosX*KeyboardScale, keyMatch.PosY*KeyboardScale), 0, ColorHelpers.ToDrawingColor(Settings.WaveColor))); } @@ -71,12 +70,12 @@ namespace Artemis.Modules.Effects.TypeWave // TODO: Get from settings var fps = 25; - _waves[i].Size += Settings.SpreadSpeed * KeyboardScale; + _waves[i].Size += Settings.SpreadSpeed*KeyboardScale; if (Settings.IsShiftColors) _waves[i].Color = ColorHelpers.ShiftColor(_waves[i].Color, Settings.ShiftColorSpeed); - var decreaseAmount = 255 / (Settings.TimeToLive / fps); + var decreaseAmount = 255/(Settings.TimeToLive/fps); _waves[i].Color = Color.FromArgb( _waves[i].Color.A - decreaseAmount, _waves[i].Color.R, _waves[i].Color.G, @@ -95,7 +94,7 @@ namespace Artemis.Modules.Effects.TypeWave return null; } - public override void Render(Graphics keyboard, out Brush mouse, out Brush headset, bool renderMice, + public override void Render(Bitmap keyboard, out Bitmap mouse, out Bitmap headset, bool renderMice, bool renderHeadsets) { mouse = null; @@ -111,7 +110,7 @@ namespace Artemis.Modules.Effects.TypeWave if (_waves[i].Size == 0) continue; var path = new GraphicsPath(); - path.AddEllipse(_waves[i].Point.X - _waves[i].Size / 2, _waves[i].Point.Y - _waves[i].Size / 2, + path.AddEllipse(_waves[i].Point.X - _waves[i].Size/2, _waves[i].Point.Y - _waves[i].Size/2, _waves[i].Size, _waves[i].Size); Color fillColor; @@ -122,16 +121,19 @@ namespace Artemis.Modules.Effects.TypeWave var pthGrBrush = new PathGradientBrush(path) { - SurroundColors = new[] { _waves[i].Color }, + SurroundColors = new[] {_waves[i].Color}, CenterColor = fillColor }; - keyboard.FillPath(pthGrBrush, path); - pthGrBrush.FocusScales = new PointF(0.3f, 0.8f); + using (var g = Graphics.FromImage(keyboard)) + { + g.FillPath(pthGrBrush, path); + pthGrBrush.FocusScales = new PointF(0.3f, 0.8f); - keyboard.FillPath(pthGrBrush, path); - keyboard.DrawEllipse(new Pen(pthGrBrush, 1), _waves[i].Point.X - _waves[i].Size / 2, - _waves[i].Point.Y - _waves[i].Size / 2, _waves[i].Size, _waves[i].Size); + g.FillPath(pthGrBrush, path); + g.DrawEllipse(new Pen(pthGrBrush, 1), _waves[i].Point.X - _waves[i].Size/2, + _waves[i].Point.Y - _waves[i].Size/2, _waves[i].Size, _waves[i].Size); + } } } } diff --git a/Artemis/Artemis/Modules/Effects/WindowsProfile/WindowsProfileDataModel.cs b/Artemis/Artemis/Modules/Effects/WindowsProfile/WindowsProfileDataModel.cs index be27cad22..ca28bc496 100644 --- a/Artemis/Artemis/Modules/Effects/WindowsProfile/WindowsProfileDataModel.cs +++ b/Artemis/Artemis/Modules/Effects/WindowsProfile/WindowsProfileDataModel.cs @@ -14,6 +14,16 @@ namespace Artemis.Modules.Effects.WindowsProfile public CpuDataModel Cpu { get; set; } public PerformanceDataModel Performance { get; set; } public Spotify Spotify { get; set; } + public CurrentTime CurrentTime { get; set; } + } + + class CurrentTime + { + public int Hours24 { get; set; } + public int Hours12 { get; set; } + public int Minutes { get; set; } + public int Seconds { get; set; } + } public class CpuDataModel diff --git a/Artemis/Artemis/Modules/Effects/WindowsProfile/WindowsProfileModel.cs b/Artemis/Artemis/Modules/Effects/WindowsProfile/WindowsProfileModel.cs index 03d1843ee..1ce50203f 100644 --- a/Artemis/Artemis/Modules/Effects/WindowsProfile/WindowsProfileModel.cs +++ b/Artemis/Artemis/Modules/Effects/WindowsProfile/WindowsProfileModel.cs @@ -6,7 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Artemis.Managers; using Artemis.Models; -using Artemis.Models.Profiles; +using Artemis.Profiles.Layers.Models; using Ninject.Extensions.Logging; using SpotifyAPI.Local; @@ -16,7 +16,8 @@ namespace Artemis.Modules.Effects.WindowsProfile { [DllImport("psapi.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool GetPerformanceInfo([Out] out PerformanceInformation performanceInformation, [In] int size); + public static extern bool GetPerformanceInfo([Out] out PerformanceInformation performanceInformation, + [In] int size); public static long GetPhysicalAvailableMemoryInMiB() { @@ -58,6 +59,8 @@ namespace Artemis.Modules.Effects.WindowsProfile } } + + public class WindowsProfileModel : EffectModel { private readonly ILogger _logger; @@ -95,6 +98,7 @@ namespace Artemis.Modules.Effects.WindowsProfile var dataModel = (WindowsProfileDataModel) DataModel; UpdateCpu(dataModel); UpdateSpotify(dataModel); + UpdateDay(dataModel); } #region CPU @@ -161,7 +165,7 @@ namespace Artemis.Modules.Effects.WindowsProfile public override List GetRenderLayers(bool renderMice, bool renderHeadsets) { - return Profile.GetRenderLayers(DataModel, renderMice, renderHeadsets, false); + return Profile.GetRenderLayers(DataModel, renderMice, renderHeadsets, false); } public static PerformanceCounter GetOverallPerformanceCounter() @@ -190,6 +194,19 @@ namespace Artemis.Modules.Effects.WindowsProfile #endregion + #region Current Time + private void UpdateDay(WindowsProfileDataModel dataModel) + { + + var now = DateTime.Now; + dataModel.CurrentTime.Hours24 = int.Parse(now.ToString("HH")); + dataModel.CurrentTime.Hours12 = int.Parse(now.ToString("hh")); + dataModel.CurrentTime.Minutes = int.Parse(now.ToString("mm")); + dataModel.CurrentTime.Seconds = int.Parse(now.ToString("ss")); + + } + #endregion + #region Spotify public void SetupSpotify() diff --git a/Artemis/Artemis/Modules/Games/CounterStrike/CounterStrike.Designer.cs b/Artemis/Artemis/Modules/Games/CounterStrike/CounterStrike.Designer.cs index e9ab9d8e4..85a966af8 100644 --- a/Artemis/Artemis/Modules/Games/CounterStrike/CounterStrike.Designer.cs +++ b/Artemis/Artemis/Modules/Games/CounterStrike/CounterStrike.Designer.cs @@ -35,102 +35,6 @@ namespace Artemis.Modules.Games.CounterStrike { } } - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("")] - public string GameDirectory { - get { - return ((string)(this["GameDirectory"])); - } - set { - this["GameDirectory"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("True")] - public bool AmmoEnabled { - get { - return ((bool)(this["AmmoEnabled"])); - } - set { - this["AmmoEnabled"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("#FFFF2900")] - public global::System.Windows.Media.Color AmmoMainColor { - get { - return ((global::System.Windows.Media.Color)(this["AmmoMainColor"])); - } - set { - this["AmmoMainColor"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("#FF26F600")] - public global::System.Windows.Media.Color AmmoSecondaryColor { - get { - return ((global::System.Windows.Media.Color)(this["AmmoSecondaryColor"])); - } - set { - this["AmmoSecondaryColor"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("True")] - public bool TeamColorEnabled { - get { - return ((bool)(this["TeamColorEnabled"])); - } - set { - this["TeamColorEnabled"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("True")] - public bool FlashEnabled { - get { - return ((bool)(this["FlashEnabled"])); - } - set { - this["FlashEnabled"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("True")] - public bool SmokeEnabled { - get { - return ((bool)(this["SmokeEnabled"])); - } - set { - this["SmokeEnabled"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("True")] - public bool LowHpEnabled { - get { - return ((bool)(this["LowHpEnabled"])); - } - set { - this["LowHpEnabled"] = value; - } - } - [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.DefaultSettingValueAttribute("Default")] @@ -142,5 +46,17 @@ namespace Artemis.Modules.Games.CounterStrike { this["LastProfile"] = value; } } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string GameDirectory { + get { + return ((string)(this["GameDirectory"])); + } + set { + this["GameDirectory"] = value; + } + } } } diff --git a/Artemis/Artemis/Modules/Games/CounterStrike/CounterStrike.settings b/Artemis/Artemis/Modules/Games/CounterStrike/CounterStrike.settings index e5b8b302b..cfa6c587e 100644 --- a/Artemis/Artemis/Modules/Games/CounterStrike/CounterStrike.settings +++ b/Artemis/Artemis/Modules/Games/CounterStrike/CounterStrike.settings @@ -1,7 +1,5 @@  - - + @@ -13,26 +11,5 @@ - - True - - - #FFFF2900 - - - #FF26F600 - - - True - - - True - - - True - - - True - \ No newline at end of file diff --git a/Artemis/Artemis/Modules/Games/CounterStrike/CounterStrikeModel.cs b/Artemis/Artemis/Modules/Games/CounterStrike/CounterStrikeModel.cs index 35f69761b..567c79bca 100644 --- a/Artemis/Artemis/Modules/Games/CounterStrike/CounterStrikeModel.cs +++ b/Artemis/Artemis/Modules/Games/CounterStrike/CounterStrikeModel.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using Artemis.Managers; using Artemis.Models; -using Artemis.Models.Profiles; +using Artemis.Profiles.Layers.Models; using Artemis.Utilities.GameState; using Newtonsoft.Json; using Ninject.Extensions.Logging; @@ -66,7 +66,7 @@ namespace Artemis.Modules.Games.CounterStrike public override List GetRenderLayers(bool renderMice, bool renderHeadsets) { - return Profile.GetRenderLayers(DataModel, renderMice, renderHeadsets); + return Profile.GetRenderLayers(DataModel, renderMice, renderHeadsets); } } } \ No newline at end of file diff --git a/Artemis/Artemis/Modules/Games/CounterStrike/CounterStrikeSettings.cs b/Artemis/Artemis/Modules/Games/CounterStrike/CounterStrikeSettings.cs index 96141065c..24b5699f1 100644 --- a/Artemis/Artemis/Modules/Games/CounterStrike/CounterStrikeSettings.cs +++ b/Artemis/Artemis/Modules/Games/CounterStrike/CounterStrikeSettings.cs @@ -1,5 +1,4 @@ -using System.Windows.Media; -using Artemis.Models; +using Artemis.Models; namespace Artemis.Modules.Games.CounterStrike { @@ -12,29 +11,11 @@ namespace Artemis.Modules.Games.CounterStrike public string GameDirectory { get; set; } - public bool AmmoEnabled { get; set; } - public Color AmmoMainColor { get; set; } - public Color AmmoSecondaryColor { get; set; } - - public bool TeamColorEnabled { get; set; } - public bool FlashEnabled { get; set; } - public bool SmokeEnabled { get; set; } - public bool LowHpEnabled { get; set; } - public sealed override void Load() { Enabled = CounterStrike.Default.Enabled; LastProfile = CounterStrike.Default.LastProfile; GameDirectory = CounterStrike.Default.GameDirectory; - - AmmoEnabled = CounterStrike.Default.AmmoEnabled; - AmmoMainColor = CounterStrike.Default.AmmoMainColor; - AmmoSecondaryColor = CounterStrike.Default.AmmoSecondaryColor; - - TeamColorEnabled = CounterStrike.Default.TeamColorEnabled; - FlashEnabled = CounterStrike.Default.FlashEnabled; - SmokeEnabled = CounterStrike.Default.SmokeEnabled; - LowHpEnabled = CounterStrike.Default.LowHpEnabled; } public sealed override void Save() @@ -42,15 +23,6 @@ namespace Artemis.Modules.Games.CounterStrike CounterStrike.Default.Enabled = Enabled; CounterStrike.Default.GameDirectory = GameDirectory; - CounterStrike.Default.AmmoEnabled = AmmoEnabled; - CounterStrike.Default.AmmoMainColor = AmmoMainColor; - CounterStrike.Default.AmmoSecondaryColor = AmmoSecondaryColor; - - CounterStrike.Default.TeamColorEnabled = TeamColorEnabled; - CounterStrike.Default.FlashEnabled = FlashEnabled; - CounterStrike.Default.SmokeEnabled = SmokeEnabled; - CounterStrike.Default.LowHpEnabled = LowHpEnabled; - CounterStrike.Default.Save(); } @@ -58,15 +30,6 @@ namespace Artemis.Modules.Games.CounterStrike { Enabled = true; GameDirectory = string.Empty; - - AmmoEnabled = true; - AmmoMainColor = Color.FromArgb(255, 38, 246, 0); - AmmoSecondaryColor = Color.FromArgb(255, 255, 41, 0); - - TeamColorEnabled = true; - FlashEnabled = true; - SmokeEnabled = true; - LowHpEnabled = true; } } } \ No newline at end of file diff --git a/Artemis/Artemis/Modules/Games/Dota2/Dota2.Designer.cs b/Artemis/Artemis/Modules/Games/Dota2/Dota2.Designer.cs index 8e1cd92e2..1f692a740 100644 --- a/Artemis/Artemis/Modules/Games/Dota2/Dota2.Designer.cs +++ b/Artemis/Artemis/Modules/Games/Dota2/Dota2.Designer.cs @@ -58,125 +58,5 @@ namespace Artemis.Modules.Games.Dota2 { this["GameDirectory"] = value; } } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("True")] - public bool CanCastAbility { - get { - return ((bool)(this["CanCastAbility"])); - } - set { - this["CanCastAbility"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("True")] - public bool ShowHealth { - get { - return ((bool)(this["ShowHealth"])); - } - set { - this["ShowHealth"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("True")] - public bool ShowDayCycle { - get { - return ((bool)(this["ShowDayCycle"])); - } - set { - this["ShowDayCycle"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("True")] - public bool ShowMana { - get { - return ((bool)(this["ShowMana"])); - } - set { - this["ShowMana"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("True")] - public bool ShowEvents { - get { - return ((bool)(this["ShowEvents"])); - } - set { - this["ShowEvents"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("#FFFF0000")] - public global::System.Windows.Media.Color MainColor { - get { - return ((global::System.Windows.Media.Color)(this["MainColor"])); - } - set { - this["MainColor"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("#FF0000FF")] - public global::System.Windows.Media.Color ManaColor { - get { - return ((global::System.Windows.Media.Color)(this["ManaColor"])); - } - set { - this["ManaColor"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("Default")] - public string KeyboardLayout { - get { - return ((string)(this["KeyboardLayout"])); - } - set { - this["KeyboardLayout"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("#FF00FF00")] - public global::System.Windows.Media.Color AbilityReadyColor { - get { - return ((global::System.Windows.Media.Color)(this["AbilityReadyColor"])); - } - set { - this["AbilityReadyColor"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("#FF6A5ACD")] - public global::System.Windows.Media.Color AbilityCooldownColor { - get { - return ((global::System.Windows.Media.Color)(this["AbilityCooldownColor"])); - } - set { - this["AbilityCooldownColor"] = value; - } - } } } diff --git a/Artemis/Artemis/Modules/Games/Dota2/Dota2.settings b/Artemis/Artemis/Modules/Games/Dota2/Dota2.settings index 557cc1a44..c3dafb56c 100644 --- a/Artemis/Artemis/Modules/Games/Dota2/Dota2.settings +++ b/Artemis/Artemis/Modules/Games/Dota2/Dota2.settings @@ -1,7 +1,5 @@  - - + @@ -13,35 +11,5 @@ - - True - - - True - - - True - - - True - - - True - - - #FFFF0000 - - - #FF0000FF - - - Default - - - #FF00FF00 - - - #FF6A5ACD - \ No newline at end of file diff --git a/Artemis/Artemis/Modules/Games/Dota2/Dota2Model.cs b/Artemis/Artemis/Modules/Games/Dota2/Dota2Model.cs index 6b3c77ce3..1c4fb8fb4 100644 --- a/Artemis/Artemis/Modules/Games/Dota2/Dota2Model.cs +++ b/Artemis/Artemis/Modules/Games/Dota2/Dota2Model.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using Artemis.Managers; using Artemis.Models; -using Artemis.Models.Profiles; +using Artemis.Profiles.Layers.Models; using Artemis.Utilities.GameState; using Newtonsoft.Json; @@ -66,7 +66,7 @@ namespace Artemis.Modules.Games.Dota2 public override List GetRenderLayers(bool renderMice, bool renderHeadsets) { - return Profile.GetRenderLayers(DataModel, renderMice, renderHeadsets); + return Profile.GetRenderLayers(DataModel, renderMice, renderHeadsets); } } } \ No newline at end of file diff --git a/Artemis/Artemis/Modules/Games/Dota2/Dota2Settings.cs b/Artemis/Artemis/Modules/Games/Dota2/Dota2Settings.cs index ada8893d1..b57b5b478 100644 --- a/Artemis/Artemis/Modules/Games/Dota2/Dota2Settings.cs +++ b/Artemis/Artemis/Modules/Games/Dota2/Dota2Settings.cs @@ -1,5 +1,4 @@ -using System.Windows.Media; -using Artemis.Models; +using Artemis.Models; namespace Artemis.Modules.Games.Dota2 { @@ -10,74 +9,28 @@ namespace Artemis.Modules.Games.Dota2 Load(); } + public string GameDirectory { get; set; } - public override void Load() + + public sealed override void Load() { - KeyboardLayout = Dota2.Default.KeyboardLayout; - MainColor = Dota2.Default.MainColor; - ManaColor = Dota2.Default.ManaColor; - ShowHealth = Dota2.Default.ShowHealth; - CanCastAbility = Dota2.Default.CanCastAbility; Enabled = Dota2.Default.Enabled; GameDirectory = Dota2.Default.GameDirectory; - ShowDayCycle = Dota2.Default.ShowDayCycle; - ShowMana = Dota2.Default.ShowMana; - ShowEvents = Dota2.Default.ShowEvents; - AbilityReadyColor = Dota2.Default.AbilityReadyColor; - AbilityCooldownColor = Dota2.Default.AbilityCooldownColor; } - public override void Save() + public sealed override void Save() { Dota2.Default.Enabled = Enabled; Dota2.Default.LastProfile = LastProfile; Dota2.Default.GameDirectory = GameDirectory; - Dota2.Default.KeyboardLayout = KeyboardLayout; - Dota2.Default.MainColor = MainColor; - Dota2.Default.ManaColor = ManaColor; - Dota2.Default.ShowDayCycle = ShowDayCycle; - Dota2.Default.ShowHealth = ShowHealth; - Dota2.Default.CanCastAbility = CanCastAbility; - Dota2.Default.ShowMana = ShowMana; - Dota2.Default.ShowEvents = ShowEvents; - Dota2.Default.AbilityCooldownColor = AbilityCooldownColor; - Dota2.Default.AbilityReadyColor = AbilityReadyColor; - Dota2.Default.Save(); } - public override void ToDefault() + public sealed override void ToDefault() { Enabled = true; GameDirectory = string.Empty; - - KeyboardLayout = "Default"; - MainColor = Color.FromArgb(255, 255, 0, 0); - ManaColor = Color.FromArgb(255, 0, 0, 255); - AbilityCooldownColor = Color.FromArgb(255, 106, 90, 205); - AbilityReadyColor = Color.FromArgb(255, 0, 255, 0); - ShowHealth = true; - CanCastAbility = true; - ShowDayCycle = true; - ShowMana = true; - ShowEvents = true; } - - #region Variables - - public string GameDirectory { get; set; } - public bool CanCastAbility { get; set; } - public bool ShowHealth { get; set; } - public bool ShowDayCycle { get; set; } - public bool ShowMana { get; set; } - public bool ShowEvents { get; set; } - public Color MainColor { get; set; } - public Color ManaColor { get; set; } - public string KeyboardLayout { get; set; } - public Color AbilityCooldownColor { get; set; } - public Color AbilityReadyColor { get; set; } - - #endregion } } \ No newline at end of file diff --git a/Artemis/Artemis/Modules/Games/Overwatch/OverwatchDataModel.cs b/Artemis/Artemis/Modules/Games/Overwatch/OverwatchDataModel.cs index 31bd573ee..f7e3ece14 100644 --- a/Artemis/Artemis/Modules/Games/Overwatch/OverwatchDataModel.cs +++ b/Artemis/Artemis/Modules/Games/Overwatch/OverwatchDataModel.cs @@ -9,13 +9,16 @@ namespace Artemis.Modules.Games.Overwatch public bool UltimateReady { get; set; } public bool Ability1Ready { get; set; } public bool Ability2Ready { get; set; } + public bool UltimateUsed { get; set; } + public bool CanChangeHero { get; set; } } public enum OverwatchStatus { - Unkown, + Unknown, InMainMenu, - InGame + InGame, + InCharacterSelect } public enum OverwatchCharacter diff --git a/Artemis/Artemis/Modules/Games/Overwatch/OverwatchModel.cs b/Artemis/Artemis/Modules/Games/Overwatch/OverwatchModel.cs index acab9c792..fa92cebaf 100644 --- a/Artemis/Artemis/Modules/Games/Overwatch/OverwatchModel.cs +++ b/Artemis/Artemis/Modules/Games/Overwatch/OverwatchModel.cs @@ -1,21 +1,28 @@ -using System.Collections.Generic; -using System.Drawing; +using System; +using System.Collections.Generic; using System.Linq; +using System.Windows.Media; using Artemis.Events; using Artemis.Managers; using Artemis.Models; using Artemis.Models.Interfaces; -using Artemis.Models.Profiles; +using Artemis.Profiles.Layers.Models; using Artemis.Utilities; using Artemis.Utilities.DataReaders; using Caliburn.Micro; -using Color = System.Windows.Media.Color; namespace Artemis.Modules.Games.Overwatch { public class OverwatchModel : GameModel { private readonly IEventAggregator _events; + private DateTime _characterChange; + // Using sticky values on these since they can cause flickering + private StickyValue _stickyStatus; + private StickyValue _stickyUltimateReady; + private StickyValue _stickyUltimateUsed; + private DateTime _ultimateReady; + private DateTime _ultimateUsed; public OverwatchModel(IEventAggregator events, MainManager mainManager, OverwatchSettings settings) : base(mainManager, settings, new OverwatchDataModel()) @@ -27,7 +34,7 @@ namespace Artemis.Modules.Games.Overwatch Enabled = Settings.Enabled; Initialized = false; - MmfReader = new MmfReader("overwatchMmf"); + MmfReader = new MmfReader("overwatchMmf", MainManager.Logger); LoadOverwatchCharacters(); } @@ -46,102 +53,184 @@ namespace Artemis.Modules.Games.Overwatch { OverwatchCharacters = new List { - new CharacterColor {OverwatchCharacter = OverwatchCharacter.Genji, Color = Color.FromRgb(13, 61, 0)}, - new CharacterColor {OverwatchCharacter = OverwatchCharacter.Mccree, Color = Color.FromRgb(24, 1, 1)}, - new CharacterColor {OverwatchCharacter = OverwatchCharacter.Pharah, Color = Color.FromRgb(0, 6, 32)}, - new CharacterColor {OverwatchCharacter = OverwatchCharacter.Reaper, Color = Color.FromRgb(7, 0, 0)}, - new CharacterColor {OverwatchCharacter = OverwatchCharacter.Soldier76, Color = Color.FromRgb(3, 5, 11)}, - new CharacterColor {OverwatchCharacter = OverwatchCharacter.Tracer, Color = Color.FromRgb(46, 12, 0)}, - new CharacterColor {OverwatchCharacter = OverwatchCharacter.Bastion, Color = Color.FromRgb(6, 10, 5)}, - new CharacterColor {OverwatchCharacter = OverwatchCharacter.Hanzo, Color = Color.FromRgb(28, 24, 8)}, - new CharacterColor {OverwatchCharacter = OverwatchCharacter.Junkrat, Color = Color.FromRgb(59, 28, 0)}, - new CharacterColor {OverwatchCharacter = OverwatchCharacter.Mei, Color = Color.FromRgb(3, 20, 55)}, - new CharacterColor {OverwatchCharacter = OverwatchCharacter.Torbjörn, Color = Color.FromRgb(31, 4, 3)}, - new CharacterColor - { - OverwatchCharacter = OverwatchCharacter.Widowmaker, - Color = Color.FromRgb(16, 3, 17) - }, - new CharacterColor {OverwatchCharacter = OverwatchCharacter.Dva, Color = Color.FromRgb(62, 12, 32)}, - new CharacterColor - { - OverwatchCharacter = OverwatchCharacter.Reinhardt, - Color = Color.FromRgb(12, 16, 16) - }, - new CharacterColor {OverwatchCharacter = OverwatchCharacter.Roadhog, Color = Color.FromRgb(26, 10, 0)}, - new CharacterColor {OverwatchCharacter = OverwatchCharacter.Winston, Color = Color.FromRgb(17, 18, 26)}, - new CharacterColor {OverwatchCharacter = OverwatchCharacter.Zarya, Color = Color.FromRgb(58, 7, 24)}, - new CharacterColor {OverwatchCharacter = OverwatchCharacter.Lúcio, Color = Color.FromRgb(8, 35, 0)}, - new CharacterColor {OverwatchCharacter = OverwatchCharacter.Mercy, Color = Color.FromRgb(60, 56, 26)}, - new CharacterColor {OverwatchCharacter = OverwatchCharacter.Symmetra, Color = Color.FromRgb(11, 29, 37)}, - new CharacterColor {OverwatchCharacter = OverwatchCharacter.Zenyatta, Color = Color.FromRgb(62, 54, 6)} + new CharacterColor {Character = OverwatchCharacter.Genji, Color = Color.FromRgb(55, 245, 0)}, + new CharacterColor {Character = OverwatchCharacter.Mccree, Color = Color.FromRgb(97, 5, 5)}, + new CharacterColor {Character = OverwatchCharacter.Pharah, Color = Color.FromRgb(0, 24, 128)}, + new CharacterColor {Character = OverwatchCharacter.Reaper, Color = Color.FromRgb(28, 0, 2)}, + new CharacterColor {Character = OverwatchCharacter.Soldier76, Color = Color.FromRgb(14, 21, 45)}, + new CharacterColor {Character = OverwatchCharacter.Tracer, Color = Color.FromRgb(186, 49, 0)}, + new CharacterColor {Character = OverwatchCharacter.Bastion, Color = Color.FromRgb(26, 43, 20)}, + new CharacterColor {Character = OverwatchCharacter.Hanzo, Color = Color.FromRgb(113, 99, 33)}, + new CharacterColor {Character = OverwatchCharacter.Junkrat, Color = Color.FromRgb(237, 113, 2)}, + new CharacterColor {Character = OverwatchCharacter.Mei, Color = Color.FromRgb(15, 82, 222)}, + new CharacterColor {Character = OverwatchCharacter.Torbjörn, Color = Color.FromRgb(125, 18, 12)}, + new CharacterColor {Character = OverwatchCharacter.Widowmaker, Color = Color.FromRgb(65, 12, 70)}, + new CharacterColor {Character = OverwatchCharacter.Dva, Color = Color.FromRgb(248, 48, 129)}, + new CharacterColor {Character = OverwatchCharacter.Reinhardt, Color = Color.FromRgb(51, 65, 66)}, + new CharacterColor {Character = OverwatchCharacter.Roadhog, Color = Color.FromRgb(107, 40, 2)}, + new CharacterColor {Character = OverwatchCharacter.Winston, Color = Color.FromRgb(70, 73, 107)}, + new CharacterColor {Character = OverwatchCharacter.Zarya, Color = Color.FromRgb(235, 28, 97)}, + new CharacterColor {Character = OverwatchCharacter.Lúcio, Color = Color.FromRgb(34, 142, 2)}, + new CharacterColor {Character = OverwatchCharacter.Mercy, Color = Color.FromRgb(243, 226, 106)}, + new CharacterColor {Character = OverwatchCharacter.Symmetra, Color = Color.FromRgb(46, 116, 148)}, + new CharacterColor {Character = OverwatchCharacter.Zenyatta, Color = Color.FromRgb(248, 218, 26)} }; } - public override void Dispose() - { - Initialized = false; - } - public override void Enable() { + _stickyStatus = new StickyValue(300); + _stickyUltimateReady = new StickyValue(350); + _stickyUltimateUsed = new StickyValue(350); Initialized = true; } + public override void Dispose() + { + _stickyStatus.Dispose(); + _stickyUltimateReady.Dispose(); + _stickyUltimateUsed.Dispose(); + Initialized = false; + } + public override void Update() + { + UpdateOverwatch(); + ApplyStickyValues(); + } + + private void ApplyStickyValues() + { + var gameDataModel = (OverwatchDataModel) DataModel; + gameDataModel.Status = _stickyStatus.Value; + gameDataModel.UltimateReady = _stickyUltimateReady.Value; + gameDataModel.UltimateUsed = _stickyUltimateUsed.Value; + } + + public void UpdateOverwatch() { var gameDataModel = (OverwatchDataModel) DataModel; var colors = MmfReader.GetColorArray(); if (colors == null) return; - var bitmap = new Bitmap(22, 6); - - using (var g = Graphics.FromImage(bitmap)) - { - for (var y = 0; y < 6; y++) - { - for (var x = 0; x < 22; x++) - { - g.DrawRectangle(new Pen(ColorHelpers.ToDrawingColor(colors[y, x])), y, x, 1, 1); - } - } - } - _events.PublishOnUIThread(new ChangeBitmap(bitmap)); + _events.PublishOnUIThread(new RazerColorArrayChanged(colors)); + //MainManager.Logger.Trace("DataModel: \r\n{0}", + // JsonConvert.SerializeObject(gameDataModel, Formatting.Indented)); // Determine general game state - gameDataModel.Status = colors[0, 0].Equals(Color.FromRgb(55, 30, 0)) - ? OverwatchStatus.InMainMenu - : OverwatchStatus.Unkown; + ParseGameSate(gameDataModel, colors); - if (gameDataModel.Status == OverwatchStatus.InMainMenu) + // Parse the lighting + var characterMatch = ParseCharacter(gameDataModel, colors); + + // Ult can't possibly be ready within 2 seconds of changing, this avoids false positives. + // Filtering on ultReady and ultUsed removes false positives from the native ultimate effects + // The control keys don't show during character select, so don't continue on those either. + if (_characterChange.AddSeconds(2) >= DateTime.Now || + _ultimateUsed.AddSeconds(2) >= DateTime.Now || + _ultimateReady.AddSeconds(2) >= DateTime.Now || + _stickyStatus.Value == OverwatchStatus.InCharacterSelect) return; - // If ingame, look for a character - var characterMatch = OverwatchCharacters.FirstOrDefault(c => c.Color == colors[0, 0]); - if (characterMatch.OverwatchCharacter == OverwatchCharacter.None) + ParseSpecialKeys(gameDataModel, characterMatch, colors); + ParseAbilities(gameDataModel, colors); + } + + private void ParseGameSate(OverwatchDataModel gameDataModel, Color[,] colors) + { + if (_ultimateUsed.AddSeconds(5) >= DateTime.Now) return; - gameDataModel.Status = OverwatchStatus.InGame; - gameDataModel.Character = characterMatch.OverwatchCharacter; + if (colors[0, 0].Equals(Color.FromRgb(55, 30, 0))) + _stickyStatus.Value = OverwatchStatus.InMainMenu; + + if (_stickyStatus.Value != OverwatchStatus.InMainMenu) + return; + + gameDataModel.Character = OverwatchCharacter.None; + gameDataModel.Ability1Ready = false; + gameDataModel.Ability2Ready = false; + _stickyUltimateReady.Value = false; + _stickyUltimateUsed.Value = false; + } + + private CharacterColor? ParseCharacter(OverwatchDataModel gameDataModel, Color[,] colors) + { + var characterMatch = OverwatchCharacters.FirstOrDefault(c => c.Color == colors[0, 20]); + // If a new character was chosen, let the other methods know + if (characterMatch.Character != gameDataModel.Character) + _characterChange = DateTime.Now; + + // If no character was found, this method shouldn't continue + if (characterMatch.Character == OverwatchCharacter.None) + return characterMatch; + + // If WASD isn't orange (any of them will do), player is in character select + _stickyStatus.Value = ControlsShown(colors) ? OverwatchStatus.InGame : OverwatchStatus.InCharacterSelect; + + // Update the datamodel + gameDataModel.Character = characterMatch.Character; + return characterMatch; + } + + private bool ControlsShown(Color[,] colors) + { + var keyColor = Color.FromRgb(222, 153, 0); + return colors[2, 3] == keyColor || colors[3, 2] == keyColor || + colors[3, 3] == keyColor || colors[3, 4] == keyColor; + } + + private void ParseSpecialKeys(OverwatchDataModel gameDataModel, CharacterColor? characterMatch, Color[,] colors) + { + if (characterMatch == null || characterMatch.Value.Character == OverwatchCharacter.None) + return; - // Ability1 is ready when LShift is lid - gameDataModel.Ability1Ready = colors[4, 1].Equals(Color.FromRgb(4, 141, 144)); - // Ability2 is ready when E is lid - gameDataModel.Ability2Ready = colors[2, 4].Equals(Color.FromRgb(4, 141, 144)); // Ultimate is ready when Q is blinking - gameDataModel.UltimateReady = !characterMatch.Color.Equals(colors[2, 2]); + var charCol = characterMatch.Value.Color; + var backlidColor = Color.FromRgb((byte) (charCol.R*0.25), (byte) (charCol.G*0.25), (byte) (charCol.B*0.25)); + var ultReady = !backlidColor.Equals(colors[2, 2]); + + if (_ultimateUsed.AddSeconds(15) <= DateTime.Now) + { + // Player can change hero if H is blinking + gameDataModel.CanChangeHero = !colors[3, 7].Equals(backlidColor); + + if (!_stickyUltimateReady.Value && ultReady && ControlsShown(colors)) + { + _ultimateReady = DateTime.Now; + _stickyUltimateReady.Value = true; + } + } + + // If ult no longer ready but it was ready before, it was used. + if (_stickyUltimateReady.Value && !ultReady) + { + _stickyUltimateReady.Value = false; + if (_ultimateUsed.AddSeconds(15) <= DateTime.Now) + _ultimateUsed = DateTime.Now; + } + + // UltimateUsed is true for 10 seconds after ultimate went on cooldown + if (_ultimateUsed != DateTime.MinValue) + _stickyUltimateUsed.Value = _ultimateUsed.AddSeconds(10) >= DateTime.Now; + } + + private void ParseAbilities(OverwatchDataModel gameDataModel, Color[,] colors) + { + gameDataModel.Ability1Ready = colors[4, 1].Equals(Color.FromRgb(4, 141, 144)); + gameDataModel.Ability2Ready = colors[2, 4].Equals(Color.FromRgb(4, 141, 144)); } public override List GetRenderLayers(bool renderMice, bool renderHeadsets) { - return Profile.GetRenderLayers(DataModel, renderMice, renderHeadsets); + return Profile.GetRenderLayers(DataModel, renderMice, renderHeadsets); } } public struct CharacterColor { - public OverwatchCharacter OverwatchCharacter { get; set; } + public OverwatchCharacter Character { get; set; } public Color Color { get; set; } } } \ No newline at end of file diff --git a/Artemis/Artemis/Modules/Games/RocketLeague/RocketLeagueModel.cs b/Artemis/Artemis/Modules/Games/RocketLeague/RocketLeagueModel.cs index 4b93dc620..8d5aa782b 100644 --- a/Artemis/Artemis/Modules/Games/RocketLeague/RocketLeagueModel.cs +++ b/Artemis/Artemis/Modules/Games/RocketLeague/RocketLeagueModel.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; using Artemis.Managers; using Artemis.Models; -using Artemis.Models.Profiles; +using Artemis.Profiles.Layers.Models; using Artemis.Settings; using Artemis.Utilities; using Artemis.Utilities.Memory; @@ -66,11 +66,20 @@ namespace Artemis.Modules.Games.RocketLeague ((RocketLeagueDataModel) DataModel).Boost = 0; if (((RocketLeagueDataModel) DataModel).Boost > 100) ((RocketLeagueDataModel) DataModel).Boost = 100; + + if (DateTime.Now.AddSeconds(-2) <= LastTrace) + return; + + MainManager.Logger.Trace("Offsets as JSON: \r\n{0}", + JsonConvert.SerializeObject(_pointer.GameAddresses, Formatting.Indented)); + MainManager.Logger.Trace("RL specific offsets: {0}", offsets); + MainManager.Logger.Trace("Boost address: {0}", boostAddress); + MainManager.Logger.Trace("Boost float: {0}", boostFloat); } public override List GetRenderLayers(bool renderMice, bool renderHeadsets) { - return Profile.GetRenderLayers(DataModel, renderMice, renderHeadsets); + return Profile.GetRenderLayers(DataModel, renderMice, renderHeadsets); } } } \ No newline at end of file diff --git a/Artemis/Artemis/Modules/Games/TheDivision/TheDivisionModel.cs b/Artemis/Artemis/Modules/Games/TheDivision/TheDivisionModel.cs index 8087cb468..d7c683626 100644 --- a/Artemis/Artemis/Modules/Games/TheDivision/TheDivisionModel.cs +++ b/Artemis/Artemis/Modules/Games/TheDivision/TheDivisionModel.cs @@ -3,7 +3,7 @@ using System.Threading; using System.Threading.Tasks; using Artemis.Managers; using Artemis.Models; -using Artemis.Models.Profiles; +using Artemis.Profiles.Layers.Models; using Artemis.Utilities; using Artemis.Utilities.LogitechDll; @@ -131,7 +131,7 @@ namespace Artemis.Modules.Games.TheDivision public override List GetRenderLayers(bool renderMice, bool renderHeadsets) { - return Profile.GetRenderLayers(DataModel, renderMice, renderHeadsets); + return Profile.GetRenderLayers(DataModel, renderMice, renderHeadsets); } } } \ No newline at end of file diff --git a/Artemis/Artemis/Modules/Games/Witcher3/Witcher3Model.cs b/Artemis/Artemis/Modules/Games/Witcher3/Witcher3Model.cs index a61da7526..3de8ee80f 100644 --- a/Artemis/Artemis/Modules/Games/Witcher3/Witcher3Model.cs +++ b/Artemis/Artemis/Modules/Games/Witcher3/Witcher3Model.cs @@ -1,20 +1,19 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Globalization; using System.IO; using System.Linq; using System.Text.RegularExpressions; using Artemis.Managers; using Artemis.Models; -using Artemis.Models.Profiles; +using Artemis.Profiles.Layers.Models; namespace Artemis.Modules.Games.Witcher3 { public class Witcher3Model : GameModel { - private readonly Stopwatch _updateSw; private readonly Regex _configRegex; + private readonly Stopwatch _updateSw; private string _witcherSettings; public Witcher3Model(MainManager mainManager, Witcher3Settings settings) @@ -139,7 +138,7 @@ namespace Artemis.Modules.Games.Witcher3 public override List GetRenderLayers(bool renderMice, bool renderHeadsets) { - return Profile.GetRenderLayers(DataModel, renderMice, renderHeadsets); + return Profile.GetRenderLayers(DataModel, renderMice, renderHeadsets); } } } \ No newline at end of file diff --git a/Artemis/Artemis/Modules/Overlays/VolumeDisplay/VolumeDisplayModel.cs b/Artemis/Artemis/Modules/Overlays/VolumeDisplay/VolumeDisplayModel.cs index 7a2af183c..435fa4d0a 100644 --- a/Artemis/Artemis/Modules/Overlays/VolumeDisplay/VolumeDisplayModel.cs +++ b/Artemis/Artemis/Modules/Overlays/VolumeDisplay/VolumeDisplayModel.cs @@ -4,9 +4,8 @@ using System.Runtime.InteropServices; using System.Windows.Forms; using Artemis.Managers; using Artemis.Models; -using Artemis.Models.Profiles; +using Artemis.Profiles.Layers.Models; using NAudio.CoreAudioApi; -using Brush = System.Windows.Media.Brush; namespace Artemis.Modules.Overlays.VolumeDisplay { @@ -46,10 +45,10 @@ namespace Artemis.Modules.Overlays.VolumeDisplay if (VolumeDisplay.Ttl < 1) return; - var decreaseAmount = 500 / fps; + var decreaseAmount = 500/fps; VolumeDisplay.Ttl = VolumeDisplay.Ttl - decreaseAmount; if (VolumeDisplay.Ttl < 128) - VolumeDisplay.Transparancy = (byte)(VolumeDisplay.Transparancy - 20); + VolumeDisplay.Transparancy = (byte) (VolumeDisplay.Transparancy - 20); try { @@ -57,7 +56,7 @@ namespace Artemis.Modules.Overlays.VolumeDisplay var volumeFloat = enumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Console) .AudioEndpointVolume.MasterVolumeLevelScalar; - VolumeDisplay.Volume = (int)(volumeFloat * 100); + VolumeDisplay.Volume = (int) (volumeFloat*100); } catch (COMException) { @@ -78,11 +77,16 @@ namespace Artemis.Modules.Overlays.VolumeDisplay VolumeDisplay.Transparancy = 255; } - public override void RenderOverlay(Graphics keyboard, ref Brush mouse, ref Brush headset, bool renderMice, + public override void RenderOverlay(Bitmap keyboard, ref Bitmap mouse, ref Bitmap headset, bool renderMice, bool renderHeadsets) { - if (MainManager.DeviceManager.ActiveKeyboard != null && VolumeDisplay != null && VolumeDisplay.Ttl >= 1) - VolumeDisplay.Draw(keyboard); + if (MainManager.DeviceManager.ActiveKeyboard == null || VolumeDisplay == null || VolumeDisplay.Ttl < 1) + return; + + using (var g = Graphics.FromImage(keyboard)) + { + VolumeDisplay.Draw(g); + } } } } \ No newline at end of file diff --git a/Artemis/Artemis/Profiles/Layers/Animations/GrowAnimation.cs b/Artemis/Artemis/Profiles/Layers/Animations/GrowAnimation.cs new file mode 100644 index 000000000..da70c3808 --- /dev/null +++ b/Artemis/Artemis/Profiles/Layers/Animations/GrowAnimation.cs @@ -0,0 +1,61 @@ +using System.Windows; +using System.Windows.Media; +using Artemis.Profiles.Layers.Interfaces; +using Artemis.Profiles.Layers.Models; + +namespace Artemis.Profiles.Layers.Animations +{ + public class GrowAnimation : ILayerAnimation + { + public string Name { get; } = "Grow"; + + public void Update(LayerModel layerModel, bool updateAnimations) + { + var progress = layerModel.Properties.AnimationProgress; + + if (MustExpire(layerModel)) + progress = 0; + progress = progress + layerModel.Properties.AnimationSpeed/2.5; + + // If not previewing, store the animation progress in the actual model for the next frame + if (updateAnimations) + layerModel.Properties.AnimationProgress = progress; + } + + public void Draw(LayerPropertiesModel props, LayerPropertiesModel applied, DrawingContext c) + { + if (applied.Brush == null) + return; + + const int scale = 4; + // Set up variables for this frame + var rect = props.Contain + ? new Rect(applied.X*scale, applied.Y*scale, applied.Width*scale, applied.Height*scale) + : new Rect(props.X*scale, props.Y*scale, props.Width*scale, props.Height*scale); + + var clip = new Rect(applied.X*scale, applied.Y*scale, applied.Width*scale, applied.Height*scale); + + // Take an offset of 4 to allow layers to slightly leave their bounds + var progress = (6.0 - props.AnimationProgress)*10.0; + if (progress < 0) + { + // Can't meddle with the original brush because it's frozen. + var brush = applied.Brush.Clone(); + brush.Opacity = 1 + 0.025*progress; + if (brush.Opacity < 0) + brush.Opacity = 0; + if (brush.Opacity > 1) + brush.Opacity = 1; + applied.Brush = brush; + } + rect.Inflate(-rect.Width/100.0*progress, -rect.Height/100.0*progress); + clip.Inflate(-clip.Width/100.0*progress, -clip.Height/100.0*progress); + + c.PushClip(new RectangleGeometry(clip)); + c.DrawRectangle(applied.Brush, null, rect); + c.Pop(); + } + + public bool MustExpire(LayerModel layer) => layer.Properties.AnimationProgress > 10; + } +} \ No newline at end of file diff --git a/Artemis/Artemis/Profiles/Layers/Animations/NoneAnimation.cs b/Artemis/Artemis/Profiles/Layers/Animations/NoneAnimation.cs new file mode 100644 index 000000000..3bbe7d9d1 --- /dev/null +++ b/Artemis/Artemis/Profiles/Layers/Animations/NoneAnimation.cs @@ -0,0 +1,24 @@ +using System.Windows.Media; +using Artemis.Profiles.Layers.Interfaces; +using Artemis.Profiles.Layers.Models; + +namespace Artemis.Profiles.Layers.Animations +{ + public class NoneAnimation : ILayerAnimation + { + public string Name { get; } = "None"; + + public void Update(LayerModel layerModel, bool updateAnimations) + { + } + + public void Draw(LayerPropertiesModel props, LayerPropertiesModel applied, DrawingContext c) + { + } + + public bool MustExpire(LayerModel layer) + { + return true; + } + } +} \ No newline at end of file diff --git a/Artemis/Artemis/Profiles/Layers/Animations/PulseAnimation.cs b/Artemis/Artemis/Profiles/Layers/Animations/PulseAnimation.cs new file mode 100644 index 000000000..0d5e34253 --- /dev/null +++ b/Artemis/Artemis/Profiles/Layers/Animations/PulseAnimation.cs @@ -0,0 +1,50 @@ +using System; +using System.Windows; +using System.Windows.Media; +using Artemis.Profiles.Layers.Interfaces; +using Artemis.Profiles.Layers.Models; + +namespace Artemis.Profiles.Layers.Animations +{ + public class PulseAnimation : ILayerAnimation + { + public string Name { get; } = "Pulse"; + + public void Update(LayerModel layerModel, bool updateAnimations) + { + var progress = layerModel.Properties.AnimationProgress; + if (MustExpire(layerModel)) + progress = 0; + progress = progress + layerModel.Properties.AnimationSpeed/2; + + // If not previewing, store the animation progress in the actual model for the next frame + if (updateAnimations) + layerModel.Properties.AnimationProgress = progress; + } + + public void Draw(LayerPropertiesModel props, LayerPropertiesModel applied, DrawingContext c) + { + if (applied.Brush == null) + return; + + const int scale = 4; + // Set up variables for this frame + var rect = props.Contain + ? new Rect(applied.X*scale, applied.Y*scale, applied.Width*scale, applied.Height*scale) + : new Rect(props.X*scale, props.Y*scale, props.Width*scale, props.Height*scale); + + var clip = new Rect(applied.X*scale, applied.Y*scale, applied.Width*scale, applied.Height*scale); + + // Can't meddle with the original brush because it's frozen. + var brush = applied.Brush.Clone(); + brush.Opacity = (Math.Sin(props.AnimationProgress*Math.PI) + 1)*(props.Opacity/2); + applied.Brush = brush; + + c.PushClip(new RectangleGeometry(clip)); + c.DrawRectangle(applied.Brush, null, rect); + c.Pop(); + } + + public bool MustExpire(LayerModel layer) => layer.Properties.AnimationProgress > 2; + } +} \ No newline at end of file diff --git a/Artemis/Artemis/Profiles/Layers/Animations/SlideDownAnimation.cs b/Artemis/Artemis/Profiles/Layers/Animations/SlideDownAnimation.cs new file mode 100644 index 000000000..e25af27df --- /dev/null +++ b/Artemis/Artemis/Profiles/Layers/Animations/SlideDownAnimation.cs @@ -0,0 +1,51 @@ +using System.Windows; +using System.Windows.Media; +using Artemis.Profiles.Layers.Interfaces; +using Artemis.Profiles.Layers.Models; + +namespace Artemis.Profiles.Layers.Animations +{ + public class SlideDownAnimation : ILayerAnimation + { + public string Name { get; } = "Slide down"; + + public void Update(LayerModel layerModel, bool updateAnimations) + { + var progress = layerModel.Properties.AnimationProgress; + if (MustExpire(layerModel)) + progress = 0; + progress = progress + layerModel.Properties.AnimationSpeed*2; + + // If not previewing, store the animation progress in the actual model for the next frame + if (updateAnimations) + layerModel.Properties.AnimationProgress = progress; + } + + public void Draw(LayerPropertiesModel props, LayerPropertiesModel applied, DrawingContext c) + { + if (applied.Brush == null) + return; + + const int scale = 4; + // Set up variables for this frame + var rect = props.Contain + ? new Rect(applied.X*scale, applied.Y*scale, applied.Width*scale, applied.Height*scale) + : new Rect(props.X*scale, props.Y*scale, props.Width*scale, props.Height*scale); + + var s1 = new Rect(new Point(rect.X, rect.Y + props.AnimationProgress), new Size(rect.Width, rect.Height)); + var s2 = new Rect(new Point(s1.X, s1.Y - rect.Height), new Size(rect.Width, rect.Height)); + + var clip = new Rect(applied.X*scale, applied.Y*scale, applied.Width*scale, applied.Height*scale); + + c.PushClip(new RectangleGeometry(clip)); + c.DrawRectangle(applied.Brush, null, s1); + c.DrawRectangle(applied.Brush, null, s2); + c.Pop(); + } + + public bool MustExpire(LayerModel layer) + { + return layer.Properties.AnimationProgress + layer.Properties.AnimationSpeed*2 >= layer.Properties.Height*4; + } + } +} \ No newline at end of file diff --git a/Artemis/Artemis/Profiles/Layers/Animations/SlideLeftAnimation.cs b/Artemis/Artemis/Profiles/Layers/Animations/SlideLeftAnimation.cs new file mode 100644 index 000000000..ff277c1a9 --- /dev/null +++ b/Artemis/Artemis/Profiles/Layers/Animations/SlideLeftAnimation.cs @@ -0,0 +1,52 @@ +using System.Windows; +using System.Windows.Media; +using Artemis.Profiles.Layers.Interfaces; +using Artemis.Profiles.Layers.Models; + +namespace Artemis.Profiles.Layers.Animations +{ + public class SlideLeftAnimation : ILayerAnimation + { + public string Name { get; } = "Slide left"; + + public void Update(LayerModel layerModel, bool updateAnimations) + { + var progress = layerModel.Properties.AnimationProgress; + if (MustExpire(layerModel)) + progress = 0; + progress = progress + layerModel.Properties.AnimationSpeed*2; + + // If not previewing, store the animation progress in the actual model for the next frame + if (updateAnimations) + layerModel.Properties.AnimationProgress = progress; + } + + public void Draw(LayerPropertiesModel props, LayerPropertiesModel applied, DrawingContext c) + { + if (applied.Brush == null) + return; + + const int scale = 4; + // Set up variables for this frame + var rect = props.Contain + ? new Rect(applied.X*scale, applied.Y*scale, applied.Width*scale, applied.Height*scale) + : new Rect(props.X*scale, props.Y*scale, props.Width*scale, props.Height*scale); + + var s1 = new Rect(new Point(rect.X - props.AnimationProgress, rect.Y), + new Size(rect.Width + 0.05, rect.Height)); + var s2 = new Rect(new Point(s1.X + rect.Width, rect.Y), new Size(rect.Width, rect.Height)); + + var clip = new Rect(applied.X*scale, applied.Y*scale, applied.Width*scale, applied.Height*scale); + + c.PushClip(new RectangleGeometry(clip)); + c.DrawRectangle(applied.Brush, null, s1); + c.DrawRectangle(applied.Brush, null, s2); + c.Pop(); + } + + public bool MustExpire(LayerModel layer) + { + return layer.Properties.AnimationProgress + layer.Properties.AnimationSpeed*2 >= layer.Properties.Width*4; + } + } +} \ No newline at end of file diff --git a/Artemis/Artemis/Profiles/Layers/Animations/SlideRightAnimation.cs b/Artemis/Artemis/Profiles/Layers/Animations/SlideRightAnimation.cs new file mode 100644 index 000000000..88b02828b --- /dev/null +++ b/Artemis/Artemis/Profiles/Layers/Animations/SlideRightAnimation.cs @@ -0,0 +1,51 @@ +using System.Windows; +using System.Windows.Media; +using Artemis.Profiles.Layers.Interfaces; +using Artemis.Profiles.Layers.Models; + +namespace Artemis.Profiles.Layers.Animations +{ + public class SlideRightAnimation : ILayerAnimation + { + public string Name { get; } = "Slide right"; + + public void Update(LayerModel layerModel, bool updateAnimations) + { + var progress = layerModel.Properties.AnimationProgress; + if (MustExpire(layerModel)) + progress = 0; + progress = progress + layerModel.Properties.AnimationSpeed*2; + + // If not previewing, store the animation progress in the actual model for the next frame + if (updateAnimations) + layerModel.Properties.AnimationProgress = progress; + } + + public void Draw(LayerPropertiesModel props, LayerPropertiesModel applied, DrawingContext c) + { + if (applied.Brush == null) + return; + + const int scale = 4; + // Set up variables for this frame + var rect = props.Contain + ? new Rect(applied.X*scale, applied.Y*scale, applied.Width*scale, applied.Height*scale) + : new Rect(props.X*scale, props.Y*scale, props.Width*scale, props.Height*scale); + + var s1 = new Rect(new Point(rect.X + props.AnimationProgress, rect.Y), new Size(rect.Width, rect.Height)); + var s2 = new Rect(new Point(s1.X - rect.Width, rect.Y), new Size(rect.Width + 1, rect.Height)); + + var clip = new Rect(applied.X*scale, applied.Y*scale, applied.Width*scale, applied.Height*scale); + + c.PushClip(new RectangleGeometry(clip)); + c.DrawRectangle(applied.Brush, null, s1); + c.DrawRectangle(applied.Brush, null, s2); + c.Pop(); + } + + public bool MustExpire(LayerModel layer) + { + return layer.Properties.AnimationProgress + layer.Properties.AnimationSpeed*2 >= layer.Properties.Width*4; + } + } +} \ No newline at end of file diff --git a/Artemis/Artemis/Profiles/Layers/Animations/SlideUpAnimation.cs b/Artemis/Artemis/Profiles/Layers/Animations/SlideUpAnimation.cs new file mode 100644 index 000000000..05c2f8b6c --- /dev/null +++ b/Artemis/Artemis/Profiles/Layers/Animations/SlideUpAnimation.cs @@ -0,0 +1,51 @@ +using System.Windows; +using System.Windows.Media; +using Artemis.Profiles.Layers.Interfaces; +using Artemis.Profiles.Layers.Models; + +namespace Artemis.Profiles.Layers.Animations +{ + public class SlideUpAnimation : ILayerAnimation + { + public string Name { get; } = "Slide up"; + + public void Update(LayerModel layerModel, bool updateAnimations) + { + var progress = layerModel.Properties.AnimationProgress; + if (MustExpire(layerModel)) + progress = 0; + progress = progress + layerModel.Properties.AnimationSpeed*2; + + // If not previewing, store the animation progress in the actual model for the next frame + if (updateAnimations) + layerModel.Properties.AnimationProgress = progress; + } + + public void Draw(LayerPropertiesModel props, LayerPropertiesModel applied, DrawingContext c) + { + if (applied.Brush == null) + return; + + const int scale = 4; + // Set up variables for this frame + var rect = props.Contain + ? new Rect(applied.X*scale, applied.Y*scale, applied.Width*scale, applied.Height*scale) + : new Rect(props.X*scale, props.Y*scale, props.Width*scale, props.Height*scale); + + var s1 = new Rect(new Point(rect.X, rect.Y - props.AnimationProgress), new Size(rect.Width, rect.Height)); + var s2 = new Rect(new Point(s1.X, s1.Y + rect.Height), new Size(rect.Width, rect.Height)); + + var clip = new Rect(applied.X*scale, applied.Y*scale, applied.Width*scale, applied.Height*scale); + + c.PushClip(new RectangleGeometry(clip)); + c.DrawRectangle(applied.Brush, null, s1); + c.DrawRectangle(applied.Brush, null, s2); + c.Pop(); + } + + public bool MustExpire(LayerModel layer) + { + return layer.Properties.AnimationProgress + layer.Properties.AnimationSpeed*2 >= layer.Properties.Height*4; + } + } +} \ No newline at end of file diff --git a/Artemis/Artemis/Profiles/Layers/Conditions/DataModelCondition.cs b/Artemis/Artemis/Profiles/Layers/Conditions/DataModelCondition.cs new file mode 100644 index 000000000..aba92d32f --- /dev/null +++ b/Artemis/Artemis/Profiles/Layers/Conditions/DataModelCondition.cs @@ -0,0 +1,15 @@ +using System.Linq; +using Artemis.Models.Interfaces; +using Artemis.Profiles.Layers.Interfaces; +using Artemis.Profiles.Layers.Models; + +namespace Artemis.Profiles.Layers.Conditions +{ + public class DataModelCondition : ILayerCondition + { + public bool ConditionsMet(LayerModel layer, IDataModel dataModel) + { + return layer.Properties.Conditions.All(cm => cm.ConditionMet(dataModel)); + } + } +} \ No newline at end of file diff --git a/Artemis/Artemis/Profiles/Layers/Conditions/EventCondition.cs b/Artemis/Artemis/Profiles/Layers/Conditions/EventCondition.cs new file mode 100644 index 000000000..5a7adc7d0 --- /dev/null +++ b/Artemis/Artemis/Profiles/Layers/Conditions/EventCondition.cs @@ -0,0 +1,21 @@ +using System.Linq; +using Artemis.Models.Interfaces; +using Artemis.Profiles.Layers.Interfaces; +using Artemis.Profiles.Layers.Models; + +namespace Artemis.Profiles.Layers.Conditions +{ + public class EventCondition : ILayerCondition + { + public bool ConditionsMet(LayerModel layer, IDataModel dataModel) + { + var conditionsMet = layer.Properties.Conditions.All(cm => cm.ConditionMet(dataModel)); + layer.EventProperties.Update(layer, conditionsMet); + + if (conditionsMet && layer.EventProperties.MustTrigger) + layer.EventProperties.TriggerEvent(layer); + + return conditionsMet && layer.EventProperties.MustDraw; + } + } +} \ No newline at end of file diff --git a/Artemis/Artemis/Profiles/Layers/Interfaces/ILayerAnimation.cs b/Artemis/Artemis/Profiles/Layers/Interfaces/ILayerAnimation.cs new file mode 100644 index 000000000..c0df0f6a4 --- /dev/null +++ b/Artemis/Artemis/Profiles/Layers/Interfaces/ILayerAnimation.cs @@ -0,0 +1,13 @@ +using System.Windows.Media; +using Artemis.Profiles.Layers.Models; + +namespace Artemis.Profiles.Layers.Interfaces +{ + public interface ILayerAnimation + { + string Name { get; } + void Update(LayerModel layerModel, bool updateAnimations); + void Draw(LayerPropertiesModel props, LayerPropertiesModel applied, DrawingContext c); + bool MustExpire(LayerModel layer); + } +} \ No newline at end of file diff --git a/Artemis/Artemis/Profiles/Layers/Interfaces/ILayerCondition.cs b/Artemis/Artemis/Profiles/Layers/Interfaces/ILayerCondition.cs new file mode 100644 index 000000000..56588ad8d --- /dev/null +++ b/Artemis/Artemis/Profiles/Layers/Interfaces/ILayerCondition.cs @@ -0,0 +1,10 @@ +using Artemis.Models.Interfaces; +using Artemis.Profiles.Layers.Models; + +namespace Artemis.Profiles.Layers.Interfaces +{ + public interface ILayerCondition + { + bool ConditionsMet(LayerModel layer, IDataModel dataModel); + } +} \ No newline at end of file diff --git a/Artemis/Artemis/Profiles/Layers/Interfaces/ILayerType.cs b/Artemis/Artemis/Profiles/Layers/Interfaces/ILayerType.cs new file mode 100644 index 000000000..e83eb6382 --- /dev/null +++ b/Artemis/Artemis/Profiles/Layers/Interfaces/ILayerType.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using System.Windows.Media; +using Artemis.Models.Interfaces; +using Artemis.Profiles.Layers.Models; +using Artemis.ViewModels.Profiles.Layers; + +namespace Artemis.Profiles.Layers.Interfaces +{ + public interface ILayerType + { + /// + /// Layer type name + /// + string Name { get; } + + /// + /// Gets whether this type must be drawn on the keyboard/the editor or not + /// + bool MustDraw { get; } + + /// + /// The the thumbnail for this layer type + /// + /// The layer to draw the thumbnail for + ImageSource DrawThumbnail(LayerModel layer); + + /// + /// Draws the layer + /// + /// The layer to draw + /// The drawing context to draw with + void Draw(LayerModel layer, DrawingContext c); + + /// + /// Updates the provided layer layerModel according to this type + /// + /// The layerModel to apply to + /// The datamodel to base the layer on + /// Set to true if previewing this layer + void Update(LayerModel layerModel, IDataModel dataModel, bool isPreview = false); + + /// + /// Sets up the layer's properties to accommodate this layerType + /// + /// + void SetupProperties(LayerModel layerModel); + + /// + /// Sets up a viewmodel to accomodate this layerType + /// + /// The current viewmodel + /// + /// The datamodel to use in the new viewmodel + /// The layer to use in the new viewmodel + LayerPropertiesViewModel SetupViewModel(LayerPropertiesViewModel layerPropertiesViewModel, List layerAnimations, IDataModel dataModel, LayerModel proposedLayer); + } +} \ No newline at end of file diff --git a/Artemis/Artemis/Models/Profiles/Properties/DynamicPropertiesModel.cs b/Artemis/Artemis/Profiles/Layers/Models/DynamicPropertiesModel.cs similarity index 76% rename from Artemis/Artemis/Models/Profiles/Properties/DynamicPropertiesModel.cs rename to Artemis/Artemis/Profiles/Layers/Models/DynamicPropertiesModel.cs index 8c3956744..7c05955d3 100644 --- a/Artemis/Artemis/Models/Profiles/Properties/DynamicPropertiesModel.cs +++ b/Artemis/Artemis/Profiles/Layers/Models/DynamicPropertiesModel.cs @@ -1,9 +1,8 @@ using System.ComponentModel; using Artemis.Models.Interfaces; using Artemis.Utilities; -using static System.Decimal; -namespace Artemis.Models.Profiles.Properties +namespace Artemis.Profiles.Layers.Models { public class DynamicPropertiesModel { @@ -37,31 +36,31 @@ namespace Artemis.Models.Profiles.Properties /// public LayerPropertyOptions LayerPropertyOptions { get; set; } - internal void ApplyProperty(IDataModel dataModel, ref AppliedProperties properties) + internal void ApplyProperty(IDataModel dataModel, LayerPropertiesModel properties) { if (LayerPropertyType == LayerPropertyType.PercentageOf) - ApplyPercentageOf(dataModel, ref properties, PercentageSource); + ApplyPercentageOf(dataModel, properties, PercentageSource); if (LayerPropertyType == LayerPropertyType.PercentageOfProperty) - ApplyPercentageOfProperty(dataModel, ref properties); + ApplyPercentageOfProperty(dataModel, properties); } - private void ApplyPercentageOf(IDataModel dataModel, ref AppliedProperties properties, double src) + private void ApplyPercentageOf(IDataModel dataModel, LayerPropertiesModel properties, double src) { if (GameProperty == null) return; var gameProperty = dataModel.GetPropValue(GameProperty); - var percentage = ToDouble(gameProperty)/src; + var percentage = decimal.ToDouble(gameProperty)/src; if (LayerProperty == "Width") - ApplyWidth(ref properties, percentage); + ApplyWidth(properties, percentage); else if (LayerProperty == "Height") - ApplyHeight(ref properties, percentage); + ApplyHeight(properties, percentage); else if (LayerProperty == "Opacity") - ApplyOpacity(ref properties, percentage); + ApplyOpacity(properties, percentage); } - private void ApplyWidth(ref AppliedProperties properties, double percentage) + private void ApplyWidth(LayerPropertiesModel properties, double percentage) { var newWidth = percentage*properties.Width; var difference = properties.Width - newWidth; @@ -72,7 +71,7 @@ namespace Artemis.Models.Profiles.Properties properties.X = properties.X + difference; } - private void ApplyHeight(ref AppliedProperties properties, double percentage) + private void ApplyHeight(LayerPropertiesModel properties, double percentage) { var newHeight = percentage*properties.Height; var difference = properties.Height - newHeight; @@ -82,7 +81,7 @@ namespace Artemis.Models.Profiles.Properties properties.Y = properties.Y + difference; } - private void ApplyOpacity(ref AppliedProperties properties, double percentage) + private void ApplyOpacity(LayerPropertiesModel properties, double percentage) { properties.Opacity = percentage*properties.Opacity; if (properties.Opacity < 0.0) @@ -93,12 +92,16 @@ namespace Artemis.Models.Profiles.Properties // Apply the inverse/decrease option if (LayerPropertyOptions == LayerPropertyOptions.Decrease) properties.Opacity = 1.0 - properties.Opacity; + + var brush = properties.Brush.Clone(); + brush.Opacity = properties.Opacity; + properties.Brush = brush; } - private void ApplyPercentageOfProperty(IDataModel dataModel, ref AppliedProperties properties) + private void ApplyPercentageOfProperty(IDataModel dataModel, LayerPropertiesModel properties) { var value = dataModel.GetPropValue(PercentageProperty); - ApplyPercentageOf(dataModel, ref properties, value); + ApplyPercentageOf(dataModel, properties, value); } } diff --git a/Artemis/Artemis/Profiles/Layers/Models/EventPropertiesModel.cs b/Artemis/Artemis/Profiles/Layers/Models/EventPropertiesModel.cs new file mode 100644 index 000000000..8acd950da --- /dev/null +++ b/Artemis/Artemis/Profiles/Layers/Models/EventPropertiesModel.cs @@ -0,0 +1,49 @@ +using System; +using Newtonsoft.Json; + +namespace Artemis.Profiles.Layers.Models +{ + public abstract class EventPropertiesModel + { + public ExpirationType ExpirationType { get; set; } + public TimeSpan Length { get; set; } + public TimeSpan TriggerDelay { get; set; } + + [JsonIgnore] + public bool MustTrigger { get; set; } + + [JsonIgnore] + public DateTime AnimationStart { get; set; } + + [JsonIgnore] + public bool MustDraw { get; set; } + + /// + /// Resets the event's properties and triggers it + /// + public abstract void TriggerEvent(LayerModel layer); + + /// + /// Gets whether the event should stop + /// + /// + /// + public abstract bool MustStop(LayerModel layer); + + // Called every frame, if parent conditions met. + public void Update(LayerModel layerModel, bool conditionsMet) + { + if (MustStop(layerModel)) + MustDraw = false; + + if (!conditionsMet) + MustTrigger = true; + } + } + + public enum ExpirationType + { + Time, + Animation + } +} \ No newline at end of file diff --git a/Artemis/Artemis/Profiles/Layers/Models/KeyboardEventPropertiesModel.cs b/Artemis/Artemis/Profiles/Layers/Models/KeyboardEventPropertiesModel.cs new file mode 100644 index 000000000..ce0cb8bd2 --- /dev/null +++ b/Artemis/Artemis/Profiles/Layers/Models/KeyboardEventPropertiesModel.cs @@ -0,0 +1,47 @@ +using System; +using Artemis.Profiles.Layers.Types.Keyboard; +using Artemis.Profiles.Layers.Types.KeyboardGif; + +namespace Artemis.Profiles.Layers.Models +{ + public class KeyboardEventPropertiesModel : EventPropertiesModel + { + public override void TriggerEvent(LayerModel layer) + { + var keyboardProperties = layer.Properties as KeyboardPropertiesModel; + if (keyboardProperties == null) + throw new ArgumentException("Layer's properties cannot be null " + + "and must be of type KeyboardPropertiesModel"); + if (!MustTrigger) + return; + + MustTrigger = false; + MustDraw = true; + keyboardProperties.AnimationProgress = 0.0; + if (layer.GifImage != null) + layer.GifImage.CurrentFrame = 0; + } + + public override bool MustStop(LayerModel layer) + { + var keyboardProperties = layer.Properties as KeyboardPropertiesModel; + if (keyboardProperties == null) + throw new ArgumentException("Layer's properties cannot be null " + + "and must be of type KeyboardPropertiesModel"); + + switch (ExpirationType) + { + case ExpirationType.Time: + if (AnimationStart == DateTime.MinValue) + return false; + return DateTime.Now - Length > AnimationStart; + case ExpirationType.Animation: + if (layer.LayerType is KeyboardGifType) + return layer.GifImage?.CurrentFrame >= layer.GifImage?.FrameCount - 1; + return layer.LayerAnimation == null || layer.LayerAnimation.MustExpire(layer); + default: + return true; + } + } + } +} \ No newline at end of file diff --git a/Artemis/Artemis/Models/Profiles/LayerConditionModel.cs b/Artemis/Artemis/Profiles/Layers/Models/LayerConditionModel.cs similarity index 85% rename from Artemis/Artemis/Models/Profiles/LayerConditionModel.cs rename to Artemis/Artemis/Profiles/Layers/Models/LayerConditionModel.cs index cc8f723dd..3e93af114 100644 --- a/Artemis/Artemis/Models/Profiles/LayerConditionModel.cs +++ b/Artemis/Artemis/Profiles/Layers/Models/LayerConditionModel.cs @@ -1,58 +1,58 @@ -using System; -using Artemis.Models.Interfaces; -using Artemis.Utilities; -using DynamicExpresso; - -namespace Artemis.Models.Profiles -{ - public class LayerConditionModel - { - private readonly Interpreter _interpreter; - - public LayerConditionModel() - { - _interpreter = new Interpreter(); - } - - public string Field { get; set; } - public string Value { get; set; } - public string Operator { get; set; } - public string Type { get; set; } - - public bool ConditionMet(IDataModel subject) - { - if (string.IsNullOrEmpty(Field) || string.IsNullOrEmpty(Value) || string.IsNullOrEmpty(Type)) - return false; - - var inspect = GeneralHelpers.GetPropertyValue(subject, Field); - if (inspect == null) - return false; - - // Put the subject in a list, allowing Dynamic Linq to be used. - if (Type == "String") - { - return _interpreter.Eval($"subject.{Field}.ToLower() {Operator} value", - new Parameter("subject", typeof(T), subject), - new Parameter("value", Value.ToLower())); - } - - Parameter rightParam = null; - switch (Type) - { - case "Enum": - var enumType = GeneralHelpers.GetPropertyValue(subject, Field).GetType(); - rightParam = new Parameter("value", Enum.Parse(enumType, Value)); - break; - case "Boolean": - rightParam = new Parameter("value", bool.Parse(Value)); - break; - case "Int32": - rightParam = new Parameter("value", int.Parse(Value)); - break; - } - - return _interpreter.Eval($"subject.{Field} {Operator} value", - new Parameter("subject", typeof(T), subject), rightParam); - } - } +using System; +using Artemis.Models.Interfaces; +using Artemis.Utilities; +using DynamicExpresso; + +namespace Artemis.Profiles.Layers.Models +{ + public class LayerConditionModel + { + private readonly Interpreter _interpreter; + + public LayerConditionModel() + { + _interpreter = new Interpreter(); + } + + public string Field { get; set; } + public string Value { get; set; } + public string Operator { get; set; } + public string Type { get; set; } + + public bool ConditionMet(IDataModel subject) + { + if (string.IsNullOrEmpty(Field) || string.IsNullOrEmpty(Value) || string.IsNullOrEmpty(Type)) + return false; + + var inspect = GeneralHelpers.GetPropertyValue(subject, Field); + if (inspect == null) + return false; + + // Put the subject in a list, allowing Dynamic Linq to be used. + if (Type == "String") + { + return _interpreter.Eval($"subject.{Field}.ToLower() {Operator} value", + new Parameter("subject", subject.GetType(), subject), + new Parameter("value", Value.ToLower())); + } + + Parameter rightParam = null; + switch (Type) + { + case "Enum": + var enumType = GeneralHelpers.GetPropertyValue(subject, Field).GetType(); + rightParam = new Parameter("value", Enum.Parse(enumType, Value)); + break; + case "Boolean": + rightParam = new Parameter("value", bool.Parse(Value)); + break; + case "Int32": + rightParam = new Parameter("value", int.Parse(Value)); + break; + } + + return _interpreter.Eval($"subject.{Field} {Operator} value", + new Parameter("subject", subject.GetType(), subject), rightParam); + } + } } \ No newline at end of file diff --git a/Artemis/Artemis/Models/Profiles/LayerModel.cs b/Artemis/Artemis/Profiles/Layers/Models/LayerModel.cs similarity index 50% rename from Artemis/Artemis/Models/Profiles/LayerModel.cs rename to Artemis/Artemis/Profiles/Layers/Models/LayerModel.cs index 2b8d647ae..5c2cf057e 100644 --- a/Artemis/Artemis/Models/Profiles/LayerModel.cs +++ b/Artemis/Artemis/Profiles/Layers/Models/LayerModel.cs @@ -1,276 +1,259 @@ -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Windows.Media; -using System.Xml.Serialization; -using Artemis.Models.Interfaces; -using Artemis.Models.Profiles.Properties; -using Artemis.Utilities; -using Artemis.Utilities.Layers; -using Artemis.Utilities.ParentChild; - -namespace Artemis.Models.Profiles -{ - public class LayerModel : IChildItem, IChildItem - { - public LayerModel() - { - Children = new ChildItemCollection(this); - } - - public string Name { get; set; } - public int Order { get; set; } - public LayerType LayerType { get; set; } - public bool Enabled { get; set; } - public bool Expanded { get; set; } - public LayerPropertiesModel Properties { get; set; } - public ChildItemCollection Children { get; } - - [XmlIgnore] - public ImageSource LayerImage => Drawer.DrawThumbnail(this); - - [XmlIgnore] - public LayerModel Parent { get; internal set; } - - [XmlIgnore] - public ProfileModel Profile { get; internal set; } - - [XmlIgnore] - public GifImage GifImage { get; set; } - - public bool ConditionsMet(IDataModel dataModel) - { - return Enabled && Properties.Conditions.All(cm => cm.ConditionMet(dataModel)); - } - - public void Draw(IDataModel dataModel, DrawingContext c, bool preview, bool updateAnimations) - { - if (LayerType != LayerType.Keyboard && LayerType != LayerType.KeyboardGif) - return; - - // Preview simply shows the properties as they are. When not previewing they are applied - var appliedProperties = !preview - ? Properties.GetAppliedProperties(dataModel) - : Properties.GetAppliedProperties(dataModel, true); - - // Update animations - AnimationUpdater.UpdateAnimation((KeyboardPropertiesModel) Properties, updateAnimations); - - if (LayerType == LayerType.Keyboard) - Drawer.Draw(c, (KeyboardPropertiesModel) Properties, appliedProperties); - else if (LayerType == LayerType.KeyboardGif) - GifImage = Drawer.DrawGif(c, (KeyboardPropertiesModel) Properties, appliedProperties, GifImage); - } - - public Brush GenerateBrush(LayerType type, IDataModel dataModel, bool preview, bool updateAnimations) - { - if (!Enabled) - return null; - if (LayerType != LayerType.Folder && LayerType != type) - return null; - - // Preview simply shows the properties as they are. When not previewing they are applied - AppliedProperties appliedProperties; - if (!preview) - { - if (!ConditionsMet(dataModel)) - return null; // Return null when not previewing and the conditions arent met - appliedProperties = Properties.GetAppliedProperties(dataModel); - } - else - appliedProperties = Properties.GetAppliedProperties(dataModel, true); - - // TODO: Mouse/headset animations - - if (LayerType != LayerType.Folder) - return appliedProperties.Brush; - - Brush res = null; - foreach (var layerModel in Children.OrderByDescending(l => l.Order)) - { - var brush = layerModel.GenerateBrush(type, dataModel, preview, updateAnimations); - if (brush != null) - res = brush; - } - return res; - } - - public void SetupProperties() - { - if ((LayerType == LayerType.Keyboard || LayerType == LayerType.KeyboardGif) && - !(Properties is KeyboardPropertiesModel)) - { - Properties = new KeyboardPropertiesModel - { - Brush = new SolidColorBrush(ColorHelpers.GetRandomRainbowMediaColor()), - Animation = LayerAnimation.None, - Height = 1, - Width = 1, - X = 0, - Y = 0, - Opacity = 1 - }; - } - else if (LayerType == LayerType.Mouse && !(Properties is MousePropertiesModel)) - Properties = new MousePropertiesModel - { - Brush = new SolidColorBrush(ColorHelpers.GetRandomRainbowMediaColor()) - }; - else if (LayerType == LayerType.Headset && !(Properties is HeadsetPropertiesModel)) - Properties = new HeadsetPropertiesModel - { - Brush = new SolidColorBrush(ColorHelpers.GetRandomRainbowMediaColor()) - }; - } - - public void FixOrder() - { - Children.Sort(l => l.Order); - for (var i = 0; i < Children.Count; i++) - Children[i].Order = i; - } - - /// - /// Returns whether the layer meets the requirements to be drawn - /// - /// - public bool MustDraw() - { - // If any of the parents are disabled, this layer must not be drawn - var parent = Parent; - while (parent != null) - { - if (!parent.Enabled) - return false; - parent = parent.Parent; - } - return Enabled && (LayerType == LayerType.Keyboard || LayerType == LayerType.KeyboardGif); - } - - public IEnumerable GetLayers() - { - var layers = new List(); - foreach (var layerModel in Children) - { - layers.Add(layerModel); - layers.AddRange(layerModel.GetLayers()); - } - - return layers; - } - - public static LayerModel CreateLayer() - { - return new LayerModel - { - Name = "New layer", - Enabled = true, - Order = -1, - LayerType = LayerType.Keyboard, - Properties = new KeyboardPropertiesModel - { - Brush = new SolidColorBrush(ColorHelpers.GetRandomRainbowMediaColor()), - Animation = LayerAnimation.None, - Height = 1, - Width = 1, - X = 0, - Y = 0, - Opacity = 1 - } - }; - } - - public void InsertBefore(LayerModel source) - { - source.Order = Order; - Insert(source); - } - - public void InsertAfter(LayerModel source) - { - source.Order = Order + 1; - Insert(source); - } - - private void Insert(LayerModel source) - { - if (Parent != null) - { - foreach (var child in Parent.Children.OrderBy(c => c.Order)) - { - if (child.Order >= source.Order) - child.Order++; - } - Parent.Children.Add(source); - } - else if (Profile != null) - { - foreach (var layer in Profile.Layers.OrderBy(l => l.Order)) - { - if (layer.Order >= source.Order) - layer.Order++; - } - Profile.Layers.Add(source); - } - } - - /// - /// Generates a flat list containing all layers that must be rendered on the keyboard, - /// the first mouse layer to be rendered and the first headset layer to be rendered - /// - /// The game data model to base the conditions on - /// Instance of said game data model - /// Whether or not to include mice in the list - /// Whether or not to include headsets in the list - /// - /// A flat list containing all layers that must be rendered - public List GetRenderLayers(IDataModel dataModel, bool includeMice, bool includeHeadsets, - bool ignoreConditions = false) - { - var layers = new List(); - foreach (var layerModel in Children.OrderByDescending(c => c.Order)) - { - if (!layerModel.Enabled || - !includeMice && layerModel.LayerType == LayerType.Mouse || - !includeHeadsets && layerModel.LayerType == LayerType.Headset) - continue; - - if (!ignoreConditions) - { - if (!layerModel.ConditionsMet(dataModel)) - continue; - } - - layers.Add(layerModel); - layers.AddRange(layerModel.GetRenderLayers(dataModel, includeMice, includeHeadsets, ignoreConditions)); - } - - return layers; - } - - #region IChildItem Members - - LayerModel IChildItem.Parent - { - get { return Parent; } - set { Parent = value; } - } - - ProfileModel IChildItem.Parent - { - get { return Profile; } - set { Profile = value; } - } - - #endregion - } - - public enum LayerType - { - [Description("Folder")] Folder, - [Description("Keyboard")] Keyboard, - [Description("Keyboard - GIF")] KeyboardGif, - [Description("Mouse")] Mouse, - [Description("Headset")] Headset - } +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Media; +using Artemis.Models.Interfaces; +using Artemis.Profiles.Layers.Conditions; +using Artemis.Profiles.Layers.Interfaces; +using Artemis.Profiles.Layers.Types.Headset; +using Artemis.Profiles.Layers.Types.Keyboard; +using Artemis.Profiles.Layers.Types.Mouse; +using Artemis.Utilities; +using Artemis.Utilities.ParentChild; +using Newtonsoft.Json; + +namespace Artemis.Profiles.Layers.Models +{ + public class LayerModel : IChildItem, IChildItem + { + public LayerModel() + { + Children = new ChildItemCollection(this); + + var model = Properties as KeyboardPropertiesModel; + if (model != null) + GifImage = new GifImage(model.GifFile); + } + + public ILayerType LayerType { get; set; } + public ILayerCondition LayerCondition { get; set; } + public ILayerAnimation LayerAnimation { get; set; } + + public string Name { get; set; } + public int Order { get; set; } + + public bool Enabled { get; set; } + public bool Expanded { get; set; } + public bool IsEvent { get; set; } + public LayerPropertiesModel Properties { get; set; } + public EventPropertiesModel EventProperties { get; set; } + public ChildItemCollection Children { get; } + + [JsonIgnore] + public LayerPropertiesModel AppliedProperties { get; set; } + + [JsonIgnore] + public ImageSource LayerImage => LayerType.DrawThumbnail(this); + + [JsonIgnore] + public LayerModel Parent { get; internal set; } + + [JsonIgnore] + public ProfileModel Profile { get; internal set; } + + [JsonIgnore] + public GifImage GifImage { get; set; } + + /// + /// Checks whether this layers conditions are met. + /// If they are met and this layer is an event, this also triggers that event. + /// + /// + /// + public bool ConditionsMet(IDataModel dataModel) + { + // Conditions are not even checked if the layer isn't enabled + return Enabled && LayerCondition.ConditionsMet(this, dataModel); + } + + public void Update(IDataModel dataModel, bool preview, bool updateAnimations) + { + LayerType.Update(this, dataModel, preview); + LayerAnimation?.Update(this, updateAnimations); + } + + public void Draw(IDataModel dataModel, DrawingContext c, bool preview, bool updateAnimations) + { + LayerType.Draw(this, c); + } + + public void SetupProperties() + { + LayerType.SetupProperties(this); + + // If the type is an event, set it up + if (IsEvent && EventProperties == null) + { + EventProperties = new KeyboardEventPropertiesModel + { + ExpirationType = ExpirationType.Time, + Length = new TimeSpan(0, 0, 1), + TriggerDelay = new TimeSpan(0) + }; + } + } + + public void FixOrder() + { + Children.Sort(l => l.Order); + for (var i = 0; i < Children.Count; i++) + Children[i].Order = i; + } + + /// + /// Returns whether the layer meets the requirements to be drawn in the profile editor + /// + /// + public bool MustDraw() + { + // If any of the parents are disabled, this layer must not be drawn + var parent = Parent; + while (parent != null) + { + if (!parent.Enabled) + return false; + parent = parent.Parent; + } + return Enabled && LayerType.MustDraw; + } + + /// + /// Returns every descendant of this layer + /// + /// + public IEnumerable GetLayers() + { + var layers = new List(); + foreach (var layerModel in Children) + { + layers.Add(layerModel); + layers.AddRange(layerModel.GetLayers()); + } + + return layers; + } + + /// + /// Creates a new Keyboard layer with default settings + /// + /// + public static LayerModel CreateLayer() + { + return new LayerModel + { + Name = "New layer", + Enabled = true, + Order = -1, + LayerType = new KeyboardType(), + LayerCondition = new DataModelCondition(), + Properties = new KeyboardPropertiesModel + { + Brush = new SolidColorBrush(ColorHelpers.GetRandomRainbowMediaColor()), + Height = 1, + Width = 1, + X = 0, + Y = 0, + Opacity = 1 + } + }; + } + + public void InsertBefore(LayerModel source) + { + source.Order = Order; + Insert(source); + } + + public void InsertAfter(LayerModel source) + { + source.Order = Order + 1; + Insert(source); + } + + private void Insert(LayerModel source) + { + if (Parent != null) + { + foreach (var child in Parent.Children.OrderBy(c => c.Order)) + { + if (child.Order >= source.Order) + child.Order++; + } + Parent.Children.Add(source); + } + else if (Profile != null) + { + foreach (var layer in Profile.Layers.OrderBy(l => l.Order)) + { + if (layer.Order >= source.Order) + layer.Order++; + } + Profile.Layers.Add(source); + } + } + + public void Replace(LayerModel layer) + { + layer.Order = Order; + layer.Parent = null; + layer.Profile = null; + + if (Parent != null) + { + Parent.Children.Add(layer); + Parent.Children.Remove(this); + } + else if (Profile != null) + { + Profile.Layers.Add(layer); + Profile.Layers.Remove(this); + } + } + + /// + /// Generates a flat list containing all layers that must be rendered on the keyboard, + /// the first mouse layer to be rendered and the first headset layer to be rendered + /// + /// The game data model to base the conditions on + /// Instance of said game data model + /// Whether or not to include mice in the list + /// Whether or not to include headsets in the list + /// + /// A flat list containing all layers that must be rendered + public List GetRenderLayers(IDataModel dataModel, bool includeMice, bool includeHeadsets, + bool ignoreConditions = false) + { + var layers = new List(); + foreach (var layerModel in Children.OrderByDescending(c => c.Order)) + { + if (!layerModel.Enabled || !includeMice && layerModel.LayerType is MouseType || + !includeHeadsets && layerModel.LayerType is HeadsetType) + continue; + + if (!ignoreConditions && !layerModel.ConditionsMet(dataModel)) + continue; + + layers.Add(layerModel); + layers.AddRange(layerModel.GetRenderLayers(dataModel, includeMice, includeHeadsets, ignoreConditions)); + } + + return layers; + } + + #region IChildItem Members + + LayerModel IChildItem.Parent + { + get { return Parent; } + set { Parent = value; } + } + + ProfileModel IChildItem.Parent + { + get { return Profile; } + set { Profile = value; } + } + + #endregion + } } \ No newline at end of file diff --git a/Artemis/Artemis/Profiles/Layers/Models/LayerPropertiesModel.cs b/Artemis/Artemis/Profiles/Layers/Models/LayerPropertiesModel.cs new file mode 100644 index 000000000..6c80bc49f --- /dev/null +++ b/Artemis/Artemis/Profiles/Layers/Models/LayerPropertiesModel.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; +using System.Windows.Media; +using Artemis.Utilities.Converters; +using Newtonsoft.Json; + +namespace Artemis.Profiles.Layers.Models +{ + public abstract class LayerPropertiesModel + { + public LayerPropertiesModel(LayerPropertiesModel source = null) + { + if (source == null) + return; + + // Clone the source's properties onto the new properties model (useful when changing property type) + X = source.X; + Y = source.Y; + Width = source.Width; + Height = source.Height; + Contain = source.Contain; + Opacity = source.Opacity; + AnimationSpeed = source.AnimationSpeed; + Conditions = source.Conditions; + DynamicProperties = source.DynamicProperties; + Brush = source.Brush; + } + + private Brush _brush; + + public double X { get; set; } + public double Y { get; set; } + public double Width { get; set; } + public double Height { get; set; } + public bool Contain { get; set; } + public double Opacity { get; set; } + public double AnimationSpeed { get; set; } + public List Conditions { get; set; } = new List(); + public List DynamicProperties { get; set; } = new List(); + + [JsonIgnore] + + public double AnimationProgress { get; set; } + + [JsonConverter(typeof(BrushJsonConverter))] + public Brush Brush + { + get { return _brush; } + set + { + if (value == null) + { + _brush = null; + return; + } + + if (value.IsFrozen) + { + _brush = value; + return; + } + + // Clone the brush off of the UI thread and freeze it + var cloned = value.Dispatcher.Invoke(value.CloneCurrentValue); + cloned.Freeze(); + _brush = cloned; + } + } + } +} \ No newline at end of file diff --git a/Artemis/Artemis/Profiles/Layers/Models/SimplePropertiesModel.cs b/Artemis/Artemis/Profiles/Layers/Models/SimplePropertiesModel.cs new file mode 100644 index 000000000..463ca0006 --- /dev/null +++ b/Artemis/Artemis/Profiles/Layers/Models/SimplePropertiesModel.cs @@ -0,0 +1,12 @@ +namespace Artemis.Profiles.Layers.Models +{ + /// + /// An empty layer properties model used by layers that don't have their own special properties + /// + public class SimplePropertiesModel : LayerPropertiesModel + { + public SimplePropertiesModel(LayerPropertiesModel properties = null) : base(properties) + { + } + } +} \ No newline at end of file diff --git a/Artemis/Artemis/Profiles/Layers/Types/Folder/FolderType.cs b/Artemis/Artemis/Profiles/Layers/Types/Folder/FolderType.cs new file mode 100644 index 000000000..08b24cb63 --- /dev/null +++ b/Artemis/Artemis/Profiles/Layers/Types/Folder/FolderType.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Windows; +using System.Windows.Media; +using Artemis.Models.Interfaces; +using Artemis.Profiles.Layers.Interfaces; +using Artemis.Profiles.Layers.Models; +using Artemis.Properties; +using Artemis.Utilities; +using Artemis.ViewModels.Profiles.Layers; + +namespace Artemis.Profiles.Layers.Types.Folder +{ + public class FolderType : ILayerType + { + public string Name { get; } = "Folder"; + public bool MustDraw { get; } = false; + + public ImageSource DrawThumbnail(LayerModel layer) + { + var thumbnailRect = new Rect(0, 0, 18, 18); + var visual = new DrawingVisual(); + using (var c = visual.RenderOpen()) + c.DrawImage(ImageUtilities.BitmapToBitmapImage(Resources.folder), thumbnailRect); + + var image = new DrawingImage(visual.Drawing); + return image; + } + + public void Draw(LayerModel layer, DrawingContext c) + { + } + + public void Update(LayerModel layerModel, IDataModel dataModel, bool isPreview = false) + { + } + + public void SetupProperties(LayerModel layerModel) + { + if (layerModel.Properties is SimplePropertiesModel) + return; + + layerModel.Properties = new SimplePropertiesModel(layerModel.Properties); + } + + public LayerPropertiesViewModel SetupViewModel(LayerPropertiesViewModel layerPropertiesViewModel, + List layerAnimations, IDataModel dataModel, LayerModel proposedLayer) + { + if (layerPropertiesViewModel is FolderPropertiesViewModel) + return layerPropertiesViewModel; + return new FolderPropertiesViewModel(proposedLayer, dataModel); + } + } +} \ No newline at end of file diff --git a/Artemis/Artemis/Profiles/Layers/Types/Headset/HeadsetType.cs b/Artemis/Artemis/Profiles/Layers/Types/Headset/HeadsetType.cs new file mode 100644 index 000000000..b50921b8b --- /dev/null +++ b/Artemis/Artemis/Profiles/Layers/Types/Headset/HeadsetType.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Windows; +using System.Windows.Media; +using Artemis.Models.Interfaces; +using Artemis.Profiles.Layers.Animations; +using Artemis.Profiles.Layers.Interfaces; +using Artemis.Profiles.Layers.Models; +using Artemis.Properties; +using Artemis.Utilities; +using Artemis.ViewModels.Profiles.Layers; + +namespace Artemis.Profiles.Layers.Types.Headset +{ + public class HeadsetType : ILayerType + { + public string Name { get; } = "Headset"; + public bool MustDraw { get; } = false; + + public ImageSource DrawThumbnail(LayerModel layer) + { + var thumbnailRect = new Rect(0, 0, 18, 18); + var visual = new DrawingVisual(); + using (var c = visual.RenderOpen()) + c.DrawImage(ImageUtilities.BitmapToBitmapImage(Resources.headset), thumbnailRect); + + var image = new DrawingImage(visual.Drawing); + return image; + } + + public void Draw(LayerModel layer, DrawingContext c) + { + // If an animation is present, let it handle the drawing + if (layer.LayerAnimation != null && !(layer.LayerAnimation is NoneAnimation)) + { + layer.LayerAnimation.Draw(layer.Properties, layer.AppliedProperties, c); + return; + } + + // Otherwise draw the rectangle with its applied dimensions and brush + var rect = new Rect(layer.AppliedProperties.X * 4, + layer.AppliedProperties.Y * 4, + layer.AppliedProperties.Width * 4, + layer.AppliedProperties.Height * 4); + + c.PushClip(new RectangleGeometry(rect)); + c.DrawRectangle(layer.AppliedProperties.Brush, null, rect); + c.Pop(); + } + + public void Update(LayerModel layerModel, IDataModel dataModel, bool isPreview = false) + { + // Headset layers are always drawn 10*10 (which is 40*40 when scaled up) + layerModel.Properties.Width = 10; + layerModel.Properties.Height = 10; + layerModel.Properties.X = 0; + layerModel.Properties.Y = 0; + layerModel.Properties.Contain = true; + + layerModel.AppliedProperties = new SimplePropertiesModel(layerModel.Properties); + + if (isPreview || dataModel == null) + return; + + // If not previewing, apply dynamic properties according to datamodel + var props = (SimplePropertiesModel)layerModel.AppliedProperties; + foreach (var dynamicProperty in props.DynamicProperties) + dynamicProperty.ApplyProperty(dataModel, layerModel.AppliedProperties); + } + + public void SetupProperties(LayerModel layerModel) + { + if (layerModel.Properties is SimplePropertiesModel) + return; + + layerModel.Properties = new SimplePropertiesModel(layerModel.Properties); + } + + public LayerPropertiesViewModel SetupViewModel(LayerPropertiesViewModel layerPropertiesViewModel, + List layerAnimations, IDataModel dataModel, LayerModel proposedLayer) + { + if (layerPropertiesViewModel is HeadsetPropertiesViewModel) + return layerPropertiesViewModel; + return new HeadsetPropertiesViewModel(proposedLayer, dataModel, layerAnimations); + } + } +} \ No newline at end of file diff --git a/Artemis/Artemis/Profiles/Layers/Types/Keyboard/KeyboardPropertiesModel.cs b/Artemis/Artemis/Profiles/Layers/Types/Keyboard/KeyboardPropertiesModel.cs new file mode 100644 index 000000000..4ae12f40c --- /dev/null +++ b/Artemis/Artemis/Profiles/Layers/Types/Keyboard/KeyboardPropertiesModel.cs @@ -0,0 +1,19 @@ +using System.Windows; +using Artemis.Profiles.Layers.Models; + +namespace Artemis.Profiles.Layers.Types.Keyboard +{ + public class KeyboardPropertiesModel : LayerPropertiesModel + { + public KeyboardPropertiesModel(LayerPropertiesModel properties = null) : base(properties) + { + } + + public string GifFile { get; set; } + + public Rect GetRect(int scale = 4) + { + return new Rect(X*scale, Y*scale, Width*scale, Height*scale); + } + } +} \ No newline at end of file diff --git a/Artemis/Artemis/Profiles/Layers/Types/Keyboard/KeyboardType.cs b/Artemis/Artemis/Profiles/Layers/Types/Keyboard/KeyboardType.cs new file mode 100644 index 000000000..a9b3bdfcf --- /dev/null +++ b/Artemis/Artemis/Profiles/Layers/Types/Keyboard/KeyboardType.cs @@ -0,0 +1,100 @@ +using System.Collections.Generic; +using System.Windows; +using System.Windows.Media; +using Artemis.Models.Interfaces; +using Artemis.Profiles.Layers.Animations; +using Artemis.Profiles.Layers.Interfaces; +using Artemis.Profiles.Layers.Models; +using Artemis.Utilities; +using Artemis.ViewModels.Profiles.Layers; + + +namespace Artemis.Profiles.Layers.Types.Keyboard +{ + public class KeyboardType : ILayerType + { + public string Name { get; } = "Keyboard"; + public bool MustDraw { get; } = true; + + public ImageSource DrawThumbnail(LayerModel layer) + { + var thumbnailRect = new Rect(0, 0, 18, 18); + var visual = new DrawingVisual(); + using (var c = visual.RenderOpen()) + { + if (layer.Properties.Brush != null) + { + c.DrawRectangle(layer.Properties.Brush, + new Pen(new SolidColorBrush(Colors.White), 1), + thumbnailRect); + } + } + + var image = new DrawingImage(visual.Drawing); + return image; + } + + public void Draw(LayerModel layer, DrawingContext c) + { + // If an animation is present, let it handle the drawing + if (layer.LayerAnimation != null && !(layer.LayerAnimation is NoneAnimation)) + { + layer.LayerAnimation.Draw(layer.Properties, layer.AppliedProperties, c); + return; + } + + // Otherwise draw the rectangle with its layer.AppliedProperties dimensions and brush + var rect = layer.Properties.Contain + ? new Rect(layer.AppliedProperties.X*4, + layer.AppliedProperties.Y*4, + layer.AppliedProperties.Width*4, + layer.AppliedProperties.Height*4) + : new Rect(layer.Properties.X*4, + layer.Properties.Y*4, + layer.Properties.Width*4, + layer.Properties.Height*4); + + var clip = new Rect(layer.AppliedProperties.X*4, layer.AppliedProperties.Y*4, + layer.AppliedProperties.Width*4, layer.AppliedProperties.Height*4); + + + c.PushClip(new RectangleGeometry(clip)); + c.DrawRectangle(layer.AppliedProperties.Brush, null, rect); + c.Pop(); + } + + public void Update(LayerModel layerModel, IDataModel dataModel, bool isPreview = false) + { + layerModel.AppliedProperties = new KeyboardPropertiesModel(layerModel.Properties); + if (isPreview || dataModel == null) + return; + + // If not previewing, apply dynamic properties according to datamodel + var keyboardProps = (KeyboardPropertiesModel) layerModel.AppliedProperties; + foreach (var dynamicProperty in keyboardProps.DynamicProperties) + dynamicProperty.ApplyProperty(dataModel, layerModel.AppliedProperties); + } + + public void SetupProperties(LayerModel layerModel) + { + if (layerModel.Properties is KeyboardPropertiesModel) + return; + + layerModel.Properties = new KeyboardPropertiesModel(layerModel.Properties); + } + + public LayerPropertiesViewModel SetupViewModel(LayerPropertiesViewModel layerPropertiesViewModel, + List layerAnimations, IDataModel dataModel, LayerModel proposedLayer) + { + var model = layerPropertiesViewModel as KeyboardPropertiesViewModel; + if (model == null) + return new KeyboardPropertiesViewModel(proposedLayer, dataModel, layerAnimations) + { + IsGif = false + }; + + model.IsGif = false; + return layerPropertiesViewModel; + } + } +} \ No newline at end of file diff --git a/Artemis/Artemis/Profiles/Layers/Types/KeyboardGif/KeyboardGifType.cs b/Artemis/Artemis/Profiles/Layers/Types/KeyboardGif/KeyboardGifType.cs new file mode 100644 index 000000000..e4814e51a --- /dev/null +++ b/Artemis/Artemis/Profiles/Layers/Types/KeyboardGif/KeyboardGifType.cs @@ -0,0 +1,93 @@ +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Windows; +using System.Windows.Media; +using Artemis.Models.Interfaces; +using Artemis.Profiles.Layers.Interfaces; +using Artemis.Profiles.Layers.Models; +using Artemis.Profiles.Layers.Types.Keyboard; +using Artemis.Properties; +using Artemis.Utilities; +using Artemis.ViewModels.Profiles.Layers; + + +namespace Artemis.Profiles.Layers.Types.KeyboardGif +{ + internal class KeyboardGifType : ILayerType + { + public string Name { get; } = "Keyboard - GIF"; + public bool MustDraw { get; } = true; + + public ImageSource DrawThumbnail(LayerModel layer) + { + var thumbnailRect = new Rect(0, 0, 18, 18); + var visual = new DrawingVisual(); + using (var c = visual.RenderOpen()) + c.DrawImage(ImageUtilities.BitmapToBitmapImage(Resources.gif), thumbnailRect); + + var image = new DrawingImage(visual.Drawing); + return image; + } + + public void Draw(LayerModel layer, DrawingContext c) + { + var props = (KeyboardPropertiesModel) layer.Properties; + if (string.IsNullOrEmpty(props.GifFile)) + return; + if (!File.Exists(props.GifFile)) + return; + + // Only reconstruct GifImage if the underlying source has changed + if (layer.GifImage == null) + layer.GifImage = new GifImage(props.GifFile); + if (layer.GifImage.Source != props.GifFile) + layer.GifImage = new GifImage(props.GifFile); + + var rect = new Rect(layer.AppliedProperties.X*4, + layer.AppliedProperties.Y*4, + layer.AppliedProperties.Width*4, + layer.AppliedProperties.Height*4); + + lock (layer.GifImage) + { + var draw = layer.GifImage.GetNextFrame(); + c.DrawImage(ImageUtilities.BitmapToBitmapImage(new Bitmap(draw)), rect); + } + } + + public void Update(LayerModel layerModel, IDataModel dataModel, bool isPreview = false) + { + layerModel.AppliedProperties = new KeyboardPropertiesModel(layerModel.Properties); + if (isPreview) + return; + + // If not previewing, apply dynamic properties according to datamodel + var keyboardProps = (KeyboardPropertiesModel) layerModel.AppliedProperties; + foreach (var dynamicProperty in keyboardProps.DynamicProperties) + dynamicProperty.ApplyProperty(dataModel, layerModel.AppliedProperties); + } + + public void SetupProperties(LayerModel layerModel) + { + if (layerModel.Properties is KeyboardPropertiesModel) + return; + + layerModel.Properties = new KeyboardPropertiesModel(layerModel.Properties); + } + + public LayerPropertiesViewModel SetupViewModel(LayerPropertiesViewModel layerPropertiesViewModel, + List layerAnimations, IDataModel dataModel, LayerModel proposedLayer) + { + var model = layerPropertiesViewModel as KeyboardPropertiesViewModel; + if (model == null) + return new KeyboardPropertiesViewModel(proposedLayer, dataModel, layerAnimations) + { + IsGif = true + }; + + model.IsGif = true; + return layerPropertiesViewModel; + } + } +} \ No newline at end of file diff --git a/Artemis/Artemis/Profiles/Layers/Types/Mouse/MouseType.cs b/Artemis/Artemis/Profiles/Layers/Types/Mouse/MouseType.cs new file mode 100644 index 000000000..1ad7d68ba --- /dev/null +++ b/Artemis/Artemis/Profiles/Layers/Types/Mouse/MouseType.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Windows; +using System.Windows.Media; +using Artemis.Models.Interfaces; +using Artemis.Profiles.Layers.Animations; +using Artemis.Profiles.Layers.Interfaces; +using Artemis.Profiles.Layers.Models; +using Artemis.Properties; +using Artemis.Utilities; +using Artemis.ViewModels.Profiles.Layers; + + +namespace Artemis.Profiles.Layers.Types.Mouse +{ + public class MouseType : ILayerType + { + public string Name { get; } = "Mouse"; + public bool MustDraw { get; } = false; + + public ImageSource DrawThumbnail(LayerModel layer) + { + var thumbnailRect = new Rect(0, 0, 18, 18); + var visual = new DrawingVisual(); + using (var c = visual.RenderOpen()) + c.DrawImage(ImageUtilities.BitmapToBitmapImage(Resources.mouse), thumbnailRect); + + var image = new DrawingImage(visual.Drawing); + return image; + } + + public void Draw(LayerModel layer, DrawingContext c) + { + // If an animation is present, let it handle the drawing + if (layer.LayerAnimation != null && !(layer.LayerAnimation is NoneAnimation)) + { + layer.LayerAnimation.Draw(layer.Properties, layer.AppliedProperties, c); + return; + } + + // Otherwise draw the rectangle with its applied dimensions and brush + var rect = new Rect(layer.AppliedProperties.X * 4, + layer.AppliedProperties.Y * 4, + layer.AppliedProperties.Width * 4, + layer.AppliedProperties.Height * 4); + + c.PushClip(new RectangleGeometry(rect)); + c.DrawRectangle(layer.AppliedProperties.Brush, null, rect); + c.Pop(); + } + + public void Update(LayerModel layerModel, IDataModel dataModel, bool isPreview = false) + { + // Mouse layers are always drawn 10*10 (which is 40*40 when scaled up) + layerModel.Properties.Width = 10; + layerModel.Properties.Height = 10; + layerModel.Properties.X = 0; + layerModel.Properties.Y = 0; + layerModel.Properties.Contain = true; + + layerModel.AppliedProperties = new SimplePropertiesModel(layerModel.Properties); + + if (isPreview || dataModel == null) + return; + + // If not previewing, apply dynamic properties according to datamodel + var props = (SimplePropertiesModel)layerModel.AppliedProperties; + foreach (var dynamicProperty in props.DynamicProperties) + dynamicProperty.ApplyProperty(dataModel, layerModel.AppliedProperties); + } + + public void SetupProperties(LayerModel layerModel) + { + if (layerModel.Properties is SimplePropertiesModel) + return; + + layerModel.Properties = new SimplePropertiesModel(layerModel.Properties); + } + + public LayerPropertiesViewModel SetupViewModel(LayerPropertiesViewModel layerPropertiesViewModel, + List layerAnimations, IDataModel dataModel, LayerModel proposedLayer) + { + if (layerPropertiesViewModel is MousePropertiesViewModel) + return layerPropertiesViewModel; + return new MousePropertiesViewModel(proposedLayer, dataModel, layerAnimations); + } + } +} \ No newline at end of file diff --git a/Artemis/Artemis/Models/Profiles/ProfileModel.cs b/Artemis/Artemis/Profiles/ProfileModel.cs similarity index 61% rename from Artemis/Artemis/Models/Profiles/ProfileModel.cs rename to Artemis/Artemis/Profiles/ProfileModel.cs index 12c2ed998..66f2962d9 100644 --- a/Artemis/Artemis/Models/Profiles/ProfileModel.cs +++ b/Artemis/Artemis/Profiles/ProfileModel.cs @@ -1,223 +1,201 @@ -using System.Collections.Generic; -using System.Drawing; -using System.Linq; -using System.Windows; -using System.Windows.Media; -using System.Xml.Serialization; -using Artemis.Models.Interfaces; -using Artemis.Models.Profiles.Properties; -using Artemis.Utilities; -using Artemis.Utilities.ParentChild; -using Brush = System.Windows.Media.Brush; -using Color = System.Windows.Media.Color; -using Point = System.Windows.Point; -using Size = System.Windows.Size; - -namespace Artemis.Models.Profiles -{ - public class ProfileModel - { - public ProfileModel() - { - Layers = new ChildItemCollection(this); - DrawingVisual = new DrawingVisual(); - } - - public ChildItemCollection Layers { get; } - - public string Name { get; set; } - public bool IsDefault { get; set; } - public string KeyboardSlug { get; set; } - public string GameName { get; set; } - - [XmlIgnore] - public DrawingVisual DrawingVisual { get; set; } - - protected bool Equals(ProfileModel other) - { - return string.Equals(Name, other.Name) && - string.Equals(KeyboardSlug, other.KeyboardSlug) && - string.Equals(GameName, other.GameName); - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != GetType()) return false; - return Equals((ProfileModel)obj); - } - - public override int GetHashCode() - { - unchecked - { - var hashCode = Name?.GetHashCode() ?? 0; - hashCode = (hashCode * 397) ^ (KeyboardSlug?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (GameName?.GetHashCode() ?? 0); - return hashCode; - } - } - - public void FixOrder() - { - Layers.Sort(l => l.Order); - for (var i = 0; i < Layers.Count; i++) - Layers[i].Order = i; - } - - public void DrawProfile(Graphics keyboard, Rect keyboardRect, IDataModel dataModel, bool preview, - bool updateAnimations) - { - var visual = new DrawingVisual(); - using (var c = visual.RenderOpen()) - { - // Setup the DrawingVisual's size - c.PushClip(new RectangleGeometry(keyboardRect)); - c.DrawRectangle(new SolidColorBrush(Color.FromArgb(0, 0, 0, 0)), null, keyboardRect); - - // Draw the layers - foreach (var layerModel in Layers.OrderByDescending(l => l.Order)) - layerModel.Draw(dataModel, c, preview, updateAnimations); - - // Remove the clip - c.Pop(); - } - - using (Bitmap bmp = ImageUtilities.DrawinVisualToBitmap(visual, keyboardRect)) - keyboard.DrawImage(bmp, new PointF(0, 0)); - } - - public Brush GenerateBrush(IDataModel dataModel, LayerType type, bool preview, bool updateAnimations) - { - Brush result = null; - // Draw the layers - foreach (var layerModel in Layers.OrderByDescending(l => l.Order)) - { - var generated = layerModel.GenerateBrush(type, dataModel, preview, updateAnimations); - if (generated != null) - result = generated; - } - - return result; - } - - /// - /// Gives all the layers and their children in a flat list - /// - public List GetLayers() - { - var layers = new List(); - foreach (var layerModel in Layers) - { - layers.Add(layerModel); - layers.AddRange(layerModel.GetLayers()); - } - - return layers; - } - - /// - /// Generates a flat list containing all layers that must be rendered on the keyboard, - /// the first mouse layer to be rendered and the first headset layer to be rendered - /// - /// The game data model to base the conditions on - /// Instance of said game data model - /// Whether or not to include mice in the list - /// Whether or not to include headsets in the list - /// - /// A flat list containing all layers that must be rendered - public List GetRenderLayers(IDataModel dataModel, bool includeMice, bool includeHeadsets, - bool ignoreConditions = false) - { - var layers = new List(); - foreach (var layerModel in Layers.OrderByDescending(l => l.Order)) - { - if (!layerModel.Enabled || - !includeMice && layerModel.LayerType == LayerType.Mouse || - !includeHeadsets && layerModel.LayerType == LayerType.Headset) - continue; - - if (!ignoreConditions) - { - if (!layerModel.ConditionsMet(dataModel)) - continue; - } - - layers.Add(layerModel); - layers.AddRange(layerModel.GetRenderLayers(dataModel, includeMice, includeHeadsets, ignoreConditions)); - } - - return layers; - } - - /// - /// Looks at all the layers wthin the profile and makes sure they are within boundaries of the given rectangle - /// - /// - public void FixBoundaries(Rect keyboardRectangle) - { - foreach (var layer in GetLayers()) - { - if (layer.LayerType != LayerType.Keyboard && layer.LayerType != LayerType.KeyboardGif) - continue; - - var props = (KeyboardPropertiesModel)layer.Properties; - var layerRect = new Rect(new Point(props.X, props.Y), new Size(props.Width, props.Height)); - if (keyboardRectangle.Contains(layerRect)) - continue; - - props.X = 0; - props.Y = 0; - layer.Properties = props; - } - } - - /// - /// Draw all the provided layers of type Keyboard and KeyboardGif - /// - /// The graphics to draw on - /// The layers to render - /// The data model to base the layer's properties on - /// A rectangle matching the current keyboard's size on a scale of 4, used for clipping - /// Indicates wheter the layer is drawn as a preview, ignoring dynamic properties - /// Wheter or not to update the layer's animations - internal void DrawProfile(Graphics keyboard, List renderLayers, IDataModel dataModel, Rect keyboardRect, - bool preview, - bool updateAnimations) - { - var visual = new DrawingVisual(); - using (var c = visual.RenderOpen()) - { - // Setup the DrawingVisual's size - c.PushClip(new RectangleGeometry(keyboardRect)); - c.DrawRectangle(new SolidColorBrush(Color.FromArgb(0, 0, 0, 0)), null, keyboardRect); - - // Draw the layers - foreach (var layerModel in renderLayers - .Where(l => l.LayerType == LayerType.Keyboard || - l.LayerType == LayerType.KeyboardGif)) - { - layerModel.Draw(dataModel, c, preview, updateAnimations); - } - - // Remove the clip - c.Pop(); - } - - using (Bitmap bmp = ImageUtilities.DrawinVisualToBitmap(visual, keyboardRect)) - keyboard.DrawImage(bmp, new PointF(0, 0)); - } - - /// - /// Generates a brush out of the given layer, for usage with mice and headsets - /// - /// The layer to base the brush on - /// The game data model to base the layer's properties on - /// The generated brush - public Brush GenerateBrush(LayerModel layerModel, IDataModel dataModel) - { - return layerModel?.Properties.GetAppliedProperties(dataModel).Brush; - } - } +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Windows; +using System.Windows.Media; +using Artemis.Models.Interfaces; +using Artemis.Profiles.Layers.Models; +using Artemis.Profiles.Layers.Types.Headset; +using Artemis.Profiles.Layers.Types.Keyboard; +using Artemis.Profiles.Layers.Types.Mouse; +using Artemis.Utilities; +using Artemis.Utilities.ParentChild; +using Newtonsoft.Json; +using Color = System.Windows.Media.Color; +using Point = System.Windows.Point; +using Size = System.Windows.Size; + +namespace Artemis.Profiles +{ + public class ProfileModel + { + public ProfileModel() + { + Layers = new ChildItemCollection(this); + DrawingVisual = new DrawingVisual(); + } + + public ChildItemCollection Layers { get; } + + public string Name { get; set; } + public bool IsDefault { get; set; } + public string KeyboardSlug { get; set; } + public string GameName { get; set; } + + [JsonIgnore] + + public DrawingVisual DrawingVisual { get; set; } + + public void FixOrder() + { + Layers.Sort(l => l.Order); + for (var i = 0; i < Layers.Count; i++) + Layers[i].Order = i; + } + + public void DrawLayers(Graphics keyboard, Rect keyboardRect, IDataModel dataModel, bool preview, + bool updateAnimations) + { + var visual = new DrawingVisual(); + using (var c = visual.RenderOpen()) + { + // Setup the DrawingVisual's size + c.PushClip(new RectangleGeometry(keyboardRect)); + c.DrawRectangle(new SolidColorBrush(Color.FromArgb(0, 0, 0, 0)), null, keyboardRect); + + // Draw the layers + foreach (var layerModel in Layers.OrderByDescending(l => l.Order)) + { + layerModel.Update(dataModel, preview, updateAnimations); + layerModel.Draw(dataModel, c, preview, updateAnimations); + } + // Remove the clip + c.Pop(); + } + + using (var bmp = ImageUtilities.DrawinVisualToBitmap(visual, keyboardRect)) + keyboard.DrawImage(bmp, new PointF(0, 0)); + } + + /// + /// Gives all the layers and their children in a flat list + /// + public List GetLayers() + { + var layers = new List(); + foreach (var layerModel in Layers) + { + layers.Add(layerModel); + layers.AddRange(layerModel.GetLayers()); + } + + return layers; + } + + /// + /// Generates a flat list containing all layers that must be rendered on the keyboard, + /// the first mouse layer to be rendered and the first headset layer to be rendered + /// + /// The game data model to base the conditions on + /// Instance of said game data model + /// Whether or not to include mice in the list + /// Whether or not to include headsets in the list + /// + /// A flat list containing all layers that must be rendered + public List GetRenderLayers(IDataModel dataModel, bool includeMice, bool includeHeadsets, + bool ignoreConditions = false) + { + var layers = new List(); + foreach (var layerModel in Layers.OrderByDescending(l => l.Order)) + { + if (!layerModel.Enabled || !includeMice && layerModel.LayerType is MouseType || + !includeHeadsets && layerModel.LayerType is HeadsetType) + continue; + + if (!ignoreConditions && !layerModel.ConditionsMet(dataModel)) + continue; + + layers.Add(layerModel); + layers.AddRange(layerModel.GetRenderLayers(dataModel, includeMice, includeHeadsets, ignoreConditions)); + } + + return layers; + } + + /// + /// Looks at all the layers wthin the profile and makes sure they are within boundaries of the given rectangle + /// + /// + public void FixBoundaries(Rect keyboardRectangle) + { + foreach (var layer in GetLayers()) + { + if (!layer.LayerType.MustDraw) + continue; + + var props = (KeyboardPropertiesModel) layer.Properties; + var layerRect = new Rect(new Point(props.X, props.Y), new Size(props.Width, props.Height)); + if (keyboardRectangle.Contains(layerRect)) + continue; + + props.X = 0; + props.Y = 0; + layer.Properties = props; + } + } + + /// + /// Draw all the given layers on the given rect + /// + /// The graphics to draw on + /// The layers to render + /// The data model to base the layer's properties on + /// A rectangle matching the current keyboard's size on a scale of 4, used for clipping + /// Indicates wheter the layer is drawn as a preview, ignoring dynamic properties + /// Wheter or not to update the layer's animations + internal void DrawLayers(Graphics g, IEnumerable renderLayers, IDataModel dataModel, Rect rect, + bool preview, bool updateAnimations) + { + var visual = new DrawingVisual(); + using (var c = visual.RenderOpen()) + { + // Setup the DrawingVisual's size + c.PushClip(new RectangleGeometry(rect)); + c.DrawRectangle(new SolidColorBrush(Color.FromArgb(0, 0, 0, 0)), null, rect); + + // Draw the layers + foreach (var layerModel in renderLayers) + { + layerModel.Update(dataModel, preview, updateAnimations); + layerModel.Draw(dataModel, c, preview, updateAnimations); + } + + // Remove the clip + c.Pop(); + } + + using (var bmp = ImageUtilities.DrawinVisualToBitmap(visual, rect)) + g.DrawImage(bmp, new PointF(0, 0)); + } + + #region Compare + + protected bool Equals(ProfileModel other) + { + return string.Equals(Name, other.Name) && + string.Equals(KeyboardSlug, other.KeyboardSlug) && + string.Equals(GameName, other.GameName); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals((ProfileModel) obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = Name?.GetHashCode() ?? 0; + hashCode = (hashCode*397) ^ (KeyboardSlug?.GetHashCode() ?? 0); + hashCode = (hashCode*397) ^ (GameName?.GetHashCode() ?? 0); + return hashCode; + } + } + + #endregion + } } \ No newline at end of file diff --git a/Artemis/Artemis/Resources/Witcher3/Witcher3Artemis.zip b/Artemis/Artemis/Resources/Witcher3/Witcher3Artemis.zip index 8269c99d903b7803b0c0464ce6081e14375a3cb9..002796ac6ef244770ff4968a1f58011a510ecf63 100644 GIT binary patch delta 71733 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≺%xE90tv1rnTiDEEA5T$+jtXV#vIHazvtL{ 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 delta 89725 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 z z)W{K%7J?~#7Q7frEU9?HA@9#INzU6b8*O6)yOI112mhmp9abP6Wh#gzCCh8w&}^^D zPmB5Ah=5hJ1nxRH?w%pF1le^2s7bKeuAd!`^j;Ka>h?-ILKnaR_Qe4#3>5Yo)LN3= z_)uK*w5C)Mrq^L(IC55R=egFj6kKY zjh=*<-az25D(?QK9KFf4j=Q+oORs&%ztAMb$p08+S|5vVCDUfdl6_JsRQQBIYy7Wq zv({{Q(ckb;{{fAVkE-5nHamD$N>oCQVDFFZGi^2gyYY|l8!o~T(l1}}t0{~Pf>_jM zV`W;U@)(PrpmO9@tPfqVqOir(rz`FY-R~eorT0VIS8U5;uBq@i_K9b0V@193NZZ3W zJ73|Q`g7sL`Gs-=YRu#z_s<`?6OL+e|7k_+YAyb$s|4thNLny^^jT;f*Pi@6+SGU= zL`Wq}NTC5dDx={(O6M#DiL7USogI1%q!OQFDA{6eMM&xQ3Ma_B6i(9w>^fK-<%}h4I|NGofoif_qMfbvX9ZlI(OVj4B|SfA)k4JG87oRmM4EIcu6wx%uc!= zw>;>pYd;>E`;tWi<(Y@=o>=DIzV_!4m_jfK-FU?A;r5#L7_h2FK%UPD|{mVUqV+r|%$=(`!|nz^m6Ls|MFgHTw$ zV02PDSqw-pTH?^d6H(ej2et#+c}`n@dV%x!JVaH?N-@T+)J3$n<<>qHQB~DUND#{5 z7{F%8)7?y{y7i3gj5sNWo+Ab|4I?o~Xd|$m%P6aJ@5@BF29T}{cV&1(hK%M)Inzb# zuG6x#3S$1NXI1L>Ba~lsrcgr`Jdbg(_@i#W>eoK%Q0oG*rL2E@<5Fm|*ZNI!IqH6r zZ!u<`+TNsVM8SfVi^=@3hbSZ69^8W#N-4pHRE&e>WNHiJQAJL5S;f zlWw}MZLyLkkUj4{;%CG3AE#sf|K~I={4dA-|2W{ZxpF}=YtL*?ck7SOt>)d8;|W`K zSC5=kE7>J^%==Y!|2#IyENd&1QvYQ}DCv0e#-&ECE58tY)WcHOeinE`=D>Pq{n<{e;P%4Cg zZ5=d#1RpEUwK?!-q*STWMxU^1ST>FJz6NMwShYFvTXunpFlKZ45< za%Vmdg1qgo1V=Q%>eP)2%Ec#|ZSG(D9*F1k1_^98@02^?INdOVxrT419nbcRmN0cbyz3NEqbY!l*okhxL zGi@NyNI5B+&0`Fua(J(_g(uTCh0DB8tM^CB8?DH-ZBV74ejbhbk(lWg_KASJ*UC}t zya7c)g|oa`;>oDu@BpW_8t5(k8D5f(C>58-5rW;KD>`}#iy&WMhS*2%V`r6gejow_ zJ_rjmLqZ$0mS|=fX*1OxUw;!>*X&-Adj691N9y>Kweww5Hfy7I)NnDw!zlalx1trD z|Kpb*wpoiUB2Q)N*Lni?O@$Y_9~4jOInxMDIh49TM32{)Gxl9wJGEtW2cV@12kes; z?s4BeIXpZ~nNO@=wJ7b?fY?|-0U0%bcFw5eF20v-JD@1X~afm^E0Ye9&crNuT;|Orv{oQ=_^B%=6#ZZ=Gh&aIqpDv@ESsgA4ZdP=J3Bb zHu;5oKPJ_tLlPyOoYe4j_g0J941L{$PW_XQsK=3{=8zvGPh-&Ho=p-bgOV;l;g#Q) zfNM~z*$4IRGHM~v@x-KQhlB#6IMpiT|D61WoV0CtbL!nrEI`Q`1p+33FlW6}e2xpQ zlX|~B1n|gQHp7`mgzP4y@XIMbtUwYSa&BM# zm6X}fx-jf~2aJ_&ugG8P-AGRz4zz}FN$oOkwwYJ?i*-9aPx|5WZ%SoobY^fk_S+8r z5xT!4e>aME1Tzg@)m;Wn8Rwddr&!>(eIVf^n|ouvX648iWp1M$+D)VmX2cu% zpfO4ngVu#X%6d!NWftFpJqHR15j&7q%J+R|i*?LK5-2DtJg8OA_>#d@Hs3w}SpC|K zYU-^!D3#CpSrPF+G&~h| zbtiV+pHL!?(lmUpMoSb>i+STGS8|6jbSnwF9djQlWw=hXNs zdcxL{ih4v7{V}D5D|vTP;Ofh?GpIB~j29Hk-?K(0&@`uF7H87JWoF|iSIb>9i z*b|Y0e_4<|xMIA#FkQMx@RG1v+M)~iWW0A~Zn}-Q{Z$zyP9fmv*c64GTu?E|+11V< zV6ObU0VJz1)a)}6&?1!ZoF@5h+xKs&)Fc%R;Tv=X8YtnSmb)lS5%8Kp@7aF+(Us{8 zy$Q5y(B31p8zKWEspxYyHND4=O*wG&BqxG6{X!_}cf9ql5Il{~QD>NL7OJ|fk-1aV z*c0vw&`E2He4y>6g%T>C$@8-2kw;-+{^>M*1(0q6uG|J|$DS-*D16z~r${2Bzc`k? zbhAz2M0vdu5MDns5?+_Y4|R51C`$?9G7ouMY1bU4fn_YPW!RyD+8UW=$0IM&)?I20 zPmLHVaWwINo>{4o9yF3n(Bpa1TIc+ytQ61{!dLP6GWO%*nNnGh?$TRm~I*l(TPYHwp3 zsomI^uW4wkb zVRGKU%Fmxwp^!f^u{$3Ow_{`6V0ZtY+>M?~1|hvcLpZe%`^D(IY+~Eo1nW52w;@W9 zRjz`Iw~pOc(c#2zQ5IDTav63*tylCyA+5tcwLiYcxQ%_YI9X?XzB4WIp;eIO2ZRhs zmFctkauS!+cZ-Flo-{aJCwetW(k@f$I!ldDpB~HKxoyH}(Hr=8T(Ly2Ad4hV_L#&E z;zwC!yD|idUCE;OfJ4d3Dz8&_T@=oFUqX!}?5a8jdk3!%$a63&1>B_!>H@BEpp}#`q z^Zu;TX%Gdf;7sA2hw6@qZo0-$U%wz{yA^|R^A}^%Y8UwnxzvHBz4licuQDx^hI#iL zht$M53yjCzHnLEJ1&NFJmXttUsc)9sM-RM2+?&fWy>!`=OM@+KAvI*@z~P$RqAsy+ ziT|Q47e@^B=^ih-MUEMqw@Je|h9M#5yVTfHyfu!#FSuW`GA zpu<|Sm>FSO8?e39U=;3BI{!39680+pxLl$A`fsIZxoupHCX;CM#38>;_97>gdS*+| zn6^&nu4K60nP|$0wW;+j&=j8Zll>{V%PCdTwpO#+qvdz3&n{k;`@_Z+7)S(K3Mmx> zI^55X!>;ce$2{^DJw+6|1gx7?cG5?{o5o!McNjeh>DNBu#T3;My&R~q{EbcYP3EaE z^Cz5P^XwOj6tG*bLG3HD-P*X8_sVO=IKWO2R8xXU^q}gj@)80(=XG=C0%>P-XTz0O z+QHt13bU0@#h29q`qT3`i>!KmFlnVZjE>OEDVY6HR=sEXIRA~vZ1;2m(FpvZy!J`6 z0Z7L2o1-%dclz2tkNnr#=i4Ot^4Ep22zO{hb1pP9fmP$y;Mna$NVhWIuDaMT&x-EZ zqD8h?b+g{HI5WTwoo3*7G)|xpERfEv5^KGt>N_C;IgkVK#@2jgjq3g|Z7=$K$0oG* z&9vWU;yFn5aOWX6xP@1swbe`n=QBq|bUjmp8UEp;AWdeF;9kgxhI;Y zR-ig&Z$Pt@)nZI*3y(ejHsr4Hbht+IUiVW6t>4C+KKq7-dn-%k7*o0!v@769Qr%3A z+qUlM9vF40uukGIcR$8LcPs*{0=x>z*ddYlCMhSAL66uFQR59P8 zS520M8`zFDPfH-Ip(|^=m9hhEh3|W4^QomS0RAS?H4f@r&8PGeMoYC4PFarT7|v77 zZ>Tat=g0et<)*=`i!lG#`gX;lPwsuSqwyw_Br_g(Pgs*n>jA?5ITgn#5f%&1oS8(P zM~`I?*=Ba_VX!-&6vS&q(gvr}IjfFX%RzU6z5*683s$H1Pi{XC$|6R`Vxv-JJ@rxs z_OA$|djLn7MN>7~g`_=`oNPN6n@pI-p5OqvS==m9Oe{R8%LugntATf^82V!rx>C#^ zC&QCCm;;{e`t-(+pZ;r|+7)pJDug5p>p36hAL$;Rk0nCl!kxUMUZWKLm8qNo=W(~m zbmvrrb457z;GL;n4Z*}yS5+jR81?~o5$BuEPpGt&+eRC{ z@R{Ppk9Ot^O+elBp69YsESJkSP?w)&g?{_Z^jteLb2M4N@S1gUw{LKtFCa+9iHKGe z?{mOOT)-FO<*F)O7CNqQRxf>UkD24;#?r9{g_|5R8JRHq&!*&zFLhJ#I#n7##fF08 zTS8R%f~#evR+P49So-(uF&EkEYNVe~Cqfvgj{>RfsS0(5>%58_U$85tkkpyu(4$#H1 z^C+|mt#vZ2y0nIS6R96;!-D$=YCuM3?h(iCf_BLNKH4!-;yXOT>{9>}@vY0?uqfNhH}VhZ+OZJSEj;PY8yQgSTp5sdX{GA2%mk z=>93j33Em;s4s`LZAc0gS)bNpDXVd&frJI zg)DwX?9&aauM0CH9s~R@+p9K1ej3X9S06g{mp027Yk7@HNAaT8L)|Drq`j36x4sB; zDPZ3KY><{5T_}FrY1kI3WwG*FqN4MxrL#SH(6b5@64V0npzWh<4I%UkKUrvb`l}+U8{@=%=VqF3$7%`~Lw}K&ij;6T;wE-Fy!#N2}#E z+^{^ZvXjYu@z+cA!eU{wQH4Ek5~t3dKt}nPbR*VKlNfuML9Lz6>tXy4+7d z%WlO^@)%gl(rw^nT(=T{FCX;nIa>v8P^TlV1bMnTA3m~N%19p4>trC5HXehE2f-|< zao+=%^`$o5LhLeWyR5pL`TcR#0d4?sT>}_Zlc315Tt?(po^k9vhWZK)KvsA6?H<^m zhk&CiRyDVOT`T(^SR5Qj^*N+cs{Bn>g_gy}k-)}-;&JVH&@B@?EPpHfK0Vu$;Jme> z7LXlvCOp@<^VOZxWl^&>aua%z-JRi+j+R~OVo%W?q_6R%@Sfy60gS!fK2QI3-K#uFxh>c)Y+W5D? z%5rE!axT&|f3Gm-d{%WkvH?H25(B3N+9!;BI0jko%<8g3xc71>?5kqE!dw0YWY*mj z=OTB1tt_6h7^*GH!`!+E7joUWmoEUbJ&1oVE3cm!`@i8_N!&@Qq)1evI35EaAw2>( z9h7|Rc$VO$g~Z*9X})Us=b4zd3p~35OyIl4mJ1B zHQA7c8V%vFrcK)(BW8RJdNY>|npjm|es}i(R^v3u*Zlp4QW|H?lSKVjd z6o)_s{*wkmF8Li)mR&lha;*8JM4hU*-;GOL2xkjU(=)hT_Sut?GE&|@9CaVOc?R2< z3GyL27GX9$<}VfX#N}7rmhv1t=EJIEGkTh|-rV?nzHhGuCobIMH7m&BqTE`Hh8*NwpeJK3AsP7r z#A}wB8bz6ab?Xjb-D&{q@}Sr(*5;IdYMpn8Y%FV5f^eRPvKV^42a4;y6m=Jz&rm;U z^*#+8_t1_S_i@oYh^AVzJH z@NM>rvA~5JKi8K$n_u0hmXxo9sTX8+VbHDkriyvX~|2*+-d7>VdKe080 zMrvNnJBWO|wcNs4wV&BI(Z22bZfJE9M6Y7rzwAX8MmL`Vuf&)g^x(v1jYSE{75Ux3 zkD;#oz}rO`_d8EiHffqZ*XV%d&FsDQ&H0oHjzOC~vzqrYoUU=z^R+&I7WmxKDdK6L z(P2&;G1u*LBZ=i(&uoXL3Dek*D*Erhp2u^Bb6_<3_^$HS-Nq@C9(4wEq4(7MY&qmCo&=Q=?h`@y#*o|Da)iV12dyDz0xOk|(ySJWC_exUqABr#k;G zc92Tn$^hOx4>4=Wd*SlH#op}d*KNNYPo-(`~Gpy9>W~p0tXNCxYa_$%b zvtqQXOV}``jG#-$2T0klex>&mE{zc>dAxeI8FYGSKI$gKoPS$;<*HrO?-Y70!srqg z(~&wE*r6lDo9kVFdz5+mc4nY~aNg%OSKx~6YA&bn$Kn~yO-{W(P7^+-=gi;_`jCz* zt;*Dz$kn2d4*p6Sort@%Yx~(7>9n3k!|&-o_gu<`qb@A0k69w_wnp3{ z48y9@!wK9)Uzn~m_97k9o}u}C+?xP9_gu%LB(!&p9fE3q4uszF1At`+G**9$eH$L( zP4G8KerO=;7B^X5V7VqK88@}&mo}ruKf&^^*Tt2(3MqBENJjVOd|o|s8dib3IXfBp zgI9~vi$jmqMZz5V_f_5$mn7+AjBU>8WmU7zjWw~B6RgK{p-7;3M$uwzR)zNPzM}27 znfw{bxK0;;+3@fXYRyhc@T<3x_B<|Hwfjm{NMsEXf3|onlD0RVzkl32@Iy@xG=Em? z2v#bmTAyhJc93*k?N;(B(EOxrGdqpyzqKN3Xe*E)Z>6zmROF1iS)+C=Z`m!n>Iqq6 zyfVE?%$&``G3Huh^!!IQa`@zMraG&s)uNQO4?f6$db`?sQ7*)yp=${%#YzM_@?1+^ z5x%h)o6C;v_ZN;on{>OGz2aDp))nQneVsYFLfRe;d=D&~+esT%ZYKN>kz9C_f%GSM z6^Vb#@%mm)y{`Bc$*SrCL8so5-2$)}k<$-df^Xt2Cy*?j;*S70; z528{AApbUb*VF?Wf!>1pezUjtfR+aht4?Yha;tc>pxalzSD)#a$B7MmF-{!w{O(lr zmN$Am&wLV{)p?F(PKW+XD*|&JuZhpN15#t0Yv`WmnBC9OjHcWF)sgu+?mG59^Y^uX zYdJZ^2^&v-&V9BL`2P`K-^xv-MX84n|DsTazk@02NKxI-N{oaLV{T4T-b2m=Rgw^pdhq`POR6~_v6R7BYAn};k@$sUJ&V}TQKmHVf209 zTSwP^gI4Vk_=GSsA7k=k{F}ppnCH5G+kv%)xYhp(ehmXDc^V|%xn>f_U2NaJ3i{D1 z^~IyG_*!C-9itxUvCFr)RV|cO^*v-QFT`%Ee_n8;QX&BmC z*NSp3%KZVL%G2`8gr1L3#Hdo+qn*gS+A7-?Uz&GBrvOedZtrzS5EI&W`aW@g>7!l6 zeLP6UgX^R7xNU76?lu_DESEZe*~2?N3wJ?VI)94$&K_n(zTW>1e;nkp-w3Zor#*DJ3^OMWtwlE6I$^G;kzu}}Vup!O_ z`8?ei90s4OgtJe)lS)rY`OSxaz(sNRC%qN1TDg2`|BbR}*T9IC$6f74zB_eY2;SRj z-uu>r81F{lMwOnwb*OM}p819O3)jIDn8UUF+)OtQ`gmfN+BiN=?70}(9T_W_*?@eK z0Z^`_fmyKka=K4_m~Zmk;?b8%oAQ#pj03bY&BmR42%C(hLw6slxsKp}q?Hh{i*=^Y zS!Cz~*qOWzN+d1eh}a|Z!se|#W1}K1jEh$l%se|r#_@OTvsiKSb8%bqEbiJFKoZuz z?|2z;heFGGW-0g#o~4X}y_%;8!lwYk#DmGuouAJTyLUzm>QwfFu@rr>7RwRKHsE}| zTO5n0P5>Hhv_rCxOo6LxQ@B{M65-;IXvr%1ZpOdYfyB@2hyFK`|1~?@hmd+p|3*DYsF-*W{jW zPT_gLdL!m*e2-PCBuq#u$Cb<%)fmNZYYK#GaKdwGKF7W-cMG4*?;M@p(-L^) zHAKFg*mzZ*9nx%fo7KX6l-y0Z>btLn_z3nR-6i4KR6;zMQtAQ70I#NRzJ$NK#WU#c z@!Ry@JsTO^OV&z%Y!+Sr0K8z@-o`rC;a_nn4R5ls7da*SVp82MpR!4^n_q_GmL6B9 ztpyccsHbplugqmhVMmsZK3W=ls!EFO8Kj(;)aG-Cj@XwDWX)nPs!r8k&S7uyS*SQ- zo)tnHi(}crc?=cv;(5UIuUmOD=`WYIn$b=jRm5aF#c%rX z?8alEQztmKi}hI0{2C!t=#e0<)3!8yZmVKoJT0C{N7)lQN6draD4Fj3=6ASG7xUcX zvJcYJ>6!U|ZAz6#$veG?7p3&#TX@#dNp-V~8Fz`D`Lbx$+)quScs3y#Id+!#lTD&` zHiH=FyO-G|^tsWOoC6<#M1wJT<4hSoM%{~uHfbUUIyf+2NRtQaBsiJj^ve48d7a@F zoco*V9lXU%tlt;)dEJf{j^H{&y#flsyJZNU$c{OGF&kN35A-;U`_?PlFILn#AT(0W z2%+SXgVXrPokqCPWD?}`v8aJ()-FTws@&>uWWD6Zr!tIi42tuor)0dVHmepH+X9Y+ zwrT8bL=q>*1zGvbUn!jF=SDv@IeQw{t-iM(2YKAnTZ#%rWA&Zy?MLq0y<*HVr0xL@ zd~b_?JhdDn7*#zfS_`Z7#G}4;)iL>}T_LH4S{RUbVne2(X?7R7gL58x+A^{Q1de`M2t)27{X@l%8zgY-F)nP^Ygs5&)k9X(|n z64w=Dx3bZ<^ckqr(M4rFud}Dpji&Xx{Ti7+hBJMh^gCg1rb9S^0}FjT!p(5#x0Y*_ z5YgFFveEgxRC(buiz{&uI=zyleWTy+fpmPCTmhHddsZSq-InVOTtW)%w_7!F-XVW~ z<{B|yvxEa{f4r?c5@D9giuc!j@X;fW_ylkJb(*lI>qV6xJE|&ROkh<%vC3m~JM#A_ z_r6uFs=!NN`+K#yY`o4(C$kbz4qowo=zF!Uke6cg zlgsQvs>bTMzfE^?oV9LQ@ydUu9jyv~5vzowjgVTU;mdJ)+b+4cK~a3T~RwxxkTD`(kIvKJE#6Li)E@(^SY;A}9m>lWQdAwCT6Y*LqQ3-X9^*2{NxR;z0`w>dAd{ZCm=XYqw@ z6%S_Gg;e~y!+dM*n1zS15A*PUr&8fin*7LdnabMuEF?%jgJexv0d`VA?$8(Zk1#(Z zCHK9tTEx)$n)2REXARWN>9Q#X#YlO3DC*;&Rzoq5=+CK>1mAlaDURQ4ZO$6-mdWXZ zqObPjcwLMt-DKTb49G(L#pdIj0rw}K<~(Jlf|j&%q6~2XzkPwb z-ANRaYTR?H4G*o%UP67J&-Q7&Pj2B0EfP!1S_#cQ>pD`pPqX>k0R~b#y-4ivD3Inm&Bx|hIL+68 z*SP0v)38slE%%hAuul*y2lsOQY3!#Zs1eBR111>_p}qi@p}5@i1u%Cmx-8#RF zgwbx%vvXh{Rm|}l7lWD3+H9-D9*oOf<!nIk_3NF@PUl0l zS#+x&;@zrN@|wyiUfOS4McKH*+CAY_&dq2jRXDUxq(_#2OI+A>_A85b7uGg_UA=`p zMvMFe{7S?#&n@4(xWm16J;|3z-M7^&w9Zq=Gou~+Z1v-ubvIcjt@1Z(;jZ_fjA4a0 zpo9MDea}7AVVo>gI^nvdUY%Lqy~mN7DMsKt-$PDeO)T4K3iRbTE4|Fg7#pl?Q61w#2`J}ZNs z**adJ9fN!K{26j&oP%ioZ8Pt?^t#vcO+Dt=U)|AkU1NKqG1l0R`+Wg71>R`mD86Ma z!MGispfGpk7-npK=Nv?HfWJqK>ML0$M)_a-cOTDx`Oz(Jt60>2MtqCQEY>~9Eo;4{ zObh2TlzW6wHtA!vt(;l-=(BmLAu*b@{eF`T{+Z6i2ac`;8bV zD9Xlv>F`5~9gKwF{6mgQF`Fq%Eb&o)^cJJvkOTFrNWuAXVV)$qj3D&+^>FGrK3P!u zuySb?ksf878*Aj$XJQ>gu9fj9noKLBE$kI`X0x zQO}yj@4`ByrD-jz+8SEzF_w?s#&HeZ=<#|KU3g-1bR4h1`mB0KocjCS)gY3+PAQ}7 zjkTJMz+;;3(}HgoJsK}6cAc@~3P2%f_K75KY z+`-f%Fmf?Q+3DwIHPAeDAAuRc)N$DYDQEyz7~+uK;-aEb-%qd_+-v6lv3%Hnu;03U2Az_y^9CRBTkhauc5&7Y zF5hoOKV{zB&q_l)o{3q}xD)JgkQ1dQ<)@hTMBDnHe{qf`8U?0c63W4iW? z%*?KXUx(PQOu^r9mBw2MnTGu7Jieg=Hi!#|;5e@!aup|jSKbMwiVs4Y#Z!4{aau3u zoY`*SJnLv|x%IX3sk70=x+l17{8Ft~&<76FD{dnrIB50v$==$@sM}!dyD|>HV?T9S z{q)H@pgg?YPl5hE72{fe`OL4P5T^`X{wXVqo==Hf6WSLUBDa+Wr15CdV$7Ja@vyH1 z%Kv?pyZ9p+=x%^#$KFM$<#l2e4j*}zx!RJ)Mcq35$#}j%6JugcQTx{>;UpVFfaKAZj8RLkljr8c?2^50HP{~pp9Y; zS;u1+H6A~2`tII;wKSW%&$u^^e#r9Z;{q#ZT)?g%hccFbzDqv0lDJ*$=vzx2{ozta z|54D$Uqc%qO)MXd+&c*=vU&9g=gBbrFI=S>%uA2^+IZ~RPG&WoC&3Bgqx=kaXxzGz z>q#R&)`!T+~)|sqsqkBEat0(s$AxV_-N0> zp^Y))8H}cXHmT(8VO4MEJCq|{+4z3Pds6wX#ve$2W*UV~+6W@x$mhQbbmu%GG5!(e ztTvs4@AW-fwKc7M{kY)xq#qaN*9}Jw>!zFviGEz2+H^gYP*yD$wEArkq0qF$gXuN( zVJW!VR&bY#BSSZb0o^>Z=XGp9=kYJ4{e;e~+ZQ^2(R|;QwHN807o1P5cy>{DZ|~VC zaEet<`@W=oRf`Oj-83nW!2LbnEJviH?Xq~L{#)mamp-6yn2>j z4Jo9<#b6I5d*p6h8Z*E1|6)?webLo$dEEQ&Nsi%5*#6rur~2b_=w)nhOZ`b&AgAoZ zd=xqL7UGBo@NSCh>hZ+Hch7-Zz71ZBaHY6^?ft4&0P+Y!Q-0oZ)A-@=M9LYjtv6_7 zD?yc_b9c_XVV}bPWA9m;o5+=&ufA3HKN!B)VND2GxNu>!bweJII?KXI0@T*lmdnHr zF&mPU$3Stm`0v;1R*$-+mPTVcA!iSjf;>DUwOXz2RzIg+hwKoBypv<#*RpQ$tlY1E zG=Vx#`ZO!ntJ!(&7VYdfuy!&MY+i38eV<1z)AwANyq2We&tN^7GLC*;<)BFZTLN!A zwl|h@@bd7i;ieSQL^AbkWxx9Y)m(-&?0hrId|I4eBkH^gJ$0TF@Sm=kE*^74K69#d z>h9U~5i3MrBe6sCMO%yk6Yp!|B+h_;&Up8TJ@&VAuv_5y!Z_?vWF%f2LWeot-J<@C z=oqDr`A?ZwId&RkG*Bv1l)n4BkavP8KeSzpS^now-e~e1;WIx#j%W;iiC<%-#Trpz zma5#-jewpoKa>3P;colm`8&Je(M_|;FzzRZsrMD8-WNXwEA=t;Ucr>am7^Vh31_?( zv7Gbw*&KfayfvJqo~l}cveMC?b)7^Ok&!W-aZ!mF8?(9o8|E|Md#7+G;&))l-6=x5 zgwM;2xV>HR23pQzoEUEp=y9UxJl}u(py=21<~8#Xk3x^*G4QZAz#T7a92w`o^-qVh z$l3&(0OdKJK)~}svn6@J#KU3m)5f4mvl|G z4iop5go%4sf{A;Vh>814!o>Y6!NmPb#KeOoVd6neZ&~vM_XpQ;vXyM?JP_;V^Knh( zNoD8n&V{FkOTyE`E1{T&mqsz`#rje(v0kiS0VZ&k%fiHiC1K*hm0;q3!O}6|biN-& zZcYhu9^s+)_)0XNwy!n*=s}X6|K8m5+-o$?{kiA4pO{CpCHR!(Fj~$HgR^HySelPR zJ(Z3X%iUGlnU?3>cm{WltMj=(ye8|)uLRSSO3b@)nm;Z^tkMZ)rt_=^M8^EkGvoTL%tJA@Fqetw;dI$N8w=l5qA`E~ZwFVJyt2V#Zx zp*zU0P)mHbs=l=~yJJ0{tz^?i6z$C3*(`pGdN=E-$L$hT62TptRQ0B{tPxxjugo>E(qtzT0{Ie%L%-pTAAt^^9%$ePXMw9$W8pw^$K>Fgq&Ru{4Bxja?f4 zVPkkp6@Jsf@mXwVv>ZJB+_5&P2UA!vXjyJ+yWIMwQLjkAG|1noq|zFM%qUJO3|b2RPn*A-pQ3 z=(gc{akgRJFGZ=I=#Hfk-cg|g(eJVXH+t&EULAQFX*uEnbut9U(WU-Y$UTQ?|-RRk!N5bJr(eaMD+Gf#bxPLKw{}ASp znCEy$+OCTzgzG-agJ6d%pN-9+k+c)^Xz-l^=UGnFmzQ z*X1~=OmdBXi@Ud9!bv}gzWst_HzE(-sq|JUi+I*kuXti}%qEp~HO%1nW;U0-=BiL~ z&!SoV%i;~=Lg|=fw<~GizVAuncettII~!DW>Nvifw`e>1MBM9i0(0%#+A33z1Dhuw z!D)V$d`|O2m5|Bw#NJg~@Rjxm#cVwo7cyowZ_llNCW)F+S)2&3ory6exrgiXxp)kh z#QQ?4(INO4-`QJ9-Kj}q)o|o`7}6N`ece93agIIvSF+R@*!fdwTnxN7W0oI>*SdAz zC^jrL?ZbI?weIpf?EV_l9`pFh4j6_*fAVR?Z~Jyd{mSOTeajss9@x#!VfiY4)A-P3 z!pa-ieVvE$17;?*j`bMhs6aMhzWk zl;T(Y#S?HSpui{;-_GIJufjEc0R3iKRh-(gx*ng$RYIj_dtSuVUPXEF7{MMyMGshY zT^Y+lai6k=ZupEG<4N&FiB;s$l|L7~pi(Wa+-V|urP3b5?->4~ojQ&Oj$^{7tLET; zKC6pYB7ft&aZ4IUlav$EjYbpNK8Jgt@0`9@`Fjy!KZR}0(o3?mE9hZGS-b3Inink* z(^9n^yo!hoTx~e}YPBjJyAjt5wGrwgKo*I?PRqsvpx`6hD~nUH>nwHv9CvDE!Bz#~ z;uVcuMGUNSe*mx$#g}$GR~fIqZXce1j*nuEi&rQvl@1s~hY?f7ub}Kb_f0Isq%rDXZAhskukxR^teLkp2ld zvO6DE`Jhp_PUxs{2lg2CB976X+IK;ZEeWgAtU(1Bq{3;S`*O<6f!1lF-v(ZPosKse zcdA)aI(YM+ktX!nFefH`-_lO;eEl;*xO+DK8tJC5%$3vqEw;Mbc6E=v6-lao4z+?! z-JLbT?PzSO7!<;m+pl;fYXrP&jM-bMdnfyzSRHSgB!-f$1xyh$djrw))?lK z9aBf}^X-ZjMqu~DngfysINpA?k!1f1*;({lh}*mBo}r&8P6LF0i~VXeKKn{!oks!p z=KKlkwbvuJDuOfB|o1d@rZNqp2BwaaQ4yQ2J zyM}6$P}C%jzGxjNtc-~3KL;)7D5J)L+Z){REzw1Uq}_;r>Dew`xFaGoUH8{c)dyhl zF~?OW*G@(R>eD&84mrQhnC%eiK&Q+vlp4cy`Q0p@h#Lss`;?DzagR=D(T~x>2D5Z7 z8uOHzBR@lKV@`|QEp~Epqv}Ge>uX5k7BzJvSK`slZ4r?ilR16HDSAa)jZgM@X}X`` z{Jf}B&w#MfWaYPYBKBirV;xx|p>|O21a*jjbXTQJ+)Z((b3f7hj zZ|+(8YQM+xhc%9N`8(_+U27p($MvSmlXV7SU*5UllWZ*&ymCvp9nL~hYe+F!QVD?jr zbv=TA7(4o_%U3S#7$3lXJcP4Ew*CvCrBAS8v{w%+ypo^d-1}=9#;MIL^W|84UvAXw z3(t|4R;WV{zvh(se{*!wNlL6RfA*%`KlIM`84j?|3VNm2Gvd)w8Om;!W0sAC-h{s0 zJF{~)vv0hSapEq-FB^I3V|(>?hwdTlmk83_oo8Pxt>Vt$nFVAvpemQrz|86{g z{&&fJ^193O`y1`*2rP6csVBAu&bX>S)2-s8wDCyK!#g$cIp>4hs-jPv7F$ObZZ+6x zRc2NaW$=~cR#edEyBtcqt5V{(q{aEEu_eCbs9(L`i7Kzlx+Q#d=jTRsihNts7&?f` zDqx((>Zsai?QdaZlGdAoW2SDM*8_WhO|#O!bENCVk$(Knk*>wMC4BMff=+w;6>h|m zU!d*xX3bM!oIq2n6;`<;3!BXR!a8+)K8j_){luKVm~AC)x1t@znqp>rY0LPaWZe&J zFGGxzJ%Jq>Wmo)}hYF?FKc27PMrajni`6{q89uuqsy^{|VvvSWKrWl9sAG&w`XBW%Qjbd-I(XgGja25G%YNOPj@bNv!eMulY{VN))4R z=Lnuv_;RtcSTD@QmCGJ_h99|q^|+r(Yr#A!PSm~NyDi^ru@0?nueRWCPDeC+!M(vL zMw`2==)F!0adYe5q(`j&D<8pNu2r<6_j>FHg&vk+P=$m0RLAfn*-xlW#V%mo#MYmJ z1VZU$`v36S3mu^)`!MN{a#&G2pTOR~gBA1IaAc)N{XO>7s+SF5KF8dDiO+VOI72e$_NGK(sQ#cBiYe9ULUO))%myz zZy_PJTI}mvtliP|qN@+j+;u#0^Pe@Y@qIsQP3X<-nj*H(fIEotML znjeM(lt2k1)>{SXh$zFwdbDl)>z|e75-H*wDVU|^KNZyZ%iRk9jWnJf<+1OX? z8<6T`y;o(z z5zhA-=zd(hRdEqg$MUh0nnCX<&fk!|v2R13yLswo)_MGPva2{*J8xKD^zugO8RO;B+(FPb!ut5^q#oGx>dZf zen#0HNpgin=b-Q0{-k+@ecvdjmSam3(yGlX$Rb}{6SU$gf9Jf{63}AQjYc(hGQQlC z?c)E60h_&SJW7*l&v4QgUIgU`=)j< z|Er96$n{=M7-i72smf@S7vOIEj|COCsu4GYS7|kyaznb+XtMWjQ-`{j*vZ%}t_z-! zk+`$v_#-(-Kd5$y_lFz>)pugBg;`$DtraA!)+;${OVG;(oPGM8aA>lCb^xXiz^&p? zpkBv+>4m)K@&3wA3*wO6u&@|g+$+%KM)tmf`~zzkeXw}xZaDUNy(eBpbcpKzB zb)CFF!_S(aWNp-E87rw9x0$u3iH33rJZ1`ib-O2U%iV;tCgJNA!z-Ro8-}T{4>Tq>1pTTkJa~cbUv(3DssX;+exWo zr0l#peQ|xDDV;1L@mHeu#|wD#Q)S7UCNuY&oaZjPQ+4|izs%iktJt?4zD>99r{W%bHD57Ht8QK00*-WCOs!?|z}o0s-{P|33o+{} z-E)2hT>s^r;-ici9yiQU=9yH~skeTXu0mHi(DD8yU4=Ukb=@j%d6Kxao`=uC&HRzc zx{vlrk{5qMOxr%gVJWO=hR1V1L^$eyXW#^F))CzI{ZT&f)cPOHC+ImF7GFHuGlvUT z=Q?R>{iTM$mFuwDt?tL+UHQDYs74%EuSaQXcA$jUWU>DaYTy?^zTPWQrPcRX%Y$JVIiEhppMtiqZ#_O2K~}O`Q#uoL0wauU_XN}d{JrE6Rhq6#_Ukc!vr7Mz zdGfe@8{hl^?Aa-6cFxFXt0p}Msn3m>cYk&(MA>r8YDNq|*Ej{tj;fQ69>Nh=N`0sg z;EZoUtVEMF^eX6cI?v?be2+nCcFoNoZ%+DI$C!W2J{T9rtZN6^8gaCqdanIB^jzJi zpgp{WJ#l(}>Gc*eG($BgWqnA0_f8j1-p*_l{VW+0LYTf2wctdAw&edV{wp&|U}SyI z<`c)0o&2Rb2^eLA7DM!cb}XhlftT?u^H-0o-F|58DsB$Ee>9Vnlttru*@m=p*>rQg zX(mQ5x_2R0!scQxl(uQv+FoiETIX#HXVp3Sf_55h8fO`%omls)5H$HI5e zRxQ1T&d2*tA9~6tH_yDSaSMdmL5n$N>oa(dYjE)ixB;iE_`bYm&r%fDw5r54g8COD@P9l5 zeqeZ39u&tcBC@4_t`IFyX5Zg{tsQqHmL(xaM_VKBcG;xge>><>&EcZFzGF)lzs_j& zm)Tbja{T-^+VDmB&p+O{3AQkfrn`g_pZ0V)3Gd@&9puCd8<}_x))P~ImEbHA1Jyjn9jWTMGBbM5 z&e_u`?F9Xr21jM3nQYm9d*{UmLANj2v!Q-LXQou+?03XSuCA*V8$X(HoH0OvQZ`)C zlf+zGU_&a4krILEAZQYk@8Hr_@F=>vaD-v75^V*kI0GGHQ?TLbQG|~#RU`HFyk=*i zz9L4-KVam42d>}<{Pk_?i^6-hvp_bZCzlto4HE1OYChwtuoFDS_}^4dpI<3Q=V#kh zOxU=3Tem3U^Z7f2Tu+0nV|5^ou};xb>4C3+cD4x?rT>Mhr{g@SF5AnXh|)%e9(mM8 z&oraZji7?a_1#&2#W}mH>$BP{Tk7adyBiQZ;Z1~p?;%Fy>im+P(|KvUzDNo2w}-F` za+P?hI|+D>VUH+UgyOe+@AsfMVw8Li9{e(rjiW)ojI12K@|V`WdtRJXSfDdXv2NW6 zmdQt`OTC^Vb99U8GmynJYd11pU5<|iVScU;cH}_2#+l+}b3ebeaOxhZIP7&iiM(`! zDr(|?w2HH!Ix$1gw?euhOumhB*9{xL6r&1BdH!7ksei4awx6QUtQQZ3FFmzm&UPPC zz*yOD|NZ*C(DwPOy07}4Ojs;X8!GBdAmRo)*176XyAbn-KX)OT($CR#aqn(L9C}&5 zxgEjo-Qs_|6*V7Yy||a)Vnlyv<21Gamsz@hJZftqtLX&LY}Z$|Vpjt9S&nSwcdOM1 z@7NHy{v9H?kn-^RyJA$cALid5(OHkIgu<8~SrsDo4)15;`1?je=Z%fzW38R32m1O6 zzg_X$X*amRa{PDU#G-v$o|C>9(Y)oV#yUP#zkj`=orOExC*u{wlQ*&1{(Dw3b*!r|sbRLG6QQMx#y@;}kEIdpV9o zb^R}x27_azILhVPs+(BMa`fj#EoIH`F<~rP=t-W`#E)LEoMWfaP^AK%3FY?{N6Uk} zM>n^7p*G=nT`J*RxX$#(8u%sHg?(Fp_hSHWofO^dxXpKfml5P#yH)k9#bFry^{wS= z{@-2BK-$tYwkLeA*j~^=n$jRzK0RGMMUj;G`_NQuMXlxwk$?-Xk(gxM_6iMzE3vPV-0MiRcbLsscK9-3za^h`qAO+o3mAZ zffXP%zAS#3{q47zZnF>hs=v&1=fmp8k9yGn?7sa#+wQ|!@!7Q*B9dwI-M)%AIcMLxl zV4G?yF*wxa(p9~{dk@ATT-c0=&s83V9wm2Xv;0bt$rsv7sQw_{#m=kwPT})+EW2YM zW(&un)h#D-=q_aYA15&Q(RSKEDnNSxeO_97lv$bUIx`Tbc>+I8s>rpC%0`p-hLl_f zjovhIelNkgYOH$w;+3+0mbh)ek0B+hbD1Fh260q_;O%&|pzv8gP9fxq;j?_CJ*eh& z&SC`O?{5b0gxo%V-A<St+cr*#+9OCsSbR8w!=WoOi{Mmuve(>q&uyQ0HgZd zKV4g^9f@r*yZXMy?l5HV#q19I(C2`!gwM!10Pe;R_|t&ZWu#|+cJ1f!)nG-%J=lSZ zD$A`~VW-FHM4Iz^YW4h*c?b+2FwY^s>H+UsgD;fxC^2_I7kCQYGqJMkCg?z)s-2>` znLmi9cOaJH)YduV^`!hK{hEkeS;o^zr&Q zGA`!VB91Yl`(FT`j0&X{IRAY*>eWSz?#$?MAFSZ6Mn})q%sHqk%ai@@Gwf?GQ7PQl z&*C-2HBa^XYG2+LKY7#`?#}xRkF>-k`jz<3bOZkP`)u5QKfsIip?D{xW9qrruugxC zU)A+Wn5ag!tJ=e>Iry;ZI=()ptP9}cwDyW)$O4>i?9ewylE2G5)c+{Xtb`?R1YvU1 zo>Iy_G<%=38rdZnN1{I1n_p3VOCS&qeTfQOu!v_6e5?d{fOhvd<=&pkN}JuluV zy|3yjr1N=yhPG-R>OuU;I$p{BedhPLS5ChJ_hH@W#~i}_ZVd1B?P)$z>^XcsXN`CZ zlwN}D7`>t+RyU_q5U0yU@QEWY77V8$X zu=#6~E2sV%6t}0ZzKMqAKerD&Oj>^=M)NTtSLSMeIi0||&x&av2|E>K^^1~ekGVNL z+qQC;IgwwA=b3QEvwOZarsq|U^MohTtx#I>`o*ZqOdD1?NBQ$M-^uWd#P3k?%sA)$ zaXF4{F9T^={Hc@R)gfPc>sc^bAe)^XgI3vP_Y3^uw~kf&vo2r1E|_(q?`B?(XKa>v zEH!9<&;`yuII=dM(GOxwSJ$F_N|Ay8k+Pf5_7|*zA zwbgCUYPtiMN%1U0rP8ji+4C2*==&O`&#@#-WQ}eo?EKd)*Dke+xHY~^tcT#Ow{Ulp zaHK5R%`MCGaMToZGLG5&=DevgszZnawdGrYs^Yy0R>Q=e*QQTOn3peA{L zXvqF|3N)PRDAOy@n+N*2PpZbih4Lg1K-QrD%4C_~Onq2|l$HjzHQir|o+bUap(-T| z03$~pp6`2oSO_;(D#BCv6O1n3@O!eFFb`Qk3HH!xT2>WEvwNOHy@ej^?+JLEY<#19 z39ZC6YumO^+mhM<&AV6d#O4A} zhkY&SI6D$qG6emEI7~a*VVQU<5BcKQ@<*=tbWT^l;;Vlhe72?aDfgMLhvzD5UKVG% zk?eI~JvFj8(^b;tU4U0=iTUZwTlic!8aRAIM+zLMzFic#5L%Dqbn0yp#*hDh3@wUP z%FAcF3vIK)oV+*gv3Tv(Zk_=4%DwY_g?E0a@Xn7F-q{w9D!sK`>8%~-ExXo})Upm$ zzWAZ;QQ}rca;cWaEjGuPPenI&S67U!F2q-S=jEdh?T-E7g4~gOCiw)7f&L8i=Zv&}a}1ch zhGz^MM5XY`WxT=T5^8qS?=@|cd_$|M# z#c_##P%~$qnyb*6R)^!_^)8!x5dt@Q{a-)^3Il5;Gs3qwSDdV zu3yta5C0JSBO&YHmGa(y{#24Sd5a zsj?#Qx6zN>OM={ncu!*^bS4s|Y#N&Hair9fF=Qjaxp9sTqoB-n#TjHq07fL)D_AAh zVg!lldx6j0eIDldH_{G=6!PV+k^sr-8M?ak( zm!cJp;k{Fp&F~jMc?JHq12I;;R80TQwyIy~zRfk9yCUKIySgrU*B6(yz#sxEAa5y=Gx%O{`gx%3tYFW@wy%?2>Ev zFTnIfX}oWB@4(jp@db-OZHW6x)z(m+=VPY~{*=x}Z4uM*W>s9H&< zPQH4%owTF&QaNpGcO@%ytHYFE+5OVBd>7|!Gkcn5z@u@DeqiK4TQt1!%kGqOc57vJc{@^_e$rG`PyZ;Y5JF_ku zK;DmnUXr1X6ATjBQXKD=Z+TRf5gYRj^(^il~{1EEg!EJP%nn&>pJkHgNC>YsYb@LY_eozhs^e_1(}c521bYU7g3PNaD$PrD0Q-?Lk5 zX^i0*Ppy%n*z$2pRVyX$bUoJiZ|4@pR6a`7g^gP$qGNH?0y#2KG zN6R!+A66-isrvlQSkL{6zT4Gt`L>tKxs>2sUJk2-x#Dxi?gqQO9kgB3?e%8pfpzh~ ze~y<2R%KCGw2S#_@lP`rODK$~TLsqmd^Zte_46#xkoNJeJQpsZYUEVMQJc=f59i-r z>2o7v{lLp%Xk9!J>lNhmmzr|;C^Hp1KJ=icWz)$+YqM^2d_^4UMd}DW)17R5`U;(T z&-Dq?WR*inM=`u!KGUob?Qu18Mn{^&f96QbW}_Rndv?obLiuU*FRwyYROzosKW#;F zlKc=Z8V5+whsS?v)jAD#yK!k_;3}4eWpv*B{tUy8F{3VQ)uPPv+?}pBJ^(mEaK@{L z;lKEK>xTarD9xva_=l1g(p&Qr?57#Xw^Q*!N5EqLDy;g}gR=)~2EW&D%C~s$f68-x z%kDw9$cA_JoBXo|a8|1nL>PSwrx8}iAzb+@=*xZcQ(}DvgLN8zQ&&Rn0c=@dMt%z6 z`l_8mebLu*(^^y4_z`0F`?)omULlLDFC?LTWBRm=7|z^XUj=u(=6c;2r1MgK_m`FP zjPl-x1M0eX2ssW?iWXzRr5Mfkf0RWkq|)F3WKAXp)=0%T-@2u;R(5;9Oz}&dIk0tW zWMWmWw=>w!!FFW&&Y6l(c2x$Mi*LWvwX6t|<3f92tfFVDjvL~3-apcgvGDV}ExE&Q zZzkXx434p5uc_`Kl0DEs*DTa}r7SfrRV&KF(l$xK^#X#HLN3BTb_r|29u-_M+S!fQES z`?a#Lqi&5J4%tKKNA?U#e<6-7A78}qgCSJVKh}`BR!k*(RQ@?XzCC*J*nBh|Y2Zr0;_B9}l%HB1+|squ zMP;WeEo1V2)0){oerqW!92K5$<<_wed#2||INLgKpY3+MLuITte>7jadz&WBc~h-~ z_#m^oFv;C#-m%B~1X?z~y|*ncqPK@&#mUq;Jym-8XLW-3Ai@OFY^vJg9I$#34x|JK+1!^cV00PwVxc5QRswbgsl zBAykTzGphZ#GWtzf8?d;Ivi-9tEQsbv&d6%je|6eFs1VEe}I0@pFmf|HB*@zf22)?Bu~g88&1wyd@HV5f}0)?y^wP{njjf5j{ZE&5LOL7PZ{y{(}p zyLebriR1Q&LDdvPHZospp03xChGPEb_GcmzlahFpxSmDE!AL_^K_3gnJFGvZpXd3qduwl&|2J0AGx?2l>gK7U)8 zb-5BPPZN9Jpa*_C2kTk^lz9!L9F*>SUy`gosXeo&suRbV(qO1>wjB*}0FwLH+&Vc; z-CL!t2d&*Xscm&qj$Uj;+e=gbfB1NgPgJ+%ddB=6D3*y?;2g897~jq^ z7aw$jrIi0P`QoAUmleo!j_bTkw_%UpZ$r?@NUQg-1577; ze+7UyP-Do{j30z64>s)e}dl2llgB`IDs2l*FY(Js&=fBF76 zn9H7L;??};XpZCGywBY=O$2qb9@sd@1Jz<4_5Al_9h-s_AA#I)`QOC0-4mBdBZT6} zQr~vHTdxz}3;DUe1Ww~Lc*3Vn!vk3ZWQd&;)OtqSqyW&9$yh0(u$e~TF5ty+x6 zJ^en~t*{5TAA8McG^RTaYO|q{%#A&N!*@qI);wh5**nUbJYso+HeI_wyP0~P{&qH} z>_)J5)1;V@y{AmpVOgX1oH#9x2dW1m^R~Clp%H$L|ATyxz9v-l;yyt1B)$jXuYAC9)g=6HQGr^WL*&z``^^k)8-ccSBc`ey&Q)M$u5VF%uWy?F_m z)Mvf_PFW|=Ds=(Is)o)KD&ov6&(R@Nk+}xH9cE(rWC>+l(;3Hez)THN@Sn%oPJ zr5_Z8r)s9OJdXib0<)66<6Wv9sw$|N@@&hx7#)b@#QH`v@X>c+EbIb1Rkia8;`N2{ z4f7sl+D~e?5l{^?es?tRLabpF;jo7j&GD zkcla0p!kBKfAQ$8uF{cu;#9{_ZPbj*qU?y`B`-$I`Er}F*%rj-+^};V(XUuRL9Z3&QB~~G*hynFB(kGw*5prc89!8@01T} zD%y!}ywbf-QhaH<0eA^U9x?O+Vp_49-U(DBG&ilce{d2~jAy&>&5rf~W^9e2yhO~B z=&8C|Wii)Kv0eudE5)DPUT_YM>(CrKkvXbiHNpC8J$F;QPs)NFT686R z*>8q4&QR_{{&v4n06X66WyQM3UOg|LQ_tt)S={AjjYrlkLiP7-bp={}vl6eg zYT$@8=*~d%zpNlJ=$tAOKN7kzgrVSeA2Sor!Ygwg*>_G?0#%>EoJP$+-krfdfB#Ue zJ0Ib)Tn@l0w(nv0Orbsn=ZP{D&GSWlE4B5afBgE&&Xl&El)8JVkxue0_w5VtXOM^M zt$31))W)poymj~zEH`LL=I`V4ui3XI?r9AE8Z*{1qONccV-(e<9`hyS5RvVe-fJtr zKwnUG=nPJ6zutkj{S0g;11oCH&>&U2u=k^}Kf85caW5Z$ZQ>fd5%LK?uNmCL{7n4a ze<}E9ddkNY8f&*=9ik5Kb3eDH6OiKMgL$oLlVQ$4v(PJUutvPQ2bha-Z!6$iz;DSn znD+(H3@!HU|FO?&&Mx4P6z_wU8sCP7ztI%yxwrWD>khWa;$=e><^}%TChIfZ5f_T@ zMl6@Fm^Ni)W>pRe$-+zP$rI>uI*V3if1x)=%3pr1-c5O@2lSF_c~#r z{oSX?W3oQK=e`x|(cSB(`2!_~=;k4zGmLnuPNV_wdU=}Biepr3CfZoz_BFaS zWuB2@v2x`t@L?M~)jYz=cw_N7@kZzy?Q!%DRF~i2vDjUp@~T^JqCQ+cf36jm+bhB4 z_5!#}v*V{@+4kpzJvwz_`N_gqPVquLSvkk1;qpnXxZGI@E_Z6-*s7J5%XUto3O^=C z^#$Oer(zSS&d-tP7upgVOS8cHY~BO~y_bhJ7X8CV9ZZfNNC^Q z){#V^r}H_o+ff)VRRj{_MNoP4QgCT*~{y%egM}M*K1V{6ZW+ zUcjFgZ~J4e2^Ir^@x?w8XORf%yi9MD!tzc9Ebr8e<(*rIl5@a}9B6RcSNz5Z{3Nexpg*slAwZLPPi zQFoRy>f@!1`ozoae7icM#Qe^^i2R6Nra&FoxYd-QNda7eP#nKaZ9B^kSse@ZUEda?%l@4y>^@g&mA z$@!|8!yx9Y^86D2Of#@$k7}7GY#Xt4E#C;$>=|^E=e3Po?nvJg?mj4v%t^Isq-AE& zsQzwL{(d*49hf>$ZNM;|7hHd!??X}WCO?eFE#-G&c%{DT2s5ZdUd48kDmey@9q%{D z-sRC(p%v4=e|fy;WyO*6Or8runrA~ay)*2=9s8eR8-BW=-Hl+E4dG_rRT*BA(<#4e zOH~|_`R&i3zJ{zY`z!F@33&ETbnYtWFuLI%L)0H;wM+fNXcp$1HpGL3GvobSXXDJr zcP_@Tf`pB?aKwb9u&)X%#T146uPhQP;_4_}kXl#Zf8p!G95bL<7kU$7mkPotk6ZZ} zdfPnJ9{4&T>cLJJf|N#nzhYFjqh}t>DZB2)cp2}x4+{VKcQwy`C;lC}6QCT(K!O{8 z=X+Hfxgjz=cN_kG4SyZNTFC#3bE5sL2a#tjz{$M7@y+1nYMlI5!82Kim(%V%-+1{O zFE7H&F-_?Y(B95K>zM$Y8-cskDtPd&Q;0N1USH{jm0ybXsT0xFlp(sbwQq{w9Wg87 zlcwSYqvGzuQtqGPhxzUN3Dn@5L4@A~?)C$SfAby8e?M1}ZgK5+`oIuED3#>jaz!c zc8uR4rj*wa;}|d%mQ4KQJ!V0SWxo*yH&kwsd<(zBew!}U%qP?UVk@$12imr!SY(gA zf3{s~lr;XyK&qi9AY>N|&r)WMgl|7IVH>M!w+qugvnt;CG8K27k&7*Cd*0|WclGEi z9I4((4c*0U3>bYVdRFHp{$8jer-320Lzjy;yLWadIHM9j%D4?ULo)URAA>lzcs=nQQ0MN-k6zCXyGy&lhoi5%BVruw za&e5u(|vdT^Ud$6lY51~oiU9~#&*8|jdh~@L8jKFNZ_f{J+v8J#_j=sH8+z6tsUn+&o|ColUcf5eByq6gv{hZ*_5#Um{rQr2`dmv)Enf2=Fmg^IWb zPWFSlpWXRFI$)l1VTzci^6Rms*p)M$JIk(xJ*w{zgV}uIr{lKMD|S+`Gi+H34wpi~ z7!8LJ8s_m`j()zX@pcF24)O9xCDB`oEuhn_q!v!#R0`mA3j!b7Q}f3v0~9bw`@*sAQDfus}DI z9C4<61TymR490xu+)$8LJ{NL_+`n{6%b0w!ETbqm1WvtGfqx|~(BZOBdcgsF-_bFI#KY?dQ-a*YiXn?N=`!^0m->OrABcP{i zpddRp0Z|OMJ!tfiLG-ldh)uK zG_?Bl#eZnsubHGfhAJi!WU_akeD^{;SAC>Y$1Uq+Gg^b;fATy$w$wByJ7F-4(|lAx zIKse&YN%ojh9wu?AA@_|03@muI+DM z#VDKubT8CXVU6?u*?ZQewvJ@aSN?_K`vKu7WSuxkRR~)J20tpsCV-R8O>I#a$wnJM zL=q;ht^D`ZbWczB%=FBeBOT#KTrS%noimT_o}Pa9e+5ML#L+Gt*EP)4$kgItVooVN zx^?SLaasvSzq@Y-GDF{5>+=+Jj6ZFi!H%^!Q&a?2nL_z=9Uwii`IC6Bo>&}VN4!Zj z5nd{`UvqrN#LBNj_`F|@tVj3^vTm*{m2{n7iB9pgJLi|x8*z(MRLTJ8G1OG?=~!lo zHGfH8f88WZ4OaTDx+mV#*O2Rq_xh>e(V-PhdY82K=v&6@I^0KWQOtQsnC3ae!_aFggnUSLQ+;kz4*1IsEOq`H*kKDG9Ln+ zVb{;G^^jnlvool{_XgI2w89$tYkMjuK$r41f4YKGh~5u874z7_Xbj@orbUknzJ-nB~s*Xg1fcGl}al^wQ^9 zneLW5|I^v*&#Hgj+J1kT?el5%(9L&S`}`StJ?#Q!X--`Z&R3U3yY9$(0FhU*RI1am zf1#*jmHIWAnf_1lf+3tx|BjT=4581IIwTlbMt0t$yN5nM6Z`1Iimg5oH-kp{`{eJAP zogHoiRT+EZ$F!?`xa&W*T|I(N3^jgSfBV9lVN{LZyIq-k=*-%KH2XTvM*0wWtU7kg zk=2M!f#(|u_9e)#KFstRU{jUvo62(p!DYubw}hGfF>N9?JFeCk#-Jfxf0p*V zkR6N7`u4yYfs)??po}Y!ea0XOj%?-#+7pIi93zMPY;72$YFsSIoWi&JFka7E4aQ&t zLGR-k>>hKh@-xOBJiNotMMoj-WF9{~1zsChVB7Gpx}xlTGgwJ?rv3IH3abbIrYsgr z2_*9lg^X*sM{IjL6?AZ7|2=o`f2Fh7#EVbyrzSV*3g#V)s6+PUo3)uUylvqkefUqn z4u-yyF}yjB{QOVg{0|`0=seJb7vb5jt+ny8>I*F9u~pL7-sE)=UY`l7Bwrkk@z~NQ zo;sfjFqYx2t=IOA*ny->jW37m)9ppXcX_mRhh@WUj%*Impz11c_*|u0fAjaA?dl)D z^F{I<+#$Y~JJY4OM%+@jv5wdOMVaQ7lc8v|w4A{Fej7d+WcmK88);RsqL*;mf7D7ej#|ep!}@5U zZ+T*$J9qUnhXRKrPrR~_yf|rfD$GC_(0BDw0U)u3ir34mofW4Tm7nvl7mlSTl_$<&m_UuE}->?#-?@ zx`Blp^(9nz83Ami#7Xj`9w#ql%-kwy?lpml6VQ`JGp*{(-frhkPM4TOsIXLzUY)+J zNOK7*f6!%mO&ipu!4kO)VXf_sMj6`pag4ZU==MB|cS>(-7souVF6F)tJ4RYctq-FR zbbO^VwK$VaqX^tF`na(AQQ!KGIOd3;a%s;2=0=^m+!pkWOuSjc70{kgr$KMziKx#x z@#?a%78{PUoX$_QBciDTu`dnVI*;PD`Zc^#e|&o4IC5zPlRC~`THh<|iO*Wvjg2Ai z!|pGvq=tELWm@)5^d>>ItBzXgi$VJkMoN{wQyO7~r+ivdNnzhT_0KtKA&}o$%czQB z_9Nh=8|`@B_yW1ZG%v-o@D@D$Xlvm;=dqWIxS$dIEPd|NIInkHudJAwFI77#ms8AR ze_`(adNowCi{q#M2xJ+EX8267|t(&n@+8yLMM|mPGmj_ z@jQwo;%G^_XH7I1jdB;DRmZ*XcA{E^=+9^FSjM&Hkxxr(c0IAjVM|;K&y~~{Hf!&O zxPR~uaQ5QQ{|RGVf-ME1f<0wT+(&+|f2(tICx*^NxoPLJnjfN?%Dy?5rtMbC2<2n(h%4SmMZ zu^~UybG7EPq7SQnD|IoKVS#7L?KiCH6GDt>F?FNi93{8ef0OnE9j9G8wpKMTf8;Ws z6>u3|$@?VjzTYQUKHqn5`Lir?Zo7@|oq12|Jo0afKTArQhbrpj#Q2sel^0)CUGgB{$#5W^`IS|dHf`@ihKSgVrH`b-enV0aTy~iuL zXR8FoILy7XvBVX9HOGp+5-a+$y1Rr`eLcsjzFsPv?$5EJ`%7KXH*>7We`FtW=0w|C zNypW6KpVLb8DTdd)3EK8Y2X^>Bi|cF%AR;u*#Ycp z>}A%Ept1mUOw>GlDH`A$?i--wiFc<{7UQs*3Y(V6vz}Vo=T3|vB)TBSy|pKSc53|M ze?nKHe|YyYt`cWEwbpX(VRQzS$4Az_j9F|a)w!$a>1QEwO~Qd4e+S1ztJu`V6kyc_ zLnHpFN6*_-Wb;jJ%>s%0CY_9k7JK46rB7zcgUJ=C2--Rac>z0blZ!BSptb08l-PCe z+Mgu+CV|vWqpX8WZZPl`po26>^oLdW-oWXkDv<+^V-4+TqmD+eEH|c#dRX0lZ1WHY zR)ZLbhz{N(jX#EHe*~uYC(duXYR{6PKpBJJuZc3YuYmWMv0hi}s*E}jw&jtKBl-I^ zWsg%=*>vRXSd)usB!)AN=4bA5%mG2T@AXKIBZ_%7exE6>kL^QGaxmga<_&*#jY2xn zTo=GM2jIOrvs&m~m(y?}xE|vSPtOoMgz<=1&TDk3RBfXOf1!!8yMxBw{s}}ZVOGEd zbO^c)LT#63V>zi#|Jco(*Ah7*t|8t;Hqb}#G4D$x?}Mdd9Gu@#oVBlZvA zN>9CB!CRI#QBuMDY*}3vZMk(a{i1g&ufG@2SL#l}dthqgpUm0PPS2t3zmU`Nd(xhVx5rw7VzA#P!B}>?-1BzCm|AnS`(yZ0eIdE()q>|G zEX$rBz}m)8yP;|K=^l(BBkv6L+_y9bD4VtI5fBgdMIrBO^&}_6o>Ws zdpNSrCne?6d`+yhJU^W#>6eQmi8N}~&H(CPG^w#4OVUF)ag;|cK`@+{2~6Z7=k%;8R5Ag9j$ZCh8 z_tn5Y<=c+ro+j`#=^N$mYWLG03$7=1{%DPBEkF)ATsx2(0d7x49n#dvYTj`?b;pO% zQk@`Pstp{Z;;T|;hfd?-b5mr#4qLQd=-;LJy?+YZTO)BM6VG0E6X+ZPY0$rxi+OrZ%dRKsde6LBqS$v*&4Rx{Y_dH3h?#cS` zzA7H?D>ojkHr@xGMQQaotbVtal$6cmUedox@ZGFBt!DJ`x@G=R7>9|Ut=qV&8&TnQ zFR(}L+e)h4T72{hu%X@R^PfG4HVhGNEYm~<5B0Ng@ z{BTa5yo^}h=6356`)?DSI_*y;FS?Zf+w#V&z@M1rdY-2f@tU(|ymGnA{OtUeyT$Je z-pM4%T|~Nh?U9Q**aJ_%?`qFh$#V`ZM`$i-O{;6K^TNIu z$DE=gTNPt$yXN|kcu_rZyyF}`e+fq-2-eU15YAM;mAPrG`dJS_I~c(za>dMb+gMNi zEb&Ui&cV%bdjhM#Svd}kcC#z$KbV>_@v}VQqW*Vzqd$c%np<#s29UYk4g9xvBL4l0 z7k{x{eDl)n!#6(59d|r=u$f1^ow$Aft;axF6X=tEZXXR15A7+2=U*sVh^YTS^DWckf6IsI6VQbUp7K+hX&pk z?*1ZeVN1O+Oa)Dd}5f4}P2Af{|0;;3H&9loq4F1naw@Nb0GPj){>`zPwlG^2LLn^?Yk z1=;y)9xavJkvgUQZZ+6AwvL|5TNgH~PD3ki>_5%T@UHE_pE>LAlguh<_b;F(-3UB3 z2D@_Cr4~ay*7vU(OkR7r4zczv#cs}$}c~sh)C0a^TZXep{H=XsI&JmCI zNIYvG`zpbyfA#xlrN-`CHZ#A`4c1-_Y#U*-KgMvAM? zSE!50%lGo5>{HZOE}}JD1hNcELlc{GJMuEJnBME&y%F*@WtUwBnFI2S5tick^-#YI z^JvRtFL~Rq73m?k(<2*?f*HMI=<|YiDaF-41$k-!fBow&om_BjwWA~GxMT8*f3A*2 zH}u`=C$K;7&;IPr&PA`$&Daa}qjSIfossI2o|96>iFZg2?NkqfUiD^m-Q?b+N9_fi zqLb=XBn7OgQt7yM{VUG0aAY%s$52USry4jZ2k#zQ4^!W2)pOk_eHlxxMT>z&dc1)d zFYHMRfBRPNfZbbumzLb-xjDD3C!(i#B~sKaXB- z3G<6jEMEAj~3f2IEhC!>yMd{)3UVv0zu1mn04s)6YC zOlPX=L8xRL`N;u5as)oDZkCo9=D8hUop16oJf2~t2Oyq zKhC~;JHy?d)`on?T3N5FV=sID*!HAFY3XA2`-zAfI)CT#_sRZ9q`lw`E;+ysaElv|xnA+f8$dhuvdRYBy zHp2dF>|;3916ZfrLHXP4+0r4v_8+Y#f6tk#`QS&Uv$b5SLqawrdZt%(M86t2R>!Vg zuOGo9r0~_r`t|mK&Q1U>_uPF_^7L20?P3ogTTr&`%DN)9)`t<>LHq8;O6jZe-MV}a zT78U9iBpPJG+MCqjq;iGE#W1;$tta z(eaE{zEL!3l2r}?X7n_}Zrfo~A5Zte#5Y*|e)oWGhfo){tsQ^c@{Yf4b;plErg~Go zZEe5bwY=Z&smZN7vQox!|JhTncR)!!V({a zUl-4hg%0h+(i5={dIv?!hf&l<8VEm=G15qpyyCC3vQU^MCr8qJRq6APii~I8-BxV; zT7nM3@eiR^Ct?t>Zy@5&e?jj)@N^HJIh2eDX8LaP79sr|q)E9$%{ox3pXaSU^5=OX z``f@}{XFj@eoI!}1o!phy&JmF?h(}HFhk@vxf*4qNZs@Ziu8wG2(R2b( z^Qm2TzE8UjZ_N$ouL{RB@QCXTP+ax|sB~cYVjpVtOZkER z=kCNB%@_#Nf0OgXC8c7{dI@ZXyhM|BaHo2-l0K%+E7%K29>4JIUe$51k*8-OmD`T+py1Ld+QJ$+|w!Wh7fogYuQyWR~?swPbzH;}hWCMjb)p4|sK0X;OW&r}!Dt!aEZ4RKYU=P=i{t0C2G zIDX1z&2VBVsaX}B+;g$TwWN}}kGrta195e(@sy5QEvbx^t2rj4?H8>%wqq;n8%lfI ze{$uqfoCCO8yWiarBsK|>|0*}=WlA`Gw&#?Z1!Q)HIsI~`D$?K*zl>8=`^%lwwDPW zpWzi(zYJq1-cvBe_EF>#B7MKwlcpb*@&SMKIX4JjQ!!R=Y|%J zW>o#i);xF!|30x*5VmHge6M<5kTah1I{RzU?HWHphjvL15*B$MbCg9f$CQl2YGIV+U@yKSa;h1U8T=G8hjmH4ZzU4czvS`XP zx? z$6u95aC}esu$i}RMq0Kb8MsNUe+z#fWwL`0(KaOqiS0a8V|k2fm1q!Osg*(Gp2{Q; zKbyAM?YWjc$G&XRfk-0FUOyk?nn7)mYAp3})VRMJLB;H4N!MED*o|wENyhb?PvP0I zt@tspcJPsf>6b39>ZPrLHgU1z?~CQ;xtE!-XLA(>-g&2L2+wSESjgXSf28_|OyfzD zJUaH``n(zUXnb1bH3IxwHpSmr6RaC?qjq1K_i`I-=5Grk=F4sXJy#m%4@W@GTz8J; z8kSqF?nU)%i52Vkr`UdKPA;D1Q`o(vi@3^t{aKtwXNsXc?;We3@Z4Rc+l9y~v>WVM zp5xMcpgBKU911aP-!%;8f3GQ^TS*py56`&P0<>i{o;Joq-ZO1!w0$f4OahoT$Ge1D z6ItO??UzrrU*7N9FY>p%zOn4OFHNnX@|=CD^D=*(mnOAYh$lCz4ny)cW!Q$A4&}j4 ztW`wkbE|OMyoLT@EH9Y)V8>#F)j+49x#!`fW%I3NZ|r4NuW5$6e{UP&PFA{ECicF* zW$b;eVz09#{C(RR{=RF7Kd#%eOzhpiW$fL*RqVA@S*RmsX|JwODmOE~B8+o;Y3)lR zuw6vEKe_VP?%A6(aK(Gr>pq@8&K8!9z z8BME>(9X_d7un4Y&t+|9#4^R1<$N~D-awUSndZo=s2YNKe#=yFB3KSa zbL}SmJnFntSfi=YIwuhCni(C3J7WmhPNeZ_fAGJ&H!&GqGiKZ3uSe!L5kkydTtkoO z;&nYnN#d$9DPJbt`N=u8*59!B%Q(^iDGua3&{XA*a=?0P8;4}W23Q#VU@Xea=5*F) zp9|A7Yu|c}Q`m%AfGIo%;2|HF`_=p4jr$+d;a<=9!vd2rL;x$6Oa41p8VK!sRRx*( zf8&hpn4-ov28y$lblwG7MkQnL=bOhFLcU;aZUUb&A1SR|$(X4%565;RZ$Pfo47{t9 zu|=b#_}lm3rDeAKzHDQl-{-*EwCGiynY?N2t2eQ93Ui~Jg-kLCvm;&ae#~Q>!)YF~ zA>Xk01kZ=8!3v*%ztQ(ZrgaCO17%w}e@rIUn#XDvGfPQ|WnTu>!^l_u9%?qD|88!^ zti6tGWx5aJ6_-|u^I08-JT%ye# zbr0-<|AL%n`rFxXIQF?65x>7>e)4%Dbboih?SjRUy72CW@ljluDAL5pciJsAt6}dd zdY%|vftj2qKy|zUG`73naaGAlVB4IX%1^e$+oW0VWFmVmgA!wvaqizUGndd77^@MB zKkgeYtiie(gH3)2PxnBJ?=Iedu;7u0>NtV7J}rJQ-8{+SzMYK~;k^bP>D%>s-+z|P zVZbc=0pOZ@+Q0Qzb}w}~%F64-Gtuz8;VMGtsvML7Q94R25AqsuVyjQZ5;<2hWY@+| z4ovld-$AO5z+QHz*uM8yN_wweTZw*NeTEtG0F8sYh4L`aBS~497)klVL_Bhl4tjrC zipys?e~g<8xv_bLJdwVaEAfL|;eYCH{#cej($p{EjYGA=ZY^CUm(*&9RVe>kI;>~) zL#j5f`0b1x|F!*dn7iLJr|?cLXagRC*W_H)-jiSN%&Tba_$t@t4EQ@(vOPHKzcT&; zti|a1TWb3kX;T4zUl@A_Paq!_|EKmYrqm^dp5V6xiP}A|qYqSVpF{iKd4JpZnXU44 z1VSJyC?z2X_h-gBzZ5*(F|<_A8OaAvKjV?4s;ZjPw5|4wJuUd2t#(k*0$&Zr0X#cd z3C8^eCrws{F%+to#nDn+i7g|k@UI`lm6kl~mF<&L3DHvt3i>YToDxQ)5AELYp@_kz3OZe#>e1F&aX!RcXkH!Cci~lcezlwW{Zzo{s5o9{Mv>oX`lOsu5 zBbEq9;M0E%vf!zaK}1E#kAs=6dkMPt3M7o5G0eHkon`TasjI!vn;cJl8XVYuGj~*f zsoA<7NHf?4S37I+e}4Ko`+fchwx8MP@+svEi_$zbsmjZa$z`FDsecO<${Dg{UHuKx z{J$2!dZ{+dw`lFjXLorgEjI$@i7rc1>D<3;Mh36Fd~UkXVN~`AzQGEp)L#R=|Jpm1 zibFdq{?z3Cj?F!db6BCN$MZ4VP6Do@LnY9T=#3Khnd#Vc4p5yZA3cq9FLVOF(Q)u@ zY@LKW>Z&onGjX7^#D9pKf@I?~Ke;K4DZgEyEm1nH9uj%>x%ZLhAl&l6!US%eVFc3R2Y z-Ef?NhN>w_dVgY%QA&anLgKLYKpIT|588EFTJ76Cr=ODVcS!H{?3!+?Y6X2pF`PIX zW;O0uUpGo$TI@91GJHRTr%mLW!)=VZDmFDu0yCg~M;g94nhl?Ty90m5d=x z{)|4M)6tbPDshGQ8u_-NWGVC#crDLY@>?;UAMJ19H))=`R)A9(jU6p!0X>>J z<960At2&Xq>O|33rzeio8{>VMx8~iekVj3_lwwLm_kR^Ffv?XKN(SC$20ozytgny?`8(;uK_BKnOs@IB z+Svn}e?DlG#A&UYn-y}Xq?dAU^}RRwj(!4?OMl1Oo6kUT^iUe%w*GeGj^gwXcMO^QEE!;+4jx+vDSbsM1iIPSv^XcU{FdeDY^ZXIM^S?=BMcVu) zz-9`2c?PEh7t!uRrZdP>SU?RA;;=NQy(t-L#`#s9BU{(9J}Mh|9Y$BZ=;GdcAFbyO*tz*ztCL`+wQ~ z06g^N&(~pfD3cgDAxVQ~f%$oc(<{NX!iP4|yDD_dSCQ z0g7^P9NWz-wzg$^JH&n8a@z<_1@S%)^H-&wPxKqfU-_o3+~qRGsUc>2@PAv_nS&}c zkL}HN%{4y)?QOo$K+u);krp}Y^y4otS<+ofc@9JsSrcavlMIXWwJ<}Ua_oaNO z9vPp5_T2ok`1}R*NIQfl4u9c^r@+5X>SO(PJs*VUv{G(&h1fUA8!mBH&9;y4Rg_7{ zlDd>b%NfmjZJkmDcL>!fK}ZYMi-oLDl!4sG-4+s($cK z;`io7wsz{IcmOu@L|tb0a?^Uc6%9{)N2bos3_LhmU%9*XVGj)Ysc$DNfBUGZ%L!T^ zDLx6kATLpw;{LECVy;Or1^BA-Fb3Edz9wY1C&L;H!V*5>#A_UqqK|6#1^@ZOG27;@au-np<9 zA--9T} zZP-{3@OL+&3gd0Fe!9XuT-J}{p>SNvrrHPjedbl+Moexebr|{xF4;=-f;x+2S)*_4 z(Cng%q@{n%X3fBVarmCnj3#W$45a>g4clbkVzq03uA**7u12t7@H5T-!2O7t!~w zm;dr~j3qpWx>c}-n+;X-9lW+x^WB?R&u1e^*^sWij4W(h2YcV-C;g#5WLL|LKY%`a zK1R)b9cp_zeQf0PR9t_0k20V+ez;<>r$mO$?|*K@-}h{mtLwLUynu8k=oIT_Nmj## zx~u*H_UX;!ylyPfKP1F8|8yFAj8WF+uE5AtB|c^|=?YOeqN?vN?`$LXN(0jFg?~v)H9@NY|RL6`SocsH&AU_8x&eWwnZ#F^aI?;KV zCV$3u>ZxwxOC(yA_bN)7)Yq%d#e{bT9vTB%=nRy)#jxpav{S#7>4U zcx7aXJFbc&{54C<%=L7((x=Mb;Thy9-l>&tlGXN%-lmrj+BeO0X6U)C#vaB;ixa@l z3u^2@H{)LxG~8YA^b_b~ya(s8N9qOCS$}-Ac=EB@O`ZmGeZgaCbX(mC&S8YJwgoao3@?^f#i^?q-9 zLz7R7ZeO_`b*1r>d9Zi*?UaG=J=U*J?+9t3{v_ zZ2u7~9@pcz@n<&yW*h7OFq)^f&AGL&Psxt*@9e##xjnsV5AG3sgE~_4zkim}JtZE& zCnIwg%E3VI5J?>3u<-P`3A%8?JS50pac4>Fd4ZnB39Q8hz}0(7TJhR+8Sr_}%|Dc3 z)S(@@*2mMuGH}1QiB8ea9Cues$$vfaf}!N0-P-hyrf)aG=2*=bK3&n$yQOY_l*-+LGH00S!opR5wwob5=HMZ-RY(7NOLu_Fz2wYm3m|NWy4~dk``O4 z%GUlSdJ-r1)B|%bFVyXQlVjU!SX&oA_qpmQ^H#`lH(FwieMoY+ZwCCImVd9?YahE( z{h~eO_9^EY>iQY*|`wj2;BU_Ps z%fsc@g-!4>@$6>NC|H4E=w+tR-JvoFvM+E8I(rd$1s2AVd#r`xC3i54*DCD z9DHfkM&nKOvAZmm8t%}pID-GoM)0@KPq|*E`7V)hJ#>GZbxMO6wtutDx0t1#d!yMl z$vq*jpe0LEO=jz z9I`HHe$G>9`Rm;rp9WGV$!;=&8=o~?zQ|RdHu^~)lRxg^f@f;8vvId;8RL&l?}j&+ z38Pfzjz9Z!G&+0|D}RdnGYTusr(?EnmDqf>)ra$8#$e;hklNc83Qx&rhw}!$SkbYyb*8oSW~=P z({Vomb>G6gFG9r1<0CH*E%w}oCo1IJ8N`|@xY0R-{D`Sn9e=)w%I+VQ)5NetyYx6ZXL zcAd=8`@-XyynpNL7<6j;^@%-Q`B*-k3ralG6IEyI65kt`if{Qgz2D=cJYo}4d*IQL zPHR|88E?-GB0Jc{6Yz;?2XJQ z6R=u{;whUkg#0P}rT7%ahT>Rx-|osz%Tt1^$FW211b_J2&*Z7lY@LTVl-XUOE)Ol2#}KR8gO2O(z-OVKaM5Q!-t7ES zS`(B+lk+_E0+f^(y83 zk_4y3d*AUathD6gYkV#8(ULt8u1YV+HgT)E6@PHfCX4O5+CNQiO5SRX$4hZ0+y>!$ zA&ge-!nzCt8$?=_K@pX?Jn*@4>k4Cwc~1U3R$RC@LVcUxJ6{IQxp;lJZzP_f{j&%8 zbH`Ax21k4ZYc)=GNko|IiJ<%Xp5p0wp82Lxx@|{tH|=Fj$DWs|4nfw+>K=?op!G3J zF@N?B z9AXdD4^7_x{_e@0nx7loju&UN?~>QQ=4 z_Ej&b&G#W%{0@A;KIA4UXg0TZ`1|nz)5ESN#4683Fxh>w=v7+wyL$cY8{(Ztjl!%KhKpI ztI-$Yks&LvlkkSbx$_u}bGZ8%^D>n9$#!}8A=Fpq=D_nT^bpfy+t2^F0lp*&2axZ4 z0kMOLR|{g0h&nB2sjv4YMcb1`41b~YT(gW1YQWYKIa2mWxpkSVxp?z}-q}{0OPpx9 zM_1+}-}g`6BF?=sN2Me(W;bIrm*4Aou%PSF;^zlA@A;>ZTFrtSe*N7CAtLCirsFuS zM$9D2tHcbpb8}agV$gKj3=RPnQv_@zRxJu{LmRtNZpW z=N&~Ad1+j%(}DN!uUqOKtLuTJ2*>UO+ILG;0nE+d`4e-~?~kA7vUZkTGr#5TRDJ!v z#Qgle4$e={KpvAc^#RKPA{?le-f!j>(%u5z6VOlLlXr{-*??&(~OTUk}1 zpChf6(xbK0WSSb}ae4=LcZcS@PERzS^ZD%w>9qf^aSuDT4NA}0+on?AtE?_>iou`< zx-<$?hFMCdyOkta8~esx4B0)qN^G|sJ$8ZXC+eKVMtdTjC38eBq{=j02KlyL*X))F zWC`P~;cj*otAE_q&*Qod-J0Xab=*_!e8Rh`K9$yYXSMTL?ycr_;~UUz(^mUVs)_b( zmUHE}7g>Z;nE3_7vGZ9%Kf+vxb|?2gV|7kFXWR|H1y~)c*JiHAMy{P+VKusMZU3Hu zs#?=8_Y$=pb+?6NOMF@WynpMRoZUT*2X=I>-MEBxUC8M! z)4r(y`NL=9Ji3eH&Mn9A%+PcazB+Eh8%IrE1b*u)Mb~{3Q>0Pudh3{qJog*A-SSVD z(H4h6e#)Mqgwx!9W$?iYRm*qaq()uQ@)Q!u+LX}?fW^kCf>=d4{ z6lclEI7-U>Z$Q*uHI$9<-(BU7&TJQY^eEPuT@`e>#R)MS| z%YOic)&+IncvqePuIFol`p{Hx9z#E_OL%Ll#%qH9zec8L9{34l=lWnj0qRb`roA*B zSbxWclhy(MxzYQR#cyw59&;FBJn- z-_PU4Dd-fe>X90Yx};*haeu#;;L5$4?Q~TN@8$Wt{=CVDI4;%;h+(!J6j{f589e*9< zNS5pipb9Vk8)U8iypZ{iS6UPI8**8U8~XgyXKu+B#2HDo4)?sLW51{S$3ML&jCz(* zVKW20ECL$8myUe8lLYTh&#S+t{T-F+XGd)t745W)RfcwPlb_m2XTRDgcHu;S0RJS{}yY^-<`y%W_b?x|01r8 z>s$Ldn-yejO1Su16JhA5w^^3w%_xsknkmogmGW zA}ZvvH#?r}mjCwua`}5BBjIiqQKy~fb}K`h^B~t=!rISD<+J#h^yr{Z7=PtRZTS8a zZb0b2yC54dqnzKXKt1B}nqC`k<9P(3v~$cV+H78M)2B^Ey)*0WD5;copT+Ox&i7}A z`^gva+T4kw{_e8>;3=G^rF%hhh(Gyt)o_;lvq|6~ZN6=E()zB^I;;QrvqR5E-e-gJ zyMWq0{=aYzZq?IXB7*j<_kY)S2^{Dj{XMcbW>%}Y=SCo|Aq{P+{kZ&6h9X23W=EUxc_MJXail-<^aC6-1 z=AOn1H5o>{h(gD9u3a^Vc5uHT7lGSC@$I*G{Wstrhk`TrN2UCqaDVmlw$17^#GPGA z!Ksb%ampc#et`zo^ux2XgQPQiV;Z^H`4R}Jbf?%V`P4ji3f|1OUKjF^-$Gr`7iJ^R zhj#J|Xd&I`tKe_xO{B$vs_nSa?Y=xtDXj9=U5D8^`Oorppl?y7BXe|{ak;tj`)2y= zoR_BIS)5W>P=r7xl4Y`fYFJ2o@;u*IoF>P6lDRP%Ik(tfNSFNrzQt}WJW zB9`F{{k3j0UwMYNzVLqzOZQu){+nfgNKgDfd(Ya`#;)Xi<$qtyIr{-SaezF|?z!P` zs=y(E+7PmsBzr$>Q3gBUdm&JaliYKw^556$R!gntXgo9K5z3X|$4KgzTCG-h>o&h) z@)e5!K%e;md_RW!1;zQJ84EiM4(j)`Kru4((`9BvS)1Es@Sa&84{X(U*ySI6EP>Il!c@0$ip0q!`lV= z;_0H`Wz1fT^;1Ss-^q^IJ5i`UcPX|Ct3rwRwEbDze}72G^?BhCeEH~Cd}-aI@f~P# zC41z*rs`XM#t_$({K*X?J~hO16FQ2+-G#X+&JCK-A6=EjGk?FX9`8oZd&@IzDb7Kh zR_W-zQTB<^qvzz?UNcEJ$}X8K{A$Gmn{sONC8b*E#F4DJ+}Tup)#nJ_tMv_bGxdAh z0*RhB(tp=`;N~!FK4cyV*ltXpS`CaZn$Po*nbx8?}%z;^Qdp1*4Z3iN~OraFa*qGw{cM zW^3QUR<_R@C>r$P4B-nzcXU+5M3o&~*?O?5&VQbX188d_|M8p;JuU^*HQa|fWly9n zMfd38_vCFscuk$7XWswF{>{Bam+2yr%ip&+ha;Gn5k2&5Osg^Om-Ygr2ERL$x`-sOlBWw=eK09n4{8MbSHO3WuhV<0!erYI8uLI??+-+P${iYyZGkW##r~QvAZXk8=l}(DFqsHb5K2+Jx@7 z#b3>xg>8fJov43C#-5={fjokb{HC3t52E>d73bk-H)@0E?B>u3@&xq5M0%YOE<6zu znCn=jSF`Vrz$+kZ`x6yy+Rbt=b#`P{ZGZKARud!Yov*R+3}b;&YzF)Z7(LH^3-EQG zoH?{s{drNsQr(xNA}^^oBi8&^o*CpjYuC_XltjGNhi|5Bw4Nzn7&^j+*wkA59Nn#e z;MXe~o?gv%{^$w!48YdVtE#HfNY%IA1+7R{#(NdFrm>_<70UiS&BiQd(0{H-k*nK( zM>5wXhy54McG?-A%bLSBpGGQ91-%#@SylXt^jJ_$sG$={Kv0gdOIGc>o9w+162NIe zl)$yzww~z5ZCkUudU(;6b6Eu@a1+f;e5<7^b|H_l63#a>HGr2t#z8s;&M zbz$s7srWA?F-3%eKOy_VmJ)MEB?u~oi0q$HWc?A+sLHl@`Mv435)t3u~l zhe1bPLD5Ty*Lt77m49u@Q&XwT~g z9D`&`<$8s72kNjwWzZpCP52%$86+Qr!2AlJB(2GD=DSTnCtFkR$f_ks8@p0#4m5ca z)fuMGIlLl>b%0}I%9knGSVJQ;;=Uu-v5|X_Rftgx*ME|#V>r85k3dj&Zi(5YPrR4B zdupEzkk_)y)7pljnC!hi|376%MijrFd$4LdutQa4$H{o|d^5+z)}fbBGu`;H}S%AUh4MKHJ;p&ktOHgA=WmDzhC}z zXcwooM}O0Hw$I8Y`x-5Ks-AJ`#QNbmkzHSru&g5M<}6xbnAi1*&5o*A0iTX8v==_rJiK$vqRox;Rm1IarC{-3+I`F>47{s=#Av ziuk+~nO<8mj=_v-D9d>_M{%F#Uhy7StmLD_=zl`A2PR-IP&A_Lhh0pz`-P(MHh7h& zuGBTyGgGj3@#aeRw1r1*rExRPxp34*tQD?*%h?vl;J;?SZ^_()Wi?Og-6ixQIuraS zZYC{x{YCUN;ppO>Nu3M%*H>eK%WKLvao<}CHC`L@XUVag5l>#D z%73)^#rf+%)>0gUjz&F(Ez)O2Z0A((v@`X5)85P281dLZ%;{VPKftw$M%LQEpY!Qr z$u&rB-Cmx{osw>!dha@~WR3k%?9CMJKJ3aEQ_h*Y%7L_O+cL^O$~IK1*q!urM}Dqu z$=`!EujO|u+2^Q?$Ei&7)r8I5GspJ0sDEE{?2+sLc6FYvsY>xYg2p~yhP|uc`#-ne z17*P=|PK@WKPo8qedFYM=w zXY{*SyY8)UFS6XBp-Hv0^WwI6_DX3KU(C>u`7L#vP@k=bJ@8oZb1DsqyT{|cV1M4G z=e@6gXi(s#yuXfP^p>b-&L%}o&_(*UhCPV25d_a zL$qYPnvKgsPIHc#BgJ@&{n=J#NRIy~iBBxgehB4+*)r+xpFguA&r;l3iZ}M0N(zph zNN5d1n~`H7dF9b>`o@Snc$=1~JAdbiOS7$pP3U{nxV=_Bo8p$kMnv)+C0QTgeN&hl ze?}#`E>37Sg+}+uWZN8`#~!Rph1Epsk)J>M#!kIOlkyyi4(k`bW}0o$tR&Q^4Z-8rd8=^q*d&{>RgZr2t%B|iy?9Hz2y&!eb%HPQjK#C7QHmR>Fvjs zA{TTFC%1-eOPFA&+Orv}9Xztr19@eXj?K{>J^^aj&nHV-MdTG|ZPaXq~`dOg~uzK2wj1=2~CtU1s96DLTF}iY17Hu%1 zU;;Y#BaApPPV$yj1dJGEd$z7#!48G50vz+@67R2uX9ISwV&{i9pMM-eHV%4&u7cK# zrUu~@qD3gPke{w>$;h5liR*VyKdzqyeF;L^HX(|K<6p?0K6!hJ=`4<$Sb6A%yG=D!QVHwxJdT2JlES$}*j>#*QCb|+}%g!-MZ zoD`bN;*1ba$F%fXAo;(HMYrMY0xN6TuHg>AV*m`d{8S8@8YxKdn2@*fa&Xq(~ zg;&_W|AL-L3fw)6k3PgG@8mp6|b&6FtD8pIU zWp!!tT4qLI)aPFeuDB*j%f94R?x2Ew9v%U0f;Gn;BflE21tRdVrM&|uoI>Bd1N_H^ zm(uRlpMS$8AT;|xsD8D+r9JkNdQs2FtEw^+=GA;_SV~n2UB!>i-;o;E@_bUXb9*R) zXYm3ae?+Cf1-JY0jBJ{nS!*U*2UX9mZTY61C`RjiWzh`to{7_6^ib^0Wb6>u+T9tK z-$69V8tfPMXRFP2b|58l6RQ4g&A#1-&(whci%Af^%!-l4J;tjMZeu0hrM0 zny8zkucbFeSHMM&-=*@~<dTM2xtKvR1Tl+P=mHqBzJsrwb6x!K(Z&2AyDE~I9q6J6nP z1QJ}nBWbd7w2qaxp){$fd&D02uYWFvy8m`7RFmeS-u=6oGTb;!DcE)8cqU}LO=qUB z`_WNNea@qfW36IYh4GdvhD?Se(1aU-o>H)k1?+TrUPQdOpJ-O+5o=Rm_Ns3*gJ z#|Wq%M39&xFB$I3q@ zYE=&KRl3ohmS5COdp!Hvtu4`w=%f=>UHaOIH238(=vqWvZ|iuNbuUHe+=jI41f&x= zG?LQvPw=B@ojY`La|??Cv|b~hW0AG|_F|4(+`g zAF#^X@0GmpT7PP~PWZm6P1Gs11BGOqv)kmJ12w*9Lv>z>>#dIHr8B=%_{?jg+jFA> znUTY+V0Yc?iV*{A6m8+9-5ne6iSALvPnaPRhUEb0Jyy5F|DM>+jY52?p0l$?=B?gS zH3K}=!$viI(Egf$*1pVkWM0j=%CG5w2Gvi6<7fF(I)8`Fjh3}#kaicmJ!ZsFT&)Wg zhg(z4?P0}u(#%!!VU^w&$8aNvz3#7^(z+h}meQIo6x}&}ew15qoy2pOODXW{G3Jj8 zk7tP3P}x<7#=+Qos@!iz$&=3EmQ;CPwsqy7<@NeRd&s4NYa973Ah8nuFRN95m|Zfv zjCQc->3^Tf=!W!+4xO`0EB^`nK6Y$IL0kF_&;JH;yuVnhxL(ly4f{@Gvwne|cRsu6 z6Xd>5CB&M3)on*1b?$H4N4O+^kp*^U;*nf=+;+gY0pI{u!WtmS(!?|9o4=>}@ z991aB3_Y+LGCx$@jlVPF$9x>fI2^o7n(-fGS2gg-3+0rP#9iuvfSyr53qm7l|}rXdOmRh z)GX(=yThlJ8%gryu9-uK(V=f``N7{;D$%5?Y*2(}7tS1pK>LwHA>02&@2cgn3puMM z7geR=1FV@Y6$)5;b>^m~)P=HI@QLEyNPj~aEpAEzt}O|;W|2Q=iCxJ0DboP|C+Z9` zCJQ{GNS~tdNJ-LhVK0Kff5^0NTFm0@kqnZBI#lt|p?iz@Yv0_WF|vR8nfaYgrMt#| zjGNN<;OC#2*M5pr;cJ?WyeaF|pQyZ-kz7@9jU`E=6}IpBm7U6n(+jCmeJwhK6Sqs5=Xb77;P5oEuq<%y zbG0iMp%xp07gap-M>cSJr`nMt*80z&cD#a>&hMu-E4Y+tOX|t*J&yI&&4x)MnR*V# zTWOHGevR2Ar}chvAE0_8?@iK>bbqq$z2H?UchV?wbDY6XTiNhlatSRJ9EUJ!f_q`A zEs~RXaya%%Rq?>XIq20s_jEPEuK*E7G@5V}YK7CVUs)?&QDd#_{=}AQ>e4Mb#JWoIe^G>0s=BUV#%*@>YKMR%H9!*yu_J}K>?Ldx1YA0xi-tcc=F2XMO@ z_1N?B%k*X0GSUPDZ&;rxFn^db*#rddRgWp-qqH|U{+8-7WqkckS1z{Y!pwasn?sMY z4SDCD;z^m(r=X2|aGAs~>^%h`$ z@u1TlblNS_X-`_$X-`_$X-`_#X-@(=&D9F=dF$6RX?dw37>`*0M}P2lWYm61)plVh z`E9LxvMTpPW>uZYDCuv3%$ZQGR}7vyXR-S-1MmxQLu=W$P%j5(o}1HuCUZTo&eGU8 z6VF*~dG6B)jTH8^l(&7EIELT)AA=OW$}Z$7cLD3sH|R-!i&^v(|Ak&7Y8(1p_fx)) z(2(>&nx|Rc%3OAzK!4~``qq$ydv~8n?SII!C3PkJCT_+5EF4QAkJAW!w^U0Nfu6?a}bJTk^`@A>% z_a@s0AIUz%fescxNv-$l=)d11(q3k>-xSXT)VEjS1$2v9Wp9pxi<5c*X`0l#wO8!Q zE4U9rOH$uAOz_}p+?loS0~{ZLPeZlu=*koP+-k80d&yY4n5W#gx>vJdd5xi6n3-6` ztZ>#vmw$F~A4r=vU=wrKv;%$ql)H>g$oF*nun1{`a{kkiVV19{_*e z&3^4w$cyDv^x^V&;n=*-cA+wH|6X9THRH{P?0*$}FXl;IoMN8;Iu%v@xo-A(XuPNT z)OtM|=^)##i1p)pfcJsCCm1o$8SdJKi$lySF7E(kMhrI7&ax}Smf1Q_?hJ+Ju_r<+ z@XtqsX~3?!54fjUEg@O>87c9@yOuV;f4B>?qn$m+U6Tm?&OYVSv#s6UOKUn+^7XM9B;zwW&{aJ1Pc7MF`4_5#ExB6YlY0vI=ZC$?=p9a%fX{+ki ze<;6d*|{;KEVs(EYEt$;?)Q%Xrg-Em%EZ3J93!jFo$TY)xlX*-lep*m<@P}C^4b?^ z>OTY2y@VnP%i7W3lsM%TCIPRbF>0vI%6G`|OaO}Ic$%#lv!gDod>NF5NGW%FXeq3?L-s+p(2^OYznpC}u8E|%96A7L{a$@?Ps)3H;h^#*8S@~0Wv zbZSti89cfz(mHKLY(hrYTH~;P#hf4J_t`+nPF>7Vf>3kiP>H2U4cl#0&JfzB;ahD` zb93I*?PFEuuKp*3OH^NJj+Pr9%zx*lz<{mmb=TUWq_3vdTOZ8nYi<#2eKYGW$0lWc zDcJL6YfS9f@>M3*y3X3r42@_WtYp8>c8Jfdy;;*T)dV*>HTlsTPTgD0VG~wAr`xkJ zLi2I0HT;^hHTWp?>$q30ng>Al)U++);$p9T`iZ;d2hfnOH$oYcQ%zB55#E3@ zT)M5$JXVRUJ(*o=k=ttltmYqOzO~xdPu-O|O$&D$mOO;Gc^Fq4)$g23^?Rx7Cvbai z$5WukN%s|`tj$Hs-hw#XRe#p#BH}Sv%9d_yt>$>8twvRLtM&G(vDS-UyW8_0q)S;d zTv?P4AqB7Qt><-$C!uZYBs5FcTEw%yN5MZ%T|33>t2tYxY-Q@UApHn=JFu=czV30( zHPcgF|AN2wHf$KXqFvZ_q4=U}qps7A^K{3l5Mh3zTlB>Hy{r$>#DA9%!6EwRQ8K*Z zET>`)aUkbV5wlRZ)WdsVd9V+mS~B6@Ob+mRBl!e`drAg@pw>KN8~`^@;R&8y(DT$l z3`N-$&Y8f*J&p3mm_8TZKVhW_$JaF@srI>^?Ikn0a+~G{(Q~~K__WSUrHfwqUAv|! zUiqQA%d>!I1i3Nj{D1xZ(*U+cckxfx1-67OQXkxnRnnfLzFFEX74NdF`fGN$5__9u z!4Gp-B79NiXd(73Xq&LSVpdU^B{uZN0_QSPxE;A^suHQs+DWY%`plq+pb7C&f0ss` zDz)%T2zm>u%Iew{rX^0*y7f%~e*?)eE4meMM+CmljoNQE4}W~O1)wMODXV^$Lx$y^ zHTI^wvEZBBv2D+=+=DC@`^mq-K8$L0yCY&=$usA^7+W*j8UeZbDjUli)+TDi!gB}w z|Fie3O>G^?&R72nB_A%5qks)KsawRZi!s=2jO_(%@4cxll|TYy3&Bbv#JlDHel^|G zGd-_!<~$^egnyzEuyoE$zow_Vr>C2qPp_7aeQ$hh@Fs>`Rc|FyJ-%h}w7$IsErnb^{|ThMfOKejGV}j4V3W>uEG=9Pe&Yw9eRdnhZah zBR}gIdy!UrUI(5O9IEu;NvV@@%f8tcz1+SYZD(gD3V)Ivf$xvhzd}*7lv!HIok2M) z2-^*GPojO#e1`irw^;F${QfIr=p@n=1K`t%_@p|Cm^*pxnme%2Zw|S?3OMfM``(Ca z>mFel8OY;tjH{uFo^k8X?B)VT`s_Hg3#n5auf9Q%WjT&Z4gRit=S6@r+p}lC z%;vwC{eOR=PYCGSX6~Aa--=zl?O4rnR!$$%M8r&fS zcXt>P2=49>+#$I0;_kt1aCdiicbDMK0E6r0e^+;Ld(%~?&Z0M6efr4nQCVsOX0f z7XcZS88y7*CZj^)Y(|i{;3||x(vJ0%Zm9ON_3dyJw=*r~5};^sVij;FRv4Z2fas=Q zxs&PV@qxp!XJvT)gKJ?1_2u_(D%G*u9lE$l%xA)Esa$EX&Cu=p$d8Xro(y6HKz#pH zKbT7t1DZ5PfD6<5VGJLQVJ5J4p7EEKb|}=&wcvIa+k><2ogI%ia|GhA0DRon1IdWP zNmGV`&h07&fc2o1$}FM5-v#B2K9cFEGq>C-=#*rE4I5*tZ_IHu+e@#Wm3Di7jXv#N z<7OhBZ8X^i{?i*?bI~DSVJyGz`*6Dh+SQ4pzzc@YbxgvJTJm?!A%&7fckDe%8;55a z4}gVQs{|BXUv?juZkv*;4W$-QB^De?#MZ)Gg)Q#{fdx4)W%D91$<{%!W+C#WU~R%9 zU3$>F#1=T{5e1yvxR7{U2r79_@vg~1fL%}H!q#V|!hrnF&}9aj=5z|nxh4s$BY29E zeWMrk&psb5NfGww^>jDqbPA-$C|{mPE>8Fod^tR0hBLuZL%J3i)$h!yk*;?se&Q77RYz?LV!A*}YkLr#1%e{`R2sf@Qg|z#V9oRyfG^_7^-8 zQ-5%%;1cVEvf-fkFrnkLDp8XV2?V#jll^0XE4M~iRwtfZi@)Q`aGopjDpe}>0&#Va z?ugu!JV2kH8>Z6o(5^ek<|}S4A^Tw^u^%eAm=MjHFBbEEU@Ig=P0XI~KD*JMk(?q3 zbA=10oNx@N!OBGnHD}em$>PaOTTfq!`Mw6nnify$=Z|*o-ixME{p*!&S0d%-I@YiR zstN_nA5Qa6d!_iw-;%Q@H~L^~oLQx;d*obDiI z+~IWWvOn8iI}2`ynQsKA;r6Wxy!q?6eQ5H#=mz0ya3iaL<%yW%^y5CyP9PzIZce?v8lLk_LI3T< zKhh_0YO}kNIMk%3{h8@zfbqUu9M(|aP2i&3mL_=c$0|9eei^?;9CCAug`-T`gWc|JGP;ffmu*FvI7`Hv&5X3 zR1etJ+Ug3G+baSrf0@B7>Qj;Zb-y$>k*e((GZWfIKdUwZipz0-py>0#pTcHezPTX9 zcudaRygs$AW3P@Fnnz zWYdhzao%GI^Ew=|Bst1nDjp;JMA4-c3`cS-htzL>0MSA&+NVF(QGzbM&NhAatWPH{ zPA0}S)yo-=d4mpcz$LRb(O@RZFUu4VRm!oi-ipd)_4ggnr}95#tn;E)V>J)K!J!U2 zD+oIY?Kw)-;gwC6L;Qk)4x%OYKBix}MiNm)vTAQ|ANZdZxMgs8cCDA(vCA3qgj!64 zY;|CaWp9!(xNSvbR-9RlLtNC3BjulGH-vYnvYcgyKD@rs00+vL5ZRqG>C95u6g8a- zUvg{Y3%pDu1Z{h6Qiw>`<-p&lPrPrtmqWP*SdiIz{)Vr0v$ z=sR5@qPM5wqW7YF*x^%iKps){&Pv_o_~wlA1r=b;YpAlKG;~*(AlB9MvTrK zW0R=etYBUgkfqNqlp`!%r{!?i9hLnYeNjupy}U-K>DjAEe`ygzogx6)K+sshxb;Mp zktJ(6hUmcp$)`r(KHr z;lzuZu3c59fWpNr+RUr|ro7++DnopgSh8uLTv0KFYDI zrMzwo=!$inSZ7ow>Q#E`aH31gM5si`aT+=PH)9EDms62lww@r4xD702Yy7@b0Vju^ znu=|0BKPkxM8mM6eID-QW`1m608aO|dtmUO$QfsVA;@6JqQ9=Zsrrp+B&oL3-1xWa z$E*DVq&%~W3-x3)mj9UUVwN{=t=;_Cfi{;!U|hy)cdtbNc9YCl+vBe3LxniPOLvV# z3yf46MDk=CzF#btZVcIoP@zL?W?o)_%3KW0GP>F2xAM|jlo5TPv&@&y)9pTKD{b!a3HAUnoo}+{OTf!{d^Sd;bTudkFKn(<5> zlEADIRi}9X?!@WVq^@nwbop6;gqDO+*8|>oWo_x!5^_43>4w4qVy-O1C=18kmUQdD zo$LvWijGAlSuv`6REU`W+5w)h53I=QsBARK!Y#4e4R@3QVH zz-g{NIHieXq?q%zr(5iO5Egl%v7MhUS*iOj3xe%Q7 z2CGuNfA35nVlmKZ)2Qm1lGv?U%R^6KzHtcu&j5`}Elgi2rG<$sYWxm;h?;Nk)$`Q6 zFk^YX%~_8g9ZG(jE$_ir#Ykp%YJ3ru*jK)b zQi`{a&cP7}7q7VFcQ*@?ufruS=?xY~N3P1eZJSrKt?>@LKtw^4oB?RP-0u_E^`#K zN)er)Iw}FTw@JJp@A|xoEz~pBWJM`H(IavAE9%xe;r{{J!eCx%D^iCbNqIcFZEw=4 zmZ@of*Jts{4sLhBZhuf!mCM`sgSct#P~NhH+~}GQ?{2{1``zTa=58U(o*F5Xq_Us4 z&%2H)Em%AkQ{qQ!;b$tvbJ(~9tU;;?uCVC2NsJ^^ZGYTyv4_0HZCFp2T$5^=%p@9m z2X<uH4In*Yrb18P%!lZA< zaGjac=7;eZRF5`VW>+r!Z^XQ}eTaDa`tneiok<3?zRJ~?FCP`ZR=#dUiJ?>=r%7R- zXjsF8w#Z@p<8J;j7P26?O?m+t$SJd?z#<_9(qEXM+-nITb_0mGIFmE--;LT0ui=_8 z-xd5yu;0w9bXQ?rX%mwO3zfu$Rw!C5gYqg9;-S6fJ#@^!)7=r8iM4y#;}id6bAsB^ zO7A`!oL?dxYZ-@MDk;_KTJsQkXmc52#YS(tT||FF{haT=D8zwPJ!3ivF$6cNQlsty zgMKI5`bSn-%5Er1C{X>el4E9>H9O|Rk4@L-501^K$z$tAuK``1q?CCpVgZQ}89 zf|DA7VmAX>`7>I2*pq=z6=RM%H|j3H9z)5d!7}Tjtk;fJb9iOU#c({pyGb1Zb(ctA zJcz30@hu`ah>YX59V0UJ-k0ZENTaCUNatHngdc}LprWwzW_3yLE$q>6clow4d7E^$ z3_MMppg>-;+FKCU?RO`xRXqd3IFS!S?y&hq@Qt-LL+#{DcE#(GQgjt$&ZGd`!=Bd; zPug<`!;YNBVU3`0s1US`Zc;6NTr%jW?FnTxwk7P~(9`I>{-B$1<>Q#~~uq zeVI-g`MdZNwAfTsn6nkK7>_y)vl@x(ArA^2>q}$RSgUwW)X)j@13(SqV6N7k1t&oG zpp)a%Q9fI;p-R0?nX5~d$uH{X#z`{tpUFgF<3)1;a$l+#`;-R{i&whcx>Csxk=t-< zLB?FrBjrkX<+Bq)u?U3UD0$jy{7+IK!>djkQqV@>>bHar_XPsK zdBS|{2!c$`n|bz4$vU21-XoUnbu=lrP9v)K*3EdjiHO(Ur9ZgscT^0xDSpR0-@t@E z6Xf8fQ_USjR00CIa({q$&A!8nsGu)y&iRfDcd_ZTx~Rc58x32*lyvyLy^y`cDyIfklLhpy@|#7R@&}TRIUR? zRB#+C7yQ3{@_ol(VU2~JDhz|HG;EDo$@R*bY82@($9jfk?$S!(-|4H; zhs{z7{09*Q4&7egzE`_?TS6-FVhcC#9=>&~4Q?B6^0JYuo-IdP1X_3{`es&QZ1)lO z?K4RY)+Um58o#H&;*RVm*B`H(_WRebQUJcYwQ{C6GtgRjT&Uy!?8W=N>$E8XPMg>@Ym zI?O`a-fO@YwXT@1TplK#Y_`uFmjRKU#DU*Vu%*LvjHM`EI7?~yp|#|M2Dj4OZ?I}s zsbzs@$bu`FsTuU#us}cJ`}4otDlcI4y&(UaHFTVyuYX9HJ-qn=z z=R7iL^tVBY21D`9185}@y-Jh1b4UNhRkpg`2f@Xp5bz1&{=uW)Lv!^9ZyS0vK`!jE z$;eMo2M3tvHSO=-HjK&Ow3O#(%_TFR=UX;tK6SsR_$hA}4C*vjhpZ6)p%(;wwznRv|XR_K)PCJ;im zYI5Vl+MZ8d&-{ltvA*`$ky%`k%yT*k z{2%4*aE>PFKjw!Kr=4yqz2&KXXGjMeYM;y%11p^pj1}1iS?kDh-g52tHe1V5I%+e& zU9wwk^j+*^gF*aGu#gL$!fje}RInLs-SWofr%ZXs6+lNT*^>ET(?*3z*y9CI6GBjc z2Ii4#nW|1xz}lHiBX|_*_@6CE#UxfFt3&SU&V(UZ39g8f1~PNmxV&@rwwkBmp|3bV zBf5l1iJAO1sxvW1stLhAAygc%>Gfr{RW{4|zg4yDv_InGMEdG4`)Zep1|Pd>;O{Ls zIOC>2tHDGq^zljzhx;JJ!i(@sYDro@-q@$ zHrNEtBM*MkY1&)qsqmax(6H@bCYcZbV6UOxR(Z5!nxS9KDvH0pHwXUt5DV@|p4*mL z24Nn>M%UIip>8PSJbra3hkp8pJX%3lxz|n}kZ@w0M|U&3*YGBm4D$l27!^+K6BJ4R z_>9h$WT`*3n%y|EjB|@(hr*8=?mg8R_w*d2Pv#j_uzz|fFxzdjG@Qmnj`Zcj&S0^^3bH^2Xk8_qni2R- zFiFs5MO@nXKj3<+RsTaf#r4X%7T=`ma)@PHKe|OXD6y-&)6_n?G1^bDEyJVebEF{G zdR&;PL!Mv%Fqtlk`4LVO+EJdZchs19;<6o!hY_50Yz05!zc0Wlv|>8w;cr51__NtC zXD#BVJ)>0DHkcxS*%RRwSi;UVkG`})YF&K7qZH^@V?!0FWu1MA?xS@SpWtICl0`dG z;zIm)E7}%}k^(2}ZJ~}d$zE3AKK3GW;8 z;f&5Hinm%d8jQn1k{YEkm=L6!kExQ2_IW_QzvJ0~|4P{I7f&w_;3=BR+0Pz)Z09;f z;#@uIe`Nfhbom%4xj_cUzp~?|8Qyt-<~mjoO-<4;<47*{L{WDm+>1%{R0wjFa;Vo& z+1QS>DfO$hRjpTA%jM!%)uEei^(?84t$n8IUEL{<77u%s9aS0fuS-u1=vc{yFN(^6 z=QW7j$}V(rl-UjnSd?6S7xg0Bn>W{sTKV`hec`&Vje7iS5+fwix!x;uCB9u?e-i6qk>Rp)sdiPQOZR`K#y;)b6N%L?2CC<_~1{HR4_Q(0{S~jLLRf z1eue|{RPt_m*H;VvcHK7ZYod7_5Vf89TR_u_3tO3feDeq@&PY%?YII>N45 z--TggMqv)?i$@%+LV|N*F}A7?s&@G5#7=1=T@a?qL#lU$2R3 zj|*foE(N(#^W>AiidVW&*9ryS^Su%fGg8)L+&MIW99y1!{S`$eYp9A z=9}m)J#1Z%Lf{&}w+P2we&7*Bf<|J7TY;eCQ?ywY8_gcq*}tcgVaO`zFOgx-UnTx` zBLoju#a-HGPX?jm_@c?_Lb63)eEgDY_CHw6^2eb+4aQXiad*Kz)# zu9r2}wS}Ri-*1^uiaVy)E~4ZbiGlNn3M&%Emx76_(N7xo)b81V-_zG`%N`2_oKeOH z0NtB`T*o&nJOUU_&R}Jj0nWP>7MQqnWm=IhVS6H^oNyq#cq@7Pxlq&k4EAo#bkB}N&R_0)p=9S>dBO$?LaIrf>$JI_4)vE_PW9jE+bOpfT&F+|JCiF1QoBO39YWfH5%+4NuB(WIB}y>}3bUEd|N)P?jeJH?8$X zRSU;HbHv<&vv7tPX8ybS&jR>o8#OOvc#5q$Xrpg^ioGD!_$P0|D_3#GIvXwWC(EMI zW(!5Pu0E1KZ+;L0IH|{lVkvp_KD9HUkQ{L%{VQ+f^Jp9vFQaCq3q$02Nac9u8?N`$ zY^bU32f-bNjOmuBb;P$ZKw^YrI4N)S*3K9?baU#2G2(B<#~p)U{4BtR28m>GHPaut zNEj67uw=Rt)OTsOsO)Svt>_TG|K8nH@;yD-ZjtsRW`}xktvbr|ZC=>$=g0%*DvoWgWq!njuBgKj zg+M9%il&mvaYFi)YB_*?1m8|dr$;RZL2&(t;#k5I$nlxKaJ@K0Ac~pInqzQ+uY+=$ za|5OR>x=+-;$UJyUkvwuR#0qutkypT?f%mZTW&@DRdBh$V093CKADrj%cs+jL(4>7 zOL1F^u-V^qRzDY*k(R&f;M1hf$a0b~Yl3NSy;r%Z2EW}ajranYXsrn_lXZmBe5$&P zvp=M=ts(20eHeJHB8PPsv|ME4Rx(whX93Qz4sEzH389I`!k;76J?7mS31S5L3;JuS zqpX|5uP+xHZkGwCn79z$C{|=ja`DbH`-}G!r5(OisJ=1`q_l``sMWXt5gjyIbIvI; zg?H+xqu*%$6ifjJ`TZirzxr*4*E4p_zUnM4cjEa5dA zrvPaaB)%~#;108P+!X88_)iTl@%F8G$e@t@;v7C)XKx{g78fEIhcA9sA7X7kw~}fz z3FSg00Wg?0ir3c12d5U_rW%tLJ|uNjECz70>AEhf}2!_wWEyKd zcjG6NO9fqpy1)>#uvC}1p}cu>xnz5@&7*h;Enm+(V@ zTZG>Ep``$b?)2FS+xhpU&dvX@&VR!tBH<0!(_FO8|JTu;8xq=)PiOXJ(97e6hc3Ny zd5&|E^}vy&AYCTS<0ZiqJl7!ZMZh0)bg?9Cil&5+os%c7by!m`%m?EG<$on za~v~ko9#hJG&kU_1E!!wa`p<47n^Oz{Bwp{63hWlK3h#bk6iFM{G_oP7JJFvm9lUz zbK|ZGMb$xz^g@!Z#!UE0CX6tTkMnH|z*p%-)=&aBwY7d3QO9DQYFGcgzbDIBIFd)=t`e;TAwM+kKj}U5?{LvmJWKEvMnIm6`Azzs* z&nn5rG0B7;V~(OB@q;riCi3I11-d;4Omkv@w$zUbD$+%a7^c55Oc*hhaWR$XF#v(Y z(7VqGxlwvBz67xaxoc7EZnmi6L>`3<8BB9(fVRw!3JTIi^q3Qgp_`ApqvX4zA9qX1 zcS}F+ilW<-!94JO!8^#Fe=qn@x|(qe3zvQ&)|9!Lvas)zgs7K(8LB7W)%?$1;7fC+ zk7r7NXNr$!LV#z2&ziW94f!`cDu8r{zb-`RmRvVr;q_s)IC{x1Q;D?^ib^GfAo%R?$?(4}08#$Wm8w_Ea#3 z5i-B9T{2M>uvd~S+uX4m9Y!g~ z#IR08)MN$90-9H#uUmyk={e1{ub0!Xdj$hk3U?Onm338ul;yUZk?`oK*-xZhiM4bJ zHmQdXW~2;L)zZPALgM(@4@)dPJwMoVYbRtDi9n;n*T{0fm^&y*Y@%tKK&AYby`a-H zoBZpx=T!=o6ZT9dr(Xy>0}+ouL@ zGDDL6txS4R)IP}KzrCLXGbtkt>Jdgkk*i7CYB-_|j%Ag~z6Q8#zT;Sq>diq6pWTOAz*S7;OWxhhS`16d$fN!w7bxMb$rH1)^s%pn7DmvH4CKZ1 z--I`K`{Ahsv`TSGATX5C{fM4KTRD33IgQMX4n*MM_RN~sP5hwF=6b7t59!U$TMyO? zyDZ4y%Yu$trTxBKsdFcr7kp{z5nRrDFRL>X^doejb$tak3 zPFCBz^lg1x6(cMxn2?TCFe(lZX?cUvqw-MD;k;<@zx3o=&2#`_ydvhdl=k3&t& zzlM-{VZ(H~e#MFhg!UJXL_9~^f6n%A1&-yaXKUu}WBxa_q~*%Y@B>{h?nFg~T-CMW zn!CpC)c~wy!w1%T;Dluk?t+ZRIB=JB-oizBbL|RE1eE+Fw0VTZ0*#}|1%1-axt{M0|I(rHk4lkACV2}GxBycAk0^c{BHd9+SKMQf$8 z)R-mPiQRz0Vn<)PuIfxS(}#I>MY8U7vdWiamG7Q>G=m4uhkbT%)njY$-o3iA=?|ax z4xd!Q_xOW~Y#Z`62c-=(`du?&hBERmQ<(s~hD;wt_tOy8j`GK(tyRuf4o{%3gVWPZ7l}NSJnMfS; zH_Eu%_K=e?3M}mi-gP(zot8K+U(3eN7*1pOQsF-BDnDPR71poV*=S^~w>34A{VTGL znG?5rNoc{=BMMm6zIFr>m1=smL&wQwYlzhf4brQ|73@~vk5$7l z8hL1C38i!?WY_kKxL;7%^f**o=5t(|y^r}rw()6Kw!<`-!j_W; zRxxe*F`B6QE(du@OgPVmH10`<|H*&l-BTY%yH0oaE$FK#JuY;9I_L9N1y)CKX;OjZ z$z;n2BhQY!!)yXP=D@=xwLtI4STE?`Zf@$A$kKa)!FqWnTJ<8wX+nt`)Edfp zKM{@&JI^cKAZpR4oHH7!&NvrqP1LHN|BTOT`+2+*bI`KQ)C3$Lx0`AirEZIs2TyLN zSMh)7T*Nyqwi%xiR7M>XMFmpsXD`~8Zh>n>u43NciZln--DiPIwzW^rpIwxt1p7_x zKHs3;lHy-~%_*BMseCZ3y=Wqcwpm-<21JSPJVHJ-`G0P$U^$tOT0b}LFxklev7?~) zn~?Zl80}vz=vuGm_URx;kb9Op|)bNc_ zjn#L!$dIL<0!z=-SUea(fT*m?R+Ya`D!AZQ6Pi{u;n!k_)ZR9F)Em z#f?y%ON)C8v#nOcUzd(Ia~Vu2U2?47eo?Ym`rt~vwV_p-np@2l`$?{9a!c1Dv5|ZB zHqLuQvVFWPVPV*I9cN>C~B1z46jhoKr&q5 zPHIBtwDb}Sje?GJI!x4k-;()b?+n5)<{nYg@^u{LH1#TVl&-mBssrsCD?fTXsB5ad zS{0`?F@r~+=XIRYbS)5E}Se_$wTT6P@9x zYNf%6*(q7zK=Mi?x$s)x*1Hu|nQRO0`NQZB0S~6)j(&SACgv+o78fcl>5<;=t!LQ% z6*A6)<9#!j=11->Ve$qkMn&Te+6O7N7iMnO0G(T|tkcOu3YDOzIgQJ;b(J2}`j2LZ zKrYe?<-8~;X#H7!O%p_zD9i|VcoPX${qv&CwNehyWIP+#3y!rvLx?4s9QJm!S@}aq zA9onr8Dt}PV%WN_C+$6D|IW@+W*4W#rxzre+-fB-LdWkxI%ifjxtDHZ@o)QWgA-Zj zEy(8Dli4eu)zS`&oDLFpc@RlTE<7#cS@h;&#=R1 zJI00xd}E`z&VzdE?JDG=RziEK(OVyMm_RL}GWcc}l3KB9WG{M#RfZba2 zOedcs9LOGO?z~`RNPdS(iY1A?Ebnmxh0N$S1Ee=yhoCtKwyC=1%fXk%ZADl9y&A&s zi30GJIwotpTxU#3xP7_UCW6{SseOjUGc@10xt-hOD7NwL5W_JMqm!mu5;9sX&%A>asw2Mu@m$iKEy-QRB&zu`itIjnUid;$%AxXP!|JLVx(IlR6d_$mxu2?hbJRpuLMyL<(Txn2RB_7@xQsDqOZ0dM1=t!-ja=v~c zD2?6lec8f3HK-0#H)&5py?Ux|d z9_y}eJlKti$~SJFvzC11!q3NM#kGZAf*;N^4mGZ|qAP3ES6^)gz_%0gsFI`8xs%r^ zi2^})IeJ%hp(#Fh$GT4)0C#RW%d49Ix5pn2@!6EhgVmy>bx(C@?@z2#k`+$F@8H`c zWuodvVbBtCy56*RE!XZVoVq)AEs{Wmo!fb3*tGS>+mWH24-S#bu5%b{I!qkkYO6N( z#Zj((mA6qn9b@)xU}3?tjKO1~Bgu)qmBVOJ?oLg?E;VthQKP&7$lvPh_@OOCXS#lh zIdL9QnXyk6QG{424_>L*H-SDiii!SJ*me|ZGnpl4YM9%tW{4G82%D=hY{N-3qKhJ& zp&P5vuxNCWwlw-s2S_ms+~bV;YyR;{_L4CTh#dM%fI*_qLzEYGxi2+Sp~OU1!FK3f zlIVTTE0)gOkkym`^bxtC(S<^m-C!5Wxp9JaxH|H`d$0_yR7$lU$bGo9X1^M>Ahhak+NG*7g>t`Ub3)2wJ85dx!e;e{1$f%<^Do2Z%W0xl17{Ug;-N0!VFUHd zpzIc=y=p!{rCeEr#{o5iPYY3 zCwTuUK4zyHERV6cDY_*Hg9!`{(5$UrK5H!PSI!7tclK@)Q(MN+k&@tYK5G8*ALRS% z<=)~NXr$EfPW?&>o{V%RsHa9rQNOzHyfj<%88GywSw&h43Axi5CreO@_d2y?afB-~ z*{lY%UO{(z52-KotZ$Tq*aF6K{_x&vc!s1lU5QNM1OhNGtzXdSXLrMM+AgIZ7V4yL zABzU+oaw_`KX**iGu)IYzZ_k=fV|eM$os z2bm+A8=vN!*HFxKsJ`WTd}}6wN0qLG&^ouu*WV6~FER{rakMHdE$#1p2&yLNyaiUd z>kkyGdo;4(*6xx@nTwMppyQuZ)TT1%1myVGS(CiMICEadC$ zaBg$-^dn?ARMW1W@dgk*ie$ulJ<#*MTX;{-e_0$%up~+4oW{Rz-(7S1$3N-d_LH-M z@`y*qF8}#kl5#Et-^O)DG7TXw+263#^uro4k4dg=0imjr!1WG)QC22Rh zco7s5?=X`}1pn5bP1XEZ{Klzga=(mTRbgSXs^P=pM!)!^HV-eQ?8XKwILjeV@}FXV zkm)dv|DCPTQvSN-x0Qu74q-r4O3+G;aF!<0wzMKNB9Hv8?~?fQ9l%&HS-S06|uYR^6Sg=_DY9i8=Qwv`V%;PynEoW`H=}K&O z8Vqf{@ldaTyp3yxtS(?}Thx3S8XX&ZalluFgwn;k^54X3cQVx4 za02xkXDf;)L8-M1oB}0(4dWlXB%3%ldE21#*v+7&4_;)l3@c(77h^yCvR*?s&Lhm` z3P-^6Lr*FhNFKu|4%Q$oaalkp&;oh$+h|3=xA z#{aH=L~awA8;gaN*iW$I>|Y9p1grj7jRiMZ{__L{9so^#tT6zcL4WK&fvK+4<0x?9 zH;QVMW4wEf7#vw&0_Mz5E|*{7*L|X^Bsjql8}u!Fp7vRGgFlPg;CArKGZ1rJ8~qw& zhxODt@}uJ+YX#5y_v!=*^~X_dEs55=@#c5!cA0d3P{}~+!m5UcWDaHT)EWIo9}L|o zFrD?EdEofYZvHiSF1)v9cy9N)wVF6{@48B3hRYX|%Y#KM@+FXEc+-tMitAh9^(dwHqlI_9 zY9I*uRCsfKTD53@$YNl7P8p2`FtCWdPl>UP**J=QSPiPJ?jXHknfU9rXvty7Vv5WV zH$$mnwaBdsQ8NtXgLU(?iNF!`$ir&pY^plrhqirgpvYkOoi^=k#egJ-=xuq_Xu#>> z=|DD>`7$s%^B7u3qg%9k)f^|9mSgX+3(Uu|CE!Dp?8M)>IiJ*-gbj6Rv-Pa}os`b& zP>lx7eDe*y_Q4;}M1JW|LJ20;a7wG@Lqzk1@tE> zf7>4Yd}|wBlTA1$<+srGw~lz-af4miJy>TSbp&Q$jxQ)?Z|rWYudKE zC>?yRw|kXeenC{G)!Z5zSwX2F8DL{~`FB!If54_tJpV2c*J>?Xh zw->BTov$DITYsV#7XFnWl-fP7Mf?pzkKJnRFtLflIh?8IUFN(=_DZ@6WFiRq2? ze{Pi`RU^3CVdoG8RO9pGt$?!*f8w04?Co*X04HPHvtd*&;1?25Nh@C0t8jNmm_j&^ zf;m$}k&AC~h;@(*1w)sg=R%CDaFk}`E-;3)L!6dH>AA|E8Z}<2HXIms%yj}Dv=2NN zJx*g2825GC{>yUhF#sf&@5c7kMWmTQ%1r8=PhtWt5tB56!S170p*Jp1p{}Vvr9*9g z>FsV%v!f}t-Dhyk9=9IKN$D78W2MdlYIoWV)h10{8i&qp6A)6<(x|g7ZOVg3D|x+|TWF{t*( zTMO4UrJq@iTx3oXz+M~jD5hq=adwc*Vg_R@9Z5CF%mTwld*GmYjFFYczYIfRbow%4; zQb5GsDP}VZ!)({nQ(%yL}+{+Pmc;2Wn|n4q~uLEx@B_oA%~@x1l(F z8s(=-a7AzZLuBw_u zrzG{F_0~B?>$#N`*AA?}1^Z0ZElk@!63R+6;ZY=FLBy)`9OKIN9Ht`HC#Z371ItZ1 zvoF^iW0rix^m~7RabQ!XeQ=>ZFosv!?#g9|Tr@Y~QozUTfId_3ZJfP`aP` z^G3%_ax^zLX8hL1j7?72w1`^y6=puA>Z`GQdSz#kd@5B}IYrdUuFLZ2l<`F8 zQz++c&Zkk`V^c)0jMq4yMxN)T8l`OSN;NvwJSfHSPC*GTZa$T=p4xmWRlKqJluk-` zMe`|?jaz>zredCrR~=Ks8Wtw)jJtYI%|89DMl1h>x4s27*O!#xuO6kmQ_1pF>P30v zbC|}%D6beozZ#}{RGY{^cz_Zt}e|t2F)q2 z*8=Z0wH4;5@HT`k>)~c&R9f#JF^k>-?U_O8j46MK3L@3xlgrTcYm9gU`%G`gVUqNX-%wGJt&(!u4o=v0EMc5B`o3Ze``ELiw-N)05WZAa3^Mn4nr?TXINl_T~7%RSnybLvph( z#}fUuJxLV5va*=_?ts@0OTKeF=2}Y;ey4xz%Bl)LUVTX4+i1K7CGp6v*ga!E>_J>t zHoLOo_iOLlsm@4NIE{{o`}+xb^X4r7c3k7^$5cG|bt!Ln#dpOX%O|1_-aNt2 ztmKv6Cob9JSEeuaIOpw0$a@2i$~E^yAJur8IOXZVW4hoPyOTIOg2+_<+Ur@#8Lxk8 zTTVdVydzp}oc7mf9BVYBmY!$klQ)0MLaKUw5A8bC#w*o9Qd#B9_NVHNCzb1R+G9P~{_2hFUQmX1TCXy3wy8H710%Kq|LCup{svrQR?z%A zYcDxlP_6B8O10O#IYVh&w?QM`gG7JO;h)C)az+$ZP>*b-GnEP2ahZWqBHH2=f~@yH z@{U%P@f53|AfAUz+^SwXWg{bdt7d0iN9omF<+Cqvp8g4(o|dTgnv@}lewXzj>NZIp z|5|#qA7dufzDg(vSgSRvywUW0-Z-?`hDjX%v|@kxRuoo( zMp+ka_iN0(y*48~(dvq-u}n~9`<$)2tV^`6nMYhDv&T8NRJ>gnS6)lD^9#hTh?e|? zEvS(5@rwGJl`M~u(MQpL#OM1F<|e`ad*fSodtQWdMd>R~>utjXLY+c+X|`8?2Z-+w zb6bQsxGT)prwIgGhF^#z-qL@h)n#gi{|Ivm_4En$sNG@FeM{Z(-{IetpSyi5{kTs3 z&b0l&`zw(04v5<|wVQ*A-XlC$%>Fb;JQRt($kDpVvYRLF*JW&&qiseHl0G_ zJAp=cwr{Xka0^Y>{pY|qeQckeLgyMTSph8{VHZGiKR zJBN_nJ7ZgSaC(2`AfA7&*AGctbKJ9iuiY?k7ny6iTL9xZY&}I9xq)og`wLMaGvJHj z+e2=nbE|dt_&)3@P|}LS6=c%)aKfih=iZ-ui_?`h-iP+chqmxW&ft0rYjVGOdkZzn z>5iv6=;V&mH9T&3iA{7rarB!TXx?!pmB`7Sm4(hNn=2!MBW-_Pa2J_tx}#Z?yfLbB zRiy2hLx-^5YqvGrMCDrhF0^~i2iV2WxmOZi7jdq~`gVp_#i`P@GP9MOVXRSgTAERcgc1R`0iVu_r`9ba?3i% z3`_K#i+cleZu5Eqmb@nJ3d1qq!(4WV;ApjT`3-knsn5_3rYk(Oa_g=|x{m3S{JS2xD)#7@b47JK zS_PhY$LGH0tSr|!VW)Tk{IBb_p=`4^j6?OEt`MDa_S=6EqCjT2-yb+}6X1_TUW9xE z@~61%lDspd?h5tr*^F1PJ)U95xJzSlSGVQ%_-;(dH&F#v86k5P-yb?lq414ot<)ASqTziJ#$6YuBcB^T|ZzX|&Feqc546QoXH9y5T+6a#L)@H0Vy*<5U@=TK8*TaMn>VmVV>DPx~es z_sZ)+&k=$FpVeM5x9n^j`S~}*F8qA;>6Le;owtgJy`}d1K4z<1aLLNYak=(1<*2xX z?uJHA3;8qcogciO)+QwV9F}0g`5+#9BMtQ>KYM?sQ9DDN)zW*6`-eS^v*a`26OIks zsqe#HD0!b}$kSxT6CYV9^E5mGt_kQp&&>Yv9JK|AgZd;d7mfZ#^7G_-Z~1&pe@FjJ zjcV$>hg#0ryzC}-oca7aR9ASl>f=-7Q4wvwWkjB2Wi_#z*JZG>Y+EafaJqWpmsJlq zTj_st*ZY0-_g#i0+199+V@v$ich8ZlvsWTJiOh6B=O8nT3;85{QzyfR;6b}ws!Bom z2IlAq=8QOrDam(>GPfK6ffz-y|5Fbe;&a{SvWb5qEjLP(nkU87QxX3UE4nXN{pk#I z=p|+Wx|us#(Z2Aa2XWUmIM?*vWBKT=>==J1SNGb9ThXk%AKxlGM&#%v`hS4-({FQA z`)+0MQ1gYK)AAuWQvRxYoSw>W)hFhdd*Qw18S0Fwr`a@h8au!lE!{}mKsMa33iL?w zg@z`>cqMg*wI|6~jP=9|SjP4I0u;q;>PA{3F%NB2v_?XzVJT}R@1IL<={MZz+v9&f%>Xy=MBuYU$8ZMhM&@|QQJQAHnl1`AMpKV!o~%*rjgjq$+>y{W!M7zE+B8KU6P8^z$`ijv={*lb`0T@_yiK zaP(*0;*r%*Z;;jav0&|(ZXflvj(R$q%NK=Q(rjqJbAK8F(!S0|m!3GLV@cwOsK zmv17c9?jh8`;p#S1+t#E6{C4lBI{ny$a!Tx-=!4W$ag5_w*O`+_VMk#7yN&h%W=$m z%ZE#$kDG9{ewcBjkqc6iM>)VMb`WFxZ*DiU;+Q>|^j3`6D^{+?N)KLP`G&1M-Njnf z#z{&%VpzEvhxDB2-@nqn&-2#M)2~yg$)P8|Nug&x@kV>1Y>SBf=@rfj7U=!w znpbxS-;BHsnJ4ix?}QQ2&nth!SO!VMNbcj>bnRoIyveku4pFIW%yrXTz|yT|q+jq3^GkZ4mE*Vwg?z3a zjydnauDzfTQL|d<JbJu^&dYe21(+qQI z`+Z~wE3$ckEor{D(CIntut$x}J>^@7to}@^OgUvQ0%e%*qNQk+aLeZrBR>1?@ff?p zimqE+bF6LVd`{ThdZv6ks!GL6eB;9CU&qwM=r48~X}bxwcyk?Q^6<{;bj-p10gm*Ky zq*maKO3f}^qKja$&Yi(_2QT%dp|h}aWHVm-H?=P-Ytau%tSZ}CeZMnWNY*$N*{C>s8!M$WOxZoUy^)??k3Zx{MZ3Bxi|2nj_BZ6O0Up+~tD_mS zXC`u>kJC-?ZnScYRa(fk&F6gQllT)Vlpa5&u0CcBRi%>ER4F!- zV~}2YFCUDwi2I!Dp^QD60m`5#vm&8e3}vWg--MZSR+u~VSNYufEhXyLV|>2!PK>wA zmRWx{ne}}TQ=-f+7rA$zOoSA(duQa~(w~<4%0f%p(?3x+9=5Swxxg9tcD!Hd;ZCZl zPaB*>EyaIN=-JKi=tH|rP~OA2&J?%BW@v*Zxa^}>%liFCuzoXGgC@BB%=c?(&V3IL zT2s6Z(drZLJl3c5y`;C-LwHeUzd7Qedhqv5U@K_v>s#eDz^paJ$oXhpl2hbizr`8L z(dvKGM%qTK)E8I}h@K$lP1~}(=QHF0{alVF`_g~teBQ=49Gd2I0&9QCwk_m)>=(GP z9kJC;$MAXl|0e9X-QK=#!|l?S_S-JiG}~>LL!RBX%PHGt+vSsQul>J`z4jZo*S0yN z+iJfBTiqNF-A*^hrJRjc{MDNLbMMR2>+~6CIksbO1J113<1;hcvvMD&J0bi4P4Rde z`yPKrY*~b!bw8ACeoI7wO&XNx0K2dNJ!h8SWZx2BVaG_fgPM{k{Vcs1UdgkCX1M&0 zIOe^J+?8CtTa_qA?C>_RMd=p7|}?Grt9U=Kt;Nnct#4^INfJ zzEv)Ne|zRPZqL>^6s_mZF{aNgt>|`OEiQl9E39rZH{qY4JUkN~Wyf4w_B_sCv}sk_ z`fZ8{G9%u=;*<88k#CN1O&)RjJ-`^TE!#dgQLh-o4{v`(G(n?^KcMwRSvBu9Sh z*xGgN@kv>hU;guk$JP3XZ*olet&S<%+_J}$-}rd)8yioyDHn|;zm>7H&ZUZ%VhDdM zGiZNm1!vy*y>0ul6{8mtb$3>gpP9qdyNOHMr+VkZ7w}_W^A2Qa$e5lJ(_Umju?Mxw zxQFuo(QZ+e@e|udSO;l2OWma|^DTWuZUJTuX_=dvYz!KI(58F#Wa7*d6bIiRP!1YQLEzVE}~V{`%^@(jPJEf{8~`T z>s>^tyeFuLQZ;W+5w(+29^@h_Wt%IO)FO}W~_)xwV1FXYE@#sifC1h z=_;aEH4dPNUiK>fIkMZ;rHJxFPxT%an}LqcsZkvrBJ!U&p+O8|{CQuJP2w zn?8W@w+YH0VT&fX4_=q5zlQgUtM!bbiTZTkBPC7gNr{Y_`e*BR4xkrgc2dJa<69@NQd^M{#UI9@3SpOC)wcZOOIHxx9R3xrto-ORRr*#n}MuESi-s zjbnMas!{)I_57q&d5B{(@=Q=$woQ2?$7baDz;&J5lSguFMxG`5a=Q{Kp5C zB~Y}||HaR~KtzAVCvbYo-#%MCxtUQ1FeyV4{VwZ+>hno1|5`oqZ(#fh^}i25L%x}u zHT2n~O!dC%Ep)L~+g16^>G`~IX|o-ZIR0tLYAq=&2935Z+V9txdwcChdZN`B)zF%t z;`TY)dr@I-GgUK>;>w_uNl(RUhP)0Rr?mA8#4m}~{DpsQ>2uUs|KPpxUdDC*pK4ql zaok0OSxcb!-uTw-J{aMIRC@8#`rR;rQm0T}qV3h+0b+T?j29tx@{0S}X#&HR;d>&f zw={`+hFncy7-6oVoSQdWV05 zcwOZx&H=rUTpSe58^qs{%*HMTZ#W%ny%$;NC?wzu~JBUNLaukyb z>y9M0Ic|%;*KTdNjm||KE`akKa{@(Cxr1)k`wKBIGhmG3J6rA}bg!lN_~!O0kkU%U zC3Jt%*KxupXXoCZd?(nY_TGmU$&a`2MowXS3rlmqdVg#6RKz!h-9jjLv@T+D$9rue z{E1`#+(Gn?D>X&V_OvX7?%8D-869c!hui2})FI8f& z(Kjxkn!pztI)!QBl--gZ#U1o5MZq=uI^cVSecpe&i_AUCA@hRKcdqU=%zb0JN2pu0 zw11TO^18(Gj&GI(-rN}+a?R@+9v8-=?uR3;1Mg#eF|R4@9{YjDUN6qDa|_Ex^!$qS-m-u81B|c{ z+INDi_X^{i$`}{i0+fwM-6r3x+*9bxW*oO^#5ERFFP1cWxM|sg4qiQ&k3t?Mv>^Fx zdT$=Y#cY2`_3QZe`Ykw3;j`(zUc>)QMf`ov#XRAfHI#c^w(U0w`rlf}uAU~zR}bVP z&WA1$S3=qvmE`-f#=IBcoRNPrZCt@8U(X8dppCCVY0g=qX4<&&EznD1d}V>=9dcnt zwa34r8Rlqn(|c)!ZW8UA$}O(XKM!dilTuRNs<;k0`vI*!BQHpxdu4%M5~B^8$@cIq z&btjQrML)DQ_H4HAe_P_=Fw(GcGTA}*z(W>hFEHnDW8w)fas?&conqu~pI-z;t zp*k+dB`Hf)F1zua_$kk33)r-J)M%HnT%+^4CY^fYR-w3rc0R`0BAP1~f` zv=dp2Bi_ZGxn%pxpJFc_a;CKl_?~|+61ODflLPH3-FG*I zK6F&O-Y&f2Cmf|9R!sWcsca<4{ADU{nVb)aXOB;=$7m7LL$9*;?XhM<)*fk0-kCj% zbD5o`(0Eb-rFcVn+m|uw1?0}d7o}#UTtB5*dSR^H^nAC!Y8+1!@8{Ln75zrZ5ym;- z#6I&f{>02^O;vwDgaureEno9#)Xq@lXgx2fqM%E=foyXs&-RvzPUY_=I1`h`2IcncBy}!h0^}ekVEBhqMWSR-#$y! znNqe;^vR8!H-E&O^b%PJm3&1=UTVA^cdZ2qGPNVmcS{cQaO0W@Su zs-SMITKDIb44Cx)nUsnH~u73Wu`gC{o^K@(V)B&FO3{M?+d-(Sh{|-T3v0Z>Z?1^@s601f~S038MZ09{`J0001d>9v;t diff --git a/Artemis/Artemis/Settings/General.settings b/Artemis/Artemis/Settings/General.settings index 521a80d33..326ce89c3 100644 --- a/Artemis/Artemis/Settings/General.settings +++ b/Artemis/Artemis/Settings/General.settings @@ -1,5 +1,7 @@  - + + diff --git a/Artemis/Artemis/Settings/GeneralSettings.cs b/Artemis/Artemis/Settings/GeneralSettings.cs index 296ed2f52..573f4e20c 100644 --- a/Artemis/Artemis/Settings/GeneralSettings.cs +++ b/Artemis/Artemis/Settings/GeneralSettings.cs @@ -5,7 +5,6 @@ using System.Runtime.InteropServices.ComTypes; using System.Windows; using Artemis.Utilities; using MahApps.Metro; -using NLog; namespace Artemis.Settings { diff --git a/Artemis/Artemis/Styles/ColorBox.xaml b/Artemis/Artemis/Styles/ColorBox.xaml index 40a64f1fc..bbd797280 100644 --- a/Artemis/Artemis/Styles/ColorBox.xaml +++ b/Artemis/Artemis/Styles/ColorBox.xaml @@ -84,7 +84,7 @@ + BorderBrush="{TemplateBinding BorderBrush}" /> @@ -491,7 +491,8 @@ SelectedGradient="{Binding}" Margin="0,0,0,2"> - + diff --git a/Artemis/Artemis/Utilities/ColorHelpers.cs b/Artemis/Artemis/Utilities/ColorHelpers.cs index fb96440e3..0a28d5a77 100644 --- a/Artemis/Artemis/Utilities/ColorHelpers.cs +++ b/Artemis/Artemis/Utilities/ColorHelpers.cs @@ -33,7 +33,7 @@ namespace Artemis.Utilities { var colors = new List(); for (var i = 0; i < 3; i++) - colors.Add((byte)_rand.Next(0, 256)); + colors.Add((byte) _rand.Next(0, 256)); var highest = colors.Max(); var lowest = colors.Min(); diff --git a/Artemis/Artemis/Utilities/Converters/JsonConverters.cs b/Artemis/Artemis/Utilities/Converters/JsonConverters.cs new file mode 100644 index 000000000..cfc95ae1f --- /dev/null +++ b/Artemis/Artemis/Utilities/Converters/JsonConverters.cs @@ -0,0 +1,43 @@ +using System; +using System.Windows.Markup; +using System.Windows.Media; +using System.Xml; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Artemis.Utilities.Converters +{ + /// + /// Stores a brush by temporarily serializing it to XAML because Json.NET has trouble + /// saving it as JSON + /// + public class BrushJsonConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + // Turn the brush into an XML node + var doc = new XmlDocument(); + doc.LoadXml(XamlWriter.Save(value)); + + // Serialize the XML node as JSON + var jo = JObject.Parse(JsonConvert.SerializeXmlNode(doc.DocumentElement)); + jo.WriteTo(writer); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, + JsonSerializer serializer) + { + // Load JObject from stream + var jObject = JObject.Load(reader); + + // Seriaze the JSON node to XML + var xml = JsonConvert.DeserializeXmlNode(jObject.ToString()); + return XamlReader.Parse(xml.InnerXml); + } + + public override bool CanConvert(Type objectType) + { + return typeof(Brush).IsAssignableFrom(objectType); + } + } +} \ No newline at end of file diff --git a/Artemis/Artemis/Utilities/ValueConverters.cs b/Artemis/Artemis/Utilities/Converters/ValueConverters.cs similarity index 78% rename from Artemis/Artemis/Utilities/ValueConverters.cs rename to Artemis/Artemis/Utilities/Converters/ValueConverters.cs index bd60915d0..7af474bfb 100644 --- a/Artemis/Artemis/Utilities/ValueConverters.cs +++ b/Artemis/Artemis/Utilities/Converters/ValueConverters.cs @@ -1,66 +1,79 @@ -using System; -using System.Collections; -using System.ComponentModel; -using System.Globalization; -using System.Linq; -using System.Windows.Data; -using Artemis.Models.Profiles; -using Artemis.Utilities.ParentChild; - -namespace Artemis.Utilities -{ - /// - /// Fredrik Hedblad - http://stackoverflow.com/a/3987099/5015269 - /// - public class EnumDescriptionConverter : IValueConverter - { - object IValueConverter.Convert(object value, Type targetType, object parameter, CultureInfo culture) - { - var myEnum = (Enum) value; - var description = GetEnumDescription(myEnum); - return description; - } - - object IValueConverter.ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) - { - return string.Empty; - } - - private string GetEnumDescription(Enum enumObj) - { - var fieldInfo = enumObj.GetType().GetField(enumObj.ToString()); - - var attribArray = fieldInfo.GetCustomAttributes(false); - - if (attribArray.Length == 0) - { - return enumObj.ToString(); - } - var attrib = attribArray[0] as DescriptionAttribute; - return attrib?.Description; - } - } - - public class LayerOrderConverter : IValueConverter - { - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) - { - IList collection; - if (value is ChildItemCollection) - collection = ((ChildItemCollection) value).ToList(); - else - collection = (IList) value; - - var view = new ListCollectionView(collection); - var sort = new SortDescription(parameter.ToString(), ListSortDirection.Ascending); - view.SortDescriptions.Add(sort); - - return view; - } - - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) - { - return null; - } - } +using System; +using System.Collections; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Windows.Data; +using Artemis.Profiles.Layers.Models; +using Artemis.Utilities.ParentChild; + +namespace Artemis.Utilities.Converters +{ + /// + /// Fredrik Hedblad - http://stackoverflow.com/a/3987099/5015269 + /// + public class EnumDescriptionConverter : IValueConverter + { + object IValueConverter.Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + var myEnum = (Enum) value; + var description = GetEnumDescription(myEnum); + return description; + } + + object IValueConverter.ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return string.Empty; + } + + private string GetEnumDescription(Enum enumObj) + { + var fieldInfo = enumObj.GetType().GetField(enumObj.ToString()); + + var attribArray = fieldInfo.GetCustomAttributes(false); + + if (attribArray.Length == 0) + { + return enumObj.ToString(); + } + var attrib = attribArray[0] as DescriptionAttribute; + return attrib?.Description; + } + } + + public class LayerOrderConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + IList collection; + if (value is ChildItemCollection) + collection = ((ChildItemCollection) value).ToList(); + else + collection = (IList) value; + + var view = new ListCollectionView(collection); + var sort = new SortDescription(parameter.ToString(), ListSortDirection.Ascending); + view.SortDescriptions.Add(sort); + + return view; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return null; + } + } + + public class MilliSecondTimespanConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value == null ? 0.0 : ((TimeSpan)value).TotalMilliseconds; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return value == null ? new TimeSpan() : TimeSpan.FromMilliseconds((double)value); + } + } } \ No newline at end of file diff --git a/Artemis/Artemis/Utilities/DataReaders/MmfReader.cs b/Artemis/Artemis/Utilities/DataReaders/MmfReader.cs index 744b9576f..51c0aaf57 100644 --- a/Artemis/Artemis/Utilities/DataReaders/MmfReader.cs +++ b/Artemis/Artemis/Utilities/DataReaders/MmfReader.cs @@ -4,6 +4,7 @@ using System.IO.MemoryMappedFiles; using System.Linq; using System.Text; using System.Windows.Media; +using Ninject.Extensions.Logging; namespace Artemis.Utilities.DataReaders { @@ -12,10 +13,11 @@ namespace Artemis.Utilities.DataReaders /// public class MmfReader { - private DateTime _lastFailure; + private readonly ILogger _logger; - public MmfReader(string mmfName) + public MmfReader(string mmfName, ILogger logger) { + _logger = logger; MmfName = mmfName; } @@ -54,8 +56,9 @@ namespace Artemis.Utilities.DataReaders } return colors; } - catch (FormatException) + catch (FormatException e) { + _logger.Trace(e, "Failed to parse to color array"); return null; } } @@ -67,10 +70,6 @@ namespace Artemis.Utilities.DataReaders /// private string ReadMmf(string fileName) { - // Don't read the file within one second after failing - //if (DateTime.Now - _lastFailure > new TimeSpan(0, 0, 1)) - // return null; - try { using (var mmf = MemoryMappedFile.OpenExisting(fileName)) @@ -85,9 +84,9 @@ namespace Artemis.Utilities.DataReaders } } } - catch (FileNotFoundException) + catch (FileNotFoundException e) { - _lastFailure = DateTime.Now; + _logger.Trace(e, "Failed to read mff"); return null; //ignored } diff --git a/Artemis/Artemis/Utilities/GeneralHelpers.cs b/Artemis/Artemis/Utilities/GeneralHelpers.cs index 8dc160b6e..3eb2c5ac2 100644 --- a/Artemis/Artemis/Utilities/GeneralHelpers.cs +++ b/Artemis/Artemis/Utilities/GeneralHelpers.cs @@ -5,8 +5,8 @@ using System.IO; using System.Reflection; using System.Text.RegularExpressions; using System.Windows; -using System.Xml.Serialization; using Microsoft.Win32; +using Newtonsoft.Json; using static System.String; namespace Artemis.Utilities @@ -39,7 +39,7 @@ namespace Artemis.Utilities } /// - /// Perform a deep Copy of the object. + /// Perform a deep Copy of the object, using Json as a serialisation method. /// /// The type of object being copied. /// The object instance to copy. @@ -50,35 +50,20 @@ namespace Artemis.Utilities if (ReferenceEquals(source, null)) return default(T); - var serializer = new XmlSerializer(typeof(T)); - Stream stream = new MemoryStream(); - using (stream) + var deserializeSettings = new JsonSerializerSettings { - serializer.Serialize(stream, source); - stream.Seek(0, SeekOrigin.Begin); - return (T) serializer.Deserialize(stream); - } - } - - public static string Serialize(T source) - { - // Don't serialize a null object, simply return the default for that object - if (ReferenceEquals(source, null)) - return null; - - var serializer = new XmlSerializer(typeof(T)); - var stream = new StringWriter(); - using (stream) - { - serializer.Serialize(stream, source); - return stream.ToString(); - } + ObjectCreationHandling = ObjectCreationHandling.Replace, + TypeNameHandling = TypeNameHandling.Auto + }; + return (T)JsonConvert.DeserializeObject(JsonConvert.SerializeObject(source), source.GetType(), + deserializeSettings); } public static object GetPropertyValue(object o, string path) { var propertyNames = path.Split('.'); - var value = o.GetType().GetProperty(propertyNames[0]).GetValue(o, null); + var prop = o.GetType().GetProperty(propertyNames[0]); + var value = prop.GetValue(o, null); if (propertyNames.Length == 1 || value == null) return value; diff --git a/Artemis/Artemis/Utilities/GifImage.cs b/Artemis/Artemis/Utilities/GifImage.cs index bf06ea040..085e51165 100644 --- a/Artemis/Artemis/Utilities/GifImage.cs +++ b/Artemis/Artemis/Utilities/GifImage.cs @@ -8,9 +8,7 @@ namespace Artemis.Utilities { private readonly int _delay; private readonly FrameDimension _dimension; - private readonly int _frameCount; private readonly Image _gifImage; - private int _currentFrame = -1; private DateTime _lastRequest; private int _step = 1; @@ -19,7 +17,7 @@ namespace Artemis.Utilities _lastRequest = DateTime.Now; _gifImage = Image.FromFile(path); //initialize _dimension = new FrameDimension(_gifImage.FrameDimensionsList[0]); //gets the GUID - _frameCount = _gifImage.GetFrameCount(_dimension); //total frames in the animation + FrameCount = _gifImage.GetFrameCount(_dimension); //total frames in the animation Source = path; @@ -27,7 +25,20 @@ namespace Artemis.Utilities _delay = (item.Value[0] + item.Value[1]*256)*10; // Time is in 1/100th of a second } - public string Source { get; set; } + /// + /// Gets the path the GifImage is based on + /// + public string Source { get; private set; } + + /// + /// Gets or sets the current frame, set to -1 to reset + /// + public int CurrentFrame { get; set; } = -1; + + /// + /// Gets the total amount of frames in the GIF + /// + public int FrameCount { get; } /// /// Whether the gif should play backwards when it reaches the end @@ -39,23 +50,23 @@ namespace Artemis.Utilities // Only pass the next frame if the proper amount of time has passed if ((DateTime.Now - _lastRequest).Milliseconds > _delay) { - _currentFrame += _step; + CurrentFrame += _step; _lastRequest = DateTime.Now; } //if the animation reaches a boundary... - if (_currentFrame < _frameCount && _currentFrame >= 1) - return GetFrame(_currentFrame); + if (CurrentFrame < FrameCount && CurrentFrame >= 1) + return GetFrame(CurrentFrame); if (ReverseAtEnd) { _step *= -1; //...reverse the count - _currentFrame += _step; //apply it + CurrentFrame += _step; //apply it } else - _currentFrame = 0; //...or start over + CurrentFrame = 0; //...or start over - return GetFrame(_currentFrame); + return GetFrame(CurrentFrame); } public Image GetFrame(int index) diff --git a/Artemis/Artemis/Utilities/ImageUtilities.cs b/Artemis/Artemis/Utilities/ImageUtilities.cs index 57203e821..c45875482 100644 --- a/Artemis/Artemis/Utilities/ImageUtilities.cs +++ b/Artemis/Artemis/Utilities/ImageUtilities.cs @@ -70,5 +70,24 @@ namespace Artemis.Utilities } return bitmap; } + + /// + /// Loads the BowIcon from resources and colors it according to the current theme + /// + /// + public static RenderTargetBitmap GenerateWindowIcon() + { + var iconImage = new System.Windows.Controls.Image + { + Source = (DrawingImage) Application.Current.MainWindow.Resources["BowIcon"], + Stretch = Stretch.Uniform, + Margin = new Thickness(20) + }; + + iconImage.Arrange(new Rect(0, 0, 100, 100)); + var bitmap = new RenderTargetBitmap(100, 100, 96, 96, PixelFormats.Pbgra32); + bitmap.Render(iconImage); + return bitmap; + } } } \ No newline at end of file diff --git a/Artemis/Artemis/Utilities/Layers/AnimationUpdater.cs b/Artemis/Artemis/Utilities/Layers/AnimationUpdater.cs deleted file mode 100644 index c4430b174..000000000 --- a/Artemis/Artemis/Utilities/Layers/AnimationUpdater.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Artemis.Models.Profiles.Properties; - -namespace Artemis.Utilities.Layers -{ - public static class AnimationUpdater - { - public static void UpdateAnimation(KeyboardPropertiesModel properties, bool updateAnimations) - { - const int scale = 4; - var progress = properties.AnimationProgress; - - switch (properties.Animation) - { - case LayerAnimation.SlideRight: - case LayerAnimation.SlideLeft: - if (progress + properties.AnimationSpeed * 2 >= properties.Width*scale) - progress = 0; - progress = progress + properties.AnimationSpeed*2; - break; - case LayerAnimation.SlideDown: - case LayerAnimation.SlideUp: - if (progress + properties.AnimationSpeed * 2 >= properties.Height*scale) - progress = 0; - progress = progress + properties.AnimationSpeed*2; - break; - case LayerAnimation.Pulse: - if (progress > 2) - progress = 0; - progress = progress + properties.AnimationSpeed/2; - break; - case LayerAnimation.Grow: - if (progress > 10) - progress = 0; - progress = progress + properties.AnimationSpeed/2.5; - break; - default: - progress = progress + properties.AnimationSpeed*2; - break; - } - - // If not previewing, store the animation progress in the actual model for the next frame - if (updateAnimations) - properties.AnimationProgress = progress; - } - } -} \ No newline at end of file diff --git a/Artemis/Artemis/Utilities/Layers/Drawer.cs b/Artemis/Artemis/Utilities/Layers/Drawer.cs deleted file mode 100644 index 1fe261602..000000000 --- a/Artemis/Artemis/Utilities/Layers/Drawer.cs +++ /dev/null @@ -1,161 +0,0 @@ -using System; -using System.Drawing; -using System.IO; -using System.Windows; -using System.Windows.Media; -using Artemis.Models.Profiles; -using Artemis.Models.Profiles.Properties; -using Artemis.Properties; -using Pen = System.Windows.Media.Pen; -using Point = System.Windows.Point; -using Size = System.Windows.Size; - -namespace Artemis.Utilities.Layers -{ - public static class Drawer - { - public static void Draw(DrawingContext c, KeyboardPropertiesModel props, AppliedProperties applied) - { - if (applied.Brush == null) - return; - - const int scale = 4; - // Set up variables for this frame - var rect = props.Contain - ? new Rect(applied.X*scale, applied.Y*scale, applied.Width*scale, applied.Height*scale) - : new Rect(props.X*scale, props.Y*scale, props.Width*scale, props.Height*scale); - - var s1 = new Rect(); - var s2 = new Rect(); - - if (props.Animation == LayerAnimation.SlideRight) - { - s1 = new Rect(new Point(rect.X + props.AnimationProgress, rect.Y), new Size(rect.Width, rect.Height)); - s2 = new Rect(new Point(s1.X - rect.Width, rect.Y), new Size(rect.Width + 1, rect.Height)); - } - if (props.Animation == LayerAnimation.SlideLeft) - { - s1 = new Rect(new Point(rect.X - props.AnimationProgress, rect.Y), - new Size(rect.Width + 0.05, rect.Height)); - s2 = new Rect(new Point(s1.X + rect.Width, rect.Y), new Size(rect.Width, rect.Height)); - } - if (props.Animation == LayerAnimation.SlideDown) - { - s1 = new Rect(new Point(rect.X, rect.Y + props.AnimationProgress), new Size(rect.Width, rect.Height)); - s2 = new Rect(new Point(s1.X, s1.Y - rect.Height), new Size(rect.Width, rect.Height)); - } - if (props.Animation == LayerAnimation.SlideUp) - { - s1 = new Rect(new Point(rect.X, rect.Y - props.AnimationProgress), new Size(rect.Width, rect.Height)); - s2 = new Rect(new Point(s1.X, s1.Y + rect.Height), new Size(rect.Width, rect.Height)); - } - - var clip = new Rect(applied.X*scale, applied.Y*scale, applied.Width*scale, applied.Height*scale); - DrawRectangle(c, props, applied, clip, rect, s1, s2); - } - - private static void DrawRectangle(DrawingContext c, KeyboardPropertiesModel props, AppliedProperties applied, - Rect clip, Rect rectangle, Rect slide1, Rect slide2) - { - // Apply the pulse animation - if (props.Animation == LayerAnimation.Pulse) - applied.Brush.Opacity = (Math.Sin(props.AnimationProgress*Math.PI) + 1)*(props.Opacity/2); - else - applied.Brush.Opacity = props.Opacity; - - if (props.Animation == LayerAnimation.Grow) - { - // Take an offset of 4 to allow layers to slightly leave their bounds - var progress = (6.0 - props.AnimationProgress)*10.0; - if (progress < 0) - { - applied.Brush.Opacity = 1 + 0.025*progress; - if (applied.Brush.Opacity < 0) - applied.Brush.Opacity = 0; - if (applied.Brush.Opacity > 1) - applied.Brush.Opacity = 1; - } - rectangle.Inflate(-rectangle.Width/100.0*progress, -rectangle.Height/100.0*progress); - clip.Inflate(-clip.Width/100.0*progress, -clip.Height/100.0*progress); - } - - c.PushClip(new RectangleGeometry(clip)); - // Most animation types can be drawn regularly - if (props.Animation == LayerAnimation.None || - props.Animation == LayerAnimation.Grow || - props.Animation == LayerAnimation.Pulse) - { - c.DrawRectangle(applied.Brush, null, rectangle); - } - // Sliding animations however, require offsetting two rects - else - { - c.PushClip(new RectangleGeometry(clip)); - c.DrawRectangle(applied.Brush, null, slide1); - c.DrawRectangle(applied.Brush, null, slide2); - c.Pop(); - } - c.Pop(); - } - - public static GifImage DrawGif(DrawingContext c, KeyboardPropertiesModel props, AppliedProperties applied, - GifImage gifImage) - { - if (string.IsNullOrEmpty(props.GifFile)) - return null; - if (!File.Exists(props.GifFile)) - return null; - - const int scale = 4; - - // Only reconstruct GifImage if the underlying source has changed - if (gifImage == null) - gifImage = new GifImage(props.GifFile); - if (gifImage.Source != props.GifFile) - gifImage = new GifImage(props.GifFile); - - var gifRect = new Rect(applied.X*scale, applied.Y*scale, applied.Width*scale, - applied.Height*scale); - - lock (gifImage) - { - var draw = gifImage.GetNextFrame(); - c.DrawImage(ImageUtilities.BitmapToBitmapImage(new Bitmap(draw)), gifRect); - } - - return gifImage; - } - - public static void UpdateMouse(LayerPropertiesModel properties) - { - } - - public static void UpdateHeadset(LayerPropertiesModel properties) - { - } - - public static ImageSource DrawThumbnail(LayerModel layerModel) - { - var thumbnailRect = new Rect(0, 0, 18, 18); - var visual = new DrawingVisual(); - using (var c = visual.RenderOpen()) - { - // Draw the appropiate icon or draw the brush - if (layerModel.LayerType == LayerType.Folder) - c.DrawImage(ImageUtilities.BitmapToBitmapImage(Resources.folder), thumbnailRect); - else if (layerModel.LayerType == LayerType.Headset) - c.DrawImage(ImageUtilities.BitmapToBitmapImage(Resources.headset), thumbnailRect); - else if (layerModel.LayerType == LayerType.Mouse) - c.DrawImage(ImageUtilities.BitmapToBitmapImage(Resources.mouse), thumbnailRect); - else if (layerModel.LayerType == LayerType.KeyboardGif) - c.DrawImage(ImageUtilities.BitmapToBitmapImage(Resources.gif), thumbnailRect); - else if (layerModel.LayerType == LayerType.Keyboard && layerModel.Properties.Brush != null) - c.DrawRectangle(layerModel.Properties.Brush, new Pen(new SolidColorBrush(Colors.White), 1), - thumbnailRect); - } - - var image = new DrawingImage(visual.Drawing); - return image; - } - } -} \ No newline at end of file diff --git a/Artemis/Artemis/Utilities/StickyValue.cs b/Artemis/Artemis/Utilities/StickyValue.cs index a9ad4c1fd..7df32ae79 100644 --- a/Artemis/Artemis/Utilities/StickyValue.cs +++ b/Artemis/Artemis/Utilities/StickyValue.cs @@ -48,14 +48,14 @@ namespace Artemis.Utilities { if (_waitTime < _stickyTime) { - Thread.Sleep(100); + Thread.Sleep(10); continue; } while (_waitTime > 0) { - Thread.Sleep(50); - _waitTime -= 50; + Thread.Sleep(10); + _waitTime -= 10; } _value = _toStick; } diff --git a/Artemis/Artemis/ViewModels/DebugViewModel.cs b/Artemis/Artemis/ViewModels/DebugViewModel.cs new file mode 100644 index 000000000..95e74588e --- /dev/null +++ b/Artemis/Artemis/ViewModels/DebugViewModel.cs @@ -0,0 +1,58 @@ +using System.Windows; +using System.Windows.Media; +using Artemis.Events; +using Caliburn.Micro; + +namespace Artemis.ViewModels +{ + public class DebugViewModel : Screen, IHandle + { + private readonly IEventAggregator _events; + private DrawingImage _razerDisplay; + + public DebugViewModel(IEventAggregator events) + { + _events = events; + } + + public DrawingImage RazerDisplay + { + get { return _razerDisplay; } + set + { + if (Equals(value, _razerDisplay)) return; + _razerDisplay = value; + NotifyOfPropertyChange(() => RazerDisplay); + } + } + + public void Handle(RazerColorArrayChanged message) + { + var visual = new DrawingVisual(); + using (var dc = visual.RenderOpen()) + { + dc.PushClip(new RectangleGeometry(new Rect(0, 0, 22, 6))); + for (var y = 0; y < 6; y++) + { + for (var x = 0; x < 22; x++) + dc.DrawRectangle(new SolidColorBrush(message.Colors[y, x]), null, new Rect(x, y, 1, 1)); + } + } + var drawnDisplay = new DrawingImage(visual.Drawing); + drawnDisplay.Freeze(); + RazerDisplay = drawnDisplay; + } + + protected override void OnActivate() + { + _events.Subscribe(this); + base.OnActivate(); + } + + protected override void OnDeactivate(bool close) + { + _events.Unsubscribe(this); + base.OnDeactivate(close); + } + } +} \ No newline at end of file diff --git a/Artemis/Artemis/ViewModels/Flyouts/FlyoutSettingsViewModel.cs b/Artemis/Artemis/ViewModels/Flyouts/FlyoutSettingsViewModel.cs index 2d927fde8..946cba921 100644 --- a/Artemis/Artemis/ViewModels/Flyouts/FlyoutSettingsViewModel.cs +++ b/Artemis/Artemis/ViewModels/Flyouts/FlyoutSettingsViewModel.cs @@ -1,9 +1,11 @@ using System.ComponentModel; using System.Diagnostics; +using System.Dynamic; using System.Linq; using Artemis.Events; using Artemis.Managers; using Artemis.Settings; +using Artemis.Utilities; using Caliburn.Micro; using MahApps.Metro.Controls; using NLog; @@ -14,15 +16,18 @@ namespace Artemis.ViewModels.Flyouts public sealed class FlyoutSettingsViewModel : FlyoutBaseViewModel, IHandle, IHandle { + private readonly DebugViewModel _debugViewModel; private readonly ILogger _logger; private string _activeEffectName; private bool _enableDebug; private GeneralSettings _generalSettings; private string _selectedKeyboardProvider; - public FlyoutSettingsViewModel(MainManager mainManager, IEventAggregator events, ILogger logger) + public FlyoutSettingsViewModel(MainManager mainManager, IEventAggregator events, ILogger logger, + DebugViewModel debugViewModel) { _logger = logger; + _debugViewModel = debugViewModel; MainManager = mainManager; Header = "Settings"; @@ -178,6 +183,18 @@ namespace Artemis.ViewModels.Flyouts MainManager.EnableProgram(); } + public void ShowDebug() + { + IWindowManager manager = new WindowManager(); + dynamic settings = new ExpandoObject(); + var icon = ImageUtilities.GenerateWindowIcon(); + + settings.Title = "Artemis | Debugger"; + settings.Icon = icon; + + manager.ShowWindow(_debugViewModel, null, settings); + } + public void ResetSettings() { GeneralSettings.ResetSettings(); diff --git a/Artemis/Artemis/ViewModels/Profiles/Events/EventPropertiesViewModel.cs b/Artemis/Artemis/ViewModels/Profiles/Events/EventPropertiesViewModel.cs new file mode 100644 index 000000000..8b1af58c7 --- /dev/null +++ b/Artemis/Artemis/ViewModels/Profiles/Events/EventPropertiesViewModel.cs @@ -0,0 +1,42 @@ +using System; +using Artemis.Profiles.Layers.Models; +using Artemis.Utilities; +using Caliburn.Micro; + + +namespace Artemis.ViewModels.Profiles.Events +{ + public class EventPropertiesViewModel : PropertyChangedBase + { + private EventPropertiesModel _proposedProperties; + + public EventPropertiesViewModel(EventPropertiesModel eventPropertiesModel) + { + if (eventPropertiesModel == null) + ProposedProperties = new KeyboardEventPropertiesModel + { + ExpirationType = ExpirationType.Time, + Length = new TimeSpan(0, 0, 1), + TriggerDelay = new TimeSpan(0) + }; + else + ProposedProperties = GeneralHelpers.Clone(eventPropertiesModel); + } + + public EventPropertiesModel ProposedProperties + { + get { return _proposedProperties; } + set + { + if (Equals(value, _proposedProperties)) return; + _proposedProperties = value; + NotifyOfPropertyChange(() => ProposedProperties); + } + } + + public EventPropertiesModel GetAppliedProperties() + { + return GeneralHelpers.Clone(ProposedProperties); + } + } +} \ No newline at end of file diff --git a/Artemis/Artemis/ViewModels/Profiles/LayerConditionViewModel.cs b/Artemis/Artemis/ViewModels/Profiles/LayerConditionViewModel.cs index 554173910..db5f5c219 100644 --- a/Artemis/Artemis/ViewModels/Profiles/LayerConditionViewModel.cs +++ b/Artemis/Artemis/ViewModels/Profiles/LayerConditionViewModel.cs @@ -1,6 +1,6 @@ using System.ComponentModel; using System.Linq; -using Artemis.Models.Profiles; +using Artemis.Profiles.Layers.Models; using Artemis.Utilities; using Caliburn.Micro; diff --git a/Artemis/Artemis/ViewModels/Profiles/LayerDynamicPropertiesViewModel.cs b/Artemis/Artemis/ViewModels/Profiles/LayerDynamicPropertiesViewModel.cs index 9505f2ef4..bd5061cbd 100644 --- a/Artemis/Artemis/ViewModels/Profiles/LayerDynamicPropertiesViewModel.cs +++ b/Artemis/Artemis/ViewModels/Profiles/LayerDynamicPropertiesViewModel.cs @@ -1,10 +1,11 @@ using System.ComponentModel; using System.Linq; -using Artemis.Models.Profiles.Properties; +using Artemis.Profiles.Layers.Models; using Artemis.Utilities; using Caliburn.Micro; using Castle.Core.Internal; + namespace Artemis.ViewModels.Profiles { public sealed class LayerDynamicPropertiesViewModel : PropertyChangedBase @@ -21,13 +22,13 @@ namespace Artemis.ViewModels.Profiles public LayerDynamicPropertiesViewModel(string property, BindableCollection dataModelProps, - KeyboardPropertiesModel keyboardProperties) + LayerPropertiesModel layerPropertiesModel) { _property = property; // Look for the existing property model Proposed = new DynamicPropertiesModel(); - var original = keyboardProperties.DynamicProperties.FirstOrDefault(lp => lp.LayerProperty == _property); + var original = layerPropertiesModel.DynamicProperties.FirstOrDefault(lp => lp.LayerProperty == _property); if (original == null) { Proposed.LayerProperty = property; @@ -161,22 +162,22 @@ namespace Artemis.ViewModels.Profiles case "Width": LayerPropertyOptions = new BindableCollection { - Models.Profiles.Properties.LayerPropertyOptions.LeftToRight, - Models.Profiles.Properties.LayerPropertyOptions.RightToLeft + Artemis.Profiles.Layers.Models.LayerPropertyOptions.LeftToRight, + Artemis.Profiles.Layers.Models.LayerPropertyOptions.RightToLeft }; break; case "Height": LayerPropertyOptions = new BindableCollection { - Models.Profiles.Properties.LayerPropertyOptions.Downwards, - Models.Profiles.Properties.LayerPropertyOptions.Upwards + Artemis.Profiles.Layers.Models.LayerPropertyOptions.Downwards, + Artemis.Profiles.Layers.Models.LayerPropertyOptions.Upwards }; break; case "Opacity": LayerPropertyOptions = new BindableCollection { - Models.Profiles.Properties.LayerPropertyOptions.Increase, - Models.Profiles.Properties.LayerPropertyOptions.Decrease + Artemis.Profiles.Layers.Models.LayerPropertyOptions.Increase, + Artemis.Profiles.Layers.Models.LayerPropertyOptions.Decrease }; break; } @@ -206,14 +207,14 @@ namespace Artemis.ViewModels.Profiles } } - public void Apply(KeyboardPropertiesModel keyboardProperties) + public void Apply(LayerModel layerModel) { - var original = keyboardProperties.DynamicProperties.FirstOrDefault(lp => lp.LayerProperty == _property); + var original = layerModel.Properties.DynamicProperties.FirstOrDefault(lp => lp.LayerProperty == _property); if (original != null) - keyboardProperties.DynamicProperties.Remove(original); + layerModel.Properties.DynamicProperties.Remove(original); if (!Proposed.GameProperty.IsNullOrEmpty()) - keyboardProperties.DynamicProperties.Add(Proposed); + layerModel.Properties.DynamicProperties.Add(Proposed); } } } \ No newline at end of file diff --git a/Artemis/Artemis/ViewModels/Profiles/LayerEditorViewModel.cs b/Artemis/Artemis/ViewModels/Profiles/LayerEditorViewModel.cs index 46621f813..f43f71a54 100644 --- a/Artemis/Artemis/ViewModels/Profiles/LayerEditorViewModel.cs +++ b/Artemis/Artemis/ViewModels/Profiles/LayerEditorViewModel.cs @@ -1,14 +1,20 @@ using System; +using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Linq; using Artemis.Models.Interfaces; -using Artemis.Models.Profiles; -using Artemis.Models.Profiles.Properties; +using Artemis.Profiles.Layers.Interfaces; +using Artemis.Profiles.Layers.Models; +using Artemis.Profiles.Layers.Types.Keyboard; +using Artemis.Profiles.Layers.Types.KeyboardGif; using Artemis.Services; using Artemis.Utilities; -using Artemis.ViewModels.Profiles.Properties; +using Artemis.ViewModels.Profiles.Events; +using Artemis.ViewModels.Profiles.Layers; using Caliburn.Micro; + +using Newtonsoft.Json; using Ninject; namespace Artemis.ViewModels.Profiles @@ -16,15 +22,18 @@ namespace Artemis.ViewModels.Profiles public sealed class LayerEditorViewModel : Screen { private readonly IDataModel _dataModel; + private readonly List _layerAnimations; + private EventPropertiesViewModel _eventPropertiesViewModel; private LayerModel _layer; private LayerPropertiesViewModel _layerPropertiesViewModel; - private LayerType _layerType; private LayerModel _proposedLayer; - private LayerPropertiesModel _proposedProperties; + private ILayerType _selectedLayerType; - public LayerEditorViewModel(IDataModel dataModel, LayerModel layer) + public LayerEditorViewModel(IDataModel dataModel, LayerModel layer, IEnumerable layerTypes, + List layerAnimations) { _dataModel = dataModel; + _layerAnimations = layerAnimations; Layer = layer; ProposedLayer = GeneralHelpers.Clone(layer); @@ -32,26 +41,28 @@ namespace Artemis.ViewModels.Profiles if (Layer.Properties == null) Layer.SetupProperties(); - DataModelProps = new BindableCollection(); - DataModelProps.AddRange(GeneralHelpers.GenerateTypeMap(dataModel)); - LayerConditionVms = new BindableCollection(layer.Properties.Conditions - .Select(c => new LayerConditionViewModel(this, c, DataModelProps))); + LayerTypes = new BindableCollection(layerTypes); + DataModelProps = new BindableCollection( + GeneralHelpers.GenerateTypeMap(dataModel)); + LayerConditionVms = new BindableCollection( + layer.Properties.Conditions.Select(c => new LayerConditionViewModel(this, c, DataModelProps))); PropertyChanged += PropertiesViewModelHandler; PreSelect(); } + public bool ModelChanged { get; set; } [Inject] public MetroDialogService DialogService { get; set; } + public BindableCollection LayerTypes { get; set; } public BindableCollection DataModelProps { get; set; } - - public BindableCollection LayerTypes => new BindableCollection(); - public BindableCollection LayerConditionVms { get; set; } + public bool KeyboardGridIsVisible => ProposedLayer.LayerType is KeyboardType; + public bool GifGridIsVisible => ProposedLayer.LayerType is KeyboardGifType; public LayerModel Layer { @@ -75,17 +86,6 @@ namespace Artemis.ViewModels.Profiles } } - public LayerType LayerType - { - get { return _layerType; } - set - { - if (value == _layerType) return; - _layerType = value; - NotifyOfPropertyChange(() => LayerType); - } - } - public LayerPropertiesViewModel LayerPropertiesViewModel { get { return _layerPropertiesViewModel; } @@ -97,59 +97,66 @@ namespace Artemis.ViewModels.Profiles } } - public bool KeyboardGridIsVisible => ProposedLayer.LayerType == LayerType.Keyboard; - public bool GifGridIsVisible => ProposedLayer.LayerType == LayerType.KeyboardGif; + public EventPropertiesViewModel EventPropertiesViewModel + { + get { return _eventPropertiesViewModel; } + set + { + if (Equals(value, _eventPropertiesViewModel)) return; + _eventPropertiesViewModel = value; + NotifyOfPropertyChange(() => EventPropertiesViewModel); + } + } + + public ILayerType SelectedLayerType + { + get { return _selectedLayerType; } + set + { + if (Equals(value, _selectedLayerType)) return; + _selectedLayerType = value; + NotifyOfPropertyChange(() => SelectedLayerType); + } + } public void PreSelect() { - LayerType = ProposedLayer.LayerType; - - if (LayerType == LayerType.Folder && !(LayerPropertiesViewModel is FolderPropertiesViewModel)) - LayerPropertiesViewModel = new FolderPropertiesViewModel(_dataModel, ProposedLayer.Properties); + SelectedLayerType = LayerTypes.FirstOrDefault(t => t.Name == ProposedLayer.LayerType.Name); + ToggleIsEvent(); } private void PropertiesViewModelHandler(object sender, PropertyChangedEventArgs e) { - if (e.PropertyName != "LayerType") + if (e.PropertyName != "SelectedLayerType") return; // Store the brush in case the user wants to reuse it - var oldBrush = LayerPropertiesViewModel?.GetAppliedProperties().Brush; + var oldBrush = ProposedLayer.Properties.Brush; // Update the model - if (ProposedLayer.LayerType != LayerType) + if (ProposedLayer.LayerType.GetType() != SelectedLayerType.GetType()) { - ProposedLayer.LayerType = LayerType; + ProposedLayer.LayerType = SelectedLayerType; ProposedLayer.SetupProperties(); } + // Let the layer type handle the viewmodel setup + LayerPropertiesViewModel = ProposedLayer.LayerType.SetupViewModel(LayerPropertiesViewModel, _layerAnimations, + _dataModel, ProposedLayer); + if (oldBrush != null) ProposedLayer.Properties.Brush = oldBrush; - // Update the KeyboardPropertiesViewModel if it's being used - var model = LayerPropertiesViewModel as KeyboardPropertiesViewModel; - if (model != null) - model.IsGif = LayerType == LayerType.KeyboardGif; - - // Apply the proper PropertiesViewModel - if ((LayerType == LayerType.Keyboard || LayerType == LayerType.KeyboardGif) && - !(LayerPropertiesViewModel is KeyboardPropertiesViewModel)) - { - LayerPropertiesViewModel = new KeyboardPropertiesViewModel(_dataModel, ProposedLayer.Properties) - { - IsGif = LayerType == LayerType.KeyboardGif - }; - } - else if (LayerType == LayerType.Mouse && !(LayerPropertiesViewModel is MousePropertiesViewModel)) - LayerPropertiesViewModel = new MousePropertiesViewModel(_dataModel, ProposedLayer.Properties); - else if (LayerType == LayerType.Headset && !(LayerPropertiesViewModel is HeadsetPropertiesViewModel)) - LayerPropertiesViewModel = new HeadsetPropertiesViewModel(_dataModel, ProposedLayer.Properties); - else if (LayerType == LayerType.Folder && !(LayerPropertiesViewModel is FolderPropertiesViewModel)) - LayerPropertiesViewModel = new FolderPropertiesViewModel(_dataModel, ProposedLayer.Properties); - NotifyOfPropertyChange(() => LayerPropertiesViewModel); } + public void ToggleIsEvent() + { + EventPropertiesViewModel = ProposedLayer.IsEvent + ? new EventPropertiesViewModel(Layer.EventProperties) + : null; + } + public void AddCondition() { var condition = new LayerConditionModel(); @@ -158,19 +165,20 @@ namespace Artemis.ViewModels.Profiles public void Apply() { - Layer.Name = ProposedLayer.Name; - Layer.LayerType = ProposedLayer.LayerType; + LayerPropertiesViewModel?.ApplyProperties(); + Layer = GeneralHelpers.Clone(ProposedLayer); + + // TODO: EventPropVM must have layer too + if (EventPropertiesViewModel != null) + Layer.EventProperties = EventPropertiesViewModel.GetAppliedProperties(); - if (LayerPropertiesViewModel != null) - Layer.Properties = LayerPropertiesViewModel.GetAppliedProperties(); Layer.Properties.Conditions.Clear(); foreach (var conditionViewModel in LayerConditionVms) - { Layer.Properties.Conditions.Add(conditionViewModel.LayerConditionModel); - } - if (Layer.LayerType != LayerType.KeyboardGif) - return; // Don't bother checking for a GIF path unless the type is GIF + // Don't bother checking for a GIF path unless the type is GIF + if (!(Layer.LayerType is KeyboardGifType)) + return; if (!File.Exists(((KeyboardPropertiesModel) Layer.Properties).GifFile)) DialogService.ShowErrorMessageBox("Couldn't find or access the provided GIF file."); } @@ -185,25 +193,26 @@ namespace Artemis.ViewModels.Profiles public override async void CanClose(Action callback) { // Create a fake layer and apply the properties to it - var fakeLayer = GeneralHelpers.Clone(ProposedLayer); - if (LayerPropertiesViewModel != null) - fakeLayer.Properties = LayerPropertiesViewModel.GetAppliedProperties(); - fakeLayer.Properties.Conditions.Clear(); + LayerPropertiesViewModel?.ApplyProperties(); + // TODO: EventPropVM must have layer too + if (EventPropertiesViewModel != null) + ProposedLayer.EventProperties = EventPropertiesViewModel.GetAppliedProperties(); + ProposedLayer.Properties.Conditions.Clear(); foreach (var conditionViewModel in LayerConditionVms) - fakeLayer.Properties.Conditions.Add(conditionViewModel.LayerConditionModel); + ProposedLayer.Properties.Conditions.Add(conditionViewModel.LayerConditionModel); - var fake = GeneralHelpers.Serialize(fakeLayer); - var real = GeneralHelpers.Serialize(Layer); + var current = JsonConvert.SerializeObject(Layer, Formatting.Indented); + var proposed = JsonConvert.SerializeObject(ProposedLayer, Formatting.Indented); - if (fake.Equals(real)) + if (current.Equals(proposed)) { callback(true); return; } - var close = - await DialogService.ShowQuestionMessageBox("Unsaved changes", "Do you want to discard your changes?"); + var close = await DialogService + .ShowQuestionMessageBox("Unsaved changes", "Do you want to discard your changes?"); callback(close.Value); } } diff --git a/Artemis/Artemis/ViewModels/Profiles/Layers/FolderPropertiesViewModel.cs b/Artemis/Artemis/ViewModels/Profiles/Layers/FolderPropertiesViewModel.cs new file mode 100644 index 000000000..fd0ffaf50 --- /dev/null +++ b/Artemis/Artemis/ViewModels/Profiles/Layers/FolderPropertiesViewModel.cs @@ -0,0 +1,16 @@ +using Artemis.Models.Interfaces; +using Artemis.Profiles.Layers.Models; + +namespace Artemis.ViewModels.Profiles.Layers +{ + public class FolderPropertiesViewModel : LayerPropertiesViewModel + { + public FolderPropertiesViewModel(LayerModel layerModel, IDataModel dataModel) : base(layerModel, dataModel) + { + } + + public override void ApplyProperties() + { + } + } +} \ No newline at end of file diff --git a/Artemis/Artemis/ViewModels/Profiles/Layers/HeadsetPropertiesViewModel.cs b/Artemis/Artemis/ViewModels/Profiles/Layers/HeadsetPropertiesViewModel.cs new file mode 100644 index 000000000..8c3eb2906 --- /dev/null +++ b/Artemis/Artemis/ViewModels/Profiles/Layers/HeadsetPropertiesViewModel.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using Artemis.Models.Interfaces; +using Artemis.Profiles.Layers.Interfaces; +using Artemis.Profiles.Layers.Models; +using Artemis.Utilities; +using Caliburn.Micro; + +namespace Artemis.ViewModels.Profiles.Layers +{ + public class HeadsetPropertiesViewModel : LayerPropertiesViewModel + { + private ILayerAnimation _selectedLayerAnimation; + + public HeadsetPropertiesViewModel(LayerModel layerModel, IDataModel dataModel, + IEnumerable layerAnimations) : base(layerModel, dataModel) + { + LayerAnimations = new BindableCollection(layerAnimations); + OpacityProperties = new LayerDynamicPropertiesViewModel("Opacity", + new BindableCollection(GeneralHelpers.GenerateTypeMap(dataModel)), + layerModel.Properties); + } + + public BindableCollection LayerAnimations { get; set; } + public LayerDynamicPropertiesViewModel OpacityProperties { get; set; } + + public ILayerAnimation SelectedLayerAnimation + { + get { return _selectedLayerAnimation; } + set + { + if (Equals(value, _selectedLayerAnimation)) return; + _selectedLayerAnimation = value; + NotifyOfPropertyChange(() => SelectedLayerAnimation); + } + } + + public override void ApplyProperties() + { + OpacityProperties.Apply(LayerModel); + LayerModel.Properties.Brush = Brush; + LayerModel.LayerAnimation = SelectedLayerAnimation; + } + } +} \ No newline at end of file diff --git a/Artemis/Artemis/ViewModels/Profiles/Layers/KeyboardPropertiesViewModel.cs b/Artemis/Artemis/ViewModels/Profiles/Layers/KeyboardPropertiesViewModel.cs new file mode 100644 index 000000000..28a935a4d --- /dev/null +++ b/Artemis/Artemis/ViewModels/Profiles/Layers/KeyboardPropertiesViewModel.cs @@ -0,0 +1,83 @@ +using System.Collections.Generic; +using System.Linq; +using System.Windows.Forms; +using Artemis.Models.Interfaces; +using Artemis.Profiles.Layers.Interfaces; +using Artemis.Profiles.Layers.Models; +using Artemis.Profiles.Layers.Types.Keyboard; +using Caliburn.Micro; +using static Artemis.Utilities.GeneralHelpers; + +namespace Artemis.ViewModels.Profiles.Layers +{ + public class KeyboardPropertiesViewModel : LayerPropertiesViewModel + { + private bool _isGif; + private ILayerAnimation _selectedLayerAnimation; + + public KeyboardPropertiesViewModel(LayerModel layerModel, IDataModel dataModel, + IEnumerable layerAnimations) : base(layerModel, dataModel) + { + LayerAnimations = new BindableCollection(layerAnimations); + + var dataModelProps = new BindableCollection(GenerateTypeMap(dataModel)); + HeightProperties = new LayerDynamicPropertiesViewModel("Height", dataModelProps, layerModel.Properties); + WidthProperties = new LayerDynamicPropertiesViewModel("Width", dataModelProps, layerModel.Properties); + OpacityProperties = new LayerDynamicPropertiesViewModel("Opacity", dataModelProps, layerModel.Properties); + + SelectedLayerAnimation = LayerAnimations.FirstOrDefault(l => l.Name == layerModel.LayerAnimation?.Name) ?? + LayerAnimations.First(l => l.Name == "None"); + } + + public bool ShowGif => IsGif; + public bool ShowBrush => !IsGif; + public BindableCollection DataModelProps { get; set; } + public BindableCollection LayerAnimations { get; set; } + public LayerDynamicPropertiesViewModel HeightProperties { get; set; } + public LayerDynamicPropertiesViewModel WidthProperties { get; set; } + public LayerDynamicPropertiesViewModel OpacityProperties { get; set; } + + public bool IsGif + { + get { return _isGif; } + set + { + _isGif = value; + NotifyOfPropertyChange(() => ShowGif); + NotifyOfPropertyChange(() => ShowBrush); + } + } + + public ILayerAnimation SelectedLayerAnimation + { + get { return _selectedLayerAnimation; } + set + { + if (Equals(value, _selectedLayerAnimation)) return; + _selectedLayerAnimation = value; + NotifyOfPropertyChange(() => SelectedLayerAnimation); + } + } + + public void BrowseGif() + { + var dialog = new OpenFileDialog {Filter = "Animated image file (*.gif)|*.gif"}; + var result = dialog.ShowDialog(); + if (result != DialogResult.OK) + return; + + ((KeyboardPropertiesModel) LayerModel.Properties).GifFile = dialog.FileName; + NotifyOfPropertyChange(() => LayerModel); + } + + public override void ApplyProperties() + { + HeightProperties.Apply(LayerModel); + WidthProperties.Apply(LayerModel); + OpacityProperties.Apply(LayerModel); + LayerModel.Properties.Brush = Brush; + + LayerModel.LayerAnimation = SelectedLayerAnimation; + } + } +} \ No newline at end of file diff --git a/Artemis/Artemis/ViewModels/Profiles/Layers/LayerPropertiesViewModel.cs b/Artemis/Artemis/ViewModels/Profiles/Layers/LayerPropertiesViewModel.cs new file mode 100644 index 000000000..f633520b0 --- /dev/null +++ b/Artemis/Artemis/ViewModels/Profiles/Layers/LayerPropertiesViewModel.cs @@ -0,0 +1,47 @@ +using System.Drawing; +using Artemis.Models.Interfaces; +using Artemis.Profiles.Layers.Models; +using Caliburn.Micro; +using Brush = System.Windows.Media.Brush; + +namespace Artemis.ViewModels.Profiles.Layers +{ + public abstract class LayerPropertiesViewModel : PropertyChangedBase + { + private LayerModel _layerModel; + private Brush _brush; + + protected LayerPropertiesViewModel(LayerModel layerModel, IDataModel dataModel) + { + LayerModel = layerModel; + DataModel = dataModel; + Brush = LayerModel.Properties.Brush.Clone(); + } + + public Brush Brush + { + get { return _brush; } + set + { + if (Equals(value, _brush)) return; + _brush = value; + NotifyOfPropertyChange(() => Brush); + } + } + + public LayerModel LayerModel + { + get { return _layerModel; } + set + { + if (Equals(value, _layerModel)) return; + _layerModel = value; + NotifyOfPropertyChange(() => LayerModel); + } + } + + public IDataModel DataModel { get; set; } + + public abstract void ApplyProperties(); + } +} \ No newline at end of file diff --git a/Artemis/Artemis/ViewModels/Profiles/Layers/MousePropertiesViewModel.cs b/Artemis/Artemis/ViewModels/Profiles/Layers/MousePropertiesViewModel.cs new file mode 100644 index 000000000..0bfbc0eaa --- /dev/null +++ b/Artemis/Artemis/ViewModels/Profiles/Layers/MousePropertiesViewModel.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.Linq; +using Artemis.Models.Interfaces; +using Artemis.Profiles.Layers.Interfaces; +using Artemis.Profiles.Layers.Models; +using Artemis.Utilities; +using Caliburn.Micro; + +namespace Artemis.ViewModels.Profiles.Layers +{ + public class MousePropertiesViewModel : LayerPropertiesViewModel + { + private ILayerAnimation _selectedLayerAnimation; + + public MousePropertiesViewModel(LayerModel layerModel, IDataModel dataModel, + IEnumerable layerAnimations) : base(layerModel, dataModel) + { + LayerAnimations = new BindableCollection(layerAnimations); + OpacityProperties = new LayerDynamicPropertiesViewModel("Opacity", + new BindableCollection(GeneralHelpers.GenerateTypeMap(dataModel)), + layerModel.Properties); + + SelectedLayerAnimation = LayerAnimations.FirstOrDefault(l => l.Name == layerModel.LayerAnimation.Name) ?? + LayerAnimations.First(l => l.Name == "None"); + } + + public BindableCollection LayerAnimations { get; set; } + public LayerDynamicPropertiesViewModel OpacityProperties { get; set; } + + public ILayerAnimation SelectedLayerAnimation + { + get { return _selectedLayerAnimation; } + set + { + if (Equals(value, _selectedLayerAnimation)) return; + _selectedLayerAnimation = value; + NotifyOfPropertyChange(() => SelectedLayerAnimation); + } + } + + public override void ApplyProperties() + { + OpacityProperties.Apply(LayerModel); + LayerModel.Properties.Brush = Brush; + LayerModel.LayerAnimation = SelectedLayerAnimation; + } + } +} \ No newline at end of file diff --git a/Artemis/Artemis/ViewModels/Profiles/ProfileEditorViewModel.cs b/Artemis/Artemis/ViewModels/Profiles/ProfileEditorViewModel.cs index e28eed39d..acda2190d 100644 --- a/Artemis/Artemis/ViewModels/Profiles/ProfileEditorViewModel.cs +++ b/Artemis/Artemis/ViewModels/Profiles/ProfileEditorViewModel.cs @@ -3,27 +3,26 @@ using System.Dynamic; using System.Linq; using System.Threading; using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; using System.Windows.Forms; using System.Windows.Input; using System.Windows.Media; -using System.Windows.Media.Imaging; using Artemis.DAL; using Artemis.DeviceProviders; using Artemis.Events; using Artemis.InjectionFactories; using Artemis.Managers; using Artemis.Models; -using Artemis.Models.Profiles; +using Artemis.Profiles; +using Artemis.Profiles.Layers.Models; +using Artemis.Profiles.Layers.Types.Folder; using Artemis.Services; using Artemis.Styles.DropTargetAdorners; using Artemis.Utilities; using Caliburn.Micro; using GongSolutions.Wpf.DragDrop; using MahApps.Metro.Controls.Dialogs; + using Ninject; -using Application = System.Windows.Application; using DragDropEffects = System.Windows.DragDropEffects; using IDropTarget = GongSolutions.Wpf.DragDrop.IDropTarget; using MouseEventArgs = System.Windows.Input.MouseEventArgs; @@ -131,7 +130,7 @@ namespace Artemis.ViewModels.Profiles return; if (dropInfo.InsertPosition == RelativeInsertPosition.TargetItemCenter && - target.LayerType == LayerType.Folder) + target.LayerType is FolderType) { dropInfo.DropTargetAdorner = typeof(DropTargetMetroHighlightAdorner); dropInfo.Effects = DragDropEffects.Copy; @@ -169,7 +168,7 @@ namespace Artemis.ViewModels.Profiles } if (dropInfo.InsertPosition == RelativeInsertPosition.TargetItemCenter && - target.LayerType == LayerType.Folder) + target.LayerType is FolderType) { // Insert into folder source.Order = -1; @@ -257,7 +256,7 @@ namespace Artemis.ViewModels.Profiles public void EditLayerFromDoubleClick() { - if (ProfileViewModel.SelectedLayer?.LayerType == LayerType.Folder) + if (ProfileViewModel.SelectedLayer?.LayerType is FolderType) return; EditLayer(); @@ -280,23 +279,19 @@ namespace Artemis.ViewModels.Profiles IWindowManager manager = new WindowManager(); var editorVm = _layerEditorVmFactory.CreateLayerEditorVm(_gameModel.DataModel, layer); dynamic settings = new ExpandoObject(); - var iconImage = new Image - { - Source = (DrawingImage) Application.Current.MainWindow.Resources["BowIcon"], - Stretch = Stretch.Uniform, - Margin = new Thickness(20) - }; - iconImage.Arrange(new Rect(0, 0, 100, 100)); - var bitmap = new RenderTargetBitmap(100, 100, 96, 96, PixelFormats.Pbgra32); - bitmap.Render(iconImage); + var icon = ImageUtilities.GenerateWindowIcon(); settings.Title = "Artemis | Edit " + layer.Name; - settings.Icon = bitmap; + settings.Icon = icon; manager.ShowDialog(editorVm, null, settings); + // The layer editor VM may have created a new instance of the layer, reapply it to the list + layer.Replace(editorVm.Layer); + layer = editorVm.Layer; + // If the layer was a folder, but isn't anymore, assign it's children to it's parent. - if (layer.LayerType != LayerType.Folder && layer.Children.Any()) + if (!(layer.LayerType is FolderType) && layer.Children.Any()) { while (layer.Children.Any()) { @@ -596,7 +591,7 @@ namespace Artemis.ViewModels.Profiles "To import a profile, please select a keyboard in the options menu first."); return; } - var dialog = new OpenFileDialog {Filter = "Artemis profile (*.xml)|*.xml"}; + var dialog = new OpenFileDialog {Filter = "Artemis profile (*.json)|*.json"}; var result = dialog.ShowDialog(); if (result != DialogResult.OK) return; @@ -654,7 +649,7 @@ namespace Artemis.ViewModels.Profiles if (SelectedProfile == null) return; - var dialog = new SaveFileDialog {Filter = "Artemis profile (*.xml)|*.xml"}; + var dialog = new SaveFileDialog {Filter = "Artemis profile (*.json)|*.json"}; var result = dialog.ShowDialog(); if (result != DialogResult.OK) return; diff --git a/Artemis/Artemis/ViewModels/Profiles/ProfileViewModel.cs b/Artemis/Artemis/ViewModels/Profiles/ProfileViewModel.cs index 660673f9c..4f7b0aede 100644 --- a/Artemis/Artemis/ViewModels/Profiles/ProfileViewModel.cs +++ b/Artemis/Artemis/ViewModels/Profiles/ProfileViewModel.cs @@ -7,9 +7,10 @@ using System.Windows.Input; using System.Windows.Media; using Artemis.Events; using Artemis.Managers; -using Artemis.Models.Profiles; -using Artemis.Models.Profiles.Properties; using Artemis.Modules.Effects.ProfilePreview; +using Artemis.Profiles; +using Artemis.Profiles.Layers.Models; +using Artemis.Profiles.Layers.Types.Keyboard; using Artemis.Properties; using Artemis.Utilities; using Caliburn.Micro; @@ -109,10 +110,12 @@ namespace Artemis.ViewModels.Profiles drawingContext.DrawRectangle(new SolidColorBrush(Color.FromArgb(0, 0, 0, 0)), null, keyboardRect); // Draw the layers - var drawLayers = SelectedProfile.GetRenderLayers( - new ProfilePreviewDataModel(), false, false, true); + var drawLayers = SelectedProfile.GetRenderLayers(new ProfilePreviewDataModel(), false, false, true); foreach (var layer in drawLayers) + { + layer.Update(null, true, false); layer.Draw(null, drawingContext, true, false); + } // Get the selection color var accentColor = ThemeManager.DetectAppStyle(Application.Current)?.Item2?.Resources["AccentColor"]; diff --git a/Artemis/Artemis/ViewModels/Profiles/Properties/FolderPropertiesViewModel.cs b/Artemis/Artemis/ViewModels/Profiles/Properties/FolderPropertiesViewModel.cs deleted file mode 100644 index 4a0cb16e1..000000000 --- a/Artemis/Artemis/ViewModels/Profiles/Properties/FolderPropertiesViewModel.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Artemis.Models.Interfaces; -using Artemis.Models.Profiles.Properties; -using Artemis.Utilities; - -namespace Artemis.ViewModels.Profiles.Properties -{ - public class FolderPropertiesViewModel : LayerPropertiesViewModel - { - private LayerPropertiesModel _proposedProperties; - - public FolderPropertiesViewModel(IDataModel dataModel, LayerPropertiesModel properties) - : base(dataModel) - { - ProposedProperties = GeneralHelpers.Clone(properties); - } - - public LayerPropertiesModel ProposedProperties - { - get { return _proposedProperties; } - set - { - if (Equals(value, _proposedProperties)) return; - _proposedProperties = value; - NotifyOfPropertyChange(() => ProposedProperties); - } - } - - public override LayerPropertiesModel GetAppliedProperties() - { - return GeneralHelpers.Clone(ProposedProperties); - } - } -} \ No newline at end of file diff --git a/Artemis/Artemis/ViewModels/Profiles/Properties/HeadsetPropertiesViewModel.cs b/Artemis/Artemis/ViewModels/Profiles/Properties/HeadsetPropertiesViewModel.cs deleted file mode 100644 index 23d853420..000000000 --- a/Artemis/Artemis/ViewModels/Profiles/Properties/HeadsetPropertiesViewModel.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Windows.Media; -using Artemis.Models.Interfaces; -using Artemis.Models.Profiles.Properties; -using Artemis.Utilities; - -namespace Artemis.ViewModels.Profiles.Properties -{ - public class HeadsetPropertiesViewModel : LayerPropertiesViewModel - { - private Brush _brush; - private LayerPropertiesModel _proposedProperties; - - public HeadsetPropertiesViewModel(IDataModel dataModel, LayerPropertiesModel properties) - : base(dataModel) - { - ProposedProperties = GeneralHelpers.Clone(properties); - Brush = ProposedProperties.Brush.CloneCurrentValue(); - } - - public Brush Brush - { - get { return _brush; } - set - { - if (Equals(value, _brush)) return; - _brush = value; - NotifyOfPropertyChange(() => Brush); - } - } - - public LayerPropertiesModel ProposedProperties - { - get { return _proposedProperties; } - set - { - if (Equals(value, _proposedProperties)) return; - _proposedProperties = value; - NotifyOfPropertyChange(() => ProposedProperties); - } - } - - public override LayerPropertiesModel GetAppliedProperties() - { - var properties = GeneralHelpers.Clone(ProposedProperties); - properties.Brush = Brush; - return properties; - } - } -} \ No newline at end of file diff --git a/Artemis/Artemis/ViewModels/Profiles/Properties/KeyboardPropertiesViewModel.cs b/Artemis/Artemis/ViewModels/Profiles/Properties/KeyboardPropertiesViewModel.cs deleted file mode 100644 index 1359dbed5..000000000 --- a/Artemis/Artemis/ViewModels/Profiles/Properties/KeyboardPropertiesViewModel.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System.Windows.Forms; -using System.Windows.Media; -using Artemis.Models.Interfaces; -using Artemis.Models.Profiles.Properties; -using Artemis.Utilities; -using Caliburn.Micro; - -namespace Artemis.ViewModels.Profiles.Properties -{ - public class KeyboardPropertiesViewModel : LayerPropertiesViewModel - { - private Brush _brush; - private bool _isGif; - private KeyboardPropertiesModel _proposedProperties; - - public KeyboardPropertiesViewModel(IDataModel dataModel, LayerPropertiesModel properties) - : base(dataModel) - { - var keyboardProperties = (KeyboardPropertiesModel) properties; - ProposedProperties = GeneralHelpers.Clone(keyboardProperties); - Brush = ProposedProperties.Brush.CloneCurrentValue(); - - DataModelProps = new BindableCollection(); - DataModelProps.AddRange(GeneralHelpers.GenerateTypeMap(dataModel)); - - HeightProperties = new LayerDynamicPropertiesViewModel("Height", DataModelProps, keyboardProperties); - WidthProperties = new LayerDynamicPropertiesViewModel("Width", DataModelProps, keyboardProperties); - OpacityProperties = new LayerDynamicPropertiesViewModel("Opacity", DataModelProps, keyboardProperties); - } - - - public KeyboardPropertiesModel ProposedProperties - { - get { return _proposedProperties; } - set - { - if (Equals(value, _proposedProperties)) return; - _proposedProperties = value; - NotifyOfPropertyChange(() => ProposedProperties); - } - } - - public bool IsGif - { - get { return _isGif; } - set - { - _isGif = value; - NotifyOfPropertyChange(() => ShowGif); - NotifyOfPropertyChange(() => ShowBrush); - } - } - - public Brush Brush - { - get { return _brush; } - set - { - if (Equals(value, _brush)) return; - _brush = value; - NotifyOfPropertyChange(() => Brush); - } - } - - public bool ShowGif => IsGif; - - public bool ShowBrush => !IsGif; - - public BindableCollection DataModelProps { get; set; } - - public LayerDynamicPropertiesViewModel HeightProperties { get; set; } - - public LayerDynamicPropertiesViewModel WidthProperties { get; set; } - - public LayerDynamicPropertiesViewModel OpacityProperties { get; set; } - - public void BrowseGif() - { - var dialog = new OpenFileDialog {Filter = "Animated image file (*.gif)|*.gif"}; - var result = dialog.ShowDialog(); - if (result != DialogResult.OK) - return; - - ProposedProperties.GifFile = dialog.FileName; - NotifyOfPropertyChange(() => ProposedProperties); - } - - public override LayerPropertiesModel GetAppliedProperties() - { - HeightProperties.Apply(ProposedProperties); - WidthProperties.Apply(ProposedProperties); - OpacityProperties.Apply(ProposedProperties); - - var properties = GeneralHelpers.Clone(ProposedProperties); - properties.Brush = Brush; - return properties; - } - } -} \ No newline at end of file diff --git a/Artemis/Artemis/ViewModels/Profiles/Properties/LayerPropertiesViewModel.cs b/Artemis/Artemis/ViewModels/Profiles/Properties/LayerPropertiesViewModel.cs deleted file mode 100644 index 5cbc63f5b..000000000 --- a/Artemis/Artemis/ViewModels/Profiles/Properties/LayerPropertiesViewModel.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Artemis.Models.Interfaces; -using Artemis.Models.Profiles.Properties; -using Caliburn.Micro; - -namespace Artemis.ViewModels.Profiles.Properties -{ - public abstract class LayerPropertiesViewModel : PropertyChangedBase - { - protected LayerPropertiesViewModel(IDataModel dataModel) - { - DataModel = dataModel; - } - - public IDataModel DataModel { get; set; } - - public abstract LayerPropertiesModel GetAppliedProperties(); - } -} \ No newline at end of file diff --git a/Artemis/Artemis/ViewModels/Profiles/Properties/MousePropertiesViewModel.cs b/Artemis/Artemis/ViewModels/Profiles/Properties/MousePropertiesViewModel.cs deleted file mode 100644 index 85106eff9..000000000 --- a/Artemis/Artemis/ViewModels/Profiles/Properties/MousePropertiesViewModel.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Windows.Media; -using Artemis.Models.Interfaces; -using Artemis.Models.Profiles.Properties; -using Artemis.Utilities; - -namespace Artemis.ViewModels.Profiles.Properties -{ - public class MousePropertiesViewModel : LayerPropertiesViewModel - { - private Brush _brush; - private LayerPropertiesModel _proposedProperties; - - public MousePropertiesViewModel(IDataModel dataModel, LayerPropertiesModel properties) - : base(dataModel) - { - ProposedProperties = GeneralHelpers.Clone(properties); - Brush = ProposedProperties.Brush.CloneCurrentValue(); - } - - public Brush Brush - { - get { return _brush; } - set - { - if (Equals(value, _brush)) return; - _brush = value; - NotifyOfPropertyChange(() => Brush); - } - } - - public LayerPropertiesModel ProposedProperties - { - get { return _proposedProperties; } - set - { - if (Equals(value, _proposedProperties)) return; - _proposedProperties = value; - NotifyOfPropertyChange(() => ProposedProperties); - } - } - - public override LayerPropertiesModel GetAppliedProperties() - { - var properties = GeneralHelpers.Clone(ProposedProperties); - properties.Brush = Brush; - return properties; - } - } -} \ No newline at end of file diff --git a/Artemis/Artemis/ViewModels/ShellViewModel.cs b/Artemis/Artemis/ViewModels/ShellViewModel.cs index e7a6f4fba..e8cc77059 100644 --- a/Artemis/Artemis/ViewModels/ShellViewModel.cs +++ b/Artemis/Artemis/ViewModels/ShellViewModel.cs @@ -1,5 +1,4 @@ using System.Linq; -using System.Threading.Tasks; using Artemis.Managers; using Artemis.Services; using Artemis.ViewModels.Abstract; @@ -42,7 +41,7 @@ namespace Artemis.ViewModels ActiveItem = _viewModels.FirstOrDefault(); } - + public void Settings() { Flyouts.First().IsOpen = !Flyouts.First().IsOpen; diff --git a/Artemis/Artemis/ViewModels/SystemTrayViewModel.cs b/Artemis/Artemis/ViewModels/SystemTrayViewModel.cs index 33cdc0fca..c4b4223f5 100644 --- a/Artemis/Artemis/ViewModels/SystemTrayViewModel.cs +++ b/Artemis/Artemis/ViewModels/SystemTrayViewModel.cs @@ -1,5 +1,4 @@ using System; -using System.Threading; using System.Threading.Tasks; using System.Windows; using Artemis.Events; @@ -8,7 +7,6 @@ using Artemis.Services; using Artemis.Settings; using Artemis.Utilities; using Caliburn.Micro; -using Ninject; namespace Artemis.ViewModels { @@ -21,7 +19,8 @@ namespace Artemis.ViewModels private bool _enabled; private string _toggleText; - public SystemTrayViewModel(IWindowManager windowManager, IEventAggregator events, MetroDialogService dialogService, ShellViewModel shellViewModel, + public SystemTrayViewModel(IWindowManager windowManager, IEventAggregator events, + MetroDialogService dialogService, ShellViewModel shellViewModel, MainManager mainManager) { _windowManager = windowManager; @@ -125,7 +124,7 @@ namespace Artemis.ViewModels private async void ShowKeyboardDialog() { - while(!_shellViewModel.IsActive) + while (!_shellViewModel.IsActive) await Task.Delay(200); NotifyOfPropertyChange(() => CanHideWindow); diff --git a/Artemis/Artemis/Views/DebugView.xaml b/Artemis/Artemis/Views/DebugView.xaml new file mode 100644 index 000000000..5c9dd7b8d --- /dev/null +++ b/Artemis/Artemis/Views/DebugView.xaml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Artemis/Artemis/Views/DebugView.xaml.cs b/Artemis/Artemis/Views/DebugView.xaml.cs new file mode 100644 index 000000000..f08610dab --- /dev/null +++ b/Artemis/Artemis/Views/DebugView.xaml.cs @@ -0,0 +1,15 @@ +using MahApps.Metro.Controls; + +namespace Artemis.Views +{ + /// + /// Interaction logic for DebugView.xaml + /// + public partial class DebugView : MetroWindow + { + public DebugView() + { + InitializeComponent(); + } + } +} \ No newline at end of file diff --git a/Artemis/Artemis/Views/Flyouts/FlyoutSettingsView.xaml b/Artemis/Artemis/Views/Flyouts/FlyoutSettingsView.xaml index 105fdbd39..1ea2699c1 100644 --- a/Artemis/Artemis/Views/Flyouts/FlyoutSettingsView.xaml +++ b/Artemis/Artemis/Views/Flyouts/FlyoutSettingsView.xaml @@ -4,24 +4,10 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:controls="http://metro.mahapps.com/winfx/xaml/controls" - xmlns:cal="http://www.caliburnproject.org" - xmlns:utilities="clr-namespace:Artemis.Utilities" - xmlns:sys="clr-namespace:System;assembly=mscorlib" - xmlns:profileEnumerations="clr-namespace:Artemis.Models.Profiles" + xmlns:cal="http://www.caliburnproject.org" mc:Ignorable="d" d:DesignHeight="600" d:DesignWidth="300" Width="300"> - - - - - - - - - @@ -38,6 +24,7 @@ + @@ -103,17 +90,20 @@ +