// 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.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.UI;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Windowing;
using PowerDisplay.Common.Drivers;
using PowerDisplay.Common.Drivers.DDC;
using PowerDisplay.Common.Models;
using PowerDisplay.Common.Services;
using PowerDisplay.Helpers;
using PowerDisplay.Models;
using PowerDisplay.PowerDisplayXAML;
namespace PowerDisplay.ViewModels;
///
/// Main ViewModel for the PowerDisplay application.
/// Split into partial classes for better maintainability:
/// - MainViewModel.cs: Core properties, construction, and disposal
/// - MainViewModel.Monitors.cs: Monitor discovery and management
/// - MainViewModel.Settings.cs: Settings UI synchronization and profiles
///
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicMethods)]
public partial class MainViewModel : ObservableObject, IDisposable
{
[LibraryImport("user32.dll", EntryPoint = "GetMonitorInfoW", StringMarshalling = StringMarshalling.Utf16)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool GetMonitorInfo(IntPtr hMonitor, ref MonitorInfoEx lpmi);
private readonly MonitorManager _monitorManager;
private readonly DispatcherQueue _dispatcherQueue;
private readonly CancellationTokenSource _cancellationTokenSource;
private readonly SettingsUtils _settingsUtils;
private readonly MonitorStateManager _stateManager;
private readonly DisplayChangeWatcher _displayChangeWatcher;
private readonly ISystemClock _clock;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasMonitors))]
[NotifyPropertyChangedFor(nameof(ShowNoMonitorsMessage))]
[NotifyPropertyChangedFor(nameof(IsInteractionEnabled))]
[NotifyPropertyChangedFor(nameof(IsLinkedBrightnessSliderEnabled))]
public partial bool IsScanning { get; set; }
private bool _isInitialized;
private bool _isLoading;
[ObservableProperty]
public partial ObservableCollection Monitors { get; set; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasProfiles))]
[NotifyPropertyChangedFor(nameof(ShowProfileSwitcherButton))]
public partial ObservableCollection Profiles { get; set; }
///
/// Event triggered when UI refresh is requested due to settings changes
///
public event EventHandler? UIRefreshRequested;
///
/// Event triggered when initial monitor discovery is completed.
/// Used by MainWindow to know when data is ready for display.
///
public event EventHandler? InitializationCompleted;
public MainViewModel()
: this(new SystemClock())
{
}
internal MainViewModel(ISystemClock clock)
{
_clock = clock;
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
_cancellationTokenSource = new CancellationTokenSource();
Monitors = new ObservableCollection();
Profiles = new ObservableCollection();
IsScanning = true;
ShowProfileSwitcher = true;
ShowIdentifyMonitorsButton = true;
// Initialize settings utils
_settingsUtils = SettingsUtils.Default;
_stateManager = new MonitorStateManager();
// Initialize the monitor manager
_monitorManager = new MonitorManager();
// Load profiles for quick apply feature
LoadProfiles();
// Load UI display settings (profile switcher, identify button, color temp switcher)
LoadUIDisplaySettings();
// Initialize display change watcher for auto-refresh on monitor plug/unplug
// Use MonitorRefreshDelay from settings to allow hardware to stabilize after plug/unplug
var settings = _settingsUtils.GetSettingsOrDefault(PowerDisplaySettings.ModuleName);
int delaySeconds = Math.Clamp(settings?.Properties?.MonitorRefreshDelay ?? 5, 1, 30);
_displayChangeWatcher = new DisplayChangeWatcher(_dispatcherQueue, TimeSpan.FromSeconds(delaySeconds));
_displayChangeWatcher.DisplayChanged += OnDisplayChanged;
_displayChangeWatcher.DisplayChanging += OnDisplayChanging;
// Start initial discovery
_ = InitializeAsync(_cancellationTokenSource.Token);
}
public bool HasProfiles => Profiles.Count > 0;
// UI display control properties - loaded from settings
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ShowProfileSwitcherButton))]
public partial bool ShowProfileSwitcher { get; set; }
[ObservableProperty]
public partial bool ShowIdentifyMonitorsButton { get; set; }
///
/// Gets or sets a value indicating whether brightness slider changes are broadcast to all
/// non-excluded monitors as one linked level. Persisted in PowerDisplaySettings so
/// the choice survives restarts. The toggle is meaningful only when two or more monitors
/// are connected — see for the visibility gate.
///
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ShowIndividualDisplays))]
[NotifyPropertyChangedFor(nameof(ShowLinkLevelsToggle))]
[NotifyPropertyChangedFor(nameof(ShowLinkLevelsInactiveIcon))]
public partial bool LinkedLevelsActive { get; set; }
///
/// Gets or sets the brightness value driving the linked "All Displays" slider. Setter is
/// debounced and broadcasts on commit to every linked target: a
/// monitor not excluded from sync (see
/// OnLinkedBrightnessChanged). Synchronously updates each linked monitor's brightness
/// value so it is correct if the monitor is later excluded or link mode is disabled — the
/// linked broadcast is the single source of hardware writes while link mode is active.
///
[ObservableProperty]
public partial int LinkedBrightness { get; set; }
///
/// Gets a value indicating whether the linked brightness slider has at least one linked
/// brightness-capable monitor to control.
///
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsLinkedBrightnessSliderEnabled))]
public partial bool IsLinkedBrightnessAvailable { get; set; }
///
/// Gets a value indicating whether the linked "All Displays" brightness slider accepts input.
/// Disabled both when no linked target is available and while monitor discovery/restore is busy.
///
public bool IsLinkedBrightnessSliderEnabled => IsLinkedBrightnessAvailable && IsInteractionEnabled;
///
/// Gets the count of monitors that participate in linked brightness — used for the
/// "All Displays" card subtitle ("N linked"). Counts brightness-capable monitors the user
/// has not excluded (see ).
///
public int LinkedMonitorsCount => Monitors.Count(m => m.SupportsBrightness && !IsMonitorExcludedFromSync(m.Id));
///
/// Gets the count of brightness-capable monitors excluded from linked brightness.
///
public int ExcludedMonitorsCount => Monitors.Count(m => m.SupportsBrightness && IsMonitorExcludedFromSync(m.Id));
///
/// Gets a value indicating whether the "N excluded" subtitle fragment should be visible on
/// the "All displays" card.
///
public bool HasExcludedMonitors => ExcludedMonitorsCount > 0;
///
/// Gets the localized "N linked" subtitle shown on the "All Displays" card. Recomputed
/// alongside via RecomputeLinkedBrightnessAvailability.
///
public string LinkedMonitorsCountText =>
string.Format(
System.Globalization.CultureInfo.CurrentCulture,
Helpers.ResourceLoaderInstance.ResourceLoader.GetString("AllDisplaysLinkedCountFormat"),
LinkedMonitorsCount);
///
/// Gets the localized "N excluded" subtitle fragment shown on the "All Displays" card.
///
public string ExcludedMonitorsCountText =>
string.Format(
System.Globalization.CultureInfo.CurrentCulture,
Helpers.ResourceLoaderInstance.ResourceLoader.GetString("AllDisplaysExcludedCountFormat"),
ExcludedMonitorsCount);
///
/// Gets or sets a value indicating whether the "Individual displays" section is expanded
/// while link mode is on. Collapsed by default so linked mode reads as a single master
/// slider; expanding reveals the per-monitor cards (with linked monitors' sliders disabled).
/// Ignored when link mode is off — see .
///
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ShowIndividualDisplays))]
[NotifyPropertyChangedFor(nameof(ShowIndividualDisplaysCollapsedIcon))]
public partial bool IndividualDisplaysExpanded { get; set; }
///
/// Gets a value indicating whether the per-monitor cards list is shown. Always shown when
/// link mode is off (the classic layout); when link mode is on the cards are tucked into the
/// collapsible "Individual displays" section and only shown once the user expands it.
///
public bool ShowIndividualDisplays => !LinkedLevelsActive || IndividualDisplaysExpanded;
public bool ShowIndividualDisplaysCollapsedIcon => !IndividualDisplaysExpanded;
///
/// Gets a value indicating whether the link-levels toggle should be visible. Shown when at
/// least two monitors report (the entry-point
/// gate — counting raw entries would show the toggle even when only one display can actually be
/// driven), OR whenever link mode is already active. The second clause guarantees the user can
/// always turn link mode back off, even if monitors were unplugged down to one controllable
/// display while linked — otherwise the "All displays" card would strand them with no way out.
/// Recomputed by and on change.
///
public bool ShowLinkLevelsToggle => LinkedLevelsActive || Monitors.Count(m => m.SupportsBrightness) >= 2;
public bool ShowLinkLevelsInactiveIcon => !LinkedLevelsActive;
///
/// Gets a value indicating whether to show the profile switcher button.
/// Combines settings value with HasProfiles check.
///
public bool ShowProfileSwitcherButton => ShowProfileSwitcher && HasProfiles;
// Custom VCP mappings - loaded from settings
private List _customVcpMappings = new();
///
/// Gets or sets the custom VCP value name mappings.
/// These mappings override the default VCP value names for color temperature and input source.
///
public List CustomVcpMappings
{
get => _customVcpMappings;
set
{
_customVcpMappings = value ?? new List();
OnPropertyChanged();
}
}
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();
OnPropertyChanged(nameof(IsInteractionEnabled));
OnPropertyChanged(nameof(IsLinkedBrightnessSliderEnabled));
}
}
///
/// Gets a value indicating whether user interaction is enabled (not loading or scanning).
///
public bool IsInteractionEnabled => !IsLoading && !IsScanning;
[RelayCommand]
private async Task RefreshAsync() => await RefreshMonitorsAsync();
[RelayCommand]
private unsafe void IdentifyMonitors()
{
try
{
// Get all display areas (virtual desktop regions)
var displayAreas = DisplayArea.FindAll();
// Get all monitor info from QueryDisplayConfig
var allDisplayInfo = DisplayConfigInventory.GetAllMonitorDisplayInfo().Values.ToList();
// Build GDI name to MonitorNumber(s) mapping
// Note: In mirror mode, multiple monitors may share the same GdiDeviceName
var gdiToMonitorNumbers = allDisplayInfo
.Where(info => info.MonitorNumber > 0)
.GroupBy(info => info.GdiDeviceName, StringComparer.OrdinalIgnoreCase)
.ToDictionary(
g => g.Key,
g => g.Select(info => info.MonitorNumber).Distinct().OrderBy(n => n).ToList(),
StringComparer.OrdinalIgnoreCase);
// For each DisplayArea, get its HMONITOR, then get GDI device name to find MonitorNumber(s)
int windowsCreated = 0;
for (int i = 0; i < displayAreas.Count; i++)
{
var displayArea = displayAreas[i];
// Convert DisplayId to HMONITOR
var hMonitor = Win32Interop.GetMonitorFromDisplayId(displayArea.DisplayId);
if (hMonitor == IntPtr.Zero)
{
continue;
}
// Get GDI device name from HMONITOR
var monitorInfo = new MonitorInfoEx { CbSize = (uint)sizeof(MonitorInfoEx) };
if (!GetMonitorInfo(hMonitor, ref monitorInfo))
{
continue;
}
var gdiDeviceName = monitorInfo.GetDeviceName();
// Look up MonitorNumber(s) by GDI device name
if (!gdiToMonitorNumbers.TryGetValue(gdiDeviceName, out var monitorNumbers) || monitorNumbers.Count == 0)
{
continue;
}
// Format display text: single number for normal mode, "1|2" for mirror mode
var displayText = string.Join("|", monitorNumbers);
// Create and position identify window.
// Position before Activate so the window appears directly at the target
// location — avoiding a visible flicker from the default spawn position
// and skipping a WM_DPICHANGED round-trip when crossing DPI monitors.
var identifyWindow = new IdentifyWindow(displayText);
identifyWindow.PositionOnDisplay(displayArea);
identifyWindow.Activate();
windowsCreated++;
}
}
catch (Exception ex)
{
Logger.LogError($"Failed to identify monitors: {ex.Message}");
}
}
[RelayCommand]
private async Task ApplyProfile(PowerDisplayProfile? profile)
{
if (profile != null && profile.IsValid())
{
await ApplyProfileAsync(profile.MonitorSettings);
}
}
public void Dispose()
{
// Cancel all async operations first
_cancellationTokenSource?.Cancel();
// Stop the linked-brightness debounce timer so its Tick handler does not fire after
// we have already cleared Monitors below (broadcast would iterate an empty list).
try
{
_linkedBrightnessCommitTimer?.Stop();
}
catch
{
}
// Dispose each resource independently to ensure all get cleaned up
try
{
if (_displayChangeWatcher is not null)
{
_displayChangeWatcher.DisplayChanging -= OnDisplayChanging;
_displayChangeWatcher.DisplayChanged -= OnDisplayChanged;
_displayChangeWatcher.Dispose();
}
}
catch
{
}
// Dispose monitor view models
foreach (var vm in Monitors)
{
try
{
vm.Dispose();
}
catch
{
}
}
try
{
_monitorManager?.Dispose();
}
catch
{
}
try
{
_stateManager?.Dispose();
}
catch
{
}
try
{
_cancellationTokenSource?.Dispose();
}
catch
{
}
try
{
Monitors.Clear();
}
catch
{
}
GC.SuppressFinalize(this);
}
///
/// Load profiles from disk for quick apply feature
///
private void LoadProfiles()
{
try
{
var profilesData = ProfileService.LoadProfiles();
Profiles.Clear();
foreach (var profile in profilesData.Profiles)
{
Profiles.Add(profile);
}
OnPropertyChanged(nameof(HasProfiles));
OnPropertyChanged(nameof(ShowProfileSwitcherButton));
}
catch (Exception ex)
{
Logger.LogError($"[Profile] Failed to load profiles: {ex.Message}");
}
}
///
/// Load UI display settings from settings file
///
private void LoadUIDisplaySettings()
{
try
{
var settings = _settingsUtils.GetSettingsOrDefault(PowerDisplaySettings.ModuleName);
ShowProfileSwitcher = settings.Properties.ShowProfileSwitcher;
ShowIdentifyMonitorsButton = settings.Properties.ShowIdentifyMonitorsButton;
// Load the linked-brightness exclusion set before applying LinkedLevelsActive. If this
// method runs after monitors are already discovered, the toggle hook can seed the master
// slider immediately and must see the persisted exclusions.
LoadExcludedMonitorIds(settings.Properties.ExcludedFromSyncMonitorIds);
LinkedLevelsActive = settings.Properties.LinkedLevelsActive;
// Load custom VCP mappings (now using shared type from PowerDisplay.Common.Models)
CustomVcpMappings = settings.Properties.CustomVcpMappings?.ToList() ?? new List();
Logger.LogInfo($"[Settings] Loaded {CustomVcpMappings.Count} custom VCP mappings");
}
catch (Exception ex)
{
Logger.LogError($"[Settings] Failed to load UI display settings: {ex.Message}");
}
}
///
/// Invoked synchronously as soon as a display configuration change is
/// detected (device added/removed, or wake from sleep), before the debounce
/// delay elapses. Locks the interactive UI by setting IsScanning = true so
/// the user cannot operate on monitors that are about to disappear or be
/// re-enumerated by the rediscovery pass
/// will run once debounce completes.
///
private void OnDisplayChanging(object? sender, EventArgs e)
{
CancelPendingLinkedBrightnessCommit();
if (!IsScanning)
{
Logger.LogInfo("[MainViewModel] Display change detected — locking UI ahead of rediscovery");
IsScanning = true;
}
}
///
/// Handles display configuration changes once the DisplayChangeWatcher's
/// debounce delay has elapsed. IsScanning was already set by
/// when the change was first detected, so
/// we just run discovery here.
///
private async void OnDisplayChanged(object? sender, EventArgs e)
{
await RefreshMonitorsAsync(skipScanningCheck: true);
}
///
/// Starts watching for display changes. Call after initialization is complete.
///
public void StartDisplayWatching()
{
_displayChangeWatcher.Start();
}
///
/// Stops watching for display changes.
///
public void StopDisplayWatching()
{
_displayChangeWatcher.Stop();
}
}