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, + } +}