1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-10 23:44:24 +00:00

MacOS: Better support for SystemTrayIconComponent on Mojave

This commit is contained in:
Tom Poole 2019-04-04 18:03:34 +01:00
parent 40371c6b93
commit 7c45ad695c
8 changed files with 145 additions and 126 deletions

View file

@ -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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View file

@ -37,7 +37,8 @@
{
DemoTaskbarComponent()
{
setIconImage (getImageFromAssets ("juce_icon.png"));
setIconImage (getImageFromAssets ("juce_icon.png"),
getImageFromAssets ("juce_icon_template.png"));
setIconTooltip ("JUCE demo runner!");
}

View file

@ -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);

View file

@ -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> 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)
};

View file

@ -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);

View file

@ -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);
static ButtonEventForwarderClass cls;
eventForwarder.reset ([cls.createInstance() init]);
ButtonEventForwarderClass::setOwner (eventForwarder.get(), this);
setIconSize();
configureIcon();
statusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength: NSSquareStatusItemLength] retain];
[statusItem setView: view];
SystemTrayViewClass::frameChanged (view, SEL(), nullptr);
[[NSNotificationCenter defaultCenter] addObserver: view
selector: @selector (frameChanged:)
name: NSWindowDidMoveNotification
object: nil];
}
~Pimpl() override
{
[[NSNotificationCenter defaultCenter] removeObserver: view];
[[NSStatusBar systemStatusBar] removeStatusItem: statusItem];
SystemTrayViewClass::setOwner (view, nullptr);
SystemTrayViewClass::setImage (view, nil);
[statusItem release];
[view release];
[statusIcon release];
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
}
//==============================================================================
void updateIcon (const Image& newImage)
{
[statusIcon release];
statusIcon = imageToNSImage (newImage);
setIconSize();
SystemTrayViewClass::setImage (view, statusIcon);
[statusItem setView: view];
statusIcon.reset (imageToNSImage (newImage));
configureIcon();
[statusItem.get() button].image = statusIcon.get();
}
void setHighlighted (bool shouldHighlight)
{
isHighlighted = shouldHighlight;
[view setNeedsDisplay: true];
[[statusItem.get() button] setHighlighted: shouldHighlight];
}
void handleStatusItemAction (NSEvent* e)
void showMenu (const PopupMenu& menu)
{
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);
}
}
//==============================================================================
NSStatusItem* getStatusItem()
{
return statusItem.get();
}
//==============================================================================
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, {},
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 (MouseEvent (mouseSource, {}, eventMods.withoutMouseButtons(), pressure,
pressure,
MouseInputSource::invalidOrientation, MouseInputSource::invalidRotation,
MouseInputSource::invalidTiltX, MouseInputSource::invalidTiltY,
&owner, &owner, now, {}, now, 1, false));
&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<NSControl>
//==============================================================================
class ButtonEventForwarderClass : public ObjCClass<NSObject>
{
SystemTrayViewClass() : ObjCClass<NSControl> ("JUCESystemTrayView_")
public:
ButtonEventForwarderClass() : ObjCClass<NSObject> ("JUCEButtonEventForwarderClass_")
{
addIvar<Pimpl*> ("owner");
addIvar<NSImage*> ("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<Pimpl*> (self, "owner"); }
static NSImage* getImage (id self) { return getIvar<NSImage*> (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<NSStatusItem, NSObjectDeleter> statusItem;
std::unique_ptr<NSObject, NSObjectDeleter> eventForwarder;
std::unique_ptr<NSImage, NSObjectDeleter> 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)

View file

@ -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()));