Skip to content

Commit 8284dd9

Browse files
committed
Open files initial implementation
1 parent 02a3f75 commit 8284dd9

File tree

3 files changed

+160
-4
lines changed

3 files changed

+160
-4
lines changed

AssetRipper.NativeDialogs.Example/Arguments.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ internal sealed partial class Arguments
1111
[Description("Show the open file dialog.")]
1212
public bool OpenFile { get; set; } = false;
1313

14+
[CommandLineArgument]
15+
[Description("Show the open file dialog and allow multiple files.")]
16+
public bool OpenFiles { get; set; } = false;
17+
1418
[CommandLineArgument]
1519
[Description("Show the open folder dialog.")]
1620
public bool OpenFolder { get; set; } = false;

AssetRipper.NativeDialogs.Example/Program.cs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ internal static class Program
44
{
55
static async Task Main(string[] args)
66
{
7-
/*Arguments? arguments = Arguments.Parse(args);
7+
Arguments? arguments = Arguments.Parse(args);
88
if (arguments is null)
99
{
1010
return;
@@ -15,6 +15,21 @@ static async Task Main(string[] args)
1515
string? file = await OpenFileDialog.OpenFileAsync();
1616
Print(file);
1717
}
18+
else if (arguments.OpenFiles)
19+
{
20+
string[]? files = await OpenFilesDialog.OpenFilesAsync();
21+
if (files is null || files.Length == 0)
22+
{
23+
Console.WriteLine("No files selected.");
24+
}
25+
else
26+
{
27+
foreach (string file in files)
28+
{
29+
Print(file);
30+
}
31+
}
32+
}
1833
else if (arguments.OpenFolder)
1934
{
2035
string? folder = null;
@@ -32,9 +47,7 @@ static async Task Main(string[] args)
3247
else
3348
{
3449
Console.WriteLine("No action specified. Use --help for usage information.");
35-
}*/
36-
string? file = await OpenFileDialog.OpenFileAsync();
37-
Print(file);
50+
}
3851
}
3952

