Audio plugin host https://kx.studio/carla
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.

interposer-jack-x11.cpp 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  1. /*
  2. * Carla Interposer for JACK Applications X11 control
  3. * Copyright (C) 2014-2022 Filipe Coelho <falktx@falktx.com>
  4. *
  5. * This program is free software; you can redistribute it and/or
  6. * modify it under the terms of the GNU General Public License as
  7. * published by the Free Software Foundation; either version 2 of
  8. * the License, or any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * For a full copy of the GNU General Public License see the doc/GPL.txt file.
  16. */
  17. #include "CarlaLibJackHints.h"
  18. #include "CarlaUtils.hpp"
  19. #include <dlfcn.h>
  20. #ifdef HAVE_X11
  21. # include <X11/Xlib.h>
  22. #endif
  23. // --------------------------------------------------------------------------------------------------------------------
  24. struct ScopedLibOpen {
  25. void* handle;
  26. long long winId;
  27. ScopedLibOpen() noexcept
  28. #ifdef CARLA_OS_MAC
  29. : handle(dlopen("libjack.dylib", RTLD_NOW|RTLD_LOCAL)),
  30. #else
  31. : handle(dlopen("libjack.so.0", RTLD_NOW|RTLD_LOCAL)),
  32. #endif
  33. winId(-1)
  34. {
  35. CARLA_SAFE_ASSERT_RETURN(handle != nullptr,);
  36. if (const char* const winIdStr = std::getenv("CARLA_FRONTEND_WIN_ID"))
  37. {
  38. CARLA_SAFE_ASSERT_RETURN(winIdStr[0] != '\0',);
  39. winId = std::strtoll(winIdStr, nullptr, 16);
  40. }
  41. }
  42. ~ScopedLibOpen() noexcept
  43. {
  44. if (handle != nullptr)
  45. {
  46. dlclose(handle);
  47. handle = nullptr;
  48. }
  49. }
  50. static const ScopedLibOpen& getInstance() noexcept
  51. {
  52. static const ScopedLibOpen slo;
  53. return slo;
  54. }
  55. CARLA_DECLARE_NON_COPYABLE(ScopedLibOpen);
  56. };
  57. // --------------------------------------------------------------------------------------------------------------------
  58. // Current state
  59. typedef enum {
  60. WindowMapNone,
  61. WindowMapNormal,
  62. WindowMapRaised,
  63. WindowMapSubwindows
  64. } WindowMappingType;
  65. typedef enum {
  66. WindowUnmapNone,
  67. WindowUnmapNormal,
  68. WindowUnmapDestroy
  69. } WindowUnmappingType;
  70. #ifdef HAVE_X11
  71. static Display* gCurrentlyMappedDisplay = nullptr;
  72. static Window gCurrentlyMappedWindow = 0;
  73. static bool gSupportsOptionalGui = true;
  74. #endif
  75. static CarlaInterposedCallback gInterposedCallback = nullptr;
  76. static uint gInterposedSessionManager = LIBJACK_SESSION_MANAGER_NONE;
  77. static uint gInterposedHints = 0x0;
  78. static WindowMappingType gCurrentWindowType = WindowMapNone;
  79. static bool gCurrentWindowMapped = false;
  80. static bool gCurrentWindowVisible = false;
  81. #ifdef HAVE_X11
  82. // --------------------------------------------------------------------------------------------------------------------
  83. // Function typedefs
  84. typedef int (*XWindowFunc)(Display*, Window);
  85. typedef int (*XNextEventFunc)(Display*, XEvent*);
  86. // --------------------------------------------------------------------------------------------------------------------
  87. // Calling the real X11 functions
  88. static int real_XMapWindow(Display* display, Window window)
  89. {
  90. static const XWindowFunc func = (XWindowFunc)::dlsym(RTLD_NEXT, "XMapWindow");
  91. CARLA_SAFE_ASSERT_RETURN(func != nullptr, 0);
  92. return func(display, window);
  93. }
  94. static int real_XMapRaised(Display* display, Window window)
  95. {
  96. static const XWindowFunc func = (XWindowFunc)::dlsym(RTLD_NEXT, "XMapRaised");
  97. CARLA_SAFE_ASSERT_RETURN(func != nullptr, 0);
  98. return func(display, window);
  99. }
  100. static int real_XMapSubwindows(Display* display, Window window)
  101. {
  102. static const XWindowFunc func = (XWindowFunc)::dlsym(RTLD_NEXT, "XMapSubwindows");
  103. CARLA_SAFE_ASSERT_RETURN(func != nullptr, 0);
  104. return func(display, window);
  105. }
  106. static int real_XUnmapWindow(Display* display, Window window)
  107. {
  108. static const XWindowFunc func = (XWindowFunc)::dlsym(RTLD_NEXT, "XUnmapWindow");
  109. CARLA_SAFE_ASSERT_RETURN(func != nullptr, 0);
  110. return func(display, window);
  111. }
  112. static int real_XDestroyWindow(Display* display, Window window)
  113. {
  114. static const XWindowFunc func = (XWindowFunc)::dlsym(RTLD_NEXT, "XDestroyWindow");
  115. CARLA_SAFE_ASSERT_RETURN(func != nullptr, 0);
  116. return func(display, window);
  117. }
  118. static int real_XNextEvent(Display* display, XEvent* event)
  119. {
  120. static const XNextEventFunc func = (XNextEventFunc)::dlsym(RTLD_NEXT, "XNextEvent");
  121. CARLA_SAFE_ASSERT_RETURN(func != nullptr, 0);
  122. return func(display, event);
  123. }
  124. // --------------------------------------------------------------------------------------------------------------------
  125. // Custom carla window handling
  126. static int carlaWindowMap(Display* const display, const Window window, const WindowMappingType fallbackFnType)
  127. {
  128. const ScopedLibOpen& slo(ScopedLibOpen::getInstance());
  129. for (;;)
  130. {
  131. if (slo.winId < 0)
  132. break;
  133. if ((gInterposedHints & LIBJACK_FLAG_CONTROL_WINDOW) == 0x0)
  134. break;
  135. Atom atom;
  136. int atomFormat;
  137. unsigned char* atomPtrs;
  138. unsigned long numItems, ignored;
  139. const Atom wmWindowType = XInternAtom(display, "_NET_WM_WINDOW_TYPE", False);
  140. if (XGetWindowProperty(display, window, wmWindowType, 0, ~0L, False, AnyPropertyType,
  141. &atom, &atomFormat, &numItems, &ignored, &atomPtrs) != Success)
  142. {
  143. carla_debug("carlaWindowMap(%p, %lu, %i) - XGetWindowProperty failed", display, window, fallbackFnType);
  144. break;
  145. }
  146. const Atom* const atomValues = (const Atom*)atomPtrs;
  147. bool isMainWindow = (numItems == 0);
  148. for (ulong i=0; i<numItems; ++i)
  149. {
  150. const char* const atomValue(XGetAtomName(display, atomValues[i]));
  151. CARLA_SAFE_ASSERT_CONTINUE(atomValue != nullptr && atomValue[0] != '\0');
  152. if (std::strcmp(atomValue, "_NET_WM_WINDOW_TYPE_COMBO" ) == 0 ||
  153. std::strcmp(atomValue, "_NET_WM_WINDOW_TYPE_DIALOG" ) == 0 ||
  154. std::strcmp(atomValue, "_NET_WM_WINDOW_TYPE_DND" ) == 0 ||
  155. std::strcmp(atomValue, "_NET_WM_WINDOW_TYPE_DOCK" ) == 0 ||
  156. std::strcmp(atomValue, "_NET_WM_WINDOW_TYPE_DROPDOWN_MENU") == 0 ||
  157. std::strcmp(atomValue, "_NET_WM_WINDOW_TYPE_MENU" ) == 0 ||
  158. std::strcmp(atomValue, "_NET_WM_WINDOW_TYPE_NOTIFICATION" ) == 0 ||
  159. std::strcmp(atomValue, "_NET_WM_WINDOW_TYPE_POPUP_MENU" ) == 0 ||
  160. std::strcmp(atomValue, "_NET_WM_WINDOW_TYPE_SPLASH" ) == 0 ||
  161. std::strcmp(atomValue, "_NET_WM_WINDOW_TYPE_TOOLBAR" ) == 0 ||
  162. std::strcmp(atomValue, "_NET_WM_WINDOW_TYPE_TOOLTIP" ) == 0 ||
  163. std::strcmp(atomValue, "_NET_WM_WINDOW_TYPE_UTILITY" ) == 0)
  164. {
  165. isMainWindow = false;
  166. continue;
  167. }
  168. if (std::strcmp(atomValue, "_NET_WM_WINDOW_TYPE_NORMAL") == 0)
  169. {
  170. // window is good, use it if no other types are set
  171. isMainWindow = true;
  172. }
  173. else
  174. {
  175. carla_stdout("=======================================> %s", atomValue);
  176. }
  177. }
  178. if (! isMainWindow)
  179. {
  180. carla_debug("carlaWindowMap(%p, %lu, %i) - not main window, ignoring", display, window, fallbackFnType);
  181. // this has always bothered me...
  182. if (gCurrentlyMappedWindow != 0 && gCurrentWindowMapped && gCurrentWindowVisible)
  183. XSetTransientForHint(display, window, gCurrentlyMappedWindow);
  184. break;
  185. }
  186. Window transientWindow = 0;
  187. if (XGetTransientForHint(display, window, &transientWindow) == Success && transientWindow != 0)
  188. {
  189. carla_stdout("Window has transient set already, ignoring it");
  190. break;
  191. }
  192. // got a new window, we may need to forget last one
  193. if (gCurrentlyMappedDisplay != nullptr && gCurrentlyMappedWindow != 0)
  194. {
  195. // ignore requests against the current mapped window
  196. if (gCurrentlyMappedWindow == window)
  197. {
  198. carla_debug("carlaWindowMap(%p, %lu, %i) - asked to show window, ignoring it",
  199. display, window, fallbackFnType);
  200. return 0;
  201. }
  202. // we already have a mapped window, with carla visible button on, should be a dialog of sorts..
  203. if (gCurrentWindowMapped && gCurrentWindowVisible)
  204. {
  205. XSetTransientForHint(display, window, gCurrentlyMappedWindow);
  206. break;
  207. }
  208. // ignore empty windows created after the main one
  209. if (numItems == 0)
  210. {
  211. carla_debug("carlaWindowMap(%p, %lu, %i) - ignoring empty window", display, window, fallbackFnType);
  212. break;
  213. }
  214. carla_stdout("NOTICE: XMapWindow now showing previous window");
  215. switch (gCurrentWindowType)
  216. {
  217. case WindowMapNone:
  218. break;
  219. case WindowMapNormal:
  220. real_XMapWindow(gCurrentlyMappedDisplay, gCurrentlyMappedWindow);
  221. break;
  222. case WindowMapRaised:
  223. real_XMapRaised(gCurrentlyMappedDisplay, gCurrentlyMappedWindow);
  224. break;
  225. case WindowMapSubwindows:
  226. real_XMapSubwindows(gCurrentlyMappedDisplay, gCurrentlyMappedWindow);
  227. break;
  228. }
  229. }
  230. gCurrentlyMappedDisplay = display;
  231. gCurrentlyMappedWindow = window;
  232. gCurrentWindowMapped = true;
  233. gCurrentWindowType = fallbackFnType;
  234. if (slo.winId > 0)
  235. XSetTransientForHint(display, window, static_cast<Window>(slo.winId));
  236. if (gCurrentWindowVisible)
  237. {
  238. carla_stdout("JACK application window found, showing it now");
  239. break;
  240. }
  241. gCurrentWindowMapped = false;
  242. carla_stdout("JACK application window found and captured");
  243. if (gInterposedSessionManager == LIBJACK_SESSION_MANAGER_NSM && gSupportsOptionalGui)
  244. break;
  245. return 0;
  246. }
  247. carla_debug("carlaWindowMap(%p, %lu, %i) - not captured", display, window, fallbackFnType);
  248. switch (fallbackFnType)
  249. {
  250. case WindowMapNormal:
  251. return real_XMapWindow(display, window);
  252. case WindowMapRaised:
  253. return real_XMapRaised(display, window);
  254. case WindowMapSubwindows:
  255. return real_XMapSubwindows(display, window);
  256. default:
  257. return 0;
  258. }
  259. }
  260. static int carlaWindowUnmap(Display* const display, const Window window, const WindowUnmappingType fallbackFnType)
  261. {
  262. if (gCurrentlyMappedWindow == window)
  263. {
  264. carla_stdout("NOTICE: now hiding previous window");
  265. gCurrentlyMappedDisplay = nullptr;
  266. gCurrentlyMappedWindow = 0;
  267. gCurrentWindowType = WindowMapNone;
  268. gCurrentWindowMapped = false;
  269. gCurrentWindowVisible = false;
  270. if (gInterposedCallback != nullptr)
  271. gInterposedCallback(LIBJACK_INTERPOSER_CALLBACK_UI_HIDE, nullptr);
  272. }
  273. else
  274. {
  275. carla_debug("carlaWindowUnmap(%p, %lu, %i) - not captured", display, window, fallbackFnType);
  276. }
  277. switch (fallbackFnType)
  278. {
  279. case WindowUnmapNormal:
  280. return real_XUnmapWindow(display, window);
  281. case WindowUnmapDestroy:
  282. return real_XDestroyWindow(display, window);
  283. default:
  284. return 0;
  285. }
  286. }
  287. // --------------------------------------------------------------------------------------------------------------------
  288. // Our custom X11 functions
  289. CARLA_PLUGIN_EXPORT
  290. int XMapWindow(Display* display, Window window)
  291. {
  292. carla_debug("XMapWindow(%p, %lu)", display, window);
  293. return carlaWindowMap(display, window, WindowMapNormal);
  294. }
  295. CARLA_PLUGIN_EXPORT
  296. int XMapRaised(Display* display, Window window)
  297. {
  298. carla_debug("XMapRaised(%p, %lu)", display, window);
  299. return carlaWindowMap(display, window, WindowMapRaised);
  300. }
  301. CARLA_PLUGIN_EXPORT
  302. int XMapSubwindows(Display* display, Window window)
  303. {
  304. carla_debug("XMapSubwindows(%p, %lu)", display, window);
  305. return carlaWindowMap(display, window, WindowMapSubwindows);
  306. }
  307. CARLA_PLUGIN_EXPORT
  308. int XUnmapWindow(Display* display, Window window)
  309. {
  310. carla_debug("XUnmapWindow(%p, %lu)", display, window);
  311. return carlaWindowUnmap(display, window, WindowUnmapNormal);
  312. }
  313. CARLA_PLUGIN_EXPORT
  314. int XDestroyWindow(Display* display, Window window)
  315. {
  316. carla_debug("XDestroyWindow(%p, %lu)", display, window);
  317. return carlaWindowUnmap(display, window, WindowUnmapDestroy);
  318. }
  319. CARLA_PLUGIN_EXPORT
  320. int XNextEvent(Display* display, XEvent* event)
  321. {
  322. const int ret = real_XNextEvent(display, event);
  323. if ((gInterposedHints & LIBJACK_FLAG_CONTROL_WINDOW) == 0x0)
  324. return ret;
  325. if (gInterposedSessionManager == LIBJACK_SESSION_MANAGER_NSM && gSupportsOptionalGui)
  326. return ret;
  327. if (ret != 0)
  328. return ret;
  329. if (gCurrentlyMappedWindow == 0)
  330. return ret;
  331. if (event->type != ClientMessage)
  332. return ret;
  333. if (event->xclient.window != gCurrentlyMappedWindow)
  334. return ret;
  335. char* const type = XGetAtomName(display, event->xclient.message_type);
  336. CARLA_SAFE_ASSERT_RETURN(type != nullptr, 0);
  337. if (std::strcmp(type, "WM_PROTOCOLS") != 0)
  338. return ret;
  339. if ((Atom)event->xclient.data.l[0] != XInternAtom(display, "WM_DELETE_WINDOW", False))
  340. return ret;
  341. gCurrentWindowVisible = false;
  342. gCurrentWindowMapped = false;
  343. if (gInterposedCallback != nullptr)
  344. gInterposedCallback(LIBJACK_INTERPOSER_CALLBACK_UI_HIDE, nullptr);
  345. event->type = 0;
  346. carla_stdout("XNextEvent close event caught, hiding UI instead");
  347. return real_XUnmapWindow(display, gCurrentlyMappedWindow);
  348. }
  349. #endif // HAVE_X11
  350. // --------------------------------------------------------------------------------------------------------------------
  351. // Full control helper
  352. CARLA_PLUGIN_EXPORT
  353. int jack_carla_interposed_action(uint action, uint value, void* ptr)
  354. {
  355. carla_debug("jack_carla_interposed_action(%i, %i, %p)", action, value, ptr);
  356. switch (action)
  357. {
  358. case LIBJACK_INTERPOSER_ACTION_SET_HINTS_AND_CALLBACK:
  359. gInterposedHints = value;
  360. gInterposedCallback = (CarlaInterposedCallback)ptr;
  361. return 1;
  362. case LIBJACK_INTERPOSER_ACTION_SET_SESSION_MANAGER:
  363. gInterposedSessionManager = value;
  364. return 1;
  365. case LIBJACK_INTERPOSER_ACTION_SHOW_HIDE_GUI:
  366. #ifdef HAVE_X11
  367. gSupportsOptionalGui = false;
  368. #endif
  369. // show gui
  370. if (value != 0)
  371. {
  372. #ifdef HAVE_X11
  373. gCurrentWindowVisible = true;
  374. if (gCurrentlyMappedDisplay == nullptr || gCurrentlyMappedWindow == 0)
  375. #endif
  376. {
  377. carla_stdout("NOTICE: Interposer show-gui request ignored");
  378. return 0;
  379. }
  380. #ifdef HAVE_X11
  381. gCurrentWindowMapped = true;
  382. switch (gCurrentWindowType)
  383. {
  384. case WindowMapNormal:
  385. return real_XMapWindow(gCurrentlyMappedDisplay, gCurrentlyMappedWindow);
  386. case WindowMapRaised:
  387. return real_XMapRaised(gCurrentlyMappedDisplay, gCurrentlyMappedWindow);
  388. case WindowMapSubwindows:
  389. return real_XMapSubwindows(gCurrentlyMappedDisplay, gCurrentlyMappedWindow);
  390. default:
  391. return 0;
  392. }
  393. #endif
  394. }
  395. // hide gui
  396. else
  397. {
  398. #ifdef HAVE_X11
  399. gCurrentWindowVisible = false;
  400. if (gCurrentlyMappedDisplay == nullptr || gCurrentlyMappedWindow == 0)
  401. #endif
  402. {
  403. carla_stdout("NOTICE: Interposer hide-gui request ignored");
  404. return 0;
  405. }
  406. #ifdef HAVE_X11
  407. gCurrentWindowMapped = false;
  408. return real_XUnmapWindow(gCurrentlyMappedDisplay, gCurrentlyMappedWindow);
  409. #endif
  410. }
  411. break;
  412. case LIBJACK_INTERPOSER_ACTION_CLOSE_EVERYTHING:
  413. gCurrentWindowType = WindowMapNone;
  414. gCurrentWindowMapped = false;
  415. gCurrentWindowVisible = false;
  416. #ifdef HAVE_X11
  417. gCurrentlyMappedDisplay = nullptr;
  418. gCurrentlyMappedWindow = 0;
  419. #endif
  420. return 0;
  421. }
  422. return -1;
  423. }
  424. // --------------------------------------------------------------------------------------------------------------------