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