From d3dd3ab7c71c5dded73abe773c26cdbc07295994 Mon Sep 17 00:00:00 2001 From: Ottermandias <70807659+Ottermandias@users.noreply.github.com> Date: Wed, 9 Apr 2025 22:14:47 +0200 Subject: [PATCH] Add configurable default sorting to FileDialog. (#2233) --- .../ImGuiFileDialog/FileDialog.Files.cs | 329 +++++++++++------- .../ImGuiFileDialog/FileDialog.UI.cs | 18 +- .../ImGuiFileDialog/FileDialogManager.cs | 39 ++- 3 files changed, 239 insertions(+), 147 deletions(-) diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialog.Files.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialog.Files.cs index 705c0f100..e5b7fc15e 100644 --- a/Dalamud/Interface/ImGuiFileDialog/FileDialog.Files.cs +++ b/Dalamud/Interface/ImGuiFileDialog/FileDialog.Files.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using Dalamud.Utility; + namespace Dalamud.Interface.ImGuiFileDialog; /// @@ -13,11 +15,44 @@ public partial class FileDialog private readonly DriveListLoader driveListLoader = new(); - private List files = new(); - private List filteredFiles = new(); + private readonly List files = []; + private readonly List filteredFiles = []; private SortingField currentSortingField = SortingField.FileName; - private bool[] sortDescending = { false, false, false, false }; + + /// Fired whenever the sorting field changes. + public event Action? SortOrderChanged; + + /// The sorting type of the file selector. + public enum SortingField + { + /// No sorting specified. + None = 0, + + /// Sort for ascending file names in culture-specific order. + FileName = 1, + + /// Sort for ascending file types in culture-specific order. + Type = 2, + + /// Sort for ascending file sizes. + Size = 3, + + /// Sort for ascending last update dates. + Date = 4, + + /// Sort for descending file names in culture-specific order. + FileNameDescending = 5, + + /// Sort for descending file types in culture-specific order. + TypeDescending = 6, + + /// Sort for descending file sizes. + SizeDescending = 7, + + /// Sort for descending last update dates. + DateDescending = 8, + } private enum FileStructType { @@ -25,48 +60,64 @@ public partial class FileDialog Directory, } - private enum SortingField + /// Specify the current and subsequent sort order. + /// The new sort order. None is invalid and will not have any effect. + public void SortFields(SortingField sortingField) { - None, - FileName, - Type, - Size, - Date, + Comparison? sortFunc = sortingField switch + { + SortingField.FileName => SortByFileNameAsc, + SortingField.FileNameDescending => SortByFileNameDesc, + SortingField.Type => SortByTypeAsc, + SortingField.TypeDescending => SortByTypeDesc, + SortingField.Size => SortBySizeAsc, + SortingField.SizeDescending => SortBySizeDesc, + SortingField.Date => SortByDateAsc, + SortingField.DateDescending => SortByDateDesc, + _ => null, + }; + + if (sortFunc is null) + { + return; + } + + this.files.Sort(sortFunc); + this.currentSortingField = sortingField; + this.ApplyFilteringOnFileList(); + this.SortOrderChanged?.InvokeSafely(this.currentSortingField); } - private static string ComposeNewPath(List decomp) + private static string ComposeNewPath(List decomposition) { - // Handle UNC paths (network paths) - if (decomp.Count >= 2 && string.IsNullOrEmpty(decomp[0]) && string.IsNullOrEmpty(decomp[1])) + switch (decomposition.Count) { - var pathParts = new List(decomp); - pathParts.RemoveRange(0, 2); - // Can not access server level or UNC root - if (pathParts.Count <= 1) - { - return string.Empty; - } + // Handle UNC paths (network paths) + case >= 2 when string.IsNullOrEmpty(decomposition[0]) && string.IsNullOrEmpty(decomposition[1]): + var pathParts = new List(decomposition); + pathParts.RemoveRange(0, 2); - return $"\\\\{string.Join('\\', pathParts)}"; + // Can not access server level or UNC root + if (pathParts.Count <= 1) + { + return string.Empty; + } + + return $@"\\{string.Join('\\', pathParts)}"; + case 1: + var drivePath = decomposition[0]; + if (drivePath[^1] != Path.DirectorySeparatorChar) + { // turn C: into C:\ + drivePath += Path.DirectorySeparatorChar; + } + + return drivePath; + default: return Path.Combine(decomposition.ToArray()); } - - if (decomp.Count == 1) - { - var drivePath = decomp[0]; - if (drivePath[^1] != Path.DirectorySeparatorChar) - { // turn C: into C:\ - drivePath += Path.DirectorySeparatorChar; - } - - return drivePath; - } - - return Path.Combine(decomp.ToArray()); } private static FileStruct GetFile(FileInfo file, string path) - { - return new FileStruct + => new() { FileName = file.Name, FilePath = path, @@ -76,11 +127,9 @@ public partial class FileDialog Type = FileStructType.File, Ext = file.Extension.Trim('.'), }; - } private static FileStruct GetDir(DirectoryInfo dir, string path) - { - return new FileStruct + => new() { FileName = dir.Name, FilePath = path, @@ -90,136 +139,191 @@ public partial class FileDialog Type = FileStructType.Directory, Ext = string.Empty, }; - } private static int SortByFileNameDesc(FileStruct a, FileStruct b) { - if (a.FileName[0] == '.' && b.FileName[0] != '.') + switch (a.FileName, b.FileName) { - return 1; + case ("..", ".."): return 0; + case ("..", _): return -1; + case (_, ".."): return 1; } - if (a.FileName[0] != '.' && b.FileName[0] == '.') + if (a.FileName[0] is '.') { - return -1; - } - - if (a.FileName[0] == '.' && b.FileName[0] == '.') - { - if (a.FileName.Length == 1) - { - return -1; - } - - if (b.FileName.Length == 1) + if (b.FileName[0] is not '.') { return 1; } - return -1 * string.Compare(a.FileName[1..], b.FileName[1..]); + if (a.FileName.Length is 1) + { + return -1; + } + + if (b.FileName.Length is 1) + { + return 1; + } + + return -1 * string.Compare(a.FileName[1..], b.FileName[1..], StringComparison.CurrentCulture); + } + + if (b.FileName[0] is '.') + { + return -1; } if (a.Type != b.Type) { - return a.Type == FileStructType.Directory ? 1 : -1; + return a.Type is FileStructType.Directory ? 1 : -1; } - return -1 * string.Compare(a.FileName, b.FileName); + return -string.Compare(a.FileName, b.FileName, StringComparison.CurrentCulture); } private static int SortByFileNameAsc(FileStruct a, FileStruct b) { - if (a.FileName[0] == '.' && b.FileName[0] != '.') + switch (a.FileName, b.FileName) { - return -1; + case ("..", ".."): return 0; + case ("..", _): return -1; + case (_, ".."): return 1; } - if (a.FileName[0] != '.' && b.FileName[0] == '.') + if (a.FileName[0] is '.') { - return 1; - } - - if (a.FileName[0] == '.' && b.FileName[0] == '.') - { - if (a.FileName.Length == 1) - { - return 1; - } - - if (b.FileName.Length == 1) + if (b.FileName[0] is not '.') { return -1; } - return string.Compare(a.FileName[1..], b.FileName[1..]); + if (a.FileName.Length is 1) + { + return 1; + } + + if (b.FileName.Length is 1) + { + return -1; + } + + return string.Compare(a.FileName[1..], b.FileName[1..], StringComparison.CurrentCulture); + } + + if (b.FileName[0] is '.') + { + return 1; } if (a.Type != b.Type) { - return a.Type == FileStructType.Directory ? -1 : 1; + return a.Type is FileStructType.Directory ? -1 : 1; } - return string.Compare(a.FileName, b.FileName); + return string.Compare(a.FileName, b.FileName, StringComparison.CurrentCulture); } private static int SortByTypeDesc(FileStruct a, FileStruct b) { + switch (a.FileName, b.FileName) + { + case ("..", ".."): return 0; + case ("..", _): return -1; + case (_, ".."): return 1; + } + if (a.Type != b.Type) { return (a.Type == FileStructType.Directory) ? 1 : -1; } - return string.Compare(a.Ext, b.Ext); + return string.Compare(a.Ext, b.Ext, StringComparison.CurrentCulture); } private static int SortByTypeAsc(FileStruct a, FileStruct b) { + switch (a.FileName, b.FileName) + { + case ("..", ".."): return 0; + case ("..", _): return -1; + case (_, ".."): return 1; + } + if (a.Type != b.Type) { return (a.Type == FileStructType.Directory) ? -1 : 1; } - return -1 * string.Compare(a.Ext, b.Ext); + return -string.Compare(a.Ext, b.Ext, StringComparison.CurrentCulture); } private static int SortBySizeDesc(FileStruct a, FileStruct b) { - if (a.Type != b.Type) + switch (a.FileName, b.FileName) { - return (a.Type == FileStructType.Directory) ? 1 : -1; + case ("..", ".."): return 0; + case ("..", _): return -1; + case (_, ".."): return 1; } - return (a.FileSize > b.FileSize) ? 1 : -1; + if (a.Type != b.Type) + { + return (a.Type is FileStructType.Directory) ? 1 : -1; + } + + return a.FileSize.CompareTo(b.FileSize); } private static int SortBySizeAsc(FileStruct a, FileStruct b) { - if (a.Type != b.Type) + switch (a.FileName, b.FileName) { - return (a.Type == FileStructType.Directory) ? -1 : 1; + case ("..", ".."): return 0; + case ("..", _): return -1; + case (_, ".."): return 1; } - return (a.FileSize > b.FileSize) ? -1 : 1; + if (a.Type != b.Type) + { + return (a.Type is FileStructType.Directory) ? -1 : 1; + } + + return -a.FileSize.CompareTo(b.FileSize); } private static int SortByDateDesc(FileStruct a, FileStruct b) { + switch (a.FileName, b.FileName) + { + case ("..", ".."): return 0; + case ("..", _): return -1; + case (_, ".."): return 1; + } + if (a.Type != b.Type) { return (a.Type == FileStructType.Directory) ? 1 : -1; } - return string.Compare(a.FileModifiedDate, b.FileModifiedDate); + return string.Compare(a.FileModifiedDate, b.FileModifiedDate, StringComparison.CurrentCulture); } private static int SortByDateAsc(FileStruct a, FileStruct b) { + switch (a.FileName, b.FileName) + { + case ("..", ".."): return 0; + case ("..", _): return -1; + case (_, ".."): return 1; + } + if (a.Type != b.Type) { return (a.Type == FileStructType.Directory) ? -1 : 1; } - return -1 * string.Compare(a.FileModifiedDate, b.FileModifiedDate); + return -string.Compare(a.FileModifiedDate, b.FileModifiedDate, StringComparison.CurrentCulture); } private bool CreateDir(string dirPath) @@ -336,52 +440,17 @@ public partial class FileDialog this.quickAccess.Add(new SideBarItem("Videos", Environment.GetFolderPath(Environment.SpecialFolder.MyVideos), FontAwesomeIcon.Video)); } - private void SortFields(SortingField sortingField, bool canChangeOrder = false) - { - switch (sortingField) + private SortingField GetNewSorting(int column) + => column switch { - case SortingField.FileName: - if (canChangeOrder && sortingField == this.currentSortingField) - { - this.sortDescending[0] = !this.sortDescending[0]; - } - - this.files.Sort(this.sortDescending[0] ? SortByFileNameDesc : SortByFileNameAsc); - break; - - case SortingField.Type: - if (canChangeOrder && sortingField == this.currentSortingField) - { - this.sortDescending[1] = !this.sortDescending[1]; - } - - this.files.Sort(this.sortDescending[1] ? SortByTypeDesc : SortByTypeAsc); - break; - - case SortingField.Size: - if (canChangeOrder && sortingField == this.currentSortingField) - { - this.sortDescending[2] = !this.sortDescending[2]; - } - - this.files.Sort(this.sortDescending[2] ? SortBySizeDesc : SortBySizeAsc); - break; - - case SortingField.Date: - if (canChangeOrder && sortingField == this.currentSortingField) - { - this.sortDescending[3] = !this.sortDescending[3]; - } - - this.files.Sort(this.sortDescending[3] ? SortByDateDesc : SortByDateAsc); - break; - } - - if (sortingField != SortingField.None) - { - this.currentSortingField = sortingField; - } - - this.ApplyFilteringOnFileList(); - } + 0 when this.currentSortingField is SortingField.FileName => SortingField.FileNameDescending, + 0 => SortingField.FileName, + 1 when this.currentSortingField is SortingField.Type => SortingField.TypeDescending, + 1 => SortingField.Type, + 2 when this.currentSortingField is SortingField.Size => SortingField.SizeDescending, + 2 => SortingField.Size, + 3 when this.currentSortingField is SortingField.Date => SortingField.DateDescending, + 3 => SortingField.Date, + _ => SortingField.None, + }; } diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs index e4747b1e6..608455650 100644 --- a/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs +++ b/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs @@ -373,22 +373,8 @@ public partial class FileDialog ImGui.PopID(); if (ImGui.IsItemClicked()) { - if (column == 0) - { - this.SortFields(SortingField.FileName, true); - } - else if (column == 1) - { - this.SortFields(SortingField.Type, true); - } - else if (column == 2) - { - this.SortFields(SortingField.Size, true); - } - else - { - this.SortFields(SortingField.Date, true); - } + var sorting = this.GetNewSorting(column); + this.SortFields(sorting); } } diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs index ae9c8ef38..12a391903 100644 --- a/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs +++ b/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs @@ -9,9 +9,15 @@ namespace Dalamud.Interface.ImGuiFileDialog; /// public class FileDialogManager { + /// Gets or sets a function that returns the desired default sort order in the file dialog. + public Func? GetDefaultSortOrder { get; set; } + + /// Gets or sets an action to invoke when a file dialog changes its sort order. + public Action? SetDefaultSortOrder { get; set; } + #pragma warning disable SA1401 /// Additional quick access items for the side bar. - public readonly List<(string Name, string Path, FontAwesomeIcon Icon, int Position)> CustomSideBarItems = new(); + public readonly List<(string Name, string Path, FontAwesomeIcon Icon, int Position)> CustomSideBarItems = []; /// Additional flags with which to draw the window. public ImGuiWindowFlags AddedWindowFlags = ImGuiWindowFlags.None; @@ -189,10 +195,41 @@ public class FileDialogManager this.callback = callback as Action; } + if (this.dialog is not null) + { + this.dialog.SortOrderChanged -= this.OnSortOrderChange; + } + this.dialog = new FileDialog(id, title, filters, path, defaultFileName, defaultExtension, selectionCountMax, isModal, flags); + if (this.GetDefaultSortOrder is not null) + { + try + { + var order = this.GetDefaultSortOrder(); + this.dialog.SortFields(order); + } + catch + { + // ignored. + } + } + + this.dialog.SortOrderChanged += this.OnSortOrderChange; this.dialog.WindowFlags |= this.AddedWindowFlags; foreach (var (name, location, icon, position) in this.CustomSideBarItems) this.dialog.SetQuickAccess(name, location, icon, position); this.dialog.Show(); } + + private void OnSortOrderChange(FileDialog.SortingField sortOrder) + { + try + { + this.SetDefaultSortOrder?.Invoke(sortOrder); + } + catch + { + // ignored. + } + } }