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