diff --git a/doc/devdocs/modules/powerdisplay/design.md b/doc/devdocs/modules/powerdisplay/design.md
index 4886970330..18cbf99de5 100644
--- a/doc/devdocs/modules/powerdisplay/design.md
+++ b/doc/devdocs/modules/powerdisplay/design.md
@@ -173,28 +173,51 @@ src/modules/powerdisplay/
│ │ └── IMonitorController.cs # Controller abstraction
│ ├── Models/
│ │ ├── Monitor.cs # Runtime monitor data
+│ │ ├── MonitorOperationResult.cs # Operation result enum
│ │ ├── PowerDisplayProfile.cs # Profile definition
+│ │ ├── PowerDisplayProfiles.cs # Profile collection
│ │ └── ProfileMonitorSetting.cs # Per-monitor settings
│ ├── Services/
-│ │ ├── ProfileService.cs # Profile persistence
-│ │ ├── MonitorStateManager.cs # State persistence
-│ │ └── LightSwitchListener.cs # Theme change listener
+│ │ ├── LightSwitchListener.cs # Theme change listener
+│ │ ├── MonitorStateManager.cs # State persistence (debounced)
+│ │ └── ProfileService.cs # Profile persistence
│ └── Utils/
+│ ├── ColorTemperatureHelper.cs # Color temp utilities
│ ├── MccsCapabilitiesParser.cs # DDC/CI capabilities parser
-│ └── ColorTemperatureHelper.cs
+│ └── VcpCapabilities.cs # VCP capabilities model
│
├── PowerDisplay/ # WinUI 3 application
-│ ├── Core/
-│ │ └── MonitorManager.cs # Discovery orchestrator
+│ ├── Assets/ # App icons and images
+│ ├── Common/
+│ │ ├── Debouncer/
+│ │ │ └── SimpleDebouncer.cs # Slider input debouncing
+│ │ └── Models/
+│ │ └── Monitor.cs # UI-layer monitor model
+│ ├── Converters/ # XAML value converters
+│ ├── Helpers/
+│ │ ├── DisplayChangeWatcher.cs # Monitor hot-plug detection (WinRT DeviceWatcher)
+│ │ ├── DisplayRotationService.cs # Display rotation control
+│ │ ├── MonitorManager.cs # Discovery orchestrator
+│ │ ├── NativeMethodsHelper.cs # Window positioning
+│ │ ├── TrayIconService.cs # System tray integration
+│ │ └── WindowHelpers.cs # Window utilities
+│ ├── Strings/ # Localization resources
+│ ├── Styles/ # Custom control styles
│ ├── ViewModels/
-│ │ ├── MainViewModel.cs
-│ │ └── MonitorViewModel.cs
+│ │ ├── MainViewModel.cs # Main VM (partial class)
+│ │ ├── MainViewModel.Monitors.cs # Monitor discovery methods
+│ │ ├── MainViewModel.Profiles.cs # Profile management methods
+│ │ ├── MainViewModel.Settings.cs # Settings persistence methods
+│ │ └── MonitorViewModel.cs # Per-monitor VM
│ └── Views/
-│ └── MainWindow.xaml
+│ ├── MainWindow.xaml # Main UI window
+│ └── MainWindow.xaml.cs
│
└── PowerDisplayModuleInterface/ # C++ DLL (module interface)
├── dllmain.cpp # PowertoyModuleIface impl
- └── Constants.h # Module constants
+ ├── Constants.h # Module constants
+ ├── pch.h / pch.cpp # Precompiled headers
+ └── trace.h / trace.cpp # Telemetry tracing
```
---
@@ -221,6 +244,7 @@ flowchart TB
MainViewModel
MonitorViewModel
MonitorManager
+ DisplayChangeWatcher["DisplayChangeWatcher
(Hot-Plug Detection)"]
end
subgraph PowerDisplayLib["PowerDisplay.Lib"]
@@ -259,6 +283,7 @@ flowchart TB
LightSwitchListener -.->|"ThemeChanged event"| MainViewModel
MainViewModel --> MonitorViewModel
MonitorViewModel --> MonitorManager
+ DisplayChangeWatcher -.->|"DisplayChanged event"| MainViewModel
%% App to Lib services
MainViewModel --> ProfileService
@@ -287,6 +312,35 @@ flowchart TB
---
+### DisplayChangeWatcher - Monitor Hot-Plug Detection
+
+The `DisplayChangeWatcher` component provides automatic detection of monitor connect/disconnect events using the WinRT DeviceWatcher API.
+
+**Key Features:**
+- Uses `DisplayMonitor.GetDeviceSelector()` to watch for display device changes
+- Implements 1-second debouncing to coalesce rapid connect/disconnect events
+- Triggers `DisplayChanged` event to notify `MainViewModel` for monitor list refresh
+- Runs continuously after initial monitor discovery completes
+
+**Implementation Details:**
+```csharp
+// Device selector for display monitors
+string selector = DisplayMonitor.GetDeviceSelector();
+_deviceWatcher = DeviceInformation.CreateWatcher(selector);
+
+// Events monitored
+_deviceWatcher.Added += OnDeviceAdded; // New monitor connected
+_deviceWatcher.Removed += OnDeviceRemoved; // Monitor disconnected
+_deviceWatcher.Updated += OnDeviceUpdated; // Monitor properties changed
+```
+
+**Debouncing Strategy:**
+- Each device change event schedules a `DisplayChanged` event after 1 second
+- Subsequent events within the debounce window cancel the previous timer
+- This prevents excessive refreshes when multiple monitors change simultaneously
+
+---
+
### DDC/CI and WMI Interaction Architecture
```mermaid
@@ -620,7 +674,8 @@ flowchart TB
InitLoop --> UpdateCollection["Update _monitors Collection"]
UpdateCollection --> FireEvent["Fire MonitorsChanged Event"]
- FireEvent --> End([Discovery Complete])
+ FireEvent --> StartWatcher["Start DisplayChangeWatcher"]
+ StartWatcher --> End([Discovery Complete])
style ParallelDiscover fill:#e3f2fd
style InitLoop fill:#e8f5e9
@@ -1058,11 +1113,12 @@ classDiagram
1. **Hardware Cursor Brightness**: Support for displays with hardware cursor brightness
2. **Multi-GPU Support**: Better handling of monitors across different GPUs
-3. **Monitor Hot-Plug**: Improved detection and recovery for monitor connect/disconnect
+3. ~~**Monitor Hot-Plug**: Improved detection and recovery for monitor connect/disconnect~~ **Implemented** - `DisplayChangeWatcher` uses WinRT DeviceWatcher + DisplayMonitor API with 1-second debouncing
4. **Advanced Color Management**: Integration with Windows Color Management
5. **Scheduled Profiles**: Time-based automatic profile switching (beyond LightSwitch)
6. **Monitor Groups**: Ability to control multiple monitors as a single entity
7. **Remote Control**: Network-based control for multi-system setups
+8. ~~**Display Rotation**: Control display orientation~~ **Implemented** - `DisplayRotationService` uses Windows ChangeDisplaySettingsEx API
---
diff --git a/src/modules/powerdisplay/PowerDisplay/Helpers/DisplayChangeWatcher.cs b/src/modules/powerdisplay/PowerDisplay/Helpers/DisplayChangeWatcher.cs
new file mode 100644
index 0000000000..db3ee49f3d
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/Helpers/DisplayChangeWatcher.cs
@@ -0,0 +1,239 @@
+// 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.Threading;
+using System.Threading.Tasks;
+using ManagedCommon;
+using Microsoft.UI.Dispatching;
+using Windows.Devices.Display;
+using Windows.Devices.Enumeration;
+
+namespace PowerDisplay.Helpers;
+
+///
+/// Watches for display/monitor connection changes using WinRT DeviceWatcher.
+/// Triggers DisplayChanged event when monitors are added, removed, or updated.
+///
+public sealed class DisplayChangeWatcher : IDisposable
+{
+ private readonly DispatcherQueue _dispatcherQueue;
+ private readonly TimeSpan _debounceDelay = TimeSpan.FromSeconds(1);
+
+ private DeviceWatcher? _deviceWatcher;
+ private CancellationTokenSource? _debounceCts;
+ private bool _isRunning;
+ private bool _disposed;
+
+ ///
+ /// Event triggered when display configuration changes (after debounce period).
+ ///
+ public event EventHandler? DisplayChanged;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The dispatcher queue for UI thread marshalling.
+ public DisplayChangeWatcher(DispatcherQueue dispatcherQueue)
+ {
+ _dispatcherQueue = dispatcherQueue ?? throw new ArgumentNullException(nameof(dispatcherQueue));
+ }
+
+ ///
+ /// Gets a value indicating whether the watcher is currently running.
+ ///
+ public bool IsRunning => _isRunning;
+
+ ///
+ /// Starts watching for display changes.
+ ///
+ public void Start()
+ {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+
+ if (_isRunning)
+ {
+ Logger.LogDebug("[DisplayChangeWatcher] Already running, ignoring Start()");
+ return;
+ }
+
+ try
+ {
+ // Get the device selector for display monitors
+ string selector = DisplayMonitor.GetDeviceSelector();
+ Logger.LogInfo($"[DisplayChangeWatcher] Using device selector: {selector}");
+
+ // Create the device watcher
+ _deviceWatcher = DeviceInformation.CreateWatcher(selector);
+
+ // Subscribe to events
+ _deviceWatcher.Added += OnDeviceAdded;
+ _deviceWatcher.Removed += OnDeviceRemoved;
+ _deviceWatcher.Updated += OnDeviceUpdated;
+ _deviceWatcher.EnumerationCompleted += OnEnumerationCompleted;
+ _deviceWatcher.Stopped += OnWatcherStopped;
+
+ // Start watching
+ _deviceWatcher.Start();
+ _isRunning = true;
+
+ Logger.LogInfo("[DisplayChangeWatcher] Started watching for display changes");
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"[DisplayChangeWatcher] Failed to start: {ex.Message}");
+ _isRunning = false;
+ }
+ }
+
+ ///
+ /// Stops watching for display changes.
+ ///
+ public void Stop()
+ {
+ if (!_isRunning || _deviceWatcher == null)
+ {
+ return;
+ }
+
+ try
+ {
+ // Cancel any pending debounce
+ CancelDebounce();
+
+ // Stop the watcher
+ _deviceWatcher.Stop();
+
+ Logger.LogInfo("[DisplayChangeWatcher] Stopped watching for display changes");
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"[DisplayChangeWatcher] Error stopping watcher: {ex.Message}");
+ }
+ }
+
+ private void OnDeviceAdded(DeviceWatcher sender, DeviceInformation args)
+ {
+ Logger.LogInfo($"[DisplayChangeWatcher] Display added: {args.Name} ({args.Id})");
+ ScheduleDisplayChanged();
+ }
+
+ private void OnDeviceRemoved(DeviceWatcher sender, DeviceInformationUpdate args)
+ {
+ Logger.LogInfo($"[DisplayChangeWatcher] Display removed: {args.Id}");
+ ScheduleDisplayChanged();
+ }
+
+ private void OnDeviceUpdated(DeviceWatcher sender, DeviceInformationUpdate args)
+ {
+ Logger.LogDebug($"[DisplayChangeWatcher] Display updated: {args.Id}");
+
+ // Only trigger refresh for significant updates, not every property change.
+ // For now, we'll skip updates to avoid excessive refreshes.
+ // The Added and Removed events are the primary triggers for monitor changes.
+ }
+
+ private void OnEnumerationCompleted(DeviceWatcher sender, object args)
+ {
+ Logger.LogInfo("[DisplayChangeWatcher] Initial enumeration completed");
+
+ // Don't trigger refresh on initial enumeration - MainViewModel handles initial discovery
+ }
+
+ private void OnWatcherStopped(DeviceWatcher sender, object args)
+ {
+ _isRunning = false;
+ Logger.LogInfo("[DisplayChangeWatcher] Watcher stopped");
+ }
+
+ ///
+ /// Schedules a DisplayChanged event with debouncing.
+ /// Multiple rapid changes will only trigger one event after the debounce period.
+ ///
+ private void ScheduleDisplayChanged()
+ {
+ // Cancel any pending debounce
+ CancelDebounce();
+
+ // Create new cancellation token
+ _debounceCts = new CancellationTokenSource();
+ var token = _debounceCts.Token;
+
+ // Schedule the event after debounce delay
+ Task.Run(async () =>
+ {
+ try
+ {
+ await Task.Delay(_debounceDelay, token);
+
+ if (!token.IsCancellationRequested)
+ {
+ // Dispatch to UI thread
+ _dispatcherQueue.TryEnqueue(() =>
+ {
+ if (!_disposed)
+ {
+ Logger.LogInfo("[DisplayChangeWatcher] Triggering DisplayChanged event");
+ DisplayChanged?.Invoke(this, EventArgs.Empty);
+ }
+ });
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // Debounce was cancelled by a newer event, this is expected
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"[DisplayChangeWatcher] Error in debounce task: {ex.Message}");
+ }
+ });
+ }
+
+ private void CancelDebounce()
+ {
+ try
+ {
+ _debounceCts?.Cancel();
+ _debounceCts?.Dispose();
+ _debounceCts = null;
+ }
+ catch (ObjectDisposedException)
+ {
+ // Already disposed, ignore
+ }
+ }
+
+ ///
+ /// Disposes resources used by the watcher.
+ ///
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _disposed = true;
+
+ // Stop watching
+ Stop();
+
+ // Unsubscribe from events
+ if (_deviceWatcher != null)
+ {
+ _deviceWatcher.Added -= OnDeviceAdded;
+ _deviceWatcher.Removed -= OnDeviceRemoved;
+ _deviceWatcher.Updated -= OnDeviceUpdated;
+ _deviceWatcher.EnumerationCompleted -= OnEnumerationCompleted;
+ _deviceWatcher.Stopped -= OnWatcherStopped;
+ _deviceWatcher = null;
+ }
+
+ // Cancel debounce
+ CancelDebounce();
+
+ Logger.LogInfo("[DisplayChangeWatcher] Disposed");
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.Monitors.cs b/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.Monitors.cs
index f2dcbda02f..41dffda9e6 100644
--- a/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.Monitors.cs
+++ b/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.Monitors.cs
@@ -39,6 +39,9 @@ public partial class MainViewModel
IsScanning = false;
IsInitialized = true;
+ // Start watching for display changes after initialization
+ StartDisplayWatching();
+
if (monitors.Count > 0)
{
StatusText = $"Found {monitors.Count} monitors";
diff --git a/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs b/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs
index f8827e3a00..d3b79a363f 100644
--- a/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs
+++ b/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs
@@ -38,6 +38,7 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
private readonly ISettingsUtils _settingsUtils;
private readonly MonitorStateManager _stateManager;
private readonly LightSwitchListener _lightSwitchListener;
+ private readonly DisplayChangeWatcher _displayChangeWatcher;
private ObservableCollection _monitors;
private ObservableCollection _profiles;
@@ -78,6 +79,10 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
// Load profiles for quick apply feature
LoadProfiles();
+ // Initialize display change watcher for auto-refresh on monitor plug/unplug
+ _displayChangeWatcher = new DisplayChangeWatcher(_dispatcherQueue);
+ _displayChangeWatcher.DisplayChanged += OnDisplayChanged;
+
// Start initial discovery
_ = InitializeAsync(_cancellationTokenSource.Token);
}
@@ -236,6 +241,7 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
_cancellationTokenSource?.Cancel();
// Dispose all resources safely (don't throw from Dispose)
+ SafeDispose(_displayChangeWatcher, "DisplayChangeWatcher");
SafeDispose(_lightSwitchListener, "LightSwitchListener");
// Dispose monitor view models
@@ -304,4 +310,30 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
Logger.LogError($"[Profile] Failed to load profiles: {ex.Message}");
}
}
+
+ ///
+ /// Handles display configuration changes detected by the DisplayChangeWatcher.
+ /// Triggers a monitor refresh to update the UI.
+ ///
+ private async void OnDisplayChanged(object? sender, EventArgs e)
+ {
+ Logger.LogInfo("[MainViewModel] Display change detected, refreshing monitors...");
+ await RefreshMonitorsAsync();
+ }
+
+ ///
+ /// Starts watching for display changes. Call after initialization is complete.
+ ///
+ public void StartDisplayWatching()
+ {
+ _displayChangeWatcher.Start();
+ }
+
+ ///
+ /// Stops watching for display changes.
+ ///
+ public void StopDisplayWatching()
+ {
+ _displayChangeWatcher.Stop();
+ }
}