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