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