Hi, So, I've been working on a runtime world editor for months now. I just recently had enough in my editor to implement a saving and loading routine, so I did. Had to learn how first, but I went with System.IO and just parsing my own text file (I don't really mind if the user can change it so it works for me). Any way, I wanted the user to be able to organize their saved creations how ever they see fit (in folders). So I made the following script. It's a columns view file browser (inspired by OS X Finder). Currently it doesn't have the ability to move files or folders, but it can: view them, open them, save them, and create new folders. The window can be resized at the bottom right, every column can be resized for easier viewing, and it the scrolling functions like Pre-Lion Finder. If you just want to play with the GUI code, and not worry about integrating it into your game... just grab the script at the bottom and uncomment the Start function at the top, that will basically just open the browser and let you browse your Application.dataPath (your Project view if you just run it in Unity). Otherwise read on... All you need do to set it up is, find the comment about a third the way down and add a single line to call what ever function you already have for opening save files. Then just place the script in an object you can reference. Just call: Code (csharp): Browser.OpenFile( The_Path_To_Your_MainSaves_Folder ); To allow the user to access any file or folder in the directory you pass to the OpenFile function. Then for saving files you have to run what ever saving script you have to get a file (or folder) you can pass to this function, sorry no player prefs, you need to give it a path. Then the user will basically pick where they want to move that file and what they want to call it, if the click cancel it will delete the temporary save file for you. This way you can just do your saving routine, and pass the output to this script and let it do its thing (no need to wait for it with coroutines or anything). So basically pass it the path to your main saves folder, and the path to the temporary save file your scripts will create, like this: Code (csharp): Browser.SaveFile(Temporary_Path_Of_The_Save_File, The_Path_To_Your_MainSaves_Folder); Note: this will overwrite any files with the same name that the user picks, and wont warn them. WARNING!!! Make sure that you do not pass a directory to Temporary_Path_Of_The_Save_File that you do not want to lose, as if the user clicks cancel, it will be instantly deleted without warning! This is normal behavior as it is meant to be a temporary file, I designed browser so other coders could implement it with only 2 lines of code, but I can not predict how you wish to save files, so all browser does is move them, if the user clicks cancel then the temporary file is deleted without the user knowing of it's existence. I'll probably update this soon and make it a little more capable, but heres what I have so far. Let me know what you think! PHP: // Browser.cs// Basic Columns view file browser for saveing and loading save games at runtime.// Created Sep 17 by Charlie Mehlenbeck// Contact me here for any questions: http://forum.unity3d.com/members/27746-inventor2010using UnityEngine;using System.Collections;using System;using System.IO;public class Browser : MonoBehaviour{ public int defaultWidth = 100; public int minWidth = 50; public GUISkin skin; bool WindowOpen = false; WindowType WinType; String MainFolder = ""; String FileToSave = ""; String NameOfFileToSave = ""; Path[] PathTree = new Path[0]; String[] OpenPaths = new String[0]; String[] OpenPaths_Reduced = new String[0]; String WindowName = ""; Vector2 OuterScrollPosition = Vector2.zero; Vector2[] ScrollPositions = new Vector2[0]; int[] Widths = new int[0]; Drag DragStat; bool creatingFolder = false; String folderName = ""; Vector2 minWindowScale = new Vector2(455, 150); static Rect BrowserRect = new Rect(20, 20, 500, 400); public void OpenFile(String mainFolder) { if(Directory.Exists(mainFolder)) { WindowOpen = true; WinType = WindowType.Open; DragStat.Dragging = false; // Ensure nothing is being draged. MainFolder = mainFolder; // example: Application.dataPath/Saves; GetAllSubDirectoriesAndFiles( MainFolder ); AddWidth(); AddScrollPosition(0); AddPath(MainFolder, 0); } else { if(File.Exists(mainFolder)) Debug.LogError("I need a main directory, not a file!"); else Debug.LogError("I need a path for a main directory that actually exists!"); } } public void SaveFile(String FilePath, String mainFolder) { if(Directory.Exists(mainFolder)) { if(File.Exists(FilePath) || Directory.Exists(FilePath)) { WindowOpen = true; WinType = WindowType.Save; DragStat.Dragging = false; // Ensure nothing is being draged. FileToSave = FilePath; MainFolder = mainFolder; // example: Application.dataPath/Saves; GetAllSubDirectoriesAndFiles( MainFolder ); AddWidth(); AddScrollPosition(0); AddPath(MainFolder, 0); } else Debug.LogError("I need a path for a temporary save file that actually exists!"); } else { if(File.Exists(mainFolder)) Debug.LogError("I need a main directory, not a file!"); else Debug.LogError("I need a path for a main directory that actually exists!"); } } void OnGUI() { if(WindowOpen) { GUI.skin = skin; BrowserRect = GUI.Window(0, BrowserRect, DrawBrowser, WindowName); } } void DrawBrowser(int windowID) { Event E = Event.current; Rect bottom = new Rect(5, BrowserRect.height-40, BrowserRect.width-10, 4); GUI.Box(bottom, ""); bottom.x=10; //Get bottom bar rect. bottom.y+=10; bottom.width = 120; bottom.height = 20; if(creatingFolder) // Folder creation tool. { folderName = GUI.TextField(bottom, folderName); bottom.x += bottom.width+5; bottom.width = 52; if(GUI.Button(bottom, "Create")) { String newDirectory; if( Directory.Exists(OpenPaths[OpenPaths.Length-1]) ) newDirectory = OpenPaths[OpenPaths.Length-1] + "/" + folderName; else newDirectory = OpenPaths[OpenPaths.Length-2] + "/" + folderName; if(!Directory.Exists(newDirectory)) { Directory.CreateDirectory(newDirectory); GetAllSubDirectoriesAndFiles( MainFolder ); } creatingFolder = false; } } else if(GUI.Button(bottom, "Create New Folder")) { folderName = ""; creatingFolder = true; } bottom.width = 12; // Draw resize handle bottom.height = 12; bottom.x = BrowserRect.width-bottom.width-5; bottom.y+=12; GUI.Box(bottom, ""); Rect realBottom = bottom; realBottom.x += BrowserRect.x; realBottom.y += BrowserRect.y; if( Input.GetMouseButtonDown(0) realBottom.Contains(GetMouseCord()) ) { DragStat.Dragging = true; DragStat.startMouse = GetMouseCord(); DragStat.startPosition = new Vector2(BrowserRect.width, BrowserRect.height); DragStat.type = DragType.WindowScaler; } else if(DragStat.Dragging DragStat.type==DragType.WindowScaler) { Vector2 MouseCord = GetMouseCord(); Vector2 newScale = DragStat.startPosition - new Vector2( DragStat.startMouse.x - MouseCord.x, DragStat.startMouse.y - MouseCord.y); BrowserRect.width = newScale.x; BrowserRect.height = newScale.y; if(BrowserRect.width < minWindowScale.x) BrowserRect.width = minWindowScale.x; if(BrowserRect.height < minWindowScale.y) BrowserRect.height = minWindowScale.y; } // Draw Open/Save Cancel buttons, and make them function: bottom.y-=12; bottom.height = 20; bottom.width = 42; bottom.x -= bottom.width+5; if(WinType == WindowType.Open GUI.Button(bottom, "Open")) { String FileToOpen = OpenPaths[OpenPaths.Length-1]; if(File.Exists(FileToOpen) || Directory.Exists(FileToOpen)) { ////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // Run Your Open file code here! Have your open file code in a function that accepts a path (string) // // and pass to it: FileToOpen. // // // ////////////////////////////////////////////////////////////////////////////////////////////////////////// Debug.Log("Opening File: " + FileToOpen); } else Debug.LogError("The File you are trying to open must have gotten moved, or deleated."); WindowOpen = false; } if(WinType == WindowType.Save GUI.Button(bottom, "Save")) { if(File.Exists(FileToSave) || Directory.Exists(FileToSave)) { //NameOfFileToSave String SaveLocation = OpenPaths[OpenPaths.Length-1]; if(Directory.Exists( SaveLocation )) OverwriteDuplicates(FileToSave, SaveLocation+"/"+NameOfFileToSave); else { if(File.Exists(SaveLocation)) // If the user chooses a file instead of a folder the parent folder will be used. { SaveLocation = (new FileInfo(SaveLocation)).DirectoryName; OverwriteDuplicates(FileToSave, SaveLocation+"/"+NameOfFileToSave); } else Debug.LogError("The directory you're trying to save in no longer exists"); } } else Debug.LogError("The file/folder your trying to save no longer exists."); WindowOpen = false; } bottom.width = 52; bottom.x -= bottom.width+5; if(GUI.Button(bottom, "Cancel")) { if(WinType == WindowType.Save) { if(File.Exists(FileToSave)) File.Delete(FileToSave); // Deleate FileToSave if the user chooses cancel. if(Directory.Exists(FileToSave)) Directory.Delete(FileToSave, true); // Deleate FileToSave if the user chooses cancel. Debug.Log("Deleate: " + FileToSave); } //Directory.Delete(FileToSave, true); // Deleate the file to save if the user chooses cancel. WindowOpen = false; } if(WinType == WindowType.Save) { bottom.width = 130; bottom.x -= bottom.width+5; NameOfFileToSave = GUI.TextField(bottom, NameOfFileToSave); } ////////////////////////////////////////////////////////////////// // The rest of OnGUI is to draw the columns part of the window. // ////////////////////////////////////////////////////////////////// const int verticalSpace = 2; const int horiznontalSpace = 10; Rect OuterPosition = BrowserRect; OuterPosition.x=5; OuterPosition.y=20; OuterPosition.width-=10; OuterPosition.height-=67; Rect OuterViewRect = OuterPosition; OuterViewRect.height-=17; OuterViewRect.width = 0; for(int i=0; i<ScrollPositions.Length; ++i) OuterViewRect.width += horiznontalSpace+Widths[i]; OuterViewRect.width += 15; //OuterPosition.width = 0; //for(int i=0; i<Widths.Length; ++i) OuterPosition.width += 12+Widths[i]; // Makes scroll wheel on columns view scroll horizontally, as well allowing vertical scrolling in each column. if(!Input.GetMouseButton(0)) { DragStat.Dragging = false; Rect realOuterPosition = OuterPosition; realOuterPosition.x += BrowserRect.x; realOuterPosition.y += BrowserRect.y; if( realOuterPosition.Contains(GetMouseCord()) E.type == EventType.ScrollWheel ) OuterScrollPosition.x += Mathf.Clamp(E.delta.x,-2f,2f)*20; } if(OuterPosition.width >= OuterViewRect.width) { OuterPosition.height+=20; OuterViewRect.height+=20; } OuterScrollPosition = GUI.BeginScrollView (OuterPosition, OuterScrollPosition, OuterViewRect); GUI.BeginGroup (OuterViewRect); for(int i=0; i<ScrollPositions.Length; ++i) { int ButtonHeight = 20; int[] PathIndexes = GetPathIndexesAt( OpenPaths[i] ); Rect Position = GetRect(i, horiznontalSpace, (int)OuterViewRect.height); Rect viewRect = Position; viewRect.height = PathIndexes.Length*ButtonHeight + (PathIndexes.Length-1)*verticalSpace; viewRect.width -=17; viewRect.x = 0; viewRect.y = 0; // Make the buttons fill the space available: if(PathIndexes.Length*ButtonHeight + (PathIndexes.Length-1)*verticalSpace <= Position.height) viewRect.width += 15; ScrollPositions[i] = GUI.BeginScrollView (Position, ScrollPositions[i], viewRect); // Draw Column i. viewRect.height = ButtonHeight; for(int j=0; j<PathIndexes.Length; ++j) { int index = PathIndexes[j]; if(i+1<OpenPaths_Reduced.Length PathTree[index].Name == OpenPaths_Reduced[i+1]) GUI.backgroundColor = Color.blue; else GUI.backgroundColor = Color.white; Vector2 posbackup = ScrollPositions[i]; // Prevent it from scrolling to the top on button click. if(GUI.Button(viewRect, PathTree[index].Name)) { OuterScrollPosition.x = Mathf.Infinity; // Scroll to end of outer scroll view. if(PathTree[index].File) { if(i==OpenPaths.Length-1) AddWidth(); AddScrollPosition(i); } else { if(i==OpenPaths.Length-1) AddWidth(); AddScrollPosition(i+1); } AddPath(PathTree[index].Parent + "/" + PathTree[index].Name, i+1); } ScrollPositions[i] = posbackup; viewRect.y += ButtonHeight+verticalSpace; } GUI.EndScrollView(); Position.x += Position.width; // Draw Column Resizer Bar: Position.width = horiznontalSpace-2; GUI.Box(Position, ""); Rect realPosition = Position; // Make it function: realPosition.x -= OuterScrollPosition.x - OuterPosition.x-2; //realPosition.y -= OuterScrollPosition.y; realPosition.x += BrowserRect.x; realPosition.y += BrowserRect.y; if( Input.GetMouseButtonDown(0) realPosition.Contains(GetMouseCord()) ) { DragStat.Dragging = true; DragStat.startMouse = Input.mousePosition; DragStat.startPosition = new Vector2(Widths[i], 0); DragStat.type = DragType.Column; DragStat.column = i; } else if(DragStat.Dragging DragStat.type==DragType.Column DragStat.column==i) { if(realPosition.x > BrowserRect.x+BrowserRect.width) BrowserRect.width = realPosition.x-BrowserRect.x; int newWidth = (int)(DragStat.startPosition.x - DragStat.startMouse.x + Input.mousePosition.x); if(newWidth < minWidth) newWidth=minWidth; Widths[i] = newWidth; } } GUI.EndGroup (); GUI.EndScrollView(); //GUI.Button(new Rect(10, 20, 400, 20), "Can't drag me"); //GUI.DragWindow(); if(Input.GetMouseButtonDown(0)) { Rect topBar = BrowserRect; topBar.height = 15; if( topBar.Contains(GetMouseCord()) ) { DragStat.Dragging = true; DragStat.startMouse = GetMouseCord(); DragStat.startPosition = new Vector2(BrowserRect.x, BrowserRect.y); DragStat.type = DragType.Window; } } if(DragStat.Dragging DragStat.type==DragType.Window) { Vector2 MouseCord = GetMouseCord(); Vector2 newPos = DragStat.startPosition - new Vector2( DragStat.startMouse.x - MouseCord.x, DragStat.startMouse.y - MouseCord.y); BrowserRect.x = newPos.x; BrowserRect.y = newPos.y; } } Rect GetRect(int index, int space, int height) { int x = 10; for(int i=0; i<index; ++i) x += space+Widths[i]; return new Rect(x, 0, Widths[index], height); } int[] GetPathIndexesAt(string Parent) { int[] Temp = new int[PathTree.Length]; int n = 0; // Number of paths found at Parent. for(int i=0; i<PathTree.Length; ++i) { if(System.IO.Path.GetFullPath(PathTree[i].Parent) == System.IO.Path.GetFullPath(Parent)) { Temp[n] = i; n++; } } int[] OUT = new int[n]; for(int i=0; i<OUT.Length; ++i) { OUT[i] = Temp[i]; } return OUT; } void GetAllSubDirectoriesAndFiles(String MainDirectory) { PathTree = new Path[0]; WalkTheTree( new DirectoryInfo(MainDirectory) ); } void WalkTheTree(DirectoryInfo DI) { String PARENT = DI.FullName; DirectoryInfo[] diArr = DI.GetDirectories(); FileInfo[] fiArr = DI.GetFiles(); //Debug Path[] PathTreeTemp = new Path[ diArr.Length + fiArr.Length ]; for (int i=0; i<diArr.Length; ++i) { //Debug.Log(diArr[i].Name); PathTreeTemp[i].Name = diArr[i].Name; PathTreeTemp[i].Parent = PARENT; PathTreeTemp[i].File = false; } int diArrLength = diArr.Length; for (int i=diArrLength; i<PathTreeTemp.Length; ++i) { PathTreeTemp[i].Name = fiArr[i-diArrLength].Name; PathTreeTemp[i].Parent = PARENT; PathTreeTemp[i].File = true; } PathTree = AddPathArrays( PathTree, PathTreeTemp); foreach (DirectoryInfo di in diArr) WalkTheTree( di ); } void OverwriteDuplicates(String FTS, String FL) { if(Directory.Exists(FL)) Directory.Delete(FL, true); else if (File.Exists(FL)) File.Delete(FL); Directory.Move(FTS, FL); } void AddPath(String add, int indexOfAdd) { String[] Temp = new String[indexOfAdd+1]; for(int i=0; i<indexOfAdd; ++i) Temp[i] = OpenPaths[i]; Temp[Temp.Length-1] = add; OpenPaths = Temp; Temp = new String[indexOfAdd+1]; for(int i=0; i<indexOfAdd; ++i) Temp[i] = OpenPaths_Reduced[i]; Temp[Temp.Length-1] = (new DirectoryInfo(add)).Name; OpenPaths_Reduced = Temp; WindowName = ""; // Get the Name of the window. for(int i=0; i<OpenPaths_Reduced.Length; ++i) WindowName += "/"+OpenPaths_Reduced[i]; } void AddWidth() { int[] Temp = new int[Widths.Length+1]; Widths.CopyTo(Temp, 0); Temp[Temp.Length-1] = defaultWidth; Widths = Temp; } void AddScrollPosition(int indexOfAdd) { Vector2[] Temp = new Vector2[indexOfAdd+1]; for(int i=0; i<indexOfAdd; ++i) Temp[i] = ScrollPositions[i]; Temp[Temp.Length-1] = Vector2.zero; ScrollPositions = Temp; } Path[] AddPathArrays(Path[] IN1, Path[] IN2) { Path[] Temp = new Path[IN1.Length + IN2.Length]; IN1.CopyTo(Temp, 0); IN2.CopyTo(Temp, IN1.Length); return Temp; } Vector2 GetMouseCord() { Vector2 mouse = Input.mousePosition; mouse.y = Screen.height-mouse.y; return mouse; }}struct Path{ public String Parent; public String Name; public bool File; // If false, it's a folder.}struct Drag{ public bool Dragging; public Vector2 startMouse; public Vector2 startPosition; public DragType type; public int column;}enum DragType{ Column, Window, WindowScaler}enum WindowType{ Open, Save}
To any one who has looked at this before and had to contend with the Code (csharp): Assets/Script/Browser.cs(134,74): error CS0103: The name `Fun' does not exist in the current context error, I apologize. I thought that I removed all dependency on other scripts that I have in my project, but apparently I didn't test that. Fun is just short for functions, its just a script I wrote with a bunch of standard functions I use regularly like MouseCord which just inverts the y position provided by Input.mousePosition. This script will work on its own now, I edited my original post, and I actually tested it in a separate project this time Thanks to Michele for bringing that to my attention! I will probably update this script soon as well, I've just been really busy with school.
Has anyone used this on Windows stand alone build. It works great in my mac editor and my Mac build but it shows no directory on WindowsXP for me. cheers, Grant
hi! great thing - exactly what i was looking for the last few weeks!! thank you for sharing! i have only one problem: i don't get it running think because i'm the worst programmer on earth - is there a demo scene on the wiki available also? merry christmas!
Thnaks ryuuguu and suppenhuhn, ryuuguu, Sorry I haven't gotten back to you on this sooner, I've been busy with school and working on an in-game editor (the in-game editor being the reason why I made this script). I'm almost ready to release the editor on the Asset Store, and now that I'm on break I'm going to work with this script further. I'm going to go see whats wrong with the windows build right now. Even when I release my editor though, I will keep updating this script as I improve it, to help the community. suppenhuhn, Are you also trying to run it on windows? In either case I'll post a demo project of it in a few min. Merry christmas to you too!
yes, i'm working on a windows machine - win7 64bit. yeah, a short demoscene would be great! but now for me it's time to go sleeping .. gn8!
...Several hours after a few min, I figured out the problem Browser was having on windows and I made a demo Project. Here is the demo project: View attachment $Browser Demo Project.unitypackage . Just load it into a empty project and run. There is no "Read Me" file, but there are several lengthy comments in "Browser Test window.js" that you may want to read to understand whats actually going on. Any way, for any one who is curious, the problem Browser was having on windows was... Windows... (can tell I'm a mac person here ). But seriously, theres a line in Browser where 2 paths need to be checked to see if they are identical or not, but in windows sometimes it would give forward slashes as the separators between folder names, and sometimes it would give backward slashes. example: "C:/folder/bla"=="C:\folder\bla" So basically even when the paths are in fact identical, the strings representing them were not, so it wouldn't execute any of the code that draws the file names on the screen. Solution: pass both of the strings into "System.IO.Path.GetFullPath( String )" before checking if they are identical, "GetFullPath" always returns paths with the same slashes for separators. I've updated the browser script here and on the wiki accordingly. I also fixed a minor bug with the new folder button not working when a file is selected and not a folder. Enjoy, and mary christmas!
I've download this code asset and tried it in my project. I don't know what's happened or why, but when I tried to Save File to my project's folder it just erase it, causing me to lost several days of work... (yes, last time I used Time Machine to backup my files was a week ago). So, as far as I'm concerned, this code asset is a public menace
public menace? I'm sorry gads for what happened, I'm just coding in my free time and sharing a few things that I learn as I go, I never meant to harm anyone, it's just an honest mistake. Most of what you see there is just interface, and a few functions and classes for walking a directory tree. The save function.... Code (csharp): SaveFile(String FilePath, String mainFolder) is the only thing that even has a deleate command in it... I never even thought about it, but your suppose to pass that function a file that you want it to save, and then the directory you wish to restrict the user to. If the user cancels, the file that you passed it to save is deleted to prevent a pile up of temporary files. I guess if you reverse it and put your project into "FilePath" by mistake it would delete your project, believing it was a file you wanted the user to be able to save. I'm sorry for the inconvenience, I didn't even think that this thing could do something like that. I'll add a warning for now, and get to work on a dummy window "Are you sure you want to delete file ___?"
I'm sorry gads... This is my fault, I thought it may be due to user error, but it's my instructions. I reversed it in my initial description: It should be: Again, this is my fault, honest mistake, I can't imagine you'd want to use it now, but it should work if you reverse those 2 variables... Please try it on an empty project though! Let me know if you have any other issues! I've updated the tutorial, will go update the wiki now, I've added a warning as well, hopefully no one has to go through what you did again. The demo project I have in the 6th post down doesn't have this mistake so that should be safe. I'll try and figure out how to prevent it from doing this, just in case, maybe block the deletion of any folder called "assets". But it's suppose to be that you create a save file and use browser to move it, to the user it looks like they are saving normally, if they click cancel, they shouldn't even know of the temporary file, so having a warning "are you sure you want to delete ___" is kinda awkward. Please, from now on though, use an empty project for any kind of testing of third party plugins, its not your fault, but to be on the safe side next time, I'm still learning too (as every one always is) mistakes happen, please prepare your self for them.
Hi inventor2010, Yes, it was a bad feeling when I saw my project vanish in the void... However I remembered later that I had enabled OSX Lion Versions system that keeps documents' changes in disc and and it does not require that the backup disc has to be connected to the laptop. It allowed me to recover all my work Hurray for Lion Versions About you plugin. I don't quite remember what were the arguments that I sent to the SaveFile function. I think it was SaveFile(Temporary_Path_Of_The_Save_File_Inside_Project_Folder, The_Path_To_My_Project_Folder); Since the plugin is an interface for navigating through directories, probably it makes more sense to code it so it would return the selected path, instead of creating a temporary file (I don't quite understand why you are doing it). cheers,
Great! Glad to know nothing was lost, in the end! And to think, I still like Snow Leopard more. It's been a while since I coded it, but If I remember right, I didn't return a path because theres an unpredictable amount of time where the user is naming, moving files, and picking their save location. I wasn't sure how to simply return a chosen path, If you have any suggestions I'd be more than happy to hear them, may speed up some other programming I'm doing, but without accessing pointers or something I'm unsure how to have it return a path after x time. I guess it could be modified to be incorporated with your code, call a specific function when the user is done, but I wanted to keep it as plug and play as possible (void load and void save).
tiny file dialogs on sourceforge is a single C / C++ file that provides cross-platform file dialogs. It has no init and no main loop.
It's great of the community to provide this. I think it would work pretty well for save-game purposes. It doesn't work so well for more general purposes; e.g., if I were making some sort of model browser, I need to let people navigate to anywhere on their hard drive to select a model. This script only lets you browse files under one subdirectory; there's no way to go up, and if you try to start it at root, it hangs while it tries to preload every directory on the disk! Does anybody know of any good alternatives to this script?
Hehe, Yeah... I remember that! Your right, it does not work well with large directory tree's! I (or anyone) could probably implement an incremental loading of the tree as the user browses though. But, as you noted, it's good for save-game purposes, which was all I was focused on at the time. If you wanted to mod it, I would love to see it added to the community again! The primary variable that needs to be refracted appears to be PathTree, which is a rather deceptive name, because I quite lazily made it an ordinary array since I was more focused on the UI at the time. I might update it. But I'm a bit focused on other things atm (like getting a new job).
No problem... I ended up going with File Chooser Dialog Box, which isn't perfect either, but is close enough for my current project. I might revisit this in the future, though... I have pretty strong feelings about file dialogs. Good luck with the job search!
It's very small on a screen of smartfon on Android. Is there an easy way to make window and fonts independent of screen resolution?