From 2880b5afce4902101622f0118c6be2f92b04c06a Mon Sep 17 00:00:00 2001 From: Yu Leng Date: Thu, 11 Dec 2025 13:02:00 +0800 Subject: [PATCH] Improve monitor orientation sync for mirror/clone mode Refactor orientation tracking to use property change notifications in the Monitor model. Add GetCurrentOrientation to DisplayRotationService and a RefreshAllOrientations method in MonitorManager to ensure all monitors sharing a GdiDeviceName are updated after rotation. Update MonitorViewModel to subscribe to orientation changes and forward them to the UI, and clean up event subscriptions on dispose. These changes ensure accurate orientation state in both UI and data models, especially for mirrored displays. --- .../PowerDisplay.Lib/Models/Monitor.cs | 17 ++++++++-- .../Services/DisplayRotationService.cs | 32 +++++++++++++++++++ .../PowerDisplay/Helpers/MonitorManager.cs | 31 ++++++++++++++++-- .../ViewModels/MonitorViewModel.cs | 32 ++++++++++++++----- 4 files changed, 100 insertions(+), 12 deletions(-) diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Models/Monitor.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Models/Monitor.cs index d864e4fb69..24981af567 100644 --- a/src/modules/powerdisplay/PowerDisplay.Lib/Models/Monitor.cs +++ b/src/modules/powerdisplay/PowerDisplay.Lib/Models/Monitor.cs @@ -24,6 +24,7 @@ namespace PowerDisplay.Common.Models private int _currentColorTemperature = 0x05; // Default to 6500K preset (VCP 0x14 value) private int _currentInputSource; // VCP 0x60 value private bool _isAvailable = true; + private int _orientation; /// /// Gets or sets unique identifier for all purposes: UI lookups, IPC, persistent storage, and handle management. @@ -323,9 +324,21 @@ namespace PowerDisplay.Common.Models public string GdiDeviceName { get; set; } = string.Empty; /// - /// Gets or sets monitor orientation (0=0, 1=90, 2=180, 3=270) + /// Gets or sets monitor orientation (0=0, 1=90, 2=180, 3=270). + /// Fires PropertyChanged when value changes. /// - public int Orientation { get; set; } + public int Orientation + { + get => _orientation; + set + { + if (_orientation != value) + { + _orientation = value; + OnPropertyChanged(); + } + } + } /// int IMonitorData.MonitorNumber diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Services/DisplayRotationService.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Services/DisplayRotationService.cs index 53887e09fd..5529e43c06 100644 --- a/src/modules/powerdisplay/PowerDisplay.Lib/Services/DisplayRotationService.cs +++ b/src/modules/powerdisplay/PowerDisplay.Lib/Services/DisplayRotationService.cs @@ -128,6 +128,38 @@ namespace PowerDisplay.Common.Services } } + /// + /// Get current orientation for a GDI device name. + /// + /// GDI device name (e.g., "\\.\DISPLAY1") + /// Current orientation (0-3), or -1 if query failed + public unsafe int GetCurrentOrientation(string gdiDeviceName) + { + if (string.IsNullOrEmpty(gdiDeviceName)) + { + return -1; + } + + try + { + DevMode devMode = default; + devMode.DmSize = (short)sizeof(DevMode); + + if (!EnumDisplaySettings(gdiDeviceName, EnumCurrentSettings, &devMode)) + { + Logger.LogDebug($"GetCurrentOrientation: EnumDisplaySettings failed for {gdiDeviceName}"); + return -1; + } + + return devMode.DmDisplayOrientation; + } + catch (Exception ex) + { + Logger.LogDebug($"GetCurrentOrientation: Exception for {gdiDeviceName}: {ex.Message}"); + return -1; + } + } + /// /// Get human-readable error message for ChangeDisplaySettings result code. /// diff --git a/src/modules/powerdisplay/PowerDisplay/Helpers/MonitorManager.cs b/src/modules/powerdisplay/PowerDisplay/Helpers/MonitorManager.cs index 2e0c6b8a7c..2f17731f54 100644 --- a/src/modules/powerdisplay/PowerDisplay/Helpers/MonitorManager.cs +++ b/src/modules/powerdisplay/PowerDisplay/Helpers/MonitorManager.cs @@ -296,6 +296,8 @@ namespace PowerDisplay.Helpers /// /// Set rotation/orientation for a monitor. /// Uses Windows ChangeDisplaySettingsEx API (not DDC/CI). + /// After successful rotation, refreshes orientation for all monitors sharing the same GdiDeviceName + /// (important for mirror/clone mode where multiple monitors share one display source). /// /// Monitor ID /// Orientation: 0=normal, 1=90°, 2=180°, 3=270° @@ -316,8 +318,9 @@ namespace PowerDisplay.Helpers if (result.IsSuccess) { - monitor.Orientation = orientation; - monitor.LastUpdate = DateTime.Now; + // Refresh orientation for all monitors - rotation affects the GdiDeviceName (display source), + // and in mirror mode multiple monitors may share the same GdiDeviceName + RefreshAllOrientations(); Logger.LogInfo($"[MonitorManager] SetRotation: Successfully set {monitorId} to orientation {orientation}"); } else @@ -328,6 +331,30 @@ namespace PowerDisplay.Helpers return Task.FromResult(result); } + /// + /// Refresh orientation values for all monitors by querying current display settings. + /// This ensures all monitors reflect the actual system state, which is important + /// in mirror mode where multiple monitors share the same GdiDeviceName. + /// + public void RefreshAllOrientations() + { + foreach (var monitor in _monitors) + { + if (string.IsNullOrEmpty(monitor.GdiDeviceName)) + { + continue; + } + + var currentOrientation = _rotationService.GetCurrentOrientation(monitor.GdiDeviceName); + if (currentOrientation >= 0 && currentOrientation != monitor.Orientation) + { + Logger.LogDebug($"[MonitorManager] RefreshAllOrientations: {monitor.Id} orientation updated from {monitor.Orientation} to {currentOrientation}"); + monitor.Orientation = currentOrientation; + monitor.LastUpdate = DateTime.Now; + } + } + } + /// /// Get monitor by ID. Uses dictionary lookup for O(1) performance. /// diff --git a/src/modules/powerdisplay/PowerDisplay/ViewModels/MonitorViewModel.cs b/src/modules/powerdisplay/PowerDisplay/ViewModels/MonitorViewModel.cs index 68ae76c6d7..94221c8a4c 100644 --- a/src/modules/powerdisplay/PowerDisplay/ViewModels/MonitorViewModel.cs +++ b/src/modules/powerdisplay/PowerDisplay/ViewModels/MonitorViewModel.cs @@ -239,6 +239,9 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable _mainViewModel.PropertyChanged += OnMainViewModelPropertyChanged; } + // Subscribe to underlying Monitor property changes (e.g., Orientation updates in mirror mode) + _monitor.PropertyChanged += OnMonitorPropertyChanged; + // Initialize Show properties based on hardware capabilities _showContrast = monitor.SupportsContrast; _showVolume = monitor.SupportsVolume; @@ -401,7 +404,9 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable public bool IsRotation3 => CurrentRotation == 3; /// - /// Set rotation/orientation for this monitor + /// Set rotation/orientation for this monitor. + /// Note: MonitorManager.SetRotationAsync will refresh all monitors' orientations after success, + /// which triggers PropertyChanged through OnMonitorPropertyChanged - no manual notification needed here. /// /// Orientation: 0=normal, 1=90°, 2=180°, 3=270° public async Task SetRotationAsync(int orientation) @@ -427,13 +432,6 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable if (result.IsSuccess) { - // Notify all rotation-related properties changed - OnPropertyChanged(nameof(CurrentRotation)); - OnPropertyChanged(nameof(IsRotation0)); - OnPropertyChanged(nameof(IsRotation1)); - OnPropertyChanged(nameof(IsRotation2)); - OnPropertyChanged(nameof(IsRotation3)); - Logger.LogInfo($"[{Id}] Rotation set successfully to {orientation}"); } else @@ -685,6 +683,21 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable } } + private void OnMonitorPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + // Forward Orientation changes from underlying Monitor to ViewModel properties + // This is important for mirror mode where MonitorManager.RefreshAllOrientations() + // updates multiple monitors sharing the same GdiDeviceName + if (e.PropertyName == nameof(Monitor.Orientation)) + { + OnPropertyChanged(nameof(CurrentRotation)); + OnPropertyChanged(nameof(IsRotation0)); + OnPropertyChanged(nameof(IsRotation1)); + OnPropertyChanged(nameof(IsRotation2)); + OnPropertyChanged(nameof(IsRotation3)); + } + } + public void Dispose() { // Unsubscribe from MainViewModel events @@ -693,6 +706,9 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable _mainViewModel.PropertyChanged -= OnMainViewModelPropertyChanged; } + // Unsubscribe from underlying Monitor events + _monitor.PropertyChanged -= OnMonitorPropertyChanged; + // Dispose all debouncers _brightnessDebouncer?.Dispose(); _contrastDebouncer?.Dispose();