mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
Apply Avalonia 12 migration changes: replace SystemDecorations with WindowDecorations and remove ExtendClientAreaChromeHints/ExtendClientAreaTitleBarHeightHint usages; update BindingPlugins removal logic (no-op); switch clipboard usage to ClipboardExtensions.SetTextAsync; update Bitmap.CopyPixels calls to the new signature. Replace TextBox.Watermark with PlaceholderText, convert NumberBox styles to FANumberBox and adjust templates, change Checked/Unchecked handlers to IsCheckedChanged, and adapt FluentIcons usages (SymbolIconSource -> FASymbol/FAFont/FluentIcon equivalents). Fix MainWindow partial classes to inherit Window and correct missing variables/fields/usings. Add migration docs/specs/tasks under .trae and include a small TestFluentIcons project for icon testing.
1901 lines
69 KiB
C#
1901 lines
69 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Diagnostics;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Threading.Tasks;
|
||
using Avalonia;
|
||
using Avalonia.Animation;
|
||
using Avalonia.Controls;
|
||
using Avalonia.Input;
|
||
using Avalonia.Interactivity;
|
||
using Avalonia.Layout;
|
||
using Avalonia.Media;
|
||
using Avalonia.Media.Imaging;
|
||
using Avalonia.Threading;
|
||
using Avalonia.VisualTree;
|
||
using FluentAvalonia.UI.Controls;
|
||
using LanMountainDesktop.Models;
|
||
using LanMountainDesktop.PluginSdk;
|
||
using LanMountainDesktop.Services;
|
||
using LanMountainDesktop.Theme;
|
||
|
||
namespace LanMountainDesktop.Views;
|
||
|
||
public partial class MainWindow : Window
|
||
{
|
||
private const int MinDesktopPageCount = 1;
|
||
private const int MaxDesktopPageCount = 12;
|
||
private enum LauncherEntryKind
|
||
{
|
||
Folder,
|
||
Shortcut
|
||
}
|
||
|
||
private sealed record LauncherHiddenItemToken(LauncherEntryKind Kind, string Key);
|
||
|
||
private sealed record LauncherHiddenItemView(
|
||
LauncherEntryKind Kind,
|
||
string Key,
|
||
string DisplayName,
|
||
string Monogram,
|
||
Bitmap? IconBitmap);
|
||
|
||
private readonly WindowsStartMenuService _windowsStartMenuService = new();
|
||
private readonly LinuxDesktopEntryService _linuxDesktopEntryService = new();
|
||
private readonly Dictionary<string, Bitmap> _launcherIconCache = new(StringComparer.OrdinalIgnoreCase);
|
||
private readonly Stack<StartMenuFolderNode> _launcherFolderStack = [];
|
||
private readonly HashSet<string> _hiddenLauncherFolderPaths = new(StringComparer.OrdinalIgnoreCase);
|
||
private readonly HashSet<string> _hiddenLauncherAppPaths = new(StringComparer.OrdinalIgnoreCase);
|
||
private bool _showLauncherTileBackground = true;
|
||
private Button? _selectedLauncherTileButton;
|
||
private LauncherEntryKind? _selectedLauncherEntryKind;
|
||
private string? _selectedLauncherEntryKey;
|
||
private StartMenuFolderNode _startMenuRoot = new("All Apps", string.Empty);
|
||
private byte[]? _launcherFolderIconPngBytes;
|
||
private Bitmap? _launcherFolderIconBitmap;
|
||
private int _desktopPageCount = MinDesktopPageCount;
|
||
private int _currentDesktopSurfaceIndex;
|
||
private double _desktopSurfacePageWidth;
|
||
private TranslateTransform? _desktopPagesHostTransform;
|
||
private Transitions? _desktopPagesHostSnapTransitions;
|
||
private bool _desktopPagesHostTransitionsSuspended;
|
||
private bool _isDesktopSwipeActive;
|
||
private bool _isDesktopSwipeDirectionLocked;
|
||
private Point _desktopSwipeStartPoint;
|
||
private Point _desktopSwipeCurrentPoint;
|
||
private Point _desktopSwipeLastPoint;
|
||
private long _desktopSwipeLastTimestamp;
|
||
private double _desktopSwipeVelocityX;
|
||
private double _desktopSwipeBaseOffset;
|
||
private bool _desktopPageContextInitialized;
|
||
private bool _desktopPageContextEditMode;
|
||
private int _desktopPageContextActiveMask;
|
||
private int? _desktopPageContextSettlingSourceIndex;
|
||
private int? _desktopPageContextSettlingTargetIndex;
|
||
private int _desktopPageContextSettleRevision;
|
||
|
||
// 婵犵數鍋為崹鍫曞箰閹间絸鍥箥椤旂懓浜鹃柛顭戝亯婢规ɑ銇勯婊冨妤犵偛顑呴埞鎴﹀窗?闂傚倷绀侀幉锟犳偡閿旂晫绠惧┑鐘叉搐閺嬩焦銇勯幘鍗炵仼缂佺媭鍨堕弻鈥崇暤椤旂厧鏁俊銈呮噺閻撶喖鏌嶉崫鍕灓闁绘帡绠栭弻?
|
||
private bool _isThreeFingerOrRightDragSwipeActive;
|
||
private readonly HashSet<int> _activePointerIds = [];
|
||
|
||
private int LauncherSurfaceIndex => Math.Max(MinDesktopPageCount, _desktopPageCount);
|
||
|
||
private int TotalSurfaceCount => LauncherSurfaceIndex + 1;
|
||
|
||
private void InitializeDesktopSurfaceState(DesktopLayoutSettingsSnapshot snapshot)
|
||
{
|
||
var loadedPageCount = snapshot.DesktopPageCount <= 0 ? MinDesktopPageCount : snapshot.DesktopPageCount;
|
||
_desktopPageCount = Math.Clamp(loadedPageCount, MinDesktopPageCount, MaxDesktopPageCount);
|
||
_currentDesktopSurfaceIndex = Math.Clamp(snapshot.CurrentDesktopSurfaceIndex, 0, LauncherSurfaceIndex);
|
||
}
|
||
|
||
private void InitializeLauncherVisibilitySettings(LauncherSettingsSnapshot snapshot)
|
||
{
|
||
_hiddenLauncherFolderPaths.Clear();
|
||
if (snapshot.HiddenLauncherFolderPaths is not null)
|
||
{
|
||
foreach (var folderPath in snapshot.HiddenLauncherFolderPaths)
|
||
{
|
||
var key = NormalizeLauncherHiddenKey(folderPath);
|
||
if (!string.IsNullOrWhiteSpace(key))
|
||
{
|
||
_hiddenLauncherFolderPaths.Add(key);
|
||
}
|
||
}
|
||
}
|
||
|
||
_hiddenLauncherAppPaths.Clear();
|
||
if (snapshot.HiddenLauncherAppPaths is not null)
|
||
{
|
||
foreach (var appPath in snapshot.HiddenLauncherAppPaths)
|
||
{
|
||
var key = NormalizeLauncherHiddenKey(appPath);
|
||
if (!string.IsNullOrWhiteSpace(key))
|
||
{
|
||
_hiddenLauncherAppPaths.Add(key);
|
||
}
|
||
}
|
||
}
|
||
|
||
_showLauncherTileBackground = snapshot.ShowTileBackground;
|
||
}
|
||
|
||
private void InitializeDesktopSurfaceSwipeHandlers()
|
||
{
|
||
// Capture swipe intent before child controls consume pointer events.
|
||
AddHandler(PointerPressedEvent, OnDesktopPagesPointerPressed, RoutingStrategies.Tunnel, handledEventsToo: true);
|
||
AddHandler(PointerMovedEvent, OnDesktopPagesPointerMoved, RoutingStrategies.Tunnel, handledEventsToo: true);
|
||
AddHandler(PointerReleasedEvent, OnDesktopPagesPointerReleased, RoutingStrategies.Tunnel, handledEventsToo: true);
|
||
AddHandler(PointerCaptureLostEvent, OnDesktopPagesPointerCaptureLost, RoutingStrategies.Tunnel, handledEventsToo: true);
|
||
}
|
||
|
||
private async void LoadLauncherEntriesAsync()
|
||
{
|
||
try
|
||
{
|
||
var loadResult = await Task.Run(() =>
|
||
{
|
||
var loadedRoot = OperatingSystem.IsLinux()
|
||
? _linuxDesktopEntryService.Load()
|
||
: _windowsStartMenuService.Load();
|
||
var folderIconBytes = OperatingSystem.IsWindows()
|
||
? WindowsIconService.TryGetSystemFolderIconPngBytes()
|
||
: null;
|
||
return (Root: loadedRoot, FolderIcon: folderIconBytes);
|
||
});
|
||
await Dispatcher.UIThread.InvokeAsync(() =>
|
||
{
|
||
_startMenuRoot = loadResult.Root;
|
||
_launcherFolderIconPngBytes = loadResult.FolderIcon;
|
||
_launcherFolderIconBitmap?.Dispose();
|
||
_launcherFolderIconBitmap = null;
|
||
RenderLauncherRootTiles();
|
||
RenderLauncherHiddenItemsList();
|
||
}, DispatcherPriority.Background);
|
||
}
|
||
catch
|
||
{
|
||
_startMenuRoot = new StartMenuFolderNode("All Apps", string.Empty);
|
||
_launcherFolderIconPngBytes = null;
|
||
_launcherFolderIconBitmap?.Dispose();
|
||
_launcherFolderIconBitmap = null;
|
||
RenderLauncherRootTiles();
|
||
RenderLauncherHiddenItemsList();
|
||
}
|
||
}
|
||
|
||
private void UpdateDesktopSurfaceLayout(DesktopGridMetrics gridMetrics)
|
||
{
|
||
if (DesktopPagesViewport is null ||
|
||
DesktopPagesHost is null ||
|
||
DesktopPagesContainer is null ||
|
||
LauncherPagePanel is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_desktopPagesHostTransform = DesktopPagesHost.RenderTransform as TranslateTransform;
|
||
if (_desktopPagesHostTransform is null)
|
||
{
|
||
_desktopPagesHostTransform = new TranslateTransform();
|
||
DesktopPagesHost.RenderTransform = _desktopPagesHostTransform;
|
||
}
|
||
|
||
if (_desktopPagesHostTransitionsSuspended)
|
||
{
|
||
_desktopPagesHostTransform.Transitions = null;
|
||
}
|
||
else
|
||
{
|
||
_desktopPagesHostSnapTransitions ??= _desktopPagesHostTransform.Transitions;
|
||
}
|
||
|
||
var viewportRow = gridMetrics.RowCount > 2 ? 1 : 0;
|
||
var viewportRowSpan = gridMetrics.RowCount > 2 ? gridMetrics.RowCount - 2 : 1;
|
||
var pageWidth = Math.Max(1, gridMetrics.GridWidthPx);
|
||
var pageHeight = Math.Max(
|
||
1,
|
||
viewportRowSpan * gridMetrics.CellSize + Math.Max(0, viewportRowSpan - 1) * gridMetrics.GapPx);
|
||
|
||
Grid.SetRow(DesktopPagesViewport, viewportRow);
|
||
Grid.SetColumn(DesktopPagesViewport, 0);
|
||
Grid.SetRowSpan(DesktopPagesViewport, viewportRowSpan);
|
||
Grid.SetColumnSpan(DesktopPagesViewport, gridMetrics.ColumnCount);
|
||
DesktopPagesViewport.Width = pageWidth;
|
||
DesktopPagesViewport.Height = pageHeight;
|
||
if (DesktopEditDragLayer is not null)
|
||
{
|
||
DesktopEditDragLayer.Width = pageWidth;
|
||
DesktopEditDragLayer.Height = pageHeight;
|
||
UpdateDesktopEditOverlayViewportSize();
|
||
}
|
||
|
||
DesktopPagesHost.RowDefinitions.Clear();
|
||
DesktopPagesHost.RowDefinitions.Add(new RowDefinition(new GridLength(pageHeight, GridUnitType.Pixel)));
|
||
DesktopPagesHost.ColumnDefinitions.Clear();
|
||
DesktopPagesHost.ColumnDefinitions.Add(
|
||
new ColumnDefinition(new GridLength(pageWidth * _desktopPageCount, GridUnitType.Pixel)));
|
||
DesktopPagesHost.ColumnDefinitions.Add(new ColumnDefinition(new GridLength(pageWidth, GridUnitType.Pixel)));
|
||
DesktopPagesHost.Width = pageWidth * TotalSurfaceCount;
|
||
DesktopPagesHost.Height = pageHeight;
|
||
|
||
DesktopPagesContainer.RowDefinitions.Clear();
|
||
DesktopPagesContainer.RowDefinitions.Add(new RowDefinition(new GridLength(pageHeight, GridUnitType.Pixel)));
|
||
DesktopPagesContainer.ColumnDefinitions.Clear();
|
||
ClearTimeZoneServiceBindings(DesktopPagesContainer.Children.OfType<Control>().ToList());
|
||
DesktopPagesContainer.Children.Clear();
|
||
DesktopPagesContainer.Width = pageWidth * _desktopPageCount;
|
||
DesktopPagesContainer.Height = pageHeight;
|
||
_desktopPageComponentGrids.Clear();
|
||
InvalidateDesktopPageAwareComponentContextCache();
|
||
for (var index = 0; index < _desktopPageCount; index++)
|
||
{
|
||
DesktopPagesContainer.ColumnDefinitions.Add(new ColumnDefinition(new GridLength(pageWidth, GridUnitType.Pixel)));
|
||
|
||
var pageGrid = new Grid
|
||
{
|
||
Width = pageWidth,
|
||
Height = pageHeight,
|
||
RowSpacing = gridMetrics.GapPx,
|
||
ColumnSpacing = gridMetrics.GapPx,
|
||
Background = Brushes.Transparent,
|
||
ShowGridLines = false
|
||
};
|
||
|
||
for (var row = 0; row < viewportRowSpan; row++)
|
||
{
|
||
pageGrid.RowDefinitions.Add(new RowDefinition(new GridLength(gridMetrics.CellSize, GridUnitType.Pixel)));
|
||
}
|
||
|
||
for (var col = 0; col < gridMetrics.ColumnCount; col++)
|
||
{
|
||
pageGrid.ColumnDefinitions.Add(new ColumnDefinition(new GridLength(gridMetrics.CellSize, GridUnitType.Pixel)));
|
||
}
|
||
|
||
_desktopPageComponentGrids[index] = pageGrid;
|
||
RestoreDesktopPageComponents(index);
|
||
|
||
Grid.SetColumn(pageGrid, index);
|
||
Grid.SetRow(pageGrid, 0);
|
||
DesktopPagesContainer.Children.Add(pageGrid);
|
||
}
|
||
|
||
Grid.SetColumn(LauncherPagePanel, 1);
|
||
Grid.SetRow(LauncherPagePanel, 0);
|
||
|
||
var launcherMargin = Math.Clamp(gridMetrics.CellSize * 0.15, 6, 16);
|
||
LauncherPagePanel.Margin = new Thickness(launcherMargin);
|
||
LauncherPagePanel.Width = Math.Max(1, pageWidth - launcherMargin * 2);
|
||
LauncherPagePanel.Height = Math.Max(1, pageHeight - launcherMargin * 2);
|
||
LauncherPagePanel.MaxWidth = pageWidth - launcherMargin * 2;
|
||
LauncherPagePanel.MaxHeight = pageHeight - launcherMargin * 2;
|
||
|
||
// 闂傚倷绀侀幖顐⒚洪妶澶嬪仱闁靛ň鏅涢拑鐔封攽閻樺弶鎼愰悷娆欓檮閵囧嫰寮介妸銊ヮ棟閻炴氨鍠栧娲川婵犲嫭鍣┑鐘灪閿氶棁澶嬫叏濡炶浜鹃悗娈垮枙缁瑥鐣烽幆閭︽Ь濡炪倕绻戦幐鎶藉箖濮椻偓閹瑩鍩℃担宄邦棜
|
||
UpdateLauncherTileLayout();
|
||
|
||
_desktopSurfacePageWidth = pageWidth;
|
||
ClampSurfaceIndex();
|
||
ApplyDesktopSurfaceOffset();
|
||
}
|
||
|
||
private void UpdateLauncherTileLayout()
|
||
{
|
||
if (LauncherRootTilePanel is null || LauncherPagePanel is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var availableWidth = Math.Max(1, LauncherPagePanel.Bounds.Width - 36); // 18px padding on each side
|
||
var availableHeight = Math.Max(1, LauncherPagePanel.Bounds.Height - 100); // 婵犵妲呴崑鍛熆濡皷鍋撳鐓庢珝鐎殿喗濞婇崺鈧い鎺戝閻撴稓鈧箍鍎遍幊蹇涘窗濡眹浜滈柨婵嗘处濞呮洜绱掗鍊熷閻撱倖銇勮箛鎾村珔缂?
|
||
|
||
if (availableWidth <= 1 || availableHeight <= 1)
|
||
{
|
||
// 婵犵數濮烽。浠嬪焵椤掆偓閸熷潡鍩€椤掆偓缂嶅﹪骞冨Ο璇茬窞閻忕偠鍋愰崜銊╂⒑閸涘﹦绠撻悗姘卞厴瀹曠敻鎮㈤悡搴i獓闂佸啿鎼导鎺楀箣濠垫捁鈧寧銇勯幘璺盒e┑顖氥偢閺屻劌鈽夊Ο渚紑闂佸搫妫崜鐔煎蓟閵娿儮妲堟俊顖欒濞堫厽绻濋悽闈涗粶婵炲樊鍙冮獮鍐╃鐎n€晠鏌嶉崫鍕殭缂佹绻濋弻锝夋偐闁秵顎栭梺绋匡攻濞茬喖宕洪埀? availableWidth = 600;
|
||
availableHeight = 400;
|
||
}
|
||
|
||
// 闂備浇宕垫慨宕囨閵堝洦顫曢柡鍥ュ灪閸嬧晛鈹戦悩瀹犲閻庢艾顦甸弻宥堫檨闁告挻宀搁獮蹇涘川閺夋垹顦ㄩ梺鍛婄懃椤﹂亶銆呴銏♀拺闁告繂瀚瓭濠电偛鐪伴崐婵嗩嚕娴兼潙纾兼繝褎鍎虫禍? // 闂傚倷鑳堕崕鐢稿疾閳哄懎绐楁俊銈呮噺閸嬪鏌ㄥ┑鍡╂Ч闁哄拋鍓氶幈銊ヮ潨閸℃绠诲┑鈥崇湴閸旀垿骞冪捄琛℃婵☆垳绮幏鍗炩攽閳藉棗鐏犳い锕佷含閸?-8婵犵數鍋為崹鍫曞箹閳哄倻顩叉繝濠傚幘閻熼偊娼ㄩ柍褜鍓欓锝嗙鐎n亞鍊炴俊鐐差儏濞寸兘藝椤曗偓濮婃椽宕崟顓夈儲銇勯銏╂Ц闁伙絽鐏氶幏鍛姜閻楀牆濯伴梻濠庡亜濞诧箓骞愭ィ鍐炬晩閹兼番鍔嶉崐鐢电棯椤撶偞鍣烘い銉ヮ樀閹鎮烽幍顕嗙礊闂佺懓顨庨崑濠傜暦濮椻偓閸╋繝宕掑☉鍗炴櫔
|
||
const int minColumns = 4;
|
||
const int maxColumns = 8;
|
||
const double targetAspectRatio = 1.2; // 闂傚倷鐒﹂幃鍫曞磿閹惰棄纾婚柟鍓х帛閸嬪鏌ㄥ┑鍡樼闁稿鎹囬弻鍛槈濮樿京鍘梻浣虹帛缁诲秹宕伴弽顒夋毎?
|
||
var optimalColumnCount = Math.Clamp((int)Math.Floor(availableWidth / 120), minColumns, maxColumns);
|
||
|
||
var tileWidth = Math.Floor(availableWidth / optimalColumnCount) - 12; // 12px spacing
|
||
var tileHeight = Math.Min(tileWidth / targetAspectRatio, availableHeight / 4); // 闂傚倷鑳堕崢褔宕查弻銉ョ柈闁秆勵殕閸庡秵銇勯弽顐粶闁告瑥锕弻娑㈠箻濡炵偓顦风紒?闂?
|
||
// 缂傚倷鑳堕搹搴ㄥ矗鎼淬劌绐楅柡鍥╁У瀹曞弶鎱ㄥΟ鎸庣【閻庢艾顦甸弻宥堫檨闁告挻绋掔粋宥咁潰瀹€鈧悿鈧梺瑙勫劤閻°劑锝為崨瀛樼厽? tileWidth = Math.Max(tileWidth, 100);
|
||
tileHeight = Math.Max(tileHeight, 80);
|
||
|
||
// 闂傚倷绀侀幖顐⒚洪妶澶嬪仱闁靛ň鏅涢拑鐔封攽閸屻倖杈渁pPanel闂傚倷鐒﹂惇褰掑礉瀹€鍕惞婵帞妫渕闂備浇顕х换鎰崲閹版澘绠规い鎰跺瘜閺? LauncherRootTilePanel.Width = availableWidth;
|
||
|
||
// 闂傚倷绀侀幖顐⒚洪妶澶嬪仱闁靛ň鏅涢拑鐔封攽閻樺弶鎼愮紒鐘劦閺屽秷顧侀柛鎾跺枎椤曪綁宕归銏㈢獮婵犵數濮寸€氼參骞夐妶澶嬧拺缂佸娉曠粻浼存煕閻旂顥嬬紒顔肩墕閻f繈宕熼鈧崜顓㈡⒑閸涘﹥澶勯柛瀣噹鍗遍柍褜鍓熼弻?
|
||
foreach (var child in LauncherRootTilePanel.Children)
|
||
{
|
||
if (child is Button button)
|
||
{
|
||
button.Width = tileWidth;
|
||
button.Height = tileHeight;
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
private void ClampSurfaceIndex()
|
||
{
|
||
_currentDesktopSurfaceIndex = Math.Clamp(_currentDesktopSurfaceIndex, 0, LauncherSurfaceIndex);
|
||
}
|
||
|
||
private IBrush GetThemeBrush(string key)
|
||
{
|
||
if (Resources.TryGetResource(key, ActualThemeVariant, out var resource) && resource is IBrush brush)
|
||
{
|
||
return brush;
|
||
}
|
||
|
||
return Brushes.Transparent;
|
||
}
|
||
|
||
private void ApplyDesktopSurfaceOffset()
|
||
{
|
||
if (_desktopPagesHostTransform is null || _desktopSurfacePageWidth <= 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var targetOffset = -_currentDesktopSurfaceIndex * _desktopSurfacePageWidth;
|
||
_desktopPagesHostTransform.X = targetOffset;
|
||
|
||
if (_currentDesktopSurfaceIndex != LauncherSurfaceIndex)
|
||
{
|
||
CloseLauncherFolderOverlay();
|
||
ClearSelectedLauncherTile(refreshTaskbar: false);
|
||
}
|
||
|
||
UpdateDesktopPageAwareComponentContext();
|
||
}
|
||
|
||
private void SetDesktopPagesHostSnapAnimationEnabled(bool enabled)
|
||
{
|
||
if (_desktopPagesHostTransform is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (enabled)
|
||
{
|
||
if (!_desktopPagesHostTransitionsSuspended)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_desktopPagesHostTransform.Transitions = _desktopPagesHostSnapTransitions;
|
||
_desktopPagesHostTransitionsSuspended = false;
|
||
return;
|
||
}
|
||
|
||
if (_desktopPagesHostTransitionsSuspended)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_desktopPagesHostSnapTransitions ??= _desktopPagesHostTransform.Transitions;
|
||
_desktopPagesHostTransform.Transitions = null;
|
||
_desktopPagesHostTransitionsSuspended = true;
|
||
}
|
||
|
||
private void ClearDesktopPageContextSettle(bool refreshContext)
|
||
{
|
||
_desktopPageContextSettleRevision++;
|
||
_desktopPageContextSettlingSourceIndex = null;
|
||
_desktopPageContextSettlingTargetIndex = null;
|
||
|
||
if (refreshContext)
|
||
{
|
||
UpdateDesktopPageAwareComponentContext();
|
||
}
|
||
}
|
||
|
||
private void BeginDesktopPageContextSettle(int previousIndex, int targetIndex)
|
||
{
|
||
var sourceIndex = previousIndex >= 0 && previousIndex < _desktopPageCount
|
||
? previousIndex
|
||
: (int?)null;
|
||
var destinationIndex = targetIndex >= 0 && targetIndex < _desktopPageCount
|
||
? targetIndex
|
||
: (int?)null;
|
||
|
||
if (sourceIndex == destinationIndex && destinationIndex is not null)
|
||
{
|
||
ClearDesktopPageContextSettle(refreshContext: false);
|
||
return;
|
||
}
|
||
|
||
if (sourceIndex is null && destinationIndex is null)
|
||
{
|
||
ClearDesktopPageContextSettle(refreshContext: false);
|
||
return;
|
||
}
|
||
|
||
_desktopPageContextSettleRevision++;
|
||
var settleRevision = _desktopPageContextSettleRevision;
|
||
_desktopPageContextSettlingSourceIndex = sourceIndex;
|
||
_desktopPageContextSettlingTargetIndex = destinationIndex;
|
||
|
||
DispatcherTimer.RunOnce(
|
||
() =>
|
||
{
|
||
if (settleRevision != _desktopPageContextSettleRevision)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_desktopPageContextSettlingSourceIndex = null;
|
||
_desktopPageContextSettlingTargetIndex = null;
|
||
UpdateDesktopPageAwareComponentContext();
|
||
},
|
||
FluttermotionToken.Page + TimeSpan.FromMilliseconds(36));
|
||
}
|
||
|
||
private void MoveSurfaceBy(int delta)
|
||
{
|
||
if (delta == 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
MoveSurfaceTo(_currentDesktopSurfaceIndex + delta);
|
||
}
|
||
|
||
private void MoveSurfaceTo(int targetIndex)
|
||
{
|
||
var target = Math.Clamp(targetIndex, 0, LauncherSurfaceIndex);
|
||
if (target == _currentDesktopSurfaceIndex)
|
||
{
|
||
ApplyDesktopSurfaceOffset();
|
||
return;
|
||
}
|
||
|
||
var previousIndex = _currentDesktopSurfaceIndex;
|
||
_currentDesktopSurfaceIndex = target;
|
||
BeginDesktopPageContextSettle(previousIndex, target);
|
||
ApplyDesktopSurfaceOffset();
|
||
SchedulePersistSettings(delayMs: Math.Max(280, (int)FluttermotionToken.Page.TotalMilliseconds + 80));
|
||
}
|
||
|
||
private bool CanSwipeDesktopSurface()
|
||
{
|
||
return !_isSettingsOpen &&
|
||
!_isComponentLibraryOpen &&
|
||
!HasActiveDesktopEditSession &&
|
||
_desktopSurfacePageWidth > 1;
|
||
}
|
||
|
||
private void OnDesktopPagesPointerPressed(object? sender, PointerPressedEventArgs e)
|
||
{
|
||
if (!TryGetPointerPositionInDesktopViewport(e, out var pointerInViewport))
|
||
{
|
||
return;
|
||
}
|
||
|
||
// 婵犵數濮烽。浠嬪焵椤掆偓閸熷潡鍩€椤掆偓缂嶅﹪骞冨Ο璇茬窞闁归偊鍓氬畵宥夋⒑闂堟丹娑㈠川椤栨粌甯掓繝鐢靛仜椤曨厽鎱ㄧ€涙ɑ娅犻幖杈剧稻椤洘銇勮箛鎾村櫤缂傚秴娲弻鐔衡偓鐢告櫜鏉╃懓霉閿濆懎顥忛柛銈嗘礋閻擃偊宕惰閹癸綁鏌i悢鍛婂磳闁哄矉缍侀獮鍥敊閽樺鐣梻浣规偠閸娿倝宕板鍗炲灊婵鍩栭幆鐐烘偡濞嗗繐顏村ù鐘讳憾濮婃椽宕ㄦ繝鍕吂闂佸湱鈷堥崑濠囧箖閳ユ枼鏋庨柟鎯х摠濞呮牠鏌h箛鏇炰哗婵☆偄瀚濠囧箰鎼达絿顔曢梺鐟扮摠缁诲嫭鏅堕敃鍌涚厓鐟滄粓宕滃▎鎾嶅洭顢氶埀顒勫箠濞嗘挸绠i柨鏃囧Г濞呮牠姊洪崜鎻掍簴闁搞劌顭烽幆宀€鈧綆鈧垹缍婇幃鈺呭传閸曨厼甯块梻浣规偠閸斿﹪宕濋幋婵堟殾闁靛鏅╅弫宥嗘叏濮楀棗鍔俊銈呮噺閻撴洘绻涢崱妯哄缂佽泛寮剁换娑氣偓娑欙公閼拌法鈧鍠曠划娆忕暦閼告妲归幖杈剧秵濡?
|
||
if (_isComponentLibraryOpen &&
|
||
(_selectedDesktopComponentHost is not null || _selectedLauncherTileButton is not null))
|
||
{
|
||
if (!IsInteractivePointerSource(e.Source))
|
||
{
|
||
ClearDesktopComponentSelection();
|
||
ClearSelectedLauncherTile(refreshTaskbar: false);
|
||
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
|
||
}
|
||
}
|
||
|
||
if (!CanSwipeDesktopSurface())
|
||
{
|
||
return;
|
||
}
|
||
|
||
var appSnapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
|
||
var isThreeFingerSwipeEnabled = appSnapshot.EnableThreeFingerSwipe;
|
||
|
||
var currentPoint = e.GetCurrentPoint(DesktopPagesViewport);
|
||
var pointerId = e.Pointer?.Id ?? 0;
|
||
var isRightButtonPressed = currentPoint.Properties.IsRightButtonPressed;
|
||
var isLeftButtonPressed = currentPoint.Properties.IsLeftButtonPressed;
|
||
|
||
// 婵犵數濮伴崹鐓庘枖濞戞埃鍋撳鐓庢珝妤犵偛鍟换婵嬪礃椤忎焦鐏冨┑鐘灱濞夋盯顢栭崨瀛樺剨閻熸瑥瀚弧鈧繝鐢靛Т閸燁偊鎮橀妷銉㈡斀?闂傚倷绀侀幉锟犳偡閿旂晫绠惧┑鐘叉搐閺嬩焦銇勯幘鍗炵仼缂佺媭鍨堕弻鈥崇暤椤旂厧鏁俊銈勬缁诲棙銇勯弽銊d粶闁稿鎸搁悾鐑藉炊閳哄﹥鏁?
|
||
if (isThreeFingerSwipeEnabled)
|
||
{
|
||
if (isLeftButtonPressed || isRightButtonPressed)
|
||
{
|
||
_activePointerIds.Add(pointerId);
|
||
}
|
||
|
||
var isThreeFinger = _activePointerIds.Count >= 3;
|
||
var isRightDrag = isRightButtonPressed;
|
||
|
||
if (isThreeFinger || isRightDrag)
|
||
{
|
||
// 婵犵數鍋為崹鍫曞箰閹间絸鍥箥椤旂懓浜?闂傚倷绀侀幉锟犳偡閿旂晫绠惧┑鐘叉搐閺嬩焦銇勯幘鍗炵仼缂佺媭鍨堕弻鈥崇暤椤旂厧鏁俊銈勬缁诲棙銇勯弽銊d粶闁稿鎸搁悾鐑藉炊閳哄﹥鏁ら梻鍌欑劍鐎笛呯矙閹烘挾鈹嶆繛宸簼閸婂鏌ㄩ弮鍥撳ù婧垮€濋弻娑㈠Ψ閿濆懎顬堝銈忕稻閻擄繝寮婚敓鐘查唶婵犲灚鍔栨缂傚倷绶¢崰鏍矓閻㈢數鐭夐柟鐑橆殔鐎氬鏌涢…鎴濅簻闁衡偓椤撶喓绠鹃悗娑欘焽閻鎮介娑辨疁閽樼喖鏌涘☉娆愮稇闁藉啰鍠栭弻鏇熷緞濡櫣浠紓浣插亾濠㈣埖鍔栭悡鐔兼煃鏉炴媽鍏岄柟鐣屽█閹粙顢涘☉娆戠▏濡炪倖娲╃紞渚€宕洪埀顒併亜閹哄秶鍔嶉柛娆忕箻閹鏁愭惔鈥茬敖闂佽鐏氶崝鎴﹀蓟? ClearDesktopPageContextSettle(refreshContext: false);
|
||
_isThreeFingerOrRightDragSwipeActive = true;
|
||
_isDesktopSwipeActive = true;
|
||
_isDesktopSwipeDirectionLocked = false;
|
||
_desktopSwipeStartPoint = pointerInViewport;
|
||
_desktopSwipeCurrentPoint = _desktopSwipeStartPoint;
|
||
_desktopSwipeLastPoint = _desktopSwipeStartPoint;
|
||
_desktopSwipeVelocityX = 0;
|
||
_desktopSwipeLastTimestamp = Stopwatch.GetTimestamp();
|
||
_desktopSwipeBaseOffset = -_currentDesktopSurfaceIndex * _desktopSurfacePageWidth;
|
||
|
||
// 闂傚倷绀侀幖顐ょ矓閺夋嚚娲煛閸滀焦鏅╅梺鎼炲劘閸斿酣銆呴弻銉﹀€甸柨婵嗗€瑰▍鍡樸亜閹邦喗娅曢柍褜鍓涢幊鎾诲箟闄囬妵鎰板礃椤斻垹娲崺锟犲川椤旈棿鍝楅梻浣虹《濡插懘宕㈤崜褏鐭嗗鑸靛姈閳锋帡鏌涢幇鈺佸缂佺嫏鍕╀簻闁圭儤鎸鹃妴鎺旂磼鏉堛劌娴€规洜鍠栭、鏃堝椽娴i晲缂撻梻鍌欑閹诧紕鎹㈤崒婊呯煋閻庡灚鐡曟慨? e.Handled = true;
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 闂傚倷绀侀幉锟犫€﹂崶顒€绐楅柟閭﹀墾閼板灝銆掑锝呬壕閻庤娲╃换婵嗩嚕閹绢喗鍋勫瀣閳诲本绻濋悽闈浶㈤柨鏇樺劦瀹曞綊宕归锝呭伎闂佸啿鎼幊蹇涙倿婵犳碍鐓涢柛鏇ㄥ亞缁犳娊鎮? if (IsInteractivePointerSource(e.Source))
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (IsDesktopSwipeBlockedPointerSource(e.Source))
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (!isLeftButtonPressed)
|
||
{
|
||
return;
|
||
}
|
||
|
||
ClearDesktopPageContextSettle(refreshContext: false);
|
||
_isDesktopSwipeActive = true;
|
||
_isDesktopSwipeDirectionLocked = false;
|
||
_desktopSwipeStartPoint = pointerInViewport;
|
||
_desktopSwipeCurrentPoint = _desktopSwipeStartPoint;
|
||
_desktopSwipeLastPoint = _desktopSwipeStartPoint;
|
||
_desktopSwipeVelocityX = 0;
|
||
_desktopSwipeLastTimestamp = Stopwatch.GetTimestamp();
|
||
_desktopSwipeBaseOffset = -_currentDesktopSurfaceIndex * _desktopSurfacePageWidth;
|
||
}
|
||
|
||
private static bool IsInteractivePointerSource(object? source)
|
||
{
|
||
if (source is not Visual visual)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
foreach (var node in visual.GetSelfAndVisualAncestors())
|
||
{
|
||
if (node is Control control)
|
||
{
|
||
if (control.Classes.Contains("desktop-component") ||
|
||
control.Classes.Contains("desktop-component-host"))
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
|
||
if (node is Button button && IsLauncherTileButton(button))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (node is TextBox or ComboBox or ListBoxItem or Slider or ToggleSwitch)
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
private static bool IsLauncherTileButton(Button? button)
|
||
{
|
||
if (button is null)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
foreach (var node in button.GetSelfAndVisualAncestors())
|
||
{
|
||
if (node is WrapPanel panel && panel.Name == "LauncherRootTilePanel")
|
||
{
|
||
return true;
|
||
}
|
||
|
||
if (node is Grid grid && grid.Name == "LauncherFolderGridPanel")
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
private static bool IsDesktopSwipeBlockedPointerSource(object? source)
|
||
{
|
||
if (source is not Visual visual)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var pendingNodes = new Stack<object>();
|
||
var visitedNodes = new HashSet<object>(ReferenceEqualityComparer.Instance);
|
||
pendingNodes.Push(visual);
|
||
|
||
while (pendingNodes.Count > 0)
|
||
{
|
||
var node = pendingNodes.Pop();
|
||
if (!visitedNodes.Add(node))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (IsDesktopSwipeBlockingNode(node))
|
||
{
|
||
return true;
|
||
}
|
||
|
||
if (node is StyledElement styledElement &&
|
||
styledElement.TemplatedParent is { } templatedParent)
|
||
{
|
||
pendingNodes.Push(templatedParent);
|
||
}
|
||
|
||
if (node is Visual currentVisual &&
|
||
currentVisual.GetVisualParent() is { } parentVisual)
|
||
{
|
||
pendingNodes.Push(parentVisual);
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
private static bool IsDesktopSwipeBlockingNode(object node)
|
||
{
|
||
if (node is ScrollViewer scrollViewer && IsLauncherScrollViewer(scrollViewer))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (node is Button button && IsLauncherTileButton(button))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (node is TextBox or ComboBox or Slider or ToggleSwitch or ListBoxItem)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
if (node is Control control &&
|
||
(control.Classes.Contains("study-history-action-button") ||
|
||
control.Classes.Contains("desktop-component") ||
|
||
control.Classes.Contains("desktop-component-host")))
|
||
{
|
||
return true;
|
||
}
|
||
|
||
var typeName = node.GetType().Name;
|
||
return typeName.Contains("WebView", StringComparison.OrdinalIgnoreCase) ||
|
||
typeName.Contains("ScrollBar", StringComparison.OrdinalIgnoreCase) ||
|
||
typeName.Contains("NumericUpDown", StringComparison.OrdinalIgnoreCase) ||
|
||
typeName.Contains("TextPresenter", StringComparison.OrdinalIgnoreCase);
|
||
}
|
||
|
||
private static bool IsLauncherScrollViewer(ScrollViewer? scrollViewer)
|
||
{
|
||
if (scrollViewer is null)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
return scrollViewer.Name == "LauncherRootScrollViewer";
|
||
}
|
||
|
||
private bool TryGetPointerPositionInDesktopViewport(PointerEventArgs e, out Point point)
|
||
{
|
||
point = default;
|
||
if (DesktopPagesViewport is null)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
point = e.GetPosition(DesktopPagesViewport);
|
||
if (_isDesktopSwipeActive && _isDesktopSwipeDirectionLocked)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
var bounds = DesktopPagesViewport.Bounds;
|
||
return bounds.Width > 1 &&
|
||
bounds.Height > 1 &&
|
||
point.X >= 0 &&
|
||
point.Y >= 0 &&
|
||
point.X <= bounds.Width &&
|
||
point.Y <= bounds.Height;
|
||
}
|
||
|
||
private void UpdateDesktopSwipeVelocity(Point pointer)
|
||
{
|
||
var now = Stopwatch.GetTimestamp();
|
||
if (_desktopSwipeLastTimestamp > 0)
|
||
{
|
||
var elapsedSeconds = (now - _desktopSwipeLastTimestamp) / (double)Stopwatch.Frequency;
|
||
if (elapsedSeconds > 0.0001)
|
||
{
|
||
var instantVelocity = (pointer.X - _desktopSwipeLastPoint.X) / elapsedSeconds;
|
||
_desktopSwipeVelocityX = _desktopSwipeVelocityX * 0.7 + instantVelocity * 0.3;
|
||
}
|
||
}
|
||
|
||
_desktopSwipeLastPoint = pointer;
|
||
_desktopSwipeLastTimestamp = now;
|
||
}
|
||
|
||
private void OnDesktopPagesPointerMoved(object? sender, PointerEventArgs e)
|
||
{
|
||
if (!_isDesktopSwipeActive || !TryGetPointerPositionInDesktopViewport(e, out var pointerInViewport))
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (_desktopPagesHostTransform is null || DesktopPagesViewport is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_desktopSwipeCurrentPoint = pointerInViewport;
|
||
UpdateDesktopSwipeVelocity(pointerInViewport);
|
||
var deltaX = _desktopSwipeCurrentPoint.X - _desktopSwipeStartPoint.X;
|
||
var deltaY = _desktopSwipeCurrentPoint.Y - _desktopSwipeStartPoint.Y;
|
||
|
||
if (!_isDesktopSwipeDirectionLocked)
|
||
{
|
||
const double activationThreshold = 14;
|
||
const double horizontalBias = 1.15;
|
||
var absDeltaX = Math.Abs(deltaX);
|
||
var absDeltaY = Math.Abs(deltaY);
|
||
|
||
if (absDeltaY >= activationThreshold && absDeltaY > absDeltaX * horizontalBias)
|
||
{
|
||
CancelDesktopSwipeInteraction(e.Pointer);
|
||
return;
|
||
}
|
||
|
||
if (absDeltaX < activationThreshold || absDeltaX <= absDeltaY * horizontalBias)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_isDesktopSwipeDirectionLocked = true;
|
||
SetDesktopPagesHostSnapAnimationEnabled(enabled: false);
|
||
if (e.Pointer.Captured != DesktopPagesViewport)
|
||
{
|
||
e.Pointer.Capture(DesktopPagesViewport);
|
||
}
|
||
}
|
||
|
||
var minOffset = -LauncherSurfaceIndex * _desktopSurfacePageWidth;
|
||
var tentative = _desktopSwipeBaseOffset + deltaX;
|
||
if (tentative > 0)
|
||
{
|
||
tentative *= 0.24;
|
||
}
|
||
else if (tentative < minOffset)
|
||
{
|
||
tentative = minOffset + (tentative - minOffset) * 0.24;
|
||
}
|
||
|
||
_desktopPagesHostTransform.X = tentative;
|
||
UpdateDesktopPageAwareComponentContext();
|
||
e.Handled = true;
|
||
}
|
||
|
||
private void OnDesktopPagesPointerReleased(object? sender, PointerReleasedEventArgs e)
|
||
{
|
||
var pointerId = e.Pointer?.Id ?? 0;
|
||
_activePointerIds.Remove(pointerId);
|
||
|
||
if (EndDesktopSwipeInteraction(e.Pointer))
|
||
{
|
||
e.Handled = true;
|
||
}
|
||
}
|
||
|
||
private void OnDesktopPagesPointerCaptureLost(object? sender, PointerCaptureLostEventArgs e)
|
||
{
|
||
var pointerId = e.Pointer?.Id ?? 0;
|
||
_activePointerIds.Remove(pointerId);
|
||
|
||
EndDesktopSwipeInteraction(e.Pointer);
|
||
}
|
||
|
||
private void CancelDesktopSwipeInteraction(IPointer? pointer)
|
||
{
|
||
if (!_isDesktopSwipeActive)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var wasDirectionLocked = _isDesktopSwipeDirectionLocked;
|
||
if (pointer?.Captured == DesktopPagesViewport)
|
||
{
|
||
pointer.Capture(null);
|
||
}
|
||
|
||
_isDesktopSwipeActive = false;
|
||
_isDesktopSwipeDirectionLocked = false;
|
||
_isThreeFingerOrRightDragSwipeActive = false;
|
||
_activePointerIds.Clear();
|
||
_desktopSwipeVelocityX = 0;
|
||
_desktopSwipeLastTimestamp = 0;
|
||
if (wasDirectionLocked)
|
||
{
|
||
SetDesktopPagesHostSnapAnimationEnabled(enabled: true);
|
||
ApplyDesktopSurfaceOffset();
|
||
}
|
||
}
|
||
|
||
private bool EndDesktopSwipeInteraction(IPointer? pointer)
|
||
{
|
||
if (!_isDesktopSwipeActive)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var wasDirectionLocked = _isDesktopSwipeDirectionLocked;
|
||
var wasThreeFingerOrRightDrag = _isThreeFingerOrRightDragSwipeActive;
|
||
_isDesktopSwipeActive = false;
|
||
_isDesktopSwipeDirectionLocked = false;
|
||
_isThreeFingerOrRightDragSwipeActive = false;
|
||
_activePointerIds.Clear();
|
||
|
||
if (pointer?.Captured == DesktopPagesViewport)
|
||
{
|
||
pointer.Capture(null);
|
||
}
|
||
|
||
_desktopSwipeLastTimestamp = 0;
|
||
if (!wasDirectionLocked)
|
||
{
|
||
_desktopSwipeVelocityX = 0;
|
||
return false;
|
||
}
|
||
|
||
SetDesktopPagesHostSnapAnimationEnabled(enabled: true);
|
||
|
||
var deltaX = _desktopSwipeCurrentPoint.X - _desktopSwipeStartPoint.X;
|
||
var deltaY = _desktopSwipeCurrentPoint.Y - _desktopSwipeStartPoint.Y;
|
||
var absDeltaX = Math.Abs(deltaX);
|
||
var absDeltaY = Math.Abs(deltaY);
|
||
var distanceThreshold = Math.Max(48, _desktopSurfacePageWidth * 0.14);
|
||
var velocityThreshold = Math.Max(860, _desktopSurfacePageWidth * 1.08);
|
||
var predictedDeltaX = deltaX + _desktopSwipeVelocityX * 0.18;
|
||
var predictedOffset = _desktopSwipeBaseOffset + predictedDeltaX;
|
||
var projectedTargetIndex = (int)Math.Round(-predictedOffset / _desktopSurfacePageWidth);
|
||
projectedTargetIndex = Math.Clamp(projectedTargetIndex, 0, LauncherSurfaceIndex);
|
||
|
||
var hasDistanceIntent = absDeltaX >= distanceThreshold && absDeltaX > absDeltaY * 1.05;
|
||
var hasVelocityIntent = Math.Abs(_desktopSwipeVelocityX) >= velocityThreshold;
|
||
|
||
// 濠电姷顣藉Σ鍛村磻閳ь剟鏌涚€n偅宕岄柡宀嬬磿娴狅妇鎷犻幓鎺懶ョ紓鍌欐祰娴滎剚鏅跺Δ鍐煓濠㈣泛顑呯欢鐐烘倵閿濆簼绨芥俊?闂傚倷绀侀幉锟犳偡閿旂晫绠惧┑鐘叉搐閺嬩焦銇勯幘鍗炵仼缂佺媭鍨堕弻鈥崇暤椤旂厧鏁?&& 闂傚倷绶氬鑽ゆ嫻閻旂厧绀夐幖鎼厛閺佸嫰鏌涢妷锝呭闁崇粯妫冮弻宥堫檨闁告挻宀告俊?&& 闂傚倷绀侀幉锛勫枈瀹ュ鍨傚ù锝呭暔娴滃湱绱掔€n偒鍎ラ柣鎾卞劦閺岀喓鈧稒顭囩粻鎾舵偖?
|
||
if (wasThreeFingerOrRightDrag &&
|
||
_currentDesktopSurfaceIndex == 0 &&
|
||
deltaX > 0 && // 闂傚倷绀侀幉锛勫枈瀹ュ鍨傚ù锝呭暔娴滃湱绱掔€n偒鍎ラ柣鎾卞劦閺岀喓鈧稒顭囩粻鎾舵偖?
|
||
(hasDistanceIntent || hasVelocityIntent))
|
||
{
|
||
if (Application.Current is App app)
|
||
{
|
||
app.HideMainWindowToTray(this, "ThreeFingerOrRightDragSwipe");
|
||
}
|
||
|
||
ApplyDesktopSurfaceOffset();
|
||
_desktopSwipeVelocityX = 0;
|
||
return true;
|
||
}
|
||
|
||
if (projectedTargetIndex == _currentDesktopSurfaceIndex && (hasDistanceIntent || hasVelocityIntent))
|
||
{
|
||
projectedTargetIndex = Math.Clamp(
|
||
_currentDesktopSurfaceIndex + (deltaX < 0 ? 1 : -1),
|
||
0,
|
||
LauncherSurfaceIndex);
|
||
}
|
||
|
||
_desktopSwipeVelocityX = 0;
|
||
|
||
if (projectedTargetIndex != _currentDesktopSurfaceIndex)
|
||
{
|
||
MoveSurfaceTo(projectedTargetIndex);
|
||
return true;
|
||
}
|
||
|
||
ApplyDesktopSurfaceOffset();
|
||
return hasDistanceIntent || hasVelocityIntent;
|
||
}
|
||
|
||
private void OnDesktopPagesPointerWheelChanged(object? sender, PointerWheelEventArgs e)
|
||
{
|
||
if (!CanSwipeDesktopSurface())
|
||
{
|
||
return;
|
||
}
|
||
|
||
var prefersHorizontal = Math.Abs(e.Delta.X) > Math.Abs(e.Delta.Y) ||
|
||
e.KeyModifiers.HasFlag(KeyModifiers.Shift);
|
||
if (!prefersHorizontal)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var delta = e.Delta.X != 0 ? e.Delta.X : e.Delta.Y;
|
||
if (Math.Abs(delta) < double.Epsilon)
|
||
{
|
||
return;
|
||
}
|
||
|
||
MoveSurfaceBy(delta < 0 ? 1 : -1);
|
||
e.Handled = true;
|
||
}
|
||
|
||
private void RenderLauncherRootTiles()
|
||
{
|
||
if (LauncherRootTilePanel is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
ClearSelectedLauncherTile(refreshTaskbar: false);
|
||
LauncherRootTilePanel.Children.Clear();
|
||
var folders = _startMenuRoot.Folders;
|
||
var apps = _startMenuRoot.Apps;
|
||
|
||
foreach (var folder in folders)
|
||
{
|
||
if (!IsLauncherFolderVisible(folder))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
LauncherRootTilePanel.Children.Add(CreateLauncherFolderTile(folder));
|
||
}
|
||
|
||
foreach (var app in apps)
|
||
{
|
||
if (!IsLauncherAppVisible(app))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
LauncherRootTilePanel.Children.Add(CreateLauncherAppTile(app));
|
||
}
|
||
|
||
if (LauncherRootTilePanel.Children.Count == 0)
|
||
{
|
||
LauncherRootTilePanel.Children.Add(CreateLauncherHintTile(
|
||
GetLauncherEmptyText(),
|
||
string.Empty));
|
||
}
|
||
|
||
// 闂傚倷绶氬鑽ゆ嫻閻旂厧绀夐悘鐐电叓閻熼偊娼ㄩ柍褜鍓欓锝嗙鐎n亞鍊為梺闈涱煬閻撳牆煤椤掑嫭鈷戦柛婵嗗濠€浼存煙閸涘﹥鍊愰柟顕€绠栭、妤呭礋椤愩値鍚呴梻浣哥秺閸嬪﹪宕滃璺虹9闁汇垹鎲¢悡銉︾箾閹寸儐鐒鹃悗姘缁辨帡濡搁敂鎯у绩闂佽鍠曠划娆愪繆閹间礁唯鐟滄粍瀵煎畝鍕厽闊洦娲栨禍褰掓煕鐎n偅宕岄柟顔款潐缁楃喐绻濋崟顓ㄧ吹闂? Dispatcher.UIThread.Post(() => UpdateLauncherTileLayout(), DispatcherPriority.Background);
|
||
}
|
||
|
||
private Button CreateLauncherFolderTile(StartMenuFolderNode folder)
|
||
{
|
||
var title = folder.Name;
|
||
var subtitle = Lf("launcher.folder_items_format", "{0} apps", folder.TotalAppCount);
|
||
var folderIconBitmap = GetLauncherFolderIconBitmap();
|
||
var folderKey = NormalizeLauncherHiddenKey(folder.RelativePath);
|
||
return CreateLauncherTileButton(
|
||
title,
|
||
subtitle,
|
||
monogram: "DIR",
|
||
iconBitmap: folderIconBitmap,
|
||
() => OpenLauncherFolder(folder),
|
||
LauncherEntryKind.Folder,
|
||
folderKey);
|
||
}
|
||
|
||
private Button CreateLauncherAppTile(StartMenuAppEntry app)
|
||
{
|
||
var iconBitmap = GetLauncherIconBitmap(app);
|
||
var monogram = BuildMonogram(app.DisplayName);
|
||
var appKey = NormalizeLauncherHiddenKey(app.RelativePath);
|
||
return CreateLauncherTileButton(
|
||
app.DisplayName,
|
||
subtitle: string.Empty,
|
||
monogram,
|
||
iconBitmap,
|
||
() => LaunchStartMenuEntry(app),
|
||
LauncherEntryKind.Shortcut,
|
||
appKey);
|
||
}
|
||
|
||
private Control CreateLauncherHintTile(string title, string subtitle)
|
||
{
|
||
var panel = new StackPanel
|
||
{
|
||
Spacing = 6,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
HorizontalAlignment = HorizontalAlignment.Center
|
||
};
|
||
panel.Children.Add(new TextBlock
|
||
{
|
||
Text = title,
|
||
FontWeight = FontWeight.SemiBold,
|
||
HorizontalAlignment = HorizontalAlignment.Center
|
||
});
|
||
if (!string.IsNullOrWhiteSpace(subtitle))
|
||
{
|
||
panel.Children.Add(new TextBlock
|
||
{
|
||
Text = subtitle,
|
||
Opacity = 0.75,
|
||
HorizontalAlignment = HorizontalAlignment.Center
|
||
});
|
||
}
|
||
|
||
return new Border
|
||
{
|
||
Classes = { "glass-panel" },
|
||
BorderThickness = new Thickness(0),
|
||
Margin = new Thickness(0, 0, 12, 12),
|
||
CornerRadius = new CornerRadius(20),
|
||
Child = panel,
|
||
};
|
||
}
|
||
|
||
private Button CreateLauncherTileButton(
|
||
string title,
|
||
string subtitle,
|
||
string monogram,
|
||
Bitmap? iconBitmap,
|
||
Action clickAction,
|
||
LauncherEntryKind entryKind,
|
||
string entryKey)
|
||
{
|
||
Control iconControl = iconBitmap is not null
|
||
? new Image
|
||
{
|
||
Source = iconBitmap,
|
||
Width = 40,
|
||
Height = 40,
|
||
Stretch = Stretch.Uniform
|
||
}
|
||
: new Border
|
||
{
|
||
Width = 40,
|
||
Height = 40,
|
||
CornerRadius = new CornerRadius(999),
|
||
Background = GetThemeBrush("AdaptiveButtonBackgroundBrush"),
|
||
HorizontalAlignment = HorizontalAlignment.Center,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
BorderThickness = new Thickness(0),
|
||
Child = new TextBlock
|
||
{
|
||
Text = monogram,
|
||
FontWeight = FontWeight.Bold,
|
||
HorizontalAlignment = HorizontalAlignment.Center,
|
||
VerticalAlignment = VerticalAlignment.Center
|
||
}
|
||
};
|
||
|
||
var textPanel = new StackPanel
|
||
{
|
||
Spacing = 3,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
HorizontalAlignment = HorizontalAlignment.Stretch
|
||
};
|
||
textPanel.Children.Add(new TextBlock
|
||
{
|
||
Text = title,
|
||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||
MaxLines = 2,
|
||
TextAlignment = TextAlignment.Center,
|
||
HorizontalAlignment = HorizontalAlignment.Stretch
|
||
});
|
||
|
||
if (!string.IsNullOrWhiteSpace(subtitle))
|
||
{
|
||
textPanel.Children.Add(new TextBlock
|
||
{
|
||
Text = subtitle,
|
||
Opacity = 0.72,
|
||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||
TextAlignment = TextAlignment.Center,
|
||
HorizontalAlignment = HorizontalAlignment.Stretch
|
||
});
|
||
}
|
||
|
||
var content = new StackPanel
|
||
{
|
||
Spacing = 8,
|
||
VerticalAlignment = VerticalAlignment.Center
|
||
};
|
||
content.Children.Add(iconControl);
|
||
content.Children.Add(textPanel);
|
||
|
||
var button = new Button
|
||
{
|
||
Margin = new Thickness(0, 0, 12, 12),
|
||
BorderThickness = new Thickness(0),
|
||
BorderBrush = Brushes.Transparent,
|
||
CornerRadius = new CornerRadius(20),
|
||
Padding = new Thickness(10),
|
||
Content = content,
|
||
};
|
||
if (_showLauncherTileBackground)
|
||
{
|
||
button.Classes.Add("glass-panel");
|
||
}
|
||
else
|
||
{
|
||
button.Background = Brushes.Transparent;
|
||
}
|
||
button.Click += (_, _) =>
|
||
{
|
||
if (_isComponentLibraryOpen)
|
||
{
|
||
if (!string.IsNullOrWhiteSpace(entryKey))
|
||
{
|
||
SetSelectedLauncherTile(button, entryKind, entryKey);
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
clickAction();
|
||
};
|
||
return button;
|
||
}
|
||
|
||
private static string NormalizeLauncherHiddenKey(string? key)
|
||
{
|
||
return string.IsNullOrWhiteSpace(key) ? string.Empty : key.Trim();
|
||
}
|
||
|
||
private bool IsLauncherFolderVisible(StartMenuFolderNode folder)
|
||
{
|
||
var key = NormalizeLauncherHiddenKey(folder.RelativePath);
|
||
return string.IsNullOrWhiteSpace(key) || !_hiddenLauncherFolderPaths.Contains(key);
|
||
}
|
||
|
||
private bool IsLauncherAppVisible(StartMenuAppEntry app)
|
||
{
|
||
var key = NormalizeLauncherHiddenKey(app.RelativePath);
|
||
return string.IsNullOrWhiteSpace(key) || !_hiddenLauncherAppPaths.Contains(key);
|
||
}
|
||
|
||
private bool IsLauncherTileSelected()
|
||
{
|
||
return _selectedLauncherEntryKind.HasValue && !string.IsNullOrWhiteSpace(_selectedLauncherEntryKey);
|
||
}
|
||
|
||
private void SetSelectedLauncherTile(Button button, LauncherEntryKind entryKind, string entryKey)
|
||
{
|
||
if (!_isComponentLibraryOpen || string.IsNullOrWhiteSpace(entryKey))
|
||
{
|
||
return;
|
||
}
|
||
|
||
var normalizedKey = NormalizeLauncherHiddenKey(entryKey);
|
||
if (string.IsNullOrWhiteSpace(normalizedKey))
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (_selectedDesktopComponentHost is not null)
|
||
{
|
||
ClearDesktopComponentSelection();
|
||
}
|
||
|
||
if (_selectedLauncherTileButton is not null && _selectedLauncherTileButton != button)
|
||
{
|
||
ApplyLauncherTileSelectionVisual(_selectedLauncherTileButton, isSelected: false);
|
||
}
|
||
|
||
_selectedLauncherTileButton = button;
|
||
_selectedLauncherEntryKind = entryKind;
|
||
_selectedLauncherEntryKey = normalizedKey;
|
||
ApplyLauncherTileSelectionVisual(button, isSelected: true);
|
||
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
|
||
}
|
||
|
||
private void ClearSelectedLauncherTile(bool refreshTaskbar)
|
||
{
|
||
if (_selectedLauncherTileButton is not null)
|
||
{
|
||
ApplyLauncherTileSelectionVisual(_selectedLauncherTileButton, isSelected: false);
|
||
}
|
||
|
||
_selectedLauncherTileButton = null;
|
||
_selectedLauncherEntryKind = null;
|
||
_selectedLauncherEntryKey = null;
|
||
|
||
if (refreshTaskbar)
|
||
{
|
||
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
|
||
}
|
||
}
|
||
|
||
private void ApplyLauncherTileSelectionVisual(Button button, bool isSelected)
|
||
{
|
||
var showSelection = isSelected && _isComponentLibraryOpen;
|
||
button.BorderThickness = showSelection
|
||
? new Thickness(Math.Clamp(_currentDesktopCellSize * 0.04, 1, 3))
|
||
: new Thickness(0);
|
||
button.BorderBrush = showSelection ? GetThemeBrush("AdaptiveAccentBrush") : Brushes.Transparent;
|
||
}
|
||
|
||
private void HideSelectedLauncherEntry()
|
||
{
|
||
if (!_isComponentLibraryOpen ||
|
||
_currentDesktopSurfaceIndex != LauncherSurfaceIndex ||
|
||
_selectedLauncherEntryKind is null ||
|
||
string.IsNullOrWhiteSpace(_selectedLauncherEntryKey))
|
||
{
|
||
return;
|
||
}
|
||
|
||
var entryKind = _selectedLauncherEntryKind.Value;
|
||
var entryKey = _selectedLauncherEntryKey!;
|
||
ClearSelectedLauncherTile(refreshTaskbar: false);
|
||
|
||
var changed = entryKind switch
|
||
{
|
||
LauncherEntryKind.Folder => _hiddenLauncherFolderPaths.Add(entryKey),
|
||
LauncherEntryKind.Shortcut => _hiddenLauncherAppPaths.Add(entryKey),
|
||
_ => false
|
||
};
|
||
|
||
if (changed)
|
||
{
|
||
ApplyLauncherVisibilitySettingsChange();
|
||
return;
|
||
}
|
||
|
||
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
|
||
}
|
||
|
||
private void ApplyLauncherVisibilitySettingsChange()
|
||
{
|
||
ClearSelectedLauncherTile(refreshTaskbar: false);
|
||
RenderLauncherRootTiles();
|
||
if (_launcherFolderStack.Count > 0)
|
||
{
|
||
RenderLauncherFolderFromStack();
|
||
}
|
||
|
||
RenderLauncherHiddenItemsList();
|
||
PersistSettings();
|
||
}
|
||
|
||
private void RenderLauncherHiddenItemsList()
|
||
{
|
||
if (LauncherHiddenItemsSettingsExpander is null || LauncherHiddenItemsEmptyTextBlock is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
LauncherHiddenItemsSettingsExpander.Items.Clear();
|
||
var hiddenItems = BuildLauncherHiddenItems();
|
||
LauncherHiddenItemsEmptyTextBlock.IsVisible = hiddenItems.Count == 0;
|
||
if (hiddenItems.Count == 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
foreach (var hiddenItem in hiddenItems)
|
||
{
|
||
LauncherHiddenItemsSettingsExpander.Items.Add(CreateLauncherHiddenItemRow(hiddenItem));
|
||
}
|
||
}
|
||
|
||
private IReadOnlyList<LauncherHiddenItemView> BuildLauncherHiddenItems()
|
||
{
|
||
var items = new List<LauncherHiddenItemView>();
|
||
var seenFolders = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||
var seenApps = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||
|
||
CollectHiddenLauncherItems(_startMenuRoot, items, seenFolders, seenApps);
|
||
|
||
foreach (var key in _hiddenLauncherFolderPaths.OrderBy(path => path, StringComparer.OrdinalIgnoreCase))
|
||
{
|
||
if (!seenFolders.Contains(key))
|
||
{
|
||
items.Add(new LauncherHiddenItemView(
|
||
LauncherEntryKind.Folder,
|
||
key,
|
||
BuildLauncherHiddenFallbackDisplayName(key),
|
||
"DIR",
|
||
GetLauncherFolderIconBitmap()));
|
||
}
|
||
}
|
||
|
||
foreach (var key in _hiddenLauncherAppPaths.OrderBy(path => path, StringComparer.OrdinalIgnoreCase))
|
||
{
|
||
if (!seenApps.Contains(key))
|
||
{
|
||
var fallbackName = BuildLauncherHiddenFallbackDisplayName(key);
|
||
items.Add(new LauncherHiddenItemView(
|
||
LauncherEntryKind.Shortcut,
|
||
key,
|
||
fallbackName,
|
||
BuildMonogram(fallbackName),
|
||
IconBitmap: null));
|
||
}
|
||
}
|
||
|
||
return items
|
||
.OrderBy(item => item.DisplayName, StringComparer.CurrentCultureIgnoreCase)
|
||
.ThenBy(item => item.Key, StringComparer.OrdinalIgnoreCase)
|
||
.ToList();
|
||
}
|
||
|
||
private void CollectHiddenLauncherItems(
|
||
StartMenuFolderNode folder,
|
||
List<LauncherHiddenItemView> items,
|
||
HashSet<string> seenFolders,
|
||
HashSet<string> seenApps)
|
||
{
|
||
foreach (var subFolder in folder.Folders)
|
||
{
|
||
var folderKey = NormalizeLauncherHiddenKey(subFolder.RelativePath);
|
||
if (!string.IsNullOrWhiteSpace(folderKey) &&
|
||
_hiddenLauncherFolderPaths.Contains(folderKey) &&
|
||
seenFolders.Add(folderKey))
|
||
{
|
||
items.Add(new LauncherHiddenItemView(
|
||
LauncherEntryKind.Folder,
|
||
folderKey,
|
||
subFolder.Name,
|
||
"DIR",
|
||
GetLauncherFolderIconBitmap()));
|
||
}
|
||
|
||
CollectHiddenLauncherItems(subFolder, items, seenFolders, seenApps);
|
||
}
|
||
|
||
foreach (var app in folder.Apps)
|
||
{
|
||
var appKey = NormalizeLauncherHiddenKey(app.RelativePath);
|
||
if (string.IsNullOrWhiteSpace(appKey) ||
|
||
!_hiddenLauncherAppPaths.Contains(appKey) ||
|
||
!seenApps.Add(appKey))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
items.Add(new LauncherHiddenItemView(
|
||
LauncherEntryKind.Shortcut,
|
||
appKey,
|
||
app.DisplayName,
|
||
BuildMonogram(app.DisplayName),
|
||
GetLauncherIconBitmap(app)));
|
||
}
|
||
}
|
||
|
||
private static string BuildLauncherHiddenFallbackDisplayName(string key)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(key))
|
||
{
|
||
return "Unknown";
|
||
}
|
||
|
||
var normalized = key.Replace('\\', '/');
|
||
var fileName = Path.GetFileNameWithoutExtension(normalized);
|
||
return string.IsNullOrWhiteSpace(fileName)
|
||
? key
|
||
: fileName;
|
||
}
|
||
|
||
private FASettingsExpanderItem CreateLauncherHiddenItemRow(LauncherHiddenItemView hiddenItem)
|
||
{
|
||
var typeText = hiddenItem.Kind == LauncherEntryKind.Folder
|
||
? L("settings.launcher.hidden_type_folder", "Folder")
|
||
: L("settings.launcher.hidden_type_shortcut", "Shortcut");
|
||
|
||
var restoreButton = new Button
|
||
{
|
||
Width = 36,
|
||
Height = 36,
|
||
Padding = new Thickness(0),
|
||
Background = Brushes.Transparent,
|
||
BorderThickness = new Thickness(0),
|
||
Tag = new LauncherHiddenItemToken(hiddenItem.Kind, hiddenItem.Key)
|
||
};
|
||
restoreButton.Content = new FluentIcons.Avalonia.SymbolIcon
|
||
{
|
||
Symbol = FluentIcons.Common.Symbol.Eye,
|
||
IconVariant = FluentIcons.Common.IconVariant.Regular,
|
||
FontSize = 18,
|
||
HorizontalAlignment = HorizontalAlignment.Center,
|
||
VerticalAlignment = VerticalAlignment.Center
|
||
};
|
||
ToolTip.SetTip(restoreButton, L("settings.launcher.restore_button", "Unhide"));
|
||
restoreButton.Click += OnRestoreLauncherHiddenItemClick;
|
||
|
||
return new FASettingsExpanderItem
|
||
{
|
||
Content = hiddenItem.DisplayName,
|
||
Description = typeText,
|
||
IconSource = CreateLauncherHiddenItemIconSource(hiddenItem),
|
||
IsClickEnabled = false,
|
||
Footer = restoreButton
|
||
};
|
||
}
|
||
|
||
private FAIconSource? CreateLauncherHiddenItemIconSource(LauncherHiddenItemView hiddenItem)
|
||
{
|
||
if (hiddenItem.IconBitmap is not null)
|
||
{
|
||
return new FAImageIconSource
|
||
{
|
||
Source = hiddenItem.IconBitmap
|
||
};
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private void OnRestoreLauncherHiddenItemClick(object? sender, RoutedEventArgs e)
|
||
{
|
||
if (sender is not Button { Tag: LauncherHiddenItemToken token })
|
||
{
|
||
return;
|
||
}
|
||
|
||
var removed = token.Kind switch
|
||
{
|
||
LauncherEntryKind.Folder => _hiddenLauncherFolderPaths.Remove(token.Key),
|
||
LauncherEntryKind.Shortcut => _hiddenLauncherAppPaths.Remove(token.Key),
|
||
_ => false
|
||
};
|
||
|
||
if (!removed)
|
||
{
|
||
return;
|
||
}
|
||
|
||
ApplyLauncherVisibilitySettingsChange();
|
||
}
|
||
|
||
private Bitmap? GetLauncherIconBitmap(StartMenuAppEntry app)
|
||
{
|
||
if (app.IconPngBytes is null || app.IconPngBytes.Length == 0)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
if (_launcherIconCache.TryGetValue(app.RelativePath, out var cached))
|
||
{
|
||
return cached;
|
||
}
|
||
|
||
try
|
||
{
|
||
using var stream = new MemoryStream(app.IconPngBytes, writable: false);
|
||
var bitmap = new Bitmap(stream);
|
||
_launcherIconCache[app.RelativePath] = bitmap;
|
||
return bitmap;
|
||
}
|
||
catch
|
||
{
|
||
return null;
|
||
}
|
||
}
|
||
|
||
private Bitmap? GetLauncherFolderIconBitmap()
|
||
{
|
||
if (_launcherFolderIconBitmap is not null)
|
||
{
|
||
return _launcherFolderIconBitmap;
|
||
}
|
||
|
||
if (_launcherFolderIconPngBytes is null || _launcherFolderIconPngBytes.Length == 0)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
try
|
||
{
|
||
using var stream = new MemoryStream(_launcherFolderIconPngBytes, writable: false);
|
||
_launcherFolderIconBitmap = new Bitmap(stream);
|
||
return _launcherFolderIconBitmap;
|
||
}
|
||
catch
|
||
{
|
||
_launcherFolderIconBitmap = null;
|
||
return null;
|
||
}
|
||
}
|
||
|
||
private void OpenLauncherFolder(StartMenuFolderNode folder)
|
||
{
|
||
_launcherFolderStack.Push(folder);
|
||
RenderLauncherFolderFromStack();
|
||
}
|
||
|
||
private void CloseLauncherFolderOverlay()
|
||
{
|
||
ClearSelectedLauncherTile(refreshTaskbar: false);
|
||
_launcherFolderStack.Clear();
|
||
if (LauncherFolderOverlay is not null)
|
||
{
|
||
LauncherFolderOverlay.IsVisible = false;
|
||
}
|
||
|
||
if (LauncherFolderGridPanel is not null)
|
||
{
|
||
LauncherFolderGridPanel.Children.Clear();
|
||
}
|
||
}
|
||
|
||
private void RenderLauncherFolderFromStack()
|
||
{
|
||
if (LauncherFolderOverlay is null ||
|
||
LauncherFolderGridPanel is null ||
|
||
LauncherFolderTitleTextBlock is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
ClearSelectedLauncherTile(refreshTaskbar: false);
|
||
if (_launcherFolderStack.Count == 0)
|
||
{
|
||
CloseLauncherFolderOverlay();
|
||
return;
|
||
}
|
||
|
||
var folder = _launcherFolderStack.Peek();
|
||
LauncherFolderOverlay.IsVisible = true;
|
||
LauncherFolderTitleTextBlock.Text = folder.Name;
|
||
|
||
LauncherFolderGridPanel.Children.Clear();
|
||
|
||
const int maxCols = 4;
|
||
const int maxRows = 3;
|
||
const int maxItems = maxCols * maxRows;
|
||
|
||
var visibleFolders = folder.Folders.Where(IsLauncherFolderVisible).ToList();
|
||
var visibleApps = folder.Apps.Where(IsLauncherAppVisible).ToList();
|
||
|
||
if (visibleFolders.Count == 0 && visibleApps.Count == 0)
|
||
{
|
||
LauncherFolderGridPanel.Children.Add(CreateLauncherFolderGridHintCell(
|
||
L("launcher.empty_folder", "This folder is empty.")));
|
||
return;
|
||
}
|
||
|
||
var allItems = new List<(StartMenuFolderNode? Folder, StartMenuAppEntry? App)>();
|
||
foreach (var f in visibleFolders)
|
||
{
|
||
allItems.Add((f, null));
|
||
}
|
||
foreach (var a in visibleApps)
|
||
{
|
||
allItems.Add((null, a));
|
||
}
|
||
|
||
var displayCount = Math.Min(allItems.Count, maxItems);
|
||
for (var i = 0; i < displayCount; i++)
|
||
{
|
||
var col = i % maxCols;
|
||
var row = i / maxCols;
|
||
var (itemFolder, itemApp) = allItems[i];
|
||
|
||
Control cell;
|
||
if (itemFolder is not null)
|
||
{
|
||
var capturedFolder = itemFolder;
|
||
cell = CreateLauncherFolderGridTile(itemFolder.Name, GetLauncherFolderIconBitmap(), () => OpenLauncherFolder(capturedFolder));
|
||
}
|
||
else if (itemApp is not null)
|
||
{
|
||
var capturedApp = itemApp;
|
||
cell = CreateLauncherFolderGridTile(capturedApp, () => LaunchStartMenuEntry(capturedApp));
|
||
}
|
||
else
|
||
{
|
||
continue;
|
||
}
|
||
|
||
Grid.SetColumn(cell, col);
|
||
Grid.SetRow(cell, row);
|
||
LauncherFolderGridPanel.Children.Add(cell);
|
||
}
|
||
}
|
||
|
||
private Button CreateLauncherFolderGridTile(StartMenuAppEntry app, Action clickAction)
|
||
{
|
||
var iconBitmap = GetLauncherIconBitmap(app);
|
||
var monogram = BuildMonogram(app.DisplayName);
|
||
|
||
Control iconControl = iconBitmap is not null
|
||
? new Image
|
||
{
|
||
Source = iconBitmap,
|
||
Width = 32,
|
||
Height = 32,
|
||
Stretch = Stretch.Uniform
|
||
}
|
||
: new Border
|
||
{
|
||
Width = 32,
|
||
Height = 32,
|
||
CornerRadius = new CornerRadius(8),
|
||
Background = GetThemeBrush("AdaptiveButtonBackgroundBrush"),
|
||
HorizontalAlignment = HorizontalAlignment.Center,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
Child = new TextBlock
|
||
{
|
||
Text = monogram,
|
||
FontSize = 13,
|
||
FontWeight = FontWeight.Bold,
|
||
HorizontalAlignment = HorizontalAlignment.Center,
|
||
VerticalAlignment = VerticalAlignment.Center
|
||
}
|
||
};
|
||
|
||
var content = new StackPanel
|
||
{
|
||
Spacing = 6,
|
||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||
VerticalAlignment = VerticalAlignment.Center
|
||
};
|
||
content.Children.Add(iconControl);
|
||
content.Children.Add(new TextBlock
|
||
{
|
||
Text = app.DisplayName,
|
||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||
MaxLines = 2,
|
||
TextAlignment = TextAlignment.Center,
|
||
FontSize = 11,
|
||
HorizontalAlignment = HorizontalAlignment.Stretch
|
||
});
|
||
|
||
var button = new Button
|
||
{
|
||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||
VerticalAlignment = VerticalAlignment.Stretch,
|
||
BorderThickness = new Thickness(0),
|
||
CornerRadius = new CornerRadius(12),
|
||
Padding = new Thickness(8, 8, 8, 6),
|
||
Content = content
|
||
};
|
||
|
||
// 闂傚倷绀侀幖顐ょ矓閻戞枻缍栧璺猴功閺嗐倕霉閿濆洤鍔嬪┑顖氥偢閺屾盯骞樺Δ鈧幊蹇涙倵椤撱垺鈷戦柛娑橈工婵洭鏌涢悢閿嬪仴闁诡喚鍋撻妶锝夊礃閵娿儱鎸ゆ俊鐐€栭悧妤冨枈瀹ュ纾垮┑鐘叉处閻撴盯鏌涢弴銊ヤ簻闁抽攱妫冮弻鏇㈠炊閵娿儱鎽甸梺纭呮珪椤ㄥ牊绂掗敃鍌涘€锋い鎺戝€哥拋?
|
||
if (_showLauncherTileBackground)
|
||
{
|
||
button.Classes.Add("glass-panel");
|
||
}
|
||
else
|
||
{
|
||
button.Background = Brushes.Transparent;
|
||
}
|
||
|
||
button.Click += (_, _) =>
|
||
{
|
||
if (_isComponentLibraryOpen)
|
||
{
|
||
return;
|
||
}
|
||
|
||
clickAction();
|
||
};
|
||
return button;
|
||
}
|
||
|
||
private Button CreateLauncherFolderGridTile(string folderName, Bitmap? iconBitmap, Action clickAction)
|
||
{
|
||
var monogram = "DIR";
|
||
|
||
Control iconControl = iconBitmap is not null
|
||
? new Image
|
||
{
|
||
Source = iconBitmap,
|
||
Width = 32,
|
||
Height = 32,
|
||
Stretch = Stretch.Uniform
|
||
}
|
||
: new Border
|
||
{
|
||
Width = 32,
|
||
Height = 32,
|
||
CornerRadius = new CornerRadius(8),
|
||
Background = GetThemeBrush("AdaptiveButtonBackgroundBrush"),
|
||
HorizontalAlignment = HorizontalAlignment.Center,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
Child = new TextBlock
|
||
{
|
||
Text = monogram,
|
||
FontSize = 11,
|
||
FontWeight = FontWeight.Bold,
|
||
HorizontalAlignment = HorizontalAlignment.Center,
|
||
VerticalAlignment = VerticalAlignment.Center
|
||
}
|
||
};
|
||
|
||
var content = new StackPanel
|
||
{
|
||
Spacing = 6,
|
||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||
VerticalAlignment = VerticalAlignment.Center
|
||
};
|
||
content.Children.Add(iconControl);
|
||
content.Children.Add(new TextBlock
|
||
{
|
||
Text = folderName,
|
||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||
MaxLines = 2,
|
||
TextAlignment = TextAlignment.Center,
|
||
FontSize = 11,
|
||
HorizontalAlignment = HorizontalAlignment.Stretch
|
||
});
|
||
|
||
var button = new Button
|
||
{
|
||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||
VerticalAlignment = VerticalAlignment.Stretch,
|
||
BorderThickness = new Thickness(0),
|
||
CornerRadius = new CornerRadius(12),
|
||
Padding = new Thickness(8, 8, 8, 6),
|
||
Content = content
|
||
};
|
||
|
||
// 闂傚倷绀侀幖顐ょ矓閻戞枻缍栧璺猴功閺嗐倕霉閿濆洤鍔嬪┑顖氥偢閺屾盯骞樺Δ鈧幊蹇涙倵椤撱垺鈷戦柛娑橈工婵洭鏌涢悢閿嬪仴闁诡喚鍋撻妶锝夊礃閵娿儱鎸ゆ俊鐐€栭悧妤冨枈瀹ュ纾垮┑鐘叉处閻撴盯鏌涢弴銊ヤ簻闁抽攱妫冮弻鏇㈠炊閵娿儱鎽甸梺纭呮珪椤ㄥ牊绂掗敃鍌涘€锋い鎺戝€哥拋?
|
||
if (_showLauncherTileBackground)
|
||
{
|
||
button.Classes.Add("glass-panel");
|
||
}
|
||
else
|
||
{
|
||
button.Background = Brushes.Transparent;
|
||
}
|
||
|
||
button.Click += (_, _) =>
|
||
{
|
||
if (_isComponentLibraryOpen)
|
||
{
|
||
return;
|
||
}
|
||
|
||
clickAction();
|
||
};
|
||
return button;
|
||
}
|
||
|
||
private Control CreateLauncherFolderGridHintCell(string message)
|
||
{
|
||
return CreateLauncherFolderGridHintCell(message, 0, 0);
|
||
}
|
||
|
||
private Control CreateLauncherFolderGridHintCell(string message, int col, int row)
|
||
{
|
||
var textBlock = new TextBlock
|
||
{
|
||
Text = message,
|
||
FontSize = 12,
|
||
FontWeight = FontWeight.SemiBold,
|
||
HorizontalAlignment = HorizontalAlignment.Center,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
Opacity = 0.6
|
||
};
|
||
|
||
var cell = new Border
|
||
{
|
||
Classes = { "glass-panel" },
|
||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||
VerticalAlignment = VerticalAlignment.Stretch,
|
||
CornerRadius = new CornerRadius(12),
|
||
Child = textBlock
|
||
};
|
||
|
||
Grid.SetColumn(cell, col);
|
||
Grid.SetRow(cell, row);
|
||
return cell;
|
||
}
|
||
|
||
private static string BuildMonogram(string text)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(text))
|
||
{
|
||
return "?";
|
||
}
|
||
|
||
var letters = text
|
||
.Trim()
|
||
.Split(' ', StringSplitOptions.RemoveEmptyEntries)
|
||
.Select(part => part[0])
|
||
.Take(2)
|
||
.ToArray();
|
||
if (letters.Length == 0)
|
||
{
|
||
return "?";
|
||
}
|
||
|
||
return new string(letters).ToUpperInvariant();
|
||
}
|
||
|
||
private string GetLauncherEmptyText()
|
||
{
|
||
return OperatingSystem.IsLinux()
|
||
? L("launcher.empty_linux", "No Linux desktop entries were found.")
|
||
: L("launcher.empty", "No Start Menu entries found.");
|
||
}
|
||
|
||
private static void LaunchStartMenuEntry(StartMenuAppEntry app)
|
||
{
|
||
try
|
||
{
|
||
if (OperatingSystem.IsLinux() &&
|
||
!string.IsNullOrWhiteSpace(app.LaunchExecutable))
|
||
{
|
||
var linuxStartInfo = new ProcessStartInfo
|
||
{
|
||
FileName = app.LaunchExecutable,
|
||
UseShellExecute = false
|
||
};
|
||
|
||
if (!string.IsNullOrWhiteSpace(app.WorkingDirectory))
|
||
{
|
||
linuxStartInfo.WorkingDirectory = app.WorkingDirectory;
|
||
}
|
||
|
||
foreach (var argument in app.LaunchArguments)
|
||
{
|
||
linuxStartInfo.ArgumentList.Add(argument);
|
||
}
|
||
|
||
Process.Start(linuxStartInfo);
|
||
return;
|
||
}
|
||
|
||
var startInfo = new ProcessStartInfo
|
||
{
|
||
FileName = app.FilePath,
|
||
UseShellExecute = true
|
||
};
|
||
Process.Start(startInfo);
|
||
}
|
||
catch
|
||
{
|
||
// Ignore failures to launch malformed shortcuts.
|
||
}
|
||
}
|
||
|
||
private void OnLauncherFolderOverlayPointerPressed(object? sender, PointerPressedEventArgs e)
|
||
{
|
||
if (LauncherFolderPanel is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var point = e.GetCurrentPoint(LauncherFolderPanel).Position;
|
||
if (point.X >= 0 &&
|
||
point.Y >= 0 &&
|
||
point.X <= LauncherFolderPanel.Bounds.Width &&
|
||
point.Y <= LauncherFolderPanel.Bounds.Height)
|
||
{
|
||
return;
|
||
}
|
||
|
||
CloseLauncherFolderOverlay();
|
||
e.Handled = true;
|
||
}
|
||
|
||
private void DisposeLauncherResources()
|
||
{
|
||
foreach (var bitmap in _launcherIconCache.Values)
|
||
{
|
||
bitmap.Dispose();
|
||
}
|
||
|
||
_launcherIconCache.Clear();
|
||
_launcherFolderIconBitmap?.Dispose();
|
||
_launcherFolderIconBitmap = null;
|
||
}
|
||
}
|