VST3: fetch midi mappings all at once, use it for note/sound-off
[carla.git] / source / modules / juce_gui_basics / buttons / juce_Button.cpp
blobb66b88c2a28c2a1f8e14a685c714ecd2975b51f6
1 /*
2 ==============================================================================
4 This file is part of the JUCE library.
5 Copyright (c) 2022 - Raw Material Software Limited
7 JUCE is an open source library subject to commercial or open-source
8 licensing.
10 By using JUCE, you agree to the terms of both the JUCE 7 End-User License
11 Agreement and JUCE Privacy Policy.
13 End User License Agreement: www.juce.com/juce-7-licence
14 Privacy Policy: www.juce.com/juce-privacy-policy
16 Or: You may also use this code under the terms of the GPL v3 (see
17 www.gnu.org/licenses).
19 JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
20 EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
21 DISCLAIMED.
23 ==============================================================================
26 namespace juce
29 struct Button::CallbackHelper : public Timer,
30 public ApplicationCommandManagerListener,
31 public Value::Listener,
32 public KeyListener
34 CallbackHelper (Button& b) : button (b) {}
36 void timerCallback() override
38 button.repeatTimerCallback();
41 bool keyStateChanged (bool, Component*) override
43 return button.keyStateChangedCallback();
46 void valueChanged (Value& value) override
48 if (value.refersToSameSourceAs (button.isOn))
49 button.setToggleState (button.isOn.getValue(), dontSendNotification, sendNotification);
52 bool keyPressed (const KeyPress&, Component*) override
54 // returning true will avoid forwarding events for keys that we're using as shortcuts
55 return button.isShortcutPressed();
58 void applicationCommandInvoked (const ApplicationCommandTarget::InvocationInfo& info) override
60 if (info.commandID == button.commandID
61 && (info.commandFlags & ApplicationCommandInfo::dontTriggerVisualFeedback) == 0)
62 button.flashButtonState();
65 void applicationCommandListChanged() override
67 button.applicationCommandListChangeCallback();
70 Button& button;
72 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CallbackHelper)
75 //==============================================================================
76 Button::Button (const String& name) : Component (name), text (name)
78 callbackHelper.reset (new CallbackHelper (*this));
80 setWantsKeyboardFocus (true);
81 isOn.addListener (callbackHelper.get());
84 Button::~Button()
86 clearShortcuts();
88 if (commandManagerToUse != nullptr)
89 commandManagerToUse->removeListener (callbackHelper.get());
91 isOn.removeListener (callbackHelper.get());
92 callbackHelper.reset();
95 //==============================================================================
96 void Button::setButtonText (const String& newText)
98 if (text != newText)
100 text = newText;
101 repaint();
105 void Button::setTooltip (const String& newTooltip)
107 SettableTooltipClient::setTooltip (newTooltip);
108 generateTooltip = false;
111 void Button::updateAutomaticTooltip (const ApplicationCommandInfo& info)
113 if (generateTooltip && commandManagerToUse != nullptr)
115 auto tt = info.description.isNotEmpty() ? info.description
116 : info.shortName;
118 for (auto& kp : commandManagerToUse->getKeyMappings()->getKeyPressesAssignedToCommand (commandID))
120 auto key = kp.getTextDescription();
122 tt << " [";
124 if (key.length() == 1)
125 tt << TRANS("shortcut") << ": '" << key << "']";
126 else
127 tt << key << ']';
130 SettableTooltipClient::setTooltip (tt);
134 void Button::setConnectedEdges (int newFlags)
136 if (connectedEdgeFlags != newFlags)
138 connectedEdgeFlags = newFlags;
139 repaint();
143 //==============================================================================
144 void Button::checkToggleableState (bool wasToggleable)
146 if (isToggleable() != wasToggleable)
147 invalidateAccessibilityHandler();
150 void Button::setToggleable (bool isNowToggleable)
152 const auto wasToggleable = isToggleable();
154 canBeToggled = isNowToggleable;
155 checkToggleableState (wasToggleable);
158 void Button::setToggleState (bool shouldBeOn, NotificationType notification)
160 setToggleState (shouldBeOn, notification, notification);
163 void Button::setToggleState (bool shouldBeOn, NotificationType clickNotification, NotificationType stateNotification)
165 if (shouldBeOn != lastToggleState)
167 WeakReference<Component> deletionWatcher (this);
169 if (shouldBeOn)
171 turnOffOtherButtonsInGroup (clickNotification, stateNotification);
173 if (deletionWatcher == nullptr)
174 return;
177 // This test is done so that if the value is void rather than explicitly set to
178 // false, the value won't be changed unless the required value is true.
179 if (getToggleState() != shouldBeOn)
181 isOn = shouldBeOn;
183 if (deletionWatcher == nullptr)
184 return;
187 lastToggleState = shouldBeOn;
188 repaint();
190 if (clickNotification != dontSendNotification)
192 // async callbacks aren't possible here
193 jassert (clickNotification != sendNotificationAsync);
195 sendClickMessage (ModifierKeys::currentModifiers);
197 if (deletionWatcher == nullptr)
198 return;
201 if (stateNotification != dontSendNotification)
202 sendStateMessage();
203 else
204 buttonStateChanged();
206 if (auto* handler = getAccessibilityHandler())
207 handler->notifyAccessibilityEvent (AccessibilityEvent::valueChanged);
211 void Button::setToggleState (bool shouldBeOn, bool sendChange)
213 setToggleState (shouldBeOn, sendChange ? sendNotification : dontSendNotification);
216 void Button::setClickingTogglesState (bool shouldToggle) noexcept
218 const auto wasToggleable = isToggleable();
220 clickTogglesState = shouldToggle;
221 checkToggleableState (wasToggleable);
223 // if you've got clickTogglesState turned on, you shouldn't also connect the button
224 // up to be a command invoker. Instead, your command handler must flip the state of whatever
225 // it is that this button represents, and the button will update its state to reflect this
226 // in the applicationCommandListChanged() method.
227 jassert (commandManagerToUse == nullptr || ! clickTogglesState);
230 void Button::setRadioGroupId (int newGroupId, NotificationType notification)
232 if (radioGroupId != newGroupId)
234 radioGroupId = newGroupId;
236 if (lastToggleState)
237 turnOffOtherButtonsInGroup (notification, notification);
239 setToggleable (true);
240 invalidateAccessibilityHandler();
244 void Button::turnOffOtherButtonsInGroup (NotificationType clickNotification, NotificationType stateNotification)
246 if (auto* p = getParentComponent())
248 if (radioGroupId != 0)
250 WeakReference<Component> deletionWatcher (this);
252 for (auto* c : p->getChildren())
254 if (c != this)
256 if (auto b = dynamic_cast<Button*> (c))
258 if (b->getRadioGroupId() == radioGroupId)
260 b->setToggleState (false, clickNotification, stateNotification);
262 if (deletionWatcher == nullptr)
263 return;
272 //==============================================================================
273 void Button::enablementChanged()
275 updateState();
276 repaint();
279 Button::ButtonState Button::updateState()
281 return updateState (isMouseOver (true), isMouseButtonDown());
284 Button::ButtonState Button::updateState (bool over, bool down)
286 ButtonState newState = buttonNormal;
288 if (isEnabled() && isVisible() && ! isCurrentlyBlockedByAnotherModalComponent())
290 if ((down && (over || (triggerOnMouseDown && buttonState == buttonDown))) || isKeyDown)
291 newState = buttonDown;
292 else if (over)
293 newState = buttonOver;
296 setState (newState);
297 return newState;
300 void Button::setState (ButtonState newState)
302 if (buttonState != newState)
304 buttonState = newState;
305 repaint();
307 if (buttonState == buttonDown)
309 buttonPressTime = Time::getApproximateMillisecondCounter();
310 lastRepeatTime = 0;
313 sendStateMessage();
317 bool Button::isDown() const noexcept { return buttonState == buttonDown; }
318 bool Button::isOver() const noexcept { return buttonState != buttonNormal; }
320 void Button::buttonStateChanged() {}
322 uint32 Button::getMillisecondsSinceButtonDown() const noexcept
324 auto now = Time::getApproximateMillisecondCounter();
325 return now > buttonPressTime ? now - buttonPressTime : 0;
328 void Button::setTriggeredOnMouseDown (bool isTriggeredOnMouseDown) noexcept
330 triggerOnMouseDown = isTriggeredOnMouseDown;
333 bool Button::getTriggeredOnMouseDown() const noexcept
335 return triggerOnMouseDown;
338 //==============================================================================
339 void Button::clicked()
343 void Button::clicked (const ModifierKeys&)
345 clicked();
348 enum { clickMessageId = 0x2f3f4f99 };
350 void Button::triggerClick()
352 postCommandMessage (clickMessageId);
355 void Button::internalClickCallback (const ModifierKeys& modifiers)
357 if (clickTogglesState)
359 const bool shouldBeOn = (radioGroupId != 0 || ! lastToggleState);
361 if (shouldBeOn != getToggleState())
363 setToggleState (shouldBeOn, sendNotification);
364 return;
368 sendClickMessage (modifiers);
371 void Button::flashButtonState()
373 if (isEnabled())
375 needsToRelease = true;
376 setState (buttonDown);
377 callbackHelper->startTimer (100);
381 void Button::handleCommandMessage (int commandId)
383 if (commandId == clickMessageId)
385 if (isEnabled())
387 flashButtonState();
388 internalClickCallback (ModifierKeys::currentModifiers);
391 else
393 Component::handleCommandMessage (commandId);
397 //==============================================================================
398 void Button::addListener (Listener* l) { buttonListeners.add (l); }
399 void Button::removeListener (Listener* l) { buttonListeners.remove (l); }
401 void Button::sendClickMessage (const ModifierKeys& modifiers)
403 Component::BailOutChecker checker (this);
405 if (commandManagerToUse != nullptr && commandID != 0)
407 ApplicationCommandTarget::InvocationInfo info (commandID);
408 info.invocationMethod = ApplicationCommandTarget::InvocationInfo::fromButton;
409 info.originatingComponent = this;
411 commandManagerToUse->invoke (info, true);
414 clicked (modifiers);
416 if (checker.shouldBailOut())
417 return;
419 buttonListeners.callChecked (checker, [this] (Listener& l) { l.buttonClicked (this); });
421 if (checker.shouldBailOut())
422 return;
424 if (onClick != nullptr)
425 onClick();
428 void Button::sendStateMessage()
430 Component::BailOutChecker checker (this);
432 buttonStateChanged();
434 if (checker.shouldBailOut())
435 return;
437 buttonListeners.callChecked (checker, [this] (Listener& l) { l.buttonStateChanged (this); });
439 if (checker.shouldBailOut())
440 return;
442 if (onStateChange != nullptr)
443 onStateChange();
446 //==============================================================================
447 void Button::paint (Graphics& g)
449 if (needsToRelease && isEnabled())
451 needsToRelease = false;
452 needsRepainting = true;
455 paintButton (g, isOver(), isDown());
456 lastStatePainted = buttonState;
459 //==============================================================================
460 void Button::mouseEnter (const MouseEvent&) { updateState (true, false); }
461 void Button::mouseExit (const MouseEvent&) { updateState (false, false); }
463 void Button::mouseDown (const MouseEvent& e)
465 updateState (true, true);
467 if (isDown())
469 if (autoRepeatDelay >= 0)
470 callbackHelper->startTimer (autoRepeatDelay);
472 if (triggerOnMouseDown)
473 internalClickCallback (e.mods);
477 void Button::mouseUp (const MouseEvent& e)
479 const auto wasDown = isDown();
480 const auto wasOver = isOver();
481 updateState (isMouseSourceOver (e), false);
483 if (wasDown && wasOver && ! triggerOnMouseDown)
485 if (lastStatePainted != buttonDown)
486 flashButtonState();
488 WeakReference<Component> deletionWatcher (this);
490 internalClickCallback (e.mods);
492 if (deletionWatcher != nullptr)
493 updateState (isMouseSourceOver (e), false);
497 void Button::mouseDrag (const MouseEvent& e)
499 auto oldState = buttonState;
500 updateState (isMouseSourceOver (e), true);
502 if (autoRepeatDelay >= 0 && buttonState != oldState && isDown())
503 callbackHelper->startTimer (autoRepeatSpeed);
506 bool Button::isMouseSourceOver (const MouseEvent& e)
508 if (e.source.isTouch() || e.source.isPen())
509 return getLocalBounds().toFloat().contains (e.position);
511 return isMouseOver();
514 void Button::focusGained (FocusChangeType)
516 updateState();
517 repaint();
520 void Button::focusLost (FocusChangeType)
522 updateState();
523 repaint();
526 void Button::visibilityChanged()
528 needsToRelease = false;
529 updateState();
532 void Button::parentHierarchyChanged()
534 auto* newKeySource = shortcuts.isEmpty() ? nullptr : getTopLevelComponent();
536 if (newKeySource != keySource.get())
538 if (keySource != nullptr)
539 keySource->removeKeyListener (callbackHelper.get());
541 keySource = newKeySource;
543 if (keySource != nullptr)
544 keySource->addKeyListener (callbackHelper.get());
548 //==============================================================================
549 void Button::setCommandToTrigger (ApplicationCommandManager* newCommandManager,
550 CommandID newCommandID, bool generateTip)
552 commandID = newCommandID;
553 generateTooltip = generateTip;
555 if (commandManagerToUse != newCommandManager)
557 if (commandManagerToUse != nullptr)
558 commandManagerToUse->removeListener (callbackHelper.get());
560 commandManagerToUse = newCommandManager;
562 if (commandManagerToUse != nullptr)
563 commandManagerToUse->addListener (callbackHelper.get());
565 // if you've got clickTogglesState turned on, you shouldn't also connect the button
566 // up to be a command invoker. Instead, your command handler must flip the state of whatever
567 // it is that this button represents, and the button will update its state to reflect this
568 // in the applicationCommandListChanged() method.
569 jassert (commandManagerToUse == nullptr || ! clickTogglesState);
572 if (commandManagerToUse != nullptr)
573 applicationCommandListChangeCallback();
574 else
575 setEnabled (true);
578 void Button::applicationCommandListChangeCallback()
580 if (commandManagerToUse != nullptr)
582 ApplicationCommandInfo info (0);
584 if (commandManagerToUse->getTargetForCommand (commandID, info) != nullptr)
586 updateAutomaticTooltip (info);
587 setEnabled ((info.flags & ApplicationCommandInfo::isDisabled) == 0);
588 setToggleState ((info.flags & ApplicationCommandInfo::isTicked) != 0, dontSendNotification);
590 else
592 setEnabled (false);
597 //==============================================================================
598 void Button::addShortcut (const KeyPress& key)
600 if (key.isValid())
602 jassert (! isRegisteredForShortcut (key)); // already registered!
604 shortcuts.add (key);
605 parentHierarchyChanged();
609 void Button::clearShortcuts()
611 shortcuts.clear();
612 parentHierarchyChanged();
615 bool Button::isShortcutPressed() const
617 if (isShowing() && ! isCurrentlyBlockedByAnotherModalComponent())
618 for (auto& s : shortcuts)
619 if (s.isCurrentlyDown())
620 return true;
622 return false;
625 bool Button::isRegisteredForShortcut (const KeyPress& key) const
627 for (auto& s : shortcuts)
628 if (key == s)
629 return true;
631 return false;
634 bool Button::keyStateChangedCallback()
636 if (! isEnabled())
637 return false;
639 const bool wasDown = isKeyDown;
640 isKeyDown = isShortcutPressed();
642 if (autoRepeatDelay >= 0 && (isKeyDown && ! wasDown))
643 callbackHelper->startTimer (autoRepeatDelay);
645 updateState();
647 if (isEnabled() && wasDown && ! isKeyDown)
649 internalClickCallback (ModifierKeys::currentModifiers);
651 // (return immediately - this button may now have been deleted)
652 return true;
655 return wasDown || isKeyDown;
658 bool Button::keyPressed (const KeyPress& key)
660 if (isEnabled() && key.isKeyCode (KeyPress::returnKey))
662 triggerClick();
663 return true;
666 return false;
669 //==============================================================================
670 void Button::setRepeatSpeed (int initialDelayMillisecs,
671 int repeatMillisecs,
672 int minimumDelayInMillisecs) noexcept
674 autoRepeatDelay = initialDelayMillisecs;
675 autoRepeatSpeed = repeatMillisecs;
676 autoRepeatMinimumDelay = jmin (autoRepeatSpeed, minimumDelayInMillisecs);
679 void Button::repeatTimerCallback()
681 if (needsRepainting)
683 callbackHelper->stopTimer();
684 updateState();
685 needsRepainting = false;
687 else if (autoRepeatSpeed > 0 && (isKeyDown || (updateState() == buttonDown)))
689 auto repeatSpeed = autoRepeatSpeed;
691 if (autoRepeatMinimumDelay >= 0)
693 auto timeHeldDown = jmin (1.0, getMillisecondsSinceButtonDown() / 4000.0);
694 timeHeldDown *= timeHeldDown;
696 repeatSpeed = repeatSpeed + (int) (timeHeldDown * (autoRepeatMinimumDelay - repeatSpeed));
699 repeatSpeed = jmax (1, repeatSpeed);
701 auto now = Time::getMillisecondCounter();
703 // if we've been blocked from repeating often enough, speed up the repeat timer to compensate..
704 if (lastRepeatTime != 0 && (int) (now - lastRepeatTime) > repeatSpeed * 2)
705 repeatSpeed = jmax (1, repeatSpeed / 2);
707 lastRepeatTime = now;
708 callbackHelper->startTimer (repeatSpeed);
710 internalClickCallback (ModifierKeys::currentModifiers);
712 else if (! needsToRelease)
714 callbackHelper->stopTimer();
718 //==============================================================================
719 class ButtonAccessibilityHandler : public AccessibilityHandler
721 public:
722 explicit ButtonAccessibilityHandler (Button& buttonToWrap, AccessibilityRole roleIn)
723 : AccessibilityHandler (buttonToWrap,
724 isRadioButton (buttonToWrap) ? AccessibilityRole::radioButton : roleIn,
725 getAccessibilityActions (buttonToWrap),
726 getAccessibilityInterfaces (buttonToWrap)),
727 button (buttonToWrap)
731 AccessibleState getCurrentState() const override
733 auto state = AccessibilityHandler::getCurrentState();
735 if (button.isToggleable())
737 state = state.withCheckable();
739 if (button.getToggleState())
740 state = state.withChecked();
743 return state;
746 String getTitle() const override
748 auto title = AccessibilityHandler::getTitle();
750 if (title.isEmpty())
751 return button.getButtonText();
753 return title;
756 String getHelp() const override { return button.getTooltip(); }
758 private:
759 class ButtonValueInterface : public AccessibilityTextValueInterface
761 public:
762 explicit ButtonValueInterface (Button& buttonToWrap)
763 : button (buttonToWrap)
767 bool isReadOnly() const override { return true; }
768 String getCurrentValueAsString() const override { return button.getToggleState() ? "On" : "Off"; }
769 void setValueAsString (const String&) override {}
771 private:
772 Button& button;
774 //==============================================================================
775 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ButtonValueInterface)
778 static bool isRadioButton (const Button& button) noexcept
780 return button.getRadioGroupId() != 0;
783 static AccessibilityActions getAccessibilityActions (Button& button)
785 auto actions = AccessibilityActions().addAction (AccessibilityActionType::press,
786 [&button] { button.triggerClick(); });
788 if (button.isToggleable())
789 actions = actions.addAction (AccessibilityActionType::toggle,
790 [&button] { button.setToggleState (! button.getToggleState(), sendNotification); });
792 return actions;
795 static Interfaces getAccessibilityInterfaces (Button& button)
797 if (button.isToggleable())
798 return { std::make_unique<ButtonValueInterface> (button) };
800 return {};
803 Button& button;
805 //==============================================================================
806 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ButtonAccessibilityHandler)
809 std::unique_ptr<AccessibilityHandler> Button::createAccessibilityHandler()
811 return std::make_unique<ButtonAccessibilityHandler> (*this, AccessibilityRole::button);
814 } // namespace juce