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.

647 lines
18KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2017 - ROLI Ltd.
  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 5 End-User License
  8. Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
  9. 27th April 2017).
  10. End User License Agreement: www.juce.com/juce-5-licence
  11. Privacy Policy: www.juce.com/juce-5-privacy-policy
  12. Or: You may also use this code under the terms of the GPL v3 (see
  13. www.gnu.org/licenses).
  14. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  15. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  16. DISCLAIMED.
  17. ==============================================================================
  18. */
  19. ComboBox::ComboBox (const String& name)
  20. : Component (name),
  21. lastCurrentId (0),
  22. isButtonDown (false),
  23. menuActive (false),
  24. scrollWheelEnabled (false),
  25. mouseWheelAccumulator (0),
  26. noChoicesMessage (TRANS("(no choices)")),
  27. labelEditableState (editableUnknown)
  28. {
  29. setRepaintsOnMouseActivity (true);
  30. lookAndFeelChanged();
  31. currentId.addListener (this);
  32. }
  33. ComboBox::~ComboBox()
  34. {
  35. currentId.removeListener (this);
  36. hidePopup();
  37. label = nullptr;
  38. }
  39. //==============================================================================
  40. void ComboBox::setEditableText (const bool isEditable)
  41. {
  42. if (label->isEditableOnSingleClick() != isEditable || label->isEditableOnDoubleClick() != isEditable)
  43. {
  44. label->setEditable (isEditable, isEditable, false);
  45. labelEditableState = (isEditable ? labelIsEditable : labelIsNotEditable);
  46. setWantsKeyboardFocus (labelEditableState == labelIsNotEditable);
  47. resized();
  48. }
  49. }
  50. bool ComboBox::isTextEditable() const noexcept
  51. {
  52. return label->isEditable();
  53. }
  54. void ComboBox::setJustificationType (Justification justification)
  55. {
  56. label->setJustificationType (justification);
  57. }
  58. Justification ComboBox::getJustificationType() const noexcept
  59. {
  60. return label->getJustificationType();
  61. }
  62. void ComboBox::setTooltip (const String& newTooltip)
  63. {
  64. SettableTooltipClient::setTooltip (newTooltip);
  65. label->setTooltip (newTooltip);
  66. }
  67. //==============================================================================
  68. void ComboBox::addItem (const String& newItemText, const int newItemId)
  69. {
  70. // you can't add empty strings to the list..
  71. jassert (newItemText.isNotEmpty());
  72. // IDs must be non-zero, as zero is used to indicate a lack of selecion.
  73. jassert (newItemId != 0);
  74. // you shouldn't use duplicate item IDs!
  75. jassert (getItemForId (newItemId) == nullptr);
  76. if (newItemText.isNotEmpty() && newItemId != 0)
  77. {
  78. currentMenu.addItem (newItemId, newItemText, true, false);
  79. }
  80. }
  81. void ComboBox::addItemList (const StringArray& itemsToAdd, const int firstItemIdOffset)
  82. {
  83. for (int i = 0; i < itemsToAdd.size(); ++i)
  84. currentMenu.addItem (i + firstItemIdOffset, itemsToAdd[i]);
  85. }
  86. void ComboBox::addSeparator()
  87. {
  88. currentMenu.addSeparator();
  89. }
  90. void ComboBox::addSectionHeading (const String& headingName)
  91. {
  92. // you can't add empty strings to the list..
  93. jassert (headingName.isNotEmpty());
  94. if (headingName.isNotEmpty())
  95. {
  96. currentMenu.addSectionHeader (headingName);
  97. }
  98. }
  99. void ComboBox::setItemEnabled (const int itemId, const bool shouldBeEnabled)
  100. {
  101. if (PopupMenu::Item* item = getItemForId (itemId))
  102. item->isEnabled = shouldBeEnabled;
  103. }
  104. bool ComboBox::isItemEnabled (int itemId) const noexcept
  105. {
  106. const PopupMenu::Item* item = getItemForId (itemId);
  107. return item != nullptr && item->isEnabled;
  108. }
  109. void ComboBox::changeItemText (const int itemId, const String& newText)
  110. {
  111. if (PopupMenu::Item* item = getItemForId (itemId))
  112. item->text = newText;
  113. else
  114. jassertfalse;
  115. }
  116. void ComboBox::clear (const NotificationType notification)
  117. {
  118. currentMenu.clear();
  119. if (! label->isEditable())
  120. setSelectedItemIndex (-1, notification);
  121. }
  122. //==============================================================================
  123. PopupMenu::Item* ComboBox::getItemForId (const int itemId) const noexcept
  124. {
  125. if (itemId != 0)
  126. {
  127. PopupMenu::MenuItemIterator iterator (currentMenu, true);
  128. while (iterator.next())
  129. {
  130. PopupMenu::Item &item = iterator.getItem();
  131. if (item.itemID == itemId)
  132. return &item;
  133. }
  134. }
  135. return nullptr;
  136. }
  137. PopupMenu::Item* ComboBox::getItemForIndex (const int index) const noexcept
  138. {
  139. int n = 0;
  140. PopupMenu::MenuItemIterator iterator (currentMenu, true);
  141. while (iterator.next())
  142. {
  143. PopupMenu::Item &item = iterator.getItem();
  144. if (item.itemID != 0)
  145. if (n++ == index)
  146. return &item;
  147. }
  148. return nullptr;
  149. }
  150. int ComboBox::getNumItems() const noexcept
  151. {
  152. int n = 0;
  153. PopupMenu::MenuItemIterator iterator (currentMenu, true);
  154. while (iterator.next())
  155. {
  156. PopupMenu::Item &item = iterator.getItem();
  157. if (item.itemID != 0)
  158. n++;
  159. }
  160. return n;
  161. }
  162. String ComboBox::getItemText (const int index) const
  163. {
  164. if (auto* item = getItemForIndex (index))
  165. return item->text;
  166. return {};
  167. }
  168. int ComboBox::getItemId (const int index) const noexcept
  169. {
  170. if (auto* item = getItemForIndex (index))
  171. return item->itemID;
  172. return 0;
  173. }
  174. int ComboBox::indexOfItemId (const int itemId) const noexcept
  175. {
  176. if (itemId != 0)
  177. {
  178. int n = 0;
  179. PopupMenu::MenuItemIterator iterator (currentMenu, true);
  180. while (iterator.next())
  181. {
  182. auto& item = iterator.getItem();
  183. if (item.itemID == itemId)
  184. return n;
  185. else if (item.itemID != 0)
  186. n++;
  187. }
  188. }
  189. return -1;
  190. }
  191. //==============================================================================
  192. int ComboBox::getSelectedItemIndex() const
  193. {
  194. int index = indexOfItemId (currentId.getValue());
  195. if (getText() != getItemText (index))
  196. index = -1;
  197. return index;
  198. }
  199. void ComboBox::setSelectedItemIndex (const int index, const NotificationType notification)
  200. {
  201. setSelectedId (getItemId (index), notification);
  202. }
  203. int ComboBox::getSelectedId() const noexcept
  204. {
  205. const PopupMenu::Item* const item = getItemForId (currentId.getValue());
  206. return (item != nullptr && getText() == item->text) ? item->itemID : 0;
  207. }
  208. void ComboBox::setSelectedId (const int newItemId, const NotificationType notification)
  209. {
  210. const PopupMenu::Item* const item = getItemForId (newItemId);
  211. const String newItemText (item != nullptr ? item->text : String());
  212. if (lastCurrentId != newItemId || label->getText() != newItemText)
  213. {
  214. label->setText (newItemText, dontSendNotification);
  215. lastCurrentId = newItemId;
  216. currentId = newItemId;
  217. repaint(); // for the benefit of the 'none selected' text
  218. sendChange (notification);
  219. }
  220. }
  221. bool ComboBox::selectIfEnabled (const int index)
  222. {
  223. if (const PopupMenu::Item* const item = getItemForIndex (index))
  224. {
  225. if (item->isEnabled)
  226. {
  227. setSelectedItemIndex (index);
  228. return true;
  229. }
  230. }
  231. return false;
  232. }
  233. bool ComboBox::nudgeSelectedItem (int delta)
  234. {
  235. for (int i = getSelectedItemIndex() + delta; isPositiveAndBelow (i, getNumItems()); i += delta)
  236. if (selectIfEnabled (i))
  237. return true;
  238. return false;
  239. }
  240. void ComboBox::valueChanged (Value&)
  241. {
  242. if (lastCurrentId != (int) currentId.getValue())
  243. setSelectedId (currentId.getValue());
  244. }
  245. //==============================================================================
  246. String ComboBox::getText() const
  247. {
  248. return label->getText();
  249. }
  250. void ComboBox::setText (const String& newText, const NotificationType notification)
  251. {
  252. PopupMenu::MenuItemIterator iterator (currentMenu, true);
  253. while (iterator.next())
  254. {
  255. PopupMenu::Item &item = iterator.getItem();
  256. if (item.itemID != 0
  257. && item.text == newText)
  258. {
  259. setSelectedId (item.itemID, notification);
  260. return;
  261. }
  262. }
  263. lastCurrentId = 0;
  264. currentId = 0;
  265. repaint();
  266. if (label->getText() != newText)
  267. {
  268. label->setText (newText, dontSendNotification);
  269. sendChange (notification);
  270. }
  271. }
  272. void ComboBox::showEditor()
  273. {
  274. jassert (isTextEditable()); // you probably shouldn't do this to a non-editable combo box?
  275. label->showEditor();
  276. }
  277. //==============================================================================
  278. void ComboBox::setTextWhenNothingSelected (const String& newMessage)
  279. {
  280. if (textWhenNothingSelected != newMessage)
  281. {
  282. textWhenNothingSelected = newMessage;
  283. repaint();
  284. }
  285. }
  286. String ComboBox::getTextWhenNothingSelected() const
  287. {
  288. return textWhenNothingSelected;
  289. }
  290. void ComboBox::setTextWhenNoChoicesAvailable (const String& newMessage)
  291. {
  292. noChoicesMessage = newMessage;
  293. }
  294. String ComboBox::getTextWhenNoChoicesAvailable() const
  295. {
  296. return noChoicesMessage;
  297. }
  298. //==============================================================================
  299. void ComboBox::paint (Graphics& g)
  300. {
  301. getLookAndFeel().drawComboBox (g, getWidth(), getHeight(), isButtonDown,
  302. label->getRight(), 0, getWidth() - label->getRight(), getHeight(),
  303. *this);
  304. if (textWhenNothingSelected.isNotEmpty()
  305. && label->getText().isEmpty()
  306. && ! label->isBeingEdited())
  307. {
  308. g.setColour (findColour (textColourId).withMultipliedAlpha (0.5f));
  309. g.setFont (label->getFont());
  310. g.drawFittedText (textWhenNothingSelected, label->getBounds().reduced (2, 1),
  311. label->getJustificationType(),
  312. jmax (1, (int) (label->getHeight() / label->getFont().getHeight())));
  313. }
  314. }
  315. void ComboBox::resized()
  316. {
  317. if (getHeight() > 0 && getWidth() > 0)
  318. getLookAndFeel().positionComboBoxText (*this, *label);
  319. }
  320. void ComboBox::enablementChanged()
  321. {
  322. repaint();
  323. }
  324. void ComboBox::colourChanged()
  325. {
  326. lookAndFeelChanged();
  327. }
  328. void ComboBox::parentHierarchyChanged()
  329. {
  330. lookAndFeelChanged();
  331. }
  332. void ComboBox::lookAndFeelChanged()
  333. {
  334. repaint();
  335. {
  336. ScopedPointer<Label> newLabel (getLookAndFeel().createComboBoxTextBox (*this));
  337. jassert (newLabel != nullptr);
  338. if (label != nullptr)
  339. {
  340. newLabel->setEditable (label->isEditable());
  341. newLabel->setJustificationType (label->getJustificationType());
  342. newLabel->setTooltip (label->getTooltip());
  343. newLabel->setText (label->getText(), dontSendNotification);
  344. }
  345. label = newLabel;
  346. }
  347. addAndMakeVisible (label);
  348. EditableState newEditableState = (label->isEditable() ? labelIsEditable : labelIsNotEditable);
  349. if (newEditableState != labelEditableState)
  350. {
  351. labelEditableState = newEditableState;
  352. setWantsKeyboardFocus (labelEditableState == labelIsNotEditable);
  353. }
  354. label->addListener (this);
  355. label->addMouseListener (this, false);
  356. label->setColour (Label::backgroundColourId, Colours::transparentBlack);
  357. label->setColour (Label::textColourId, findColour (ComboBox::textColourId));
  358. label->setColour (TextEditor::textColourId, findColour (ComboBox::textColourId));
  359. label->setColour (TextEditor::backgroundColourId, Colours::transparentBlack);
  360. label->setColour (TextEditor::highlightColourId, findColour (TextEditor::highlightColourId));
  361. label->setColour (TextEditor::outlineColourId, Colours::transparentBlack);
  362. resized();
  363. }
  364. //==============================================================================
  365. bool ComboBox::keyPressed (const KeyPress& key)
  366. {
  367. if (key == KeyPress::upKey || key == KeyPress::leftKey)
  368. {
  369. nudgeSelectedItem (-1);
  370. return true;
  371. }
  372. if (key == KeyPress::downKey || key == KeyPress::rightKey)
  373. {
  374. nudgeSelectedItem (1);
  375. return true;
  376. }
  377. if (key == KeyPress::returnKey)
  378. {
  379. showPopupIfNotActive();
  380. return true;
  381. }
  382. return false;
  383. }
  384. bool ComboBox::keyStateChanged (const bool isKeyDown)
  385. {
  386. // only forward key events that aren't used by this component
  387. return isKeyDown
  388. && (KeyPress::isKeyCurrentlyDown (KeyPress::upKey)
  389. || KeyPress::isKeyCurrentlyDown (KeyPress::leftKey)
  390. || KeyPress::isKeyCurrentlyDown (KeyPress::downKey)
  391. || KeyPress::isKeyCurrentlyDown (KeyPress::rightKey));
  392. }
  393. //==============================================================================
  394. void ComboBox::focusGained (FocusChangeType) { repaint(); }
  395. void ComboBox::focusLost (FocusChangeType) { repaint(); }
  396. void ComboBox::labelTextChanged (Label*)
  397. {
  398. triggerAsyncUpdate();
  399. }
  400. //==============================================================================
  401. void ComboBox::showPopupIfNotActive()
  402. {
  403. if (! menuActive)
  404. {
  405. menuActive = true;
  406. showPopup();
  407. }
  408. }
  409. void ComboBox::hidePopup()
  410. {
  411. if (menuActive)
  412. {
  413. menuActive = false;
  414. PopupMenu::dismissAllActiveMenus();
  415. repaint();
  416. }
  417. }
  418. static void comboBoxPopupMenuFinishedCallback (int result, ComboBox* combo)
  419. {
  420. if (combo != nullptr)
  421. {
  422. combo->hidePopup();
  423. if (result != 0)
  424. combo->setSelectedId (result);
  425. }
  426. }
  427. void ComboBox::showPopup()
  428. {
  429. PopupMenu noChoicesMenu;
  430. const bool hasItems = (currentMenu.getNumItems() > 0);
  431. if (hasItems)
  432. {
  433. PopupMenu::MenuItemIterator iterator (currentMenu, true);
  434. const int selectedId = getSelectedId();
  435. while (iterator.next())
  436. {
  437. PopupMenu::Item &item = iterator.getItem();
  438. if (item.itemID != 0)
  439. item.isTicked = (item.itemID == selectedId);
  440. }
  441. }
  442. else
  443. {
  444. noChoicesMenu.addItem (1, noChoicesMessage, false, false);
  445. }
  446. PopupMenu& menuToShow = (hasItems ? currentMenu : noChoicesMenu);
  447. menuToShow.setLookAndFeel (&getLookAndFeel());
  448. menuToShow.showMenuAsync (PopupMenu::Options().withTargetComponent (this)
  449. .withItemThatMustBeVisible (getSelectedId())
  450. .withMinimumWidth (getWidth())
  451. .withMaximumNumColumns (1)
  452. .withStandardItemHeight (label->getHeight()),
  453. ModalCallbackFunction::forComponent (comboBoxPopupMenuFinishedCallback, this));
  454. }
  455. //==============================================================================
  456. void ComboBox::mouseDown (const MouseEvent& e)
  457. {
  458. beginDragAutoRepeat (300);
  459. isButtonDown = isEnabled() && ! e.mods.isPopupMenu();
  460. if (isButtonDown && (e.eventComponent == this || ! label->isEditable()))
  461. showPopupIfNotActive();
  462. }
  463. void ComboBox::mouseDrag (const MouseEvent& e)
  464. {
  465. beginDragAutoRepeat (50);
  466. if (isButtonDown && e.mouseWasDraggedSinceMouseDown())
  467. showPopupIfNotActive();
  468. }
  469. void ComboBox::mouseUp (const MouseEvent& e2)
  470. {
  471. if (isButtonDown)
  472. {
  473. isButtonDown = false;
  474. repaint();
  475. const MouseEvent e (e2.getEventRelativeTo (this));
  476. if (reallyContains (e.getPosition(), true)
  477. && (e2.eventComponent == this || ! label->isEditable()))
  478. {
  479. showPopupIfNotActive();
  480. }
  481. }
  482. }
  483. void ComboBox::mouseWheelMove (const MouseEvent& e, const MouseWheelDetails& wheel)
  484. {
  485. if (! menuActive && scrollWheelEnabled && e.eventComponent == this && wheel.deltaY != 0.0f)
  486. {
  487. auto oldPos = (int) mouseWheelAccumulator;
  488. mouseWheelAccumulator += wheel.deltaY * 5.0f;
  489. if (auto delta = oldPos - (int) mouseWheelAccumulator)
  490. nudgeSelectedItem (delta);
  491. }
  492. else
  493. {
  494. Component::mouseWheelMove (e, wheel);
  495. }
  496. }
  497. void ComboBox::setScrollWheelEnabled (bool enabled) noexcept
  498. {
  499. scrollWheelEnabled = enabled;
  500. }
  501. //==============================================================================
  502. void ComboBox::addListener (ComboBoxListener* listener) { listeners.add (listener); }
  503. void ComboBox::removeListener (ComboBoxListener* listener) { listeners.remove (listener); }
  504. void ComboBox::handleAsyncUpdate()
  505. {
  506. Component::BailOutChecker checker (this);
  507. listeners.callChecked (checker, &ComboBoxListener::comboBoxChanged, this); // (can't use ComboBox::Listener due to idiotic VC2005 bug)
  508. }
  509. void ComboBox::sendChange (const NotificationType notification)
  510. {
  511. if (notification != dontSendNotification)
  512. triggerAsyncUpdate();
  513. if (notification == sendNotificationSync)
  514. handleUpdateNowIfNeeded();
  515. }
  516. // Old deprecated methods - remove eventually...
  517. void ComboBox::clear (const bool dontSendChange) { clear (dontSendChange ? dontSendNotification : sendNotification); }
  518. void ComboBox::setSelectedItemIndex (const int index, const bool dontSendChange) { setSelectedItemIndex (index, dontSendChange ? dontSendNotification : sendNotification); }
  519. void ComboBox::setSelectedId (const int newItemId, const bool dontSendChange) { setSelectedId (newItemId, dontSendChange ? dontSendNotification : sendNotification); }
  520. void ComboBox::setText (const String& newText, const bool dontSendChange) { setText (newText, dontSendChange ? dontSendNotification : sendNotification); }