The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

437 lines
17KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE 6 technical preview.
  4. Copyright (c) 2017 - ROLI Ltd.
  5. You may use this code under the terms of the GPL v3
  6. (see www.gnu.org/licenses).
  7. For this technical preview, this file is not subject to commercial licensing.
  8. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  9. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  10. DISCLAIMED.
  11. ==============================================================================
  12. */
  13. namespace juce
  14. {
  15. #pragma clang diagnostic push
  16. #pragma clang diagnostic ignored "-Wunguarded-availability"
  17. #if JUCE_CLANG && defined (MAC_OS_X_VERSION_10_14) && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_14
  18. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  19. #endif
  20. extern NSMenu* createNSMenu (const PopupMenu&, const String& name, int topLevelMenuId,
  21. int topLevelIndex, bool addDelegate);
  22. //==============================================================================
  23. struct StatusItemContainer : public Timer
  24. {
  25. //==============================================================================
  26. StatusItemContainer (SystemTrayIconComponent& iconComp, const Image& im)
  27. : owner (iconComp), statusIcon (imageToNSImage (im))
  28. {
  29. }
  30. virtual void configureIcon() = 0;
  31. virtual void setHighlighted (bool shouldHighlight) = 0;
  32. //==============================================================================
  33. void setIconSize()
  34. {
  35. [statusIcon.get() setSize: NSMakeSize (20.0f, 20.0f)];
  36. }
  37. void updateIcon (const Image& newImage)
  38. {
  39. statusIcon.reset (imageToNSImage (newImage));
  40. setIconSize();
  41. configureIcon();
  42. }
  43. void showMenu (const PopupMenu& menu)
  44. {
  45. if (NSMenu* m = createNSMenu (menu, "MenuBarItem", -2, -3, true))
  46. {
  47. setHighlighted (true);
  48. stopTimer();
  49. // There's currently no good alternative to this.
  50. [statusItem.get() popUpStatusItemMenu: m];
  51. startTimer (1);
  52. }
  53. }
  54. //==============================================================================
  55. void timerCallback() override
  56. {
  57. stopTimer();
  58. setHighlighted (false);
  59. }
  60. //==============================================================================
  61. SystemTrayIconComponent& owner;
  62. std::unique_ptr<NSStatusItem, NSObjectDeleter> statusItem;
  63. std::unique_ptr<NSImage, NSObjectDeleter> statusIcon;
  64. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (StatusItemContainer)
  65. };
  66. //==============================================================================
  67. struct ButtonBasedStatusItem : public StatusItemContainer
  68. {
  69. //==============================================================================
  70. ButtonBasedStatusItem (SystemTrayIconComponent& iconComp, const Image& im)
  71. : StatusItemContainer (iconComp, im)
  72. {
  73. static ButtonEventForwarderClass cls;
  74. eventForwarder.reset ([cls.createInstance() init]);
  75. ButtonEventForwarderClass::setOwner (eventForwarder.get(), this);
  76. setIconSize();
  77. configureIcon();
  78. statusItem.reset ([[[NSStatusBar systemStatusBar] statusItemWithLength: NSSquareStatusItemLength] retain]);
  79. auto button = [statusItem.get() button];
  80. button.image = statusIcon.get();
  81. button.target = eventForwarder.get();
  82. button.action = @selector (handleEvent:);
  83. #if defined (MAC_OS_X_VERSION_10_12) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_12
  84. [button sendActionOn: NSEventMaskLeftMouseDown | NSEventMaskRightMouseDown | NSEventMaskScrollWheel];
  85. #else
  86. [button sendActionOn: NSLeftMouseDownMask | NSRightMouseDownMask | NSScrollWheelMask];
  87. #endif
  88. }
  89. void configureIcon() override
  90. {
  91. [statusIcon.get() setTemplate: true];
  92. [statusItem.get() button].image = statusIcon.get();
  93. }
  94. void setHighlighted (bool shouldHighlight) override
  95. {
  96. [[statusItem.get() button] setHighlighted: shouldHighlight];
  97. }
  98. //==============================================================================
  99. void handleEvent()
  100. {
  101. auto e = [NSApp currentEvent];
  102. NSEventType type = [e type];
  103. const bool isLeft = (type == NSEventTypeLeftMouseDown);
  104. const bool isRight = (type == NSEventTypeRightMouseDown);
  105. if (owner.isCurrentlyBlockedByAnotherModalComponent())
  106. {
  107. if (isLeft || isRight)
  108. if (auto* current = Component::getCurrentlyModalComponent())
  109. current->inputAttemptWhenModal();
  110. }
  111. else
  112. {
  113. auto eventMods = ComponentPeer::getCurrentModifiersRealtime();
  114. if (([e modifierFlags] & NSEventModifierFlagCommand) != 0)
  115. eventMods = eventMods.withFlags (ModifierKeys::commandModifier);
  116. auto now = Time::getCurrentTime();
  117. auto mouseSource = Desktop::getInstance().getMainMouseSource();
  118. auto pressure = (float) e.pressure;
  119. if (isLeft || isRight)
  120. {
  121. owner.mouseDown ({ mouseSource, {},
  122. eventMods.withFlags (isLeft ? ModifierKeys::leftButtonModifier
  123. : ModifierKeys::rightButtonModifier),
  124. pressure,
  125. MouseInputSource::invalidOrientation, MouseInputSource::invalidRotation,
  126. MouseInputSource::invalidTiltX, MouseInputSource::invalidTiltY,
  127. &owner, &owner, now, {}, now, 1, false });
  128. owner.mouseUp ({ mouseSource, {},
  129. eventMods.withoutMouseButtons(),
  130. pressure,
  131. MouseInputSource::invalidOrientation, MouseInputSource::invalidRotation,
  132. MouseInputSource::invalidTiltX, MouseInputSource::invalidTiltY,
  133. &owner, &owner, now, {}, now, 1, false });
  134. }
  135. else if (type == NSEventTypeMouseMoved)
  136. {
  137. owner.mouseMove (MouseEvent (mouseSource, {}, eventMods, pressure,
  138. MouseInputSource::invalidOrientation, MouseInputSource::invalidRotation,
  139. MouseInputSource::invalidTiltX, MouseInputSource::invalidTiltY,
  140. &owner, &owner, now, {}, now, 1, false));
  141. }
  142. }
  143. }
  144. //==============================================================================
  145. class ButtonEventForwarderClass : public ObjCClass<NSObject>
  146. {
  147. public:
  148. ButtonEventForwarderClass() : ObjCClass<NSObject> ("JUCEButtonEventForwarderClass_")
  149. {
  150. addIvar<ButtonBasedStatusItem*> ("owner");
  151. addMethod (@selector (handleEvent:), handleEvent, "v@:@");
  152. registerClass();
  153. }
  154. static ButtonBasedStatusItem* getOwner (id self) { return getIvar<ButtonBasedStatusItem*> (self, "owner"); }
  155. static void setOwner (id self, ButtonBasedStatusItem* owner) { object_setInstanceVariable (self, "owner", owner); }
  156. private:
  157. static void handleEvent (id self, SEL, id)
  158. {
  159. if (auto* owner = getOwner (self))
  160. owner->handleEvent();
  161. }
  162. };
  163. //==============================================================================
  164. std::unique_ptr<NSObject, NSObjectDeleter> eventForwarder;
  165. };
  166. //==============================================================================
  167. struct ViewBasedStatusItem : public StatusItemContainer
  168. {
  169. //==============================================================================
  170. ViewBasedStatusItem (SystemTrayIconComponent& iconComp, const Image& im)
  171. : StatusItemContainer (iconComp, im)
  172. {
  173. static SystemTrayViewClass cls;
  174. view.reset ([cls.createInstance() init]);
  175. SystemTrayViewClass::setOwner (view.get(), this);
  176. SystemTrayViewClass::setImage (view.get(), statusIcon.get());
  177. setIconSize();
  178. statusItem.reset ([[[NSStatusBar systemStatusBar] statusItemWithLength: NSSquareStatusItemLength] retain]);
  179. [statusItem.get() setView: view.get()];
  180. SystemTrayViewClass::frameChanged (view.get(), SEL(), nullptr);
  181. [[NSNotificationCenter defaultCenter] addObserver: view.get()
  182. selector: @selector (frameChanged:)
  183. name: NSWindowDidMoveNotification
  184. object: nil];
  185. }
  186. ~ViewBasedStatusItem() override
  187. {
  188. [[NSNotificationCenter defaultCenter] removeObserver: view.get()];
  189. [[NSStatusBar systemStatusBar] removeStatusItem: statusItem.get()];
  190. SystemTrayViewClass::setOwner (view.get(), nullptr);
  191. SystemTrayViewClass::setImage (view.get(), nil);
  192. }
  193. void configureIcon() override
  194. {
  195. SystemTrayViewClass::setImage (view.get(), statusIcon.get());
  196. [statusItem.get() setView: view.get()];
  197. }
  198. void setHighlighted (bool shouldHighlight) override
  199. {
  200. isHighlighted = shouldHighlight;
  201. [view.get() setNeedsDisplay: true];
  202. }
  203. //==============================================================================
  204. void handleStatusItemAction (NSEvent* e)
  205. {
  206. NSEventType type = [e type];
  207. const bool isLeft = (type == NSEventTypeLeftMouseDown || type == NSEventTypeLeftMouseUp);
  208. const bool isRight = (type == NSEventTypeRightMouseDown || type == NSEventTypeRightMouseUp);
  209. if (owner.isCurrentlyBlockedByAnotherModalComponent())
  210. {
  211. if (isLeft || isRight)
  212. if (auto* current = Component::getCurrentlyModalComponent())
  213. current->inputAttemptWhenModal();
  214. }
  215. else
  216. {
  217. auto eventMods = ComponentPeer::getCurrentModifiersRealtime();
  218. if (([e modifierFlags] & NSEventModifierFlagCommand) != 0)
  219. eventMods = eventMods.withFlags (ModifierKeys::commandModifier);
  220. auto now = Time::getCurrentTime();
  221. auto mouseSource = Desktop::getInstance().getMainMouseSource();
  222. auto pressure = (float) e.pressure;
  223. if (isLeft || isRight) // Only mouse up is sent by the OS, so simulate a down/up
  224. {
  225. setHighlighted (true);
  226. startTimer (150);
  227. owner.mouseDown (MouseEvent (mouseSource, {},
  228. eventMods.withFlags (isLeft ? ModifierKeys::leftButtonModifier
  229. : ModifierKeys::rightButtonModifier),
  230. pressure, MouseInputSource::invalidOrientation, MouseInputSource::invalidRotation,
  231. MouseInputSource::invalidTiltX, MouseInputSource::invalidTiltY,
  232. &owner, &owner, now, {}, now, 1, false));
  233. owner.mouseUp (MouseEvent (mouseSource, {}, eventMods.withoutMouseButtons(), pressure,
  234. MouseInputSource::invalidOrientation, MouseInputSource::invalidRotation,
  235. MouseInputSource::invalidTiltX, MouseInputSource::invalidTiltY,
  236. &owner, &owner, now, {}, now, 1, false));
  237. }
  238. else if (type == NSEventTypeMouseMoved)
  239. {
  240. owner.mouseMove (MouseEvent (mouseSource, {}, eventMods, pressure,
  241. MouseInputSource::invalidOrientation, MouseInputSource::invalidRotation,
  242. MouseInputSource::invalidTiltX, MouseInputSource::invalidTiltY,
  243. &owner, &owner, now, {}, now, 1, false));
  244. }
  245. }
  246. }
  247. //==============================================================================
  248. struct SystemTrayViewClass : public ObjCClass<NSControl>
  249. {
  250. SystemTrayViewClass() : ObjCClass<NSControl> ("JUCESystemTrayView_")
  251. {
  252. addIvar<ViewBasedStatusItem*> ("owner");
  253. addIvar<NSImage*> ("image");
  254. addMethod (@selector (mouseDown:), handleEventDown, "v@:@");
  255. addMethod (@selector (rightMouseDown:), handleEventDown, "v@:@");
  256. addMethod (@selector (drawRect:), drawRect, "v@:@");
  257. addMethod (@selector (frameChanged:), frameChanged, "v@:@");
  258. registerClass();
  259. }
  260. static ViewBasedStatusItem* getOwner (id self) { return getIvar<ViewBasedStatusItem*> (self, "owner"); }
  261. static NSImage* getImage (id self) { return getIvar<NSImage*> (self, "image"); }
  262. static void setOwner (id self, ViewBasedStatusItem* owner) { object_setInstanceVariable (self, "owner", owner); }
  263. static void setImage (id self, NSImage* image) { object_setInstanceVariable (self, "image", image); }
  264. static void frameChanged (id self, SEL, NSNotification*)
  265. {
  266. if (auto* owner = getOwner (self))
  267. {
  268. NSRect r = [[[owner->statusItem.get() view] window] frame];
  269. NSRect sr = [[[NSScreen screens] objectAtIndex: 0] frame];
  270. r.origin.y = sr.size.height - r.origin.y - r.size.height;
  271. owner->owner.setBounds (convertToRectInt (r));
  272. }
  273. }
  274. private:
  275. static void handleEventDown (id self, SEL, NSEvent* e)
  276. {
  277. if (auto* owner = getOwner (self))
  278. owner->handleStatusItemAction (e);
  279. }
  280. static void drawRect (id self, SEL, NSRect)
  281. {
  282. NSRect bounds = [self bounds];
  283. if (auto* owner = getOwner (self))
  284. [owner->statusItem.get() drawStatusBarBackgroundInRect: bounds
  285. withHighlight: owner->isHighlighted];
  286. if (NSImage* const im = getImage (self))
  287. {
  288. NSSize imageSize = [im size];
  289. [im drawInRect: NSMakeRect (bounds.origin.x + ((bounds.size.width - imageSize.width) / 2.0f),
  290. bounds.origin.y + ((bounds.size.height - imageSize.height) / 2.0f),
  291. imageSize.width, imageSize.height)
  292. fromRect: NSZeroRect
  293. operation: NSCompositingOperationSourceOver
  294. fraction: 1.0f];
  295. }
  296. }
  297. };
  298. //==============================================================================
  299. std::unique_ptr<NSControl, NSObjectDeleter> view;
  300. bool isHighlighted = false;
  301. };
  302. //==============================================================================
  303. class SystemTrayIconComponent::Pimpl
  304. {
  305. public:
  306. //==============================================================================
  307. Pimpl (SystemTrayIconComponent& iconComp, const Image& im)
  308. {
  309. if (std::floor (NSFoundationVersionNumber) > NSFoundationVersionNumber10_10)
  310. statusItemHolder = std::make_unique<ButtonBasedStatusItem> (iconComp, im);
  311. else
  312. statusItemHolder = std::make_unique<ViewBasedStatusItem> (iconComp, im);
  313. }
  314. //==============================================================================
  315. std::unique_ptr<StatusItemContainer> statusItemHolder;
  316. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl)
  317. };
  318. //==============================================================================
  319. void SystemTrayIconComponent::setIconImage (const Image&, const Image& templateImage)
  320. {
  321. if (templateImage.isValid())
  322. {
  323. if (pimpl == nullptr)
  324. pimpl.reset (new Pimpl (*this, templateImage));
  325. else
  326. pimpl->statusItemHolder->updateIcon (templateImage);
  327. }
  328. else
  329. {
  330. pimpl.reset();
  331. }
  332. }
  333. void SystemTrayIconComponent::setIconTooltip (const String&)
  334. {
  335. // xxx not yet implemented!
  336. }
  337. void SystemTrayIconComponent::setHighlighted (bool shouldHighlight)
  338. {
  339. if (pimpl != nullptr)
  340. pimpl->statusItemHolder->setHighlighted (shouldHighlight);
  341. }
  342. void SystemTrayIconComponent::showInfoBubble (const String& /*title*/, const String& /*content*/)
  343. {
  344. // xxx Not implemented!
  345. }
  346. void SystemTrayIconComponent::hideInfoBubble()
  347. {
  348. // xxx Not implemented!
  349. }
  350. void* SystemTrayIconComponent::getNativeHandle() const
  351. {
  352. return pimpl != nullptr ? pimpl->statusItemHolder->statusItem.get() : nullptr;
  353. }
  354. void SystemTrayIconComponent::showDropdownMenu (const PopupMenu& menu)
  355. {
  356. if (pimpl != nullptr)
  357. pimpl->statusItemHolder->showMenu (menu);
  358. }
  359. #pragma clang diagnostic pop
  360. } // namespace juce