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.

588 lines
20KB

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