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.

443 lines
17KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE 7 technical preview.
  4. Copyright (c) 2022 - Raw Material Software Limited
  5. You may use this code under the terms of the GPL v3
  6. (see www.gnu.org/licenses).
  7. For the technical preview this file cannot be licensed commercially.
  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. JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations")
  16. extern NSMenu* createNSMenu (const PopupMenu&, const String& name, int topLevelMenuId,
  17. int topLevelIndex, bool addDelegate);
  18. //==============================================================================
  19. struct StatusItemContainer : public Timer
  20. {
  21. //==============================================================================
  22. StatusItemContainer (SystemTrayIconComponent& iconComp, const Image& im)
  23. : owner (iconComp), statusIcon (imageToNSImage (ScaledImage (im)))
  24. {
  25. }
  26. virtual void configureIcon() = 0;
  27. virtual void setHighlighted (bool shouldHighlight) = 0;
  28. //==============================================================================
  29. void setIconSize()
  30. {
  31. [statusIcon.get() setSize: NSMakeSize (20.0f, 20.0f)];
  32. }
  33. void updateIcon (const Image& newImage)
  34. {
  35. statusIcon.reset (imageToNSImage (ScaledImage (newImage)));
  36. setIconSize();
  37. configureIcon();
  38. }
  39. void showMenu (const PopupMenu& menu)
  40. {
  41. if (NSMenu* m = createNSMenu (menu, "MenuBarItem", -2, -3, true))
  42. {
  43. setHighlighted (true);
  44. stopTimer();
  45. // There's currently no good alternative to this.
  46. [statusItem.get() popUpStatusItemMenu: m];
  47. startTimer (1);
  48. }
  49. }
  50. //==============================================================================
  51. void timerCallback() override
  52. {
  53. stopTimer();
  54. setHighlighted (false);
  55. }
  56. //==============================================================================
  57. SystemTrayIconComponent& owner;
  58. NSUniquePtr<NSStatusItem> statusItem;
  59. NSUniquePtr<NSImage> statusIcon;
  60. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (StatusItemContainer)
  61. };
  62. //==============================================================================
  63. struct API_AVAILABLE (macos (10.10)) ButtonBasedStatusItem : public StatusItemContainer
  64. {
  65. //==============================================================================
  66. ButtonBasedStatusItem (SystemTrayIconComponent& iconComp, const Image& im)
  67. : StatusItemContainer (iconComp, im)
  68. {
  69. static ButtonEventForwarderClass cls;
  70. eventForwarder.reset ([cls.createInstance() init]);
  71. ButtonEventForwarderClass::setOwner (eventForwarder.get(), this);
  72. setIconSize();
  73. configureIcon();
  74. statusItem.reset ([[[NSStatusBar systemStatusBar] statusItemWithLength: NSSquareStatusItemLength] retain]);
  75. auto button = [statusItem.get() button];
  76. button.image = statusIcon.get();
  77. button.target = eventForwarder.get();
  78. button.action = @selector (handleEvent:);
  79. #if defined (MAC_OS_X_VERSION_10_12) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_12
  80. [button sendActionOn: NSEventMaskLeftMouseDown | NSEventMaskRightMouseDown | NSEventMaskScrollWheel];
  81. #else
  82. [button sendActionOn: NSLeftMouseDownMask | NSRightMouseDownMask | NSScrollWheelMask];
  83. #endif
  84. }
  85. ~ButtonBasedStatusItem() override
  86. {
  87. [statusItem.get() button].image = nullptr;
  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::defaultOrientation, MouseInputSource::defaultRotation,
  126. MouseInputSource::defaultTiltX, MouseInputSource::defaultTiltY,
  127. &owner, &owner, now, {}, now, 1, false });
  128. owner.mouseUp ({ mouseSource, {},
  129. eventMods.withoutMouseButtons(),
  130. pressure,
  131. MouseInputSource::defaultOrientation, MouseInputSource::defaultRotation,
  132. MouseInputSource::defaultTiltX, MouseInputSource::defaultTiltY,
  133. &owner, &owner, now, {}, now, 1, false });
  134. }
  135. else if (type == NSEventTypeMouseMoved)
  136. {
  137. owner.mouseMove (MouseEvent (mouseSource, {}, eventMods, pressure,
  138. MouseInputSource::defaultOrientation, MouseInputSource::defaultRotation,
  139. MouseInputSource::defaultTiltX, MouseInputSource::defaultTiltY,
  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);
  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. NSUniquePtr<NSObject> 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. JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector")
  182. [[NSNotificationCenter defaultCenter] addObserver: view.get()
  183. selector: @selector (frameChanged:)
  184. name: NSWindowDidMoveNotification
  185. object: nil];
  186. JUCE_END_IGNORE_WARNINGS_GCC_LIKE
  187. }
  188. ~ViewBasedStatusItem() override
  189. {
  190. [[NSNotificationCenter defaultCenter] removeObserver: view.get()];
  191. [[NSStatusBar systemStatusBar] removeStatusItem: statusItem.get()];
  192. SystemTrayViewClass::setOwner (view.get(), nullptr);
  193. SystemTrayViewClass::setImage (view.get(), nil);
  194. }
  195. void configureIcon() override
  196. {
  197. SystemTrayViewClass::setImage (view.get(), statusIcon.get());
  198. [statusItem.get() setView: view.get()];
  199. }
  200. void setHighlighted (bool shouldHighlight) override
  201. {
  202. isHighlighted = shouldHighlight;
  203. [view.get() setNeedsDisplay: true];
  204. }
  205. //==============================================================================
  206. void handleStatusItemAction (NSEvent* e)
  207. {
  208. NSEventType type = [e type];
  209. const bool isLeft = (type == NSEventTypeLeftMouseDown || type == NSEventTypeLeftMouseUp);
  210. const bool isRight = (type == NSEventTypeRightMouseDown || type == NSEventTypeRightMouseUp);
  211. if (owner.isCurrentlyBlockedByAnotherModalComponent())
  212. {
  213. if (isLeft || isRight)
  214. if (auto* current = Component::getCurrentlyModalComponent())
  215. current->inputAttemptWhenModal();
  216. }
  217. else
  218. {
  219. auto eventMods = ComponentPeer::getCurrentModifiersRealtime();
  220. if (([e modifierFlags] & NSEventModifierFlagCommand) != 0)
  221. eventMods = eventMods.withFlags (ModifierKeys::commandModifier);
  222. auto now = Time::getCurrentTime();
  223. auto mouseSource = Desktop::getInstance().getMainMouseSource();
  224. auto pressure = (float) e.pressure;
  225. if (isLeft || isRight) // Only mouse up is sent by the OS, so simulate a down/up
  226. {
  227. setHighlighted (true);
  228. startTimer (150);
  229. owner.mouseDown (MouseEvent (mouseSource, {},
  230. eventMods.withFlags (isLeft ? ModifierKeys::leftButtonModifier
  231. : ModifierKeys::rightButtonModifier),
  232. pressure, MouseInputSource::defaultOrientation, MouseInputSource::defaultRotation,
  233. MouseInputSource::defaultTiltX, MouseInputSource::defaultTiltY,
  234. &owner, &owner, now, {}, now, 1, false));
  235. owner.mouseUp (MouseEvent (mouseSource, {}, eventMods.withoutMouseButtons(), pressure,
  236. MouseInputSource::defaultOrientation, MouseInputSource::defaultRotation,
  237. MouseInputSource::defaultTiltX, MouseInputSource::defaultTiltY,
  238. &owner, &owner, now, {}, now, 1, false));
  239. }
  240. else if (type == NSEventTypeMouseMoved)
  241. {
  242. owner.mouseMove (MouseEvent (mouseSource, {}, eventMods, pressure,
  243. MouseInputSource::defaultOrientation, MouseInputSource::defaultRotation,
  244. MouseInputSource::defaultTiltX, MouseInputSource::defaultTiltY,
  245. &owner, &owner, now, {}, now, 1, false));
  246. }
  247. }
  248. }
  249. //==============================================================================
  250. struct SystemTrayViewClass : public ObjCClass<NSControl>
  251. {
  252. SystemTrayViewClass() : ObjCClass<NSControl> ("JUCESystemTrayView_")
  253. {
  254. addIvar<ViewBasedStatusItem*> ("owner");
  255. addIvar<NSImage*> ("image");
  256. addMethod (@selector (mouseDown:), handleEventDown);
  257. addMethod (@selector (rightMouseDown:), handleEventDown);
  258. addMethod (@selector (drawRect:), drawRect);
  259. JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector")
  260. addMethod (@selector (frameChanged:), frameChanged);
  261. JUCE_END_IGNORE_WARNINGS_GCC_LIKE
  262. registerClass();
  263. }
  264. static ViewBasedStatusItem* getOwner (id self) { return getIvar<ViewBasedStatusItem*> (self, "owner"); }
  265. static NSImage* getImage (id self) { return getIvar<NSImage*> (self, "image"); }
  266. static void setOwner (id self, ViewBasedStatusItem* owner) { object_setInstanceVariable (self, "owner", owner); }
  267. static void setImage (id self, NSImage* image) { object_setInstanceVariable (self, "image", image); }
  268. static void frameChanged (id self, SEL, NSNotification*)
  269. {
  270. if (auto* owner = getOwner (self))
  271. {
  272. NSRect r = [[[owner->statusItem.get() view] window] frame];
  273. NSRect sr = [[[NSScreen screens] objectAtIndex: 0] frame];
  274. r.origin.y = sr.size.height - r.origin.y - r.size.height;
  275. owner->owner.setBounds (convertToRectInt (r));
  276. }
  277. }
  278. private:
  279. static void handleEventDown (id self, SEL, NSEvent* e)
  280. {
  281. if (auto* owner = getOwner (self))
  282. owner->handleStatusItemAction (e);
  283. }
  284. static void drawRect (id self, SEL, NSRect)
  285. {
  286. NSRect bounds = [self bounds];
  287. if (auto* owner = getOwner (self))
  288. [owner->statusItem.get() drawStatusBarBackgroundInRect: bounds
  289. withHighlight: owner->isHighlighted];
  290. if (NSImage* const im = getImage (self))
  291. {
  292. NSSize imageSize = [im size];
  293. [im drawInRect: NSMakeRect (bounds.origin.x + ((bounds.size.width - imageSize.width) / 2.0f),
  294. bounds.origin.y + ((bounds.size.height - imageSize.height) / 2.0f),
  295. imageSize.width, imageSize.height)
  296. fromRect: NSZeroRect
  297. operation: NSCompositingOperationSourceOver
  298. fraction: 1.0f];
  299. }
  300. }
  301. };
  302. //==============================================================================
  303. NSUniquePtr<NSControl> view;
  304. bool isHighlighted = false;
  305. };
  306. //==============================================================================
  307. class SystemTrayIconComponent::Pimpl
  308. {
  309. public:
  310. //==============================================================================
  311. Pimpl (SystemTrayIconComponent& iconComp, const Image& im)
  312. {
  313. if (@available (macOS 10.10, *))
  314. statusItemHolder = std::make_unique<ButtonBasedStatusItem> (iconComp, im);
  315. else
  316. statusItemHolder = std::make_unique<ViewBasedStatusItem> (iconComp, im);
  317. }
  318. //==============================================================================
  319. std::unique_ptr<StatusItemContainer> statusItemHolder;
  320. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl)
  321. };
  322. //==============================================================================
  323. void SystemTrayIconComponent::setIconImage (const Image&, const Image& templateImage)
  324. {
  325. if (templateImage.isValid())
  326. {
  327. if (pimpl == nullptr)
  328. pimpl.reset (new Pimpl (*this, templateImage));
  329. else
  330. pimpl->statusItemHolder->updateIcon (templateImage);
  331. }
  332. else
  333. {
  334. pimpl.reset();
  335. }
  336. }
  337. void SystemTrayIconComponent::setIconTooltip (const String&)
  338. {
  339. // xxx not yet implemented!
  340. }
  341. void SystemTrayIconComponent::setHighlighted (bool shouldHighlight)
  342. {
  343. if (pimpl != nullptr)
  344. pimpl->statusItemHolder->setHighlighted (shouldHighlight);
  345. }
  346. void SystemTrayIconComponent::showInfoBubble (const String& /*title*/, const String& /*content*/)
  347. {
  348. // xxx Not implemented!
  349. }
  350. void SystemTrayIconComponent::hideInfoBubble()
  351. {
  352. // xxx Not implemented!
  353. }
  354. void* SystemTrayIconComponent::getNativeHandle() const
  355. {
  356. return pimpl != nullptr ? pimpl->statusItemHolder->statusItem.get() : nullptr;
  357. }
  358. void SystemTrayIconComponent::showDropdownMenu (const PopupMenu& menu)
  359. {
  360. if (pimpl != nullptr)
  361. pimpl->statusItemHolder->showMenu (menu);
  362. }
  363. JUCE_END_IGNORE_WARNINGS_GCC_LIKE
  364. } // namespace juce