Add configurable default sorting to FileDialog. (#2233)

This commit is contained in:
Ottermandias 2025-04-09 22:14:47 +02:00 committed by GitHub
parent bc18198435
commit d3dd3ab7c7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 239 additions and 147 deletions

View file

@ -2,6 +2,8 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Dalamud.Utility;
namespace Dalamud.Interface.ImGuiFileDialog; namespace Dalamud.Interface.ImGuiFileDialog;
/// <summary> /// <summary>
@ -13,11 +15,44 @@ public partial class FileDialog
private readonly DriveListLoader driveListLoader = new(); private readonly DriveListLoader driveListLoader = new();
private List<FileStruct> files = new(); private readonly List<FileStruct> files = [];
private List<FileStruct> filteredFiles = new(); private readonly List<FileStruct> filteredFiles = [];
private SortingField currentSortingField = SortingField.FileName; private SortingField currentSortingField = SortingField.FileName;
private bool[] sortDescending = { false, false, false, false };
/// <summary> Fired whenever the sorting field changes. </summary>
public event Action<SortingField>? SortOrderChanged;
/// <summary> The sorting type of the file selector. </summary>
public enum SortingField
{
/// <summary> No sorting specified. </summary>
None = 0,
/// <summary> Sort for ascending file names in culture-specific order. </summary>
FileName = 1,
/// <summary> Sort for ascending file types in culture-specific order. </summary>
Type = 2,
/// <summary> Sort for ascending file sizes. </summary>
Size = 3,
/// <summary> Sort for ascending last update dates. </summary>
Date = 4,
/// <summary> Sort for descending file names in culture-specific order. </summary>
FileNameDescending = 5,
/// <summary> Sort for descending file types in culture-specific order. </summary>
TypeDescending = 6,
/// <summary> Sort for descending file sizes. </summary>
SizeDescending = 7,
/// <summary> Sort for descending last update dates. </summary>
DateDescending = 8,
}
private enum FileStructType private enum FileStructType
{ {
@ -25,48 +60,64 @@ public partial class FileDialog
Directory, Directory,
} }
private enum SortingField /// <summary> Specify the current and subsequent sort order. </summary>
/// <param name="sortingField"> The new sort order. None is invalid and will not have any effect. </param>
public void SortFields(SortingField sortingField)
{ {
None, Comparison<FileStruct>? sortFunc = sortingField switch
FileName, {
Type, SortingField.FileName => SortByFileNameAsc,
Size, SortingField.FileNameDescending => SortByFileNameDesc,
Date, 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<string> decomp) private static string ComposeNewPath(List<string> decomposition)
{ {
// Handle UNC paths (network paths) switch (decomposition.Count)
if (decomp.Count >= 2 && string.IsNullOrEmpty(decomp[0]) && string.IsNullOrEmpty(decomp[1]))
{ {
var pathParts = new List<string>(decomp); // Handle UNC paths (network paths)
pathParts.RemoveRange(0, 2); case >= 2 when string.IsNullOrEmpty(decomposition[0]) && string.IsNullOrEmpty(decomposition[1]):
// Can not access server level or UNC root var pathParts = new List<string>(decomposition);
if (pathParts.Count <= 1) pathParts.RemoveRange(0, 2);
{
return string.Empty;
}
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) private static FileStruct GetFile(FileInfo file, string path)
{ => new()
return new FileStruct
{ {
FileName = file.Name, FileName = file.Name,
FilePath = path, FilePath = path,
@ -76,11 +127,9 @@ public partial class FileDialog
Type = FileStructType.File, Type = FileStructType.File,
Ext = file.Extension.Trim('.'), Ext = file.Extension.Trim('.'),
}; };
}
private static FileStruct GetDir(DirectoryInfo dir, string path) private static FileStruct GetDir(DirectoryInfo dir, string path)
{ => new()
return new FileStruct
{ {
FileName = dir.Name, FileName = dir.Name,
FilePath = path, FilePath = path,
@ -90,136 +139,191 @@ public partial class FileDialog
Type = FileStructType.Directory, Type = FileStructType.Directory,
Ext = string.Empty, Ext = string.Empty,
}; };
}
private static int SortByFileNameDesc(FileStruct a, FileStruct b) 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 (b.FileName[0] is not '.')
}
if (a.FileName[0] == '.' && b.FileName[0] == '.')
{
if (a.FileName.Length == 1)
{
return -1;
}
if (b.FileName.Length == 1)
{ {
return 1; 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) 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) 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 (b.FileName[0] is not '.')
}
if (a.FileName[0] == '.' && b.FileName[0] == '.')
{
if (a.FileName.Length == 1)
{
return 1;
}
if (b.FileName.Length == 1)
{ {
return -1; 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) 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) 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) if (a.Type != b.Type)
{ {
return (a.Type == FileStructType.Directory) ? 1 : -1; 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) 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) if (a.Type != b.Type)
{ {
return (a.Type == FileStructType.Directory) ? -1 : 1; 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) 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) 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) 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) if (a.Type != b.Type)
{ {
return (a.Type == FileStructType.Directory) ? 1 : -1; 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) 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) if (a.Type != b.Type)
{ {
return (a.Type == FileStructType.Directory) ? -1 : 1; 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) 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)); this.quickAccess.Add(new SideBarItem("Videos", Environment.GetFolderPath(Environment.SpecialFolder.MyVideos), FontAwesomeIcon.Video));
} }
private void SortFields(SortingField sortingField, bool canChangeOrder = false) private SortingField GetNewSorting(int column)
{ => column switch
switch (sortingField)
{ {
case SortingField.FileName: 0 when this.currentSortingField is SortingField.FileName => SortingField.FileNameDescending,
if (canChangeOrder && sortingField == this.currentSortingField) 0 => SortingField.FileName,
{ 1 when this.currentSortingField is SortingField.Type => SortingField.TypeDescending,
this.sortDescending[0] = !this.sortDescending[0]; 1 => SortingField.Type,
} 2 when this.currentSortingField is SortingField.Size => SortingField.SizeDescending,
2 => SortingField.Size,
this.files.Sort(this.sortDescending[0] ? SortByFileNameDesc : SortByFileNameAsc); 3 when this.currentSortingField is SortingField.Date => SortingField.DateDescending,
break; 3 => SortingField.Date,
_ => SortingField.None,
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();
}
} }

