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