diff --git a/src/Artemis.Core/Plugins/DeviceProviders/DeviceProvider.cs b/src/Artemis.Core/Plugins/DeviceProviders/DeviceProvider.cs index ee9f256a9..263748f0d 100644 --- a/src/Artemis.Core/Plugins/DeviceProviders/DeviceProvider.cs +++ b/src/Artemis.Core/Plugins/DeviceProviders/DeviceProvider.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Linq; +using System.Threading.Tasks; using RGB.NET.Core; namespace Artemis.Core.DeviceProviders; @@ -15,7 +16,7 @@ public abstract class DeviceProvider : PluginFeature /// The RGB.NET device provider backing this Artemis device provider /// public abstract IRGBDeviceProvider RgbDeviceProvider { get; } - + /// /// A boolean indicating whether this device provider detects the physical layout of connected keyboards. /// @@ -48,6 +49,11 @@ public abstract class DeviceProvider : PluginFeature /// public bool RemoveExcessiveLedsSupported { get; protected set; } = true; + /// + /// Gets or sets a boolean indicating whether suspending the device provider is supported + /// + public bool SuspendSupported { get; protected set; } + /// /// Loads a layout for the specified device and wraps it in an /// @@ -109,4 +115,12 @@ public virtual string GetDeviceLayoutName(ArtemisDevice device) return fileName + ".xml"; } + + /// + /// Called when the device provider is being suspended, like when the system is going to sleep. + /// Note: This will be called while the plugin is disabled. + /// + public virtual void Suspend() + { + } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/DeviceService.cs b/src/Artemis.Core/Services/DeviceService.cs index 111594c4a..5d2da4db4 100644 --- a/src/Artemis.Core/Services/DeviceService.cs +++ b/src/Artemis.Core/Services/DeviceService.cs @@ -2,13 +2,13 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Artemis.Core.DeviceProviders; using Artemis.Core.Providers; using Artemis.Core.Services.Models; using Artemis.Storage.Entities.Surface; using Artemis.Storage.Repositories.Interfaces; -using DryIoc; using RGB.NET.Core; using Serilog; @@ -21,8 +21,10 @@ internal class DeviceService : IDeviceService private readonly IDeviceRepository _deviceRepository; private readonly Lazy _renderService; private readonly Func> _getLayoutProviders; - private readonly List _enabledDevices = new(); - private readonly List _devices = new(); + private readonly List _enabledDevices = []; + private readonly List _devices = []; + private readonly List _suspendedDeviceProviders = []; + private readonly object _suspensionLock = new(); public DeviceService(ILogger logger, IPluginManagementService pluginManagementService, @@ -69,7 +71,7 @@ public void AddDeviceProvider(DeviceProvider deviceProvider) OnDeviceRemoved(new DeviceEventArgs(device)); } - List providerExceptions = new(); + List providerExceptions = []; void DeviceProviderOnException(object? sender, ExceptionEventArgs e) { @@ -95,7 +97,7 @@ void DeviceProviderOnException(object? sender, ExceptionEventArgs e) return; } - List addedDevices = new(); + List addedDevices = []; foreach (IRGBDevice rgbDevice in rgbDeviceProvider.Devices) { ArtemisDevice artemisDevice = GetArtemisDevice(rgbDevice); @@ -184,7 +186,7 @@ public void LoadDeviceLayout(ArtemisDevice device) device.ApplyLayout(null, false, false); else provider?.ApplyLayout(device, layout); - + UpdateLeds(); } catch (Exception e) @@ -241,6 +243,83 @@ public void SaveDevices() UpdateLeds(); } + /// + public void SuspendDeviceProviders() + { + lock (_suspensionLock) + { + _logger.Information("Suspending all device providers"); + + bool wasPaused = _renderService.Value.IsPaused; + try + { + _renderService.Value.IsPaused = true; + foreach (DeviceProvider deviceProvider in _pluginManagementService.GetFeaturesOfType().Where(d => d.SuspendSupported)) + SuspendDeviceProvider(deviceProvider); + } + finally + { + _renderService.Value.IsPaused = wasPaused; + } + } + } + + /// + public void ResumeDeviceProviders() + { + lock (_suspensionLock) + { + _logger.Information("Resuming all device providers"); + + bool wasPaused = _renderService.Value.IsPaused; + try + { + _renderService.Value.IsPaused = true; + foreach (DeviceProvider deviceProvider in _suspendedDeviceProviders.ToList()) + ResumeDeviceProvider(deviceProvider); + } + finally + { + _renderService.Value.IsPaused = wasPaused; + } + } + } + + private void SuspendDeviceProvider(DeviceProvider deviceProvider) + { + if (_suspendedDeviceProviders.Contains(deviceProvider)) + { + _logger.Warning("Device provider {DeviceProvider} is already suspended", deviceProvider.Info.Name); + return; + } + + try + { + _pluginManagementService.DisablePluginFeature(deviceProvider, false); + deviceProvider.Suspend(); + _suspendedDeviceProviders.Add(deviceProvider); + _logger.Information("Device provider {DeviceProvider} suspended", deviceProvider.Info.Name); + } + catch (Exception e) + { + _logger.Error(e, "Device provider {DeviceProvider} failed to suspend", deviceProvider.Info.Name); + } + } + + private void ResumeDeviceProvider(DeviceProvider deviceProvider) + { + try + { + _pluginManagementService.EnablePluginFeature(deviceProvider, false, true); + _suspendedDeviceProviders.Remove(deviceProvider); + _logger.Information("Device provider {DeviceProvider} resumed", deviceProvider.Info.Name); + } + catch (Exception e) + { + _logger.Error(e, "Device provider {DeviceProvider} failed to resume", deviceProvider.Info.Name); + } + } + private ArtemisDevice GetArtemisDevice(IRGBDevice rgbDevice) { string deviceIdentifier = rgbDevice.GetDeviceIdentifier(); diff --git a/src/Artemis.Core/Services/Interfaces/IDeviceService.cs b/src/Artemis.Core/Services/Interfaces/IDeviceService.cs index 51b19d937..5c5c60ad7 100644 --- a/src/Artemis.Core/Services/Interfaces/IDeviceService.cs +++ b/src/Artemis.Core/Services/Interfaces/IDeviceService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Artemis.Core.DeviceProviders; namespace Artemis.Core.Services; @@ -71,6 +72,16 @@ public interface IDeviceService : IArtemisService /// void SaveDevices(); + /// + /// Suspends all active device providers + /// + void SuspendDeviceProviders(); + + /// + /// Resumes all previously active device providers + /// + void ResumeDeviceProviders(); + /// /// Occurs when a single device was added. /// diff --git a/src/Artemis.UI.Shared/Controls/DataModelPicker/DataModelPicker.cs b/src/Artemis.UI.Shared/Controls/DataModelPicker/DataModelPicker.cs index e3168eaba..996dc842c 100644 --- a/src/Artemis.UI.Shared/Controls/DataModelPicker/DataModelPicker.cs +++ b/src/Artemis.UI.Shared/Controls/DataModelPicker/DataModelPicker.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; @@ -69,12 +70,19 @@ public class DataModelPicker : TemplatedControl public static readonly StyledProperty IsEventPickerProperty = AvaloniaProperty.Register(nameof(IsEventPicker)); + private readonly ObservableCollection _searchResults = []; private TextBlock? _currentPathDescription; private TextBlock? _currentPathDisplay; private MaterialIcon? _currentPathIcon; private TreeView? _dataModelTreeView; + private ListBox? _searchListBox; + private TextBox? _searchBox; + private Panel? _searchContainer; + private StackPanel? _searchEmpty; private DispatcherTimer? _updateTimer; + private string? _lastSearch; + private bool _firstUpdate; static DataModelPicker() { @@ -200,7 +208,14 @@ private void Update(object? sender, EventArgs e) if (DataModelUIService == null) return; - DataModelViewModel?.Update(DataModelUIService, new DataModelUpdateConfiguration(!IsEventPicker)); + DataModelViewModel?.Update(DataModelUIService, new DataModelUpdateConfiguration(!IsEventPicker, !string.IsNullOrEmpty(_searchBox?.Text))); + ApplySearch(); + + if (_firstUpdate) + { + _firstUpdate = false; + UpdateCurrentPath(true); + } } private void GetDataModel() @@ -229,14 +244,15 @@ private void DataModelOnUpdateRequested(object? sender, EventArgs e) if (DataModelUIService == null || DataModelViewModel == null) return; - DataModelViewModel.Update(DataModelUIService, new DataModelUpdateConfiguration(IsEventPicker)); + DataModelViewModel.Update(DataModelUIService, new DataModelUpdateConfiguration(!IsEventPicker, !string.IsNullOrEmpty(_searchBox?.Text))); DataModelViewModel.ApplyTypeFilter(true, FilterTypes?.ToArray() ?? Type.EmptyTypes); + ApplySearch(); } - private void DataModelTreeViewOnSelectionChanged(object? sender, SelectionChangedEventArgs e) + private void TreeViewOnSelectionChanged(object? sender, SelectionChangedEventArgs e) { - // Multi-select isn't a think so grab the first one - object? selected = _dataModelTreeView?.SelectedItems[0]; + // Multi-select isn't a thing so grab the first one + object? selected = e.AddedItems[0]; if (selected == null) return; @@ -255,12 +271,48 @@ private void DataModelTreeViewOnDoubleTapped(object? sender, RoutedEventArgs e) treeViewItem.IsExpanded = !treeViewItem.IsExpanded; } + private void ApplySearch() + { + if (DataModelViewModel == null || string.IsNullOrWhiteSpace(_searchBox?.Text)) + { + if (_dataModelTreeView != null) + _dataModelTreeView.IsVisible = true; + if (_searchContainer != null) + _searchContainer.IsVisible = false; + return; + } + + if (_dataModelTreeView != null) + _dataModelTreeView.IsVisible = false; + if (_searchContainer != null) + _searchContainer.IsVisible = true; + + if (_searchBox.Text != _lastSearch) + { + _searchResults.Clear(); + foreach (DataModelVisualizationViewModel searchResult in DataModelViewModel.GetSearchResults(_searchBox.Text).DistinctBy(r => r.Path).Take(20)) + _searchResults.Add(searchResult); + _lastSearch = _searchBox.Text; + } + + if (_searchListBox != null) + { + _searchListBox.IsVisible = _searchResults.Count > 0; + _searchListBox.SelectedItem = _searchResults.FirstOrDefault(r => r.DataModelPath?.Path == DataModelPath?.Path); + } + + if (_searchEmpty != null) + _searchEmpty.IsVisible = _searchResults.Count == 0; + + + } + private void UpdateCurrentPath(bool selectCurrentPath) { - if (DataModelPath == null) + if (DataModelPath == null || _dataModelTreeView == null) return; - if (_dataModelTreeView != null && selectCurrentPath) + if (selectCurrentPath) { // Expand the path DataModel? start = DataModelPath.Target; @@ -286,7 +338,7 @@ private void UpdateCurrentPath(bool selectCurrentPath) protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { if (_dataModelTreeView != null) - _dataModelTreeView.SelectionChanged -= DataModelTreeViewOnSelectionChanged; + _dataModelTreeView.SelectionChanged -= TreeViewOnSelectionChanged; if (_dataModelTreeView != null) _dataModelTreeView.DoubleTapped -= DataModelTreeViewOnDoubleTapped; @@ -294,11 +346,22 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) _currentPathDisplay = e.NameScope.Find("CurrentPathDisplay"); _currentPathDescription = e.NameScope.Find("CurrentPathDescription"); _dataModelTreeView = e.NameScope.Find("DataModelTreeView"); + _searchBox = e.NameScope.Find("SearchBox"); + _searchContainer = e.NameScope.Find("SearchContainer"); + _searchListBox = e.NameScope.Find("SearchListBox"); + _searchEmpty = e.NameScope.Find("SearchEmpty"); if (_dataModelTreeView != null) - _dataModelTreeView.SelectionChanged += DataModelTreeViewOnSelectionChanged; - if (_dataModelTreeView != null) + { + _dataModelTreeView.SelectionChanged += TreeViewOnSelectionChanged; _dataModelTreeView.DoubleTapped += DataModelTreeViewOnDoubleTapped; + } + + if (_searchListBox != null) + { + _searchListBox.ItemsSource = _searchResults; + _searchListBox.SelectionChanged += TreeViewOnSelectionChanged; + } } #region Overrides of Visual @@ -307,17 +370,23 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { GetDataModel(); - UpdateCurrentPath(true); + _updateTimer = new DispatcherTimer(TimeSpan.FromMilliseconds(200), DispatcherPriority.Background, Update); _updateTimer.Start(); + _firstUpdate = true; } /// protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { DataModelViewModel?.Dispose(); + _updateTimer?.Stop(); _updateTimer = null; + + _lastSearch = null; + if (_searchBox != null) + _searchBox.Text = null; } #endregion diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelEventViewModel.cs b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelEventViewModel.cs index b3c16c860..312d95a36 100644 --- a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelEventViewModel.cs +++ b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelEventViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using Artemis.Core; using Artemis.Core.Modules; @@ -41,13 +42,21 @@ public override void Update(IDataModelUIService dataModelUIService, DataModelUpd } // Only update children if the parent is expanded - if (Parent != null && !Parent.IsRootViewModel && !Parent.IsVisualizationExpanded) + if (Parent != null && !Parent.IsRootViewModel && !Parent.IsVisualizationExpanded && (configuration == null || !configuration.UpdateAllChildren)) return; foreach (DataModelVisualizationViewModel dataModelVisualizationViewModel in Children) dataModelVisualizationViewModel.Update(dataModelUIService, configuration); } + /// + public override IEnumerable GetSearchResults(string search) + { + if (PropertyDescription?.Name != null && PropertyDescription.Name.Contains(search, StringComparison.OrdinalIgnoreCase)) + return [this]; + return []; + } + /// /// Always returns for data model events /// diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListItemViewModel.cs b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListItemViewModel.cs index 0a553339c..fd7a7a74f 100644 --- a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListItemViewModel.cs +++ b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListItemViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Artemis.UI.Shared.Services; using ReactiveUI; @@ -62,6 +63,12 @@ public object? DisplayValue internal set => this.RaiseAndSetIfChanged(ref _displayValue, value); } + /// + public override IEnumerable GetSearchResults(string search) + { + return []; + } + /// public override object? GetCurrentValue() { diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListViewModel.cs b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListViewModel.cs index 0d0ca66dd..b04d61d2b 100644 --- a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListViewModel.cs +++ b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListViewModel.cs @@ -1,5 +1,6 @@ using System; using System.Collections; +using System.Collections.Generic; using System.Collections.ObjectModel; using Artemis.Core; using Artemis.Core.Modules; @@ -119,6 +120,17 @@ public override void Update(IDataModelUIService dataModelUIService, DataModelUpd CountDisplay = $"{ListChildren.Count} {(ListChildren.Count == 1 ? "item" : "items")}"; } + /// + public override IEnumerable GetSearchResults(string search) + { + if (PropertyDescription?.Name != null && PropertyDescription.Name.Contains(search, StringComparison.OrdinalIgnoreCase)) + return [this]; + if (PropertyDescription?.Description != null && PropertyDescription.Description.Contains(search, StringComparison.OrdinalIgnoreCase)) + return [this]; + + return []; + } + /// public override string ToString() { diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelPropertiesViewModel.cs b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelPropertiesViewModel.cs index fe1d226c4..ca03c5c26 100644 --- a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelPropertiesViewModel.cs +++ b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelPropertiesViewModel.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using Artemis.Core; using Artemis.Core.Modules; using Artemis.UI.Shared.Services; @@ -52,14 +54,20 @@ public override void Update(IDataModelUIService dataModelUIService, DataModelUpd // Always populate properties PopulateProperties(dataModelUIService, configuration); - // Only update children if the parent is expanded - if (Parent != null && !Parent.IsRootViewModel && !Parent.IsVisualizationExpanded) + // Only update children if the parent is expanded or when searching + if (Parent != null && !Parent.IsRootViewModel && !Parent.IsVisualizationExpanded && (configuration == null || !configuration.UpdateAllChildren)) return; - + foreach (DataModelVisualizationViewModel dataModelVisualizationViewModel in Children) dataModelVisualizationViewModel.Update(dataModelUIService, configuration); } + /// + public override IEnumerable GetSearchResults(string search) + { + return Children.SelectMany(c => c.GetSearchResults(search)); + } + /// public override object? GetCurrentValue() { diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelPropertyViewModel.cs b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelPropertyViewModel.cs index a68ee78a2..69e60578d 100644 --- a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelPropertyViewModel.cs +++ b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelPropertyViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Artemis.Core; using Artemis.Core.Modules; using Artemis.UI.Shared.Services; @@ -71,6 +72,17 @@ public override void Update(IDataModelUIService dataModelUIService, DataModelUpd DisplayViewModel?.UpdateValue(DisplayValue); } + /// + public override IEnumerable GetSearchResults(string search) + { + if (PropertyDescription?.Name != null && PropertyDescription.Name.Contains(search, StringComparison.OrdinalIgnoreCase)) + return [this]; + if (DisplayValue != null && DisplayValue.ToString()?.Contains(search, StringComparison.OrdinalIgnoreCase) == true) + return [this]; + + return []; + } + /// public override string ToString() { diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelUpdateConfiguration.cs b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelUpdateConfiguration.cs index 1cf9dcce7..f4f30e082 100644 --- a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelUpdateConfiguration.cs +++ b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelUpdateConfiguration.cs @@ -8,14 +8,21 @@ public class DataModelUpdateConfiguration /// /// Creates a new instance of the class /// - /// A boolean indicating whether or not event children should be created - public DataModelUpdateConfiguration(bool createEventChildren) + /// A boolean indicating whether event children should be created + /// A boolean indicating whether all children should be updated + public DataModelUpdateConfiguration(bool createEventChildren, bool updateAllChildren) { CreateEventChildren = createEventChildren; + UpdateAllChildren = updateAllChildren; } /// - /// Gets a boolean indicating whether or not event children should be created + /// Gets a boolean indicating whether event children should be created /// public bool CreateEventChildren { get; } + + /// + /// Gets a boolean indicating whether all children should be updated + /// + public bool UpdateAllChildren { get; } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelVisualizationViewModel.cs b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelVisualizationViewModel.cs index 77f429408..95d07a35b 100644 --- a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelVisualizationViewModel.cs +++ b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelVisualizationViewModel.cs @@ -144,6 +144,13 @@ public bool IsVisualizationExpanded /// The configuration to apply while updating public abstract void Update(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration? configuration); + /// + /// Gets the search results for the provided search string + /// + /// The search string + /// The search results + public abstract IEnumerable GetSearchResults(string search); + /// /// Gets the current value of the property being visualized /// @@ -281,7 +288,7 @@ internal void PopulateProperties(IDataModelUIService dataModelUIService, DataMod foreach (PropertyInfo propertyInfo in modelType.GetProperties(BindingFlags.Public | BindingFlags.Instance).OrderBy(t => t.MetadataToken)) { string childPath = AppendToPath(propertyInfo.Name); - + if (!ShouldIncludePath(childPath, propertyInfo)) continue; diff --git a/src/Artemis.UI.Shared/Styles/Controls/DataModelPicker.axaml b/src/Artemis.UI.Shared/Styles/Controls/DataModelPicker.axaml index 0175ae880..18df7201a 100644 --- a/src/Artemis.UI.Shared/Styles/Controls/DataModelPicker.axaml +++ b/src/Artemis.UI.Shared/Styles/Controls/DataModelPicker.axaml @@ -11,38 +11,43 @@