Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions Robust.Client/Graphics/Clyde/Windowing/Sdl3.FileDialog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,19 @@ internal partial class Clyde
{
private sealed partial class Sdl3WindowingImpl : IFileDialogManagerImplementation
{
public async Task<string?> OpenFile(FileDialogFilters? filters)
private const string SdlFileDialogLocationProperty = "SDL.filedialog.location";

public async Task<string?> 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<string?> SaveFile(FileDialogFilters? filters)
public async Task<string?> 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<string?> ShowFileDialogOfType(int type, FileDialogFilters? filters)
private unsafe Task<string?> ShowFileDialogOfType(int type, FileDialogFilters? filters, string? defaultLocation)
{
var props = SDL.SDL_CreateProperties();

Expand All @@ -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);

Expand Down
64 changes: 53 additions & 11 deletions Robust.Client/UserInterface/FileDialogManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -22,20 +24,54 @@ private static FileShare Validate(FileAccess access, FileShare? share)
return realShare;
}

private async Task<string?> Prompt(bool isSave, FileDialogFilters? filters)
private async Task<string?> 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);

Expand All @@ -46,21 +82,25 @@ private static FileShare Validate(FileAccess access, FileShare? share)
public async Task<string?> 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);
}

public async Task<Stream?> 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);
}
Expand All @@ -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;

Expand Down
25 changes: 19 additions & 6 deletions Robust.Client/UserInterface/IFileDialogManager.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.IO;
using System.Threading.Tasks;
using Robust.Client.Graphics;
using Robust.Shared.Utility;

namespace Robust.Client.UserInterface;

Expand All @@ -22,6 +23,10 @@ public interface IFileDialogManager
/// <see langword="null" /> if the user canceled the action.
/// </returns>
/// <param name="filters">Filters for file types that the user can select.</param>
/// <param name="defaultLocation">Best-effort default folder or file path to start the native dialog at.</param>
/// <param name="defaultUserDataLocation">
/// Best-effort default folder or file path under the user-data directory to start the native dialog at.
/// </param>
/// <param name="access">What access is desired from the file operation.</param>
/// <param name="share">
/// What sharing mode is desired from the file operation.
Expand All @@ -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);

/// <summary>
/// Open a file dialog used for getting the file name of the selected file.
Expand All @@ -44,7 +51,9 @@ public interface IFileDialogManager
Task<string?> GetName(
FileDialogFilters? filters = null,
FileAccess access = FileAccess.ReadWrite,
FileShare? share = null);
FileShare? share = null,
string? defaultLocation = null,
ResPath? defaultUserDataLocation = null);

/// <summary>
/// Open a file dialog used for opening the selected file.
Expand All @@ -57,7 +66,9 @@ public interface IFileDialogManager
Task<Stream?> OpenFile(
FileDialogFilters? filters = null,
FileAccess access = FileAccess.ReadWrite,
FileShare? share = null);
FileShare? share = null,
string? defaultLocation = null,
ResPath? defaultUserDataLocation = null);

/// <summary>
/// Open a file dialog used for saving a single file.
Expand All @@ -72,14 +83,16 @@ 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);
}

/// <summary>
/// Internal implementation interface used to connect <see cref="FileDialogManager"/> and <see cref="IClydeInternal"/>.
/// </summary>
internal interface IFileDialogManagerImplementation
{
Task<string?> OpenFile(FileDialogFilters? filters);
Task<string?> SaveFile(FileDialogFilters? filters);
Task<string?> OpenFile(FileDialogFilters? filters, string? defaultLocation);
Task<string?> SaveFile(FileDialogFilters? filters, string? defaultLocation);
}
Loading