mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-16 19:57:57 +01:00
464 lines
15 KiB
C#
464 lines
15 KiB
C#
// 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.ComponentModel;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Threading.Tasks;
|
|
using System.Windows.Input;
|
|
using ManagedCommon;
|
|
using Microsoft.UI.Xaml;
|
|
using PowerDisplay.Commands;
|
|
using PowerDisplay.Core;
|
|
using PowerDisplay.Core.Models;
|
|
using PowerDisplay.Helpers;
|
|
using Monitor = PowerDisplay.Core.Models.Monitor;
|
|
|
|
namespace PowerDisplay.ViewModels;
|
|
|
|
/// <summary>
|
|
/// ViewModel for individual monitor
|
|
/// </summary>
|
|
public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
|
|
{
|
|
private readonly Monitor _monitor;
|
|
private readonly MonitorManager _monitorManager;
|
|
private readonly MainViewModel? _mainViewModel;
|
|
|
|
// Simple debouncers for each property (KISS principle - simpler than complex queue)
|
|
private readonly SimpleDebouncer _brightnessDebouncer = new(300);
|
|
private readonly SimpleDebouncer _colorTempDebouncer = new(300);
|
|
private readonly SimpleDebouncer _contrastDebouncer = new(300);
|
|
private readonly SimpleDebouncer _volumeDebouncer = new(300);
|
|
|
|
private int _brightness;
|
|
private int _colorTemperature;
|
|
private int _contrast;
|
|
private int _volume;
|
|
private bool _isAvailable;
|
|
|
|
// Visibility settings (controlled by Settings UI)
|
|
private bool _showColorTemperature;
|
|
private bool _showContrast;
|
|
private bool _showVolume;
|
|
|
|
/// <summary>
|
|
/// Updates a property value directly without triggering hardware updates.
|
|
/// Used during initialization to update UI from saved state.
|
|
/// </summary>
|
|
internal void UpdatePropertySilently(string propertyName, int value)
|
|
{
|
|
switch (propertyName)
|
|
{
|
|
case nameof(ColorTemperature):
|
|
_colorTemperature = value;
|
|
OnPropertyChanged(nameof(ColorTemperature));
|
|
OnPropertyChanged(nameof(ColorTemperaturePercent));
|
|
break;
|
|
case nameof(Brightness):
|
|
_brightness = value;
|
|
OnPropertyChanged(nameof(Brightness));
|
|
break;
|
|
case nameof(Contrast):
|
|
_contrast = value;
|
|
OnPropertyChanged(nameof(Contrast));
|
|
OnPropertyChanged(nameof(ContrastPercent));
|
|
break;
|
|
case nameof(Volume):
|
|
_volume = value;
|
|
OnPropertyChanged(nameof(Volume));
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Conversion function for x:Bind (AOT-compatible alternative to converters)
|
|
public Visibility ConvertBoolToVisibility(bool value) => value ? Visibility.Visible : Visibility.Collapsed;
|
|
|
|
// Property to access IsInteractionEnabled from parent ViewModel
|
|
public bool IsInteractionEnabled => _mainViewModel?.IsInteractionEnabled ?? true;
|
|
|
|
public MonitorViewModel(Monitor monitor, MonitorManager monitorManager, MainViewModel mainViewModel)
|
|
{
|
|
_monitor = monitor;
|
|
_monitorManager = monitorManager;
|
|
_mainViewModel = mainViewModel;
|
|
|
|
// Subscribe to MainViewModel property changes to update IsInteractionEnabled
|
|
if (_mainViewModel != null)
|
|
{
|
|
_mainViewModel.PropertyChanged += OnMainViewModelPropertyChanged;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
// Initialize basic properties from monitor
|
|
_brightness = monitor.CurrentBrightness;
|
|
_contrast = monitor.CurrentContrast;
|
|
_volume = monitor.CurrentVolume;
|
|
_isAvailable = monitor.IsAvailable;
|
|
}
|
|
|
|
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";
|
|
|
|
public string? CapabilitiesRaw => _monitor.CapabilitiesRaw;
|
|
|
|
public VcpCapabilities? VcpCapabilitiesInfo => _monitor.VcpCapabilitiesInfo;
|
|
|
|
/// <summary>
|
|
/// Gets the icon glyph based on monitor type
|
|
/// </summary>
|
|
public string MonitorIconGlyph => Type == MonitorType.Internal ? "\uEA37" : "\uE7F4";
|
|
|
|
// 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)
|
|
{
|
|
// Update UI state immediately - keep slider smooth
|
|
_brightness = value;
|
|
OnPropertyChanged(); // UI responds immediately
|
|
|
|
// Debounce hardware update - much simpler than complex queue!
|
|
var capturedValue = value; // Capture value for async closure
|
|
_brightnessDebouncer.Debounce(async () =>
|
|
{
|
|
try
|
|
{
|
|
await _monitorManager.SetBrightnessAsync(Id, capturedValue);
|
|
_mainViewModel?.SaveMonitorSettingDirect(_monitor.HardwareId, "Brightness", capturedValue);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError($"Failed to set brightness for {Id}: {ex.Message}");
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
public int ColorTemperature
|
|
{
|
|
get => _colorTemperature;
|
|
set
|
|
{
|
|
if (_colorTemperature != value)
|
|
{
|
|
_colorTemperature = value;
|
|
OnPropertyChanged();
|
|
|
|
// Debounce hardware update - simple and clean!
|
|
var capturedValue = value;
|
|
_colorTempDebouncer.Debounce(async () =>
|
|
{
|
|
try
|
|
{
|
|
var result = await _monitorManager.SetColorTemperatureAsync(Id, capturedValue);
|
|
if (result.IsSuccess)
|
|
{
|
|
_monitor.CurrentColorTemperature = capturedValue;
|
|
_mainViewModel?.SaveMonitorSettingDirect(_monitor.HardwareId, "ColorTemperature", capturedValue);
|
|
}
|
|
else
|
|
{
|
|
Logger.LogError($"[{Id}] Failed to set color temperature: {result.ErrorMessage}");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError($"Failed to set color temperature for {Id}: {ex.Message}");
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
public int Contrast
|
|
{
|
|
get => _contrast;
|
|
set
|
|
{
|
|
if (_contrast != value)
|
|
{
|
|
_contrast = value;
|
|
OnPropertyChanged();
|
|
|
|
// Debounce hardware update
|
|
var capturedValue = value;
|
|
_contrastDebouncer.Debounce(async () =>
|
|
{
|
|
try
|
|
{
|
|
await _monitorManager.SetContrastAsync(Id, capturedValue);
|
|
_mainViewModel?.SaveMonitorSettingDirect(_monitor.HardwareId, "Contrast", capturedValue);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError($"Failed to set contrast for {Id}: {ex.Message}");
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
public int Volume
|
|
{
|
|
get => _volume;
|
|
set
|
|
{
|
|
if (_volume != value)
|
|
{
|
|
_volume = value;
|
|
OnPropertyChanged();
|
|
|
|
// Debounce hardware update
|
|
var capturedValue = value;
|
|
_volumeDebouncer.Debounce(async () =>
|
|
{
|
|
try
|
|
{
|
|
await _monitorManager.SetVolumeAsync(Id, capturedValue);
|
|
_mainViewModel?.SaveMonitorSettingDirect(_monitor.HardwareId, "Volume", capturedValue);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError($"Failed to set volume for {Id}: {ex.Message}");
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool IsAvailable
|
|
{
|
|
get => _isAvailable;
|
|
set
|
|
{
|
|
_isAvailable = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
|
|
public ICommand SetBrightnessCommand => new RelayCommand<int?>((brightness) =>
|
|
{
|
|
if (brightness.HasValue)
|
|
{
|
|
Brightness = brightness.Value;
|
|
}
|
|
});
|
|
|
|
public ICommand SetColorTemperatureCommand => new RelayCommand<int?>((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<int?>((contrast) =>
|
|
{
|
|
if (contrast.HasValue)
|
|
{
|
|
Contrast = contrast.Value;
|
|
}
|
|
});
|
|
|
|
public ICommand SetVolumeCommand => new RelayCommand<int?>((volume) =>
|
|
{
|
|
if (volume.HasValue)
|
|
{
|
|
Volume = volume.Value;
|
|
}
|
|
});
|
|
|
|
// 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)));
|
|
}
|
|
}
|
|
|
|
private void OnMainViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
|
{
|
|
if (e.PropertyName == nameof(MainViewModel.IsInteractionEnabled))
|
|
{
|
|
OnPropertyChanged(nameof(IsInteractionEnabled));
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
// Unsubscribe from MainViewModel events
|
|
if (_mainViewModel != null)
|
|
{
|
|
_mainViewModel.PropertyChanged -= OnMainViewModelPropertyChanged;
|
|
}
|
|
|
|
// Dispose all debouncers
|
|
_brightnessDebouncer?.Dispose();
|
|
_colorTempDebouncer?.Dispose();
|
|
_contrastDebouncer?.Dispose();
|
|
_volumeDebouncer?.Dispose();
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
}
|