DISTRHO Plugin Framework
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

535 lines
18KB

  1. /*
  2. * DISTRHO Plugin Framework (DPF)
  3. * Copyright (C) 2012-2021 Filipe Coelho <falktx@falktx.com>
  4. *
  5. * Permission to use, copy, modify, and/or distribute this software for any purpose with
  6. * or without fee is hereby granted, provided that the above copyright notice and this
  7. * permission notice appear in all copies.
  8. *
  9. * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
  10. * TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN
  11. * NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
  12. * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER
  13. * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
  14. * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  15. */
  16. #include "FileBrowserDialog.hpp"
  17. #include "ScopedPointer.hpp"
  18. #include "String.hpp"
  19. #ifdef DISTRHO_OS_MAC
  20. # import <Cocoa/Cocoa.h>
  21. #endif
  22. #ifdef DISTRHO_OS_WINDOWS
  23. # include <process.h>
  24. # include <winsock2.h>
  25. # include <windows.h>
  26. # include <vector>
  27. #endif
  28. #ifdef HAVE_DBUS
  29. # include <dbus/dbus.h>
  30. #endif
  31. #ifdef HAVE_X11
  32. # define DBLCLKTME 400
  33. # include "sofd/libsofd.h"
  34. # include "sofd/libsofd.c"
  35. #endif
  36. START_NAMESPACE_DISTRHO
  37. // --------------------------------------------------------------------------------------------------------------------
  38. // static pointer used for signal null/none action taken
  39. static const char* const kSelectedFileCancelled = "__dpf_cancelled__";
  40. struct FileBrowserData {
  41. const char* selectedFile;
  42. #ifdef DISTRHO_OS_MAC
  43. NSOpenPanel* nspanel;
  44. #endif
  45. #ifdef HAVE_DBUS
  46. DBusConnection* dbuscon;
  47. #endif
  48. #ifdef HAVE_X11
  49. Display* x11display;
  50. #endif
  51. #ifdef DISTRHO_OS_WINDOWS
  52. OPENFILENAMEW ofn;
  53. volatile bool threadCancelled;
  54. uintptr_t threadHandle;
  55. std::vector<WCHAR> fileNameW;
  56. std::vector<WCHAR> startDirW;
  57. std::vector<WCHAR> titleW;
  58. const bool isEmbed;
  59. FileBrowserData(const bool embed)
  60. : selectedFile(nullptr),
  61. threadCancelled(false),
  62. threadHandle(0),
  63. fileNameW(32768),
  64. isEmbed(embed)
  65. {
  66. std::memset(&ofn, 0, sizeof(ofn));
  67. ofn.lStructSize = sizeof(ofn);
  68. ofn.lpstrFile = fileNameW.data();
  69. ofn.nMaxFile = (DWORD)fileNameW.size();
  70. }
  71. ~FileBrowserData()
  72. {
  73. if (cancelAndStop() && selectedFile != nullptr && selectedFile != kSelectedFileCancelled)
  74. std::free(const_cast<char*>(selectedFile));
  75. }
  76. void setupAndStart(const char* const startDir,
  77. const char* const windowTitle,
  78. const uintptr_t winId,
  79. const FileBrowserOptions options)
  80. {
  81. ofn.hwndOwner = (HWND)winId;
  82. ofn.Flags = OFN_EXPLORER | OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR;
  83. if (options.buttons.showHidden == FileBrowserOptions::kButtonVisibleChecked)
  84. ofn.Flags |= OFN_FORCESHOWHIDDEN;
  85. ofn.FlagsEx = 0x0;
  86. if (options.buttons.showPlaces == FileBrowserOptions::kButtonInvisible)
  87. ofn.FlagsEx |= OFN_EX_NOPLACESBAR;
  88. startDirW.resize(std::strlen(startDir) + 1);
  89. if (MultiByteToWideChar(CP_UTF8, 0, startDir, -1, startDirW.data(), static_cast<int>(startDirW.size())))
  90. ofn.lpstrInitialDir = startDirW.data();
  91. titleW.resize(std::strlen(windowTitle) + 1);
  92. if (MultiByteToWideChar(CP_UTF8, 0, windowTitle, -1, titleW.data(), static_cast<int>(titleW.size())))
  93. ofn.lpstrTitle = titleW.data();
  94. uint threadId;
  95. threadCancelled = false;
  96. threadHandle = _beginthreadex(nullptr, 0, _run, this, 0, &threadId);
  97. }
  98. bool cancelAndStop()
  99. {
  100. threadCancelled = true;
  101. if (threadHandle == 0)
  102. return true;
  103. // if previous dialog running, carefully close its window
  104. const HWND owner = isEmbed ? GetParent(ofn.hwndOwner) : ofn.hwndOwner;
  105. if (owner != nullptr && owner != INVALID_HANDLE_VALUE)
  106. {
  107. const HWND window = GetWindow(owner, GW_HWNDFIRST);
  108. if (window != nullptr && window != INVALID_HANDLE_VALUE)
  109. {
  110. SendMessage(window, WM_SYSCOMMAND, SC_CLOSE, 0);
  111. SendMessage(window, WM_CLOSE, 0, 0);
  112. WaitForSingleObject((HANDLE)threadHandle, 5000);
  113. }
  114. }
  115. if (threadHandle == 0)
  116. return true;
  117. // not good if thread still running, but let's close the handle anyway
  118. CloseHandle((HANDLE)threadHandle);
  119. threadHandle = 0;
  120. return false;
  121. }
  122. void run()
  123. {
  124. const char* nextFile = nullptr;
  125. if (GetOpenFileNameW(&ofn))
  126. {
  127. if (threadCancelled)
  128. {
  129. threadHandle = 0;
  130. return;
  131. }
  132. // back to UTF-8
  133. std::vector<char> fileNameA(4 * 32768);
  134. if (WideCharToMultiByte(CP_UTF8, 0, fileNameW.data(), -1,
  135. fileNameA.data(), (int)fileNameA.size(),
  136. nullptr, nullptr))
  137. {
  138. nextFile = strdup(fileNameA.data());
  139. }
  140. }
  141. if (threadCancelled)
  142. {
  143. threadHandle = 0;
  144. return;
  145. }
  146. if (nextFile == nullptr)
  147. nextFile = kSelectedFileCancelled;
  148. selectedFile = nextFile;
  149. threadHandle = 0;
  150. }
  151. static unsigned __stdcall _run(void* const arg)
  152. {
  153. // CoInitializeEx(nullptr, COINIT_MULTITHREADED);
  154. static_cast<FileBrowserData*>(arg)->run();
  155. // CoUninitialize();
  156. _endthreadex(0);
  157. return 0;
  158. }
  159. #else // DISTRHO_OS_WINDOWS
  160. FileBrowserData(bool)
  161. : selectedFile(nullptr)
  162. {
  163. #ifdef DISTRHO_OS_MAC
  164. nspanel = [[NSOpenPanel openPanel]retain];
  165. #endif
  166. #ifdef HAVE_DBUS
  167. DBusError err;
  168. dbus_error_init(&err);
  169. if ((dbuscon = dbus_bus_get(DBUS_BUS_SESSION, &err)) != nullptr)
  170. dbus_connection_set_exit_on_disconnect(dbuscon, false);
  171. dbus_error_free(&err);
  172. #endif
  173. #ifdef HAVE_X11
  174. x11display = XOpenDisplay(nullptr);
  175. #endif
  176. }
  177. ~FileBrowserData()
  178. {
  179. #ifdef DISTRHO_OS_MAC
  180. [nspanel release];
  181. #endif
  182. #ifdef HAVE_X11
  183. if (x11display != nullptr)
  184. XCloseDisplay(x11display);
  185. #endif
  186. if (selectedFile != nullptr && selectedFile != kSelectedFileCancelled)
  187. std::free(const_cast<char*>(selectedFile));
  188. }
  189. #endif
  190. };
  191. // --------------------------------------------------------------------------------------------------------------------
  192. #ifdef DISTRHO_FILE_BROWSER_DIALOG_EXTRA_NAMESPACE
  193. namespace DISTRHO_FILE_BROWSER_DIALOG_EXTRA_NAMESPACE {
  194. #endif
  195. // --------------------------------------------------------------------------------------------------------------------
  196. FileBrowserHandle fileBrowserCreate(const bool isEmbed,
  197. const uintptr_t windowId,
  198. const double scaleFactor,
  199. const FileBrowserOptions& options)
  200. {
  201. String startDir(options.startDir);
  202. if (startDir.isEmpty())
  203. {
  204. #ifdef DISTRHO_OS_WINDOWS
  205. if (char* const cwd = _getcwd(nullptr, 0))
  206. {
  207. startDir = cwd;
  208. std::free(cwd);
  209. }
  210. #else
  211. if (char* const cwd = getcwd(nullptr, 0))
  212. {
  213. startDir = cwd;
  214. std::free(cwd);
  215. }
  216. #endif
  217. }
  218. DISTRHO_SAFE_ASSERT_RETURN(startDir.isNotEmpty(), nullptr);
  219. if (! startDir.endsWith(DISTRHO_OS_SEP))
  220. startDir += DISTRHO_OS_SEP_STR;
  221. String windowTitle(options.title);
  222. if (windowTitle.isEmpty())
  223. windowTitle = "FileBrowser";
  224. ScopedPointer<FileBrowserData> handle;
  225. handle = new FileBrowserData(isEmbed);
  226. #ifdef DISTRHO_OS_MAC
  227. NSOpenPanel* const nspanel = handle->nspanel;
  228. DISTRHO_SAFE_ASSERT_RETURN(nspanel != nullptr, nullptr);
  229. [nspanel setAllowsMultipleSelection:NO];
  230. [nspanel setCanChooseDirectories:NO];
  231. [nspanel setCanChooseFiles:YES];
  232. [nspanel setDirectoryURL:[NSURL fileURLWithPath:[NSString stringWithUTF8String:startDir]]];
  233. // TODO file filter using allowedContentTypes: [UTType]
  234. if (options.buttons.listAllFiles == FileBrowserOptions::kButtonVisibleChecked)
  235. [nspanel setAllowsOtherFileTypes:YES];
  236. if (options.buttons.showHidden == FileBrowserOptions::kButtonVisibleChecked)
  237. [nspanel setShowsHiddenFiles:YES];
  238. NSString* const titleString = [[NSString alloc]
  239. initWithBytes:windowTitle
  240. length:strlen(windowTitle)
  241. encoding:NSUTF8StringEncoding];
  242. [nspanel setTitle:titleString];
  243. dispatch_async(dispatch_get_main_queue(), ^
  244. {
  245. [nspanel beginSheetModalForWindow:[(NSView*)windowId window]
  246. completionHandler:^(NSModalResponse result)
  247. {
  248. DISTRHO_SAFE_ASSERT_RETURN(handle != nullptr,);
  249. if (result == NSModalResponseOK && [[nspanel URL] isFileURL])
  250. {
  251. NSString* const path = [[nspanel URL] path];
  252. handle->selectedFile = strdup([path UTF8String]);
  253. }
  254. else
  255. {
  256. handle->selectedFile = kSelectedFileCancelled;
  257. }
  258. }];
  259. });
  260. #endif
  261. #ifdef DISTRHO_OS_WINDOWS
  262. handle->setupAndStart(startDir, windowTitle, windowId, options);
  263. #endif
  264. #ifdef HAVE_DBUS
  265. // optional, can be null
  266. if (DBusConnection* dbuscon = handle->dbuscon)
  267. {
  268. if (DBusMessage* const message = dbus_message_new_method_call("org.freedesktop.portal.Desktop",
  269. "/org/freedesktop/portal/desktop",
  270. "org.freedesktop.portal.FileChooser",
  271. options.saving ? "SaveFile" : "OpenFile"))
  272. {
  273. char windowIdStr[32];
  274. memset(windowIdStr, 0, sizeof(windowIdStr));
  275. snprintf(windowIdStr, sizeof(windowIdStr)-1, "x11:%llx", (ulonglong)windowId);
  276. const char* windowIdStrPtr = windowIdStr;
  277. dbus_message_append_args(message,
  278. DBUS_TYPE_STRING, &windowIdStrPtr,
  279. DBUS_TYPE_STRING, &windowTitle,
  280. DBUS_TYPE_INVALID);
  281. DBusMessageIter iter, array;
  282. dbus_message_iter_init_append(message, &iter);
  283. dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &array);
  284. // here merely as example, does nothing yet
  285. {
  286. DBusMessageIter dict, variant;
  287. const char* const property = "property";
  288. const char* const value = "value";
  289. dbus_message_iter_open_container(&array, DBUS_TYPE_DICT_ENTRY, nullptr, &dict);
  290. dbus_message_iter_append_basic(&dict, DBUS_TYPE_STRING, &property);
  291. dbus_message_iter_open_container(&dict, DBUS_TYPE_VARIANT, "s", &variant);
  292. dbus_message_iter_append_basic(&variant, DBUS_TYPE_STRING, &value);
  293. dbus_message_iter_close_container(&dict, &variant);
  294. dbus_message_iter_close_container(&array, &dict);
  295. }
  296. dbus_message_iter_close_container(&iter, &array);
  297. dbus_connection_send(dbuscon, message, nullptr);
  298. dbus_message_unref(message);
  299. return handle.release();
  300. }
  301. }
  302. #endif
  303. #ifdef HAVE_X11
  304. Display* const x11display = handle->x11display;
  305. DISTRHO_SAFE_ASSERT_RETURN(x11display != nullptr, nullptr);
  306. // unsupported at the moment
  307. if (options.saving)
  308. return nullptr;
  309. DISTRHO_SAFE_ASSERT_RETURN(x_fib_configure(0, startDir) == 0, nullptr);
  310. DISTRHO_SAFE_ASSERT_RETURN(x_fib_configure(1, windowTitle) == 0, nullptr);
  311. const int button1 = options.buttons.showHidden == FileBrowserOptions::kButtonVisibleChecked ? 1
  312. : options.buttons.showHidden == FileBrowserOptions::kButtonVisibleUnchecked ? 0 : -1;
  313. const int button2 = options.buttons.showPlaces == FileBrowserOptions::kButtonVisibleChecked ? 1
  314. : options.buttons.showPlaces == FileBrowserOptions::kButtonVisibleUnchecked ? 0 : -1;
  315. const int button3 = options.buttons.listAllFiles == FileBrowserOptions::kButtonVisibleChecked ? 1
  316. : options.buttons.listAllFiles == FileBrowserOptions::kButtonVisibleUnchecked ? 0 : -1;
  317. x_fib_cfg_buttons(1, button1);
  318. x_fib_cfg_buttons(2, button2);
  319. x_fib_cfg_buttons(3, button3);
  320. if (x_fib_show(x11display, windowId, 0, 0, scaleFactor + 0.5) != 0)
  321. return nullptr;
  322. #endif
  323. return handle.release();
  324. // might be unused
  325. (void)windowId;
  326. (void)scaleFactor;
  327. }
  328. // --------------------------------------------------------------------------------------------------------------------
  329. // returns true if dialog was closed (with or without a file selection)
  330. bool fileBrowserIdle(const FileBrowserHandle handle)
  331. {
  332. #ifdef HAVE_DBUS
  333. if (DBusConnection* dbuscon = handle->dbuscon)
  334. {
  335. while (dbus_connection_dispatch(dbuscon) == DBUS_DISPATCH_DATA_REMAINS) {}
  336. dbus_connection_read_write_dispatch(dbuscon, 0);
  337. if (DBusMessage * message = dbus_connection_pop_message(dbuscon))
  338. {
  339. const char* const interface = dbus_message_get_interface(message);
  340. const char* const member = dbus_message_get_member(message);
  341. if (interface != nullptr && std::strcmp(interface, "org.freedesktop.portal.Request") == 0
  342. && member != nullptr && std::strcmp(member, "Response") == 0)
  343. {
  344. do {
  345. DBusMessageIter iter;
  346. dbus_message_iter_init(message, &iter);
  347. // starts with uint32 for return/exit code
  348. DISTRHO_SAFE_ASSERT_BREAK(dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_UINT32);
  349. uint32_t ret = 1;
  350. dbus_message_iter_get_basic(&iter, &ret);
  351. if (ret != 0)
  352. break;
  353. // next must be array
  354. dbus_message_iter_next(&iter);
  355. DISTRHO_SAFE_ASSERT_BREAK(dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_ARRAY);
  356. // open dict array
  357. DBusMessageIter dictArray;
  358. dbus_message_iter_recurse(&iter, &dictArray);
  359. DISTRHO_SAFE_ASSERT_BREAK(dbus_message_iter_get_arg_type(&dictArray) == DBUS_TYPE_DICT_ENTRY);
  360. // open containing dict
  361. DBusMessageIter dict;
  362. dbus_message_iter_recurse(&dictArray, &dict);
  363. // start with the string "uris"
  364. DISTRHO_SAFE_ASSERT_BREAK(dbus_message_iter_get_arg_type(&dict) == DBUS_TYPE_STRING);
  365. const char* key = nullptr;
  366. dbus_message_iter_get_basic(&dict, &key);
  367. DISTRHO_SAFE_ASSERT_BREAK(key != nullptr && std::strcmp(key, "uris") == 0);
  368. // then comes variant
  369. dbus_message_iter_next(&dict);
  370. DISTRHO_SAFE_ASSERT_BREAK(dbus_message_iter_get_arg_type(&dict) == DBUS_TYPE_VARIANT);
  371. DBusMessageIter variant;
  372. dbus_message_iter_recurse(&dict, &variant);
  373. DISTRHO_SAFE_ASSERT_BREAK(dbus_message_iter_get_arg_type(&variant) == DBUS_TYPE_ARRAY);
  374. // open variant array (variant type is string)
  375. DBusMessageIter variantArray;
  376. dbus_message_iter_recurse(&variant, &variantArray);
  377. DISTRHO_SAFE_ASSERT_BREAK(dbus_message_iter_get_arg_type(&variantArray) == DBUS_TYPE_STRING);
  378. const char* value = nullptr;
  379. dbus_message_iter_get_basic(&variantArray, &value);
  380. // and finally we have our dear value, just make sure it is local
  381. DISTRHO_SAFE_ASSERT_BREAK(value != nullptr);
  382. if (const char* const localvalue = std::strstr(value, "file:///"))
  383. handle->selectedFile = strdup(localvalue + 7);
  384. } while(false);
  385. if (handle->selectedFile == nullptr)
  386. handle->selectedFile = kSelectedFileCancelled;
  387. }
  388. }
  389. }
  390. #endif
  391. #ifdef HAVE_X11
  392. Display* const x11display = handle->x11display;
  393. if (x11display == nullptr)
  394. return false;
  395. XEvent event;
  396. while (XPending(x11display) > 0)
  397. {
  398. XNextEvent(x11display, &event);
  399. if (x_fib_handle_events(x11display, &event) == 0)
  400. continue;
  401. if (x_fib_status() > 0)
  402. handle->selectedFile = x_fib_filename();
  403. else
  404. handle->selectedFile = kSelectedFileCancelled;
  405. x_fib_close(x11display);
  406. XCloseDisplay(x11display);
  407. handle->x11display = nullptr;
  408. break;
  409. }
  410. #endif
  411. return handle->selectedFile != nullptr;
  412. }
  413. // --------------------------------------------------------------------------------------------------------------------
  414. // close sofd file dialog
  415. void fileBrowserClose(const FileBrowserHandle handle)
  416. {
  417. #ifdef HAVE_X11
  418. if (Display* const x11display = handle->x11display)
  419. x_fib_close(x11display);
  420. #endif
  421. delete handle;
  422. }
  423. // --------------------------------------------------------------------------------------------------------------------
  424. // get path chosen via sofd file dialog
  425. const char* fileBrowserGetPath(const FileBrowserHandle handle)
  426. {
  427. return handle->selectedFile != kSelectedFileCancelled ? handle->selectedFile : nullptr;
  428. }
  429. // --------------------------------------------------------------------------------------------------------------------
  430. #ifdef DISTRHO_FILE_BROWSER_DIALOG_EXTRA_NAMESPACE
  431. }
  432. #endif
  433. END_NAMESPACE_DISTRHO