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.
This commit is contained in:
Yu Leng
2025-12-11 13:02:00 +08:00
parent 9a175df510
commit 2880b5afce
4 changed files with 100 additions and 12 deletions

View File

@@ -24,6 +24,7 @@ namespace PowerDisplay.Common.Models
private int _currentColorTemperature = 0x05; // Default to 6500K preset (VCP 0x14 value) private int _currentColorTemperature = 0x05; // Default to 6500K preset (VCP 0x14 value)
private int _currentInputSource; // VCP 0x60 value private int _currentInputSource; // VCP 0x60 value
private bool _isAvailable = true; private bool _isAvailable = true;
private int _orientation;
/// <summary> /// <summary>
/// Gets or sets unique identifier for all purposes: UI lookups, IPC, persistent storage, and handle management. /// 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; public string GdiDeviceName { get; set; } = string.Empty;
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
public int Orientation { get; set; } public int Orientation
{
get => _orientation;
set
{
if (_orientation != value)
{
_orientation = value;
OnPropertyChanged();
}
}
}
/// <inheritdoc /> /// <inheritdoc />
int IMonitorData.MonitorNumber int IMonitorData.MonitorNumber

View File

@@ -128,6 +128,38 @@ namespace PowerDisplay.Common.Services
} }
} }
/// <summary>
/// Get current orientation for a GDI device name.
/// </summary>
/// <param name="gdiDeviceName">GDI device name (e.g., "\\.\DISPLAY1")</param>
/// <returns>Current orientation (0-3), or -1 if query failed</returns>
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;
}
}
/// <summary> /// <summary>
/// Get human-readable error message for ChangeDisplaySettings result code. /// Get human-readable error message for ChangeDisplaySettings result code.
/// </summary> /// </summary>

View File

@@ -296,6 +296,8 @@ namespace PowerDisplay.Helpers
/// <summary> /// <summary>
/// Set rotation/orientation for a monitor. /// Set rotation/orientation for a monitor.
/// Uses Windows ChangeDisplaySettingsEx API (not DDC/CI). /// 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).
/// </summary> /// </summary>
/// <param name="monitorId">Monitor ID</param> /// <param name="monitorId">Monitor ID</param>
/// <param name="orientation">Orientation: 0=normal, 1=90°, 2=180°, 3=270°</param> /// <param name="orientation">Orientation: 0=normal, 1=90°, 2=180°, 3=270°</param>
@@ -316,8 +318,9 @@ namespace PowerDisplay.Helpers
if (result.IsSuccess) if (result.IsSuccess)
{ {
monitor.Orientation = orientation; // Refresh orientation for all monitors - rotation affects the GdiDeviceName (display source),
monitor.LastUpdate = DateTime.Now; // and in mirror mode multiple monitors may share the same GdiDeviceName
RefreshAllOrientations();
Logger.LogInfo($"[MonitorManager] SetRotation: Successfully set {monitorId} to orientation {orientation}"); Logger.LogInfo($"[MonitorManager] SetRotation: Successfully set {monitorId} to orientation {orientation}");
} }
else else
@@ -328,6 +331,30 @@ namespace PowerDisplay.Helpers
return Task.FromResult(result); return Task.FromResult(result);
} }
/// <summary>
/// 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.
/// </summary>
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;
}
}
}
/// <summary> /// <summary>
/// Get monitor by ID. Uses dictionary lookup for O(1) performance. /// Get monitor by ID. Uses dictionary lookup for O(1) performance.
/// </summary> /// </summary>

View File

@@ -239,6 +239,9 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
_mainViewModel.PropertyChanged += OnMainViewModelPropertyChanged; _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 // Initialize Show properties based on hardware capabilities
_showContrast = monitor.SupportsContrast; _showContrast = monitor.SupportsContrast;
_showVolume = monitor.SupportsVolume; _showVolume = monitor.SupportsVolume;
@@ -401,7 +404,9 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
public bool IsRotation3 => CurrentRotation == 3; public bool IsRotation3 => CurrentRotation == 3;
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
/// <param name="orientation">Orientation: 0=normal, 1=90°, 2=180°, 3=270°</param> /// <param name="orientation">Orientation: 0=normal, 1=90°, 2=180°, 3=270°</param>
public async Task SetRotationAsync(int orientation) public async Task SetRotationAsync(int orientation)
@@ -427,13 +432,6 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
if (result.IsSuccess) 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}"); Logger.LogInfo($"[{Id}] Rotation set successfully to {orientation}");
} }
else 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() public void Dispose()
{ {
// Unsubscribe from MainViewModel events // Unsubscribe from MainViewModel events
@@ -693,6 +706,9 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
_mainViewModel.PropertyChanged -= OnMainViewModelPropertyChanged; _mainViewModel.PropertyChanged -= OnMainViewModelPropertyChanged;
} }
// Unsubscribe from underlying Monitor events
_monitor.PropertyChanged -= OnMonitorPropertyChanged;
// Dispose all debouncers // Dispose all debouncers
_brightnessDebouncer?.Dispose(); _brightnessDebouncer?.Dispose();
_contrastDebouncer?.Dispose(); _contrastDebouncer?.Dispose();