Files
PowerToys/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/App.xaml.cs

346 lines
13 KiB
C#
Raw Normal View History

2025-10-20 16:22:47 +08:00
// 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.Threading;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
2025-10-20 16:22:47 +08:00
using Microsoft.PowerToys.Telemetry;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.Windows.AppLifecycle;
using PowerDisplay.Common;
2025-10-20 16:22:47 +08:00
using PowerDisplay.Helpers;
using PowerDisplay.Serialization;
using PowerToys.Interop;
2025-10-20 16:22:47 +08:00
namespace PowerDisplay
{
/// <summary>
/// PowerDisplay application main class
/// </summary>
#pragma warning disable CA1001 // CancellationTokenSource is disposed in Shutdown/ForceExit methods
2025-10-20 16:22:47 +08:00
public partial class App : Application
#pragma warning restore CA1001
2025-10-20 16:22:47 +08:00
{
2025-12-01 06:09:26 +08:00
/// <summary>
/// Event name for signaling that PowerDisplay process is ready.
/// Must match the constant in C++ PowerDisplayModuleInterface.
/// </summary>
private const string ProcessReadyEventName = "Local\\PowerToys_PowerDisplay_Ready";
private readonly ISettingsUtils _settingsUtils = SettingsUtils.Default;
2025-10-20 16:22:47 +08:00
private Window? _mainWindow;
private int _powerToysRunnerPid;
private TrayIconService? _trayIconService;
2025-10-20 16:22:47 +08:00
public App(int runnerPid)
2025-10-20 16:22:47 +08:00
{
_powerToysRunnerPid = runnerPid;
this.InitializeComponent();
// Ensure types used in XAML are preserved for AOT compilation
TypePreservation.PreserveTypes();
// Note: Logger is already initialized in Program.cs before App constructor
2025-10-20 16:22:47 +08:00
// Initialize PowerToys telemetry
try
{
PowerToysTelemetry.Log.WriteEvent(new Telemetry.Events.PowerDisplayStartEvent());
}
catch
{
// Telemetry errors should not crash the app
}
// Initialize language settings
string appLanguage = LanguageHelper.LoadLanguage();
if (!string.IsNullOrEmpty(appLanguage))
{
Microsoft.Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = appLanguage;
}
// Handle unhandled exceptions
this.UnhandledException += OnUnhandledException;
}
/// <summary>
/// Handle unhandled exceptions
/// </summary>
private void OnUnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{
Logger.LogError("Unhandled exception", e.Exception);
2025-10-20 16:22:47 +08:00
}
/// <summary>
/// Called when the application is launched
/// </summary>
/// <param name="args">Launch arguments</param>
protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
{
try
{
// Single instance is already ensured by AppInstance.FindOrRegisterForKey() in Program.cs
// PID is already parsed in Program.cs and passed to constructor
2025-10-20 16:22:47 +08:00
// Set up Windows Events monitoring (Awake pattern)
2025-11-17 18:21:46 +08:00
// Note: PowerDisplay.exe should NOT listen to RefreshMonitorsEvent
// That event is sent BY PowerDisplay TO Settings UI for one-way notification
RegisterWindowEvent(Constants.ShowPowerDisplayEvent(), mw => mw.ShowWindow(), "Show");
RegisterWindowEvent(Constants.TogglePowerDisplayEvent(), mw => mw.ToggleWindow(), "Toggle");
RegisterEvent(Constants.TerminatePowerDisplayEvent(), () => Environment.Exit(0), "Terminate");
RegisterViewModelEvent(
Constants.SettingsUpdatedPowerDisplayEvent(),
vm =>
{
vm.ApplySettingsFromUI();
// Refresh tray icon based on updated settings
_trayIconService?.SetupTrayIcon();
},
"SettingsUpdated");
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)
2025-10-20 16:22:47 +08:00
{
Logger.LogInfo($"PowerDisplay started from PowerToys Runner. Runner pid={_powerToysRunnerPid}");
RunnerHelper.WaitForPowerToysRunner(_powerToysRunnerPid, () =>
{
Logger.LogInfo("PowerToys Runner exited. Exiting PowerDisplay");
Environment.Exit(0);
});
2025-10-20 16:22:47 +08:00
}
else
{
Logger.LogInfo("PowerDisplay started in standalone mode");
2025-10-20 16:22:47 +08:00
}
// Create main window
2025-10-20 16:22:47 +08:00
_mainWindow = new MainWindow();
// Initialize tray icon service
_trayIconService = new TrayIconService(
_settingsUtils,
ToggleMainWindow,
() => Environment.Exit(0),
OpenSettings);
_trayIconService.SetupTrayIcon();
// Window visibility depends on launch mode
bool isStandaloneMode = _powerToysRunnerPid <= 0;
if (isStandaloneMode)
{
// Standalone mode - activate and show window immediately
_mainWindow.Activate();
Logger.LogInfo("Window activated (standalone mode)");
// Signal ready immediately in standalone mode
SignalProcessReady();
}
else
{
// PowerToys mode - window remains hidden until show event received
Logger.LogInfo("Window created, waiting for show event (PowerToys mode)");
// Start background initialization to scan monitors even when hidden
// Signal process ready AFTER initialization completes to prevent race condition
_ = Task.Run(async () =>
{
// Give window a moment to finish construction
await Task.Delay(100);
// Trigger initialization on UI thread and wait for completion
var initComplete = new TaskCompletionSource<bool>();
_mainWindow?.DispatcherQueue.TryEnqueue(async () =>
{
try
{
if (_mainWindow is MainWindow mainWindow)
{
await mainWindow.EnsureInitializedAsync();
Logger.LogInfo("Background initialization completed");
}
initComplete.SetResult(true);
}
catch (Exception ex)
{
Logger.LogError($"Background initialization failed: {ex.Message}");
initComplete.SetResult(false);
}
});
// Wait for initialization to complete before signaling ready
await initComplete.Task;
// NOW signal that process is ready to receive events
// This ensures window is fully initialized before C++ module can send Toggle/Show events
SignalProcessReady();
Logger.LogInfo("Process ready signal sent after initialization");
});
}
2025-10-20 16:22:47 +08:00
}
catch (Exception ex)
{
Logger.LogError("PowerDisplay startup failed", ex);
2025-10-20 16:22:47 +08:00
}
}
/// <summary>
/// Register a simple event handler (no window access needed)
/// </summary>
private void RegisterEvent(string eventName, Action action, string logName)
{
NativeEventWaiter.WaitForEventLoop(
eventName,
() =>
{
Logger.LogInfo($"[EVENT] {logName} event received");
action();
},
CancellationToken.None);
}
/// <summary>
/// Register an event handler that operates on MainWindow directly
/// NativeEventWaiter already marshals to UI thread
/// </summary>
private void RegisterWindowEvent(string eventName, Action<MainWindow> action, string logName)
{
NativeEventWaiter.WaitForEventLoop(
eventName,
() =>
{
Logger.LogInfo($"[EVENT] {logName} event received");
if (_mainWindow is MainWindow mainWindow)
{
action(mainWindow);
}
else
{
Logger.LogError($"[EVENT] _mainWindow type mismatch for {logName}");
}
},
CancellationToken.None);
}
/// <summary>
/// Register an event handler that operates on ViewModel via DispatcherQueue
/// Used for Settings UI IPC events that need ViewModel access
/// </summary>
private void RegisterViewModelEvent(string eventName, Action<ViewModels.MainViewModel> action, string logName)
{
NativeEventWaiter.WaitForEventLoop(
eventName,
() =>
{
Logger.LogInfo($"[EVENT] {logName} event received");
_mainWindow?.DispatcherQueue.TryEnqueue(() =>
{
if (_mainWindow is MainWindow mainWindow && mainWindow.ViewModel != null)
{
action(mainWindow.ViewModel);
}
});
},
CancellationToken.None);
}
2025-10-20 16:22:47 +08:00
/// <summary>
/// Gets the main window instance
/// </summary>
public Window? MainWindow => _mainWindow;
/// <summary>
/// Show the main window
/// </summary>
private void ShowMainWindow()
{
if (_mainWindow is MainWindow mainWindow)
{
mainWindow.ShowWindow();
}
}
/// <summary>
/// Toggle the main window visibility
/// </summary>
private void ToggleMainWindow()
{
if (_mainWindow is MainWindow mainWindow)
{
mainWindow.ToggleWindow();
}
}
/// <summary>
/// Open PowerDisplay settings in PowerToys Settings UI
/// </summary>
private void OpenSettings()
{
// mainExecutableIsOnTheParentFolder = true because PowerDisplay is a WinUI 3 app
// deployed in a subfolder (PowerDisplay\) while PowerToys.exe is in the parent folder
SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.PowerDisplay, true);
}
/// <summary>
/// Refresh tray icon based on current settings
/// </summary>
public void RefreshTrayIcon()
{
_trayIconService?.SetupTrayIcon();
}
2025-10-20 16:22:47 +08:00
/// <summary>
/// Check if running standalone (not launched from PowerToys Runner)
/// </summary>
public bool IsRunningDetachedFromPowerToys()
{
return _powerToysRunnerPid == -1;
}
/// <summary>
/// Shutdown application (Awake pattern - simple and clean)
2025-10-20 16:22:47 +08:00
/// </summary>
public void Shutdown()
{
Logger.LogInfo("PowerDisplay shutting down");
_trayIconService?.Destroy();
Environment.Exit(0);
2025-10-20 16:22:47 +08:00
}
2025-12-01 06:09:26 +08:00
/// <summary>
/// Signal that PowerDisplay process is ready to receive events.
/// Uses a ManualReset event so the C++ module can wait on it.
/// </summary>
private static void SignalProcessReady()
{
try
{
using var readyEvent = new EventWaitHandle(
false,
EventResetMode.ManualReset,
ProcessReadyEventName);
readyEvent.Set();
Logger.LogInfo("Signaled process ready event");
}
catch (Exception ex)
{
Logger.LogError($"Failed to signal process ready event: {ex.Message}");
}
}
2025-10-20 16:22:47 +08:00
}
}