Client LuaCsForBarotrauma
FileSelection.cs
1 #nullable enable
2 using Microsoft.Xna.Framework;
3 using System;
4 using System.Collections.Generic;
5 using Barotrauma.IO;
6 using System.Linq;
8 
9 namespace Barotrauma
10 {
11  public static class FileSelection
12  {
13  private static bool open;
14  public static bool Open
15  {
16  get
17  {
18  return open;
19  }
20  set
21  {
22  if (value) { InitIfNecessary(); }
23  if (!value)
24  {
25  fileSystemWatcher?.Dispose();
26  fileSystemWatcher = null;
27  }
28  open = value;
29  }
30  }
31 
32  private static GUIFrame? backgroundFrame;
33  private static GUIFrame? window;
34  private static GUIListBox? sidebar;
35  private static GUIListBox? fileList;
36  private static GUITextBox? directoryBox;
37  private static GUITextBox? filterBox;
38  private static GUITextBox? fileBox;
39  private static GUIDropDown? fileTypeDropdown;
40  private static GUIButton? openButton;
41 
42  private static System.IO.FileSystemWatcher? fileSystemWatcher;
43 
44  private enum ItemIsDirectory
45  {
46  Yes, No
47  }
48 
49  private static string? currentFileTypePattern;
50 
51  private static readonly string[] ignoredDrivePrefixes =
52  {
53  "/sys/", "/snap/"
54  };
55 
56  private static string currentDirectory = "";
57  public static string CurrentDirectory
58  {
59  get
60  {
61  return currentDirectory;
62  }
63  set
64  {
65  string[] dirSplit = value.Replace('\\', '/').Split('/');
66  List<string> dirs = new List<string>();
67  for (int i = 0; i < dirSplit.Length; i++)
68  {
69  if (dirSplit[i].Trim() == "..")
70  {
71  if (dirs.Count > 1)
72  {
73  dirs.RemoveAt(dirs.Count - 1);
74  }
75  }
76  else if (dirSplit[i].Trim() != ".")
77  {
78  dirs.Add(dirSplit[i]);
79  }
80  }
81  currentDirectory = string.Join("/", dirs);
82  if (!currentDirectory.EndsWith("/"))
83  {
84  currentDirectory += "/";
85  }
86  try
87  {
88  fileSystemWatcher?.Dispose();
89  fileSystemWatcher = new System.IO.FileSystemWatcher(currentDirectory)
90  {
91  Filter = "*",
92  NotifyFilter = System.IO.NotifyFilters.LastWrite | System.IO.NotifyFilters.FileName | System.IO.NotifyFilters.DirectoryName
93  };
94  fileSystemWatcher.Created += OnFileSystemChanges;
95  fileSystemWatcher.Deleted += OnFileSystemChanges;
96  fileSystemWatcher.Renamed += OnFileSystemChanges;
97  fileSystemWatcher.EnableRaisingEvents = true;
98  }
99  catch (System.IO.FileNotFoundException exception)
100  {
101  DebugConsole.ThrowError("Failed to set the current directory, possibly due to insufficient access permissions.", exception);
102  }
103  catch (ArgumentException exception)
104  {
105  DebugConsole.ThrowError("Failed to set the current directory, possibly because it was deleted.", exception);
106  }
107  catch (Exception exception)
108  {
109  DebugConsole.ThrowError("Failed to set the current directory for an unknown reason.", exception);
110  }
111  RefreshFileList();
112  }
113  }
114 
115  public static Action<string>? OnFileSelected
116  {
117  get;
118  set;
119  }
120 
121  private static void OnFileSystemChanges(object sender, System.IO.FileSystemEventArgs e)
122  {
123  if (fileList is null) { return; }
124  switch (e.ChangeType)
125  {
126  case System.IO.WatcherChangeTypes.Created:
127  {
128  var itemFrame = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), fileList.Content.RectTransform), e.Name ?? string.Empty)
129  {
130  UserData = Directory.Exists(e.FullPath) ? ItemIsDirectory.Yes : ItemIsDirectory.No
131  };
132  if (itemFrame.UserData is ItemIsDirectory.Yes)
133  {
134  itemFrame.Text += "/";
135  }
136  fileList.Content.RectTransform.SortChildren(SortFiles);
137  }
138  break;
139  case System.IO.WatcherChangeTypes.Deleted:
140  {
141  var itemFrame = fileList.Content.FindChild(c => (c is GUITextBlock tb) && (tb.Text == e.Name || tb.Text == e.Name + "/"));
142  if (itemFrame != null) { fileList.RemoveChild(itemFrame); }
143  }
144  break;
145  case System.IO.WatcherChangeTypes.Renamed:
146  {
147  System.IO.RenamedEventArgs renameArgs = e as System.IO.RenamedEventArgs ?? throw new InvalidCastException($"Unable to cast {nameof(System.IO.FileSystemEventArgs)} to {nameof(System.IO.RenamedEventArgs)}.");
148  var itemFrame =
149  fileList.Content.FindChild(c => (c is GUITextBlock tb) && (tb.Text == renameArgs.OldName || tb.Text == renameArgs.OldName + "/")) as GUITextBlock
150  ?? throw new Exception($"Could not find file list item with name \"{renameArgs.OldName}\"");
151  itemFrame.UserData = Directory.Exists(e.FullPath) ? ItemIsDirectory.Yes : ItemIsDirectory.No;
152  itemFrame.Text = renameArgs.Name ?? string.Empty;
153  if (itemFrame.UserData is ItemIsDirectory.Yes)
154  {
155  itemFrame.Text += "/";
156  }
157  fileList.Content.RectTransform.SortChildren(SortFiles);
158  }
159  break;
160  }
161  }
162 
163  private static int SortFiles(RectTransform r1, RectTransform r2)
164  {
165  string file1 = (r1.GUIComponent as GUITextBlock)?.Text?.SanitizedValue ?? "";
166  string file2 = (r2.GUIComponent as GUITextBlock)?.Text?.SanitizedValue ?? "";
167  bool dir1 = r1.GUIComponent.UserData is ItemIsDirectory.Yes;
168  bool dir2 = r2.GUIComponent.UserData is ItemIsDirectory.Yes;
169  if (dir1 && !dir2)
170  {
171  return -1;
172  }
173  else if (!dir1 && dir2)
174  {
175  return 1;
176  }
177 
178  return string.Compare(file1, file2, StringComparison.OrdinalIgnoreCase);
179  }
180 
181  private static void InitIfNecessary()
182  {
183  if (backgroundFrame == null) { Init(); }
184  }
185 
186  public static void Init()
187  {
188  backgroundFrame = new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, GUI.Canvas), style: null)
189  {
190  Color = Color.Black * 0.5f,
191  HoverColor = Color.Black * 0.5f,
192  SelectedColor = Color.Black * 0.5f,
193  PressedColor = Color.Black * 0.5f,
194  };
195 
196  window = new GUIFrame(new RectTransform(Vector2.One * 0.8f, backgroundFrame.RectTransform, Anchor.Center));
197 
198  var horizontalLayout = new GUILayoutGroup(new RectTransform(Vector2.One * 0.9f, window.RectTransform, Anchor.Center), true);
199  sidebar = new GUIListBox(new RectTransform(new Vector2(0.29f, 1.0f), horizontalLayout.RectTransform))
200  {
201  PlaySoundOnSelect = true
202  };
203 
204  var drives = System.IO.DriveInfo.GetDrives();
205  foreach (var drive in drives)
206  {
207  if (drive.DriveType == System.IO.DriveType.Ram) { continue; }
208  if (ignoredDrivePrefixes.Any(p => drive.Name.StartsWith(p))) { continue; }
209  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), sidebar.Content.RectTransform), drive.Name.Replace('\\','/'));
210  }
211 
212  sidebar.OnSelected = (child, userdata) =>
213  {
214  CurrentDirectory = (child as GUITextBlock)?.Text.SanitizedValue ?? throw new Exception("Sidebar selection is invalid");
215 
216  return false;
217  };
218 
219  //spacing between sidebar and fileListLayout
220  new GUIFrame(new RectTransform(new Vector2(0.01f, 1.0f), horizontalLayout.RectTransform), style: null);
221 
222  var fileListLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 1.0f), horizontalLayout.RectTransform));
223  var firstRow = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.04f), fileListLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft);
224  new GUIButton(new RectTransform(new Vector2(0.05f, 1.0f), firstRow.RectTransform), "^")
225  {
226  OnClicked = MoveToParentDirectory
227  };
228  directoryBox = new GUITextBox(new RectTransform(new Vector2(0.7f, 1.0f), firstRow.RectTransform))
229  {
230  OverflowClip = true,
231  OnEnterPressed = (tb, txt) =>
232  {
233  if (Directory.Exists(txt))
234  {
235  var attributes = System.IO.File.GetAttributes(txt);
236  if (attributes.HasAnyFlag(System.IO.FileAttributes.System) || attributes.HasAnyFlag(System.IO.FileAttributes.Hidden))
237  {
238  // System and hidden folders should be filtered out when populating the options, but the user can still write or copy-paste the path in the text field,
239  // which will throw a file not found exception when the file system watcher starts. Therefore, this extra check.
240  tb.Text = CurrentDirectory;
241  return false;
242  }
243  CurrentDirectory = txt;
244  return true;
245  }
246  else
247  {
248  tb.Text = CurrentDirectory;
249  return false;
250  }
251  }
252  };
253  filterBox = new GUITextBox(new RectTransform(new Vector2(0.25f, 1.0f), firstRow.RectTransform))
254  {
255  OverflowClip = true
256  };
257  firstRow.RectTransform.MinSize = new Point(0, firstRow.RectTransform.Children.Max(c => c.MinSize.Y));
258 
259  filterBox.OnTextChanged += (txtbox, txt) =>
260  {
261  RefreshFileList();
262  return true;
263  };
264  //spacing between rows
265  new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), fileListLayout.RectTransform), style: null);
266 
267  fileList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.85f), fileListLayout.RectTransform))
268  {
269  PlaySoundOnSelect = true,
270  OnSelected = (child, userdata) =>
271  {
272  if (userdata is null) { return false; }
273  if (fileBox is null) { return false; }
274 
275  var fileName = (child as GUITextBlock)!.Text.SanitizedValue;
276  fileBox.Text = fileName;
277  if (PlayerInput.DoubleClicked())
278  {
279  bool isDir = userdata is ItemIsDirectory.Yes;
280  if (isDir)
281  {
282  CurrentDirectory += fileName;
283  }
284  else
285  {
286  OnFileSelected?.Invoke(CurrentDirectory + fileName);
287  Open = false;
288  }
289  }
290 
291  return true;
292  }
293  };
294 
295  //spacing between rows
296  new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), fileListLayout.RectTransform), style: null);
297 
298  var thirdRow = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.04f), fileListLayout.RectTransform), true);
299  fileBox = new GUITextBox(new RectTransform(new Vector2(0.7f, 1.0f), thirdRow.RectTransform))
300  {
301  OnEnterPressed = (tb, txt) => openButton?.OnClicked?.Invoke(openButton, null) ?? false
302  };
303 
304  fileTypeDropdown = new GUIDropDown(new RectTransform(new Vector2(0.3f, 1.0f), thirdRow.RectTransform), dropAbove: true)
305  {
306  OnSelected = (child, userdata) =>
307  {
308  currentFileTypePattern = (child as GUITextBlock)!.UserData as string;
309  RefreshFileList();
310 
311  return true;
312  }
313  };
314 
315  fileTypeDropdown.Select(4);
316 
317  //spacing between rows
318  new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), fileListLayout.RectTransform), style: null);
319  var fourthRow = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.04f), fileListLayout.RectTransform), true);
320 
321  //padding for open/cancel buttons
322  new GUIFrame(new RectTransform(new Vector2(0.7f, 1.0f), fourthRow.RectTransform), style: null);
323 
324  openButton = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), fourthRow.RectTransform), TextManager.Get("opensubbutton"))
325  {
326  OnClicked = (btn, obj) =>
327  {
328  if (Directory.Exists(Path.Combine(CurrentDirectory, fileBox.Text)))
329  {
330  CurrentDirectory += fileBox.Text;
331  }
332  if (!File.Exists(CurrentDirectory + fileBox.Text)) { return false; }
333  OnFileSelected?.Invoke(CurrentDirectory + fileBox.Text);
334  Open = false;
335  return false;
336  }
337  };
338  new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), fourthRow.RectTransform), TextManager.Get("cancel"))
339  {
340  OnClicked = (btn, obj) =>
341  {
342  Open = false;
343  return false;
344  }
345  };
346 
347  CurrentDirectory = Directory.GetCurrentDirectory();
348  }
349 
350  public static void ClearFileTypeFilters()
351  {
352  InitIfNecessary();
353  fileTypeDropdown!.ClearChildren();
354  }
355 
356  public static void AddFileTypeFilter(string name, string pattern)
357  {
358  InitIfNecessary();
359  fileTypeDropdown!.AddItem(name + " (" + pattern + ")", pattern);
360  }
361 
362  public static void SelectFileTypeFilter(string pattern)
363  {
364  InitIfNecessary();
365  fileTypeDropdown!.SelectItem(pattern);
366  }
367 
368  public static void RefreshFileList()
369  {
370  InitIfNecessary();
371  fileList!.Content.ClearChildren();
372  fileList.BarScroll = 0.0f;
373 
374  try
375  {
376  var directories = Directory.EnumerateDirectories(currentDirectory, "*" + filterBox!.Text + "*");
377  foreach (var directory in directories)
378  {
379  try
380  {
381  //this will intentionally throw an exception if the directory can't be opened
382  System.IO.Directory.GetDirectories(directory);
383  }
384  catch (UnauthorizedAccessException)
385  {
386  // Skip the folders that can't be accessed.
387  continue;
388  }
389  string txt = directory;
390  if (txt.StartsWith(currentDirectory)) { txt = txt.Substring(currentDirectory.Length); }
391  if (!txt.EndsWith("/")) { txt += "/"; }
392  var itemFrame = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), fileList.Content.RectTransform), txt)
393  {
394  UserData = ItemIsDirectory.Yes
395  };
396  var folderIcon = new GUIImage(new RectTransform(new Point((int)(itemFrame.Rect.Height * 0.8f)), itemFrame.RectTransform, Anchor.CenterLeft)
397  {
398  AbsoluteOffset = new Point((int)(itemFrame.Rect.Height * 0.25f), 0)
399  }, style: "OpenButton", scaleToFit: true);
400  itemFrame.Padding = new Vector4(folderIcon.Rect.Width * 1.5f, itemFrame.Padding.Y, itemFrame.Padding.Z, itemFrame.Padding.W);
401  }
402 
403  IEnumerable<string> files = Enumerable.Empty<string>();
404  if (currentFileTypePattern.IsNullOrEmpty())
405  {
406  files = Directory.GetFiles(currentDirectory);
407  }
408  else
409  {
410  foreach (string pattern in currentFileTypePattern!.Split(','))
411  {
412  string patternTrimmed = pattern.Trim();
413  patternTrimmed = "*" + filterBox.Text + "*" + patternTrimmed;
414  if (files.None())
415  {
416  files = Directory.EnumerateFiles(currentDirectory, patternTrimmed);
417  }
418  else
419  {
420  files = files.Concat(Directory.EnumerateFiles(currentDirectory, patternTrimmed));
421  }
422  }
423  }
424 
425  foreach (var file in files)
426  {
427  string txt = file;
428  if (txt.StartsWith(currentDirectory)) { txt = txt.Substring(currentDirectory.Length); }
429  var itemFrame = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), fileList.Content.RectTransform), txt)
430  {
431  UserData = ItemIsDirectory.No
432  };
433  }
434  }
435  catch (Exception e)
436  {
437  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), fileList.Content.RectTransform), "Could not list items in directory: " + e.Message)
438  {
439  CanBeFocused = false
440  };
441  }
442 
443  fileList.Content.RectTransform.SortChildren(SortFiles);
444 
445  directoryBox!.Text = currentDirectory;
446  fileBox!.Text = "";
447  fileList.Deselect();
448  }
449 
450  public static bool MoveToParentDirectory(GUIButton button, object userdata)
451  {
452  string dir = CurrentDirectory;
453  if (dir.EndsWith("/")) { dir = dir.Substring(0, dir.Length - 1); }
454  int index = dir.LastIndexOf("/");
455  if (index < 0) { return false; }
456  CurrentDirectory = CurrentDirectory.Substring(0, index + 1);
457 
458  return true;
459  }
460 
461  public static void AddToGUIUpdateList()
462  {
463  if (!Open) { return; }
464  backgroundFrame?.AddToGUIUpdateList();
465  }
466  }
467 }