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
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
23 ==============================================================================
29 struct Button::CallbackHelper
: public Timer
,
30 public ApplicationCommandManagerListener
,
31 public Value::Listener
,
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();
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());
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
)
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
118 for (auto& kp
: commandManagerToUse
->getKeyMappings()->getKeyPressesAssignedToCommand (commandID
))
120 auto key
= kp
.getTextDescription();
124 if (key
.length() == 1)
125 tt
<< TRANS("shortcut") << ": '" << key
<< "']";
130 SettableTooltipClient::setTooltip (tt
);
134 void Button::setConnectedEdges (int newFlags
)
136 if (connectedEdgeFlags
!= newFlags
)
138 connectedEdgeFlags
= newFlags
;
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);
171 turnOffOtherButtonsInGroup (clickNotification
, stateNotification
);
173 if (deletionWatcher
== nullptr)
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
)
183 if (deletionWatcher
== nullptr)
187 lastToggleState
= shouldBeOn
;
190 if (clickNotification
!= dontSendNotification
)
192 // async callbacks aren't possible here
193 jassert (clickNotification
!= sendNotificationAsync
);
195 sendClickMessage (ModifierKeys::currentModifiers
);
197 if (deletionWatcher
== nullptr)
201 if (stateNotification
!= dontSendNotification
)
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
;
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())
256 if (auto b
= dynamic_cast<Button
*> (c
))
258 if (b
->getRadioGroupId() == radioGroupId
)
260 b
->setToggleState (false, clickNotification
, stateNotification
);
262 if (deletionWatcher
== nullptr)
272 //==============================================================================
273 void Button::enablementChanged()
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
;
293 newState
= buttonOver
;
300 void Button::setState (ButtonState newState
)
302 if (buttonState
!= newState
)
304 buttonState
= newState
;
307 if (buttonState
== buttonDown
)
309 buttonPressTime
= Time::getApproximateMillisecondCounter();
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
&)
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
);
368 sendClickMessage (modifiers
);
371 void Button::flashButtonState()
375 needsToRelease
= true;
376 setState (buttonDown
);
377 callbackHelper
->startTimer (100);
381 void Button::handleCommandMessage (int commandId
)
383 if (commandId
== clickMessageId
)
388 internalClickCallback (ModifierKeys::currentModifiers
);
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);
416 if (checker
.shouldBailOut())
419 buttonListeners
.callChecked (checker
, [this] (Listener
& l
) { l
.buttonClicked (this); });
421 if (checker
.shouldBailOut())
424 if (onClick
!= nullptr)
428 void Button::sendStateMessage()
430 Component::BailOutChecker
checker (this);
432 buttonStateChanged();
434 if (checker
.shouldBailOut())
437 buttonListeners
.callChecked (checker
, [this] (Listener
& l
) { l
.buttonStateChanged (this); });
439 if (checker
.shouldBailOut())
442 if (onStateChange
!= nullptr)
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);
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
)
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
)
520 void Button::focusLost (FocusChangeType
)
526 void Button::visibilityChanged()
528 needsToRelease
= false;
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();
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
);
597 //==============================================================================
598 void Button::addShortcut (const KeyPress
& key
)
602 jassert (! isRegisteredForShortcut (key
)); // already registered!
605 parentHierarchyChanged();
609 void Button::clearShortcuts()
612 parentHierarchyChanged();
615 bool Button::isShortcutPressed() const
617 if (isShowing() && ! isCurrentlyBlockedByAnotherModalComponent())
618 for (auto& s
: shortcuts
)
619 if (s
.isCurrentlyDown())
625 bool Button::isRegisteredForShortcut (const KeyPress
& key
) const
627 for (auto& s
: shortcuts
)
634 bool Button::keyStateChangedCallback()
639 const bool wasDown
= isKeyDown
;
640 isKeyDown
= isShortcutPressed();
642 if (autoRepeatDelay
>= 0 && (isKeyDown
&& ! wasDown
))
643 callbackHelper
->startTimer (autoRepeatDelay
);
647 if (isEnabled() && wasDown
&& ! isKeyDown
)
649 internalClickCallback (ModifierKeys::currentModifiers
);
651 // (return immediately - this button may now have been deleted)
655 return wasDown
|| isKeyDown
;
658 bool Button::keyPressed (const KeyPress
& key
)
660 if (isEnabled() && key
.isKeyCode (KeyPress::returnKey
))
669 //==============================================================================
670 void Button::setRepeatSpeed (int initialDelayMillisecs
,
672 int minimumDelayInMillisecs
) noexcept
674 autoRepeatDelay
= initialDelayMillisecs
;
675 autoRepeatSpeed
= repeatMillisecs
;
676 autoRepeatMinimumDelay
= jmin (autoRepeatSpeed
, minimumDelayInMillisecs
);
679 void Button::repeatTimerCallback()
683 callbackHelper
->stopTimer();
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
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();
746 String
getTitle() const override
748 auto title
= AccessibilityHandler::getTitle();
751 return button
.getButtonText();
756 String
getHelp() const override
{ return button
.getTooltip(); }
759 class ButtonValueInterface
: public AccessibilityTextValueInterface
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
{}
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
); });
795 static Interfaces
getAccessibilityInterfaces (Button
& button
)
797 if (button
.isToggleable())
798 return { std::make_unique
<ButtonValueInterface
> (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
);