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.

464 lines
13KB

  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. class MenuBarComponent::AccessibleItemComponent final : public Component
  21. {
  22. public:
  23. AccessibleItemComponent (MenuBarComponent& comp, const String& menuItemName)
  24. : owner (comp),
  25. name (menuItemName)
  26. {
  27. setInterceptsMouseClicks (false, false);
  28. }
  29. const String& getName() const noexcept { return name; }
  30. private:
  31. std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override
  32. {
  33. class ComponentHandler final : public AccessibilityHandler
  34. {
  35. public:
  36. explicit ComponentHandler (AccessibleItemComponent& item)
  37. : AccessibilityHandler (item,
  38. AccessibilityRole::menuItem,
  39. getAccessibilityActions (item)),
  40. itemComponent (item)
  41. {
  42. }
  43. String getTitle() const override { return itemComponent.name; }
  44. private:
  45. static AccessibilityActions getAccessibilityActions (AccessibleItemComponent& item)
  46. {
  47. auto showMenu = [&item] { item.owner.showMenu (item.owner.indexOfItemComponent (&item)); };
  48. return AccessibilityActions().addAction (AccessibilityActionType::focus,
  49. [&item] { item.owner.setItemUnderMouse (item.owner.indexOfItemComponent (&item)); })
  50. .addAction (AccessibilityActionType::press, showMenu)
  51. .addAction (AccessibilityActionType::showMenu, showMenu);
  52. }
  53. AccessibleItemComponent& itemComponent;
  54. };
  55. return std::make_unique<ComponentHandler> (*this);
  56. }
  57. MenuBarComponent& owner;
  58. const String name;
  59. };
  60. MenuBarComponent::MenuBarComponent (MenuBarModel* m)
  61. {
  62. setRepaintsOnMouseActivity (true);
  63. setWantsKeyboardFocus (false);
  64. setMouseClickGrabsKeyboardFocus (false);
  65. setModel (m);
  66. }
  67. MenuBarComponent::~MenuBarComponent()
  68. {
  69. setModel (nullptr);
  70. Desktop::getInstance().removeGlobalMouseListener (this);
  71. }
  72. MenuBarModel* MenuBarComponent::getModel() const noexcept
  73. {
  74. return model;
  75. }
  76. void MenuBarComponent::setModel (MenuBarModel* const newModel)
  77. {
  78. if (model != newModel)
  79. {
  80. if (model != nullptr)
  81. model->removeListener (this);
  82. model = newModel;
  83. if (model != nullptr)
  84. model->addListener (this);
  85. repaint();
  86. menuBarItemsChanged (nullptr);
  87. }
  88. }
  89. //==============================================================================
  90. void MenuBarComponent::paint (Graphics& g)
  91. {
  92. const auto isMouseOverBar = (currentPopupIndex >= 0 || itemUnderMouse >= 0 || isMouseOver());
  93. getLookAndFeel().drawMenuBarBackground (g, getWidth(), getHeight(), isMouseOverBar, *this);
  94. if (model == nullptr)
  95. return;
  96. for (size_t i = 0; i < itemComponents.size(); ++i)
  97. {
  98. const auto& itemComponent = itemComponents[i];
  99. const auto itemBounds = itemComponent->getBounds();
  100. Graphics::ScopedSaveState ss (g);
  101. g.setOrigin (itemBounds.getX(), 0);
  102. g.reduceClipRegion (0, 0, itemBounds.getWidth(), itemBounds.getHeight());
  103. getLookAndFeel().drawMenuBarItem (g,
  104. itemBounds.getWidth(),
  105. itemBounds.getHeight(),
  106. (int) i,
  107. itemComponent->getName(),
  108. (int) i == itemUnderMouse,
  109. (int) i == currentPopupIndex,
  110. isMouseOverBar,
  111. *this);
  112. }
  113. }
  114. void MenuBarComponent::resized()
  115. {
  116. int x = 0;
  117. for (size_t i = 0; i < itemComponents.size(); ++i)
  118. {
  119. auto& itemComponent = itemComponents[i];
  120. auto w = getLookAndFeel().getMenuBarItemWidth (*this, (int) i, itemComponent->getName());
  121. itemComponent->setBounds (x, 0, w, getHeight());
  122. x += w;
  123. }
  124. }
  125. int MenuBarComponent::getItemAt (Point<int> p)
  126. {
  127. for (size_t i = 0; i < itemComponents.size(); ++i)
  128. if (itemComponents[i]->getBounds().contains (p) && reallyContains (p, true))
  129. return (int) i;
  130. return -1;
  131. }
  132. void MenuBarComponent::repaintMenuItem (int index)
  133. {
  134. if (isPositiveAndBelow (index, (int) itemComponents.size()))
  135. {
  136. auto itemBounds = itemComponents[(size_t) index]->getBounds();
  137. repaint (itemBounds.getX() - 2,
  138. 0,
  139. itemBounds.getWidth() + 4,
  140. itemBounds.getHeight());
  141. }
  142. }
  143. void MenuBarComponent::setItemUnderMouse (int index)
  144. {
  145. if (itemUnderMouse == index)
  146. return;
  147. repaintMenuItem (itemUnderMouse);
  148. itemUnderMouse = index;
  149. repaintMenuItem (itemUnderMouse);
  150. if (isPositiveAndBelow (itemUnderMouse, (int) itemComponents.size()))
  151. if (auto* handler = itemComponents[(size_t) itemUnderMouse]->getAccessibilityHandler())
  152. handler->grabFocus();
  153. }
  154. void MenuBarComponent::setOpenItem (int index)
  155. {
  156. if (currentPopupIndex != index)
  157. {
  158. if (currentPopupIndex < 0 && index >= 0)
  159. model->handleMenuBarActivate (true);
  160. else if (currentPopupIndex >= 0 && index < 0)
  161. model->handleMenuBarActivate (false);
  162. repaintMenuItem (currentPopupIndex);
  163. currentPopupIndex = index;
  164. repaintMenuItem (currentPopupIndex);
  165. auto& desktop = Desktop::getInstance();
  166. if (index >= 0)
  167. desktop.addGlobalMouseListener (this);
  168. else
  169. desktop.removeGlobalMouseListener (this);
  170. }
  171. }
  172. void MenuBarComponent::updateItemUnderMouse (Point<int> p)
  173. {
  174. setItemUnderMouse (getItemAt (p));
  175. }
  176. void MenuBarComponent::showMenu (int index)
  177. {
  178. if (index == currentPopupIndex)
  179. return;
  180. const auto needToOpenNewSubMenu = isPositiveAndBelow (index, (int) itemComponents.size());
  181. if (needToOpenNewSubMenu)
  182. ++numActiveMenus;
  183. PopupMenu::dismissAllActiveMenus();
  184. menuBarItemsChanged (nullptr);
  185. setOpenItem (index);
  186. setItemUnderMouse (index);
  187. if (needToOpenNewSubMenu)
  188. {
  189. const auto& itemComponent = itemComponents[(size_t) index];
  190. auto m = model->getMenuForIndex (itemUnderMouse, itemComponent->getName());
  191. if (m.lookAndFeel == nullptr)
  192. m.setLookAndFeel (&getLookAndFeel());
  193. auto itemBounds = itemComponent->getBounds();
  194. const auto callback = [ref = SafePointer<MenuBarComponent> (this), index] (int result)
  195. {
  196. if (ref != nullptr)
  197. ref->menuDismissed (index, result);
  198. };
  199. m.showMenuAsync (PopupMenu::Options().withTargetComponent (this)
  200. .withTargetScreenArea (localAreaToGlobal (itemBounds))
  201. .withMinimumWidth (itemBounds.getWidth()),
  202. callback);
  203. }
  204. }
  205. void MenuBarComponent::menuDismissed (int topLevelIndex, int itemId)
  206. {
  207. topLevelIndexDismissed = topLevelIndex;
  208. --numActiveMenus;
  209. postCommandMessage (itemId);
  210. }
  211. void MenuBarComponent::handleCommandMessage (int commandId)
  212. {
  213. updateItemUnderMouse (getMouseXYRelative());
  214. if (numActiveMenus == 0)
  215. setOpenItem (-1);
  216. if (commandId != 0 && model != nullptr)
  217. model->menuItemSelected (commandId, topLevelIndexDismissed);
  218. }
  219. //==============================================================================
  220. void MenuBarComponent::mouseEnter (const MouseEvent& e)
  221. {
  222. if (e.eventComponent == this)
  223. updateItemUnderMouse (e.getPosition());
  224. }
  225. void MenuBarComponent::mouseExit (const MouseEvent& e)
  226. {
  227. if (e.eventComponent == this)
  228. updateItemUnderMouse (e.getPosition());
  229. }
  230. void MenuBarComponent::mouseDown (const MouseEvent& e)
  231. {
  232. if (currentPopupIndex < 0)
  233. {
  234. updateItemUnderMouse (e.getEventRelativeTo (this).getPosition());
  235. currentPopupIndex = -2;
  236. showMenu (itemUnderMouse);
  237. }
  238. }
  239. void MenuBarComponent::mouseDrag (const MouseEvent& e)
  240. {
  241. const auto item = getItemAt (e.getEventRelativeTo (this).getPosition());
  242. if (item >= 0)
  243. showMenu (item);
  244. }
  245. void MenuBarComponent::mouseUp (const MouseEvent& e)
  246. {
  247. const auto e2 = e.getEventRelativeTo (this);
  248. updateItemUnderMouse (e2.getPosition());
  249. if (itemUnderMouse < 0 && getLocalBounds().contains (e2.x, e2.y))
  250. {
  251. setOpenItem (-1);
  252. PopupMenu::dismissAllActiveMenus();
  253. }
  254. }
  255. void MenuBarComponent::mouseMove (const MouseEvent& e)
  256. {
  257. const auto e2 = e.getEventRelativeTo (this);
  258. if (lastMousePos != e2.getPosition())
  259. {
  260. if (currentPopupIndex >= 0)
  261. {
  262. const auto item = getItemAt (e2.getPosition());
  263. if (item >= 0)
  264. showMenu (item);
  265. }
  266. else
  267. {
  268. updateItemUnderMouse (e2.getPosition());
  269. }
  270. lastMousePos = e2.getPosition();
  271. }
  272. }
  273. bool MenuBarComponent::keyPressed (const KeyPress& key)
  274. {
  275. const auto numMenus = (int) itemComponents.size();
  276. if (numMenus > 0)
  277. {
  278. const auto currentIndex = jlimit (0, numMenus - 1, currentPopupIndex);
  279. if (key.isKeyCode (KeyPress::leftKey))
  280. {
  281. showMenu ((currentIndex + numMenus - 1) % numMenus);
  282. return true;
  283. }
  284. if (key.isKeyCode (KeyPress::rightKey))
  285. {
  286. showMenu ((currentIndex + 1) % numMenus);
  287. return true;
  288. }
  289. }
  290. return false;
  291. }
  292. void MenuBarComponent::menuBarItemsChanged (MenuBarModel*)
  293. {
  294. StringArray newNames;
  295. if (model != nullptr)
  296. newNames = model->getMenuBarNames();
  297. auto itemsHaveChanged = [this, &newNames]
  298. {
  299. if ((int) itemComponents.size() != newNames.size())
  300. return true;
  301. for (size_t i = 0; i < itemComponents.size(); ++i)
  302. if (itemComponents[i]->getName() != newNames[(int) i])
  303. return true;
  304. return false;
  305. }();
  306. if (itemsHaveChanged)
  307. {
  308. updateItemComponents (newNames);
  309. repaint();
  310. resized();
  311. }
  312. }
  313. void MenuBarComponent::updateItemComponents (const StringArray& menuNames)
  314. {
  315. itemComponents.clear();
  316. for (const auto& name : menuNames)
  317. {
  318. itemComponents.push_back (std::make_unique<AccessibleItemComponent> (*this, name));
  319. addAndMakeVisible (*itemComponents.back());
  320. }
  321. }
  322. int MenuBarComponent::indexOfItemComponent (AccessibleItemComponent* itemComponent) const
  323. {
  324. const auto iter = std::find_if (itemComponents.cbegin(), itemComponents.cend(),
  325. [itemComponent] (const std::unique_ptr<AccessibleItemComponent>& c) { return c.get() == itemComponent; });
  326. if (iter != itemComponents.cend())
  327. return (int) std::distance (itemComponents.cbegin(), iter);
  328. jassertfalse;
  329. return -1;
  330. }
  331. void MenuBarComponent::menuCommandInvoked (MenuBarModel*, const ApplicationCommandTarget::InvocationInfo& info)
  332. {
  333. if (model == nullptr || (info.commandFlags & ApplicationCommandInfo::dontTriggerVisualFeedback) != 0)
  334. return;
  335. for (size_t i = 0; i < itemComponents.size(); ++i)
  336. {
  337. const auto menu = model->getMenuForIndex ((int) i, itemComponents[i]->getName());
  338. if (menu.containsCommandItem (info.commandID))
  339. {
  340. setItemUnderMouse ((int) i);
  341. startTimer (200);
  342. break;
  343. }
  344. }
  345. }
  346. void MenuBarComponent::timerCallback()
  347. {
  348. stopTimer();
  349. updateItemUnderMouse (getMouseXYRelative());
  350. }
  351. //==============================================================================
  352. std::unique_ptr<AccessibilityHandler> MenuBarComponent::createAccessibilityHandler()
  353. {
  354. struct MenuBarComponentAccessibilityHandler final : public AccessibilityHandler
  355. {
  356. explicit MenuBarComponentAccessibilityHandler (MenuBarComponent& menuBarComponent)
  357. : AccessibilityHandler (menuBarComponent, AccessibilityRole::menuBar)
  358. {
  359. }
  360. AccessibleState getCurrentState() const override { return AccessibleState().withIgnored(); }
  361. };
  362. return std::make_unique<MenuBarComponentAccessibilityHandler> (*this);
  363. }
  364. } // namespace juce