View file

@ -373,22 +373,8 @@ public partial class FileDialog
ImGui.PopID(); ImGui.PopID();
if (ImGui.IsItemClicked()) if (ImGui.IsItemClicked())
{ {
if (column == 0) var sorting = this.GetNewSorting(column);
{ this.SortFields(sorting);
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);
}
} }
} }

View file

@ -9,9 +9,15 @@ namespace Dalamud.Interface.ImGuiFileDialog;
/// </summary> /// </summary>
public class FileDialogManager public class FileDialogManager
{ {
/// <summary> Gets or sets a function that returns the desired default sort order in the file dialog. </summary>
public Func<FileDialog.SortingField>? GetDefaultSortOrder { get; set; }
/// <summary> Gets or sets an action to invoke when a file dialog changes its sort order. </summary>
public Action<FileDialog.SortingField>? SetDefaultSortOrder { get; set; }
#pragma warning disable SA1401 #pragma warning disable SA1401
/// <summary> Additional quick access items for the side bar.</summary> /// <summary> Additional quick access items for the side bar.</summary>
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 = [];
/// <summary> Additional flags with which to draw the window. </summary> /// <summary> Additional flags with which to draw the window. </summary>
public ImGuiWindowFlags AddedWindowFlags = ImGuiWindowFlags.None; public ImGuiWindowFlags AddedWindowFlags = ImGuiWindowFlags.None;
@ -189,10 +195,41 @@ public class FileDialogManager
this.callback = callback as Action<bool, string>; this.callback = callback as Action<bool, string>;
} }
if (this.dialog is not null)
{
this.dialog.SortOrderChanged -= this.OnSortOrderChange;
}
this.dialog = new FileDialog(id, title, filters, path, defaultFileName, defaultExtension, selectionCountMax, isModal, flags); 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; this.dialog.WindowFlags |= this.AddedWindowFlags;
foreach (var (name, location, icon, position) in this.CustomSideBarItems) foreach (var (name, location, icon, position) in this.CustomSideBarItems)
this.dialog.SetQuickAccess(name, location, icon, position); this.dialog.SetQuickAccess(name, location, icon, position);
this.dialog.Show(); this.dialog.Show();
} }
private void OnSortOrderChange(FileDialog.SortingField sortOrder)
{
try
{
this.SetDefaultSortOrder?.Invoke(sortOrder);
}
catch
{
// ignored.
}
}
} }