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