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;
|
2025-11-24 21:58:34 +08:00
|
|
|
using PowerDisplay.Common.Drivers;
|
|
|
|
|
using PowerDisplay.Common.Drivers.DDC;
|
|
|
|
|
using PowerDisplay.Common.Drivers.WMI;
|
2025-11-24 18:08:11 +08:00
|
|
|
using PowerDisplay.Common.Interfaces;
|
|
|
|
|
using PowerDisplay.Common.Models;
|
2025-11-28 05:08:55 +08:00
|
|
|
using PowerDisplay.Common.Services;
|
2025-11-24 18:08:11 +08:00
|
|
|
using PowerDisplay.Common.Utils;
|
|
|
|
|
using Monitor = PowerDisplay.Common.Models.Monitor;
|
2025-10-20 16:22:47 +08:00
|
|
|
|
2025-12-04 06:14:01 +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();
|
2025-11-24 23:36:25 +08:00
|
|
|
private readonly Dictionary<string, Monitor> _monitorLookup = new();
|
2025-10-20 16:22:47 +08:00
|
|
|
private readonly List<IMonitorController> _controllers = new();
|
|
|
|
|
private readonly SemaphoreSlim _discoveryLock = new(1, 1);
|
2025-11-28 05:08:55 +08:00
|
|
|
private readonly DisplayRotationService _rotationService = new();
|
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)
|
|
|
|
|
_controllers.Add(new DdcCiController());
|
|
|
|
|
}
|
|
|
|
|
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())
|
|
|
|
|
{
|
|
|
|
|
_controllers.Add(new WmiController());
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2025-11-13 14:14:49 +08:00
|
|
|
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>
|
2025-11-26 05:02:49 +08:00
|
|
|
/// Discover all monitors from all controllers.
|
2025-12-10 11:00:36 +08:00
|
|
|
/// All initialization (brightness, capabilities, input source) is done during controller discovery.
|
2025-10-20 16:22:47 +08:00
|
|
|
/// </summary>
|
|
|
|
|
public async Task<IReadOnlyList<Monitor>> DiscoverMonitorsAsync(CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
await _discoveryLock.WaitAsync(cancellationToken);
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
2025-11-26 05:02:49 +08:00
|
|
|
var discoveryResults = await DiscoverFromAllControllersAsync(cancellationToken);
|
2025-10-20 16:22:47 +08:00
|
|
|
|
2025-12-10 11:00:36 +08:00
|
|
|
// Update collections
|
|
|
|
|
_monitors.Clear();
|
|
|
|
|
_monitorLookup.Clear();
|
|
|
|
|
|
|
|
|
|
var newMonitors = discoveryResults
|
|
|
|
|
.SelectMany(r => r.Monitors)
|
|
|
|
|
.OrderBy(m => m.MonitorNumber)
|
|
|
|
|
.ToList();
|
2025-10-20 16:22:47 +08:00
|
|
|
|
2025-12-10 11:00:36 +08:00
|
|
|
_monitors.AddRange(newMonitors);
|
|
|
|
|
foreach (var monitor in newMonitors)
|
|
|
|
|
{
|
|
|
|
|
_monitorLookup[monitor.Id] = monitor;
|
|
|
|
|
}
|
2025-11-19 15:08:00 +08:00
|
|
|
|
2025-11-26 05:02:49 +08:00
|
|
|
return _monitors.AsReadOnly();
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
_discoveryLock.Release();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Discover monitors from all registered controllers in parallel.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private async Task<List<(IMonitorController Controller, List<Monitor> Monitors)>> DiscoverFromAllControllersAsync(
|
|
|
|
|
CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
var discoveryTasks = _controllers.Select(async controller =>
|
|
|
|
|
{
|
|
|
|
|
try
|
2025-10-20 16:22:47 +08:00
|
|
|
{
|
2025-11-26 05:02:49 +08:00
|
|
|
var monitors = await controller.DiscoverMonitorsAsync(cancellationToken);
|
|
|
|
|
return (Controller: controller, Monitors: monitors.ToList());
|
2025-10-20 16:22:47 +08:00
|
|
|
}
|
2025-11-26 05:02:49 +08:00
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogWarning($"Controller {controller.Name} discovery failed: {ex.Message}");
|
|
|
|
|
return (Controller: controller, Monitors: new List<Monitor>());
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
var results = await Task.WhenAll(discoveryTasks);
|
|
|
|
|
return results.ToList();
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-20 16:22:47 +08:00
|
|
|
/// <summary>
|
|
|
|
|
/// Get brightness of the specified monitor
|
|
|
|
|
/// </summary>
|
|
|
|
|
public async Task<BrightnessInfo> GetBrightnessAsync(string monitorId, CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
var monitor = GetMonitor(monitorId);
|
|
|
|
|
if (monitor == null)
|
|
|
|
|
{
|
|
|
|
|
return BrightnessInfo.Invalid;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-17 14:53:43 +08:00
|
|
|
var controller = await GetControllerForMonitorAsync(monitor, cancellationToken);
|
2025-10-20 16:22:47 +08:00
|
|
|
if (controller == null)
|
|
|
|
|
{
|
|
|
|
|
return BrightnessInfo.Invalid;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 BrightnessInfo.Invalid;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Set brightness of the specified monitor
|
|
|
|
|
/// </summary>
|
2025-11-27 22:43:28 +08:00
|
|
|
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 brightness of all monitors
|
|
|
|
|
/// </summary>
|
|
|
|
|
public async Task<IEnumerable<MonitorOperationResult>> SetAllBrightnessAsync(int brightness, CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
var tasks = _monitors
|
|
|
|
|
.Where(m => m.IsAvailable)
|
|
|
|
|
.Select(async monitor =>
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
return await SetBrightnessAsync(monitor.Id, brightness, cancellationToken);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
return MonitorOperationResult.Failure($"Failed to set brightness for {monitor.Name}: {ex.Message}");
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return await Task.WhenAll(tasks);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <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<BrightnessInfo> GetColorTemperatureAsync(string monitorId, CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
var monitor = GetMonitor(monitorId);
|
|
|
|
|
if (monitor == null)
|
|
|
|
|
{
|
|
|
|
|
return BrightnessInfo.Invalid;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-17 14:53:43 +08:00
|
|
|
var controller = await GetControllerForMonitorAsync(monitor, cancellationToken);
|
2025-10-20 16:22:47 +08:00
|
|
|
if (controller == null)
|
|
|
|
|
{
|
|
|
|
|
return BrightnessInfo.Invalid;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
return await controller.GetColorTemperatureAsync(monitor, cancellationToken);
|
|
|
|
|
}
|
2025-11-24 18:08:11 +08:00
|
|
|
catch (Exception ex) when (ex is not OutOfMemoryException)
|
2025-10-20 16:22:47 +08:00
|
|
|
{
|
2025-11-24 18:08:11 +08:00
|
|
|
Logger.LogDebug($"GetColorTemperatureAsync failed: {ex.Message}");
|
2025-10-20 16:22:47 +08:00
|
|
|
return BrightnessInfo.Invalid;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <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);
|
|
|
|
|
|
2025-11-27 14:51:31 +08:00
|
|
|
/// <summary>
|
|
|
|
|
/// Get current input source for a monitor
|
|
|
|
|
/// </summary>
|
|
|
|
|
public async Task<BrightnessInfo> GetInputSourceAsync(string monitorId, CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
var monitor = GetMonitor(monitorId);
|
|
|
|
|
if (monitor == null)
|
|
|
|
|
{
|
|
|
|
|
return BrightnessInfo.Invalid;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var controller = await GetControllerForMonitorAsync(monitor, cancellationToken);
|
|
|
|
|
if (controller == null)
|
|
|
|
|
{
|
|
|
|
|
return BrightnessInfo.Invalid;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
return await controller.GetInputSourceAsync(monitor, cancellationToken);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex) when (ex is not OutOfMemoryException)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogDebug($"GetInputSourceAsync failed: {ex.Message}");
|
|
|
|
|
return BrightnessInfo.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);
|
|
|
|
|
|
2025-11-28 05:08:55 +08:00
|
|
|
/// <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"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (monitor.MonitorNumber <= 0)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogError($"[MonitorManager] SetRotation: Invalid monitor number for {monitorId}");
|
|
|
|
|
return Task.FromResult(MonitorOperationResult.Failure("Invalid monitor number"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Rotation uses Windows display settings API, not DDC/CI controller
|
|
|
|
|
var result = _rotationService.SetRotation(monitor.MonitorNumber, 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-11-27 14:51:31 +08:00
|
|
|
/// <summary>
|
|
|
|
|
/// Initialize input source for a monitor (async operation)
|
|
|
|
|
/// </summary>
|
|
|
|
|
public async Task InitializeInputSourceAsync(string monitorId, CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var sourceInfo = await GetInputSourceAsync(monitorId, cancellationToken);
|
|
|
|
|
if (sourceInfo.IsValid)
|
|
|
|
|
{
|
|
|
|
|
var monitor = GetMonitor(monitorId);
|
|
|
|
|
if (monitor != null)
|
|
|
|
|
{
|
|
|
|
|
// Store raw VCP 0x60 value (e.g., 0x11 for HDMI-1)
|
|
|
|
|
monitor.CurrentInputSource = sourceInfo.Current;
|
|
|
|
|
Logger.LogInfo($"[{monitorId}] Input source initialized: {monitor.InputSourceName}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogWarning($"Failed to initialize input source for {monitorId}: {ex.Message}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-20 16:22:47 +08:00
|
|
|
/// <summary>
|
|
|
|
|
/// Initialize color temperature for a monitor (async operation)
|
|
|
|
|
/// </summary>
|
|
|
|
|
public async Task InitializeColorTemperatureAsync(string monitorId, CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var tempInfo = await GetColorTemperatureAsync(monitorId, cancellationToken);
|
|
|
|
|
if (tempInfo.IsValid)
|
|
|
|
|
{
|
|
|
|
|
var monitor = GetMonitor(monitorId);
|
|
|
|
|
if (monitor != null)
|
|
|
|
|
{
|
2025-11-14 13:17:55 +08:00
|
|
|
// Store raw VCP 0x14 preset value (e.g., 0x05 for 6500K)
|
|
|
|
|
// No Kelvin conversion - we use discrete presets
|
|
|
|
|
monitor.CurrentColorTemperature = tempInfo.Current;
|
2025-10-20 16:22:47 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogWarning($"Failed to initialize color temperature for {monitorId}: {ex.Message}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-11-24 23:36:25 +08:00
|
|
|
/// 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)
|
|
|
|
|
{
|
2025-11-24 23:36:25 +08:00
|
|
|
return _monitorLookup.TryGetValue(monitorId, out var monitor) ? monitor : null;
|
2025-10-20 16:22:47 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Get controller for the monitor
|
|
|
|
|
/// </summary>
|
2025-11-17 14:53:43 +08:00
|
|
|
private async Task<IMonitorController?> GetControllerForMonitorAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
2025-10-20 16:22:47 +08:00
|
|
|
{
|
Refactor PowerDisplay for dynamic monitor capabilities
Removed reliance on static `MonitorType` enumeration, replacing it with dynamic `CommunicationMethod` for better flexibility. Updated `IMonitorController` and `MonitorManager` to dynamically determine monitor control capabilities.
Refactored `Monitor` model to streamline properties and improve color temperature handling. Enhanced `MonitorViewModel` with unified methods for brightness, contrast, volume, and color temperature updates, improving UI responsiveness and hardware synchronization.
Improved settings handling by adding support for hidden monitors, preserving user preferences, and separating UI configuration from hardware parameter updates. Updated the PowerDisplay Settings UI with warnings, confirmation dialogs, and better VCP capabilities formatting.
Removed legacy IPC code in favor of event-driven settings updates. Conducted general code cleanup, improving logging, error handling, and documentation for maintainability.
2025-11-14 16:45:22 +08:00
|
|
|
// WMI monitors use WmiController, DDC/CI monitors use DdcCiController
|
2025-11-17 14:53:43 +08:00
|
|
|
foreach (var controller in _controllers)
|
|
|
|
|
{
|
|
|
|
|
if (await controller.CanControlMonitorAsync(monitor, cancellationToken))
|
|
|
|
|
{
|
|
|
|
|
return controller;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 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");
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-17 14:53:43 +08:00
|
|
|
var controller = await GetControllerForMonitorAsync(monitor, cancellationToken);
|
2025-10-20 16:22:47 +08:00
|
|
|
if (controller == null)
|
|
|
|
|
{
|
Refactor PowerDisplay for dynamic monitor capabilities
Removed reliance on static `MonitorType` enumeration, replacing it with dynamic `CommunicationMethod` for better flexibility. Updated `IMonitorController` and `MonitorManager` to dynamically determine monitor control capabilities.
Refactored `Monitor` model to streamline properties and improve color temperature handling. Enhanced `MonitorViewModel` with unified methods for brightness, contrast, volume, and color temperature updates, improving UI responsiveness and hardware synchronization.
Improved settings handling by adding support for hidden monitors, preserving user preferences, and separating UI configuration from hardware parameter updates. Updated the PowerDisplay Settings UI with warnings, confirmation dialogs, and better VCP capabilities formatting.
Removed legacy IPC code in favor of event-driven settings updates. Conducted general code cleanup, improving logging, error handling, and documentation for maintainability.
2025-11-14 16:45:22 +08:00
|
|
|
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 all controllers
|
|
|
|
|
foreach (var controller in _controllers)
|
|
|
|
|
{
|
|
|
|
|
controller?.Dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_controllers.Clear();
|
|
|
|
|
_monitors.Clear();
|
2025-11-24 23:36:25 +08:00
|
|
|
_monitorLookup.Clear();
|
2025-10-20 16:22:47 +08:00
|
|
|
_disposed = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|