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.

706 lines
20KB

  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. ComboBox::ComboBox (const String& name)
  16. : Component (name),
  17. noChoicesMessage (TRANS("(no choices)"))
  18. {
  19. setRepaintsOnMouseActivity (true);
  20. lookAndFeelChanged();
  21. currentId.addListener (this);
  22. }
  23. ComboBox::~ComboBox()
  24. {
  25. currentId.removeListener (this);
  26. hidePopup();
  27. label.reset();
  28. }
  29. //==============================================================================
  30. void ComboBox::setEditableText (const bool isEditable)
  31. {
  32. if (label->isEditableOnSingleClick() != isEditable || label->isEditableOnDoubleClick() != isEditable)
  33. {
  34. label->setEditable (isEditable, isEditable, false);
  35. labelEditableState = (isEditable ? labelIsEditable : labelIsNotEditable);
  36. const auto isLabelEditable = (labelEditableState == labelIsEditable);
  37. setWantsKeyboardFocus (! isLabelEditable);
  38. label->setAccessible (isLabelEditable);
  39. resized();
  40. }
  41. }
  42. bool ComboBox::isTextEditable() const noexcept
  43. {
  44. return label->isEditable();
  45. }
  46. void ComboBox::setJustificationType (Justification justification)
  47. {
  48. label->setJustificationType (justification);
  49. }
  50. Justification ComboBox::getJustificationType() const noexcept
  51. {
  52. return label->getJustificationType();
  53. }
  54. void ComboBox::setTooltip (const String& newTooltip)
  55. {
  56. SettableTooltipClient::setTooltip (newTooltip);
  57. label->setTooltip (newTooltip);
  58. }
  59. //==============================================================================
  60. void ComboBox::addItem (const String& newItemText, int newItemId)
  61. {
  62. // you can't add empty strings to the list..
  63. jassert (newItemText.isNotEmpty());
  64. // IDs must be non-zero, as zero is used to indicate a lack of selection.
  65. jassert (newItemId != 0);
  66. // you shouldn't use duplicate item IDs!
  67. jassert (getItemForId (newItemId) == nullptr);
  68. if (newItemText.isNotEmpty() && newItemId != 0)
  69. currentMenu.addItem (newItemId, newItemText, true, false);
  70. }
  71. void ComboBox::addItemList (const StringArray& itemsToAdd, int firstItemID)
  72. {
  73. for (auto& i : itemsToAdd)
  74. currentMenu.addItem (firstItemID++, i);
  75. }
  76. void ComboBox::addSeparator()
  77. {
  78. currentMenu.addSeparator();
  79. }
  80. void ComboBox::addSectionHeading (const String& headingName)
  81. {
  82. // you can't add empty strings to the list..
  83. jassert (headingName.isNotEmpty());
  84. if (headingName.isNotEmpty())
  85. currentMenu.addSectionHeader (headingName);
  86. }
  87. void ComboBox::setItemEnabled (int itemId, bool shouldBeEnabled)
  88. {
  89. if (auto* item = getItemForId (itemId))
  90. item->isEnabled = shouldBeEnabled;
  91. }
  92. bool ComboBox::isItemEnabled (int itemId) const noexcept
  93. {
  94. if (auto* item = getItemForId (itemId))
  95. return item->isEnabled;
  96. return false;
  97. }
  98. void ComboBox::changeItemText (int itemId, const String& newText)
  99. {
  100. if (auto* item = getItemForId (itemId))
  101. item->text = newText;
  102. else
  103. jassertfalse;
  104. }
  105. void ComboBox::clear (const NotificationType notification)
  106. {
  107. currentMenu.clear();
  108. if (! label->isEditable())
  109. setSelectedItemIndex (-1, notification);
  110. }
  111. //==============================================================================
  112. PopupMenu::Item* ComboBox::getItemForId (int itemId) const noexcept
  113. {
  114. if (itemId != 0)
  115. {
  116. for (PopupMenu::MenuItemIterator iterator (currentMenu, true); iterator.next();)
  117. {
  118. auto& item = iterator.getItem();
  119. if (item.itemID == itemId)
  120. return &item;
  121. }
  122. }
  123. return nullptr;
  124. }
  125. PopupMenu::Item* ComboBox::getItemForIndex (const int index) const noexcept
  126. {
  127. int n = 0;
  128. for (PopupMenu::MenuItemIterator iterator (currentMenu, true); iterator.next();)
  129. {
  130. auto& item = iterator.getItem();
  131. if (item.itemID != 0)
  132. if (n++ == index)
  133. return &item;
  134. }
  135. return nullptr;
  136. }
  137. int ComboBox::getNumItems() const noexcept
  138. {
  139. int n = 0;
  140. for (PopupMenu::MenuItemIterator iterator (currentMenu, true); iterator.next();)
  141. {
  142. auto& item = iterator.getItem();
  143. if (item.itemID != 0)
  144. n++;
  145. }
  146. return n;
  147. }
  148. String ComboBox::getItemText (const int index) const
  149. {
  150. if (auto* item = getItemForIndex (index))
  151. return item->text;
  152. return {};
  153. }
  154. int ComboBox::getItemId (const int index) const noexcept
  155. {
  156. if (auto* item = getItemForIndex (index))
  157. return item->itemID;
  158. return 0;
  159. }
  160. int ComboBox::indexOfItemId (const int itemId) const noexcept
  161. {
  162. if (itemId != 0)
  163. {
  164. int n = 0;
  165. for (PopupMenu::MenuItemIterator iterator (currentMenu, true); iterator.next();)
  166. {
  167. auto& item = iterator.getItem();
  168. if (item.itemID == itemId)
  169. return n;
  170. else if (item.itemID != 0)
  171. n++;
  172. }
  173. }
  174. return -1;
  175. }
  176. //==============================================================================
  177. int ComboBox::getSelectedItemIndex() const
  178. {
  179. auto index = indexOfItemId (currentId.getValue());
  180. if (getText() != getItemText (index))
  181. index = -1;
  182. return index;
  183. }
  184. void ComboBox::setSelectedItemIndex (const int index, const NotificationType notification)
  185. {
  186. setSelectedId (getItemId (index), notification);
  187. }
  188. int ComboBox::getSelectedId() const noexcept
  189. {
  190. if (auto* item = getItemForId (currentId.getValue()))
  191. if (getText() == item->text)
  192. return item->itemID;
  193. return 0;
  194. }
  195. void ComboBox::setSelectedId (const int newItemId, const NotificationType notification)
  196. {
  197. auto* item = getItemForId (newItemId);
  198. auto newItemText = item != nullptr ? item->text : String();
  199. if (lastCurrentId != newItemId || label->getText() != newItemText)
  200. {
  201. label->setText (newItemText, dontSendNotification);
  202. lastCurrentId = newItemId;
  203. currentId = newItemId;
  204. repaint(); // for the benefit of the 'none selected' text
  205. sendChange (notification);
  206. }
  207. }
  208. bool ComboBox::selectIfEnabled (const int index)
  209. {
  210. if (auto* item = getItemForIndex (index))
  211. {
  212. if (item->isEnabled)
  213. {
  214. setSelectedItemIndex (index);
  215. return true;
  216. }
  217. }
  218. return false;
  219. }
  220. bool ComboBox::nudgeSelectedItem (int delta)
  221. {
  222. for (int i = getSelectedItemIndex() + delta; isPositiveAndBelow (i, getNumItems()); i += delta)
  223. if (selectIfEnabled (i))
  224. return true;
  225. return false;
  226. }
  227. void ComboBox::valueChanged (Value&)
  228. {
  229. if (lastCurrentId != (int) currentId.getValue())
  230. setSelectedId (currentId.getValue());
  231. }
  232. //==============================================================================
  233. String ComboBox::getText() const
  234. {
  235. return label->getText();
  236. }
  237. void ComboBox::setText (const String& newText, const NotificationType notification)
  238. {
  239. for (PopupMenu::MenuItemIterator iterator (currentMenu, true); iterator.next();)
  240. {
  241. auto& item = iterator.getItem();
  242. if (item.itemID != 0
  243. && item.text == newText)
  244. {
  245. setSelectedId (item.itemID, notification);
  246. return;
  247. }
  248. }
  249. lastCurrentId = 0;
  250. currentId = 0;
  251. repaint();
  252. if (label->getText() != newText)
  253. {
  254. label->setText (newText, dontSendNotification);
  255. sendChange (notification);
  256. }
  257. }
  258. void ComboBox::showEditor()
  259. {
  260. jassert (isTextEditable()); // you probably shouldn't do this to a non-editable combo box?
  261. label->showEditor();
  262. }
  263. //==============================================================================
  264. void ComboBox::setTextWhenNothingSelected (const String& newMessage)
  265. {
  266. if (textWhenNothingSelected != newMessage)
  267. {
  268. textWhenNothingSelected = newMessage;
  269. repaint();
  270. }
  271. }
  272. String ComboBox::getTextWhenNothingSelected() const
  273. {
  274. return textWhenNothingSelected;
  275. }
  276. void ComboBox::setTextWhenNoChoicesAvailable (const String& newMessage)
  277. {
  278. noChoicesMessage = newMessage;
  279. }
  280. String ComboBox::getTextWhenNoChoicesAvailable() const
  281. {
  282. return noChoicesMessage;
  283. }
  284. //==============================================================================
  285. void ComboBox::paint (Graphics& g)
  286. {
  287. getLookAndFeel().drawComboBox (g, getWidth(), getHeight(), isButtonDown,
  288. label->getRight(), 0, getWidth() - label->getRight(), getHeight(),
  289. *this);
  290. if (textWhenNothingSelected.isNotEmpty() && label->getText().isEmpty() && ! label->isBeingEdited())
  291. getLookAndFeel().drawComboBoxTextWhenNothingSelected (g, *this, *label);
  292. }
  293. void ComboBox::resized()
  294. {
  295. if (getHeight() > 0 && getWidth() > 0)
  296. getLookAndFeel().positionComboBoxText (*this, *label);
  297. }
  298. void ComboBox::enablementChanged()
  299. {
  300. if (! isEnabled())
  301. hidePopup();
  302. repaint();
  303. }
  304. void ComboBox::colourChanged()
  305. {
  306. lookAndFeelChanged();
  307. }
  308. void ComboBox::parentHierarchyChanged()
  309. {
  310. lookAndFeelChanged();
  311. }
  312. void ComboBox::lookAndFeelChanged()
  313. {
  314. repaint();
  315. {
  316. std::unique_ptr<Label> newLabel (getLookAndFeel().createComboBoxTextBox (*this));
  317. jassert (newLabel != nullptr);
  318. if (label != nullptr)
  319. {
  320. newLabel->setEditable (label->isEditable());
  321. newLabel->setJustificationType (label->getJustificationType());
  322. newLabel->setTooltip (label->getTooltip());
  323. newLabel->setText (label->getText(), dontSendNotification);
  324. }
  325. std::swap (label, newLabel);
  326. }
  327. addAndMakeVisible (label.get());
  328. EditableState newEditableState = (label->isEditable() ? labelIsEditable : labelIsNotEditable);
  329. if (newEditableState != labelEditableState)
  330. {
  331. labelEditableState = newEditableState;
  332. setWantsKeyboardFocus (labelEditableState == labelIsNotEditable);
  333. }
  334. label->onTextChange = [this] { triggerAsyncUpdate(); };
  335. label->addMouseListener (this, false);
  336. label->setAccessible (labelEditableState == labelIsEditable);
  337. label->setColour (Label::backgroundColourId, Colours::transparentBlack);
  338. label->setColour (Label::textColourId, findColour (ComboBox::textColourId));
  339. label->setColour (TextEditor::textColourId, findColour (ComboBox::textColourId));
  340. label->setColour (TextEditor::backgroundColourId, Colours::transparentBlack);
  341. label->setColour (TextEditor::highlightColourId, findColour (TextEditor::highlightColourId));
  342. label->setColour (TextEditor::outlineColourId, Colours::transparentBlack);
  343. resized();
  344. }
  345. //==============================================================================
  346. bool ComboBox::keyPressed (const KeyPress& key)
  347. {
  348. if (key == KeyPress::upKey || key == KeyPress::leftKey)
  349. {
  350. nudgeSelectedItem (-1);
  351. return true;
  352. }
  353. if (key == KeyPress::downKey || key == KeyPress::rightKey)
  354. {
  355. nudgeSelectedItem (1);
  356. return true;
  357. }
  358. if (key == KeyPress::returnKey)
  359. {
  360. showPopupIfNotActive();
  361. return true;
  362. }
  363. return false;
  364. }
  365. bool ComboBox::keyStateChanged (const bool isKeyDown)
  366. {
  367. // only forward key events that aren't used by this component
  368. return isKeyDown
  369. && (KeyPress::isKeyCurrentlyDown (KeyPress::upKey)
  370. || KeyPress::isKeyCurrentlyDown (KeyPress::leftKey)
  371. || KeyPress::isKeyCurrentlyDown (KeyPress::downKey)
  372. || KeyPress::isKeyCurrentlyDown (KeyPress::rightKey));
  373. }
  374. //==============================================================================
  375. void ComboBox::focusGained (FocusChangeType) { repaint(); }
  376. void ComboBox::focusLost (FocusChangeType) { repaint(); }
  377. //==============================================================================
  378. void ComboBox::showPopupIfNotActive()
  379. {
  380. if (! menuActive)
  381. {
  382. menuActive = true;
  383. // as this method was triggered by a mouse event, the same mouse event may have
  384. // exited the modal state of other popups currently on the screen. By calling
  385. // showPopup asynchronously, we are giving the other popups a chance to properly
  386. // close themselves
  387. MessageManager::callAsync ([safePointer = SafePointer<ComboBox> { this }]() mutable { if (safePointer != nullptr) safePointer->showPopup(); });
  388. repaint();
  389. }
  390. }
  391. void ComboBox::hidePopup()
  392. {
  393. if (menuActive)
  394. {
  395. menuActive = false;
  396. PopupMenu::dismissAllActiveMenus();
  397. repaint();
  398. }
  399. }
  400. static void comboBoxPopupMenuFinishedCallback (int result, ComboBox* combo)
  401. {
  402. if (combo != nullptr)
  403. {
  404. combo->hidePopup();
  405. if (result != 0)
  406. combo->setSelectedId (result);
  407. }
  408. }
  409. void ComboBox::showPopup()
  410. {
  411. if (! menuActive)
  412. menuActive = true;
  413. auto menu = currentMenu;
  414. if (menu.getNumItems() > 0)
  415. {
  416. auto selectedId = getSelectedId();
  417. for (PopupMenu::MenuItemIterator iterator (menu, true); iterator.next();)
  418. {
  419. auto& item = iterator.getItem();
  420. if (item.itemID != 0)
  421. item.isTicked = (item.itemID == selectedId);
  422. }
  423. }
  424. else
  425. {
  426. menu.addItem (1, noChoicesMessage, false, false);
  427. }
  428. auto& lf = getLookAndFeel();
  429. menu.setLookAndFeel (&lf);
  430. menu.showMenuAsync (lf.getOptionsForComboBoxPopupMenu (*this, *label),
  431. ModalCallbackFunction::forComponent (comboBoxPopupMenuFinishedCallback, this));
  432. }
  433. //==============================================================================
  434. void ComboBox::mouseDown (const MouseEvent& e)
  435. {
  436. beginDragAutoRepeat (300);
  437. isButtonDown = isEnabled() && ! e.mods.isPopupMenu();
  438. if (isButtonDown && (e.eventComponent == this || ! label->isEditable()))
  439. showPopupIfNotActive();
  440. }
  441. void ComboBox::mouseDrag (const MouseEvent& e)
  442. {
  443. beginDragAutoRepeat (50);
  444. if (isButtonDown && e.mouseWasDraggedSinceMouseDown())
  445. showPopupIfNotActive();
  446. }
  447. void ComboBox::mouseUp (const MouseEvent& e2)
  448. {
  449. if (isButtonDown)
  450. {
  451. isButtonDown = false;
  452. repaint();
  453. auto e = e2.getEventRelativeTo (this);
  454. if (reallyContains (e.getPosition(), true)
  455. && (e2.eventComponent == this || ! label->isEditable()))
  456. {
  457. showPopupIfNotActive();
  458. }
  459. }
  460. }
  461. void ComboBox::mouseWheelMove (const MouseEvent& e, const MouseWheelDetails& wheel)
  462. {
  463. if (! menuActive && scrollWheelEnabled && e.eventComponent == this && wheel.deltaY != 0.0f)
  464. {
  465. mouseWheelAccumulator += wheel.deltaY * 5.0f;
  466. while (mouseWheelAccumulator > 1.0f)
  467. {
  468. mouseWheelAccumulator -= 1.0f;
  469. nudgeSelectedItem (-1);
  470. }
  471. while (mouseWheelAccumulator < -1.0f)
  472. {
  473. mouseWheelAccumulator += 1.0f;
  474. nudgeSelectedItem (1);
  475. }
  476. }
  477. else
  478. {
  479. Component::mouseWheelMove (e, wheel);
  480. }
  481. }
  482. void ComboBox::setScrollWheelEnabled (bool enabled) noexcept
  483. {
  484. scrollWheelEnabled = enabled;
  485. }
  486. //==============================================================================
  487. void ComboBox::addListener (ComboBox::Listener* l) { listeners.add (l); }
  488. void ComboBox::removeListener (ComboBox::Listener* l) { listeners.remove (l); }
  489. void ComboBox::handleAsyncUpdate()
  490. {
  491. Component::BailOutChecker checker (this);
  492. listeners.callChecked (checker, [this] (Listener& l) { l.comboBoxChanged (this); });
  493. if (checker.shouldBailOut())
  494. return;
  495. if (onChange != nullptr)
  496. onChange();
  497. if (auto* handler = getAccessibilityHandler())
  498. handler->notifyAccessibilityEvent (AccessibilityEvent::valueChanged);
  499. }
  500. void ComboBox::sendChange (const NotificationType notification)
  501. {
  502. if (notification != dontSendNotification)
  503. triggerAsyncUpdate();
  504. if (notification == sendNotificationSync)
  505. handleUpdateNowIfNeeded();
  506. }
  507. // Old deprecated methods - remove eventually...
  508. void ComboBox::clear (const bool dontSendChange) { clear (dontSendChange ? dontSendNotification : sendNotification); }
  509. void ComboBox::setSelectedItemIndex (const int index, const bool dontSendChange) { setSelectedItemIndex (index, dontSendChange ? dontSendNotification : sendNotification); }
  510. void ComboBox::setSelectedId (const int newItemId, const bool dontSendChange) { setSelectedId (newItemId, dontSendChange ? dontSendNotification : sendNotification); }
  511. void ComboBox::setText (const String& newText, const bool dontSendChange) { setText (newText, dontSendChange ? dontSendNotification : sendNotification); }
  512. //==============================================================================
  513. class ComboBoxAccessibilityHandler : public AccessibilityHandler
  514. {
  515. public:
  516. explicit ComboBoxAccessibilityHandler (ComboBox& comboBoxToWrap)
  517. : AccessibilityHandler (comboBoxToWrap,
  518. AccessibilityRole::comboBox,
  519. getAccessibilityActions (comboBoxToWrap),
  520. { std::make_unique<ComboBoxValueInterface> (comboBoxToWrap) }),
  521. comboBox (comboBoxToWrap)
  522. {
  523. }
  524. AccessibleState getCurrentState() const override
  525. {
  526. auto state = AccessibilityHandler::getCurrentState().withExpandable();
  527. return comboBox.isPopupActive() ? state.withExpanded() : state.withCollapsed();
  528. }
  529. String getTitle() const override { return comboBox.getText(); }
  530. String getHelp() const override { return comboBox.getTooltip(); }
  531. private:
  532. class ComboBoxValueInterface : public AccessibilityTextValueInterface
  533. {
  534. public:
  535. explicit ComboBoxValueInterface (ComboBox& comboBoxToWrap)
  536. : comboBox (comboBoxToWrap)
  537. {
  538. }
  539. bool isReadOnly() const override { return true; }
  540. String getCurrentValueAsString() const override { return comboBox.getText(); }
  541. void setValueAsString (const String&) override {}
  542. private:
  543. ComboBox& comboBox;
  544. //==============================================================================
  545. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ComboBoxValueInterface)
  546. };
  547. static AccessibilityActions getAccessibilityActions (ComboBox& comboBox)
  548. {
  549. return AccessibilityActions().addAction (AccessibilityActionType::press, [&comboBox] { comboBox.showPopup(); })
  550. .addAction (AccessibilityActionType::showMenu, [&comboBox] { comboBox.showPopup(); });
  551. }
  552. ComboBox& comboBox;
  553. //==============================================================================
  554. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ComboBoxAccessibilityHandler)
  555. };
  556. std::unique_ptr<AccessibilityHandler> ComboBox::createAccessibilityHandler()
  557. {
  558. return std::make_unique<ComboBoxAccessibilityHandler> (*this);
  559. }
  560. } // namespace juce