using System; using System.Linq; using Avalonia; using Avalonia.Controls; using Avalonia.Media; using Avalonia.Styling; using Avalonia.Threading; using LanMountainDesktop.Models; using LanMountainDesktop.PluginSdk; using LanMountainDesktop.Services; using LanMountainDesktop.Theme; using LanMountainDesktop.ViewModels; using LanMountainDesktop.Views; namespace LanMountainDesktop.Services.Settings; public enum SettingsWindowAnchorTarget { DesktopDockTrailingEdge = 0 } public enum SettingsWindowFallbackMode { None = 0, ScreenBottomRight = 1 } public readonly record struct SettingsWindowOpenRequest( string Source, Window? Owner = null, string? PageId = null, SettingsWindowAnchorTarget AnchorTarget = SettingsWindowAnchorTarget.DesktopDockTrailingEdge, SettingsWindowFallbackMode FallbackMode = SettingsWindowFallbackMode.ScreenBottomRight); public interface ISettingsWindowAnchorProvider { bool TryGetSettingsWindowAnchorBounds(out PixelRect anchorBounds); } public interface ISettingsWindowService { bool IsOpen { get; } event EventHandler? StateChanged; void Open(SettingsWindowOpenRequest request); void Close(); void Toggle(SettingsWindowOpenRequest request); } internal sealed class SettingsWindowService : ISettingsWindowService { private readonly ISettingsPageRegistry _pageRegistry; private readonly IHostApplicationLifecycle _hostApplicationLifecycle; private readonly ISettingsFacadeService _settingsFacade; private readonly IAppearanceThemeService _appearanceThemeService; private readonly LocalizationService _localizationService; private SettingsWindowViewModel _viewModel = null!; private SettingsWindow? _window; public SettingsWindowService( ISettingsPageRegistry pageRegistry, IHostApplicationLifecycle hostApplicationLifecycle, ISettingsFacadeService settingsFacade) { _pageRegistry = pageRegistry; _hostApplicationLifecycle = hostApplicationLifecycle; _settingsFacade = settingsFacade; _appearanceThemeService = HostAppearanceThemeProvider.GetOrCreate(); _localizationService = new(); _settingsFacade.Settings.Changed += OnSettingsChanged; _appearanceThemeService.Changed += OnAppearanceThemeChanged; } private string L(string key) { var regionState = _settingsFacade.Region.Get(); var languageCode = regionState.LanguageCode ?? "zh-CN"; return _localizationService.GetString(languageCode, key, key); } public bool IsOpen => _window is { IsVisible: true }; public event EventHandler? StateChanged; public void Open(SettingsWindowOpenRequest request) { _pageRegistry.Rebuild(); _window ??= CreateWindow(); var appearanceSnapshot = _appearanceThemeService.GetCurrent(); _window.ApplyChromeMode(appearanceSnapshot.UseSystemChrome); ApplyTheme(_window); _window.ReloadPages(request.PageId); PositionWindow(_window, request); if (!_window.IsVisible) { if (request.Owner is not null && request.Owner.IsVisible) { _window.Show(request.Owner); } else { _window.Show(); } NotifyStateChanged(); PositionWindowLater(_window, request); return; } _window.Activate(); PositionWindowLater(_window, request); } public void Close() { _window?.Close(); } public void Toggle(SettingsWindowOpenRequest request) { if (IsOpen) { Close(); return; } Open(request); } private SettingsWindow CreateWindow() { var regionState = _settingsFacade.Region.Get(); var languageCode = regionState.LanguageCode ?? "zh-CN"; _viewModel = new SettingsWindowViewModel(_localizationService, languageCode).Initialize(); var appearanceSnapshot = _appearanceThemeService.GetCurrent(); var useSystemChrome = appearanceSnapshot.UseSystemChrome; var window = new SettingsWindow( _viewModel, _pageRegistry, _hostApplicationLifecycle, useSystemChrome); ApplyTheme(window); window.ShowInTaskbar = false; window.Closed += (_, _) => { _window = null; NotifyStateChanged(); }; return window; } private void PositionWindowLater(SettingsWindow window, SettingsWindowOpenRequest request) { Dispatcher.UIThread.Post( () => { if (!window.IsVisible) { return; } PositionWindow(window, request); }, DispatcherPriority.Background); } private static void PositionWindow(SettingsWindow window, SettingsWindowOpenRequest request) { if (request.AnchorTarget == SettingsWindowAnchorTarget.DesktopDockTrailingEdge && request.Owner is ISettingsWindowAnchorProvider anchorProvider && anchorProvider.TryGetSettingsWindowAnchorBounds(out var anchorBounds)) { PositionWindowAboveAnchor(window, anchorBounds, request); return; } if (request.FallbackMode == SettingsWindowFallbackMode.ScreenBottomRight) { PositionWindowNearScreenBottomRight(window, request); } } private static void PositionWindowAboveAnchor(Window window, PixelRect anchorBounds, SettingsWindowOpenRequest request) { var workingArea = GetWorkingArea(window, request); if (anchorBounds.Width <= 0 || anchorBounds.Height <= 0 || anchorBounds.Right < workingArea.X || anchorBounds.Y > workingArea.Bottom) { PositionWindowNearScreenBottomRight(window, request); return; } var scale = window.RenderScaling > 0 ? window.RenderScaling : 1d; var width = ResolveWindowWidth(window, scale); var height = ResolveWindowHeight(window, scale); var inset = (int)Math.Round(24 * scale); var gap = (int)Math.Round(16 * scale); var x = anchorBounds.Right - width - inset; var y = anchorBounds.Y - height - gap; x = Math.Clamp(x, workingArea.X + inset, Math.Max(workingArea.X + inset, workingArea.Right - width - inset)); y = Math.Clamp(y, workingArea.Y + inset, Math.Max(workingArea.Y + inset, workingArea.Bottom - height - inset)); window.Position = new PixelPoint(x, y); } private static void PositionWindowNearScreenBottomRight(Window window, SettingsWindowOpenRequest request) { var workingArea = GetWorkingArea(window, request); var scale = window.RenderScaling > 0 ? window.RenderScaling : 1d; var width = ResolveWindowWidth(window, scale); var height = ResolveWindowHeight(window, scale); var inset = (int)Math.Round(24 * scale); var x = Math.Max(workingArea.X + inset, workingArea.Right - width - inset); var y = Math.Max(workingArea.Y + inset, workingArea.Bottom - height - inset); window.Position = new PixelPoint(x, y); } private static PixelRect GetWorkingArea(Window window, SettingsWindowOpenRequest request) { if (request.Owner is not null && request.Owner.Screens?.ScreenFromWindow(request.Owner) is { } ownerScreen) { return ownerScreen.WorkingArea; } if (window.Screens?.ScreenFromWindow(window) is { } windowScreen) { return windowScreen.WorkingArea; } return window.Screens?.Primary?.WorkingArea ?? new PixelRect( 0, 0, Math.Max(1280, ResolveWindowWidth(window, 1d) + 96), Math.Max(720, ResolveWindowHeight(window, 1d) + 96)); } private static int ResolveWindowWidth(Window window, double scale) { var widthDip = window.Bounds.Width > 1 ? window.Bounds.Width : Math.Max(window.Width, window.MinWidth); return Math.Max(320, (int)Math.Round(widthDip * scale)); } private static int ResolveWindowHeight(Window window, double scale) { var heightDip = window.Bounds.Height > 1 ? window.Bounds.Height : Math.Max(window.Height, window.MinHeight); return Math.Max(240, (int)Math.Round(heightDip * scale)); } private void NotifyStateChanged() { StateChanged?.Invoke(this, EventArgs.Empty); } private void OnSettingsChanged(object? sender, SettingsChangedEvent e) { _ = sender; if (e.Scope != SettingsScope.App) { return; } Dispatcher.UIThread.Post(() => { if (_window is null || _viewModel is null) { return; } var changedKeys = e.ChangedKeys?.ToArray(); var refreshAll = changedKeys is null || changedKeys.Length == 0; var languageChanged = refreshAll || changedKeys.Contains(nameof(AppSettingsSnapshot.LanguageCode), StringComparer.OrdinalIgnoreCase); var liveAppearance = _appearanceThemeService.GetCurrent(); var themeChanged = refreshAll || changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), 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.UseSystemChrome), StringComparer.OrdinalIgnoreCase); if (languageChanged) { var regionState = _settingsFacade.Region.Get(); // 清除本地化缓存,强制重新加载语言文件 _localizationService.ClearCache(); _viewModel.RefreshLanguage(regionState.LanguageCode); _pageRegistry.Rebuild(); _window.ReloadPages(_viewModel.CurrentPageId); _window.RefreshShellText(); } if (themeChanged) { var appearanceSnapshot = _appearanceThemeService.GetCurrent(); _window.ApplyChromeMode(appearanceSnapshot.UseSystemChrome); ApplyTheme(_window); } }, DispatcherPriority.Background); } private void ApplyTheme(SettingsWindow window) { var appearanceSnapshot = _appearanceThemeService.GetCurrent(); window.RequestedThemeVariant = appearanceSnapshot.IsNightMode ? ThemeVariant.Dark : ThemeVariant.Light; _appearanceThemeService.ApplyThemeResources(window.Resources); _appearanceThemeService.ApplyWindowMaterial(window, MaterialSurfaceRole.SettingsWindowBackground); } private void OnAppearanceThemeChanged(object? sender, AppearanceThemeSnapshot e) { _ = sender; _ = e; Dispatcher.UIThread.Post(() => { if (_window is null || _viewModel is null) { return; } ApplyTheme(_window); }, DispatcherPriority.Background); } }