Files
PowerToys/src/modules/powerdisplay/PowerDisplay/Helpers/MonitorManager.cs

432 lines
16 KiB
C#
Raw Normal View History

2025-10-20 16:22:47 +08:00
// 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.Linq;
using System.Threading;
using System.Threading.Tasks;
using ManagedCommon;
using PowerDisplay.Common.Drivers;
using PowerDisplay.Common.Drivers.DDC;
using PowerDisplay.Common.Drivers.WMI;
using PowerDisplay.Common.Interfaces;
using PowerDisplay.Common.Models;
using PowerDisplay.Common.Services;
using PowerDisplay.Common.Utils;
using Monitor = PowerDisplay.Common.Models.Monitor;
2025-10-20 16:22:47 +08:00
namespace PowerDisplay.Helpers
2025-10-20 16:22:47 +08:00
{
/// <summary>
/// Monitor manager for unified control of all monitors
/// No interface abstraction - KISS principle (only one implementation needed)
/// </summary>
public partial class MonitorManager : IDisposable
{
private readonly List<Monitor> _monitors = new();
private readonly Dictionary<string, Monitor> _monitorLookup = new();
2025-10-20 16:22:47 +08:00
private readonly SemaphoreSlim _discoveryLock = new(1, 1);
private readonly DisplayRotationService _rotationService = new();
// Controllers stored by type for O(1) lookup based on CommunicationMethod
private DdcCiController? _ddcController;
private WmiController? _wmiController;
2025-10-20 16:22:47 +08:00
private bool _disposed;
public IReadOnlyList<Monitor> Monitors => _monitors.AsReadOnly();
public MonitorManager()
{
// Initialize controllers
InitializeControllers();
}
/// <summary>
/// Initialize controllers
/// </summary>
private void InitializeControllers()
{
try
{
// DDC/CI controller (external monitors)
_ddcController = new DdcCiController();
2025-10-20 16:22:47 +08:00
}
catch (Exception ex)
{
Logger.LogWarning($"Failed to initialize DDC/CI controller: {ex.Message}");
}
try
{
// WMI controller (internal monitors)
// First check if WMI is available
if (WmiController.IsWmiAvailable())
{
_wmiController = new WmiController();
2025-10-20 16:22:47 +08:00
}
else
{
Logger.LogWarning("WMI brightness control not available on this system");
2025-10-20 16:22:47 +08:00
}
}
catch (Exception ex)
{
Logger.LogWarning($"Failed to initialize WMI controller: {ex.Message}");
}
}
/// <summary>
/// Discover all monitors from all controllers.
/// Each controller is responsible for fully initializing its monitors
/// (including brightness, capabilities, input source, color temperature, etc.)
2025-10-20 16:22:47 +08:00
/// </summary>
public async Task<IReadOnlyList<Monitor>> DiscoverMonitorsAsync(CancellationToken cancellationToken = default)
{
await _discoveryLock.WaitAsync(cancellationToken);
try
{
var discoveredMonitors = await DiscoverFromAllControllersAsync(cancellationToken);
2025-10-20 16:22:47 +08:00
// Update collections
_monitors.Clear();
_monitorLookup.Clear();
var sortedMonitors = discoveredMonitors
.OrderBy(m => m.MonitorNumber)
.ToList();
2025-10-20 16:22:47 +08:00
_monitors.AddRange(sortedMonitors);
foreach (var monitor in sortedMonitors)
{
_monitorLookup[monitor.Id] = monitor;
}
return _monitors.AsReadOnly();
}
finally
{
_discoveryLock.Release();
}
}
/// <summary>
/// Discover monitors from all registered controllers in parallel.
/// </summary>
private async Task<List<Monitor>> DiscoverFromAllControllersAsync(CancellationToken cancellationToken)
{
var tasks = new List<Task<IEnumerable<Monitor>>>();
if (_ddcController != null)
{
tasks.Add(SafeDiscoverAsync(_ddcController, cancellationToken));
}
if (_wmiController != null)
{
tasks.Add(SafeDiscoverAsync(_wmiController, cancellationToken));
}
var results = await Task.WhenAll(tasks);
return results.SelectMany(m => m).ToList();
}
/// <summary>
/// Safely discover monitors from a controller, returning empty list on failure.
/// </summary>
private static async Task<IEnumerable<Monitor>> SafeDiscoverAsync(
IMonitorController controller,
CancellationToken cancellationToken)
{
try
{
return await controller.DiscoverMonitorsAsync(cancellationToken);
}
catch (Exception ex)
{
Logger.LogWarning($"Controller {controller.Name} discovery failed: {ex.Message}");
return Enumerable.Empty<Monitor>();
}
}
2025-10-20 16:22:47 +08:00
/// <summary>
/// Get brightness of the specified monitor
/// </summary>
public async Task<VcpFeatureValue> GetBrightnessAsync(string monitorId, CancellationToken cancellationToken = default)
2025-10-20 16:22:47 +08:00
{
var monitor = GetMonitor(monitorId);
if (monitor == null)
{
return VcpFeatureValue.Invalid;
2025-10-20 16:22:47 +08:00
}
var controller = GetControllerForMonitor(monitor);
2025-10-20 16:22:47 +08:00
if (controller == null)
{
return VcpFeatureValue.Invalid;
2025-10-20 16:22:47 +08:00
}
try
{
var brightnessInfo = await controller.GetBrightnessAsync(monitor, cancellationToken);
// Update cached brightness value
if (brightnessInfo.IsValid)
{
monitor.UpdateStatus(brightnessInfo.ToPercentage(), true);
}
return brightnessInfo;
}
catch (Exception ex)
{
// Mark monitor as unavailable
Logger.LogError($"Failed to get brightness for monitor {monitorId}: {ex.Message}");
monitor.IsAvailable = false;
return VcpFeatureValue.Invalid;
2025-10-20 16:22:47 +08:00
}
}
/// <summary>
/// Set brightness of the specified monitor
/// </summary>
public Task<MonitorOperationResult> SetBrightnessAsync(string monitorId, int brightness, CancellationToken cancellationToken = default)
=> ExecuteMonitorOperationAsync(
monitorId,
brightness,
(ctrl, mon, val, ct) => ctrl.SetBrightnessAsync(mon, val, ct),
(mon, val) => mon.UpdateStatus(val, true),
cancellationToken);
2025-10-20 16:22:47 +08:00
/// <summary>
/// Set contrast of the specified monitor
/// </summary>
public Task<MonitorOperationResult> SetContrastAsync(string monitorId, int contrast, CancellationToken cancellationToken = default)
=> ExecuteMonitorOperationAsync(
monitorId,
contrast,
(ctrl, mon, val, ct) => ctrl.SetContrastAsync(mon, val, ct),
(mon, val) => mon.CurrentContrast = val,
cancellationToken);
/// <summary>
/// Set volume of the specified monitor
/// </summary>
public Task<MonitorOperationResult> SetVolumeAsync(string monitorId, int volume, CancellationToken cancellationToken = default)
=> ExecuteMonitorOperationAsync(
monitorId,
volume,
(ctrl, mon, val, ct) => ctrl.SetVolumeAsync(mon, val, ct),
(mon, val) => mon.CurrentVolume = val,
cancellationToken);
/// <summary>
/// Get monitor color temperature
/// </summary>
public async Task<VcpFeatureValue> GetColorTemperatureAsync(string monitorId, CancellationToken cancellationToken = default)
2025-10-20 16:22:47 +08:00
{
var monitor = GetMonitor(monitorId);
if (monitor == null)
{
return VcpFeatureValue.Invalid;
2025-10-20 16:22:47 +08:00
}
var controller = GetControllerForMonitor(monitor);
2025-10-20 16:22:47 +08:00
if (controller == null)
{
return VcpFeatureValue.Invalid;
2025-10-20 16:22:47 +08:00
}
try
{
return await controller.GetColorTemperatureAsync(monitor, cancellationToken);
}
catch (Exception ex) when (ex is not OutOfMemoryException)
2025-10-20 16:22:47 +08:00
{
Logger.LogDebug($"GetColorTemperatureAsync failed: {ex.Message}");
return VcpFeatureValue.Invalid;
2025-10-20 16:22:47 +08:00
}
}
/// <summary>
/// Set monitor color temperature
/// </summary>
public Task<MonitorOperationResult> SetColorTemperatureAsync(string monitorId, int colorTemperature, CancellationToken cancellationToken = default)
=> ExecuteMonitorOperationAsync(
monitorId,
colorTemperature,
(ctrl, mon, val, ct) => ctrl.SetColorTemperatureAsync(mon, val, ct),
(mon, val) => mon.CurrentColorTemperature = val,
cancellationToken);
/// <summary>
/// Get current input source for a monitor
/// </summary>
public async Task<VcpFeatureValue> GetInputSourceAsync(string monitorId, CancellationToken cancellationToken = default)
{
var monitor = GetMonitor(monitorId);
if (monitor == null)
{
return VcpFeatureValue.Invalid;
}
var controller = GetControllerForMonitor(monitor);
if (controller == null)
{
return VcpFeatureValue.Invalid;
}
try
{
return await controller.GetInputSourceAsync(monitor, cancellationToken);
}
catch (Exception ex) when (ex is not OutOfMemoryException)
{
Logger.LogDebug($"GetInputSourceAsync failed: {ex.Message}");
return VcpFeatureValue.Invalid;
}
}
/// <summary>
/// Set input source for a monitor
/// </summary>
public Task<MonitorOperationResult> SetInputSourceAsync(string monitorId, int inputSource, CancellationToken cancellationToken = default)
=> ExecuteMonitorOperationAsync(
monitorId,
inputSource,
(ctrl, mon, val, ct) => ctrl.SetInputSourceAsync(mon, val, ct),
(mon, val) => mon.CurrentInputSource = val,
cancellationToken);
/// <summary>
/// Set rotation/orientation for a monitor.
/// Uses Windows ChangeDisplaySettingsEx API (not DDC/CI).
/// </summary>
/// <param name="monitorId">Monitor ID</param>
/// <param name="orientation">Orientation: 0=normal, 1=90°, 2=180°, 3=270°</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Operation result</returns>
public Task<MonitorOperationResult> SetRotationAsync(string monitorId, int orientation, CancellationToken cancellationToken = default)
{
var monitor = GetMonitor(monitorId);
if (monitor == null)
{
Logger.LogError($"[MonitorManager] SetRotation: Monitor not found: {monitorId}");
return Task.FromResult(MonitorOperationResult.Failure("Monitor not found"));
}
// Rotation uses Windows display settings API, not DDC/CI controller
// Prefer using Monitor object which contains GdiDeviceName for accurate adapter targeting
var result = _rotationService.SetRotation(monitor, orientation);
if (result.IsSuccess)
{
monitor.Orientation = orientation;
monitor.LastUpdate = DateTime.Now;
Logger.LogInfo($"[MonitorManager] SetRotation: Successfully set {monitorId} to orientation {orientation}");
}
else
{
Logger.LogError($"[MonitorManager] SetRotation: Failed for {monitorId}: {result.ErrorMessage}");
}
return Task.FromResult(result);
}
2025-10-20 16:22:47 +08:00
/// <summary>
/// Get monitor by ID. Uses dictionary lookup for O(1) performance.
2025-10-20 16:22:47 +08:00
/// </summary>
public Monitor? GetMonitor(string monitorId)
{
return _monitorLookup.TryGetValue(monitorId, out var monitor) ? monitor : null;
2025-10-20 16:22:47 +08:00
}
/// <summary>
/// Get controller for the monitor based on CommunicationMethod.
/// O(1) lookup - no async validation needed since controller type is determined at discovery.
2025-10-20 16:22:47 +08:00
/// </summary>
private IMonitorController? GetControllerForMonitor(Monitor monitor)
2025-10-20 16:22:47 +08:00
{
return monitor.CommunicationMethod switch
2025-11-17 14:53:43 +08:00
{
"WMI" => _wmiController,
"DDC/CI" => _ddcController,
_ => null,
};
2025-10-20 16:22:47 +08:00
}
/// <summary>
/// Generic helper to execute monitor operations with common error handling.
/// Eliminates code duplication across Set* methods.
/// </summary>
private async Task<MonitorOperationResult> ExecuteMonitorOperationAsync<T>(
string monitorId,
T value,
Func<IMonitorController, Monitor, T, CancellationToken, Task<MonitorOperationResult>> operation,
Action<Monitor, T> onSuccess,
CancellationToken cancellationToken = default)
{
var monitor = GetMonitor(monitorId);
if (monitor == null)
{
Logger.LogError($"[MonitorManager] Monitor not found: {monitorId}");
return MonitorOperationResult.Failure("Monitor not found");
}
var controller = GetControllerForMonitor(monitor);
2025-10-20 16:22:47 +08:00
if (controller == null)
{
Logger.LogError($"[MonitorManager] No controller available for monitor {monitorId}");
2025-10-20 16:22:47 +08:00
return MonitorOperationResult.Failure("No controller available for this monitor");
}
try
{
var result = await operation(controller, monitor, value, cancellationToken);
if (result.IsSuccess)
{
onSuccess(monitor, value);
monitor.LastUpdate = DateTime.Now;
}
else
{
monitor.IsAvailable = false;
}
return result;
}
catch (Exception ex)
{
monitor.IsAvailable = false;
Logger.LogError($"[MonitorManager] Operation failed for {monitorId}: {ex.Message}");
return MonitorOperationResult.Failure($"Exception: {ex.Message}");
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed && disposing)
{
_discoveryLock?.Dispose();
// Release controllers
_ddcController?.Dispose();
_wmiController?.Dispose();
2025-10-20 16:22:47 +08:00
_monitors.Clear();
_monitorLookup.Clear();
2025-10-20 16:22:47 +08:00
_disposed = true;
}
}
}
}