Audio plugin host https://kx.studio/carla
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.

808 lines
22KB

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