1/*
2  Copyright 2011-2017 David Robillard <d@drobilla.net>
3  Copyright 2014 Robin Gareus <robin@gareus.org>
4
5  Permission to use, copy, modify, and/or distribute this software for any
6  purpose with or without fee is hereby granted, provided that the above
7  copyright notice and this permission notice appear in all copies.
8
9  THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10  WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11  MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12  ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13  WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14  ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15  OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16*/
17
18#include "suil_internal.h"
19
20#include "lv2/options/options.h"
21#include "lv2/urid/urid.h"
22
23#include <gdk/gdkquartz.h>
24#include <gtk/gtk.h>
25
26#include <string.h>
27
28#ifndef MAC_OS_X_VERSION_10_12
29#  define MAC_OS_X_VERSION_10_12 101200
30#endif
31
32#if MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_12
33#  define NSEventTypeFlagsChanged NSFlagsChanged
34#  define NSEventTypeLeftMouseDown NSLeftMouseDown
35#  define NSEventTypeLeftMouseDragged NSLeftMouseDragged
36#  define NSEventTypeLeftMouseUp NSLeftMouseUp
37#  define NSEventTypeMouseEntered NSMouseEntered
38#  define NSEventTypeMouseExited NSMouseExited
39#  define NSEventTypeMouseMoved NSMouseMoved
40#  define NSEventTypeRightMouseDown NSRightMouseDown
41#  define NSEventTypeRightMouseUp NSRightMouseUp
42#  define NSEventTypeScrollWheel NSScrollWheel
43#endif
44
45extern "C" {
46
47#define SUIL_TYPE_COCOA_WRAPPER (suil_cocoa_wrapper_get_type())
48#define SUIL_COCOA_WRAPPER(obj) \
49  (G_TYPE_CHECK_INSTANCE_CAST((obj), SUIL_TYPE_COCOA_WRAPPER, SuilCocoaWrapper))
50
51typedef struct _SuilCocoaWrapper      SuilCocoaWrapper;
52typedef struct _SuilCocoaWrapperClass SuilCocoaWrapperClass;
53
54struct _SuilCocoaWrapper {
55  GtkWidget     widget;
56  SuilWrapper*  wrapper;
57  SuilInstance* instance;
58
59  GdkWindow* flt_win;
60  bool       custom_size;
61  bool       mapped;
62  int        req_width;
63  int        req_height;
64  int        alo_width;
65  int        alo_height;
66
67  const LV2UI_Idle_Interface* idle_iface;
68  guint                       idle_id;
69  guint                       idle_ms;
70};
71
72struct _SuilCocoaWrapperClass {
73  GtkWidgetClass parent_class;
74};
75
76GType
77suil_cocoa_wrapper_get_type(void); // Accessor for SUIL_TYPE_COCOA_WRAPPER
78
79G_DEFINE_TYPE(SuilCocoaWrapper, suil_cocoa_wrapper, GTK_TYPE_WIDGET)
80
81static void
82suil_cocoa_wrapper_finalize(GObject* gobject)
83{
84  SuilCocoaWrapper* const self = SUIL_COCOA_WRAPPER(gobject);
85
86  self->wrapper->impl = NULL;
87
88  G_OBJECT_CLASS(suil_cocoa_wrapper_parent_class)->finalize(gobject);
89}
90
91static void
92suil_cocoa_realize(GtkWidget* widget)
93{
94  SuilCocoaWrapper* const self = SUIL_COCOA_WRAPPER(widget);
95  g_return_if_fail(self != NULL);
96
97  GTK_WIDGET_SET_FLAGS(widget, GTK_REALIZED);
98
99  GdkWindowAttr attrs;
100  attrs.x           = widget->allocation.x;
101  attrs.y           = widget->allocation.y;
102  attrs.width       = widget->allocation.width;
103  attrs.height      = widget->allocation.height;
104  attrs.wclass      = GDK_INPUT_OUTPUT;
105  attrs.window_type = GDK_WINDOW_CHILD;
106  attrs.visual      = gtk_widget_get_visual(widget);
107  attrs.colormap    = gtk_widget_get_colormap(widget);
108  attrs.event_mask  = gtk_widget_get_events(widget) | GDK_EXPOSURE_MASK |
109                     GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
110                     GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK;
111
112  widget->window =
113    gdk_window_new(widget->parent->window,
114                   &attrs,
115                   GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL | GDK_WA_COLORMAP);
116
117  widget->style = gtk_style_attach(widget->style, widget->window);
118
119  gdk_window_set_user_data(widget->window, widget);
120  gtk_style_set_background(widget->style, widget->window, GTK_STATE_ACTIVE);
121  gtk_widget_queue_resize(widget);
122}
123
124static void
125suil_cocoa_size_request(GtkWidget* widget, GtkRequisition* requisition)
126{
127  SuilCocoaWrapper* const self = SUIL_COCOA_WRAPPER(widget);
128  if (self->custom_size) {
129    requisition->width  = self->req_width;
130    requisition->height = self->req_height;
131  } else {
132    NSView* view        = (NSView*)self->instance->ui_widget;
133    NSRect  frame       = [view frame];
134    requisition->width  = CGRectGetWidth(NSRectToCGRect(frame));
135    requisition->height = CGRectGetHeight(NSRectToCGRect(frame));
136  }
137}
138
139static void
140suil_cocoa_size_allocate(GtkWidget* widget, GtkAllocation* allocation)
141{
142  SuilCocoaWrapper* const self = SUIL_COCOA_WRAPPER(widget);
143  self->alo_width              = allocation->width;
144  self->alo_height             = allocation->height;
145
146  if (!self->mapped) {
147    return;
148  }
149
150  gint xx, yy;
151  gtk_widget_translate_coordinates(
152    gtk_widget_get_parent(widget), widget, 0, 0, &xx, &yy);
153
154  NSView* view = (NSView*)self->instance->ui_widget;
155  [view setFrame:NSMakeRect(xx, yy, self->alo_width, self->alo_height)];
156}
157
158static void
159suil_cocoa_map(GtkWidget* widget)
160{
161  SuilCocoaWrapper* const self = SUIL_COCOA_WRAPPER(widget);
162  self->mapped                 = true;
163
164  if (self->alo_width == 0 || self->alo_height == 0) {
165    return;
166  }
167
168  gint xx, yy;
169  gtk_widget_translate_coordinates(
170    gtk_widget_get_parent(widget), widget, 0, 0, &xx, &yy);
171
172  NSView* view = (NSView*)self->instance->ui_widget;
173  [view setHidden:NO];
174  [view setFrame:NSMakeRect(xx, yy, self->alo_width, self->alo_height)];
175}
176
177static void
178suil_cocoa_unmap(GtkWidget* widget)
179{
180  SuilCocoaWrapper* const self = SUIL_COCOA_WRAPPER(widget);
181  NSView*                 view = (NSView*)self->instance->ui_widget;
182
183  self->mapped = false;
184  [view setHidden:YES];
185}
186
187static gboolean
188suil_cocoa_key_press(GtkWidget* widget, GdkEventKey* event)
189{
190  SuilCocoaWrapper* const self = SUIL_COCOA_WRAPPER(widget);
191  if (!self->instance || !self->wrapper || !self->wrapper->impl) {
192    return FALSE;
193  }
194
195  NSEvent* nsevent = gdk_quartz_event_get_nsevent((GdkEvent*)event);
196  NSView*  view    = (NSView*)self->instance->ui_widget;
197  [view keyDown:nsevent];
198  return TRUE;
199}
200
201static gboolean
202suil_cocoa_key_release(GtkWidget* widget, GdkEventKey* event)
203{
204  SuilCocoaWrapper* const self = SUIL_COCOA_WRAPPER(widget);
205  if (!self->instance || !self->wrapper || !self->wrapper->impl) {
206    return FALSE;
207  }
208
209  NSEvent* nsevent = gdk_quartz_event_get_nsevent((GdkEvent*)event);
210  NSView*  view    = (NSView*)self->instance->ui_widget;
211  [view keyUp:nsevent];
212  return TRUE;
213}
214
215static gboolean
216suil_cocoa_expose(GtkWidget* widget, GdkEventExpose* event)
217{
218  SuilCocoaWrapper* const self = SUIL_COCOA_WRAPPER(widget);
219  NSView*                 view = (NSView*)self->instance->ui_widget;
220  [view drawRect:NSMakeRect(event->area.x,
221                            event->area.y,
222                            event->area.width,
223                            event->area.height)];
224  return TRUE;
225}
226
227static void
228suil_cocoa_wrapper_class_init(SuilCocoaWrapperClass* klass)
229{
230  GObjectClass* const   gobject_class = G_OBJECT_CLASS(klass);
231  GtkWidgetClass* const widget_class  = (GtkWidgetClass*)(klass);
232
233  gobject_class->finalize = suil_cocoa_wrapper_finalize;
234
235  widget_class->realize           = suil_cocoa_realize;
236  widget_class->expose_event      = suil_cocoa_expose;
237  widget_class->size_request      = suil_cocoa_size_request;
238  widget_class->size_allocate     = suil_cocoa_size_allocate;
239  widget_class->map               = suil_cocoa_map;
240  widget_class->unmap             = suil_cocoa_unmap;
241  widget_class->key_press_event   = suil_cocoa_key_press;
242  widget_class->key_release_event = suil_cocoa_key_release;
243}
244
245static void
246suil_cocoa_wrapper_init(SuilCocoaWrapper* self)
247{
248  self->wrapper     = NULL;
249  self->instance    = NULL;
250  self->flt_win     = NULL;
251  self->custom_size = false;
252  self->mapped      = false;
253  self->req_width   = 0;
254  self->req_height  = 0;
255  self->alo_width   = 0;
256  self->alo_height  = 0;
257  self->idle_iface  = NULL;
258  self->idle_ms     = 1000 / 30; // 30 Hz default
259}
260
261static int
262wrapper_resize(LV2UI_Feature_Handle handle, int width, int height)
263{
264  SuilCocoaWrapper* const wrap = SUIL_COCOA_WRAPPER(handle);
265
266  wrap->req_width   = width;
267  wrap->req_height  = height;
268  wrap->custom_size = true;
269
270  gtk_widget_queue_resize(GTK_WIDGET(handle));
271
272  return 0;
273}
274
275static gboolean
276suil_cocoa_wrapper_idle(void* data)
277{
278  SuilCocoaWrapper* const wrap = SUIL_COCOA_WRAPPER(data);
279  wrap->idle_iface->idle(wrap->instance->handle);
280  return TRUE; // Continue calling
281}
282
283static GdkFilterReturn
284event_filter(GdkXEvent* xevent, GdkEvent* event, gpointer data)
285{
286  SuilCocoaWrapper* wrap = (SuilCocoaWrapper*)data;
287  if (!wrap->instance || !wrap->wrapper || !wrap->wrapper->impl) {
288    return GDK_FILTER_CONTINUE;
289  }
290
291  NSEvent* nsevent = (NSEvent*)xevent;
292  NSView*  view    = (NSView*)wrap->instance->ui_widget;
293  if (view && nsevent) {
294    switch ([nsevent type]) {
295    case NSEventTypeFlagsChanged:
296      [view flagsChanged:nsevent];
297      return GDK_FILTER_REMOVE;
298    case NSEventTypeMouseEntered:
299      [view mouseEntered:nsevent];
300      return GDK_FILTER_REMOVE;
301    case NSEventTypeMouseExited:
302      [view mouseExited:nsevent];
303      return GDK_FILTER_REMOVE;
304
305    /* Explicitly pass though mouse events.  Needed for mouse-drags leaving
306       the window, and mouse-up after that. */
307    case NSEventTypeMouseMoved:
308      [view mouseMoved:nsevent];
309      break;
310    case NSEventTypeLeftMouseDragged:
311      [view mouseDragged:nsevent];
312      break;
313    case NSEventTypeLeftMouseDown:
314      [view mouseDown:nsevent];
315      break;
316    case NSEventTypeLeftMouseUp:
317      [view mouseUp:nsevent];
318      break;
319    case NSEventTypeRightMouseDown:
320      [view rightMouseDown:nsevent];
321      break;
322    case NSEventTypeRightMouseUp:
323      [view rightMouseUp:nsevent];
324      break;
325    case NSEventTypeScrollWheel:
326      [view scrollWheel:nsevent];
327      break;
328    default:
329      break;
330    }
331  }
332  return GDK_FILTER_CONTINUE;
333}
334
335static int
336wrapper_wrap(SuilWrapper* wrapper, SuilInstance* instance)
337{
338  SuilCocoaWrapper* const wrap = SUIL_COCOA_WRAPPER(wrapper->impl);
339
340  instance->host_widget = GTK_WIDGET(wrap);
341  wrap->wrapper         = wrapper;
342  wrap->instance        = instance;
343
344  const LV2UI_Idle_Interface* idle_iface = NULL;
345  if (instance->descriptor->extension_data) {
346    idle_iface =
347      (const LV2UI_Idle_Interface*)instance->descriptor->extension_data(
348        LV2_UI__idleInterface);
349  }
350
351  if (idle_iface) {
352    wrap->idle_iface = idle_iface;
353    wrap->idle_id = g_timeout_add(wrap->idle_ms, suil_cocoa_wrapper_idle, wrap);
354  }
355
356  return 0;
357}
358
359static void
360wrapper_free(SuilWrapper* wrapper)
361{
362  if (wrapper->impl) {
363    SuilCocoaWrapper* const wrap = SUIL_COCOA_WRAPPER(wrapper->impl);
364    if (wrap->idle_id) {
365      g_source_remove(wrap->idle_id);
366      wrap->idle_id = 0;
367    }
368
369    gdk_window_remove_filter(wrap->flt_win, event_filter, wrapper->impl);
370    gtk_object_destroy(GTK_OBJECT(wrap));
371  }
372}
373
374SUIL_LIB_EXPORT
375SuilWrapper*
376suil_wrapper_new(SuilHost*      host,
377                 const char*    host_type_uri,
378                 const char*    ui_type_uri,
379                 LV2_Feature*** features,
380                 unsigned       n_features)
381{
382  GtkWidget* parent = NULL;
383  for (unsigned i = 0; i < n_features; ++i) {
384    if (!strcmp((*features)[i]->URI, LV2_UI__parent)) {
385      parent = (GtkWidget*)(*features)[i]->data;
386    }
387  }
388
389  if (!GTK_CONTAINER(parent)) {
390    SUIL_ERRORF("No GtkContainer parent given for %s UI\n", ui_type_uri);
391    return NULL;
392  }
393
394  SuilWrapper* wrapper = (SuilWrapper*)calloc(1, sizeof(SuilWrapper));
395  wrapper->wrap        = wrapper_wrap;
396  wrapper->free        = wrapper_free;
397
398  SuilCocoaWrapper* const wrap =
399    SUIL_COCOA_WRAPPER(g_object_new(SUIL_TYPE_COCOA_WRAPPER, NULL));
400
401  wrapper->impl             = wrap;
402  wrapper->resize.handle    = wrap;
403  wrapper->resize.ui_resize = wrapper_resize;
404
405  gtk_container_add(GTK_CONTAINER(parent), GTK_WIDGET(wrap));
406  gtk_widget_set_can_focus(GTK_WIDGET(wrap), TRUE);
407  gtk_widget_set_sensitive(GTK_WIDGET(wrap), TRUE);
408  gtk_widget_realize(GTK_WIDGET(wrap));
409
410  GdkWindow* window = gtk_widget_get_window(GTK_WIDGET(wrap));
411  wrap->flt_win     = gtk_widget_get_window(parent);
412  gdk_window_add_filter(wrap->flt_win, event_filter, wrap);
413
414  NSView* parent_view = gdk_quartz_window_get_nsview(window);
415  suil_add_feature(features, &n_features, LV2_UI__parent, parent_view);
416  suil_add_feature(features, &n_features, LV2_UI__resize, &wrapper->resize);
417  suil_add_feature(features, &n_features, LV2_UI__idleInterface, NULL);
418
419  // Scan for URID map and options
420  LV2_URID_Map*       map     = NULL;
421  LV2_Options_Option* options = NULL;
422  for (LV2_Feature** f = *features; *f && (!map || !options); ++f) {
423    if (!strcmp((*f)->URI, LV2_OPTIONS__options)) {
424      options = (LV2_Options_Option*)(*f)->data;
425    } else if (!strcmp((*f)->URI, LV2_URID__map)) {
426      map = (LV2_URID_Map*)(*f)->data;
427    }
428  }
429
430  if (map && options) {
431    // Set UI update rate if given
432    LV2_URID ui_updateRate = map->map(map->handle, LV2_UI__updateRate);
433    for (LV2_Options_Option* o = options; o->key; ++o) {
434      if (o->key == ui_updateRate) {
435        wrap->idle_ms = 1000.0f / *(const float*)o->value;
436        break;
437      }
438    }
439  }
440
441  return wrapper;
442}
443
444} // extern "C"
445