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