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