Skip to content

Commit c420967

Browse files
committed
Implement open file and open folder for Windows and Linux
1 parent 8a076dc commit c420967

File tree

2 files changed

+182
-23
lines changed

2 files changed

+182
-23
lines changed

AssetRipper.NativeDialogs/OpenFileDialog.cs

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,8 @@ public static class OpenFileDialog
4040
char[] buffer = ArrayPool<char>.Shared.Rent(ushort.MaxValue + 1); // Should be enough for the overwhelming majority of cases.
4141
new Span<char>(buffer).Clear();
4242

43-
string filter = "All Files\0*.*\0\0";
44-
4543
fixed (char* bufferPtr = buffer)
46-
fixed (char* filterPtr = filter)
44+
fixed (char* filterPtr = "All Files\0*.*\0")
4745
{
4846
OPENFILENAMEW ofn = default;
4947
ofn.lStructSize = (uint)Unsafe.SizeOf<OPENFILENAMEW>();
@@ -90,7 +88,7 @@ public static class OpenFileDialog
9088

9189
if (dlg.Run() == (int)Gtk.ResponseType.Accept)
9290
{
93-
result = dlg.File?.Path;
91+
result = dlg.Filename;
9492
}
9593
else
9694
{
@@ -139,10 +137,8 @@ public static class OpenFileDialog
139137
char[] buffer = ArrayPool<char>.Shared.Rent(ushort.MaxValue + 1); // Should be enough for the overwhelming majority of cases.
140138
new Span<char>(buffer).Clear();
141139

142-
string filter = "All Files\0*.*\0\0";
143-
144140
fixed (char* bufferPtr = buffer)
145-
fixed (char* filterPtr = filter)
141+
fixed (char* filterPtr = "All Files\0*.*\0")
146142
{
147143
OPENFILENAMEW ofn = default;
148144
ofn.lStructSize = (uint)Unsafe.SizeOf<OPENFILENAMEW>();
@@ -207,14 +203,40 @@ public static class OpenFileDialog
207203
}
208204

209205
[SupportedOSPlatform("linux")]
210-
private static async Task<string[]?> OpenFilesLinux()
206+
private static Task<string[]?> OpenFilesLinux()
211207
{
212-
// Todo: proper Linux implementation
213-
string? path = await OpenFile();
214-
if (string.IsNullOrEmpty(path))
208+
if (Gtk.Global.IsSupported)
215209
{
216-
return null; // User canceled the dialog
210+
string[]? result;
211+
Gtk.Application.Init(); // spins a main loop
212+
try
213+
{
214+
using Gtk.FileChooserNative dlg = new(
215+
"Open files", null,
216+
Gtk.FileChooserAction.Open, "Open", "Cancel");
217+
218+
dlg.SelectMultiple = true; // Allow multiple folder selection
219+
220+
if (dlg.Run() == (int)Gtk.ResponseType.Accept)
221+
{
222+
result = dlg.Filenames;
223+
}
224+
else
225+
{
226+
result = null; // User canceled the dialog
227+
}
228+
}
229+
finally
230+
{
231+
Gtk.Application.Quit(); // stops the main loop
232+
}
233+
234+
return Task.FromResult(result);
235+
}
236+
else
237+
{
238+
// Fallback
239+
return Task.FromResult<string[]?>(null);
217240
}
218-
return [path];
219241
}
220242
}

AssetRipper.NativeDialogs/OpenFolderDialog.cs

Lines changed: 147 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
using System.Runtime.Versioning;
1+
using System.Diagnostics;
2+
using System.Runtime.Versioning;
3+
using TerraFX.Interop.Windows;
24

35
namespace AssetRipper.NativeDialogs;
46

@@ -30,9 +32,88 @@ public static class OpenFolderDialog
3032
}
3133

