From 6a32ecc3a469f80bad494ae9c457f9233fe8e6a1 Mon Sep 17 00:00:00 2001 From: SpoinkyNL Date: Sun, 16 Aug 2020 23:44:59 +0200 Subject: [PATCH] Modules - Added activation mechanism and conditions --- .../Extensions/ProcessExtensions.cs | 33 +++++++ src/Artemis.Core/Plugins/Abstract/Module.cs | 90 +++++++++++++++++- .../Plugins/Abstract/ProfileModule.cs | 15 ++- .../ProcessActivationRequirement.cs | 39 ++++++++ src/Artemis.Core/Services/CoreService.cs | 63 ++++++++---- .../Storage/Interfaces/IProfileService.cs | 11 ++- .../Services/Storage/ProfileService.cs | 56 +++++------ src/Artemis.Core/Utilities/IntroAnimation.cs | 45 +++++---- src/Artemis.UI/Artemis.UI.csproj | 2 + .../Images/Sidebar/sidebar-header.png | Bin 0 -> 7429 bytes src/Artemis.UI/Screens/RootView.xaml | 4 +- src/Artemis.UI/Screens/RootViewModel.cs | 12 +-- .../Screens/Sidebar/SidebarView.xaml | 29 +++--- .../Screens/Sidebar/SidebarViewModel.cs | 42 +++++++- .../DataModel/Windows/WindowDataModel.cs | 3 +- .../GeneralModule.cs | 38 +++++--- .../Utilities/WindowUtilities.cs | 24 ----- 17 files changed, 357 insertions(+), 149 deletions(-) create mode 100644 src/Artemis.Core/Extensions/ProcessExtensions.cs create mode 100644 src/Artemis.Core/Plugins/ModuleActivationRequirements/ProcessActivationRequirement.cs create mode 100644 src/Artemis.UI/Resources/Images/Sidebar/sidebar-header.png diff --git a/src/Artemis.Core/Extensions/ProcessExtensions.cs b/src/Artemis.Core/Extensions/ProcessExtensions.cs new file mode 100644 index 000000000..3353ed60d --- /dev/null +++ b/src/Artemis.Core/Extensions/ProcessExtensions.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; + +namespace Artemis.Core.Extensions +{ + public static class ProcessExtensions + { + public static string GetProcessFilename(this Process p) + { + var capacity = 2000; + var builder = new StringBuilder(capacity); + var ptr = OpenProcess(ProcessAccessFlags.QueryLimitedInformation, false, p.Id); + if (!QueryFullProcessImageName(ptr, 0, builder, ref capacity)) return string.Empty; + + return builder.ToString(); + } + + [DllImport("kernel32.dll")] + private static extern bool QueryFullProcessImageName([In] IntPtr hProcess, [In] int dwFlags, [Out] StringBuilder lpExeName, ref int lpdwSize); + + [DllImport("kernel32.dll")] + private static extern IntPtr OpenProcess(ProcessAccessFlags processAccess, bool bInheritHandle, int processId); + + [Flags] + private enum ProcessAccessFlags : uint + { + QueryLimitedInformation = 0x00001000 + } + } +} diff --git a/src/Artemis.Core/Plugins/Abstract/Module.cs b/src/Artemis.Core/Plugins/Abstract/Module.cs index 5592004e9..c0a26a264 100644 --- a/src/Artemis.Core/Plugins/Abstract/Module.cs +++ b/src/Artemis.Core/Plugins/Abstract/Module.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; +using System.Linq; using Artemis.Core.Models.Surface; using Artemis.Core.Plugins.Abstract.DataModels; using Artemis.Core.Plugins.Abstract.DataModels.Attributes; using Artemis.Core.Plugins.Abstract.ViewModels; +using Artemis.Core.Plugins.ModuleActivationRequirements; using SkiaSharp; namespace Artemis.Core.Plugins.Abstract @@ -26,7 +28,7 @@ namespace Artemis.Core.Plugins.Abstract /// Gets or sets whether this module must also expand the main data model /// /// Note: If expanding the main data model is all you want your plugin to do, create a - /// plugin instead. + /// plugin instead. /// /// public bool ExpandsDataModel @@ -66,9 +68,11 @@ namespace Artemis.Core.Plugins.Abstract /// public abstract class Module : Plugin { - internal DataModel InternalDataModel { get; set; } - - internal bool InternalExpandsMainDataModel { get; set; } + protected Module() + { + ActivationRequirements = new List(); + ActivationRequirementMode = ActivationRequirementType.Any; + } /// /// The modules display name that's shown in the menu @@ -81,6 +85,27 @@ namespace Artemis.Core.Plugins.Abstract /// public string DisplayIcon { get; set; } + /// + /// Gets whether this module is activated. A module can only be active while its + /// are met + /// + public bool IsActivated { get; internal set; } + + /// + /// A list of activation requirements + /// Note: if empty the module is always activated + /// + public List ActivationRequirements { get; } + + /// + /// Gets or sets the activation requirement mode, defaults to + /// + public ActivationRequirementType ActivationRequirementMode { get; set; } + + internal DataModel InternalDataModel { get; set; } + + internal bool InternalExpandsMainDataModel { get; set; } + /// /// Called each frame when the module must update /// @@ -101,5 +126,62 @@ namespace Artemis.Core.Plugins.Abstract /// /// public abstract IEnumerable GetViewModels(); + + /// + /// Called when the are met + /// + public abstract void ModuleActivated(); + + /// + /// Called when the are no longer met + /// + public abstract void ModuleDeactivated(); + + /// + /// Evaluates the activation requirements following the and returns the result + /// + /// The evaluated result of the activation requirements + public bool EvaluateActivationRequirements() + { + if (!ActivationRequirements.Any()) + return true; + if (ActivationRequirementMode == ActivationRequirementType.All) + return ActivationRequirements.All(r => r.Evaluate()); + if (ActivationRequirementMode == ActivationRequirementType.Any) + return ActivationRequirements.Any(r => r.Evaluate()); + + return false; + } + + internal virtual void Activate() + { + if (IsActivated) + return; + + ModuleActivated(); + IsActivated = true; + } + + internal virtual void Deactivate() + { + if (!IsActivated) + return; + + IsActivated = false; + ModuleDeactivated(); + } + } + + public enum ActivationRequirementType + { + /// + /// Any activation requirement must be met for the module to activate + /// + Any, + + /// + /// All activation requirements must be met for the module to activate + /// + All } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Abstract/ProfileModule.cs b/src/Artemis.Core/Plugins/Abstract/ProfileModule.cs index 55965ca63..1fa60785b 100644 --- a/src/Artemis.Core/Plugins/Abstract/ProfileModule.cs +++ b/src/Artemis.Core/Plugins/Abstract/ProfileModule.cs @@ -143,7 +143,8 @@ namespace Artemis.Core.Plugins.Abstract { if (profile != null && profile.Module != this) throw new ArtemisCoreException($"Cannot activate a profile of module {profile.Module} on a module of plugin {PluginInfo}."); - + if (!IsActivated) + throw new ArtemisCoreException("Cannot activate a profile on a deactivated module"); if (profile == ActiveProfile || AnimatingProfileChange) return; @@ -164,6 +165,9 @@ namespace Artemis.Core.Plugins.Abstract { if (profile != null && profile.Module != this) throw new ArtemisCoreException($"Cannot activate a profile of module {profile.Module} on a module of plugin {PluginInfo}."); + if (!IsActivated) + throw new ArtemisCoreException("Cannot activate a profile on a deactivated module"); + lock (this) { if (profile == ActiveProfile) @@ -188,6 +192,15 @@ namespace Artemis.Core.Plugins.Abstract /// public bool AnimatingProfileChange { get; private set; } + internal override void Deactivate() + { + base.Deactivate(); + + var profile = ActiveProfile; + ActiveProfile = null; + profile?.Dispose(); + } + #region Events public event EventHandler ActiveProfileChanged; diff --git a/src/Artemis.Core/Plugins/ModuleActivationRequirements/ProcessActivationRequirement.cs b/src/Artemis.Core/Plugins/ModuleActivationRequirements/ProcessActivationRequirement.cs new file mode 100644 index 000000000..87cd32dbf --- /dev/null +++ b/src/Artemis.Core/Plugins/ModuleActivationRequirements/ProcessActivationRequirement.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using Artemis.Core.Extensions; + +namespace Artemis.Core.Plugins.ModuleActivationRequirements +{ + public class ProcessActivationRequirement : IModuleActivationRequirement + { + public string ProcessName { get; set; } + public string Location { get; set; } + + public ProcessActivationRequirement(string processName, string location = null) + { + ProcessName = processName; + Location = location; + } + + + public bool Evaluate() + { + if (ProcessName == null && Location == null) + return false; + + var processes = ProcessName != null ? Process.GetProcessesByName(ProcessName).Where(p => !p.HasExited) : Process.GetProcesses().Where(p => !p.HasExited); + return Location != null + ? processes.Any(p => Path.GetDirectoryName(p.GetProcessFilename()) == Location) + : processes.Any(); + } + } + + public interface IModuleActivationRequirement + { + bool Evaluate(); + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/CoreService.cs b/src/Artemis.Core/Services/CoreService.cs index ec0654995..5193b45d1 100644 --- a/src/Artemis.Core/Services/CoreService.cs +++ b/src/Artemis.Core/Services/CoreService.cs @@ -39,6 +39,7 @@ namespace Artemis.Core.Services private List _dataModelExpansions; private List _modules; private IntroAnimation _introAnimation; + private DateTime _lastModuleActivationUpdate; // ReSharper disable once UnusedParameter.Local - Storage migration service is injected early to ensure it runs before anything else internal CoreService(ILogger logger, StorageMigrationService _, ISettingsService settingsService, IPluginService pluginService, @@ -95,8 +96,6 @@ namespace Artemis.Core.Services _logger.Information("Initialized without an active surface entity"); PlayIntroAnimation(); - _profileService.ActivateLastActiveProfiles(); - OnInitialized(); } @@ -170,6 +169,11 @@ namespace Artemis.Core.Services try { _frameStopWatch.Restart(); + + // Only run the module activation update every 2 seconds + if (DateTime.Now - _lastModuleActivationUpdate > TimeSpan.FromSeconds(2)) + ModuleActivationUpdate(); + lock (_dataModelExpansions) { // Update all active modules @@ -177,13 +181,16 @@ namespace Artemis.Core.Services dataModelExpansion.Update(args.DeltaTime); } + List modules; lock (_modules) { - // Update all active modules - foreach (var module in _modules) - module.Update(args.DeltaTime); + modules = _modules.Where(m => m.IsActivated).ToList(); } + // Update all active modules + foreach (var module in modules) + module.Update(args.DeltaTime); + // If there is no ready bitmap brush, skip the frame if (_rgbService.BitmapBrush == null) return; @@ -194,20 +201,15 @@ namespace Artemis.Core.Services return; // Render all active modules - using (var canvas = new SKCanvas(_rgbService.BitmapBrush.Bitmap)) + using var canvas = new SKCanvas(_rgbService.BitmapBrush.Bitmap); + canvas.Clear(new SKColor(0, 0, 0)); + if (!ModuleRenderingDisabled) { - canvas.Clear(new SKColor(0, 0, 0)); - if (!ModuleRenderingDisabled) - { - lock (_modules) - { - foreach (var module in _modules) - module.Render(args.DeltaTime, _surfaceService.ActiveSurface, canvas, _rgbService.BitmapBrush.Bitmap.Info); - } - } - - OnFrameRendering(new FrameRenderingEventArgs(_modules, canvas, args.DeltaTime, _rgbService.Surface)); + foreach (var module in modules) + module.Render(args.DeltaTime, _surfaceService.ActiveSurface, canvas, _rgbService.BitmapBrush.Bitmap.Info); } + + OnFrameRendering(new FrameRenderingEventArgs(modules, canvas, args.DeltaTime, _rgbService.Surface)); } } catch (Exception e) @@ -221,6 +223,33 @@ namespace Artemis.Core.Services } } + private void ModuleActivationUpdate() + { + _lastModuleActivationUpdate = DateTime.Now; + var stopwatch = new Stopwatch(); + stopwatch.Start(); + lock (_modules) + { + foreach (var module in _modules) + { + var shouldBeActivated = module.EvaluateActivationRequirements(); + if (shouldBeActivated && !module.IsActivated) + { + module.Activate(); + // If this is a profile module, activate the last active profile after module activation + if (module is ProfileModule profileModule) + _profileService.ActivateLastProfile(profileModule); + } + else if (!shouldBeActivated && module.IsActivated) + module.Deactivate(); + } + } + + stopwatch.Stop(); + if (stopwatch.ElapsedMilliseconds > 100) + _logger.Warning("Activation requirements evaluation took too long: {moduleCount} module(s) in {elapsed}", _modules.Count, stopwatch.Elapsed); + } + private void SurfaceOnUpdated(UpdatedEventArgs args) { if (_rgbService.IsRenderPaused) diff --git a/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs b/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs index 7ebbae54a..b4cb7235b 100644 --- a/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs +++ b/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs @@ -11,11 +11,6 @@ namespace Artemis.Core.Services.Storage.Interfaces /// public interface IProfileService : IArtemisService { - /// - /// Activates the last profile for each module - /// - void ActivateLastActiveProfiles(); - /// /// Creates a new profile for the given module and returns a descriptor pointing to it /// @@ -50,6 +45,12 @@ namespace Artemis.Core.Services.Storage.Interfaces /// The descriptor pointing to the profile to delete void DeleteProfile(ProfileDescriptor profileDescriptor); + /// + /// Activates the last profile of the given profile module + /// + /// + void ActivateLastProfile(ProfileModule profileModule); + /// /// Activates the profile described in the given with the currently active surface /// diff --git a/src/Artemis.Core/Services/Storage/ProfileService.cs b/src/Artemis.Core/Services/Storage/ProfileService.cs index 0eee8c30d..e79e8f450 100644 --- a/src/Artemis.Core/Services/Storage/ProfileService.cs +++ b/src/Artemis.Core/Services/Storage/ProfileService.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.IO.Compression; using System.Linq; using System.Threading.Tasks; using Artemis.Core.Events; @@ -11,7 +10,6 @@ using Artemis.Core.Plugins.Abstract; using Artemis.Core.Plugins.LayerEffect.Abstract; using Artemis.Core.Services.Interfaces; using Artemis.Core.Services.Storage.Interfaces; -using Artemis.Core.Utilities; using Artemis.Storage.Entities.Profile; using Artemis.Storage.Repositories.Interfaces; using Newtonsoft.Json; @@ -44,15 +42,6 @@ namespace Artemis.Core.Services.Storage public JsonSerializerSettings MementoSettings { get; set; } = new JsonSerializerSettings {TypeNameHandling = TypeNameHandling.All}; public JsonSerializerSettings ExportSettings { get; set; } = new JsonSerializerSettings {TypeNameHandling = TypeNameHandling.All, Formatting = Formatting.Indented}; - public void ActivateLastActiveProfiles() - { - foreach (var profileModule in _pluginService.GetPluginsOfType()) - { - var activeProfile = GetLastActiveProfile(profileModule); - ActivateProfile(activeProfile); - } - } - public List GetProfileDescriptors(ProfileModule module) { var profileEntities = _profileRepository.GetByPluginGuid(module.PluginInfo.Guid); @@ -67,6 +56,12 @@ namespace Artemis.Core.Services.Storage return new ProfileDescriptor(module, profileEntity); } + public void ActivateLastProfile(ProfileModule profileModule) + { + var activeProfile = GetLastActiveProfile(profileModule); + ActivateProfile(activeProfile); + } + public Profile ActivateProfile(ProfileDescriptor profileDescriptor) { if (profileDescriptor.ProfileModule.ActiveProfile?.EntityId == profileDescriptor.Id) @@ -109,16 +104,6 @@ namespace Artemis.Core.Services.Storage SaveActiveProfile(module); } - private void SaveActiveProfile(ProfileModule module) - { - var profileEntities = _profileRepository.GetByPluginGuid(module.PluginInfo.Guid); - foreach (var profileEntity in profileEntities) - { - profileEntity.IsActive = module.ActiveProfile.EntityId == profileEntity.Id; - _profileRepository.Save(profileEntity); - } - } - public async Task ClearActiveProfileAnimated(ProfileModule module) { await module.ChangeActiveProfileAnimated(null, _surfaceService.ActiveSurface); @@ -210,13 +195,6 @@ namespace Artemis.Core.Services.Storage } } - public ProfileDescriptor GetLastActiveProfile(ProfileModule module) - { - var moduleProfiles = _profileRepository.GetByPluginGuid(module.PluginInfo.Guid); - var profileEntity = moduleProfiles.FirstOrDefault(p => p.IsActive) ?? moduleProfiles.FirstOrDefault(); - return profileEntity == null ? null : new ProfileDescriptor(module, profileEntity); - } - public void InstantiateProfile(Profile profile) { profile.PopulateLeds(_surfaceService.ActiveSurface); @@ -245,6 +223,23 @@ namespace Artemis.Core.Services.Storage return new ProfileDescriptor(profileModule, profileEntity); } + public ProfileDescriptor GetLastActiveProfile(ProfileModule module) + { + var moduleProfiles = _profileRepository.GetByPluginGuid(module.PluginInfo.Guid); + var profileEntity = moduleProfiles.FirstOrDefault(p => p.IsActive) ?? moduleProfiles.FirstOrDefault(); + return profileEntity == null ? null : new ProfileDescriptor(module, profileEntity); + } + + private void SaveActiveProfile(ProfileModule module) + { + var profileEntities = _profileRepository.GetByPluginGuid(module.PluginInfo.Guid); + foreach (var profileEntity in profileEntities) + { + profileEntity.IsActive = module.ActiveProfile.EntityId == profileEntity.Id; + _profileRepository.Save(profileEntity); + } + } + /// /// Initializes the properties on the layers of the given profile /// @@ -347,11 +342,6 @@ namespace Artemis.Core.Services.Storage ActiveProfilesInstantiatePlugins(); if (e.PluginInfo.Instance is LayerEffectProvider) ActiveProfilesInstantiatePlugins(); - else if (e.PluginInfo.Instance is ProfileModule profileModule) - { - var activeProfile = GetLastActiveProfile(profileModule); - ActivateProfile(activeProfile); - } } #endregion diff --git a/src/Artemis.Core/Utilities/IntroAnimation.cs b/src/Artemis.Core/Utilities/IntroAnimation.cs index d44860799..879e6d9a4 100644 --- a/src/Artemis.Core/Utilities/IntroAnimation.cs +++ b/src/Artemis.Core/Utilities/IntroAnimation.cs @@ -2,18 +2,13 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text; using Artemis.Core.Extensions; using Artemis.Core.Models.Profile; -using Artemis.Core.Models.Surface; using Artemis.Core.Plugins.Abstract; using Artemis.Core.Plugins.Abstract.ViewModels; -using Artemis.Core.Plugins.LayerBrush; -using Artemis.Core.Services.Interfaces; using Artemis.Core.Services.Storage.Interfaces; using Artemis.Storage.Entities.Profile; using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using Serilog; using SkiaSharp; @@ -33,6 +28,17 @@ namespace Artemis.Core.Utilities CreateIntroProfile(); } + public Profile AnimationProfile { get; set; } + + public void Render(double deltaTime, SKCanvas canvas, SKImageInfo bitmapInfo) + { + if (AnimationProfile == null) + return; + + AnimationProfile.Update(deltaTime); + AnimationProfile.Render(deltaTime, canvas, bitmapInfo); + } + private void CreateIntroProfile() { try @@ -43,10 +49,10 @@ namespace Artemis.Core.Utilities // Inject every LED on the surface into each layer foreach (var profileEntityLayer in profileEntity.Layers) { - profileEntityLayer.Leds.AddRange(_surfaceService.ActiveSurface.Devices.SelectMany(d => d.Leds).Select(l => new LedEntity() + profileEntityLayer.Leds.AddRange(_surfaceService.ActiveSurface.Devices.SelectMany(d => d.Leds).Select(l => new LedEntity { DeviceIdentifier = l.Device.RgbDevice.GetDeviceIdentifier(), - LedName = l.RgbLed.Id.ToString(), + LedName = l.RgbLed.Id.ToString() })); } @@ -61,32 +67,33 @@ namespace Artemis.Core.Utilities _logger.Warning(e, "Failed to load intro profile"); } } - - public Profile AnimationProfile { get; set; } - - public void Render(double deltaTime, SKCanvas canvas, SKImageInfo bitmapInfo) - { - if (AnimationProfile == null) - return; - - AnimationProfile.Update(deltaTime); - AnimationProfile.Render(deltaTime, canvas, bitmapInfo); - } } internal class DummyModule : ProfileModule { public override void EnablePlugin() { + throw new NotImplementedException(); } public override void DisablePlugin() { + throw new NotImplementedException(); + } + + public override void ModuleActivated() + { + throw new NotImplementedException(); + } + + public override void ModuleDeactivated() + { + throw new NotImplementedException(); } public override IEnumerable GetViewModels() { - return new List(); + throw new NotImplementedException(); } } } \ No newline at end of file diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj index 7d5a3bd20..67ac10f68 100644 --- a/src/Artemis.UI/Artemis.UI.csproj +++ b/src/Artemis.UI/Artemis.UI.csproj @@ -121,6 +121,7 @@ + @@ -296,6 +297,7 @@ + diff --git a/src/Artemis.UI/Resources/Images/Sidebar/sidebar-header.png b/src/Artemis.UI/Resources/Images/Sidebar/sidebar-header.png new file mode 100644 index 0000000000000000000000000000000000000000..d377df9d7efcf921d8a7fcdd7eb339f869189e7f GIT binary patch literal 7429 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 - + diff --git a/src/Artemis.UI/Screens/RootViewModel.cs b/src/Artemis.UI/Screens/RootViewModel.cs index cdc5c2fe0..cb4a09650 100644 --- a/src/Artemis.UI/Screens/RootViewModel.cs +++ b/src/Artemis.UI/Screens/RootViewModel.cs @@ -66,13 +66,7 @@ namespace Artemis.UI.Screens get => _mainMessageQueue; set => SetAndNotify(ref _mainMessageQueue, value); } - - public bool IsSidebarVisible - { - get => _isSidebarVisible; - set => SetAndNotify(ref _isSidebarVisible, value); - } - + public bool ActiveItemReady { get => _activeItemReady; @@ -140,7 +134,7 @@ namespace Artemis.UI.Screens { if (e.PropertyName == nameof(SidebarViewModel.SelectedItem)) { - IsSidebarVisible = false; + SidebarViewModel.IsSidebarOpen = false; ActiveItemReady = false; // Allow the menu to close, it's slower but feels more responsive, funny how that works right @@ -254,7 +248,7 @@ namespace Artemis.UI.Screens protected override void OnClose() { SidebarViewModel.Dispose(); - + // Lets force the GC to run after closing the window so it is obvious to users watching task manager // that closing the UI will decrease the memory footprint of the application. diff --git a/src/Artemis.UI/Screens/Sidebar/SidebarView.xaml b/src/Artemis.UI/Screens/Sidebar/SidebarView.xaml index c0e13a66d..ae8d4acfb 100644 --- a/src/Artemis.UI/Screens/Sidebar/SidebarView.xaml +++ b/src/Artemis.UI/Screens/Sidebar/SidebarView.xaml @@ -10,22 +10,19 @@ mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800" d:DataContext="{d:DesignInstance sidebar:SidebarViewModel}"> - - - - - - Active module - - - Profile 1 - Profile 2 - Profile 3 - Profile 4 - - + + + + + + + + - - + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs b/src/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs index c45634af6..2b708ee6f 100644 --- a/src/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs +++ b/src/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using System.Timers; using Artemis.Core.Events; using Artemis.Core.Services.Interfaces; using Artemis.UI.Events; @@ -27,6 +28,9 @@ namespace Artemis.UI.Screens.Sidebar private BindableCollection _sidebarItems; private Dictionary _sidebarModules; private IScreen _selectedItem; + private bool _isSidebarOpen; + private readonly Timer _activeModulesUpdateTimer; + private string _activeModules; public SidebarViewModel(IKernel kernel, IEventAggregator eventAggregator, IModuleVmFactory moduleVmFactory, IPluginService pluginService) { @@ -37,6 +41,10 @@ namespace Artemis.UI.Screens.Sidebar SidebarModules = new Dictionary(); SidebarItems = new BindableCollection(); + _activeModulesUpdateTimer = new Timer(1000); + _activeModulesUpdateTimer.Start(); + _activeModulesUpdateTimer.Elapsed += ActiveModulesUpdateTimerOnElapsed; + _pluginService.PluginEnabled += PluginServiceOnPluginEnabled; _pluginService.PluginDisabled += PluginServiceOnPluginDisabled; @@ -44,6 +52,15 @@ namespace Artemis.UI.Screens.Sidebar eventAggregator.Subscribe(this); } + private void ActiveModulesUpdateTimerOnElapsed(object sender, EventArgs e) + { + if (!IsSidebarOpen) + return; + + var activeModules = SidebarModules.Count(m => m.Value.IsActivated); + ActiveModules = activeModules == 1 ? "1 active module" : $"{activeModules} active modules"; + } + public BindableCollection SidebarItems { get => _sidebarItems; @@ -56,19 +73,35 @@ namespace Artemis.UI.Screens.Sidebar set => SetAndNotify(ref _sidebarModules, value); } + public string ActiveModules + { + get => _activeModules; + set => SetAndNotify(ref _activeModules, value); + } + public IScreen SelectedItem { get => _selectedItem; set => SetAndNotify(ref _selectedItem, value); } + public bool IsSidebarOpen + { + get => _isSidebarOpen; + set + { + SetAndNotify(ref _isSidebarOpen, value); + if (value) + ActiveModulesUpdateTimerOnElapsed(this, EventArgs.Empty); + } + } + public void SetupSidebar() { - SidebarItems.Clear(); + SidebarItems.Clear(); SidebarModules.Clear(); // Add all default sidebar items - SidebarItems.Add(new DividerNavigationItem()); SidebarItems.Add(new FirstLevelNavigationItem {Icon = PackIconKind.Home, Label = "Home"}); SidebarItems.Add(new FirstLevelNavigationItem {Icon = PackIconKind.Newspaper, Label = "News"}); SidebarItems.Add(new FirstLevelNavigationItem {Icon = PackIconKind.TestTube, Label = "Workshop"}); @@ -83,7 +116,7 @@ namespace Artemis.UI.Screens.Sidebar AddModule(module); // Select the top item, which will be one of the defaults - Task.Run(() => SelectSidebarItem(SidebarItems[1])); + Task.Run(() => SelectSidebarItem(SidebarItems[0])); } // ReSharper disable once UnusedMember.Global - Called by view @@ -202,6 +235,9 @@ namespace Artemis.UI.Screens.Sidebar _pluginService.PluginEnabled -= PluginServiceOnPluginEnabled; _pluginService.PluginDisabled -= PluginServiceOnPluginDisabled; + + _activeModulesUpdateTimer.Stop(); + _activeModulesUpdateTimer.Elapsed -= ActiveModulesUpdateTimerOnElapsed; } } } \ No newline at end of file diff --git a/src/Plugins/Artemis.Plugins.Modules.General/DataModel/Windows/WindowDataModel.cs b/src/Plugins/Artemis.Plugins.Modules.General/DataModel/Windows/WindowDataModel.cs index 17e07abd4..e5e7e1da2 100644 --- a/src/Plugins/Artemis.Plugins.Modules.General/DataModel/Windows/WindowDataModel.cs +++ b/src/Plugins/Artemis.Plugins.Modules.General/DataModel/Windows/WindowDataModel.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using Artemis.Core.Extensions; using Artemis.Core.Plugins.Abstract.DataModels.Attributes; using Artemis.Plugins.Modules.General.Utilities; @@ -16,7 +17,7 @@ namespace Artemis.Plugins.Modules.General.DataModel.Windows ProcessName = process.ProcessName; // Accessing MainModule requires admin privileges, this way does not - ProgramLocation = WindowUtilities.GetProcessFilename(process); + ProgramLocation = process.GetProcessFilename(); } public string WindowTitle { get; set; } diff --git a/src/Plugins/Artemis.Plugins.Modules.General/GeneralModule.cs b/src/Plugins/Artemis.Plugins.Modules.General/GeneralModule.cs index cb5af53a0..9eda911f1 100644 --- a/src/Plugins/Artemis.Plugins.Modules.General/GeneralModule.cs +++ b/src/Plugins/Artemis.Plugins.Modules.General/GeneralModule.cs @@ -12,9 +12,28 @@ namespace Artemis.Plugins.Modules.General { public class GeneralModule : ProfileModule { - public override IEnumerable GetViewModels() + public override void EnablePlugin() + { + DisplayName = "General"; + DisplayIcon = "AllInclusive"; + ExpandsDataModel = true; + + DataModel.TestTimeList.Add(new TimeDataModel { CurrentTime = DateTime.Now.AddDays(1), CurrentTimeUTC = DateTime.UtcNow.AddDays(1) }); + DataModel.TestTimeList.Add(new TimeDataModel { CurrentTime = DateTime.Now.AddDays(2), CurrentTimeUTC = DateTime.UtcNow.AddDays(2) }); + DataModel.TestTimeList.Add(new TimeDataModel { CurrentTime = DateTime.Now.AddDays(3), CurrentTimeUTC = DateTime.UtcNow.AddDays(3) }); + DataModel.TestTimeList.Add(new TimeDataModel { CurrentTime = DateTime.Now.AddDays(4), CurrentTimeUTC = DateTime.UtcNow.AddDays(4) }); + } + + public override void DisablePlugin() + { + } + + public override void ModuleActivated() + { + } + + public override void ModuleDeactivated() { - return new List {new GeneralViewModel(this)}; } public override void Update(double deltaTime) @@ -26,20 +45,9 @@ namespace Artemis.Plugins.Modules.General base.Update(deltaTime); } - public override void EnablePlugin() - { - DisplayName = "General"; - DisplayIcon = "AllInclusive"; - ExpandsDataModel = true; - - DataModel.TestTimeList.Add(new TimeDataModel {CurrentTime = DateTime.Now.AddDays(1), CurrentTimeUTC = DateTime.UtcNow.AddDays(1)}); - DataModel.TestTimeList.Add(new TimeDataModel {CurrentTime = DateTime.Now.AddDays(2), CurrentTimeUTC = DateTime.UtcNow.AddDays(2)}); - DataModel.TestTimeList.Add(new TimeDataModel {CurrentTime = DateTime.Now.AddDays(3), CurrentTimeUTC = DateTime.UtcNow.AddDays(3)}); - DataModel.TestTimeList.Add(new TimeDataModel {CurrentTime = DateTime.Now.AddDays(4), CurrentTimeUTC = DateTime.UtcNow.AddDays(4)}); - } - - public override void DisablePlugin() + public override IEnumerable GetViewModels() { + return new List { new GeneralViewModel(this) }; } #region Open windows diff --git a/src/Plugins/Artemis.Plugins.Modules.General/Utilities/WindowUtilities.cs b/src/Plugins/Artemis.Plugins.Modules.General/Utilities/WindowUtilities.cs index 37267cc4f..e8b32b08b 100644 --- a/src/Plugins/Artemis.Plugins.Modules.General/Utilities/WindowUtilities.cs +++ b/src/Plugins/Artemis.Plugins.Modules.General/Utilities/WindowUtilities.cs @@ -1,7 +1,5 @@ using System; -using System.Diagnostics; using System.Runtime.InteropServices; -using System.Text; namespace Artemis.Plugins.Modules.General.Utilities { @@ -14,33 +12,11 @@ namespace Artemis.Plugins.Modules.General.Utilities return (int) processId; } - public static string GetProcessFilename(Process p) - { - var capacity = 2000; - var builder = new StringBuilder(capacity); - var ptr = OpenProcess(ProcessAccessFlags.QueryLimitedInformation, false, p.Id); - if (!QueryFullProcessImageName(ptr, 0, builder, ref capacity)) return string.Empty; - - return builder.ToString(); - } [DllImport("user32.dll")] private static extern IntPtr GetForegroundWindow(); [DllImport("user32.dll")] private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); - - [DllImport("kernel32.dll")] - private static extern bool QueryFullProcessImageName([In] IntPtr hProcess, [In] int dwFlags, [Out] StringBuilder lpExeName, ref int lpdwSize); - - [DllImport("kernel32.dll")] - private static extern IntPtr OpenProcess(ProcessAccessFlags processAccess, bool bInheritHandle, int processId); - - - [Flags] - private enum ProcessAccessFlags : uint - { - QueryLimitedInformation = 0x00001000 - } } } \ No newline at end of file