Files
LanMountainDesktop/LanMountainDesktop/App.axaml.cs
2026-05-17 19:36:07 +08:00

1950 lines
67 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core;
using Avalonia.Data.Core.Plugins;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Styling;
using Avalonia.Threading;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.DesktopHost;
using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.ExternalIpc;
using LanMountainDesktop.Services.Loading;
using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.Services.Update;
using LanMountainDesktop.Shared.Contracts.Launcher;
using LanMountainDesktop.Shared.IPC;
using LanMountainDesktop.Shared.IPC.Abstractions.Services;
using LanMountainDesktop.Theme;
using LanMountainDesktop.ViewModels;
using LanMountainDesktop.Views;
namespace LanMountainDesktop;
public partial class App : Application
{
private static readonly Color DefaultAccentColor = Color.Parse("#FF3B82F6");
private enum DesktopShellState
{
ForegroundDesktop = 0,
MinimizedToTaskbar = 1,
TrayOnly = 2
}
private enum ShutdownIntent
{
None = 0,
ExitRequested = 1,
RestartRequested = 2
}
private readonly ISettingsFacadeService _settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
private readonly IAppearanceThemeService _appearanceThemeService = HostAppearanceThemeProvider.GetOrCreate();
private readonly IAppLogoService _appLogoService = HostAppLogoProvider.GetOrCreate();
private readonly LocalizationService _localizationService = new();
private readonly FontFamilyService _fontFamilyService = new();
private readonly IHostApplicationLifecycle _hostApplicationLifecycle = new HostApplicationLifecycleService();
private readonly HostShutdownGate _shutdownGate = new();
private readonly IDetachedComponentLibraryWindowService _detachedComponentLibraryWindowService = new DetachedComponentLibraryWindowService();
private readonly IMainWindowDesktopLayerService _mainWindowDesktopLayerService = MainWindowDesktopLayerServiceFactory.GetOrCreate();
private readonly ILocationService _locationService = HostLocationServiceProvider.GetOrCreate();
private readonly DateTimeOffset _startupAt = DateTimeOffset.UtcNow;
private readonly string _launchSource = LauncherRuntimeMetadata.GetLaunchSource(Environment.GetCommandLineArgs()) ?? "normal";
private readonly RestartPresentationMode? _requestedRestartPresentationMode =
LauncherRuntimeMetadata.GetRestartPresentationMode(Environment.GetCommandLineArgs());
private ISettingsPageRegistry? _settingsPageRegistry;
private ISettingsWindowService? _settingsWindowService;
private WeatherLocationRefreshService? _weatherLocationRefreshService;
private INotificationService? _notificationService;
private bool _exitCleanupCompleted;
private DesktopShellState _desktopShellState = DesktopShellState.ForegroundDesktop;
private ShutdownIntent _shutdownIntent;
private DesktopTrayService? _desktopTrayService;
private DispatcherTimer? _shellRecoveryTimer;
private PluginRuntimeService? _pluginRuntimeService;
private MainWindow? _mainWindow;
private TransparentOverlayWindow? _transparentOverlayWindow;
private FusedDesktopComponentLibraryWindow? _fusedComponentLibraryWindow;
private bool _isExitingFusedDesktopEditMode;
private bool _mainWindowClosed;
private DesktopShellHost? _desktopShellHost;
private PublicIpcHostService? _publicIpcHostService;
private LoadingStateManager? _loadingStateManager;
private LoadingStateReporter? _loadingStateReporter;
private int _forcedExitScheduled;
private volatile bool _desktopShellInitializationStarted;
private bool _mainWindowOpened;
private bool _trayInitialized;
private readonly object _launcherProgressLock = new();
private readonly List<StartupProgressMessage> _pendingLauncherProgressMessages = [];
internal static IHostApplicationLifecycle? CurrentHostApplicationLifecycle =>
(Current as App)?._hostApplicationLifecycle;
internal static INotificationService? CurrentNotificationService =>
(Current as App)?._notificationService;
// 闅愮鏀跨瓥鏌ョ湅浜嬩欢
public static event Action? CurrentPrivacyPolicyViewRequested;
public static void RaisePrivacyPolicyViewRequested()
{
CurrentPrivacyPolicyViewRequested?.Invoke();
}
public PluginRuntimeService? PluginRuntimeService => _pluginRuntimeService;
public ISettingsFacadeService SettingsFacade => _settingsFacade;
public IHostApplicationLifecycle HostApplicationLifecycle => _hostApplicationLifecycle;
internal ISettingsWindowService? SettingsWindowService => _settingsWindowService;
internal INotificationService? NotificationService => _notificationService;
internal bool IsShutdownInProgress => _shutdownGate.IsShutdownRequested || _shutdownIntent != ShutdownIntent.None;
internal RestartPresentationMode GetCurrentRestartPresentationMode()
{
return _desktopShellState switch
{
DesktopShellState.TrayOnly => RestartPresentationMode.Tray,
DesktopShellState.MinimizedToTaskbar => RestartPresentationMode.Minimized,
_ => RestartPresentationMode.Foreground
};
}
internal void OpenIndependentSettingsModule(string source, string? pageTag = null)
{
if (IsShutdownInProgress)
{
AppLogger.Info(
"SettingsFacade",
$"Settings open ignored because shutdown is in progress. Source='{source}'; PageTag='{pageTag ?? "<default>"}'.");
return;
}
EnsureSettingsWindowService();
AppLogger.Info(
"SettingsFacade",
$"Opening settings window. Source='{source}'; PageTag='{pageTag ?? "<default>"}'.");
_settingsWindowService?.Open(new SettingsWindowOpenRequest(
Source: source,
PageId: pageTag,
ScreenReferenceWindow: _mainWindow is { IsVisible: true } ? _mainWindow : null));
}
public App()
{
if (Design.IsDesignMode)
{
return;
}
_settingsFacade.Settings.Changed += OnSettingsChanged;
_appearanceThemeService.Changed += OnAppearanceThemeChanged;
// 监听系统主题变化
PropertyChanged += OnAppPropertyChanged;
}
private void OnAppPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == ActualThemeVariantProperty)
{
// 系统主题变化时,检查是否需要更新
var themeMode = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App).ThemeMode;
if (string.Equals(themeMode, ThemeAppearanceValues.ThemeModeFollowSystem, StringComparison.OrdinalIgnoreCase))
{
var newThemeVariant = (ThemeVariant?)e.NewValue;
var isDark = newThemeVariant == ThemeVariant.Dark;
// 同步到设置
var currentThemeState = _settingsFacade.Theme.Get();
if (currentThemeState.IsNightMode != isDark)
{
_settingsFacade.Theme.Save(currentThemeState with { IsNightMode = isDark });
}
// 应用主题
Dispatcher.UIThread.Post(() =>
{
ApplyThemeFromSettings();
RefreshTrayIconContent();
}, DispatcherPriority.Background);
}
}
}
public override void Initialize()
{
AppLogger.Info("App", "Initializing application resources.");
AvaloniaXamlLoader.Load(this);
if (Design.IsDesignMode)
{
ApplyDesignTimeTheme();
return;
}
ApplyThemeFromSettings();
ApplyCurrentCultureFromSettings();
EnsureSettingsWindowService();
EnsureWeatherLocationRefreshService();
EnsureNotificationService();
}
public override void OnFrameworkInitializationCompleted()
{
if (Design.IsDesignMode)
{
base.OnFrameworkInitializationCompleted();
return;
}
AppLogger.Info("App", "Framework initialization completed.");
LinuxDesktopEntryInstaller.EnsureInstalled();
InitializePublicIpc();
_ = InitializeLauncherIpcAsync();
DesktopBootstrap.InitializeApplication(this, InitializeDesktopShell);
if (!Design.IsDesignMode && OperatingSystem.IsWindows())
{
FusedDesktopManagerServiceFactory.GetOrCreate().Initialize();
}
base.OnFrameworkInitializationCompleted();
}
private async Task InitializeLauncherIpcAsync()
{
if (_loadingStateManager is not null)
return;
try
{
bool hadBufferedMessages;
lock (_launcherProgressLock)
{
hadBufferedMessages = _pendingLauncherProgressMessages.Count > 0;
}
_loadingStateManager = new LoadingStateManager();
_loadingStateReporter = new LoadingStateReporter(_loadingStateManager, _publicIpcHostService);
_loadingStateReporter.Start();
_loadingStateManager.RegisterItem("system.init", LoadingItemType.System, "System Initialization", "Initialize core application services.");
_loadingStateManager.StartItem("system.init", "Public IPC host ready.");
await FlushPendingLauncherProgressAsync();
if (!hadBufferedMessages)
{
ReportStartupProgress(StartupStage.Initializing, 10, "Initializing application...");
ReportStartupProgress(StartupStage.LoadingSettings, 20, "Loading settings...");
}
}
catch (Exception ex)
{
AppLogger.Warn("LauncherIpc", $"Failed to initialize Launcher IPC: {ex.Message}");
}
}
private void ReportStartupProgress(StartupStage stage, int percent, string message)
{
QueueOrSendLauncherProgress(new StartupProgressMessage
{
Stage = stage,
ProgressPercent = percent,
Message = message,
Timestamp = DateTimeOffset.UtcNow
}, logSuccess: false);
}
private void ReportStartupProgressSync(StartupStage stage, int percent, string message)
{
QueueOrSendLauncherProgress(new StartupProgressMessage
{
Stage = stage,
ProgressPercent = percent,
Message = message,
Timestamp = DateTimeOffset.UtcNow
}, logSuccess: true);
}
private void QueueOrSendLauncherProgress(StartupProgressMessage message, bool logSuccess)
{
var publicIpcHostService = _publicIpcHostService;
if (publicIpcHostService is null)
{
lock (_launcherProgressLock)
{
_pendingLauncherProgressMessages.Add(message);
}
AppLogger.Info("LauncherIpc", $"Buffered launcher stage '{message.Stage}' because IPC is not connected yet.");
return;
}
_ = SendLauncherProgressAsync(publicIpcHostService, message, logSuccess);
}
private async Task FlushPendingLauncherProgressAsync()
{
var publicIpcHostService = _publicIpcHostService;
if (publicIpcHostService is null)
{
return;
}
StartupProgressMessage[] pendingMessages;
lock (_launcherProgressLock)
{
pendingMessages = _pendingLauncherProgressMessages.ToArray();
_pendingLauncherProgressMessages.Clear();
}
foreach (var pendingMessage in pendingMessages)
{
await SendLauncherProgressAsync(publicIpcHostService, pendingMessage, logSuccess: false);
}
}
private async Task SendLauncherProgressAsync(PublicIpcHostService publicIpcHostService, StartupProgressMessage message, bool logSuccess)
{
try
{
await publicIpcHostService.PublishStartupProgressAsync(message);
if (logSuccess)
{
AppLogger.Info("LauncherIpc", $"Successfully reported stage: {message.Stage}");
}
}
catch (Exception ex)
{
AppLogger.Warn("LauncherIpc", $"Failed to report progress: {ex.Message}");
lock (_launcherProgressLock)
{
_pendingLauncherProgressMessages.Add(message);
}
}
}
private void ApplyDesignTimeTheme()
{
RequestedThemeVariant = ThemeVariant.Light;
try
{
ApplyAdaptiveThemeResources();
}
catch (Exception ex)
{
AppLogger.Warn("Previewer", "Failed to apply adaptive theme resources in design mode.", ex);
}
}
private void InitializeDesktopShell()
{
_desktopShellInitializationStarted = true;
_desktopShellHost ??= new DesktopShellHost(
InitializePluginRuntime,
InitializeTrayIcon,
desktop =>
{
// Avoid duplicate validations from both Avalonia and the CommunityToolkit.
// More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins
DisableAvaloniaDataAnnotationValidation();
desktop.ShutdownMode = Avalonia.Controls.ShutdownMode.OnExplicitShutdown;
ReportStartupProgress(StartupStage.InitializingUI, 60, "姝e湪鍒濆鍖栫晫闈?..");
CreateAndAssignMainWindow(desktop, "FrameworkInitialization");
},
OnDesktopLifetimeExit,
static () => { },
StartWeatherLocationRefreshIfNeeded);
_desktopShellHost.Initialize(this);
}
private void OnDesktopLifetimeExit()
{
AppLogger.Info("App", "Desktop lifetime exit triggered.");
PerformExitCleanup();
ScheduleForcedProcessTermination("DesktopLifetimeExit");
}
private void OnTrayExitClick(object? sender, EventArgs e)
{
_ = _hostApplicationLifecycle.TryExit(new HostApplicationLifecycleRequest(
Source: "TrayMenu",
Reason: "User selected Exit App from the tray menu."));
}
private void OnTrayShowDesktopClick(object? sender, EventArgs e)
{
if (IsShutdownInProgress)
{
AppLogger.Info("DesktopShell", "Tray Open Desktop ignored because shutdown is in progress.");
return;
}
RestoreOrCreateMainWindow("TrayMenu");
}
private void OnTrayRestartClick(object? sender, EventArgs e)
{
if (IsShutdownInProgress)
{
AppLogger.Info("HostLifecycle", "Tray Restart ignored because shutdown is already in progress.");
return;
}
_ = _hostApplicationLifecycle.TryRestart(new HostApplicationLifecycleRequest(
Source: "TrayMenu",
Reason: "User selected Restart App from the tray menu."));
}
private void OnTraySettingsClick(object? sender, EventArgs e)
{
_ = sender;
_ = e;
if (IsShutdownInProgress)
{
AppLogger.Info("SettingsFacade", "Tray Settings ignored because shutdown is in progress.");
return;
}
OpenIndependentSettingsModule("TrayMenu");
}
private void OnTrayComponentLibraryClick(object? sender, EventArgs e)
{
_ = sender;
_ = e;
if (IsShutdownInProgress)
{
AppLogger.Info("FusedDesktop", "Tray Component Library ignored because shutdown is in progress.");
return;
}
if (!OperatingSystem.IsWindows())
{
AppLogger.Warn("FusedDesktop", "Fused desktop is only supported on Windows.");
return;
}
Dispatcher.UIThread.Post(
() => OpenFusedDesktopComponentLibraryFromUi(centerInWorkArea: false),
DispatcherPriority.Send);
}
private void OpenFusedDesktopComponentLibraryFromUi(bool centerInWorkArea)
{
if (IsShutdownInProgress)
{
AppLogger.Info("FusedDesktop", "Deferred Component Library open ignored because shutdown is in progress.");
return;
}
try
{
var fusedDesktopManager = FusedDesktopManagerServiceFactory.GetOrCreate();
fusedDesktopManager.EnterEditMode();
EnsureTransparentOverlayWindow();
if (_transparentOverlayWindow is not null && !_transparentOverlayWindow.IsVisible)
{
_transparentOverlayWindow.Show();
}
if (_fusedComponentLibraryWindow is { } existingWindow)
{
if (_transparentOverlayWindow is not null)
{
existingWindow.SetOverlayWindow(_transparentOverlayWindow);
}
if (!existingWindow.IsVisible)
{
existingWindow.Show();
}
if (centerInWorkArea)
{
existingWindow.CenterInWorkArea(_transparentOverlayWindow);
}
existingWindow.Activate();
return;
}
var window = new FusedDesktopComponentLibraryWindow();
_fusedComponentLibraryWindow = window;
if (_transparentOverlayWindow is not null)
{
window.SetOverlayWindow(_transparentOverlayWindow);
}
window.Closed += OnFusedComponentLibraryWindowClosed;
window.Show();
if (centerInWorkArea)
{
window.CenterInWorkArea(_transparentOverlayWindow);
}
window.Activate();
}
catch (Exception ex)
{
AppLogger.Warn("FusedDesktop", "Failed to open fused desktop component library.", ex);
ExitFusedDesktopEditModeFromUi(closeLibrary: true);
}
}
private void OnFusedComponentLibraryWindowClosed(object? sender, EventArgs e)
{
if (sender is not FusedDesktopComponentLibraryWindow window)
{
return;
}
window.Closed -= OnFusedComponentLibraryWindowClosed;
if (ReferenceEquals(_fusedComponentLibraryWindow, window))
{
_fusedComponentLibraryWindow = null;
}
if (!window.PreserveEditModeOnClose && !_isExitingFusedDesktopEditMode)
{
ExitFusedDesktopEditModeFromUi(closeLibrary: false);
}
}
private void ExitFusedDesktopEditModeFromUi(bool closeLibrary)
{
if (_isExitingFusedDesktopEditMode)
{
return;
}
_isExitingFusedDesktopEditMode = true;
try
{
if (closeLibrary && _fusedComponentLibraryWindow is { } libraryWindow)
{
_fusedComponentLibraryWindow = null;
libraryWindow.Closed -= OnFusedComponentLibraryWindowClosed;
libraryWindow.Close();
}
try
{
_transparentOverlayWindow?.SaveLayoutAndHide();
}
catch (Exception overlayEx)
{
AppLogger.Warn("FusedDesktop", "Failed to hide fused desktop overlay.", overlayEx);
}
try
{
FusedDesktopManagerServiceFactory.GetOrCreate().ExitEditMode();
}
catch (Exception exitEx)
{
AppLogger.Warn("FusedDesktop", "Failed to exit fused desktop edit mode.", exitEx);
}
}
finally
{
_isExitingFusedDesktopEditMode = false;
}
}
private void DisableAvaloniaDataAnnotationValidation()
{
// Avalonia 12 中 BindingPlugins 已移除,数据验证插件不再需要手动禁用
// 编译型绑定默认开启,数据注解验证行为已改变
}
private void InitializePluginRuntime()
{
ReportStartupProgress(StartupStage.LoadingPlugins, 30, "姝e湪鍔犺浇鎻掍欢...");
try
{
_pluginRuntimeService?.Dispose();
_pluginRuntimeService = new PluginRuntimeService(_settingsFacade, _publicIpcHostService);
HostSettingsFacadeProvider.BindPluginRuntime(_pluginRuntimeService);
_pluginRuntimeService.LoadInstalledPlugins();
}
catch (Exception ex)
{
AppLogger.Warn("PluginRuntime", "Failed to initialize plugin runtime.", ex);
}
}
private void InitializeTrayIcon()
{
EnsureDesktopTrayService();
_desktopTrayService?.StartWatchdog();
_trayInitialized = _desktopTrayService?.EnsureReady("Startup") == true;
if (_trayInitialized)
{
ReportStartupProgress(StartupStage.TrayReady, 75, "Tray ready.");
AppLogger.Info("TrayIcon", $"Tray initialized successfully. Pid={Environment.ProcessId}.");
return;
}
AppLogger.Warn("TrayIcon", "Tray initialization did not reach the ready state.");
}
private void RefreshTrayIconContent()
{
EnsureDesktopTrayService();
_desktopTrayService?.Refresh("RefreshTrayContent");
_trayInitialized = _desktopTrayService?.IsReady == true;
}
private void RefreshFusedDesktopMenuItemVisibility()
{
RefreshTrayIconContent();
}
private void DisposeTrayIcon()
{
_desktopTrayService?.Dispose();
_trayInitialized = false;
}
private void EnsureDesktopTrayService()
{
if (_desktopTrayService is not null)
{
return;
}
_desktopTrayService = new DesktopTrayService(
this,
_appLogoService,
L,
ShouldShowTrayComponentLibraryMenuItem,
OnTrayShowDesktopClick,
OnTraySettingsClick,
OnTrayComponentLibraryClick,
OnTrayRestartClick,
OnTrayExitClick);
_desktopTrayService.StateChanged += OnTrayAvailabilityStateChanged;
_desktopTrayService.StartWatchdog();
EnsureShellRecoveryWatchdog();
}
private void EnsureShellRecoveryWatchdog()
{
_shellRecoveryTimer ??= new DispatcherTimer(
TimeSpan.FromSeconds(10),
DispatcherPriority.Background,
OnShellRecoveryWatchdogTick);
if (!_shellRecoveryTimer.IsEnabled)
{
_shellRecoveryTimer.Start();
}
}
private void StopShellRecoveryWatchdog()
{
if (_shellRecoveryTimer?.IsEnabled == true)
{
_shellRecoveryTimer.Stop();
}
}
private void OnShellRecoveryWatchdogTick(object? sender, EventArgs e)
{
_ = sender;
_ = e;
if (_shutdownIntent != ShutdownIntent.None)
{
return;
}
EnsureTrayReady("ShellRecoveryWatchdog");
if (!ShouldShowMainWindowInTaskbar())
{
return;
}
if (_desktopShellState != DesktopShellState.ForegroundDesktop)
{
EnsureTaskbarEntry("ShellRecoveryWatchdog");
return;
}
if (_mainWindow is not null && _mainWindow.IsVisible && !_mainWindow.ShowInTaskbar)
{
_mainWindow.ShowInTaskbar = true;
}
}
private bool EnsureTrayReady(string reason)
{
EnsureDesktopTrayService();
var wasReady = _trayInitialized;
var ready = _desktopTrayService?.EnsureReady(reason) == true;
_trayInitialized = ready;
if (ready && !wasReady)
{
ReportStartupProgress(StartupStage.TrayReady, 75, "Tray ready.");
}
return ready;
}
private void OnTrayAvailabilityStateChanged(TrayAvailabilityState state)
{
_trayInitialized = state == TrayAvailabilityState.Ready;
if (state != TrayAvailabilityState.Failed)
{
return;
}
if (_desktopShellState == DesktopShellState.TrayOnly)
{
RestoreOrCreateMainWindow("TrayAvailabilityFailed");
return;
}
var foregroundVisible = _mainWindow?.IsVisible == true &&
_mainWindow.WindowState != WindowState.Minimized;
var taskbarUsable = BuildPublicTaskbarStatus().IsUsable;
if (!foregroundVisible &&
!taskbarUsable &&
(_desktopTrayService?.ConsecutiveRecoveryFailures ?? 0) >= 3)
{
RestoreOrCreateMainWindow("TrayAvailabilityRepeatedFailure");
}
}
private bool ShouldShowTrayComponentLibraryMenuItem()
{
if (!OperatingSystem.IsWindows())
{
return false;
}
var appSnapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
return appSnapshot.EnableFusedDesktop;
}
private void EnsureSettingsWindowService()
{
_settingsPageRegistry ??= new SettingsPageRegistry(
_settingsFacade,
_hostApplicationLifecycle,
_localizationService,
() => _pluginRuntimeService);
_settingsWindowService ??= new SettingsWindowService(
_settingsPageRegistry,
_hostApplicationLifecycle,
_settingsFacade);
}
private void EnsureWeatherLocationRefreshService()
{
_weatherLocationRefreshService ??= new WeatherLocationRefreshService(
_settingsFacade,
_locationService,
_localizationService);
}
private void EnsureNotificationService()
{
_notificationService ??= new NotificationService(_appearanceThemeService);
}
private void StartWeatherLocationRefreshIfNeeded()
{
EnsureWeatherLocationRefreshService();
if (_weatherLocationRefreshService is null)
{
return;
}
_ = Task.Run(async () =>
{
try
{
await _weatherLocationRefreshService.TryRefreshOnStartupAsync();
}
catch (Exception ex)
{
AppLogger.Warn("Weather.Location", "Failed to refresh weather location during startup.", ex);
}
});
}
private void ApplyThemeFromSettings()
{
var snapshot = _appearanceThemeService.GetCurrent();
var themeMode = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App).ThemeMode;
// 处理跟随系统主题模式
if (string.Equals(themeMode, ThemeAppearanceValues.ThemeModeFollowSystem, StringComparison.OrdinalIgnoreCase))
{
// 使用 Avalonia 的系统主题检测
var systemTheme = ActualThemeVariant;
RequestedThemeVariant = systemTheme;
// 同步 IsNightMode 到设置
var isSystemDark = systemTheme == ThemeVariant.Dark;
var currentThemeState = _settingsFacade.Theme.Get();
if (currentThemeState.IsNightMode != isSystemDark)
{
_settingsFacade.Theme.Save(currentThemeState with { IsNightMode = isSystemDark });
}
}
else
{
RequestedThemeVariant = snapshot.IsNightMode
? ThemeVariant.Dark
: ThemeVariant.Light;
}
ApplyAdaptiveThemeResources();
}
private void ApplyCurrentCultureFromSettings()
{
var snapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
var languageCode = _localizationService.NormalizeLanguageCode(snapshot.LanguageCode);
CultureInfo culture;
try
{
culture = CultureInfo.GetCultureInfo(languageCode);
}
catch (CultureNotFoundException)
{
culture = CultureInfo.GetCultureInfo("zh-CN");
}
CultureInfo.DefaultThreadCurrentCulture = culture;
CultureInfo.DefaultThreadCurrentUICulture = culture;
Thread.CurrentThread.CurrentCulture = culture;
Thread.CurrentThread.CurrentUICulture = culture;
ApplyLanguageSpecificFont(languageCode);
}
private void ApplyLanguageSpecificFont(string languageCode)
{
var fontFamily = _fontFamilyService.GetFontFamilyForLanguage(languageCode);
if (Resources.TryGetValue("AppFontFamily", out var currentFont) &&
currentFont is FontFamily currentFontFamily &&
currentFontFamily.Name == fontFamily.Name)
{
return;
}
Resources["AppFontFamily"] = fontFamily;
}
private void RestoreOrCreateMainWindow(string source)
{
if (IsShutdownInProgress)
{
AppLogger.Info("DesktopShell", $"Restore ignored because shutdown is in progress. Source='{source}'.");
return;
}
Dispatcher.UIThread.Post(() =>
{
_ = RestoreOrCreateMainWindowCore(source);
}, DispatcherPriority.Send);
}
private bool RestoreOrCreateMainWindowCore(string source)
{
if (IsShutdownInProgress)
{
AppLogger.Info("DesktopShell", $"Restore skipped because shutdown is in progress. Source='{source}'.");
return false;
}
if (ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
{
AppLogger.Warn("DesktopShell", $"Restore skipped because desktop lifetime is unavailable. Source='{source}'.");
return false;
}
try
{
AppLogger.Info("DesktopShell", $"Restoring desktop shell started. Source='{source}'.");
if (_transparentOverlayWindow is not null && _transparentOverlayWindow.IsVisible)
{
_transparentOverlayWindow.Hide();
}
var mainWindow = GetOrCreateMainWindow(desktop, source);
mainWindow.PrepareEnterAnimation();
mainWindow.ShowInTaskbar = ShouldShowMainWindowInTaskbar() && !IsMainWindowDesktopLayerEnabled();
if (!mainWindow.IsVisible)
{
mainWindow.Show();
}
if (mainWindow.WindowState == WindowState.Minimized)
{
mainWindow.WindowState = WindowState.Normal;
}
mainWindow.EnsureForegroundWindowLayout();
if (mainWindow.ShouldUseFullscreenWindow() &&
mainWindow.WindowState != WindowState.FullScreen)
{
mainWindow.WindowState = WindowState.FullScreen;
}
ActivateOrRefreshMainWindowLayer(mainWindow, $"Restore:{source}");
Dispatcher.UIThread.Post(() =>
{
mainWindow.PlayEnterAnimation();
}, DispatcherPriority.Background);
SetDesktopShellState(DesktopShellState.ForegroundDesktop, $"Restore:{source}");
AppLogger.Info(
"DesktopShell",
$"Desktop restored. Source='{source}'; MainWindowClosed={_mainWindowClosed}; WindowState='{mainWindow.WindowState}'.");
return true;
}
catch (Exception ex)
{
AppLogger.Warn("DesktopShell", $"Failed to restore desktop shell. Source='{source}'.", ex);
return false;
}
}
private void EnsureTransparentOverlayWindow()
{
if (_transparentOverlayWindow is null)
{
_transparentOverlayWindow = new TransparentOverlayWindow();
_transparentOverlayWindow.RestoreMainWindowRequested += (s, e) =>
{
RestoreOrCreateMainWindow("TransparentOverlay");
};
_transparentOverlayWindow.ExitEditRequested += (s, e) =>
{
ExitFusedDesktopEditModeFromUi(closeLibrary: true);
};
_transparentOverlayWindow.RestoreComponentLibraryRequested += (s, e) =>
{
OpenFusedDesktopComponentLibraryFromUi(centerInWorkArea: true);
};
}
}
internal bool TrySubmitShutdown(HostShutdownMode mode, HostApplicationLifecycleRequest? request)
{
if (ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
{
AppLogger.Warn(
"HostLifecycle",
$"Shutdown request ignored because desktop lifetime is unavailable. Mode='{mode}'; Source='{request?.Source ?? "Unknown"}'.");
return false;
}
return Dispatcher.UIThread.CheckAccess()
? TrySubmitShutdownCore(mode, request, desktop)
: Dispatcher.UIThread.InvokeAsync(
() => TrySubmitShutdownCore(mode, request, desktop),
DispatcherPriority.Send).GetAwaiter().GetResult();
}
private bool TrySubmitShutdownCore(
HostShutdownMode mode,
HostApplicationLifecycleRequest? request,
IClassicDesktopStyleApplicationLifetime desktop)
{
var source = request?.Source ?? "Unknown";
var submission = _shutdownGate.Submit(mode);
if (!submission.IsFirstSubmission)
{
AppLogger.Warn(
"HostLifecycle",
$"Shutdown request ignored because shutdown is already in progress. Requested='{submission.RequestedMode}'; Effective='{submission.EffectiveMode}'; Source='{source}'.");
return submission.Accepted;
}
_shutdownIntent = mode == HostShutdownMode.Restart
? ShutdownIntent.RestartRequested
: ShutdownIntent.ExitRequested;
AppLogger.Info(
"DesktopShell",
$"Shutdown committed. Intent='{_shutdownIntent}'; Source='{source}'; Reason='{request?.Reason ?? string.Empty}'; CurrentShellState='{_desktopShellState}'.");
ScheduleForcedProcessTermination($"ShutdownRequest:{source}");
StopShellRecoveryWatchdog();
PerformExitCleanup();
try
{
desktop.Shutdown();
return true;
}
catch (Exception ex)
{
AppLogger.Warn("HostLifecycle", $"Desktop lifetime shutdown failed. Source='{source}'.", ex);
return true;
}
}
internal void PrepareForShutdown(bool isRestart, string source)
{
void Mark()
{
_shutdownIntent = isRestart
? ShutdownIntent.RestartRequested
: ShutdownIntent.ExitRequested;
AppLogger.Info(
"DesktopShell",
$"Shutdown intent marked. Intent='{_shutdownIntent}'; Source='{source}'; CurrentShellState='{_desktopShellState}'.");
}
if (Dispatcher.UIThread.CheckAccess())
{
Mark();
return;
}
Dispatcher.UIThread.InvokeAsync(Mark, DispatcherPriority.Send).GetAwaiter().GetResult();
}
internal void ResetShutdownIntent(string source)
{
void Reset()
{
if (_shutdownIntent == ShutdownIntent.None)
{
return;
}
AppLogger.Warn(
"DesktopShell",
$"Shutdown intent cleared without process exit. PreviousIntent='{_shutdownIntent}'; Source='{source}'.");
_shutdownIntent = ShutdownIntent.None;
}
if (Dispatcher.UIThread.CheckAccess())
{
Reset();
return;
}
Dispatcher.UIThread.InvokeAsync(Reset, DispatcherPriority.Send).GetAwaiter().GetResult();
}
private void OnSettingsChanged(object? sender, SettingsChangedEvent e)
{
_ = sender;
if (e.Scope != SettingsScope.App)
{
return;
}
Dispatcher.UIThread.Post(() =>
{
var changedKeys = e.ChangedKeys?.ToArray();
var refreshAll = changedKeys is null || changedKeys.Length == 0;
var liveAppearance = _appearanceThemeService.GetCurrent();
var themeChanged =
refreshAll ||
changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), StringComparer.OrdinalIgnoreCase) ||
changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeMode), StringComparer.OrdinalIgnoreCase) ||
changedKeys.Contains(nameof(AppSettingsSnapshot.UseSystemChrome), StringComparer.OrdinalIgnoreCase) ||
changedKeys.Contains(nameof(AppSettingsSnapshot.CornerRadiusStyle), StringComparer.OrdinalIgnoreCase) ||
(string.Equals(liveAppearance.ThemeColorMode, ThemeAppearanceValues.ColorModeSeedMonet, StringComparison.OrdinalIgnoreCase) &&
changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColor), StringComparer.OrdinalIgnoreCase)) ||
(string.Equals(liveAppearance.ThemeColorMode, ThemeAppearanceValues.ColorModeWallpaperMonet, StringComparison.OrdinalIgnoreCase) &&
(changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperPath), StringComparer.OrdinalIgnoreCase) ||
changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperType), StringComparer.OrdinalIgnoreCase) ||
changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperColor), StringComparer.OrdinalIgnoreCase) ||
changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeWallpaperColorSource), StringComparer.OrdinalIgnoreCase) ||
changedKeys.Contains(nameof(AppSettingsSnapshot.UseNativeWallpaperChangeEvents), StringComparer.OrdinalIgnoreCase) ||
changedKeys.Contains(nameof(AppSettingsSnapshot.SystemWallpaperRefreshIntervalSeconds), StringComparer.OrdinalIgnoreCase)));
var languageChanged =
refreshAll ||
changedKeys.Contains(nameof(AppSettingsSnapshot.LanguageCode), StringComparer.OrdinalIgnoreCase);
if (themeChanged)
{
ApplyThemeFromSettings();
RefreshTrayIconContent();
}
if (languageChanged)
{
// 娓呴櫎鏈湴鍖栫紦瀛橈紝寮哄埗閲嶆柊鍔犺浇璇█鏂囦欢
_localizationService.ClearCache();
ApplyCurrentCultureFromSettings();
RefreshTrayIconContent();
}
var fusedDesktopChanged =
refreshAll ||
changedKeys.Contains(nameof(AppSettingsSnapshot.EnableFusedDesktop), StringComparer.OrdinalIgnoreCase);
if (fusedDesktopChanged)
{
ApplyFusedDesktopRuntimeState();
RefreshFusedDesktopMenuItemVisibility();
}
var mainWindowDesktopLayerChanged =
refreshAll ||
changedKeys.Contains(nameof(AppSettingsSnapshot.EnableMainWindowDesktopLayer), StringComparer.OrdinalIgnoreCase);
if (mainWindowDesktopLayerChanged)
{
ApplyMainWindowDesktopLayerRuntimeState("SettingsChanged");
}
var showInTaskbarChanged =
refreshAll ||
changedKeys.Contains(nameof(AppSettingsSnapshot.ShowInTaskbar), StringComparer.OrdinalIgnoreCase);
if (showInTaskbarChanged)
{
EnsureTrayReady("SettingsChanged");
if (ShouldShowMainWindowInTaskbar())
{
EnsureTaskbarEntry("SettingsChanged");
}
else if (_mainWindow is not null && _mainWindow.IsVisible)
{
_mainWindow.ShowInTaskbar = false;
}
}
}, DispatcherPriority.Background);
}
private void OnAppearanceThemeChanged(object? sender, AppearanceThemeSnapshot e)
{
_ = sender;
_ = e;
Dispatcher.UIThread.Post(() =>
{
ApplyThemeFromSettings();
RefreshTrayIconContent();
}, DispatcherPriority.Background);
}
private void ApplyAdaptiveThemeResources()
{
_appearanceThemeService.ApplyThemeResources(Resources);
}
private void ScheduleForcedProcessTermination(string source)
{
if (Interlocked.Exchange(ref _forcedExitScheduled, 1) != 0)
{
return;
}
_ = Task.Run(async () =>
{
try
{
await Task.Delay(TimeSpan.FromSeconds(8)).ConfigureAwait(false);
AppLogger.Warn(
"DesktopShell",
$"Process did not terminate after desktop exit cleanup. Forcing process exit. Source='{source}'; ShutdownIntent='{_shutdownIntent}'.");
Environment.Exit(0);
}
catch (Exception ex)
{
AppLogger.Warn("DesktopShell", $"Forced process termination scheduler failed. Source='{source}'.", ex);
}
});
}
private void PerformExitCleanup()
{
if (_exitCleanupCompleted)
{
return;
}
_exitCleanupCompleted = true;
StopShellRecoveryWatchdog();
_settingsFacade.Settings.Changed -= OnSettingsChanged;
_appearanceThemeService.Changed -= OnAppearanceThemeChanged;
try
{
TelemetryServices.Usage?.Shutdown(
_shutdownIntent == ShutdownIntent.RestartRequested,
"App.PerformExitCleanup");
}
catch (Exception ex)
{
AppLogger.Warn("Analytics", "Failed to shut down usage telemetry during exit cleanup.", ex);
}
try
{
HostUpdateOrchestratorProvider.GetOrCreate().TryApplyOnExit();
}
catch (Exception ex)
{
AppLogger.Warn("UpdateWorkflow", "Failed to apply pending update during exit cleanup.", ex);
}
try
{
_pluginRuntimeService?.Dispose();
}
catch (Exception ex)
{
AppLogger.Warn("PluginRuntime", "Failed to dispose plugin runtime during shutdown.", ex);
}
finally
{
_pluginRuntimeService = null;
}
try
{
_publicIpcHostService?.Dispose();
}
catch (Exception ex)
{
AppLogger.Warn("PublicIpc", "Failed to dispose public IPC host during shutdown.", ex);
}
finally
{
_publicIpcHostService = null;
}
_settingsWindowService?.Close();
if (_settingsPageRegistry is IDisposable disposableRegistry)
{
disposableRegistry.Dispose();
}
if (_fusedComponentLibraryWindow is not null)
{
try
{
_fusedComponentLibraryWindow.Close();
}
catch (Exception ex)
{
AppLogger.Warn("FusedDesktop", "Failed to close fused desktop component library during shutdown.", ex);
}
finally
{
_fusedComponentLibraryWindow = null;
try
{
FusedDesktopManagerServiceFactory.GetOrCreate().ExitEditMode();
}
catch (Exception ex)
{
AppLogger.Warn("FusedDesktop", "Failed to exit fused desktop edit mode during shutdown.", ex);
}
}
}
if (_transparentOverlayWindow is not null)
{
try
{
_transparentOverlayWindow.Close();
}
catch (Exception ex)
{
AppLogger.Warn("DesktopShell", "Failed to close transparent overlay during exit cleanup.", ex);
}
finally
{
_transparentOverlayWindow = null;
}
}
AudioRecorderServiceFactory.DisposeSharedServices();
StudyAnalyticsServiceFactory.DisposeSharedService();
DisposeTrayIcon();
try
{
TelemetryServices.Crash?.CaptureShutdown(
_shutdownIntent == ShutdownIntent.RestartRequested,
"App.PerformExitCleanup");
}
catch (Exception ex)
{
AppLogger.Warn("Analytics", "Failed to capture crash shutdown telemetry during exit cleanup.", ex);
}
try
{
TelemetryServices.Crash?.Dispose();
TelemetryServices.Usage?.Dispose();
}
catch (Exception ex)
{
AppLogger.Warn("Analytics", "Failed to dispose telemetry services during exit cleanup.", ex);
}
}
private MainWindow CreateAndAssignMainWindow(
IClassicDesktopStyleApplicationLifetime desktop,
string reason)
{
var mainWindow = new MainWindow
{
DataContext = new MainWindowViewModel(),
ShowInTaskbar = ShouldShowMainWindowInTaskbar() && !IsMainWindowDesktopLayerEnabled()
};
_mainWindowOpened = false;
AttachMainWindow(mainWindow);
desktop.MainWindow = mainWindow;
AppLogger.Info("App", $"Main window created. Reason='{reason}'. LogFile={AppLogger.LogFilePath}");
LogBrowserStartupDiagnostics();
SetDesktopShellState(DesktopShellState.ForegroundDesktop, $"MainWindowCreated:{reason}");
ReportStartupProgress(StartupStage.ShellInitialized, 85, "Desktop shell initialized.");
AppLogger.Info(
"App",
$"Shell initialized. Reason='{reason}'; TrayInitialized={_trayInitialized}; MainWindowVisible={mainWindow.IsVisible}.");
mainWindow.Opened += OnMainWindowOpened;
if (!mainWindow.IsVisible)
{
mainWindow.Show();
}
_ = Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromSeconds(10)).ConfigureAwait(false);
if (!_mainWindowOpened)
{
AppLogger.Warn("App", "Main window Opened event did not fire within 10 seconds. DesktopVisible was not reported.");
}
});
return mainWindow;
}
private void OnMainWindowOpened(object? sender, EventArgs e)
{
if (sender is MainWindow mainWindow)
{
mainWindow.Opened -= OnMainWindowOpened;
_mainWindowOpened = true;
ApplyMainWindowDesktopLayerRuntimeState("MainWindowOpened");
_loadingStateManager?.CompleteItem("system.init", "System initialization completed.");
if (TryApplyStartupPresentation(mainWindow))
{
AppLogger.Info(
"App",
$"Main window opened and startup presentation was applied. LaunchSource='{_launchSource}'; RestartPresentation='{_requestedRestartPresentationMode?.ToString() ?? "<none>"}'; ShellState='{_desktopShellState}'.");
ReportStartupProgressSync(StartupStage.Ready, 100, "Ready.");
_loadingStateReporter?.Stop();
return;
}
AppLogger.Info(
"App",
$"Main window opened. Reporting DesktopVisible. TrayInitialized={_trayInitialized}; ShellState='{_desktopShellState}'.");
ReportStartupProgressSync(StartupStage.DesktopVisible, 100, "Desktop visible.");
ReportStartupProgressSync(StartupStage.Ready, 100, "Ready.");
_loadingStateReporter?.Stop();
}
}
private bool TryApplyStartupPresentation(MainWindow mainWindow)
{
if (!string.Equals(_launchSource, "restart", StringComparison.OrdinalIgnoreCase) ||
_requestedRestartPresentationMode is null ||
_requestedRestartPresentationMode == RestartPresentationMode.Foreground)
{
return false;
}
switch (_requestedRestartPresentationMode)
{
case RestartPresentationMode.Minimized:
mainWindow.ShowInTaskbar = true;
mainWindow.WindowState = WindowState.Minimized;
SetDesktopShellState(DesktopShellState.MinimizedToTaskbar, "StartupRestartPresentation");
ReportStartupProgressSync(StartupStage.BackgroundReady, 95, "Background ready.");
return true;
case RestartPresentationMode.Tray:
HideMainWindowToTray(mainWindow, "StartupRestartPresentation");
return true;
default:
return false;
}
}
private MainWindow GetOrCreateMainWindow(
IClassicDesktopStyleApplicationLifetime desktop,
string reason)
{
if (_mainWindow is not null && !_mainWindowClosed)
{
return _mainWindow;
}
if (desktop.MainWindow is MainWindow desktopMainWindow && !_mainWindowClosed)
{
AttachMainWindow(desktopMainWindow);
return desktopMainWindow;
}
return CreateAndAssignMainWindow(desktop, reason);
}
private void AttachMainWindow(MainWindow mainWindow)
{
if (ReferenceEquals(_mainWindow, mainWindow))
{
_mainWindowClosed = false;
return;
}
if (_mainWindow is not null)
{
_mainWindowDesktopLayerService.Disable(_mainWindow);
_mainWindow.Closing -= OnMainWindowClosing;
_mainWindow.Closed -= OnMainWindowClosed;
_mainWindow.PropertyChanged -= OnMainWindowPropertyChanged;
}
_mainWindow = mainWindow;
_mainWindowClosed = false;
mainWindow.Closing += OnMainWindowClosing;
mainWindow.Closed += OnMainWindowClosed;
mainWindow.PropertyChanged += OnMainWindowPropertyChanged;
}
private void OnMainWindowClosing(object? sender, WindowClosingEventArgs e)
{
if (sender is not MainWindow mainWindow)
{
return;
}
AppLogger.Info(
"DesktopShell",
$"Main window closing requested. Intent='{_shutdownIntent}'; ShellState='{_desktopShellState}'; WindowState='{mainWindow.WindowState}'; IsVisible={mainWindow.IsVisible}.");
if (_shutdownIntent is ShutdownIntent.ExitRequested or ShutdownIntent.RestartRequested)
{
AppLogger.Info(
"DesktopShell",
$"Main window close allowed. Intent='{_shutdownIntent}'; ShellState='{_desktopShellState}'.");
return;
}
e.Cancel = true;
HideMainWindowToTray(mainWindow, "MainWindowClosing");
}
private void OnMainWindowClosed(object? sender, EventArgs e)
{
if (sender is not MainWindow mainWindow)
{
return;
}
mainWindow.Closing -= OnMainWindowClosing;
mainWindow.Closed -= OnMainWindowClosed;
mainWindow.PropertyChanged -= OnMainWindowPropertyChanged;
_mainWindowDesktopLayerService.Disable(mainWindow);
if (ReferenceEquals(_mainWindow, mainWindow))
{
_mainWindow = null;
}
_mainWindowClosed = true;
AppLogger.Info(
"DesktopShell",
$"Main window closed. Intent='{_shutdownIntent}'; ShellState='{_desktopShellState}'.");
if (_shutdownIntent == ShutdownIntent.None)
{
if (EnsureTrayReady("MainWindowClosedUnexpected"))
{
_desktopTrayService?.StartWatchdog();
SetDesktopShellState(DesktopShellState.TrayOnly, "MainWindowClosedUnexpected");
}
else
{
SetDesktopShellState(DesktopShellState.ForegroundDesktop, "MainWindowClosedUnexpectedWithoutTray");
}
}
}
private void OnMainWindowPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (sender is not MainWindow mainWindow)
{
return;
}
if (e.Property != Window.WindowStateProperty)
{
return;
}
if (_shutdownIntent != ShutdownIntent.None || !mainWindow.IsVisible)
{
return;
}
if (mainWindow.WindowState == WindowState.Minimized)
{
SetDesktopShellState(DesktopShellState.MinimizedToTaskbar, "MainWindowMinimized");
return;
}
SetDesktopShellState(DesktopShellState.ForegroundDesktop, "MainWindowRestored");
}
internal void HideMainWindowToTray(MainWindow mainWindow, string source)
{
try
{
if (ShouldShowMainWindowInTaskbar())
{
EnsureTrayReady($"TaskbarBackground:{source}");
mainWindow.ShowInTaskbar = true;
if (!mainWindow.IsVisible)
{
mainWindow.Show();
}
mainWindow.WindowState = WindowState.Minimized;
SetDesktopShellState(DesktopShellState.MinimizedToTaskbar, source);
ReportStartupProgress(StartupStage.BackgroundReady, 95, "Background ready via taskbar.");
AppLogger.Info(
"DesktopShell",
$"Main window minimized to taskbar because taskbar entry is enabled. Source='{source}'.");
return;
}
if (!EnsureTrayReady($"HideToTray:{source}"))
{
RecoverFromTrayUnavailable(mainWindow, source);
return;
}
mainWindow.ShowInTaskbar = false;
mainWindow.Hide();
SetDesktopShellState(DesktopShellState.TrayOnly, source);
ReportStartupProgress(StartupStage.BackgroundReady, 95, "Background ready.");
AppLogger.Info(
"DesktopShell",
$"Main window hidden to tray. Source='{source}'; WindowState='{mainWindow.WindowState}'.");
var appSnapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
if (appSnapshot.EnableThreeFingerSwipe && appSnapshot.EnableFusedDesktop)
{
EnsureTransparentOverlayWindow();
_transparentOverlayWindow?.Show();
}
}
catch (Exception ex)
{
AppLogger.Warn("DesktopShell", $"Failed to hide main window to tray. Source='{source}'.", ex);
RecoverFromTrayUnavailable(mainWindow, source);
}
}
private void RecoverFromTrayUnavailable(MainWindow mainWindow, string source)
{
AppLogger.Warn(
"DesktopShell",
$"Tray was unavailable. Recovering to a visible or taskbar-backed state instead of TrayOnly. Source='{source}'.");
var showInTaskbar = ShouldShowMainWindowInTaskbar();
if (showInTaskbar)
{
mainWindow.ShowInTaskbar = true;
if (!mainWindow.IsVisible)
{
mainWindow.Show();
}
mainWindow.WindowState = WindowState.Minimized;
SetDesktopShellState(DesktopShellState.MinimizedToTaskbar, $"TrayFallbackTaskbar:{source}");
ReportStartupProgress(StartupStage.BackgroundReady, 95, "Background ready via taskbar fallback.");
return;
}
mainWindow.ShowInTaskbar = true;
if (!mainWindow.IsVisible)
{
mainWindow.Show();
}
if (mainWindow.WindowState == WindowState.Minimized)
{
mainWindow.WindowState = WindowState.Normal;
}
mainWindow.EnsureForegroundWindowLayout();
if (mainWindow.ShouldUseFullscreenWindow() &&
mainWindow.WindowState != WindowState.FullScreen)
{
mainWindow.WindowState = WindowState.FullScreen;
}
ActivateOrRefreshMainWindowLayer(mainWindow, $"TrayFallbackForeground:{source}");
SetDesktopShellState(DesktopShellState.ForegroundDesktop, $"TrayFallbackForeground:{source}");
ReportStartupProgress(StartupStage.DesktopVisible, 100, "Desktop restored because tray was unavailable.");
}
private bool ShouldShowMainWindowInTaskbar()
{
var snapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
return snapshot.ShowInTaskbar && !snapshot.EnableMainWindowDesktopLayer;
}
private bool IsMainWindowDesktopLayerEnabled()
{
return _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App).EnableMainWindowDesktopLayer;
}
private void ActivateOrRefreshMainWindowLayer(MainWindow mainWindow, string source)
{
if (IsMainWindowDesktopLayerEnabled())
{
mainWindow.ShowInTaskbar = false;
_mainWindowDesktopLayerService.EnableOrRefresh(mainWindow);
AppLogger.Info("DesktopShell", $"Main window kept on desktop layer. Source='{source}'.");
return;
}
_mainWindowDesktopLayerService.Disable(mainWindow);
mainWindow.Activate();
mainWindow.Topmost = true;
mainWindow.Topmost = false;
}
private void ApplyMainWindowDesktopLayerRuntimeState(string source)
{
if (_mainWindow is null)
{
return;
}
if (IsMainWindowDesktopLayerEnabled())
{
ExitFusedDesktopEditModeFromUi(closeLibrary: true);
FusedDesktopManagerServiceFactory.GetOrCreate().Shutdown();
_mainWindow.ShowInTaskbar = false;
_mainWindowDesktopLayerService.EnableOrRefresh(_mainWindow);
AppLogger.Info("DesktopShell", $"Main window desktop layer enabled. Source='{source}'.");
return;
}
_mainWindowDesktopLayerService.Disable(_mainWindow);
_mainWindow.ShowInTaskbar = ShouldShowMainWindowInTaskbar();
AppLogger.Info("DesktopShell", $"Main window desktop layer disabled. Source='{source}'.");
}
private void ApplyFusedDesktopRuntimeState()
{
var snapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
try
{
if (snapshot.EnableFusedDesktop)
{
if (_mainWindow is not null)
{
_mainWindowDesktopLayerService.Disable(_mainWindow);
}
FusedDesktopManagerServiceFactory.GetOrCreate().Initialize();
return;
}
ExitFusedDesktopEditModeFromUi(closeLibrary: true);
FusedDesktopManagerServiceFactory.GetOrCreate().Shutdown();
}
catch (Exception ex)
{
AppLogger.Warn("FusedDesktop", "Failed to apply fused desktop runtime state.", ex);
}
}
private bool EnsureTaskbarEntry(string source)
{
if (IsShutdownInProgress)
{
AppLogger.Info("DesktopShell", $"Taskbar repair skipped because shutdown is in progress. Source='{source}'.");
return false;
}
if (!ShouldShowMainWindowInTaskbar())
{
return false;
}
if (ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
{
AppLogger.Warn("DesktopShell", $"Taskbar repair skipped because desktop lifetime is unavailable. Source='{source}'.");
return false;
}
try
{
var mainWindow = GetOrCreateMainWindow(desktop, $"TaskbarRepair:{source}");
mainWindow.ShowInTaskbar = true;
if (!mainWindow.IsVisible)
{
mainWindow.Show();
}
if (_desktopShellState != DesktopShellState.ForegroundDesktop)
{
mainWindow.WindowState = WindowState.Minimized;
SetDesktopShellState(DesktopShellState.MinimizedToTaskbar, $"TaskbarRepair:{source}");
ReportStartupProgress(StartupStage.BackgroundReady, 95, "Background ready via taskbar repair.");
}
AppLogger.Info(
"DesktopShell",
$"Taskbar entry ensured. Source='{source}'; IsVisible={mainWindow.IsVisible}; ShowInTaskbar={mainWindow.ShowInTaskbar}; WindowState='{mainWindow.WindowState}'.");
return true;
}
catch (Exception ex)
{
AppLogger.Warn("DesktopShell", $"Failed to ensure taskbar entry. Source='{source}'.", ex);
return false;
}
}
private void SetDesktopShellState(DesktopShellState state, string source)
{
if (_desktopShellState == state)
{
return;
}
var previous = _desktopShellState;
_desktopShellState = state;
AppLogger.Info(
"DesktopShell",
$"Shell state changed. Previous='{previous}'; Current='{state}'; Source='{source}'.");
}
private void LogBrowserStartupDiagnostics()
{
try
{
var snapshot = new DesktopLayoutSettingsService().Load();
var browserPlacements = snapshot.DesktopComponentPlacements
.Where(placement => string.Equals(
placement.ComponentId,
BuiltInComponentIds.DesktopBrowser,
StringComparison.OrdinalIgnoreCase))
.ToList();
var runtimeAvailability = WebView2RuntimeProbe.GetAvailability();
AppLogger.Info(
"StartupDiagnostics",
$"Browser component diagnostics. HasBrowserPlacement={browserPlacements.Count > 0}; " +
$"ActivePageHasBrowser={browserPlacements.Any(item => item.PageIndex == snapshot.CurrentDesktopSurfaceIndex)}; " +
$"CurrentDesktopSurfaceIndex={snapshot.CurrentDesktopSurfaceIndex}; " +
$"WebViewRuntimeAvailable={runtimeAvailability.IsAvailable}; " +
$"WebViewRuntimeVersion={runtimeAvailability.Version ?? string.Empty}; " +
$"WebViewRuntimeMessage={runtimeAvailability.Message}");
}
catch (Exception ex)
{
AppLogger.Warn("StartupDiagnostics", "Failed to log browser component diagnostics.", ex);
}
}
private string L(string key, string fallback)
{
var snapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
var languageCode = _localizationService.NormalizeLanguageCode(snapshot.LanguageCode);
return _localizationService.GetString(languageCode, key, fallback);
}
internal bool TryActivateMainWindowFromExternalIpc(string source)
{
return TryActivateMainWindowWithStatusFromExternalIpc(source).Accepted;
}
internal PublicShellActivationResult TryActivateMainWindowWithStatusFromExternalIpc(string source)
{
if (!_desktopShellInitializationStarted && _mainWindow is null)
{
return new PublicShellActivationResult(
false,
"startup_pending",
"Desktop process is running, but the shell has not started yet.",
GetPublicShellStatus());
}
var restored = RestoreOrCreateMainWindowCore(source);
var status = GetPublicShellStatus();
if (restored)
{
return new PublicShellActivationResult(true, "activated", "Desktop window activation was requested.", status);
}
if (IsShutdownInProgress)
{
return new PublicShellActivationResult(false, "shutdown_in_progress", "Desktop is shutting down.", status);
}
var code = status.PublicIpcReady && (!status.MainWindowCreated || !status.MainWindowOpened)
? "startup_pending"
: status.PublicIpcReady && !status.DesktopVisible
? "shell_not_ready"
: "activation_failed";
var message = code switch
{
"startup_pending" => "Desktop process is running, but the shell is still creating the main window.",
"shell_not_ready" => "Desktop process is running, but the shell is not ready for activation yet.",
_ => "Desktop window activation failed."
};
return new PublicShellActivationResult(false, code, message, status);
}
internal PublicTrayStatus EnsureTrayReadyFromExternalIpc(string source)
{
EnsureTrayReady($"ExternalIpc:{source}");
return BuildPublicTrayStatus();
}
internal PublicTaskbarStatus EnsureTaskbarEntryFromExternalIpc(string source)
{
EnsureTaskbarEntry($"ExternalIpc:{source}");
return BuildPublicTaskbarStatus();
}
internal PublicShellStatus GetPublicShellStatus()
{
return new PublicShellStatus(
Environment.ProcessId,
_startupAt,
_launchSource,
_desktopShellState.ToString(),
_mainWindow is not null && !_mainWindowClosed,
_mainWindow?.IsVisible == true,
_mainWindowOpened,
_mainWindow?.IsVisible == true && _mainWindow.WindowState != WindowState.Minimized,
_publicIpcHostService is not null,
BuildPublicTrayStatus(),
BuildPublicTaskbarStatus());
}
private PublicTrayStatus BuildPublicTrayStatus()
{
return new PublicTrayStatus(
_desktopTrayService?.State.ToString() ?? TrayAvailabilityState.Unavailable.ToString(),
_desktopTrayService?.IsReady == true,
_desktopTrayService?.HasIcon == true,
_desktopTrayService?.HasMenu == true,
_desktopTrayService?.IsVisible == true,
_desktopTrayService?.ConsecutiveRecoveryFailures ?? 0);
}
private PublicTaskbarStatus BuildPublicTaskbarStatus()
{
var requested = ShouldShowMainWindowInTaskbar();
var mainWindowExists = _mainWindow is not null && !_mainWindowClosed;
var showInTaskbar = _mainWindow?.ShowInTaskbar == true;
var visible = _mainWindow?.IsVisible == true;
var minimized = _mainWindow?.WindowState == WindowState.Minimized;
return new PublicTaskbarStatus(
requested,
mainWindowExists,
showInTaskbar,
visible,
minimized,
requested && mainWindowExists && showInTaskbar && visible);
}
private void InitializePublicIpc()
{
if (_publicIpcHostService is not null)
{
return;
}
try
{
var versionInfo = AppVersionProvider.ResolveForCurrentProcess();
_publicIpcHostService = new PublicIpcHostService();
_publicIpcHostService.PluginDescriptorProvider = BuildPublicPluginDescriptors;
_publicIpcHostService.RegisterPublicService<IPublicAppInfoService>(
new PublicAppInfoService(_startupAt));
_publicIpcHostService.RegisterPublicService<IPublicShellControlService>(
new PublicShellControlService());
_publicIpcHostService.RegisterPublicService<IPublicPluginCatalogService>(
new PublicPluginCatalogService(_publicIpcHostService));
_publicIpcHostService.Start();
AppLogger.Info(
"PublicIpc",
$"Public IPC host started. PipeName='{IpcConstants.DefaultPipeName}'; Version='{versionInfo.Version}'; Codename='{versionInfo.Codename}'.");
}
catch (Exception ex)
{
AppLogger.Warn("PublicIpc", "Failed to initialize public IPC host.", ex);
}
}
private IReadOnlyList<PublicPluginDescriptor> BuildPublicPluginDescriptors()
{
var runtime = _pluginRuntimeService;
if (runtime is null)
{
return Array.Empty<PublicPluginDescriptor>();
}
return runtime.Catalog
.Select(entry => new PublicPluginDescriptor(
entry.Manifest.Id,
entry.Manifest.Name,
entry.Manifest.Version,
entry.IsLoaded,
entry.IsEnabled))
.ToArray();
}
}