3234
[SupportedOSPlatform("windows")]
33-
private unsafe static Task<string?> OpenFolderWindows()
35+
private static Task<string?> OpenFolderWindows()
3436
{
35-
return Task.FromResult<string?>(null);
37+
TaskCompletionSource<string?> tcs = new();
38+
39+
Thread thread = new(() =>
40+
{
41+
try
42+
{
43+
// Run the STA work
44+
Debug.Assert(OperatingSystem.IsWindows());
45+
string? result = OpenFolderWindowsInternal();
46+
47+
// Mark task complete
48+
tcs.SetResult(result);
49+
}
50+
catch (Exception ex)
51+
{
52+
tcs.SetException(ex);
53+
}
54+
});
55+
thread.SetApartmentState(ApartmentState.STA);
56+
thread.Start();
57+
58+
return tcs.Task;
59+
}
60+
61+
[SupportedOSPlatform("windows")]
62+
private unsafe static string? OpenFolderWindowsInternal()
63+
{
64+
string? result = null;
65+
66+
HRESULT hr = Windows.CoInitializeEx(null, (uint)COINIT.COINIT_APARTMENTTHREADED);
67+
switch (hr.Value)
68+
{
69+
case S.S_OK:
70+
Windows.CoUninitialize();
71+
throw new InvalidOperationException("CoInitializeEx failed with S_OK, which should never happen because .NET is supposed to initialize the thread.");
72+
case S.S_FALSE:
73+
// The thread is already initialized, which is expected.
74+
break;
75+
case RPC.RPC_E_CHANGED_MODE:
76+
// The thread is already initialized with a different mode, which is unexpected.
77+
throw new InvalidOperationException("CoInitializeEx failed with RPC_E_CHANGED_MODE, which should never happen because we only call this method in STA threads.");
78+
}
79+
80+
IFileOpenDialog* pFileDialog = null;
81+
82+
// Assign the CLSID and IID for the FileOpenDialog.
83+
Guid CLSID_FileOpenDialog = new("DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7");
84+
Guid IID_IFileOpenDialog = new("D57C7288-D4AD-4768-BE02-9D969532D960");
85+
86+
// Create the FileOpenDialog object.
87+
hr = Windows.CoCreateInstance(&CLSID_FileOpenDialog, null, (uint)CLSCTX.CLSCTX_INPROC_SERVER, &IID_IFileOpenDialog, (void**)&pFileDialog);
88+
if (Windows.SUCCEEDED(hr))
89+
{
90+
// Set the options on the dialog.
91+
uint dwOptions;
92+
pFileDialog->GetOptions(&dwOptions);
93+
pFileDialog->SetOptions(dwOptions | (uint)FILEOPENDIALOGOPTIONS.FOS_PICKFOLDERS | (uint)FILEOPENDIALOGOPTIONS.FOS_FORCEFILESYSTEM);
94+
95+
// Show the dialog
96+
hr = pFileDialog->Show(default);
97+
if (Windows.SUCCEEDED(hr))
98+
{
99+
IShellItem* pItem;
100+
hr = pFileDialog->GetResult(&pItem);
101+
if (Windows.SUCCEEDED(hr))
102+
{
103+
char* pszFilePath = null;
104+
hr = pItem->GetDisplayName(SIGDN.SIGDN_FILESYSPATH, &pszFilePath);
105+
if (Windows.SUCCEEDED(hr))
106+
{
107+
result = new string(pszFilePath);
108+
Windows.CoTaskMemFree(pszFilePath);
109+
}
110+
pItem->Release();
111+
}
112+
}
113+
pFileDialog->Release();
114+
}
115+
116+
return result;
36117
}
37118

38119
[SupportedOSPlatform("macos")]
@@ -44,7 +125,37 @@ public static class OpenFolderDialog
44125
[SupportedOSPlatform("linux")]
45126
private static Task<string?> OpenFolderLinux()
46127
{
47-
return Task.FromResult<string?>(null);
128+
if (Gtk.Global.IsSupported)
129+
{
130+
string? result;
131+
Gtk.Application.Init(); // spins a main loop
132+
try
133+
{
134+
using Gtk.FileChooserNative dlg = new(
135+
"Open a folder", null,
136+
Gtk.FileChooserAction.SelectFolder, "Open", "Cancel");
137+
138+
if (dlg.Run() == (int)Gtk.ResponseType.Accept)
139+
{
140+
result = dlg.Filename;
141+
}
142+
else
143+
{
144+
result = null; // User canceled the dialog
145+
}
146+
}
147+
finally
148+
{
149+
Gtk.Application.Quit(); // stops the main loop
150+
}
151+
152+
return Task.FromResult(result);
153+
}
154+
else
155+
{
156+
// Fallback
157+
return Task.FromResult<string?>(null);
158+
}
48159
}
49160

50161
public static Task<string[]?> OpenFolders()
@@ -101,14 +212,40 @@ public static class OpenFolderDialog
101212
}
102213

103214
[SupportedOSPlatform("linux")]
104-
private static async Task<string[]?> OpenFoldersLinux()
215+
private static Task<string[]?> OpenFoldersLinux()
105216
{
106-
// Todo: proper Linux implementation
107-
string? path = await OpenFolder();
108-
if (string.IsNullOrEmpty(path))
217+
if (Gtk.Global.IsSupported)
109218
{
110-
return null; // User canceled the dialog
219+
string[]? result;
220+
Gtk.Application.Init(); // spins a main loop
221+
try
222+
{
223+
using Gtk.FileChooserNative dlg = new(
224+
"Open folders", null,
225+
Gtk.FileChooserAction.SelectFolder, "Open", "Cancel");
226+
227+
dlg.SelectMultiple = true; // Allow multiple folder selection
228+
229+
if (dlg.Run() == (int)Gtk.ResponseType.Accept)
230+
{
231+
result = dlg.Filenames;
232+
}
233+
else
234+
{
235+
result = null; // User canceled the dialog
236+
}
237+
}
238+
finally
239+
{
240+
Gtk.Application.Quit(); // stops the main loop
241+
}
242+
243+
return Task.FromResult(result);
244+
}
245+
else
246+
{
247+
// Fallback
248+
return Task.FromResult<string[]?>(null);
111249
}
112-
return [path];
113250
}
114251
}

0 commit comments

Comments
 (0)