Browse Source

MacOS: Better support for SystemTrayIconComponent on Mojave

tags/2021-05-28
Tom Poole 6 years ago
parent
commit
7c45ad695c
8 changed files with 146 additions and 127 deletions
  1. +27
    -0
      BREAKING-CHANGES.txt
  2. BIN
      examples/Assets/juce_icon_template.png
  3. +2
    -1
      examples/DemoRunner/Source/Main.cpp
  4. +4
    -0
      modules/juce_gui_basics/native/juce_mac_MainMenu.mm
  5. +15
    -2
      modules/juce_gui_extra/misc/juce_SystemTrayIconComponent.h
  6. +3
    -3
      modules/juce_gui_extra/native/juce_linux_X11_SystemTrayIcon.cpp
  7. +92
    -118
      modules/juce_gui_extra/native/juce_mac_SystemTrayIcon.cpp
  8. +3
    -3
      modules/juce_gui_extra/native/juce_win32_SystemTrayIcon.cpp

+ 27
- 0
BREAKING-CHANGES.txt View File

@@ -4,6 +4,33 @@ JUCE breaking changes
Develop 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 Change
------ ------
The AudioBlock class now differentiates between const and non-const data. The AudioBlock class now differentiates between const and non-const data.


BIN
examples/Assets/juce_icon_template.png View File

Before After
Width: 512  |  Height: 512  |  Size: 18KB

+ 2
- 1
examples/DemoRunner/Source/Main.cpp View File

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


+ 4
- 0
modules/juce_gui_basics/native/juce_mac_MainMenu.mm View File

@@ -283,7 +283,11 @@ public:
[item setTag: topLevelIndex]; [item setTag: topLevelIndex];
[item setEnabled: i.isEnabled]; [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]; [item setState: i.isTicked ? NSOnState : NSOffState];
#endif
[item setTarget: (id) callback]; [item setTarget: (id) callback];
auto* juceItem = new PopupMenu::Item (i); auto* juceItem = new PopupMenu::Item (i);


+ 15
- 2
modules/juce_gui_extra/misc/juce_SystemTrayIconComponent.h View File

@@ -56,14 +56,23 @@ class JUCE_API SystemTrayIconComponent : public Component
{ {
public: public:
//============================================================================== //==============================================================================
/** Constructor. */
SystemTrayIconComponent(); SystemTrayIconComponent();
/** Destructor. */ /** Destructor. */
~SystemTrayIconComponent() override; ~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). */ /** Changes the icon's tooltip (if the current OS supports this). */
void setIconTooltip (const String& tooltip); void setIconTooltip (const String& tooltip);
@@ -98,6 +107,10 @@ private:
JUCE_PUBLIC_IN_DLL_BUILD (class Pimpl) JUCE_PUBLIC_IN_DLL_BUILD (class Pimpl)
std::unique_ptr<Pimpl> 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) JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SystemTrayIconComponent)
}; };


+ 3
- 3
modules/juce_gui_extra/native/juce_linux_X11_SystemTrayIcon.cpp View File

@@ -96,16 +96,16 @@ private:
//============================================================================== //==============================================================================
void SystemTrayIconComponent::setIconImage (const Image& newImage)
void SystemTrayIconComponent::setIconImage (const Image& colourImage, const Image&)
{ {
pimpl.reset(); pimpl.reset();
if (newImage.isValid())
if (colourImage.isValid())
{ {
if (! isOnDesktop()) if (! isOnDesktop())
addToDesktop (0); addToDesktop (0);
pimpl.reset (new Pimpl (newImage, (Window) getWindowHandle()));
pimpl.reset (new Pimpl (colourImage, (Window) getWindowHandle()));
setVisible (true); setVisible (true);
toFront (false); toFront (false);


+ 92
- 118
modules/juce_gui_extra/native/juce_mac_SystemTrayIcon.cpp View File

@@ -33,59 +33,79 @@ extern NSMenu* createNSMenu (const PopupMenu&, const String& name, int topLevelM
class SystemTrayIconComponent::Pimpl : private Timer class SystemTrayIconComponent::Pimpl : private Timer
{ {
public: public:
//==============================================================================
Pimpl (SystemTrayIconComponent& iconComp, const Image& im) Pimpl (SystemTrayIconComponent& iconComp, const Image& im)
: owner (iconComp), statusIcon (imageToNSImage (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]; 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()) if (owner.isCurrentlyBlockedByAnotherModalComponent())
{ {
@@ -104,22 +124,22 @@ public:
auto mouseSource = Desktop::getInstance().getMainMouseSource(); auto mouseSource = Desktop::getInstance().getMainMouseSource();
auto pressure = (float) e.pressure; 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) 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: 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 void timerCallback() override
@@ -161,79 +165,49 @@ private:
setHighlighted (false); 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<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(); registerClass();
} }
static Pimpl* getOwner (id self) { return getIvar<Pimpl*> (self, "owner"); } 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 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: private:
static void handleEventDown (id self, SEL, NSEvent* e)
static void handleEvent (id self, SEL, id)
{ {
if (auto* owner = getOwner (self)) 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) 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) if (pimpl == nullptr)
pimpl.reset (new Pimpl (*this, newImage));
pimpl.reset (new Pimpl (*this, templateImage));
else else
pimpl->updateIcon (newImage);
pimpl->updateIcon (templateImage);
} }
else else
{ {
@@ -264,7 +238,7 @@ void SystemTrayIconComponent::hideInfoBubble()
void* SystemTrayIconComponent::getNativeHandle() const void* SystemTrayIconComponent::getNativeHandle() const
{ {
return pimpl != nullptr ? pimpl->statusItem : nullptr;
return pimpl != nullptr ? pimpl->getStatusItem() : nullptr;
} }
void SystemTrayIconComponent::showDropdownMenu (const PopupMenu& menu) void SystemTrayIconComponent::showDropdownMenu (const PopupMenu& menu)


+ 3
- 3
modules/juce_gui_extra/native/juce_win32_SystemTrayIcon.cpp 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) if (pimpl == nullptr)
pimpl.reset (new Pimpl (*this, hicon, (HWND) getWindowHandle())); pimpl.reset (new Pimpl (*this, hicon, (HWND) getWindowHandle()));


Loading…
Cancel
Save