-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMacOpenPanel.cs
More file actions
221 lines (186 loc) · 11.1 KB
/
Copy pathMacOpenPanel.cs
File metadata and controls
221 lines (186 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
using System;
using System.Linq;
using System.Runtime.InteropServices;
using UnityEngine;
namespace NativeFileBrowser
{
public class MacOpenPanel
{
// libobjc.dylib: Interact with Objective-C through pure C functions via a reflection-like API
// https://developer.apple.com/documentation/objectivec/objective-c_runtime
[DllImport("/usr/lib/libobjc.dylib")]
private static extern IntPtr objc_getClass([MarshalAs(UnmanagedType.LPUTF8Str)] string name);
[DllImport("/usr/lib/libobjc.dylib")]
private static extern IntPtr sel_getUid([MarshalAs(UnmanagedType.LPUTF8Str)] string name);
// objc_msgSend is a weird one: it's a varargs-like C function (but not quite varargs, e.g. floats are not promoted to doubles).
// Declare multiple overload aliases to the same EntryPoint in C#, and it works out fine. Add overloads for whatever you end up needing.
[DllImport("/usr/lib/libobjc.dylib", EntryPoint = "objc_msgSend")]
private static extern IntPtr objc_msgSend_IntPtr_void(IntPtr receiver, IntPtr selector);
[DllImport("/usr/lib/libobjc.dylib", EntryPoint = "objc_msgSend")]
private static extern IntPtr objc_msgSend_IntPtr_uint(IntPtr receiver, IntPtr selector, uint value);
[DllImport("/usr/lib/libobjc.dylib", EntryPoint = "objc_msgSend")]
private static extern IntPtr objc_msgSend_IntPtr_ulong(IntPtr receiver, IntPtr selector, ulong value);
[DllImport("/usr/lib/libobjc.dylib", EntryPoint = "objc_msgSend")]
private static extern IntPtr objc_msgSend_IntPtr_IntPtr(IntPtr receiver, IntPtr selector, IntPtr value);
[DllImport("/usr/lib/libobjc.dylib", EntryPoint = "objc_msgSend")]
private static extern IntPtr objc_msgSend_IntPtr_IntPtr_ulong(IntPtr receiver, IntPtr selector, IntPtr value1,
ulong value2);
[DllImport("/usr/lib/libobjc.dylib", EntryPoint = "objc_msgSend")]
private static extern IntPtr objc_msgSend_IntPtr_string(IntPtr receiver, IntPtr selector,
[MarshalAs(UnmanagedType.LPUTF8Str)] string value);
[DllImport("/usr/lib/libobjc.dylib", EntryPoint = "objc_msgSend")]
private static extern int objc_msgSend_int_void(IntPtr receiver, IntPtr selector);
[DllImport("/usr/lib/libobjc.dylib", EntryPoint = "objc_msgSend")]
private static extern ulong objc_msgSend_ulong_void(IntPtr receiver, IntPtr selector);
[DllImport("/usr/lib/libobjc.dylib", EntryPoint = "objc_msgSend")]
private static extern void objc_msgSend_void_byte(IntPtr receiver, IntPtr selector, byte value);
[DllImport("/usr/lib/libobjc.dylib", EntryPoint = "objc_msgSend")]
private static extern void objc_msgSend_void_IntPtr(IntPtr receiver, IntPtr selector, IntPtr value);
private enum NSModalResponse
{
Continue = -1002,
Abort = -1001,
Stop = -1000,
Cancel = 0,
Ok = 1,
}
// NSOpenPanel docs: https://developer.apple.com/documentation/appkit/nsopenpanel
private static readonly IntPtr NsOpenPanel = objc_getClass("NSOpenPanel");
private static readonly IntPtr NsString = objc_getClass("NSString");
private static readonly IntPtr NsArray = objc_getClass("NSArray");
private static readonly IntPtr NsUrl = objc_getClass("NSURL");
private static readonly IntPtr UtType = objc_getClass("UTType");
private static readonly IntPtr OpenPanel = sel_getUid("openPanel");
private static readonly IntPtr SetCanChooseFiles = sel_getUid("setCanChooseFiles:");
private static readonly IntPtr SetCanChooseDirectories = sel_getUid("setCanChooseDirectories:");
private static readonly IntPtr SetAllowsMultipleSelection = sel_getUid("setAllowsMultipleSelection:");
private static readonly IntPtr SetAllowedContentTypes = sel_getUid("setAllowedContentTypes:");
private static readonly IntPtr SetMessage = sel_getUid("setMessage:");
private static readonly IntPtr SetDirectoryUrl = sel_getUid("setDirectoryURL:");
private static readonly IntPtr URLs = sel_getUid("URLs");
private static readonly IntPtr RunModal = sel_getUid("runModal");
private static readonly IntPtr Path = sel_getUid("path");
private static readonly IntPtr CStringUsingEncoding = sel_getUid("cStringUsingEncoding:");
private static readonly IntPtr Count = sel_getUid("count");
private static readonly IntPtr ObjectAtIndex = sel_getUid("objectAtIndex:");
private static readonly IntPtr StringWithUtf8String = sel_getUid("stringWithUTF8String:");
private static readonly IntPtr TypeWithFilenameExtension = sel_getUid("typeWithFilenameExtension:");
private static readonly IntPtr FileUrlWithPath = sel_getUid("fileURLWithPath:");
private static readonly IntPtr ArrayWithObjectsCount = sel_getUid("arrayWithObjects:count:");
// The documentation states canChooseFiles is a property, but "S" is missing from property_getAttributes
// (and so property_copyAttributeValue("S") also returns null). Not sure what's up with that. I then
// manually printed class_copyMethodList and found the name of "setCanChooseFiles:" (with colon).
public static string[] Pick(FileBrowserSettings settings)
{
var panel = objc_msgSend_IntPtr_void(NsOpenPanel, OpenPanel);
objc_msgSend_void_byte(panel, SetCanChooseFiles,
settings.DialogKind == DialogKind.File ? (byte)1 : (byte)0);
objc_msgSend_void_byte(panel, SetCanChooseDirectories,
settings.DialogKind == DialogKind.Folder ? (byte)1 : (byte)0);
objc_msgSend_void_byte(panel, SetAllowsMultipleSelection,
settings.MultiSelectKind == MultiSelectKind.Multiple ? (byte)1 : (byte)0);
if (settings.Title != null)
{
// Title is no longer displayed on macos. Set message instead.
var titleStr = objc_msgSend_IntPtr_string(NsString, StringWithUtf8String, settings.Title);
objc_msgSend_void_IntPtr(panel, SetMessage, titleStr);
}
if (settings.InitialFolder != null)
{
var strPath = objc_msgSend_IntPtr_string(NsString, StringWithUtf8String, settings.InitialFolder);
var url = objc_msgSend_IntPtr_IntPtr(NsUrl, FileUrlWithPath, strPath);
objc_msgSend_void_IntPtr(panel, SetDirectoryUrl, url);
}
if (settings.FileExtensionFilters != null)
{
var filters = settings.FileExtensionFilters.SelectMany(f => f.Patterns).Distinct().ToArray();
var utTypes = Marshal.AllocHGlobal(filters.Length * Marshal.SizeOf<IntPtr>());
var actualCount = 0;
foreach (var filterI in filters)
{
var filter = filterI;
// mac wants just the extension, not a *.cs glob pattern
if (filter.StartsWith("*."))
filter = filter.Substring(2);
var filterStr = objc_msgSend_IntPtr_string(NsString, StringWithUtf8String, filter);
var utType = objc_msgSend_IntPtr_IntPtr(UtType, TypeWithFilenameExtension, filterStr);
if (utType == IntPtr.Zero)
{
Debug.LogError($"Creating a UTType with the filename extension \"{filter}\" returned null");
}
else
{
Marshal.WriteIntPtr(utTypes, actualCount++ * Marshal.SizeOf<IntPtr>(), utType);
}
}
if (actualCount > 0)
{
var array = objc_msgSend_IntPtr_IntPtr_ulong(NsArray, ArrayWithObjectsCount, utTypes,
(ulong)actualCount);
objc_msgSend_void_IntPtr(panel, SetAllowedContentTypes, array);
}
Marshal.FreeHGlobal(utTypes);
}
var response = (NSModalResponse)objc_msgSend_int_void(panel, RunModal);
if (response != NSModalResponse.Ok)
return Array.Empty<string>();
var urls = objc_msgSend_IntPtr_void(panel, URLs);
var count = objc_msgSend_ulong_void(urls, Count);
var result = new string[count];
for (var i = 0; i < result.Length; i++)
{
var url = objc_msgSend_IntPtr_ulong(urls, ObjectAtIndex, (ulong)i);
var str = objc_msgSend_IntPtr_void(url, Path);
const uint utf8 = 4;
var ptr = objc_msgSend_IntPtr_uint(str, CStringUsingEncoding, utf8);
var path = Marshal.PtrToStringUTF8(ptr);
result[i] = path;
}
// No need to [panel release] or similar due to things being auto-released, I guess
// at least, I hope that unity runtime has an auto release pool running
return result;
}
// Debugging help:
/*
[DllImport("/usr/lib/libobjc.dylib")]
private static extern IntPtr object_getClass(IntPtr cls);
[DllImport("/usr/lib/libobjc.dylib")]
private static extern IntPtr class_copyMethodList(IntPtr cls, out uint count);
[DllImport("/usr/lib/libobjc.dylib")]
private static extern uint method_getNumberOfArguments(IntPtr method);
[DllImport("/usr/lib/libobjc.dylib")]
private static extern IntPtr method_copyArgumentType(IntPtr method, uint index);
[DllImport("/usr/lib/libobjc.dylib")]
private static extern IntPtr method_getName(IntPtr method);
[DllImport("/usr/lib/libobjc.dylib")]
private static extern IntPtr sel_getName(IntPtr sel);
[DllImport("/usr/lib/libobjc.dylib")]
private static extern IntPtr class_getSuperclass(IntPtr sel);
[DllImport("/usr/lib/libobjc.dylib")]
private static extern IntPtr class_getName(IntPtr sel);
private static void DumpObjectMethods(IntPtr obj)
{
var cls = object_getClass(obj);
DumpClassMethods(cls);
}
private static void DumpClassMethods(IntPtr cls)
{
while (cls != IntPtr.Zero)
{
Debug.Log("--- class name: " + Marshal.PtrToStringUTF8(class_getName(cls)));
var methods = class_copyMethodList(cls, out var count);
for (var i = 0; i < count; i++)
{
var method = Marshal.ReadIntPtr(methods, i * Marshal.SizeOf<IntPtr>());
var args = new string[method_getNumberOfArguments(method)];
for (var argIndex = 0u; argIndex < args.Length; argIndex++)
args[argIndex] = Marshal.PtrToStringUTF8(method_copyArgumentType(method, argIndex));
var methodName = Marshal.PtrToStringUTF8(sel_getName(method_getName(method)));
Debug.Log($"{methodName} --- {string.Join(", ", args)}");
}
cls = class_getSuperclass(cls);
}
}
*/
}
}