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