diff --git a/ExplorerEx/App.xaml.cs b/ExplorerEx/App.xaml.cs index 63b54d6..d6a0680 100644 --- a/ExplorerEx/App.xaml.cs +++ b/ExplorerEx/App.xaml.cs @@ -2,6 +2,7 @@ using System.Diagnostics; using System.Text; using System.Threading; +using System.Threading.Tasks; using System.Windows; using System.Windows.Input; using System.Windows.Media; @@ -46,8 +47,8 @@ protected override async void OnStartup(StartupEventArgs e) { Current.Shutdown(); return; } - if (Args.RequireDebugger && Debugger.Launch()) { - Debugger.Break(); + if (Args.RequireDebugger) { + Debugger.Launch(); } mutex = new Mutex(true, "ExplorerExMut", out var createdNew); @@ -67,9 +68,7 @@ protected override async void OnStartup(StartupEventArgs e) { isRunning = true; notifyMmf = new NotifyMemoryMappedFile("ExplorerExIPC", 1024, true); - new Thread(IPCWork) { - IsBackground = true - }.Start(); + _ = Task.Factory.StartNew(IPCWork, TaskCreationOptions.LongRunning); ProcessorCount = Environment.ProcessorCount; IconHelper.Initialize(); @@ -159,6 +158,9 @@ private void IPCWork() { case "Open": Dispatcher.Invoke(() => View.MainWindow.OpenPath(msg.Length == 2 ? msg[1] : null)); break; + // case "Select": + // Dispatcher.Invoke(() => View.MainWindow.OpenPath(msg.Length == 2 ? msg[1] : null)); + // break; } } } diff --git a/ExplorerEx/Assets/Geometries.xaml b/ExplorerEx/Assets/Geometries.xaml index 551e8ff..33e4a3e 100644 --- a/ExplorerEx/Assets/Geometries.xaml +++ b/ExplorerEx/Assets/Geometries.xaml @@ -1204,13 +1204,13 @@ - + - + - + @@ -1222,7 +1222,7 @@ - + @@ -1244,6 +1244,22 @@ + + + + + + + + + + + + + + + + M 955.1 433.2 H 301.3 c -17.7 0 -25.6 -21.7 -13.8 -33.5 l 189 -189 c 11.8 -11.8 11.8 -29.5 0 -41.4 L 433.2 126 c -11.8 -11.8 -29.5 -11.8 -41.4 0 L 47.3 470.6 c -11.8 11.8 -11.8 29.5 0 41.4 l 344.6 344.6 c 11.8 11.8 29.5 11.8 41.4 0 l 41.4 -41.4 c 11.8 -11.8 11.8 -29.5 0 -41.4 l -189 -189 c -11.8 -11.8 -3.9 -33.5 13.8 -33.5 h 653.8 c 15.8 0 29.5 -13.8 29.5 -29.5 v -59.1 c 2 -15.8 -11.8 -29.5 -27.6 -29.5 z M -955.1 433.2 H -301.3 c 17.7 0 25.6 -21.7 13.8 -33.5 l -189 -189 c -11.8 -11.8 -11.8 -29.5 0 -41.4 L -433.2 126 c 11.8 -11.8 29.5 -11.8 41.4 0 L -47.3 470.6 c 11.8 11.8 11.8 29.5 0 41.4 l -344.6 344.6 c -11.8 11.8 -29.5 11.8 -41.4 0 l -41.4 -41.4 c -11.8 -11.8 -11.8 -29.5 0 -41.4 l 189 -189 c 11.8 -11.8 3.9 -33.5 -13.8 -33.5 h -653.8 c -15.8 0 -29.5 -13.8 -29.5 -29.5 v -59.1 c -2 -15.8 11.8 -29.5 27.6 -29.5 z M 414 809.7 V 155.9 c 0 -17.7 -21.7 -25.6 -33.5 -13.8 l -189 189 c -11.8 11.8 -29.5 11.8 -41.4 0 l -43.3 -43.3 c -11.8 -11.8 -11.8 -29.5 0 -41.4 l 344.6 -344.5 c 11.8 -11.8 29.5 -11.8 41.4 0 l 344.6 344.6 c 11.8 11.8 11.8 29.5 0 41.4 L 796 329.3 c -11.8 11.8 -29.5 11.8 -41.4 0 l -189 -189 c -11.8 -11.8 -33.5 -3.9 -33.5 13.8 v 653.8 c 0 15.8 -13.8 29.5 -29.5 29.5 h -59.1 c -15.8 2 -29.5 -11.8 -29.5 -27.6 Z diff --git a/ExplorerEx/Assets/Svg/CollapseAll.svg b/ExplorerEx/Assets/Svg/CollapseAll.svg new file mode 100644 index 0000000..1ffe959 --- /dev/null +++ b/ExplorerEx/Assets/Svg/CollapseAll.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ExplorerEx/Command/FileItemCommand.cs b/ExplorerEx/Command/FileItemCommand.cs index 2fbc2c4..ee5ae39 100644 --- a/ExplorerEx/Command/FileItemCommand.cs +++ b/ExplorerEx/Command/FileItemCommand.cs @@ -97,9 +97,13 @@ public async void Execute(object? param) { case "Cut": { var items = Items; if (items.Count > 0) { + var isCut = !Folder.IsReadonly && str == "Cut"; var data = new DataObject(DataFormats.FileDrop, items.Where(item => item is FileSystemItem or DiskDriveItem).Select(item => item.FullPath).ToArray()); - data.SetData("IsCut", !Folder.IsReadonly && str == "Cut"); + data.SetData("IsCut", isCut); Clipboard.SetDataObject(data); + foreach (var item in items) { + item.Opacity = 0.3d; + } } break; } @@ -339,7 +343,7 @@ private static void OpenFileWith(FileListViewItem item, string app) { hc.MessageBox.Error(e.Message, "FailedToOpenFile".L()); } } - + public event EventHandler? CanExecuteChanged; } diff --git a/ExplorerEx/Converter/FileGridListBoxTemplateConverter.cs b/ExplorerEx/Converter/FileGridListBoxTemplateConverter.cs index e799ca1..150e079 100644 --- a/ExplorerEx/Converter/FileGridListBoxTemplateConverter.cs +++ b/ExplorerEx/Converter/FileGridListBoxTemplateConverter.cs @@ -28,7 +28,7 @@ internal class FileGridListBoxTemplateConverter { public DataTemplate? TileHomeTemplate { get; set; } public DataTemplate? Convert() { - return FileListView!.FileView!.FileViewType switch { + return FileListView!.FileView.FileViewType switch { FileViewType.Icons => IconTemplate, FileViewType.List => ListTemplate, FileViewType.Tiles when FileListView.FileView.PathType == PathType.Home => TileHomeTemplate, diff --git a/ExplorerEx/Model/FileListViewItem/DiskDriveItem.cs b/ExplorerEx/Model/FileListViewItem/DiskDriveItem.cs index 1fee9c0..b26f998 100644 --- a/ExplorerEx/Model/FileListViewItem/DiskDriveItem.cs +++ b/ExplorerEx/Model/FileListViewItem/DiskDriveItem.cs @@ -23,15 +23,18 @@ public sealed class DiskDriveItem : FolderItem { public long TotalSpace { get; private set; } - public double FreeSpaceRatio => TotalSpace == -1 ? 0 : (double)(TotalSpace - FreeSpace) / TotalSpace; + /// + /// 使用占比 + /// + public double PercentFull => TotalSpace == -1 ? 0 : (double)(TotalSpace - FreeSpace) / TotalSpace; - public Brush ProgressBarBackground => FreeSpaceRatio > 0.8d ? new SolidColorBrush(GradientColor.Eval((FreeSpaceRatio - 0.5d) * 2d)) : NormalProgressBrush; + public Brush ProgressBarBackground => PercentFull > 0.8d ? new SolidColorBrush(GradientColor.Eval((PercentFull - 0.8d) * 10d)) : NormalProgressBrush; public string SpaceOverviewString => $"{"Available: ".L()}{FileUtils.FormatByteSize(FreeSpace)}{", ".L()}{"Total: ".L()}{FileUtils.FormatByteSize(TotalSpace)}"; private static readonly SolidColorBrush NormalProgressBrush = new(Colors.ForestGreen); - private static readonly Gradient GradientColor = new(Colors.ForestGreen, Colors.Orange, Colors.Red); + private static readonly Gradient GradientColor = new(Colors.ForestGreen, Colors.OrangeRed, Colors.Red); public DiskDriveItem(DriveInfo drive) : base(new DirectoryInfo(drive.Name), LoadDetailsOptions.Default) { Drive = drive; @@ -51,7 +54,7 @@ protected override void LoadAttributes() { } OnPropertyChanged(nameof(FreeSpace)); OnPropertyChanged(nameof(TotalSpace)); - OnPropertyChanged(nameof(FreeSpaceRatio)); + OnPropertyChanged(nameof(PercentFull)); OnPropertyChanged(nameof(ProgressBarBackground)); OnPropertyChanged(nameof(SpaceOverviewString)); } @@ -79,7 +82,6 @@ protected override void InternalRename(string newName) { private class Gradient { private readonly int segmentLength; private readonly HSVColor[] colors; - private readonly Color endColor; public Gradient(params Color[] colors) { Debug.Assert(colors is { Length: > 1 }); @@ -88,16 +90,19 @@ public Gradient(params Color[] colors) { this.colors[i] = new HSVColor(colors[i]); } segmentLength = colors.Length - 1; - endColor = colors[segmentLength]; } public Color Eval(double v) { - if (v >= 1f) { - return endColor; + switch (v) { + case <= 0d: + return colors[0].ToRGB(); + case >= 1d: + return colors[^1].ToRGB(); + default: + var j = v * segmentLength; + var i = (int)j; + return HSVColor.Lerp(colors[i], colors[i + 1], j - i); } - var j = v * segmentLength; - var i = (int)j; - return HSVColor.Lerp(colors[i], colors[i + 1], j - i); } } } \ No newline at end of file diff --git a/ExplorerEx/Model/FileListViewItem/FileListViewItem.cs b/ExplorerEx/Model/FileListViewItem/FileListViewItem.cs index 7e9232a..bc57621 100644 --- a/ExplorerEx/Model/FileListViewItem/FileListViewItem.cs +++ b/ExplorerEx/Model/FileListViewItem/FileListViewItem.cs @@ -20,7 +20,7 @@ protected FileListViewItem(bool isFolder, LoadDetailsOptions options) { This = this; } - protected FileListViewItem(ImageSource defaultIcon, LoadDetailsOptions options) { + protected FileListViewItem(ImageSource? defaultIcon, LoadDetailsOptions options) { this.defaultIcon = defaultIcon; Options = options; This = this; @@ -34,7 +34,7 @@ protected FileListViewItem(string fullPath, string name, bool isFolder, LoadDeta This = this; } - protected FileListViewItem(string fullPath, string name, ImageSource defaultIcon, LoadDetailsOptions options) { + protected FileListViewItem(string fullPath, string name, ImageSource? defaultIcon, LoadDetailsOptions options) { FullPath = fullPath; Name = name; this.defaultIcon = defaultIcon; @@ -50,10 +50,7 @@ public ImageSource? Icon { if (icon != null) { return icon; } - Task.Run(() => { - LoadIcon(); - LoadAttributes(); - }); // TODO + LazyLoad(); return defaultIcon; } protected set { @@ -66,9 +63,18 @@ protected set { private ImageSource? icon; - protected readonly ImageSource defaultIcon; + protected readonly ImageSource? defaultIcon; + + /// + /// 当get_Icon的时候运行 + /// + protected virtual void LazyLoad() { + Task.Run(() => { + LoadIcon(); + LoadAttributes(); + }); // TODO + } - public double Opacity { get => opacity; set { diff --git a/ExplorerEx/Model/FileListViewItem/FileSystemItem.cs b/ExplorerEx/Model/FileListViewItem/FileSystemItem.cs index c8898cf..dbe0200 100644 --- a/ExplorerEx/Model/FileListViewItem/FileSystemItem.cs +++ b/ExplorerEx/Model/FileListViewItem/FileSystemItem.cs @@ -1,7 +1,6 @@ using System; using System.IO; using System.Windows.Media; -using ExplorerEx.Shell32; using ExplorerEx.Utils; using ExplorerEx.View.Controls; using File = System.IO.File; @@ -9,7 +8,7 @@ namespace ExplorerEx.Model; public abstract class FileSystemItem : FileListViewItem { - protected FileSystemInfo? FileSystemInfo { get; } + protected FileSystemInfo? FileSystemInfo { get; init; } /// /// 自动更新UI @@ -41,7 +40,7 @@ protected set { private DateTime dateCreated = DateTime.MaxValue; - public override string GetRenameName() { + public override string? GetRenameName() { return Name; } @@ -72,15 +71,17 @@ public void Refresh() { /// protected FileSystemItem(bool isFolder) : base(isFolder, LoadDetailsOptions.Default) { } + protected FileSystemItem(ImageSource? defaultIcon) : base(defaultIcon, LoadDetailsOptions.Default) { } + protected FileSystemItem(string fullPath, string name, bool isFolder, LoadDetailsOptions options) : base(fullPath, name, isFolder, options) { } - protected FileSystemItem(string fullPath, string name, ImageSource defaultIcon, LoadDetailsOptions options) : base(fullPath, name, defaultIcon, options) { } + protected FileSystemItem(string fullPath, string name, ImageSource? defaultIcon, LoadDetailsOptions options) : base(fullPath, name, defaultIcon, options) { } protected FileSystemItem(FileSystemInfo fileSystemInfo, bool isFolder, LoadDetailsOptions options) : base(fileSystemInfo.FullName, fileSystemInfo.Name, isFolder, options) { FileSystemInfo = fileSystemInfo; } - protected FileSystemItem(FileSystemInfo fileSystemInfo, ImageSource defaultIcon, LoadDetailsOptions options) : base(fileSystemInfo.FullName, fileSystemInfo.Name, defaultIcon, options) { + protected FileSystemItem(FileSystemInfo fileSystemInfo, ImageSource? defaultIcon, LoadDetailsOptions options) : base(fileSystemInfo.FullName, fileSystemInfo.Name, defaultIcon, options) { FileSystemInfo = fileSystemInfo; } } diff --git a/ExplorerEx/Model/FileListViewItem/FolderItem.cs b/ExplorerEx/Model/FileListViewItem/FolderItem.cs index 69252c5..1589b89 100644 --- a/ExplorerEx/Model/FileListViewItem/FolderItem.cs +++ b/ExplorerEx/Model/FileListViewItem/FolderItem.cs @@ -69,6 +69,8 @@ public static (FolderItem?, PathType) ParsePath(string path) { protected FolderItem() : base(true) { } + protected FolderItem(ImageSource? defaultIcon) : base(defaultIcon) { } + public FolderItem(DirectoryInfo directoryInfo, LoadDetailsOptions options) : base(directoryInfo, InitializeIsEmptyFolder(directoryInfo.FullName), options) { IsFolder = true; FileSize = -1; diff --git a/ExplorerEx/Model/FileListViewItem/FolderOnlyItem.cs b/ExplorerEx/Model/FileListViewItem/FolderOnlyItem.cs index 7f6ef5e..970a9b2 100644 --- a/ExplorerEx/Model/FileListViewItem/FolderOnlyItem.cs +++ b/ExplorerEx/Model/FileListViewItem/FolderOnlyItem.cs @@ -45,14 +45,13 @@ internal sealed class FolderOnlyItem : FolderItem { public override string DisplayText => Name; private CancellationTokenSource? cts; - private readonly bool hasItems; private readonly string? zipPath; /// /// 用于存储zip里的相对路径,注意是/而不是\ /// private readonly string? relativePath; - private FolderOnlyItem(FolderOnlyItem? parent) { + private FolderOnlyItem(FolderOnlyItem? parent) : base(null) { if (parent == null) { InitializeChildren(); UpdateDriveChildren(); @@ -63,6 +62,7 @@ private FolderOnlyItem(FolderOnlyItem? parent) { Parent = parent; FullPath = "$Unknown"; } + IsVirtual = true; } /// @@ -71,10 +71,11 @@ private FolderOnlyItem(FolderOnlyItem? parent) { /// /// /// - public FolderOnlyItem(string zipPath, string relativePath, FolderOnlyItem parent) { + public FolderOnlyItem(string zipPath, string relativePath, FolderOnlyItem parent) : base(null) { if (!File.Exists(zipPath) || zipPath[^4..] != ".zip") { throw new ArgumentException("Not a zip file"); } + FileSystemInfo = new FileInfo(zipPath); this.zipPath = zipPath; this.relativePath = relativePath; if (relativePath == string.Empty) { @@ -86,59 +87,64 @@ public FolderOnlyItem(string zipPath, string relativePath, FolderOnlyItem parent } Parent = parent; IsReadonly = true; - using var zip = ZipFile.OpenRead(zipPath); - foreach (var entryName in zip.Entries.Select(e => e.FullName)) { - if (entryName.StartsWith(relativePath)) { - hasItems = true; - if (entryName[relativePath.Length..].Contains('/')) { - InitializeChildren(); - break; - } - } - } + InitializeChildren(); } - public FolderOnlyItem(DirectoryInfo directoryInfo, FolderOnlyItem parent) : base(directoryInfo, LoadDetailsOptions.Default) { + public FolderOnlyItem(DirectoryInfo directoryInfo, FolderOnlyItem parent) : base(null) { + FullPath = directoryInfo.FullName; + Name = directoryInfo.Name; Parent = parent; - // 只看有没有文件夹,不能用FolderUtils.IsEmptyFolder - try { - if (Directory.EnumerateDirectories(FullPath).Any() || Directory.EnumerateFiles(FullPath, "*.zip").Any()) { - InitializeChildren(); - } - } catch { - // Ignore - } + InitializeChildren(); } - public FolderOnlyItem(DriveInfo driveInfo): base(new DirectoryInfo(driveInfo.Name), LoadDetailsOptions.Default) { + public FolderOnlyItem(DriveInfo driveInfo): base(null) { + FullPath = driveInfo.Name; Parent = Home; - if (driveInfo.IsReady) { - InitializeChildren(); - } + InitializeChildren(); Name = DriveUtils.GetFriendlyName(driveInfo); Icon = IconHelper.GetDriveThumbnail(driveInfo.Name); } + /// + /// 需要注意的是,这个只看有没有文件夹或者zip + /// + /// + private bool HasChildren() { + if (zipPath != null) { + System.Diagnostics.Debug.Assert(relativePath != null); + using var zip = ZipFile.OpenRead(zipPath); + return zip.Entries.Select(e => e.FullName).Where(entryName => entryName.StartsWith(relativePath)).Any(entryName => entryName[relativePath.Length..].Contains('/')); + } + return Directory.Exists(FullPath) && new FolderOnlyItemEnumerator(FullPath, null!).Any(); + } + private void InitializeChildren() { - Children = LoadingChildren; - actualChildren = new ConcurrentObservableCollection(); + Task.Run(() => { + if (HasChildren()) { + Children = LoadingChildren; + actualChildren = new ConcurrentObservableCollection(); + } + }); } protected override void LoadAttributes() { } protected override void LoadIcon() { + if (IsVirtual) { + return; + } if (zipPath != null) { if (relativePath == string.Empty) { Icon = IconHelper.GetSmallIcon(".zip", true); - } else if (hasItems) { + } else if (HasChildren()) { Icon = IconHelper.FolderDrawingImage; } else { Icon = IconHelper.EmptyFolderDrawingImage; } - } else if (FolderUtils.IsEmptyFolder(FullPath)) { - Icon = IconHelper.EmptyFolderDrawingImage; - } else { + } else if (HasChildren()) { Icon = IconHelper.FolderDrawingImage; + } else { + Icon = IconHelper.EmptyFolderDrawingImage; } } @@ -186,14 +192,14 @@ public bool IsExpanded { actualChildren.Clear(); } - var showHidden = Settings.Current[Settings.CommonSettings.ShowHiddenFilesAndFolders].GetBoolean(); - var showSystem = Settings.Current[Settings.CommonSettings.ShowProtectedSystemFilesAndFolders].GetBoolean(); - cts = new CancellationTokenSource(); var token = cts.Token; Task.Run(() => { try { if (zipPath != null && relativePath != null) { + var showHidden = Settings.Current[Settings.CommonSettings.ShowHiddenFilesAndFolders].GetBoolean(); + var showSystem = Settings.Current[Settings.CommonSettings.ShowProtectedSystemFilesAndFolders].GetBoolean(); + using var archive = ZipFile.Open(zipPath, ZipArchiveMode.Read, Encoding.GetEncoding("gbk")); foreach (var entry in archive.Entries) { if (token.IsCancellationRequested) { diff --git a/ExplorerEx/Model/FileView.cs b/ExplorerEx/Model/FileView.cs index c549b09..033494f 100644 --- a/ExplorerEx/Model/FileView.cs +++ b/ExplorerEx/Model/FileView.cs @@ -9,6 +9,7 @@ using ExplorerEx.Database.Shared; using ExplorerEx.Model.Enums; using ExplorerEx.Utils; +using HandyControl.Tools.Extension; namespace ExplorerEx.Model; @@ -340,8 +341,14 @@ public FileViewType FileViewType { }; public Size ItemSize { - get => new(ItemWidth, ItemHeight); + get => new(ItemWidth <= 0 ? double.NaN : ItemWidth, ItemHeight <= 0 ? double.NaN : ItemHeight); set { + if (double.IsNaN(value.Width)) { + value.Width = 0; + } + if (double.IsNaN(value.Height)) { + value.Height = 0; + } if (!ItemWidth.Equals(value.Width) || !ItemHeight.Equals(value.Height)) { ItemWidth = value.Width; ItemHeight = value.Height; @@ -459,7 +466,8 @@ private int InternalCompare(object? x, object? y) { return SortBy switch { DetailListType.Type => string.Compare(i1.Type, i2.Type, StringComparison.Ordinal), DetailListType.FileSize => i1.FileSize.CompareTo(i2.FileSize), - _ => string.Compare(i1.Name, i2.Name, StringComparison.Ordinal), + DetailListType.DateModified => i1 is FileSystemItem f1 && i2 is FileSystemItem f2 ? f1.DateModified.CompareTo(f2.DateModified) : string.Compare(i1.Name, i2.Name, StringComparison.OrdinalIgnoreCase), + _ => string.Compare(i1.Name, i2.Name, StringComparison.OrdinalIgnoreCase), }; } throw new NotImplementedException(); diff --git a/ExplorerEx/Strings/Resources.Designer.cs b/ExplorerEx/Strings/Resources.Designer.cs index 95ea458..7226135 100644 --- a/ExplorerEx/Strings/Resources.Designer.cs +++ b/ExplorerEx/Strings/Resources.Designer.cs @@ -744,6 +744,15 @@ internal static string CloseWindow { } } + /// + /// 查找类似 Collapse all 的本地化字符串。 + /// + internal static string CollapseAll { + get { + return ResourceManager.GetString("CollapseAll", resourceCulture); + } + } + /// /// 查找类似 Color mode 的本地化字符串。 /// @@ -1671,15 +1680,6 @@ internal static string Question { } } - /// - /// 查找类似 Quick access 的本地化字符串。 - /// - internal static string Quick_access { - get { - return ResourceManager.GetString("Quick_access", resourceCulture); - } - } - /// /// 查找类似 Recycle Bin 的本地化字符串。 /// diff --git a/ExplorerEx/Strings/Resources.resx b/ExplorerEx/Strings/Resources.resx index 1334864..147493f 100644 --- a/ExplorerEx/Strings/Resources.resx +++ b/ExplorerEx/Strings/Resources.resx @@ -229,9 +229,6 @@ Are you sure you want to change? Bookmarks - - Quick access - Settings @@ -758,4 +755,7 @@ Can't find it? Don't worry, click "yes" and I will regenerate one on the desktop Sorry + + Collapse all + \ No newline at end of file diff --git a/ExplorerEx/Strings/Resources.zh.resx b/ExplorerEx/Strings/Resources.zh.resx index 2823895..1fbe7bc 100644 --- a/ExplorerEx/Strings/Resources.zh.resx +++ b/ExplorerEx/Strings/Resources.zh.resx @@ -276,9 +276,6 @@ 收藏夹 - - 快速访问 - 设置 @@ -802,4 +799,7 @@ ExplorerEx [-h|--help] [-r|--runInBackground] [-o|--openInNewWindow] [-d|--requi 很抱歉 + + 折叠全部 + \ No newline at end of file diff --git a/ExplorerEx/Utils/IconHelper.cs b/ExplorerEx/Utils/IconHelper.cs index 22f0e68..c62e952 100644 --- a/ExplorerEx/Utils/IconHelper.cs +++ b/ExplorerEx/Utils/IconHelper.cs @@ -25,7 +25,6 @@ internal static class IconHelper { /// 可以获取缩略图的文件格式 /// private static readonly HashSet ExtensionsWithThumbnail = new() { - ".exe", ".lnk", ".jpg", ".jpeg", @@ -34,7 +33,6 @@ internal static class IconHelper { ".tif", ".tiff", ".gif", - ".ico", ".svg", ".mp3", ".flac", @@ -155,26 +153,33 @@ public static ImageSource GetSmallIcon(string path, bool useFileAttr) { /// public static ImageSource GetLargeIcon(string path, bool useFileAttr) { var extension = Path.GetExtension(path); - bool isLnk; + var useCache = useFileAttr && path.Length != 3; // 是硬盘根路径 - if (!string.IsNullOrEmpty(extension) && extension.ToLower() == ".lnk") { - isLnk = true; - } else { - isLnk = false; + if (path.Length != 3 && string.IsNullOrEmpty(extension)) { + return UnknownFileDrawingImage; } + extension = extension.ToLower(); + if (useCache) { + lock (CachedLargeIcons) { + if (CachedLargeIcons.TryGetValue(extension, out var icon)) { + return icon; + } + } + } + var dwFa = useFileAttr ? FileAttribute.Normal : 0; var flags = SHGFI.SysIconIndex; if (useFileAttr) { flags |= SHGFI.UseFileAttributes; - } else if (isLnk) { + } else if (extension == ".lnk") { flags |= SHGFI.LinkOverlay; } var shFileInfo = new ShFileInfo(); var hr = SHGetFileInfo(path, dwFa, ref shFileInfo, Marshal.SizeOf(shFileInfo), flags); - Trace.WriteLine(path + ' ' + hr); + // Trace.WriteLine(path + ' ' + hr); if (hr == 0) { return UnknownFileDrawingImage; } @@ -193,8 +198,10 @@ public static ImageSource GetLargeIcon(string path, bool useFileAttr) { var bs = (ImageSource)HIcon2BitmapSource(hIcon); DestroyIcon(hIcon); - lock (CachedLargeIcons) { - CachedLargeIcons[extension] = bs; + if (useCache) { + lock (CachedLargeIcons) { + CachedLargeIcons[extension] = bs; + } } return bs; } @@ -258,11 +265,6 @@ public static ImageSource GetPathThumbnail(string path) { DeleteObject(hBitmap); return bs; } - lock (CachedLargeIcons) { - if (CachedLargeIcons.TryGetValue(extension, out var image)) { - return image; - } - } return GetLargeIcon(path, false); } } \ No newline at end of file diff --git a/ExplorerEx/View/Controls/ContentDialog.xaml.cs b/ExplorerEx/View/Controls/ContentDialog.xaml.cs index 1ed196f..e96e963 100644 --- a/ExplorerEx/View/Controls/ContentDialog.xaml.cs +++ b/ExplorerEx/View/Controls/ContentDialog.xaml.cs @@ -280,7 +280,7 @@ public static ContentDialogResult Ask(string msg, string? caption = null, MainWi throw new InvalidOperationException("No MainWindow is shown."); } return new ContentDialog { - Title = caption ?? "Error".L(), + Title = caption ?? "Question".L(), Content = msg, PrimaryButtonText = "Ok".L(), CancelButtonText = "Cancel".L(), diff --git a/ExplorerEx/View/Controls/CustomControls.xaml b/ExplorerEx/View/Controls/CustomControls.xaml index 542cb9b..90e432c 100644 --- a/ExplorerEx/View/Controls/CustomControls.xaml +++ b/ExplorerEx/View/Controls/CustomControls.xaml @@ -213,9 +213,13 @@ - @@ -341,8 +345,8 @@ - - + + diff --git a/ExplorerEx/View/Controls/FileListView.xaml b/ExplorerEx/View/Controls/FileListView.xaml index 8da18d3..484f42c 100644 --- a/ExplorerEx/View/Controls/FileListView.xaml +++ b/ExplorerEx/View/Controls/FileListView.xaml @@ -20,16 +20,22 @@ Background="Transparent" UseLayoutRounding="True"> + + + + + + + + + + - - - + @@ -41,9 +47,7 @@ - + @@ -54,9 +58,7 @@ - + @@ -77,10 +79,8 @@ - - + + @@ -88,9 +88,9 @@ - @@ -99,8 +99,7 @@ - + @@ -169,12 +168,12 @@ - + @@ -399,25 +398,19 @@ - + - - - - - - + - - - - + + + - + @@ -569,12 +562,14 @@ - + + + - + + + + + + diff --git a/ExplorerEx/View/Controls/FileListView.xaml.cs b/ExplorerEx/View/Controls/FileListView.xaml.cs index 1846f45..6d82c07 100644 --- a/ExplorerEx/View/Controls/FileListView.xaml.cs +++ b/ExplorerEx/View/Controls/FileListView.xaml.cs @@ -7,7 +7,6 @@ using System.IO; using System.Linq; using System.Runtime.CompilerServices; -using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; @@ -29,7 +28,6 @@ using HandyControl.Tools; using GridView = System.Windows.Controls.GridView; using ScrollViewer = System.Windows.Controls.ScrollViewer; -using TextBox = HandyControl.Controls.TextBox; namespace ExplorerEx.View.Controls; @@ -88,18 +86,18 @@ public event ItemDoubleClickEventHandler ItemDoubleClicked { public static readonly DependencyProperty FileViewProperty = DependencyProperty.Register( nameof(FileView), typeof(FileView), typeof(FileListView), new PropertyMetadata(default(FileView), OnFileViewChanged)); - public FileView? FileView { + public FileView FileView { get => (FileView)GetValue(FileViewProperty); set => SetValue(FileViewProperty, value); } private ListCollectionView? listCollectionView; - public double ItemWidth => FileView?.ItemWidth ?? 0d; + public double ItemWidth => FileView.ItemWidth <= 0 ? double.NaN : FileView.ItemWidth; - public double ItemHeight => FileView?.ItemHeight ?? 30d; + public double ItemHeight => FileView.ItemHeight <= 0 ? double.NaN : FileView.ItemHeight; - public Size ActualItemSize => new(ItemWidth <= 0 ? ActualWidth : ItemWidth + 2d, ItemHeight + 6d); + public Size ActualItemSize => new(double.IsNaN(ItemWidth) ? ActualWidth : ItemWidth + 2d, ItemHeight + 6d); public static readonly DependencyProperty FullPathProperty = DependencyProperty.Register( nameof(FullPath), typeof(string), typeof(FileListView), new PropertyMetadata(null, OnFullPathChanged)); @@ -150,15 +148,11 @@ public FileListViewItem? MouseItem { /// public SimpleCommand SelectCommand { get; } - /// - /// 选择并重命名一个文件,参数为string文件名,不含路径 - /// - public SimpleCommand StartRenameCommand { get; } - public SimpleCommand SwitchViewCommand { get; } private readonly FileGridDataGridColumnsConverter columnsConverter; private readonly FileGridListBoxTemplateConverter listBoxTemplateConverter; + private readonly ItemsPanelTemplate virtualizingWrapPanel, virtualizingStackPanel; private ContextMenu? openedContextMenu; @@ -169,15 +163,17 @@ public FileListViewItem? MouseItem { public FileListView() { DataContextChanged += (_, e) => ViewModel = (FileTabViewModel)e.NewValue; InitializeComponent(); + ((FileListViewBindingContext)Resources["BindingContext"]).FileListView = this; ApplyTemplate(); SelectCommand = new SimpleCommand(Select); - StartRenameCommand = new SimpleCommand(e => ViewModel.StartRename((string?)e)); SwitchViewCommand = new SimpleCommand(OnSwitchView); columnsConverter = (FileGridDataGridColumnsConverter)Resources["ColumnsConverter"]; listBoxTemplateConverter = (FileGridListBoxTemplateConverter)Resources["ListBoxTemplateConverter"]; + ((FileGridListBoxTemplateConverter)Resources["ListBoxTemplateConverter"]).FileListView = this; + virtualizingWrapPanel = (ItemsPanelTemplate)Resources["VirtualizingWrapPanel"]; virtualizingStackPanel = (ItemsPanelTemplate)Resources["VirtualizingStackPanel"]; - ((FileGridListBoxTemplateConverter)Resources["ListBoxTemplateConverter"]).FileListView = this; + if (!isPreviewTimerInitialized) { MainWindow.FrequentTimerElapsed += MouseHoverTimerWork; @@ -203,18 +199,16 @@ private static void OnFileViewChanged(DependencyObject d, DependencyPropertyChan } private void OnFileViewPropertyChanged(object? sender, PropertyChangedEventArgs e) { - var fileView = FileView!; + var fileView = FileView; switch (e.PropertyName) { case nameof(fileView.FileViewType): if (fileView.FileViewType == FileViewType.Details) { - FileGroupStyle.Panel = ItemsPanel = virtualizingStackPanel; var view = new GridView(); columnsConverter.Convert(view.Columns, fileView); View = view; var padding = Padding; contentPanel.Margin = new Thickness(padding.Left, 30d + padding.Top, padding.Right, padding.Bottom); } else { - FileGroupStyle.Panel = ItemsPanel = virtualizingWrapPanel; ItemTemplate = listBoxTemplateConverter.Convert(); View = null; contentPanel.Margin = Padding; @@ -237,6 +231,7 @@ private void OnFileViewPropertyChanged(object? sender, PropertyChangedEventArgs var groups = listCollectionView!.GroupDescriptions!; groups.Clear(); if (fileView.GroupBy.HasValue) { + GroupStyle.Panel = virtualizingStackPanel; listCollectionView.IsLiveGrouping = true; IValueConverter? converter = null; switch (fileView.GroupBy) { @@ -255,6 +250,7 @@ private void OnFileViewPropertyChanged(object? sender, PropertyChangedEventArgs } groups.Add(new PropertyGroupDescription(fileView.GroupBy.ToString(), converter)); } else { + GroupStyle.Panel = virtualizingWrapPanel; listCollectionView.IsLiveGrouping = false; } break; @@ -378,18 +374,11 @@ public void SelectItems(IEnumerable? fileNames) { /// protected override void OnPreviewMouseDown(MouseButtonEventArgs e) { isDoubleClicked = false; - if (e.OriginalSource.FindParent() == null) { // 如果没有点击在VirtualizingPanel的范围内 - return; // 如果没有点击在VirtualizingPanel或者点击在了TextBox内就不处理事件,直接返回 + if (e.OriginalSource.FindParent() == null) { // 如果没有点击在VirtualizingPanel的范围内 + return; } - - if (e.OriginalSource.FindParent() != null) { // 点击在了项目上 - if (e.OriginalSource.FindParent() != null) { // 如果点击在了重命名的TextBox里,就直接返回 - return; - } - } else { - if (e.OriginalSource.FindParent() != null) { // 如果点击在了Expander上,也直接返回 - return; - } + if (e.OriginalSource.FindParent() != null) { // 如果点击在了Expander上,也直接返回 + return; } Focus(); @@ -442,11 +431,11 @@ protected override void OnPreviewMouseDown(MouseButtonEventArgs e) { var selectedItems = SelectedItems; if (selectedItems.Count == 0) { item.IsSelected = true; - } else if (!item.IsSelected) { + } else if (item.IsSelected && selectedItems.Count == 1) { + isPreparedForRenaming = true; + } else { UnselectAll(); item.IsSelected = true; - } else if (selectedItems.Count == 1) { - isPreparedForRenaming = true; } } } @@ -461,6 +450,7 @@ protected override void OnPreviewMouseDown(MouseButtonEventArgs e) { } } } else { + UnselectAll(); mouseDownRowIndex = -1; if (Settings.Current[Settings.CommonSettings.DoubleClickGoUpperLevel].GetBoolean() && lastMouseUpItem == null && e.ChangedButton == MouseButton.Left) { if (Math.Abs(mouseDownPoint.X - prevMouseUpPoint.X) < SystemParameters.MinimumHorizontalDragDistance && @@ -614,8 +604,11 @@ protected override void OnPreviewMouseUp(MouseButtonEventArgs e) { break; } - if (e.OriginalSource.FindParent() == null || e.OriginalSource.FindParent() != null) { - break; // 如果没有点击在VirtualizingPanel或者点击在了TextBox内就不处理事件,直接返回 + if (e.OriginalSource.FindParent() == null) { // 如果没有点击在VirtualizingPanel的范围内 + break; + } + if (e.OriginalSource.FindParent() != null) { // 如果点击在了Expander上,也直接返回 + break; } var isClickOnItem = mouseDownRowIndex >= 0 && mouseDownRowIndex < ItemsSource.Count; if (isPreparedForRenaming) { @@ -624,10 +617,10 @@ protected override void OnPreviewMouseUp(MouseButtonEventArgs e) { var item = (FileListViewItem)Items[mouseDownRowIndex]; if (!shouldRename) { shouldRename = true; - Task.Run(() => { - Thread.Sleep(Win32Interop.GetDoubleClickTime()); + Task.Run(async () => { + await Task.Delay(Win32Interop.GetDoubleClickTime()); if (shouldRename) { - ViewModel.StartRename(item); + await Dispatcher.BeginInvoke(() => ViewModel.StartRename(item)); } shouldRename = false; }); @@ -635,41 +628,25 @@ protected override void OnPreviewMouseUp(MouseButtonEventArgs e) { shouldRename = false; } } - } else { - var isCtrlOrShiftPressed = Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl) || Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift); - switch (e.ChangedButton) { - case MouseButton.Left: - if (isClickOnItem) { - var item = (FileListViewItem)Items[mouseDownRowIndex]; - if (!isCtrlOrShiftPressed && SelectedItems.Count > 1) { - UnselectAll(); - } - item.IsSelected = true; - } else if (!isCtrlOrShiftPressed) { - UnselectAll(); - } - break; - case MouseButton.Right: - if (isClickOnItem && e.OriginalSource is DependencyObject o) { - var item = (FileListViewItem)Items[mouseDownRowIndex]; - openedContextMenu = ((FrameworkElement)ContainerFromElement(o)!).ContextMenu!; - openedContextMenu.SetValue(FileItemAttach.FileItemProperty, item); - openedContextMenu.DataContext = this; - var ext = Path.GetExtension(item.FullPath); - FileAssocList.Clear(); - if (!string.IsNullOrWhiteSpace(ext) && ViewModel.SelectedItems.Count == 1) { - foreach (var fileAssocItem in FileAssocItem.GetAssocList(ext)) { - FileAssocList.Add(fileAssocItem); - } + } else if (e.ChangedButton == MouseButton.Right) { + if (isClickOnItem && e.OriginalSource is DependencyObject o) { + var item = (FileListViewItem)Items[mouseDownRowIndex]; + openedContextMenu = ((FrameworkElement)ContainerFromElement(o)!).ContextMenu!; + openedContextMenu.SetValue(FileItemAttach.FileItemProperty, item); + openedContextMenu.DataContext = this; + var ext = Path.GetExtension(item.FullPath); + FileAssocList.Clear(); + if (!string.IsNullOrWhiteSpace(ext) && ViewModel.SelectedItems.Count == 1) { + foreach (var fileAssocItem in FileAssocItem.GetAssocList(ext)) { + FileAssocList.Add(fileAssocItem); } - openedContextMenu.IsOpen = true; - } else { - UnselectAll(); - openedContextMenu = ContextMenu!; - openedContextMenu.DataContext = ViewModel; - openedContextMenu.IsOpen = true; } - break; + openedContextMenu.IsOpen = true; + } else { + UnselectAll(); + openedContextMenu = ContextMenu!; + openedContextMenu.DataContext = ViewModel; + openedContextMenu.IsOpen = true; } } if (isClickOnItem) { @@ -692,7 +669,7 @@ protected override void OnSelectionChanged(SelectionChangedEventArgs e) { isDataContextChanging = false; return; } - + ViewModel.ChangeSelection(e); } @@ -851,7 +828,7 @@ protected override void OnDragOver(DragEventArgs e) { destination = mouseItem.DisplayText; contains = draggingPaths.Any(path => path == mouseItem.FullPath); } else { - if (FileView!.PathType == PathType.Home) { + if (FileView.PathType == PathType.Home) { e.Effects = DragDropEffects.None; if (lastDragOnItem != null) { lastDragOnItem.IsSelected = false; @@ -920,7 +897,7 @@ protected override void OnPreviewGiveFeedback(GiveFeedbackEventArgs e) { protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo) { base.OnRenderSizeChanged(sizeInfo); - if (sizeInfo.WidthChanged) { + if (sizeInfo.WidthChanged && double.IsNaN(ItemWidth)) { OnPropertyChanged(nameof(ActualItemSize)); } } @@ -991,3 +968,13 @@ protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } + +public class FileListViewBindingContext : DependencyObject { + public static readonly DependencyProperty FileListViewProperty = DependencyProperty.Register( + nameof(FileListView), typeof(FileListView), typeof(FileListViewBindingContext), new PropertyMetadata(default(FileListView))); + + public FileListView FileListView { + get => (FileListView)GetValue(FileListViewProperty); + set => SetValue(FileListViewProperty, value); + } +} diff --git a/ExplorerEx/View/Controls/FileViewGrid.xaml b/ExplorerEx/View/Controls/FileViewGrid.xaml index 086c10a..5d5dbcc 100644 --- a/ExplorerEx/View/Controls/FileViewGrid.xaml +++ b/ExplorerEx/View/Controls/FileViewGrid.xaml @@ -15,141 +15,12 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -201,15 +72,15 @@ + PreviewKeyDown="AddressBar_OnPreviewKeyDown" PopupItemClicked="AddressBar_OnPopupItemClicked" Height="32"/> - - + @@ -222,18 +93,151 @@ - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ExplorerEx/View/MainWindow.xaml b/ExplorerEx/View/MainWindow.xaml index 8c9e81d..f464aae 100644 --- a/ExplorerEx/View/MainWindow.xaml +++ b/ExplorerEx/View/MainWindow.xaml @@ -209,11 +209,17 @@ + AllowDrop="True" DragEnter="TabItem_OnDragEnter"> + + + @@ -241,7 +247,7 @@ + diff --git a/ExplorerEx/View/MainWindow.xaml.cs b/ExplorerEx/View/MainWindow.xaml.cs index 7b7a680..2a90fb8 100644 --- a/ExplorerEx/View/MainWindow.xaml.cs +++ b/ExplorerEx/View/MainWindow.xaml.cs @@ -69,8 +69,6 @@ public sealed partial class MainWindow { private static volatile uint globalEverythingQueryId; private readonly HashSet everythingQueryIds = new(); - private readonly string? startupPath; - private readonly FileSystemItemContextMenuConverter bookmarkItemContextMenuConverter; /// /// 由于侧边栏ThisPC不可选中,所以右键按下时用这个代表选中的Item @@ -86,7 +84,6 @@ public sealed partial class MainWindow { private readonly ContextMenu bookmarkCategoryItemContextMenu; public MainWindow(string? startupPath, bool startUpLoad = true) { - this.startupPath = startupPath; All.Add(this); var screenWidth = SystemParameters.PrimaryScreenWidth; @@ -185,19 +182,15 @@ public MainWindow(string? startupPath, bool startUpLoad = true) { ChangeTheme(); if (startUpLoad) { - StartupLoad(); - } - } - - private void StartupLoad() { - try { - if (startupPath == null) { - _ = SplitGrid.FileTabControl.StartUpLoad(App.Args.Paths.ToArray()); - } else { - _ = SplitGrid.FileTabControl.StartUpLoad(startupPath); + try { + if (startupPath == null) { + _ = SplitGrid.FileTabControl.StartUpLoad(App.Args.Paths.ToArray()); + } else { + _ = SplitGrid.FileTabControl.StartUpLoad(startupPath); + } + } catch (Exception e) { + App.Fatal(e); } - } catch (Exception e) { - App.Fatal(e); } } @@ -603,6 +596,20 @@ private static async void SideBarItem_OnClick(object? args) { } } + private void BookmarkCollapseAllButton_OnClick(object sender, RoutedEventArgs e) { + foreach (var bookmarkCategory in DbMain.BookmarkDbContext.AsObservableCollection()) { + bookmarkCategory.IsExpanded = false; + } + } + + private void ThisPCCollapseAllButton_OnClick(object sender, RoutedEventArgs e) { + if (FolderOnlyItem.Home.Children == null) { + return; + } + foreach (var folderOnlyItem in FolderOnlyItem.Home.Children) { + folderOnlyItem.IsExpanded = false; + } + } private void OnDragAreaMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { DragMove(); diff --git a/ExplorerEx/ViewModel/FileTabViewModel.cs b/ExplorerEx/ViewModel/FileTabViewModel.cs index d7118ac..b640b4e 100644 --- a/ExplorerEx/ViewModel/FileTabViewModel.cs +++ b/ExplorerEx/ViewModel/FileTabViewModel.cs @@ -399,7 +399,7 @@ public async Task SwitchViewType(ViewSortGroup type) { break; case ViewSortGroup.Details: // 详细信息 FileViewType = FileViewType.Details; - ItemSize = new Size(0d, 30d); + ItemSize = new Size(double.NaN, 30d); break; case ViewSortGroup.Tiles: // 平铺 FileViewType = FileViewType.Tiles; @@ -407,7 +407,7 @@ public async Task SwitchViewType(ViewSortGroup type) { break; case ViewSortGroup.Content: // 内容 FileViewType = FileViewType.Content; - ItemSize = new Size(0d, 70d); + ItemSize = new Size(double.NaN, 70d); break; case ViewSortGroup.SortByName: @@ -784,9 +784,9 @@ public Task GoToUpperLevelAsync() { } var lastIndexOfSlash = FullPath.LastIndexOf('\\'); return lastIndexOfSlash switch { - -1 => LoadDirectoryAsync(null), - 2 => LoadDirectoryAsync(FullPath[..3]), // 例如F:\,此时需要保留最后的\ - _ => LoadDirectoryAsync(FullPath[..lastIndexOfSlash]) + -1 => LoadDirectoryAsync(null, true, FullPath), + 2 => LoadDirectoryAsync(FullPath[..3], true, FullPath), // 例如F:\,此时需要保留最后的\ + _ => LoadDirectoryAsync(FullPath[..lastIndexOfSlash], true, FullPath) }; } return Task.FromResult(false); diff --git a/ExplorerProxy/ExplorerExProxy.cs b/ExplorerProxy/ExplorerExProxy.cs index e3b2a26..2b6f9b7 100644 --- a/ExplorerProxy/ExplorerExProxy.cs +++ b/ExplorerProxy/ExplorerExProxy.cs @@ -7,6 +7,7 @@ using ExplorerProxy.Interop; using Microsoft.Win32; using SHDocVw; +using IServiceProvider = ExplorerProxy.Interop.IServiceProvider; namespace ExplorerProxy { /// @@ -14,11 +15,20 @@ namespace ExplorerProxy { /// [ComVisible(true), Guid("11451400-8700-480c-a27f-000001919810"), ClassInterface(ClassInterfaceType.None)] public class ExplorerExProxy : IObjectWithSite { + private IntPtr pUnkSite; private WebBrowserClass explorer; private const string RegKeyName = @"Software\Microsoft\Windows\CurrentVersion\Explorer\Browser Helper Objects\"; - // ReSharper disable once InconsistentNaming + // ReSharper disable InconsistentNaming private static Guid IID_IWebBrowserApp = new Guid("0002DF05-0000-0000-C000-000000000046"); + private static Guid IID_IServiceProvider = new Guid("6d5140c1-7436-11ce-8034-00aa006009fa"); + private static Guid SID_STopLevelBrowser = new Guid(0x4C96BE40, 0x915C, 0x11CF, 0x99, 0xD3, 0x00, 0xAA, 0x00, 0x4A, 0xE8, 0x37); + private static Guid IID_IShellBrowser = new Guid("000214E2-0000-0000-C000-000000000046"); + private static Guid IID_IFolderView = new Guid("cde725b0-ccc9-4519-917e-325d72fab4ce"); + private static Guid IID_IPersistFolder = new Guid("000214EA-0000-0000-C000-000000000046"); + private static Guid IID_IPersistFolder2 = new Guid("1AC3D9F0-175C-11d1-95BE-00609797EA4F"); + private static Guid IID_IShellFolder = new Guid("000214E6-0000-0000-C000-000000000046"); + // ReSharper restore InconsistentNaming [ComRegisterFunction] public static void Register(Type t) { @@ -82,50 +92,82 @@ public void SetSite(IntPtr pUnkSite) { if (Process.GetCurrentProcess().ProcessName != "explorer") { return; // ˵ie } - //if (Debugger.IsAttached) { - // Debugger.Break(); - //} else { - // Debugger.Launch(); - //} +#if DEBUG + if (Debugger.IsAttached) { + Debugger.Break(); + } else { + Debugger.Launch(); + } +#endif if (Marshal.QueryInterface(pUnkSite, ref IID_IWebBrowserApp, out var pExplorer) == 0) { // ļȻIE׿ǣ + this.pUnkSite = pUnkSite; explorer = (WebBrowserClass)Marshal.GetTypedObjectForIUnknown(pExplorer, typeof(WebBrowserClass)); - explorer.NavigateComplete += Explorer_OnNavigateComplete; + explorer.DocumentComplete += Explorer_OnDocumentComplete; + explorer.WindowStateChanged += ExplorerOnWindowStateChanged; } } - private void Explorer_OnNavigateComplete(string url) { - explorer.NavigateComplete -= Explorer_OnNavigateComplete; - if (string.IsNullOrWhiteSpace(url)) { + private void ExplorerOnWindowStateChanged(uint dwwindowstateflags, uint dwvalidflagsmask) { + + } + + private void Explorer_OnDocumentComplete(object pdisp, ref object url) { + explorer.DocumentComplete -= Explorer_OnDocumentComplete; + var urlStr = (string)url; + if (string.IsNullOrWhiteSpace(urlStr)) { OpenPathInExplorerEx(null); - } else if (url.Length == 40) { + } else if (urlStr.Length == 40) { // Shellλ - if (url[0] == ':' && url[1] == ':' && url[2] == '{' && url[39] == '}') { - if (Guid.TryParse(url.Substring(3, 36), out var clsId)) { + if (urlStr[0] == ':' && urlStr[1] == ':' && urlStr[2] == '{' && urlStr[39] == '}') { + if (Guid.TryParse(urlStr.Substring(3, 36), out var clsId)) { if (clsId == new Guid(0x20D04FE0, 0x3AEA, 0x1069, 0xA2, 0xD8, 0x08, 0x00, 0x2B, 0x30, 0x30, 0x9D) || - clsId == new Guid(0x5E5F29CE, 0xE0A8, 0x49D3, 0xAF, 0x32, 0x7A, 0x7B, 0xDC, 0x17, 0x34, 0x78)) { + clsId == new Guid(0x5E5F29CE, 0xE0A8, 0x49D3, 0xAF, 0x32, 0x7A, 0x7B, 0xDC, 0x17, 0x34, 0x78)) { // This PC OpenPathInExplorerEx(null); } } } - } else if (Directory.Exists(url)) { - OpenPathInExplorerEx(url); + } else if (Directory.Exists(urlStr)) { + OpenPathInExplorerEx(urlStr); + } + } + + private void Do() { + if (Marshal.QueryInterface(pUnkSite, ref IID_IServiceProvider, out var psp) != 0) { + return; + } + var sp = (IServiceProvider)Marshal.GetTypedObjectForIUnknown(psp, typeof(IServiceProvider)); + if (sp.QueryService(ref SID_STopLevelBrowser, ref IID_IShellBrowser, out var psb) != 0) { + Marshal.ReleaseComObject(sp); + return; + } + Marshal.ReleaseComObject(sp); + var sb = (IShellBrowser)psb; + if (sb.QueryActiveShellView(out var psv) != 0) { + Marshal.ReleaseComObject(sb); + return; } + Marshal.ReleaseComObject(sb); + Marshal.QueryInterface(psv, ref IID_IFolderView, out var pfv); + var fv = (IFolderView)Marshal.GetTypedObjectForIUnknown(pfv, typeof(IFolderView)); + fv.GetFolder(ref IID_IPersistFolder, out var ppf); + fv.GetFocusedItem(out var idxFocus); + fv.Item(idxFocus, out var pidlItem); + Marshal.QueryInterface(ppf, ref IID_IShellFolder, out var psf); + var sf = (IShellFolder)Marshal.GetTypedObjectForIUnknown(psf, typeof(IShellFolder)); + sf.GetDisplayNameOf(pidlItem, SHGDNF.SHGDN_INFOLDER, out var name); + var fileName = Marshal.PtrToStringUni(name.data); } private void OpenPathInExplorerEx(string path) { try { - var mutex = Mutex.OpenExisting("ExplorerExMut"); - try { - mutex.ReleaseMutex(); - } catch { - // Ignore - } - try { - mutex.Dispose(); - } catch { - // Ignore + using (var mutex = Mutex.OpenExisting("ExplorerExMut")) { + try { + mutex.ReleaseMutex(); + } catch { + // Ignore + } } // 򿪳ɹ˵ExplorerExIPC using (var ipc = new ExplorerIpc("ExplorerExIPC", 1024)) { @@ -138,7 +180,7 @@ private void OpenPathInExplorerEx(string path) { using (var key = Registry.CurrentUser.OpenSubKey(@"Software\Dear.Va\ExplorerEx", false)) { if (key?.GetValue("Path") is string explorerExPath && File.Exists(explorerExPath)) { try { - Process.Start(new ProcessStartInfo(explorerExPath, path)); + Process.Start(new ProcessStartInfo(explorerExPath, '"' + path + '"')); explorer.Quit(); } catch { // Ignore diff --git a/ExplorerProxy/ExplorerProxy.csproj b/ExplorerProxy/ExplorerProxy.csproj index 5f3d4a0..643435a 100644 --- a/ExplorerProxy/ExplorerProxy.csproj +++ b/ExplorerProxy/ExplorerProxy.csproj @@ -51,8 +51,18 @@ + + + + + + + + + + diff --git a/ExplorerProxy/Interop/IFolderView.cs b/ExplorerProxy/Interop/IFolderView.cs new file mode 100644 index 0000000..085d1ef --- /dev/null +++ b/ExplorerProxy/Interop/IFolderView.cs @@ -0,0 +1,105 @@ +using System; +using System.Runtime.InteropServices; + +namespace ExplorerProxy.Interop { + [ComImport, Guid("cde725b0-ccc9-4519-917e-325d72fab4ce")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface IFolderView { + /// + /// Gets an address containing a value representing the folder's current view mode. + /// + /// A pointer to a memory location at which to store the folder's current view mode. + /// TODO: Use FOLDERVIEWMODE + void GetCurrentViewMode(out uint pViewMode); + + /// + /// Sets the selected folder's view mode. + /// + /// One of the following values from the FOLDERVIEWMODE enumeration. + /// TODO: Use FOLDERVIEWMODE + void SetCurrentViewMode(uint ViewMode); + + /// + /// Gets the folder object. + /// + /// Reference to the desired IID to represent the folder. + /// When this method returns, contains the interface pointer requested in riid. This is typically IShellFolder or a related interface. This can also be an IShellItemArray with a single element. + void GetFolder(ref Guid riid, out IntPtr ppv); + + /// + /// Gets the identifier of a specific item in the folder view, by index. + /// + /// The index of the item in the view. + /// The address of a pointer to a PIDL containing the item's identifier information. + void Item(int iItemIndex, out IntPtr ppidl); + + /// + /// Gets the number of items in the folder. This can be the number of all items, or a subset such as the number of selected items. + /// + /// Flags from the _SVGIO enumeration that limit the count to certain types of items. + /// Pointer to an integer that receives the number of items (files and folders) displayed in the folder view. + void ItemCount(uint uFlags, out int pcItems); + + /// + /// Gets the address of an enumeration object based on the collection of items in the folder view. + /// + /// _SVGIO values that limit the enumeration to certain types of items. + /// Reference to the desired IID to represent the folder. + /// When this method returns, contains the interface pointer requested in riid. This is typically an IEnumIDList, IDataObject, or IShellItemArray. If an error occurs, this value is NULL. + void Items(uint uFlags, ref Guid riid, ref IntPtr ppv); + + /// + /// Gets the index of an item in the folder's view which has been marked by using the SVSI_SELECTIONMARK in IFolderView::SelectItem. + /// + /// A pointer to the index of the marked item. + void GetSelectionMarkedItem(out int piItem); + + /// + /// Gets the index of the item that currently has focus in the folder's view. + /// + /// A pointer to the index of the item. + void GetFocusedItem(out int piItem); + + /// + /// Gets the position of an item in the folder's view. + /// + /// A pointer to an ITEMIDLIST interface. + /// A pointer to a structure that receives the position of the item's upper-left corner. + void GetItemPosition(IntPtr pidl, out POINT ppt); + + /// + /// Gets a POINT structure containing the width (x) and height (y) dimensions, including the surrounding white space, of an item. + /// + /// A pointer to an existing structure to be filled with the current sizing dimensions of the items in the folder's view. + void GetSpacing(out POINT ppt); + + /// + /// Gets a pointer to a POINT structure containing the default width (x) and height (y) measurements of an item, including the surrounding white space. + /// + /// Pointer to an existing structure to be filled with the default sizing dimensions of the items in the folder's view. + void GetDefaultSpacing(out POINT ppt); + + /// + /// Gets the current state of the folder's Auto Arrange mode. + /// + /// Returns S_OK if the folder is in Auto Arrange mode; S_FALSE if it is not. + [PreserveSig] + int GetAutoArrange(); + + /// + /// Selects an item in the folder's view. + /// + /// The index of the item to select in the folder's view. + /// One of the _SVSIF constants that specify the type of selection to apply. + void SelectItem(int iItem, uint dwFlags); + + /// + /// Allows the selection and positioning of items visible in the folder's view. + /// + /// The number of items to select. + /// A pointer to an array of size cidl that contains the PIDLs of the items. + /// A pointer to an array of cidl structures containing the locations each corresponding element in apidl should be positioned. + /// One of the _SVSIF constants that specifies the type of selection to apply. + void SelectAndPositionItems(uint cidl, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] IntPtr[] apidl, ref POINT apt, uint dwFlags); + } +} diff --git a/ExplorerProxy/Interop/IPersist.cs b/ExplorerProxy/Interop/IPersist.cs new file mode 100644 index 0000000..558ddfb --- /dev/null +++ b/ExplorerProxy/Interop/IPersist.cs @@ -0,0 +1,22 @@ +using System; +using System.Runtime.InteropServices; + +namespace ExplorerProxy.Interop { + /// + /// Provides the CLSID of an object that can be stored persistently in the system. + /// Allows the object to specify which object handler to use in the client process, as it is used in the default implementation of marshaling. + /// + [ComImport] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("0000010c-0000-0000-c000-000000000046")] + internal interface IPersist { + /// + /// Retrieves the class identifier (CLSID) of the object. + /// + /// A pointer to the location that receives the CLSID on return. + /// The CLSID is a globally unique identifier (GUID) that uniquely represents an object class that defines the code that can manipulate the object's data. + /// If the method succeeds, the return value is S_OK. Otherwise, it is E_FAIL. + [PreserveSig] + int GetClassID(out Guid pClassID); + } +} diff --git a/ExplorerProxy/Interop/IPersistFolder.cs b/ExplorerProxy/Interop/IPersistFolder.cs new file mode 100644 index 0000000..f165868 --- /dev/null +++ b/ExplorerProxy/Interop/IPersistFolder.cs @@ -0,0 +1,34 @@ +using System; +using System.Runtime.InteropServices; + +namespace ExplorerProxy.Interop { + /// + /// Exposes a method that initializes Shell folder objects. + /// + [ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("000214EA-0000-0000-C000-000000000046")] + internal interface IPersistFolder : IPersist { + #region Overrides for C#/COM compatibility. + + /// + /// Retrieves the class identifier (CLSID) of the object. + /// + /// A pointer to the location that receives the CLSID on return. + /// The CLSID is a globally unique identifier (GUID) that uniquely represents an object class that defines the code that can manipulate the object's data. + /// + /// If the method succeeds, the return value is S_OK. Otherwise, it is E_FAIL. + /// + [PreserveSig] + new int GetClassID(out Guid pClassID); + + #endregion + + /// + /// Instructs a Shell folder object to initialize itself based on the information passed. + /// + /// The address of the ITEMIDLIST (item identifier list) structure that specifies the absolute location of the folder. + /// If this method succeeds, it returns S_OK. Otherwise, it returns an HRESULT error code. + [PreserveSig] + int Initialize(IntPtr pidl); + } +} diff --git a/ExplorerProxy/Interop/IPersistFolder2.cs b/ExplorerProxy/Interop/IPersistFolder2.cs new file mode 100644 index 0000000..7f91451 --- /dev/null +++ b/ExplorerProxy/Interop/IPersistFolder2.cs @@ -0,0 +1,45 @@ +using System; +using System.Runtime.InteropServices; + +namespace ExplorerProxy.Interop { + /// + /// Exposes methods that obtain information from Shell folder objects. + /// + [ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("1AC3D9F0-175C-11d1-95BE-00609797EA4F")] + internal interface IPersistFolder2 : IPersistFolder { + #region Overrides for C#/COM compatibility. + + /// + /// Retrieves the class identifier (CLSID) of the object. + /// + /// A pointer to the location that receives the CLSID on return. + /// The CLSID is a globally unique identifier (GUID) that uniquely represents an object class that defines the code that can manipulate the object's data. + /// + /// If the method succeeds, the return value is S_OK. Otherwise, it is E_FAIL. + /// + [PreserveSig] + new int GetClassID(out Guid pClassID); + + /// + /// Instructs a Shell folder object to initialize itself based on the information passed. + /// + /// The address of the ITEMIDLIST (item identifier list) structure that specifies the absolute location of the folder. + /// + /// If this method succeeds, it returns S_OK. Otherwise, it returns an HRESULT error code. + /// + [PreserveSig] + new int Initialize(IntPtr pidl); + + #endregion + + /// + /// Gets the ITEMIDLIST for the folder object. + /// + /// The address of an ITEMIDLIST pointer. This PIDL represents the absolute location of the folder and must be relative to the desktop. This is typically a copy of the PIDL passed to Initialize. + /// If this method succeeds, it returns S_OK. Otherwise, it returns an HRESULT error code. + /// If the folder object has not been initialized, this method returns S_FALSE and ppidl is set to NULL. + [PreserveSig] + int GetCurFolder(out IntPtr ppidl); + } +} diff --git a/ExplorerProxy/Interop/IServiceProvider.cs b/ExplorerProxy/Interop/IServiceProvider.cs new file mode 100644 index 0000000..2e3c2f3 --- /dev/null +++ b/ExplorerProxy/Interop/IServiceProvider.cs @@ -0,0 +1,11 @@ +using System; +using System.Runtime.InteropServices; + +namespace ExplorerProxy.Interop { + [ComImport] + [Guid("6d5140c1-7436-11ce-8034-00aa006009fa")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface IServiceProvider { + int QueryService(ref Guid guidService, ref Guid riid, [MarshalAs(UnmanagedType.IUnknown)] out object ppvObject); + } +} diff --git a/ExplorerProxy/Interop/IShellBrowser.cs b/ExplorerProxy/Interop/IShellBrowser.cs new file mode 100644 index 0000000..8cc45a3 --- /dev/null +++ b/ExplorerProxy/Interop/IShellBrowser.cs @@ -0,0 +1,29 @@ +using System; +using System.Runtime.InteropServices; + +namespace ExplorerProxy.Interop { + [ComImport] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("000214E2-0000-0000-C000-000000000046")] + public interface IShellBrowser { + void GetWindow(out IntPtr phwnd); + void ContextSensitiveHelp(bool fEnterMode); + + void InsertMenusSB(IntPtr IntPtrShared, IntPtr lpMenuWidths); + + void SetMenuSB(IntPtr IntPtrShared, IntPtr holemenuRes, IntPtr IntPtrActiveObject); + + void RemoveMenusSB(IntPtr IntPtrShared); + void SetStatusTextSB(IntPtr pszStatusText); + void EnableModelessSB(bool fEnable); + void TranslateAcceleratorSB(IntPtr pmsg, ushort wID); + + void BrowseObject(IntPtr pidl, uint wFlags); + void GetViewStateStream(uint grfMode, IntPtr ppStrm); + void GetControlWindow(uint id, out IntPtr lpIntPtr); + void SendControlMsg(uint id, uint uMsg, uint wParam, uint lParam, IntPtr pret); + int QueryActiveShellView(out IntPtr ppshv); + void OnViewWindowActive(IntPtr ppshv); + void SetToolbarItems(IntPtr lpButtons, uint nButtons, uint uFlags); + } +} \ No newline at end of file diff --git a/ExplorerProxy/Interop/IShellFolder.cs b/ExplorerProxy/Interop/IShellFolder.cs new file mode 100644 index 0000000..1be123e --- /dev/null +++ b/ExplorerProxy/Interop/IShellFolder.cs @@ -0,0 +1,146 @@ +using System; +using System.Runtime.InteropServices; + +namespace ExplorerProxy.Interop { + /// + /// Exposed by all Shell namespace folder objects, its methods are used to manage folders. + /// + [ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("000214E6-0000-0000-C000-000000000046")] + internal interface IShellFolder { + /// + /// Translates the display name of a file object or a folder into an item identifier list. + /// + /// A window handle. The client should provide a window handle if it displays a dialog or message box. Otherwise set hwnd to NULL. + /// Optional. A pointer to a bind context used to pass parameters as inputs and outputs to the parsing function. + /// A null-terminated Unicode string with the display name. + /// A pointer to a ULONG value that receives the number of characters of the display name that was parsed. If your application does not need this information, set pchEaten to NULL, and no value will be returned. + /// When this method returns, contains a pointer to the PIDL for the object. + /// The value used to query for file attributes. If not used, it should be set to NULL. + /// If this method succeeds, it returns S_OK. Otherwise, it returns an HRESULT error code. + [PreserveSig] + int ParseDisplayName(IntPtr hwnd, IntPtr pbc, string pszDisplayName, ref uint pchEaten, out IntPtr ppidl, IntPtr pdwAttributes); + + + /// + ///Allows a client to determine the contents of a folder by creating an item identifier enumeration object and returning its IEnumIDList interface. + ///Return value: error code, if any + /// + /// If user input is required to perform the enumeration, this window handle should be used by the enumeration object as the parent window to take user input. + /// Flags indicating which items to include in the enumeration. For a list of possible values, see the SHCONTF enum. + /// Address that receives a pointer to the IEnumIDList interface of the enumeration object created by this method. + /// If this method succeeds, it returns S_OK. Otherwise, it returns an HRESULT error code. + [PreserveSig] + int EnumObjects(IntPtr hwnd, int grfFlags, IntPtr ppenumIDList); + + /// + /// Retrieves an IShellFolder object for a subfolder. + /// Return value: error code, if any + /// + /// Address of an ITEMIDLIST structure (PIDL) that identifies the subfolder. + /// Optional address of an IBindCtx interface on a bind context object to be used during this operation. + /// Identifier of the interface to return. + /// Address that receives the interface pointer. + /// If this method succeeds, it returns S_OK. Otherwise, it returns an HRESULT error code. + [PreserveSig] + int BindToObject(IntPtr pidl, IntPtr pbc, [In] ref Guid riid, out IntPtr ppv); + + /// + /// Requests a pointer to an object's storage interface. + /// Return value: error code, if any + /// + /// Address of an ITEMIDLIST structure that identifies the subfolder relative to its parent folder. + /// Optional address of an IBindCtx interface on a bind context object to be used during this operation. + /// Interface identifier (IID) of the requested storage interface. + /// Address that receives the interface pointer specified by riid. + /// If this method succeeds, it returns S_OK. Otherwise, it returns an HRESULT error code. + [PreserveSig] + int BindToStorage(IntPtr pidl, IntPtr pbc, [In] ref Guid riid, out IntPtr ppv); + + /// + /// Determines the relative order of two file objects or folders, given + /// their item identifier lists. Return value: If this method is + /// successful, the CODE field of the HRESULT contains one of the + /// following values (the code can be retrived using the helper function + /// GetHResultCode): Negative A negative return value indicates that the first item should precede the second (pidl1 < pidl2). + ///Positive A positive return value indicates that the first item should + ///follow the second (pidl1 > pidl2). Zero A return value of zero + ///indicates that the two items are the same (pidl1 = pidl2). + /// + /// Value that specifies how the comparison should be performed. The lower Sixteen bits of lParam define the sorting rule. + /// The upper sixteen bits of lParam are used for flags that modify the sorting rule. values can be from the SHCIDS enum + /// + /// Pointer to the first item's ITEMIDLIST structure. + /// Pointer to the second item's ITEMIDLIST structure. + /// + /// If this method succeeds, it returns S_OK. Otherwise, it returns an HRESULT error code. + [PreserveSig] + int CompareIDs(IntPtr lParam, IntPtr pidl1, IntPtr pidl2); + + /// + /// Requests an object that can be used to obtain information from or interact + /// with a folder object. + /// Return value: error code, if any + /// + /// Handle to the owner window. + /// Identifier of the requested interface. + /// Address of a pointer to the requested interface. + /// If this method succeeds, it returns S_OK. Otherwise, it returns an HRESULT error code. + [PreserveSig] + int CreateViewObject(IntPtr hwndOwner, [In] ref Guid riid, out IntPtr ppv); + + /// + /// Retrieves the attributes of one or more file objects or subfolders. + /// Return value: error code, if any + /// + /// Number of file objects from which to retrieve attributes. + /// Address of an array of pointers to ITEMIDLIST structures, each of which uniquely identifies a file object relative to the parent folder. + /// Address of a single ULONG value that, on entry contains the attributes that the caller is + /// requesting. On exit, this value contains the requested attributes that are common to all of the specified objects. this value can be from the SFGAO enum + /// + /// If this method succeeds, it returns S_OK. Otherwise, it returns an HRESULT error code. + [PreserveSig] + int GetAttributesOf(UInt32 cidl, IntPtr apidl, IntPtr rgfInOut); + + /// + /// Retrieves an OLE interface that can be used to carry out actions on the + /// specified file objects or folders. Return value: error code, if any + /// + /// Handle to the owner window that the client should specify if it displays a dialog box or message box. + /// Number of file objects or subfolders specified in the apidl parameter. + /// Address of an array of pointers to ITEMIDLIST structures, each of which uniquely identifies a file object or subfolder relative to the parent folder. + /// Identifier of the COM interface object to return. + /// Reserved. + /// Pointer to the requested interface. + /// If this method succeeds, it returns S_OK. Otherwise, it returns an HRESULT error code. + [PreserveSig] + int GetUIObjectOf(IntPtr hwndOwner, UInt32 cidl, + IntPtr apidl, [In] ref Guid riid, + UInt32 rgfReserved, out IntPtr ppv); + + /// + /// Retrieves the display name for the specified file object or subfolder. + /// Return value: error code, if any + /// + /// Address of an ITEMIDLIST structure (PIDL) that uniquely identifies the file object or subfolder relative to the parent folder. + /// Flags used to request the type of display name to return. For a list of possible values. + /// Address of a STRRET structure in which to return the display name. + /// If this method succeeds, it returns S_OK. Otherwise, it returns an HRESULT error code. + [PreserveSig] + int GetDisplayNameOf(IntPtr pidl, SHGDNF uFlags, out STRRET pName); + + /// + /// Sets the display name of a file object or subfolder, changing the item + /// identifier in the process. + /// Return value: error code, if any + /// + /// Handle to the owner window of any dialog or message boxes that the client displays. + /// Pointer to an ITEMIDLIST structure that uniquely identifies the file object or subfolder relative to the parent folder. + /// Pointer to a null-terminated string that specifies the new display name. + /// Flags indicating the type of name specified by the lpszName parameter. For a list of possible values, see the description of the SHGNO enum. + /// + /// If this method succeeds, it returns S_OK. Otherwise, it returns an HRESULT error code. + [PreserveSig] + int SetNameOf(IntPtr hwnd, IntPtr pidl, String pszName, SHGDNF uFlags, out IntPtr ppidlOut); + } +} diff --git a/ExplorerProxy/Interop/POINT.cs b/ExplorerProxy/Interop/POINT.cs new file mode 100644 index 0000000..8f7c7dd --- /dev/null +++ b/ExplorerProxy/Interop/POINT.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace ExplorerProxy.Interop { + [StructLayout(LayoutKind.Sequential)] + internal struct POINT { + public int x; + public int y; + } +} diff --git a/ExplorerProxy/Interop/SHGDNF.cs b/ExplorerProxy/Interop/SHGDNF.cs new file mode 100644 index 0000000..e8ef5fd --- /dev/null +++ b/ExplorerProxy/Interop/SHGDNF.cs @@ -0,0 +1,36 @@ +using System; + +namespace ExplorerProxy.Interop { + // ReSharper disable InconsistentNaming + + /// + /// Defines the values used with the IShellFolder::GetDisplayNameOf and IShellFolder::SetNameOf methods to specify the type of file or folder names used by those methods. + /// + [Flags] + internal enum SHGDNF { + /// + /// When not combined with another flag, return the parent-relative name that identifies the item, suitable for displaying to the user. This name often does not include extra information such as the file name extension and does not need to be unique. This name might include information that identifies the folder that contains the item. For instance, this flag could cause IShellFolder::GetDisplayNameOf to return the string "username (on Machine)" for a particular user's folder. + /// + SHGDN_NORMAL = 0x0000, + + /// + /// The name is relative to the folder from which the request was made. This is the name display to the user when used in the context of the folder. For example, it is used in the view and in the address bar path segment for the folder. This name should not include disambiguation information—for instance "username" instead of "username (on Machine)" for a particular user's folder. + /// Use this flag in combinations with SHGDN_FORPARSING and SHGDN_FOREDITING. + /// + SHGDN_INFOLDER = 0x0001, + /// + /// The name is used for in-place editing when the user renames the item. + /// + SHGDN_FOREDITING = 0x1000, + /// + /// The name is displayed in an address bar combo box. + /// + SHGDN_FORADDRESSBAR = 0x4000, + /// + /// The name is used for parsing. That is, it can be passed to IShellFolder::ParseDisplayName to recover the object's PIDL. The form this name takes depends on the particular object. When SHGDN_FORPARSING is used alone, the name is relative to the desktop. When combined with SHGDN_INFOLDER, the name is relative to the folder from which the request was made. + /// + SHGDN_FORPARSING = 0x8000, + } + + // ReSharper restore InconsistentNaming +} diff --git a/ExplorerProxy/Interop/STRRET.cs b/ExplorerProxy/Interop/STRRET.cs new file mode 100644 index 0000000..314914e --- /dev/null +++ b/ExplorerProxy/Interop/STRRET.cs @@ -0,0 +1,64 @@ +using System; +using System.Runtime.InteropServices; + +namespace ExplorerProxy.Interop { + // ReSharper disable InconsistentNaming + + /// + /// Contains strings returned from the IShellFolder interface methods. + /// + [StructLayout(LayoutKind.Sequential, Size = 272)] + internal struct STRRET { + /// + /// Gets the actual string value of a STRRET. + /// + /// The string represented by the STRRET. + /// + /// + /// + public string GetStringValue() { + switch (uType) { + case STRRETTYPE.STRRET_WSTR: + return Marshal.PtrToStringUni(data); + case STRRETTYPE.STRRET_OFFSET: + throw new NotImplementedException(); + case STRRETTYPE.STRRET_CSTR: + return Marshal.PtrToStringAnsi(data); + default: + throw new ArgumentOutOfRangeException(); + } + } + + /// + /// A value that specifies the desired format of the string. This can be one of the following values. + /// + public enum STRRETTYPE { + /// + /// The string is at the address specified by pOleStr member. + /// + STRRET_WSTR = 0x0000, + + /// + /// The uOffset member value indicates the number of bytes from the beginning of the item identifier list where the string is located. + /// + STRRET_OFFSET = 0x0001, + + /// + /// The string is returned in the cStr member. + /// + STRRET_CSTR = 0x0002 + } + + /// + /// A value that specifies the desired format of the string. + /// + public STRRETTYPE uType; + + /// + /// The string data. + /// + public IntPtr data; + } + + // ReSharper restore InconsistentNaming +} diff --git a/External/HandyControl/Themes/Styles/Base/ProgressBarBaseStyle.xaml b/External/HandyControl/Themes/Styles/Base/ProgressBarBaseStyle.xaml index 3064c51..aeabb1f 100644 --- a/External/HandyControl/Themes/Styles/Base/ProgressBarBaseStyle.xaml +++ b/External/HandyControl/Themes/Styles/Base/ProgressBarBaseStyle.xaml @@ -15,48 +15,42 @@ - - + + - - - - - + + + + - - - + + + - - - + + + - + - - - + + - - - - - + - + @@ -66,20 +60,19 @@ - + - + - + - - - + + diff --git a/External/HandyControl/Themes/Theme.xaml b/External/HandyControl/Themes/Theme.xaml index 1a2ec5c..f257bd1 100644 --- a/External/HandyControl/Themes/Theme.xaml +++ b/External/HandyControl/Themes/Theme.xaml @@ -2482,10 +2482,9 @@ - - - - + + + @@ -2504,17 +2503,12 @@ - + - - + - - - - - + @@ -2540,7 +2534,6 @@ - diff --git a/External/VirtualizingWrapPanel/VirtualizingPanelBase.cs b/External/VirtualizingWrapPanel/VirtualizingPanelBase.cs index c3783c8..0c9669e 100644 --- a/External/VirtualizingWrapPanel/VirtualizingPanelBase.cs +++ b/External/VirtualizingWrapPanel/VirtualizingPanelBase.cs @@ -7,474 +7,442 @@ using System.Windows.Controls.Primitives; using System.Windows.Media; -namespace WpfToolkit.Controls -{ - /// - /// Base class for panels which are supporting virtualization. - /// - public abstract class VirtualizingPanelBase : VirtualizingPanel, IScrollInfo - { - public static readonly DependencyProperty ScrollLineDeltaProperty = DependencyProperty.Register(nameof(ScrollLineDelta), typeof(double), typeof(VirtualizingPanelBase), new FrameworkPropertyMetadata(16.0)); - public static readonly DependencyProperty MouseWheelDeltaProperty = DependencyProperty.Register(nameof(MouseWheelDelta), typeof(double), typeof(VirtualizingPanelBase), new FrameworkPropertyMetadata(48.0)); - public static readonly DependencyProperty ScrollLineDeltaItemProperty = DependencyProperty.Register(nameof(ScrollLineDeltaItem), typeof(int), typeof(VirtualizingPanelBase), new FrameworkPropertyMetadata(1)); - public static readonly DependencyProperty MouseWheelDeltaItemProperty = DependencyProperty.Register(nameof(MouseWheelDeltaItem), typeof(int), typeof(VirtualizingPanelBase), new FrameworkPropertyMetadata(3)); - - public ScrollViewer? ScrollOwner { get; set; } - - public bool CanVerticallyScroll { get; set; } - public bool CanHorizontallyScroll { get; set; } - - protected override bool CanHierarchicallyScrollAndVirtualizeCore => true; - - /// - /// Scroll line delta for pixel based scrolling. The default value is 16 dp. - /// - public double ScrollLineDelta { get => (double)GetValue(ScrollLineDeltaProperty); set => SetValue(ScrollLineDeltaProperty, value); } - - /// - /// Mouse wheel delta for pixel based scrolling. The default value is 48 dp. - /// - public double MouseWheelDelta { get => (double)GetValue(MouseWheelDeltaProperty); set => SetValue(MouseWheelDeltaProperty, value); } - - /// - /// Scroll line delta for item based scrolling. The default value is 1 item. - /// - public double ScrollLineDeltaItem { get => (int)GetValue(ScrollLineDeltaItemProperty); set => SetValue(ScrollLineDeltaItemProperty, value); } - - /// - /// Mouse wheel delta for item based scrolling. The default value is 3 items. - /// - public int MouseWheelDeltaItem { get => (int)GetValue(MouseWheelDeltaItemProperty); set => SetValue(MouseWheelDeltaItemProperty, value); } - - protected ScrollUnit ScrollUnit => GetScrollUnit(ItemsControl); - - /// - /// The direction in which the panel scrolls when user turns the mouse wheel. - /// - protected ScrollDirection MouseWheelScrollDirection { get; set; } = ScrollDirection.Vertical; - - - protected bool IsVirtualizing => GetIsVirtualizing(ItemsControl); - - protected VirtualizationMode VirtualizationMode => GetVirtualizationMode(ItemsControl); - - /// - /// Returns true if the panel is in VirtualizationMode.Recycling, otherwise false. - /// - protected bool IsRecycling => VirtualizationMode == VirtualizationMode.Recycling; - - /// - /// The cache length before and after the viewport. - /// - protected VirtualizationCacheLength CacheLength { get; private set; } - - /// - /// The Unit of the cache length. Can be Pixel, Item or Page. - /// When the ItemsOwner is a group item it can only be pixel or item. - /// - protected VirtualizationCacheLengthUnit CacheLengthUnit { get; private set; } - - - /// - /// The ItemsControl (e.g. ListView). - /// - protected ItemsControl ItemsControl => ItemsControl.GetItemsOwner(this); - - /// - /// The ItemsControl (e.g. ListView) or if the ItemsControl is grouping a GroupItem. - /// - protected DependencyObject ItemsOwner - { - get - { - if (_itemsOwner is null) - { - /* Use reflection to access internal method because the public - * GetItemsOwner method does always return the itmes control instead - * of the real items owner for example the group item when grouping */ - MethodInfo getItemsOwnerInternalMethod = typeof(ItemsControl).GetMethod( - "GetItemsOwnerInternal", - BindingFlags.Static | BindingFlags.NonPublic, - null, - new Type[] { typeof(DependencyObject) }, - null - )!; - _itemsOwner = (DependencyObject)getItemsOwnerInternalMethod.Invoke(null, new object[] { this })!; - } - return _itemsOwner; - } - } - private DependencyObject? _itemsOwner; - - protected ReadOnlyCollection Items => ((ItemContainerGenerator)ItemContainerGenerator).Items; - - protected new IRecyclingItemContainerGenerator ItemContainerGenerator - { - get - { - if (_itemContainerGenerator is null) - { - /* Because of a bug in the framework the ItemContainerGenerator - * is null until InternalChildren accessed at least one time. */ - var children = InternalChildren; - _itemContainerGenerator = (IRecyclingItemContainerGenerator)base.ItemContainerGenerator; - } - return _itemContainerGenerator; - } - } - private IRecyclingItemContainerGenerator? _itemContainerGenerator; - - public double ExtentWidth => Extent.Width; - public double ExtentHeight => Extent.Height; - protected Size Extent { get; private set; } = new Size(0, 0); - - public double HorizontalOffset => Offset.X; - public double VerticalOffset => Offset.Y; - protected Size Viewport { get; private set; } = new Size(0, 0); - - public double ViewportWidth => Viewport.Width; - public double ViewportHeight => Viewport.Height; - protected Point Offset { get; private set; } = new Point(0, 0); - - /// - /// The range of items that a realized in viewport or cache. - /// - protected ItemRange ItemRange { get; set; } - - private Visibility previousVerticalScrollBarVisibility = Visibility.Collapsed; - private Visibility previousHorizontalScrollBarVisibility = Visibility.Collapsed; - - protected virtual void UpdateScrollInfo(Size availableSize, Size extent) - { - bool invalidateScrollInfo = false; - - if (extent != Extent) - { - Extent = extent; - invalidateScrollInfo = true; - - } - if (availableSize != Viewport) - { - Viewport = availableSize; - invalidateScrollInfo = true; - } - - if (ViewportHeight != 0 && VerticalOffset != 0 && VerticalOffset + ViewportHeight + 1 >= ExtentHeight) - { - Offset = new Point(Offset.X, extent.Height - availableSize.Height); - invalidateScrollInfo = true; - } - if (ViewportWidth != 0 && HorizontalOffset != 0 && HorizontalOffset + ViewportWidth + 1 >= ExtentWidth) - { - Offset = new Point(extent.Width - availableSize.Width, Offset.Y); - invalidateScrollInfo = true; - } - - if (invalidateScrollInfo) - { - ScrollOwner?.InvalidateScrollInfo(); - } - } - - public virtual Rect MakeVisible(Visual visual, Rect rectangle) - { - Point pos = visual.TransformToAncestor(this).Transform(Offset); - - double scrollAmountX = 0; - double scrollAmountY = 0; - - if (pos.X < Offset.X) - { - scrollAmountX = -(Offset.X - pos.X); - } - else if ((pos.X + rectangle.Width) > (Offset.X + Viewport.Width)) - { - double notVisibleX = (pos.X + rectangle.Width) - (Offset.X + Viewport.Width); - double maxScrollX = pos.X - Offset.X; // keep left of the visual visible - scrollAmountX = Math.Min(notVisibleX, maxScrollX); - } - - if (pos.Y < Offset.Y) - { - scrollAmountY = -(Offset.Y - pos.Y); - } - else if ((pos.Y + rectangle.Height) > (Offset.Y + Viewport.Height)) - { - double notVisibleY = (pos.Y + rectangle.Height) - (Offset.Y + Viewport.Height); - double maxScrollY = pos.Y - Offset.Y; // keep top of the visual visible - scrollAmountY = Math.Min(notVisibleY, maxScrollY); - } - - SetHorizontalOffset(Offset.X + scrollAmountX); - SetVerticalOffset(Offset.Y + scrollAmountY); - - double visibleRectWidth = Math.Min(rectangle.Width, Viewport.Width); - double visibleRectHeight = Math.Min(rectangle.Height, Viewport.Height); - - return new Rect(scrollAmountX, scrollAmountY, visibleRectWidth, visibleRectHeight); - } - - protected override void OnItemsChanged(object sender, ItemsChangedEventArgs args) - { - switch (args.Action) - { - case NotifyCollectionChangedAction.Remove: - case NotifyCollectionChangedAction.Replace: - RemoveInternalChildRange(args.Position.Index, args.ItemUICount); - break; - case NotifyCollectionChangedAction.Move: - RemoveInternalChildRange(args.OldPosition.Index, args.ItemUICount); - break; - } - } - - protected int GetItemIndexFromChildIndex(int childIndex) - { - var generatorPosition = GetGeneratorPositionFromChildIndex(childIndex); - return ItemContainerGenerator.IndexFromGeneratorPosition(generatorPosition); - } - - protected virtual GeneratorPosition GetGeneratorPositionFromChildIndex(int childIndex) - { - return new GeneratorPosition(childIndex, 0); - } - - protected override Size MeasureOverride(Size availableSize) - { - /* Sometimes when scrolling the scrollbar gets hidden without any reason. In this case the "IsMeasureValid" - * property of the ScrollOwner is false. To prevent a infinite circle the mesasure call is ignored. */ - if (ScrollOwner != null) - { - bool verticalScrollBarGotHidden = ScrollOwner.VerticalScrollBarVisibility == ScrollBarVisibility.Auto - && ScrollOwner.ComputedVerticalScrollBarVisibility != Visibility.Visible - && ScrollOwner.ComputedVerticalScrollBarVisibility != previousVerticalScrollBarVisibility; - - bool horizontalScrollBarGotHidden = ScrollOwner.HorizontalScrollBarVisibility == ScrollBarVisibility.Auto - && ScrollOwner.ComputedHorizontalScrollBarVisibility != Visibility.Visible - && ScrollOwner.ComputedHorizontalScrollBarVisibility != previousHorizontalScrollBarVisibility; - - previousVerticalScrollBarVisibility = ScrollOwner.ComputedVerticalScrollBarVisibility; - previousHorizontalScrollBarVisibility = ScrollOwner.ComputedHorizontalScrollBarVisibility; - - if (!ScrollOwner.IsMeasureValid && verticalScrollBarGotHidden || horizontalScrollBarGotHidden) - { - return availableSize; - } - } - - var groupItem = ItemsOwner as IHierarchicalVirtualizationAndScrollInfo; - - Size extent; - Size desiredSize; - - if (groupItem != null) - { - /* If the ItemsOwner is a group item the availableSize is ifinity. - * Therfore the vieport size provided by the group item is used. */ - var viewportSize = groupItem.Constraints.Viewport.Size; - var headerSize = groupItem.HeaderDesiredSizes.PixelSize; - double availableWidth = Math.Max(viewportSize.Width - 5, 0); // left margin of 5 dp - double availableHeight = Math.Max(viewportSize.Height - headerSize.Height, 0); - availableSize = new Size(availableWidth, availableHeight); - - extent = CalculateExtent(availableSize); - - desiredSize = new Size(extent.Width, extent.Height); - - Extent = extent; - Offset = groupItem.Constraints.Viewport.Location; - Viewport = groupItem.Constraints.Viewport.Size; - CacheLength = groupItem.Constraints.CacheLength; - CacheLengthUnit = groupItem.Constraints.CacheLengthUnit; // can be Item or Pixel - } - else - { - extent = CalculateExtent(availableSize); - double desiredWidth = Math.Min(availableSize.Width, extent.Width); - double desiredHeight = Math.Min(availableSize.Height, extent.Height); - desiredSize = new Size(desiredWidth, desiredHeight); - - UpdateScrollInfo(desiredSize, extent); - CacheLength = GetCacheLength(ItemsOwner); - CacheLengthUnit = GetCacheLengthUnit(ItemsOwner); // can be Page, Item or Pixel - } - - ItemRange = UpdateItemRange(); - - RealizeItems(); - VirtualizeItems(); - - return desiredSize; - } - - /// - /// Realizes visible and cached items. - /// - protected virtual void RealizeItems() - { - var startPosition = ItemContainerGenerator.GeneratorPositionFromIndex(ItemRange.StartIndex); - - int childIndex = startPosition.Offset == 0 ? startPosition.Index : startPosition.Index + 1; - - using (ItemContainerGenerator.StartAt(startPosition, GeneratorDirection.Forward, true)) - { - for (int i = ItemRange.StartIndex; i <= ItemRange.EndIndex; i++, childIndex++) - { - UIElement child = (UIElement)ItemContainerGenerator.GenerateNext(out bool isNewlyRealized); - if (isNewlyRealized || /*recycled*/!InternalChildren.Contains(child)) - { - if (childIndex >= InternalChildren.Count) - { - AddInternalChild(child); - } - else - { - InsertInternalChild(childIndex, child); - } - ItemContainerGenerator.PrepareItemContainer(child); - - child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); - } - - if (child is IHierarchicalVirtualizationAndScrollInfo groupItem) - { - groupItem.Constraints = new HierarchicalVirtualizationConstraints( - new VirtualizationCacheLength(0), - VirtualizationCacheLengthUnit.Item, - new Rect(0, 0, ViewportWidth, ViewportHeight)); - child.Measure(new Size(ViewportWidth, ViewportHeight)); - } - } - } - } - - /// - /// Virtualizes (cleanups) no longer visible or cached items. - /// - protected virtual void VirtualizeItems() - { - for (int childIndex = InternalChildren.Count - 1; childIndex >= 0; childIndex--) - { - var generatorPosition = GetGeneratorPositionFromChildIndex(childIndex); - - int itemIndex = ItemContainerGenerator.IndexFromGeneratorPosition(generatorPosition); - - if (itemIndex != -1 && !ItemRange.Contains(itemIndex)) - { - if (VirtualizationMode == VirtualizationMode.Recycling) - { - ItemContainerGenerator.Recycle(generatorPosition, 1); - } - else - { - ItemContainerGenerator.Remove(generatorPosition, 1); - } - RemoveInternalChildRange(childIndex, 1); - } - } - } - - /// - /// Calculates the extent that would be needed to show all items. - /// - protected abstract Size CalculateExtent(Size availableSize); - - /// - /// Calculates the item range that is visible in the viewport or cached. - /// - protected abstract ItemRange UpdateItemRange(); - - public void SetVerticalOffset(double offset) - { - if (offset < 0 || Viewport.Height >= Extent.Height) - { - offset = 0; - } - else if (offset + Viewport.Height >= Extent.Height) - { - offset = Extent.Height - Viewport.Height; - } - Offset = new Point(Offset.X, offset); - ScrollOwner?.InvalidateScrollInfo(); - InvalidateMeasure(); - } - - public void SetHorizontalOffset(double offset) - { - if (offset < 0 || Viewport.Width >= Extent.Width) - { - offset = 0; - } - else if (offset + Viewport.Width >= Extent.Width) - { - offset = Extent.Width - Viewport.Width; - } - Offset = new Point(offset, Offset.Y); - ScrollOwner?.InvalidateScrollInfo(); - InvalidateMeasure(); - } - - protected void ScrollVertical(double amount) - { - SetVerticalOffset(VerticalOffset + amount); - } - - protected void ScrollHorizontal(double amount) - { - SetHorizontalOffset(HorizontalOffset + amount); - } - - public void LineUp() => ScrollVertical(ScrollUnit == ScrollUnit.Pixel ? -ScrollLineDelta : GetLineUpScrollAmount()); - public void LineDown() => ScrollVertical(ScrollUnit == ScrollUnit.Pixel ? ScrollLineDelta : GetLineDownScrollAmount()); - public void LineLeft() => ScrollHorizontal(ScrollUnit == ScrollUnit.Pixel ? -ScrollLineDelta : GetLineLeftScrollAmount()); - public void LineRight() => ScrollHorizontal(ScrollUnit == ScrollUnit.Pixel ? ScrollLineDelta : GetLineRightScrollAmount()); - - public void MouseWheelUp() - { - if (MouseWheelScrollDirection == ScrollDirection.Vertical) - { - ScrollVertical(ScrollUnit == ScrollUnit.Pixel ? -MouseWheelDelta : GetMouseWheelUpScrollAmount()); - } - else - { - MouseWheelLeft(); - } - } - - public void MouseWheelDown() - { - if (MouseWheelScrollDirection == ScrollDirection.Vertical) - { - ScrollVertical(ScrollUnit == ScrollUnit.Pixel ? MouseWheelDelta : GetMouseWheelDownScrollAmount()); - } - else - { - MouseWheelRight(); - } - } - - public void MouseWheelLeft() => ScrollHorizontal(ScrollUnit == ScrollUnit.Pixel ? -MouseWheelDelta : GetMouseWheelLeftScrollAmount()); - public void MouseWheelRight() => ScrollHorizontal(ScrollUnit == ScrollUnit.Pixel ? MouseWheelDelta : GetMouseWheelRightScrollAmount()); - - public void PageUp() => ScrollVertical(ScrollUnit == ScrollUnit.Pixel ? -ViewportHeight : GetPageUpScrollAmount()); - public void PageDown() => ScrollVertical(ScrollUnit == ScrollUnit.Pixel ? ViewportHeight : GetPageDownScrollAmount()); - public void PageLeft() => ScrollHorizontal(ScrollUnit == ScrollUnit.Pixel ? -ViewportHeight : GetPageLeftScrollAmount()); - public void PageRight() => ScrollHorizontal(ScrollUnit == ScrollUnit.Pixel ? ViewportHeight : GetPageRightScrollAmount()); - - protected abstract double GetLineUpScrollAmount(); - protected abstract double GetLineDownScrollAmount(); - protected abstract double GetLineLeftScrollAmount(); - protected abstract double GetLineRightScrollAmount(); - - protected abstract double GetMouseWheelUpScrollAmount(); - protected abstract double GetMouseWheelDownScrollAmount(); - protected abstract double GetMouseWheelLeftScrollAmount(); - protected abstract double GetMouseWheelRightScrollAmount(); - - protected abstract double GetPageUpScrollAmount(); - protected abstract double GetPageDownScrollAmount(); - protected abstract double GetPageLeftScrollAmount(); - protected abstract double GetPageRightScrollAmount(); - } -} +namespace WpfToolkit.Controls; + +/// +/// Base class for panels which are supporting virtualization. +/// +public abstract class VirtualizingPanelBase : VirtualizingPanel, IScrollInfo { + public static readonly DependencyProperty ScrollLineDeltaProperty = DependencyProperty.Register(nameof(ScrollLineDelta), typeof(double), typeof(VirtualizingPanelBase), new FrameworkPropertyMetadata(16.0)); + public static readonly DependencyProperty MouseWheelDeltaProperty = DependencyProperty.Register(nameof(MouseWheelDelta), typeof(double), typeof(VirtualizingPanelBase), new FrameworkPropertyMetadata(48.0)); + public static readonly DependencyProperty ScrollLineDeltaItemProperty = DependencyProperty.Register(nameof(ScrollLineDeltaItem), typeof(int), typeof(VirtualizingPanelBase), new FrameworkPropertyMetadata(1)); + public static readonly DependencyProperty MouseWheelDeltaItemProperty = DependencyProperty.Register(nameof(MouseWheelDeltaItem), typeof(int), typeof(VirtualizingPanelBase), new FrameworkPropertyMetadata(3)); + private IRecyclingItemContainerGenerator? _itemContainerGenerator; + private DependencyObject? _itemsOwner; + private Visibility previousHorizontalScrollBarVisibility = Visibility.Collapsed; + + private Visibility previousVerticalScrollBarVisibility = Visibility.Collapsed; + + protected override bool CanHierarchicallyScrollAndVirtualizeCore => true; + + /// + /// Scroll line delta for pixel based scrolling. The default value is 16 dp. + /// + public double ScrollLineDelta { + get => (double)GetValue(ScrollLineDeltaProperty); + set => SetValue(ScrollLineDeltaProperty, value); + } + + /// + /// Mouse wheel delta for pixel based scrolling. The default value is 48 dp. + /// + public double MouseWheelDelta { + get => (double)GetValue(MouseWheelDeltaProperty); + set => SetValue(MouseWheelDeltaProperty, value); + } + + /// + /// Scroll line delta for item based scrolling. The default value is 1 item. + /// + public double ScrollLineDeltaItem { + get => (int)GetValue(ScrollLineDeltaItemProperty); + set => SetValue(ScrollLineDeltaItemProperty, value); + } + + /// + /// Mouse wheel delta for item based scrolling. The default value is 3 items. + /// + public int MouseWheelDeltaItem { + get => (int)GetValue(MouseWheelDeltaItemProperty); + set => SetValue(MouseWheelDeltaItemProperty, value); + } + + protected ScrollUnit ScrollUnit => GetScrollUnit(ItemsControl); + + /// + /// The direction in which the panel scrolls when user turns the mouse wheel. + /// + protected ScrollDirection MouseWheelScrollDirection { get; set; } = ScrollDirection.Vertical; + + + protected bool IsVirtualizing => GetIsVirtualizing(ItemsControl); + + protected VirtualizationMode VirtualizationMode => GetVirtualizationMode(ItemsControl); + + /// + /// Returns true if the panel is in VirtualizationMode.Recycling, otherwise false. + /// + protected bool IsRecycling => VirtualizationMode == VirtualizationMode.Recycling; + + /// + /// The cache length before and after the viewport. + /// + protected VirtualizationCacheLength CacheLength { get; private set; } + + /// + /// The Unit of the cache length. Can be Pixel, Item or Page. + /// When the ItemsOwner is a group item it can only be pixel or item. + /// + protected VirtualizationCacheLengthUnit CacheLengthUnit { get; private set; } + + + /// + /// The ItemsControl (e.g. ListView). + /// + protected ItemsControl ItemsControl => ItemsControl.GetItemsOwner(this); + + /// + /// The ItemsControl (e.g. ListView) or if the ItemsControl is grouping a GroupItem. + /// + protected DependencyObject ItemsOwner { + get { + if (_itemsOwner is null) { + /* Use reflection to access internal method because the public + * GetItemsOwner method does always return the items control instead + * of the real items owner for example the group item when grouping */ + var getItemsOwnerInternalMethod = typeof(ItemsControl).GetMethod( + "GetItemsOwnerInternal", + BindingFlags.Static | BindingFlags.NonPublic, + null, + new[] { typeof(DependencyObject) }, + null + )!; + _itemsOwner = (DependencyObject)getItemsOwnerInternalMethod.Invoke(null, new object[] { this })!; + } + return _itemsOwner; + } + } + + protected ReadOnlyCollection Items => ((ItemContainerGenerator)ItemContainerGenerator).Items; + + /* Because of a bug in the framework the ItemContainerGenerator + * is null until InternalChildren accessed at least one time. */ + protected new IRecyclingItemContainerGenerator ItemContainerGenerator => + _itemContainerGenerator ??= (IRecyclingItemContainerGenerator)base.ItemContainerGenerator; + + protected Size Extent { get; private set; } = new(0, 0); + protected Size Viewport { get; private set; } = new(0, 0); + protected Point Offset { get; private set; } = new(0, 0); + + /// + /// The range of items that a realized in viewport or cache. + /// + protected ItemRange ItemRange { get; set; } + + public ScrollViewer? ScrollOwner { get; set; } + + public bool CanVerticallyScroll { get; set; } + public bool CanHorizontallyScroll { get; set; } + + public double ExtentWidth => Extent.Width; + public double ExtentHeight => Extent.Height; + + public double HorizontalOffset => Offset.X; + public double VerticalOffset => Offset.Y; + + public double ViewportWidth => Viewport.Width; + public double ViewportHeight => Viewport.Height; + + public virtual Rect MakeVisible(Visual visual, Rect rectangle) { + var pos = visual.TransformToAncestor(this).Transform(Offset); + + double scrollAmountX = 0; + double scrollAmountY = 0; + + if (pos.X < Offset.X) { + scrollAmountX = -(Offset.X - pos.X); + } else if (pos.X + rectangle.Width > Offset.X + Viewport.Width) { + var notVisibleX = pos.X + rectangle.Width - (Offset.X + Viewport.Width); + var maxScrollX = pos.X - Offset.X; // keep left of the visual visible + scrollAmountX = Math.Min(notVisibleX, maxScrollX); + } + + if (pos.Y < Offset.Y) { + scrollAmountY = -(Offset.Y - pos.Y); + } else if (pos.Y + rectangle.Height > Offset.Y + Viewport.Height) { + var notVisibleY = pos.Y + rectangle.Height - (Offset.Y + Viewport.Height); + var maxScrollY = pos.Y - Offset.Y; // keep top of the visual visible + scrollAmountY = Math.Min(notVisibleY, maxScrollY); + } + + SetHorizontalOffset(Offset.X + scrollAmountX); + SetVerticalOffset(Offset.Y + scrollAmountY); + + var visibleRectWidth = Math.Min(rectangle.Width, Viewport.Width); + var visibleRectHeight = Math.Min(rectangle.Height, Viewport.Height); + + return new Rect(scrollAmountX, scrollAmountY, visibleRectWidth, visibleRectHeight); + } + + public void SetVerticalOffset(double offset) { + if (offset < 0 || Viewport.Height >= Extent.Height) { + offset = 0; + } else if (offset + Viewport.Height >= Extent.Height) { + offset = Extent.Height - Viewport.Height; + } + Offset = Offset with { Y = offset }; + ScrollOwner?.InvalidateScrollInfo(); + InvalidateMeasure(); + } + + public void SetHorizontalOffset(double offset) { + if (offset < 0 || Viewport.Width >= Extent.Width) { + offset = 0; + } else if (offset + Viewport.Width >= Extent.Width) { + offset = Extent.Width - Viewport.Width; + } + Offset = Offset with { X = offset }; + ScrollOwner?.InvalidateScrollInfo(); + InvalidateMeasure(); + } + + public void LineUp() { + ScrollVertical(ScrollUnit == ScrollUnit.Pixel ? -ScrollLineDelta : GetLineUpScrollAmount()); + } + + public void LineDown() { + ScrollVertical(ScrollUnit == ScrollUnit.Pixel ? ScrollLineDelta : GetLineDownScrollAmount()); + } + + public void LineLeft() { + ScrollHorizontal(ScrollUnit == ScrollUnit.Pixel ? -ScrollLineDelta : GetLineLeftScrollAmount()); + } + + public void LineRight() { + ScrollHorizontal(ScrollUnit == ScrollUnit.Pixel ? ScrollLineDelta : GetLineRightScrollAmount()); + } + + public void MouseWheelUp() { + if (MouseWheelScrollDirection == ScrollDirection.Vertical) { + ScrollVertical(ScrollUnit == ScrollUnit.Pixel ? -MouseWheelDelta : GetMouseWheelUpScrollAmount()); + } else { + MouseWheelLeft(); + } + } + + public void MouseWheelDown() { + if (MouseWheelScrollDirection == ScrollDirection.Vertical) { + ScrollVertical(ScrollUnit == ScrollUnit.Pixel ? MouseWheelDelta : GetMouseWheelDownScrollAmount()); + } else { + MouseWheelRight(); + } + } + + public void MouseWheelLeft() { + ScrollHorizontal(ScrollUnit == ScrollUnit.Pixel ? -MouseWheelDelta : GetMouseWheelLeftScrollAmount()); + } + + public void MouseWheelRight() { + ScrollHorizontal(ScrollUnit == ScrollUnit.Pixel ? MouseWheelDelta : GetMouseWheelRightScrollAmount()); + } + + public void PageUp() { + ScrollVertical(ScrollUnit == ScrollUnit.Pixel ? -ViewportHeight : GetPageUpScrollAmount()); + } + + public void PageDown() { + ScrollVertical(ScrollUnit == ScrollUnit.Pixel ? ViewportHeight : GetPageDownScrollAmount()); + } + + public void PageLeft() { + ScrollHorizontal(ScrollUnit == ScrollUnit.Pixel ? -ViewportHeight : GetPageLeftScrollAmount()); + } + + public void PageRight() { + ScrollHorizontal(ScrollUnit == ScrollUnit.Pixel ? ViewportHeight : GetPageRightScrollAmount()); + } + + protected virtual void UpdateScrollInfo(Size availableSize, Size extent) { + var invalidateScrollInfo = false; + + if (extent != Extent) { + Extent = extent; + invalidateScrollInfo = true; + } + if (availableSize != Viewport) { + Viewport = availableSize; + invalidateScrollInfo = true; + } + + if (ViewportHeight != 0 && VerticalOffset != 0 && VerticalOffset + ViewportHeight + 1 >= ExtentHeight) { + Offset = Offset with { Y = extent.Height - availableSize.Height }; + invalidateScrollInfo = true; + } + if (ViewportWidth != 0 && HorizontalOffset != 0 && HorizontalOffset + ViewportWidth + 1 >= ExtentWidth) { + Offset = Offset with { X = extent.Width - availableSize.Width }; + invalidateScrollInfo = true; + } + + if (invalidateScrollInfo) { + ScrollOwner?.InvalidateScrollInfo(); + } + } + + protected override void OnItemsChanged(object sender, ItemsChangedEventArgs args) { + switch (args.Action) { + case NotifyCollectionChangedAction.Remove: + case NotifyCollectionChangedAction.Replace: + RemoveInternalChildRange(args.Position.Index, args.ItemUICount); + break; + case NotifyCollectionChangedAction.Move: + RemoveInternalChildRange(args.OldPosition.Index, args.ItemUICount); + break; + } + } + + protected int GetItemIndexFromChildIndex(int childIndex) { + var generatorPosition = GetGeneratorPositionFromChildIndex(childIndex); + return ItemContainerGenerator.IndexFromGeneratorPosition(generatorPosition); + } + + protected virtual GeneratorPosition GetGeneratorPositionFromChildIndex(int childIndex) { + return new GeneratorPosition(childIndex, 0); + } + + protected override Size MeasureOverride(Size availableSize) { + /* Sometimes when scrolling the scrollbar gets hidden without any reason. In this case the "IsMeasureValid" + * property of the ScrollOwner is false. To prevent a infinite circle the mesasure call is ignored. */ + if (ScrollOwner != null) { + var verticalScrollBarGotHidden = ScrollOwner.VerticalScrollBarVisibility == ScrollBarVisibility.Auto + && ScrollOwner.ComputedVerticalScrollBarVisibility != Visibility.Visible + && ScrollOwner.ComputedVerticalScrollBarVisibility != previousVerticalScrollBarVisibility; + + var horizontalScrollBarGotHidden = ScrollOwner.HorizontalScrollBarVisibility == ScrollBarVisibility.Auto + && ScrollOwner.ComputedHorizontalScrollBarVisibility != Visibility.Visible + && ScrollOwner.ComputedHorizontalScrollBarVisibility != previousHorizontalScrollBarVisibility; + + previousVerticalScrollBarVisibility = ScrollOwner.ComputedVerticalScrollBarVisibility; + previousHorizontalScrollBarVisibility = ScrollOwner.ComputedHorizontalScrollBarVisibility; + + if ((!ScrollOwner.IsMeasureValid && verticalScrollBarGotHidden) || horizontalScrollBarGotHidden) { + return availableSize; + } + } + + + Size extent; + Size desiredSize; + + if (ItemsOwner is IHierarchicalVirtualizationAndScrollInfo groupItem) { + /* If the ItemsOwner is a group item the availableSize is ifinity. + * Therfore the vieport size provided by the group item is used. */ + var viewportSize = groupItem.Constraints.Viewport.Size; + var headerSize = groupItem.HeaderDesiredSizes.PixelSize; + var availableWidth = Math.Max(viewportSize.Width - 5, 0); // left margin of 5 dp + var availableHeight = Math.Max(viewportSize.Height - headerSize.Height, 0); + availableSize = new Size(availableWidth, availableHeight); + + extent = CalculateExtent(availableSize); + + desiredSize = new Size(extent.Width, extent.Height); + + Extent = extent; + Offset = groupItem.Constraints.Viewport.Location; + Viewport = viewportSize; + CacheLength = groupItem.Constraints.CacheLength; + CacheLengthUnit = groupItem.Constraints.CacheLengthUnit; // can be Item or Pixel + } else { + extent = CalculateExtent(availableSize); + var desiredWidth = Math.Min(availableSize.Width, extent.Width); + var desiredHeight = Math.Min(availableSize.Height, extent.Height); + desiredSize = new Size(desiredWidth, desiredHeight); + + UpdateScrollInfo(desiredSize, extent); + CacheLength = GetCacheLength(ItemsOwner); + CacheLengthUnit = GetCacheLengthUnit(ItemsOwner); // can be Page, Item or Pixel + } + + ItemRange = UpdateItemRange(); + + RealizeItems(); + VirtualizeItems(); + + return desiredSize; + } + + /// + /// Realizes visible and cached items. + /// + protected virtual void RealizeItems() { + var startPosition = ItemContainerGenerator.GeneratorPositionFromIndex(ItemRange.StartIndex); + + var childIndex = startPosition.Offset == 0 ? startPosition.Index : startPosition.Index + 1; + + using (ItemContainerGenerator.StartAt(startPosition, GeneratorDirection.Forward, true)) { + for (var i = ItemRange.StartIndex; i <= ItemRange.EndIndex; i++, childIndex++) { + var child = (UIElement)ItemContainerGenerator.GenerateNext(out var isNewlyRealized); + if (isNewlyRealized || /*recycled*/!InternalChildren.Contains(child)) { + if (childIndex >= InternalChildren.Count) { + AddInternalChild(child); + } else { + InsertInternalChild(childIndex, child); + } + ItemContainerGenerator.PrepareItemContainer(child); + + child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + } + + if (child is IHierarchicalVirtualizationAndScrollInfo groupItem) { + groupItem.Constraints = new HierarchicalVirtualizationConstraints( + new VirtualizationCacheLength(0), + VirtualizationCacheLengthUnit.Item, + new Rect(0, 0, ViewportWidth, ViewportHeight)); + child.Measure(new Size(ViewportWidth, ViewportHeight)); + } + } + } + } + + /// + /// Virtualizes (cleanups) no longer visible or cached items. + /// + protected virtual void VirtualizeItems() { + for (var childIndex = InternalChildren.Count - 1; childIndex >= 0; childIndex--) { + var generatorPosition = GetGeneratorPositionFromChildIndex(childIndex); + + var itemIndex = ItemContainerGenerator.IndexFromGeneratorPosition(generatorPosition); + + if (itemIndex != -1 && !ItemRange.Contains(itemIndex)) { + if (VirtualizationMode == VirtualizationMode.Recycling) { + ItemContainerGenerator.Recycle(generatorPosition, 1); + } else { + ItemContainerGenerator.Remove(generatorPosition, 1); + } + RemoveInternalChildRange(childIndex, 1); + } + } + } + + /// + /// Calculates the extent that would be needed to show all items. + /// + protected abstract Size CalculateExtent(Size availableSize); + + /// + /// Calculates the item range that is visible in the viewport or cached. + /// + protected abstract ItemRange UpdateItemRange(); + + protected void ScrollVertical(double amount) { + SetVerticalOffset(VerticalOffset + amount); + } + + protected void ScrollHorizontal(double amount) { + SetHorizontalOffset(HorizontalOffset + amount); + } + + protected abstract double GetLineUpScrollAmount(); + protected abstract double GetLineDownScrollAmount(); + protected abstract double GetLineLeftScrollAmount(); + protected abstract double GetLineRightScrollAmount(); + + protected abstract double GetMouseWheelUpScrollAmount(); + protected abstract double GetMouseWheelDownScrollAmount(); + protected abstract double GetMouseWheelLeftScrollAmount(); + protected abstract double GetMouseWheelRightScrollAmount(); + + protected abstract double GetPageUpScrollAmount(); + protected abstract double GetPageDownScrollAmount(); + protected abstract double GetPageLeftScrollAmount(); + protected abstract double GetPageRightScrollAmount(); +} \ No newline at end of file diff --git a/External/VirtualizingWrapPanel/VirtualizingWrapPanel.cs b/External/VirtualizingWrapPanel/VirtualizingWrapPanel.cs index 48680e3..d6d9e92 100644 --- a/External/VirtualizingWrapPanel/VirtualizingWrapPanel.cs +++ b/External/VirtualizingWrapPanel/VirtualizingWrapPanel.cs @@ -4,444 +4,427 @@ using System.Windows.Controls; using System.Windows.Controls.Primitives; -namespace WpfToolkit.Controls -{ - /// - /// A implementation of a wrap panel that supports virtualization and can be used in horizontal and vertical orientation. - ///

In order to work properly all items must have the same size.

- ///
- public class VirtualizingWrapPanel : VirtualizingPanelBase - { - public static readonly DependencyProperty SpacingModeProperty = DependencyProperty.Register(nameof(SpacingMode), typeof(SpacingMode), typeof(VirtualizingWrapPanel), new FrameworkPropertyMetadata(SpacingMode.Uniform, FrameworkPropertyMetadataOptions.AffectsMeasure)); - - public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register(nameof(Orientation), typeof(Orientation), typeof(VirtualizingWrapPanel), new FrameworkPropertyMetadata(Orientation.Vertical, FrameworkPropertyMetadataOptions.AffectsMeasure, (obj, args) => ((VirtualizingWrapPanel)obj).Orientation_Changed())); - - public static readonly DependencyProperty ItemSizeProperty = DependencyProperty.Register(nameof(ItemSize), typeof(Size), typeof(VirtualizingWrapPanel), new FrameworkPropertyMetadata(Size.Empty, FrameworkPropertyMetadataOptions.AffectsMeasure)); - - public static readonly DependencyProperty StretchItemsProperty = DependencyProperty.Register(nameof(StretchItems), typeof(bool), typeof(VirtualizingWrapPanel), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsArrange)); - - /// - /// Gets or sets the spacing mode used when arranging the items. The default value is . - /// - public SpacingMode SpacingMode { get => (SpacingMode)GetValue(SpacingModeProperty); set => SetValue(SpacingModeProperty, value); } - - /// - /// Gets or sets a value that specifies the orientation in which items are arranged. The default value is . - /// - public Orientation Orientation { get => (Orientation)GetValue(OrientationProperty); set => SetValue(OrientationProperty, value); } - - /// - /// Gets or sets a value that specifies the size of the items. The default value is . - /// If the value is the size of the items gots measured by the first realized item. - /// - public Size ItemSize { get => (Size)GetValue(ItemSizeProperty); set => SetValue(ItemSizeProperty, value); } - - /// - /// Gets or sets a value that specifies if the items get stretched to fill up remaining space. The default value is false. - /// - /// - /// The MaxWidth and MaxHeight properties of the ItemContainerStyle can be used to limit the stretching. - /// In this case the use of the remaining space will be determined by the SpacingMode property. - /// - public bool StretchItems { get => (bool)GetValue(StretchItemsProperty); set => SetValue(StretchItemsProperty, value); } - - protected Size childSize; - - protected int rowCount; - - protected int itemsPerRowCount; - - private void Orientation_Changed() - { - MouseWheelScrollDirection = Orientation == Orientation.Vertical ? ScrollDirection.Vertical : ScrollDirection.Horizontal; - } - - protected override Size MeasureOverride(Size availableSize) - { - UpdateChildSize(availableSize); - return base.MeasureOverride(availableSize); - } - - private void UpdateChildSize(Size availableSize) - { - if (ItemsOwner is IHierarchicalVirtualizationAndScrollInfo groupItem - && VirtualizingPanel.GetIsVirtualizingWhenGrouping(ItemsControl)) - { - if (Orientation == Orientation.Vertical) - { - availableSize.Width = groupItem.Constraints.Viewport.Size.Width; - availableSize.Width = Math.Max(availableSize.Width - (Margin.Left + Margin.Right), 0); - } - else - { - availableSize.Height = groupItem.Constraints.Viewport.Size.Height; - availableSize.Height = Math.Max(availableSize.Height - (Margin.Top + Margin.Bottom), 0); - } - } - - if (ItemSize != Size.Empty) - { - childSize = ItemSize; - } - else if (InternalChildren.Count != 0) - { - childSize = InternalChildren[0].DesiredSize; - } - else - { - childSize = CalculateChildSize(availableSize); - } - - if (double.IsInfinity(GetWidth(availableSize))) - { - itemsPerRowCount = Items.Count; - } - else - { - itemsPerRowCount = Math.Max(1, (int)Math.Floor(GetWidth(availableSize) / GetWidth(childSize))); - } - - rowCount = (int)Math.Ceiling((double)Items.Count / itemsPerRowCount); - } - - private Size CalculateChildSize(Size availableSize) - { - if (Items.Count == 0) - { - return new Size(0, 0); - } - var startPosition = ItemContainerGenerator.GeneratorPositionFromIndex(0); - using (ItemContainerGenerator.StartAt(startPosition, GeneratorDirection.Forward, true)) - { - var child = (UIElement)ItemContainerGenerator.GenerateNext(); - AddInternalChild(child); - ItemContainerGenerator.PrepareItemContainer(child); - child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); - return child.DesiredSize; - } - } - - protected override Size CalculateExtent(Size availableSize) - { - double extentWidth = SpacingMode != SpacingMode.None && !double.IsInfinity(GetWidth(availableSize)) - ? GetWidth(availableSize) - : GetWidth(childSize) * itemsPerRowCount; - - if (ItemsOwner is IHierarchicalVirtualizationAndScrollInfo groupItem) - { - if (Orientation == Orientation.Vertical) - { - extentWidth = Math.Max(extentWidth - (Margin.Left + Margin.Right), 0); - } - else - { - extentWidth = Math.Max(extentWidth - (Margin.Top + Margin.Bottom), 0); - } - } - - double extentHeight = GetHeight(childSize) * rowCount; - return CreateSize(extentWidth, extentHeight); - } - - protected void CalculateSpacing(Size finalSize, out double innerSpacing, out double outerSpacing) - { - Size childSize = CalculateChildArrangeSize(finalSize); - - double finalWidth = GetWidth(finalSize); - - double totalItemsWidth = Math.Min(GetWidth(childSize) * itemsPerRowCount, finalWidth); - double unusedWidth = finalWidth - totalItemsWidth; - - switch (SpacingMode) - { - case SpacingMode.Uniform: - innerSpacing = outerSpacing = unusedWidth / (itemsPerRowCount + 1); - break; - - case SpacingMode.BetweenItemsOnly: - innerSpacing = unusedWidth / Math.Max(itemsPerRowCount - 1, 1); - outerSpacing = 0; - break; - - case SpacingMode.StartAndEndOnly: - innerSpacing = 0; - outerSpacing = unusedWidth / 2; - break; - - case SpacingMode.None: - default: - innerSpacing = 0; - outerSpacing = 0; - break; - } - } - - protected override Size ArrangeOverride(Size finalSize) - { - double offsetX = GetX(Offset); - double offsetY = GetY(Offset); - - /* When the items owner is a group item offset is handled by the parent panel. */ - if (ItemsOwner is IHierarchicalVirtualizationAndScrollInfo groupItem) - { - offsetY = 0; - } - - Size childSize = CalculateChildArrangeSize(finalSize); - - CalculateSpacing(finalSize, out double innerSpacing, out double outerSpacing); - - for (int childIndex = 0; childIndex < InternalChildren.Count; childIndex++) - { - UIElement child = InternalChildren[childIndex]; - - int itemIndex = GetItemIndexFromChildIndex(childIndex); - - int columnIndex = itemIndex % itemsPerRowCount; - int rowIndex = itemIndex / itemsPerRowCount; - - double x = outerSpacing + columnIndex * (GetWidth(childSize) + innerSpacing); - double y = rowIndex * GetHeight(childSize); - - if (GetHeight(finalSize) == 0.0) - { - /* When the parent panel is grouping and a cached group item is not - * in the viewport it has no valid arrangement. That means that the - * height/width is 0. Therefore the items should not be visible so - * that they are not falsely displayed. */ - child.Arrange(new Rect(0, 0, 0, 0)); - } - else - { - child.Arrange(CreateRect(x - offsetX, y - offsetY, childSize.Width, childSize.Height)); - } - } - - return finalSize; - } - - protected Size CalculateChildArrangeSize(Size finalSize) - { - if (StretchItems) - { - if (Orientation == Orientation.Vertical) - { - double childMaxWidth = ReadItemContainerStyle(MaxWidthProperty, double.PositiveInfinity); - double maxPossibleChildWith = finalSize.Width / itemsPerRowCount; - double childWidth = Math.Min(maxPossibleChildWith, childMaxWidth); - return new Size(childWidth, childSize.Height); - } - else - { - double childMaxHeight = ReadItemContainerStyle(MaxHeightProperty, double.PositiveInfinity); - double maxPossibleChildHeight = finalSize.Height / itemsPerRowCount; - double childHeight = Math.Min(maxPossibleChildHeight, childMaxHeight); - return new Size(childSize.Width, childHeight); - } - } - else - { - return childSize; - } - } - - private T ReadItemContainerStyle(DependencyProperty property, T fallbackValue) where T : notnull - { - var value = ItemsControl.ItemContainerStyle?.Setters.OfType() - .FirstOrDefault(setter => setter.Property == property)?.Value; - return (T)(value ?? fallbackValue); - } - - protected override ItemRange UpdateItemRange() - { - if (!IsVirtualizing) - { - return new ItemRange(0, Items.Count - 1); - } - - int startIndex; - int endIndex; - - if (ItemsOwner is IHierarchicalVirtualizationAndScrollInfo groupItem) - { - if (!VirtualizingPanel.GetIsVirtualizingWhenGrouping(ItemsControl)) - { - return new ItemRange(0, Items.Count - 1); - } - - var offset = new Point(Offset.X, groupItem.Constraints.Viewport.Location.Y); - - int offsetRowIndex; - double offsetInPixel; - - int rowCountInViewport; - - if (ScrollUnit == ScrollUnit.Item) - { - offsetRowIndex = GetY(offset) >= 1 ? (int)GetY(offset) - 1 : 0; // ignore header - offsetInPixel = offsetRowIndex * GetHeight(childSize); - } - else - { - offsetInPixel = Math.Min(Math.Max(GetY(offset) - GetHeight(groupItem.HeaderDesiredSizes.PixelSize), 0), GetHeight(Extent)); - offsetRowIndex = GetRowIndex(offsetInPixel); - } - - double viewportHeight = Math.Min(GetHeight(Viewport), Math.Max(GetHeight(Extent) - offsetInPixel, 0)); - - rowCountInViewport = (int)Math.Ceiling((offsetInPixel + viewportHeight) / GetHeight(childSize)) - (int)Math.Floor(offsetInPixel / GetHeight(childSize)); - - startIndex = offsetRowIndex * itemsPerRowCount; - endIndex = Math.Min(((offsetRowIndex + rowCountInViewport) * itemsPerRowCount) - 1, Items.Count - 1); - - if (CacheLengthUnit == VirtualizationCacheLengthUnit.Pixel) - { - double cacheBeforeInPixel = Math.Min(CacheLength.CacheBeforeViewport, offsetInPixel); - double cacheAfterInPixel = Math.Min(CacheLength.CacheAfterViewport, GetHeight(Extent) - viewportHeight - offsetInPixel); - int rowCountInCacheBefore = (int)(cacheBeforeInPixel / GetHeight(childSize)); - int rowCountInCacheAfter = ((int)Math.Ceiling((offsetInPixel + viewportHeight + cacheAfterInPixel) / GetHeight(childSize))) - (int)Math.Ceiling((offsetInPixel + viewportHeight) / GetHeight(childSize)); - startIndex = Math.Max(startIndex - rowCountInCacheBefore * itemsPerRowCount, 0); - endIndex = Math.Min(endIndex + rowCountInCacheAfter * itemsPerRowCount, Items.Count - 1); - } - else if (CacheLengthUnit == VirtualizationCacheLengthUnit.Item) - { - startIndex = Math.Max(startIndex - (int)CacheLength.CacheBeforeViewport, 0); - endIndex = Math.Min(endIndex + (int)CacheLength.CacheAfterViewport, Items.Count - 1); - } - } - else - { - double viewportSartPos = GetY(Offset); - double viewportEndPos = GetY(Offset) + GetHeight(Viewport); - - if (CacheLengthUnit == VirtualizationCacheLengthUnit.Pixel) - { - viewportSartPos = Math.Max(viewportSartPos - CacheLength.CacheBeforeViewport, 0); - viewportEndPos = Math.Min(viewportEndPos + CacheLength.CacheAfterViewport, GetHeight(Extent)); - } - - int startRowIndex = GetRowIndex(viewportSartPos); - startIndex = startRowIndex * itemsPerRowCount; - - int endRowIndex = GetRowIndex(viewportEndPos); - endIndex = Math.Min(endRowIndex * itemsPerRowCount + (itemsPerRowCount - 1), Items.Count - 1); - - if (CacheLengthUnit == VirtualizationCacheLengthUnit.Page) - { - int itemsPerPage = endIndex - startIndex + 1; - startIndex = Math.Max(startIndex - (int)CacheLength.CacheBeforeViewport * itemsPerPage, 0); - endIndex = Math.Min(endIndex + (int)CacheLength.CacheAfterViewport * itemsPerPage, Items.Count - 1); - } - else if (CacheLengthUnit == VirtualizationCacheLengthUnit.Item) - { - startIndex = Math.Max(startIndex - (int)CacheLength.CacheBeforeViewport, 0); - endIndex = Math.Min(endIndex + (int)CacheLength.CacheAfterViewport, Items.Count - 1); - } - } - - return new ItemRange(startIndex, endIndex); - } - - private int GetRowIndex(double location) - { - int calculatedRowIndex = (int)Math.Floor(location / GetHeight(childSize)); - int maxRowIndex = (int)Math.Ceiling((double)Items.Count / (double)itemsPerRowCount); - return Math.Max(Math.Min(calculatedRowIndex, maxRowIndex), 0); - } - - protected override void BringIndexIntoView(int index) - { - if (index < 0 || index >= Items.Count) - { - throw new ArgumentOutOfRangeException(nameof(index), $"The argument {nameof(index)} must be >= 0 and < the number of items."); - } - - if (itemsPerRowCount == 0) - { - throw new InvalidOperationException(); - } - - var offset = (index / itemsPerRowCount) * GetHeight(childSize); - - if (Orientation == Orientation.Horizontal) - { - SetHorizontalOffset(offset); - } - else - { - SetVerticalOffset(offset); - } - } - - protected override double GetLineUpScrollAmount() - { - return -Math.Min(childSize.Height * ScrollLineDeltaItem, Viewport.Height); - } - - protected override double GetLineDownScrollAmount() - { - return Math.Min(childSize.Height * ScrollLineDeltaItem, Viewport.Height); - } - - protected override double GetLineLeftScrollAmount() - { - return -Math.Min(childSize.Width * ScrollLineDeltaItem, Viewport.Width); - } - - protected override double GetLineRightScrollAmount() - { - return Math.Min(childSize.Width * ScrollLineDeltaItem, Viewport.Width); - } - - protected override double GetMouseWheelUpScrollAmount() - { - return -Math.Min(childSize.Height * MouseWheelDeltaItem, Viewport.Height); - } - - protected override double GetMouseWheelDownScrollAmount() - { - return Math.Min(childSize.Height * MouseWheelDeltaItem, Viewport.Height); - } - - protected override double GetMouseWheelLeftScrollAmount() - { - return -Math.Min(childSize.Width * MouseWheelDeltaItem, Viewport.Width); - } - - protected override double GetMouseWheelRightScrollAmount() - { - return Math.Min(childSize.Width * MouseWheelDeltaItem, Viewport.Width); - } - - protected override double GetPageUpScrollAmount() - { - return -Viewport.Height; - } - - protected override double GetPageDownScrollAmount() - { - return Viewport.Height; - } - - protected override double GetPageLeftScrollAmount() - { - return -Viewport.Width; - } - - protected override double GetPageRightScrollAmount() - { - return Viewport.Width; - } - - /* orientation aware helper methods */ - - protected double GetX(Point point) => Orientation == Orientation.Vertical ? point.X : point.Y; - protected double GetY(Point point) => Orientation == Orientation.Vertical ? point.Y : point.X; - - protected double GetWidth(Size size) => Orientation == Orientation.Vertical ? size.Width : size.Height; - protected double GetHeight(Size size) => Orientation == Orientation.Vertical ? size.Height : size.Width; - - protected Size CreateSize(double width, double height) => Orientation == Orientation.Vertical ? new Size(width, height) : new Size(height, width); - protected Rect CreateRect(double x, double y, double width, double height) => Orientation == Orientation.Vertical ? new Rect(x, y, width, height) : new Rect(y, x, width, height); - } -} +namespace WpfToolkit.Controls; + +/// +/// A implementation of a wrap panel that supports virtualization and can be used in horizontal and vertical +/// orientation. +///

In order to work properly all items must have the same size.

+///
+public class VirtualizingWrapPanel : VirtualizingPanelBase { + public static readonly DependencyProperty SpacingModeProperty = DependencyProperty.Register(nameof(SpacingMode), typeof(SpacingMode), typeof(VirtualizingWrapPanel), new FrameworkPropertyMetadata(SpacingMode.Uniform, FrameworkPropertyMetadataOptions.AffectsMeasure)); + + public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register(nameof(Orientation), typeof(Orientation), typeof(VirtualizingWrapPanel), new FrameworkPropertyMetadata(Orientation.Vertical, FrameworkPropertyMetadataOptions.AffectsMeasure, (obj, _) => ((VirtualizingWrapPanel)obj).Orientation_Changed())); + + public static readonly DependencyProperty ItemSizeProperty = DependencyProperty.Register(nameof(ItemSize), typeof(Size), typeof(VirtualizingWrapPanel), new FrameworkPropertyMetadata(Size.Empty, FrameworkPropertyMetadataOptions.AffectsMeasure)); + + public static readonly DependencyProperty StretchItemsProperty = DependencyProperty.Register(nameof(StretchItems), typeof(bool), typeof(VirtualizingWrapPanel), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsArrange)); + + protected Size childSize; + + protected int itemsPerRowCount; + + protected int rowCount; + + /// + /// Gets or sets the spacing mode used when arranging the items. The default value is + /// . + /// + public SpacingMode SpacingMode { + get => (SpacingMode)GetValue(SpacingModeProperty); + set => SetValue(SpacingModeProperty, value); + } + + /// + /// Gets or sets a value that specifies the orientation in which items are arranged. The default value is + /// . + /// + public Orientation Orientation { + get => (Orientation)GetValue(OrientationProperty); + set => SetValue(OrientationProperty, value); + } + + /// + /// Gets or sets a value that specifies the size of the items. The default value is . + /// If the value is the size of the items got measured by the first realized item. + /// + public Size ItemSize { + get => (Size)GetValue(ItemSizeProperty); + set => SetValue(ItemSizeProperty, value); + } + + public bool IsGroupPanel { get; set; } + + /// + /// Gets or sets a value that specifies if the items get stretched to fill up remaining space. The default value is + /// false. + /// + /// + /// The MaxWidth and MaxHeight properties of the ItemContainerStyle can be used to limit the stretching. + /// In this case the use of the remaining space will be determined by the SpacingMode property. + /// + public bool StretchItems { + get => (bool)GetValue(StretchItemsProperty); + set => SetValue(StretchItemsProperty, value); + } + + private void Orientation_Changed() { + MouseWheelScrollDirection = Orientation == Orientation.Vertical ? ScrollDirection.Vertical : ScrollDirection.Horizontal; + } + + protected override Size MeasureOverride(Size availableSize) { + UpdateChildSize(availableSize); + return base.MeasureOverride(availableSize); + } + + private void UpdateChildSize(Size availableSize) { + if (ItemsOwner is IHierarchicalVirtualizationAndScrollInfo groupItem + && GetIsVirtualizingWhenGrouping(ItemsControl)) { + if (Orientation == Orientation.Vertical) { + availableSize.Width = groupItem.Constraints.Viewport.Size.Width; + availableSize.Width = Math.Max(availableSize.Width - (Margin.Left + Margin.Right), 0); + } else { + availableSize.Height = groupItem.Constraints.Viewport.Size.Height; + availableSize.Height = Math.Max(availableSize.Height - (Margin.Top + Margin.Bottom), 0); + } + } + + var itemSize = ItemSize; + if (itemSize != Size.Empty) { + childSize = itemSize; + } else if (InternalChildren.Count != 0) { + childSize = InternalChildren[0].DesiredSize; + } else { + childSize = CalculateChildSize(); + } + + if (double.IsInfinity(GetWidth(availableSize))) { + itemsPerRowCount = Items.Count; + } else { + itemsPerRowCount = Math.Max(1, (int)Math.Floor(GetWidth(availableSize) / GetWidth(childSize))); + } + + rowCount = (int)Math.Ceiling((double)Items.Count / itemsPerRowCount); + } + + private Size CalculateChildSize() { + if (Items.Count == 0) { + return new Size(0, 0); + } + var startPosition = ItemContainerGenerator.GeneratorPositionFromIndex(0); + using (ItemContainerGenerator.StartAt(startPosition, GeneratorDirection.Forward, true)) { + var child = (UIElement?)ItemContainerGenerator.GenerateNext(); + if (child == null) { + return new Size(0, 0); + } + AddInternalChild(child); + ItemContainerGenerator.PrepareItemContainer(child); + child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + return child.DesiredSize; + } + } + + protected override Size CalculateExtent(Size availableSize) { + var extentWidth = SpacingMode != SpacingMode.None && !double.IsInfinity(GetWidth(availableSize)) + ? GetWidth(availableSize) + : GetWidth(childSize) * itemsPerRowCount; + + if (ItemsOwner is IHierarchicalVirtualizationAndScrollInfo groupItem) { + if (Orientation == Orientation.Vertical) { + extentWidth = Math.Max(extentWidth - (Margin.Left + Margin.Right), 0); + } else { + extentWidth = Math.Max(extentWidth - (Margin.Top + Margin.Bottom), 0); + } + } + + var extentHeight = GetHeight(childSize) * rowCount; + return CreateSize(extentWidth, extentHeight); + } + + protected void CalculateSpacing(Size finalSize, out double innerSpacing, out double outerSpacing) { + var childSize = CalculateChildArrangeSize(finalSize); + + var finalWidth = GetWidth(finalSize); + + var totalItemsWidth = Math.Min(GetWidth(childSize) * itemsPerRowCount, finalWidth); + var unusedWidth = finalWidth - totalItemsWidth; + + switch (SpacingMode) { + case SpacingMode.Uniform: + innerSpacing = outerSpacing = unusedWidth / (itemsPerRowCount + 1); + break; + + case SpacingMode.BetweenItemsOnly: + innerSpacing = unusedWidth / Math.Max(itemsPerRowCount - 1, 1); + outerSpacing = 0; + break; + + case SpacingMode.StartAndEndOnly: + innerSpacing = 0; + outerSpacing = unusedWidth / 2; + break; + + case SpacingMode.None: + default: + innerSpacing = 0; + outerSpacing = 0; + break; + } + } + + protected override Size ArrangeOverride(Size finalSize) { + var offsetX = GetX(Offset); + var offsetY = GetY(Offset); + + /* When the items owner is a group item offset is handled by the parent panel. */ + if (ItemsOwner is IHierarchicalVirtualizationAndScrollInfo groupItem) { + offsetY = 0; + } + + var useGroupingArrange = IsGroupPanel && ItemsControl.IsGrouping; + + var childSize = Size.Empty; + if (!useGroupingArrange) { + childSize = CalculateChildArrangeSize(finalSize); + } + + CalculateSpacing(finalSize, out var innerSpacing, out var outerSpacing); + + for (var childIndex = 0; childIndex < InternalChildren.Count; childIndex++) { + var child = InternalChildren[childIndex]; + + var itemIndex = GetItemIndexFromChildIndex(childIndex); + + var columnIndex = itemIndex % itemsPerRowCount; + var rowIndex = itemIndex / itemsPerRowCount; + + if (useGroupingArrange) { + childSize = child.DesiredSize; + } + + var x = outerSpacing + columnIndex * (GetWidth(childSize) + innerSpacing); + var y = rowIndex * GetHeight(childSize); + + if (GetHeight(finalSize) == 0.0) { + /* When the parent panel is grouping and a cached group item is not + * in the viewport it has no valid arrangement. That means that the + * height/width is 0. Therefore the items should not be visible so + * that they are not falsely displayed. */ + child.Arrange(new Rect(0, 0, 0, 0)); + } else { + child.Arrange(CreateRect(x - offsetX, y - offsetY, childSize.Width, childSize.Height)); + } + } + + return finalSize; + } + + protected Size CalculateChildArrangeSize(Size finalSize) { + if (StretchItems) { + if (Orientation == Orientation.Vertical) { + var childMaxWidth = ReadItemContainerStyle(MaxWidthProperty, double.PositiveInfinity); + var maxPossibleChildWith = finalSize.Width / itemsPerRowCount; + var childWidth = Math.Min(maxPossibleChildWith, childMaxWidth); + return new Size(childWidth, childSize.Height); + } + var childMaxHeight = ReadItemContainerStyle(MaxHeightProperty, double.PositiveInfinity); + var maxPossibleChildHeight = finalSize.Height / itemsPerRowCount; + var childHeight = Math.Min(maxPossibleChildHeight, childMaxHeight); + return new Size(childSize.Width, childHeight); + } + return childSize; + } + + private T ReadItemContainerStyle(DependencyProperty property, T fallbackValue) where T : notnull { + var value = ItemsControl.ItemContainerStyle?.Setters.OfType() + .FirstOrDefault(setter => setter.Property == property)?.Value; + return (T)(value ?? fallbackValue); + } + + protected override ItemRange UpdateItemRange() { + if (!IsVirtualizing) { + return new ItemRange(0, Items.Count - 1); + } + + int startIndex; + int endIndex; + + if (ItemsOwner is IHierarchicalVirtualizationAndScrollInfo groupItem) { + if (!GetIsVirtualizingWhenGrouping(ItemsControl)) { + return new ItemRange(0, Items.Count - 1); + } + + var offset = new Point(Offset.X, groupItem.Constraints.Viewport.Location.Y); + + int offsetRowIndex; + double offsetInPixel; + + if (ScrollUnit == ScrollUnit.Item) { + offsetRowIndex = GetY(offset) >= 1 ? (int)GetY(offset) - 1 : 0; // ignore header + offsetInPixel = offsetRowIndex * GetHeight(childSize); + } else { + offsetInPixel = Math.Min(Math.Max(GetY(offset) - GetHeight(groupItem.HeaderDesiredSizes.PixelSize), 0), GetHeight(Extent)); + offsetRowIndex = GetRowIndex(offsetInPixel); + } + + var viewportHeight = Math.Min(GetHeight(Viewport), Math.Max(GetHeight(Extent) - offsetInPixel, 0)); + + var rowCountInViewport = (int)Math.Ceiling((offsetInPixel + viewportHeight) / GetHeight(childSize)) - (int)Math.Floor(offsetInPixel / GetHeight(childSize)); + + startIndex = offsetRowIndex * itemsPerRowCount; + endIndex = Math.Min((offsetRowIndex + rowCountInViewport) * itemsPerRowCount - 1, Items.Count - 1); + + switch (CacheLengthUnit) { + case VirtualizationCacheLengthUnit.Pixel: { + var cacheBeforeInPixel = Math.Min(CacheLength.CacheBeforeViewport, offsetInPixel); + var cacheAfterInPixel = Math.Min(CacheLength.CacheAfterViewport, GetHeight(Extent) - viewportHeight - offsetInPixel); + var rowCountInCacheBefore = (int)(cacheBeforeInPixel / GetHeight(childSize)); + var rowCountInCacheAfter = (int)Math.Ceiling((offsetInPixel + viewportHeight + cacheAfterInPixel) / GetHeight(childSize)) - (int)Math.Ceiling((offsetInPixel + viewportHeight) / GetHeight(childSize)); + startIndex = Math.Max(startIndex - rowCountInCacheBefore * itemsPerRowCount, 0); + endIndex = Math.Min(endIndex + rowCountInCacheAfter * itemsPerRowCount, Items.Count - 1); + break; + } + case VirtualizationCacheLengthUnit.Item: + startIndex = Math.Max(startIndex - (int)CacheLength.CacheBeforeViewport, 0); + endIndex = Math.Min(endIndex + (int)CacheLength.CacheAfterViewport, Items.Count - 1); + break; + } + } else { + var viewportStartPos = GetY(Offset); + var viewportEndPos = GetY(Offset) + GetHeight(Viewport); + + if (CacheLengthUnit == VirtualizationCacheLengthUnit.Pixel) { + viewportStartPos = Math.Max(viewportStartPos - CacheLength.CacheBeforeViewport, 0); + viewportEndPos = Math.Min(viewportEndPos + CacheLength.CacheAfterViewport, GetHeight(Extent)); + } + + var startRowIndex = GetRowIndex(viewportStartPos); + startIndex = startRowIndex * itemsPerRowCount; + + var endRowIndex = GetRowIndex(viewportEndPos); + endIndex = Math.Min(endRowIndex * itemsPerRowCount + (itemsPerRowCount - 1), Items.Count - 1); + + switch (CacheLengthUnit) { + case VirtualizationCacheLengthUnit.Page: { + var itemsPerPage = endIndex - startIndex + 1; + startIndex = Math.Max(startIndex - (int)CacheLength.CacheBeforeViewport * itemsPerPage, 0); + endIndex = Math.Min(endIndex + (int)CacheLength.CacheAfterViewport * itemsPerPage, Items.Count - 1); + break; + } + case VirtualizationCacheLengthUnit.Item: + startIndex = Math.Max(startIndex - (int)CacheLength.CacheBeforeViewport, 0); + endIndex = Math.Min(endIndex + (int)CacheLength.CacheAfterViewport, Items.Count - 1); + break; + } + } + + return new ItemRange(startIndex, endIndex); + } + + private int GetRowIndex(double location) { + if (IsGroupPanel && ItemsControl.IsGrouping) { + for (var index = 0; index < InternalChildren.Count; index++) { + location -= GetHeight(InternalChildren[index].DesiredSize); + if (location <= 0) { + return index; + } + } + return InternalChildren.Count - 1; + } + var calculatedRowIndex = (int)Math.Floor(location / GetHeight(childSize)); + var maxRowIndex = (int)Math.Ceiling(Items.Count / (double)itemsPerRowCount); + return Math.Max(Math.Min(calculatedRowIndex, maxRowIndex), 0); + } + + protected override void BringIndexIntoView(int index) { + if (index < 0 || index >= Items.Count) { + throw new ArgumentOutOfRangeException(nameof(index), $"The argument {nameof(index)} must be >= 0 and < the number of items."); + } + + if (itemsPerRowCount == 0) { + throw new InvalidOperationException(); + } + + var offset = index / itemsPerRowCount * GetHeight(childSize); + + if (Orientation == Orientation.Horizontal) { + SetHorizontalOffset(offset); + } else { + SetVerticalOffset(offset); + } + } + + protected override double GetLineUpScrollAmount() { + return -Math.Min(childSize.Height * ScrollLineDeltaItem, Viewport.Height); + } + + protected override double GetLineDownScrollAmount() { + return Math.Min(childSize.Height * ScrollLineDeltaItem, Viewport.Height); + } + + protected override double GetLineLeftScrollAmount() { + return -Math.Min(childSize.Width * ScrollLineDeltaItem, Viewport.Width); + } + + protected override double GetLineRightScrollAmount() { + return Math.Min(childSize.Width * ScrollLineDeltaItem, Viewport.Width); + } + + protected override double GetMouseWheelUpScrollAmount() { + return -Math.Min(childSize.Height * MouseWheelDeltaItem, Viewport.Height); + } + + protected override double GetMouseWheelDownScrollAmount() { + return Math.Min(childSize.Height * MouseWheelDeltaItem, Viewport.Height); + } + + protected override double GetMouseWheelLeftScrollAmount() { + return -Math.Min(childSize.Width * MouseWheelDeltaItem, Viewport.Width); + } + + protected override double GetMouseWheelRightScrollAmount() { + return Math.Min(childSize.Width * MouseWheelDeltaItem, Viewport.Width); + } + + protected override double GetPageUpScrollAmount() { + return -Viewport.Height; + } + + protected override double GetPageDownScrollAmount() { + return Viewport.Height; + } + + protected override double GetPageLeftScrollAmount() { + return -Viewport.Width; + } + + protected override double GetPageRightScrollAmount() { + return Viewport.Width; + } + + /* orientation aware helper methods */ + + protected double GetX(Point point) { + return Orientation == Orientation.Vertical ? point.X : point.Y; + } + + protected double GetY(Point point) { + return Orientation == Orientation.Vertical ? point.Y : point.X; + } + + protected double GetWidth(Size size) { + return Orientation == Orientation.Vertical ? size.Width : size.Height; + } + + protected double GetHeight(Size size) { + return Orientation == Orientation.Vertical ? size.Height : size.Width; + } + + protected Size CreateSize(double width, double height) { + return Orientation == Orientation.Vertical ? new Size(width, height) : new Size(height, width); + } + + protected Rect CreateRect(double x, double y, double width, double height) { + return Orientation == Orientation.Vertical ? new Rect(x, y, width, height) : new Rect(y, x, width, height); + } +} \ No newline at end of file diff --git a/Readme.md b/Readme.md index b752241..4f56e18 100644 --- a/Readme.md +++ b/Readme.md @@ -6,7 +6,7 @@ ![Preview](https://raw.githubusercontent.com/DearVa/ExplorerEx/master/Images/preview.png) -[🇨🇳中文版](https://github.com/DearVa/ExplorerEx/blob/master/Readme_zh_CN.md) +## [👉🇨🇳中文版介绍点这里👈](https://github.com/DearVa/ExplorerEx/blob/master/Readme_zh_CN.md) Due to unknown reasons, the file manager (Explorer) of windows 11 is very slow, especially when opening folders, selecting files, copying and pasting. It even stop responding for a few seconds sometimes.