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.
This commit is contained in:
Yu Leng
2025-12-11 10:08:51 +08:00
parent 54006a8ef1
commit 87eb7cc07f
6 changed files with 128 additions and 283 deletions

View File

@@ -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
{
/// <summary>
/// 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).
/// </summary>
public sealed partial class LightSwitchListener : IDisposable
{
private const string LogPrefix = "[LightSwitch Listener]";
private Thread? _listenerThread;
private CancellationTokenSource? _cancellationTokenSource;
private bool _disposed;
/// <summary>
/// Fired when LightSwitch signals a theme change and a profile should be applied
/// </summary>
public event EventHandler<ThemeChangedEventArgs>? ThemeChanged;
/// <summary>
/// Starts the background thread to listen for LightSwitch theme change events
/// </summary>
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");
}
/// <summary>
/// Stops the background listener thread
/// </summary>
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}");
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="isLightMode">Whether the theme is light mode (determined from the signaled event)</param>
/// <returns>The profile name to apply, or null if not configured</returns>
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);
}
}
}

View File

@@ -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
{
/// <summary>
/// Service for handling LightSwitch theme change events.
/// Provides methods to process theme changes and read LightSwitch settings.
/// Event listening is handled externally via NativeEventWaiter.
/// </summary>
public static class LightSwitchService
{
private const string LogPrefix = "[LightSwitch]";
/// <summary>
/// Process a theme change event and return the profile name to apply.
/// </summary>
/// <param name="isLightMode">Whether the theme changed to light mode.</param>
/// <returns>The profile name to apply, or null if no profile is configured.</returns>
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;
}
}
/// <summary>
/// Reads LightSwitch settings and returns the profile name to apply for the given theme.
/// </summary>
/// <param name="isLightMode">Whether the theme is light mode.</param>
/// <returns>The profile name to apply, or null if not configured.</returns>
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;
}
}
}

View File

@@ -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
{
/// <summary>
/// Event arguments for theme change notifications from LightSwitch
/// </summary>
public class ThemeChangedEventArgs : EventArgs
{
/// <summary>
/// Gets a value indicating whether the system is currently in light mode
/// </summary>
public bool IsLightMode { get; }
/// <summary>
/// Gets profile name to apply (null if no profile configured for current theme)
/// </summary>
public string? ProfileToApply { get; }
public ThemeChangedEventArgs(bool isLightMode, string? profileToApply)
{
IsLightMode = isLightMode;
ProfileToApply = profileToApply;
}
}
}

View File

@@ -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)
{

View File

@@ -209,11 +209,15 @@ public partial class MainViewModel
}
/// <summary>
/// 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.
/// </summary>
private void OnLightSwitchThemeChanged(object? sender, ThemeChangedEventArgs e)
/// <param name="isLightMode">Whether the theme changed to light mode.</param>
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)
{

View File

@@ -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<MonitorViewModel> _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)