mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
3304 lines
118 KiB
C#
3304 lines
118 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.Threading.Tasks;
|
||
using Avalonia;
|
||
using Avalonia.Controls;
|
||
using Avalonia.Controls.Shapes;
|
||
using Avalonia.Input;
|
||
using Avalonia.Interactivity;
|
||
using Avalonia.Layout;
|
||
using Avalonia.Media;
|
||
using Avalonia.Threading;
|
||
using Avalonia.VisualTree;
|
||
using FluentAvalonia.UI.Controls;
|
||
using FluentIcons.Avalonia;
|
||
using FluentIcons.Common;
|
||
using LanMountainDesktop.ComponentSystem;
|
||
using LanMountainDesktop.DesktopEditing;
|
||
using LanMountainDesktop.Host.Abstractions;
|
||
using LanMountainDesktop.Models;
|
||
using LanMountainDesktop.Services;
|
||
using LanMountainDesktop.Services.Settings;
|
||
using LanMountainDesktop.Settings.Core;
|
||
using LanMountainDesktop.Theme;
|
||
using LanMountainDesktop.Views.Components;
|
||
using PathShape = Avalonia.Controls.Shapes.Path;
|
||
using Symbol = FluentIcons.Common.Symbol;
|
||
using SymbolIcon = FluentIcons.Avalonia.SymbolIcon;
|
||
|
||
namespace LanMountainDesktop.Views;
|
||
|
||
public partial class MainWindow
|
||
{
|
||
private readonly List<DesktopComponentPlacementSnapshot> _desktopComponentPlacements = [];
|
||
private readonly Dictionary<int, Grid> _desktopPageComponentGrids = new();
|
||
|
||
private const string DesktopComponentClass = "desktop-component";
|
||
private const string DesktopComponentHostClass = "desktop-component-host";
|
||
private const string DesktopComponentContentHostTag = "desktop-component-content-host";
|
||
private const string DesktopComponentResizeHandleTag = "desktop-component-resize-handle";
|
||
|
||
private string? _componentLibraryActiveCategoryId;
|
||
private int _componentLibraryCategoryIndex;
|
||
private int _componentLibraryComponentIndex;
|
||
private double _componentLibraryCategoryPageWidth;
|
||
private double _componentLibraryComponentPageWidth;
|
||
private TranslateTransform? _componentLibraryCategoryHostTransform;
|
||
private TranslateTransform? _componentLibraryComponentHostTransform;
|
||
private IReadOnlyList<ComponentLibraryCategory> _componentLibraryCategories = Array.Empty<ComponentLibraryCategory>();
|
||
private IReadOnlyList<ComponentLibraryComponentEntry> _componentLibraryActiveComponents = Array.Empty<ComponentLibraryComponentEntry>();
|
||
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 sealed record ComponentLibraryCategory(
|
||
string Id,
|
||
Symbol Icon,
|
||
string Title,
|
||
IReadOnlyList<ComponentLibraryComponentEntry> Components);
|
||
|
||
private readonly record struct ComponentScaleRule(int WidthUnit, int HeightUnit, int MinScale);
|
||
private readonly record struct TaskbarProfilePopupMaterialPalette(
|
||
Color SurfaceColor,
|
||
Color OutlineColor,
|
||
Color AvatarSurfaceColor,
|
||
Color PrimaryTextColor,
|
||
Color AccentColor,
|
||
Color HoverColor,
|
||
Color PressedColor,
|
||
Color DividerColor);
|
||
|
||
private void InitializeTaskbarProfileFlyout()
|
||
{
|
||
if (TaskbarProfileButton is null || TaskbarProfilePopup is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
TaskbarProfilePopup.PlacementTarget = TaskbarProfileButton;
|
||
RefreshTaskbarProfilePresentation();
|
||
}
|
||
|
||
private void RefreshTaskbarProfilePresentation()
|
||
{
|
||
if (TaskbarProfileButton is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var profile = _currentUserProfileService.GetCurrentProfile();
|
||
ApplyProfileAvatarVisual(TaskbarProfileAvatarImage, TaskbarProfileAvatarFallbackText, profile);
|
||
ApplyProfileAvatarVisual(TaskbarProfileHeaderAvatarImage, TaskbarProfileHeaderAvatarFallbackText, profile);
|
||
TaskbarProfileDisplayNameTextBlock.Text = profile.DisplayName;
|
||
TaskbarProfileSettingsActionTextBlock.Text = L("tooltip.open_settings", "Settings");
|
||
TaskbarProfileDesktopEditActionTextBlock.Text = L("button.component_library", "Edit Desktop");
|
||
ApplyTaskbarProfilePopupTheme(_appearanceThemeService.GetCurrent());
|
||
|
||
ToolTip.SetTip(TaskbarProfileButton, profile.DisplayName);
|
||
}
|
||
|
||
private static void ApplyProfileAvatarVisual(Image? image, TextBlock? fallbackText, CurrentUserProfileSnapshot profile)
|
||
{
|
||
if (image is not null)
|
||
{
|
||
image.Source = profile.AvatarBitmap;
|
||
image.IsVisible = profile.AvatarBitmap is not null;
|
||
}
|
||
|
||
if (fallbackText is not null)
|
||
{
|
||
fallbackText.Text = profile.FallbackMonogram;
|
||
fallbackText.IsVisible = profile.AvatarBitmap is null;
|
||
}
|
||
}
|
||
|
||
private void ApplyTaskbarProfilePopupTheme(AppearanceThemeSnapshot snapshot)
|
||
{
|
||
if (TaskbarProfilePopupPanel is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var palette = BuildTaskbarProfilePopupMaterialPalette(snapshot);
|
||
SetTaskbarProfilePopupBrush("TaskbarProfilePopupSurfaceBrush", palette.SurfaceColor);
|
||
SetTaskbarProfilePopupBrush("TaskbarProfilePopupOutlineBrush", palette.OutlineColor);
|
||
SetTaskbarProfilePopupBrush("TaskbarProfilePopupAvatarSurfaceBrush", palette.AvatarSurfaceColor);
|
||
SetTaskbarProfilePopupBrush("TaskbarProfilePopupTextBrush", palette.PrimaryTextColor);
|
||
SetTaskbarProfilePopupBrush("TaskbarProfilePopupAccentBrush", palette.AccentColor);
|
||
SetTaskbarProfilePopupBrush("TaskbarProfilePopupActionHoverBrush", palette.HoverColor);
|
||
SetTaskbarProfilePopupBrush("TaskbarProfilePopupActionPressedBrush", palette.PressedColor);
|
||
SetTaskbarProfilePopupBrush("TaskbarProfilePopupDividerBrush", palette.DividerColor);
|
||
}
|
||
|
||
private void SetTaskbarProfilePopupBrush(string resourceKey, Color color)
|
||
{
|
||
TaskbarProfilePopupPanel.Resources[resourceKey] = new SolidColorBrush(color);
|
||
}
|
||
|
||
private static TaskbarProfilePopupMaterialPalette BuildTaskbarProfilePopupMaterialPalette(AppearanceThemeSnapshot snapshot)
|
||
{
|
||
var primary = snapshot.MonetPalette.Primary.A > 0
|
||
? snapshot.MonetPalette.Primary
|
||
: snapshot.AccentColor;
|
||
if (primary == default)
|
||
{
|
||
primary = Color.Parse("#FF6750A4");
|
||
}
|
||
|
||
var neutral = snapshot.MonetPalette.Neutral.A > 0
|
||
? snapshot.MonetPalette.Neutral
|
||
: snapshot.IsNightMode
|
||
? Color.Parse("#FF1A1F27")
|
||
: Color.Parse("#FFF7F9FD");
|
||
var neutralVariant = snapshot.MonetPalette.NeutralVariant.A > 0
|
||
? snapshot.MonetPalette.NeutralVariant
|
||
: ColorMath.Blend(neutral, primary, snapshot.IsNightMode ? 0.20 : 0.10);
|
||
|
||
var surfaceBase = snapshot.IsNightMode
|
||
? Color.Parse("#FF141A22")
|
||
: Color.Parse("#FFFCFCFF");
|
||
var surface = ColorMath.Blend(surfaceBase, neutral, snapshot.IsNightMode ? 0.52 : 0.46);
|
||
surface = ColorMath.Blend(surface, primary, snapshot.IsNightMode ? 0.12 : 0.05);
|
||
|
||
var outlineSeed = snapshot.IsNightMode
|
||
? ColorMath.Blend(neutralVariant, Color.Parse("#FFFFFFFF"), 0.28)
|
||
: ColorMath.Blend(neutralVariant, Color.Parse("#FF111827"), 0.12);
|
||
var outline = Color.FromArgb(
|
||
snapshot.IsNightMode ? (byte)0x82 : (byte)0x38,
|
||
outlineSeed.R,
|
||
outlineSeed.G,
|
||
outlineSeed.B);
|
||
|
||
var primaryTextPreferred = snapshot.IsNightMode
|
||
? Color.Parse("#FFF4F7FB")
|
||
: Color.Parse("#FF14171B");
|
||
var primaryText = ColorMath.EnsureContrast(primaryTextPreferred, surface, 7.0);
|
||
var accent = ColorMath.EnsureContrast(primary, surface, 3.0);
|
||
var avatarSurface = ColorMath.Blend(surface, primary, snapshot.IsNightMode ? 0.26 : 0.16);
|
||
var hover = ColorMath.Blend(surface, primary, snapshot.IsNightMode ? 0.20 : 0.10);
|
||
var pressed = ColorMath.Blend(surface, primary, snapshot.IsNightMode ? 0.30 : 0.18);
|
||
var divider = Color.FromArgb(
|
||
snapshot.IsNightMode ? (byte)0x44 : (byte)0x20,
|
||
outlineSeed.R,
|
||
outlineSeed.G,
|
||
outlineSeed.B);
|
||
|
||
return new TaskbarProfilePopupMaterialPalette(
|
||
surface,
|
||
outline,
|
||
avatarSurface,
|
||
primaryText,
|
||
accent,
|
||
hover,
|
||
pressed,
|
||
divider);
|
||
}
|
||
|
||
private void OnTaskbarProfileButtonClick(object? sender, RoutedEventArgs e)
|
||
{
|
||
_ = sender;
|
||
_ = e;
|
||
|
||
if (TaskbarProfileButton is null || TaskbarProfilePopup is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (TaskbarProfilePopup.IsOpen)
|
||
{
|
||
TaskbarProfilePopup.IsOpen = false;
|
||
return;
|
||
}
|
||
|
||
RefreshTaskbarProfilePresentation();
|
||
TaskbarProfilePopup.IsOpen = true;
|
||
}
|
||
|
||
private void OnOpenComponentLibraryClick(object? sender, RoutedEventArgs e)
|
||
{
|
||
_ = sender;
|
||
_ = e;
|
||
if (TaskbarProfilePopup is not null)
|
||
{
|
||
TaskbarProfilePopup.IsOpen = false;
|
||
}
|
||
ExecuteTaskbarDesktopEditAction();
|
||
}
|
||
|
||
private void OnOpenSettingsClick(object? sender, RoutedEventArgs e)
|
||
{
|
||
_ = sender;
|
||
_ = e;
|
||
if (TaskbarProfilePopup is not null)
|
||
{
|
||
TaskbarProfilePopup.IsOpen = false;
|
||
}
|
||
ExecuteTaskbarSettingsAction();
|
||
}
|
||
|
||
private void ExecuteTaskbarDesktopEditAction()
|
||
{
|
||
if (_isComponentLibraryOpen)
|
||
{
|
||
CloseComponentLibraryWindow(reopenSettings: false);
|
||
return;
|
||
}
|
||
|
||
var settingsWindowService = (Application.Current as App)?.SettingsWindowService;
|
||
_reopenSettingsAfterComponentLibraryClose = settingsWindowService?.IsOpen == true;
|
||
if (_reopenSettingsAfterComponentLibraryClose)
|
||
{
|
||
settingsWindowService?.Close();
|
||
}
|
||
|
||
OpenComponentLibraryWindow();
|
||
}
|
||
|
||
private void ExecuteTaskbarSettingsAction()
|
||
{
|
||
if (_isComponentLibraryOpen)
|
||
{
|
||
CloseComponentLibraryWindow(reopenSettings: false);
|
||
}
|
||
|
||
var app = Application.Current as App;
|
||
if (app?.SettingsWindowService is { } settingsWindowService)
|
||
{
|
||
settingsWindowService.Toggle(new SettingsWindowOpenRequest(
|
||
Source: "MainWindowTaskbar",
|
||
Owner: this));
|
||
return;
|
||
}
|
||
|
||
app?.OpenIndependentSettingsModule("MainWindowTaskbar");
|
||
}
|
||
|
||
private void OnCloseComponentLibraryClick(object? sender, RoutedEventArgs e)
|
||
{
|
||
_componentLibraryWindowService.Close(this);
|
||
}
|
||
|
||
private void OnCloseComponentSettingsClick(object? sender, RoutedEventArgs e)
|
||
{
|
||
_ = sender;
|
||
_ = e;
|
||
}
|
||
|
||
private void OnStatusBarClockChecked(object? sender, RoutedEventArgs e)
|
||
{
|
||
if (_suppressStatusBarToggleEvents)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_topStatusComponentIds.Add(BuiltInComponentIds.Clock);
|
||
ApplyTopStatusComponentVisibility();
|
||
PersistSettings();
|
||
}
|
||
|
||
private void OnStatusBarClockUnchecked(object? sender, RoutedEventArgs e)
|
||
{
|
||
if (_suppressStatusBarToggleEvents)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_topStatusComponentIds.Remove(BuiltInComponentIds.Clock);
|
||
ApplyTopStatusComponentVisibility();
|
||
PersistSettings();
|
||
}
|
||
|
||
private void ApplyTaskbarSettings(AppSettingsSnapshot snapshot)
|
||
{
|
||
_topStatusComponentIds.Clear();
|
||
if (snapshot.TopStatusComponentIds is not null)
|
||
{
|
||
foreach (var componentId in snapshot.TopStatusComponentIds)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(componentId))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var normalizedId = componentId.Trim();
|
||
if (_componentRegistry.IsKnownComponent(normalizedId) &&
|
||
_componentRegistry.AllowsStatusBarPlacement(normalizedId))
|
||
{
|
||
_topStatusComponentIds.Add(normalizedId);
|
||
}
|
||
}
|
||
}
|
||
|
||
_pinnedTaskbarActions.Clear();
|
||
if (snapshot.PinnedTaskbarActions is not null)
|
||
{
|
||
foreach (var actionText in snapshot.PinnedTaskbarActions)
|
||
{
|
||
if (Enum.TryParse<TaskbarActionId>(actionText, ignoreCase: true, out var action))
|
||
{
|
||
_pinnedTaskbarActions.Add(action);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (_pinnedTaskbarActions.Count == 0)
|
||
{
|
||
foreach (var action in DefaultPinnedTaskbarActions)
|
||
{
|
||
_pinnedTaskbarActions.Add(action);
|
||
}
|
||
}
|
||
|
||
_enableDynamicTaskbarActions = snapshot.EnableDynamicTaskbarActions;
|
||
_taskbarLayoutMode = string.IsNullOrWhiteSpace(snapshot.TaskbarLayoutMode)
|
||
? TaskbarLayoutBottomFullRowMacStyle
|
||
: snapshot.TaskbarLayoutMode;
|
||
|
||
_clockDisplayFormat = snapshot.ClockDisplayFormat == "HourMinute"
|
||
? ClockDisplayFormat.HourMinute
|
||
: ClockDisplayFormat.HourMinuteSecond;
|
||
_statusBarClockTransparentBackground = snapshot.StatusBarClockTransparentBackground;
|
||
|
||
if (ClockWidget is not null)
|
||
{
|
||
ClockWidget.SetDisplayFormat(_clockDisplayFormat);
|
||
ClockWidget.SetTransparentBackground(_statusBarClockTransparentBackground);
|
||
}
|
||
}
|
||
|
||
private void ApplyTopStatusComponentVisibility()
|
||
{
|
||
var showClock = _topStatusComponentIds.Contains(BuiltInComponentIds.Clock);
|
||
var hasVisibleTopStatusComponent = false;
|
||
|
||
if (ClockWidget is not null)
|
||
{
|
||
ClockWidget.IsVisible = showClock;
|
||
ClockWidget.SetTransparentBackground(_statusBarClockTransparentBackground);
|
||
if (showClock)
|
||
{
|
||
ClockWidget.SetDisplayFormat(_clockDisplayFormat);
|
||
var columnSpan = _clockDisplayFormat == ClockDisplayFormat.HourMinute ? 2 : 3;
|
||
Grid.SetColumnSpan(ClockWidget, columnSpan);
|
||
hasVisibleTopStatusComponent = true;
|
||
}
|
||
}
|
||
|
||
if (TopStatusBarHost is not null)
|
||
{
|
||
TopStatusBarHost.IsVisible = hasVisibleTopStatusComponent;
|
||
}
|
||
}
|
||
|
||
private TaskbarContext GetCurrentTaskbarContext()
|
||
{
|
||
return TaskbarContext.Desktop;
|
||
}
|
||
|
||
private void ApplyTaskbarActionVisibility(TaskbarContext context)
|
||
{
|
||
if (BackToWindowsButton is null ||
|
||
TaskbarProfileButton is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var showMinimize = _pinnedTaskbarActions.Contains(TaskbarActionId.MinimizeToWindows);
|
||
|
||
BackToWindowsButton.IsVisible = showMinimize;
|
||
TaskbarProfileButton.IsVisible = true;
|
||
|
||
if (TaskbarFixedActionsHost is not null)
|
||
{
|
||
TaskbarFixedActionsHost.IsVisible = showMinimize;
|
||
}
|
||
|
||
if (TaskbarSettingsActionHost is not null)
|
||
{
|
||
TaskbarSettingsActionHost.IsVisible = true;
|
||
}
|
||
|
||
UpdateOpenSettingsActionVisualState();
|
||
|
||
var dynamicActions = ResolveDynamicTaskbarActions(context)
|
||
.Where(action => action.IsVisible)
|
||
.ToList();
|
||
var hasDynamicActions = dynamicActions.Count > 0;
|
||
BuildDynamicTaskbarVisuals(dynamicActions, _currentDesktopCellSize);
|
||
|
||
if (TaskbarDynamicActionsHost is not null)
|
||
{
|
||
TaskbarDynamicActionsHost.IsVisible = hasDynamicActions;
|
||
}
|
||
}
|
||
|
||
private void UpdateOpenSettingsActionVisualState()
|
||
{
|
||
var effectiveCellSize = _currentDesktopCellSize > 0
|
||
? _currentDesktopCellSize
|
||
: Math.Max(32, Math.Min(Bounds.Width, Bounds.Height) / Math.Max(1, _targetShortSideCells));
|
||
RefreshTaskbarProfilePresentation();
|
||
ApplyWidgetSizing(effectiveCellSize);
|
||
}
|
||
|
||
private void OpenComponentLibraryWindow()
|
||
{
|
||
if (ComponentLibraryWindow is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_isComponentLibraryOpen = true;
|
||
UpdateDesktopComponentHostEditState();
|
||
ShowComponentLibraryCategoryView();
|
||
RestoreComponentLibraryAfterDesktopEdit();
|
||
ComponentLibraryWindow.IsVisible = true;
|
||
ComponentLibraryWindow.Opacity = 0;
|
||
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
|
||
RestoreComponentLibraryWindowPosition();
|
||
|
||
Dispatcher.UIThread.Post(() =>
|
||
{
|
||
if (!_isComponentLibraryOpen || ComponentLibraryWindow is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
BuildComponentLibraryCategoryPages();
|
||
ComponentLibraryWindow.Opacity = 1;
|
||
SyncComponentLibraryCollapseExpandedState();
|
||
}, DispatcherPriority.Background);
|
||
}
|
||
|
||
private void CloseComponentLibraryWindow(bool reopenSettings)
|
||
{
|
||
if (!_isComponentLibraryOpen || ComponentLibraryWindow is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
RestoreComponentLibraryAfterDesktopEdit();
|
||
_isComponentLibraryOpen = false;
|
||
CancelDesktopComponentDrag();
|
||
CancelDesktopComponentResize(restoreOriginalSpan: true);
|
||
ClearDesktopComponentSelection();
|
||
ClearSelectedLauncherTile(refreshTaskbar: false);
|
||
UpdateDesktopComponentHostEditState();
|
||
ComponentLibraryWindow.Opacity = 0;
|
||
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
|
||
|
||
DispatcherTimer.RunOnce(() =>
|
||
{
|
||
if (_isComponentLibraryOpen || ComponentLibraryWindow is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
ComponentLibraryWindow.IsVisible = false;
|
||
ClearComponentLibraryPreviewControls();
|
||
|
||
var shouldReopenSettings = reopenSettings && _reopenSettingsAfterComponentLibraryClose;
|
||
_reopenSettingsAfterComponentLibraryClose = false;
|
||
if (shouldReopenSettings)
|
||
{
|
||
(Application.Current as App)?.OpenIndependentSettingsModule("ComponentLibraryClose");
|
||
}
|
||
}, FluttermotionToken.Slow);
|
||
}
|
||
|
||
private void OpenDetachedComponentLibraryWindow()
|
||
{
|
||
_detachedComponentLibraryWindow ??= CreateDetachedComponentLibraryWindow();
|
||
_detachedComponentLibraryWindow.Reload();
|
||
if (!_detachedComponentLibraryWindow.IsVisible)
|
||
{
|
||
if (IsVisible)
|
||
{
|
||
_detachedComponentLibraryWindow.Show(this);
|
||
}
|
||
else
|
||
{
|
||
_detachedComponentLibraryWindow.Show();
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
_detachedComponentLibraryWindow.Activate();
|
||
}
|
||
|
||
private void CloseDetachedComponentLibraryWindow()
|
||
{
|
||
if (_detachedComponentLibraryWindow is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_detachedComponentLibraryWindow.Hide();
|
||
}
|
||
|
||
private ComponentLibraryWindow CreateDetachedComponentLibraryWindow()
|
||
{
|
||
var window = new ComponentLibraryWindow(
|
||
_componentLibraryService,
|
||
cellSize =>
|
||
{
|
||
var appearanceSnapshot = HostAppearanceThemeProvider.GetOrCreate().GetCurrent();
|
||
return new ComponentLibraryCreateContext(
|
||
cellSize,
|
||
appearanceSnapshot.GlobalCornerRadiusScale,
|
||
_timeZoneService,
|
||
_weatherDataService,
|
||
_recommendationInfoService,
|
||
_calculatorDataService,
|
||
_settingsFacade);
|
||
},
|
||
L,
|
||
previewKeyResolver: ResolveDetachedLibraryPreviewKey,
|
||
previewEntryResolver: ResolveDetachedLibraryPreviewEntry,
|
||
warmPreviewRequested: RequestDetachedLibraryPreviewWarm,
|
||
renderPreviewRequested: RequestDetachedLibraryPreviewRender);
|
||
window.AddComponentRequested += OnDetachedComponentLibraryAddComponentRequested;
|
||
window.Closed += OnDetachedComponentLibraryClosed;
|
||
return window;
|
||
}
|
||
|
||
private void OnDetachedComponentLibraryAddComponentRequested(object? sender, string componentId)
|
||
{
|
||
_ = sender;
|
||
if (string.IsNullOrWhiteSpace(componentId) ||
|
||
_currentDesktopSurfaceIndex < 0 ||
|
||
_currentDesktopSurfaceIndex >= _desktopPageCount ||
|
||
!_desktopPageComponentGrids.TryGetValue(_currentDesktopSurfaceIndex, out var pageGrid) ||
|
||
!_componentRuntimeRegistry.TryGetDescriptor(componentId, out var descriptor))
|
||
{
|
||
return;
|
||
}
|
||
|
||
var span = NormalizeComponentCellSpan(
|
||
componentId,
|
||
ComponentPlacementRules.EnsureMinimumSize(
|
||
descriptor.Definition,
|
||
descriptor.Definition.MinWidthCells,
|
||
descriptor.Definition.MinHeightCells));
|
||
var row = Math.Max(0, (pageGrid.RowDefinitions.Count - span.HeightCells) / 2);
|
||
var column = Math.Max(0, (pageGrid.ColumnDefinitions.Count - span.WidthCells) / 2);
|
||
PlaceDesktopComponentOnPage(componentId, _currentDesktopSurfaceIndex, row, column);
|
||
}
|
||
|
||
private void OnDetachedComponentLibraryClosed(object? sender, EventArgs e)
|
||
{
|
||
_ = e;
|
||
if (ReferenceEquals(sender, _detachedComponentLibraryWindow))
|
||
{
|
||
_detachedComponentLibraryWindow.AddComponentRequested -= OnDetachedComponentLibraryAddComponentRequested;
|
||
_detachedComponentLibraryWindow.Closed -= OnDetachedComponentLibraryClosed;
|
||
_detachedComponentLibraryWindow = null;
|
||
}
|
||
}
|
||
|
||
private static DesktopComponentPlacementSnapshot ClonePlacementSnapshot(DesktopComponentPlacementSnapshot placement)
|
||
{
|
||
return new DesktopComponentPlacementSnapshot
|
||
{
|
||
PlacementId = placement.PlacementId,
|
||
PageIndex = placement.PageIndex,
|
||
ComponentId = placement.ComponentId,
|
||
Row = placement.Row,
|
||
Column = placement.Column,
|
||
WidthCells = placement.WidthCells,
|
||
HeightCells = placement.HeightCells
|
||
};
|
||
}
|
||
|
||
private void OnSettingsWindowStateChanged(object? sender, EventArgs e)
|
||
{
|
||
_ = sender;
|
||
_ = e;
|
||
SyncSettingsWindowState();
|
||
}
|
||
|
||
private void SyncSettingsWindowState()
|
||
{
|
||
var isOpen = (Application.Current as App)?.SettingsWindowService?.IsOpen == true;
|
||
_isSettingsOpen = isOpen;
|
||
UpdateDesktopPageAwareComponentContext();
|
||
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
|
||
UpdateOpenSettingsActionVisualState();
|
||
}
|
||
|
||
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<TaskbarActionItem> ResolveDynamicTaskbarActions(TaskbarContext context)
|
||
{
|
||
if (context == TaskbarContext.Desktop && _isComponentLibraryOpen)
|
||
{
|
||
var actions = new List<TaskbarActionItem>();
|
||
var isLauncherSurface = _currentDesktopSurfaceIndex == LauncherSurfaceIndex;
|
||
if (isLauncherSurface && IsLauncherTileSelected())
|
||
{
|
||
actions.Add(new TaskbarActionItem(
|
||
TaskbarActionId.HideLauncherEntry,
|
||
L("launcher.action.hide", "Hide"),
|
||
"Hide",
|
||
IsVisible: true,
|
||
CommandKey: "launcher.hide"));
|
||
return actions;
|
||
}
|
||
|
||
if (_selectedDesktopComponentHost is not null)
|
||
{
|
||
if (TryGetSelectedDesktopPlacement(out var selectedPlacement) &&
|
||
_componentEditorRegistry.TryGetDescriptor(selectedPlacement.ComponentId, out _))
|
||
{
|
||
actions.Add(new TaskbarActionItem(
|
||
TaskbarActionId.EditComponent,
|
||
L("component.edit", "Edit"),
|
||
"Edit",
|
||
IsVisible: true,
|
||
CommandKey: "component.edit"));
|
||
}
|
||
|
||
actions.Add(new TaskbarActionItem(
|
||
TaskbarActionId.DeleteComponent,
|
||
L("component.delete", "Delete"),
|
||
"Delete",
|
||
IsVisible: true,
|
||
CommandKey: "component.delete"));
|
||
|
||
return actions;
|
||
}
|
||
|
||
var canAddPage = _desktopPageCount < MaxDesktopPageCount;
|
||
var canDeletePage = _desktopPageCount > MinDesktopPageCount;
|
||
|
||
if (canAddPage)
|
||
{
|
||
actions.Add(new TaskbarActionItem(
|
||
TaskbarActionId.AddDesktopPage,
|
||
L("desktop.add_page", "Add page"),
|
||
"Add",
|
||
IsVisible: true,
|
||
CommandKey: "desktop.add_page"));
|
||
}
|
||
|
||
if (canDeletePage)
|
||
{
|
||
actions.Add(new TaskbarActionItem(
|
||
TaskbarActionId.DeleteDesktopPage,
|
||
L("desktop.delete_page", "Delete page"),
|
||
"Delete",
|
||
IsVisible: true,
|
||
CommandKey: "desktop.delete_page"));
|
||
}
|
||
|
||
return actions;
|
||
}
|
||
|
||
if (!_enableDynamicTaskbarActions)
|
||
{
|
||
return Array.Empty<TaskbarActionItem>();
|
||
}
|
||
|
||
_ = context;
|
||
return Array.Empty<TaskbarActionItem>();
|
||
}
|
||
|
||
private void BuildDynamicTaskbarVisuals(IReadOnlyList<TaskbarActionItem> actions, double cellSize)
|
||
{
|
||
if (TaskbarDynamicActionsPanel is not null)
|
||
{
|
||
TaskbarDynamicActionsPanel.Children.Clear();
|
||
}
|
||
|
||
if (actions.Count == 0 || TaskbarDynamicActionsPanel is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// Match taskbar typographic scale to the current grid cell size.
|
||
var taskbarCellHeight = Math.Clamp(cellSize * 0.76, 36, 76);
|
||
var fontSize = Math.Clamp(taskbarCellHeight * 0.36, 11, 22);
|
||
var iconSize = Math.Clamp(taskbarCellHeight * 0.44, 12, 26);
|
||
var padding = Math.Clamp(taskbarCellHeight * 0.20, 6, 14);
|
||
var cornerRadius = Math.Clamp(taskbarCellHeight * 0.32, 8, 16);
|
||
var spacing = Math.Clamp(taskbarCellHeight * 0.18, 4, 10);
|
||
|
||
var pageCountText = $"{_currentDesktopSurfaceIndex + 1}/{_desktopPageCount}";
|
||
var pageCountBlock = new TextBlock
|
||
{
|
||
Text = pageCountText,
|
||
Foreground = GetThemeBrush("AdaptiveTextSecondaryBrush"),
|
||
FontSize = fontSize,
|
||
FontWeight = FontWeight.SemiBold,
|
||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
|
||
Margin = new Thickness(0, 0, spacing, 0)
|
||
};
|
||
|
||
var pageCountContainer = new Border
|
||
{
|
||
Background = GetThemeBrush("AdaptiveButtonBackgroundBrush"),
|
||
CornerRadius = new CornerRadius(cornerRadius),
|
||
Padding = new Thickness(padding),
|
||
Child = pageCountBlock,
|
||
Margin = new Thickness(0, 0, spacing, 0)
|
||
};
|
||
|
||
TaskbarDynamicActionsPanel.Children.Add(pageCountContainer);
|
||
|
||
foreach (var action in actions)
|
||
{
|
||
if (!action.IsVisible)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var isDeleteAction = action.Id == TaskbarActionId.DeleteDesktopPage ||
|
||
action.Id == TaskbarActionId.DeleteComponent;
|
||
var isHideAction = action.Id == TaskbarActionId.HideLauncherEntry;
|
||
|
||
var iconSymbol = action.Id switch
|
||
{
|
||
TaskbarActionId.EditComponent => Symbol.Edit,
|
||
_ when isDeleteAction || isHideAction => Symbol.Delete,
|
||
_ => Symbol.Add
|
||
};
|
||
|
||
Control icon = new SymbolIcon
|
||
{
|
||
Symbol = iconSymbol,
|
||
IconVariant = IconVariant.Regular,
|
||
FontSize = iconSize
|
||
};
|
||
|
||
var buttonContent = new StackPanel
|
||
{
|
||
Orientation = Orientation.Horizontal,
|
||
Spacing = spacing * 0.6,
|
||
Children =
|
||
{
|
||
icon,
|
||
new TextBlock
|
||
{
|
||
Text = action.Title,
|
||
FontSize = fontSize,
|
||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
|
||
}
|
||
}
|
||
};
|
||
|
||
var button = new Button
|
||
{
|
||
Content = buttonContent,
|
||
Background = Brushes.Transparent,
|
||
BorderThickness = new Thickness(0),
|
||
Padding = new Thickness(padding),
|
||
Foreground = (isDeleteAction || isHideAction)
|
||
? new SolidColorBrush(Color.Parse("#FFFF6B6B"))
|
||
: Foreground,
|
||
Tag = action.CommandKey
|
||
};
|
||
button.Click += OnDynamicTaskbarActionClick;
|
||
|
||
TaskbarDynamicActionsPanel.Children.Add(button);
|
||
|
||
}
|
||
}
|
||
|
||
private void OnDynamicTaskbarActionClick(object? sender, RoutedEventArgs e)
|
||
{
|
||
if (sender is not Button button || button.Tag is not string commandKey)
|
||
{
|
||
return;
|
||
}
|
||
|
||
switch (commandKey)
|
||
{
|
||
case "desktop.add_page":
|
||
AddDesktopPage();
|
||
break;
|
||
case "desktop.delete_page":
|
||
ConfirmAndDeleteCurrentDesktopPage();
|
||
break;
|
||
case "component.delete":
|
||
DeleteSelectedComponent();
|
||
break;
|
||
case "component.edit":
|
||
OpenSelectedComponentEditor();
|
||
break;
|
||
case "launcher.hide":
|
||
HideSelectedLauncherEntry();
|
||
break;
|
||
}
|
||
}
|
||
|
||
private async void ConfirmAndDeleteCurrentDesktopPage()
|
||
{
|
||
if (_desktopPageCount <= MinDesktopPageCount)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var dialog = new ContentDialog
|
||
{
|
||
Title = L("desktop.delete_page_confirm.title", "确认删除页面"),
|
||
Content = L("desktop.delete_page_confirm.message", "确定要删除当前页面吗?\n\n此操作将删除当前页面上的所有组件,且无法撤销。"),
|
||
PrimaryButtonText = L("desktop.delete_page_confirm.close", "取消"),
|
||
SecondaryButtonText = L("desktop.delete_page_confirm.primary", "删除"),
|
||
DefaultButton = ContentDialogButton.Primary
|
||
};
|
||
|
||
var result = await dialog.ShowAsync(this);
|
||
if (result == ContentDialogResult.Secondary)
|
||
{
|
||
DeleteCurrentDesktopPage();
|
||
}
|
||
}
|
||
|
||
private void DeleteSelectedComponent()
|
||
{
|
||
if (_selectedDesktopComponentHost is null || _selectedDesktopComponentHost.Tag is not string placementId)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var placement = _desktopComponentPlacements.FirstOrDefault(p =>
|
||
string.Equals(p.PlacementId, placementId, StringComparison.OrdinalIgnoreCase));
|
||
if (placement is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var before = ClonePlacementSnapshot(placement);
|
||
|
||
if (string.Equals(_componentEditorWindowService.CurrentPlacementId, placement.PlacementId, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
_componentEditorWindowService.Close();
|
||
}
|
||
|
||
ClearTimeZoneServiceBindings(_selectedDesktopComponentHost);
|
||
DisposeComponentIfNeeded(_selectedDesktopComponentHost);
|
||
|
||
if (_desktopPageComponentGrids.TryGetValue(placement.PageIndex, out var pageGrid))
|
||
{
|
||
pageGrid.Children.Remove(_selectedDesktopComponentHost);
|
||
}
|
||
|
||
_desktopComponentPlacements.Remove(placement);
|
||
_componentSettingsStore.DeleteForComponent(placement.ComponentId, placement.PlacementId);
|
||
RemovePlacementPreviewImage(placement.PlacementId);
|
||
|
||
ClearDesktopComponentSelection();
|
||
|
||
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
|
||
|
||
PersistSettings();
|
||
TelemetryServices.Usage?.TrackDesktopComponentDeleted(before, "component.delete");
|
||
}
|
||
|
||
private void OpenSelectedComponentEditor()
|
||
{
|
||
if (!TryGetSelectedDesktopPlacement(out var placement) ||
|
||
!_componentEditorRegistry.TryGetDescriptor(placement.ComponentId, out var descriptor))
|
||
{
|
||
return;
|
||
}
|
||
|
||
_componentEditorWindowService.Open(new ComponentEditorOpenRequest(
|
||
Owner: this,
|
||
Descriptor: descriptor,
|
||
ComponentId: placement.ComponentId,
|
||
PlacementId: placement.PlacementId,
|
||
RefreshAction: () => RefreshDesktopComponentPlacement(placement.PlacementId)));
|
||
|
||
TelemetryServices.Usage?.TrackDesktopComponentEditorOpened(ClonePlacementSnapshot(placement), "component.edit");
|
||
}
|
||
|
||
private bool TryGetSelectedDesktopPlacement(out DesktopComponentPlacementSnapshot placement)
|
||
{
|
||
placement = null!;
|
||
if (_selectedDesktopComponentHost?.Tag is not string placementId)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var matchedPlacement = _desktopComponentPlacements.FirstOrDefault(candidate =>
|
||
string.Equals(candidate.PlacementId, placementId, StringComparison.OrdinalIgnoreCase));
|
||
if (matchedPlacement is null)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
placement = matchedPlacement;
|
||
return true;
|
||
}
|
||
|
||
private void RefreshDesktopComponentPlacement(string placementId)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(placementId))
|
||
{
|
||
return;
|
||
}
|
||
|
||
var placement = _desktopComponentPlacements.FirstOrDefault(candidate =>
|
||
string.Equals(candidate.PlacementId, placementId, StringComparison.OrdinalIgnoreCase));
|
||
if (placement is null ||
|
||
!_desktopPageComponentGrids.TryGetValue(placement.PageIndex, out var pageGrid))
|
||
{
|
||
return;
|
||
}
|
||
|
||
var host = pageGrid.Children
|
||
.OfType<Border>()
|
||
.FirstOrDefault(candidate => string.Equals(candidate.Tag as string, placementId, StringComparison.OrdinalIgnoreCase));
|
||
if (host is null)
|
||
{
|
||
RestoreDesktopPageComponents(placement.PageIndex);
|
||
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
|
||
QueuePlacementPreviewRefresh(placement);
|
||
return;
|
||
}
|
||
|
||
var component = CreateDesktopComponentControl(placement.ComponentId, placement.PlacementId, placement.PageIndex);
|
||
if (component is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (TryGetContentHost(host) is not Border contentHost)
|
||
{
|
||
RestoreDesktopPageComponents(placement.PageIndex);
|
||
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
|
||
return;
|
||
}
|
||
|
||
ClearTimeZoneServiceBindings(host);
|
||
DisposeComponentIfNeeded(host);
|
||
contentHost.Child = component;
|
||
ApplyDesktopEditStateToHost(host, _isComponentLibraryOpen);
|
||
InvalidateDesktopPageAwareComponentContextCache();
|
||
UpdateDesktopPageAwareComponentContext();
|
||
if (_selectedDesktopComponentHost == host)
|
||
{
|
||
ApplySelectionStateToHost(host, true);
|
||
}
|
||
|
||
QueuePlacementPreviewRefresh(placement);
|
||
}
|
||
|
||
private static void DisposeComponentIfNeeded(Border host)
|
||
{
|
||
if (TryGetContentHost(host) is Border contentHost && contentHost.Child is Control componentControl)
|
||
{
|
||
if (componentControl is IDisposable disposableComponent)
|
||
{
|
||
disposableComponent.Dispose();
|
||
}
|
||
}
|
||
}
|
||
|
||
// Legacy in-window popup editor is removed; component editing now routes through the Material editor window service.
|
||
|
||
private void AddDesktopPage()
|
||
{
|
||
if (_desktopPageCount >= MaxDesktopPageCount)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_desktopPageCount = Math.Clamp(_desktopPageCount + 1, MinDesktopPageCount, MaxDesktopPageCount);
|
||
_currentDesktopSurfaceIndex = Math.Clamp(_desktopPageCount - 1, 0, LauncherSurfaceIndex);
|
||
RebuildDesktopGrid();
|
||
PersistSettings();
|
||
|
||
// Refresh taskbar actions after page count changes.
|
||
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
|
||
}
|
||
|
||
private void DeleteCurrentDesktopPage()
|
||
{
|
||
if (_desktopPageCount <= MinDesktopPageCount)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var placementsToRemove = _desktopComponentPlacements
|
||
.Where(p => p.PageIndex == _currentDesktopSurfaceIndex)
|
||
.ToList();
|
||
|
||
if (_desktopPageComponentGrids.TryGetValue(_currentDesktopSurfaceIndex, out var pageGrid))
|
||
{
|
||
ClearTimeZoneServiceBindings(pageGrid.Children.OfType<Control>().ToList());
|
||
foreach (var child in pageGrid.Children.OfType<Border>())
|
||
{
|
||
DisposeComponentIfNeeded(child);
|
||
}
|
||
}
|
||
|
||
foreach (var placement in placementsToRemove)
|
||
{
|
||
_desktopComponentPlacements.Remove(placement);
|
||
_componentSettingsStore.DeleteForComponent(placement.ComponentId, placement.PlacementId);
|
||
}
|
||
RemovePlacementPreviewImages(placementsToRemove);
|
||
|
||
_desktopPageCount = Math.Clamp(_desktopPageCount - 1, MinDesktopPageCount, MaxDesktopPageCount);
|
||
|
||
// Clamp current page index to valid range after deletion.
|
||
_currentDesktopSurfaceIndex = Math.Clamp(_currentDesktopSurfaceIndex, 0, _desktopPageCount - 1);
|
||
|
||
// Update remaining page indices after deletion.
|
||
foreach (var placement in _desktopComponentPlacements)
|
||
{
|
||
if (placement.PageIndex > _currentDesktopSurfaceIndex)
|
||
{
|
||
placement.PageIndex--;
|
||
}
|
||
}
|
||
|
||
RebuildDesktopGrid();
|
||
PersistSettings();
|
||
|
||
// Refresh taskbar actions after page count changes.
|
||
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
|
||
}
|
||
|
||
private void InitializeDesktopComponentPlacements(DesktopLayoutSettingsSnapshot 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 (!_componentRuntimeRegistry.TryGetDescriptor(componentId, out var runtimeDescriptor) ||
|
||
!runtimeDescriptor.Definition.AllowDesktopPlacement)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var (widthCells, heightCells) = NormalizeComponentCellSpan(
|
||
componentId,
|
||
ComponentPlacementRules.EnsureMinimumSize(
|
||
runtimeDescriptor.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;
|
||
}
|
||
|
||
ClearTimeZoneServiceBindings(pageGrid.Children.OfType<Control>().ToList());
|
||
pageGrid.Children.Clear();
|
||
InvalidateDesktopPageAwareComponentContextCache();
|
||
|
||
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 (!_componentRuntimeRegistry.TryGetDescriptor(placement.ComponentId, out var runtimeDescriptor) ||
|
||
!runtimeDescriptor.Definition.AllowDesktopPlacement)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var (widthCells, heightCells) = NormalizeComponentCellSpan(
|
||
placement.ComponentId,
|
||
ComponentPlacementRules.EnsureMinimumSize(
|
||
runtimeDescriptor.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);
|
||
}
|
||
|
||
UpdateDesktopPageAwareComponentContext();
|
||
}
|
||
|
||
private void PlaceDesktopComponentOnPage(string componentId, int pageIndex, int row, int column)
|
||
{
|
||
if (!_desktopPageComponentGrids.TryGetValue(pageIndex, out var pageGrid))
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (!_componentRuntimeRegistry.TryGetDescriptor(componentId, out var runtimeDescriptor) ||
|
||
!runtimeDescriptor.Definition.AllowDesktopPlacement)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var (widthCells, heightCells) = NormalizeComponentCellSpan(
|
||
componentId,
|
||
ComponentPlacementRules.EnsureMinimumSize(
|
||
runtimeDescriptor.Definition,
|
||
runtimeDescriptor.Definition.MinWidthCells,
|
||
runtimeDescriptor.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);
|
||
QueuePlacementPreviewRefresh(placement);
|
||
InvalidateDesktopPageAwareComponentContextCache();
|
||
UpdateDesktopPageAwareComponentContext();
|
||
PersistSettings();
|
||
TelemetryServices.Usage?.TrackDesktopComponentPlaced(ClonePlacementSnapshot(placement), "component.create");
|
||
|
||
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
|
||
}
|
||
|
||
private Border? CreateDesktopComponentHost(DesktopComponentPlacementSnapshot placement)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(placement.PlacementId))
|
||
{
|
||
placement.PlacementId = Guid.NewGuid().ToString("N");
|
||
}
|
||
|
||
var component = CreateDesktopComponentControl(placement.ComponentId, placement.PlacementId, placement.PageIndex);
|
||
if (component is null)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
var componentCornerRadius = GetComponentCornerRadius(placement.ComponentId);
|
||
|
||
var visualInset = GetDesktopComponentVisualInset(
|
||
Math.Max(1, placement.WidthCells),
|
||
Math.Max(1, placement.HeightCells));
|
||
|
||
var contentHost = new Border
|
||
{
|
||
Tag = DesktopComponentContentHostTag,
|
||
Background = Brushes.Transparent,
|
||
CornerRadius = new CornerRadius(componentCornerRadius),
|
||
ClipToBounds = true,
|
||
Padding = visualInset,
|
||
Child = component
|
||
};
|
||
|
||
// Separate visual arc size from hit target size for better touch usability.
|
||
var handleTouchSize = Math.Clamp(_currentDesktopCellSize * 0.72, 30, 54);
|
||
var handleVisualSize = Math.Clamp(_currentDesktopCellSize * 0.56, 20, 40);
|
||
var handlePadding = Math.Max(2, (handleTouchSize - handleVisualSize) / 2);
|
||
var arcThickness = Math.Clamp(_currentDesktopCellSize * 0.17, 7, 14);
|
||
var arcData = Geometry.Parse("M 24,6 A 18,18 0 0 1 6,24");
|
||
|
||
var resizeHandleVisual = new Grid
|
||
{
|
||
Width = handleVisualSize,
|
||
Height = handleVisualSize,
|
||
IsHitTestVisible = false
|
||
};
|
||
resizeHandleVisual.Children.Add(new PathShape
|
||
{
|
||
Data = arcData,
|
||
Stretch = Stretch.Fill,
|
||
Stroke = GetThemeBrush("AdaptiveTextAccentBrush"),
|
||
StrokeThickness = arcThickness + 3,
|
||
StrokeLineCap = PenLineCap.Round
|
||
});
|
||
resizeHandleVisual.Children.Add(new PathShape
|
||
{
|
||
Data = arcData,
|
||
Stretch = Stretch.Fill,
|
||
Stroke = GetThemeBrush("AdaptiveAccentBrush"),
|
||
StrokeThickness = arcThickness,
|
||
StrokeLineCap = PenLineCap.Round
|
||
});
|
||
|
||
var resizeHandle = new Border
|
||
{
|
||
Tag = DesktopComponentResizeHandleTag,
|
||
Width = handleTouchSize,
|
||
Height = handleTouchSize,
|
||
Background = Brushes.Transparent,
|
||
BorderBrush = Brushes.Transparent,
|
||
BorderThickness = new Thickness(0),
|
||
CornerRadius = new CornerRadius(handleTouchSize * 0.5),
|
||
Padding = new Thickness(handlePadding),
|
||
HorizontalAlignment = HorizontalAlignment.Right,
|
||
VerticalAlignment = VerticalAlignment.Bottom,
|
||
Margin = new Thickness(
|
||
0,
|
||
0,
|
||
-Math.Clamp(handleTouchSize * 0.42, 10, 24),
|
||
-Math.Clamp(handleTouchSize * 0.42, 10, 24)),
|
||
Child = resizeHandleVisual,
|
||
Opacity = 1,
|
||
IsVisible = false,
|
||
IsHitTestVisible = false
|
||
};
|
||
resizeHandle.PointerPressed += OnDesktopComponentResizeHandlePointerPressed;
|
||
|
||
var hostChrome = new Grid
|
||
{
|
||
ClipToBounds = false
|
||
};
|
||
hostChrome.Children.Add(contentHost);
|
||
hostChrome.Children.Add(resizeHandle);
|
||
|
||
var host = new Border
|
||
{
|
||
Tag = placement.PlacementId,
|
||
Background = Brushes.Transparent,
|
||
ClipToBounds = false,
|
||
CornerRadius = new CornerRadius(componentCornerRadius),
|
||
Child = hostChrome
|
||
};
|
||
host.Classes.Add(DesktopComponentHostClass);
|
||
ApplyDesktopEditStateToHost(host, _isComponentLibraryOpen);
|
||
host.PointerPressed += OnDesktopComponentHostPointerPressed;
|
||
return host;
|
||
}
|
||
|
||
private (int WidthCells, int HeightCells) NormalizeComponentCellSpan(
|
||
string componentId,
|
||
(int WidthCells, int HeightCells) span)
|
||
{
|
||
if (_componentRuntimeRegistry.TryGetDescriptor(componentId, out var runtimeDescriptor))
|
||
{
|
||
var normalized = ComponentPlacementRules.EnsureMinimumSize(
|
||
runtimeDescriptor.Definition,
|
||
span.WidthCells,
|
||
span.HeightCells);
|
||
if (runtimeDescriptor.Definition.ResizeMode == DesktopComponentResizeMode.Free)
|
||
{
|
||
return normalized;
|
||
}
|
||
|
||
return NormalizeAspectRatioForComponent(componentId, normalized);
|
||
}
|
||
|
||
return NormalizeAspectRatioForComponent(
|
||
componentId,
|
||
(Math.Max(1, span.WidthCells), Math.Max(1, span.HeightCells)));
|
||
}
|
||
|
||
private DesktopComponentResizeMode GetComponentResizeMode(string componentId)
|
||
{
|
||
if (_componentRuntimeRegistry.TryGetDescriptor(componentId, out var runtimeDescriptor))
|
||
{
|
||
return runtimeDescriptor.Definition.ResizeMode;
|
||
}
|
||
|
||
return DesktopComponentResizeMode.Proportional;
|
||
}
|
||
|
||
private static (int WidthCells, int HeightCells) NormalizeAspectRatioForComponent(
|
||
string componentId,
|
||
(int WidthCells, int HeightCells) span)
|
||
{
|
||
if (string.Equals(componentId, BuiltInComponentIds.DesktopWhiteboard, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
// Support both portrait ratios and snap to nearest viable scale tier.
|
||
return SnapSpanToScaleRules(
|
||
span,
|
||
new ComponentScaleRule(WidthUnit: 1, HeightUnit: 2, MinScale: 2), // 2x4, 3x6, 4x8...
|
||
new ComponentScaleRule(WidthUnit: 3, HeightUnit: 4, MinScale: 1)); // 3x4, 6x8...
|
||
}
|
||
|
||
if (string.Equals(componentId, BuiltInComponentIds.DesktopBlackboardLandscape, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
// Support both landscape ratios and snap to nearest viable scale tier.
|
||
return SnapSpanToScaleRules(
|
||
span,
|
||
new ComponentScaleRule(WidthUnit: 2, HeightUnit: 1, MinScale: 2), // 4x2, 6x3, 8x4...
|
||
new ComponentScaleRule(WidthUnit: 4, HeightUnit: 3, MinScale: 1)); // 4x3, 8x6...
|
||
}
|
||
|
||
if (string.Equals(componentId, BuiltInComponentIds.DesktopDailyPoetry, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
// Keep recommendation card at a 2:1 ratio with a minimum footprint of 4x2.
|
||
return SnapSpanToScaleRules(
|
||
span,
|
||
new ComponentScaleRule(WidthUnit: 2, HeightUnit: 1, MinScale: 2));
|
||
}
|
||
|
||
if (string.Equals(componentId, BuiltInComponentIds.DesktopCnrDailyNews, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
// Keep CNR widget at a 2:1 ratio: 4x2, 6x3, 8x4...
|
||
return SnapSpanToScaleRules(
|
||
span,
|
||
new ComponentScaleRule(WidthUnit: 2, HeightUnit: 1, MinScale: 2));
|
||
}
|
||
|
||
if (string.Equals(componentId, BuiltInComponentIds.DesktopIfengNews, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
// Keep iFeng news widget square with a minimum footprint of 4x4.
|
||
return SnapSpanToScaleRules(
|
||
span,
|
||
new ComponentScaleRule(WidthUnit: 1, HeightUnit: 1, MinScale: 4));
|
||
}
|
||
|
||
if (string.Equals(componentId, BuiltInComponentIds.DesktopBilibiliHotSearch, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
// Keep Bilibili hot search widget at a 2:1 ratio: 4x2, 6x3, 8x4...
|
||
return SnapSpanToScaleRules(
|
||
span,
|
||
new ComponentScaleRule(WidthUnit: 2, HeightUnit: 1, MinScale: 2));
|
||
}
|
||
|
||
if (string.Equals(componentId, BuiltInComponentIds.DesktopBaiduHotSearch, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
// Keep Baidu hot search widget at a 2:1 ratio: 4x2, 6x3, 8x4...
|
||
return SnapSpanToScaleRules(
|
||
span,
|
||
new ComponentScaleRule(WidthUnit: 2, HeightUnit: 1, MinScale: 2));
|
||
}
|
||
|
||
if (string.Equals(componentId, BuiltInComponentIds.DesktopStcn24Forum, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
// Keep STCN forum widget square with a minimum footprint of 4x4.
|
||
return SnapSpanToScaleRules(
|
||
span,
|
||
new ComponentScaleRule(WidthUnit: 1, HeightUnit: 1, MinScale: 4));
|
||
}
|
||
|
||
if (string.Equals(componentId, BuiltInComponentIds.DesktopExchangeRateCalculator, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
// Keep exchange rate converter square with minimum size 4x4.
|
||
return SnapSpanToScaleRules(
|
||
span,
|
||
new ComponentScaleRule(WidthUnit: 1, HeightUnit: 1, MinScale: 4));
|
||
}
|
||
|
||
if (string.Equals(componentId, BuiltInComponentIds.DesktopStudyNoiseCurve, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
// Keep noise curve widget in a 2:1 ratio with minimum 4x2.
|
||
return SnapSpanToScaleRules(
|
||
span,
|
||
new ComponentScaleRule(WidthUnit: 2, HeightUnit: 1, MinScale: 2));
|
||
}
|
||
|
||
if (string.Equals(componentId, BuiltInComponentIds.DesktopWorldClock, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
// Keep world clock widget at 2:1 ratio: 4x2, 6x3, 8x4...
|
||
return SnapSpanToScaleRules(
|
||
span,
|
||
new ComponentScaleRule(WidthUnit: 2, HeightUnit: 1, MinScale: 2));
|
||
}
|
||
|
||
if (string.Equals(componentId, BuiltInComponentIds.DesktopStudyScoreOverview, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
// Keep score overview widget square: 4x4, 5x5, 6x6...
|
||
return SnapSpanToScaleRules(
|
||
span,
|
||
new ComponentScaleRule(WidthUnit: 1, HeightUnit: 1, MinScale: 4));
|
||
}
|
||
|
||
if (string.Equals(componentId, BuiltInComponentIds.DesktopZhiJiaoHub, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
// ZhiJiao Hub allows free resize but starts at 2x2
|
||
// Allow any aspect ratio, minimum 2x2
|
||
var width = Math.Max(2, span.WidthCells);
|
||
var height = Math.Max(2, span.HeightCells);
|
||
return (width, height);
|
||
}
|
||
|
||
return span;
|
||
}
|
||
|
||
private static (int WidthCells, int HeightCells) SnapSpanToScaleRules(
|
||
(int WidthCells, int HeightCells) span,
|
||
params ComponentScaleRule[] rules)
|
||
{
|
||
var targetWidth = Math.Max(1, span.WidthCells);
|
||
var targetHeight = Math.Max(1, span.HeightCells);
|
||
|
||
var hasCandidate = false;
|
||
var bestWidth = targetWidth;
|
||
var bestHeight = targetHeight;
|
||
var bestArea = -1;
|
||
var bestDistance = double.MaxValue;
|
||
|
||
foreach (var rule in rules)
|
||
{
|
||
if (rule.WidthUnit <= 0 || rule.HeightUnit <= 0 || rule.MinScale <= 0)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var maxScale = Math.Min(targetWidth / rule.WidthUnit, targetHeight / rule.HeightUnit);
|
||
if (maxScale < rule.MinScale)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
for (var scale = rule.MinScale; scale <= maxScale; scale++)
|
||
{
|
||
var width = rule.WidthUnit * scale;
|
||
var height = rule.HeightUnit * scale;
|
||
var area = width * height;
|
||
var dx = targetWidth - width;
|
||
var dy = targetHeight - height;
|
||
var distance = dx * dx + dy * dy;
|
||
|
||
if (!hasCandidate ||
|
||
area > bestArea ||
|
||
(area == bestArea && distance < bestDistance))
|
||
{
|
||
hasCandidate = true;
|
||
bestWidth = width;
|
||
bestHeight = height;
|
||
bestArea = area;
|
||
bestDistance = distance;
|
||
}
|
||
}
|
||
}
|
||
|
||
return hasCandidate
|
||
? (bestWidth, bestHeight)
|
||
: (targetWidth, targetHeight);
|
||
}
|
||
|
||
private double GetComponentCornerRadius(string componentId)
|
||
{
|
||
var appearanceSnapshot = HostAppearanceThemeProvider.GetOrCreate().GetCurrent();
|
||
|
||
if (_componentRuntimeRegistry.TryGetDescriptor(componentId, out var runtimeDescriptor))
|
||
{
|
||
return runtimeDescriptor.ResolveCornerRadius(new ComponentChromeContext(
|
||
componentId,
|
||
null,
|
||
_currentDesktopCellSize,
|
||
appearanceSnapshot.GlobalCornerRadiusScale,
|
||
appearanceSnapshot.CornerRadiusTokens));
|
||
}
|
||
|
||
var scale = Math.Max(GlobalAppearanceSettings.MinimumCornerRadiusScale, appearanceSnapshot.GlobalCornerRadiusScale);
|
||
return Math.Clamp(_currentDesktopCellSize * 0.22, 8, 18) * scale;
|
||
}
|
||
|
||
private Thickness GetDesktopComponentVisualInset(int widthCells, int heightCells)
|
||
{
|
||
// Keep the drop/selection bounds on grid cells while reducing visual footprint.
|
||
var baseInset = Math.Clamp(_currentDesktopCellSize * 0.08, 2, 10);
|
||
var horizontal = Math.Clamp(baseInset + Math.Max(0, widthCells - 1) * 0.25, 2, 12);
|
||
var vertical = Math.Clamp(baseInset * 0.85 + Math.Max(0, heightCells - 1) * 0.2, 2, 10);
|
||
return new Thickness(horizontal, vertical, horizontal, vertical);
|
||
}
|
||
|
||
private static Border? FindDesktopComponentHost(Visual? visual)
|
||
{
|
||
var current = visual;
|
||
while (current is not null)
|
||
{
|
||
if (current is Border border && border.Classes.Contains(DesktopComponentHostClass))
|
||
{
|
||
return border;
|
||
}
|
||
|
||
current = current.GetVisualParent();
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private static Border? TryGetContentHost(Border host)
|
||
{
|
||
if (host.Child is Grid hostChrome)
|
||
{
|
||
return hostChrome.Children
|
||
.OfType<Border>()
|
||
.FirstOrDefault(child =>
|
||
string.Equals(child.Tag?.ToString(), DesktopComponentContentHostTag, StringComparison.Ordinal));
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private static void ClearTimeZoneServiceBindings(IEnumerable<Control> roots)
|
||
{
|
||
foreach (var root in roots)
|
||
{
|
||
ClearTimeZoneServiceBindings(root);
|
||
}
|
||
}
|
||
|
||
private static void ClearTimeZoneServiceBindings(Control root)
|
||
{
|
||
if (root is ITimeZoneAwareComponentWidget timeZoneAwareRoot)
|
||
{
|
||
timeZoneAwareRoot.ClearTimeZoneService();
|
||
}
|
||
|
||
foreach (var descendant in root.GetVisualDescendants())
|
||
{
|
||
if (descendant is ITimeZoneAwareComponentWidget timeZoneAwareChild)
|
||
{
|
||
timeZoneAwareChild.ClearTimeZoneService();
|
||
}
|
||
}
|
||
}
|
||
|
||
private void InvalidateDesktopPageAwareComponentContextCache()
|
||
{
|
||
_desktopPageContextInitialized = false;
|
||
_desktopPageContextActiveMask = 0;
|
||
}
|
||
|
||
private int BuildDesktopPageAwareComponentActiveMask()
|
||
{
|
||
if (_isSettingsOpen)
|
||
{
|
||
return 0;
|
||
}
|
||
|
||
var activeMask = 0;
|
||
if (_desktopSurfacePageWidth > 1 &&
|
||
_desktopPagesHostTransform is not null &&
|
||
(_isDesktopSwipeActive ||
|
||
_desktopPageContextSettlingSourceIndex is not null ||
|
||
_desktopPageContextSettlingTargetIndex is not null))
|
||
{
|
||
var viewportLeft = -_desktopPagesHostTransform.X;
|
||
var viewportRight = viewportLeft + _desktopSurfacePageWidth;
|
||
for (var pageIndex = 0; pageIndex < _desktopPageCount; pageIndex++)
|
||
{
|
||
var pageLeft = pageIndex * _desktopSurfacePageWidth;
|
||
var pageRight = pageLeft + _desktopSurfacePageWidth;
|
||
if (pageRight > viewportLeft + 0.5d && pageLeft < viewportRight - 0.5d)
|
||
{
|
||
activeMask |= 1 << pageIndex;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (_currentDesktopSurfaceIndex >= 0 && _currentDesktopSurfaceIndex < _desktopPageCount)
|
||
{
|
||
activeMask |= 1 << _currentDesktopSurfaceIndex;
|
||
}
|
||
|
||
if (_desktopPageContextSettlingSourceIndex is int sourceIndex &&
|
||
sourceIndex >= 0 &&
|
||
sourceIndex < _desktopPageCount)
|
||
{
|
||
activeMask |= 1 << sourceIndex;
|
||
}
|
||
|
||
if (_desktopPageContextSettlingTargetIndex is int targetIndex &&
|
||
targetIndex >= 0 &&
|
||
targetIndex < _desktopPageCount)
|
||
{
|
||
activeMask |= 1 << targetIndex;
|
||
}
|
||
|
||
return activeMask;
|
||
}
|
||
|
||
private void UpdateDesktopPageAwareComponentContext()
|
||
{
|
||
var isEditMode = _isComponentLibraryOpen || _isSettingsOpen;
|
||
var activeMask = BuildDesktopPageAwareComponentActiveMask();
|
||
var pageUpdateMask = !_desktopPageContextInitialized || isEditMode != _desktopPageContextEditMode
|
||
? _desktopPageComponentGrids.Keys.Aggregate(0, (mask, pageIndex) => mask | (1 << pageIndex))
|
||
: activeMask ^ _desktopPageContextActiveMask;
|
||
|
||
if (_desktopPageContextInitialized &&
|
||
pageUpdateMask == 0 &&
|
||
isEditMode == _desktopPageContextEditMode &&
|
||
activeMask == _desktopPageContextActiveMask)
|
||
{
|
||
return;
|
||
}
|
||
|
||
foreach (var pair in _desktopPageComponentGrids)
|
||
{
|
||
var pageBit = 1 << pair.Key;
|
||
if ((pageUpdateMask & pageBit) == 0)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var isOnActivePage = (activeMask & pageBit) != 0;
|
||
foreach (var host in pair.Value.Children.OfType<Border>())
|
||
{
|
||
if (!host.Classes.Contains(DesktopComponentHostClass))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (TryGetContentHost(host)?.Child is Control componentRoot)
|
||
{
|
||
ApplyDesktopPageContext(componentRoot, isOnActivePage, isEditMode);
|
||
}
|
||
}
|
||
}
|
||
|
||
_desktopPageContextInitialized = true;
|
||
_desktopPageContextEditMode = isEditMode;
|
||
_desktopPageContextActiveMask = activeMask;
|
||
}
|
||
|
||
private static void ApplyDesktopPageContext(Control root, bool isOnActivePage, bool isEditMode)
|
||
{
|
||
if (root is IDesktopPageVisibilityAwareComponentWidget awareRoot)
|
||
{
|
||
awareRoot.SetDesktopPageContext(isOnActivePage, isEditMode);
|
||
}
|
||
|
||
foreach (var descendant in root.GetVisualDescendants())
|
||
{
|
||
if (descendant is IDesktopPageVisibilityAwareComponentWidget awareChild)
|
||
{
|
||
awareChild.SetDesktopPageContext(isOnActivePage, isEditMode);
|
||
}
|
||
}
|
||
}
|
||
|
||
private static Border? TryGetResizeHandle(Border host)
|
||
{
|
||
if (host.Child is Grid hostChrome)
|
||
{
|
||
return hostChrome.Children
|
||
.OfType<Border>()
|
||
.FirstOrDefault(child =>
|
||
string.Equals(child.Tag?.ToString(), DesktopComponentResizeHandleTag, StringComparison.Ordinal));
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private bool IsPointerOnSelectedFrameBorder(Border host, Point pointerInHost)
|
||
{
|
||
if (host != _selectedDesktopComponentHost || !_isComponentLibraryOpen)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var width = host.Bounds.Width;
|
||
var height = host.Bounds.Height;
|
||
if (width <= 1 || height <= 1)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var borderBand = Math.Clamp(_currentDesktopCellSize * 0.15, 8, 22);
|
||
var onLeft = pointerInHost.X <= borderBand;
|
||
var onRight = pointerInHost.X >= width - borderBand;
|
||
var onTop = pointerInHost.Y <= borderBand;
|
||
var onBottom = pointerInHost.Y >= height - borderBand;
|
||
return onLeft || onRight || onTop || onBottom;
|
||
}
|
||
|
||
private Control? CreateDesktopComponentControl(string componentId, string? placementId = null, int? pageIndex = null)
|
||
{
|
||
if (!_componentRuntimeRegistry.TryGetDescriptor(componentId, out var runtimeDescriptor))
|
||
{
|
||
return null;
|
||
}
|
||
|
||
return CreateDesktopComponentControl(runtimeDescriptor, _currentDesktopCellSize, placementId, pageIndex, "DesktopSurface");
|
||
}
|
||
|
||
private Control? CreateDesktopComponentControl(
|
||
string componentId,
|
||
double cellSize,
|
||
string? placementId,
|
||
int? pageIndex,
|
||
string action)
|
||
{
|
||
if (!_componentRuntimeRegistry.TryGetDescriptor(componentId, out var runtimeDescriptor))
|
||
{
|
||
return null;
|
||
}
|
||
|
||
return CreateDesktopComponentControl(runtimeDescriptor, cellSize, placementId, pageIndex, action);
|
||
}
|
||
|
||
private Control? CreateDesktopComponentControl(
|
||
DesktopComponentRuntimeDescriptor runtimeDescriptor,
|
||
double cellSize,
|
||
string? placementId,
|
||
int? pageIndex,
|
||
string action)
|
||
{
|
||
try
|
||
{
|
||
var appearanceSnapshot = HostAppearanceThemeProvider.GetOrCreate().GetCurrent();
|
||
var createContext = new ComponentLibraryCreateContext(
|
||
cellSize,
|
||
appearanceSnapshot.GlobalCornerRadiusScale,
|
||
_timeZoneService,
|
||
_weatherDataService,
|
||
_recommendationInfoService,
|
||
_calculatorDataService,
|
||
_settingsFacade,
|
||
placementId);
|
||
if (!_componentLibraryService.TryCreateControl(runtimeDescriptor.Definition.Id, createContext, out var component, out var exception) ||
|
||
component is null)
|
||
{
|
||
throw exception ?? new InvalidOperationException("Component library service returned no control.");
|
||
}
|
||
|
||
component.Classes.Add(DesktopComponentClass);
|
||
return component;
|
||
}
|
||
catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex))
|
||
{
|
||
AppLogger.Warn(
|
||
"ComponentRuntime",
|
||
$"Action={action}; ComponentId={runtimeDescriptor.Definition.Id}; PlacementId={placementId ?? string.Empty}; PageIndex={pageIndex?.ToString() ?? string.Empty}; ExceptionType={ex.GetType().FullName}; IsFatal=false",
|
||
ex);
|
||
|
||
var failureView = new DesktopComponentFailureView(
|
||
runtimeDescriptor.Definition.DisplayName,
|
||
runtimeDescriptor.Definition.Id,
|
||
placementId,
|
||
pageIndex,
|
||
action,
|
||
ex);
|
||
failureView.ApplyCellSize(cellSize);
|
||
failureView.Classes.Add(DesktopComponentClass);
|
||
return failureView;
|
||
}
|
||
}
|
||
|
||
internal bool IsComponentLibraryOpenFromService => _isComponentLibraryOpen;
|
||
internal bool IsDetachedComponentLibraryWindowOpenFromService => _detachedComponentLibraryWindow is { IsVisible: true };
|
||
|
||
internal void OpenComponentLibraryWindowFromService()
|
||
{
|
||
OpenComponentLibraryWindow();
|
||
}
|
||
|
||
internal void CloseComponentLibraryWindowFromService()
|
||
{
|
||
CloseComponentLibraryWindow(reopenSettings: false);
|
||
}
|
||
|
||
internal void OpenDetachedComponentLibraryWindowFromService()
|
||
{
|
||
OpenDetachedComponentLibraryWindow();
|
||
}
|
||
|
||
internal void CloseDetachedComponentLibraryWindowFromService()
|
||
{
|
||
CloseDetachedComponentLibraryWindow();
|
||
}
|
||
|
||
public bool TryGetSettingsWindowAnchorBounds(out PixelRect anchorBounds)
|
||
{
|
||
anchorBounds = default;
|
||
if (!IsVisible || BottomTaskbarContainer is null)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var origin = BottomTaskbarContainer.TranslatePoint(new Point(0, 0), this);
|
||
if (origin is null)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var scale = RenderScaling > 0 ? RenderScaling : 1d;
|
||
var width = (int)Math.Round(BottomTaskbarContainer.Bounds.Width * scale);
|
||
var height = (int)Math.Round(BottomTaskbarContainer.Bounds.Height * scale);
|
||
if (width <= 0 || height <= 0)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var x = Position.X + (int)Math.Round(origin.Value.X * scale);
|
||
var y = Position.Y + (int)Math.Round(origin.Value.Y * scale);
|
||
anchorBounds = new PixelRect(x, y, width, height);
|
||
return true;
|
||
}
|
||
|
||
private void CollapseComponentLibraryPanel()
|
||
{
|
||
// Animate component library panel collapsing downward
|
||
if (ComponentLibraryWindow is not null)
|
||
{
|
||
ComponentLibraryWindow.Height = 0;
|
||
ComponentLibraryWindow.IsVisible = false;
|
||
}
|
||
|
||
_isComponentLibraryOpen = false;
|
||
CancelDesktopComponentDrag();
|
||
CancelDesktopComponentResize(restoreOriginalSpan: true);
|
||
CloseDetachedComponentLibraryWindow();
|
||
ClearDesktopComponentSelection();
|
||
ClearSelectedLauncherTile(refreshTaskbar: false);
|
||
UpdateDesktopComponentHostEditState();
|
||
ClearComponentLibraryPreviewControls();
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
|
||
UpdateDesktopPageAwareComponentContext();
|
||
}
|
||
|
||
private void ApplyDesktopEditStateToHost(Border host, bool isEditMode)
|
||
{
|
||
host.IsHitTestVisible = true;
|
||
var keepContentInteractive = ShouldKeepContentInteractiveInEditMode(host);
|
||
|
||
if (TryGetContentHost(host) is Border contentHost)
|
||
{
|
||
// In edit mode, keep selected interactive widgets usable; drag/resize still uses host border/handles.
|
||
contentHost.IsHitTestVisible = !isEditMode || keepContentInteractive;
|
||
if (contentHost.Child is Control componentControl)
|
||
{
|
||
componentControl.IsHitTestVisible = !isEditMode || keepContentInteractive;
|
||
}
|
||
}
|
||
|
||
var isSelected = host == _selectedDesktopComponentHost;
|
||
ApplySelectionStateToHost(host, isSelected);
|
||
}
|
||
|
||
private bool ShouldKeepContentInteractiveInEditMode(Border host)
|
||
{
|
||
if (!_isComponentLibraryOpen ||
|
||
host.Tag is not string placementId)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var placement = _desktopComponentPlacements.FirstOrDefault(p =>
|
||
string.Equals(p.PlacementId, placementId, StringComparison.OrdinalIgnoreCase));
|
||
if (placement is null)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
return string.Equals(
|
||
placement.ComponentId,
|
||
BuiltInComponentIds.DesktopStudySessionHistory,
|
||
StringComparison.OrdinalIgnoreCase);
|
||
}
|
||
|
||
private void OnDesktopComponentHostPointerPressed(object? sender, PointerPressedEventArgs e)
|
||
{
|
||
if (!_isComponentLibraryOpen || HasActiveDesktopEditSession)
|
||
{
|
||
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;
|
||
}
|
||
|
||
var wasSelected = host == _selectedDesktopComponentHost;
|
||
SetSelectedDesktopComponent(host);
|
||
if (!wasSelected)
|
||
{
|
||
e.Handled = true;
|
||
return;
|
||
}
|
||
|
||
var pointerInHost = e.GetPosition(host);
|
||
if (IsPointerOnSelectedFrameBorder(host, pointerInHost))
|
||
{
|
||
BeginDesktopComponentResizeDrag(host, placement, e);
|
||
if (IsDesktopEditResizeMode)
|
||
{
|
||
e.Handled = true;
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
BeginDesktopComponentMoveDrag(host, placement, e);
|
||
e.Handled = true;
|
||
}
|
||
|
||
private void SetSelectedDesktopComponent(Border? host)
|
||
{
|
||
ClearSelectedLauncherTile(refreshTaskbar: false);
|
||
|
||
// Clear previous selection
|
||
if (_selectedDesktopComponentHost is not null && _selectedDesktopComponentHost != host)
|
||
{
|
||
ApplySelectionStateToHost(_selectedDesktopComponentHost, false);
|
||
}
|
||
|
||
// Set new selection
|
||
_selectedDesktopComponentHost = host;
|
||
if (host is not null)
|
||
{
|
||
ApplySelectionStateToHost(host, true);
|
||
}
|
||
|
||
// Refresh taskbar actions to show delete/edit buttons
|
||
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
|
||
}
|
||
|
||
private void ApplySelectionStateToHost(Border host, bool isSelected)
|
||
{
|
||
var showSelection = isSelected && _isComponentLibraryOpen;
|
||
host.BorderThickness = showSelection
|
||
? new Thickness(Math.Clamp(_currentDesktopCellSize * 0.04, 1, 3))
|
||
: new Thickness(0);
|
||
host.BorderBrush = showSelection ? GetThemeBrush("AdaptiveAccentBrush") : null;
|
||
|
||
if (TryGetResizeHandle(host) is Border resizeHandle)
|
||
{
|
||
resizeHandle.IsVisible = showSelection;
|
||
resizeHandle.IsHitTestVisible = showSelection;
|
||
}
|
||
}
|
||
|
||
private void ClearDesktopComponentSelection()
|
||
{
|
||
if (_selectedDesktopComponentHost is not null)
|
||
{
|
||
ApplySelectionStateToHost(_selectedDesktopComponentHost, false);
|
||
_selectedDesktopComponentHost = null;
|
||
}
|
||
|
||
_componentEditorWindowService.Close();
|
||
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
|
||
}
|
||
|
||
private void BeginDesktopComponentMoveDrag(Border sourceHost, DesktopComponentPlacementSnapshot placement, PointerPressedEventArgs e)
|
||
{
|
||
if (HasActiveDesktopEditSession ||
|
||
DesktopPagesViewport is null ||
|
||
!TryGetCurrentDesktopGridGeometry(out var grid) ||
|
||
!_componentRuntimeRegistry.TryGetDescriptor(placement.ComponentId, out var runtimeDescriptor))
|
||
{
|
||
return;
|
||
}
|
||
|
||
var (widthCells, heightCells) = NormalizeComponentCellSpan(
|
||
placement.ComponentId,
|
||
ComponentPlacementRules.EnsureMinimumSize(
|
||
runtimeDescriptor.Definition,
|
||
placement.WidthCells,
|
||
placement.HeightCells));
|
||
|
||
var pointerInViewport = e.GetPosition(DesktopPagesViewport);
|
||
_desktopEditOriginalRect = DesktopPlacementMath.GetCellRect(grid, placement.Column, placement.Row, widthCells, heightCells);
|
||
_desktopEditStartRow = placement.Row;
|
||
_desktopEditStartColumn = placement.Column;
|
||
var pointerOffset = DesktopPlacementMath.Subtract(
|
||
pointerInViewport,
|
||
new Point(_desktopEditOriginalRect.X, _desktopEditOriginalRect.Y));
|
||
|
||
_desktopEditSession = DesktopEditSession.CreateDraggingExisting(
|
||
placement.ComponentId,
|
||
placement.PlacementId,
|
||
placement.PageIndex,
|
||
widthCells,
|
||
heightCells,
|
||
pointerInViewport,
|
||
pointerOffset,
|
||
GetComponentLibraryBoundsInViewport());
|
||
|
||
CollapseComponentLibraryForDesktopEdit(ResolveDesktopEditTitle(placement.ComponentId));
|
||
SetDesktopEditSourceHost(sourceHost, 0.22);
|
||
EnsureDesktopEditOverlayPresenter();
|
||
UpdateDesktopEditOverlayMetadata(placement.ComponentId, widthCells, heightCells, L("component.move", "Move"));
|
||
ApplyDesktopEditOverlayPreviewImage(placement.ComponentId, placement.PlacementId, widthCells, heightCells);
|
||
PrimeDesktopEditPreviewImage(
|
||
placement.ComponentId,
|
||
placement.PlacementId,
|
||
placement.PageIndex,
|
||
widthCells,
|
||
heightCells);
|
||
_desktopEditOverlayPresenter?.SetPreviewRect(_desktopEditOriginalRect);
|
||
_desktopEditOverlayPresenter?.SetCandidateRect(_desktopEditOriginalRect);
|
||
_desktopEditOverlayPresenter?.SetInvalid(false);
|
||
_desktopEditOverlayPresenter?.Show(DesktopEditGhostVisualStyle.StandardLift);
|
||
UpdateDesktopEditSession(pointerInViewport);
|
||
|
||
e.Pointer.Capture(this);
|
||
}
|
||
|
||
private void BeginDesktopComponentNewDrag(string componentId, PointerPressedEventArgs e)
|
||
{
|
||
if (!_isComponentLibraryOpen ||
|
||
HasActiveDesktopEditSession ||
|
||
DesktopPagesViewport is null ||
|
||
_currentDesktopCellSize <= 0 ||
|
||
!_componentRuntimeRegistry.TryGetDescriptor(componentId, out var runtimeDescriptor) ||
|
||
!runtimeDescriptor.Definition.AllowDesktopPlacement)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var (widthCells, heightCells) = NormalizeComponentCellSpan(
|
||
componentId,
|
||
ComponentPlacementRules.EnsureMinimumSize(
|
||
runtimeDescriptor.Definition,
|
||
runtimeDescriptor.Definition.MinWidthCells,
|
||
runtimeDescriptor.Definition.MinHeightCells));
|
||
|
||
var pointerInViewport = e.GetPosition(DesktopPagesViewport);
|
||
var previewSize = GetComponentPixelSize(widthCells, heightCells, _currentDesktopCellSize, _currentDesktopCellGap);
|
||
var pointerOffset = new Point(previewSize.Width * 0.5, previewSize.Height * 0.5);
|
||
|
||
_desktopEditOriginalRect = new Rect(
|
||
DesktopPlacementMath.Subtract(pointerInViewport, pointerOffset),
|
||
previewSize);
|
||
_desktopEditSession = DesktopEditSession.CreatePendingNew(
|
||
componentId,
|
||
_currentDesktopSurfaceIndex,
|
||
widthCells,
|
||
heightCells,
|
||
pointerInViewport,
|
||
pointerOffset,
|
||
GetComponentLibraryBoundsInViewport());
|
||
|
||
EnsureDesktopEditOverlayPresenter();
|
||
UpdateDesktopEditOverlayMetadata(componentId, widthCells, heightCells, L("component_library.drag_hint", "Drag to place"));
|
||
ApplyDesktopEditOverlayPreviewImage(componentId, placementId: null, widthCells, heightCells);
|
||
PrimeDesktopEditPreviewImage(
|
||
componentId,
|
||
placementId: null,
|
||
_currentDesktopSurfaceIndex,
|
||
widthCells,
|
||
heightCells);
|
||
_desktopEditOverlayPresenter?.SetPreviewRect(_desktopEditOriginalRect);
|
||
_desktopEditOverlayPresenter?.SetCandidateRect(null);
|
||
_desktopEditOverlayPresenter?.SetInvalid(false);
|
||
|
||
e.Pointer.Capture(this);
|
||
}
|
||
|
||
private void OnDesktopComponentResizeHandlePointerPressed(object? sender, PointerPressedEventArgs e)
|
||
{
|
||
if (!_isComponentLibraryOpen ||
|
||
HasActiveDesktopEditSession ||
|
||
DesktopPagesViewport is null ||
|
||
sender is not Border handle ||
|
||
!e.GetCurrentPoint(handle).Properties.IsLeftButtonPressed)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var host = FindDesktopComponentHost(handle);
|
||
if (host?.Tag is not string placementId)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var placement = _desktopComponentPlacements.FirstOrDefault(p =>
|
||
string.Equals(p.PlacementId, placementId, StringComparison.OrdinalIgnoreCase));
|
||
if (placement is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
SetSelectedDesktopComponent(host);
|
||
BeginDesktopComponentResizeDrag(host, placement, e);
|
||
if (IsDesktopEditResizeMode)
|
||
{
|
||
e.Handled = true;
|
||
}
|
||
}
|
||
|
||
private void BeginDesktopComponentResizeDrag(
|
||
Border sourceHost,
|
||
DesktopComponentPlacementSnapshot placement,
|
||
PointerPressedEventArgs e)
|
||
{
|
||
if (HasActiveDesktopEditSession ||
|
||
DesktopPagesViewport is null ||
|
||
_currentDesktopCellSize <= 0 ||
|
||
!_componentRuntimeRegistry.TryGetDescriptor(placement.ComponentId, out var runtimeDescriptor) ||
|
||
!_desktopPageComponentGrids.TryGetValue(placement.PageIndex, out var pageGrid) ||
|
||
!TryGetCurrentDesktopGridGeometry(out var grid))
|
||
{
|
||
return;
|
||
}
|
||
|
||
var startSpan = NormalizeComponentCellSpan(
|
||
placement.ComponentId,
|
||
ComponentPlacementRules.EnsureMinimumSize(
|
||
runtimeDescriptor.Definition,
|
||
placement.WidthCells,
|
||
placement.HeightCells));
|
||
|
||
var minSpan = NormalizeComponentCellSpan(
|
||
placement.ComponentId,
|
||
ComponentPlacementRules.EnsureMinimumSize(
|
||
runtimeDescriptor.Definition,
|
||
runtimeDescriptor.Definition.MinWidthCells,
|
||
runtimeDescriptor.Definition.MinHeightCells));
|
||
|
||
var maxWidthCells = Math.Max(startSpan.WidthCells, pageGrid.ColumnDefinitions.Count - placement.Column);
|
||
var maxHeightCells = Math.Max(startSpan.HeightCells, pageGrid.RowDefinitions.Count - placement.Row);
|
||
if (maxWidthCells <= 0 || maxHeightCells <= 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var pointerInViewport = e.GetPosition(DesktopPagesViewport);
|
||
_desktopEditOriginalRect = DesktopPlacementMath.GetCellRect(
|
||
grid,
|
||
placement.Column,
|
||
placement.Row,
|
||
startSpan.WidthCells,
|
||
startSpan.HeightCells);
|
||
_desktopEditStartWidthCells = startSpan.WidthCells;
|
||
_desktopEditStartHeightCells = startSpan.HeightCells;
|
||
_desktopEditMinWidthCells = Math.Max(1, Math.Min(minSpan.WidthCells, maxWidthCells));
|
||
_desktopEditMinHeightCells = Math.Max(1, Math.Min(minSpan.HeightCells, maxHeightCells));
|
||
_desktopEditMaxWidthCells = maxWidthCells;
|
||
_desktopEditMaxHeightCells = maxHeightCells;
|
||
_desktopEditResizeMode = runtimeDescriptor.Definition.ResizeMode;
|
||
|
||
_desktopEditSession = DesktopEditSession.CreateResizingExisting(
|
||
placement.ComponentId,
|
||
placement.PlacementId,
|
||
placement.PageIndex,
|
||
startSpan.WidthCells,
|
||
startSpan.HeightCells,
|
||
pointerInViewport,
|
||
GetComponentLibraryBoundsInViewport()) with
|
||
{
|
||
TargetRow = placement.Row,
|
||
TargetColumn = placement.Column
|
||
};
|
||
|
||
CollapseComponentLibraryForDesktopEdit(ResolveDesktopEditTitle(placement.ComponentId));
|
||
SetDesktopEditSourceHost(sourceHost, 0.22);
|
||
EnsureDesktopEditOverlayPresenter();
|
||
UpdateDesktopEditOverlayMetadata(placement.ComponentId, startSpan.WidthCells, startSpan.HeightCells, L("component.resize", "Resize"));
|
||
ApplyDesktopEditOverlayPreviewImage(placement.ComponentId, placement.PlacementId, startSpan.WidthCells, startSpan.HeightCells);
|
||
PrimeDesktopEditPreviewImage(
|
||
placement.ComponentId,
|
||
placement.PlacementId,
|
||
placement.PageIndex,
|
||
startSpan.WidthCells,
|
||
startSpan.HeightCells);
|
||
_desktopEditOverlayPresenter?.SetPreviewRect(_desktopEditOriginalRect);
|
||
_desktopEditOverlayPresenter?.SetCandidateRect(_desktopEditOriginalRect);
|
||
_desktopEditOverlayPresenter?.SetInvalid(false);
|
||
_desktopEditOverlayPresenter?.Show(DesktopEditGhostVisualStyle.StandardLift);
|
||
UpdateDesktopEditSession(pointerInViewport);
|
||
e.Pointer.Capture(this);
|
||
}
|
||
|
||
private void CancelDesktopComponentResize(bool restoreOriginalSpan)
|
||
{
|
||
if (!IsDesktopEditResizeMode && !_isDesktopEditCommitPending)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (restoreOriginalSpan && _desktopEditSourceHost is not null)
|
||
{
|
||
Grid.SetColumnSpan(_desktopEditSourceHost, Math.Max(1, _desktopEditStartWidthCells));
|
||
Grid.SetRowSpan(_desktopEditSourceHost, Math.Max(1, _desktopEditStartHeightCells));
|
||
}
|
||
|
||
CancelDesktopEditSession(animate: false);
|
||
}
|
||
|
||
private void OnDesktopComponentDragPointerMoved(object? sender, PointerEventArgs e)
|
||
{
|
||
if (DesktopPagesViewport is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (!HasActiveDesktopEditSession || _isDesktopEditCommitPending)
|
||
{
|
||
return;
|
||
}
|
||
|
||
UpdateDesktopEditSession(e.GetPosition(DesktopPagesViewport));
|
||
}
|
||
|
||
private void OnDesktopComponentDragPointerReleased(object? sender, PointerReleasedEventArgs e)
|
||
{
|
||
if (DesktopPagesViewport is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (!HasActiveDesktopEditSession)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var pointerInViewport = e.GetPosition(DesktopPagesViewport);
|
||
var success = CompleteDesktopEditSession(pointerInViewport);
|
||
if (!success)
|
||
{
|
||
CancelDesktopEditSession(animate: !_desktopEditSession.IsPendingNew);
|
||
}
|
||
|
||
e.Pointer.Capture(null);
|
||
if (success)
|
||
{
|
||
e.Handled = true;
|
||
}
|
||
}
|
||
|
||
private void OnDesktopComponentDragPointerCaptureLost(object? sender, PointerCaptureLostEventArgs e)
|
||
{
|
||
if (_isDesktopEditCommitPending)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (!HasActiveDesktopEditSession)
|
||
{
|
||
return;
|
||
}
|
||
|
||
CancelDesktopEditSession(animate: !_desktopEditSession.IsPendingNew);
|
||
}
|
||
|
||
private bool TryMoveExistingDesktopComponent(string placementId, int row, int column)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(placementId))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (!TryGetDesktopPlacementById(placementId, out var placement))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var before = ClonePlacementSnapshot(placement);
|
||
if (!DesktopPlacementMath.HasCellPositionChanged(placement.Row, placement.Column, row, column))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var host = _desktopEditSourceHost;
|
||
if (host is null &&
|
||
_desktopPageComponentGrids.TryGetValue(placement.PageIndex, out var pageGrid))
|
||
{
|
||
host = pageGrid.Children
|
||
.OfType<Border>()
|
||
.FirstOrDefault(candidate => string.Equals(candidate.Tag as string, placementId, StringComparison.OrdinalIgnoreCase));
|
||
}
|
||
|
||
placement.Row = Math.Max(0, row);
|
||
placement.Column = Math.Max(0, column);
|
||
|
||
if (host is not null)
|
||
{
|
||
Grid.SetRow(host, placement.Row);
|
||
Grid.SetColumn(host, placement.Column);
|
||
ApplyDesktopEditStateToHost(host, _isComponentLibraryOpen);
|
||
}
|
||
|
||
PersistSettings();
|
||
TelemetryServices.Usage?.TrackDesktopComponentMoved(before, ClonePlacementSnapshot(placement), "component.move");
|
||
return true;
|
||
}
|
||
|
||
private void CancelDesktopComponentDrag()
|
||
{
|
||
if (!IsDesktopEditDragMode && !_isDesktopEditCommitPending)
|
||
{
|
||
return;
|
||
}
|
||
|
||
CancelDesktopEditSession(animate: false);
|
||
}
|
||
|
||
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();
|
||
ComponentLibraryCategoryPagesContainer.Width = double.NaN;
|
||
ComponentLibraryCategoryPagesContainer.Height = double.NaN;
|
||
ComponentLibraryCategoryPagesHost.Width = double.NaN;
|
||
ComponentLibraryCategoryPagesHost.Height = double.NaN;
|
||
|
||
if (categoryCount == 0)
|
||
{
|
||
_componentLibraryCategoryIndex = 0;
|
||
_componentLibraryActiveCategoryId = null;
|
||
UpdateComponentLibraryComponentNavigationButtons();
|
||
return;
|
||
}
|
||
|
||
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;
|
||
|
||
ComponentLibraryCategoryPagesContainer.ColumnDefinitions.Add(new ColumnDefinition(new GridLength(1, GridUnitType.Star)));
|
||
for (var i = 0; i < categoryCount; i++)
|
||
{
|
||
var category = _componentLibraryCategories[i];
|
||
var isSelected = i == _componentLibraryCategoryIndex;
|
||
var row = new RowDefinition(GridLength.Auto);
|
||
ComponentLibraryCategoryPagesContainer.RowDefinitions.Add(row);
|
||
|
||
var icon = new SymbolIcon
|
||
{
|
||
Symbol = category.Icon,
|
||
IconVariant = IconVariant.Regular,
|
||
FontSize = 18,
|
||
VerticalAlignment = VerticalAlignment.Center
|
||
};
|
||
|
||
var title = new TextBlock
|
||
{
|
||
Text = category.Title,
|
||
FontSize = 15,
|
||
FontWeight = isSelected ? FontWeight.Bold : FontWeight.SemiBold,
|
||
Foreground = GetThemeBrush("AdaptiveTextPrimaryBrush"),
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
TextTrimming = TextTrimming.CharacterEllipsis
|
||
};
|
||
|
||
var contentGrid = new Grid
|
||
{
|
||
ColumnDefinitions = new ColumnDefinitions("Auto,*"),
|
||
ColumnSpacing = 10,
|
||
Children = { icon, title }
|
||
};
|
||
Grid.SetColumn(icon, 0);
|
||
Grid.SetColumn(title, 1);
|
||
|
||
var itemButton = new Button
|
||
{
|
||
Tag = i,
|
||
Margin = new Thickness(0, 0, 0, i < categoryCount - 1 ? 8 : 0),
|
||
Padding = new Thickness(12, 10),
|
||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||
HorizontalContentAlignment = HorizontalAlignment.Stretch,
|
||
VerticalContentAlignment = VerticalAlignment.Center,
|
||
Background = isSelected
|
||
? GetThemeBrush("AdaptiveNavItemSelectedBackgroundBrush")
|
||
: GetThemeBrush("AdaptiveNavItemBackgroundBrush"),
|
||
BorderBrush = GetThemeBrush("AdaptiveButtonBorderBrush"),
|
||
BorderThickness = new Thickness(isSelected ? 1.5 : 1),
|
||
Content = contentGrid
|
||
};
|
||
itemButton.Click += OnComponentLibraryCategoryItemClick;
|
||
|
||
Grid.SetRow(itemButton, i);
|
||
Grid.SetColumn(itemButton, 0);
|
||
ComponentLibraryCategoryPagesContainer.Children.Add(itemButton);
|
||
}
|
||
|
||
_componentLibraryCategoryHostTransform = null;
|
||
_componentLibraryCategoryPageWidth = 0;
|
||
|
||
if (ComponentLibraryBackTextBlock is not null)
|
||
{
|
||
ComponentLibraryBackTextBlock.Text = L("common.back", "Back");
|
||
}
|
||
|
||
EnsureComponentLibraryPreviewWarmup();
|
||
}
|
||
|
||
private IReadOnlyList<ComponentLibraryCategory> GetComponentLibraryCategories()
|
||
{
|
||
var categories = _componentLibraryService.GetDesktopCategories();
|
||
if (categories.Count == 0)
|
||
{
|
||
return Array.Empty<ComponentLibraryCategory>();
|
||
}
|
||
|
||
return categories
|
||
.Select(category => new ComponentLibraryCategory(
|
||
category.Id,
|
||
ResolveComponentLibraryCategoryIcon(category.Id),
|
||
GetLocalizedComponentLibraryCategoryTitle(category.Id),
|
||
category.Components))
|
||
.ToList();
|
||
}
|
||
|
||
private Symbol ResolveComponentLibraryCategoryIcon(string categoryId)
|
||
{
|
||
if (string.Equals(categoryId, "Clock", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return Symbol.Clock;
|
||
}
|
||
|
||
if (string.Equals(categoryId, "Date", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return Symbol.CalendarDate;
|
||
}
|
||
|
||
if (string.Equals(categoryId, "Weather", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return Symbol.WeatherSunny;
|
||
}
|
||
|
||
if (string.Equals(categoryId, "Board", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return Symbol.Edit;
|
||
}
|
||
|
||
if (string.Equals(categoryId, "Media", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return Symbol.Play;
|
||
}
|
||
|
||
if (string.Equals(categoryId, "Info", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return Symbol.Apps;
|
||
}
|
||
|
||
if (string.Equals(categoryId, "Calculator", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return Symbol.Calculator;
|
||
}
|
||
|
||
if (string.Equals(categoryId, "Study", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return Symbol.Apps;
|
||
}
|
||
|
||
if (string.Equals(categoryId, "File", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return Symbol.Folder;
|
||
}
|
||
|
||
return Symbol.Apps;
|
||
}
|
||
|
||
private string GetLocalizedComponentLibraryCategoryTitle(string categoryId)
|
||
{
|
||
if (string.Equals(categoryId, "Clock", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return L("component_category.clock", "Clock");
|
||
}
|
||
|
||
if (string.Equals(categoryId, "Date", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return L("component_category.date", "Calendar");
|
||
}
|
||
|
||
if (string.Equals(categoryId, "Weather", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return L("component_category.weather", "Weather");
|
||
}
|
||
|
||
if (string.Equals(categoryId, "Board", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return L("component_category.board", "Board");
|
||
}
|
||
|
||
if (string.Equals(categoryId, "Media", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return L("component_category.media", "Media");
|
||
}
|
||
|
||
if (string.Equals(categoryId, "Info", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return L("component_category.info", "Info");
|
||
}
|
||
|
||
if (string.Equals(categoryId, "Calculator", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return L("component_category.calculator", "Calculator");
|
||
}
|
||
|
||
if (string.Equals(categoryId, "Study", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return L("component_category.study", "Study");
|
||
}
|
||
|
||
if (string.Equals(categoryId, "File", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return L("component_category.file", "File");
|
||
}
|
||
|
||
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;
|
||
UpdateComponentLibraryComponentNavigationButtons();
|
||
}
|
||
|
||
private void UpdateComponentLibraryComponentNavigationButtons()
|
||
{
|
||
if (ComponentLibraryPrevComponentButton is null || ComponentLibraryNextComponentButton is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var maxIndex = Math.Max(0, _componentLibraryActiveComponents.Count - 1);
|
||
var hasMultiplePages = maxIndex > 0;
|
||
|
||
ComponentLibraryPrevComponentButton.IsVisible = hasMultiplePages;
|
||
ComponentLibraryNextComponentButton.IsVisible = hasMultiplePages;
|
||
|
||
if (!hasMultiplePages)
|
||
{
|
||
ComponentLibraryPrevComponentButton.IsEnabled = false;
|
||
ComponentLibraryNextComponentButton.IsEnabled = false;
|
||
return;
|
||
}
|
||
|
||
ComponentLibraryPrevComponentButton.IsEnabled = _componentLibraryComponentIndex > 0;
|
||
ComponentLibraryNextComponentButton.IsEnabled = _componentLibraryComponentIndex < maxIndex;
|
||
}
|
||
|
||
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;
|
||
_ = WarmComponentLibraryCategoryPreviewsAsync(category);
|
||
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;
|
||
|
||
ClearTimeZoneServiceBindings(ComponentLibraryComponentPagesContainer.Children.OfType<Control>().ToList());
|
||
ComponentLibraryComponentPagesContainer.Children.Clear();
|
||
ComponentLibraryComponentPagesContainer.RowDefinitions.Clear();
|
||
ComponentLibraryComponentPagesContainer.ColumnDefinitions.Clear();
|
||
ClearComponentLibraryPreviewVisualTargets();
|
||
if (componentCount == 0)
|
||
{
|
||
_componentLibraryComponentIndex = 0;
|
||
UpdateComponentLibraryComponentNavigationButtons();
|
||
return;
|
||
}
|
||
|
||
var viewportWidth = ComponentLibraryComponentViewport.Bounds.Width;
|
||
if (viewportWidth <= 1)
|
||
{
|
||
if (ComponentLibraryComponentViewport.Parent is Control parent && parent.Bounds.Width > 1)
|
||
{
|
||
// Parent includes left/right nav buttons; reserve space to get true viewport width.
|
||
viewportWidth = Math.Max(1, parent.Bounds.Width - 96);
|
||
}
|
||
else if (ComponentLibraryWindow is not null)
|
||
{
|
||
viewportWidth = Math.Max(1, ComponentLibraryWindow.Bounds.Width - 150);
|
||
}
|
||
}
|
||
|
||
var viewportHeight = ComponentLibraryComponentViewport.Bounds.Height;
|
||
if (viewportHeight <= 1)
|
||
{
|
||
if (ComponentLibraryComponentViewport.Parent is Control parent && parent.Bounds.Height > 1)
|
||
{
|
||
viewportHeight = Math.Max(1, parent.Bounds.Height);
|
||
}
|
||
else if (ComponentLibraryWindow is not null)
|
||
{
|
||
viewportHeight = Math.Max(1, ComponentLibraryWindow.Bounds.Height - 170);
|
||
}
|
||
}
|
||
|
||
_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 component = _componentLibraryActiveComponents[i];
|
||
|
||
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.94;
|
||
var previewMaxHeight = viewportHeight * 0.86;
|
||
var previewSpan = NormalizeComponentCellSpan(
|
||
component.ComponentId,
|
||
(component.MinWidthCells, component.MinHeightCells));
|
||
var previewCellSize = Math.Min(
|
||
previewMaxWidth / Math.Max(1, previewSpan.WidthCells),
|
||
previewMaxHeight / Math.Max(1, previewSpan.HeightCells));
|
||
previewCellSize = Math.Clamp(previewCellSize, 24, 96);
|
||
|
||
var previewWidth = previewSpan.WidthCells * previewCellSize;
|
||
var previewHeight = previewSpan.HeightCells * previewCellSize;
|
||
var previewKey = CreateComponentTypePreviewKey(component.ComponentId, previewSpan.WidthCells, previewSpan.HeightCells);
|
||
var cachedPreviewImage = ResolveComponentTypePreviewImage(component.ComponentId, previewSpan.WidthCells, previewSpan.HeightCells);
|
||
|
||
var previewImage = new Image
|
||
{
|
||
Width = previewWidth,
|
||
Height = previewHeight,
|
||
Stretch = Stretch.Uniform,
|
||
Source = cachedPreviewImage,
|
||
IsVisible = cachedPreviewImage is not null,
|
||
IsHitTestVisible = false
|
||
};
|
||
|
||
var previewFallback = new Border
|
||
{
|
||
Width = previewWidth,
|
||
Height = previewHeight,
|
||
Background = GetThemeBrush("AdaptiveCardBackgroundBrush"),
|
||
BorderBrush = GetThemeBrush("AdaptiveButtonBorderBrush"),
|
||
BorderThickness = new Thickness(1),
|
||
CornerRadius = new CornerRadius(Math.Clamp(Math.Min(previewWidth, previewHeight) * 0.18, 12, 28)),
|
||
IsVisible = cachedPreviewImage is null,
|
||
Child = new TextBlock
|
||
{
|
||
Text = L("component_library.preview_loading", "Preparing preview"),
|
||
FontSize = 11,
|
||
Foreground = GetThemeBrush("AdaptiveTextSecondaryBrush"),
|
||
HorizontalAlignment = HorizontalAlignment.Center,
|
||
VerticalAlignment = VerticalAlignment.Center
|
||
}
|
||
};
|
||
RegisterComponentLibraryPreviewVisual(previewKey, previewImage, previewFallback);
|
||
|
||
var previewSurface = new Grid
|
||
{
|
||
Width = previewWidth,
|
||
Height = previewHeight,
|
||
IsHitTestVisible = false,
|
||
Children =
|
||
{
|
||
previewImage,
|
||
previewFallback
|
||
}
|
||
};
|
||
|
||
var previewBorder = new Border
|
||
{
|
||
Width = previewWidth,
|
||
Height = previewHeight,
|
||
ClipToBounds = false,
|
||
Background = Brushes.Transparent,
|
||
BorderThickness = new Thickness(0),
|
||
Child = previewSurface,
|
||
Tag = component.ComponentId
|
||
};
|
||
previewBorder.PointerPressed += OnComponentLibraryComponentPreviewPointerPressed;
|
||
|
||
var label = new TextBlock
|
||
{
|
||
Text = GetLocalizedComponentDisplayName(component),
|
||
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 = 8,
|
||
HorizontalAlignment = HorizontalAlignment.Center,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
Children =
|
||
{
|
||
previewBorder,
|
||
label,
|
||
hint
|
||
}
|
||
};
|
||
|
||
page.Children.Add(stack);
|
||
|
||
Grid.SetRow(page, 0);
|
||
Grid.SetColumn(page, i);
|
||
ComponentLibraryComponentPagesContainer.Children.Add(page);
|
||
|
||
if (cachedPreviewImage is null)
|
||
{
|
||
_ = EnsureComponentTypePreviewImageAsync(component.ComponentId, previewSpan.WidthCells, previewSpan.HeightCells);
|
||
}
|
||
else
|
||
{
|
||
ApplyPreviewEntryToEmbeddedVisuals(previewKey);
|
||
}
|
||
}
|
||
|
||
_componentLibraryComponentHostTransform = ComponentLibraryComponentPagesHost.RenderTransform as TranslateTransform;
|
||
if (_componentLibraryComponentHostTransform is null)
|
||
{
|
||
_componentLibraryComponentHostTransform = new TranslateTransform();
|
||
ComponentLibraryComponentPagesHost.RenderTransform = _componentLibraryComponentHostTransform;
|
||
}
|
||
|
||
ApplyComponentLibraryComponentOffset();
|
||
UpdateComponentLibraryComponentNavigationButtons();
|
||
}
|
||
|
||
private void ClearComponentLibraryPreviewControls()
|
||
{
|
||
if (ComponentLibraryComponentPagesContainer is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
ClearTimeZoneServiceBindings(ComponentLibraryComponentPagesContainer.Children.OfType<Control>().ToList());
|
||
ComponentLibraryComponentPagesContainer.Children.Clear();
|
||
ComponentLibraryComponentPagesContainer.RowDefinitions.Clear();
|
||
ComponentLibraryComponentPagesContainer.ColumnDefinitions.Clear();
|
||
ClearComponentLibraryPreviewVisualTargets();
|
||
}
|
||
|
||
private string GetLocalizedComponentDisplayName(ComponentLibraryComponentEntry component)
|
||
{
|
||
return string.IsNullOrWhiteSpace(component.DisplayNameLocalizationKey)
|
||
? component.DisplayName
|
||
: L(component.DisplayNameLocalizationKey, component.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 (HasActiveDesktopEditSession)
|
||
{
|
||
e.Handled = true;
|
||
}
|
||
}
|
||
|
||
private bool _isComponentLibraryWindowDragging;
|
||
private Point _componentLibraryWindowDragStartPoint;
|
||
private Thickness _componentLibraryWindowOriginalMargin;
|
||
private bool _isComponentLibraryWindowPositionCustomized;
|
||
|
||
private void OnComponentLibraryWindowPointerPressed(object? sender, PointerPressedEventArgs e)
|
||
{
|
||
if (ComponentLibraryWindow is null || !_isComponentLibraryOpen || IsComponentLibraryTemporarilyCollapsedForDesktopEdit())
|
||
{
|
||
return;
|
||
}
|
||
|
||
var point = e.GetPosition(ComponentLibraryWindow);
|
||
if (point.Y > 40) // 閺嶅洭顣介弽蹇涚彯鎼达妇瀹虫稉?0px
|
||
{
|
||
return;
|
||
}
|
||
|
||
_isComponentLibraryWindowDragging = true;
|
||
_componentLibraryWindowDragStartPoint = e.GetPosition(this);
|
||
_componentLibraryWindowOriginalMargin = ComponentLibraryWindow.Margin;
|
||
|
||
e.Pointer.Capture(ComponentLibraryWindow);
|
||
e.Handled = true;
|
||
}
|
||
|
||
private void OnComponentLibraryWindowPointerMoved(object? sender, PointerEventArgs e)
|
||
{
|
||
if (IsComponentLibraryTemporarilyCollapsedForDesktopEdit())
|
||
{
|
||
if (_isComponentLibraryWindowDragging)
|
||
{
|
||
_isComponentLibraryWindowDragging = false;
|
||
e.Pointer.Capture(null);
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
if (!_isComponentLibraryWindowDragging || ComponentLibraryWindow is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var currentPoint = e.GetPosition(this);
|
||
var delta = currentPoint - _componentLibraryWindowDragStartPoint;
|
||
|
||
var newMargin = new Thickness(
|
||
Math.Max(10, _componentLibraryWindowOriginalMargin.Left + delta.X),
|
||
Math.Max(10, _componentLibraryWindowOriginalMargin.Top + delta.Y),
|
||
Math.Max(10, _componentLibraryWindowOriginalMargin.Right - delta.X),
|
||
Math.Max(10, _componentLibraryWindowOriginalMargin.Bottom - delta.Y)
|
||
);
|
||
|
||
ComponentLibraryWindow.Margin = newMargin;
|
||
SyncComponentLibraryCollapseExpandedState();
|
||
e.Handled = true;
|
||
}
|
||
|
||
private void OnComponentLibraryWindowPointerReleased(object? sender, PointerReleasedEventArgs e)
|
||
{
|
||
if (IsComponentLibraryTemporarilyCollapsedForDesktopEdit())
|
||
{
|
||
if (_isComponentLibraryWindowDragging)
|
||
{
|
||
_isComponentLibraryWindowDragging = false;
|
||
e.Pointer.Capture(null);
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
if (!_isComponentLibraryWindowDragging)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_isComponentLibraryWindowDragging = false;
|
||
e.Pointer.Capture(null);
|
||
|
||
if (ComponentLibraryWindow is not null)
|
||
{
|
||
SaveComponentLibraryWindowPosition();
|
||
}
|
||
|
||
e.Handled = true;
|
||
}
|
||
|
||
private void OnComponentLibraryBackClick(object? sender, RoutedEventArgs e)
|
||
{
|
||
ShowComponentLibraryCategoryView();
|
||
BuildComponentLibraryCategoryPages();
|
||
}
|
||
|
||
private void OnComponentLibraryCategoryItemClick(object? sender, RoutedEventArgs e)
|
||
{
|
||
if (sender is not Button button ||
|
||
button.Tag is not int categoryIndex ||
|
||
_componentLibraryCategories.Count == 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_componentLibraryCategoryIndex = Math.Clamp(categoryIndex, 0, Math.Max(0, _componentLibraryCategories.Count - 1));
|
||
OpenComponentLibraryCurrentCategory();
|
||
}
|
||
|
||
private void OnComponentLibraryPrevComponentClick(object? sender, RoutedEventArgs e)
|
||
{
|
||
if (_componentLibraryActiveComponents.Count <= 1)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_componentLibraryComponentIndex = Math.Max(0, _componentLibraryComponentIndex - 1);
|
||
ApplyComponentLibraryComponentOffset();
|
||
}
|
||
|
||
private void OnComponentLibraryNextComponentClick(object? sender, RoutedEventArgs e)
|
||
{
|
||
var maxIndex = Math.Max(0, _componentLibraryActiveComponents.Count - 1);
|
||
if (maxIndex <= 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_componentLibraryComponentIndex = Math.Min(maxIndex, _componentLibraryComponentIndex + 1);
|
||
ApplyComponentLibraryComponentOffset();
|
||
}
|
||
|
||
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 SaveComponentLibraryWindowPosition()
|
||
{
|
||
if (ComponentLibraryWindow is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var margin = ComponentLibraryWindow.Margin;
|
||
_savedComponentLibraryMargin = margin;
|
||
_isComponentLibraryWindowPositionCustomized = true;
|
||
SyncComponentLibraryCollapseExpandedState();
|
||
}
|
||
|
||
private void RestoreComponentLibraryWindowPosition()
|
||
{
|
||
if (ComponentLibraryWindow is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
ComponentLibraryWindow.Margin = _savedComponentLibraryMargin;
|
||
SyncComponentLibraryCollapseExpandedState();
|
||
}
|
||
|
||
private Thickness _savedComponentLibraryMargin = new Thickness(24, 24, 24, 100);
|
||
|
||
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();
|
||
}
|
||
|
||
internal void SaveAllWhiteboardNotes()
|
||
{
|
||
foreach (var pageGrid in _desktopPageComponentGrids.Values)
|
||
{
|
||
foreach (var host in pageGrid.Children.OfType<Border>())
|
||
{
|
||
var contentHost = TryGetContentHost(host);
|
||
if (contentHost?.Child is WhiteboardWidget whiteboard)
|
||
{
|
||
whiteboard.ForceSaveNote();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|