From 44b87ba12ed658905bf80a0bb9d6d8b35b81b601 Mon Sep 17 00:00:00 2001 From: lincube Date: Fri, 3 Apr 2026 11:42:00 +0800 Subject: [PATCH] =?UTF-8?q?fead.=E6=A1=8C=E9=9D=A2=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LanMountainDesktop/App.axaml.cs | 21 ++ .../Services/FusedDesktopManagerService.cs | 195 ++++++++++++ .../Views/ComponentLibraryWindow.axaml.cs | 2 +- .../Views/DesktopWidgetWindow.axaml | 23 ++ .../Views/DesktopWidgetWindow.axaml.cs | 61 ++++ .../FusedDesktopComponentLibraryControl.axaml | 205 ++++++++----- ...sedDesktopComponentLibraryControl.axaml.cs | 152 ++++------ ...usedDesktopComponentLibraryWindow.axaml.cs | 1 + .../Views/MainWindow.DesktopPaging.cs | 284 ++++++++++++++---- LanMountainDesktop/Views/MainWindow.axaml | 57 +--- .../Views/TransparentOverlayWindow.axaml | 1 - .../Views/TransparentOverlayWindow.axaml.cs | 50 +-- 12 files changed, 752 insertions(+), 300 deletions(-) create mode 100644 LanMountainDesktop/Services/FusedDesktopManagerService.cs create mode 100644 LanMountainDesktop/Views/DesktopWidgetWindow.axaml create mode 100644 LanMountainDesktop/Views/DesktopWidgetWindow.axaml.cs diff --git a/LanMountainDesktop/App.axaml.cs b/LanMountainDesktop/App.axaml.cs index 84396e4..8d41fb7 100644 --- a/LanMountainDesktop/App.axaml.cs +++ b/LanMountainDesktop/App.axaml.cs @@ -149,6 +149,11 @@ public partial class App : Application LinuxDesktopEntryInstaller.EnsureInstalled(); DesktopBootstrap.InitializeApplication(this, InitializeDesktopShell); + if (!Design.IsDesignMode && OperatingSystem.IsWindows()) + { + FusedDesktopManagerServiceFactory.GetOrCreate().Initialize(); + } + base.OnFrameworkInitializationCompleted(); } @@ -226,6 +231,9 @@ public partial class App : Application AppLogger.Warn("FusedDesktop", "Fused desktop is only supported on Windows."); return; } + + // 切换进入编辑模式,隐藏常态零散的小部件 + FusedDesktopManagerServiceFactory.GetOrCreate().EnterEditMode(); // 确保透明覆盖层窗口存在并显示 EnsureTransparentOverlayWindow(); @@ -248,6 +256,19 @@ public partial class App : Application window.SetOverlayWindow(_transparentOverlayWindow); } + // 当组件库关闭时,退出编辑态 + window.Closed += (s, ev) => + { + if (_transparentOverlayWindow is not null) + { + // 触发画布保存,并隐藏画布 + _transparentOverlayWindow.SaveLayoutAndHide(); + } + + // 让管理器根据已存储的最新快照重建生成所有实体小组件 + FusedDesktopManagerServiceFactory.GetOrCreate().ExitEditMode(); + }; + window.Show(); window.Activate(); } diff --git a/LanMountainDesktop/Services/FusedDesktopManagerService.cs b/LanMountainDesktop/Services/FusedDesktopManagerService.cs new file mode 100644 index 0000000..595571b --- /dev/null +++ b/LanMountainDesktop/Services/FusedDesktopManagerService.cs @@ -0,0 +1,195 @@ +using System; +using System.Collections.Generic; +using Avalonia; +using Avalonia.Controls; +using LanMountainDesktop.ComponentSystem; +using LanMountainDesktop.Models; +using LanMountainDesktop.PluginSdk; +using LanMountainDesktop.Services.Settings; +using LanMountainDesktop.Views; +using LanMountainDesktop.Views.Components; + +namespace LanMountainDesktop.Services; + +/// +/// 融合桌面中央管理器服务接口 +/// +public interface IFusedDesktopManagerService +{ + void Initialize(); + void EnterEditMode(); + void ExitEditMode(); + void ReloadWidgets(); +} + +/// +/// 融合桌面中央管理器服务实现。用于管理常态下的各个小窗口实体。 +/// +internal sealed class FusedDesktopManagerService : IFusedDesktopManagerService +{ + private readonly IFusedDesktopLayoutService _layoutService; + private readonly ISettingsFacadeService _settingsFacade; + private readonly Dictionary _widgetWindows = []; + + // 基础服务依赖 + private readonly IWeatherInfoService _weatherDataService; + private readonly TimeZoneService _timeZoneService; + private readonly IRecommendationInfoService _recommendationInfoService = new RecommendationDataService(); + private readonly ICalculatorDataService _calculatorDataService = new CalculatorDataService(); + + private ComponentRegistry? _componentRegistry; + private DesktopComponentRuntimeRegistry? _componentRuntimeRegistry; + private bool _isEditMode; + + private const double DefaultCellSize = 100; + + public FusedDesktopManagerService( + IFusedDesktopLayoutService layoutService, + ISettingsFacadeService settingsFacade) + { + _layoutService = layoutService; + _settingsFacade = settingsFacade; + + _weatherDataService = _settingsFacade.Weather.GetWeatherInfoService(); + _timeZoneService = _settingsFacade.Region.GetTimeZoneService(); + } + + public void Initialize() + { + if (!OperatingSystem.IsWindows()) return; + + EnsureRegistries(); + ReloadWidgets(); + } + + private void EnsureRegistries() + { + if (_componentRuntimeRegistry is not null) return; + + var pluginRuntimeService = (Application.Current as App)?.PluginRuntimeService; + _componentRegistry = DesktopComponentRegistryFactory.Create(pluginRuntimeService); + _componentRuntimeRegistry = DesktopComponentRegistryFactory.CreateRuntimeRegistry( + _componentRegistry, + pluginRuntimeService, + _settingsFacade); + } + + public void EnterEditMode() + { + if (_isEditMode) return; + _isEditMode = true; + + // 隐藏所有底层小窗口 + foreach (var window in _widgetWindows.Values) + { + window.Hide(); + } + } + + public void ExitEditMode() + { + if (!_isEditMode) return; + _isEditMode = false; + + // 编辑完成,重新加载布局(可能已发生更改)并显示 + ReloadWidgets(); + } + + public void ReloadWidgets() + { + if (_isEditMode) return; // 编辑模式下不渲染小窗口 + + var layout = _layoutService.Load(); + var existingIds = new HashSet(_widgetWindows.Keys); + + foreach (var placement in layout.ComponentPlacements) + { + existingIds.Remove(placement.PlacementId); + + if (_widgetWindows.TryGetValue(placement.PlacementId, out var existingWindow)) + { + // 已存在,可能只更新位置或尺寸 + existingWindow.Position = new Avalonia.PixelPoint((int)placement.X, (int)placement.Y); + if (existingWindow.IsVisible == false) + { + existingWindow.Show(); + } + } + else + { + // 新组件,生成窗口 + try + { + var window = CreateWidgetWindow(placement); + if (window != null) + { + _widgetWindows[placement.PlacementId] = window; + window.Show(); + window.Position = new Avalonia.PixelPoint((int)placement.X, (int)placement.Y); + } + } + catch (Exception ex) + { + AppLogger.Warn("FusedDesktopMgr", $"Failed to render tiny window for {placement.ComponentId}", ex); + } + } + } + + // 移除被删除的组件 + foreach (var id in existingIds) + { + if (_widgetWindows.Remove(id, out var windowToRemove)) + { + windowToRemove.Close(); + } + } + } + + private DesktopWidgetWindow? CreateWidgetWindow(FusedDesktopComponentPlacementSnapshot placement) + { + EnsureRegistries(); + if (_componentRuntimeRegistry is null || !_componentRuntimeRegistry.TryGetDescriptor(placement.ComponentId, out var descriptor)) + { + AppLogger.Warn("FusedDesktopMgr", $"Unknown component: {placement.ComponentId}"); + return null; + } + + var control = descriptor.CreateControl( + DefaultCellSize, + _timeZoneService, + _weatherDataService, + _recommendationInfoService, + _calculatorDataService, + _settingsFacade, + placement.PlacementId); + + // 将组件包装到一个具有准确宽高的容器内(如果组件自身没有设置宽度) + control.Width = placement.Width; + control.Height = placement.Height; + + var window = new DesktopWidgetWindow(control); + return window; + } +} + +/// +/// 工厂 +/// +public static class FusedDesktopManagerServiceFactory +{ + private static IFusedDesktopManagerService? _instance; + private static readonly object _lock = new(); + + public static IFusedDesktopManagerService GetOrCreate() + { + if (_instance is not null) return _instance; + + lock (_lock) + { + var layoutService = FusedDesktopLayoutServiceProvider.GetOrCreate(); + var settings = HostSettingsFacadeProvider.GetOrCreate(); + _instance ??= new FusedDesktopManagerService(layoutService, settings); + return _instance; + } + } +} diff --git a/LanMountainDesktop/Views/ComponentLibraryWindow.axaml.cs b/LanMountainDesktop/Views/ComponentLibraryWindow.axaml.cs index 7c6cf86..428bc29 100644 --- a/LanMountainDesktop/Views/ComponentLibraryWindow.axaml.cs +++ b/LanMountainDesktop/Views/ComponentLibraryWindow.axaml.cs @@ -220,7 +220,7 @@ public partial class ComponentLibraryWindow : Window if (string.Equals(categoryId, "Info", StringComparison.OrdinalIgnoreCase)) { - return Symbol.Apps; + return Symbol.Info; } if (string.Equals(categoryId, "Calculator", StringComparison.OrdinalIgnoreCase)) diff --git a/LanMountainDesktop/Views/DesktopWidgetWindow.axaml b/LanMountainDesktop/Views/DesktopWidgetWindow.axaml new file mode 100644 index 0000000..47c7afc --- /dev/null +++ b/LanMountainDesktop/Views/DesktopWidgetWindow.axaml @@ -0,0 +1,23 @@ + + + + + + diff --git a/LanMountainDesktop/Views/DesktopWidgetWindow.axaml.cs b/LanMountainDesktop/Views/DesktopWidgetWindow.axaml.cs new file mode 100644 index 0000000..6a3ef6c --- /dev/null +++ b/LanMountainDesktop/Views/DesktopWidgetWindow.axaml.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using Avalonia; +using Avalonia.Controls; +using LanMountainDesktop.Services; +using Avalonia.Threading; + +namespace LanMountainDesktop.Views; + +/// +/// 表示一个独立的组件挂载窗口。它不含有任何自己的边窗,仅仅负责包裹组件并将自身植入系统最底层。 +/// +public partial class DesktopWidgetWindow : Window +{ + private readonly IWindowBottomMostService _bottomMostService = WindowBottomMostServiceFactory.GetOrCreate(); + private readonly IRegionPassthroughService _regionPassthroughService = RegionPassthroughServiceFactory.GetOrCreate(); + + public DesktopWidgetWindow() + { + InitializeComponent(); + } + + public DesktopWidgetWindow(Control componentContent) : this() + { + ComponentContainer.Child = componentContent; + } + + protected override void OnOpened(EventArgs e) + { + base.OnOpened(e); + + if (OperatingSystem.IsWindows()) + { + // 通过现有的置底服务将独立的小窗口锁定到底层 + _bottomMostService.SetupBottomMost(this); + _bottomMostService.SendToBottom(this); + + // 当窗口展示完毕且有了尺寸后,更新可交互区域,使得整个组件都能被点击 + Dispatcher.UIThread.Post(UpdateInteractiveRegion, DispatcherPriority.Render); + } + } + + protected override void OnSizeChanged(SizeChangedEventArgs e) + { + base.OnSizeChanged(e); + + if (OperatingSystem.IsWindows() && IsVisible) + { + UpdateInteractiveRegion(); + } + } + + private void UpdateInteractiveRegion() + { + // 既然是一个完全紧贴在组件身上的小窗,它的全部都是可交互的 + _regionPassthroughService.SetInteractiveRegions(this, new List + { + new(0, 0, Bounds.Width, Bounds.Height) + }); + } +} diff --git a/LanMountainDesktop/Views/FusedDesktopComponentLibraryControl.axaml b/LanMountainDesktop/Views/FusedDesktopComponentLibraryControl.axaml index f6ab62c..454d232 100644 --- a/LanMountainDesktop/Views/FusedDesktopComponentLibraryControl.axaml +++ b/LanMountainDesktop/Views/FusedDesktopComponentLibraryControl.axaml @@ -1,15 +1,17 @@ + x:Class="LanMountainDesktop.Views.FusedDesktopComponentLibraryControl" + x:DataType="vm:ComponentLibraryWindowViewModel"> - + - + - - - + SelectionChanged="OnCategorySelectionChanged" + ItemsSource="{Binding Categories}"> - - - - - + + + + + + + - + - - - - - - - - - - - - - - - + + + + + diff --git a/LanMountainDesktop/Views/FusedDesktopComponentLibraryControl.axaml.cs b/LanMountainDesktop/Views/FusedDesktopComponentLibraryControl.axaml.cs index fdaa61c..72447d4 100644 --- a/LanMountainDesktop/Views/FusedDesktopComponentLibraryControl.axaml.cs +++ b/LanMountainDesktop/Views/FusedDesktopComponentLibraryControl.axaml.cs @@ -1,18 +1,15 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Linq; using Avalonia; using Avalonia.Controls; -using Avalonia.Input; using Avalonia.Interactivity; -using Avalonia.Layout; -using Avalonia.Media; +using FluentIcons.Common; using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.Services; using LanMountainDesktop.Services.Settings; +using LanMountainDesktop.ViewModels; using LanMountainDesktop.Views.Components; -using LanMountainDesktop.Models; namespace LanMountainDesktop.Views; @@ -20,10 +17,9 @@ public partial class FusedDesktopComponentLibraryControl : UserControl { public event EventHandler? AddComponentRequested; - private readonly ObservableCollection _categories = new(); - private readonly ObservableCollection _components = new(); + private readonly ComponentLibraryWindowViewModel _viewModel = new(); private List _allDefinitions = new(); - + private ComponentRegistry? _componentRegistry; private DesktopComponentRuntimeRegistry? _componentRuntimeRegistry; private readonly ISettingsFacadeService _settingsFacade = HostSettingsFacadeProvider.GetOrCreate(); @@ -35,18 +31,17 @@ public partial class FusedDesktopComponentLibraryControl : UserControl public FusedDesktopComponentLibraryControl() { InitializeComponent(); + DataContext = _viewModel; + _weatherDataService = _settingsFacade.Weather.GetWeatherInfoService(); _timeZoneService = _settingsFacade.Region.GetTimeZoneService(); - - CategoryListBox.ItemsSource = _categories; - ComponentItemsControl.ItemsSource = _components; - + LoadRegistry(); LoadCategories(); SearchBox.KeyUp += (s, e) => FilterComponents(); - + // 默认选择第一个分类 - if (_categories.Count > 0) + if (_viewModel.Categories.Count > 0) { CategoryListBox.SelectedIndex = 0; } @@ -60,7 +55,7 @@ public partial class FusedDesktopComponentLibraryControl : UserControl _componentRegistry, pluginRuntimeService, _settingsFacade); - + _allDefinitions = _componentRegistry.GetAll() .Where(d => d.AllowDesktopPlacement) .ToList(); @@ -68,18 +63,27 @@ public partial class FusedDesktopComponentLibraryControl : UserControl private void LoadCategories() { - _categories.Clear(); - _categories.Add(new LibraryCategoryItem("all", "全部组件", "Apps")); - - var categoryMap = new Dictionary + _viewModel.Categories.Clear(); + _viewModel.Components.Clear(); + + // 添加"全部组件"分类 + _viewModel.Categories.Add(new ComponentLibraryCategoryViewModel( + "all", + "全部组件", + Symbol.Apps, + Array.Empty())); + + var categoryMap = new Dictionary { - { "clock", ("时钟", "Clock") }, - { "date", ("日历", "Calendar") }, - { "weather", ("天气", "WeatherCloudy") }, - { "info", ("资讯", "News") }, - { "calculator", ("工具", "Calculator") }, - { "study", ("学习", "Book") }, - { "file", ("文件", "Document") } + { "clock", ("时钟", Symbol.Clock) }, + { "date", ("日历", Symbol.CalendarDate) }, + { "weather", ("天气", Symbol.WeatherSunny) }, + { "board", ("画板", Symbol.Edit) }, + { "media", ("媒体", Symbol.Play) }, + { "info", ("资讯", Symbol.News) }, + { "calculator", ("工具", Symbol.Calculator) }, + { "study", ("学习", Symbol.Hourglass) }, + { "file", ("文件", Symbol.Folder) } }; var usedCategories = _allDefinitions @@ -91,15 +95,36 @@ public partial class FusedDesktopComponentLibraryControl : UserControl { if (categoryMap.TryGetValue(cat.ToLower(), out var info)) { - _categories.Add(new LibraryCategoryItem(cat, info.Display, info.Icon)); - } - else - { - _categories.Add(new LibraryCategoryItem(cat, cat, "Cube")); + var categoryComponents = _allDefinitions + .Where(d => string.Equals(d.Category, cat, StringComparison.OrdinalIgnoreCase)) + .OrderBy(d => d.DisplayName) + .Select(d => CreateComponentItem(d)) + .ToArray(); + + _viewModel.Categories.Add(new ComponentLibraryCategoryViewModel( + cat, + info.Display, + info.Icon, + categoryComponents)); } } } + private ComponentLibraryItemViewModel CreateComponentItem(DesktopComponentDefinition definition) + { + var previewKey = ComponentPreviewKey.ForComponentType( + definition.Id, + definition.MinWidthCells, + definition.MinHeightCells); + + return new ComponentLibraryItemViewModel( + definition.Id, + definition.DisplayName, + previewKey, + "正在加载预览...", + "预览不可用"); + } + private void OnCategorySelectionChanged(object? sender, SelectionChangedEventArgs e) { FilterComponents(); @@ -107,7 +132,7 @@ public partial class FusedDesktopComponentLibraryControl : UserControl private void FilterComponents() { - var selectedCategory = (CategoryListBox.SelectedItem as LibraryCategoryItem)?.Id; + var selectedCategory = (CategoryListBox.SelectedItem as ComponentLibraryCategoryViewModel)?.Id; var searchText = SearchBox.Text?.ToLower() ?? ""; var filtered = _allDefinitions.Where(d => @@ -117,57 +142,10 @@ public partial class FusedDesktopComponentLibraryControl : UserControl return matchesCategory && matchesSearch; }); - _components.Clear(); + _viewModel.Components.Clear(); foreach (var def in filtered) { - _components.Add(new LibraryComponentItem - { - Id = def.Id, - DisplayName = def.DisplayName, - Description = GetDescription(def.Id), - PreviewContent = CreatePreview(def.Id) - }); - } - } - - private string GetDescription(string id) - { - // 简单映射描述信息 - return id.Contains("clock") ? "实时显示当前时间与日期。" : - id.Contains("weather") ? "为您提供精准的天气预报。" : - "多功能桌面组件,提升您的操作效率。"; - } - - private Control? CreatePreview(string id) - { - if (_componentRuntimeRegistry == null || !_componentRuntimeRegistry.TryGetDescriptor(id, out var descriptor)) - { - return null; - } - - try - { - var control = descriptor.CreateControl( - 100, // Previews assume 100px base - _timeZoneService, - _weatherDataService, - _recommendationInfoService, - _calculatorDataService, - _settingsFacade, - "preview_" + id); - - control.IsHitTestVisible = false; - - return new Viewbox - { - Child = control, - Stretch = Stretch.Uniform, - Margin = new Thickness(12) - }; - } - catch (Exception ex) - { - return new TextBlock { Text = "无法预览", VerticalAlignment = VerticalAlignment.Center, HorizontalAlignment = HorizontalAlignment.Center }; + _viewModel.Components.Add(CreateComponentItem(def)); } } @@ -179,15 +157,3 @@ public partial class FusedDesktopComponentLibraryControl : UserControl } } } - -public record LibraryCategoryItem(string Id, string DisplayName, string Icon); - -public class LibraryComponentItem -{ - public string Id { get; set; } = ""; - public string DisplayName { get; set; } = ""; - public string Description { get; set; } = ""; - public Control? PreviewContent { get; set; } - public bool HasPreview => PreviewContent != null; -} - diff --git a/LanMountainDesktop/Views/FusedDesktopComponentLibraryWindow.axaml.cs b/LanMountainDesktop/Views/FusedDesktopComponentLibraryWindow.axaml.cs index dd88982..b6dcc9f 100644 --- a/LanMountainDesktop/Views/FusedDesktopComponentLibraryWindow.axaml.cs +++ b/LanMountainDesktop/Views/FusedDesktopComponentLibraryWindow.axaml.cs @@ -1,4 +1,5 @@ using System; +using Avalonia; using Avalonia.Controls; using Avalonia.Interactivity; using LanMountainDesktop.ComponentSystem; diff --git a/LanMountainDesktop/Views/MainWindow.DesktopPaging.cs b/LanMountainDesktop/Views/MainWindow.DesktopPaging.cs index 7a661b0..b066de4 100644 --- a/LanMountainDesktop/Views/MainWindow.DesktopPaging.cs +++ b/LanMountainDesktop/Views/MainWindow.DesktopPaging.cs @@ -269,12 +269,6 @@ public partial class MainWindow LauncherPagePanel.MaxWidth = pageWidth - launcherMargin * 2; LauncherPagePanel.MaxHeight = pageHeight - launcherMargin * 2; - if (LauncherFolderPanel is not null) - { - LauncherFolderPanel.MaxWidth = Math.Max(320, pageWidth - 96); - LauncherFolderPanel.MaxHeight = Math.Max(220, pageHeight - 96); - } - // 更新启动台图标布局 UpdateLauncherTileLayout(); @@ -331,19 +325,6 @@ public partial class MainWindow } } - // 同样更新文件夹视图的图标尺寸 - if (LauncherFolderTilePanel is not null) - { - LauncherFolderTilePanel.Width = availableWidth; - foreach (var child in LauncherFolderTilePanel.Children) - { - if (child is Button button) - { - button.Width = tileWidth; - button.Height = tileHeight; - } - } - } } private void ClampSurfaceIndex() @@ -630,8 +611,12 @@ public partial class MainWindow foreach (var node in button.GetSelfAndVisualAncestors()) { - if (node is WrapPanel panel && - (panel.Name == "LauncherRootTilePanel" || panel.Name == "LauncherFolderTilePanel")) + if (node is WrapPanel panel && panel.Name == "LauncherRootTilePanel") + { + return true; + } + + if (node is Grid grid && grid.Name == "LauncherFolderGridPanel") { return true; } @@ -719,8 +704,7 @@ public partial class MainWindow return false; } - return scrollViewer.Name == "LauncherRootScrollViewer" || - scrollViewer.Name == "LauncherFolderScrollViewer"; + return scrollViewer.Name == "LauncherRootScrollViewer"; } private bool TryGetPointerPositionInDesktopViewport(PointerEventArgs e, out Point point) @@ -1561,18 +1545,17 @@ public partial class MainWindow LauncherFolderOverlay.IsVisible = false; } - if (LauncherFolderTilePanel is not null) + if (LauncherFolderGridPanel is not null) { - LauncherFolderTilePanel.Children.Clear(); + LauncherFolderGridPanel.Children.Clear(); } } private void RenderLauncherFolderFromStack() { if (LauncherFolderOverlay is null || - LauncherFolderTilePanel is null || - LauncherFolderTitleTextBlock is null || - LauncherFolderBackButton is null) + LauncherFolderGridPanel is null || + LauncherFolderTitleTextBlock is null) { return; } @@ -1587,38 +1570,230 @@ public partial class MainWindow var folder = _launcherFolderStack.Peek(); LauncherFolderOverlay.IsVisible = true; LauncherFolderTitleTextBlock.Text = folder.Name; - LauncherFolderBackButton.IsVisible = _launcherFolderStack.Count > 1; - LauncherFolderTilePanel.Children.Clear(); - foreach (var subFolder in folder.Folders) + LauncherFolderGridPanel.Children.Clear(); + + const int maxCols = 4; + const int maxRows = 3; + const int maxItems = maxCols * maxRows; + + var visibleFolders = folder.Folders.Where(IsLauncherFolderVisible).ToList(); + var visibleApps = folder.Apps.Where(IsLauncherAppVisible).ToList(); + + if (visibleFolders.Count == 0 && visibleApps.Count == 0) { - if (!IsLauncherFolderVisible(subFolder)) + LauncherFolderGridPanel.Children.Add(CreateLauncherFolderGridHintCell( + L("launcher.empty_folder", "This folder is empty."))); + return; + } + + var allItems = new List<(StartMenuFolderNode? Folder, StartMenuAppEntry? App)>(); + foreach (var f in visibleFolders) + { + allItems.Add((f, null)); + } + foreach (var a in visibleApps) + { + allItems.Add((null, a)); + } + + var displayCount = Math.Min(allItems.Count, maxItems); + for (var i = 0; i < displayCount; i++) + { + var col = i % maxCols; + var row = i / maxCols; + var (itemFolder, itemApp) = allItems[i]; + + Control cell; + if (itemFolder is not null) + { + var capturedFolder = itemFolder; + cell = CreateLauncherFolderGridTile(itemFolder.Name, GetLauncherFolderIconBitmap(), () => OpenLauncherFolder(capturedFolder)); + } + else if (itemApp is not null) + { + var capturedApp = itemApp; + cell = CreateLauncherFolderGridTile(capturedApp, () => LaunchStartMenuEntry(capturedApp)); + } + else { continue; } - LauncherFolderTilePanel.Children.Add(CreateLauncherFolderTile(subFolder)); + Grid.SetColumn(cell, col); + Grid.SetRow(cell, row); + LauncherFolderGridPanel.Children.Add(cell); } + } - foreach (var app in folder.Apps) - { - if (!IsLauncherAppVisible(app)) + private Button CreateLauncherFolderGridTile(StartMenuAppEntry app, Action clickAction) + { + var iconBitmap = GetLauncherIconBitmap(app); + var monogram = BuildMonogram(app.DisplayName); + + Control iconControl = iconBitmap is not null + ? new Image { - continue; + Source = iconBitmap, + Width = 32, + Height = 32, + Stretch = Stretch.Uniform + } + : new Border + { + Width = 32, + Height = 32, + CornerRadius = new CornerRadius(8), + Background = GetThemeBrush("AdaptiveButtonBackgroundBrush"), + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Child = new TextBlock + { + Text = monogram, + FontSize = 13, + FontWeight = FontWeight.Bold, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + } + }; + + var content = new StackPanel + { + Spacing = 6, + HorizontalAlignment = HorizontalAlignment.Stretch, + VerticalAlignment = VerticalAlignment.Center + }; + content.Children.Add(iconControl); + content.Children.Add(new TextBlock + { + Text = app.DisplayName, + TextTrimming = TextTrimming.CharacterEllipsis, + MaxLines = 2, + TextAlignment = TextAlignment.Center, + FontSize = 11, + HorizontalAlignment = HorizontalAlignment.Stretch + }); + + var button = new Button + { + Classes = { "glass-panel" }, + HorizontalAlignment = HorizontalAlignment.Stretch, + VerticalAlignment = VerticalAlignment.Stretch, + BorderThickness = new Thickness(0), + CornerRadius = new CornerRadius(12), + Padding = new Thickness(8, 8, 8, 6), + Content = content + }; + button.Click += (_, _) => + { + if (_isComponentLibraryOpen) + { + return; } - LauncherFolderTilePanel.Children.Add(CreateLauncherAppTile(app)); - } + clickAction(); + }; + return button; + } - if (LauncherFolderTilePanel.Children.Count == 0) + private Button CreateLauncherFolderGridTile(string folderName, Bitmap? iconBitmap, Action clickAction) + { + var monogram = "DIR"; + + Control iconControl = iconBitmap is not null + ? new Image + { + Source = iconBitmap, + Width = 32, + Height = 32, + Stretch = Stretch.Uniform + } + : new Border + { + Width = 32, + Height = 32, + CornerRadius = new CornerRadius(8), + Background = GetThemeBrush("AdaptiveButtonBackgroundBrush"), + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Child = new TextBlock + { + Text = monogram, + FontSize = 11, + FontWeight = FontWeight.Bold, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + } + }; + + var content = new StackPanel { - LauncherFolderTilePanel.Children.Add(CreateLauncherHintTile( - L("launcher.empty_folder", "This folder is empty."), - string.Empty)); - } + Spacing = 6, + HorizontalAlignment = HorizontalAlignment.Stretch, + VerticalAlignment = VerticalAlignment.Center + }; + content.Children.Add(iconControl); + content.Children.Add(new TextBlock + { + Text = folderName, + TextTrimming = TextTrimming.CharacterEllipsis, + MaxLines = 2, + TextAlignment = TextAlignment.Center, + FontSize = 11, + HorizontalAlignment = HorizontalAlignment.Stretch + }); - // 在图标渲染完成后,应用布局计算 - Dispatcher.UIThread.Post(() => UpdateLauncherTileLayout(), DispatcherPriority.Background); + var button = new Button + { + Classes = { "glass-panel" }, + HorizontalAlignment = HorizontalAlignment.Stretch, + VerticalAlignment = VerticalAlignment.Stretch, + BorderThickness = new Thickness(0), + CornerRadius = new CornerRadius(12), + Padding = new Thickness(8, 8, 8, 6), + Content = content + }; + button.Click += (_, _) => + { + if (_isComponentLibraryOpen) + { + return; + } + + clickAction(); + }; + return button; + } + + private Control CreateLauncherFolderGridHintCell(string message) + { + return CreateLauncherFolderGridHintCell(message, 0, 0); + } + + private Control CreateLauncherFolderGridHintCell(string message, int col, int row) + { + var textBlock = new TextBlock + { + Text = message, + FontSize = 12, + FontWeight = FontWeight.SemiBold, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Opacity = 0.6 + }; + + var cell = new Border + { + Classes = { "glass-panel" }, + HorizontalAlignment = HorizontalAlignment.Stretch, + VerticalAlignment = VerticalAlignment.Stretch, + CornerRadius = new CornerRadius(12), + Child = textBlock + }; + + Grid.SetColumn(cell, col); + Grid.SetRow(cell, row); + return cell; } private static string BuildMonogram(string text) @@ -1689,18 +1864,6 @@ public partial class MainWindow } } - private void OnLauncherFolderBackClick(object? sender, RoutedEventArgs e) - { - if (_launcherFolderStack.Count <= 1) - { - CloseLauncherFolderOverlay(); - return; - } - - _launcherFolderStack.Pop(); - RenderLauncherFolderFromStack(); - } - private void OnLauncherFolderOverlayPointerPressed(object? sender, PointerPressedEventArgs e) { if (LauncherFolderPanel is null) @@ -1721,11 +1884,6 @@ public partial class MainWindow e.Handled = true; } - private void OnLauncherFolderCloseClick(object? sender, RoutedEventArgs e) - { - CloseLauncherFolderOverlay(); - } - private void DisposeLauncherResources() { foreach (var bitmap in _launcherIconCache.Values) diff --git a/LanMountainDesktop/Views/MainWindow.axaml b/LanMountainDesktop/Views/MainWindow.axaml index b3f9c7e..4711688 100644 --- a/LanMountainDesktop/Views/MainWindow.axaml +++ b/LanMountainDesktop/Views/MainWindow.axaml @@ -189,50 +189,21 @@ Classes="surface-solid-strong" HorizontalAlignment="Center" VerticalAlignment="Center" - Margin="52" - MaxWidth="760" - MaxHeight="520" - CornerRadius="36" - Padding="14"> - - - - - - - - - + Width="464" + Height="384" + CornerRadius="24" + Padding="16,14,16,12"> + + - - - + diff --git a/LanMountainDesktop/Views/TransparentOverlayWindow.axaml b/LanMountainDesktop/Views/TransparentOverlayWindow.axaml index d5b8838..c75ef4a 100644 --- a/LanMountainDesktop/Views/TransparentOverlayWindow.axaml +++ b/LanMountainDesktop/Views/TransparentOverlayWindow.axaml @@ -1,7 +1,6 @@ public partial class TransparentOverlayWindow : Window { - private readonly ISettingsFacadeService _settingsFacade = HostSettingsFacadeProvider.GetOrCreate(); - private readonly IWindowBottomMostService _bottomMostService = WindowBottomMostServiceFactory.GetOrCreate(); - private readonly IRegionPassthroughService _regionPassthroughService = RegionPassthroughServiceFactory.GetOrCreate(); private readonly IFusedDesktopLayoutService _layoutService = FusedDesktopLayoutServiceProvider.GetOrCreate(); // 滑动状态 @@ -67,23 +64,45 @@ public partial class TransparentOverlayWindow : Window public TransparentOverlayWindow() { InitializeComponent(); - _weatherDataService = _settingsFacade.Weather.GetWeatherInfoService(); - _timeZoneService = _settingsFacade.Region.GetTimeZoneService(); + var facade = HostSettingsFacadeProvider.GetOrCreate(); + _weatherDataService = facade.Weather.GetWeatherInfoService(); + _timeZoneService = facade.Region.GetTimeZoneService(); + _settingsFacade = facade; + } + + private readonly ISettingsFacadeService _settingsFacade; + + public void SaveLayoutAndHide() + { + SaveLayout(); + Hide(); - // 仅在 Windows 上启用置底功能 - if (OperatingSystem.IsWindows()) + // Remove all components so that next time we open it builds fresh from snapshot + if (Content is Canvas canvas) { - _bottomMostService.SetupBottomMost(this); + canvas.Children.Clear(); } + _componentHosts.Clear(); } protected override void OnOpened(EventArgs e) { base.OnOpened(e); - if (OperatingSystem.IsWindows()) + if (Screens.Primary is { } primaryScreen) { - _bottomMostService.SendToBottom(this); + // 避开系统任务栏 + var workArea = primaryScreen.WorkingArea; + var scaling = primaryScreen.Scaling; + Position = new PixelPoint(workArea.X, workArea.Y); + Width = workArea.Width / scaling; + Height = workArea.Height / scaling; + } + + if (Content is Canvas canvas) + { + // 保证透明区域也能被抓取事件 + canvas.Background = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0)); } // 确保注册表已初始化 @@ -147,16 +166,7 @@ public partial class TransparentOverlayWindow : Window /// private void UpdateInteractiveRegions() { - _interactiveRegions.Clear(); - - foreach (var host in _componentHosts.Values) - { - var x = Canvas.GetLeft(host); - var y = Canvas.GetTop(host); - _interactiveRegions.Add(new Rect(x, y, host.Width, host.Height)); - } - - _regionPassthroughService.SetInteractiveRegions(this, _interactiveRegions); + // 编辑模式下不再需要底层穿透功能计算,这里留空或移除 } ///