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