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