1 /*
2   ==============================================================================
3 
4    This file is part of the JUCE library.
5    Copyright (c) 2020 - Raw Material Software Limited
6 
7    JUCE is an open source library subject to commercial or open-source
8    licensing.
9 
10    By using JUCE, you agree to the terms of both the JUCE 6 End-User License
11    Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
12 
13    End User License Agreement: www.juce.com/juce-6-licence
14    Privacy Policy: www.juce.com/juce-privacy-policy
15 
16    Or: You may also use this code under the terms of the GPL v3 (see
17    www.gnu.org/licenses).
18 
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.
22 
23   ==============================================================================
24 */
25 
26 namespace juce
27 {
28 
29 struct Button::CallbackHelper  : public Timer,
30                                  public ApplicationCommandManagerListener,
31                                  public Value::Listener,
32                                  public KeyListener
33 {
CallbackHelperjuce::Button::CallbackHelper34     CallbackHelper (Button& b) : button (b)   {}
35 
timerCallbackjuce::Button::CallbackHelper36     void timerCallback() override
37     {
38         button.repeatTimerCallback();
39     }
40 
keyStateChangedjuce::Button::CallbackHelper41     bool keyStateChanged (bool, Component*) override
42     {
43         return button.keyStateChangedCallback();
44     }
45 
valueChangedjuce::Button::CallbackHelper46     void valueChanged (Value& value) override
47     {
48         if (value.refersToSameSourceAs (button.isOn))
49             button.setToggleState (button.isOn.getValue(), dontSendNotification, sendNotification);
50     }
51 
keyPressedjuce::Button::CallbackHelper52     bool keyPressed (const KeyPress&, Component*) override
53     {
54         // returning true will avoid forwarding events for keys that we're using as shortcuts
55         return button.isShortcutPressed();
56     }
57 
applicationCommandInvokedjuce::Button::CallbackHelper58     void applicationCommandInvoked (const ApplicationCommandTarget::InvocationInfo& info) override
59     {
60         if (info.commandID == button.commandID
61              && (info.commandFlags & ApplicationCommandInfo::dontTriggerVisualFeedback) == 0)
62             button.flashButtonState();
63     }
64 
applicationCommandListChangedjuce::Button::CallbackHelper65     void applicationCommandListChanged() override
66     {
67         button.applicationCommandListChangeCallback();
68     }
69 
70     Button& button;
71 
72     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CallbackHelper)
73 };
74 
75 //==============================================================================
Button(const String & name)76 Button::Button (const String& name)  : Component (name), text (name)
77 {
78     callbackHelper.reset (new CallbackHelper (*this));
79 
80     setWantsKeyboardFocus (true);
81     isOn.addListener (callbackHelper.get());
82 }
83 
~Button()84 Button::~Button()
85 {
86     clearShortcuts();
87 
88     if (commandManagerToUse != nullptr)
89         commandManagerToUse->removeListener (callbackHelper.get());
90 
91     isOn.removeListener (callbackHelper.get());
92     callbackHelper.reset();
93 }
94 
95 //==============================================================================
setButtonText(const String & newText)96 void Button::setButtonText (const String& newText)
97 {
98     if (text != newText)
99     {
100         text = newText;
101         repaint();
102     }
103 }
104 
setTooltip(const String & newTooltip)105 void Button::setTooltip (const String& newTooltip)
106 {
107     SettableTooltipClient::setTooltip (newTooltip);
108     generateTooltip = false;
109 }
110 
updateAutomaticTooltip(const ApplicationCommandInfo & info)111 void Button::updateAutomaticTooltip (const ApplicationCommandInfo& info)
112 {
113     if (generateTooltip && commandManagerToUse != nullptr)
114     {
115         auto tt = info.description.isNotEmpty() ? info.description
116                                                 : info.shortName;
117 
118         for (auto& kp : commandManagerToUse->getKeyMappings()->getKeyPressesAssignedToCommand (commandID))
119         {
120             auto key = kp.getTextDescription();
121 
122             tt << " [";
123 
124             if (key.length() == 1)
125                 tt << TRANS("shortcut") << ": '" << key << "']";
126             else
127                 tt << key << ']';
128         }
129 
130         SettableTooltipClient::setTooltip (tt);
131     }
132 }
133 
setConnectedEdges(int newFlags)134 void Button::setConnectedEdges (int newFlags)
135 {
136     if (connectedEdgeFlags != newFlags)
137     {
138         connectedEdgeFlags = newFlags;
139         repaint();
140     }
141 }
142 
143 //==============================================================================
setToggleState(bool shouldBeOn,NotificationType notification)144 void Button::setToggleState (bool shouldBeOn, NotificationType notification)
145 {
146     setToggleState (shouldBeOn, notification, notification);
147 }
148 
setToggleState(bool shouldBeOn,NotificationType clickNotification,NotificationType stateNotification)149 void Button::setToggleState (bool shouldBeOn, NotificationType clickNotification, NotificationType stateNotification)
150 {
151     if (shouldBeOn != lastToggleState)
152     {
153         WeakReference<Component> deletionWatcher (this);
154 
155         if (shouldBeOn)
156         {
157             turnOffOtherButtonsInGroup (clickNotification, stateNotification);
158 
159             if (deletionWatcher == nullptr)
160                 return;
161         }
162 
163         // This test is done so that if the value is void rather than explicitly set to
164         // false, the value won't be changed unless the required value is true.
165         if (getToggleState() != shouldBeOn)
166         {
167             isOn = shouldBeOn;
168 
169             if (deletionWatcher == nullptr)
170                 return;
171         }
172 
173         lastToggleState = shouldBeOn;
174         repaint();
175 
176         if (clickNotification != dontSendNotification)
177         {
178             // async callbacks aren't possible here
179             jassert (clickNotification != sendNotificationAsync);
180 
181             sendClickMessage (ModifierKeys::currentModifiers);
182 
183             if (deletionWatcher == nullptr)
184                 return;
185         }
186 
187         if (stateNotification != dontSendNotification)
188             sendStateMessage();
189         else
190             buttonStateChanged();
191     }
192 }
193 
setToggleState(bool shouldBeOn,bool sendChange)194 void Button::setToggleState (bool shouldBeOn, bool sendChange)
195 {
196     setToggleState (shouldBeOn, sendChange ? sendNotification : dontSendNotification);
197 }
198 
setClickingTogglesState(bool shouldToggle)199 void Button::setClickingTogglesState (bool shouldToggle) noexcept
200 {
201     clickTogglesState = shouldToggle;
202 
203     // if you've got clickTogglesState turned on, you shouldn't also connect the button
204     // up to be a command invoker. Instead, your command handler must flip the state of whatever
205     // it is that this button represents, and the button will update its state to reflect this
206     // in the applicationCommandListChanged() method.
207     jassert (commandManagerToUse == nullptr || ! clickTogglesState);
208 }
209 
getClickingTogglesState() const210 bool Button::getClickingTogglesState() const noexcept
211 {
212     return clickTogglesState;
213 }
214 
setRadioGroupId(int newGroupId,NotificationType notification)215 void Button::setRadioGroupId (int newGroupId, NotificationType notification)
216 {
217     if (radioGroupId != newGroupId)
218     {
219         radioGroupId = newGroupId;
220 
221         if (lastToggleState)
222             turnOffOtherButtonsInGroup (notification, notification);
223     }
224 }
225 
turnOffOtherButtonsInGroup(NotificationType clickNotification,NotificationType stateNotification)226 void Button::turnOffOtherButtonsInGroup (NotificationType clickNotification, NotificationType stateNotification)
227 {
228     if (auto* p = getParentComponent())
229     {
230         if (radioGroupId != 0)
231         {
232             WeakReference<Component> deletionWatcher (this);
233 
234             for (auto* c : p->getChildren())
235             {
236                 if (c != this)
237                 {
238                     if (auto b = dynamic_cast<Button*> (c))
239                     {
240                         if (b->getRadioGroupId() == radioGroupId)
241                         {
242                             b->setToggleState (false, clickNotification, stateNotification);
243 
244                             if (deletionWatcher == nullptr)
245                                 return;
246                         }
247                     }
248                 }
249             }
250         }
251     }
252 }
253 
254 //==============================================================================
enablementChanged()255 void Button::enablementChanged()
256 {
257     updateState();
258     repaint();
259 }
260 
updateState()261 Button::ButtonState Button::updateState()
262 {
263     return updateState (isMouseOver (true), isMouseButtonDown());
264 }
265 
updateState(bool over,bool down)266 Button::ButtonState Button::updateState (bool over, bool down)
267 {
268     ButtonState newState = buttonNormal;
269 
270     if (isEnabled() && isVisible() && ! isCurrentlyBlockedByAnotherModalComponent())
271     {
272         if ((down && (over || (triggerOnMouseDown && buttonState == buttonDown))) || isKeyDown)
273             newState = buttonDown;
274         else if (over)
275             newState = buttonOver;
276     }
277 
278     setState (newState);
279     return newState;
280 }
281 
setState(ButtonState newState)282 void Button::setState (ButtonState newState)
283 {
284     if (buttonState != newState)
285     {
286         buttonState = newState;
287         repaint();
288 
289         if (buttonState == buttonDown)
290         {
291             buttonPressTime = Time::getApproximateMillisecondCounter();
292             lastRepeatTime = 0;
293         }
294 
295         sendStateMessage();
296     }
297 }
298 
isDown() const299 bool Button::isDown() const noexcept    { return buttonState == buttonDown; }
isOver() const300 bool Button::isOver() const noexcept    { return buttonState != buttonNormal; }
301 
buttonStateChanged()302 void Button::buttonStateChanged() {}
303 
getMillisecondsSinceButtonDown() const304 uint32 Button::getMillisecondsSinceButtonDown() const noexcept
305 {
306     auto now = Time::getApproximateMillisecondCounter();
307     return now > buttonPressTime ? now - buttonPressTime : 0;
308 }
309 
setTriggeredOnMouseDown(bool isTriggeredOnMouseDown)310 void Button::setTriggeredOnMouseDown (bool isTriggeredOnMouseDown) noexcept
311 {
312     triggerOnMouseDown = isTriggeredOnMouseDown;
313 }
314 
getTriggeredOnMouseDown() const315 bool Button::getTriggeredOnMouseDown() const noexcept
316 {
317     return triggerOnMouseDown;
318 }
319 
320 //==============================================================================
clicked()321 void Button::clicked()
322 {
323 }
324 
clicked(const ModifierKeys &)325 void Button::clicked (const ModifierKeys&)
326 {
327     clicked();
328 }
329 
330 enum { clickMessageId = 0x2f3f4f99 };
331 
triggerClick()332 void Button::triggerClick()
333 {
334     postCommandMessage (clickMessageId);
335 }
336 
internalClickCallback(const ModifierKeys & modifiers)337 void Button::internalClickCallback (const ModifierKeys& modifiers)
338 {
339     if (clickTogglesState)
340     {
341         const bool shouldBeOn = (radioGroupId != 0 || ! lastToggleState);
342 
343         if (shouldBeOn != getToggleState())
344         {
345             setToggleState (shouldBeOn, sendNotification);
346             return;
347         }
348     }
349 
350     sendClickMessage (modifiers);
351 }
352 
flashButtonState()353 void Button::flashButtonState()
354 {
355     if (isEnabled())
356     {
357         needsToRelease = true;
358         setState (buttonDown);
359         callbackHelper->startTimer (100);
360     }
361 }
362 
handleCommandMessage(int commandId)363 void Button::handleCommandMessage (int commandId)
364 {
365     if (commandId == clickMessageId)
366     {
367         if (isEnabled())
368         {
369             flashButtonState();
370             internalClickCallback (ModifierKeys::currentModifiers);
371         }
372     }
373     else
374     {
375         Component::handleCommandMessage (commandId);
376     }
377 }
378 
379 //==============================================================================
addListener(Listener * l)380 void Button::addListener (Listener* l)      { buttonListeners.add (l); }
removeListener(Listener * l)381 void Button::removeListener (Listener* l)   { buttonListeners.remove (l); }
382 
sendClickMessage(const ModifierKeys & modifiers)383 void Button::sendClickMessage (const ModifierKeys& modifiers)
384 {
385     Component::BailOutChecker checker (this);
386 
387     if (commandManagerToUse != nullptr && commandID != 0)
388     {
389         ApplicationCommandTarget::InvocationInfo info (commandID);
390         info.invocationMethod = ApplicationCommandTarget::InvocationInfo::fromButton;
391         info.originatingComponent = this;
392 
393         commandManagerToUse->invoke (info, true);
394     }
395 
396     clicked (modifiers);
397 
398     if (checker.shouldBailOut())
399         return;
400 
401     buttonListeners.callChecked (checker, [this] (Listener& l) { l.buttonClicked (this); });
402 
403     if (checker.shouldBailOut())
404         return;
405 
406     if (onClick != nullptr)
407         onClick();
408 }
409 
sendStateMessage()410 void Button::sendStateMessage()
411 {
412     Component::BailOutChecker checker (this);
413 
414     buttonStateChanged();
415 
416     if (checker.shouldBailOut())
417         return;
418 
419     buttonListeners.callChecked (checker, [this] (Listener& l) { l.buttonStateChanged (this); });
420 
421     if (checker.shouldBailOut())
422         return;
423 
424     if (onStateChange != nullptr)
425         onStateChange();
426 }
427 
428 //==============================================================================
paint(Graphics & g)429 void Button::paint (Graphics& g)
430 {
431     if (needsToRelease && isEnabled())
432     {
433         needsToRelease = false;
434         needsRepainting = true;
435     }
436 
437     paintButton (g, isOver(), isDown());
438     lastStatePainted = buttonState;
439 }
440 
441 //==============================================================================
mouseEnter(const MouseEvent &)442 void Button::mouseEnter (const MouseEvent&)     { updateState (true,  false); }
mouseExit(const MouseEvent &)443 void Button::mouseExit (const MouseEvent&)      { updateState (false, false); }
444 
mouseDown(const MouseEvent & e)445 void Button::mouseDown (const MouseEvent& e)
446 {
447     updateState (true, true);
448 
449     if (isDown())
450     {
451         if (autoRepeatDelay >= 0)
452             callbackHelper->startTimer (autoRepeatDelay);
453 
454         if (triggerOnMouseDown)
455             internalClickCallback (e.mods);
456     }
457 }
458 
mouseUp(const MouseEvent & e)459 void Button::mouseUp (const MouseEvent& e)
460 {
461     const bool wasDown = isDown();
462     const bool wasOver = isOver();
463     updateState (isMouseSourceOver (e), false);
464 
465     if (wasDown && wasOver && ! triggerOnMouseDown)
466     {
467         if (lastStatePainted != buttonDown)
468             flashButtonState();
469 
470         internalClickCallback (e.mods);
471     }
472 }
473 
mouseDrag(const MouseEvent & e)474 void Button::mouseDrag (const MouseEvent& e)
475 {
476     auto oldState = buttonState;
477     updateState (isMouseSourceOver (e), true);
478 
479     if (autoRepeatDelay >= 0 && buttonState != oldState && isDown())
480         callbackHelper->startTimer (autoRepeatSpeed);
481 }
482 
isMouseSourceOver(const MouseEvent & e)483 bool Button::isMouseSourceOver (const MouseEvent& e)
484 {
485     if (e.source.isTouch() || e.source.isPen())
486         return getLocalBounds().toFloat().contains (e.position);
487 
488     return isMouseOver();
489 }
490 
focusGained(FocusChangeType)491 void Button::focusGained (FocusChangeType)
492 {
493     updateState();
494     repaint();
495 }
496 
focusLost(FocusChangeType)497 void Button::focusLost (FocusChangeType)
498 {
499     updateState();
500     repaint();
501 }
502 
visibilityChanged()503 void Button::visibilityChanged()
504 {
505     needsToRelease = false;
506     updateState();
507 }
508 
parentHierarchyChanged()509 void Button::parentHierarchyChanged()
510 {
511     auto* newKeySource = shortcuts.isEmpty() ? nullptr : getTopLevelComponent();
512 
513     if (newKeySource != keySource.get())
514     {
515         if (keySource != nullptr)
516             keySource->removeKeyListener (callbackHelper.get());
517 
518         keySource = newKeySource;
519 
520         if (keySource != nullptr)
521             keySource->addKeyListener (callbackHelper.get());
522     }
523 }
524 
525 //==============================================================================
setCommandToTrigger(ApplicationCommandManager * newCommandManager,CommandID newCommandID,bool generateTip)526 void Button::setCommandToTrigger (ApplicationCommandManager* newCommandManager,
527                                   CommandID newCommandID, bool generateTip)
528 {
529     commandID = newCommandID;
530     generateTooltip = generateTip;
531 
532     if (commandManagerToUse != newCommandManager)
533     {
534         if (commandManagerToUse != nullptr)
535             commandManagerToUse->removeListener (callbackHelper.get());
536 
537         commandManagerToUse = newCommandManager;
538 
539         if (commandManagerToUse != nullptr)
540             commandManagerToUse->addListener (callbackHelper.get());
541 
542         // if you've got clickTogglesState turned on, you shouldn't also connect the button
543         // up to be a command invoker. Instead, your command handler must flip the state of whatever
544         // it is that this button represents, and the button will update its state to reflect this
545         // in the applicationCommandListChanged() method.
546         jassert (commandManagerToUse == nullptr || ! clickTogglesState);
547     }
548 
549     if (commandManagerToUse != nullptr)
550         applicationCommandListChangeCallback();
551     else
552         setEnabled (true);
553 }
554 
applicationCommandListChangeCallback()555 void Button::applicationCommandListChangeCallback()
556 {
557     if (commandManagerToUse != nullptr)
558     {
559         ApplicationCommandInfo info (0);
560 
561         if (commandManagerToUse->getTargetForCommand (commandID, info) != nullptr)
562         {
563             updateAutomaticTooltip (info);
564             setEnabled ((info.flags & ApplicationCommandInfo::isDisabled) == 0);
565             setToggleState ((info.flags & ApplicationCommandInfo::isTicked) != 0, dontSendNotification);
566         }
567         else
568         {
569             setEnabled (false);
570         }
571     }
572 }
573 
574 //==============================================================================
addShortcut(const KeyPress & key)575 void Button::addShortcut (const KeyPress& key)
576 {
577     if (key.isValid())
578     {
579         jassert (! isRegisteredForShortcut (key));  // already registered!
580 
581         shortcuts.add (key);
582         parentHierarchyChanged();
583     }
584 }
585 
clearShortcuts()586 void Button::clearShortcuts()
587 {
588     shortcuts.clear();
589     parentHierarchyChanged();
590 }
591 
isShortcutPressed() const592 bool Button::isShortcutPressed() const
593 {
594     if (isShowing() && ! isCurrentlyBlockedByAnotherModalComponent())
595         for (auto& s : shortcuts)
596             if (s.isCurrentlyDown())
597                 return true;
598 
599     return false;
600 }
601 
isRegisteredForShortcut(const KeyPress & key) const602 bool Button::isRegisteredForShortcut (const KeyPress& key) const
603 {
604     for (auto& s : shortcuts)
605         if (key == s)
606             return true;
607 
608     return false;
609 }
610 
keyStateChangedCallback()611 bool Button::keyStateChangedCallback()
612 {
613     if (! isEnabled())
614         return false;
615 
616     const bool wasDown = isKeyDown;
617     isKeyDown = isShortcutPressed();
618 
619     if (autoRepeatDelay >= 0 && (isKeyDown && ! wasDown))
620         callbackHelper->startTimer (autoRepeatDelay);
621 
622     updateState();
623 
624     if (isEnabled() && wasDown && ! isKeyDown)
625     {
626         internalClickCallback (ModifierKeys::currentModifiers);
627 
628         // (return immediately - this button may now have been deleted)
629         return true;
630     }
631 
632     return wasDown || isKeyDown;
633 }
634 
keyPressed(const KeyPress & key)635 bool Button::keyPressed (const KeyPress& key)
636 {
637     if (isEnabled() && key.isKeyCode (KeyPress::returnKey))
638     {
639         triggerClick();
640         return true;
641     }
642 
643     return false;
644 }
645 
646 //==============================================================================
setRepeatSpeed(int initialDelayMillisecs,int repeatMillisecs,int minimumDelayInMillisecs)647 void Button::setRepeatSpeed (int initialDelayMillisecs,
648                              int repeatMillisecs,
649                              int minimumDelayInMillisecs) noexcept
650 {
651     autoRepeatDelay = initialDelayMillisecs;
652     autoRepeatSpeed = repeatMillisecs;
653     autoRepeatMinimumDelay = jmin (autoRepeatSpeed, minimumDelayInMillisecs);
654 }
655 
repeatTimerCallback()656 void Button::repeatTimerCallback()
657 {
658     if (needsRepainting)
659     {
660         callbackHelper->stopTimer();
661         updateState();
662         needsRepainting = false;
663     }
664     else if (autoRepeatSpeed > 0 && (isKeyDown || (updateState() == buttonDown)))
665     {
666         auto repeatSpeed = autoRepeatSpeed;
667 
668         if (autoRepeatMinimumDelay >= 0)
669         {
670             auto timeHeldDown = jmin (1.0, getMillisecondsSinceButtonDown() / 4000.0);
671             timeHeldDown *= timeHeldDown;
672 
673             repeatSpeed = repeatSpeed + (int) (timeHeldDown * (autoRepeatMinimumDelay - repeatSpeed));
674         }
675 
676         repeatSpeed = jmax (1, repeatSpeed);
677 
678         auto now = Time::getMillisecondCounter();
679 
680         // if we've been blocked from repeating often enough, speed up the repeat timer to compensate..
681         if (lastRepeatTime != 0 && (int) (now - lastRepeatTime) > repeatSpeed * 2)
682             repeatSpeed = jmax (1, repeatSpeed / 2);
683 
684         lastRepeatTime = now;
685         callbackHelper->startTimer (repeatSpeed);
686 
687         internalClickCallback (ModifierKeys::currentModifiers);
688     }
689     else if (! needsToRelease)
690     {
691         callbackHelper->stopTimer();
692     }
693 }
694 
695 } // namespace juce
696