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.

637 lines
21KB

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