// 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.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
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.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 : INotifyPropertyChanged, 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 ObservableCollection _monitors;
private ObservableCollection _profiles;
private bool _isScanning;
private bool _isInitialized;
private bool _isLoading;
///
/// 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()
{
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
_cancellationTokenSource = new CancellationTokenSource();
_monitors = new ObservableCollection();
_profiles = new ObservableCollection();
_isScanning = 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;
// Start initial discovery
_ = InitializeAsync(_cancellationTokenSource.Token);
}
public ObservableCollection Monitors
{
get => _monitors;
set
{
_monitors = value;
OnPropertyChanged();
}
}
public ObservableCollection Profiles
{
get => _profiles;
set
{
_profiles = value;
OnPropertyChanged();
OnPropertyChanged(nameof(HasProfiles));
}
}
public bool HasProfiles => Profiles.Count > 0;
// UI display control properties - loaded from settings
private bool _showProfileSwitcher = true;
private bool _showIdentifyMonitorsButton = true;
///
/// Gets a value indicating whether to show the profile switcher button.
/// Combines settings value with HasProfiles check.
///
public bool ShowProfileSwitcherButton => _showProfileSwitcher && HasProfiles;
///
/// Gets or sets a value indicating whether to show the profile switcher (from settings).
///
public bool ShowProfileSwitcher
{
get => _showProfileSwitcher;
set
{
if (_showProfileSwitcher != value)
{
_showProfileSwitcher = value;
OnPropertyChanged();
OnPropertyChanged(nameof(ShowProfileSwitcherButton));
}
}
}
///
/// Gets or sets a value indicating whether to show the identify monitors button.
///
public bool ShowIdentifyMonitorsButton
{
get => _showIdentifyMonitorsButton;
set
{
if (_showIdentifyMonitorsButton != value)
{
_showIdentifyMonitorsButton = value;
OnPropertyChanged();
}
}
}
public bool IsScanning
{
get => _isScanning;
set
{
if (_isScanning != value)
{
_isScanning = value;
OnPropertyChanged();
// Dependent properties that change with IsScanning
OnPropertyChanged(nameof(HasMonitors));
OnPropertyChanged(nameof(ShowNoMonitorsMessage));
OnPropertyChanged(nameof(IsInteractionEnabled));
}
}
}
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));
}
}
///
/// 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 = DdcCiNative.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
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 event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public void Dispose()
{
// Cancel all async operations first
_cancellationTokenSource?.Cancel();
// Dispose each resource independently to ensure all get cleaned up
try
{
_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;
}
catch (Exception ex)
{
Logger.LogError($"[Settings] Failed to load UI display settings: {ex.Message}");
}
}
///
/// Handles display configuration changes detected by the DisplayChangeWatcher.
/// The DisplayChangeWatcher already applies the configured delay (MonitorRefreshDelay)
/// to allow hardware to stabilize, so we can refresh immediately here.
///
private async void OnDisplayChanged(object? sender, EventArgs e)
{
// Set scanning state to provide visual feedback
IsScanning = true;
// Perform refresh - DisplayChangeWatcher has already waited for hardware to stabilize
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();
}
}