Files
PowerToys/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs
moooyo 28a9bbe8f0 [PowerDisplay] Add configurable mouse wheel increment for slider controls (#49002)
## Summary of the Pull Request

Adds a Settings option that controls how much the PowerDisplay flyout
sliders (brightness, contrast, volume) change per mouse-wheel notch. The
value is chosen from a **preset dropdown** (`1, 2, 5, 10, 15, 20, 25`)
and defaults to **5**, preserving today's behavior. Previously the
per-notch step was hardcoded as
`helpers:SliderExtensions.MouseWheelChange="5"` in four places in the
flyout.

<img width="1282" height="620" alt="image"
src="https://github.com/user-attachments/assets/3b299a47-eb7b-4b53-b3dc-0540fbb25bfc"
/>


## PR Checklist

- [x] Closes: #48805
- [x] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [x] **Tests:** Added/updated and all pass — _added
`MouseWheelIncrementSettingsTests` to the existing
`PowerDisplay.Lib.UnitTests`; passing via `vstest.console.exe`._
- [x] **Localization:** All end-user-facing strings can be localized —
_new `PowerDisplay_MouseWheelIncrement.Header`/`.Description` in
`en-us/Resources.resw`, surfaced via `x:Uid`._
- [ ] **Dev docs:** Added/updated — _N/A: small settings addition, no
behavioral/architecture docs affected._
- [ ] **New binaries:** Added on the required places — _N/A: no new
binaries or projects (the unit test was added to an existing test
project)._
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** _N/A._

## Detailed Description of the Pull Request / Additional comments

A single `int MouseWheelIncrement` is added to `PowerDisplayProperties`
and persisted to `settings.json` under `mouse_wheel_increment`; it is
edited in the Settings UI and read by the flyout app. The value applies
uniformly to all four flyout sliders.

**Data model** —
`src/settings-ui/Settings.UI.Library/PowerDisplayProperties.cs`
- `MouseWheelIncrement` (`int`,
`[JsonPropertyName("mouse_wheel_increment")]`, default `5` set in the
constructor, mirroring `MonitorRefreshDelay`). Old `settings.json`
without the key deserializes to `5` (no migration). The whole type is
already registered with the source-generated JSON contexts, so the new
property serializes on both the Settings and flyout sides with no
context change.

**Settings UI (write side)** — `src/settings-ui/Settings.UI/...`
- `PowerDisplayViewModel`: `MouseWheelIncrement` (get/set via
`SetSettingsProperty`, calls `SignalSettingsUpdated()` so an open flyout
updates live) and `MouseWheelIncrementOptions` = `{ 1, 2, 5, 10, 15, 20,
25 }`.
- `PowerDisplayPage.xaml`: a `ComboBox` `SettingsCard` in the
flyout-settings expander, immediately after "Monitor refresh delay",
matching that card's markup.
- `Strings/en-us/Resources.resw`: `Mouse wheel increment` + description.

**Flyout (read side)** — `src/modules/powerdisplay/PowerDisplay/...`
- `MainViewModel`: `[ObservableProperty] int MouseWheelIncrement`
(default 5), loaded from settings in `LoadUIDisplaySettings()` (runs at
startup and on the settings-updated IPC event, so the all-displays
slider updates live).
- `MonitorViewModel`: a read-only proxy `MouseWheelIncrement =>
_mainViewModel?.MouseWheelIncrement ?? 5` plus
`RefreshMouseWheelIncrement()`, called from `ApplySettingsFromUI`'s
per-monitor loop so the per-monitor sliders update live.
- `MainWindow.xaml`: the four sliders'
`SliderExtensions.MouseWheelChange` now bind to the setting — the
all-displays slider to `ViewModel.MouseWheelIncrement`, the three
per-monitor sliders (brightness/contrast/volume, inside the
`MonitorViewModel` `DataTemplate`) to `MouseWheelIncrement`.

## Validation Steps Performed

**Automated**
- Unit tests
(`PowerDisplay.Lib.UnitTests/MouseWheelIncrementSettingsTests.cs`), run
via `vstest.console.exe` — passing:
  - default value is `5`;
- legacy `settings.json` missing the key deserializes to `5` (no
migration);
  - round-trip preserves a non-default value;
  - serialization emits the `mouse_wheel_increment` snake_case key.
- `PowerToys.Settings` and the PowerDisplay flyout app both compile
clean (the XAML compiler validates the new `x:Bind` bindings).
- Confirmed the property flows through the source-generated JSON
contexts (whole-type `[JsonSerializable(typeof(PowerDisplaySettings))]`)
on both the Settings and flyout sides.

**Pending (manual, on-device — reason this PR is a draft)**
- [ ] Settings: the dropdown shows `5` on a fresh profile; changing it
writes the new `mouse_wheel_increment` value to `settings.json`.
- [ ] Flyout: scrolling each slider (all-displays brightness,
per-monitor brightness/contrast/volume) steps by the selected value,
including live update while the flyout is open.

---------

Co-authored-by: Yu Leng <yuleng@microsoft.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 07:56:44 +00:00

555 lines
21 KiB
C#

// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.UI;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Windowing;
using PowerDisplay.Common.Drivers;
using PowerDisplay.Common.Drivers.DDC;
using PowerDisplay.Common.Models;
using PowerDisplay.Common.Services;
using PowerDisplay.Helpers;
using PowerDisplay.Models;
using PowerDisplay.PowerDisplayXAML;
namespace PowerDisplay.ViewModels;
/// <summary>
/// Main ViewModel for the PowerDisplay application.
/// Split into partial classes for better maintainability:
/// - MainViewModel.cs: Core properties, construction, and disposal
/// - MainViewModel.Monitors.cs: Monitor discovery and management
/// - MainViewModel.Settings.cs: Settings UI synchronization and profiles
/// </summary>
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicMethods)]
public partial class MainViewModel : ObservableObject, IDisposable
{
[LibraryImport("user32.dll", EntryPoint = "GetMonitorInfoW", StringMarshalling = StringMarshalling.Utf16)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool GetMonitorInfo(IntPtr hMonitor, ref MonitorInfoEx lpmi);
private readonly MonitorManager _monitorManager;
private readonly DispatcherQueue _dispatcherQueue;
private readonly CancellationTokenSource _cancellationTokenSource;
private readonly SettingsUtils _settingsUtils;
private readonly MonitorStateManager _stateManager;
private readonly DisplayChangeWatcher _displayChangeWatcher;
private readonly ISystemClock _clock;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasMonitors))]
[NotifyPropertyChangedFor(nameof(ShowNoMonitorsMessage))]
[NotifyPropertyChangedFor(nameof(IsInteractionEnabled))]
[NotifyPropertyChangedFor(nameof(IsLinkedBrightnessSliderEnabled))]
public partial bool IsScanning { get; set; }
private bool _isInitialized;
private bool _isLoading;
[ObservableProperty]
public partial ObservableCollection<MonitorViewModel> Monitors { get; set; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasProfiles))]
[NotifyPropertyChangedFor(nameof(ShowProfileSwitcherButton))]
public partial ObservableCollection<PowerDisplayProfile> Profiles { get; set; }
/// <summary>
/// Event triggered when UI refresh is requested due to settings changes
/// </summary>
public event EventHandler? UIRefreshRequested;
/// <summary>
/// Event triggered when initial monitor discovery is completed.
/// Used by MainWindow to know when data is ready for display.
/// </summary>
public event EventHandler? InitializationCompleted;
public MainViewModel()
: this(new SystemClock())
{
}
internal MainViewModel(ISystemClock clock)
{
_clock = clock;
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
_cancellationTokenSource = new CancellationTokenSource();
Monitors = new ObservableCollection<MonitorViewModel>();
Profiles = new ObservableCollection<PowerDisplayProfile>();
IsScanning = true;
ShowProfileSwitcher = true;
ShowIdentifyMonitorsButton = true;
MouseWheelIncrement = 5;
// Initialize settings utils
_settingsUtils = SettingsUtils.Default;
_stateManager = new MonitorStateManager();
// Initialize the monitor manager
_monitorManager = new MonitorManager();
// Load profiles for quick apply feature
LoadProfiles();
// Load UI display settings (profile switcher, identify button, color temp switcher)
LoadUIDisplaySettings();
// Initialize display change watcher for auto-refresh on monitor plug/unplug
// Use MonitorRefreshDelay from settings to allow hardware to stabilize after plug/unplug
var settings = _settingsUtils.GetSettingsOrDefault<PowerDisplaySettings>(PowerDisplaySettings.ModuleName);
int delaySeconds = Math.Clamp(settings?.Properties?.MonitorRefreshDelay ?? 5, 1, 30);
_displayChangeWatcher = new DisplayChangeWatcher(_dispatcherQueue, TimeSpan.FromSeconds(delaySeconds));
_displayChangeWatcher.DisplayChanged += OnDisplayChanged;
_displayChangeWatcher.DisplayChanging += OnDisplayChanging;
// Start initial discovery
_ = InitializeAsync(_cancellationTokenSource.Token);
}
public bool HasProfiles => Profiles.Count > 0;
// UI display control properties - loaded from settings
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ShowProfileSwitcherButton))]
public partial bool ShowProfileSwitcher { get; set; }
[ObservableProperty]
public partial bool ShowIdentifyMonitorsButton { get; set; }
/// <summary>
/// Gets or sets the per-mouse-wheel-notch step applied to every flyout slider. Loaded from
/// PowerDisplaySettings; defaults to 5 (the historical hardcoded step).
/// </summary>
[ObservableProperty]
public partial int MouseWheelIncrement { get; set; }
/// <summary>
/// Gets or sets a value indicating whether brightness slider changes are broadcast to all
/// non-excluded monitors as one linked level. Persisted in <c>PowerDisplaySettings</c> so
/// the choice survives restarts. The toggle is meaningful only when two or more monitors
/// are connected — see <see cref="ShowLinkLevelsToggle"/> for the visibility gate.
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ShowIndividualDisplays))]
[NotifyPropertyChangedFor(nameof(ShowLinkLevelsToggle))]
[NotifyPropertyChangedFor(nameof(ShowLinkLevelsInactiveIcon))]
public partial bool LinkedLevelsActive { get; set; }
/// <summary>
/// Gets or sets the brightness value driving the linked "All Displays" slider. Setter is
/// debounced and broadcasts on commit to every linked target: a
/// <see cref="MonitorViewModel.SupportsBrightness"/> monitor not excluded from sync (see
/// <c>OnLinkedBrightnessChanged</c>). Synchronously updates each linked monitor's brightness
/// value so it is correct if the monitor is later excluded or link mode is disabled — the
/// linked broadcast is the single source of hardware writes while link mode is active.
/// </summary>
[ObservableProperty]
public partial int LinkedBrightness { get; set; }
/// <summary>
/// Gets a value indicating whether the linked brightness slider has at least one linked
/// brightness-capable monitor to control.
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsLinkedBrightnessSliderEnabled))]
public partial bool IsLinkedBrightnessAvailable { get; set; }
/// <summary>
/// Gets a value indicating whether the linked "All Displays" brightness slider accepts input.
/// Disabled both when no linked target is available and while monitor discovery/restore is busy.
/// </summary>
public bool IsLinkedBrightnessSliderEnabled => IsLinkedBrightnessAvailable && IsInteractionEnabled;
/// <summary>
/// Gets the count of monitors that participate in linked brightness — used for the
/// "All Displays" card subtitle ("N linked"). Counts brightness-capable monitors the user
/// has not excluded (see <see cref="MonitorViewModel.IsExcludedFromSync"/>).
/// </summary>
public int LinkedMonitorsCount => Monitors.Count(m => m.SupportsBrightness && !IsMonitorExcludedFromSync(m.Id));
/// <summary>
/// Gets the count of brightness-capable monitors excluded from linked brightness.
/// </summary>
public int ExcludedMonitorsCount => Monitors.Count(m => m.SupportsBrightness && IsMonitorExcludedFromSync(m.Id));
/// <summary>
/// Gets a value indicating whether the "N excluded" subtitle fragment should be visible on
/// the "All displays" card.
/// </summary>
public bool HasExcludedMonitors => ExcludedMonitorsCount > 0;
/// <summary>
/// Gets the localized "N linked" subtitle shown on the "All Displays" card. Recomputed
/// alongside <see cref="LinkedMonitorsCount"/> via <c>RecomputeLinkedBrightnessAvailability</c>.
/// </summary>
public string LinkedMonitorsCountText =>
string.Format(
System.Globalization.CultureInfo.CurrentCulture,
Helpers.ResourceLoaderInstance.ResourceLoader.GetString("AllDisplaysLinkedCountFormat"),
LinkedMonitorsCount);
/// <summary>
/// Gets the localized "N excluded" subtitle fragment shown on the "All Displays" card.
/// </summary>
public string ExcludedMonitorsCountText =>
string.Format(
System.Globalization.CultureInfo.CurrentCulture,
Helpers.ResourceLoaderInstance.ResourceLoader.GetString("AllDisplaysExcludedCountFormat"),
ExcludedMonitorsCount);
/// <summary>
/// Gets or sets a value indicating whether the "Individual displays" section is expanded
/// while link mode is on. Collapsed by default so linked mode reads as a single master
/// slider; expanding reveals the per-monitor cards (with linked monitors' sliders disabled).
/// Ignored when link mode is off — see <see cref="ShowIndividualDisplays"/>.
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ShowIndividualDisplays))]
[NotifyPropertyChangedFor(nameof(ShowIndividualDisplaysCollapsedIcon))]
public partial bool IndividualDisplaysExpanded { get; set; }
/// <summary>
/// Gets a value indicating whether the per-monitor cards list is shown. Always shown when
/// link mode is off (the classic layout); when link mode is on the cards are tucked into the
/// collapsible "Individual displays" section and only shown once the user expands it.
/// </summary>
public bool ShowIndividualDisplays => !LinkedLevelsActive || IndividualDisplaysExpanded;
public bool ShowIndividualDisplaysCollapsedIcon => !IndividualDisplaysExpanded;
/// <summary>
/// Gets a value indicating whether the link-levels toggle should be visible. Shown when at
/// least two monitors report <see cref="MonitorViewModel.SupportsBrightness"/> (the entry-point
/// gate — counting raw entries would show the toggle even when only one display can actually be
/// driven), OR whenever link mode is already active. The second clause guarantees the user can
/// always turn link mode back off, even if monitors were unplugged down to one controllable
/// display while linked — otherwise the "All displays" card would strand them with no way out.
/// Recomputed by <see cref="UpdateMonitorList"/> and on <see cref="LinkedLevelsActive"/> change.
/// </summary>
public bool ShowLinkLevelsToggle => LinkedLevelsActive || Monitors.Count(m => m.SupportsBrightness) >= 2;
public bool ShowLinkLevelsInactiveIcon => !LinkedLevelsActive;
/// <summary>
/// Gets a value indicating whether to show the profile switcher button.
/// Combines settings value with HasProfiles check.
/// </summary>
public bool ShowProfileSwitcherButton => ShowProfileSwitcher && HasProfiles;
// Custom VCP mappings - loaded from settings
private List<CustomVcpValueMapping> _customVcpMappings = new();
/// <summary>
/// Gets or sets the custom VCP value name mappings.
/// These mappings override the default VCP value names for color temperature and input source.
/// </summary>
public List<CustomVcpValueMapping> CustomVcpMappings
{
get => _customVcpMappings;
set
{
_customVcpMappings = value ?? new List<CustomVcpValueMapping>();
OnPropertyChanged();
}
}
public bool HasMonitors => !IsScanning && Monitors.Count > 0;
public bool ShowNoMonitorsMessage => !IsScanning && Monitors.Count == 0;
public bool IsInitialized
{
get => _isInitialized;
private set
{
_isInitialized = value;
OnPropertyChanged();
}
}
public bool IsLoading
{
get => _isLoading;
private set
{
_isLoading = value;
OnPropertyChanged();
OnPropertyChanged(nameof(IsInteractionEnabled));
OnPropertyChanged(nameof(IsLinkedBrightnessSliderEnabled));
}
}
/// <summary>
/// Gets a value indicating whether user interaction is enabled (not loading or scanning).
/// </summary>
public bool IsInteractionEnabled => !IsLoading && !IsScanning;
[RelayCommand]
private async Task RefreshAsync() => await RefreshMonitorsAsync();
[RelayCommand]
private unsafe void IdentifyMonitors()
{
try
{
// Get all display areas (virtual desktop regions)
var displayAreas = DisplayArea.FindAll();
// Get all monitor info from QueryDisplayConfig
var allDisplayInfo = DisplayConfigInventory.GetAllMonitorDisplayInfo().Values.ToList();
// Build GDI name to MonitorNumber(s) mapping
// Note: In mirror mode, multiple monitors may share the same GdiDeviceName
var gdiToMonitorNumbers = allDisplayInfo
.Where(info => info.MonitorNumber > 0)
.GroupBy(info => info.GdiDeviceName, StringComparer.OrdinalIgnoreCase)
.ToDictionary(
g => g.Key,
g => g.Select(info => info.MonitorNumber).Distinct().OrderBy(n => n).ToList(),
StringComparer.OrdinalIgnoreCase);
// For each DisplayArea, get its HMONITOR, then get GDI device name to find MonitorNumber(s)
int windowsCreated = 0;
for (int i = 0; i < displayAreas.Count; i++)
{
var displayArea = displayAreas[i];
// Convert DisplayId to HMONITOR
var hMonitor = Win32Interop.GetMonitorFromDisplayId(displayArea.DisplayId);
if (hMonitor == IntPtr.Zero)
{
continue;
}
// Get GDI device name from HMONITOR
var monitorInfo = new MonitorInfoEx { CbSize = (uint)sizeof(MonitorInfoEx) };
if (!GetMonitorInfo(hMonitor, ref monitorInfo))
{
continue;
}
var gdiDeviceName = monitorInfo.GetDeviceName();
// Look up MonitorNumber(s) by GDI device name
if (!gdiToMonitorNumbers.TryGetValue(gdiDeviceName, out var monitorNumbers) || monitorNumbers.Count == 0)
{
continue;
}
// Format display text: single number for normal mode, "1|2" for mirror mode
var displayText = string.Join("|", monitorNumbers);
// Create and position identify window.
// Position before Activate so the window appears directly at the target
// location — avoiding a visible flicker from the default spawn position
// and skipping a WM_DPICHANGED round-trip when crossing DPI monitors.
var identifyWindow = new IdentifyWindow(displayText);
identifyWindow.PositionOnDisplay(displayArea);
identifyWindow.Activate();
windowsCreated++;
}
}
catch (Exception ex)
{
Logger.LogError($"Failed to identify monitors: {ex.Message}");
}
}
[RelayCommand]
private async Task ApplyProfile(PowerDisplayProfile? profile)
{
if (profile != null && profile.IsValid())
{
await ApplyProfileAsync(profile.MonitorSettings);
}
}
public void Dispose()
{
// Cancel all async operations first
_cancellationTokenSource?.Cancel();
// Stop the linked-brightness debounce timer so its Tick handler does not fire after
// we have already cleared Monitors below (broadcast would iterate an empty list).
try
{
_linkedBrightnessCommitTimer?.Stop();
}
catch
{
}
// Dispose each resource independently to ensure all get cleaned up
try
{
if (_displayChangeWatcher is not null)
{
_displayChangeWatcher.DisplayChanging -= OnDisplayChanging;
_displayChangeWatcher.DisplayChanged -= OnDisplayChanged;
_displayChangeWatcher.Dispose();
}
}
catch
{
}
// Dispose monitor view models
foreach (var vm in Monitors)
{
try
{
vm.Dispose();
}
catch
{
}
}
try
{
_monitorManager?.Dispose();
}
catch
{
}
try
{
_stateManager?.Dispose();
}
catch
{
}
try
{
_cancellationTokenSource?.Dispose();
}
catch
{
}
try
{
Monitors.Clear();
}
catch
{
}
GC.SuppressFinalize(this);
}
/// <summary>
/// Load profiles from disk for quick apply feature
/// </summary>
private void LoadProfiles()
{
try
{
var profilesData = ProfileService.LoadProfiles();
Profiles.Clear();
foreach (var profile in profilesData.Profiles)
{
Profiles.Add(profile);
}
OnPropertyChanged(nameof(HasProfiles));
OnPropertyChanged(nameof(ShowProfileSwitcherButton));
}
catch (Exception ex)
{
Logger.LogError($"[Profile] Failed to load profiles: {ex.Message}");
}
}
/// <summary>
/// Load UI display settings from settings file
/// </summary>
private void LoadUIDisplaySettings()
{
try
{
var settings = _settingsUtils.GetSettingsOrDefault<PowerDisplaySettings>(PowerDisplaySettings.ModuleName);
ShowProfileSwitcher = settings.Properties.ShowProfileSwitcher;
ShowIdentifyMonitorsButton = settings.Properties.ShowIdentifyMonitorsButton;
MouseWheelIncrement = settings.Properties.MouseWheelIncrement;
// Load the linked-brightness exclusion set before applying LinkedLevelsActive. If this
// method runs after monitors are already discovered, the toggle hook can seed the master
// slider immediately and must see the persisted exclusions.
LoadExcludedMonitorIds(settings.Properties.ExcludedFromSyncMonitorIds);
LinkedLevelsActive = settings.Properties.LinkedLevelsActive;
// Load custom VCP mappings (now using shared type from PowerDisplay.Common.Models)
CustomVcpMappings = settings.Properties.CustomVcpMappings?.ToList() ?? new List<CustomVcpValueMapping>();
Logger.LogInfo($"[Settings] Loaded {CustomVcpMappings.Count} custom VCP mappings");
}
catch (Exception ex)
{
Logger.LogError($"[Settings] Failed to load UI display settings: {ex.Message}");
}
}
/// <summary>
/// Invoked synchronously as soon as a display configuration change is
/// detected (device added/removed, or wake from sleep), before the debounce
/// delay elapses. Locks the interactive UI by setting IsScanning = true so
/// the user cannot operate on monitors that are about to disappear or be
/// re-enumerated by the rediscovery pass <see cref="OnDisplayChanged"/>
/// will run once debounce completes.
/// </summary>
private void OnDisplayChanging(object? sender, EventArgs e)
{
CancelPendingLinkedBrightnessCommit();
if (!IsScanning)
{
Logger.LogInfo("[MainViewModel] Display change detected — locking UI ahead of rediscovery");
IsScanning = true;
}
}
/// <summary>
/// Handles display configuration changes once the DisplayChangeWatcher's
/// debounce delay has elapsed. IsScanning was already set by
/// <see cref="OnDisplayChanging"/> when the change was first detected, so
/// we just run discovery here.
/// </summary>
private async void OnDisplayChanged(object? sender, EventArgs e)
{
await RefreshMonitorsAsync(skipScanningCheck: true);
}
/// <summary>
/// Starts watching for display changes. Call after initialization is complete.
/// </summary>
public void StartDisplayWatching()
{
_displayChangeWatcher.Start();
}
/// <summary>
/// Stops watching for display changes.
/// </summary>
public void StopDisplayWatching()
{
_displayChangeWatcher.Stop();
}
}