Add ImGuiFileDialog

This commit is contained in:
ocealot 2021-07-23 15:52:27 -04:00
parent 1dc5d917da
commit a85a24116c
8 changed files with 1871 additions and 0 deletions

View file

@ -0,0 +1,415 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Interface;
namespace Dalamud.Interface.ImGuiFileDialog
{
/// <summary>
/// A file or folder picker.
/// </summary>
public partial class FileDialog
{
private readonly object filesLock = new();
private List<FileStruct> files = new();
private List<FileStruct> 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<string> 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();
}
}
}

View file

@ -0,0 +1,110 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace Dalamud.Interface.ImGuiFileDialog
{
/// <summary>
/// A file or folder picker.
/// </summary>
public partial class FileDialog
{
private static Regex filterRegex = new(@"[^,{}]+(\{([^{}]*?)\})?", RegexOptions.Compiled);
private List<FilterStruct> 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<string>(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);
}
}
}
}
}
}

View file

@ -0,0 +1,26 @@
using System;
namespace Dalamud.Interface.ImGuiFileDialog
{
/// <summary>
/// A file or folder picker.
/// </summary>
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];
}
}
}

View file

@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Numerics;
namespace Dalamud.Interface.ImGuiFileDialog
{
/// <summary>
/// A file or folder picker.
/// </summary>
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<string> 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;
}
}
}

View file

@ -0,0 +1,861 @@
using System.Collections.Generic;
using System.IO;
using System.Numerics;
using ImGuiNET;
namespace Dalamud.Interface.ImGuiFileDialog
{
/// <summary>
/// A file or folder picker.
/// </summary>
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<string, IconColorItem> iconMap;
/// <summary>
/// Draws the dialog.
/// </summary>
/// <returns>Whether a selection or cancel action was performed.</returns>
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;
}
}
}

View file

@ -0,0 +1,240 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace Dalamud.Interface.ImGuiFileDialog
{
/// <summary>
/// A file or folder picker.
/// </summary>
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<string> 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<string> selectedFileNames = new();
private float footerHeight = 0;
private string selectedSideBar = string.Empty;
private List<SideBarItem> drives = new();
private List<SideBarItem> quickAccess = new();
/// <summary>
/// Initializes a new instance of the <see cref="FileDialog"/> class.
/// </summary>
/// <param name="id">A unique id for the dialog.</param>
/// <param name="title">The text which is shown at the top of the dialog.</param>
/// <param name="filters">Which file extension filters to apply. This should be left blank to select directories.</param>
/// <param name="path">The directory which the dialog should start inside of.</param>
/// <param name="defaultFileName">The default file or directory name.</param>
/// <param name="defaultExtension">The default extension when creating new files.</param>
/// <param name="selectionCountMax">The maximum amount of files or directories which can be selected. Set to 0 for an infinite number.</param>
/// <param name="isModal">Whether the dialog should be a modal popup.</param>
/// <param name="flags">Settings flags for the dialog, see <see cref="ImGuiFileDialogFlags"/>.</param>
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();
}
/// <summary>
/// Shows the dialog.
/// </summary>
public void Show()
{
this.visible = true;
}
/// <summary>
/// Hides the dialog.
/// </summary>
public void Hide()
{
this.visible = false;
}
/// <summary>
/// Gets whether a file or folder was successfully selected.
/// </summary>
/// <returns>The success state. Will be false if the selection was canceled or was otherwise unsuccessful.</returns>
public bool GetIsOk()
{
return this.isOk;
}
/// <summary>
/// Gets the result of the selection.
/// </summary>
/// <returns>The result of the selection (file or folder path). If multiple entries were selected, they are separated with commas.</returns>
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());
}
/// <summary>
/// Gets the current path of the dialog.
/// </summary>
/// <returns>The path of the directory which the dialog is current viewing.</returns>
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<string>(this.currentPath.Split(Path.DirectorySeparatorChar));
}
private bool IsDirectoryMode()
{
return this.filters.Count == 0;
}
private void ResetEvents()
{
this.pathClicked = false;
}
}
}

View file

