From 87f47e1887611bd6270bd0e1937c610abd18f4ed Mon Sep 17 00:00:00 2001 From: lincube Date: Sun, 1 Mar 2026 16:50:06 +0800 Subject: [PATCH] 0.2.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 组件系统 --- .../ComponentSystem/BuiltInComponentIds.cs | 1 + .../ComponentSystem/ComponentRegistry.cs | 14 +- LanMontainDesktop/LanMontainDesktop.csproj | 2 +- LanMontainDesktop/Localization/en-US.json | 12 +- LanMontainDesktop/Localization/zh-CN.json | 12 +- .../Models/AppSettingsSnapshot.cs | 4 + .../DesktopComponentPlacementSnapshot.cs | 18 + LanMontainDesktop/Models/TaskbarActionId.cs | 4 +- LanMontainDesktop/Services/TimeZoneService.cs | 97 ++ .../Views/Components/ClockWidget.axaml | 6 +- .../Views/Components/ClockWidget.axaml.cs | 25 +- .../Views/Components/DateWidget.axaml | 83 ++ .../Views/Components/DateWidget.axaml.cs | 180 +++ .../Views/MainWindow.ComponentSystem.cs | 1324 ++++++++++++++++- .../Views/MainWindow.DesktopPaging.cs | 58 +- .../Views/MainWindow.Localization.cs | 27 +- .../Views/MainWindow.Settings.cs | 32 +- LanMontainDesktop/Views/MainWindow.axaml | 286 +++- .../Views/MainWindow.axaml.backup | 1048 +++++++++++++ LanMontainDesktop/Views/MainWindow.axaml.cs | 280 +++- 20 files changed, 3312 insertions(+), 201 deletions(-) create mode 100644 LanMontainDesktop/Models/DesktopComponentPlacementSnapshot.cs create mode 100644 LanMontainDesktop/Services/TimeZoneService.cs create mode 100644 LanMontainDesktop/Views/Components/DateWidget.axaml create mode 100644 LanMontainDesktop/Views/Components/DateWidget.axaml.cs create mode 100644 LanMontainDesktop/Views/MainWindow.axaml.backup diff --git a/LanMontainDesktop/ComponentSystem/BuiltInComponentIds.cs b/LanMontainDesktop/ComponentSystem/BuiltInComponentIds.cs index 1a318eb..f714f8b 100644 --- a/LanMontainDesktop/ComponentSystem/BuiltInComponentIds.cs +++ b/LanMontainDesktop/ComponentSystem/BuiltInComponentIds.cs @@ -4,4 +4,5 @@ public static class BuiltInComponentIds { public const string Clock = "Clock"; public const string Blank2x4 = "Blank2x4"; + public const string Date = "Date"; } diff --git a/LanMontainDesktop/ComponentSystem/ComponentRegistry.cs b/LanMontainDesktop/ComponentSystem/ComponentRegistry.cs index 8ff2c83..1f48d84 100644 --- a/LanMontainDesktop/ComponentSystem/ComponentRegistry.cs +++ b/LanMontainDesktop/ComponentSystem/ComponentRegistry.cs @@ -29,14 +29,14 @@ public sealed class ComponentRegistry MinWidthCells: 1, MinHeightCells: 1, AllowStatusBarPlacement: true, - AllowDesktopPlacement: true), + AllowDesktopPlacement: false), new DesktopComponentDefinition( - BuiltInComponentIds.Blank2x4, - "Blank 2x4", - "Rectangle", - "Layout", - MinWidthCells: 2, - MinHeightCells: 4, + BuiltInComponentIds.Date, + "Date", + "Calendar", + "Date", + MinWidthCells: 4, + MinHeightCells: 2, AllowStatusBarPlacement: false, AllowDesktopPlacement: true) }; diff --git a/LanMontainDesktop/LanMontainDesktop.csproj b/LanMontainDesktop/LanMontainDesktop.csproj index 0d1073d..47aa3aa 100644 --- a/LanMontainDesktop/LanMontainDesktop.csproj +++ b/LanMontainDesktop/LanMontainDesktop.csproj @@ -1,4 +1,4 @@ - + WinExe net10.0 diff --git a/LanMontainDesktop/Localization/en-US.json b/LanMontainDesktop/Localization/en-US.json index 37e3b4b..c60da5a 100644 --- a/LanMontainDesktop/Localization/en-US.json +++ b/LanMontainDesktop/Localization/en-US.json @@ -84,10 +84,14 @@ "launcher.empty": "No Start Menu entries found.", "launcher.empty_folder": "This folder is empty.", "launcher.folder_items_format": "{0} apps", - "button.component_library": "Component Library", - "tooltip.component_library": "Component Library", - "component_library.title": "Component Library", - "component_library.empty": "No components yet. Components will appear here later.", + "button.component_library": "Edit Desktop", + "tooltip.component_library": "Edit Desktop", + "component_library.title": "Edit Desktop", + "component_library.empty": "Swipe to pick a category, tap to open, then drag a widget onto the desktop.", + "component_library.drag_hint": "Drag to place", + "component_category.date": "Date", + "component.date": "Date", + "desktop.add_page": "Add page", "placement.fill": "Fill", "placement.fit": "Fit", "placement.stretch": "Stretch", diff --git a/LanMontainDesktop/Localization/zh-CN.json b/LanMontainDesktop/Localization/zh-CN.json index 5dae517..9a75b9b 100644 --- a/LanMontainDesktop/Localization/zh-CN.json +++ b/LanMontainDesktop/Localization/zh-CN.json @@ -84,10 +84,14 @@ "launcher.empty": "未找到开始菜单条目。", "launcher.empty_folder": "此文件夹为空。", "launcher.folder_items_format": "{0} 个应用", - "button.component_library": "组件库", - "tooltip.component_library": "组件库", - "component_library.title": "组件库", - "component_library.empty": "暂无组件,后续会在这里显示。", + "button.component_library": "桌面编辑", + "tooltip.component_library": "桌面编辑", + "component_library.title": "桌面编辑", + "component_library.empty": "左右滑动选择类别,点击进入,然后拖动组件到桌面放置。", + "component_library.drag_hint": "拖动放置", + "component_category.date": "日期", + "component.date": "日历", + "desktop.add_page": "新增页面", "placement.fill": "填充", "placement.fit": "适应", "placement.stretch": "拉伸", diff --git a/LanMontainDesktop/Models/AppSettingsSnapshot.cs b/LanMontainDesktop/Models/AppSettingsSnapshot.cs index 78647fe..e621957 100644 --- a/LanMontainDesktop/Models/AppSettingsSnapshot.cs +++ b/LanMontainDesktop/Models/AppSettingsSnapshot.cs @@ -18,6 +18,8 @@ public sealed class AppSettingsSnapshot public string LanguageCode { get; set; } = "zh-CN"; + public string? TimeZoneId { get; set; } + public List TopStatusComponentIds { get; set; } = []; public List PinnedTaskbarActions { get; set; } = @@ -33,4 +35,6 @@ public sealed class AppSettingsSnapshot public int DesktopPageCount { get; set; } = 1; public int CurrentDesktopSurfaceIndex { get; set; } = 0; + + public List DesktopComponentPlacements { get; set; } = []; } diff --git a/LanMontainDesktop/Models/DesktopComponentPlacementSnapshot.cs b/LanMontainDesktop/Models/DesktopComponentPlacementSnapshot.cs new file mode 100644 index 0000000..1829a09 --- /dev/null +++ b/LanMontainDesktop/Models/DesktopComponentPlacementSnapshot.cs @@ -0,0 +1,18 @@ +namespace LanMontainDesktop.Models; + +public sealed class DesktopComponentPlacementSnapshot +{ + public string PlacementId { get; set; } = string.Empty; + + public int PageIndex { get; set; } + + public string ComponentId { get; set; } = string.Empty; + + public int Row { get; set; } + + public int Column { get; set; } + + public int WidthCells { get; set; } = 1; + + public int HeightCells { get; set; } = 1; +} diff --git a/LanMontainDesktop/Models/TaskbarActionId.cs b/LanMontainDesktop/Models/TaskbarActionId.cs index 98bba5f..7a89acf 100644 --- a/LanMontainDesktop/Models/TaskbarActionId.cs +++ b/LanMontainDesktop/Models/TaskbarActionId.cs @@ -3,6 +3,6 @@ namespace LanMontainDesktop.Models; public enum TaskbarActionId { MinimizeToWindows, - OpenSettings + OpenSettings, + AddDesktopPage } - diff --git a/LanMontainDesktop/Services/TimeZoneService.cs b/LanMontainDesktop/Services/TimeZoneService.cs new file mode 100644 index 0000000..7afe2a3 --- /dev/null +++ b/LanMontainDesktop/Services/TimeZoneService.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.ObjectModel; + +namespace LanMontainDesktop.Services; + +/// +/// 时区服务,提供时区信息和时间转换功能 +/// +public sealed class TimeZoneService +{ + private TimeZoneInfo _currentTimeZone = TimeZoneInfo.Local; + + /// + /// 当前选中的时区 + /// + public TimeZoneInfo CurrentTimeZone + { + get => _currentTimeZone; + set + { + if (_currentTimeZone != value) + { + _currentTimeZone = value; + TimeZoneChanged?.Invoke(this, EventArgs.Empty); + } + } + } + + /// + /// 时区变更事件 + /// + public event EventHandler? TimeZoneChanged; + + /// + /// 获取所有可用的时区 + /// + public ReadOnlyCollection GetAllTimeZones() + { + return TimeZoneInfo.GetSystemTimeZones(); + } + + /// + /// 获取当前时区的当前时间 + /// + public DateTime GetCurrentTime() + { + return TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, _currentTimeZone); + } + + /// + /// 根据时区ID设置当前时区 + /// + public bool SetTimeZoneById(string timeZoneId) + { + try + { + var timeZone = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId); + CurrentTimeZone = timeZone; + return true; + } + catch (Exception) + { + return false; + } + } + + /// + /// 获取时区显示名称(包含UTC偏移) + /// + public string GetTimeZoneDisplayName(TimeZoneInfo timeZone) + { + var offset = timeZone.BaseUtcOffset; + var sign = offset >= TimeSpan.Zero ? "+" : "-"; + var hours = Math.Abs(offset.Hours); + var minutes = Math.Abs(offset.Minutes); + + return $"(UTC{sign}{hours:D2}:{minutes:D2}) {timeZone.DisplayName}"; + } + + /// + /// 获取常用时区列表 + /// + public TimeZoneInfo[] GetCommonTimeZones() + { + return new[] + { + TimeZoneInfo.Local, // 本地时区 + TimeZoneInfo.FindSystemTimeZoneById("China Standard Time"), // 北京时间 + TimeZoneInfo.FindSystemTimeZoneById("Tokyo Standard Time"), // 东京时间 + TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"), // 太平洋时间 + TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time"), // 东部时间 + TimeZoneInfo.FindSystemTimeZoneById("Central European Standard Time"), // 中欧时间 + TimeZoneInfo.FindSystemTimeZoneById("GMT Standard Time"), // 伦敦时间 + TimeZoneInfo.FindSystemTimeZoneById("UTC"), // 协调世界时 + }; + } +} diff --git a/LanMontainDesktop/Views/Components/ClockWidget.axaml b/LanMontainDesktop/Views/Components/ClockWidget.axaml index a41f36a..a17cc3f 100644 --- a/LanMontainDesktop/Views/Components/ClockWidget.axaml +++ b/LanMontainDesktop/Views/Components/ClockWidget.axaml @@ -8,11 +8,9 @@ x:Class="LanMontainDesktop.Views.Components.ClockWidget"> + CornerRadius="8"> + /// 设置时区服务 + /// + public void SetTimeZoneService(TimeZoneService timeZoneService) + { + if (_timeZoneService != null) + { + _timeZoneService.TimeZoneChanged -= OnTimeZoneChanged; + } + + _timeZoneService = timeZoneService; + _timeZoneService.TimeZoneChanged += OnTimeZoneChanged; + UpdateClock(); + } + private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e) { UpdateClock(); @@ -40,9 +58,14 @@ public partial class ClockWidget : UserControl UpdateClock(); } + private void OnTimeZoneChanged(object? sender, EventArgs e) + { + UpdateClock(); + } + private void UpdateClock() { - var now = DateTime.Now; + var now = _timeZoneService?.GetCurrentTime() ?? DateTime.Now; TimeTextBlock.Text = now.ToString("HH:mm:ss", CultureInfo.CurrentCulture); } diff --git a/LanMontainDesktop/Views/Components/DateWidget.axaml b/LanMontainDesktop/Views/Components/DateWidget.axaml new file mode 100644 index 0000000..3e56cf2 --- /dev/null +++ b/LanMontainDesktop/Views/Components/DateWidget.axaml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LanMontainDesktop/Views/Components/DateWidget.axaml.cs b/LanMontainDesktop/Views/Components/DateWidget.axaml.cs new file mode 100644 index 0000000..c296f1c --- /dev/null +++ b/LanMontainDesktop/Views/Components/DateWidget.axaml.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Threading; +using LanMontainDesktop.Services; + +namespace LanMontainDesktop.Views.Components; + +public partial class DateWidget : UserControl +{ + private readonly DispatcherTimer _timer = new() + { + Interval = TimeSpan.FromMinutes(1) + }; + + private TimeZoneService? _timeZoneService; + + public DateWidget() + { + InitializeComponent(); + + _timer.Tick += OnTimerTick; + AttachedToVisualTree += OnAttachedToVisualTree; + DetachedFromVisualTree += OnDetachedFromVisualTree; + UpdateDate(); + } + + /// + /// 设置时区服务 + /// + public void SetTimeZoneService(TimeZoneService timeZoneService) + { + if (_timeZoneService != null) + { + _timeZoneService.TimeZoneChanged -= OnTimeZoneChanged; + } + + _timeZoneService = timeZoneService; + _timeZoneService.TimeZoneChanged += OnTimeZoneChanged; + UpdateDate(); + } + + private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e) + { + UpdateDate(); + _timer.Start(); + } + + private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e) + { + _timer.Stop(); + } + + private void OnTimerTick(object? sender, EventArgs e) + { + UpdateDate(); + } + + private void OnTimeZoneChanged(object? sender, EventArgs e) + { + UpdateDate(); + } + + private void UpdateDate() + { + var now = _timeZoneService?.GetCurrentTime() ?? DateTime.Now; + var culture = CultureInfo.CurrentCulture; + + // 右侧:今日详情 + TodayDayTextBlock.Text = now.Day.ToString(); + TodayWeekdayTextBlock.Text = now.ToString("dddd", culture); + + // 左侧:月历 + CalendarMonthYearTextBlock.Text = now.ToString("yyyy年M月", culture); + + // 生成月历 + GenerateCalendar(now); + } + + private void GenerateCalendar(DateTime currentDate) + { + // 清空之前的日期(保留星期标题) + var childrenToRemove = new List(); + foreach (var child in CalendarGrid.Children) + { + if (child is TextBlock tb && tb.Tag?.ToString() == "day") + { + childrenToRemove.Add(tb); + } + } + foreach (var child in childrenToRemove) + { + CalendarGrid.Children.Remove(child); + } + + var year = currentDate.Year; + var month = currentDate.Month; + var today = currentDate.Day; + + // 获取该月第一天 + var firstDayOfMonth = new DateTime(year, month, 1); + var daysInMonth = DateTime.DaysInMonth(year, month); + var startDayOfWeek = (int)firstDayOfMonth.DayOfWeek; // 0 = Sunday + + // 生成日期 + for (int day = 1; day <= daysInMonth; day++) + { + var row = ((day + startDayOfWeek - 1) / 7) + 1; // +1 because row 0 is weekday headers + var col = (day + startDayOfWeek - 1) % 7; + + if (row > 5) continue; // 最多显示6行 + + var dayText = new TextBlock + { + Text = day.ToString(), + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, + FontSize = 10, + Tag = "day" + }; + + // 今天高亮 + if (day == today) + { + // 使用主题色高亮今天 + var accentBrush = this.TryFindResource("AdaptiveAccentBrush", out var accent) + ? accent as IBrush + : Brushes.Blue; + var onAccentBrush = this.TryFindResource("AdaptiveOnAccentBrush", out var onAccent) + ? onAccent as IBrush + : Brushes.White; + + dayText.Foreground = onAccentBrush; + dayText.FontWeight = FontWeight.Bold; + dayText.Background = new SolidColorBrush(Colors.Transparent); + + // 添加背景圆 + var highlight = new Border + { + Background = accentBrush, + CornerRadius = new CornerRadius(10), + Width = 20, + Height = 20, + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, + Child = dayText + }; + Grid.SetRow(highlight, row); + Grid.SetColumn(highlight, col); + CalendarGrid.Children.Add(highlight); + } + else + { + // 使用主题次要文本颜色 + var secondaryBrush = this.TryFindResource("AdaptiveTextSecondaryBrush", out var secondary) + ? secondary as IBrush + : Brushes.Gray; + dayText.Foreground = secondaryBrush; + Grid.SetRow(dayText, row); + Grid.SetColumn(dayText, col); + CalendarGrid.Children.Add(dayText); + } + } + } + + public void ApplyCellSize(double cellSize) + { + // 根据格子大小调整圆角 + RootBorder.CornerRadius = new CornerRadius(Math.Clamp(cellSize * 0.12, 8, 20)); + + // 调整字体大小 + var baseFontSize = cellSize * 0.25; + TodayDayTextBlock.FontSize = Math.Clamp(baseFontSize * 2.8, 28, 72); + TodayWeekdayTextBlock.FontSize = Math.Clamp(baseFontSize * 0.6, 10, 16); + CalendarMonthYearTextBlock.FontSize = Math.Clamp(baseFontSize * 0.55, 9, 14); + } +} diff --git a/LanMontainDesktop/Views/MainWindow.ComponentSystem.cs b/LanMontainDesktop/Views/MainWindow.ComponentSystem.cs index 37d333f..334c9f3 100644 --- a/LanMontainDesktop/Views/MainWindow.ComponentSystem.cs +++ b/LanMontainDesktop/Views/MainWindow.ComponentSystem.cs @@ -3,21 +3,82 @@ using System.Collections.Generic; using System.Linq; using Avalonia; using Avalonia.Controls; +using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Threading; +using FluentIcons.Avalonia; +using FluentIcons.Common; using LanMontainDesktop.ComponentSystem; using LanMontainDesktop.Models; +using LanMontainDesktop.Views.Components; namespace LanMontainDesktop.Views; public partial class MainWindow { + private readonly List _desktopComponentPlacements = []; + private readonly Dictionary _desktopPageComponentGrids = new(); + + private const string DesktopComponentClass = "desktop-component"; + private const string DesktopComponentHostClass = "desktop-component-host"; + + private bool _isDesktopComponentDragActive; + private DesktopComponentDragState? _desktopComponentDrag; + private Border? _desktopComponentDragGhost; + + private string? _componentLibraryActiveCategoryId; + private int _componentLibraryCategoryIndex; + private int _componentLibraryComponentIndex; + private double _componentLibraryCategoryPageWidth; + private double _componentLibraryComponentPageWidth; + private TranslateTransform? _componentLibraryCategoryHostTransform; + private TranslateTransform? _componentLibraryComponentHostTransform; + private IReadOnlyList _componentLibraryCategories = Array.Empty(); + private IReadOnlyList _componentLibraryActiveComponents = Array.Empty(); + private bool _isComponentLibraryCategoryGestureActive; + private bool _isComponentLibraryComponentGestureActive; + private Point _componentLibraryCategoryGestureStartPoint; + private Point _componentLibraryCategoryGestureCurrentPoint; + private double _componentLibraryCategoryGestureBaseOffset; + private Point _componentLibraryComponentGestureStartPoint; + private Point _componentLibraryComponentGestureCurrentPoint; + private double _componentLibraryComponentGestureBaseOffset; + + private enum DesktopComponentDragKind + { + None, + NewFromLibrary, + MoveExisting + } + + private sealed class DesktopComponentDragState + { + public DesktopComponentDragKind Kind { get; init; } + public string ComponentId { get; init; } = string.Empty; + public string PlacementId { get; init; } = string.Empty; + public int PageIndex { get; init; } + public int WidthCells { get; init; } + public int HeightCells { get; init; } + public Point PointerOffset { get; init; } + public Border? SourceHost { get; init; } + public int TargetRow { get; set; } + public int TargetColumn { get; set; } + } + + private sealed record ComponentLibraryCategory( + string Id, + Symbol Icon, + string Title, + IReadOnlyList Components); + private void OnOpenComponentLibraryClick(object? sender, RoutedEventArgs e) { + // "Desktop edit" toggle. While editing, show the component library window. if (_isComponentLibraryOpen) { + CloseComponentLibraryWindow(reopenSettings: false); return; } @@ -160,13 +221,13 @@ public partial class MainWindow var showMinimize = _pinnedTaskbarActions.Contains(TaskbarActionId.MinimizeToWindows); var showSettings = _pinnedTaskbarActions.Contains(TaskbarActionId.OpenSettings); - var showComponentLibrary = _isSettingsOpen || _isComponentLibraryOpen; + var showDesktopEdit = true; BackToWindowsButton.IsVisible = showMinimize; - OpenComponentLibraryButton.IsVisible = showComponentLibrary; + OpenComponentLibraryButton.IsVisible = showDesktopEdit; OpenSettingsButton.IsVisible = showSettings; WallpaperPreviewBackButtonVisual.IsVisible = showMinimize; - WallpaperPreviewComponentLibraryVisual.IsVisible = showComponentLibrary; + WallpaperPreviewComponentLibraryVisual.IsVisible = showDesktopEdit; WallpaperPreviewSettingsButtonIcon.IsVisible = showSettings; if (TaskbarFixedActionsHost is not null) @@ -176,7 +237,7 @@ public partial class MainWindow if (TaskbarSettingsActionHost is not null) { - TaskbarSettingsActionHost.IsVisible = showSettings || showComponentLibrary; + TaskbarSettingsActionHost.IsVisible = showSettings || showDesktopEdit; } if (WallpaperPreviewTaskbarFixedActionsHost is not null) @@ -186,10 +247,12 @@ public partial class MainWindow if (WallpaperPreviewTaskbarSettingsActionHost is not null) { - WallpaperPreviewTaskbarSettingsActionHost.IsVisible = showSettings || showComponentLibrary; + WallpaperPreviewTaskbarSettingsActionHost.IsVisible = showSettings || showDesktopEdit; } - var dynamicActions = ResolveDynamicTaskbarActions(context); + var dynamicActions = ResolveDynamicTaskbarActions(context) + .Where(action => action.IsVisible) + .ToList(); var hasDynamicActions = dynamicActions.Count > 0; BuildDynamicTaskbarVisuals(dynamicActions); @@ -236,6 +299,8 @@ public partial class MainWindow } _isComponentLibraryOpen = true; + UpdateDesktopComponentHostEditState(); + ShowComponentLibraryCategoryView(); ComponentLibraryWindow.IsVisible = true; ComponentLibraryWindow.Opacity = 0; ApplyTaskbarActionVisibility(GetCurrentTaskbarContext()); @@ -247,6 +312,7 @@ public partial class MainWindow return; } + BuildComponentLibraryCategoryPages(); ComponentLibraryWindow.Opacity = 1; }, DispatcherPriority.Background); } @@ -259,6 +325,8 @@ public partial class MainWindow } _isComponentLibraryOpen = false; + CancelDesktopComponentDrag(); + UpdateDesktopComponentHostEditState(); ComponentLibraryWindow.Opacity = 0; ApplyTaskbarActionVisibility(GetCurrentTaskbarContext()); @@ -280,8 +348,30 @@ public partial class MainWindow }, TimeSpan.FromMilliseconds(200)); } + private void InitializeDesktopComponentDragHandlers() + { + // Global handlers: we capture the pointer during drag, then track move/release anywhere. + AddHandler(PointerMovedEvent, OnDesktopComponentDragPointerMoved, RoutingStrategies.Tunnel); + AddHandler(PointerReleasedEvent, OnDesktopComponentDragPointerReleased, RoutingStrategies.Tunnel); + AddHandler(PointerCaptureLostEvent, OnDesktopComponentDragPointerCaptureLost, RoutingStrategies.Tunnel); + } + private IReadOnlyList ResolveDynamicTaskbarActions(TaskbarContext context) { + if (context == TaskbarContext.Desktop && _isComponentLibraryOpen) + { + var canAddPage = _desktopPageCount < MaxDesktopPageCount; + return + [ + new TaskbarActionItem( + TaskbarActionId.AddDesktopPage, + L("desktop.add_page", "Add page"), + "Add", + IsVisible: canAddPage, + CommandKey: "desktop.add_page") + ]; + } + if (!_enableDynamicTaskbarActions) { return Array.Empty(); @@ -324,8 +414,10 @@ public partial class MainWindow Background = Brushes.Transparent, BorderThickness = new Thickness(0), Padding = new Thickness(12, 6), - Foreground = Foreground + Foreground = Foreground, + Tag = action.CommandKey }; + button.Click += OnDynamicTaskbarActionClick; TaskbarDynamicActionsPanel.Children.Add(button); @@ -346,73 +438,1187 @@ public partial class MainWindow } } - private void PopulateComponentLibraryItems() + private void OnDynamicTaskbarActionClick(object? sender, RoutedEventArgs e) { - if (ComponentLibraryItemsPanel is null || ComponentLibraryEmptyTextBlock is null) + if (sender is not Button button || button.Tag is not string commandKey) { return; } - ComponentLibraryItemsPanel.Children.Clear(); + switch (commandKey) + { + case "desktop.add_page": + AddDesktopPage(); + break; + } + } + private void AddDesktopPage() + { + if (_desktopPageCount >= MaxDesktopPageCount) + { + return; + } + + _desktopPageCount = Math.Clamp(_desktopPageCount + 1, MinDesktopPageCount, MaxDesktopPageCount); + _currentDesktopSurfaceIndex = Math.Clamp(_desktopPageCount - 1, 0, LauncherSurfaceIndex); + RebuildDesktopGrid(); + PersistSettings(); + } + + private void InitializeDesktopComponentPlacements(AppSettingsSnapshot snapshot) + { + _desktopComponentPlacements.Clear(); + + if (snapshot.DesktopComponentPlacements is null) + { + return; + } + + foreach (var placement in snapshot.DesktopComponentPlacements) + { + if (placement is null || string.IsNullOrWhiteSpace(placement.ComponentId)) + { + continue; + } + + var placementId = string.IsNullOrWhiteSpace(placement.PlacementId) + ? Guid.NewGuid().ToString("N") + : placement.PlacementId.Trim(); + var componentId = placement.ComponentId.Trim(); + if (!_componentRegistry.TryGetDefinition(componentId, out var definition) || !definition.AllowDesktopPlacement) + { + continue; + } + + var (widthCells, heightCells) = ComponentPlacementRules.EnsureMinimumSize( + definition, + placement.WidthCells, + placement.HeightCells); + + _desktopComponentPlacements.Add(new DesktopComponentPlacementSnapshot + { + PlacementId = placementId, + PageIndex = Math.Max(0, placement.PageIndex), + ComponentId = componentId, + Row = Math.Max(0, placement.Row), + Column = Math.Max(0, placement.Column), + WidthCells = widthCells, + HeightCells = heightCells + }); + } + } + + private void RestoreDesktopPageComponents(int pageIndex) + { + if (!_desktopPageComponentGrids.TryGetValue(pageIndex, out var pageGrid)) + { + return; + } + + pageGrid.Children.Clear(); + + var maxColumns = pageGrid.ColumnDefinitions.Count; + var maxRows = pageGrid.RowDefinitions.Count; + if (maxColumns <= 0 || maxRows <= 0) + { + return; + } + + foreach (var placement in _desktopComponentPlacements.Where(p => p.PageIndex == pageIndex)) + { + if (!_componentRegistry.TryGetDefinition(placement.ComponentId, out var definition) || !definition.AllowDesktopPlacement) + { + continue; + } + + var (widthCells, heightCells) = ComponentPlacementRules.EnsureMinimumSize( + definition, + placement.WidthCells, + placement.HeightCells); + + var clampedColumn = Math.Clamp(placement.Column, 0, Math.Max(0, maxColumns - widthCells)); + var clampedRow = Math.Clamp(placement.Row, 0, Math.Max(0, maxRows - heightCells)); + + var host = CreateDesktopComponentHost(placement); + if (host is null) + { + continue; + } + + placement.Column = clampedColumn; + placement.Row = clampedRow; + placement.WidthCells = widthCells; + placement.HeightCells = heightCells; + + Grid.SetColumn(host, clampedColumn); + Grid.SetRow(host, clampedRow); + Grid.SetColumnSpan(host, widthCells); + Grid.SetRowSpan(host, heightCells); + pageGrid.Children.Add(host); + } + } + + private void PlaceDesktopComponentOnPage(string componentId, int pageIndex, int row, int column) + { + if (!_desktopPageComponentGrids.TryGetValue(pageIndex, out var pageGrid)) + { + return; + } + + if (!_componentRegistry.TryGetDefinition(componentId, out var definition) || !definition.AllowDesktopPlacement) + { + return; + } + + var (widthCells, heightCells) = ComponentPlacementRules.EnsureMinimumSize( + definition, + definition.MinWidthCells, + definition.MinHeightCells); + + var maxColumns = pageGrid.ColumnDefinitions.Count; + var maxRows = pageGrid.RowDefinitions.Count; + if (maxColumns <= 0 || maxRows <= 0) + { + return; + } + + column = Math.Clamp(column, 0, Math.Max(0, maxColumns - widthCells)); + row = Math.Clamp(row, 0, Math.Max(0, maxRows - heightCells)); + + var placementId = Guid.NewGuid().ToString("N"); + var placement = new DesktopComponentPlacementSnapshot + { + PlacementId = placementId, + PageIndex = pageIndex, + ComponentId = componentId, + Row = row, + Column = column, + WidthCells = widthCells, + HeightCells = heightCells + }; + + var host = CreateDesktopComponentHost(placement); + if (host is null) + { + return; + } + + Grid.SetColumn(host, column); + Grid.SetRow(host, row); + Grid.SetColumnSpan(host, widthCells); + Grid.SetRowSpan(host, heightCells); + pageGrid.Children.Add(host); + + _desktopComponentPlacements.Add(placement); + PersistSettings(); + + ApplyTaskbarActionVisibility(GetCurrentTaskbarContext()); + } + + private Border? CreateDesktopComponentHost(DesktopComponentPlacementSnapshot placement) + { + if (string.IsNullOrWhiteSpace(placement.PlacementId)) + { + placement.PlacementId = Guid.NewGuid().ToString("N"); + } + + var component = CreateDesktopComponentControl(placement.ComponentId); + if (component is null) + { + return null; + } + + var host = new Border + { + Tag = placement.PlacementId, + Background = Brushes.Transparent, + ClipToBounds = true, + Child = component + }; + host.Classes.Add(DesktopComponentHostClass); + ApplyDesktopEditStateToHost(host, _isComponentLibraryOpen); + host.PointerPressed += OnDesktopComponentHostPointerPressed; + return host; + } + + private Control? CreateDesktopComponentControl(string componentId) + { + if (componentId == BuiltInComponentIds.Date) + { + var widget = new DateWidget(); + widget.SetTimeZoneService(_timeZoneService); + widget.ApplyCellSize(_currentDesktopCellSize); + widget.Classes.Add(DesktopComponentClass); + return widget; + } + + return null; + } + + private void CollapseComponentLibraryPanel() + { + // Animate component library panel collapsing downward + if (ComponentLibraryWindow is not null) + { + ComponentLibraryWindow.Height = 0; + ComponentLibraryWindow.IsVisible = false; + } + + _isComponentLibraryOpen = false; + CancelDesktopComponentDrag(); + UpdateDesktopComponentHostEditState(); + UpdateComponentLibraryLayout(_currentDesktopCellSize); + } + + private void UpdateDesktopComponentHostEditState() + { + foreach (var pageGrid in _desktopPageComponentGrids.Values) + { + foreach (var child in pageGrid.Children) + { + if (child is Border host && host.Classes.Contains(DesktopComponentHostClass)) + { + ApplyDesktopEditStateToHost(host, _isComponentLibraryOpen); + } + } + } + } + + private void ApplyDesktopEditStateToHost(Border host, bool isEditMode) + { + host.IsHitTestVisible = isEditMode; + host.CornerRadius = new CornerRadius(Math.Clamp(_currentDesktopCellSize * 0.22, 8, 18)); + + if (isEditMode) + { + host.BorderThickness = new Thickness(Math.Clamp(_currentDesktopCellSize * 0.04, 1, 3)); + host.BorderBrush = GetThemeBrush("AdaptiveAccentBrush"); + } + else + { + host.BorderThickness = new Thickness(0); + host.BorderBrush = null; + } + + if (host.Child is Control child) + { + // In edit mode, prefer drag interactions over component interactions. + child.IsHitTestVisible = !isEditMode; + } + } + + private void OnDesktopComponentHostPointerPressed(object? sender, PointerPressedEventArgs e) + { + if (!_isComponentLibraryOpen || _isDesktopComponentDragActive) + { + return; + } + + if (DesktopPagesViewport is null || + sender is not Border host || + host.Tag is not string placementId || + !e.GetCurrentPoint(host).Properties.IsLeftButtonPressed) + { + return; + } + + var placement = _desktopComponentPlacements.FirstOrDefault(p => + string.Equals(p.PlacementId, placementId, StringComparison.OrdinalIgnoreCase)); + if (placement is null) + { + return; + } + + BeginDesktopComponentMoveDrag(host, placement, e); + e.Handled = true; + } + + private void BeginDesktopComponentMoveDrag(Border sourceHost, DesktopComponentPlacementSnapshot placement, PointerPressedEventArgs e) + { + if (DesktopEditDragLayer is null || + DesktopPagesViewport is null || + _currentDesktopCellSize <= 0 || + !_componentRegistry.TryGetDefinition(placement.ComponentId, out var definition)) + { + return; + } + + var (widthCells, heightCells) = ComponentPlacementRules.EnsureMinimumSize( + definition, + placement.WidthCells, + placement.HeightCells); + + var pointerInViewport = e.GetPosition(DesktopPagesViewport); + var topLeft = new Point(placement.Column * _currentDesktopCellSize, placement.Row * _currentDesktopCellSize); + var pointerOffset = pointerInViewport - topLeft; + + sourceHost.Opacity = 0.35; + + _desktopComponentDrag = new DesktopComponentDragState + { + Kind = DesktopComponentDragKind.MoveExisting, + ComponentId = placement.ComponentId, + PlacementId = placement.PlacementId, + PageIndex = placement.PageIndex, + WidthCells = widthCells, + HeightCells = heightCells, + PointerOffset = pointerOffset, + SourceHost = sourceHost + }; + _isDesktopComponentDragActive = true; + + EnsureDesktopComponentDragGhost(placement.ComponentId, widthCells, heightCells); + UpdateDesktopComponentDragVisual(pointerInViewport); + + e.Pointer.Capture(this); + } + + private void BeginDesktopComponentNewDrag(string componentId, PointerPressedEventArgs e) + { + if (!_isComponentLibraryOpen || + _isDesktopComponentDragActive || + DesktopEditDragLayer is null || + DesktopPagesViewport is null || + _currentDesktopCellSize <= 0 || + !_componentRegistry.TryGetDefinition(componentId, out var definition) || + !definition.AllowDesktopPlacement) + { + return; + } + + var (widthCells, heightCells) = ComponentPlacementRules.EnsureMinimumSize( + definition, + definition.MinWidthCells, + definition.MinHeightCells); + + // Center the component under the pointer while dragging from the library. + var pointerOffset = new Point( + (widthCells * _currentDesktopCellSize) * 0.5, + (heightCells * _currentDesktopCellSize) * 0.5); + + _desktopComponentDrag = new DesktopComponentDragState + { + Kind = DesktopComponentDragKind.NewFromLibrary, + ComponentId = componentId, + PageIndex = _currentDesktopSurfaceIndex, + WidthCells = widthCells, + HeightCells = heightCells, + PointerOffset = pointerOffset + }; + _isDesktopComponentDragActive = true; + + EnsureDesktopComponentDragGhost(componentId, widthCells, heightCells); + var pointerInViewport = e.GetPosition(DesktopPagesViewport); + UpdateDesktopComponentDragVisual(pointerInViewport); + + e.Pointer.Capture(this); + } + + private void EnsureDesktopComponentDragGhost(string componentId, int widthCells, int heightCells) + { + if (DesktopEditDragLayer is null) + { + return; + } + + DesktopEditDragLayer.Children.Clear(); + + var ghostWidth = Math.Max(1, widthCells * _currentDesktopCellSize); + var ghostHeight = Math.Max(1, heightCells * _currentDesktopCellSize); + + var ghostContent = CreateDesktopComponentControl(componentId); + if (ghostContent is not null) + { + ghostContent.IsHitTestVisible = false; + } + + _desktopComponentDragGhost = new Border + { + Width = ghostWidth, + Height = ghostHeight, + CornerRadius = new CornerRadius(Math.Clamp(_currentDesktopCellSize * 0.22, 8, 18)), + Background = new SolidColorBrush(Color.Parse("#331E40AF")), + BorderBrush = GetThemeBrush("AdaptiveAccentBrush"), + BorderThickness = new Thickness(Math.Clamp(_currentDesktopCellSize * 0.04, 1, 3)), + Child = ghostContent, + Opacity = 0.92, + IsHitTestVisible = false + }; + + DesktopEditDragLayer.Children.Add(_desktopComponentDragGhost); + } + + private void OnDesktopComponentDragPointerMoved(object? sender, PointerEventArgs e) + { + if (!_isDesktopComponentDragActive || _desktopComponentDrag is null || DesktopPagesViewport is null) + { + return; + } + + UpdateDesktopComponentDragVisual(e.GetPosition(DesktopPagesViewport)); + } + + private void OnDesktopComponentDragPointerReleased(object? sender, PointerReleasedEventArgs e) + { + if (!_isDesktopComponentDragActive || _desktopComponentDrag is null || DesktopPagesViewport is null) + { + return; + } + + var pointerInViewport = e.GetPosition(DesktopPagesViewport); + var success = TryCompleteDesktopComponentDrag(pointerInViewport); + CancelDesktopComponentDrag(); + e.Pointer.Capture(null); + if (success) + { + e.Handled = true; + } + } + + private void OnDesktopComponentDragPointerCaptureLost(object? sender, PointerCaptureLostEventArgs e) + { + if (!_isDesktopComponentDragActive) + { + return; + } + + CancelDesktopComponentDrag(); + } + + private void UpdateDesktopComponentDragVisual(Point pointerInViewport) + { + if (_desktopComponentDragGhost is null || _desktopComponentDrag is null || DesktopPagesViewport is null) + { + return; + } + + var withinViewport = + pointerInViewport.X >= 0 && + pointerInViewport.Y >= 0 && + pointerInViewport.X <= DesktopPagesViewport.Bounds.Width && + pointerInViewport.Y <= DesktopPagesViewport.Bounds.Height; + + if (!withinViewport || + !TryGetDesktopComponentDropCell(pointerInViewport, _desktopComponentDrag, out var row, out var column)) + { + _desktopComponentDragGhost.IsVisible = false; + return; + } + + _desktopComponentDragGhost.IsVisible = true; + _desktopComponentDrag.TargetRow = row; + _desktopComponentDrag.TargetColumn = column; + Canvas.SetLeft(_desktopComponentDragGhost, column * _currentDesktopCellSize); + Canvas.SetTop(_desktopComponentDragGhost, row * _currentDesktopCellSize); + } + + private bool TryGetDesktopComponentDropCell( + Point pointerInViewport, + DesktopComponentDragState state, + out int row, + out int column) + { + row = 0; + column = 0; + + if (_currentDesktopCellSize <= 0 || + _currentDesktopSurfaceIndex < 0 || + _currentDesktopSurfaceIndex >= _desktopPageCount || + !_desktopPageComponentGrids.TryGetValue(_currentDesktopSurfaceIndex, out var pageGrid)) + { + return false; + } + + var maxColumns = pageGrid.ColumnDefinitions.Count; + var maxRows = pageGrid.RowDefinitions.Count; + if (maxColumns <= 0 || maxRows <= 0) + { + return false; + } + + var x = pointerInViewport.X - state.PointerOffset.X; + var y = pointerInViewport.Y - state.PointerOffset.Y; + + column = (int)Math.Floor(x / _currentDesktopCellSize); + row = (int)Math.Floor(y / _currentDesktopCellSize); + + column = Math.Clamp(column, 0, Math.Max(0, maxColumns - state.WidthCells)); + row = Math.Clamp(row, 0, Math.Max(0, maxRows - state.HeightCells)); + return true; + } + + private bool TryCompleteDesktopComponentDrag(Point pointerInViewport) + { + if (_desktopComponentDrag is null || + _currentDesktopCellSize <= 0 || + _currentDesktopSurfaceIndex < 0 || + _currentDesktopSurfaceIndex >= _desktopPageCount) + { + return false; + } + + if (!TryGetDesktopComponentDropCell(pointerInViewport, _desktopComponentDrag, out var row, out var column)) + { + return false; + } + + switch (_desktopComponentDrag.Kind) + { + case DesktopComponentDragKind.NewFromLibrary: + PlaceDesktopComponentOnPage(_desktopComponentDrag.ComponentId, _currentDesktopSurfaceIndex, row, column); + return true; + case DesktopComponentDragKind.MoveExisting: + return TryMoveExistingDesktopComponent(_desktopComponentDrag.PlacementId, row, column); + default: + return false; + } + } + + private bool TryMoveExistingDesktopComponent(string placementId, int row, int column) + { + if (string.IsNullOrWhiteSpace(placementId) || + _desktopComponentDrag?.SourceHost is null || + _desktopComponentDrag.Kind != DesktopComponentDragKind.MoveExisting) + { + return false; + } + + var placement = _desktopComponentPlacements.FirstOrDefault(p => + string.Equals(p.PlacementId, placementId, StringComparison.OrdinalIgnoreCase)); + if (placement is null) + { + return false; + } + + placement.Row = Math.Max(0, row); + placement.Column = Math.Max(0, column); + + Grid.SetRow(_desktopComponentDrag.SourceHost, placement.Row); + Grid.SetColumn(_desktopComponentDrag.SourceHost, placement.Column); + + _desktopComponentDrag.SourceHost.Opacity = 1; + ApplyDesktopEditStateToHost(_desktopComponentDrag.SourceHost, _isComponentLibraryOpen); + PersistSettings(); + return true; + } + + private void CancelDesktopComponentDrag() + { + if (!_isDesktopComponentDragActive) + { + return; + } + + if (_desktopComponentDrag?.SourceHost is not null) + { + _desktopComponentDrag.SourceHost.Opacity = 1; + ApplyDesktopEditStateToHost(_desktopComponentDrag.SourceHost, _isComponentLibraryOpen); + } + + _desktopComponentDrag = null; + _isDesktopComponentDragActive = false; + + if (DesktopEditDragLayer is not null) + { + DesktopEditDragLayer.Children.Clear(); + } + + _desktopComponentDragGhost = null; + } + + private void ShowComponentLibraryCategoryView() + { + if (ComponentLibraryCategoriesView is not null) + { + ComponentLibraryCategoriesView.IsVisible = true; + } + + if (ComponentLibraryComponentsView is not null) + { + ComponentLibraryComponentsView.IsVisible = false; + } + } + + private void ShowComponentLibraryComponentsView() + { + if (ComponentLibraryCategoriesView is not null) + { + ComponentLibraryCategoriesView.IsVisible = false; + } + + if (ComponentLibraryComponentsView is not null) + { + ComponentLibraryComponentsView.IsVisible = true; + } + } + + private void BuildComponentLibraryCategoryPages() + { + if (ComponentLibraryCategoryViewport is null || + ComponentLibraryCategoryPagesHost is null || + ComponentLibraryCategoryPagesContainer is null || + ComponentLibraryEmptyTextBlock is null) + { + return; + } + + _componentLibraryCategories = GetComponentLibraryCategories(); + var categoryCount = _componentLibraryCategories.Count; + ComponentLibraryEmptyTextBlock.IsVisible = categoryCount == 0; + + ComponentLibraryCategoryPagesContainer.Children.Clear(); + ComponentLibraryCategoryPagesContainer.RowDefinitions.Clear(); + ComponentLibraryCategoryPagesContainer.ColumnDefinitions.Clear(); + if (categoryCount == 0) + { + _componentLibraryCategoryIndex = 0; + _componentLibraryActiveCategoryId = null; + return; + } + + var viewportWidth = ComponentLibraryCategoryViewport.Bounds.Width; + if (viewportWidth <= 1 && ComponentLibraryWindow is not null) + { + viewportWidth = Math.Max(1, ComponentLibraryWindow.Bounds.Width - 48); + } + + var viewportHeight = ComponentLibraryCategoryViewport.Bounds.Height; + if (viewportHeight <= 1 && ComponentLibraryWindow is not null) + { + viewportHeight = Math.Max(1, ComponentLibraryWindow.Bounds.Height - 120); + } + + _componentLibraryCategoryPageWidth = Math.Max(1, viewportWidth); + ComponentLibraryCategoryPagesHost.Width = _componentLibraryCategoryPageWidth * categoryCount; + ComponentLibraryCategoryPagesHost.Height = viewportHeight; + ComponentLibraryCategoryPagesContainer.Width = ComponentLibraryCategoryPagesHost.Width; + ComponentLibraryCategoryPagesContainer.Height = viewportHeight; + + ComponentLibraryCategoryPagesContainer.RowDefinitions.Add(new RowDefinition(new GridLength(viewportHeight, GridUnitType.Pixel))); + for (var i = 0; i < categoryCount; i++) + { + ComponentLibraryCategoryPagesContainer.ColumnDefinitions.Add( + new ColumnDefinition(new GridLength(_componentLibraryCategoryPageWidth, GridUnitType.Pixel))); + } + + if (!string.IsNullOrWhiteSpace(_componentLibraryActiveCategoryId)) + { + var activeIndex = _componentLibraryCategories + .Select((category, index) => (category, index)) + .FirstOrDefault(tuple => + string.Equals(tuple.category.Id, _componentLibraryActiveCategoryId, StringComparison.OrdinalIgnoreCase)) + .index; + _componentLibraryCategoryIndex = Math.Clamp(activeIndex, 0, Math.Max(0, categoryCount - 1)); + } + else + { + _componentLibraryCategoryIndex = Math.Clamp(_componentLibraryCategoryIndex, 0, Math.Max(0, categoryCount - 1)); + } + + _componentLibraryActiveCategoryId = _componentLibraryCategories[_componentLibraryCategoryIndex].Id; + + for (var i = 0; i < categoryCount; i++) + { + var category = _componentLibraryCategories[i]; + var page = new Grid + { + Width = _componentLibraryCategoryPageWidth, + Height = viewportHeight, + Background = Brushes.Transparent + }; + + var cardWidth = Math.Clamp(_componentLibraryCategoryPageWidth * 0.64, 160, 260); + var cardHeight = Math.Clamp(viewportHeight * 0.70, 140, 220); + + var iconSize = Math.Clamp(cardHeight * 0.34, 30, 56); + + var card = new Border + { + Classes = { "glass-panel" }, + Width = cardWidth, + Height = cardHeight, + CornerRadius = new CornerRadius(18), + Padding = new Thickness(18), + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Child = new StackPanel + { + Spacing = 12, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Children = + { + new SymbolIcon + { + Symbol = category.Icon, + IconVariant = IconVariant.Regular, + FontSize = iconSize, + HorizontalAlignment = HorizontalAlignment.Center + }, + new TextBlock + { + Text = category.Title, + FontSize = Math.Clamp(cardHeight * 0.14, 12, 18), + FontWeight = FontWeight.SemiBold, + HorizontalAlignment = HorizontalAlignment.Center, + Foreground = GetThemeBrush("AdaptiveTextPrimaryBrush") + } + } + } + }; + + page.Children.Add(card); + + Grid.SetRow(page, 0); + Grid.SetColumn(page, i); + ComponentLibraryCategoryPagesContainer.Children.Add(page); + } + + _componentLibraryCategoryHostTransform = ComponentLibraryCategoryPagesHost.RenderTransform as TranslateTransform; + if (_componentLibraryCategoryHostTransform is null) + { + _componentLibraryCategoryHostTransform = new TranslateTransform(); + ComponentLibraryCategoryPagesHost.RenderTransform = _componentLibraryCategoryHostTransform; + } + + ApplyComponentLibraryCategoryOffset(); + + if (ComponentLibraryBackTextBlock is not null) + { + ComponentLibraryBackTextBlock.Text = L("common.back", "Back"); + } + } + + private IReadOnlyList GetComponentLibraryCategories() + { var definitions = _componentRegistry .GetAll() .Where(definition => definition.AllowDesktopPlacement) .ToList(); - foreach (var definition in definitions) + if (definitions.Count == 0) { - var title = new TextBlock - { - Text = definition.DisplayName, - FontWeight = FontWeight.SemiBold, - Foreground = Foreground - }; - - var details = new TextBlock - { - Text = $"Min {definition.MinWidthCells}x{definition.MinHeightCells}", - Foreground = Foreground, - Opacity = 0.8 - }; - - var category = new TextBlock - { - Text = definition.Category, - Foreground = Foreground, - Opacity = 0.68 - }; - - var content = new StackPanel - { - Spacing = 5, - VerticalAlignment = VerticalAlignment.Center, - HorizontalAlignment = HorizontalAlignment.Stretch - }; - content.Children.Add(title); - content.Children.Add(details); - content.Children.Add(category); - - var card = new Border - { - Classes = { "glass-panel" }, - BorderThickness = new Thickness(0), - CornerRadius = new CornerRadius(12), - Padding = new Thickness(12, 10), - Margin = new Thickness(0, 0, 10, 10), - Width = 180, - MinHeight = 92, - Child = content - }; - - ComponentLibraryItemsPanel.Children.Add(card); + return Array.Empty(); } - var hasAny = definitions.Count > 0; - ComponentLibraryEmptyTextBlock.IsVisible = !hasAny; - if (ComponentLibraryItemsScrollViewer is not null) + return definitions + .GroupBy(definition => definition.Category, StringComparer.OrdinalIgnoreCase) + .OrderBy(group => group.Key, StringComparer.OrdinalIgnoreCase) + .Select(group => + { + var categoryId = string.IsNullOrWhiteSpace(group.Key) ? "Other" : group.Key.Trim(); + var components = group + .OrderBy(definition => definition.DisplayName, StringComparer.OrdinalIgnoreCase) + .ToList(); + return new ComponentLibraryCategory( + categoryId, + ResolveComponentLibraryCategoryIcon(categoryId), + GetLocalizedComponentLibraryCategoryTitle(categoryId), + components); + }) + .ToList(); + } + + private Symbol ResolveComponentLibraryCategoryIcon(string categoryId) + { + if (string.Equals(categoryId, "Date", StringComparison.OrdinalIgnoreCase)) { - ComponentLibraryItemsScrollViewer.IsVisible = hasAny; + return Symbol.CalendarDate; + } + + return Symbol.Apps; + } + + private string GetLocalizedComponentLibraryCategoryTitle(string categoryId) + { + if (string.Equals(categoryId, "Date", StringComparison.OrdinalIgnoreCase)) + { + return L("component_category.date", "Date"); + } + + return categoryId; + } + + private void ApplyComponentLibraryCategoryOffset() + { + if (_componentLibraryCategoryHostTransform is null || _componentLibraryCategoryPageWidth <= 0) + { + return; + } + + _componentLibraryCategoryHostTransform.X = -_componentLibraryCategoryIndex * _componentLibraryCategoryPageWidth; + } + + private void ApplyComponentLibraryComponentOffset() + { + if (_componentLibraryComponentHostTransform is null || _componentLibraryComponentPageWidth <= 0) + { + return; + } + + _componentLibraryComponentHostTransform.X = -_componentLibraryComponentIndex * _componentLibraryComponentPageWidth; + } + + private void OpenComponentLibraryCurrentCategory() + { + if (_componentLibraryCategories.Count == 0) + { + return; + } + + _componentLibraryCategoryIndex = Math.Clamp(_componentLibraryCategoryIndex, 0, Math.Max(0, _componentLibraryCategories.Count - 1)); + var category = _componentLibraryCategories[_componentLibraryCategoryIndex]; + _componentLibraryActiveCategoryId = category.Id; + _componentLibraryComponentIndex = 0; + BuildComponentLibraryComponentPages(category); + ShowComponentLibraryComponentsView(); + } + + private void BuildComponentLibraryComponentPages(ComponentLibraryCategory category) + { + if (ComponentLibraryComponentViewport is null || + ComponentLibraryComponentPagesHost is null || + ComponentLibraryComponentPagesContainer is null) + { + return; + } + + _componentLibraryActiveComponents = category.Components; + var componentCount = _componentLibraryActiveComponents.Count; + + ComponentLibraryComponentPagesContainer.Children.Clear(); + ComponentLibraryComponentPagesContainer.RowDefinitions.Clear(); + ComponentLibraryComponentPagesContainer.ColumnDefinitions.Clear(); + if (componentCount == 0) + { + _componentLibraryComponentIndex = 0; + return; + } + + var viewportWidth = ComponentLibraryComponentViewport.Bounds.Width; + if (viewportWidth <= 1 && ComponentLibraryWindow is not null) + { + viewportWidth = Math.Max(1, ComponentLibraryWindow.Bounds.Width - 48); + } + + var viewportHeight = ComponentLibraryComponentViewport.Bounds.Height; + if (viewportHeight <= 1 && ComponentLibraryWindow is not null) + { + viewportHeight = Math.Max(1, ComponentLibraryWindow.Bounds.Height - 160); + } + + _componentLibraryComponentPageWidth = Math.Max(1, viewportWidth); + ComponentLibraryComponentPagesHost.Width = _componentLibraryComponentPageWidth * componentCount; + ComponentLibraryComponentPagesHost.Height = viewportHeight; + ComponentLibraryComponentPagesContainer.Width = ComponentLibraryComponentPagesHost.Width; + ComponentLibraryComponentPagesContainer.Height = viewportHeight; + + ComponentLibraryComponentPagesContainer.RowDefinitions.Add(new RowDefinition(new GridLength(viewportHeight, GridUnitType.Pixel))); + for (var i = 0; i < componentCount; i++) + { + ComponentLibraryComponentPagesContainer.ColumnDefinitions.Add( + new ColumnDefinition(new GridLength(_componentLibraryComponentPageWidth, GridUnitType.Pixel))); + } + + _componentLibraryComponentIndex = Math.Clamp(_componentLibraryComponentIndex, 0, Math.Max(0, componentCount - 1)); + + for (var i = 0; i < componentCount; i++) + { + var definition = _componentLibraryActiveComponents[i]; + if (!_componentRegistry.TryGetDefinition(definition.Id, out var resolved) || !resolved.AllowDesktopPlacement) + { + continue; + } + + var page = new Grid + { + Width = _componentLibraryComponentPageWidth, + Height = viewportHeight, + Background = Brushes.Transparent + }; + + // Fit the preview to the page while preserving component cell span proportions. + var previewMaxWidth = _componentLibraryComponentPageWidth * 0.86; + var previewMaxHeight = viewportHeight * 0.72; + var previewCellSize = Math.Min( + previewMaxWidth / Math.Max(1, resolved.MinWidthCells), + previewMaxHeight / Math.Max(1, resolved.MinHeightCells)); + previewCellSize = Math.Clamp(previewCellSize, 18, 64); + + var previewWidth = resolved.MinWidthCells * previewCellSize; + var previewHeight = resolved.MinHeightCells * previewCellSize; + + var previewControl = CreateComponentLibraryPreviewControl(resolved.Id, previewCellSize); + if (previewControl is null) + { + continue; + } + + var previewBorder = new Border + { + Width = previewWidth, + Height = previewHeight, + CornerRadius = new CornerRadius(16), + ClipToBounds = true, + Background = Brushes.Transparent, + BorderThickness = new Thickness(0), + Child = previewControl, + Tag = resolved.Id + }; + previewBorder.PointerPressed += OnComponentLibraryComponentPreviewPointerPressed; + + var label = new TextBlock + { + Text = GetLocalizedComponentDisplayName(resolved), + FontSize = 14, + FontWeight = FontWeight.SemiBold, + Foreground = GetThemeBrush("AdaptiveTextPrimaryBrush"), + HorizontalAlignment = HorizontalAlignment.Center + }; + + var hint = new TextBlock + { + Text = L("component_library.drag_hint", "Drag to place"), + FontSize = 12, + Foreground = GetThemeBrush("AdaptiveTextSecondaryBrush"), + HorizontalAlignment = HorizontalAlignment.Center + }; + + var stack = new StackPanel + { + Spacing = 10, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Children = + { + new Border + { + Classes = { "glass-panel" }, + CornerRadius = new CornerRadius(18), + Padding = new Thickness(12), + Child = previewBorder + }, + label, + hint + } + }; + + page.Children.Add(stack); + + Grid.SetRow(page, 0); + Grid.SetColumn(page, i); + ComponentLibraryComponentPagesContainer.Children.Add(page); + } + + _componentLibraryComponentHostTransform = ComponentLibraryComponentPagesHost.RenderTransform as TranslateTransform; + if (_componentLibraryComponentHostTransform is null) + { + _componentLibraryComponentHostTransform = new TranslateTransform(); + ComponentLibraryComponentPagesHost.RenderTransform = _componentLibraryComponentHostTransform; + } + + ApplyComponentLibraryComponentOffset(); + } + + private Control? CreateComponentLibraryPreviewControl(string componentId, double cellSize) + { + if (componentId == BuiltInComponentIds.Date) + { + var widget = new DateWidget(); + widget.SetTimeZoneService(_timeZoneService); + widget.ApplyCellSize(cellSize); + return widget; + } + + return null; + } + + private string GetLocalizedComponentDisplayName(DesktopComponentDefinition definition) + { + if (string.Equals(definition.Id, BuiltInComponentIds.Date, StringComparison.OrdinalIgnoreCase)) + { + return L("component.date", definition.DisplayName); + } + + return definition.DisplayName; + } + + private void OnComponentLibraryComponentPreviewPointerPressed(object? sender, PointerPressedEventArgs e) + { + if (sender is not Border border || + border.Tag is not string componentId || + !e.GetCurrentPoint(border).Properties.IsLeftButtonPressed) + { + return; + } + + BeginDesktopComponentNewDrag(componentId, e); + if (_isDesktopComponentDragActive) + { + e.Handled = true; } } + + private void OnComponentLibraryBackClick(object? sender, RoutedEventArgs e) + { + ShowComponentLibraryCategoryView(); + BuildComponentLibraryCategoryPages(); + } + + private void OnComponentLibraryCategoryViewportPointerPressed(object? sender, PointerPressedEventArgs e) + { + if (!_isComponentLibraryOpen || + _componentLibraryCategories.Count == 0 || + ComponentLibraryCategoryViewport is null || + _componentLibraryCategoryHostTransform is null || + !e.GetCurrentPoint(ComponentLibraryCategoryViewport).Properties.IsLeftButtonPressed) + { + return; + } + + _isComponentLibraryCategoryGestureActive = true; + _componentLibraryCategoryGestureStartPoint = e.GetPosition(ComponentLibraryCategoryViewport); + _componentLibraryCategoryGestureCurrentPoint = _componentLibraryCategoryGestureStartPoint; + _componentLibraryCategoryGestureBaseOffset = -_componentLibraryCategoryIndex * _componentLibraryCategoryPageWidth; + e.Pointer.Capture(ComponentLibraryCategoryViewport); + } + + private void OnComponentLibraryCategoryViewportPointerMoved(object? sender, PointerEventArgs e) + { + if (!_isComponentLibraryCategoryGestureActive || + ComponentLibraryCategoryViewport is null || + _componentLibraryCategoryHostTransform is null) + { + return; + } + + _componentLibraryCategoryGestureCurrentPoint = e.GetPosition(ComponentLibraryCategoryViewport); + var deltaX = _componentLibraryCategoryGestureCurrentPoint.X - _componentLibraryCategoryGestureStartPoint.X; + var minOffset = -Math.Max(0, _componentLibraryCategories.Count - 1) * _componentLibraryCategoryPageWidth; + var tentative = _componentLibraryCategoryGestureBaseOffset + deltaX; + _componentLibraryCategoryHostTransform.X = Math.Clamp(tentative, minOffset, 0); + } + + private void OnComponentLibraryCategoryViewportPointerReleased(object? sender, PointerReleasedEventArgs e) + { + if (!_isComponentLibraryCategoryGestureActive || + ComponentLibraryCategoryViewport is null) + { + return; + } + + _isComponentLibraryCategoryGestureActive = false; + e.Pointer.Capture(null); + + var endPoint = e.GetPosition(ComponentLibraryCategoryViewport); + var deltaX = endPoint.X - _componentLibraryCategoryGestureStartPoint.X; + var deltaY = endPoint.Y - _componentLibraryCategoryGestureStartPoint.Y; + + var tapThreshold = 6; + if (Math.Abs(deltaX) <= tapThreshold && Math.Abs(deltaY) <= tapThreshold) + { + OpenComponentLibraryCurrentCategory(); + return; + } + + var swipeThreshold = Math.Max(40, _componentLibraryCategoryPageWidth * 0.18); + if (deltaX <= -swipeThreshold) + { + _componentLibraryCategoryIndex = Math.Min(_componentLibraryCategoryIndex + 1, Math.Max(0, _componentLibraryCategories.Count - 1)); + } + else if (deltaX >= swipeThreshold) + { + _componentLibraryCategoryIndex = Math.Max(_componentLibraryCategoryIndex - 1, 0); + } + + _componentLibraryActiveCategoryId = _componentLibraryCategories.Count > 0 + ? _componentLibraryCategories[_componentLibraryCategoryIndex].Id + : null; + + ApplyComponentLibraryCategoryOffset(); + } + + private void OnComponentLibraryCategoryViewportPointerCaptureLost(object? sender, PointerCaptureLostEventArgs e) + { + if (!_isComponentLibraryCategoryGestureActive) + { + return; + } + + _isComponentLibraryCategoryGestureActive = false; + ApplyComponentLibraryCategoryOffset(); + } + + private void OnComponentLibraryComponentViewportPointerPressed(object? sender, PointerPressedEventArgs e) + { + if (!_isComponentLibraryOpen || + _componentLibraryActiveComponents.Count <= 1 || + ComponentLibraryComponentViewport is null || + _componentLibraryComponentHostTransform is null || + !e.GetCurrentPoint(ComponentLibraryComponentViewport).Properties.IsLeftButtonPressed) + { + return; + } + + _isComponentLibraryComponentGestureActive = true; + _componentLibraryComponentGestureStartPoint = e.GetPosition(ComponentLibraryComponentViewport); + _componentLibraryComponentGestureCurrentPoint = _componentLibraryComponentGestureStartPoint; + _componentLibraryComponentGestureBaseOffset = -_componentLibraryComponentIndex * _componentLibraryComponentPageWidth; + e.Pointer.Capture(ComponentLibraryComponentViewport); + } + + private void OnComponentLibraryComponentViewportPointerMoved(object? sender, PointerEventArgs e) + { + if (!_isComponentLibraryComponentGestureActive || + ComponentLibraryComponentViewport is null || + _componentLibraryComponentHostTransform is null) + { + return; + } + + _componentLibraryComponentGestureCurrentPoint = e.GetPosition(ComponentLibraryComponentViewport); + var deltaX = _componentLibraryComponentGestureCurrentPoint.X - _componentLibraryComponentGestureStartPoint.X; + var minOffset = -Math.Max(0, _componentLibraryActiveComponents.Count - 1) * _componentLibraryComponentPageWidth; + var tentative = _componentLibraryComponentGestureBaseOffset + deltaX; + _componentLibraryComponentHostTransform.X = Math.Clamp(tentative, minOffset, 0); + } + + private void OnComponentLibraryComponentViewportPointerReleased(object? sender, PointerReleasedEventArgs e) + { + if (!_isComponentLibraryComponentGestureActive || + ComponentLibraryComponentViewport is null) + { + return; + } + + _isComponentLibraryComponentGestureActive = false; + e.Pointer.Capture(null); + + var endPoint = e.GetPosition(ComponentLibraryComponentViewport); + var deltaX = endPoint.X - _componentLibraryComponentGestureStartPoint.X; + + var swipeThreshold = Math.Max(40, _componentLibraryComponentPageWidth * 0.18); + if (deltaX <= -swipeThreshold) + { + _componentLibraryComponentIndex = Math.Min(_componentLibraryComponentIndex + 1, Math.Max(0, _componentLibraryActiveComponents.Count - 1)); + } + else if (deltaX >= swipeThreshold) + { + _componentLibraryComponentIndex = Math.Max(_componentLibraryComponentIndex - 1, 0); + } + + ApplyComponentLibraryComponentOffset(); + } + + private void OnComponentLibraryComponentViewportPointerCaptureLost(object? sender, PointerCaptureLostEventArgs e) + { + if (!_isComponentLibraryComponentGestureActive) + { + return; + } + + _isComponentLibraryComponentGestureActive = false; + ApplyComponentLibraryComponentOffset(); + } } diff --git a/LanMontainDesktop/Views/MainWindow.DesktopPaging.cs b/LanMontainDesktop/Views/MainWindow.DesktopPaging.cs index 1ed45e8..3ae828f 100644 --- a/LanMontainDesktop/Views/MainWindow.DesktopPaging.cs +++ b/LanMontainDesktop/Views/MainWindow.DesktopPaging.cs @@ -107,6 +107,11 @@ public partial class MainWindow Grid.SetColumnSpan(DesktopPagesViewport, gridMetrics.ColumnCount); DesktopPagesViewport.Width = pageWidth; DesktopPagesViewport.Height = pageHeight; + if (DesktopEditDragLayer is not null) + { + DesktopEditDragLayer.Width = pageWidth; + DesktopEditDragLayer.Height = pageHeight; + } DesktopPagesHost.RowDefinitions.Clear(); DesktopPagesHost.RowDefinitions.Add(new RowDefinition(new GridLength(pageHeight, GridUnitType.Pixel))); @@ -123,31 +128,35 @@ public partial class MainWindow DesktopPagesContainer.Children.Clear(); DesktopPagesContainer.Width = pageWidth * _desktopPageCount; DesktopPagesContainer.Height = pageHeight; + _desktopPageComponentGrids.Clear(); for (var index = 0; index < _desktopPageCount; index++) { DesktopPagesContainer.ColumnDefinitions.Add(new ColumnDefinition(new GridLength(pageWidth, GridUnitType.Pixel))); - var pageSurface = new Border + + var pageGrid = new Grid { + Width = pageWidth, + Height = pageHeight, Background = Brushes.Transparent, - BorderThickness = new Thickness(0), - Padding = new Thickness(10) + ShowGridLines = false }; - if (_desktopPageCount > 1) + for (var row = 0; row < viewportRowSpan; row++) { - pageSurface.Child = new TextBlock - { - HorizontalAlignment = HorizontalAlignment.Right, - VerticalAlignment = VerticalAlignment.Top, - Foreground = Foreground, - Opacity = 0.72, - Text = Lf("desktop.page_index_format", "Desktop {0}", index + 1) - }; + pageGrid.RowDefinitions.Add(new RowDefinition(new GridLength(gridMetrics.CellSize, GridUnitType.Pixel))); } - Grid.SetColumn(pageSurface, index); - Grid.SetRow(pageSurface, 0); - DesktopPagesContainer.Children.Add(pageSurface); + for (var col = 0; col < gridMetrics.ColumnCount; col++) + { + pageGrid.ColumnDefinitions.Add(new ColumnDefinition(new GridLength(gridMetrics.CellSize, GridUnitType.Pixel))); + } + + _desktopPageComponentGrids[index] = pageGrid; + RestoreDesktopPageComponents(index); + + Grid.SetColumn(pageGrid, index); + Grid.SetRow(pageGrid, 0); + DesktopPagesContainer.Children.Add(pageGrid); } Grid.SetColumn(LauncherPagePanel, 1); @@ -290,12 +299,17 @@ public partial class MainWindow private bool CanSwipeDesktopSurface() { - return !_isSettingsOpen && !_isComponentLibraryOpen && _desktopSurfacePageWidth > 1; + return !_isSettingsOpen && !_isDesktopComponentDragActive && _desktopSurfacePageWidth > 1; } private void OnDesktopPagesPointerPressed(object? sender, PointerPressedEventArgs e) { - if (!CanSwipeDesktopSurface() || DesktopPagesViewport is null) + if (DesktopPagesViewport is null) + { + return; + } + + if (!CanSwipeDesktopSurface()) { return; } @@ -326,6 +340,16 @@ public partial class MainWindow foreach (var node in visual.GetSelfAndVisualAncestors()) { + if (node is Control control) + { + // Avoid swiping pages when interacting with desktop components/widgets. + if (control.Classes.Contains("desktop-component") || + control.Classes.Contains("desktop-component-host")) + { + return true; + } + } + if (node is Button or TextBox or ComboBox or ListBoxItem or Slider or ToggleSwitch) { return true; diff --git a/LanMontainDesktop/Views/MainWindow.Localization.cs b/LanMontainDesktop/Views/MainWindow.Localization.cs index 80aa759..f38bb4d 100644 --- a/LanMontainDesktop/Views/MainWindow.Localization.cs +++ b/LanMontainDesktop/Views/MainWindow.Localization.cs @@ -59,14 +59,15 @@ public partial class MainWindow WallpaperPreviewBackButtonTextBlock.Text = L("button.back_to_windows", "Back to Windows"); ToolTip.SetTip(BackToWindowsButton, L("tooltip.back_to_windows", "Back to Windows")); - OpenComponentLibraryTextBlock.Text = L("button.component_library", "Component Library"); - WallpaperPreviewComponentLibraryTextBlock.Text = L("button.component_library", "Component Library"); - ToolTip.SetTip(OpenComponentLibraryButton, L("tooltip.component_library", "Component Library")); - ComponentLibraryTitleTextBlock.Text = L("component_library.title", "Component Library"); + OpenComponentLibraryTextBlock.Text = L("button.component_library", "Edit Desktop"); + WallpaperPreviewComponentLibraryTextBlock.Text = L("button.component_library", "Edit Desktop"); + GridPreviewComponentLibraryTextBlock.Text = L("button.component_library", "Edit Desktop"); + ToolTip.SetTip(OpenComponentLibraryButton, L("tooltip.component_library", "Edit Desktop")); + ComponentLibraryTitleTextBlock.Text = L("component_library.title", "Edit Desktop"); ToolTip.SetTip(CloseComponentLibraryButton, L("common.close", "Close")); ComponentLibraryEmptyTextBlock.Text = L( "component_library.empty", - "No components yet. Components will appear here later."); + "Swipe to pick a category, tap to open, then drag a widget onto the desktop."); LauncherTitleTextBlock.Text = L("launcher.title", "应用启动台"); LauncherSubtitleTextBlock.Text = L("launcher.subtitle", "按 Windows 开始菜单结构显示所有应用与文件夹"); @@ -74,21 +75,19 @@ public partial class MainWindow ToolTip.SetTip(LauncherFolderCloseButton, L("common.close", "关闭")); SettingsNavHeaderTextBlock.Text = L("settings.nav_header", "Settings"); - SettingsNavWallpaperItem.Content = L("settings.nav.wallpaper", "Wallpaper"); - SettingsNavGridItem.Content = L("settings.nav.grid", "Grid"); - SettingsNavColorItem.Content = L("settings.nav.color", "Color"); - SettingsNavStatusBarItem.Content = L("settings.nav.status_bar", "Status Bar"); - SettingsNavRegionItem.Content = L("settings.nav.region", "Region"); + SettingsNavWallpaperTextBlock.Text = L("settings.nav.wallpaper", "Wallpaper"); + SettingsNavGridTextBlock.Text = L("settings.nav.grid", "Grid"); + SettingsNavColorTextBlock.Text = L("settings.nav.color", "Color"); + SettingsNavStatusBarTextBlock.Text = L("settings.nav.status_bar", "Status Bar"); + SettingsNavRegionTextBlock.Text = L("settings.nav.region", "Region"); - WallpaperPanelTitleTextBlock.Text = L("settings.wallpaper.title", "个性化您的背景"); - WallpaperPanelDescriptionTextBlock.Text = L("settings.wallpaper.description", "选择图片或视频"); + WallpaperPanelTitleTextBlock.Text = L("settings.wallpaper.title", "个性化我们的背景"); WallpaperPlacementSettingsExpander.Header = L("settings.wallpaper.placement_label", "选择契合度"); WallpaperPlacementSettingsExpander.Description = L("settings.wallpaper.placement_desc", "调整图像在桌面上的填充方式。"); PickWallpaperButton.Content = L("settings.wallpaper.pick_button", "浏览照片"); ClearWallpaperButton.Content = L("settings.wallpaper.clear_button", "重置"); GridPanelTitleTextBlock.Text = L("settings.grid.title", "Grid Layout"); - ApplyGridButton.Content = L("settings.grid.apply_button", "Apply"); ColorPanelTitleTextBlock.Text = L("settings.color.title", "Color"); ThemeModeSettingsExpander.Header = L("settings.color.day_night_label", "Day/Night"); @@ -123,7 +122,7 @@ public partial class MainWindow DesktopGrid.RowDefinitions.Count, DesktopGrid.RowDefinitions.Count > 0 ? DesktopGrid.RowDefinitions[0].Height.Value : 0d); - PopulateComponentLibraryItems(); + BuildComponentLibraryCategoryPages(); RenderLauncherRootTiles(); UpdateOpenSettingsActionVisualState(); UpdateWallpaperDisplay(); diff --git a/LanMontainDesktop/Views/MainWindow.Settings.cs b/LanMontainDesktop/Views/MainWindow.Settings.cs index 04880f4..46acf3f 100644 --- a/LanMontainDesktop/Views/MainWindow.Settings.cs +++ b/LanMontainDesktop/Views/MainWindow.Settings.cs @@ -71,6 +71,12 @@ public partial class MainWindow ColorSettingsPanel.IsVisible = selectedIndex == 2; StatusBarSettingsPanel.IsVisible = selectedIndex == 3; RegionSettingsPanel.IsVisible = selectedIndex == 4; + + if (selectedIndex == 1) + { + UpdateGridPreviewLayout(); + } + ApplyTaskbarActionVisibility(GetCurrentTaskbarContext()); } @@ -633,12 +639,14 @@ public partial class MainWindow WallpaperPlacement = GetPlacementDisplayName(GetSelectedWallpaperPlacement()), SettingsTabIndex = Math.Max(0, SettingsNavListBox?.SelectedIndex ?? 0), LanguageCode = _languageCode, + TimeZoneId = _timeZoneService.CurrentTimeZone.Id, TopStatusComponentIds = _topStatusComponentIds.ToList(), PinnedTaskbarActions = _pinnedTaskbarActions.Select(action => action.ToString()).ToList(), EnableDynamicTaskbarActions = _enableDynamicTaskbarActions, TaskbarLayoutMode = _taskbarLayoutMode, DesktopPageCount = _desktopPageCount, - CurrentDesktopSurfaceIndex = _currentDesktopSurfaceIndex + CurrentDesktopSurfaceIndex = _currentDesktopSurfaceIndex, + DesktopComponentPlacements = _desktopComponentPlacements.ToList() }; _appSettingsService.Save(snapshot); @@ -1028,16 +1036,7 @@ public partial class MainWindow { WallpaperPlacementSettingsExpander.IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource { - Symbol = Symbol.Image, - IconVariant = variant - }; - } - - if (GridSizeSettingsExpander is not null) - { - GridSizeSettingsExpander.IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource - { - Symbol = Symbol.Grid, + Symbol = Symbol.Wallpaper, IconVariant = variant }; } @@ -1064,7 +1063,16 @@ public partial class MainWindow { LanguageSettingsExpander.IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource { - Symbol = Symbol.Earth, + Symbol = Symbol.Translate, + IconVariant = variant + }; + } + + if (TimeZoneSettingsExpander is not null) + { + TimeZoneSettingsExpander.IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource + { + Symbol = Symbol.GlobeClock, IconVariant = variant }; } diff --git a/LanMontainDesktop/Views/MainWindow.axaml b/LanMontainDesktop/Views/MainWindow.axaml index c8cd70c..32b919f 100644 --- a/LanMontainDesktop/Views/MainWindow.axaml +++ b/LanMontainDesktop/Views/MainWindow.axaml @@ -210,6 +210,8 @@ + @@ -259,8 +261,8 @@ HorizontalAlignment="Center" VerticalAlignment="Center" Spacing="8"> - - - - - + + - - + + - - + + - - + + - - + + @@ -461,12 +463,6 @@ Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" Margin="0,0,0,24" Text="个性化您的背景" /> - - + + Margin="0,0,0,24" + Text="调整网格布局" /> - - + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + + diff --git a/LanMontainDesktop/Views/MainWindow.axaml.backup b/LanMontainDesktop/Views/MainWindow.axaml.backup new file mode 100644 index 0000000..9951761 --- /dev/null +++ b/LanMontainDesktop/Views/MainWindow.axaml.backup @@ -0,0 +1,1048 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 22 + 28 + 0.92 + 0.95 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LanMontainDesktop/Views/MainWindow.axaml.cs b/LanMontainDesktop/Views/MainWindow.axaml.cs index 5b281c7..95bd91b 100644 --- a/LanMontainDesktop/Views/MainWindow.axaml.cs +++ b/LanMontainDesktop/Views/MainWindow.axaml.cs @@ -7,9 +7,11 @@ using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; using Avalonia.Interactivity; +using FluentAvalonia.UI.Controls; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Media.Imaging; +using Line = Avalonia.Controls.Shapes.Line; using Avalonia.Platform; using Avalonia.Platform.Storage; using Avalonia.Styling; @@ -66,6 +68,7 @@ public partial class MainWindow : Window private readonly MonetColorService _monetColorService = new(); private readonly AppSettingsService _appSettingsService = new(); private readonly LocalizationService _localizationService = new(); + private readonly TimeZoneService _timeZoneService = new(); private readonly ComponentRegistry _componentRegistry = ComponentRegistry .CreateDefault() .RegisterExtensions( @@ -109,6 +112,7 @@ public partial class MainWindow : Window InitializeComponent(); _fluentAvaloniaTheme = Application.Current?.Styles.OfType().FirstOrDefault(); PropertyChanged += OnWindowPropertyChanged; + InitializeDesktopComponentDragHandlers(); } protected override void OnOpened(EventArgs e) @@ -118,11 +122,19 @@ public partial class MainWindow : Window _suppressSettingsPersistence = true; var snapshot = _appSettingsService.Load(); + if (!string.IsNullOrWhiteSpace(snapshot.TimeZoneId)) + { + _timeZoneService.SetTimeZoneById(snapshot.TimeZoneId); + } + _targetShortSideCells = Math.Clamp( snapshot.GridShortSideCells > 0 ? snapshot.GridShortSideCells : CalculateDefaultShortSideCellCountFromDpi(), MinShortSideCells, MaxShortSideCells); GridSizeNumberBox.Value = _targetShortSideCells; + GridSizeSlider.Value = _targetShortSideCells; + GridSizeSlider.ValueChanged += OnGridSizeSliderChanged; + GridSizeNumberBox.ValueChanged += OnGridSizeNumberBoxChanged; SettingsNavListBox.SelectedIndex = Math.Clamp(snapshot.SettingsTabIndex, 0, 4); UpdateSettingsTabContent(); @@ -132,6 +144,7 @@ public partial class MainWindow : Window ApplyTaskbarSettings(snapshot); InitializeLocalization(snapshot.LanguageCode); InitializeDesktopSurfaceState(snapshot); + InitializeDesktopComponentPlacements(snapshot); InitializeSettingsIcons(); TryRestoreWallpaper(snapshot.WallpaperPath); @@ -153,9 +166,11 @@ public partial class MainWindow : Window _settingsContentPanelTransform = SettingsContentPanel.RenderTransform as TranslateTransform; DesktopHost.SizeChanged += OnDesktopHostSizeChanged; WallpaperPreviewHost.SizeChanged += OnWallpaperPreviewHostSizeChanged; + GridPreviewHost.SizeChanged += OnGridPreviewHostSizeChanged; RebuildDesktopGrid(); - PopulateComponentLibraryItems(); LoadLauncherEntriesAsync(); + InitializeTimeZoneSettings(); + ClockWidget.SetTimeZoneService(_timeZoneService); _suppressSettingsPersistence = false; PersistSettings(); @@ -181,6 +196,9 @@ public partial class MainWindow : Window PropertyChanged -= OnWindowPropertyChanged; DesktopHost.SizeChanged -= OnDesktopHostSizeChanged; WallpaperPreviewHost.SizeChanged -= OnWallpaperPreviewHostSizeChanged; + GridPreviewHost.SizeChanged -= OnGridPreviewHostSizeChanged; + GridSizeSlider.ValueChanged -= OnGridSizeSliderChanged; + GridSizeNumberBox.ValueChanged -= OnGridSizeNumberBoxChanged; base.OnClosed(e); } @@ -202,6 +220,195 @@ public partial class MainWindow : Window UpdateWallpaperPreviewLayout(); } + private void OnGridPreviewHostSizeChanged(object? sender, SizeChangedEventArgs e) + { + UpdateGridPreviewLayout(); + } + + private void OnGridSizeSliderChanged(object? sender, RoutedEventArgs e) + { + var sliderValue = (int)Math.Round(GridSizeSlider.Value); + if (Math.Abs(GridSizeNumberBox.Value - sliderValue) > double.Epsilon) + { + GridSizeNumberBox.Value = sliderValue; + } + UpdateGridPreviewLayout(); + } + + private void OnGridSizeNumberBoxChanged(object? sender, NumberBoxValueChangedEventArgs e) + { + var numberBoxValue = (int)Math.Round(GridSizeNumberBox.Value); + if (Math.Abs(GridSizeSlider.Value - numberBoxValue) > double.Epsilon) + { + GridSizeSlider.Value = numberBoxValue; + } + UpdateGridPreviewLayout(); + } + + private void UpdateGridPreviewLayout() + { + if (GridPreviewFrame is null || + GridPreviewHost is null || + GridPreviewViewport is null || + GridPreviewGrid is null) + { + return; + } + + var previewShortSideCells = (int)Math.Round(GridSizeSlider.Value); + if (previewShortSideCells < MinShortSideCells || previewShortSideCells > MaxShortSideCells) + { + previewShortSideCells = _targetShortSideCells; + } + + var desktopWidth = Math.Max(1, DesktopHost.Bounds.Width); + var desktopHeight = Math.Max(1, DesktopHost.Bounds.Height); + var aspectRatio = desktopWidth / desktopHeight; + + var availableWidth = Math.Max(100, GridPreviewHost.Bounds.Width); + + var framePadding = GridPreviewFrame.Padding; + var horizontalPadding = framePadding.Left + framePadding.Right; + var verticalPadding = framePadding.Top + framePadding.Bottom; + + var gridPreviewWidth = availableWidth; + var gridPreviewHeight = gridPreviewWidth / aspectRatio; + + GridPreviewFrame.Width = gridPreviewWidth; + GridPreviewFrame.Height = gridPreviewHeight; + + var innerWidth = Math.Max(1, gridPreviewWidth - horizontalPadding); + var innerHeight = Math.Max(1, gridPreviewHeight - verticalPadding); + var gridMetrics = CalculateGridMetrics(innerWidth, innerHeight, previewShortSideCells); + if (gridMetrics.CellSize <= 0) + { + return; + } + + GridPreviewGrid.Width = gridMetrics.ColumnCount * gridMetrics.CellSize; + GridPreviewGrid.Height = gridMetrics.RowCount * gridMetrics.CellSize; + + GridPreviewGrid.RowDefinitions.Clear(); + GridPreviewGrid.ColumnDefinitions.Clear(); + + for (var row = 0; row < gridMetrics.RowCount; row++) + { + GridPreviewGrid.RowDefinitions.Add( + new RowDefinition(new GridLength(gridMetrics.CellSize, GridUnitType.Pixel))); + } + + for (var col = 0; col < gridMetrics.ColumnCount; col++) + { + GridPreviewGrid.ColumnDefinitions.Add( + new ColumnDefinition(new GridLength(gridMetrics.CellSize, GridUnitType.Pixel))); + } + + PlaceStatusBarComponent( + GridPreviewTopStatusBarHost, + column: 0, + requestedColumnSpan: gridMetrics.ColumnCount, + totalColumns: gridMetrics.ColumnCount); + + var taskbarRow = gridMetrics.RowCount - 1; + Grid.SetRow(GridPreviewBottomTaskbarContainer, taskbarRow); + Grid.SetColumn(GridPreviewBottomTaskbarContainer, 0); + Grid.SetRowSpan(GridPreviewBottomTaskbarContainer, 1); + Grid.SetColumnSpan(GridPreviewBottomTaskbarContainer, gridMetrics.ColumnCount); + + ApplyGridPreviewWidgetSizing(gridMetrics.CellSize); + + GridInfoTextBlock.Text = Lf( + "settings.grid.info_format", + "Grid: {0} cols x {1} rows | cell {2:F1}px (1:1)", + gridMetrics.ColumnCount, + gridMetrics.RowCount, + gridMetrics.CellSize); + + DrawGridPreviewLines(gridMetrics); + } + + private void DrawGridPreviewLines(GridMetrics gridMetrics) + { + if (GridPreviewLinesCanvas is null || GridPreviewViewport is null || GridPreviewGrid is null) + { + return; + } + + var viewportBackground = GridPreviewViewport.Background as SolidColorBrush; + var backgroundColor = viewportBackground?.Color ?? Color.Parse("#30111827"); + var luminance = CalculateRelativeLuminance(backgroundColor); + var lineColor = luminance >= LightBackgroundLuminanceThreshold + ? Color.Parse("#80000000") + : Color.Parse("#80FFFFFF"); + + GridPreviewLinesCanvas.Children.Clear(); + + var cellSize = gridMetrics.CellSize; + var gridWidth = gridMetrics.ColumnCount * cellSize; + var gridHeight = gridMetrics.RowCount * cellSize; + + GridPreviewLinesCanvas.Width = gridWidth; + GridPreviewLinesCanvas.Height = gridHeight; + + Canvas.SetLeft(GridPreviewLinesCanvas, 0); + Canvas.SetTop(GridPreviewLinesCanvas, 0); + + var dashLength = cellSize * 0.3; + var gapLength = cellSize * 0.2; + + for (var row = 0; row <= gridMetrics.RowCount; row++) + { + var y = row * cellSize; + var line = new Line + { + StartPoint = new Point(0, y), + EndPoint = new Point(gridWidth, y), + Stroke = new SolidColorBrush(lineColor), + StrokeThickness = 1, + StrokeDashArray = new Avalonia.Collections.AvaloniaList { dashLength, gapLength }, + IsHitTestVisible = false + }; + GridPreviewLinesCanvas.Children.Add(line); + } + + for (var col = 0; col <= gridMetrics.ColumnCount; col++) + { + var x = col * cellSize; + var line = new Line + { + StartPoint = new Point(x, 0), + EndPoint = new Point(x, gridHeight), + Stroke = new SolidColorBrush(lineColor), + StrokeThickness = 1, + StrokeDashArray = new Avalonia.Collections.AvaloniaList { dashLength, gapLength }, + IsHitTestVisible = false + }; + GridPreviewLinesCanvas.Children.Add(line); + } + } + + private void ApplyGridPreviewWidgetSizing(double cellSize) + { + var margin = Math.Clamp(cellSize * 0.08, 1, 6); + var previewTaskbarCell = Math.Clamp(cellSize, 10, 36); + var iconSize = Math.Clamp(cellSize * 0.35, 8, 16); + + GridPreviewTopStatusBarHost.Padding = new Thickness(Math.Clamp(cellSize * 0.08, 1, 4)); + GridPreviewBottomTaskbarContainer.Margin = new Thickness(margin); + GridPreviewBottomTaskbarContainer.CornerRadius = new CornerRadius(Math.Clamp(cellSize * 0.22, 4, 10)); + GridPreviewBottomTaskbarContainer.Padding = new Thickness(Math.Clamp(cellSize * 0.06, 1, 4)); + + GridPreviewBackButtonTextBlock.FontSize = Math.Clamp(cellSize * 0.19, 5, 13); + GridPreviewComponentLibraryTextBlock.FontSize = Math.Clamp(cellSize * 0.18, 5, 12); + GridPreviewComponentLibraryIcon.FontSize = iconSize; + GridPreviewBackButtonVisual.MinHeight = previewTaskbarCell; + GridPreviewBackButtonVisual.MinWidth = Math.Clamp(cellSize * 2.1, 30, 120); + GridPreviewComponentLibraryVisual.MinHeight = previewTaskbarCell; + GridPreviewComponentLibraryVisual.MinWidth = Math.Clamp(cellSize * 2.0, 28, 110); + GridPreviewSettingsButtonIcon.Width = Math.Clamp(previewTaskbarCell * 0.42, 6, 14); + GridPreviewSettingsButtonIcon.Height = Math.Clamp(previewTaskbarCell * 0.42, 6, 14); + } + private void OnApplyGridSizeClick(object? sender, RoutedEventArgs e) { var requested = (int)Math.Round(GridSizeNumberBox.Value); @@ -217,7 +424,13 @@ public partial class MainWindow : Window GridSizeNumberBox.Value = _targetShortSideCells; } + if (Math.Abs(GridSizeSlider.Value - _targetShortSideCells) > double.Epsilon) + { + GridSizeSlider.Value = _targetShortSideCells; + } + RebuildDesktopGrid(); + PersistSettings(); } private void RebuildDesktopGrid() @@ -328,6 +541,8 @@ public partial class MainWindow : Window var verticalPadding = Math.Clamp(cellSize * 0.08, 2, 12); var horizontalPadding = Math.Clamp(cellSize * 0.20, 4, 22); var taskbarCell = Math.Clamp(cellSize, 28, 128); + var unifiedFontSize = Math.Clamp(cellSize * 0.22, 8, 22); + var unifiedIconSize = Math.Clamp(cellSize * 0.28, 10, 26); TopStatusBarHost.Padding = new Thickness(Math.Clamp(cellSize * 0.08, 1.5, 10)); ClockWidget.Margin = new Thickness(margin); @@ -339,24 +554,29 @@ public partial class MainWindow : Window BackToWindowsButton.Margin = new Thickness(0); BackToWindowsButton.Padding = new Thickness(horizontalPadding, verticalPadding); - BackToWindowsButton.FontSize = Math.Clamp(cellSize * 0.22, 8, 22); + BackToWindowsButton.FontSize = unifiedFontSize; BackToWindowsButton.MinHeight = taskbarCell; BackToWindowsButton.MinWidth = Math.Clamp(cellSize * 2.3, 90, 320); + BackToWindowsIcon.FontSize = unifiedIconSize; + OpenComponentLibraryButton.Margin = new Thickness(0); OpenComponentLibraryButton.Padding = new Thickness(horizontalPadding, verticalPadding); - OpenComponentLibraryButton.FontSize = Math.Clamp(cellSize * 0.22, 8, 22); + OpenComponentLibraryButton.FontSize = unifiedFontSize; OpenComponentLibraryButton.MinHeight = taskbarCell; OpenComponentLibraryButton.MinWidth = Math.Clamp(cellSize * 2.0, 88, 300); + OpenComponentLibraryIcon.FontSize = unifiedIconSize; OpenSettingsButton.Margin = new Thickness(0); OpenSettingsButton.Height = taskbarCell; OpenSettingsButton.MinHeight = taskbarCell; + OpenSettingsIcon.FontSize = unifiedIconSize; if (_isSettingsOpen) { OpenSettingsButton.Width = double.NaN; OpenSettingsButton.MinWidth = Math.Clamp(cellSize * 2.3, 120, 340); OpenSettingsButton.Padding = new Thickness(horizontalPadding, verticalPadding); + OpenSettingsButton.FontSize = unifiedFontSize; } else { @@ -435,21 +655,23 @@ public partial class MainWindow : Window var desktopHeight = Math.Max(1, DesktopHost.Bounds.Height); var aspectRatio = desktopWidth / desktopHeight; - // Use the host width (which is roughly 50% of the settings area) - // Subtract padding for the outer host container if needed, but let it stretch var availableWidth = Math.Max(100, WallpaperPreviewHost.Bounds.Width); - // Calculate height based on aspect ratio + var framePadding = WallpaperPreviewFrame.Padding; + var horizontalPadding = framePadding.Left + framePadding.Right; + var verticalPadding = framePadding.Top + framePadding.Bottom; + var previewWidth = availableWidth; var previewHeight = previewWidth / aspectRatio; - // Apply sizes to the monitor frame WallpaperPreviewFrame.Width = previewWidth; WallpaperPreviewFrame.Height = previewHeight; WallpaperPreviewClockTextBlock.Text = DateTime.Now.ToString("HH:mm"); - var gridMetrics = CalculateGridMetrics(previewWidth, previewHeight, _targetShortSideCells); + var innerWidth = Math.Max(1, previewWidth - horizontalPadding); + var innerHeight = Math.Max(1, previewHeight - verticalPadding); + var gridMetrics = CalculateGridMetrics(innerWidth, innerHeight, _targetShortSideCells); if (gridMetrics.CellSize <= 0) { return; @@ -542,4 +764,44 @@ public partial class MainWindow : Window } }); } -} + + private void InitializeTimeZoneSettings() + { + // 填充时区下拉框 + TimeZoneComboBox.Items.Clear(); + var timeZones = _timeZoneService.GetAllTimeZones(); + foreach (var tz in timeZones) + { + var displayText = _timeZoneService.GetTimeZoneDisplayName(tz); + var item = new ComboBoxItem + { + Content = displayText, + Tag = tz.Id + }; + TimeZoneComboBox.Items.Add(item); + + // 选中当前时区 + if (tz.Id == _timeZoneService.CurrentTimeZone.Id) + { + TimeZoneComboBox.SelectedItem = item; + } + } + } + + private void OnTimeZoneSelectionChanged(object? sender, SelectionChangedEventArgs e) + { + if (_suppressLanguageSelectionEvents || TimeZoneComboBox.SelectedItem is not ComboBoxItem item) + { + return; + } + + var timeZoneId = item.Tag?.ToString(); + if (string.IsNullOrEmpty(timeZoneId)) + { + return; + } + + _timeZoneService.SetTimeZoneById(timeZoneId); + PersistSettings(); + } + }