diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialog.Files.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialog.Files.cs
new file mode 100644
index 000000000..c7398a6b3
--- /dev/null
+++ b/Dalamud/Interface/ImGuiFileDialog/FileDialog.Files.cs
@@ -0,0 +1,415 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+using Dalamud.Interface;
+
+namespace Dalamud.Interface.ImGuiFileDialog
+{
+ ///
+ /// A file or folder picker.
+ ///
+ public partial class FileDialog
+ {
+ private readonly object filesLock = new();
+
+ private List files = new();
+ private List filteredFiles = new();
+
+ private SortingField currentSortingField = SortingField.FileName;
+ private bool[] sortDescending = new[] { false, false, false, false };
+
+ private enum FileStructType
+ {
+ File,
+ Directory,
+ }
+
+ private enum SortingField
+ {
+ None,
+ FileName,
+ Type,
+ Size,
+ Date,
+ }
+
+ private static string ComposeNewPath(List decomp)
+ {
+ 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
+ {
+ FileName = file.Name,
+ FilePath = path,
+ FileModifiedDate = FormatModifiedDate(file.LastWriteTime),
+ FileSize = file.Length,
+ FormattedFileSize = BytesToString(file.Length),
+ Type = FileStructType.File,
+ Ext = file.Extension.Trim('.'),
+ };
+ }
+
+ private static FileStruct GetDir(DirectoryInfo dir, string path)
+ {
+ return new FileStruct
+ {
+ FileName = dir.Name,
+ FilePath = path,
+ FileModifiedDate = FormatModifiedDate(dir.LastWriteTime),
+ FileSize = 0,
+ FormattedFileSize = string.Empty,
+ Type = FileStructType.Directory,
+ Ext = string.Empty,
+ };
+ }
+
+ private static int SortByFileNameDesc(FileStruct a, FileStruct b)
+ {
+ if (a.FileName[0] == '.' && b.FileName[0] != '.')
+ {
+ return 1;
+ }
+
+ if (a.FileName[0] != '.' && b.FileName[0] == '.')
+ {
+ return -1;
+ }
+
+ if (a.FileName[0] == '.' && b.FileName[0] == '.')
+ {
+ if (a.FileName.Length == 1)
+ {
+ return -1;
+ }
+
+ if (b.FileName.Length == 1)
+ {
+ return 1;
+ }
+
+ return -1 * string.Compare(a.FileName[1..], b.FileName[1..]);
+ }
+
+ if (a.Type != b.Type)
+ {
+ return a.Type == FileStructType.Directory ? 1 : -1;
+ }
+
+ return -1 * string.Compare(a.FileName, b.FileName);
+ }
+
+ private static int SortByFileNameAsc(FileStruct a, FileStruct b)
+ {
+ if (a.FileName[0] == '.' && b.FileName[0] != '.')
+ {
+ return -1;
+ }
+
+ if (a.FileName[0] != '.' && b.FileName[0] == '.')
+ {
+ return 1;
+ }
+
+ if (a.FileName[0] == '.' && b.FileName[0] == '.')
+ {
+ if (a.FileName.Length == 1)
+ {
+ return 1;
+ }
+
+ if (b.FileName.Length == 1)
+ {
+ return -1;
+ }
+
+ return string.Compare(a.FileName[1..], b.FileName[1..]);
+ }
+
+ if (a.Type != b.Type)
+ {
+ return a.Type == FileStructType.Directory ? -1 : 1;
+ }
+
+ return string.Compare(a.FileName, b.FileName);
+ }
+
+ private static int SortByTypeDesc(FileStruct a, FileStruct b)
+ {
+ if (a.Type != b.Type)
+ {
+ return (a.Type == FileStructType.Directory) ? 1 : -1;
+ }
+
+ return string.Compare(a.Ext, b.Ext);
+ }
+
+ private static int SortByTypeAsc(FileStruct a, FileStruct b)
+ {
+ if (a.Type != b.Type)
+ {
+ return (a.Type == FileStructType.Directory) ? -1 : 1;
+ }
+
+ return -1 * string.Compare(a.Ext, b.Ext);
+ }
+
+ private static int SortBySizeDesc(FileStruct a, FileStruct b)
+ {
+ if (a.Type != b.Type)
+ {
+ return (a.Type == FileStructType.Directory) ? 1 : -1;
+ }
+
+ return (a.FileSize > b.FileSize) ? 1 : -1;
+ }
+
+ private static int SortBySizeAsc(FileStruct a, FileStruct b)
+ {
+ if (a.Type != b.Type)
+ {
+ return (a.Type == FileStructType.Directory) ? -1 : 1;
+ }
+
+ return (a.FileSize > b.FileSize) ? -1 : 1;
+ }
+
+ private static int SortByDateDesc(FileStruct a, FileStruct b)
+ {
+ if (a.Type != b.Type)
+ {
+ return (a.Type == FileStructType.Directory) ? 1 : -1;
+ }
+
+ return string.Compare(a.FileModifiedDate, b.FileModifiedDate);
+ }
+
+ private static int SortByDateAsc(FileStruct a, FileStruct b)
+ {
+ if (a.Type != b.Type)
+ {
+ return (a.Type == FileStructType.Directory) ? -1 : 1;
+ }
+
+ return -1 * string.Compare(a.FileModifiedDate, b.FileModifiedDate);
+ }
+
+ private bool CreateDir(string dirPath)
+ {
+ var newPath = Path.Combine(this.currentPath, dirPath);
+ if (string.IsNullOrEmpty(newPath))
+ {
+ return false;
+ }
+
+ Directory.CreateDirectory(newPath);
+ return true;
+ }
+
+ private void ScanDir(string path)
+ {
+ if (!Directory.Exists(path))
+ {
+ return;
+ }
+
+ if (this.pathDecomposition.Count == 0)
+ {
+ this.SetCurrentDir(path);
+ }
+
+ if (this.pathDecomposition.Count > 0)
+ {
+ this.files.Clear();
+
+ if (this.pathDecomposition.Count > 1)
+ {
+ this.files.Add(new FileStruct
+ {
+ Type = FileStructType.Directory,
+ FilePath = path,
+ FileName = "..",
+ FileSize = 0,
+ FileModifiedDate = string.Empty,
+ FormattedFileSize = string.Empty,
+ Ext = string.Empty,
+ });
+ }
+
+ var dirInfo = new DirectoryInfo(path);
+
+ var dontShowHidden = this.flags.HasFlag(ImGuiFileDialogFlags.DontShowHiddenFiles);
+
+ foreach (var dir in dirInfo.EnumerateDirectories().OrderBy(d => d.Name))
+ {
+ if (string.IsNullOrEmpty(dir.Name))
+ {
+ continue;
+ }
+
+ if (dontShowHidden && dir.Name[0] == '.')
+ {
+ continue;
+ }
+
+ this.files.Add(GetDir(dir, path));
+ }
+
+ foreach (var file in dirInfo.EnumerateFiles().OrderBy(f => f.Name))
+ {
+ if (string.IsNullOrEmpty(file.Name))
+ {
+ continue;
+ }
+
+ if (dontShowHidden && file.Name[0] == '.')
+ {
+ continue;
+ }
+
+ if (!string.IsNullOrEmpty(file.Extension))
+ {
+ var ext = file.Extension;
+ if (this.filters.Count > 0 && !this.selectedFilter.Empty() && !this.selectedFilter.FilterExists(ext) && this.selectedFilter.Filter != ".*")
+ {
+ continue;
+ }
+ }
+
+ this.files.Add(GetFile(file, path));
+ }
+
+ this.SortFields(this.currentSortingField);
+ }
+ }
+
+ private void SetupSideBar()
+ {
+ var drives = DriveInfo.GetDrives();
+ foreach (var drive in drives)
+ {
+ this.drives.Add(new SideBarItem
+ {
+ Icon = (char)FontAwesomeIcon.Server,
+ Location = drive.Name,
+ Text = drive.Name,
+ });
+ }
+
+ var personal = Path.GetDirectoryName(Environment.GetFolderPath(Environment.SpecialFolder.Personal));
+
+ this.quickAccess.Add(new SideBarItem
+ {
+ Icon = (char)FontAwesomeIcon.Desktop,
+ Location = Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
+ Text = "Desktop",
+ });
+
+ this.quickAccess.Add(new SideBarItem
+ {
+ Icon = (char)FontAwesomeIcon.File,
+ Location = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
+ Text = "Documents",
+ });
+
+ this.quickAccess.Add(new SideBarItem
+ {
+ Icon = (char)FontAwesomeIcon.Download,
+ Location = Path.Combine(personal, "Downloads"),
+ Text = "Downloads",
+ });
+
+ this.quickAccess.Add(new SideBarItem
+ {
+ Icon = (char)FontAwesomeIcon.Star,
+ Location = Environment.GetFolderPath(Environment.SpecialFolder.Favorites),
+ Text = "Favorites",
+ });
+
+ this.quickAccess.Add(new SideBarItem
+ {
+ Icon = (char)FontAwesomeIcon.Music,
+ Location = Environment.GetFolderPath(Environment.SpecialFolder.MyMusic),
+ Text = "Music",
+ });
+
+ this.quickAccess.Add(new SideBarItem
+ {
+ Icon = (char)FontAwesomeIcon.Image,
+ Location = Environment.GetFolderPath(Environment.SpecialFolder.MyPictures),
+ Text = "Pictures",
+ });
+
+ this.quickAccess.Add(new SideBarItem
+ {
+ Icon = (char)FontAwesomeIcon.Video,
+ Location = Environment.GetFolderPath(Environment.SpecialFolder.MyVideos),
+ Text = "Videos",
+ });
+ }
+
+ private void SortFields(SortingField sortingField, bool canChangeOrder = false)
+ {
+ switch (sortingField)
+ {
+ 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();
+ }
+ }
+}
diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialog.Filters.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialog.Filters.cs
new file mode 100644
index 000000000..037155229
--- /dev/null
+++ b/Dalamud/Interface/ImGuiFileDialog/FileDialog.Filters.cs
@@ -0,0 +1,110 @@
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace Dalamud.Interface.ImGuiFileDialog
+{
+ ///
+ /// A file or folder picker.
+ ///
+ public partial class FileDialog
+ {
+ private static Regex filterRegex = new(@"[^,{}]+(\{([^{}]*?)\})?", RegexOptions.Compiled);
+
+ private List filters = new();
+ private FilterStruct selectedFilter;
+
+ private void ParseFilters(string filters)
+ {
+ // ".*,.cpp,.h,.hpp"
+ // "Source files{.cpp,.h,.hpp},Image files{.png,.gif,.jpg,.jpeg},.md"
+
+ this.filters.Clear();
+ if (filters.Length == 0) return;
+
+ var currentFilterFound = false;
+ var matches = filterRegex.Matches(filters);
+ foreach (Match m in matches)
+ {
+ var match = m.Value;
+ var filter = default(FilterStruct);
+
+ if (match.Contains("{"))
+ {
+ var exts = m.Groups[2].Value;
+ filter = new FilterStruct
+ {
+ Filter = match.Split('{')[0],
+ CollectionFilters = new HashSet(exts.Split(',')),
+ };
+ }
+ else
+ {
+ filter = new FilterStruct
+ {
+ Filter = match,
+ CollectionFilters = new(),
+ };
+ }
+
+ this.filters.Add(filter);
+
+ if (!currentFilterFound && filter.Filter == this.selectedFilter.Filter)
+ {
+ currentFilterFound = true;
+ this.selectedFilter = filter;
+ }
+ }
+
+ if (!currentFilterFound && !(this.filters.Count == 0))
+ {
+ this.selectedFilter = this.filters[0];
+ }
+ }
+
+ private void SetSelectedFilterWithExt(string ext)
+ {
+ if (this.filters.Count == 0) return;
+ if (string.IsNullOrEmpty(ext)) return;
+
+ foreach (var filter in this.filters)
+ {
+ if (filter.FilterExists(ext))
+ {
+ this.selectedFilter = filter;
+ }
+ }
+
+ if (this.selectedFilter.Empty())
+ {
+ this.selectedFilter = this.filters[0];
+ }
+ }
+
+ private void ApplyFilteringOnFileList()
+ {
+ lock (this.filesLock)
+ {
+ this.filteredFiles.Clear();
+
+ foreach (var file in this.files)
+ {
+ var show = true;
+ if (!string.IsNullOrEmpty(this.searchBuffer) && !file.FileName.ToLower().Contains(this.searchBuffer.ToLower()))
+ {
+ show = false;
+ }
+
+ if (this.IsDirectoryMode() && file.Type != FileStructType.Directory)
+ {
+ show = false;
+ }
+
+ if (show)
+ {
+ this.filteredFiles.Add(file);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialog.Helpers.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialog.Helpers.cs
new file mode 100644
index 000000000..16bc3e46f
--- /dev/null
+++ b/Dalamud/Interface/ImGuiFileDialog/FileDialog.Helpers.cs
@@ -0,0 +1,26 @@
+using System;
+
+namespace Dalamud.Interface.ImGuiFileDialog
+{
+ ///
+ /// A file or folder picker.
+ ///
+ public partial class FileDialog
+ {
+ private static string FormatModifiedDate(DateTime date)
+ {
+ return date.ToString("yyyy/MM/dd HH:mm");
+ }
+
+ private static string BytesToString(long byteCount)
+ {
+ string[] suf = { " B", " KB", " MB", " GB", " TB" };
+ if (byteCount == 0)
+ return "0" + suf[0];
+ var bytes = Math.Abs(byteCount);
+ var place = Convert.ToInt32(Math.Floor(Math.Log(bytes, 1024)));
+ var num = Math.Round(bytes / Math.Pow(1024, place), 1);
+ return (Math.Sign(byteCount) * num).ToString() + suf[place];
+ }
+ }
+}
diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialog.Structs.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialog.Structs.cs
new file mode 100644
index 000000000..475147518
--- /dev/null
+++ b/Dalamud/Interface/ImGuiFileDialog/FileDialog.Structs.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+
+namespace Dalamud.Interface.ImGuiFileDialog
+{
+ ///
+ /// A file or folder picker.
+ ///
+ public partial class FileDialog
+ {
+ private struct FileStruct
+ {
+ public FileStructType Type;
+ public string FilePath;
+ public string FileName;
+ public string Ext;
+ public long FileSize;
+ public string FormattedFileSize;
+ public string FileModifiedDate;
+ }
+
+ private struct SideBarItem
+ {
+ public char Icon;
+ public string Text;
+ public string Location;
+ }
+
+ private struct FilterStruct
+ {
+ public string Filter;
+ public HashSet CollectionFilters;
+
+ public void Clear()
+ {
+ this.Filter = string.Empty;
+ this.CollectionFilters.Clear();
+ }
+
+ public bool Empty()
+ {
+ return string.IsNullOrEmpty(this.Filter) && ((this.CollectionFilters == null) || (this.CollectionFilters.Count == 0));
+ }
+
+ public bool FilterExists(string filter)
+ {
+ return (this.Filter == filter) || (this.CollectionFilters != null && this.CollectionFilters.Contains(filter));
+ }
+ }
+
+ private struct IconColorItem
+ {
+ public char Icon;
+ public Vector4 Color;
+ }
+ }
+}
diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs
new file mode 100644
index 000000000..9d30a5312
--- /dev/null
+++ b/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs
@@ -0,0 +1,861 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Numerics;
+
+using ImGuiNET;
+
+namespace Dalamud.Interface.ImGuiFileDialog
+{
+ ///
+ /// A file or folder picker.
+ ///
+ public partial class FileDialog
+ {
+ private static Vector4 pathDecompColor = new(0.188f, 0.188f, 0.2f, 1f);
+ private static Vector4 selectedTextColor = new(1.00000000000f, 0.33333333333f, 0.33333333333f, 1f);
+ private static Vector4 dirTextColor = new(0.54509803922f, 0.91372549020f, 0.99215686275f, 1f);
+ private static Vector4 codeTextColor = new(0.94509803922f, 0.98039215686f, 0.54901960784f, 1f);
+ private static Vector4 miscTextColor = new(1.00000000000f, 0.47450980392f, 0.77647058824f, 1f);
+ private static Vector4 imageTextColor = new(0.31372549020f, 0.98039215686f, 0.48235294118f, 1f);
+ private static Vector4 standardTextColor = new(1f);
+
+ private static Dictionary iconMap;
+
+ ///
+ /// Draws the dialog.
+ ///
+ /// Whether a selection or cancel action was performed.
+ public bool Draw()
+ {
+ if (!this.visible) return false;
+
+ var res = false;
+ var name = this.title + "###" + this.id;
+
+ bool windowVisible;
+ this.isOk = false;
+ this.wantsToQuit = false;
+
+ this.ResetEvents();
+
+ ImGui.SetNextWindowSize(new Vector2(800, 500), ImGuiCond.FirstUseEver);
+
+ if (this.isModal && !this.okResultToConfirm)
+ {
+ ImGui.OpenPopup(name);
+ windowVisible = ImGui.BeginPopupModal(name, ref this.visible, ImGuiWindowFlags.NoScrollbar);
+ }
+ else
+ {
+ windowVisible = ImGui.Begin(name, ref this.visible, ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoNav);
+ }
+
+ if (windowVisible)
+ {
+ if (!this.visible)
+ { // window closed
+ this.isOk = false;
+ return true;
+ }
+
+ if (this.selectedFilter.Empty() && (this.filters.Count > 0))
+ {
+ this.selectedFilter = this.filters[0];
+ }
+
+ if (this.files.Count == 0)
+ {
+ if (!string.IsNullOrEmpty(this.defaultFileName))
+ {
+ this.SetDefaultFileName();
+ this.SetSelectedFilterWithExt(this.defaultExtension);
+ }
+ else if (this.IsDirectoryMode())
+ {
+ this.SetDefaultFileName();
+ }
+
+ this.ScanDir(this.currentPath);
+ }
+
+ this.DrawHeader();
+ this.DrawContent();
+ res = this.DrawFooter();
+
+ if (this.isModal && !this.okResultToConfirm)
+ {
+ ImGui.EndPopup();
+ }
+ }
+
+ if (!this.isModal || this.okResultToConfirm)
+ {
+ ImGui.End();
+ }
+
+ return this.ConfirmOrOpenOverWriteFileDialogIfNeeded(res);
+ }
+
+ private static void AddToIconMap(string[] extensions, char icon, Vector4 color)
+ {
+ foreach (var ext in extensions)
+ {
+ iconMap[ext] = new IconColorItem
+ {
+ Icon = icon,
+ Color = color,
+ };
+ }
+ }
+
+ private static IconColorItem GetIcon(string ext)
+ {
+ if (iconMap == null)
+ {
+ iconMap = new();
+ AddToIconMap(new[] { "mp4", "gif", "mov", "avi" }, (char)FontAwesomeIcon.FileVideo, miscTextColor);
+ AddToIconMap(new[] { "pdf" }, (char)FontAwesomeIcon.FilePdf, miscTextColor);
+ AddToIconMap(new[] { "png", "jpg", "jpeg", "tiff" }, (char)FontAwesomeIcon.FileImage, imageTextColor);
+ AddToIconMap(new[] { "cs", "json", "cpp", "h", "py", "xml", "yaml", "js", "html", "css", "ts", "java" }, (char)FontAwesomeIcon.FileCode, codeTextColor);
+ AddToIconMap(new[] { "txt", "md" }, (char)FontAwesomeIcon.FileAlt, standardTextColor);
+ AddToIconMap(new[] { "zip", "7z", "gz", "tar" }, (char)FontAwesomeIcon.FileArchive, miscTextColor);
+ AddToIconMap(new[] { "mp3", "m4a", "ogg", "wav" }, (char)FontAwesomeIcon.FileAudio, miscTextColor);
+ AddToIconMap(new[] { "csv" }, (char)FontAwesomeIcon.FileCsv, miscTextColor);
+ }
+
+ return iconMap.TryGetValue(ext.ToLower(), out var icon) ? icon : new IconColorItem
+ {
+ Icon = (char)FontAwesomeIcon.File,
+ Color = standardTextColor,
+ };
+ }
+
+ private void DrawHeader()
+ {
+ this.DrawPathComposer();
+
+ ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 2);
+ ImGui.Separator();
+ ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 2);
+
+ this.DrawSearchBar();
+ }
+
+ private void DrawPathComposer()
+ {
+ ImGui.PushFont(UiBuilder.IconFont);
+ if (ImGui.Button($"{(this.pathInputActivated ? (char)FontAwesomeIcon.Times : (char)FontAwesomeIcon.Edit)}"))
+ {
+ this.pathInputActivated = !this.pathInputActivated;
+ }
+
+ ImGui.PopFont();
+
+ ImGui.SameLine();
+
+ if (this.pathDecomposition.Count > 0)
+ {
+ ImGui.SameLine();
+
+ if (this.pathInputActivated)
+ {
+ ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X);
+ ImGui.InputText("##pathedit", ref this.pathInputBuffer, 255);
+ ImGui.PopItemWidth();
+ }
+ else
+ {
+ for (var idx = 0; idx < this.pathDecomposition.Count; idx++)
+ {
+ if (idx > 0)
+ {
+ ImGui.SameLine();
+ ImGui.SetCursorPosX(ImGui.GetCursorPosX() - 3);
+ }
+
+ ImGui.PushID(idx);
+ ImGui.PushStyleColor(ImGuiCol.Button, pathDecompColor);
+ var click = ImGui.Button(this.pathDecomposition[idx]);
+ ImGui.PopStyleColor();
+ ImGui.PopID();
+
+ if (click)
+ {
+ this.currentPath = ComposeNewPath(this.pathDecomposition.GetRange(0, idx + 1));
+ this.pathClicked = true;
+ break;
+ }
+
+ if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
+ {
+ this.pathInputBuffer = ComposeNewPath(this.pathDecomposition.GetRange(0, idx + 1));
+ this.pathInputActivated = true;
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ private void DrawSearchBar()
+ {
+ ImGui.PushFont(UiBuilder.IconFont);
+ if (ImGui.Button($"{(char)FontAwesomeIcon.Home}"))
+ {
+ this.SetPath(".");
+ }
+
+ ImGui.PopFont();
+
+ if (ImGui.IsItemHovered())
+ {
+ ImGui.SetTooltip("Reset to current directory");
+ }
+
+ ImGui.SameLine();
+
+ this.DrawDirectoryCreation();
+
+ if (!this.createDirectoryMode)
+ {
+ ImGui.SameLine();
+ ImGui.Text("Search :");
+ ImGui.SameLine();
+ ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X);
+ var edited = ImGui.InputText("##InputImGuiFileDialogSearchField", ref this.searchBuffer, 255);
+ ImGui.PopItemWidth();
+ if (edited)
+ {
+ this.ApplyFilteringOnFileList();
+ }
+ }
+ }
+
+ private void DrawDirectoryCreation()
+ {
+ if (this.flags.HasFlag(ImGuiFileDialogFlags.DisableCreateDirectoryButton)) return;
+
+ ImGui.PushFont(UiBuilder.IconFont);
+ if (ImGui.Button($"{(char)FontAwesomeIcon.FolderPlus}"))
+ {
+ if (!this.createDirectoryMode)
+ {
+ this.createDirectoryMode = true;
+ this.createDirectoryBuffer = string.Empty;
+ }
+ }
+
+ ImGui.PopFont();
+
+ if (ImGui.IsItemHovered())
+ {
+ ImGui.SetTooltip("Create Directory");
+ }
+
+ if (this.createDirectoryMode)
+ {
+ ImGui.SameLine();
+ ImGui.Text("New Directory Name");
+
+ ImGui.SameLine();
+ ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X - 100f);
+ ImGui.InputText("##DirectoryFileName", ref this.createDirectoryBuffer, 255);
+ ImGui.PopItemWidth();
+
+ ImGui.SameLine();
+
+ if (ImGui.Button("Ok"))
+ {
+ if (this.CreateDir(this.createDirectoryBuffer))
+ {
+ this.SetPath(Path.Combine(this.currentPath, this.createDirectoryBuffer));
+ }
+
+ this.createDirectoryMode = false;
+ }
+
+ ImGui.SameLine();
+
+ if (ImGui.Button("Cancel"))
+ {
+ this.createDirectoryMode = false;
+ }
+ }
+ }
+
+ private void DrawContent()
+ {
+ var size = ImGui.GetContentRegionAvail() - new Vector2(0, this.footerHeight);
+
+ if (!this.flags.HasFlag(ImGuiFileDialogFlags.HideSideBar))
+ {
+ ImGui.BeginChild("##FileDialog_ColumnChild", size);
+ ImGui.Columns(2, "##FileDialog_Columns");
+
+ this.DrawSideBar(new Vector2(150, size.Y));
+
+ ImGui.SetColumnWidth(0, 150);
+ ImGui.NextColumn();
+
+ this.DrawFileListView(size - new Vector2(160, 0));
+
+ ImGui.Columns(1);
+ ImGui.EndChild();
+ }
+ else
+ {
+ this.DrawFileListView(size);
+ }
+ }
+
+ private void DrawSideBar(Vector2 size)
+ {
+ ImGui.BeginChild("##FileDialog_SideBar", size);
+
+ ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 5);
+
+ foreach (var drive in this.drives)
+ {
+ ImGui.PushFont(UiBuilder.IconFont);
+ if (ImGui.Selectable($"{drive.Icon}##{drive.Text}", drive.Text == this.selectedSideBar))
+ {
+ this.SetPath(drive.Location);
+ this.selectedSideBar = drive.Text;
+ }
+
+ ImGui.PopFont();
+
+ ImGui.SameLine(25);
+
+ ImGui.Text(drive.Text);
+ }
+
+ foreach (var quick in this.quickAccess)
+ {
+ ImGui.PushFont(UiBuilder.IconFont);
+ if (ImGui.Selectable($"{quick.Icon}##{quick.Text}", quick.Text == this.selectedSideBar))
+ {
+ this.SetPath(quick.Location);
+ this.selectedSideBar = quick.Text;
+ }
+
+ ImGui.PopFont();
+
+ ImGui.SameLine(25);
+
+ ImGui.Text(quick.Text);
+ }
+
+ ImGui.EndChild();
+ }
+
+ private unsafe void DrawFileListView(Vector2 size)
+ {
+ ImGui.BeginChild("##FileDialog_FileList", size);
+
+ var tableFlags = ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.Hideable | ImGuiTableFlags.ScrollY | ImGuiTableFlags.NoHostExtendX;
+ if (ImGui.BeginTable("##FileTable", 4, tableFlags, size))
+ {
+ ImGui.TableSetupScrollFreeze(0, 1);
+
+ var hideType = this.flags.HasFlag(ImGuiFileDialogFlags.HideColumnType);
+ var hideSize = this.flags.HasFlag(ImGuiFileDialogFlags.HideColumnSize);
+ var hideDate = this.flags.HasFlag(ImGuiFileDialogFlags.HideColumnDate);
+
+ ImGui.TableSetupColumn(" File Name", ImGuiTableColumnFlags.WidthStretch, -1, 0);
+ ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthFixed | (hideType ? ImGuiTableColumnFlags.DefaultHide : ImGuiTableColumnFlags.None), -1, 1);
+ ImGui.TableSetupColumn("Size", ImGuiTableColumnFlags.WidthFixed | (hideSize ? ImGuiTableColumnFlags.DefaultHide : ImGuiTableColumnFlags.None), -1, 2);
+ ImGui.TableSetupColumn("Date", ImGuiTableColumnFlags.WidthFixed | (hideDate ? ImGuiTableColumnFlags.DefaultHide : ImGuiTableColumnFlags.None), -1, 3);
+
+ ImGui.TableNextRow(ImGuiTableRowFlags.Headers);
+ for (var column = 0; column < 4; column++)
+ {
+ ImGui.TableSetColumnIndex(column);
+ var columnName = ImGui.TableGetColumnName(column);
+ ImGui.PushID(column);
+ ImGui.TableHeader(columnName);
+ 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);
+ }
+ }
+ }
+
+ if (this.filteredFiles.Count > 0)
+ {
+ ImGuiListClipperPtr clipper;
+ unsafe
+ {
+ clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper());
+ }
+
+ lock (this.filesLock)
+ {
+ clipper.Begin(this.filteredFiles.Count);
+ while (clipper.Step())
+ {
+ for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
+ {
+ if (i < 0) continue;
+
+ var file = this.filteredFiles[i];
+ var selected = this.selectedFileNames.Contains(file.FileName);
+ var needToBreak = false;
+
+ var dir = file.Type == FileStructType.Directory;
+ var item = !dir ? GetIcon(file.Ext) : new IconColorItem
+ {
+ Color = dirTextColor,
+ Icon = (char)FontAwesomeIcon.Folder,
+ };
+
+ ImGui.PushStyleColor(ImGuiCol.Text, item.Color);
+ if (selected) ImGui.PushStyleColor(ImGuiCol.Text, selectedTextColor);
+
+ ImGui.TableNextRow();
+
+ if (ImGui.TableNextColumn())
+ {
+ needToBreak = this.SelectableItem(file, selected, item.Icon);
+ }
+
+ if (ImGui.TableNextColumn())
+ {
+ ImGui.Text(file.Ext);
+ }
+
+ if (ImGui.TableNextColumn())
+ {
+ if (file.Type == FileStructType.File)
+ {
+ ImGui.Text(file.FormattedFileSize + " ");
+ }
+ else
+ {
+ ImGui.Text(" ");
+ }
+ }
+
+ if (ImGui.TableNextColumn())
+ {
+ var sz = ImGui.CalcTextSize(file.FileModifiedDate);
+ ImGui.PushItemWidth(sz.X + 5);
+ ImGui.Text(file.FileModifiedDate + " ");
+ ImGui.PopItemWidth();
+ }
+
+ if (selected) ImGui.PopStyleColor();
+ ImGui.PopStyleColor();
+
+ if (needToBreak) break;
+ }
+ }
+
+ clipper.End();
+ }
+ }
+
+ if (this.pathInputActivated)
+ {
+ if (ImGui.IsKeyReleased(ImGui.GetKeyIndex(ImGuiKey.Enter)))
+ {
+ if (Directory.Exists(this.pathInputBuffer)) this.SetPath(this.pathInputBuffer);
+ this.pathInputActivated = false;
+ }
+
+ if (ImGui.IsKeyReleased(ImGui.GetKeyIndex(ImGuiKey.Escape)))
+ {
+ this.pathInputActivated = false;
+ }
+ }
+
+ ImGui.EndTable();
+ }
+
+ if (this.pathClicked)
+ {
+ this.SetPath(this.currentPath);
+ }
+
+ ImGui.EndChild();
+ }
+
+ private bool SelectableItem(FileStruct file, bool selected, char icon)
+ {
+ var flags = ImGuiSelectableFlags.AllowDoubleClick | ImGuiSelectableFlags.SpanAllColumns;
+
+ ImGui.PushFont(UiBuilder.IconFont);
+
+ ImGui.Text($"{icon}");
+ ImGui.PopFont();
+
+ ImGui.SameLine(25f);
+
+ if (ImGui.Selectable(file.FileName, selected, flags))
+ {
+ if (file.Type == FileStructType.Directory)
+ {
+ if (ImGui.IsMouseDoubleClicked(ImGuiMouseButton.Left))
+ {
+ this.pathClicked = this.SelectDirectory(file);
+ return true;
+ }
+ else if (this.IsDirectoryMode())
+ {
+ this.SelectFileName(file);
+ }
+ }
+ else
+ {
+ this.SelectFileName(file);
+ if (ImGui.IsMouseDoubleClicked(ImGuiMouseButton.Left))
+ {
+ this.wantsToQuit = true;
+ this.isOk = true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private bool SelectDirectory(FileStruct file)
+ {
+ var pathClick = false;
+
+ if (file.FileName == "..")
+ {
+ if (this.pathDecomposition.Count > 1)
+ {
+ this.currentPath = ComposeNewPath(this.pathDecomposition.GetRange(0, this.pathDecomposition.Count - 1));
+ pathClick = true;
+ }
+ }
+ else
+ {
+ var newPath = Path.Combine(this.currentPath, file.FileName);
+
+ if (Directory.Exists(newPath))
+ {
+ this.currentPath = newPath;
+ }
+
+ pathClick = true;
+ }
+
+ return pathClick;
+ }
+
+ private void SelectFileName(FileStruct file)
+ {
+ if (ImGui.GetIO().KeyCtrl)
+ {
+ if (this.selectionCountMax == 0)
+ { // infinite select
+ if (!this.selectedFileNames.Contains(file.FileName))
+ {
+ this.AddFileNameInSelection(file.FileName, true);
+ }
+ else
+ {
+ this.RemoveFileNameInSelection(file.FileName);
+ }
+ }
+ else
+ {
+ if (this.selectedFileNames.Count < this.selectionCountMax)
+ {
+ if (!this.selectedFileNames.Contains(file.FileName))
+ {
+ this.AddFileNameInSelection(file.FileName, true);
+ }
+ else
+ {
+ this.RemoveFileNameInSelection(file.FileName);
+ }
+ }
+ }
+ }
+ else if (ImGui.GetIO().KeyShift)
+ {
+ if (this.selectionCountMax != 1)
+ { // can select a block
+ this.selectedFileNames.Clear();
+
+ var startMultiSelection = false;
+ var fileNameToSelect = file.FileName;
+ var savedLastSelectedFileName = string.Empty;
+
+ foreach (var f in this.filteredFiles)
+ {
+ // select top-to-bottom
+ if (f.FileName == this.lastSelectedFileName)
+ { // start (the previously selected one)
+ startMultiSelection = true;
+ this.AddFileNameInSelection(this.lastSelectedFileName, false);
+ }
+ else if (startMultiSelection)
+ {
+ if (this.selectionCountMax == 0)
+ {
+ this.AddFileNameInSelection(f.FileName, false);
+ }
+ else
+ {
+ if (this.selectedFileNames.Count < this.selectionCountMax)
+ {
+ this.AddFileNameInSelection(f.FileName, false);
+ }
+ else
+ {
+ startMultiSelection = false;
+ if (!string.IsNullOrEmpty(savedLastSelectedFileName))
+ {
+ this.lastSelectedFileName = savedLastSelectedFileName;
+ }
+
+ break;
+ }
+ }
+ }
+
+ // select bottom-to-top
+ if (f.FileName == fileNameToSelect)
+ {
+ if (!startMultiSelection)
+ {
+ savedLastSelectedFileName = this.lastSelectedFileName;
+ this.lastSelectedFileName = fileNameToSelect;
+ fileNameToSelect = savedLastSelectedFileName;
+ startMultiSelection = true;
+ this.AddFileNameInSelection(this.lastSelectedFileName, false);
+ }
+ else
+ {
+ startMultiSelection = false;
+ if (!string.IsNullOrEmpty(savedLastSelectedFileName))
+ {
+ this.lastSelectedFileName = savedLastSelectedFileName;
+ }
+
+ break;
+ }
+ }
+ }
+ }
+ }
+ else
+ {
+ this.selectedFileNames.Clear();
+ this.fileNameBuffer = string.Empty;
+ this.AddFileNameInSelection(file.FileName, true);
+ }
+ }
+
+ private void AddFileNameInSelection(string name, bool setLastSelection)
+ {
+ this.selectedFileNames.Add(name);
+ if (this.selectedFileNames.Count == 1)
+ {
+ this.fileNameBuffer = name;
+ }
+ else
+ {
+ this.fileNameBuffer = $"{this.selectedFileNames.Count} files Selected";
+ }
+
+ if (setLastSelection)
+ {
+ this.lastSelectedFileName = name;
+ }
+ }
+
+ private void RemoveFileNameInSelection(string name)
+ {
+ this.selectedFileNames.Remove(name);
+ if (this.selectedFileNames.Count == 1)
+ {
+ this.fileNameBuffer = name;
+ }
+ else
+ {
+ this.fileNameBuffer = $"{this.selectedFileNames.Count} files Selected";
+ }
+ }
+
+ private bool DrawFooter()
+ {
+ var posY = ImGui.GetCursorPosY();
+
+ if (this.IsDirectoryMode())
+ {
+ ImGui.Text("Directory Path :");
+ }
+ else
+ {
+ ImGui.Text("File Name :");
+ }
+
+ ImGui.SameLine();
+
+ var width = ImGui.GetContentRegionAvail().X - 100;
+ if (this.filters.Count > 0)
+ {
+ width -= 150f;
+ }
+
+ var selectOnly = this.flags.HasFlag(ImGuiFileDialogFlags.SelectOnly);
+
+ ImGui.PushItemWidth(width);
+ if (selectOnly) ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.5f);
+ ImGui.InputText("##FileName", ref this.fileNameBuffer, 255, selectOnly ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None);
+ if (selectOnly) ImGui.PopStyleVar();
+ ImGui.PopItemWidth();
+
+ if (this.filters.Count > 0)
+ {
+ ImGui.SameLine();
+ var needToApplyNewFilter = false;
+
+ ImGui.PushItemWidth(150f);
+ if (ImGui.BeginCombo("##Filters", this.selectedFilter.Filter, ImGuiComboFlags.None))
+ {
+ var idx = 0;
+ foreach (var filter in this.filters)
+ {
+ var selected = filter.Filter == this.selectedFilter.Filter;
+ ImGui.PushID(idx++);
+ if (ImGui.Selectable(filter.Filter, selected))
+ {
+ this.selectedFilter = filter;
+ needToApplyNewFilter = true;
+ }
+
+ ImGui.PopID();
+ }
+
+ ImGui.EndCombo();
+ }
+
+ ImGui.PopItemWidth();
+
+ if (needToApplyNewFilter)
+ {
+ this.SetPath(this.currentPath);
+ }
+ }
+
+ var res = false;
+
+ ImGui.SameLine();
+
+ var disableOk = string.IsNullOrEmpty(this.fileNameBuffer) || (selectOnly && !this.IsItemSelected());
+ if (disableOk) ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.5f);
+
+ if (ImGui.Button("Ok") && !disableOk)
+ {
+ this.isOk = true;
+ res = true;
+ }
+
+ if (disableOk) ImGui.PopStyleVar();
+
+ ImGui.SameLine();
+
+ if (ImGui.Button("Cancel"))
+ {
+ this.isOk = false;
+ res = true;
+ }
+
+ this.footerHeight = ImGui.GetCursorPosY() - posY;
+
+ if (this.wantsToQuit && this.isOk)
+ {
+ res = true;
+ }
+
+ return res;
+ }
+
+ private bool IsItemSelected()
+ {
+ if (this.selectedFileNames.Count > 0) return true;
+ if (this.IsDirectoryMode()) return true; // current directory
+ return false;
+ }
+
+ private bool ConfirmOrOpenOverWriteFileDialogIfNeeded(bool lastAction)
+ {
+ if (this.IsDirectoryMode()) return lastAction;
+ if (!this.isOk && lastAction) return true; // no need to confirm anything, since it was cancelled
+
+ var confirmOverwrite = this.flags.HasFlag(ImGuiFileDialogFlags.ConfirmOverwrite);
+
+ if (this.isOk && lastAction && !confirmOverwrite) return true;
+
+ if (this.okResultToConfirm || (this.isOk && lastAction && confirmOverwrite))
+ { // if waiting on a confirmation, or need to start one
+ if (this.isOk)
+ {
+ if (!File.Exists(this.GetFilePathName()))
+ { // quit dialog, it doesn't exist anyway
+ return true;
+ }
+ else
+ { // already exists, open dialog to confirm overwrite
+ this.isOk = false;
+ this.okResultToConfirm = true;
+ }
+ }
+
+ var name = $"The file Already Exists !##{this.title}{this.id}OverWriteDialog";
+ var res = false;
+ var open = true;
+
+ ImGui.OpenPopup(name);
+ if (ImGui.BeginPopupModal(name, ref open, ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove))
+ {
+ ImGui.Text("Would you like to Overwrite it ?");
+ if (ImGui.Button("Confirm"))
+ {
+ this.okResultToConfirm = false;
+ this.isOk = true;
+ res = true;
+ ImGui.CloseCurrentPopup();
+ }
+
+ ImGui.SameLine();
+ if (ImGui.Button("Cancel"))
+ {
+ this.okResultToConfirm = false;
+ this.isOk = false;
+ res = false;
+ ImGui.CloseCurrentPopup();
+ }
+
+ ImGui.EndPopup();
+ }
+
+ return res;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialog.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialog.cs
new file mode 100644
index 000000000..9e2a77f0d
--- /dev/null
+++ b/Dalamud/Interface/ImGuiFileDialog/FileDialog.cs
@@ -0,0 +1,240 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace Dalamud.Interface.ImGuiFileDialog
+{
+ ///
+ /// A file or folder picker.
+ ///
+ public partial class FileDialog
+ {
+ private readonly string title;
+ private readonly int selectionCountMax;
+ private readonly ImGuiFileDialogFlags flags;
+ private readonly string id;
+ private readonly string defaultExtension;
+ private readonly string defaultFileName;
+
+ private bool visible;
+
+ private string currentPath;
+ private string fileNameBuffer = string.Empty;
+
+ private List pathDecomposition = new();
+ private bool pathClicked = true;
+ private bool pathInputActivated = false;
+ private string pathInputBuffer = string.Empty;
+
+ private bool isModal = false;
+ private bool okResultToConfirm = false;
+ private bool isOk;
+ private bool wantsToQuit;
+
+ private bool createDirectoryMode = false;
+ private string createDirectoryBuffer = string.Empty;
+
+ private string searchBuffer = string.Empty;
+
+ private string lastSelectedFileName = string.Empty;
+ private List selectedFileNames = new();
+
+ private float footerHeight = 0;
+
+ private string selectedSideBar = string.Empty;
+ private List drives = new();
+ private List quickAccess = new();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// A unique id for the dialog.
+ /// The text which is shown at the top of the dialog.
+ /// Which file extension filters to apply. This should be left blank to select directories.
+ /// The directory which the dialog should start inside of.
+ /// The default file or directory name.
+ /// The default extension when creating new files.
+ /// The maximum amount of files or directories which can be selected. Set to 0 for an infinite number.
+ /// Whether the dialog should be a modal popup.
+ /// Settings flags for the dialog, see .
+ public FileDialog(
+ string id,
+ string title,
+ string filters,
+ string path,
+ string defaultFileName,
+ string defaultExtension,
+ int selectionCountMax,
+ bool isModal,
+ ImGuiFileDialogFlags flags)
+ {
+ this.id = id;
+ this.title = title;
+ this.flags = flags;
+ this.selectionCountMax = selectionCountMax;
+ this.isModal = isModal;
+
+ this.currentPath = path;
+ this.defaultExtension = defaultExtension;
+ this.defaultFileName = defaultFileName;
+
+ this.ParseFilters(filters);
+ this.SetSelectedFilterWithExt(this.defaultExtension);
+ this.SetDefaultFileName();
+ this.SetPath(this.currentPath);
+
+ this.SetupSideBar();
+ }
+
+ ///
+ /// Shows the dialog.
+ ///
+ public void Show()
+ {
+ this.visible = true;
+ }
+
+ ///
+ /// Hides the dialog.
+ ///
+ public void Hide()
+ {
+ this.visible = false;
+ }
+
+ ///
+ /// Gets whether a file or folder was successfully selected.
+ ///
+ /// The success state. Will be false if the selection was canceled or was otherwise unsuccessful.
+ public bool GetIsOk()
+ {
+ return this.isOk;
+ }
+
+ ///
+ /// Gets the result of the selection.
+ ///
+ /// The result of the selection (file or folder path). If multiple entries were selected, they are separated with commas.
+ public string GetResult()
+ {
+ if (!this.flags.HasFlag(ImGuiFileDialogFlags.SelectOnly))
+ {
+ return this.GetFilePathName();
+ }
+
+ if (this.IsDirectoryMode() && this.selectedFileNames.Count == 0)
+ {
+ return this.GetFilePathName(); // current directory
+ }
+
+ var fullPaths = this.selectedFileNames.Where(x => !string.IsNullOrEmpty(x)).Select(x => Path.Combine(this.currentPath, x));
+ return string.Join(",", fullPaths.ToArray());
+ }
+
+ ///
+ /// Gets the current path of the dialog.
+ ///
+ /// The path of the directory which the dialog is current viewing.
+ public string GetCurrentPath()
+ {
+ if (this.IsDirectoryMode())
+ {
+ // combine path file with directory input
+ var selectedDirectory = this.fileNameBuffer;
+ if (!string.IsNullOrEmpty(selectedDirectory) && selectedDirectory != ".")
+ {
+ return string.IsNullOrEmpty(this.currentPath) ? selectedDirectory : Path.Combine(this.currentPath, selectedDirectory);
+ }
+ }
+
+ return this.currentPath;
+ }
+
+ private string GetFilePathName()
+ {
+ var path = this.GetCurrentPath();
+ var fileName = this.GetCurrentFileName();
+
+ if (!string.IsNullOrEmpty(fileName))
+ {
+ return Path.Combine(path, fileName);
+ }
+
+ return path;
+ }
+
+ private string GetCurrentFileName()
+ {
+ if (this.IsDirectoryMode())
+ {
+ return string.Empty;
+ }
+
+ var result = this.fileNameBuffer;
+
+ // a collection like {.cpp, .h}, so can't decide on an extension
+ if (this.selectedFilter.CollectionFilters != null && this.selectedFilter.CollectionFilters.Count > 0)
+ {
+ return result;
+ }
+
+ // a single one, like .cpp
+ if (!this.selectedFilter.Filter.Contains('*') && result != this.selectedFilter.Filter)
+ {
+ var lastPoint = result.LastIndexOf('.');
+ if (lastPoint != -1)
+ {
+ result = result.Substring(0, lastPoint);
+ }
+
+ result += this.selectedFilter.Filter;
+ }
+
+ return result;
+ }
+
+ private void SetDefaultFileName()
+ {
+ this.fileNameBuffer = this.defaultFileName;
+ }
+
+ private void SetPath(string path)
+ {
+ this.selectedSideBar = string.Empty;
+ this.currentPath = path;
+ this.files.Clear();
+ this.pathDecomposition.Clear();
+ this.selectedFileNames.Clear();
+ if (this.IsDirectoryMode())
+ {
+ this.SetDefaultFileName();
+ }
+
+ this.ScanDir(this.currentPath);
+ }
+
+ private void SetCurrentDir(string path)
+ {
+ var dir = new DirectoryInfo(path);
+ this.currentPath = dir.FullName;
+ if (this.currentPath[^1] == Path.DirectorySeparatorChar)
+ { // handle selecting a drive, like C: -> C:\
+ this.currentPath = this.currentPath[0..^1];
+ }
+
+ this.pathInputBuffer = this.currentPath;
+ this.pathDecomposition = new List(this.currentPath.Split(Path.DirectorySeparatorChar));
+ }
+
+ private bool IsDirectoryMode()
+ {
+ return this.filters.Count == 0;
+ }
+
+ private void ResetEvents()
+ {
+ this.pathClicked = false;
+ }
+ }
+}
diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs
new file mode 100644
index 000000000..18bd9dc14
--- /dev/null
+++ b/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs
@@ -0,0 +1,101 @@
+using System;
+
+namespace Dalamud.Interface.ImGuiFileDialog
+{
+ ///
+ /// A manager for the class.
+ ///
+ public class FileDialogManager
+ {
+ private FileDialog dialog;
+ private string savedPath = ".";
+ private Action callback;
+
+ ///
+ /// Create a dialog which selects an already existing folder.
+ ///
+ /// The header title of the dialog.
+ /// The action to execute when the dialog is finished.
+ public void OpenFolderDialog(string title, Action callback)
+ {
+ this.SetDialog("OpenFolderDialog", title, string.Empty, this.savedPath, ".", string.Empty, 1, false, ImGuiFileDialogFlags.SelectOnly, callback);
+ }
+
+ ///
+ /// Create a dialog which selects an already existing folder or new folder.
+ ///
+ /// The header title of the dialog.
+ /// The default name to use when creating a new folder.
+ /// The action to execute when the dialog is finished.
+ public void SaveFolderDialog(string title, string defaultFolderName, Action callback)
+ {
+ this.SetDialog("SaveFolderDialog", title, string.Empty, this.savedPath, defaultFolderName, string.Empty, 1, false, ImGuiFileDialogFlags.None, callback);
+ }
+
+ ///
+ /// Create a dialog which selects an already existing file.
+ ///
+ /// The header title of the dialog.
+ /// Which files to show in the dialog.
+ /// The action to execute when the dialog is finished.
+ public void OpenFileDialog(string title, string filters, Action callback)
+ {
+ this.SetDialog("OpenFileDialog", title, filters, this.savedPath, ".", string.Empty, 1, false, ImGuiFileDialogFlags.SelectOnly, callback);
+ }
+
+ ///
+ /// Create a dialog which selects an already existing folder or new file.
+ ///
+ /// The header title of the dialog.
+ /// Which files to show in the dialog.
+ /// The default name to use when creating a new file.
+ /// The extension to use when creating a new file.
+ /// The action to execute when the dialog is finished.
+ public void SaveFileDialog(string title, string filters, string defaultFileName, string defaultExtension, Action callback)
+ {
+ this.SetDialog("SaveFileDialog", title, filters, this.savedPath, defaultFileName, defaultExtension, 1, false, ImGuiFileDialogFlags.None, callback);
+ }
+
+ ///
+ /// Draws the current dialog, if any, and executes the callback if it is finished.
+ ///
+ public void Draw()
+ {
+ if (this.dialog == null) return;
+ if (this.dialog.Draw())
+ {
+ this.callback(this.dialog.GetIsOk(), this.dialog.GetResult());
+ this.savedPath = this.dialog.GetCurrentPath();
+ this.Reset();
+ }
+ }
+
+ ///
+ /// Removes the current dialog, if any.
+ ///
+ public void Reset()
+ {
+ this.dialog?.Hide();
+ this.dialog = null;
+ this.callback = null;
+ }
+
+ private void SetDialog(
+ string id,
+ string title,
+ string filters,
+ string path,
+ string defaultFileName,
+ string defaultExtension,
+ int selectionCountMax,
+ bool isModal,
+ ImGuiFileDialogFlags flags,
+ Action callback)
+ {
+ this.Reset();
+ this.callback = callback;
+ this.dialog = new FileDialog(id, title, filters, path, defaultFileName, defaultExtension, selectionCountMax, isModal, flags);
+ this.dialog.Show();
+ }
+ }
+}
diff --git a/Dalamud/Interface/ImGuiFileDialog/ImGuiFileDialogFlags.cs b/Dalamud/Interface/ImGuiFileDialog/ImGuiFileDialogFlags.cs
new file mode 100644
index 000000000..fe189c77c
--- /dev/null
+++ b/Dalamud/Interface/ImGuiFileDialog/ImGuiFileDialogFlags.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Dalamud.Interface.ImGuiFileDialog
+{
+ ///
+ /// Settings flags for the class.
+ ///
+ [Flags]
+ public enum ImGuiFileDialogFlags
+ {
+ ///
+ /// None.
+ ///
+ None = 0,
+
+ ///
+ /// Confirm the selection when choosing a file which already exists.
+ ///
+ ConfirmOverwrite = 1,
+
+ ///
+ /// Only allow selection of files or folders which currently exist.
+ ///
+ SelectOnly = 2,
+
+ ///
+ /// Hide files or folders which start with a period.
+ ///
+ DontShowHiddenFiles = 3,
+
+ ///
+ /// Disable the creation of new folders within the dialog.
+ ///
+ DisableCreateDirectoryButton = 4,
+
+ ///
+ /// Hide the type column.
+ ///
+ HideColumnType = 5,
+
+ ///
+ /// Hide the file size column.
+ ///
+ HideColumnSize = 6,
+
+ ///
+ /// Hide the last modified date column.
+ ///
+ HideColumnDate = 7,
+
+ ///
+ /// Hide the quick access sidebar.
+ ///
+ HideSideBar = 8,
+ }
+}