68150b2c32c3af0b1b473ac058f0747d7ae7524d
[ardour.git] / gtk2_ardour / au_pluginui.mm
1 #undef  Marker
2 #define Marker FuckYouAppleAndYourLackOfNameSpaces
3
4 #include <sys/time.h>
5 #include <gtkmm/button.h>
6 #include <gdk/gdkquartz.h>
7
8 #include "pbd/convert.h"
9 #include "pbd/error.h"
10
11 #include "ardour/audio_unit.h"
12 #include "ardour/debug.h"
13 #include "ardour/plugin_insert.h"
14
15 #undef check // stupid gtk, stupid apple
16
17 #include <gtkmm2ext/utils.h>
18 #include <gtkmm2ext/window_proxy.h>
19
20 #include "au_pluginui.h"
21 #include "gui_thread.h"
22 #include "processor_box.h"
23
24 // yes, yes we know (see wscript for various available OSX compat modes)
25 #if defined (__clang__)
26 #       pragma clang diagnostic push
27 #       pragma clang diagnostic ignored "-Wdeprecated-declarations"
28 #endif
29
30 #include "CAAudioUnit.h"
31 #include "CAComponent.h"
32
33 #if defined (__clang__)
34 #       pragma clang diagnostic pop
35 #endif
36
37 #import <AudioUnit/AUCocoaUIView.h>
38 #import <CoreAudioKit/AUGenericView.h>
39 #import <objc/runtime.h>
40
41 #ifndef __ppc__
42 #include <dispatch/dispatch.h>
43 #endif
44
45 #undef Marker
46
47 #include "keyboard.h"
48 #include "utils.h"
49 #include "public_editor.h"
50 #include "i18n.h"
51
52 #include "gtk2ardour-config.h"
53
54 #ifdef COREAUDIO105
55 #define ArdourCloseComponent CloseComponent
56 #else
57 #define ArdourCloseComponent AudioComponentInstanceDispose
58 #endif
59 using namespace ARDOUR;
60 using namespace Gtk;
61 using namespace Gtkmm2ext;
62 using namespace std;
63 using namespace PBD;
64
65 vector<string> AUPluginUI::automation_mode_strings;
66 int64_t AUPluginUI::last_timer = 0;
67 bool    AUPluginUI::timer_needed = true;
68 CFRunLoopTimerRef AUPluginUI::cf_timer;
69 sigc::connection AUPluginUI::timer_connection;
70
71 static const gchar* _automation_mode_strings[] = {
72         X_("Manual"),
73         X_("Play"),
74         X_("Write"),
75         X_("Touch"),
76         0
77 };
78
79 static void
80 dump_view_tree (NSView* view, int depth, int maxdepth)
81 {
82         NSArray* subviews = [view subviews];
83         unsigned long cnt = [subviews count];
84
85         if (depth == 0) {
86                 NSView* su = [view superview];
87                 if (su) {
88                         NSRect sf = [su frame];
89                         cerr << " PARENT view " << su << " @ " <<  sf.origin.x << ", " << sf.origin.y
90                              << ' ' << sf.size.width << " x " << sf.size.height
91                              << endl;
92                 }
93         }
94
95         for (int d = 0; d < depth; d++) {
96                 cerr << '\t';
97         }
98         NSRect frame = [view frame];
99         cerr << " view " << view << " @ " <<  frame.origin.x << ", " << frame.origin.y
100                 << ' ' << frame.size.width << " x " << frame.size.height
101                 << endl;
102
103         if (depth >= maxdepth) {
104                 return;
105         }
106         for (unsigned long i = 0; i < cnt; ++i) {
107                 NSView* subview = [subviews objectAtIndex:i];
108                 dump_view_tree (subview, depth+1, maxdepth);
109         }
110 }
111
112 /* This deeply hacky block of code exists for a rather convoluted reason.
113  *
114  * The proximal reason is that there are plugins (such as XLN's Addictive Drums
115  * 2) which redraw their GUI/editor windows using a timer, and use a drawing
116  * technique that on Retina displays ends up calling arg32_image_mark_RGB32, a
117  * function that for some reason (probably byte-swapping or pixel-doubling) is
118  * many times slower than the function used on non-Retina displays.
119  *
120  * We are not the first people to discover the problem with
121  * arg32_image_mark_RGB32.
122  *
123  * Justin Fraenkel, the lead author of Reaper, wrote a very detailed account of
124  * the performance issues with arg32_image_mark_RGB32 here:
125  * http://www.1014.org/?article=516
126  *
127  * The problem was also seen by Robert O'Callahan (lead developer of rr, the
128  * reverse debugger) as far back as 2010:
129  * http://robert.ocallahan.org/2010/05/cglayer-performance-trap-with-isflipped_03.html
130  *
131  * In fact, it is so slow that the drawing takes up close to 100% of a single
132  * core, and the event loop that the drawing occurs in never sleeps or "idles".
133  *
134  * In AU hosts built directly on top of Cocoa, or some other toolkits, this
135  * isn't inherently a major problem - it just makes the entire GUI of the
136  * application slow.
137  *
138  * However, there is an additional problem for Ardour because GTK+ is built on
139  * top of the GDK/Quartz event loop integration. This integration is rather
140  * baroque, mostly because it was written at a time when CFRunLoop did not
141  * offer a way to wait for "input" from file descriptors (which arrived in OS X
142  * 10.5). As a result, it uses a hair-raising design involving an additional
143  * thread. This design has a major problem, which is that it effectively
144  * creates two nested run loops.
145  *
146  * The GTK+/GDK/glib one runs until it has nothing to do, at which time it
147  * calls a function to wait until there is something to do. On Linux or Windows
148  * that would involve some variant or relative of poll(2), which puts the
149  * process to sleep until there is something to do.
150  *
151  * On OS X, glib ends up calling [CFRunLoop waitForNextEventMatchingMask] which
152  * will eventually put the process to sleep, but won't do so until the
153  * CFRunLoop also has nothing to do. This includes (at least) a complete redraw
154  * cycle. If redrawing takes too long, and there are timers expired for another
155  * redraw (e.g. Addictive Drums 2, again), then the CFRunLoop will just start
156  * another redraw cycle after processing any events and other stuff.
157  *
158  * If the CFRunLoop stays busy, then it will never return to the glib
159  * level at all, thus stopping any further GTK+ level activity (events,
160  * drawing) from taking place. In short, the current (spring 2016) design of
161  * the GDK/Quartz event loop integration relies on the idea that the internal
162  * CFRunLoop will go idle, and totally breaks if this does not happen.
163  *
164  * So take a fully functional Ardour, add in XLN's Addictive Drums 2, and a
165  * Retina display, and Apple's ridiculously slow blitting code, and the
166  * CFRunLoop never goes idle. As soon as Addictive Drums starts drawing (over
167  * and over again), the GTK+ event loop stops receiving events and stops
168  * drawing.
169  *
170  * One fix for this was to run a nested GTK+ event loop iteration (or two)
171  * whenever a plugin window was redrawn. This works in the sense that the
172  * immediate issue (no GTK+ events or drawing) is fixed. But the recursive GTK+
173  * event loop causes its own (very subtle) problems too.
174  *
175  * This code takes a rather radical approach. We use Objective C's ability to
176  * swizzle object methods. Specifically, we replace [NSView displayIfNeeded]
177  * with our own version which will skip redraws of plugin windows if we tell it
178  * too. If we haven't done that, or if the redraw is of a non-plugin window,
179  * then we invoke the original displayIfNeeded method.
180  *
181  * After every 10 redraws of a given plugin GUI/editor window, we queue up a
182  * GTK/glib idle callback to measure the interval between those idle
183  * callbacks. We do this globally across all plugin windows, so if the callback
184  * is already queued, we don't requeue it.
185  *
186  * If the interval is longer than 40msec (a 25fps redraw rate), we set
187  * block_plugin_redraws to some number. Each successive call to our interposed
188  * displayIfNeeded method will (a) check this value and if non-zero (b) check
189  * if the call is for a plugin-related NSView/NSWindow. If it is, then we will
190  * skip the redisplay entirely, hopefully avoiding any calls to
191  * argb32_image_mark_RGB32 or any other slow drawing code, and thus allowing
192  * the CFRunLoop to go idle. If the value is zero or the call is for a
193  * non-plugin window, then we just invoke the "original" displayIfNeeded
194  * method.
195  *
196  * This hack adds a tiny bit of overhead onto redrawing of the entire
197  * application. But in the common case this consists of 1 conditional (the
198  * check on block_plugin_redraws, which will find it to be zero) and the
199  * invocation of the original method. Given how much work is typically done
200  * during drawing, this seems acceptable.
201  *
202  * The correct fix for this is to redesign the relationship between
203  * GTK+/GDK/glib so that a glib run loop is actually a CFRunLoop, with all
204  * GSources represented as CFRunLoopSources, without any nesting and without
205  * any additional thread. This is not a task to be undertaken lightly, and is
206  * certainly substantially more work than this was. It may never be possible to
207  * do that work in a way that could be integrated back into glib, because of
208  * the rather specific semantics and types of GSources, but it would almost
209  * certainly be possible to make it work for Ardour.
210  */
211
212 static uint32_t block_plugin_redraws = 0;
213 static const uint32_t minimum_redraw_rate = 30; /* frames per second */
214 static const uint32_t block_plugin_redraw_count = 15; /* number of combined plugin redraws to block, if blocking */
215
216 #ifdef __ppc__
217
218 /* PowerPC versions of OS X do not support libdispatch, which we use below when swizzling objective C. But they also don't have Retina
219  * which is the underlying reason for this code. So just skip it on those CPUs.
220  */
221
222
223 static void add_plugin_view (id view) {}
224 static void remove_plugin_view (id view) {}
225
226 #else
227
228 static IMP original_nsview_drawIfNeeded;
229 static std::vector<id> plugin_views;
230
231 static void add_plugin_view (id view)
232 {
233         if (plugin_views.empty()) {
234                 AUPluginUI::start_cf_timer ();
235         }
236
237         plugin_views.push_back (view);
238
239 }
240
241 static void remove_plugin_view (id view)
242 {
243         std::vector<id>::iterator x = find (plugin_views.begin(), plugin_views.end(), view);
244         if (x != plugin_views.end()) {
245                 plugin_views.erase (x);
246         }
247         if (plugin_views.empty()) {
248                 AUPluginUI::stop_cf_timer ();
249         }
250 }
251
252 static void interposed_drawIfNeeded (id receiver, SEL selector, NSRect rect)
253 {
254         if (block_plugin_redraws && (find (plugin_views.begin(), plugin_views.end(), receiver) != plugin_views.end())) {
255                 block_plugin_redraws--;
256 #ifdef AU_DEBUG_PRINT
257                 std::cerr << "Plugin redraw blocked\n";
258 #endif
259                 /* YOU ... SHALL .... NOT ... DRAW!!!! */
260                 return;
261         }
262         (void) ((int (*)(id,SEL,NSRect)) original_nsview_drawIfNeeded) (receiver, selector, rect);
263 }
264
265 @implementation NSView (Tracking)
266 + (void) load {
267         static dispatch_once_t once_token;
268
269         /* this swizzles NSView::displayIfNeeded and replaces it with
270          * interposed_drawIfNeeded(), which allows us to interpose and block
271          * the redrawing of plugin UIs when their redrawing behaviour
272          * is interfering with event loop behaviour.
273          */
274
275         dispatch_once (&once_token, ^{
276                         Method target = class_getInstanceMethod ([NSView class], @selector(displayIfNeeded));
277                         original_nsview_drawIfNeeded = method_setImplementation (target, (IMP) interposed_drawIfNeeded);
278                 });
279 }
280
281 @end
282
283 #endif /* __ppc__ */
284
285 /* END OF THE PLUGIN REDRAW HACK */
286
287 @implementation NotificationObject
288
289 - (NotificationObject*) initWithPluginUI: (AUPluginUI*) apluginui andCocoaParent: (NSWindow*) cp andTopLevelParent: (NSWindow*) tlp
290 {
291         self = [ super init ];
292
293         if (self) {
294                 plugin_ui = apluginui;
295                 top_level_parent = tlp;
296
297                 if (cp) {
298                         cocoa_parent = cp;
299
300                         [[NSNotificationCenter defaultCenter]
301                              addObserver:self
302                                 selector:@selector(cocoaParentActivationHandler:)
303                                     name:NSWindowDidBecomeMainNotification
304                                   object:NULL];
305
306                         [[NSNotificationCenter defaultCenter]
307                              addObserver:self
308                                 selector:@selector(cocoaParentBecameKeyHandler:)
309                                     name:NSWindowDidBecomeKeyNotification
310                                   object:NULL];
311                 }
312         }
313
314         return self;
315 }
316
317 - (void)cocoaParentActivationHandler:(NSNotification *)notification
318 {
319         NSWindow* notification_window = (NSWindow *)[notification object];
320
321         if (top_level_parent == notification_window || cocoa_parent == notification_window) {
322                 if ([notification_window isMainWindow]) {
323                         plugin_ui->activate();
324                 } else {
325                         plugin_ui->deactivate();
326                 }
327         }
328 }
329
330 - (void)cocoaParentBecameKeyHandler:(NSNotification *)notification
331 {
332         NSWindow* notification_window = (NSWindow *)[notification object];
333
334         if (top_level_parent == notification_window || cocoa_parent == notification_window) {
335                 if ([notification_window isKeyWindow]) {
336                         plugin_ui->activate();
337                 } else {
338                         plugin_ui->deactivate();
339                 }
340         }
341 }
342
343 - (void)auViewResized:(NSNotification *)notification
344 {
345         (void) notification; // stop complaints about unusued argument
346         plugin_ui->cocoa_view_resized();
347 }
348
349 @end
350
351 @implementation LiveResizeNotificationObject
352
353 - (LiveResizeNotificationObject*) initWithPluginUI: (AUPluginUI*) apluginui
354 {
355         self = [ super init ];
356         if (self) {
357                 plugin_ui = apluginui;
358         }
359
360         return self;
361 }
362
363 - (void)windowWillStartLiveResizeHandler:(NSNotification*)notification
364 {
365         plugin_ui->start_live_resize ();
366 }
367
368 - (void)windowWillEndLiveResizeHandler:(NSNotification*)notification
369 {
370         plugin_ui->end_live_resize ();
371 }
372 @end
373
374 AUPluginUI::AUPluginUI (boost::shared_ptr<PluginInsert> insert)
375         : PlugUIBase (insert)
376         , automation_mode_label (_("Automation"))
377         , preset_label (_("Presets"))
378         , resizable (false)
379         , req_width (0)
380         , req_height (0)
381         , cocoa_window (0)
382         , au_view (0)
383         , in_live_resize (false)
384         , plugin_requested_resize (0)
385         , cocoa_parent (0)
386         , _notify (0)
387         , _resize_notify (0)
388 {
389         if (automation_mode_strings.empty()) {
390                 automation_mode_strings = I18N (_automation_mode_strings);
391         }
392
393         set_popdown_strings (automation_mode_selector, automation_mode_strings);
394         automation_mode_selector.set_active_text (automation_mode_strings.front());
395
396         if ((au = boost::dynamic_pointer_cast<AUPlugin> (insert->plugin())) == 0) {
397                 error << _("unknown type of editor-supplying plugin (note: no AudioUnit support in this version of ardour)") << endmsg;
398                 throw failed_constructor ();
399         }
400
401         /* stuff some stuff into the top of the window */
402
403         HBox* smaller_hbox = manage (new HBox);
404
405         smaller_hbox->set_spacing (6);
406         smaller_hbox->pack_start (preset_label, false, false, 4);
407         smaller_hbox->pack_start (_preset_modified, false, false);
408         smaller_hbox->pack_start (_preset_combo, false, false);
409         smaller_hbox->pack_start (add_button, false, false);
410 #if 0
411         /* Ardour does not currently allow to overwrite existing presets
412          * see save_property_list() in audio_unit.cc
413          */
414         smaller_hbox->pack_start (save_button, false, false);
415 #endif
416 #if 0
417         /* one day these might be useful with an AU plugin, but not yet */
418         smaller_hbox->pack_start (automation_mode_label, false, false);
419         smaller_hbox->pack_start (automation_mode_selector, false, false);
420 #endif
421         smaller_hbox->pack_start (reset_button, false, false);
422         smaller_hbox->pack_start (bypass_button, false, true);
423
424         VBox* v1_box = manage (new VBox);
425         VBox* v2_box = manage (new VBox);
426
427         v1_box->pack_start (*smaller_hbox, false, true);
428         v2_box->pack_start (focus_button, false, true);
429
430         top_box.set_homogeneous (false);
431         top_box.set_spacing (6);
432         top_box.set_border_width (6);
433
434         top_box.pack_end (*v2_box, false, false);
435         top_box.pack_end (*v1_box, false, false);
436
437         set_spacing (6);
438         pack_start (top_box, false, false);
439         pack_start (low_box, true, true);
440
441         preset_label.show ();
442         _preset_combo.show ();
443         automation_mode_label.show ();
444         automation_mode_selector.show ();
445         bypass_button.show ();
446         top_box.show ();
447         low_box.show ();
448
449         cocoa_parent = 0;
450         cocoa_window = 0;
451
452 #ifdef WITH_CARBON
453         _activating_from_app = false;
454         _notify = 0;
455         au_view = 0;
456         editView = 0;
457         carbon_window = 0;
458 #endif
459
460         /* prefer cocoa, fall back to cocoa, but use carbon if its there */
461
462         if (test_cocoa_view_support()) {
463                 create_cocoa_view ();
464 #ifdef WITH_CARBON
465         } else if (test_carbon_view_support()) {
466                 create_carbon_view ();
467 #endif
468         } else {
469                 create_cocoa_view ();
470         }
471
472         low_box.add_events (Gdk::VISIBILITY_NOTIFY_MASK | Gdk::EXPOSURE_MASK);
473
474         low_box.signal_realize().connect (mem_fun (this, &AUPluginUI::lower_box_realized));
475         low_box.signal_visibility_notify_event ().connect (mem_fun (this, &AUPluginUI::lower_box_visibility_notify));
476         if (au_view) {
477                 low_box.signal_size_request ().connect (mem_fun (this, &AUPluginUI::lower_box_size_request));
478                 low_box.signal_size_allocate ().connect (mem_fun (this, &AUPluginUI::lower_box_size_allocate));
479                 low_box.signal_map ().connect (mem_fun (this, &AUPluginUI::lower_box_map));
480                 low_box.signal_unmap ().connect (mem_fun (this, &AUPluginUI::lower_box_unmap));
481         }
482 }
483
484 AUPluginUI::~AUPluginUI ()
485 {
486         if (_notify) {
487                 [[NSNotificationCenter defaultCenter] removeObserver:_notify];
488         }
489
490         if (_resize_notify) {
491                 [[NSNotificationCenter defaultCenter] removeObserver:_resize_notify];
492         }
493
494         NSWindow* win = get_nswindow();
495         if (au_view) {
496                 remove_plugin_view ([[win contentView] superview]);
497         }
498
499 #ifdef WITH_CARBON
500         if (cocoa_parent) {
501                 [win removeChildWindow:cocoa_parent];
502         }
503
504         if (carbon_window) {
505                 /* not parented, just overlaid on top of our window */
506                 DisposeWindow (carbon_window);
507         }
508 #endif
509
510         if (editView) {
511                 ArdourCloseComponent (editView);
512         }
513
514         if (au_view) {
515                 /* remove whatever we packed into low_box so that GTK doesn't
516                    mess with it.
517                  */
518                 [au_view removeFromSuperview];
519         }
520 }
521
522 bool
523 AUPluginUI::test_carbon_view_support ()
524 {
525 #ifdef WITH_CARBON
526         bool ret = false;
527
528         carbon_descriptor.componentType = kAudioUnitCarbonViewComponentType;
529         carbon_descriptor.componentSubType = 'gnrc';
530         carbon_descriptor.componentManufacturer = 'appl';
531         carbon_descriptor.componentFlags = 0;
532         carbon_descriptor.componentFlagsMask = 0;
533
534         OSStatus err;
535
536         // ask the AU for its first editor component
537         UInt32 propertySize;
538         err = AudioUnitGetPropertyInfo(*au->get_au(), kAudioUnitProperty_GetUIComponentList, kAudioUnitScope_Global, 0, &propertySize, NULL);
539         if (!err) {
540                 int nEditors = propertySize / sizeof(ComponentDescription);
541                 ComponentDescription *editors = new ComponentDescription[nEditors];
542                 err = AudioUnitGetProperty(*au->get_au(), kAudioUnitProperty_GetUIComponentList, kAudioUnitScope_Global, 0, editors, &propertySize);
543                 if (!err) {
544                         // just pick the first one for now
545                         carbon_descriptor = editors[0];
546                         ret = true;
547                 }
548                 delete[] editors;
549         }
550
551         return ret;
552 #else
553         return false;
554 #endif
555 }
556
557 bool
558 AUPluginUI::test_cocoa_view_support ()
559 {
560         UInt32 dataSize   = 0;
561         Boolean isWritable = 0;
562         OSStatus err = AudioUnitGetPropertyInfo(*au->get_au(),
563                                                 kAudioUnitProperty_CocoaUI, kAudioUnitScope_Global,
564                                                 0, &dataSize, &isWritable);
565
566         return dataSize > 0 && err == noErr;
567 }
568
569 bool
570 AUPluginUI::plugin_class_valid (Class pluginClass)
571 {
572         if([pluginClass conformsToProtocol: @protocol(AUCocoaUIBase)]) {
573                 if([pluginClass instancesRespondToSelector: @selector(interfaceVersion)] &&
574                    [pluginClass instancesRespondToSelector: @selector(uiViewForAudioUnit:withSize:)]) {
575                                 return true;
576                 }
577         }
578         return false;
579 }
580
581 int
582 AUPluginUI::create_cocoa_view ()
583 {
584         bool wasAbleToLoadCustomView = false;
585         AudioUnitCocoaViewInfo* cocoaViewInfo = NULL;
586         UInt32               numberOfClasses = 0;
587         UInt32     dataSize;
588         Boolean    isWritable;
589         NSString*           factoryClassName = 0;
590         NSURL*              CocoaViewBundlePath = NULL;
591
592         OSStatus result = AudioUnitGetPropertyInfo (*au->get_au(),
593                                                     kAudioUnitProperty_CocoaUI,
594                                                     kAudioUnitScope_Global,
595                                                     0,
596                                                     &dataSize,
597                                                     &isWritable );
598
599         numberOfClasses = (dataSize - sizeof(CFURLRef)) / sizeof(CFStringRef);
600
601         // Does view have custom Cocoa UI?
602
603         if ((result == noErr) && (numberOfClasses > 0) ) {
604
605                 DEBUG_TRACE(DEBUG::AudioUnits,
606                             string_compose ( "based on %1, there are %2 cocoa UI classes\n", dataSize, numberOfClasses));
607
608                 cocoaViewInfo = (AudioUnitCocoaViewInfo *)malloc(dataSize);
609
610                 if(AudioUnitGetProperty(*au->get_au(),
611                                         kAudioUnitProperty_CocoaUI,
612                                         kAudioUnitScope_Global,
613                                         0,
614                                         cocoaViewInfo,
615                                         &dataSize) == noErr) {
616
617                         CocoaViewBundlePath     = (NSURL *)cocoaViewInfo->mCocoaAUViewBundleLocation;
618
619                         // we only take the first view in this example.
620                         factoryClassName        = (NSString *)cocoaViewInfo->mCocoaAUViewClass[0];
621
622                         DEBUG_TRACE (DEBUG::AudioUnits, string_compose ("the factory name is %1 bundle is %2\n",
623                                                                         [factoryClassName UTF8String], CocoaViewBundlePath));
624
625                 } else {
626
627                         DEBUG_TRACE (DEBUG::AudioUnits, string_compose ("No cocoaUI property cocoaViewInfo = %1\n", cocoaViewInfo));
628
629                         if (cocoaViewInfo != NULL) {
630                                 free (cocoaViewInfo);
631                                 cocoaViewInfo = NULL;
632                         }
633                 }
634         }
635
636         // [A] Show custom UI if view has it
637
638         if (CocoaViewBundlePath && factoryClassName) {
639                 NSBundle *viewBundle    = [NSBundle bundleWithPath:[CocoaViewBundlePath path]];
640
641                 DEBUG_TRACE (DEBUG::AudioUnits, string_compose ("tried to create bundle, result = %1\n", viewBundle));
642
643                 if (viewBundle == NULL) {
644                         error << _("AUPluginUI: error loading AU view's bundle") << endmsg;
645                         return -1;
646                 } else {
647                         Class factoryClass = [viewBundle classNamed:factoryClassName];
648                         DEBUG_TRACE (DEBUG::AudioUnits, string_compose ("tried to create factory class, result = %1\n", factoryClass));
649                         if (!factoryClass) {
650                                 error << _("AUPluginUI: error getting AU view's factory class from bundle") << endmsg;
651                                 return -1;
652                         }
653
654                         // make sure 'factoryClass' implements the AUCocoaUIBase protocol
655                         if (!plugin_class_valid (factoryClass)) {
656                                 error << _("AUPluginUI: U view's factory class does not properly implement the AUCocoaUIBase protocol") << endmsg;
657                                 return -1;
658                         }
659                         // make a factory
660                         id factory = [[[factoryClass alloc] init] autorelease];
661                         if (factory == NULL) {
662                                 error << _("AUPluginUI: Could not create an instance of the AU view factory") << endmsg;
663                                 return -1;
664                         }
665
666                         DEBUG_TRACE (DEBUG::AudioUnits, "got a factory instance\n");
667
668                         // make a view
669                         au_view = [factory uiViewForAudioUnit:*au->get_au() withSize:NSZeroSize];
670
671                         DEBUG_TRACE (DEBUG::AudioUnits, string_compose ("view created @ %1\n", au_view));
672
673                         // cleanup
674                         [CocoaViewBundlePath release];
675                         if (cocoaViewInfo) {
676                                 UInt32 i;
677                                 for (i = 0; i < numberOfClasses; i++)
678                                         CFRelease(cocoaViewInfo->mCocoaAUViewClass[i]);
679
680                                 free (cocoaViewInfo);
681                         }
682                         wasAbleToLoadCustomView = true;
683                 }
684         }
685
686         if (!wasAbleToLoadCustomView) {
687                 // load generic Cocoa view
688                 DEBUG_TRACE (DEBUG::AudioUnits, string_compose ("Loading generic view using %1 -> %2\n", au,
689                                                                 au->get_au()));
690                 au_view = [[AUGenericView alloc] initWithAudioUnit:*au->get_au()];
691                 DEBUG_TRACE (DEBUG::AudioUnits, string_compose ("view created @ %1\n", au_view));
692                 [(AUGenericView *)au_view setShowsExpertParameters:1];
693         }
694
695         // Get the initial size of the new AU View's frame
696         NSRect  frame = [au_view frame];
697         req_width  = frame.size.width;
698         req_height = frame.size.height;
699
700         resizable  = [au_view autoresizingMask];
701
702         low_box.queue_resize ();
703
704         return 0;
705 }
706
707 void
708 AUPluginUI::update_view_size ()
709 {
710         last_au_frame = [au_view frame];
711 }
712
713 bool
714 AUPluginUI::timer_callback ()
715 {
716         block_plugin_redraws = 0;
717 #ifdef AU_DEBUG_PRINT
718         std::cerr << "Resume redraws after idle\n";
719 #endif
720         return false;
721 }
722
723 void
724 au_cf_timer_callback (CFRunLoopTimerRef timer, void* info)
725 {
726         reinterpret_cast<AUPluginUI*> (info)->cf_timer_callback ();
727 }
728
729 void
730 AUPluginUI::cf_timer_callback ()
731 {
732         int64_t now = ARDOUR::get_microseconds ();
733
734         if (!last_timer || block_plugin_redraws) {
735                 last_timer = now;
736                 return;
737         }
738
739         const int64_t usecs_slop = (1400000 / minimum_redraw_rate); // 140%
740
741 #ifdef AU_DEBUG_PRINT
742         std::cerr << "Timer elapsed : " << now - last_timer << std::endl;
743 #endif
744
745         if ((now - last_timer) > (usecs_slop + (1000000/minimum_redraw_rate))) {
746                 block_plugin_redraws = block_plugin_redraw_count;
747                 timer_connection.disconnect ();
748                 timer_connection = Glib::signal_timeout().connect (&AUPluginUI::timer_callback, 40);
749 #ifdef AU_DEBUG_PRINT
750                 std::cerr << "Timer too slow, block plugin redraws\n";
751 #endif
752         }
753
754         last_timer = now;
755 }
756
757 void
758 AUPluginUI::start_cf_timer ()
759 {
760         if (!timer_needed) {
761                 return;
762         }
763
764         CFTimeInterval interval = 1.0 / (float) minimum_redraw_rate;
765
766         cf_timer = CFRunLoopTimerCreate (kCFAllocatorDefault,
767                                          CFAbsoluteTimeGetCurrent() + interval,
768                                          interval, 0, 0,
769                                          au_cf_timer_callback,
770                                          0);
771
772         CFRunLoopAddTimer (CFRunLoopGetCurrent(), cf_timer, kCFRunLoopCommonModes);
773         timer_needed = false;
774 }
775
776 void
777 AUPluginUI::stop_cf_timer ()
778 {
779         if (timer_needed) {
780                 return;
781         }
782
783         CFRunLoopRemoveTimer (CFRunLoopGetCurrent(), cf_timer, kCFRunLoopCommonModes);
784         timer_needed = true;
785         last_timer = 0;
786 }
787
788 void
789 AUPluginUI::cocoa_view_resized ()
790 {
791         /* we can get here for two reasons:
792
793            1) the plugin window was resized by the user, a new size was
794            allocated to the window, ::update_view_size() was called, and we
795            explicitly/manually resized the AU NSView.
796
797            2) the plugin decided to resize itself (probably in response to user
798            action, but not in response to an actual window resize)
799
800            We only want to proceed with a window resizing in the second case.
801         */
802
803         if (in_live_resize) {
804                 /* ::update_view_size() will be called at the right times and
805                  * will update the view size. We don't need to anything while a
806                  * live resize in underway.
807                  */
808                 return;
809         }
810
811         if (plugin_requested_resize) {
812                 /* we tried to change the plugin frame from inside this method
813                  * (to adjust the origin), which changes the frame of the AU
814                  * NSView, resulting in a reentrant call to the FrameDidChange
815                  * handler (this method). Ignore this reentrant call.
816                  */
817 #ifdef AU_DEBUG_PRINT
818                 std::cerr << plugin->name() << " re-entrant call to cocoa_view_resized, ignored\n";
819 #endif
820                 return;
821         }
822
823         plugin_requested_resize = 1;
824
825         ProcessorWindowProxy* wp = insert->window_proxy();
826         if (wp) {
827                 /* Once a plugin has requested a resize of its own window, do
828                  * NOT save the window. The user may save state with the plugin
829                  * editor expanded to show "extra detail" - the plugin will not
830                  * refill this space when the editor is first
831                  * instantiated. Leaving the window in the "too big" state
832                  * cannot be recovered from.
833                  *
834                  * The window will be sized to fit the plugin's own request. Done.
835                  */
836                 wp->set_state_mask (WindowProxy::Position);
837         }
838
839         NSRect new_frame = [au_view frame];
840
841         /* from here on, we know that we've been called because the plugin
842          * decided to change the NSView frame itself.
843          */
844
845         /* step one: compute the change in the frame size.
846          */
847
848         float dy = new_frame.size.height - last_au_frame.size.height;
849         float dx = new_frame.size.width - last_au_frame.size.width;
850
851         NSWindow* window = get_nswindow ();
852         NSRect windowFrame= [window frame];
853
854         /* we want the top edge of the window to remain in the same place,
855            but the Cocoa/Quartz origin is at the lower left. So, when we make
856            the window larger, we will move it down, which means shifting the
857            origin toward (x,0). This will leave the top edge in the same place.
858         */
859
860         windowFrame.origin.y    -= dy;
861         windowFrame.origin.x    -= dx;
862         windowFrame.size.height += dy;
863         windowFrame.size.width  += dx;
864
865         NSUInteger old_auto_resize = [au_view autoresizingMask];
866
867         /* Some stupid AU Views change the origin of the original AU View when
868            they are resized (I'm looking at you AUSampler). If the origin has
869            been moved, move it back.
870         */
871
872         if (last_au_frame.origin.x != new_frame.origin.x ||
873             last_au_frame.origin.y != new_frame.origin.y) {
874                 new_frame.origin = last_au_frame.origin;
875                 [au_view setFrame:new_frame];
876                 /* also be sure to redraw the topbox because this can
877                    also go wrong.
878                  */
879                 top_box.queue_draw ();
880         }
881
882         /* We resize the window using Cocoa. We can't use GTK mechanisms
883          * because of this:
884          *
885          * http://www.lists.apple.com/archives/coreaudio-api/2005/Aug/msg00245.html
886          *
887          * "The host needs to be aware that changing the size of the window in
888          * response to the NSViewFrameDidChangeNotification can cause the view
889          * size to change depending on the autoresizing mask of the view. The
890          * host may need to cache the autoresizing mask of the view, set it to
891          * NSViewNotSizable, resize the window, and then reset the autoresizing
892          * mask of the view once the window has been sized."
893          *
894          */
895
896         [au_view setAutoresizingMask:NSViewNotSizable];
897         [window setFrame:windowFrame display:1];
898         [au_view setAutoresizingMask:old_auto_resize];
899
900         /* keep a copy of the size of the AU NSView. We didn't set it - the plugin did */
901         last_au_frame = new_frame;
902         req_width  = new_frame.size.width;
903         req_height = new_frame.size.height;
904
905         plugin_requested_resize = 0;
906 }
907
908 int
909 AUPluginUI::create_carbon_view ()
910 {
911 #ifdef WITH_CARBON
912         OSStatus err;
913         ControlRef root_control;
914
915         Component editComponent = FindNextComponent(NULL, &carbon_descriptor);
916
917         OpenAComponent(editComponent, &editView);
918         if (!editView) {
919                 error << _("AU Carbon view: cannot open AU Component") << endmsg;
920                 return -1;
921         }
922
923         Rect r = { 100, 100, 100, 100 };
924         WindowAttributes attr = WindowAttributes (kWindowStandardHandlerAttribute |
925                                                   kWindowCompositingAttribute|
926                                                   kWindowNoShadowAttribute|
927                                                   kWindowNoTitleBarAttribute);
928
929         if ((err = CreateNewWindow(kUtilityWindowClass, attr, &r, &carbon_window)) != noErr) {
930                 error << string_compose (_("AUPluginUI: cannot create carbon window (err: %1)"), err) << endmsg;
931                 ArdourCloseComponent (editView);
932                 return -1;
933         }
934
935         if ((err = GetRootControl(carbon_window, &root_control)) != noErr) {
936                 error << string_compose (_("AUPlugin: cannot get root control of carbon window (err: %1)"), err) << endmsg;
937                 DisposeWindow (carbon_window);
938                 ArdourCloseComponent (editView);
939                 return -1;
940         }
941
942         ControlRef viewPane;
943         Float32Point location  = { 0.0, 0.0 };
944         Float32Point size = { 0.0, 0.0 } ;
945
946         if ((err = AudioUnitCarbonViewCreate (editView, *au->get_au(), carbon_window, root_control, &location, &size, &viewPane)) != noErr) {
947                 error << string_compose (_("AUPluginUI: cannot create carbon plugin view (err: %1)"), err) << endmsg;
948                 DisposeWindow (carbon_window);
949                 ArdourCloseComponent (editView);
950                 return -1;
951         }
952
953         // resize window
954
955         Rect bounds;
956         GetControlBounds(viewPane, &bounds);
957         size.x = bounds.right-bounds.left;
958         size.y = bounds.bottom-bounds.top;
959
960         req_width = (int) (size.x + 0.5);
961         req_height = (int) (size.y + 0.5);
962
963         SizeWindow (carbon_window, req_width, req_height,  true);
964         low_box.set_size_request (req_width, req_height);
965
966         return 0;
967 #else
968         error << _("AU Carbon GUI is not supported.") << endmsg;
969         return -1;
970 #endif
971 }
972
973 NSWindow*
974 AUPluginUI::get_nswindow ()
975 {
976         Gtk::Container* toplevel = get_toplevel();
977
978         if (!toplevel || !toplevel->is_toplevel()) {
979                 error << _("AUPluginUI: no top level window!") << endmsg;
980                 return 0;
981         }
982
983         NSWindow* true_parent = gdk_quartz_window_get_nswindow (toplevel->get_window()->gobj());
984
985         if (!true_parent) {
986                 error << _("AUPluginUI: no top level window!") << endmsg;
987                 return 0;
988         }
989
990         return true_parent;
991 }
992
993 void
994 AUPluginUI::activate ()
995 {
996 #ifdef WITH_CARBON
997         ActivateWindow (carbon_window, TRUE);
998 #endif
999 }
1000
1001 void
1002 AUPluginUI::deactivate ()
1003 {
1004 #ifdef WITH_CARBON
1005         ActivateWindow (carbon_window, FALSE);
1006 #endif
1007 }
1008
1009 int
1010 AUPluginUI::parent_carbon_window ()
1011 {
1012 #ifdef WITH_CARBON
1013         NSWindow* win = get_nswindow ();
1014         Rect windowStructureBoundsRect;
1015
1016         if (!win) {
1017                 return -1;
1018         }
1019
1020         /* figure out where the cocoa parent window is in carbon-coordinate space, which
1021            differs from both cocoa-coordinate space and GTK-coordinate space
1022         */
1023
1024         GetWindowBounds((WindowRef) [win windowRef], kWindowStructureRgn, &windowStructureBoundsRect);
1025
1026         /* compute how tall the title bar is, because we have to offset the position of the carbon window
1027            by that much.
1028         */
1029
1030         NSRect content_frame = [NSWindow contentRectForFrameRect:[win frame] styleMask:[win styleMask]];
1031         NSRect wm_frame = [NSWindow frameRectForContentRect:content_frame styleMask:[win styleMask]];
1032
1033         int titlebar_height = wm_frame.size.height - content_frame.size.height;
1034
1035         int packing_extra = 6; // this is the total vertical packing in our top level window
1036
1037         /* move into position, based on parent window position */
1038         MoveWindow (carbon_window,
1039                     windowStructureBoundsRect.left,
1040                     windowStructureBoundsRect.top + titlebar_height + top_box.get_height() + packing_extra,
1041                     false);
1042         ShowWindow (carbon_window);
1043
1044         // create the cocoa window for the carbon one and make it visible
1045         cocoa_parent = [[NSWindow alloc] initWithWindowRef: carbon_window];
1046
1047         SetWindowActivationScope (carbon_window, kWindowActivationScopeNone);
1048
1049         _notify = [ [NotificationObject alloc] initWithPluginUI:this andCocoaParent:cocoa_parent andTopLevelParent:win ];
1050
1051         [win addChildWindow:cocoa_parent ordered:NSWindowAbove];
1052         [win setAutodisplay:1]; // turn of GTK stuff for this window
1053
1054         return 0;
1055 #else
1056         return -1;
1057 #endif
1058 }
1059
1060 int
1061 AUPluginUI::parent_cocoa_window ()
1062 {
1063         NSWindow* win = get_nswindow ();
1064
1065         if (!win) {
1066                 return -1;
1067         }
1068
1069         //[win setAutodisplay:1]; // turn off GTK stuff for this window
1070
1071         NSView* view = gdk_quartz_window_get_nsview (low_box.get_window()->gobj());
1072         [view addSubview:au_view];
1073         /* despite the fact that the documentation says that [NSWindow
1074            contentView] is the highest "accessible" NSView in an NSWindow, when
1075            the redraw cycle is executed, displayIfNeeded is actually executed
1076            on the parent of the contentView. To provide a marginal speedup when
1077            checking if a given redraw is for a plugin, use this "hidden" NSView
1078            to identify the plugin, so that we do not have to call [superview]
1079            every time in interposed_drawIfNeeded().
1080         */
1081         add_plugin_view ([[win contentView] superview]);
1082
1083         /* this moves the AU NSView down and over to provide a left-hand margin
1084          * and to clear the Ardour "task bar" (with plugin preset mgmt buttons,
1085          * keyboard focus control, bypass etc).
1086          */
1087
1088         gint xx, yy;
1089         gtk_widget_translate_coordinates(
1090                         GTK_WIDGET(low_box.gobj()),
1091                         GTK_WIDGET(low_box.get_parent()->gobj()),
1092                         8, 6, &xx, &yy);
1093         [au_view setFrame:NSMakeRect(xx, yy, req_width, req_height)];
1094
1095         last_au_frame = [au_view frame];
1096         // watch for size changes of the view
1097         _notify = [ [NotificationObject alloc] initWithPluginUI:this andCocoaParent:NULL andTopLevelParent:win ];
1098
1099         [[NSNotificationCenter defaultCenter] addObserver:_notify
1100                 selector:@selector(auViewResized:) name:NSViewFrameDidChangeNotification
1101                 object:au_view];
1102
1103         // catch notifications that live resizing is about to start
1104
1105 #if HAVE_COCOA_LIVE_RESIZING
1106         _resize_notify = [ [ LiveResizeNotificationObject alloc] initWithPluginUI:this ];
1107
1108         [[NSNotificationCenter defaultCenter] addObserver:_resize_notify
1109                 selector:@selector(windowWillStartLiveResizeHandler:) name:NSWindowWillStartLiveResizeNotification
1110                 object:win];
1111
1112         [[NSNotificationCenter defaultCenter] addObserver:_resize_notify
1113                 selector:@selector(windowWillEndLiveResizeHandler:) name:NSWindowDidEndLiveResizeNotification
1114                 object:win];
1115 #else
1116         /* No way before 10.6 to identify the start of a live resize (drag
1117          * resize) without subclassing NSView and overriding two of its
1118          * methods. Instead of that, we make the window non-resizable, thus
1119          * ending confusion about whether or not resizes are plugin or user
1120          * driven (they are always plugin-driven).
1121          */
1122
1123         Gtk::Container* toplevel = get_toplevel();
1124         Requisition req;
1125
1126         resizable = false;
1127
1128         if (toplevel && toplevel->is_toplevel()) {
1129                 toplevel->size_request (req);
1130                 toplevel->set_size_request (req.width, req.height);
1131                 dynamic_cast<Gtk::Window*>(toplevel)->set_resizable (false);
1132         }
1133
1134 #endif
1135         return 0;
1136 }
1137
1138 void
1139 AUPluginUI::grab_focus()
1140 {
1141         if (au_view) {
1142                 [au_view becomeFirstResponder];
1143         }
1144 }
1145 void
1146 AUPluginUI::forward_key_event (GdkEventKey* ev)
1147 {
1148         NSEvent* nsevent = gdk_quartz_event_get_nsevent ((GdkEvent*)ev);
1149
1150         if (au_view && nsevent) {
1151
1152                 /* filter on nsevent type here because GDK massages FlagsChanged
1153                    messages into GDK_KEY_{PRESS,RELEASE} but Cocoa won't
1154                    handle a FlagsChanged message as a keyDown or keyUp
1155                 */
1156
1157                 if ([nsevent type] == NSKeyDown) {
1158                         [[[au_view window] firstResponder] keyDown:nsevent];
1159                 } else if ([nsevent type] == NSKeyUp) {
1160                         [[[au_view window] firstResponder] keyUp:nsevent];
1161                 } else if ([nsevent type] == NSFlagsChanged) {
1162                         [[[au_view window] firstResponder] flagsChanged:nsevent];
1163                 }
1164         }
1165 }
1166
1167 void
1168 AUPluginUI::on_realize ()
1169 {
1170         VBox::on_realize ();
1171
1172         /* our windows should not have that resize indicator */
1173
1174         NSWindow* win = get_nswindow ();
1175         if (win) {
1176                 [win setShowsResizeIndicator:0];
1177         }
1178 }
1179
1180 void
1181 AUPluginUI::lower_box_realized ()
1182 {
1183         if (au_view) {
1184                 parent_cocoa_window ();
1185         } else if (carbon_window) {
1186                 parent_carbon_window ();
1187         }
1188 }
1189
1190 bool
1191 AUPluginUI::lower_box_visibility_notify (GdkEventVisibility* ev)
1192 {
1193 #ifdef WITH_CARBON
1194         if (carbon_window  && ev->state != GDK_VISIBILITY_UNOBSCURED) {
1195                 ShowWindow (carbon_window);
1196                 ActivateWindow (carbon_window, TRUE);
1197                 return true;
1198         }
1199 #endif
1200         return false;
1201 }
1202
1203 void
1204 AUPluginUI::lower_box_map ()
1205 {
1206         [au_view setHidden:0];
1207         update_view_size ();
1208 }
1209
1210 void
1211 AUPluginUI::lower_box_unmap ()
1212 {
1213         [au_view setHidden:1];
1214 }
1215
1216 void
1217 AUPluginUI::lower_box_size_request (GtkRequisition* requisition)
1218 {
1219         requisition->width  = req_width;
1220         requisition->height = req_height;
1221 }
1222
1223 void
1224 AUPluginUI::lower_box_size_allocate (Gtk::Allocation& allocation)
1225 {
1226         update_view_size ();
1227 }
1228
1229 void
1230 AUPluginUI::on_window_hide ()
1231 {
1232 #ifdef WITH_CARBON
1233         if (carbon_window) {
1234                 HideWindow (carbon_window);
1235                 ActivateWindow (carbon_window, FALSE);
1236         }
1237 #endif
1238         hide_all ();
1239
1240 #if 0
1241         NSArray* wins = [NSApp windows];
1242         for (uint32_t i = 0; i < [wins count]; i++) {
1243                 id win = [wins objectAtIndex:i];
1244         }
1245 #endif
1246 }
1247
1248 bool
1249 AUPluginUI::on_window_show (const string& /*title*/)
1250 {
1251         /* this is idempotent so just call it every time we show the window */
1252
1253         gtk_widget_realize (GTK_WIDGET(low_box.gobj()));
1254
1255         show_all ();
1256
1257 #ifdef WITH_CARBON
1258         if (carbon_window) {
1259                 ShowWindow (carbon_window);
1260                 ActivateWindow (carbon_window, TRUE);
1261         }
1262 #endif
1263
1264         return true;
1265 }
1266
1267 bool
1268 AUPluginUI::start_updating (GdkEventAny*)
1269 {
1270         return false;
1271 }
1272
1273 bool
1274 AUPluginUI::stop_updating (GdkEventAny*)
1275 {
1276         return false;
1277 }
1278
1279 PlugUIBase*
1280 create_au_gui (boost::shared_ptr<PluginInsert> plugin_insert, VBox** box)
1281 {
1282         AUPluginUI* aup = new AUPluginUI (plugin_insert);
1283         (*box) = aup;
1284         return aup;
1285 }
1286
1287 void
1288 AUPluginUI::start_live_resize ()
1289 {
1290         in_live_resize = true;
1291 }
1292
1293 void
1294 AUPluginUI::end_live_resize ()
1295 {
1296         in_live_resize = false;
1297 }