4053
private static void Print(string? value)
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
using System.Buffers;
2+
using System.Runtime.CompilerServices;
3+
using System.Runtime.Versioning;
4+
using System.Text;
5+
using TerraFX.Interop.Windows;
6+
7+
namespace AssetRipper.NativeDialogs;
8+
9+
public static class OpenFilesDialog
10+
{
11+
public static bool Supported =>
12+
OperatingSystem.IsWindows() ||
13+
OperatingSystem.IsMacOS() ||
14+
(OperatingSystem.IsLinux() && Gtk.Global.IsSupported);
15+
16+
public static Task<string[]?> OpenFilesAsync(OpenFileDialogOptions? options = null)
17+
{
18+
options ??= OpenFileDialogOptions.Default;
19+
if (OperatingSystem.IsWindows())
20+
{
21+
return OpenFilesAsyncWindows(options);
22+
}
23+
else if (OperatingSystem.IsMacOS())
24+
{
25+
return OpenFilesAsyncMacOS(options);
26+
}
27+
else if (OperatingSystem.IsLinux())
28+
{
29+
return OpenFilesAsyncLinux(options);
30+
}
31+
else
32+
{
33+
return Task.FromResult<string[]?>(null);
34+
}
35+
}
36+
37+
[SupportedOSPlatform("windows")]
38+
private unsafe static Task<string[]?> OpenFilesAsyncWindows(OpenFileDialogOptions options)
39+
{
40+
// https://learn.microsoft.com/en-us/windows/win32/api/commdlg/ns-commdlg-openfilenamew
41+
42+
char[] buffer = ArrayPool<char>.Shared.Rent(ushort.MaxValue + 1); // Should be enough for the overwhelming majority of cases.
43+
new Span<char>(buffer).Clear();
44+
45+
string filter;
46+
if (options.Filters.Count == 0)
47+
{
48+
filter = "All Files\0*.*\0\0";
49+
}
50+
else
51+
{
52+
StringBuilder filterBuilder = new();
53+
foreach (KeyValuePair<string, string> pair in options.Filters)
54+
{
55+
filterBuilder
56+
.Append(pair.Key).Append('\0')
57+
.Append("*.").Append(pair.Value).Append('\0');
58+
}
59+
filterBuilder.Append('\0'); // End of filter list
60+
filter = filterBuilder.ToString();
61+
}
62+
63+
fixed (char* bufferPtr = buffer)
64+
fixed (char* filterPtr = filter)
65+
{
66+
OPENFILENAMEW ofn = default;
67+
ofn.lStructSize = (uint)Unsafe.SizeOf<OPENFILENAMEW>();
68+
ofn.hwndOwner = default; // No owner window.
69+
ofn.lpstrFile = bufferPtr;
70+
ofn.nMaxFile = (uint)buffer.Length;
71+
ofn.lpstrFilter = filterPtr;
72+
ofn.nFilterIndex = 1; // The first pair of strings has an index value of 1.
73+
ofn.Flags = OFN.OFN_PATHMUSTEXIST | OFN.OFN_FILEMUSTEXIST | OFN.OFN_ALLOWMULTISELECT | OFN.OFN_EXPLORER;
74+
if (Windows.GetOpenFileNameW(&ofn) && buffer[^1] == 0)
75+
{
76+
List<string> files = [];
77+
78+
int directoryLength = Array.IndexOf(buffer, '\0');
79+
string directory = new(buffer, 0, directoryLength);
80+
81+
int startIndex = directoryLength + 1;
82+
while (startIndex < buffer.Length && buffer[startIndex] != '\0')
83+
{
84+
int endIndex = Array.IndexOf(buffer, '\0', startIndex);
85+
string fileName = new(buffer, startIndex, endIndex - startIndex);
86+
files.Add(Path.Combine(directory, fileName));
87+
startIndex = endIndex + 1; // Move to the next file name
88+
}
89+
90+
ArrayPool<char>.Shared.Return(buffer);
91+
if (files.Count > 0)
92+
{
93+
return Task.FromResult<string[]?>(files.ToArray());
94+
}
95+
else
96+
{
97+
// If a single file was selected, the system appends it to the directory path.
98+
return Task.FromResult<string[]?>([directory]);
99+
}
100+
}
101+
}
102+
103+
ArrayPool<char>.Shared.Return(buffer);
104+
return Task.FromResult<string[]?>(null);
105+
}
106+
107+
[SupportedOSPlatform("macos")]
108+
private static async Task<string[]?> OpenFilesAsyncMacOS(OpenFileDialogOptions options)
109+
{
110+
ReadOnlySpan<string> arguments =
111+
[
112+
"-e", "set theFiles to choose file with multiple selections allowed",
113+
"-e", "set filePaths to {}",
114+
"-e", "repeat with aFile in theFiles",
115+
"-e", "set end of filePaths to POSIX path of aFile",
116+
"-e", "end repeat",
117+
"-e", "set text item delimiters to \":\"",
118+
"-e", "return filePaths as string",
119+
];
120+
string? output = await ProcessExecutor.TryRun("osascript", arguments);
121+
if (string.IsNullOrEmpty(output))
122+
{
123+
return null; // User canceled the dialog
124+
}
125+
return output.Split(':');
126+
}
127+
128+
[SupportedOSPlatform("linux")]
129+
private static async Task<string[]?> OpenFilesAsyncLinux(OpenFileDialogOptions options)
130+
{
131+
// Todo: proper Linux implementation
132+
string? path = await OpenFileDialog.OpenFileAsync();
133+
if (string.IsNullOrEmpty(path))
134+
{
135+
return null; // User canceled the dialog
136+
}
137+
return [path];
138+
}
139+
}

0 commit comments

Comments
 (0)