From 87eb7cc07f3519f2290226c4cf5ef6468c6ef489 Mon Sep 17 00:00:00 2001 From: Yu Leng Date: Thu, 11 Dec 2025 10:08:51 +0800 Subject: [PATCH] Refactor LightSwitch integration: decouple event handling Removed LightSwitchListener and ThemeChangedEventArgs. Added LightSwitchService to centralize theme/profile logic. Updated event registration and MainViewModel to handle LightSwitch theme changes via service, decoupling event listening from profile application. Event listening now handled externally. --- .../Services/LightSwitchListener.cs | 239 ------------------ .../Services/LightSwitchService.cs | 112 ++++++++ .../Services/ThemeChangedEventArgs.cs | 30 --- .../PowerDisplay/PowerDisplayXAML/App.xaml.cs | 5 + .../ViewModels/MainViewModel.Settings.cs | 18 +- .../PowerDisplay/ViewModels/MainViewModel.cs | 7 - 6 files changed, 128 insertions(+), 283 deletions(-) delete mode 100644 src/modules/powerdisplay/PowerDisplay.Lib/Services/LightSwitchListener.cs create mode 100644 src/modules/powerdisplay/PowerDisplay.Lib/Services/LightSwitchService.cs delete mode 100644 src/modules/powerdisplay/PowerDisplay.Lib/Services/ThemeChangedEventArgs.cs diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Services/LightSwitchListener.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Services/LightSwitchListener.cs deleted file mode 100644 index 14da36adc1..0000000000 --- a/src/modules/powerdisplay/PowerDisplay.Lib/Services/LightSwitchListener.cs +++ /dev/null @@ -1,239 +0,0 @@ -// 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.IO; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using ManagedCommon; - -namespace PowerDisplay.Common.Services -{ - /// - /// Listens for LightSwitch theme change events and notifies subscribers. - /// Encapsulates all LightSwitch integration logic including: - /// - Background thread management for event listening (light/dark theme events) - /// - LightSwitch settings file parsing - /// Theme is determined directly from which event was signaled (not from registry). - /// - public sealed partial class LightSwitchListener : IDisposable - { - private const string LogPrefix = "[LightSwitch Listener]"; - - private Thread? _listenerThread; - private CancellationTokenSource? _cancellationTokenSource; - private bool _disposed; - - /// - /// Fired when LightSwitch signals a theme change and a profile should be applied - /// - public event EventHandler? ThemeChanged; - - /// - /// Starts the background thread to listen for LightSwitch theme change events - /// - public void Start() - { - if (_listenerThread != null && _listenerThread.IsAlive) - { - Logger.LogWarning($"{LogPrefix} Listener already running"); - return; - } - - _cancellationTokenSource = new CancellationTokenSource(); - var token = _cancellationTokenSource.Token; - - _listenerThread = new Thread(() => ListenerThreadProc(token)) - { - IsBackground = true, - Name = "LightSwitchEventListener", - }; - - _listenerThread.Start(); - Logger.LogInfo($"{LogPrefix} Listener started"); - } - - /// - /// Stops the background listener thread - /// - public void Stop() - { - if (_cancellationTokenSource == null) - { - return; - } - - _cancellationTokenSource.Cancel(); - - if (_listenerThread != null && _listenerThread.IsAlive) - { - try - { - if (!_listenerThread.Join(TimeSpan.FromSeconds(2))) - { - Logger.LogWarning($"{LogPrefix} Listener thread did not stop in time"); - } - } - catch (Exception ex) - { - Logger.LogDebug($"{LogPrefix} Error joining listener thread: {ex.Message}"); - } - } - - _listenerThread = null; - _cancellationTokenSource.Dispose(); - _cancellationTokenSource = null; - - Logger.LogInfo($"{LogPrefix} Listener stopped"); - } - - private void ListenerThreadProc(CancellationToken cancellationToken) - { - try - { - Logger.LogInfo($"{LogPrefix} Event listener thread started"); - - // Use separate events for light and dark themes to avoid race conditions - // where we might read the registry before LightSwitch has updated it - using var lightThemeEvent = new EventWaitHandle(false, EventResetMode.AutoReset, PathConstants.LightSwitchLightThemeEventName); - using var darkThemeEvent = new EventWaitHandle(false, EventResetMode.AutoReset, PathConstants.LightSwitchDarkThemeEventName); - - var waitHandles = new WaitHandle[] { lightThemeEvent, darkThemeEvent }; - - while (!cancellationToken.IsCancellationRequested) - { - // Wait for either light or dark theme event (with timeout to allow cancellation check) - int index = WaitHandle.WaitAny(waitHandles, TimeSpan.FromSeconds(1)); - - if (index == WaitHandle.WaitTimeout) - { - continue; - } - - // Determine theme from which event was signaled - bool isLightMode = index == 0; // 0 = lightThemeEvent, 1 = darkThemeEvent - Logger.LogInfo($"{LogPrefix} Theme event received: {(isLightMode ? "Light" : "Dark")}"); - - // Process the theme change with the known theme - _ = Task.Run(() => ProcessThemeChange(isLightMode), CancellationToken.None); - } - - Logger.LogInfo($"{LogPrefix} Event listener thread stopping"); - } - catch (Exception ex) - { - Logger.LogError($"{LogPrefix} Event listener thread failed: {ex.Message}"); - } - } - - private void ProcessThemeChange(bool isLightMode) - { - try - { - Logger.LogInfo($"{LogPrefix} Processing theme change to {(isLightMode ? "light" : "dark")} mode"); - - var profileToApply = ReadProfileFromLightSwitchSettings(isLightMode); - - if (string.IsNullOrEmpty(profileToApply) || profileToApply == "(None)") - { - Logger.LogInfo($"{LogPrefix} No profile configured for {(isLightMode ? "light" : "dark")} mode"); - return; - } - - Logger.LogInfo($"{LogPrefix} Requesting profile application: {profileToApply}"); - - // Notify subscribers - ThemeChanged?.Invoke(this, new ThemeChangedEventArgs(isLightMode, profileToApply)); - } - catch (Exception ex) - { - Logger.LogError($"{LogPrefix} Failed to process theme change: {ex.Message}"); - } - } - - /// - /// Reads LightSwitch settings and returns the profile name to apply for the given theme. - /// The theme is determined by which event was signaled (light or dark), not by reading the registry. - /// - /// Whether the theme is light mode (determined from the signaled event) - /// The profile name to apply, or null if not configured - private static string? ReadProfileFromLightSwitchSettings(bool isLightMode) - { - try - { - var settingsPath = PathConstants.LightSwitchSettingsFilePath; - - if (!File.Exists(settingsPath)) - { - Logger.LogWarning($"{LogPrefix} LightSwitch settings file not found"); - return null; - } - - var json = File.ReadAllText(settingsPath); - var settings = JsonDocument.Parse(json); - var root = settings.RootElement; - - if (!root.TryGetProperty("properties", out var properties)) - { - Logger.LogWarning($"{LogPrefix} LightSwitch settings has no properties"); - return null; - } - - // Check if monitor settings integration is enabled - if (!properties.TryGetProperty("apply_monitor_settings", out var applyMonitorSettingsElement) || - !applyMonitorSettingsElement.TryGetProperty("value", out var applyValue) || - !applyValue.GetBoolean()) - { - Logger.LogInfo($"{LogPrefix} Monitor settings integration is disabled"); - return null; - } - - // Get the appropriate profile name based on the theme from the event - if (isLightMode) - { - return GetProfileFromSettings(properties, "enable_light_mode_profile", "light_mode_profile"); - } - else - { - return GetProfileFromSettings(properties, "enable_dark_mode_profile", "dark_mode_profile"); - } - } - catch (Exception ex) - { - Logger.LogError($"{LogPrefix} Failed to read LightSwitch settings: {ex.Message}"); - return null; - } - } - - private static string? GetProfileFromSettings( - JsonElement properties, - string enableKey, - string profileKey) - { - if (properties.TryGetProperty(enableKey, out var enableElement) && - enableElement.TryGetProperty("value", out var enableValue) && - enableValue.GetBoolean() && - properties.TryGetProperty(profileKey, out var profileElement) && - profileElement.TryGetProperty("value", out var profileValue)) - { - return profileValue.GetString(); - } - - return null; - } - - public void Dispose() - { - if (_disposed) - { - return; - } - - Stop(); - _disposed = true; - GC.SuppressFinalize(this); - } - } -} diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Services/LightSwitchService.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Services/LightSwitchService.cs new file mode 100644 index 0000000000..92f81be6e8 --- /dev/null +++ b/src/modules/powerdisplay/PowerDisplay.Lib/Services/LightSwitchService.cs @@ -0,0 +1,112 @@ +// 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.IO; +using System.Text.Json; +using ManagedCommon; + +namespace PowerDisplay.Common.Services +{ + /// + /// Service for handling LightSwitch theme change events. + /// Provides methods to process theme changes and read LightSwitch settings. + /// Event listening is handled externally via NativeEventWaiter. + /// + public static class LightSwitchService + { + private const string LogPrefix = "[LightSwitch]"; + + /// + /// Process a theme change event and return the profile name to apply. + /// + /// Whether the theme changed to light mode. + /// The profile name to apply, or null if no profile is configured. + public static string? GetProfileForTheme(bool isLightMode) + { + try + { + Logger.LogInfo($"{LogPrefix} Processing theme change to {(isLightMode ? "light" : "dark")} mode"); + + var profileToApply = ReadProfileFromLightSwitchSettings(isLightMode); + + if (string.IsNullOrEmpty(profileToApply) || profileToApply == "(None)") + { + Logger.LogInfo($"{LogPrefix} No profile configured for {(isLightMode ? "light" : "dark")} mode"); + return null; + } + + Logger.LogInfo($"{LogPrefix} Profile to apply: {profileToApply}"); + return profileToApply; + } + catch (Exception ex) + { + Logger.LogError($"{LogPrefix} Failed to process theme change: {ex.Message}"); + return null; + } + } + + /// + /// Reads LightSwitch settings and returns the profile name to apply for the given theme. + /// + /// Whether the theme is light mode. + /// The profile name to apply, or null if not configured. + private static string? ReadProfileFromLightSwitchSettings(bool isLightMode) + { + var settingsPath = PathConstants.LightSwitchSettingsFilePath; + + if (!File.Exists(settingsPath)) + { + Logger.LogWarning($"{LogPrefix} LightSwitch settings file not found"); + return null; + } + + var json = File.ReadAllText(settingsPath); + var settings = JsonDocument.Parse(json); + var root = settings.RootElement; + + if (!root.TryGetProperty("properties", out var properties)) + { + Logger.LogWarning($"{LogPrefix} LightSwitch settings has no properties"); + return null; + } + + // Check if monitor settings integration is enabled + if (!properties.TryGetProperty("apply_monitor_settings", out var applyMonitorSettingsElement) || + !applyMonitorSettingsElement.TryGetProperty("value", out var applyValue) || + !applyValue.GetBoolean()) + { + Logger.LogInfo($"{LogPrefix} Monitor settings integration is disabled"); + return null; + } + + // Get the appropriate profile name based on the theme + if (isLightMode) + { + return GetProfileFromSettings(properties, "enable_light_mode_profile", "light_mode_profile"); + } + else + { + return GetProfileFromSettings(properties, "enable_dark_mode_profile", "dark_mode_profile"); + } + } + + private static string? GetProfileFromSettings( + JsonElement properties, + string enableKey, + string profileKey) + { + if (properties.TryGetProperty(enableKey, out var enableElement) && + enableElement.TryGetProperty("value", out var enableValue) && + enableValue.GetBoolean() && + properties.TryGetProperty(profileKey, out var profileElement) && + profileElement.TryGetProperty("value", out var profileValue)) + { + return profileValue.GetString(); + } + + return null; + } + } +} diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Services/ThemeChangedEventArgs.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Services/ThemeChangedEventArgs.cs deleted file mode 100644 index 3badd58d17..0000000000 --- a/src/modules/powerdisplay/PowerDisplay.Lib/Services/ThemeChangedEventArgs.cs +++ /dev/null @@ -1,30 +0,0 @@ -// 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; - -namespace PowerDisplay.Common.Services -{ - /// - /// Event arguments for theme change notifications from LightSwitch - /// - public class ThemeChangedEventArgs : EventArgs - { - /// - /// Gets a value indicating whether the system is currently in light mode - /// - public bool IsLightMode { get; } - - /// - /// Gets profile name to apply (null if no profile configured for current theme) - /// - public string? ProfileToApply { get; } - - public ThemeChangedEventArgs(bool isLightMode, string? profileToApply) - { - IsLightMode = isLightMode; - ProfileToApply = profileToApply; - } - } -} diff --git a/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/App.xaml.cs b/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/App.xaml.cs index 2b0b75127f..737a9cf4dd 100644 --- a/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/App.xaml.cs +++ b/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/App.xaml.cs @@ -11,6 +11,7 @@ using Microsoft.PowerToys.Telemetry; using Microsoft.UI.Dispatching; using Microsoft.UI.Xaml; using Microsoft.Windows.AppLifecycle; +using PowerDisplay.Common; using PowerDisplay.Helpers; using PowerDisplay.Serialization; using PowerToys.Interop; @@ -106,6 +107,10 @@ namespace PowerDisplay RegisterViewModelEvent(Constants.ApplyColorTemperaturePowerDisplayEvent(), vm => vm.ApplyColorTemperatureFromSettings(), "ApplyColorTemperature"); RegisterViewModelEvent(Constants.ApplyProfilePowerDisplayEvent(), vm => vm.ApplyProfileFromSettings(), "ApplyProfile"); + // LightSwitch integration - apply profiles when theme changes + RegisterViewModelEvent(PathConstants.LightSwitchLightThemeEventName, vm => vm.ApplyLightSwitchProfile(isLightMode: true), "LightSwitch-Light"); + RegisterViewModelEvent(PathConstants.LightSwitchDarkThemeEventName, vm => vm.ApplyLightSwitchProfile(isLightMode: false), "LightSwitch-Dark"); + // Monitor Runner process (backup exit mechanism) if (_powerToysRunnerPid > 0) { diff --git a/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.Settings.cs b/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.Settings.cs index 4e7a71921d..4dc94a38c3 100644 --- a/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.Settings.cs +++ b/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.Settings.cs @@ -209,11 +209,15 @@ public partial class MainViewModel } /// - /// Handle theme change notification from LightSwitch + /// Handle theme change from LightSwitch by applying the appropriate profile. + /// Called from App.xaml.cs when LightSwitch theme events are received. /// - private void OnLightSwitchThemeChanged(object? sender, ThemeChangedEventArgs e) + /// Whether the theme changed to light mode. + public void ApplyLightSwitchProfile(bool isLightMode) { - if (string.IsNullOrEmpty(e.ProfileToApply)) + var profileName = LightSwitchService.GetProfileForTheme(isLightMode); + + if (string.IsNullOrEmpty(profileName)) { return; } @@ -222,21 +226,21 @@ public partial class MainViewModel { try { - Logger.LogInfo($"[LightSwitch Integration] Applying profile: {e.ProfileToApply}"); + Logger.LogInfo($"[LightSwitch Integration] Applying profile: {profileName}"); // Load and apply the profile var profilesData = ProfileService.LoadProfiles(); - var profile = profilesData.GetProfile(e.ProfileToApply); + var profile = profilesData.GetProfile(profileName); if (profile == null || !profile.IsValid()) { - Logger.LogWarning($"[LightSwitch Integration] Profile '{e.ProfileToApply}' not found or invalid"); + Logger.LogWarning($"[LightSwitch Integration] Profile '{profileName}' not found or invalid"); return; } // Apply the profile await ApplyProfileAsync(profile.MonitorSettings); - Logger.LogInfo($"[LightSwitch Integration] Successfully applied profile '{e.ProfileToApply}'"); + Logger.LogInfo($"[LightSwitch Integration] Successfully applied profile '{profileName}'"); } catch (Exception ex) { diff --git a/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs b/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs index 04cb619f40..f8b092b166 100644 --- a/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs +++ b/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs @@ -46,7 +46,6 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable private readonly CancellationTokenSource _cancellationTokenSource; private readonly ISettingsUtils _settingsUtils; private readonly MonitorStateManager _stateManager; - private readonly LightSwitchListener _lightSwitchListener; private readonly DisplayChangeWatcher _displayChangeWatcher; private ObservableCollection _monitors; @@ -75,11 +74,6 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable // Initialize the monitor manager _monitorManager = new MonitorManager(); - // Initialize and start LightSwitch integration listener - _lightSwitchListener = new LightSwitchListener(); - _lightSwitchListener.ThemeChanged += OnLightSwitchThemeChanged; - _lightSwitchListener.Start(); - // Load profiles for quick apply feature LoadProfiles(); @@ -256,7 +250,6 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable // Dispose all resources safely (don't throw from Dispose) SafeDispose(_displayChangeWatcher, "DisplayChangeWatcher"); - SafeDispose(_lightSwitchListener, "LightSwitchListener"); // Dispose monitor view models foreach (var vm in Monitors)