Skip to content

Commit c4a5029

Browse files
committed
Initial save file implementation
1 parent c30ad8b commit c4a5029

File tree

2 files changed

+114
-1
lines changed

2 files changed

+114
-1
lines changed

AssetRipper.NativeDialogs.Example/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ static async Task Main(string[] args)
3737
}
3838
else if (arguments.SaveFile)
3939
{
40-
string? file = null;
40+
string? file = await SaveFileDialog.SaveFileAsync();
4141
Print(file);
4242
}
4343
else if (arguments.Message)
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
using System.Buffers;
2+
using System.Runtime.CompilerServices;
3+
using System.Runtime.Versioning;
4+
using TerraFX.Interop.Windows;
5+
6+
namespace AssetRipper.NativeDialogs;
7+
8+
public static class SaveFileDialog
9+
{
10+
public static bool Supported =>
11+
OperatingSystem.IsWindows() ||
12+
OperatingSystem.IsMacOS() ||
13+
(OperatingSystem.IsLinux() && Gtk.Global.IsSupported);
14+
15+
public static Task<string?> SaveFileAsync()
16+
{
17+
if (OperatingSystem.IsWindows())
18+
{
19+
return SaveFileAsyncWindows();
20+
}
21+
else if (OperatingSystem.IsMacOS())
22+
{
23+
return SaveFileAsyncMacOS();
24+
}
25+
else if (OperatingSystem.IsLinux())
26+
{
27+
return SaveFileAsyncLinux();
28+
}
29+
else
30+
{
31+
return Task.FromResult<string?>(null);
32+
}
33+
}
34+
35+
[SupportedOSPlatform("windows")]
36+
private unsafe static Task<string?> SaveFileAsyncWindows()
37+
{
38+
// https://learn.microsoft.com/en-us/windows/win32/api/commdlg/ns-commdlg-openfilenamew
39+
40+
char[] buffer = ArrayPool<char>.Shared.Rent(ushort.MaxValue + 1); // Should be enough for the overwhelming majority of cases.
41+
new Span<char>(buffer).Clear();
42+
43+
string filter = "All Files\0*.*\0\0";
44+
45+
fixed (char* bufferPtr = buffer)
46+
fixed (char* filterPtr = filter)
47+
{
48+
OPENFILENAMEW ofn = default;
49+
ofn.lStructSize = (uint)Unsafe.SizeOf<OPENFILENAMEW>();
50+
ofn.hwndOwner = default; // No owner window.
51+
ofn.lpstrFile = bufferPtr;
52+
ofn.nMaxFile = (uint)buffer.Length;
53+
ofn.lpstrFilter = filterPtr;
54+
ofn.nFilterIndex = 1; // The first pair of strings has an index value of 1.
55+
ofn.Flags = default;
56+
if (Windows.GetSaveFileNameW(&ofn))
57+
{
58+
int length = Array.IndexOf(buffer, '\0');
59+
if (length > 0)
60+
{
61+
string result = new(buffer, 0, length);
62+
ArrayPool<char>.Shared.Return(buffer);
63+
return Task.FromResult<string?>(result);
64+
}
65+
}
66+
}
67+
68+
ArrayPool<char>.Shared.Return(buffer);
69+
return Task.FromResult<string?>(null);
70+
}
71+
72+
[SupportedOSPlatform("macos")]
73+
private static Task<string?> SaveFileAsyncMacOS()
74+
{
75+
return ProcessExecutor.TryRun("osascript", "-e", "POSIX path of (choose file)");
76+
}
77+
78+
[SupportedOSPlatform("linux")]
79+
private static Task<string?> SaveFileAsyncLinux()
80+
{
81+
if (Gtk.Global.IsSupported)
82+
{
83+
string? result;
84+
Gtk.Application.Init(); // spins a main loop
85+
try
86+
{
87+
using Gtk.FileChooserNative dlg = new(
88+
"Save a file", null,
89+
Gtk.FileChooserAction.Save, "Save", "Cancel");
90+
91+
if (dlg.Run() == (int)Gtk.ResponseType.Accept)
92+
{
93+
result = dlg.File?.Path;
94+
}
95+
else
96+
{
97+
result = null; // User canceled the dialog
98+
}
99+
}
100+
finally
101+
{
102+
Gtk.Application.Quit(); // stops the main loop
103+
}
104+
105+
return Task.FromResult(result);
106+
}
107+
else
108+
{
109+
// Fallback
110+
return Task.FromResult<string?>(null);
111+
}
112+
}
113+
}

0 commit comments

Comments
 (0)