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);
434 if (has_descriptive_presets ()) {
435 smaller_hbox->pack_start (preset_browser_button, false, false);
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);
442 if (insert->controls().size() > 0) {
443 smaller_hbox->pack_start (reset_button, false, false);
445 smaller_hbox->pack_start (bypass_button, false, true);
447 VBox* v1_box = manage (new VBox);
448 VBox* v2_box = manage (new VBox);
450 v1_box->pack_start (*smaller_hbox, false, true);
451 v2_box->pack_start (focus_button, false, true);
453 top_box.set_homogeneous (false);
454 top_box.set_spacing (6);
455 top_box.set_border_width (6);
457 top_box.pack_end (*v2_box, false, false);
458 top_box.pack_end (*v1_box, false, false);
461 pack_start (top_box, false, false);
462 pack_start (low_box, true, true);
464 preset_label.show ();
465 _preset_combo.show ();
466 automation_mode_label.show ();
467 automation_mode_selector.show ();
468 bypass_button.show ();
476 _activating_from_app = false;
483 /* prefer cocoa, fall back to cocoa, but use carbon if its there */
485 if (test_cocoa_view_support()) {
486 create_cocoa_view ();
488 } else if (test_carbon_view_support()) {
489 create_carbon_view ();
492 create_cocoa_view ();
495 low_box.add_events (Gdk::VISIBILITY_NOTIFY_MASK | Gdk::EXPOSURE_MASK);
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));
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));
507 AUPluginUI::~AUPluginUI ()
510 [[NSNotificationCenter defaultCenter] removeObserver:_notify];
513 if (_resize_notify) {
514 [[NSNotificationCenter defaultCenter] removeObserver:_resize_notify];
517 NSWindow* win = get_nswindow();
519 remove_plugin_view ([[win contentView] superview]);
524 [win removeChildWindow:cocoa_parent];
528 /* not parented, just overlaid on top of our window */
529 DisposeWindow (carbon_window);
534 ArdourCloseComponent (editView);
538 /* remove whatever we packed into low_box so that GTK doesn't
541 [au_view removeFromSuperview];
546 AUPluginUI::test_carbon_view_support ()
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;
559 // ask the AU for its first editor component
561 err = AudioUnitGetPropertyInfo(*au->get_au(), kAudioUnitProperty_GetUIComponentList, kAudioUnitScope_Global, 0, &propertySize, NULL);
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);
567 // just pick the first one for now
568 carbon_descriptor = editors[0];
581 AUPluginUI::test_cocoa_view_support ()
584 Boolean isWritable = 0;
585 OSStatus err = AudioUnitGetPropertyInfo(*au->get_au(),
586 kAudioUnitProperty_CocoaUI, kAudioUnitScope_Global,
587 0, &dataSize, &isWritable);
589 return dataSize > 0 && err == noErr;
593 AUPluginUI::plugin_class_valid (Class pluginClass)
595 if([pluginClass conformsToProtocol: @protocol(AUCocoaUIBase)]) {
596 if([pluginClass instancesRespondToSelector: @selector(interfaceVersion)] &&
597 [pluginClass instancesRespondToSelector: @selector(uiViewForAudioUnit:withSize:)]) {
605 AUPluginUI::create_cocoa_view ()
607 bool wasAbleToLoadCustomView = false;
608 AudioUnitCocoaViewInfo* cocoaViewInfo = NULL;
609 UInt32 numberOfClasses = 0;
612 NSString* factoryClassName = 0;
613 NSURL* CocoaViewBundlePath = NULL;
615 OSStatus result = AudioUnitGetPropertyInfo (*au->get_au(),
616 kAudioUnitProperty_CocoaUI,
617 kAudioUnitScope_Global,
622 numberOfClasses = (dataSize - sizeof(CFURLRef)) / sizeof(CFStringRef);
624 // Does view have custom Cocoa UI?
626 if ((result == noErr) && (numberOfClasses > 0) ) {
628 DEBUG_TRACE(DEBUG::AudioUnits,
629 string_compose ( "based on %1, there are %2 cocoa UI classes\n", dataSize, numberOfClasses));
631 cocoaViewInfo = (AudioUnitCocoaViewInfo *)malloc(dataSize);
633 if(AudioUnitGetProperty(*au->get_au(),
634 kAudioUnitProperty_CocoaUI,
635 kAudioUnitScope_Global,
638 &dataSize) == noErr) {
640 CocoaViewBundlePath = (NSURL *)cocoaViewInfo->mCocoaAUViewBundleLocation;
642 // we only take the first view in this example.
643 factoryClassName = (NSString *)cocoaViewInfo->mCocoaAUViewClass[0];
645 DEBUG_TRACE (DEBUG::AudioUnits, string_compose ("the factory name is %1 bundle is %2\n",
646 [factoryClassName UTF8String], CocoaViewBundlePath));
650 DEBUG_TRACE (DEBUG::AudioUnits, string_compose ("No cocoaUI property cocoaViewInfo = %1\n", cocoaViewInfo));
652 if (cocoaViewInfo != NULL) {
653 free (cocoaViewInfo);
654 cocoaViewInfo = NULL;
659 // [A] Show custom UI if view has it
661 if (CocoaViewBundlePath && factoryClassName) {
662 NSBundle *viewBundle = [NSBundle bundleWithPath:[CocoaViewBundlePath path]];
664 DEBUG_TRACE (DEBUG::AudioUnits, string_compose ("tried to create bundle, result = %1\n", viewBundle));
666 if (viewBundle == NULL) {
667 error << _("AUPluginUI: error loading AU view's bundle") << endmsg;
670 Class factoryClass = [viewBundle classNamed:factoryClassName];
671 DEBUG_TRACE (DEBUG::AudioUnits, string_compose ("tried to create factory class, result = %1\n", factoryClass));
673 error << _("AUPluginUI: error getting AU view's factory class from bundle") << endmsg;
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;
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;
689 DEBUG_TRACE (DEBUG::AudioUnits, "got a factory instance\n");
692 au_view = [factory uiViewForAudioUnit:*au->get_au() withSize:NSZeroSize];
694 DEBUG_TRACE (DEBUG::AudioUnits, string_compose ("view created @ %1\n", au_view));
697 [CocoaViewBundlePath release];
700 for (i = 0; i < numberOfClasses; i++)
701 CFRelease(cocoaViewInfo->mCocoaAUViewClass[i]);
703 free (cocoaViewInfo);
705 wasAbleToLoadCustomView = true;
709 if (!wasAbleToLoadCustomView) {
710 // load generic Cocoa view
711 DEBUG_TRACE (DEBUG::AudioUnits, string_compose ("Loading generic view using %1 -> %2\n", 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];
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;
723 resizable = [au_view autoresizingMask];
725 low_box.queue_resize ();
731 AUPluginUI::update_view_size ()
733 last_au_frame = [au_view frame];
737 AUPluginUI::timer_callback ()
739 block_plugin_redraws = 0;
740 #ifdef AU_DEBUG_PRINT
741 std::cerr << "Resume redraws after idle\n";
747 au_cf_timer_callback (CFRunLoopTimerRef timer, void* info)
749 reinterpret_cast<AUPluginUI*> (info)->cf_timer_callback ();
753 AUPluginUI::cf_timer_callback ()
755 int64_t now = ARDOUR::get_microseconds ();
757 if (!last_timer || block_plugin_redraws) {
762 const int64_t usecs_slop = (1400000 / minimum_redraw_rate); // 140%
764 #ifdef AU_DEBUG_PRINT
765 std::cerr << "Timer elapsed : " << now - last_timer << std::endl;
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";
781 AUPluginUI::start_cf_timer ()
787 CFTimeInterval interval = 1.0 / (float) minimum_redraw_rate;
789 cf_timer = CFRunLoopTimerCreate (kCFAllocatorDefault,
790 CFAbsoluteTimeGetCurrent() + interval,
792 au_cf_timer_callback,
795 CFRunLoopAddTimer (CFRunLoopGetCurrent(), cf_timer, kCFRunLoopCommonModes);
796 timer_needed = false;
800 AUPluginUI::stop_cf_timer ()
806 CFRunLoopRemoveTimer (CFRunLoopGetCurrent(), cf_timer, kCFRunLoopCommonModes);
812 AUPluginUI::cocoa_view_resized ()
814 /* we can get here for two reasons:
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.
820 2) the plugin decided to resize itself (probably in response to user
821 action, but not in response to an actual window resize)
823 We only want to proceed with a window resizing in the second case.
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.
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.
840 #ifdef AU_DEBUG_PRINT
841 std::cerr << plugin->name() << " re-entrant call to cocoa_view_resized, ignored\n";
846 plugin_requested_resize = 1;
848 ProcessorWindowProxy* wp = insert->window_proxy();
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.
857 * The window will be sized to fit the plugin's own request. Done.
859 wp->set_state_mask (WindowProxy::Position);
862 NSRect new_frame = [au_view frame];
864 /* from here on, we know that we've been called because the plugin
865 * decided to change the NSView frame itself.
868 /* step one: compute the change in the frame size.
871 float dy = new_frame.size.height - last_au_frame.size.height;
872 float dx = new_frame.size.width - last_au_frame.size.width;
874 NSWindow* window = get_nswindow ();
875 NSRect windowFrame= [window frame];
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.
883 windowFrame.origin.y -= dy;
884 windowFrame.origin.x -= dx;
885 windowFrame.size.height += dy;
886 windowFrame.size.width += dx;
888 NSUInteger old_auto_resize = [au_view autoresizingMask];
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.
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
902 top_box.queue_draw ();
905 /* We resize the window using Cocoa. We can't use GTK mechanisms
908 * http://www.lists.apple.com/archives/coreaudio-api/2005/Aug/msg00245.html
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."
919 [au_view setAutoresizingMask:NSViewNotSizable];
920 [window setFrame:windowFrame display:1];
921 [au_view setAutoresizingMask:old_auto_resize];
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;
928 plugin_requested_resize = 0;
932 AUPluginUI::create_carbon_view ()
936 ControlRef root_control;
938 Component editComponent = FindNextComponent(NULL, &carbon_descriptor);
940 OpenAComponent(editComponent, &editView);
942 error << _("AU Carbon view: cannot open AU Component") << endmsg;
946 Rect r = { 100, 100, 100, 100 };
947 WindowAttributes attr = WindowAttributes (kWindowStandardHandlerAttribute |
948 kWindowCompositingAttribute|
949 kWindowNoShadowAttribute|
950 kWindowNoTitleBarAttribute);
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);
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);
966 Float32Point location = { 0.0, 0.0 };
967 Float32Point size = { 0.0, 0.0 } ;
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);
979 GetControlBounds(viewPane, &bounds);
980 size.x = bounds.right-bounds.left;
981 size.y = bounds.bottom-bounds.top;
983 req_width = (int) (size.x + 0.5);
984 req_height = (int) (size.y + 0.5);
986 SizeWindow (carbon_window, req_width, req_height, true);
987 low_box.set_size_request (req_width, req_height);
991 error << _("AU Carbon GUI is not supported.") << endmsg;
997 AUPluginUI::get_nswindow ()
999 Gtk::Container* toplevel = get_toplevel();
1001 if (!toplevel || !toplevel->is_toplevel()) {
1002 error << _("AUPluginUI: no top level window!") << endmsg;
1006 NSWindow* true_parent = gdk_quartz_window_get_nswindow (toplevel->get_window()->gobj());
1009 error << _("AUPluginUI: no top level window!") << endmsg;
1017 AUPluginUI::activate ()
1020 ActivateWindow (carbon_window, TRUE);
1025 AUPluginUI::deactivate ()
1028 ActivateWindow (carbon_window, FALSE);
1033 AUPluginUI::parent_carbon_window ()
1036 NSWindow* win = get_nswindow ();
1037 Rect windowStructureBoundsRect;
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
1047 GetWindowBounds((WindowRef) [win windowRef], kWindowStructureRgn, &windowStructureBoundsRect);
1049 /* compute how tall the title bar is, because we have to offset the position of the carbon window
1053 NSRect content_frame = [NSWindow contentRectForFrameRect:[win frame] styleMask:[win styleMask]];
1054 NSRect wm_frame = [NSWindow frameRectForContentRect:content_frame styleMask:[win styleMask]];
1056 int titlebar_height = wm_frame.size.height - content_frame.size.height;
1058 int packing_extra = 6; // this is the total vertical packing in our top level window
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,
1065 ShowWindow (carbon_window);
1067 // create the cocoa window for the carbon one and make it visible
1068 cocoa_parent = [[NSWindow alloc] initWithWindowRef: carbon_window];
1070 SetWindowActivationScope (carbon_window, kWindowActivationScopeNone);
1072 _notify = [ [NotificationObject alloc] initWithPluginUI:this andCocoaParent:cocoa_parent andTopLevelParent:win ];
1074 [win addChildWindow:cocoa_parent ordered:NSWindowAbove];
1075 [win setAutodisplay:1]; // turn of GTK stuff for this window
1084 AUPluginUI::parent_cocoa_window ()
1086 NSWindow* win = get_nswindow ();
1092 //[win setAutodisplay:1]; // turn off GTK stuff for this window
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().
1104 add_plugin_view ([[win contentView] superview]);
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).
1112 gtk_widget_translate_coordinates(
1113 GTK_WIDGET(low_box.gobj()),
1114 GTK_WIDGET(low_box.get_parent()->gobj()),
1116 [au_view setFrame:NSMakeRect(xx, yy, req_width, req_height)];
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 ];
1122 [[NSNotificationCenter defaultCenter] addObserver:_notify
1123 selector:@selector(auViewResized:) name:NSViewFrameDidChangeNotification
1126 // catch notifications that live resizing is about to start
1128 #if HAVE_COCOA_LIVE_RESIZING
1129 _resize_notify = [ [ LiveResizeNotificationObject alloc] initWithPluginUI:this ];
1131 [[NSNotificationCenter defaultCenter] addObserver:_resize_notify
1132 selector:@selector(windowWillStartLiveResizeHandler:) name:NSWindowWillStartLiveResizeNotification
1135 [[NSNotificationCenter defaultCenter] addObserver:_resize_notify
1136 selector:@selector(windowWillEndLiveResizeHandler:) name:NSWindowDidEndLiveResizeNotification
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).
1146 Gtk::Container* toplevel = get_toplevel();
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);
1162 AUPluginUI::grab_focus()
1165 [au_view becomeFirstResponder];
1169 AUPluginUI::forward_key_event (GdkEventKey* ev)
1171 NSEvent* nsevent = gdk_quartz_event_get_nsevent ((GdkEvent*)ev);
1173 if (au_view && nsevent) {
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
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];
1191 AUPluginUI::on_realize ()
1193 VBox::on_realize ();
1195 /* our windows should not have that resize indicator */
1197 NSWindow* win = get_nswindow ();
1199 [win setShowsResizeIndicator:0];
1204 AUPluginUI::lower_box_realized ()
1207 parent_cocoa_window ();
1208 } else if (carbon_window) {
1209 parent_carbon_window ();
1214 AUPluginUI::lower_box_visibility_notify (GdkEventVisibility* ev)
1217 if (carbon_window && ev->state != GDK_VISIBILITY_UNOBSCURED) {
1218 ShowWindow (carbon_window);
1219 ActivateWindow (carbon_window, TRUE);
1227 AUPluginUI::lower_box_map ()
1229 [au_view setHidden:0];
1230 update_view_size ();
1234 AUPluginUI::lower_box_unmap ()
1236 [au_view setHidden:1];
1240 AUPluginUI::lower_box_size_request (GtkRequisition* requisition)
1242 requisition->width = req_width;
1243 requisition->height = req_height;
1247 AUPluginUI::lower_box_size_allocate (Gtk::Allocation& allocation)
1249 update_view_size ();
1253 AUPluginUI::on_window_hide ()
1256 if (carbon_window) {
1257 HideWindow (carbon_window);
1258 ActivateWindow (carbon_window, FALSE);
1264 NSArray* wins = [NSApp windows];
1265 for (uint32_t i = 0; i < [wins count]; i++) {
1266 id win = [wins objectAtIndex:i];
1272 AUPluginUI::on_window_show (const string& /*title*/)
1274 /* this is idempotent so just call it every time we show the window */
1276 gtk_widget_realize (GTK_WIDGET(low_box.gobj()));
1281 if (carbon_window) {
1282 ShowWindow (carbon_window);
1283 ActivateWindow (carbon_window, TRUE);
1291 AUPluginUI::start_updating (GdkEventAny*)
1297 AUPluginUI::stop_updating (GdkEventAny*)
1303 create_au_gui (boost::shared_ptr<PluginInsert> plugin_insert, VBox** box)
1305 AUPluginUI* aup = new AUPluginUI (plugin_insert);
1311 AUPluginUI::start_live_resize ()
1313 in_live_resize = true;
1317 AUPluginUI::end_live_resize ()
1319 in_live_resize = false;