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.

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