@ -0,0 +1,101 @@
using System;
namespace Dalamud.Interface.ImGuiFileDialog
{
/// <summary>
/// A manager for the <see cref="FileDialog"/> class.
/// </summary>
public class FileDialogManager
{
private FileDialog dialog;
private string savedPath = ".";
private Action<bool, string> callback;
/// <summary>
/// Create a dialog which selects an already existing folder.
/// </summary>
/// <param name="title">The header title of the dialog.</param>
/// <param name="callback">The action to execute when the dialog is finished.</param>
public void OpenFolderDialog(string title, Action<bool, string> callback)
{
this.SetDialog("OpenFolderDialog", title, string.Empty, this.savedPath, ".", string.Empty, 1, false, ImGuiFileDialogFlags.SelectOnly, callback);
}
/// <summary>
/// Create a dialog which selects an already existing folder or new folder.
/// </summary>
/// <param name="title">The header title of the dialog.</param>
/// <param name="defaultFolderName">The default name to use when creating a new folder.</param>
/// <param name="callback">The action to execute when the dialog is finished.</param>
public void SaveFolderDialog(string title, string defaultFolderName, Action<bool, string> callback)
{
this.SetDialog("SaveFolderDialog", title, string.Empty, this.savedPath, defaultFolderName, string.Empty, 1, false, ImGuiFileDialogFlags.None, callback);
}
/// <summary>
/// Create a dialog which selects an already existing file.
/// </summary>
/// <param name="title">The header title of the dialog.</param>
/// <param name="filters">Which files to show in the dialog.</param>
/// <param name="callback">The action to execute when the dialog is finished.</param>
public void OpenFileDialog(string title, string filters, Action<bool, string> callback)
{
this.SetDialog("OpenFileDialog", title, filters, this.savedPath, ".", string.Empty, 1, false, ImGuiFileDialogFlags.SelectOnly, callback);
}
/// <summary>
/// Create a dialog which selects an already existing folder or new file.
/// </summary>
/// <param name="title">The header title of the dialog.</param>
/// <param name="filters">Which files to show in the dialog.</param>
/// <param name="defaultFileName">The default name to use when creating a new file.</param>
/// <param name="defaultExtension">The extension to use when creating a new file.</param>
/// <param name="callback">The action to execute when the dialog is finished.</param>
public void SaveFileDialog(string title, string filters, string defaultFileName, string defaultExtension, Action<bool, string> callback)
{
this.SetDialog("SaveFileDialog", title, filters, this.savedPath, defaultFileName, defaultExtension, 1, false, ImGuiFileDialogFlags.None, callback);
}
/// <summary>
/// Draws the current dialog, if any, and executes the callback if it is finished.
/// </summary>
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();
}
}
/// <summary>
/// Removes the current dialog, if any.
/// </summary>
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<bool, string> callback)
{
this.Reset();
this.callback = callback;
this.dialog = new FileDialog(id, title, filters, path, defaultFileName, defaultExtension, selectionCountMax, isModal, flags);
this.dialog.Show();
}
}
}

View file

@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Dalamud.Interface.ImGuiFileDialog
{
/// <summary>
/// Settings flags for the <see cref="FileDialog"/> class.
/// </summary>
[Flags]
public enum ImGuiFileDialogFlags
{
/// <summary>
/// None.
/// </summary>
None = 0,
/// <summary>
/// Confirm the selection when choosing a file which already exists.
/// </summary>
ConfirmOverwrite = 1,
/// <summary>
/// Only allow selection of files or folders which currently exist.
/// </summary>
SelectOnly = 2,
/// <summary>
/// Hide files or folders which start with a period.
/// </summary>
DontShowHiddenFiles = 3,
/// <summary>
/// Disable the creation of new folders within the dialog.
/// </summary>
DisableCreateDirectoryButton = 4,
/// <summary>
/// Hide the type column.
/// </summary>
HideColumnType = 5,
/// <summary>
/// Hide the file size column.
/// </summary>
HideColumnSize = 6,
/// <summary>
/// Hide the last modified date column.
/// </summary>
HideColumnDate = 7,
/// <summary>
/// Hide the quick access sidebar.
/// </summary>
HideSideBar = 8,
}
}