diff --git a/BREAKING-CHANGES.txt b/BREAKING-CHANGES.txt index 40ee99eac6..9d3bde58fc 100644 --- a/BREAKING-CHANGES.txt +++ b/BREAKING-CHANGES.txt @@ -4,6 +4,33 @@ JUCE breaking changes Develop ======= +Change +------ +SystemTrayIconComponent::setIconImage now takes two arguments, rather than one. +The new argument is a template image for use on macOS where all non-transparent +regions will render in a monochrome colour determined dynamically by the +operating system. + +Possible Issues +--------------- +You will now need to provide two images to display a SystemTrayIconComponent +and the SystemTrayIconComponent will have a different appearance on macOS. + +Workaround +---------- +If you are not targeting macOS then you can provide an empty image, `{}`, for +the second argument. If you are targeting macOS then you will likely need to +design a new monochrome icon. + +Rationale +--------- +The introduction of "Dark Mode" in macOS 10.14 means that menu bar icons must +support several different colours and highlight modes to retain the same +appearance as the native Apple icons. Doing this correctly without delegating +the behaviour to the operating system is extremely cumbersome, and the APIs we +were previously using to interact with menu bar items have been deprecated. + + Change ------ The AudioBlock class now differentiates between const and non-const data. diff --git a/examples/Assets/juce_icon_template.png b/examples/Assets/juce_icon_template.png new file mode 100644 index 0000000000..e2964f1c34 Binary files /dev/null and b/examples/Assets/juce_icon_template.png differ diff --git a/examples/DemoRunner/Source/Main.cpp b/examples/DemoRunner/Source/Main.cpp index 4d8a120e2b..60d71990a1 100644 --- a/examples/DemoRunner/Source/Main.cpp +++ b/examples/DemoRunner/Source/Main.cpp @@ -37,7 +37,8 @@ { DemoTaskbarComponent() { - setIconImage (getImageFromAssets ("juce_icon.png")); + setIconImage (getImageFromAssets ("juce_icon.png"), + getImageFromAssets ("juce_icon_template.png")); setIconTooltip ("JUCE demo runner!"); } diff --git a/modules/juce_gui_basics/native/juce_mac_MainMenu.mm b/modules/juce_gui_basics/native/juce_mac_MainMenu.mm index 34e1a05d8a..976645a7e7 100644 --- a/modules/juce_gui_basics/native/juce_mac_MainMenu.mm +++ b/modules/juce_gui_basics/native/juce_mac_MainMenu.mm @@ -283,7 +283,11 @@ public: [item setTag: topLevelIndex]; [item setEnabled: i.isEnabled]; + #if defined (MAC_OS_X_VERSION_10_13) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_13 + [item setState: i.isTicked ? NSControlStateValueOn : NSControlStateValueOff]; + #else [item setState: i.isTicked ? NSOnState : NSOffState]; + #endif [item setTarget: (id) callback]; auto* juceItem = new PopupMenu::Item (i); diff --git a/modules/juce_gui_extra/misc/juce_SystemTrayIconComponent.h b/modules/juce_gui_extra/misc/juce_SystemTrayIconComponent.h index 3f84c58789..c905c6f18a 100644 --- a/modules/juce_gui_extra/misc/juce_SystemTrayIconComponent.h +++ b/modules/juce_gui_extra/misc/juce_SystemTrayIconComponent.h @@ -56,14 +56,23 @@ class JUCE_API SystemTrayIconComponent : public Component { public: //============================================================================== + /** Constructor. */ SystemTrayIconComponent(); /** Destructor. */ ~SystemTrayIconComponent() override; //============================================================================== - /** Changes the image shown in the taskbar. */ - void setIconImage (const Image& newImage); + /** Changes the image shown in the taskbar. + + On Windows and Linux a full colour Image is used as an icon. + On macOS a template image is used, where all non-transparent regions will be + rendered in a monochrome colour selected dynamically by the operating system. + + @param colourImage An colour image to use as an icon on Windows and Linux + @param templateImage A template image to use as an icon on macOS + */ + void setIconImage (const Image& colourImage, const Image& templateImage); /** Changes the icon's tooltip (if the current OS supports this). */ void setIconTooltip (const String& tooltip); @@ -98,6 +107,10 @@ private: JUCE_PUBLIC_IN_DLL_BUILD (class Pimpl) std::unique_ptr pimpl; + // The new setIconImage function signature requires different images for macOS + // and the other platforms + JUCE_DEPRECATED (void setIconImage (const Image& newImage)); + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SystemTrayIconComponent) }; diff --git a/modules/juce_gui_extra/native/juce_linux_X11_SystemTrayIcon.cpp b/modules/juce_gui_extra/native/juce_linux_X11_SystemTrayIcon.cpp index bab3c8a901..e9c6145ffa 100644 --- a/modules/juce_gui_extra/native/juce_linux_X11_SystemTrayIcon.cpp +++ b/modules/juce_gui_extra/native/juce_linux_X11_SystemTrayIcon.cpp @@ -96,16 +96,16 @@ private: //============================================================================== -void SystemTrayIconComponent::setIconImage (const Image& newImage) +void SystemTrayIconComponent::setIconImage (const Image& colourImage, const Image&) { pimpl.reset(); - if (newImage.isValid()) + if (colourImage.isValid()) { if (! isOnDesktop()) addToDesktop (0); - pimpl.reset (new Pimpl (newImage, (Window) getWindowHandle())); + pimpl.reset (new Pimpl (colourImage, (Window) getWindowHandle())); setVisible (true); toFront (false); diff --git a/modules/juce_gui_extra/native/juce_mac_SystemTrayIcon.cpp b/modules/juce_gui_extra/native/juce_mac_SystemTrayIcon.cpp index 19c9f6b7d3..886967f7a9 100644 --- a/modules/juce_gui_extra/native/juce_mac_SystemTrayIcon.cpp +++ b/modules/juce_gui_extra/native/juce_mac_SystemTrayIcon.cpp @@ -33,59 +33,79 @@ extern NSMenu* createNSMenu (const PopupMenu&, const String& name, int topLevelM class SystemTrayIconComponent::Pimpl : private Timer { public: + //============================================================================== Pimpl (SystemTrayIconComponent& iconComp, const Image& im) : owner (iconComp), statusIcon (imageToNSImage (im)) { - static SystemTrayViewClass cls; - view = [cls.createInstance() init]; - SystemTrayViewClass::setOwner (view, this); - SystemTrayViewClass::setImage (view, statusIcon); - - setIconSize(); - - statusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength: NSSquareStatusItemLength] retain]; - [statusItem setView: view]; - - SystemTrayViewClass::frameChanged (view, SEL(), nullptr); + static ButtonEventForwarderClass cls; + eventForwarder.reset ([cls.createInstance() init]); + ButtonEventForwarderClass::setOwner (eventForwarder.get(), this); + + configureIcon(); + + statusItem.reset ([[[NSStatusBar systemStatusBar] statusItemWithLength: NSSquareStatusItemLength] retain]); + auto button = [statusItem.get() button]; + button.image = statusIcon.get(); + button.target = eventForwarder.get(); + button.action = @selector (handleEvent:); + #if defined (MAC_OS_X_VERSION_10_12) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_12 + [button sendActionOn: NSEventMaskLeftMouseDown | NSEventMaskRightMouseDown | NSEventMaskScrollWheel]; + #else + [button sendActionOn: NSLeftMouseDownMask | NSRightMouseDownMask | NSScrollWheelMask]; + #endif + } - [[NSNotificationCenter defaultCenter] addObserver: view - selector: @selector (frameChanged:) - name: NSWindowDidMoveNotification - object: nil]; + //============================================================================== + void updateIcon (const Image& newImage) + { + statusIcon.reset (imageToNSImage (newImage)); + configureIcon(); + [statusItem.get() button].image = statusIcon.get(); } - ~Pimpl() override + void setHighlighted (bool shouldHighlight) { - [[NSNotificationCenter defaultCenter] removeObserver: view]; - [[NSStatusBar systemStatusBar] removeStatusItem: statusItem]; - SystemTrayViewClass::setOwner (view, nullptr); - SystemTrayViewClass::setImage (view, nil); - [statusItem release]; - [view release]; - [statusIcon release]; + [[statusItem.get() button] setHighlighted: shouldHighlight]; } - void updateIcon (const Image& newImage) + void showMenu (const PopupMenu& menu) { - [statusIcon release]; - statusIcon = imageToNSImage (newImage); - setIconSize(); - SystemTrayViewClass::setImage (view, statusIcon); - [statusItem setView: view]; + if (NSMenu* m = createNSMenu (menu, "MenuBarItem", -2, -3, true)) + { + setHighlighted (true); + stopTimer(); + + // There's currently no good alternative to this... + #if defined __clang__ && defined (MAC_OS_X_VERSION_10_14) && MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_14 + #define IGNORE_POPUP_DEPRECATION 1 + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + #endif + + [statusItem.get() popUpStatusItemMenu: m]; + + #if IGNORE_POPUP_DEPRECATION + #pragma clang diagnostic pop + #endif + + startTimer (1); + } } - void setHighlighted (bool shouldHighlight) + //============================================================================== + NSStatusItem* getStatusItem() { - isHighlighted = shouldHighlight; - [view setNeedsDisplay: true]; + return statusItem.get(); } - void handleStatusItemAction (NSEvent* e) + //============================================================================== + void handleEvent() { + auto e = [NSApp currentEvent]; NSEventType type = [e type]; - const bool isLeft = (type == NSEventTypeLeftMouseDown || type == NSEventTypeLeftMouseUp); - const bool isRight = (type == NSEventTypeRightMouseDown || type == NSEventTypeRightMouseUp); + const bool isLeft = (type == NSEventTypeLeftMouseDown); + const bool isRight = (type == NSEventTypeRightMouseDown); if (owner.isCurrentlyBlockedByAnotherModalComponent()) { @@ -104,22 +124,22 @@ public: auto mouseSource = Desktop::getInstance().getMainMouseSource(); auto pressure = (float) e.pressure; - if (isLeft || isRight) // Only mouse up is sent by the OS, so simulate a down/up + if (isLeft || isRight) { - setHighlighted (true); - startTimer (150); - - owner.mouseDown (MouseEvent (mouseSource, {}, - eventMods.withFlags (isLeft ? ModifierKeys::leftButtonModifier - : ModifierKeys::rightButtonModifier), - pressure, MouseInputSource::invalidOrientation, MouseInputSource::invalidRotation, - MouseInputSource::invalidTiltX, MouseInputSource::invalidTiltY, - &owner, &owner, now, {}, now, 1, false)); - - owner.mouseUp (MouseEvent (mouseSource, {}, eventMods.withoutMouseButtons(), pressure, - MouseInputSource::invalidOrientation, MouseInputSource::invalidRotation, - MouseInputSource::invalidTiltX, MouseInputSource::invalidTiltY, - &owner, &owner, now, {}, now, 1, false)); + owner.mouseDown ({ mouseSource, {}, + eventMods.withFlags (isLeft ? ModifierKeys::leftButtonModifier + : ModifierKeys::rightButtonModifier), + pressure, + MouseInputSource::invalidOrientation, MouseInputSource::invalidRotation, + MouseInputSource::invalidTiltX, MouseInputSource::invalidTiltY, + &owner, &owner, now, {}, now, 1, false }); + + owner.mouseUp ({ mouseSource, {}, + eventMods.withoutMouseButtons(), + pressure, + MouseInputSource::invalidOrientation, MouseInputSource::invalidRotation, + MouseInputSource::invalidTiltX, MouseInputSource::invalidTiltY, + &owner, &owner, now, {}, now, 1, false }); } else if (type == NSEventTypeMouseMoved) { @@ -131,28 +151,12 @@ public: } } - void showMenu (const PopupMenu& menu) - { - if (NSMenu* m = createNSMenu (menu, "MenuBarItem", -2, -3, true)) - { - setHighlighted (true); - stopTimer(); - [statusItem popUpStatusItemMenu: m]; - startTimer (1); - } - } - - SystemTrayIconComponent& owner; - NSStatusItem* statusItem = nil; - private: - NSImage* statusIcon = nil; - NSControl* view = nil; - bool isHighlighted = false; - - void setIconSize() + //============================================================================== + void configureIcon() { - [statusIcon setSize: NSMakeSize (20.0f, 20.0f)]; + [statusIcon.get() setSize: NSMakeSize (20.0f, 20.0f)]; + [statusIcon.get() setTemplate: true]; } void timerCallback() override @@ -161,79 +165,49 @@ private: setHighlighted (false); } - struct SystemTrayViewClass : public ObjCClass + //============================================================================== + class ButtonEventForwarderClass : public ObjCClass { - SystemTrayViewClass() : ObjCClass ("JUCESystemTrayView_") + public: + ButtonEventForwarderClass() : ObjCClass ("JUCEButtonEventForwarderClass_") { addIvar ("owner"); - addIvar ("image"); - addMethod (@selector (mouseDown:), handleEventDown, "v@:@"); - addMethod (@selector (rightMouseDown:), handleEventDown, "v@:@"); - addMethod (@selector (drawRect:), drawRect, "v@:@"); - addMethod (@selector (frameChanged:), frameChanged, "v@:@"); + addMethod (@selector (handleEvent:), handleEvent, "v@:@"); registerClass(); } static Pimpl* getOwner (id self) { return getIvar (self, "owner"); } - static NSImage* getImage (id self) { return getIvar (self, "image"); } static void setOwner (id self, Pimpl* owner) { object_setInstanceVariable (self, "owner", owner); } - static void setImage (id self, NSImage* image) { object_setInstanceVariable (self, "image", image); } - - static void frameChanged (id self, SEL, NSNotification*) - { - if (auto* owner = getOwner (self)) - { - NSRect r = [[[owner->statusItem view] window] frame]; - NSRect sr = [[[NSScreen screens] objectAtIndex: 0] frame]; - r.origin.y = sr.size.height - r.origin.y - r.size.height; - owner->owner.setBounds (convertToRectInt (r)); - } - } private: - static void handleEventDown (id self, SEL, NSEvent* e) + static void handleEvent (id self, SEL, id) { if (auto* owner = getOwner (self)) - owner->handleStatusItemAction (e); - } - - static void drawRect (id self, SEL, NSRect) - { - NSRect bounds = [self bounds]; - - if (auto* owner = getOwner (self)) - [owner->statusItem drawStatusBarBackgroundInRect: bounds - withHighlight: owner->isHighlighted]; - - if (NSImage* const im = getImage (self)) - { - NSSize imageSize = [im size]; - - [im drawInRect: NSMakeRect (bounds.origin.x + ((bounds.size.width - imageSize.width) / 2.0f), - bounds.origin.y + ((bounds.size.height - imageSize.height) / 2.0f), - imageSize.width, imageSize.height) - fromRect: NSZeroRect - operation: NSCompositingOperationSourceOver - fraction: 1.0f]; - } + owner->handleEvent(); } }; + //============================================================================== + SystemTrayIconComponent& owner; + std::unique_ptr statusItem; + std::unique_ptr eventForwarder; + std::unique_ptr statusIcon; + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl) }; //============================================================================== -void SystemTrayIconComponent::setIconImage (const Image& newImage) +void SystemTrayIconComponent::setIconImage (const Image&, const Image& templateImage) { - if (newImage.isValid()) + if (templateImage.isValid()) { if (pimpl == nullptr) - pimpl.reset (new Pimpl (*this, newImage)); + pimpl.reset (new Pimpl (*this, templateImage)); else - pimpl->updateIcon (newImage); + pimpl->updateIcon (templateImage); } else { @@ -264,7 +238,7 @@ void SystemTrayIconComponent::hideInfoBubble() void* SystemTrayIconComponent::getNativeHandle() const { - return pimpl != nullptr ? pimpl->statusItem : nullptr; + return pimpl != nullptr ? pimpl->getStatusItem() : nullptr; } void SystemTrayIconComponent::showDropdownMenu (const PopupMenu& menu) diff --git a/modules/juce_gui_extra/native/juce_win32_SystemTrayIcon.cpp b/modules/juce_gui_extra/native/juce_win32_SystemTrayIcon.cpp index 66b540a400..061bc1decd 100644 --- a/modules/juce_gui_extra/native/juce_win32_SystemTrayIcon.cpp +++ b/modules/juce_gui_extra/native/juce_win32_SystemTrayIcon.cpp @@ -196,11 +196,11 @@ private: }; //============================================================================== -void SystemTrayIconComponent::setIconImage (const Image& newImage) +void SystemTrayIconComponent::setIconImage (const Image& colourImage, const Image&) { - if (newImage.isValid()) + if (colourImage.isValid()) { - HICON hicon = IconConverters::createHICONFromImage (newImage, TRUE, 0, 0); + HICON hicon = IconConverters::createHICONFromImage (colourImage, TRUE, 0, 0); if (pimpl == nullptr) pimpl.reset (new Pimpl (*this, hicon, (HWND) getWindowHandle()));