1 /*
2 Copyright 2011-2020 David Robillard <d@drobilla.net>
3
4 Permission to use, copy, modify, and/or distribute this software for any
5 purpose with or without fee is hereby granted, provided that the above
6 copyright notice and this permission notice appear in all copies.
7
8 THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 */
16
17 #include "suil_internal.h"
18
19 #include "lv2/core/lv2.h"
20 #include "lv2/options/options.h"
21 #include "lv2/ui/ui.h"
22 #include "lv2/urid/urid.h"
23 #include "suil/suil.h"
24
25 #include <X11/X.h>
26 #include <X11/Xatom.h>
27 #include <X11/Xlib.h>
28 #include <X11/Xutil.h>
29 #include <gdk/gdk.h>
30 #include <gdk/gdkx.h>
31 #include <glib-object.h>
32 #include <glib.h>
33 #include <gobject/gclosure.h>
34 #include <gtk/gtk.h>
35
36 #include <stdbool.h>
37 #include <stdint.h>
38 #include <stdlib.h>
39 #include <string.h>
40
41 typedef struct {
42 bool is_set;
43 int width;
44 int height;
45 } SuilX11SizeHints;
46
47 typedef struct {
48 GtkSocket socket;
49 GtkPlug* plug;
50 SuilWrapper* wrapper;
51 SuilInstance* instance;
52 const LV2UI_Idle_Interface* idle_iface;
53 guint idle_id;
54 guint idle_ms;
55 SuilX11SizeHints max_size;
56 SuilX11SizeHints custom_size;
57 SuilX11SizeHints base_size;
58 SuilX11SizeHints min_size;
59 bool query_wm;
G_DEFINE_TYPE(SuilWinWrapper,suil_win_wrapper,GTK_TYPE_DRAWING_AREA)60 } SuilX11Wrapper;
61
62 typedef struct {
63 GtkSocketClass parent_class;
64 } SuilX11WrapperClass;
65
66 GType
67 suil_x11_wrapper_get_type(void); // Accessor for SUIL_TYPE_X11_WRAPPER
68
69 #define SUIL_TYPE_X11_WRAPPER (suil_x11_wrapper_get_type())
70 #define SUIL_X11_WRAPPER(obj) \
71 (G_TYPE_CHECK_INSTANCE_CAST((obj), SUIL_TYPE_X11_WRAPPER, SuilX11Wrapper))
72
73 G_DEFINE_TYPE(SuilX11Wrapper, suil_x11_wrapper, GTK_TYPE_SOCKET)
suil_win_size_allocate(GtkWidget * widget,GtkAllocation * allocation)74
75 /**
76 Check if 'swallowed' subwindow is known to the X server.
77
78 Gdk/GTK can mark the window as realized, mapped and visible even though
79 there is no window-ID on the X server for it yet. Then,
80 suil_x11_on_size_allocate() will cause a "BadWinow" X error.
81 */
82 static bool
83 x_window_is_valid(SuilX11Wrapper* socket)
84 {
85 GdkWindow* window = gtk_widget_get_window(GTK_WIDGET(socket->plug));
86 Window root = 0;
87 Window parent = 0;
88 Window* children = NULL;
89 unsigned childcount = 0;
90
91 XQueryTree(GDK_WINDOW_XDISPLAY(window),
92 GDK_WINDOW_XID(window),
93 &root,
94 &parent,
95 &children,
96 &childcount);
97
98 for (unsigned i = 0; i < childcount; ++i) {
99 if (children[i] == (Window)socket->instance->ui_widget) {
100 XFree(children);
101 return true;
102 }
103 }
104
105 if (children) {
106 XFree(children);
107 }
108
109 return false;
110 }
111
112 static Window
113 get_parent_window(Display* display, Window child)
suil_win_wrapper_init(SuilWinWrapper * self)114 {
115 Window root = 0;
116 Window parent = 0;
117 Window* children = NULL;
118 unsigned count = 0;
119
120 if (child) {
121 if (XQueryTree(display, child, &root, &parent, &children, &count)) {
122 if (children) {
123 XFree(children);
124 }
125 }
126 }
127
128 return (parent == root) ? 0 : parent;
129 }
130
wrapper_resize(LV2UI_Feature_Handle handle,int width,int height)131 static gboolean
132 on_plug_removed(GtkSocket* sock, gpointer data)
133 {
134 (void)data;
135
136 SuilX11Wrapper* const self = SUIL_X11_WRAPPER(sock);
137
138 if (self->idle_id) {
139 g_source_remove(self->idle_id);
140 self->idle_id = 0;
141 }
142
143 if (self->instance->handle) {
144 self->instance->descriptor->cleanup(self->instance->handle);
145 self->instance->handle = NULL;
146 }
147
148 self->plug = NULL;
149 return TRUE;
150 }
151
152 static void
153 suil_x11_wrapper_finalize(GObject* gobject)
154 {
155 SuilX11Wrapper* const self = SUIL_X11_WRAPPER(gobject);
156
157 self->wrapper->impl = NULL;
158
159 G_OBJECT_CLASS(suil_x11_wrapper_parent_class)->finalize(gobject);
160 }
event_filter(GdkXEvent * xevent,GdkEvent * event,gpointer data)161
162 static void
163 suil_x11_wrapper_realize(GtkWidget* w)
164 {
165 SuilX11Wrapper* const wrap = SUIL_X11_WRAPPER(w);
166 GtkSocket* const socket = GTK_SOCKET(w);
167
168 if (GTK_WIDGET_CLASS(suil_x11_wrapper_parent_class)->realize) {
169 GTK_WIDGET_CLASS(suil_x11_wrapper_parent_class)->realize(w);
170 }
171
172 gtk_socket_add_id(socket, gtk_plug_get_id(wrap->plug));
173
174 gtk_widget_set_sensitive(GTK_WIDGET(wrap->plug), TRUE);
175 gtk_widget_set_can_focus(GTK_WIDGET(wrap->plug), TRUE);
176 gtk_widget_grab_focus(GTK_WIDGET(wrap->plug));
177
178 // Setup drag/drop proxy from parent/grandparent window
179 GdkWindow* gwindow = gtk_widget_get_window(GTK_WIDGET(wrap->plug));
180 Window xwindow = GDK_WINDOW_XID(gwindow);
181 Atom xdnd_proxy_atom = gdk_x11_get_xatom_by_name("XdndProxy");
182 Window plugin = (Window)wrap->instance->ui_widget;
183
184 while (xwindow) {
185 XChangeProperty(GDK_WINDOW_XDISPLAY(gwindow),
186 xwindow,
187 xdnd_proxy_atom,
188 XA_WINDOW,
189 32,
190 PropModeReplace,
191 (unsigned char*)&plugin,
192 1);
193
194 xwindow = get_parent_window(GDK_WINDOW_XDISPLAY(gwindow), xwindow);
195 }
196 }
197
suil_wrapper_new(SuilHost * host,const char * host_type_uri,const char * ui_type_uri,LV2_Feature *** features,unsigned n_features)198 static void
199 suil_x11_wrapper_show(GtkWidget* w)
200 {
201 SuilX11Wrapper* const wrap = SUIL_X11_WRAPPER(w);
202
203 if (GTK_WIDGET_CLASS(suil_x11_wrapper_parent_class)->show) {
204 GTK_WIDGET_CLASS(suil_x11_wrapper_parent_class)->show(w);
205 }
206
207 gtk_widget_show(GTK_WIDGET(wrap->plug));
208 }
209
210 static gboolean
211 forward_key_event(SuilX11Wrapper* socket, GdkEvent* gdk_event)
212 {
213 GdkWindow* window = gtk_widget_get_window(GTK_WIDGET(socket->plug));
214 GdkScreen* screen = gdk_visual_get_screen(gdk_window_get_visual(window));
215
216 Window target_window = 0;
217 if (gdk_event->any.window == window) {
218 // Event sent up to the plug window, forward it up to the parent
219 GtkWidget* widget = GTK_WIDGET(socket->instance->host_widget);
220 GdkWindow* parent = gtk_widget_get_parent_window(widget);
221 if (parent) {
222 target_window = GDK_WINDOW_XID(parent);
223 } else {
224 return FALSE; // Wrapper is a top-level window, do nothing
225 }
226 } else {
227 // Event sent anywhere else, send to the plugin
228 target_window = (Window)socket->instance->ui_widget;
229 }
230
231 XKeyEvent xev;
232 memset(&xev, 0, sizeof(xev));
233 xev.type = (gdk_event->type == GDK_KEY_PRESS) ? KeyPress : KeyRelease;
234 xev.root = GDK_WINDOW_XID(gdk_screen_get_root_window(screen));
235 xev.window = target_window;
236 xev.subwindow = None;
237 xev.time = gdk_event->key.time;
238 xev.state = gdk_event->key.state;
239 xev.keycode = gdk_event->key.hardware_keycode;
240
241 XSendEvent(GDK_WINDOW_XDISPLAY(window),
242 target_window,
243 False,
244 NoEventMask,
245 (XEvent*)&xev);
246
247 return (gdk_event->any.window != window);
248 }
249
250 static gboolean
251 idle_size_request(gpointer user_data)
252 {
253 GtkWidget* w = GTK_WIDGET(user_data);
254 gtk_widget_queue_resize(w);
255 return FALSE;
256 }
257
258 /// Read XSizeHints and store the values for later use
259 static void
260 query_wm_hints(SuilX11Wrapper* wrap)
261 {
262 GdkWindow* window = gtk_widget_get_window(GTK_WIDGET(wrap->plug));
263 XSizeHints hints = {0};
264 long supplied = 0;
265
266 XGetWMNormalHints(GDK_WINDOW_XDISPLAY(window),
267 (Window)wrap->instance->ui_widget,
268 &hints,
269 &supplied);
270
271 if (hints.flags & PMaxSize) {
272 wrap->max_size.width = hints.max_width;
273 wrap->max_size.height = hints.max_height;
274 wrap->max_size.is_set = true;
275 }
276
277 if (hints.flags & PBaseSize) {
278 wrap->base_size.width = hints.base_width;
279 wrap->base_size.height = hints.base_height;
280 wrap->base_size.is_set = true;
281 }
282
283 if (hints.flags & PMinSize) {
284 wrap->min_size.width = hints.min_width;
285 wrap->min_size.height = hints.min_height;
286 wrap->min_size.is_set = true;
287 }
288
289 wrap->query_wm = false;
290 }
291
292 static void
293 forward_size_request(SuilX11Wrapper* socket, GtkAllocation* allocation)
294 {
295 GdkWindow* window = gtk_widget_get_window(GTK_WIDGET(socket->plug));
296 if (x_window_is_valid(socket)) {
297 // Calculate allocation size constrained to X11 limits for widget
298 int width = allocation->width;
299 int height = allocation->height;
300
301 if (socket->query_wm) {
302 query_wm_hints(socket);
303 }
304
305 if (socket->max_size.is_set) {
306 width = MIN(width, socket->max_size.width);
307 height = MIN(height, socket->max_size.height);
308 }
309
310 if (socket->min_size.is_set) {
311 width = MAX(width, socket->min_size.width);
312 height = MAX(height, socket->min_size.height);
313 }
314
315 // Resize widget window
316 XResizeWindow(GDK_WINDOW_XDISPLAY(window),
317 (Window)socket->instance->ui_widget,
318 (unsigned)width,
319 (unsigned)height);
320
321 // Get actual widget geometry
322 Window root = 0;
323 int wx = 0;
324 int wy = 0;
325 unsigned int ww = 0;
326 unsigned int wh = 0;
327 unsigned int ignored = 0;
328 XGetGeometry(GDK_WINDOW_XDISPLAY(window),
329 (Window)socket->instance->ui_widget,
330 &root,
331 &wx,
332 &wy,
333 &ww,
334 &wh,
335 &ignored,
336 &ignored);
337
338 // Center widget in allocation
339 wx = (allocation->width - (int)ww) / 2;
340 wy = (allocation->height - (int)wh) / 2;
341 XMoveWindow(
342 GDK_WINDOW_XDISPLAY(window), (Window)socket->instance->ui_widget, wx, wy);
343 } else {
344 /* Child has not been realized, so unable to resize now.
345 Queue an idle resize. */
346 g_idle_add(idle_size_request, socket->plug);
347 }
348 }
349
350 static gboolean
351 suil_x11_wrapper_key_event(GtkWidget* widget, GdkEventKey* event)
352 {
353 SuilX11Wrapper* const self = SUIL_X11_WRAPPER(widget);
354
355 if (self->plug) {
356 return forward_key_event(self, (GdkEvent*)event);
357 }
358
359 return FALSE;
360 }
361
362 static void
363 suil_x11_on_size_request(GtkWidget* widget, GtkRequisition* requisition)
364 {
365 SuilX11Wrapper* const self = SUIL_X11_WRAPPER(widget);
366
367 if (self->custom_size.is_set) {
368 requisition->width = self->custom_size.width;
369 requisition->height = self->custom_size.height;
370 } else if (self->base_size.is_set) {
371 requisition->width = self->base_size.width;
372 requisition->height = self->base_size.height;
373 } else if (self->min_size.is_set) {
374 requisition->width = self->min_size.width;
375 requisition->height = self->min_size.height;
376 }
377 }
378
379 static void
380 suil_x11_on_size_allocate(GtkWidget* widget, GtkAllocation* a)
381 {
382 SuilX11Wrapper* const self = SUIL_X11_WRAPPER(widget);
383
384 if (self->plug && GTK_WIDGET_REALIZED(widget) && GTK_WIDGET_MAPPED(widget) &&
385 GTK_WIDGET_VISIBLE(widget)) {
386 forward_size_request(self, a);
387 }
388 }
389
390 static void
391 suil_x11_on_map_event(GtkWidget* widget, GdkEvent* event)
392 {
393 (void)event;
394
395 SuilX11Wrapper* const self = SUIL_X11_WRAPPER(widget);
396
397 /* Reset the size request to the minimum sizes. This is called after the
398 initial size negotiation, where Gtk called suil_x11_on_size_request() to
399 get the size request, which might be bigger than the minimum size.
400 However, the Gtk2 size model has no proper way to handle minimum and
401 default sizes, so hack around this by setting the size request
402 properties (which really mean minimum size) back to the minimum after
403 the widget is mapped. This makes it possible for the initial mapping to
404 use the default size, but still allow the user to resize the widget
405 smaller, down to the minimum size. */
406
407 if ((self->custom_size.is_set || self->base_size.is_set) &&
408 self->min_size.is_set) {
409 g_object_set(G_OBJECT(GTK_WIDGET(self)),
410 "width-request",
411 self->min_size.width,
412 "height-request",
413 self->min_size.height,
414 NULL);
415 }
416 }
417
418 static void
419 suil_x11_wrapper_class_init(SuilX11WrapperClass* klass)
420 {
421 GObjectClass* const gobject_class = G_OBJECT_CLASS(klass);
422 GtkWidgetClass* const widget_class = GTK_WIDGET_CLASS(klass);
423
424 gobject_class->finalize = suil_x11_wrapper_finalize;
425 widget_class->realize = suil_x11_wrapper_realize;
426 widget_class->show = suil_x11_wrapper_show;
427 widget_class->key_press_event = suil_x11_wrapper_key_event;
428 widget_class->key_release_event = suil_x11_wrapper_key_event;
429 }
430
431 static void
432 suil_x11_wrapper_init(SuilX11Wrapper* self)
433 {
434 self->plug = GTK_PLUG(gtk_plug_new(0));
435 self->wrapper = NULL;
436 self->instance = NULL;
437 self->idle_iface = NULL;
438 self->idle_ms = 1000 / 30; // 30 Hz default
439 self->max_size = (SuilX11SizeHints){false, 0, 0};
440 self->custom_size = (SuilX11SizeHints){false, 0, 0};
441 self->base_size = (SuilX11SizeHints){false, 0, 0};
442 self->min_size = (SuilX11SizeHints){false, 0, 0};
443 self->query_wm = true;
444 }
445
446 static int
447 wrapper_resize(LV2UI_Feature_Handle handle, int width, int height)
448 {
449 SuilX11Wrapper* const wrap = SUIL_X11_WRAPPER(handle);
450
451 wrap->custom_size.width = width;
452 wrap->custom_size.height = height;
453 wrap->custom_size.is_set = width > 0 && height > 0;
454
455 // Assume the plugin has also updated min/max size constraints
456 wrap->query_wm = true;
457
458 gtk_widget_queue_resize(GTK_WIDGET(handle));
459 return 0;
460 }
461
462 static gboolean
463 suil_x11_wrapper_idle(void* data)
464 {
465 SuilX11Wrapper* const wrap = SUIL_X11_WRAPPER(data);
466
467 wrap->idle_iface->idle(wrap->instance->handle);
468
469 return TRUE; // Continue calling
470 }
471
472 static int
473 wrapper_wrap(SuilWrapper* wrapper, SuilInstance* instance)
474 {
475 SuilX11Wrapper* const wrap = SUIL_X11_WRAPPER(wrapper->impl);
476
477 instance->host_widget = GTK_WIDGET(wrap);
478 wrap->wrapper = wrapper;
479 wrap->instance = instance;
480
481 GdkWindow* window = gtk_widget_get_window(GTK_WIDGET(wrap->plug));
482 GdkDisplay* display = gdk_window_get_display(window);
483 Display* xdisplay = GDK_WINDOW_XDISPLAY(window);
484 Window xwindow = (Window)instance->ui_widget;
485
486 gdk_display_sync(display);
487 if (x_window_is_valid(wrap)) {
488 XWindowAttributes attrs;
489 XGetWindowAttributes(xdisplay, xwindow, &attrs);
490
491 query_wm_hints(wrap);
492
493 if (!wrap->base_size.is_set) {
494 // Fall back to using initial size as base size
495 wrap->base_size.is_set = true;
496 wrap->base_size.width = attrs.width;
497 wrap->base_size.height = attrs.height;
498 }
499 }
500
501 const LV2UI_Idle_Interface* idle_iface = NULL;
502 if (instance->descriptor->extension_data) {
503 idle_iface =
504 (const LV2UI_Idle_Interface*)instance->descriptor->extension_data(
505 LV2_UI__idleInterface);
506 }
507
508 if (idle_iface) {
509 wrap->idle_iface = idle_iface;
510 wrap->idle_id = g_timeout_add(wrap->idle_ms, suil_x11_wrapper_idle, wrap);
511 }
512
513 g_signal_connect(
514 G_OBJECT(wrap), "plug-removed", G_CALLBACK(on_plug_removed), NULL);
515
516 g_signal_connect(
517 G_OBJECT(wrap), "size-request", G_CALLBACK(suil_x11_on_size_request), NULL);
518
519 g_signal_connect(G_OBJECT(wrap),
520 "size-allocate",
521 G_CALLBACK(suil_x11_on_size_allocate),
522 NULL);
523
524 g_signal_connect(
525 G_OBJECT(wrap), "map-event", G_CALLBACK(suil_x11_on_map_event), NULL);
526
527 return 0;
528 }
529
530 static void
531 wrapper_free(SuilWrapper* wrapper)
532 {
533 if (wrapper->impl) {
534 SuilX11Wrapper* const wrap = SUIL_X11_WRAPPER(wrapper->impl);
535 gtk_object_destroy(GTK_OBJECT(wrap));
536 }
537 }
538
539 SUIL_LIB_EXPORT
540 SuilWrapper*
541 suil_wrapper_new(SuilHost* host,
542 const char* host_type_uri,
543 const char* ui_type_uri,
544 LV2_Feature*** features,
545 unsigned n_features)
546 {
547 (void)host;
548 (void)host_type_uri;
549 (void)ui_type_uri;
550
551 SuilWrapper* wrapper = (SuilWrapper*)calloc(1, sizeof(SuilWrapper));
552 wrapper->wrap = wrapper_wrap;
553 wrapper->free = wrapper_free;
554
555 SuilX11Wrapper* const wrap =
556 SUIL_X11_WRAPPER(g_object_new(SUIL_TYPE_X11_WRAPPER, NULL));
557
558 wrapper->impl = wrap;
559 wrapper->resize.handle = wrap;
560 wrapper->resize.ui_resize = wrapper_resize;
561
562 gtk_widget_set_sensitive(GTK_WIDGET(wrap), TRUE);
563 gtk_widget_set_can_focus(GTK_WIDGET(wrap), TRUE);
564
565 const intptr_t parent_id = (intptr_t)gtk_plug_get_id(wrap->plug);
566 suil_add_feature(features, &n_features, LV2_UI__parent, (void*)parent_id);
567 suil_add_feature(features, &n_features, LV2_UI__resize, &wrapper->resize);
568 suil_add_feature(features, &n_features, LV2_UI__idleInterface, NULL);
569
570 // Scan for URID map and options
571 LV2_URID_Map* map = NULL;
572 LV2_Options_Option* options = NULL;
573 for (LV2_Feature** f = *features; *f && (!map || !options); ++f) {
574 if (!strcmp((*f)->URI, LV2_OPTIONS__options)) {
575 options = (LV2_Options_Option*)(*f)->data;
576 } else if (!strcmp((*f)->URI, LV2_URID__map)) {
577 map = (LV2_URID_Map*)(*f)->data;
578 }
579 }
580
581 if (map && options) {
582 // Set UI update rate if given
583 LV2_URID ui_updateRate = map->map(map->handle, LV2_UI__updateRate);
584 for (LV2_Options_Option* o = options; o->key; ++o) {
585 if (o->key == ui_updateRate) {
586 wrap->idle_ms = (guint)(1000.0f / *(const float*)o->value);
587 break;
588 }
589 }
590 }
591
592 return wrapper;
593 }
594