Audio plugin host https://kx.studio/carla
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.

450 lines
18KB

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