// 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.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using PowerDisplay.Core;
using PowerDisplay.Core.Interfaces;
using PowerDisplay.Core.Models;
using PowerDisplay.Helpers;
using Monitor = PowerDisplay.Core.Models.Monitor;
namespace PowerDisplay.ViewModels;
///
/// Main ViewModel for the PowerDisplay application
///
public class MainViewModel : INotifyPropertyChanged, IDisposable
{
private readonly MonitorManager _monitorManager;
private readonly DispatcherQueue _dispatcherQueue;
private readonly CancellationTokenSource _cancellationTokenSource;
private readonly ISettingsUtils _settingsUtils;
private readonly MonitorStateManager _stateManager;
private FileSystemWatcher? _settingsWatcher;
private ObservableCollection _monitors;
private string _statusText;
private bool _isScanning;
private bool _isInitialized;
private bool _isLoading;
private bool _isReloadingSettings; // 防止重复加载
///
/// Event triggered when UI refresh is requested due to settings changes
///
public event EventHandler? UIRefreshRequested;
///
/// Event triggered when theme change is requested
///
public event EventHandler? ThemeChangeRequested;
public MainViewModel()
{
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
_cancellationTokenSource = new CancellationTokenSource();
_monitors = new ObservableCollection();
_statusText = "Initializing...";
_isScanning = true;
// Initialize settings utils
_settingsUtils = new SettingsUtils();
_stateManager = new MonitorStateManager();
// Initialize the monitor manager
_monitorManager = new MonitorManager();
// Subscribe to events
_monitorManager.MonitorsChanged += OnMonitorsChanged;
_monitorManager.MonitorStatusChanged += OnMonitorStatusChanged;
// Setup settings file monitoring
SetupSettingsFileWatcher();
// Start initial discovery
_ = InitializeAsync();
}
public ObservableCollection Monitors
{
get => _monitors;
set
{
_monitors = value;
OnPropertyChanged();
}
}
public string StatusText
{
get => _statusText;
set
{
_statusText = value;
OnPropertyChanged();
}
}
public bool IsScanning
{
get => _isScanning;
set
{
_isScanning = value;
OnPropertyChanged();
OnPropertyChanged(nameof(HasMonitors));
OnPropertyChanged(nameof(ShowNoMonitorsMessage));
}
}
public bool HasMonitors => !IsScanning && Monitors.Count > 0;
public bool ShowNoMonitorsMessage => !IsScanning && Monitors.Count == 0;
public bool IsInitialized
{
get => _isInitialized;
private set
{
_isInitialized = value;
OnPropertyChanged();
}
}
public bool IsLoading
{
get => _isLoading;
private set
{
_isLoading = value;
OnPropertyChanged();
// Update all monitors' interaction state
foreach (var monitor in Monitors)
{
monitor.IsInteractionEnabled = !value;
}
}
}
public ICommand RefreshCommand => new RelayCommand(async () => await RefreshMonitorsAsync());
public ICommand SetAllBrightnessCommand => new RelayCommand(async (brightness) =>
{
if (brightness.HasValue)
{
await SetAllBrightnessAsync(brightness.Value);
}
});
private async Task InitializeAsync()
{
try
{
StatusText = "Scanning monitors...";
IsScanning = true;
// Discover monitors
var monitors = await _monitorManager.DiscoverMonitorsAsync(_cancellationTokenSource.Token);
// Update UI on the dispatcher thread
_dispatcherQueue.TryEnqueue(() =>
{
UpdateMonitorList(monitors);
IsScanning = false;
IsInitialized = true;
if (monitors.Count > 0)
{
StatusText = $"Found {monitors.Count} monitors";
}
else
{
StatusText = "No controllable monitors found";
}
});
}
catch (Exception ex)
{
_dispatcherQueue.TryEnqueue(() =>
{
StatusText = $"Scan failed: {ex.Message}";
IsScanning = false;
});
}
}
public async Task RefreshMonitorsAsync()
{
if (IsScanning)
{
return;
}
try
{
StatusText = "Refreshing monitor list...";
IsScanning = true;
var monitors = await _monitorManager.DiscoverMonitorsAsync(_cancellationTokenSource.Token);
_dispatcherQueue.TryEnqueue(() =>
{
UpdateMonitorList(monitors);
IsScanning = false;
StatusText = $"Found {monitors.Count} monitors";
});
}
catch (Exception ex)
{
_dispatcherQueue.TryEnqueue(() =>
{
StatusText = $"Refresh failed: {ex.Message}";
IsScanning = false;
});
}
}
private void UpdateMonitorList(IReadOnlyList monitors)
{
Monitors.Clear();
foreach (var monitor in monitors)
{
var vm = new MonitorViewModel(monitor, _monitorManager, this);
Monitors.Add(vm);
// Asynchronously initialize color temperature for DDC/CI monitors
if (monitor.SupportsColorTemperature && monitor.Type == MonitorType.External)
{
_ = InitializeColorTemperatureSafeAsync(monitor.Id, vm);
}
}
OnPropertyChanged(nameof(HasMonitors));
OnPropertyChanged(nameof(ShowNoMonitorsMessage));
// Restore saved settings if enabled (async, don't block)
_ = ReloadMonitorSettingsAsync();
}
public async Task SetAllBrightnessAsync(int brightness)
{
try
{
StatusText = $"Setting all monitors brightness to {brightness}%...";
await _monitorManager.SetAllBrightnessAsync(brightness, _cancellationTokenSource.Token);
StatusText = $"All monitors brightness set to {brightness}%";
}
catch (Exception ex)
{
StatusText = $"Failed to set brightness: {ex.Message}";
}
}
private void OnMonitorsChanged(object? sender, MonitorListChangedEventArgs e)
{
_dispatcherQueue.TryEnqueue(() =>
{
// Handle monitors being added or removed
if (e.AddedMonitors.Count > 0)
{
foreach (var monitor in e.AddedMonitors)
{
var existingVm = GetMonitorViewModel(monitor.Id);
if (existingVm == null)
{
var vm = new MonitorViewModel(monitor, _monitorManager, this);
Monitors.Add(vm);
}
}
}
if (e.RemovedMonitors.Count > 0)
{
foreach (var monitor in e.RemovedMonitors)
{
var vm = GetMonitorViewModel(monitor.Id);
if (vm != null)
{
Monitors.Remove(vm);
vm.Dispose();
}
}
}
StatusText = $"Monitor list updated ({Monitors.Count} total)";
});
}
private void OnMonitorStatusChanged(object? sender, MonitorStatusChangedEventArgs e)
{
_dispatcherQueue.TryEnqueue(() =>
{
var vm = GetMonitorViewModel(e.Monitor.Id);
vm?.UpdateFromModel(e.Monitor);
});
}
private MonitorViewModel? GetMonitorViewModel(string monitorId)
{
foreach (var vm in Monitors)
{
if (vm.Id == monitorId)
{
return vm;
}
}
return null;
}
///
/// Setup settings file watcher
///
private void SetupSettingsFileWatcher()
{
try
{
var settingsPath = _settingsUtils.GetSettingsFilePath("PowerDisplay");
var directory = Path.GetDirectoryName(settingsPath);
var fileName = Path.GetFileName(settingsPath);
if (!string.IsNullOrEmpty(directory))
{
// Ensure directory exists
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
_settingsWatcher = new FileSystemWatcher(directory, fileName)
{
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime,
EnableRaisingEvents = true
};
_settingsWatcher.Changed += OnSettingsFileChanged;
_settingsWatcher.Created += OnSettingsFileChanged;
Logger.LogInfo($"Settings file watcher setup for: {settingsPath}");
}
}
catch (Exception ex)
{
Logger.LogError($"Failed to setup settings file watcher: {ex.Message}");
}
}
///
/// Handle settings file changes - only monitors UI configuration changes from Settings UI
/// (monitor_state.json is managed separately and doesn't trigger this)
///
private void OnSettingsFileChanged(object sender, FileSystemEventArgs e)
{
try
{
Logger.LogInfo($"Settings file changed by Settings UI: {e.FullPath}");
// Add small delay to ensure file write completion
Task.Delay(500).ContinueWith(_ =>
{
try
{
// Read updated settings
var settings = _settingsUtils.GetSettingsOrDefault("PowerDisplay");
_dispatcherQueue.TryEnqueue(() =>
{
// Update feature visibility for each monitor (UI configuration only)
foreach (var monitorVm in Monitors)
{
// Use converted internal name for lookup
var internalName = GetInternalName(monitorVm);
Logger.LogInfo($"[Settings Update] Looking for monitor settings with internal name: '{internalName}', Hardware ID: '{monitorVm.HardwareId}'");
var monitorSettings = settings.Properties.Monitors.FirstOrDefault(m =>
m.InternalName == internalName || m.HardwareId == monitorVm.HardwareId);
if (monitorSettings != null)
{
Logger.LogInfo($"[Settings Update] Found monitor settings for '{internalName}': ColorTemp={monitorSettings.EnableColorTemperature}, Contrast={monitorSettings.EnableContrast}, Volume={monitorSettings.EnableVolume}");
// Update visibility flags based on Settings UI toggles
monitorVm.ShowColorTemperature = monitorSettings.EnableColorTemperature;
monitorVm.ShowContrast = monitorSettings.EnableContrast;
monitorVm.ShowVolume = monitorSettings.EnableVolume;
}
else
{
Logger.LogWarning($"[Settings Update] No monitor settings found for '{internalName}' with Hardware ID '{monitorVm.HardwareId}'");
Logger.LogInfo($"[Settings Update] Available monitors in settings:");
foreach (var availableMonitor in settings.Properties.Monitors)
{
Logger.LogInfo($" - Internal: '{availableMonitor.InternalName}', Hardware: '{availableMonitor.HardwareId}', Name: '{availableMonitor.Name}'");
}
}
}
// Check for theme changes and apply them
var newTheme = PowerDisplay.Helpers.ThemeManager.GetThemeFromPowerToysSettings();
if (newTheme != ElementTheme.Default)
{
ThemeChangeRequested?.Invoke(this, newTheme);
Logger.LogInfo($"Theme change requested: {newTheme}");
}
// Trigger UI refresh for configuration changes
UIRefreshRequested?.Invoke(this, EventArgs.Empty);
});
Logger.LogInfo($"Settings UI configuration reloaded, monitor count: {settings.Properties.Monitors.Count}");
}
catch (Exception ex)
{
Logger.LogError($"Failed to reload settings: {ex.Message}");
}
});
}
catch (Exception ex)
{
Logger.LogError($"Error handling settings file change: {ex.Message}");
}
}
///
/// Safe wrapper for initializing color temperature asynchronously
///
private async Task InitializeColorTemperatureSafeAsync(string monitorId, MonitorViewModel vm)
{
try
{
await _monitorManager.InitializeColorTemperatureAsync(monitorId);
// Update UI on dispatcher thread - get the monitor from manager
var monitor = _monitorManager.GetMonitor(monitorId);
if (monitor != null)
{
_dispatcherQueue.TryEnqueue(() => vm.UpdateFromModel(monitor));
}
}
catch (Exception ex)
{
Logger.LogWarning($"Failed to initialize color temperature for {monitorId}: {ex.Message}");
}
}
// INotifyPropertyChanged
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
///
/// Reload monitor settings from configuration
///
public async Task ReloadMonitorSettingsAsync()
{
// 防止重复调用
if (_isReloadingSettings)
{
Logger.LogInfo("[Startup] ReloadMonitorSettingsAsync already in progress, skipping");
return;
}
try
{
_isReloadingSettings = true;
// Set loading state to block UI interactions
IsLoading = true;
StatusText = "Loading settings...";
// Read current settings
var settings = _settingsUtils.GetSettingsOrDefault("PowerDisplay");
if (settings.Properties.RestoreSettingsOnStartup)
{
// Restore saved settings from configuration file
Logger.LogInfo("[Startup] RestoreSettingsOnStartup enabled - applying saved settings");
foreach (var monitorVm in Monitors)
{
var hardwareId = monitorVm.HardwareId;
Logger.LogInfo($"[Startup] Processing monitor: '{monitorVm.Name}', HardwareId: '{hardwareId}'");
// Find and apply corresponding saved settings from state file using stable HardwareId
var savedState = _stateManager.GetMonitorParameters(hardwareId);
if (savedState.HasValue)
{
Logger.LogInfo($"[Startup] Restoring state for HardwareId '{hardwareId}': Brightness={savedState.Value.Brightness}, ColorTemp={savedState.Value.ColorTemperature}");
// 验证并应用保存的值(跳过无效值)
if (savedState.Value.Brightness >= monitorVm.MinBrightness && savedState.Value.Brightness <= monitorVm.MaxBrightness)
{
monitorVm.Brightness = savedState.Value.Brightness;
}
else
{
Logger.LogWarning($"[Startup] Invalid brightness value {savedState.Value.Brightness} for HardwareId '{hardwareId}', skipping");
}
// 色温值必须有效且在范围内
if (savedState.Value.ColorTemperature > 0 &&
savedState.Value.ColorTemperature >= monitorVm.MinColorTemperature &&
savedState.Value.ColorTemperature <= monitorVm.MaxColorTemperature)
{
monitorVm.ColorTemperature = savedState.Value.ColorTemperature;
}
else
{
Logger.LogWarning($"[Startup] Invalid color temperature value {savedState.Value.ColorTemperature} for HardwareId '{hardwareId}', skipping");
}
// 对比度值验证 - 只在硬件支持的情况下才应用
if (monitorVm.ShowContrast &&
savedState.Value.Contrast >= monitorVm.MinContrast &&
savedState.Value.Contrast <= monitorVm.MaxContrast)
{
monitorVm.Contrast = savedState.Value.Contrast;
}
else if (!monitorVm.ShowContrast)
{
Logger.LogInfo($"[Startup] Contrast not supported on HardwareId '{hardwareId}', skipping");
}
// 音量值验证 - 只在硬件支持的情况下才应用
if (monitorVm.ShowVolume &&
savedState.Value.Volume >= monitorVm.MinVolume &&
savedState.Value.Volume <= monitorVm.MaxVolume)
{
monitorVm.Volume = savedState.Value.Volume;
}
else if (!monitorVm.ShowVolume)
{
Logger.LogInfo($"[Startup] Volume not supported on HardwareId '{hardwareId}', skipping");
}
}
else
{
Logger.LogInfo($"[Startup] No saved state for HardwareId '{hardwareId}' - keeping current hardware values");
}
// Apply feature visibility settings (still need InternalName for Settings UI matching)
var internalName = GetInternalName(monitorVm);
ApplyFeatureVisibility(monitorVm, settings, internalName);
}
StatusText = "Applying settings...";
// Wait for all hardware updates to complete
try
{
await Task.WhenAll(Monitors.Select(m => m.FlushAllUpdatesAsync()));
StatusText = "Saved settings restored successfully";
}
catch (Exception ex)
{
Logger.LogError($"[Startup] Error waiting for updates: {ex.Message}");
StatusText = "Settings applied with errors";
}
}
else
{
// Save current hardware values to configuration file
Logger.LogInfo("[Startup] RestoreSettingsOnStartup disabled - saving current hardware values");
foreach (var monitorVm in Monitors)
{
var internalName = GetInternalName(monitorVm);
// Save current hardware values to settings
SaveMonitorSetting(monitorVm.Id, "Brightness", monitorVm.Brightness);
SaveMonitorSetting(monitorVm.Id, "ColorTemperature", monitorVm.ColorTemperature);
SaveMonitorSetting(monitorVm.Id, "Contrast", monitorVm.Contrast);
SaveMonitorSetting(monitorVm.Id, "Volume", monitorVm.Volume);
Logger.LogInfo($"[Startup] Saved current values for '{internalName}': Brightness={monitorVm.Brightness}, ColorTemp={monitorVm.ColorTemperature}");
// Apply feature visibility settings
ApplyFeatureVisibility(monitorVm, settings, internalName);
}
// Flush pending changes immediately
await _stateManager.FlushAsync();
StatusText = "Current monitor values saved to state file";
}
}
catch (Exception ex)
{
Logger.LogError($"Failed to reload/save settings: {ex.Message}");
StatusText = $"Failed to process settings: {ex.Message}";
}
finally
{
// Clear loading state to enable UI interactions
IsLoading = false;
_isReloadingSettings = false;
}
}
///
/// Apply feature visibility settings to a monitor ViewModel
///
private void ApplyFeatureVisibility(MonitorViewModel monitorVm, PowerDisplaySettings settings, string internalName)
{
var monitorSettings = settings.Properties.Monitors.FirstOrDefault(m =>
m.InternalName == internalName || m.HardwareId == monitorVm.HardwareId);
if (monitorSettings != null)
{
Logger.LogInfo($"[Startup] Applying feature visibility for '{internalName}': ColorTemp={monitorSettings.EnableColorTemperature}, Contrast={monitorSettings.EnableContrast}, Volume={monitorSettings.EnableVolume}");
monitorVm.ShowColorTemperature = monitorSettings.EnableColorTemperature;
monitorVm.ShowContrast = monitorSettings.EnableContrast;
monitorVm.ShowVolume = monitorSettings.EnableVolume;
}
else
{
Logger.LogWarning($"[Startup] No feature settings found for '{internalName}' - using defaults");
}
}
///
/// Save monitor settings to configuration file (one-way: save only, no read during runtime)
///
public void SaveMonitorSetting(string monitorId, string property, int value)
{
try
{
// Find the monitor VM to get the stable HardwareId
var monitorVm = GetMonitorViewModel(monitorId);
if (monitorVm == null)
{
Logger.LogError($"Monitor not found for ID: {monitorId}");
return;
}
// Use stable HardwareId as the key for state persistence
var hardwareId = monitorVm.HardwareId;
// Update parameter in state file (lock-free, non-blocking)
_stateManager.UpdateMonitorParameter(hardwareId, property, value);
Logger.LogTrace($"[State] Queued setting change for HardwareId '{hardwareId}': {property}={value}");
}
catch (Exception ex)
{
// Log error but don't interrupt user operation
Logger.LogError($"Failed to queue setting save: {ex.Message}");
StatusText = $"Warning: Failed to save settings: {ex.Message}";
}
}
///
/// Reset a monitor to default values
///
public void ResetMonitor(string monitorId)
{
try
{
var monitorVm = GetMonitorViewModel(monitorId);
if (monitorVm != null)
{
// Apply default values
monitorVm.Brightness = 30;
monitorVm.ColorTemperature = 6500;
monitorVm.Contrast = 50;
monitorVm.Volume = 50;
StatusText = $"Monitor {monitorVm.Name} reset to default values";
}
}
catch (Exception ex)
{
StatusText = $"Failed to reset monitor: {ex.Message}";
}
}
///
/// Convert monitor ID to the internal name format used by Settings UI
///
public string GetInternalName(MonitorViewModel monitor)
{
// For internal displays, use "Internal Display"
if (monitor.Type == MonitorType.Internal)
{
return "Internal Display";
}
// For external monitors, remove technical prefix to match SettingsManager logic
var id = monitor.Id;
if (!string.IsNullOrEmpty(id) && id.StartsWith("DDC_", StringComparison.Ordinal))
{
return id.Substring(4); // Remove "DDC_" prefix to match SettingsManager.GetInternalName
}
// Use the full ID if no prefix found
if (!string.IsNullOrEmpty(id))
{
return id;
}
// Use hardware ID as secondary option if unique ID is not available
if (!string.IsNullOrEmpty(monitor.HardwareId))
{
return monitor.HardwareId;
}
// For external monitors, try to use a clean identifier
if (!string.IsNullOrEmpty(monitor.Name))
{
return monitor.Name;
}
// Fall back to a default identifier if nothing else works
return "Unknown Monitor";
}
// IDisposable
public void Dispose()
{
try
{
// 首先取消所有异步操作
_cancellationTokenSource?.Cancel();
// 立即停止文件监控
_settingsWatcher?.Dispose();
_settingsWatcher = null;
// Flush any unsaved state immediately (synchronously wait)
try
{
// Use Task.Run to avoid deadlock and wait with timeout
if (_stateManager != null)
{
var flushTask = _stateManager.FlushAsync();
if (!flushTask.Wait(TimeSpan.FromSeconds(2)))
{
Logger.LogWarning("State flush timed out during dispose");
}
}
}
catch (Exception ex)
{
Logger.LogError($"Failed to flush state during dispose: {ex.Message}");
}
// 快速清理监控器视图模型
try
{
foreach (var vm in Monitors)
{
vm?.Dispose();
}
Monitors.Clear();
}
catch
{
/* 忽略清理错误 */
}
// 释放监控器管理器
try
{
_monitorManager?.Dispose();
}
catch
{
/* 忽略清理错误 */
}
// 释放状态管理器
try
{
_stateManager?.Dispose();
}
catch
{
/* 忽略清理错误 */
}
// 最后释放取消令牌
try
{
_cancellationTokenSource?.Dispose();
}
catch
{
/* 忽略清理错误 */
}
}
catch
{
// 确保 Dispose 不会抛出异常
}
}
///
/// ViewModel for individual monitor
///
public class MonitorViewModel : INotifyPropertyChanged, IDisposable
{
private readonly Monitor _monitor;
private readonly MonitorManager _monitorManager;
private readonly MainViewModel _mainViewModel;
// Property managers for preventing race conditions
private readonly MonitorPropertyManager _brightnessManager;
private readonly MonitorPropertyManager _colorTemperatureManager;
private readonly MonitorPropertyManager _contrastManager;
private readonly MonitorPropertyManager _volumeManager;
private int _brightness;
private int _colorTemperature;
private int _contrast;
private int _volume;
private bool _isAvailable;
private bool _isUpdating;
private bool _isInteractionEnabled = true;
// Visibility settings (controlled by Settings UI)
private bool _showColorTemperature;
private bool _showContrast;
private bool _showVolume;
// User intent tracking for smooth slider operation
private int _targetBrightness = -1;
private int _targetColorTemperature = -1;
private int _targetContrast = -1;
private int _targetVolume = -1;
private DateTime _lastUserInteraction = DateTime.MinValue;
public MonitorViewModel(Monitor monitor, MonitorManager monitorManager, MainViewModel mainViewModel)
{
_monitor = monitor;
_monitorManager = monitorManager;
_mainViewModel = mainViewModel;
// Initialize property managers
_brightnessManager = new MonitorPropertyManager(monitor.Id, nameof(Brightness));
_colorTemperatureManager = new MonitorPropertyManager(monitor.Id, nameof(ColorTemperature));
_contrastManager = new MonitorPropertyManager(monitor.Id, nameof(Contrast));
_volumeManager = new MonitorPropertyManager(monitor.Id, nameof(Volume));
// Initialize Show properties based on hardware capabilities
_showColorTemperature = monitor.SupportsColorTemperature; // Only show for DDC/CI monitors that support it
_showContrast = monitor.SupportsContrast;
_showVolume = monitor.SupportsVolume;
// Try to get current color temperature via DDC/CI, use default if failed
try
{
// For DDC/CI monitors that support color temperature, use 6500K as default
// The actual temperature will be loaded asynchronously after construction
if (monitor.SupportsColorTemperature)
{
_colorTemperature = 6500; // Default neutral temperature for DDC monitors
}
else
{
_colorTemperature = 6500; // Default for unsupported monitors
}
monitor.CurrentColorTemperature = _colorTemperature;
Logger.LogDebug($"Initialized {monitor.Id} with default color temperature {_colorTemperature}K");
}
catch (Exception ex)
{
Logger.LogWarning($"Failed to initialize color temperature for {monitor.Id}: {ex.Message}");
_colorTemperature = 6500; // Default neutral temperature
monitor.CurrentColorTemperature = 6500;
}
UpdateFromModel(monitor);
}
public string Id => _monitor.Id;
public string HardwareId => _monitor.HardwareId;
public string Name => _monitor.Name;
public string Manufacturer => _monitor.Manufacturer;
public MonitorType Type => _monitor.Type;
public string TypeDisplay => Type == MonitorType.Internal ? "Internal" : "External";
// Monitor property ranges
public int MinBrightness => _monitor.MinBrightness;
public int MaxBrightness => _monitor.MaxBrightness;
public int MinColorTemperature => _monitor.MinColorTemperature;
public int MaxColorTemperature => _monitor.MaxColorTemperature;
public int MinContrast => _monitor.MinContrast;
public int MaxContrast => _monitor.MaxContrast;
public int MinVolume => _monitor.MinVolume;
public int MaxVolume => _monitor.MaxVolume;
// Advanced control display logic
public bool HasAdvancedControls => ShowColorTemperature || ShowContrast || ShowVolume;
public bool ShowColorTemperature
{
get => _showColorTemperature;
set
{
if (_showColorTemperature != value)
{
_showColorTemperature = value;
OnPropertyChanged();
OnPropertyChanged(nameof(HasAdvancedControls));
}
}
}
public bool ShowContrast
{
get => _showContrast;
set
{
if (_showContrast != value)
{
_showContrast = value;
OnPropertyChanged();
OnPropertyChanged(nameof(HasAdvancedControls));
}
}
}
public bool ShowVolume
{
get => _showVolume;
set
{
if (_showVolume != value)
{
_showVolume = value;
OnPropertyChanged();
OnPropertyChanged(nameof(HasAdvancedControls));
}
}
}
public int Brightness
{
get => _brightness;
set
{
if (_brightness != value)
{
// 立即更新UI状态 - 保持滑块流畅
_brightness = value;
_targetBrightness = value; // Record user intent
_lastUserInteraction = DateTime.Now;
OnPropertyChanged(); // UI立即响应
// 队列硬件更新 - 智能错误处理:只在队列最后失败时回滚
_brightnessManager.QueueUpdate(value, async (brightness, cancellationToken) =>
{
try
{
IsUpdating = true;
await _monitorManager.SetBrightnessAsync(Id, brightness, cancellationToken);
// 硬件更新成功后保存配置(异步,不阻塞UI)
_mainViewModel?._dispatcherQueue.TryEnqueue(() =>
{
_mainViewModel.SaveMonitorSetting(Id, "Brightness", brightness);
});
return true; // 成功
}
catch (Exception ex)
{
Logger.LogError($"Failed to set brightness for {Id}: {ex.Message}");
return false; // 失败
}
finally
{
IsUpdating = false;
}
});
}
}
}
public int ColorTemperature
{
get => _colorTemperature;
set
{
if (_colorTemperature != value)
{
_colorTemperature = value;
_targetColorTemperature = value; // Record user intent
_lastUserInteraction = DateTime.Now;
OnPropertyChanged();
// 队列硬件更新 - 智能错误处理:只在队列最后失败时回滚
_colorTemperatureManager.QueueUpdate(value, async (temperature, cancellationToken) =>
{
try
{
IsUpdating = true;
Logger.LogDebug($"[{Id}] Setting color temperature to {temperature}K via DDC/CI");
// 直接使用MonitorManager的DDC/CI色温控制
var result = await _monitorManager.SetColorTemperatureAsync(Id, temperature, cancellationToken);
if (result.IsSuccess)
{
_monitor.CurrentColorTemperature = temperature;
Logger.LogInfo($"[{Id}] Successfully set color temperature to {temperature}K via DDC/CI");
// 硬件更新成功后保存配置(异步,不阻塞UI)
_mainViewModel?._dispatcherQueue.TryEnqueue(() =>
{
_mainViewModel.SaveMonitorSetting(Id, "ColorTemperature", temperature);
});
return true; // 成功
}
else
{
Logger.LogError($"[{Id}] Failed to set color temperature via DDC/CI: {result.ErrorMessage}");
return false; // 失败
}
}
catch (Exception ex)
{
Logger.LogError($"Failed to set color temperature for {Id}: {ex.Message}");
return false; // 失败
}
finally
{
IsUpdating = false;
}
});
}
}
}
public int Contrast
{
get => _contrast;
set
{
if (_contrast != value)
{
_contrast = value;
_targetContrast = value; // Record user intent
_lastUserInteraction = DateTime.Now;
OnPropertyChanged();
// 队列硬件更新 - 智能错误处理:只在队列最后失败时回滚
_contrastManager.QueueUpdate(value, async (contrast, cancellationToken) =>
{
try
{
IsUpdating = true;
await _monitorManager.SetContrastAsync(Id, contrast, cancellationToken);
// 硬件更新成功后保存配置(异步,不阻塞UI)
_mainViewModel?._dispatcherQueue.TryEnqueue(() =>
{
_mainViewModel.SaveMonitorSetting(Id, "Contrast", contrast);
});
return true; // 成功
}
catch (Exception ex)
{
Logger.LogError($"Failed to set contrast for {Id}: {ex.Message}");
return false; // 失败
}
finally
{
IsUpdating = false;
}
});
}
}
}
public int Volume
{
get => _volume;
set
{
if (_volume != value)
{
_volume = value;
_targetVolume = value; // Record user intent
_lastUserInteraction = DateTime.Now;
OnPropertyChanged();
// 队列硬件更新 - 智能错误处理:只在队列最后失败时回滚
_volumeManager.QueueUpdate(value, async (volume, cancellationToken) =>
{
try
{
IsUpdating = true;
await _monitorManager.SetVolumeAsync(Id, volume, cancellationToken);
// 硬件更新成功后保存配置(异步,不阻塞UI)
_mainViewModel?._dispatcherQueue.TryEnqueue(() =>
{
_mainViewModel.SaveMonitorSetting(Id, "Volume", volume);
});
return true; // 成功
}
catch (Exception ex)
{
Logger.LogError($"Failed to set volume for {Id}: {ex.Message}");
return false; // 失败
}
finally
{
IsUpdating = false;
}
});
}
}
}
public bool IsAvailable
{
get => _isAvailable;
set
{
_isAvailable = value;
OnPropertyChanged();
}
}
public bool IsUpdating
{
get => _isUpdating;
set
{
_isUpdating = value;
OnPropertyChanged();
}
}
///
/// Gets or sets whether user interaction is enabled (not loading)
///
public bool IsInteractionEnabled
{
get => _isInteractionEnabled;
set
{
if (_isInteractionEnabled != value)
{
_isInteractionEnabled = value;
OnPropertyChanged();
}
}
}
public ICommand SetBrightnessCommand => new RelayCommand((brightness) =>
{
if (brightness.HasValue)
{
Brightness = brightness.Value;
}
});
public ICommand SetColorTemperatureCommand => new RelayCommand((temperature) =>
{
if (temperature.HasValue && _monitor.SupportsColorTemperature)
{
Logger.LogDebug($"[{Id}] Color temperature command: {temperature.Value}K (DDC/CI)");
ColorTemperature = temperature.Value;
}
else if (temperature.HasValue && !_monitor.SupportsColorTemperature)
{
Logger.LogWarning($"[{Id}] Color temperature not supported on this monitor");
}
});
public ICommand SetContrastCommand => new RelayCommand((contrast) =>
{
if (contrast.HasValue)
{
Contrast = contrast.Value;
}
});
public ICommand SetVolumeCommand => new RelayCommand((volume) =>
{
if (volume.HasValue)
{
Volume = volume.Value;
}
});
public void UpdateFromModel(Monitor monitor)
{
bool brightnessChanged = false;
bool colorTemperatureChanged = false;
bool contrastChanged = false;
bool volumeChanged = false;
// Smart update: only update if necessary to prevent slider bouncing
if (ShouldUpdateValue(_brightness, monitor.CurrentBrightness, ref _targetBrightness, nameof(Brightness)))
{
_brightness = monitor.CurrentBrightness;
brightnessChanged = true;
}
if (ShouldUpdateValue(_colorTemperature, monitor.CurrentColorTemperature, ref _targetColorTemperature, nameof(ColorTemperature)))
{
_colorTemperature = monitor.CurrentColorTemperature;
colorTemperatureChanged = true;
}
if (ShouldUpdateValue(_contrast, monitor.CurrentContrast, ref _targetContrast, nameof(Contrast)))
{
_contrast = monitor.CurrentContrast;
contrastChanged = true;
}
if (ShouldUpdateValue(_volume, monitor.CurrentVolume, ref _targetVolume, nameof(Volume)))
{
_volume = monitor.CurrentVolume;
volumeChanged = true;
}
// Always update availability
if (_isAvailable != monitor.IsAvailable)
{
_isAvailable = monitor.IsAvailable;
OnPropertyChanged(nameof(IsAvailable));
}
// Notify property changes only for values that actually changed
if (brightnessChanged) OnPropertyChanged(nameof(Brightness));
if (colorTemperatureChanged) OnPropertyChanged(nameof(ColorTemperature));
if (contrastChanged) OnPropertyChanged(nameof(Contrast));
if (volumeChanged) OnPropertyChanged(nameof(Volume));
}
private async Task UpdateBrightnessAsync(int brightness)
{
if (IsUpdating)
{
return;
}
try
{
IsUpdating = true;
await _monitorManager.SetBrightnessAsync(Id, brightness);
}
catch
{
// Revert on error
_brightness = _monitor.CurrentBrightness;
OnPropertyChanged(nameof(Brightness));
}
finally
{
IsUpdating = false;
}
}
private async Task UpdateColorTemperatureAsync(int temperature)
{
if (IsUpdating)
{
return;
}
try
{
IsUpdating = true;
Logger.LogDebug($"[{Id}] Updating color temperature to {temperature}K via DDC/CI");
var result = await _monitorManager.SetColorTemperatureAsync(Id, temperature);
if (result.IsSuccess)
{
_monitor.CurrentColorTemperature = temperature;
Logger.LogDebug($"[{Id}] Successfully updated color temperature to {temperature}K");
}
else
{
Logger.LogError($"[{Id}] Failed to update color temperature: {result.ErrorMessage}");
// Revert on error
_colorTemperature = _monitor.CurrentColorTemperature;
OnPropertyChanged(nameof(ColorTemperature));
}
}
catch (Exception ex)
{
Logger.LogError($"[{Id}] Exception updating color temperature: {ex.Message}");
// Revert on error
_colorTemperature = _monitor.CurrentColorTemperature;
OnPropertyChanged(nameof(ColorTemperature));
}
finally
{
IsUpdating = false;
}
}
private async Task UpdateContrastAsync(int contrast)
{
if (IsUpdating)
{
return;
}
try
{
IsUpdating = true;
await _monitorManager.SetContrastAsync(_monitor.Id, contrast);
}
catch
{
// Revert on error
_contrast = _monitor.CurrentContrast;
OnPropertyChanged(nameof(Contrast));
}
finally
{
IsUpdating = false;
}
}
private async Task UpdateVolumeAsync(int volume)
{
if (IsUpdating)
{
return;
}
try
{
IsUpdating = true;
await _monitorManager.SetVolumeAsync(_monitor.Id, volume);
}
catch
{
// Revert on error
_volume = _monitor.CurrentVolume;
OnPropertyChanged(nameof(Volume));
}
finally
{
IsUpdating = false;
}
}
///
/// Determines if a UI value should be updated based on hardware feedback
/// to prevent slider bouncing during user interaction
///
private bool ShouldUpdateValue(int currentValue, int hardwareValue, ref int targetValue, string propertyName)
{
// 1. If user just interacted (800ms), don't update to preserve smooth dragging
// 增加了时间窗口,因为现在有渐进式更新需要更多时间
if ((DateTime.Now - _lastUserInteraction).TotalMilliseconds < 800)
{
return false;
}
// 2. If hardware value reached target, reset target and don't update UI
if (targetValue != -1 && Math.Abs(hardwareValue - targetValue) <= 2)
{
targetValue = -1; // Reset target
return false;
}
// 3. Only update if there's a significant difference and user isn't actively dragging
if (Math.Abs(currentValue - hardwareValue) > 3)
{
return true;
}
return false;
}
// Percentage-based properties for uniform slider behavior
public int ColorTemperaturePercent
{
get => MapToPercent(_colorTemperature, MinColorTemperature, MaxColorTemperature);
set
{
var actualValue = MapFromPercent(value, MinColorTemperature, MaxColorTemperature);
ColorTemperature = actualValue;
}
}
public int ContrastPercent
{
get => MapToPercent(_contrast, MinContrast, MaxContrast);
set
{
var actualValue = MapFromPercent(value, MinContrast, MaxContrast);
Contrast = actualValue;
}
}
// Mapping functions for percentage conversion
private int MapToPercent(int value, int min, int max)
{
if (max <= min) return 0;
return (int)Math.Round((value - min) * 100.0 / (max - min));
}
private int MapFromPercent(int percent, int min, int max)
{
if (max <= min) return min;
percent = Math.Clamp(percent, 0, 100);
return min + (int)Math.Round(percent * (max - min) / 100.0);
}
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
// Notify percentage properties when actual values change
if (propertyName == nameof(ColorTemperature))
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ColorTemperaturePercent)));
}
else if (propertyName == nameof(Contrast))
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ContrastPercent)));
}
}
///
/// Wait for all pending property updates to complete
///
public async Task FlushAllUpdatesAsync()
{
await Task.WhenAll(
_brightnessManager.FlushAsync(),
_colorTemperatureManager.FlushAsync(),
_contrastManager.FlushAsync(),
_volumeManager.FlushAsync());
}
public void Dispose()
{
// Dispose property managers
_brightnessManager?.Dispose();
_colorTemperatureManager?.Dispose();
_contrastManager?.Dispose();
_volumeManager?.Dispose();
}
}
public class RelayCommand : ICommand
{
private readonly Action _execute;
private readonly Func? _canExecute;
public RelayCommand(Action execute, Func? canExecute = null)
{
_execute = execute;
_canExecute = canExecute;
}
public event EventHandler? CanExecuteChanged;
public bool CanExecute(object? parameter) => _canExecute?.Invoke() ?? true;
public void Execute(object? parameter) => _execute();
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
///
/// Generic relay command implementation
///
public class RelayCommand : ICommand
{
private readonly Action _execute;
private readonly Func? _canExecute;
public RelayCommand(Action execute, Func? canExecute = null)
{
_execute = execute;
_canExecute = canExecute;
}
public event EventHandler? CanExecuteChanged;
public bool CanExecute(object? parameter) => _canExecute?.Invoke((T?)parameter) ?? true;
public void Execute(object? parameter) => _execute((T?)parameter);
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}