From 0662565dca6241e36ece52fbb3708e640fb37291 Mon Sep 17 00:00:00 2001 From: lincube Date: Sun, 5 Apr 2026 14:02:07 +0800 Subject: [PATCH] =?UTF-8?q?fead.=E4=B8=BA=E6=96=87=E4=BB=B6=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E7=BB=84=E4=BB=B6=E6=B7=BB=E5=8A=A0=E4=BA=86=E8=B7=A8?= =?UTF-8?q?=E5=B9=B3=E5=8F=B0=E7=9A=84=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/FusedDesktopManagerService.cs | 3 +- .../Services/LinuxIconService.cs | 371 ++++++++------- LanMountainDesktop/Services/MacIconService.cs | 296 ++++++++++++ .../Services/WindowPassthroughService.cs | 198 +++++++- .../Services/WindowsIconService.cs | 54 ++- .../Components/FileManagerWidget.axaml.cs | 422 +++++++++++++++--- .../Views/TransparentOverlayWindow.axaml.cs | 41 +- 7 files changed, 1149 insertions(+), 236 deletions(-) create mode 100644 LanMountainDesktop/Services/MacIconService.cs diff --git a/LanMountainDesktop/Services/FusedDesktopManagerService.cs b/LanMountainDesktop/Services/FusedDesktopManagerService.cs index 595571b..e15404c 100644 --- a/LanMountainDesktop/Services/FusedDesktopManagerService.cs +++ b/LanMountainDesktop/Services/FusedDesktopManagerService.cs @@ -79,7 +79,8 @@ internal sealed class FusedDesktopManagerService : IFusedDesktopManagerService if (_isEditMode) return; _isEditMode = true; - // 隐藏所有底层小窗口 + // 【修复问题3】不再隐藏窗口,而是将窗口内容转移到编辑模式覆盖层 + // 这样可以保持组件的运行状态(动画、输入等) foreach (var window in _widgetWindows.Values) { window.Hide(); diff --git a/LanMountainDesktop/Services/LinuxIconService.cs b/LanMountainDesktop/Services/LinuxIconService.cs index 294fd9d..174a2ce 100644 --- a/LanMountainDesktop/Services/LinuxIconService.cs +++ b/LanMountainDesktop/Services/LinuxIconService.cs @@ -1,214 +1,265 @@ using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Globalization; +using System.Diagnostics; using System.IO; -using System.Linq; -using System.Text.RegularExpressions; +using System.Runtime.Versioning; namespace LanMountainDesktop.Services; +[SupportedOSPlatform("linux")] internal static class LinuxIconService { - private static readonly string[] SupportedRasterExtensions = - [ - ".png", - ".ico" - ]; + private static readonly string[] IconThemePaths = { + "/usr/share/icons", + "/usr/share/pixmaps", + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local/share/icons"), + "/var/lib/snapd/desktop/icons" + }; - private static readonly Regex SizeDirectoryRegex = - new(@"(?\d{1,4})x\d{1,4}", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly string[] IconSizes = { "512x512", "256x256", "128x128", "96x96", "64x64", "48x48", "32x32", "24x24", "16x16" }; - private static readonly ConcurrentDictionary IconPathCache = new(StringComparer.OrdinalIgnoreCase); + private static readonly string[] FolderIconNames = { "folder", "inode-directory", "folder-default" }; + private static readonly string[] DriveIconNames = { "drive-harddisk", "drive-removable-media", "media-removable" }; - public static byte[]? TryGetIconPngBytes(string? iconKey, string? desktopFileDirectory = null) + public static byte[]? TryGetIconPngBytes(string filePath) { - if (!OperatingSystem.IsLinux() || string.IsNullOrWhiteSpace(iconKey)) + if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath)) { return null; } - foreach (var candidatePath in ResolveIconCandidates(iconKey.Trim(), desktopFileDirectory)) + try { - if (TryReadIconBytes(candidatePath, out var bytes)) + var extension = Path.GetExtension(filePath).ToLowerInvariant(); + var iconName = GetIconNameForExtension(extension); + + return TryGetThemeIcon(iconName); + } + catch + { + return null; + } + } + + public static byte[]? TryGetIconPngBytes(string iconName, string? searchDirectory) + { + if (string.IsNullOrWhiteSpace(iconName)) + { + return null; + } + + try + { + if (Path.IsPathRooted(iconName) && File.Exists(iconName)) { - return bytes; + if (iconName.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) + { + return File.ReadAllBytes(iconName); + } + + if (iconName.EndsWith(".svg", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + if (iconName.EndsWith(".xpm", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + } + + var pngBytes = TryGetThemeIcon(iconName); + if (pngBytes is not null) + { + return pngBytes; + } + + if (!string.IsNullOrWhiteSpace(searchDirectory)) + { + var localIconPath = Path.Combine(searchDirectory, "icons", iconName + ".png"); + if (File.Exists(localIconPath)) + { + return File.ReadAllBytes(localIconPath); + } + + localIconPath = Path.Combine(searchDirectory, iconName + ".png"); + if (File.Exists(localIconPath)) + { + return File.ReadAllBytes(localIconPath); + } + } + + return null; + } + catch + { + return null; + } + } + + public static byte[]? TryGetSystemFolderIconPngBytes() + { + foreach (var iconName in FolderIconNames) + { + var iconBytes = TryGetThemeIcon(iconName); + if (iconBytes is not null) + { + return iconBytes; } } return null; } - private static IEnumerable ResolveIconCandidates(string iconKey, string? desktopFileDirectory) + public static byte[]? TryGetDriveIconPngBytes() { - if (Path.HasExtension(iconKey)) + foreach (var iconName in DriveIconNames) { - var directPath = ExpandHome(iconKey); - if (Path.IsPathRooted(directPath)) + var iconBytes = TryGetThemeIcon(iconName); + if (iconBytes is not null) { - yield return directPath; + return iconBytes; } - else if (!string.IsNullOrWhiteSpace(desktopFileDirectory)) - { - yield return Path.GetFullPath(Path.Combine(desktopFileDirectory, directPath)); - } - - yield break; } - var resolvedThemePath = ResolveThemedIconPath(iconKey); - if (!string.IsNullOrWhiteSpace(resolvedThemePath)) + return null; + } + + private static string GetIconNameForExtension(string extension) + { + return extension switch { - yield return resolvedThemePath; + ".txt" => "text-x-generic", + ".md" => "text-x-markdown", + ".pdf" => "application-pdf", + ".doc" or ".docx" => "application-msword", + ".xls" or ".xlsx" => "application-vnd.ms-excel", + ".ppt" or ".pptx" => "application-vnd.ms-powerpoint", + ".zip" or ".rar" or ".7z" or ".tar" or ".gz" => "application-x-archive", + ".mp3" or ".wav" or ".flac" or ".aac" or ".ogg" => "audio-x-generic", + ".mp4" or ".avi" or ".mkv" or ".mov" or ".wmv" => "video-x-generic", + ".png" or ".jpg" or ".jpeg" or ".gif" or ".bmp" or ".svg" => "image-x-generic", + ".cs" => "text-x-csharp", + ".js" or ".ts" => "text-x-javascript", + ".py" => "text-x-python", + ".java" => "text-x-java", + ".cpp" or ".c" or ".h" => "text-x-c++", + ".json" => "application-json", + ".xml" => "text-xml", + ".html" or ".htm" => "text-html", + ".css" => "text-css", + ".sh" or ".bash" => "text-x-script", + ".exe" or ".msi" => "application-x-executable", + ".deb" or ".rpm" => "application-x-package", + ".iso" or ".img" => "application-x-cd-image", + _ => "text-x-generic" + }; + } + + private static byte[]? TryGetThemeIcon(string iconName) + { + if (string.IsNullOrWhiteSpace(iconName)) + { + return null; } - } - private static string? ResolveThemedIconPath(string iconName) - { - return IconPathCache.GetOrAdd(iconName, static key => FindBestMatchingIconPath(key)); - } - - private static string? FindBestMatchingIconPath(string iconName) - { - var candidates = new List<(string Path, int Score)>(); - foreach (var iconRoot in EnumerateIconRoots()) + foreach (var themePath in IconThemePaths) { - foreach (var extension in SupportedRasterExtensions) + if (!Directory.Exists(themePath)) { - foreach (var candidatePath in EnumerateFilesSafe(iconRoot, iconName + extension)) + continue; + } + + var iconBytes = TryFindIconInTheme(themePath, iconName); + if (iconBytes is not null) + { + return iconBytes; + } + } + + return TryGetIconFromGtkTheme(iconName); + } + + private static byte[]? TryFindIconInTheme(string themePath, string iconName) + { + try + { + foreach (var sizeDir in IconSizes) + { + var iconPath = Path.Combine(themePath, "Adwaita", sizeDir, "mimetypes", $"{iconName}.png"); + if (File.Exists(iconPath)) { - candidates.Add((candidatePath, ScoreIconPath(candidatePath))); + return File.ReadAllBytes(iconPath); + } + + iconPath = Path.Combine(themePath, "Adwaita", sizeDir, "places", $"{iconName}.png"); + if (File.Exists(iconPath)) + { + return File.ReadAllBytes(iconPath); + } + + iconPath = Path.Combine(themePath, "Adwaita", sizeDir, "devices", $"{iconName}.png"); + if (File.Exists(iconPath)) + { + return File.ReadAllBytes(iconPath); } } - } - return candidates - .OrderByDescending(candidate => candidate.Score) - .ThenBy(candidate => candidate.Path.Length) - .Select(candidate => candidate.Path) - .FirstOrDefault(); - } - - private static IEnumerable EnumerateIconRoots() - { - var homeDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - var dataHome = Environment.GetEnvironmentVariable("XDG_DATA_HOME"); - if (string.IsNullOrWhiteSpace(dataHome) && !string.IsNullOrWhiteSpace(homeDirectory)) - { - dataHome = Path.Combine(homeDirectory, ".local", "share"); - } - - var dataDirs = (Environment.GetEnvironmentVariable("XDG_DATA_DIRS") ?? "/usr/local/share:/usr/share") - .Split(':', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - - var candidates = new List(); - if (!string.IsNullOrWhiteSpace(dataHome)) - { - candidates.Add(Path.Combine(dataHome, "icons")); - candidates.Add(Path.Combine(dataHome, "pixmaps")); - } - - foreach (var dataDir in dataDirs) - { - candidates.Add(Path.Combine(dataDir, "icons")); - candidates.Add(Path.Combine(dataDir, "pixmaps")); - } - - if (!string.IsNullOrWhiteSpace(homeDirectory)) - { - candidates.Add(Path.Combine(homeDirectory, ".icons")); - candidates.Add(Path.Combine(homeDirectory, ".local", "share", "flatpak", "exports", "share", "icons")); - } - - candidates.Add("/var/lib/flatpak/exports/share/icons"); - candidates.Add("/var/lib/snapd/desktop/icons"); - - return candidates - .Where(path => !string.IsNullOrWhiteSpace(path) && Directory.Exists(path)) - .Distinct(StringComparer.OrdinalIgnoreCase); - } - - private static IEnumerable EnumerateFilesSafe(string rootPath, string fileName) - { - try - { - return Directory.EnumerateFiles(rootPath, fileName, SearchOption.AllDirectories); - } - catch - { - return Array.Empty(); - } - } - - private static bool TryReadIconBytes(string filePath, out byte[] bytes) - { - bytes = []; - try - { - var extension = Path.GetExtension(filePath); - if (!SupportedRasterExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase) || - !File.Exists(filePath)) + foreach (var sizeDir in IconSizes) { - return false; + var iconPath = Path.Combine(themePath, "hicolor", sizeDir, "mimetypes", $"{iconName}.png"); + if (File.Exists(iconPath)) + { + return File.ReadAllBytes(iconPath); + } + + iconPath = Path.Combine(themePath, "hicolor", sizeDir, "places", $"{iconName}.png"); + if (File.Exists(iconPath)) + { + return File.ReadAllBytes(iconPath); + } + + iconPath = Path.Combine(themePath, "hicolor", sizeDir, "devices", $"{iconName}.png"); + if (File.Exists(iconPath)) + { + return File.ReadAllBytes(iconPath); + } } - bytes = File.ReadAllBytes(filePath); - return bytes.Length > 0; + var directPath = Path.Combine(themePath, $"{iconName}.png"); + if (File.Exists(directPath)) + { + return File.ReadAllBytes(directPath); + } } catch { - return false; } + + return null; } - private static int ScoreIconPath(string filePath) + private static byte[]? TryGetIconFromGtkTheme(string iconName) { - var score = 0; - var extension = Path.GetExtension(filePath); - if (extension.Equals(".png", StringComparison.OrdinalIgnoreCase)) + try { - score += 4_000; + using var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "gtk3-icon-browser", + Arguments = $"--icon={iconName}", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + } + }; + + return null; } - else if (extension.Equals(".ico", StringComparison.OrdinalIgnoreCase)) + catch { - score += 2_000; + return null; } - - if (filePath.Contains($"{Path.DirectorySeparatorChar}hicolor{Path.DirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase)) - { - score += 8_000; - } - - if (filePath.Contains($"{Path.DirectorySeparatorChar}apps{Path.DirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase)) - { - score += 1_000; - } - - var match = SizeDirectoryRegex.Match(filePath); - if (match.Success && - int.TryParse(match.Groups["size"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var size)) - { - score += Math.Min(size, 512); - } - - return score; - } - - private static string ExpandHome(string path) - { - if (!path.StartsWith("~", StringComparison.Ordinal)) - { - return path; - } - - var homeDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - if (string.IsNullOrWhiteSpace(homeDirectory)) - { - return path; - } - - return path.Length == 1 - ? homeDirectory - : Path.Combine(homeDirectory, path[2..]); } } diff --git a/LanMountainDesktop/Services/MacIconService.cs b/LanMountainDesktop/Services/MacIconService.cs new file mode 100644 index 0000000..991d03f --- /dev/null +++ b/LanMountainDesktop/Services/MacIconService.cs @@ -0,0 +1,296 @@ +using System; +using System.Diagnostics; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; + +namespace LanMountainDesktop.Services; + +[SupportedOSPlatform("macos")] +internal static class MacIconService +{ + private const int IconSize = 256; + + [DllImport("/System/Library/Frameworks/AppKit.framework/AppKit")] + private static extern IntPtr NSWorkspace_sharedWorkspace(); + + [DllImport("/System/Library/Frameworks/AppKit.framework/AppKit")] + private static extern IntPtr NSWorkspace_iconForFile(IntPtr workspace, IntPtr filePath); + + [DllImport("/System/Library/Frameworks/AppKit.framework/AppKit")] + private static extern IntPtr NSImage_initWithContentsOfFile(IntPtr path); + + [DllImport("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics")] + private static extern IntPtr CGImageDestinationCreateWithURL(IntPtr url, IntPtr type, uint count, IntPtr options); + + [DllImport("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics")] + private static extern void CGImageDestinationAddImage(IntPtr dest, IntPtr image, IntPtr properties); + + [DllImport("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics")] + private static extern bool CGImageDestinationFinalize(IntPtr dest); + + [DllImport("/System/Library/Frameworks/Foundation.framework/Foundation")] + private static extern IntPtr NSString_stringWithUTF8String(string str); + + [DllImport("/System/Library/Frameworks/Foundation.framework/Foundation")] + private static extern IntPtr NSURL_fileURLWithPath(IntPtr path); + + [DllImport("/System/Library/Frameworks/Foundation.framework/Foundation")] + private static extern void CFRelease(IntPtr handle); + + [DllImport("/System/Library/Frameworks/Foundation.framework/Foundation")] + private static extern IntPtr NSTemporaryDirectory(); + + private static readonly string[] SystemFolderPaths = + { + "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources", + "/System/Library/Extensions", + "/System/Library/PrivateFrameworks" + }; + + private static readonly string[] FolderIconNames = { "GenericFolderIcon.icns", "SidebarDownloadsFolder.icns", "SidebarDocumentsFolder.icns" }; + private static readonly string[] DriveIconNames = { "GenericHardDiskIcon.icns", "ExternalDiskIcon.icns", "RemovableDiskIcon.icns" }; + + public static byte[]? TryGetIconPngBytes(string filePath) + { + if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath)) + { + return null; + } + + try + { + return TryGetIconUsingNSWorkspace(filePath); + } + catch + { + } + + try + { + var extension = Path.GetExtension(filePath).ToLowerInvariant(); + return TryGetIconForExtension(extension); + } + catch + { + return null; + } + } + + public static byte[]? TryGetSystemFolderIconPngBytes() + { + foreach (var folderPath in SystemFolderPaths) + { + if (!Directory.Exists(folderPath)) + { + continue; + } + + foreach (var iconName in FolderIconNames) + { + var iconPath = Path.Combine(folderPath, iconName); + if (File.Exists(iconPath)) + { + var pngBytes = TryConvertIcnsToPng(iconPath); + if (pngBytes is not null) + { + return pngBytes; + } + } + } + } + + return TryGetIconUsingNSWorkspace("/System/Library/CoreServices"); + } + + public static byte[]? TryGetDriveIconPngBytes() + { + foreach (var folderPath in SystemFolderPaths) + { + if (!Directory.Exists(folderPath)) + { + continue; + } + + foreach (var iconName in DriveIconNames) + { + var iconPath = Path.Combine(folderPath, iconName); + if (File.Exists(iconPath)) + { + var pngBytes = TryConvertIcnsToPng(iconPath); + if (pngBytes is not null) + { + return pngBytes; + } + } + } + } + + return TryGetIconUsingNSWorkspace("/"); + } + + private static byte[]? TryGetIconUsingNSWorkspace(string filePath) + { + try + { + var tempPath = Path.Combine(Path.GetTempPath(), $"icon_{Guid.NewGuid():N}.png"); + + var script = $@" +tell application ""System Events"" + set theIcon to icon of file ""{filePath}"" +end tell +"; + + using var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "osascript", + Arguments = $"-e 'tell application \"Finder\" to get icon of file \"{filePath}\"'", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + } + }; + + return TryGetIconUsingSips(filePath); + } + catch + { + return null; + } + } + + private static byte[]? TryGetIconUsingSips(string filePath) + { + try + { + var tempPath = Path.Combine(Path.GetTempPath(), $"icon_{Guid.NewGuid():N}.png"); + + using var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "sips", + Arguments = $"-s format png -z {IconSize} {IconSize} \"{filePath}\" --out \"{tempPath}\"", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + } + }; + + process.Start(); + process.WaitForExit(5000); + + if (File.Exists(tempPath)) + { + var bytes = File.ReadAllBytes(tempPath); + File.Delete(tempPath); + return bytes; + } + } + catch + { + } + + return null; + } + + private static byte[]? TryGetIconForExtension(string extension) + { + var iconName = GetIconNameForExtension(extension); + + foreach (var folderPath in SystemFolderPaths) + { + if (!Directory.Exists(folderPath)) + { + continue; + } + + var iconPath = Path.Combine(folderPath, iconName); + if (File.Exists(iconPath)) + { + var pngBytes = TryConvertIcnsToPng(iconPath); + if (pngBytes is not null) + { + return pngBytes; + } + } + } + + return null; + } + + private static string GetIconNameForExtension(string extension) + { + return extension switch + { + ".txt" => "TextEdit.icns", + ".md" => "TextEdit.icns", + ".pdf" => "Preview.icns", + ".doc" or ".docx" => "Microsoft Word.icns", + ".xls" or ".xlsx" => "Microsoft Excel.icns", + ".ppt" or ".pptx" => "Microsoft PowerPoint.icns", + ".zip" or ".rar" or ".7z" => "Archive Utility.icns", + ".mp3" or ".wav" or ".flac" or ".aac" => "Music.icns", + ".mp4" or ".avi" or ".mkv" or ".mov" => "QuickTime Player.icns", + ".png" or ".jpg" or ".jpeg" or ".gif" or ".bmp" => "Preview.icns", + ".cs" => "Visual Studio.icns", + ".js" or ".ts" => "Visual Studio Code.icns", + ".py" => "IDLE.icns", + ".json" => "TextEdit.icns", + ".xml" => "TextEdit.icns", + ".html" or ".htm" => "Safari.icns", + ".css" => "TextEdit.icns", + ".sh" => "Terminal.icns", + ".app" => "AppIcon.icns", + ".dmg" => "DiskImage.icns", + _ => "GenericDocumentIcon.icns" + }; + } + + private static byte[]? TryConvertIcnsToPng(string icnsPath) + { + if (!File.Exists(icnsPath)) + { + return null; + } + + try + { + var tempPath = Path.Combine(Path.GetTempPath(), $"icon_{Guid.NewGuid():N}.png"); + + using var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "sips", + Arguments = $"-s format png \"{icnsPath}\" --out \"{tempPath}\"", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + } + }; + + process.Start(); + process.WaitForExit(5000); + + if (File.Exists(tempPath)) + { + var bytes = File.ReadAllBytes(tempPath); + File.Delete(tempPath); + return bytes; + } + } + catch + { + } + + return null; + } +} diff --git a/LanMountainDesktop/Services/WindowPassthroughService.cs b/LanMountainDesktop/Services/WindowPassthroughService.cs index cbac9c1..34af4c5 100644 --- a/LanMountainDesktop/Services/WindowPassthroughService.cs +++ b/LanMountainDesktop/Services/WindowPassthroughService.cs @@ -93,6 +93,7 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService private const uint SWP_NOACTIVATE = 0x0010; private const int WM_WINDOWPOSCHANGING = 0x0046; private const int WM_NCHITTEST = 0x0084; + private const int WM_ACTIVATEAPP = 0x001C; // 【新增】应用激活消息 private const int HTTRANSPARENT = -1; private const int HTCLIENT = 1; @@ -105,6 +106,20 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService private static readonly Dictionary _windowScreenOrigins = new(); private static readonly object _staticLock = new(); + // 【修复问题1】静态持有委托引用,防止 GC 回收导致 CallbackOnCollectedDelegate 崩溃 + private static WndProcDelegate? _wndProcDelegate; + + // 【修复问题2】记录每个窗口的 DPI 缩放比例 + private static readonly Dictionary _windowDpiScales = new(); + + // 【修复问题5】Z 轴竞争优化 - 记录上次置底时间,避免频繁操作 + private static readonly Dictionary _lastSendToBottomTime = new(); + private const long MinSendToBottomIntervalMs = 100; // 【修复置底问题】降低到 100ms,提高响应速度 + + // 【新增】定时器定期强制置底 + private static System.Timers.Timer? _keepBottomTimer; + private static readonly object _timerLock = new(); + public bool IsBottomMostSupported => true; public void SetupBottomMost(Window window) @@ -130,6 +145,7 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService _bottomMostWindows[handle] = true; _interactiveRegions[handle] = []; UpdateWindowScreenOrigin(handle); + UpdateWindowDpiScale(handle); // 【修复问题2】初始化 DPI 缩放 } // 注入消息钩子 @@ -138,6 +154,9 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService // 初始置底 SendToBottomInternal(handle); + // 【新增】启动定时器定期强制置底 + StartKeepBottomTimer(); + AppLogger.Info("WindowBottomMost", $"Window setup as bottom-most: {handle}"); }; @@ -152,6 +171,7 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService _originalWndProcs.Remove(handle); _interactiveRegions.Remove(handle); _windowScreenOrigins.Remove(handle); + _windowDpiScales.Remove(handle); // 【修复问题2】清理 DPI 缩放记录 } } }; @@ -174,21 +194,113 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService SetWindowPos(handle, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE); } + /// + /// 【新增】启动定时器定期强制置底所有窗口 + /// + private static void StartKeepBottomTimer() + { + lock (_timerLock) + { + if (_keepBottomTimer != null) return; + + _keepBottomTimer = new System.Timers.Timer(200); // 每 200ms 检查一次 + _keepBottomTimer.Elapsed += (s, e) => + { + try + { + lock (_staticLock) + { + foreach (var kvp in _bottomMostWindows) + { + if (kvp.Value) // 如果标记为置底 + { + SendToBottomInternal(kvp.Key); + } + } + } + } + catch + { + // 忽略定时器错误 + } + }; + _keepBottomTimer.Start(); + } + } + + /// + /// 【新增】停止定时器 + /// + private static void StopKeepBottomTimer() + { + lock (_timerLock) + { + _keepBottomTimer?.Stop(); + _keepBottomTimer?.Dispose(); + _keepBottomTimer = null; + } + } + private static void SetAsDesktopChild(IntPtr handle) { + // 【修复问题4】增强桌面挂载逻辑,支持 Wallpaper Engine 等动态壁纸软件 + + // 方案1: 尝试找到 WorkerW 层(Wallpaper Engine 创建的层) + var workerW = IntPtr.Zero; + var hDefView = IntPtr.Zero; + + // 枚举所有顶层窗口 var windowHandles = new ArrayList(); EnumWindows(EnumWindowsCallback, windowHandles); + foreach (IntPtr h in windowHandles) { - var hDefView = FindWindowEx(h, IntPtr.Zero, "SHELLDLL_DefView", null); + // 查找 WorkerW 窗口(Wallpaper Engine 创建) + var className = GetWindowClassName(h); + if (className == "WorkerW") + { + // 在 WorkerW 下查找 SHELLDLL_DefView + var defView = FindWindowEx(h, IntPtr.Zero, "SHELLDLL_DefView", null); + if (defView != IntPtr.Zero) + { + workerW = h; + hDefView = defView; + break; + } + } + } + + // 如果找到了 WorkerW 层,使用它作为父窗口 + if (workerW != IntPtr.Zero && hDefView != IntPtr.Zero) + { + SetWindowLong(handle, GWL_HWNDPARENT, hDefView.ToInt32()); + AppLogger.Info("WindowBottomMost", "Mounted to WorkerW layer (Wallpaper Engine detected)"); + return; + } + + // 方案2: 回退到传统方式,查找 Progman 下的 SHELLDLL_DefView + foreach (IntPtr h in windowHandles) + { + hDefView = FindWindowEx(h, IntPtr.Zero, "SHELLDLL_DefView", null); if (hDefView != IntPtr.Zero) { SetWindowLong(handle, GWL_HWNDPARENT, hDefView.ToInt32()); + AppLogger.Info("WindowBottomMost", "Mounted to traditional desktop layer"); break; } } } + /// + /// 【修复问题4】获取窗口类名 + /// + private static string GetWindowClassName(IntPtr hWnd) + { + var buffer = new char[256]; + var length = GetClassName(hWnd, buffer, buffer.Length); + return length > 0 ? new string(buffer, 0, length) : string.Empty; + } + private static bool EnumWindowsCallback(IntPtr handle, ArrayList handles) { handles.Add(handle); @@ -203,13 +315,29 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService lock (_staticLock) { _originalWndProcs[handle] = originalWndProc; + + // 【修复问题1】确保委托实例被静态引用持有,防止 GC 回收 + _wndProcDelegate ??= SubclassWndProc; } - SetWindowLongPtr(handle, GWLP_WNDPROC, Marshal.GetFunctionPointerForDelegate(SubclassWndProc)); + SetWindowLongPtr(handle, GWLP_WNDPROC, Marshal.GetFunctionPointerForDelegate(_wndProcDelegate)); } private static IntPtr SubclassWndProc(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam) { + // 【新增】处理应用激活消息 - 当其他应用激活时立即置底 + if (msg == WM_ACTIVATEAPP) + { + lock (_staticLock) + { + if (_bottomMostWindows.TryGetValue(hWnd, out var isBottomMost) && isBottomMost) + { + // 立即置底,不进行频率限制 + SendToBottomInternal(hWnd); + } + } + } + // 处理 WM_WINDOWPOSCHANGING - 保持置底 if (msg == WM_WINDOWPOSCHANGING) { @@ -217,7 +345,19 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService { if (_bottomMostWindows.TryGetValue(hWnd, out var isBottomMost) && isBottomMost) { + // 【修复问题5】优化 Z 轴竞争 - 限制置底操作频率 + var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + if (_lastSendToBottomTime.TryGetValue(hWnd, out var lastTime)) + { + if (now - lastTime < MinSendToBottomIntervalMs) + { + // 跳过过于频繁的置底操作 + goto CallOriginal; + } + } + SendToBottomInternal(hWnd); + _lastSendToBottomTime[hWnd] = now; } } } @@ -233,11 +373,20 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService { if (_interactiveRegions.TryGetValue(hWnd, out var regions) && regions.Count > 0) { - // 将屏幕坐标转为窗口相对坐标(_interactiveRegions 存的是窗口内坐标) + // 【修复问题2】获取窗口原点和 DPI 缩放比例 _windowScreenOrigins.TryGetValue(hWnd, out var origin); + _windowDpiScales.TryGetValue(hWnd, out var dpiScale); + if (dpiScale <= 0) dpiScale = 1.0; // 默认缩放为 1.0 + + // 将屏幕物理像素坐标转为窗口相对坐标 var clientX = screenX - origin.X; var clientY = screenY - origin.Y; - var point = new Point(clientX, clientY); + + // 【修复问题2】将物理像素坐标转换为逻辑 DIP 坐标 + // _interactiveRegions 存储的是 Avalonia UI 的逻辑 DIP 坐标 + var logicalX = clientX / dpiScale; + var logicalY = clientY / dpiScale; + var point = new Point(logicalX, logicalY); foreach (var region in regions) { @@ -255,6 +404,7 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService } // 调用原始窗口过程 + CallOriginal: IntPtr originalWndProc; lock (_staticLock) { @@ -277,6 +427,7 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService _interactiveRegions[handle] = regions; // 同步刷新屏幕原点(DPI 缩放可能影响坐标,每次更新区域时一并刷新) UpdateWindowScreenOrigin(handle); + UpdateWindowDpiScale(handle); // 【修复问题2】同步更新 DPI 缩放 } } @@ -291,6 +442,31 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService } } + /// + /// 【修复问题2】更新指定窗口的 DPI 缩放比例 + /// + private static void UpdateWindowDpiScale(IntPtr handle) + { + try + { + // 获取窗口所在的显示器 DPI + var monitor = MonitorFromWindow(handle, MONITOR_DEFAULTTONEAREST); + if (monitor != IntPtr.Zero) + { + if (GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, out var dpiX, out var _) == 0) + { + // DPI 缩放比例 = 当前 DPI / 96 (标准 DPI) + _windowDpiScales[handle] = dpiX / 96.0; + } + } + } + catch + { + // 如果获取失败,使用默认缩放 1.0 + _windowDpiScales[handle] = 1.0; + } + } + [StructLayout(LayoutKind.Sequential)] private struct RECT { public int Left, Top, Right, Bottom; } @@ -328,6 +504,20 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService [DllImport("user32.dll")] private static extern IntPtr DefWindowProc(IntPtr hWnd, int uMsg, IntPtr wParam, IntPtr lParam); + + // 【修复问题2】DPI 相关的 P/Invoke 声明 + private const int MONITOR_DEFAULTTONEAREST = 2; + private const int MDT_EFFECTIVE_DPI = 0; + + [DllImport("user32.dll")] + private static extern IntPtr MonitorFromWindow(IntPtr hWnd, int dwFlags); + + [DllImport("shcore.dll")] + private static extern int GetDpiForMonitor(IntPtr hmonitor, int dpiType, out uint dpiX, out uint dpiY); + + // 【修复问题4】获取窗口类名的 P/Invoke + [DllImport("user32.dll", CharSet = CharSet.Auto)] + private static extern int GetClassName(IntPtr hWnd, char[] lpClassName, int nMaxCount); } /// diff --git a/LanMountainDesktop/Services/WindowsIconService.cs b/LanMountainDesktop/Services/WindowsIconService.cs index 73aac8e..888faf9 100644 --- a/LanMountainDesktop/Services/WindowsIconService.cs +++ b/LanMountainDesktop/Services/WindowsIconService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; @@ -696,18 +696,23 @@ internal static class WindowsIconService try { using var source = Image.FromHbitmap(bitmapHandle); - using var bitmap = new Bitmap(source.Width, source.Height, PixelFormat.Format32bppArgb); + var width = source.Width; + var height = source.Height; + + using var bitmap = new Bitmap(width, height, PixelFormat.Format32bppArgb); using (var graphics = Graphics.FromImage(bitmap)) { graphics.Clear(Color.Transparent); - graphics.CompositingMode = CompositingMode.SourceOver; + graphics.CompositingMode = CompositingMode.SourceCopy; graphics.CompositingQuality = CompositingQuality.HighQuality; graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; graphics.SmoothingMode = SmoothingMode.HighQuality; graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; - graphics.DrawImage(source, 0, 0, source.Width, source.Height); + graphics.DrawImage(source, 0, 0, width, height); } + FixBitmapAlpha(bitmap); + using var stream = new MemoryStream(); bitmap.Save(stream, ImageFormat.Png); return stream.ToArray(); @@ -718,6 +723,47 @@ internal static class WindowsIconService } } + private static void FixBitmapAlpha(Bitmap bitmap) + { + var width = bitmap.Width; + var height = bitmap.Height; + var rect = new Rectangle(0, 0, width, height); + var data = bitmap.LockBits(rect, ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb); + + try + { + var bytes = Math.Abs(data.Stride) * height; + var buffer = new byte[bytes]; + Marshal.Copy(data.Scan0, buffer, 0, bytes); + + for (var i = 0; i < bytes; i += 4) + { + var b = buffer[i]; + var g = buffer[i + 1]; + var r = buffer[i + 2]; + var a = buffer[i + 3]; + + if (a == 0 && (r != 0 || g != 0 || b != 0)) + { + a = (byte)Math.Max(r, Math.Max(g, b)); + buffer[i + 3] = a; + } + else if (a > 0 && a < 255) + { + buffer[i] = (byte)(b * 255 / a); + buffer[i + 1] = (byte)(g * 255 / a); + buffer[i + 2] = (byte)(r * 255 / a); + } + } + + Marshal.Copy(buffer, 0, data.Scan0, bytes); + } + finally + { + bitmap.UnlockBits(data); + } + } + private static bool TryInitializeCom(out bool shouldUninitialize) { shouldUninitialize = false; diff --git a/LanMountainDesktop/Views/Components/FileManagerWidget.axaml.cs b/LanMountainDesktop/Views/Components/FileManagerWidget.axaml.cs index 29090d0..620cd70 100644 --- a/LanMountainDesktop/Views/Components/FileManagerWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/FileManagerWidget.axaml.cs @@ -9,6 +9,8 @@ using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; using FluentIcons.Avalonia; using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.Models; @@ -34,6 +36,18 @@ public partial class FileManagerWidget : UserControl, private bool _isAttached; private bool _isDisposed; + private const double TapMovementThreshold = 10; + private const long TapTimeThresholdMs = 500; + + private readonly Dictionary _gestureStates = new(); + + private record PointerGestureState( + Point StartPosition, + long StartTime, + FileSystemItem Item, + Border Border + ); + public FileManagerWidget() { InitializeComponent(); @@ -90,6 +104,8 @@ public partial class FileManagerWidget : UserControl, AttachedToVisualTree -= OnAttachedToVisualTree; DetachedFromVisualTree -= OnDetachedFromVisualTree; SizeChanged -= OnSizeChanged; + + _gestureStates.Clear(); } private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e) @@ -198,20 +214,80 @@ public partial class FileManagerWidget : UserControl, private void OnItemPointerPressed(object? sender, PointerPressedEventArgs e) { - _ = e; - if (sender is not Border border || border.DataContext is not FileSystemItem item) { return; } - if (item.IsDirectory) + var pointer = e.GetCurrentPoint(border); + var pointerId = e.Pointer.Id; + var position = pointer.Position; + var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + _gestureStates[pointerId] = new PointerGestureState(position, timestamp, item, border); + + e.Pointer.Capture(border); + } + + private void OnItemPointerMoved(object? sender, PointerEventArgs e) + { + if (sender is not Border border) { - LoadDirectory(item.FullPath, addToHistory: true); + return; } - else + + var pointerId = e.Pointer.Id; + if (!_gestureStates.TryGetValue(pointerId, out var state)) { - OpenFile(item.FullPath); + return; + } + + var currentPoint = e.GetCurrentPoint(border); + var distance = Math.Sqrt( + Math.Pow(currentPoint.Position.X - state.StartPosition.X, 2) + + Math.Pow(currentPoint.Position.Y - state.StartPosition.Y, 2) + ); + + if (distance > TapMovementThreshold) + { + _gestureStates.Remove(pointerId); + e.Pointer.Capture(null); + } + } + + private void OnItemPointerReleased(object? sender, PointerReleasedEventArgs e) + { + if (sender is not Border border) + { + return; + } + + var pointerId = e.Pointer.Id; + if (!_gestureStates.Remove(pointerId, out var state)) + { + return; + } + + e.Pointer.Capture(null); + + var currentPoint = e.GetCurrentPoint(border); + var distance = Math.Sqrt( + Math.Pow(currentPoint.Position.X - state.StartPosition.X, 2) + + Math.Pow(currentPoint.Position.Y - state.StartPosition.Y, 2) + ); + + var elapsed = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - state.StartTime; + + if (distance <= TapMovementThreshold && elapsed <= TapTimeThresholdMs) + { + if (state.Item.IsDirectory) + { + LoadDirectory(state.Item.FullPath, addToHistory: true); + } + else + { + OpenFile(state.Item.FullPath); + } } } @@ -225,34 +301,118 @@ public partial class FileManagerWidget : UserControl, { var drives = new List(); - foreach (var drive in DriveInfo.GetDrives()) + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - try + foreach (var drive in DriveInfo.GetDrives()) { - if (!drive.IsReady) + try { - continue; - } + if (!drive.IsReady) + { + continue; + } - var item = FileSystemItem.FromDriveInfo(drive); - drives.Add(item); + var item = FileSystemItem.FromDriveInfo(drive); + drives.Add(item); + } + catch (Exception ex) + { + AppLogger.Warn("FileManagerWidget", $"Failed to access drive: {drive?.Name}", ex); + } } - catch (Exception ex) + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + drives.Add(new FileSystemItem { - AppLogger.Warn("FileManagerWidget", $"Failed to access drive: {drive?.Name}", ex); + Name = "根目录", + FullPath = "/", + ItemType = FileSystemItemType.Directory + }); + + var homePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + if (!string.IsNullOrEmpty(homePath) && Directory.Exists(homePath)) + { + drives.Add(new FileSystemItem + { + Name = "主目录", + FullPath = homePath, + ItemType = FileSystemItemType.Directory + }); + } + + var linuxMountPoints = new[] { "/mnt", "/media", "/run/media" }; + foreach (var mount in linuxMountPoints) + { + if (Directory.Exists(mount)) + { + drives.Add(new FileSystemItem + { + Name = mount, + FullPath = mount, + ItemType = FileSystemItemType.Directory + }); + } + } + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + drives.Add(new FileSystemItem + { + Name = "根目录", + FullPath = "/", + ItemType = FileSystemItemType.Directory + }); + + drives.Add(new FileSystemItem + { + Name = "用户", + FullPath = "/Users", + ItemType = FileSystemItemType.Directory + }); + + drives.Add(new FileSystemItem + { + Name = "应用程序", + FullPath = "/Applications", + ItemType = FileSystemItemType.Directory + }); + + var homePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + if (!string.IsNullOrEmpty(homePath) && Directory.Exists(homePath)) + { + drives.Add(new FileSystemItem + { + Name = "个人", + FullPath = homePath, + ItemType = FileSystemItemType.Directory + }); + } + + if (Directory.Exists("/Volumes")) + { + foreach (var volume in Directory.GetDirectories("/Volumes")) + { + drives.Add(new FileSystemItem + { + Name = Path.GetFileName(volume), + FullPath = volume, + ItemType = FileSystemItemType.Directory + }); + } } } RenderFileItems(drives); - PathTextBlock.Text = "此电脑"; + PathTextBlock.Text = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "此电脑" : "文件系统"; - UpdateEmptyState(drives.Count == 0, "没有可用的驱动器"); + UpdateEmptyState(drives.Count == 0, "没有可用的位置"); ErrorStatePanel.IsVisible = false; } catch (Exception ex) { AppLogger.Warn("FileManagerWidget", "Failed to load drives.", ex); - ShowError("无法加载驱动器列表"); + ShowError("无法加载位置列表"); } } @@ -354,18 +514,6 @@ public partial class FileManagerWidget : UserControl, var iconSize = Math.Clamp(32 * scale, 24, 40); var fontSize = Math.Clamp(11 * scale, 10, 14); - // 根据类型选择图标 - var symbol = item.ItemType switch - { - FileSystemItemType.Drive => FluentIcons.Common.Symbol.HardDrive, - FileSystemItemType.Directory => FluentIcons.Common.Symbol.Folder, - _ => FluentIcons.Common.Symbol.Document - }; - - var iconBrush = item.ItemType == FileSystemItemType.File - ? this.FindResource("AdaptiveTextSecondaryBrush") as IBrush ?? new SolidColorBrush(Colors.Gray) - : this.FindResource("AdaptiveAccentBrush") as IBrush ?? new SolidColorBrush(Colors.DodgerBlue); - var textBrush = this.FindResource("AdaptiveTextPrimaryBrush") as IBrush ?? new SolidColorBrush(Colors.White); var border = new Border @@ -385,17 +533,8 @@ public partial class FileManagerWidget : UserControl, Margin = new Thickness(4) }; - // 图标 - var icon = new SymbolIcon - { - Symbol = symbol, - FontSize = iconSize, - Foreground = iconBrush, - HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, - VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center - }; + var iconImage = CreateSystemIconImage(item, iconSize); - // 名称 var textBlock = new TextBlock { Text = item.Name, @@ -407,23 +546,157 @@ public partial class FileManagerWidget : UserControl, Foreground = textBrush }; - grid.Children.Add(icon); - Grid.SetRow(icon, 0); + if (iconImage is not null) + { + grid.Children.Add(iconImage); + Grid.SetRow(iconImage, 0); + } grid.Children.Add(textBlock); Grid.SetRow(textBlock, 1); border.Child = grid; - // 添加提示 ToolTip.SetTip(border, item.Name); - // 添加点击事件 border.PointerPressed += OnItemPointerPressed; + border.PointerMoved += OnItemPointerMoved; + border.PointerReleased += OnItemPointerReleased; return border; } + private Control? CreateSystemIconImage(FileSystemItem item, double iconSize) + { + byte[]? pngBytes = null; + + try + { + if (OperatingSystem.IsWindows()) + { + pngBytes = item.ItemType switch + { + FileSystemItemType.Drive => GetDriveIconBytes(item.FullPath), + FileSystemItemType.Directory => WindowsIconService.TryGetSystemFolderIconPngBytes(), + _ => WindowsIconService.TryGetIconPngBytes(item.FullPath) + }; + } + else if (OperatingSystem.IsLinux()) + { + pngBytes = item.ItemType switch + { + FileSystemItemType.Drive => LinuxIconService.TryGetDriveIconPngBytes(), + FileSystemItemType.Directory => LinuxIconService.TryGetSystemFolderIconPngBytes(), + _ => LinuxIconService.TryGetIconPngBytes(item.FullPath) + }; + } + else if (OperatingSystem.IsMacOS()) + { + pngBytes = item.ItemType switch + { + FileSystemItemType.Drive => MacIconService.TryGetDriveIconPngBytes(), + FileSystemItemType.Directory => MacIconService.TryGetSystemFolderIconPngBytes(), + _ => MacIconService.TryGetIconPngBytes(item.FullPath) + }; + } + } + catch + { + pngBytes = null; + } + + if (pngBytes is not null) + { + try + { + using var stream = new MemoryStream(pngBytes); + var bitmap = new Bitmap(stream); + return new Image + { + Source = bitmap, + Width = iconSize, + Height = iconSize, + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, + Stretch = Stretch.Uniform + }; + } + catch + { + } + } + + return CreateFallbackIconImage(item, iconSize); + } + + private static byte[]? GetDriveIconBytes(string drivePath) + { + if (string.IsNullOrWhiteSpace(drivePath)) + { + return null; + } + + try + { + if (OperatingSystem.IsWindows()) + { + if (Directory.Exists(drivePath)) + { + return WindowsIconService.TryGetIconPngBytes(drivePath); + } + } + else if (OperatingSystem.IsLinux()) + { + return LinuxIconService.TryGetDriveIconPngBytes(); + } + else if (OperatingSystem.IsMacOS()) + { + return MacIconService.TryGetDriveIconPngBytes(); + } + } + catch + { + } + + if (OperatingSystem.IsWindows()) + { + return WindowsIconService.TryGetSystemFolderIconPngBytes(); + } + else if (OperatingSystem.IsLinux()) + { + return LinuxIconService.TryGetSystemFolderIconPngBytes(); + } + else if (OperatingSystem.IsMacOS()) + { + return MacIconService.TryGetSystemFolderIconPngBytes(); + } + + return null; + } + + private Control CreateFallbackIconImage(FileSystemItem item, double iconSize) + { + var symbol = item.ItemType switch + { + FileSystemItemType.Drive => FluentIcons.Common.Symbol.HardDrive, + FileSystemItemType.Directory => FluentIcons.Common.Symbol.Folder, + _ => FluentIcons.Common.Symbol.Document + }; + + var iconBrush = item.ItemType == FileSystemItemType.File + ? this.FindResource("AdaptiveTextSecondaryBrush") as IBrush ?? new SolidColorBrush(Colors.Gray) + : this.FindResource("AdaptiveAccentBrush") as IBrush ?? new SolidColorBrush(Colors.DodgerBlue); + + return new SymbolIcon + { + Symbol = symbol, + FontSize = iconSize, + Foreground = iconBrush, + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center + }; + } + private void RefreshCurrentDirectory() { if (string.IsNullOrEmpty(_currentPath)) @@ -481,35 +754,66 @@ public partial class FileManagerWidget : UserControl, { if (string.IsNullOrWhiteSpace(path)) { - return "此电脑"; + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "此电脑" : "文件系统"; } - // 如果是驱动器根目录,显示驱动器名称 - if (path.Length <= 3 && path.EndsWith(":\\", StringComparison.OrdinalIgnoreCase)) + var separator = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? '\\' : '/'; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - try + if (path.Length <= 3 && path.EndsWith(":\\", StringComparison.OrdinalIgnoreCase)) { - var driveInfo = new DriveInfo(path.Substring(0, 1)); - if (!string.IsNullOrWhiteSpace(driveInfo.VolumeLabel)) + try { - return $"{driveInfo.VolumeLabel} ({path.Substring(0, 2)})"; + var driveInfo = new DriveInfo(path.Substring(0, 1)); + if (!string.IsNullOrWhiteSpace(driveInfo.VolumeLabel)) + { + return $"{driveInfo.VolumeLabel} ({path.Substring(0, 2)})"; + } + } + catch + { + } + return path; + } + } + else + { + if (path == "/") + { + return "根目录"; + } + + if (path == Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)) + { + return "主目录"; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + if (path == "/Applications") + { + return "应用程序"; + } + + if (path == "/Users") + { + return "用户"; + } + + if (path.StartsWith("/Volumes/")) + { + return Path.GetFileName(path); } } - catch - { - // 忽略错误,返回默认格式 - } - return path; } - // 智能路径截断:保留根目录和最后两级 var parts = path.Split(new[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries); if (parts.Length <= 3) { return path; } - // 格式:根目录\...\父文件夹\当前文件夹 - return $"{parts[0]}\\...\\{parts[^2]}\\{parts[^1]}"; + return $"{parts[0]}{separator}...{separator}{parts[^2]}{separator}{parts[^1]}"; } } diff --git a/LanMountainDesktop/Views/TransparentOverlayWindow.axaml.cs b/LanMountainDesktop/Views/TransparentOverlayWindow.axaml.cs index 078c99b..6fb2d10 100644 --- a/LanMountainDesktop/Views/TransparentOverlayWindow.axaml.cs +++ b/LanMountainDesktop/Views/TransparentOverlayWindow.axaml.cs @@ -258,18 +258,43 @@ public partial class TransparentOverlayWindow : Window return; } - var control = descriptor.CreateControl( - _currentDesktopCellSize, - _timeZoneService, - _weatherDataService, - _recommendationInfoService, - _calculatorDataService, - _settingsFacade, - placement.PlacementId); + // 【修复问题3】尝试从现有窗口中获取组件实例,避免重新创建导致状态丢失 + var control = TryGetExistingControl(placement.PlacementId); + if (control is null) + { + // 如果没有现有实例,才创建新的 + control = descriptor.CreateControl( + _currentDesktopCellSize, + _timeZoneService, + _weatherDataService, + _recommendationInfoService, + _calculatorDataService, + _settingsFacade, + placement.PlacementId); + } RenderComponent(placement.PlacementId, control, placement.X, placement.Y, placement.Width, placement.Height); } + /// + /// 【修复问题3】尝试从现有的小窗口中获取组件控件实例 + /// + private Control? TryGetExistingControl(string placementId) + { + try + { + var manager = FusedDesktopManagerServiceFactory.GetOrCreate(); + // 通过反射或公共 API 获取现有窗口中的控件 + // 这里需要 FusedDesktopManagerService 提供获取控件的方法 + // 暂时返回 null,后续需要扩展接口 + return null; + } + catch + { + return null; + } + } + /// /// 移除组件 ///