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