diff --git a/Robust.Client/Graphics/Clyde/Windowing/Sdl3.FileDialog.cs b/Robust.Client/Graphics/Clyde/Windowing/Sdl3.FileDialog.cs index 74e94ae1568..410f23896b0 100644 --- a/Robust.Client/Graphics/Clyde/Windowing/Sdl3.FileDialog.cs +++ b/Robust.Client/Graphics/Clyde/Windowing/Sdl3.FileDialog.cs @@ -13,17 +13,19 @@ internal partial class Clyde { private sealed partial class Sdl3WindowingImpl : IFileDialogManagerImplementation { - public async Task OpenFile(FileDialogFilters? filters) + private const string SdlFileDialogLocationProperty = "SDL.filedialog.location"; + + public async Task OpenFile(FileDialogFilters? filters, string? defaultLocation) { - return await ShowFileDialogOfType(SDL.SDL_FILEDIALOG_OPENFILE, filters); + return await ShowFileDialogOfType(SDL.SDL_FILEDIALOG_OPENFILE, filters, defaultLocation); } - public async Task SaveFile(FileDialogFilters? filters) + public async Task SaveFile(FileDialogFilters? filters, string? defaultLocation) { - return await ShowFileDialogOfType(SDL.SDL_FILEDIALOG_SAVEFILE, filters); + return await ShowFileDialogOfType(SDL.SDL_FILEDIALOG_SAVEFILE, filters, defaultLocation); } - private unsafe Task ShowFileDialogOfType(int type, FileDialogFilters? filters) + private unsafe Task ShowFileDialogOfType(int type, FileDialogFilters? filters, string? defaultLocation) { var props = SDL.SDL_CreateProperties(); @@ -49,6 +51,8 @@ private sealed partial class Sdl3WindowingImpl : IFileDialogManagerImplementatio // NOTE: Giving a parent window is required to avoid the file dialog being blocking on macOS. var mainWindow = (Sdl3WindowReg)_clyde._mainWindow!; SDL.SDL_SetPointerProperty(props, SDL.SDL_PROP_FILE_DIALOG_WINDOW_POINTER, mainWindow.Sdl3Window); + if (!string.IsNullOrWhiteSpace(defaultLocation)) + SDL.SDL_SetStringProperty(props, SdlFileDialogLocationProperty, defaultLocation); var task = ShowFileDialogWithProperties(type, props); diff --git a/Robust.Client/UserInterface/FileDialogManager.cs b/Robust.Client/UserInterface/FileDialogManager.cs index 27342c8755b..c17db450354 100644 --- a/Robust.Client/UserInterface/FileDialogManager.cs +++ b/Robust.Client/UserInterface/FileDialogManager.cs @@ -3,11 +3,13 @@ using System.Threading.Tasks; using Robust.Client.Graphics; using Robust.Shared.Console; +using Robust.Shared.ContentPack; using Robust.Shared.IoC; +using Robust.Shared.Utility; namespace Robust.Client.UserInterface; -internal sealed class FileDialogManager(IClydeInternal clyde) : IFileDialogManager +internal sealed class FileDialogManager(IClydeInternal clyde, IResourceManager resourceManager) : IFileDialogManager { private static FileShare Validate(FileAccess access, FileShare? share) { @@ -22,20 +24,54 @@ private static FileShare Validate(FileAccess access, FileShare? share) return realShare; } - private async Task Prompt(bool isSave, FileDialogFilters? filters) + private async Task Prompt(bool isSave, FileDialogFilters? filters, string? defaultLocation) { if (clyde.FileDialogImpl is { } impl) - return isSave ? await impl.SaveFile(filters) : await impl.OpenFile(filters); + return isSave + ? await impl.SaveFile(filters, defaultLocation) + : await impl.OpenFile(filters, defaultLocation); return null; } + private string? ResolveDefaultLocation(string? defaultLocation, ResPath? defaultUserDataLocation) + { + if (!string.IsNullOrWhiteSpace(defaultLocation)) + return NormalizeNativeDialogDefaultLocation(defaultLocation); + + if (defaultUserDataLocation == null || resourceManager.UserData is not WritableDirProvider userData) + return null; + + try + { + return NormalizeNativeDialogDefaultLocation(userData.GetFullPath(defaultUserDataLocation.Value)); + } + catch (ArgumentException) + { + return null; + } + } + + private static string NormalizeNativeDialogDefaultLocation(string location) + { + if (!Directory.Exists(location) || + location.EndsWith(Path.DirectorySeparatorChar) || + location.EndsWith(Path.AltDirectorySeparatorChar)) + { + return location; + } + + return location + Path.DirectorySeparatorChar; + } + public async Task<(Stream?, string?)> GetFileAndName( FileDialogFilters? filters = null, FileAccess access = FileAccess.ReadWrite, - FileShare? share = null) + FileShare? share = null, + string? defaultLocation = null, + ResPath? defaultUserDataLocation = null) { - var name = await Prompt(false, filters); + var name = await Prompt(false, filters, ResolveDefaultLocation(defaultLocation, defaultUserDataLocation)); if (name == null) return (null, null); @@ -46,10 +82,12 @@ private static FileShare Validate(FileAccess access, FileShare? share) public async Task GetName( FileDialogFilters? filters = null, FileAccess access = FileAccess.ReadWrite, - FileShare? share = null) + FileShare? share = null, + string? defaultLocation = null, + ResPath? defaultUserDataLocation = null) { Validate(access, share); - var name = await Prompt(false, filters); + var name = await Prompt(false, filters, ResolveDefaultLocation(defaultLocation, defaultUserDataLocation)); return name == null ? null : Path.GetFileName(name); } @@ -57,10 +95,12 @@ private static FileShare Validate(FileAccess access, FileShare? share) public async Task OpenFile( FileDialogFilters? filters = null, FileAccess access = FileAccess.ReadWrite, - FileShare? share = null) + FileShare? share = null, + string? defaultLocation = null, + ResPath? defaultUserDataLocation = null) { var realShare = Validate(access, share); - var name = await Prompt(false, filters); + var name = await Prompt(false, filters, ResolveDefaultLocation(defaultLocation, defaultUserDataLocation)); return name == null ? null : File.Open(name, FileMode.Open, access, realShare); } @@ -69,10 +109,12 @@ private static FileShare Validate(FileAccess access, FileShare? share) FileDialogFilters? filters, bool truncate = true, FileAccess access = FileAccess.ReadWrite, - FileShare share = FileShare.None) + FileShare share = FileShare.None, + string? defaultLocation = null, + ResPath? defaultUserDataLocation = null) { Validate(access, share); - var name = await Prompt(true, filters); + var name = await Prompt(true, filters, ResolveDefaultLocation(defaultLocation, defaultUserDataLocation)); if (name == null) return null; diff --git a/Robust.Client/UserInterface/IFileDialogManager.cs b/Robust.Client/UserInterface/IFileDialogManager.cs index 9dcdc9a85ea..3f0d2b62eee 100644 --- a/Robust.Client/UserInterface/IFileDialogManager.cs +++ b/Robust.Client/UserInterface/IFileDialogManager.cs @@ -1,6 +1,7 @@ using System.IO; using System.Threading.Tasks; using Robust.Client.Graphics; +using Robust.Shared.Utility; namespace Robust.Client.UserInterface; @@ -22,6 +23,10 @@ public interface IFileDialogManager /// if the user canceled the action. /// /// Filters for file types that the user can select. + /// Best-effort default folder or file path to start the native dialog at. + /// + /// Best-effort default folder or file path under the user-data directory to start the native dialog at. + /// /// What access is desired from the file operation. /// /// What sharing mode is desired from the file operation. @@ -31,7 +36,9 @@ public interface IFileDialogManager Task<(Stream?, string?)> GetFileAndName( FileDialogFilters? filters = null, FileAccess access = FileAccess.ReadWrite, - FileShare? share = null); + FileShare? share = null, + string? defaultLocation = null, + ResPath? defaultUserDataLocation = null); /// /// Open a file dialog used for getting the file name of the selected file. @@ -44,7 +51,9 @@ public interface IFileDialogManager Task GetName( FileDialogFilters? filters = null, FileAccess access = FileAccess.ReadWrite, - FileShare? share = null); + FileShare? share = null, + string? defaultLocation = null, + ResPath? defaultUserDataLocation = null); /// /// Open a file dialog used for opening the selected file. @@ -57,7 +66,9 @@ public interface IFileDialogManager Task OpenFile( FileDialogFilters? filters = null, FileAccess access = FileAccess.ReadWrite, - FileShare? share = null); + FileShare? share = null, + string? defaultLocation = null, + ResPath? defaultUserDataLocation = null); /// /// Open a file dialog used for saving a single file. @@ -72,7 +83,9 @@ public interface IFileDialogManager FileDialogFilters? filters = null, bool truncate = true, FileAccess access = FileAccess.ReadWrite, - FileShare share = FileShare.None); + FileShare share = FileShare.None, + string? defaultLocation = null, + ResPath? defaultUserDataLocation = null); } /// @@ -80,6 +93,6 @@ public interface IFileDialogManager /// internal interface IFileDialogManagerImplementation { - Task OpenFile(FileDialogFilters? filters); - Task SaveFile(FileDialogFilters? filters); + Task OpenFile(FileDialogFilters? filters, string? defaultLocation); + Task SaveFile(FileDialogFilters? filters, string? defaultLocation); }