The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

779 lines
22KB

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