/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2013 - Raw Material Software Ltd. Permission is granted to use this software under the terms of either: a) the GPL v2 (or any later version) b) the Affero GPL v3 Details of these licenses can be found at: www.gnu.org/licenses JUCE is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. ------------------------------------------------------------------------------ To release a closed-source product which uses JUCE, commercial licenses are available: visit www.juce.com for more information. ============================================================================== */ ComboBox::ItemInfo::ItemInfo (const String& nm, int iid, bool enabled, bool heading) : name (nm), itemId (iid), isEnabled (enabled), isHeading (heading) { } bool ComboBox::ItemInfo::isSeparator() const noexcept { return name.isEmpty(); } bool ComboBox::ItemInfo::isRealItem() const noexcept { return ! (isHeading || name.isEmpty()); } //============================================================================== ComboBox::ComboBox (const String& name) : Component (name), lastCurrentId (0), isButtonDown (false), separatorPending (false), menuActive (false), noChoicesMessage (TRANS("(no choices)")) { setRepaintsOnMouseActivity (true); ComboBox::lookAndFeelChanged(); currentId.addListener (this); } ComboBox::~ComboBox() { currentId.removeListener (this); if (menuActive) PopupMenu::dismissAllActiveMenus(); label = nullptr; } //============================================================================== void ComboBox::setEditableText (const bool isEditable) { if (label->isEditableOnSingleClick() != isEditable || label->isEditableOnDoubleClick() != isEditable) { label->setEditable (isEditable, isEditable, false); setWantsKeyboardFocus (! isEditable); resized(); } } bool ComboBox::isTextEditable() const noexcept { return label->isEditable(); } void ComboBox::setJustificationType (Justification justification) { label->setJustificationType (justification); } Justification ComboBox::getJustificationType() const noexcept { return label->getJustificationType(); } void ComboBox::setTooltip (const String& newTooltip) { SettableTooltipClient::setTooltip (newTooltip); label->setTooltip (newTooltip); } //============================================================================== void ComboBox::addItem (const String& newItemText, const int newItemId) { // you can't add empty strings to the list.. jassert (newItemText.isNotEmpty()); // IDs must be non-zero, as zero is used to indicate a lack of selecion. jassert (newItemId != 0); // you shouldn't use duplicate item IDs! jassert (getItemForId (newItemId) == nullptr); if (newItemText.isNotEmpty() && newItemId != 0) { if (separatorPending) { separatorPending = false; items.add (new ItemInfo (String::empty, 0, false, false)); } items.add (new ItemInfo (newItemText, newItemId, true, false)); } } void ComboBox::addItemList (const StringArray& itemsToAdd, const int firstItemIdOffset) { for (int i = 0; i < itemsToAdd.size(); ++i) addItem (itemsToAdd[i], i + firstItemIdOffset); } void ComboBox::addSeparator() { separatorPending = (items.size() > 0); } void ComboBox::addSectionHeading (const String& headingName) { // you can't add empty strings to the list.. jassert (headingName.isNotEmpty()); if (headingName.isNotEmpty()) { if (separatorPending) { separatorPending = false; items.add (new ItemInfo (String::empty, 0, false, false)); } items.add (new ItemInfo (headingName, 0, true, true)); } } void ComboBox::setItemEnabled (const int itemId, const bool shouldBeEnabled) { if (ItemInfo* const item = getItemForId (itemId)) item->isEnabled = shouldBeEnabled; } bool ComboBox::isItemEnabled (int itemId) const noexcept { const ItemInfo* const item = getItemForId (itemId); return item != nullptr && item->isEnabled; } void ComboBox::changeItemText (const int itemId, const String& newText) { if (ItemInfo* const item = getItemForId (itemId)) item->name = newText; else jassertfalse; } void ComboBox::clear (const NotificationType notification) { items.clear(); separatorPending = false; if (! label->isEditable()) setSelectedItemIndex (-1, notification); } //============================================================================== ComboBox::ItemInfo* ComboBox::getItemForId (const int itemId) const noexcept { if (itemId != 0) { for (int i = items.size(); --i >= 0;) if (items.getUnchecked(i)->itemId == itemId) return items.getUnchecked(i); } return nullptr; } ComboBox::ItemInfo* ComboBox::getItemForIndex (const int index) const noexcept { for (int n = 0, i = 0; i < items.size(); ++i) { ItemInfo* const item = items.getUnchecked(i); if (item->isRealItem()) if (n++ == index) return item; } return nullptr; } int ComboBox::getNumItems() const noexcept { int n = 0; for (int i = items.size(); --i >= 0;) if (items.getUnchecked(i)->isRealItem()) ++n; return n; } String ComboBox::getItemText (const int index) const { if (const ItemInfo* const item = getItemForIndex (index)) return item->name; return String::empty; } int ComboBox::getItemId (const int index) const noexcept { if (const ItemInfo* const item = getItemForIndex (index)) return item->itemId; return 0; } int ComboBox::indexOfItemId (const int itemId) const noexcept { for (int n = 0, i = 0; i < items.size(); ++i) { const ItemInfo* const item = items.getUnchecked(i); if (item->isRealItem()) { if (item->itemId == itemId) return n; ++n; } } return -1; } //============================================================================== int ComboBox::getSelectedItemIndex() const { int index = indexOfItemId (currentId.getValue()); if (getText() != getItemText (index)) index = -1; return index; } void ComboBox::setSelectedItemIndex (const int index, const NotificationType notification) { setSelectedId (getItemId (index), notification); } int ComboBox::getSelectedId() const noexcept { const ItemInfo* const item = getItemForId (currentId.getValue()); return (item != nullptr && getText() == item->name) ? item->itemId : 0; } void ComboBox::setSelectedId (const int newItemId, const NotificationType notification) { const ItemInfo* const item = getItemForId (newItemId); const String newItemText (item != nullptr ? item->name : String::empty); if (lastCurrentId != newItemId || label->getText() != newItemText) { label->setText (newItemText, dontSendNotification); lastCurrentId = newItemId; currentId = newItemId; repaint(); // for the benefit of the 'none selected' text sendChange (notification); } } bool ComboBox::selectIfEnabled (const int index) { if (const ItemInfo* const item = getItemForIndex (index)) { if (item->isEnabled) { setSelectedItemIndex (index); return true; } } return false; } bool ComboBox::nudgeSelectedItem (int delta) { for (int i = getSelectedItemIndex() + delta; isPositiveAndBelow (i, getNumItems()); i += delta) if (selectIfEnabled (i)) return true; return false; } void ComboBox::valueChanged (Value&) { if (lastCurrentId != (int) currentId.getValue()) setSelectedId (currentId.getValue()); } //============================================================================== String ComboBox::getText() const { return label->getText(); } void ComboBox::setText (const String& newText, const NotificationType notification) { for (int i = items.size(); --i >= 0;) { const ItemInfo* const item = items.getUnchecked(i); if (item->isRealItem() && item->name == newText) { setSelectedId (item->itemId, notification); return; } } lastCurrentId = 0; currentId = 0; repaint(); if (label->getText() != newText) { label->setText (newText, dontSendNotification); sendChange (notification); } } void ComboBox::showEditor() { jassert (isTextEditable()); // you probably shouldn't do this to a non-editable combo box? label->showEditor(); } //============================================================================== void ComboBox::setTextWhenNothingSelected (const String& newMessage) { if (textWhenNothingSelected != newMessage) { textWhenNothingSelected = newMessage; repaint(); } } String ComboBox::getTextWhenNothingSelected() const { return textWhenNothingSelected; } void ComboBox::setTextWhenNoChoicesAvailable (const String& newMessage) { noChoicesMessage = newMessage; } String ComboBox::getTextWhenNoChoicesAvailable() const { return noChoicesMessage; } //============================================================================== void ComboBox::paint (Graphics& g) { getLookAndFeel().drawComboBox (g, getWidth(), getHeight(), isButtonDown, label->getRight(), 0, getWidth() - label->getRight(), getHeight(), *this); if (textWhenNothingSelected.isNotEmpty() && label->getText().isEmpty() && ! label->isBeingEdited()) { g.setColour (findColour (textColourId).withMultipliedAlpha (0.5f)); g.setFont (label->getFont()); g.drawFittedText (textWhenNothingSelected, label->getBounds().reduced (2, 1), label->getJustificationType(), jmax (1, (int) (label->getHeight() / label->getFont().getHeight()))); } } void ComboBox::resized() { if (getHeight() > 0 && getWidth() > 0) getLookAndFeel().positionComboBoxText (*this, *label); } void ComboBox::enablementChanged() { repaint(); } void ComboBox::lookAndFeelChanged() { repaint(); { ScopedPointer <Label> newLabel (getLookAndFeel().createComboBoxTextBox (*this)); jassert (newLabel != nullptr); if (label != nullptr) { newLabel->setEditable (label->isEditable()); newLabel->setJustificationType (label->getJustificationType()); newLabel->setTooltip (label->getTooltip()); newLabel->setText (label->getText(), dontSendNotification); } label = newLabel; } addAndMakeVisible (label); setWantsKeyboardFocus (! label->isEditable()); label->addListener (this); label->addMouseListener (this, false); label->setColour (Label::backgroundColourId, Colours::transparentBlack); label->setColour (Label::textColourId, findColour (ComboBox::textColourId)); label->setColour (TextEditor::textColourId, findColour (ComboBox::textColourId)); label->setColour (TextEditor::backgroundColourId, Colours::transparentBlack); label->setColour (TextEditor::highlightColourId, findColour (TextEditor::highlightColourId)); label->setColour (TextEditor::outlineColourId, Colours::transparentBlack); resized(); } void ComboBox::colourChanged() { lookAndFeelChanged(); } //============================================================================== bool ComboBox::keyPressed (const KeyPress& key) { if (key == KeyPress::upKey || key == KeyPress::leftKey) { nudgeSelectedItem (-1); return true; } if (key == KeyPress::downKey || key == KeyPress::rightKey) { nudgeSelectedItem (1); return true; } if (key == KeyPress::returnKey) { showPopup(); return true; } return false; } bool ComboBox::keyStateChanged (const bool isKeyDown) { // only forward key events that aren't used by this component return isKeyDown && (KeyPress::isKeyCurrentlyDown (KeyPress::upKey) || KeyPress::isKeyCurrentlyDown (KeyPress::leftKey) || KeyPress::isKeyCurrentlyDown (KeyPress::downKey) || KeyPress::isKeyCurrentlyDown (KeyPress::rightKey)); } //============================================================================== void ComboBox::focusGained (FocusChangeType) { repaint(); } void ComboBox::focusLost (FocusChangeType) { repaint(); } void ComboBox::labelTextChanged (Label*) { triggerAsyncUpdate(); } //============================================================================== void ComboBox::popupMenuFinishedCallback (int result, ComboBox* box) { if (box != nullptr) { box->menuActive = false; if (result != 0) box->setSelectedId (result); } } void ComboBox::showPopup() { if (! menuActive) { const int selectedId = getSelectedId(); PopupMenu menu; menu.setLookAndFeel (&getLookAndFeel()); for (int i = 0; i < items.size(); ++i) { const ItemInfo* const item = items.getUnchecked(i); if (item->isSeparator()) menu.addSeparator(); else if (item->isHeading) menu.addSectionHeader (item->name); else menu.addItem (item->itemId, item->name, item->isEnabled, item->itemId == selectedId); } if (items.size() == 0) menu.addItem (1, noChoicesMessage, false); menuActive = true; menu.showMenuAsync (PopupMenu::Options().withTargetComponent (this) .withItemThatMustBeVisible (selectedId) .withMinimumWidth (getWidth()) .withMaximumNumColumns (1) .withStandardItemHeight (jlimit (12, 24, getHeight())), ModalCallbackFunction::forComponent (popupMenuFinishedCallback, this)); } } //============================================================================== void ComboBox::mouseDown (const MouseEvent& e) { beginDragAutoRepeat (300); isButtonDown = isEnabled() && ! e.mods.isPopupMenu(); if (isButtonDown && (e.eventComponent == this || ! label->isEditable())) showPopup(); } void ComboBox::mouseDrag (const MouseEvent& e) { beginDragAutoRepeat (50); if (isButtonDown && ! e.mouseWasClicked()) showPopup(); } void ComboBox::mouseUp (const MouseEvent& e2) { if (isButtonDown) { isButtonDown = false; repaint(); const MouseEvent e (e2.getEventRelativeTo (this)); if (reallyContains (e.getPosition(), true) && (e2.eventComponent == this || ! label->isEditable())) { showPopup(); } } } void ComboBox::mouseWheelMove (const MouseEvent& e, const MouseWheelDetails& wheel) { #if 0 // NB: this is far too irritating if enabled, because on scrollable pages containing // comboboxes (e.g. introjucer settings pages), you can easily nudge them when trying to scroll if (! menuActive && wheel.deltaY != 0) nudgeSelectedItem (wheel.deltaY > 0 ? -1 : 1); else #endif Component::mouseWheelMove (e, wheel); } //============================================================================== void ComboBox::addListener (ComboBoxListener* listener) { listeners.add (listener); } void ComboBox::removeListener (ComboBoxListener* listener) { listeners.remove (listener); } void ComboBox::handleAsyncUpdate() { Component::BailOutChecker checker (this); listeners.callChecked (checker, &ComboBoxListener::comboBoxChanged, this); // (can't use ComboBox::Listener due to idiotic VC2005 bug) } void ComboBox::sendChange (const NotificationType notification) { if (notification != dontSendNotification) triggerAsyncUpdate(); if (notification == sendNotificationSync) handleUpdateNowIfNeeded(); } // Old deprecated methods - remove eventually... void ComboBox::clear (const bool dontSendChange) { clear (dontSendChange ? dontSendNotification : sendNotification); } void ComboBox::setSelectedItemIndex (const int index, const bool dontSendChange) { setSelectedItemIndex (index, dontSendChange ? dontSendNotification : sendNotification); } void ComboBox::setSelectedId (const int newItemId, const bool dontSendChange) { setSelectedId (newItemId, dontSendChange ? dontSendNotification : sendNotification); } void ComboBox::setText (const String& newText, const bool dontSendChange) { setText (newText, dontSendChange ? dontSendNotification : sendNotification); }