use new action map API instead of ActionManager::get_action
[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         smaller_hbox->pack_start (save_button, false, false);
413         smaller_hbox->pack_start (delete_button, false, false);
414
415 #if 0
416         /* one day these might be useful with an AU plugin, but not yet */
417         smaller_hbox->pack_start (automation_mode_label, false, false);
418         smaller_hbox->pack_start (automation_mode_selector, false, false);
419 #endif
420         if (insert->controls().size() > 0) {
421                 smaller_hbox->pack_start (reset_button, false, false);
422         }
423         smaller_hbox->pack_start (bypass_button, false, true);
424
425         VBox* v1_box = manage (new VBox);
426         VBox* v2_box = manage (new VBox);
427
428         v1_box->pack_start (*smaller_hbox, false, true);
429         v2_box->pack_start (focus_button, false, true);
430
431         top_box.set_homogeneous (false);
432         top_box.set_spacing (6);
433         top_box.set_border_width (6);
434
435         top_box.pack_end (*v2_box, false, false);
436         top_box.pack_end (*v1_box, false, false);
437
438         set_spacing (6);
439         pack_start (top_box, false, false);
440         pack_start (low_box, true, true);
441
442         preset_label.show ();
443         _preset_combo.show ();
444         automation_mode_label.show ();
445         automation_mode_selector.show ();
446         bypass_button.show ();
447         top_box.show ();
448         low_box.show ();
449
450         cocoa_parent = 0;
451         cocoa_window = 0;
452
453 #ifdef WITH_CARBON
454         _activating_from_app = false;
455         _notify = 0;
456         au_view = 0;
457         editView = 0;
458         carbon_window = 0;
459 #endif
460
461         /* prefer cocoa, fall back to cocoa, but use carbon if its there */
462
463         if (test_cocoa_view_support()) {
464                 create_cocoa_view ();
465 #ifdef WITH_CARBON
466         } else if (test_carbon_view_support()) {
467                 create_carbon_view ();
468 #endif
469         } else {
470                 create_cocoa_view ();
471         }
472
473         low_box.add_events (Gdk::VISIBILITY_NOTIFY_MASK | Gdk::EXPOSURE_MASK);
474
475         low_box.signal_realize().connect (mem_fun (this, &AUPluginUI::lower_box_realized));
476         low_box.signal_visibility_notify_event ().connect (mem_fun (this, &AUPluginUI::lower_box_visibility_notify));
477         if (au_view) {
478                 low_box.signal_size_request ().connect (mem_fun (this, &AUPluginUI::lower_box_size_request));
479                 low_box.signal_size_allocate ().connect (mem_fun (this, &AUPluginUI::lower_box_size_allocate));
480                 low_box.signal_map ().connect (mem_fun (this, &AUPluginUI::lower_box_map));
481                 low_box.signal_unmap ().connect (mem_fun (this, &AUPluginUI::lower_box_unmap));
482         }
483 }
484
485 AUPluginUI::~AUPluginUI ()
486 {
487         if (_notify) {
488                 [[NSNotificationCenter defaultCenter] removeObserver:_notify];
489         }
490
491         if (_resize_notify) {
492                 [[NSNotificationCenter defaultCenter] removeObserver:_resize_notify];
493         }
494
495         NSWindow* win = get_nswindow();
496         if (au_view) {
497                 remove_plugin_view ([[win contentView] superview]);
498         }
499
500 #ifdef WITH_CARBON
501         if (cocoa_parent) {
502                 [win removeChildWindow:cocoa_parent];
503         }
504
505         if (carbon_window) {
506                 /* not parented, just overlaid on top of our window */
507                 DisposeWindow (carbon_window);
508         }
509 #endif
510
511         if (editView) {
512                 ArdourCloseComponent (editView);
513         }
514
515         if (au_view) {
516                 /* remove whatever we packed into low_box so that GTK doesn't
517                    mess with it.
518                  */
519                 [au_view removeFromSuperview];
520         }
521 }
522
523 bool
524 AUPluginUI::test_carbon_view_support ()
525 {
526 #ifdef WITH_CARBON
527         bool ret = false;
528
529         carbon_descriptor.componentType = kAudioUnitCarbonViewComponentType;
530         carbon_descriptor.componentSubType = 'gnrc';
531         carbon_descriptor.componentManufacturer = 'appl';
532         carbon_descriptor.componentFlags = 0;
533         carbon_descriptor.componentFlagsMask = 0;
534
535         OSStatus err;
536
537         // ask the AU for its first editor component
538         UInt32 propertySize;
539         err = AudioUnitGetPropertyInfo(*au->get_au(), kAudioUnitProperty_GetUIComponentList, kAudioUnitScope_Global, 0, &propertySize, NULL);
540         if (!err) {
541                 int nEditors = propertySize / sizeof(ComponentDescription);
542                 ComponentDescription *editors = new ComponentDescription[nEditors];
543                 err = AudioUnitGetProperty(*au->get_au(), kAudioUnitProperty_GetUIComponentList, kAudioUnitScope_Global, 0, editors, &propertySize);
544                 if (!err) {
545                         // just pick the first one for now
546                         carbon_descriptor = editors[0];
547                         ret = true;
548                 }
549                 delete[] editors;
550         }
551
552         return ret;
553 #else
554         return false;
555 #endif
556 }
557
558 bool
559 AUPluginUI::test_cocoa_view_support ()
560 {
561         UInt32 dataSize   = 0;
562         Boolean isWritable = 0;
563         OSStatus err = AudioUnitGetPropertyInfo(*au->get_au(),
564                                                 kAudioUnitProperty_CocoaUI, kAudioUnitScope_Global,
565                                                 0, &dataSize, &isWritable);
566
567         return dataSize > 0 && err == noErr;
568 }
569
570 bool
571 AUPluginUI::plugin_class_valid (Class pluginClass)
572 {
573         if([pluginClass conformsToProtocol: @protocol(AUCocoaUIBase)]) {
574                 if([pluginClass instancesRespondToSelector: @selector(interfaceVersion)] &&
575                    [pluginClass instancesRespondToSelector: @selector(uiViewForAudioUnit:withSize:)]) {
576                                 return true;
577                 }
578         }
579         return false;
580 }
581
582 int
583 AUPluginUI::create_cocoa_view ()
584 {
585         bool wasAbleToLoadCustomView = false;
586         AudioUnitCocoaViewInfo* cocoaViewInfo = NULL;
587         UInt32               numberOfClasses = 0;
588         UInt32     dataSize;
589         Boolean    isWritable;
590         NSString*           factoryClassName = 0;
591         NSURL*              CocoaViewBundlePath = NULL;
592
593         OSStatus result = AudioUnitGetPropertyInfo (*au->get_au(),
594                                                     kAudioUnitProperty_CocoaUI,
595                                                     kAudioUnitScope_Global,
596                                                     0,
597                                                     &dataSize,
598                                                     &isWritable );
599
600         numberOfClasses = (dataSize - sizeof(CFURLRef)) / sizeof(CFStringRef);
601
602         // Does view have custom Cocoa UI?
603
604         if ((result == noErr) && (numberOfClasses > 0) ) {
605
606                 DEBUG_TRACE(DEBUG::AudioUnits,
607                             string_compose ( "based on %1, there are %2 cocoa UI classes\n", dataSize, numberOfClasses));
608
609                 cocoaViewInfo = (AudioUnitCocoaViewInfo *)malloc(dataSize);
610
611                 if(AudioUnitGetProperty(*au->get_au(),
612                                         kAudioUnitProperty_CocoaUI,
613                                         kAudioUnitScope_Global,
614                                         0,
615                                         cocoaViewInfo,
616                                         &dataSize) == noErr) {
617
618                         CocoaViewBundlePath     = (NSURL *)cocoaViewInfo->mCocoaAUViewBundleLocation;
619
620                         // we only take the first view in this example.
621                         factoryClassName        = (NSString *)cocoaViewInfo->mCocoaAUViewClass[0];
622
623                         DEBUG_TRACE (DEBUG::AudioUnits, string_compose ("the factory name is %1 bundle is %2\n",
624                                                                         [factoryClassName UTF8String], CocoaViewBundlePath));
625
626                 } else {
627
628                         DEBUG_TRACE (DEBUG::AudioUnits, string_compose ("No cocoaUI property cocoaViewInfo = %1\n", cocoaViewInfo));
629
630                         if (cocoaViewInfo != NULL) {
631                                 free (cocoaViewInfo);
632                                 cocoaViewInfo = NULL;
633                         }
634                 }
635         }
636
637         // [A] Show custom UI if view has it
638
639         if (CocoaViewBundlePath && factoryClassName) {
640                 NSBundle *viewBundle    = [NSBundle bundleWithPath:[CocoaViewBundlePath path]];
641
642                 DEBUG_TRACE (DEBUG::AudioUnits, string_compose ("tried to create bundle, result = %1\n", viewBundle));
643
644                 if (viewBundle == NULL) {
645                         error << _("AUPluginUI: error loading AU view's bundle") << endmsg;
646                         return -1;
647                 } else {
648                         Class factoryClass = [viewBundle classNamed:factoryClassName];
649                         DEBUG_TRACE (DEBUG::AudioUnits, string_compose ("tried to create factory class, result = %1\n", factoryClass));
650                         if (!factoryClass) {
651                                 error << _("AUPluginUI: error getting AU view's factory class from bundle") << endmsg;
652                                 return -1;
653                         }
654
655                         // make sure 'factoryClass' implements the AUCocoaUIBase protocol
656                         if (!plugin_class_valid (factoryClass)) {
657                                 error << _("AUPluginUI: U view's factory class does not properly implement the AUCocoaUIBase protocol") << endmsg;
658                                 return -1;
659                         }
660                         // make a factory
661                         id factory = [[[factoryClass alloc] init] autorelease];
662                         if (factory == NULL) {
663                                 error << _("AUPluginUI: Could not create an instance of the AU view factory") << endmsg;
664                                 return -1;
665                         }
666
667                         DEBUG_TRACE (DEBUG::AudioUnits, "got a factory instance\n");
668
669                         // make a view
670                         au_view = [factory uiViewForAudioUnit:*au->get_au() withSize:NSZeroSize];
671
672                         DEBUG_TRACE (DEBUG::AudioUnits, string_compose ("view created @ %1\n", au_view));
673
674                         // cleanup
675                         [CocoaViewBundlePath release];
676                         if (cocoaViewInfo) {
677                                 UInt32 i;
678                                 for (i = 0; i < numberOfClasses; i++)
679                                         CFRelease(cocoaViewInfo->mCocoaAUViewClass[i]);
680
681                                 free (cocoaViewInfo);
682                         }
683                         wasAbleToLoadCustomView = true;
684                 }
685         }
686
687         if (!wasAbleToLoadCustomView) {
688                 // load generic Cocoa view
689                 DEBUG_TRACE (DEBUG::AudioUnits, string_compose ("Loading generic view using %1 -> %2\n", au,
690                                                                 au->get_au()));
691                 au_view = [[AUGenericView alloc] initWithAudioUnit:*au->get_au()];
692                 DEBUG_TRACE (DEBUG::AudioUnits, string_compose ("view created @ %1\n", au_view));
693                 [(AUGenericView *)au_view setShowsExpertParameters:1];
694         }
695
696         // Get the initial size of the new AU View's frame
697         NSRect  frame = [au_view frame];
698         req_width  = frame.size.width;
699         req_height = frame.size.height;
700
701         resizable  = [au_view autoresizingMask];
702
703         low_box.queue_resize ();
704
705         return 0;
706 }
707
708 void
709 AUPluginUI::update_view_size ()
710 {
711         last_au_frame = [au_view frame];
712 }
713
714 bool
715 AUPluginUI::timer_callback ()
716 {
717         block_plugin_redraws = 0;
718 #ifdef AU_DEBUG_PRINT
719         std::cerr << "Resume redraws after idle\n";
720 #endif
721         return false;
722 }
723
724 void
725 au_cf_timer_callback (CFRunLoopTimerRef timer, void* info)
726 {
727         reinterpret_cast<AUPluginUI*> (info)->cf_timer_callback ();
728 }
729
730 void
731 AUPluginUI::cf_timer_callback ()
732 {
733         int64_t now = ARDOUR::get_microseconds ();
734
735         if (!last_timer || block_plugin_redraws) {
736                 last_timer = now;
737                 return;
738         }
739
740         const int64_t usecs_slop = (1400000 / minimum_redraw_rate); // 140%
741
742 #ifdef AU_DEBUG_PRINT
743         std::cerr << "Timer elapsed : " << now - last_timer << std::endl;
744 #endif
745
746         if ((now - last_timer) > (usecs_slop + (1000000/minimum_redraw_rate))) {
747                 block_plugin_redraws = block_plugin_redraw_count;
748                 timer_connection.disconnect ();
749                 timer_connection = Glib::signal_timeout().connect (&AUPluginUI::timer_callback, 40);
750 #ifdef AU_DEBUG_PRINT
751                 std::cerr << "Timer too slow, block plugin redraws\n";
752 #endif
753         }
754
755         last_timer = now;
756 }
757
758 void
759 AUPluginUI::start_cf_timer ()
760 {
761         if (!timer_needed) {
762                 return;
763         }
764
765         CFTimeInterval interval = 1.0 / (float) minimum_redraw_rate;
766
767         cf_timer = CFRunLoopTimerCreate (kCFAllocatorDefault,
768                                          CFAbsoluteTimeGetCurrent() + interval,
769                                          interval, 0, 0,
770                                          au_cf_timer_callback,
771                                          0);
772
773         CFRunLoopAddTimer (CFRunLoopGetCurrent(), cf_timer, kCFRunLoopCommonModes);
774         timer_needed = false;
775 }
776
777 void
778 AUPluginUI::stop_cf_timer ()
779 {
780         if (timer_needed) {
781                 return;
782         }
783
784         CFRunLoopRemoveTimer (CFRunLoopGetCurrent(), cf_timer, kCFRunLoopCommonModes);
785         timer_needed = true;
786         last_timer = 0;
787 }
788
789 void
790 AUPluginUI::cocoa_view_resized ()
791 {
792         /* we can get here for two reasons:
793
794            1) the plugin window was resized by the user, a new size was
795            allocated to the window, ::update_view_size() was called, and we
796            explicitly/manually resized the AU NSView.
797
798            2) the plugin decided to resize itself (probably in response to user
799            action, but not in response to an actual window resize)
800
801            We only want to proceed with a window resizing in the second case.
802         */
803
804         if (in_live_resize) {
805                 /* ::update_view_size() will be called at the right times and
806                  * will update the view size. We don't need to anything while a
807                  * live resize in underway.
808                  */
809                 return;
810         }
811
812         if (plugin_requested_resize) {
813                 /* we tried to change the plugin frame from inside this method
814                  * (to adjust the origin), which changes the frame of the AU
815                  * NSView, resulting in a reentrant call to the FrameDidChange
816                  * handler (this method). Ignore this reentrant call.
817                  */
818 #ifdef AU_DEBUG_PRINT
819                 std::cerr << plugin->name() << " re-entrant call to cocoa_view_resized, ignored\n";
820 #endif
821                 return;
822         }
823
824         plugin_requested_resize = 1;
825
826         ProcessorWindowProxy* wp = insert->window_proxy();
827         if (wp) {
828                 /* Once a plugin has requested a resize of its own window, do
829                  * NOT save the window. The user may save state with the plugin
830                  * editor expanded to show "extra detail" - the plugin will not
831                  * refill this space when the editor is first
832                  * instantiated. Leaving the window in the "too big" state
833                  * cannot be recovered from.
834                  *
835                  * The window will be sized to fit the plugin's own request. Done.
836                  */
837                 wp->set_state_mask (WindowProxy::Position);
838         }
839
840         NSRect new_frame = [au_view frame];
841
842         /* from here on, we know that we've been called because the plugin
843          * decided to change the NSView frame itself.
844          */
845
846         /* step one: compute the change in the frame size.
847          */
848
849         float dy = new_frame.size.height - last_au_frame.size.height;
850         float dx = new_frame.size.width - last_au_frame.size.width;
851
852         NSWindow* window = get_nswindow ();
853         NSRect windowFrame= [window frame];
854
855         /* we want the top edge of the window to remain in the same place,
856            but the Cocoa/Quartz origin is at the lower left. So, when we make
857            the window larger, we will move it down, which means shifting the
858            origin toward (x,0). This will leave the top edge in the same place.
859         */
860
861         windowFrame.origin.y    -= dy;
862         windowFrame.origin.x    -= dx;
863         windowFrame.size.height += dy;
864         windowFrame.size.width  += dx;
865
866         NSUInteger old_auto_resize = [au_view autoresizingMask];
867
868         /* Some stupid AU Views change the origin of the original AU View when
869            they are resized (I'm looking at you AUSampler). If the origin has
870            been moved, move it back.
871         */
872
873         if (last_au_frame.origin.x != new_frame.origin.x ||
874             last_au_frame.origin.y != new_frame.origin.y) {
875                 new_frame.origin = last_au_frame.origin;
876                 [au_view setFrame:new_frame];
877                 /* also be sure to redraw the topbox because this can
878                    also go wrong.
879                  */
880                 top_box.queue_draw ();
881         }
882
883         /* We resize the window using Cocoa. We can't use GTK mechanisms
884          * because of this:
885          *
886          * http://www.lists.apple.com/archives/coreaudio-api/2005/Aug/msg00245.html
887          *
888          * "The host needs to be aware that changing the size of the window in
889          * response to the NSViewFrameDidChangeNotification can cause the view
890          * size to change depending on the autoresizing mask of the view. The
891          * host may need to cache the autoresizing mask of the view, set it to
892          * NSViewNotSizable, resize the window, and then reset the autoresizing
893          * mask of the view once the window has been sized."
894          *
895          */
896
897         [au_view setAutoresizingMask:NSViewNotSizable];
898         [window setFrame:windowFrame display:1];
899         [au_view setAutoresizingMask:old_auto_resize];
900
901         /* keep a copy of the size of the AU NSView. We didn't set it - the plugin did */
902         last_au_frame = new_frame;
903         req_width  = new_frame.size.width;
904         req_height = new_frame.size.height;
905
906         plugin_requested_resize = 0;
907 }
908
909 int
910 AUPluginUI::create_carbon_view ()
911 {
912 #ifdef WITH_CARBON
913         OSStatus err;
914         ControlRef root_control;
915
916         Component editComponent = FindNextComponent(NULL, &carbon_descriptor);
917
918         OpenAComponent(editComponent, &editView);
919         if (!editView) {
920                 error << _("AU Carbon view: cannot open AU Component") << endmsg;
921                 return -1;
922         }
923
924         Rect r = { 100, 100, 100, 100 };
925         WindowAttributes attr = WindowAttributes (kWindowStandardHandlerAttribute |
926                                                   kWindowCompositingAttribute|
927                                                   kWindowNoShadowAttribute|
928                                                   kWindowNoTitleBarAttribute);
929
930         if ((err = CreateNewWindow(kUtilityWindowClass, attr, &r, &carbon_window)) != noErr) {
931                 error << string_compose (_("AUPluginUI: cannot create carbon window (err: %1)"), err) << endmsg;
932                 ArdourCloseComponent (editView);
933                 return -1;
934         }
935
936         if ((err = GetRootControl(carbon_window, &root_control)) != noErr) {
937                 error << string_compose (_("AUPlugin: cannot get root control of carbon window (err: %1)"), err) << endmsg;
938                 DisposeWindow (carbon_window);
939                 ArdourCloseComponent (editView);
940                 return -1;
941         }
942
943         ControlRef viewPane;
944         Float32Point location  = { 0.0, 0.0 };
945         Float32Point size = { 0.0, 0.0 } ;
946
947         if ((err = AudioUnitCarbonViewCreate (editView, *au->get_au(), carbon_window, root_control, &location, &size, &viewPane)) != noErr) {
948                 error << string_compose (_("AUPluginUI: cannot create carbon plugin view (err: %1)"), err) << endmsg;
949                 DisposeWindow (carbon_window);
950                 ArdourCloseComponent (editView);
951                 return -1;
952         }
953
954         // resize window
955
956         Rect bounds;
957         GetControlBounds(viewPane, &bounds);
958         size.x = bounds.right-bounds.left;
959         size.y = bounds.bottom-bounds.top;
960
961         req_width = (int) (size.x + 0.5);
962         req_height = (int) (size.y + 0.5);
963
964         SizeWindow (carbon_window, req_width, req_height,  true);
965         low_box.set_size_request (req_width, req_height);
966
967         return 0;
968 #else
969         error << _("AU Carbon GUI is not supported.") << endmsg;
970         return -1;
971 #endif
972 }
973
974 NSWindow*
975 AUPluginUI::get_nswindow ()
976 {
977         Gtk::Container* toplevel = get_toplevel();
978
979         if (!toplevel || !toplevel->is_toplevel()) {
980                 error << _("AUPluginUI: no top level window!") << endmsg;
981                 return 0;
982         }
983
984         NSWindow* true_parent = gdk_quartz_window_get_nswindow (toplevel->get_window()->gobj());
985
986         if (!true_parent) {
987                 error << _("AUPluginUI: no top level window!") << endmsg;
988                 return 0;
989         }
990
991         return true_parent;
992 }
993
994 void
995 AUPluginUI::activate ()
996 {
997 #ifdef WITH_CARBON
998         ActivateWindow (carbon_window, TRUE);
999 #endif
1000 }
1001
1002 void
1003 AUPluginUI::deactivate ()
1004 {
1005 #ifdef WITH_CARBON
1006         ActivateWindow (carbon_window, FALSE);
1007 #endif
1008 }
1009
1010 int
1011 AUPluginUI::parent_carbon_window ()
1012 {
1013 #ifdef WITH_CARBON
1014         NSWindow* win = get_nswindow ();
1015         Rect windowStructureBoundsRect;
1016
1017         if (!win) {
1018                 return -1;
1019         }
1020
1021         /* figure out where the cocoa parent window is in carbon-coordinate space, which
1022            differs from both cocoa-coordinate space and GTK-coordinate space
1023         */
1024
1025         GetWindowBounds((WindowRef) [win windowRef], kWindowStructureRgn, &windowStructureBoundsRect);
1026
1027         /* compute how tall the title bar is, because we have to offset the position of the carbon window
1028            by that much.
1029         */
1030
1031         NSRect content_frame = [NSWindow contentRectForFrameRect:[win frame] styleMask:[win styleMask]];
1032         NSRect wm_frame = [NSWindow frameRectForContentRect:content_frame styleMask:[win styleMask]];
1033
1034         int titlebar_height = wm_frame.size.height - content_frame.size.height;
1035
1036         int packing_extra = 6; // this is the total vertical packing in our top level window
1037
1038         /* move into position, based on parent window position */
1039         MoveWindow (carbon_window,
1040                     windowStructureBoundsRect.left,
1041                     windowStructureBoundsRect.top + titlebar_height + top_box.get_height() + packing_extra,
1042                     false);
1043         ShowWindow (carbon_window);
1044
1045         // create the cocoa window for the carbon one and make it visible
1046         cocoa_parent = [[NSWindow alloc] initWithWindowRef: carbon_window];
1047
1048         SetWindowActivationScope (carbon_window, kWindowActivationScopeNone);
1049
1050         _notify = [ [NotificationObject alloc] initWithPluginUI:this andCocoaParent:cocoa_parent andTopLevelParent:win ];
1051
1052         [win addChildWindow:cocoa_parent ordered:NSWindowAbove];
1053         [win setAutodisplay:1]; // turn of GTK stuff for this window
1054
1055         return 0;
1056 #else
1057         return -1;
1058 #endif
1059 }
1060
1061 int
1062 AUPluginUI::parent_cocoa_window ()
1063 {
1064         NSWindow* win = get_nswindow ();
1065
1066         if (!win) {
1067                 return -1;
1068         }
1069
1070         //[win setAutodisplay:1]; // turn off GTK stuff for this window
1071
1072         NSView* view = gdk_quartz_window_get_nsview (low_box.get_window()->gobj());
1073         [view addSubview:au_view];
1074         /* despite the fact that the documentation says that [NSWindow
1075            contentView] is the highest "accessible" NSView in an NSWindow, when
1076            the redraw cycle is executed, displayIfNeeded is actually executed
1077            on the parent of the contentView. To provide a marginal speedup when
1078            checking if a given redraw is for a plugin, use this "hidden" NSView
1079            to identify the plugin, so that we do not have to call [superview]
1080            every time in interposed_drawIfNeeded().
1081         */
1082         add_plugin_view ([[win contentView] superview]);
1083
1084         /* this moves the AU NSView down and over to provide a left-hand margin
1085          * and to clear the Ardour "task bar" (with plugin preset mgmt buttons,
1086          * keyboard focus control, bypass etc).
1087          */
1088
1089         gint xx, yy;
1090         gtk_widget_translate_coordinates(
1091                         GTK_WIDGET(low_box.gobj()),
1092                         GTK_WIDGET(low_box.get_parent()->gobj()),
1093                         8, 6, &xx, &yy);
1094         [au_view setFrame:NSMakeRect(xx, yy, req_width, req_height)];
1095
1096         last_au_frame = [au_view frame];
1097         // watch for size changes of the view
1098         _notify = [ [NotificationObject alloc] initWithPluginUI:this andCocoaParent:NULL andTopLevelParent:win ];
1099
1100         [[NSNotificationCenter defaultCenter] addObserver:_notify
1101                 selector:@selector(auViewResized:) name:NSViewFrameDidChangeNotification
1102                 object:au_view];
1103
1104         // catch notifications that live resizing is about to start
1105
1106 #if HAVE_COCOA_LIVE_RESIZING
1107         _resize_notify = [ [ LiveResizeNotificationObject alloc] initWithPluginUI:this ];
1108
1109         [[NSNotificationCenter defaultCenter] addObserver:_resize_notify
1110                 selector:@selector(windowWillStartLiveResizeHandler:) name:NSWindowWillStartLiveResizeNotification
1111                 object:win];
1112
1113         [[NSNotificationCenter defaultCenter] addObserver:_resize_notify
1114                 selector:@selector(windowWillEndLiveResizeHandler:) name:NSWindowDidEndLiveResizeNotification
1115                 object:win];
1116 #else
1117         /* No way before 10.6 to identify the start of a live resize (drag
1118          * resize) without subclassing NSView and overriding two of its
1119          * methods. Instead of that, we make the window non-resizable, thus
1120          * ending confusion about whether or not resizes are plugin or user
1121          * driven (they are always plugin-driven).
1122          */
1123
1124         Gtk::Container* toplevel = get_toplevel();
1125         Requisition req;
1126
1127         resizable = false;
1128
1129         if (toplevel && toplevel->is_toplevel()) {
1130                 toplevel->size_request (req);
1131                 toplevel->set_size_request (req.width, req.height);
1132                 dynamic_cast<Gtk::Window*>(toplevel)->set_resizable (false);
1133         }
1134
1135 #endif
1136         return 0;
1137 }
1138
1139 void
1140 AUPluginUI::grab_focus()
1141 {
1142         if (au_view) {
1143                 [au_view becomeFirstResponder];
1144         }
1145 }
1146 void
1147 AUPluginUI::forward_key_event (GdkEventKey* ev)
1148 {
1149         NSEvent* nsevent = gdk_quartz_event_get_nsevent ((GdkEvent*)ev);
1150
1151         if (au_view && nsevent) {
1152
1153                 /* filter on nsevent type here because GDK massages FlagsChanged
1154                    messages into GDK_KEY_{PRESS,RELEASE} but Cocoa won't
1155                    handle a FlagsChanged message as a keyDown or keyUp
1156                 */
1157
1158                 if ([nsevent type] == NSKeyDown) {
1159                         [[[au_view window] firstResponder] keyDown:nsevent];
1160                 } else if ([nsevent type] == NSKeyUp) {
1161                         [[[au_view window] firstResponder] keyUp:nsevent];
1162                 } else if ([nsevent type] == NSFlagsChanged) {
1163                         [[[au_view window] firstResponder] flagsChanged:nsevent];
1164                 }
1165         }
1166 }
1167
1168 void
1169 AUPluginUI::on_realize ()
1170 {
1171         VBox::on_realize ();
1172
1173         /* our windows should not have that resize indicator */
1174
1175         NSWindow* win = get_nswindow ();
1176         if (win) {
1177                 [win setShowsResizeIndicator:0];
1178         }
1179 }
1180
1181 void
1182 AUPluginUI::lower_box_realized ()
1183 {
1184         if (au_view) {
1185                 parent_cocoa_window ();
1186         } else if (carbon_window) {
1187                 parent_carbon_window ();
1188         }
1189 }
1190
1191 bool
1192 AUPluginUI::lower_box_visibility_notify (GdkEventVisibility* ev)
1193 {
1194 #ifdef WITH_CARBON
1195         if (carbon_window  && ev->state != GDK_VISIBILITY_UNOBSCURED) {
1196                 ShowWindow (carbon_window);
1197                 ActivateWindow (carbon_window, TRUE);
1198                 return true;
1199         }
1200 #endif
1201         return false;
1202 }
1203
1204 void
1205 AUPluginUI::lower_box_map ()
1206 {
1207         [au_view setHidden:0];
1208         update_view_size ();
1209 }
1210
1211 void
1212 AUPluginUI::lower_box_unmap ()
1213 {
1214         [au_view setHidden:1];
1215 }
1216
1217 void
1218 AUPluginUI::lower_box_size_request (GtkRequisition* requisition)
1219 {
1220         requisition->width  = req_width;
1221         requisition->height = req_height;
1222 }
1223
1224 void
1225 AUPluginUI::lower_box_size_allocate (Gtk::Allocation& allocation)
1226 {
1227         update_view_size ();
1228 }
1229
1230 void
1231 AUPluginUI::on_window_hide ()
1232 {
1233 #ifdef WITH_CARBON
1234         if (carbon_window) {
1235                 HideWindow (carbon_window);
1236                 ActivateWindow (carbon_window, FALSE);
1237         }
1238 #endif
1239         hide_all ();
1240
1241 #if 0
1242         NSArray* wins = [NSApp windows];
1243         for (uint32_t i = 0; i < [wins count]; i++) {
1244                 id win = [wins objectAtIndex:i];
1245         }
1246 #endif
1247 }
1248
1249 bool
1250 AUPluginUI::on_window_show (const string& /*title*/)
1251 {
1252         /* this is idempotent so just call it every time we show the window */
1253
1254         gtk_widget_realize (GTK_WIDGET(low_box.gobj()));
1255
1256         show_all ();
1257
1258 #ifdef WITH_CARBON
1259         if (carbon_window) {
1260                 ShowWindow (carbon_window);
1261                 ActivateWindow (carbon_window, TRUE);
1262         }
1263 #endif
1264
1265         return true;
1266 }
1267
1268 bool
1269 AUPluginUI::start_updating (GdkEventAny*)
1270 {
1271         return false;
1272 }
1273
1274 bool
1275 AUPluginUI::stop_updating (GdkEventAny*)
1276 {
1277         return false;
1278 }
1279
1280 PlugUIBase*
1281 create_au_gui (boost::shared_ptr<PluginInsert> plugin_insert, VBox** box)
1282 {
1283         AUPluginUI* aup = new AUPluginUI (plugin_insert);
1284         (*box) = aup;
1285         return aup;
1286 }
1287
1288 void
1289 AUPluginUI::start_live_resize ()
1290 {
1291         in_live_resize = true;
1292 }
1293
1294 void
1295 AUPluginUI::end_live_resize ()
1296 {
1297         in_live_resize = false;
1298 }