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>
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.
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.
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.
22 #define Marker FuckYouAppleAndYourLackOfNameSpaces
25 #include <gtkmm/button.h>
26 #include <gtkmm/comboboxtext.h>
27 #include <gdk/gdkquartz.h>
29 #include "pbd/convert.h"
30 #include "pbd/error.h"
32 #include "ardour/audio_unit.h"
33 #include "ardour/debug.h"
34 #include "ardour/plugin_insert.h"
36 #undef check // stupid gtk, stupid apple
38 #include <gtkmm2ext/utils.h>
39 #include <gtkmm2ext/window_proxy.h>
41 #include "au_pluginui.h"
42 #include "gui_thread.h"
43 #include "processor_box.h"
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"
51 #include "CAAudioUnit.h"
52 #include "CAComponent.h"
54 #if defined (__clang__)
55 # pragma clang diagnostic pop
58 #import <AudioUnit/AUCocoaUIView.h>
59 #import <CoreAudioKit/AUGenericView.h>
60 #import <objc/runtime.h>
63 #include <dispatch/dispatch.h>
70 #include "public_editor.h"
73 #include "gtk2ardour-config.h"
76 #define ArdourCloseComponent CloseComponent
78 #define ArdourCloseComponent AudioComponentInstanceDispose
80 using namespace ARDOUR;
82 using namespace Gtkmm2ext;
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;
92 static const gchar* _automation_mode_strings[] = {
101 dump_view_tree (NSView* view, int depth, int maxdepth)
103 NSArray* subviews = [view subviews];
104 unsigned long cnt = [subviews count];
107 NSView* su = [view superview];
109 NSRect sf = [su frame];
110 cerr << " PARENT view " << su << " @ " << sf.origin.x << ", " << sf.origin.y
111 << ' ' << sf.size.width << " x " << sf.size.height
116 for (int d = 0; d < depth; d++) {
119 NSRect frame = [view frame];
120 cerr << " view " << view << " @ " << frame.origin.x << ", " << frame.origin.y
121 << ' ' << frame.size.width << " x " << frame.size.height
124 if (depth >= maxdepth) {
127 for (unsigned long i = 0; i < cnt; ++i) {
128 NSView* subview = [subviews objectAtIndex:i];
129 dump_view_tree (subview, depth+1, maxdepth);
133 /* This deeply hacky block of code exists for a rather convoluted reason.
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.
141 * We are not the first people to discover the problem with
142 * arg32_image_mark_RGB32.
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
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
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".
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
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.
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.
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.
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.
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
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.
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.
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.
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
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.
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.
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 */
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.
244 static void add_plugin_view (id view) {}
245 static void remove_plugin_view (id view) {}
249 static IMP original_nsview_drawIfNeeded;
250 static std::vector<id> plugin_views;
252 static void add_plugin_view (id view)
254 if (plugin_views.empty()) {
255 AUPluginUI::start_cf_timer ();
258 plugin_views.push_back (view);
262 static void remove_plugin_view (id view)
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);
268 if (plugin_views.empty()) {
269 AUPluginUI::stop_cf_timer ();
273 static void interposed_drawIfNeeded (id receiver, SEL selector, NSRect rect)
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";
280 /* YOU ... SHALL .... NOT ... DRAW!!!! */
283 (void) ((int (*)(id,SEL,NSRect)) original_nsview_drawIfNeeded) (receiver, selector, rect);
286 @implementation NSView (Tracking)
288 static dispatch_once_t once_token;
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.
296 dispatch_once (&once_token, ^{
297 Method target = class_getInstanceMethod ([NSView class], @selector(displayIfNeeded));
298 original_nsview_drawIfNeeded = method_setImplementation (target, (IMP) interposed_drawIfNeeded);
306 /* END OF THE PLUGIN REDRAW HACK */
308 @implementation NotificationObject
310 - (NotificationObject*) initWithPluginUI: (AUPluginUI*) apluginui andCocoaParent: (NSWindow*) cp andTopLevelParent: (NSWindow*) tlp
312 self = [ super init ];
315 plugin_ui = apluginui;
316 top_level_parent = tlp;
321 [[NSNotificationCenter defaultCenter]
323 selector:@selector(cocoaParentActivationHandler:)
324 name:NSWindowDidBecomeMainNotification
327 [[NSNotificationCenter defaultCenter]
329 selector:@selector(cocoaParentBecameKeyHandler:)
330 name:NSWindowDidBecomeKeyNotification
338 - (void)cocoaParentActivationHandler:(NSNotification *)notification
340 NSWindow* notification_window = (NSWindow *)[notification object];
342 if (top_level_parent == notification_window || cocoa_parent == notification_window) {
343 if ([notification_window isMainWindow]) {
344 plugin_ui->activate();
346 plugin_ui->deactivate();
351 - (void)cocoaParentBecameKeyHandler:(NSNotification *)notification
353 NSWindow* notification_window = (NSWindow *)[notification object];
355 if (top_level_parent == notification_window || cocoa_parent == notification_window) {
356 if ([notification_window isKeyWindow]) {
357 plugin_ui->activate();
359 plugin_ui->deactivate();
364 - (void)auViewResized:(NSNotification *)notification
366 (void) notification; // stop complaints about unusued argument
367 plugin_ui->cocoa_view_resized();
372 @implementation LiveResizeNotificationObject
374 - (LiveResizeNotificationObject*) initWithPluginUI: (AUPluginUI*) apluginui
376 self = [ super init ];
378 plugin_ui = apluginui;
384 - (void)windowWillStartLiveResizeHandler:(NSNotification*)notification
386 plugin_ui->start_live_resize ();
389 - (void)windowWillEndLiveResizeHandler:(NSNotification*)notification
391 plugin_ui->end_live_resize ();
395 AUPluginUI::AUPluginUI (boost::shared_ptr<PluginInsert> insert)
396 : PlugUIBase (insert)
397 , automation_mode_label (_("Automation"))
398 , preset_label (_("Presets"))
404 , in_live_resize (false)
405 , plugin_requested_resize (0)
410 if (automation_mode_strings.empty()) {
411 automation_mode_strings = I18N (_automation_mode_strings);
414 set_popdown_strings (automation_mode_selector, automation_mode_strings);
415 automation_mode_selector.set_active_text (automation_mode_strings.front());
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 ();
422 /* stuff some stuff into the top of the window */
424 HBox* smaller_hbox = manage (new HBox);
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);
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);
440 if (insert->controls().size() > 0) {
441 smaller_hbox->pack_start (reset_button, false, false);
443 smaller_hbox->pack_start (bypass_button, false, true);
445 VBox* v1_box = manage (new VBox);
446 VBox* v2_box = manage (new VBox);
448 v1_box->pack_start (*smaller_hbox, false, true);
449 v2_box->pack_start (focus_button, false, true);
451 top_box.set_homogeneous (false);
452 top_box.set_spacing (6);
453 top_box.set_border_width (6);
455 top_box.pack_end (*v2_box, false, false);
456 top_box.pack_end (*v1_box, false, false);
459 pack_start (top_box, false, false);
460 pack_start (low_box, true, true);
462 preset_label.show ();
463 _preset_combo.show ();
464 automation_mode_label.show ();
465 automation_mode_selector.show ();
466 bypass_button.show ();
474 _activating_from_app = false;
481 /* prefer cocoa, fall back to cocoa, but use carbon if its there */
483 if (test_cocoa_view_support()) {
484 create_cocoa_view ();
486 } else if (test_carbon_view_support()) {
487 create_carbon_view ();
490 create_cocoa_view ();
493 low_box.add_events (Gdk::VISIBILITY_NOTIFY_MASK | Gdk::EXPOSURE_MASK);
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));
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));
505 AUPluginUI::~AUPluginUI ()
508 [[NSNotificationCenter defaultCenter] removeObserver:_notify];
511 if (_resize_notify) {
512 [[NSNotificationCenter defaultCenter] removeObserver:_resize_notify];
515 NSWindow* win = get_nswindow();
517 remove_plugin_view ([[win contentView] superview]);
522 [win removeChildWindow:cocoa_parent];
526 /* not parented, just overlaid on top of our window */
527 DisposeWindow (carbon_window);
532 ArdourCloseComponent (editView);
536 /* remove whatever we packed into low_box so that GTK doesn't
539 [au_view removeFromSuperview];
544 AUPluginUI::test_carbon_view_support ()
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;
557 // ask the AU for its first editor component
559 err = AudioUnitGetPropertyInfo(*au->get_au(), kAudioUnitProperty_GetUIComponentList, kAudioUnitScope_Global, 0, &propertySize, NULL);
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);
565 // just pick the first one for now
566 carbon_descriptor = editors[0];
579 AUPluginUI::test_cocoa_view_support ()
582 Boolean isWritable = 0;
583 OSStatus err = AudioUnitGetPropertyInfo(*au->get_au(),
584 kAudioUnitProperty_CocoaUI, kAudioUnitScope_Global,
585 0, &dataSize, &isWritable);
587 return dataSize > 0 && err == noErr;
591 AUPluginUI::plugin_class_valid (Class pluginClass)
593 if([pluginClass conformsToProtocol: @protocol(AUCocoaUIBase)]) {
594 if([pluginClass instancesRespondToSelector: @selector(interfaceVersion)] &&
595 [pluginClass instancesRespondToSelector: @selector(uiViewForAudioUnit:withSize:)]) {
603 AUPluginUI::create_cocoa_view ()
605 bool wasAbleToLoadCustomView = false;
606 AudioUnitCocoaViewInfo* cocoaViewInfo = NULL;
607 UInt32 numberOfClasses = 0;
610 NSString* factoryClassName = 0;
611 NSURL* CocoaViewBundlePath = NULL;
613 OSStatus result = AudioUnitGetPropertyInfo (*au->get_au(),
614 kAudioUnitProperty_CocoaUI,
615 kAudioUnitScope_Global,
620 numberOfClasses = (dataSize - sizeof(CFURLRef)) / sizeof(CFStringRef);
622 // Does view have custom Cocoa UI?
624 if ((result == noErr) && (numberOfClasses > 0) ) {
626 DEBUG_TRACE(DEBUG::AudioUnits,
627 string_compose ( "based on %1, there are %2 cocoa UI classes\n", dataSize, numberOfClasses));
629 cocoaViewInfo = (AudioUnitCocoaViewInfo *)malloc(dataSize);
631 if(AudioUnitGetProperty(*au->get_au(),
632 kAudioUnitProperty_CocoaUI,
633 kAudioUnitScope_Global,
636 &dataSize) == noErr) {
638 CocoaViewBundlePath = (NSURL *)cocoaViewInfo->mCocoaAUViewBundleLocation;
640 // we only take the first view in this example.
641 factoryClassName = (NSString *)cocoaViewInfo->mCocoaAUViewClass[0];
643 DEBUG_TRACE (DEBUG::AudioUnits, string_compose ("the factory name is %1 bundle is %2\n",
644 [factoryClassName UTF8String], CocoaViewBundlePath));
648 DEBUG_TRACE (DEBUG::AudioUnits, string_compose ("No cocoaUI property cocoaViewInfo = %1\n", cocoaViewInfo));
650 if (cocoaViewInfo != NULL) {
651 free (cocoaViewInfo);
652 cocoaViewInfo = NULL;
657 // [A] Show custom UI if view has it
659 if (CocoaViewBundlePath && factoryClassName) {
660 NSBundle *viewBundle = [NSBundle bundleWithPath:[CocoaViewBundlePath path]];
662 DEBUG_TRACE (DEBUG::AudioUnits, string_compose ("tried to create bundle, result = %1\n", viewBundle));
664 if (viewBundle == NULL) {
665 error << _("AUPluginUI: error loading AU view's bundle") << endmsg;
668 Class factoryClass = [viewBundle classNamed:factoryClassName];
669 DEBUG_TRACE (DEBUG::AudioUnits, string_compose ("tried to create factory class, result = %1\n", factoryClass));
671 error << _("AUPluginUI: error getting AU view's factory class from bundle") << endmsg;
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;
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;
687 DEBUG_TRACE (DEBUG::AudioUnits, "got a factory instance\n");
690 au_view = [factory uiViewForAudioUnit:*au->get_au() withSize:NSZeroSize];
692 DEBUG_TRACE (DEBUG::AudioUnits, string_compose ("view created @ %1\n", au_view));
695 [CocoaViewBundlePath release];
698 for (i = 0; i < numberOfClasses; i++)
699 CFRelease(cocoaViewInfo->mCocoaAUViewClass[i]);
701 free (cocoaViewInfo);
703 wasAbleToLoadCustomView = true;
707 if (!wasAbleToLoadCustomView) {
708 // load generic Cocoa view
709 DEBUG_TRACE (DEBUG::AudioUnits, string_compose ("Loading generic view using %1 -> %2\n", 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];
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;
721 resizable = [au_view autoresizingMask];
723 low_box.queue_resize ();
729 AUPluginUI::update_view_size ()
731 last_au_frame = [au_view frame];
735 AUPluginUI::timer_callback ()
737 block_plugin_redraws = 0;
738 #ifdef AU_DEBUG_PRINT
739 std::cerr << "Resume redraws after idle\n";
745 au_cf_timer_callback (CFRunLoopTimerRef timer, void* info)
747 reinterpret_cast<AUPluginUI*> (info)->cf_timer_callback ();
751 AUPluginUI::cf_timer_callback ()
753 int64_t now = ARDOUR::get_microseconds ();
755 if (!last_timer || block_plugin_redraws) {
760 const int64_t usecs_slop = (1400000 / minimum_redraw_rate); // 140%
762 #ifdef AU_DEBUG_PRINT
763 std::cerr << "Timer elapsed : " << now - last_timer << std::endl;
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";
779 AUPluginUI::start_cf_timer ()
785 CFTimeInterval interval = 1.0 / (float) minimum_redraw_rate;
787 cf_timer = CFRunLoopTimerCreate (kCFAllocatorDefault,
788 CFAbsoluteTimeGetCurrent() + interval,
790 au_cf_timer_callback,
793 CFRunLoopAddTimer (CFRunLoopGetCurrent(), cf_timer, kCFRunLoopCommonModes);
794 timer_needed = false;
798 AUPluginUI::stop_cf_timer ()
804 CFRunLoopRemoveTimer (CFRunLoopGetCurrent(), cf_timer, kCFRunLoopCommonModes);
810 AUPluginUI::cocoa_view_resized ()
812 /* we can get here for two reasons:
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.
818 2) the plugin decided to resize itself (probably in response to user
819 action, but not in response to an actual window resize)
821 We only want to proceed with a window resizing in the second case.
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.
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.
838 #ifdef AU_DEBUG_PRINT
839 std::cerr << plugin->name() << " re-entrant call to cocoa_view_resized, ignored\n";
844 plugin_requested_resize = 1;
846 ProcessorWindowProxy* wp = insert->window_proxy();
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.
855 * The window will be sized to fit the plugin's own request. Done.
857 wp->set_state_mask (WindowProxy::Position);
860 NSRect new_frame = [au_view frame];
862 /* from here on, we know that we've been called because the plugin
863 * decided to change the NSView frame itself.
866 /* step one: compute the change in the frame size.
869 float dy = new_frame.size.height - last_au_frame.size.height;
870 float dx = new_frame.size.width - last_au_frame.size.width;
872 NSWindow* window = get_nswindow ();
873 NSRect windowFrame= [window frame];
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.
881 windowFrame.origin.y -= dy;
882 windowFrame.origin.x -= dx;
883 windowFrame.size.height += dy;
884 windowFrame.size.width += dx;
886 NSUInteger old_auto_resize = [au_view autoresizingMask];
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.
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
900 top_box.queue_draw ();
903 /* We resize the window using Cocoa. We can't use GTK mechanisms
906 * http://www.lists.apple.com/archives/coreaudio-api/2005/Aug/msg00245.html
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."
917 [au_view setAutoresizingMask:NSViewNotSizable];
918 [window setFrame:windowFrame display:1];
919 [au_view setAutoresizingMask:old_auto_resize];
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;
926 plugin_requested_resize = 0;
930 AUPluginUI::create_carbon_view ()
934 ControlRef root_control;
936 Component editComponent = FindNextComponent(NULL, &carbon_descriptor);
938 OpenAComponent(editComponent, &editView);
940 error << _("AU Carbon view: cannot open AU Component") << endmsg;
944 Rect r = { 100, 100, 100, 100 };
945 WindowAttributes attr = WindowAttributes (kWindowStandardHandlerAttribute |
946 kWindowCompositingAttribute|
947 kWindowNoShadowAttribute|
948 kWindowNoTitleBarAttribute);
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);
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);
964 Float32Point location = { 0.0, 0.0 };
965 Float32Point size = { 0.0, 0.0 } ;
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);
977 GetControlBounds(viewPane, &bounds);
978 size.x = bounds.right-bounds.left;
979 size.y = bounds.bottom-bounds.top;
981 req_width = (int) (size.x + 0.5);
982 req_height = (int) (size.y + 0.5);
984 SizeWindow (carbon_window, req_width, req_height, true);
985 low_box.set_size_request (req_width, req_height);
989 error << _("AU Carbon GUI is not supported.") << endmsg;
995 AUPluginUI::get_nswindow ()
997 Gtk::Container* toplevel = get_toplevel();
999 if (!toplevel || !toplevel->is_toplevel()) {
1000 error << _("AUPluginUI: no top level window!") << endmsg;
1004 NSWindow* true_parent = gdk_quartz_window_get_nswindow (toplevel->get_window()->gobj());
1007 error << _("AUPluginUI: no top level window!") << endmsg;
1015 AUPluginUI::activate ()
1018 ActivateWindow (carbon_window, TRUE);
1023 AUPluginUI::deactivate ()
1026 ActivateWindow (carbon_window, FALSE);
1031 AUPluginUI::parent_carbon_window ()
1034 NSWindow* win = get_nswindow ();
1035 Rect windowStructureBoundsRect;
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
1045 GetWindowBounds((WindowRef) [win windowRef], kWindowStructureRgn, &windowStructureBoundsRect);
1047 /* compute how tall the title bar is, because we have to offset the position of the carbon window
1051 NSRect content_frame = [NSWindow contentRectForFrameRect:[win frame] styleMask:[win styleMask]];
1052 NSRect wm_frame = [NSWindow frameRectForContentRect:content_frame styleMask:[win styleMask]];
1054 int titlebar_height = wm_frame.size.height - content_frame.size.height;
1056 int packing_extra = 6; // this is the total vertical packing in our top level window
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,
1063 ShowWindow (carbon_window);
1065 // create the cocoa window for the carbon one and make it visible
1066 cocoa_parent = [[NSWindow alloc] initWithWindowRef: carbon_window];
1068 SetWindowActivationScope (carbon_window, kWindowActivationScopeNone);
1070 _notify = [ [NotificationObject alloc] initWithPluginUI:this andCocoaParent:cocoa_parent andTopLevelParent:win ];
1072 [win addChildWindow:cocoa_parent ordered:NSWindowAbove];
1073 [win setAutodisplay:1]; // turn of GTK stuff for this window
1082 AUPluginUI::parent_cocoa_window ()
1084 NSWindow* win = get_nswindow ();
1090 //[win setAutodisplay:1]; // turn off GTK stuff for this window
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().
1102 add_plugin_view ([[win contentView] superview]);
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).
1110 gtk_widget_translate_coordinates(
1111 GTK_WIDGET(low_box.gobj()),
1112 GTK_WIDGET(low_box.get_parent()->gobj()),
1114 [au_view setFrame:NSMakeRect(xx, yy, req_width, req_height)];
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 ];
1120 [[NSNotificationCenter defaultCenter] addObserver:_notify
1121 selector:@selector(auViewResized:) name:NSViewFrameDidChangeNotification
1124 // catch notifications that live resizing is about to start
1126 #if HAVE_COCOA_LIVE_RESIZING
1127 _resize_notify = [ [ LiveResizeNotificationObject alloc] initWithPluginUI:this ];
1129 [[NSNotificationCenter defaultCenter] addObserver:_resize_notify
1130 selector:@selector(windowWillStartLiveResizeHandler:) name:NSWindowWillStartLiveResizeNotification
1133 [[NSNotificationCenter defaultCenter] addObserver:_resize_notify
1134 selector:@selector(windowWillEndLiveResizeHandler:) name:NSWindowDidEndLiveResizeNotification
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).
1144 Gtk::Container* toplevel = get_toplevel();
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);
1160 AUPluginUI::grab_focus()
1163 [au_view becomeFirstResponder];
1167 AUPluginUI::forward_key_event (GdkEventKey* ev)
1169 NSEvent* nsevent = gdk_quartz_event_get_nsevent ((GdkEvent*)ev);
1171 if (au_view && nsevent) {
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
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];
1189 AUPluginUI::on_realize ()
1191 VBox::on_realize ();
1193 /* our windows should not have that resize indicator */
1195 NSWindow* win = get_nswindow ();
1197 [win setShowsResizeIndicator:0];
1202 AUPluginUI::lower_box_realized ()
1205 parent_cocoa_window ();
1206 } else if (carbon_window) {
1207 parent_carbon_window ();
1212 AUPluginUI::lower_box_visibility_notify (GdkEventVisibility* ev)
1215 if (carbon_window && ev->state != GDK_VISIBILITY_UNOBSCURED) {
1216 ShowWindow (carbon_window);
1217 ActivateWindow (carbon_window, TRUE);
1225 AUPluginUI::lower_box_map ()
1227 [au_view setHidden:0];
1228 update_view_size ();
1232 AUPluginUI::lower_box_unmap ()
1234 [au_view setHidden:1];
1238 AUPluginUI::lower_box_size_request (GtkRequisition* requisition)
1240 requisition->width = req_width;
1241 requisition->height = req_height;
1245 AUPluginUI::lower_box_size_allocate (Gtk::Allocation& allocation)
1247 update_view_size ();
1251 AUPluginUI::on_window_hide ()
1254 if (carbon_window) {
1255 HideWindow (carbon_window);
1256 ActivateWindow (carbon_window, FALSE);
1262 NSArray* wins = [NSApp windows];
1263 for (uint32_t i = 0; i < [wins count]; i++) {
1264 id win = [wins objectAtIndex:i];
1270 AUPluginUI::on_window_show (const string& /*title*/)
1272 /* this is idempotent so just call it every time we show the window */
1274 gtk_widget_realize (GTK_WIDGET(low_box.gobj()));
1279 if (carbon_window) {
1280 ShowWindow (carbon_window);
1281 ActivateWindow (carbon_window, TRUE);
1289 AUPluginUI::start_updating (GdkEventAny*)
1295 AUPluginUI::stop_updating (GdkEventAny*)
1301 create_au_gui (boost::shared_ptr<PluginInsert> plugin_insert, VBox** box)
1303 AUPluginUI* aup = new AUPluginUI (plugin_insert);
1309 AUPluginUI::start_live_resize ()
1311 in_live_resize = true;
1315 AUPluginUI::end_live_resize ()
1317 in_live_resize = false;