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.

722 lines
20KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2022 - 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 7 End-User License
  8. Agreement and JUCE Privacy Policy.
  9. End User License Agreement: www.juce.com/juce-7-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 final : 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::checkToggleableState (bool wasToggleable)
  114. {
  115. if (isToggleable() != wasToggleable)
  116. invalidateAccessibilityHandler();
  117. }
  118. void Button::setToggleable (bool isNowToggleable)
  119. {
  120. const auto wasToggleable = isToggleable();
  121. canBeToggled = isNowToggleable;
  122. checkToggleableState (wasToggleable);
  123. }
  124. void Button::setToggleState (bool shouldBeOn, NotificationType notification)
  125. {
  126. setToggleState (shouldBeOn, notification, notification);
  127. }
  128. void Button::setToggleState (bool shouldBeOn, NotificationType clickNotification, NotificationType stateNotification)
  129. {
  130. if (shouldBeOn != lastToggleState)
  131. {
  132. WeakReference<Component> deletionWatcher (this);
  133. if (shouldBeOn)
  134. {
  135. turnOffOtherButtonsInGroup (clickNotification, stateNotification);
  136. if (deletionWatcher == nullptr)
  137. return;
  138. }
  139. // This test is done so that if the value is void rather than explicitly set to
  140. // false, the value won't be changed unless the required value is true.
  141. if (getToggleState() != shouldBeOn)
  142. {
  143. isOn = shouldBeOn;
  144. if (deletionWatcher == nullptr)
  145. return;
  146. }
  147. lastToggleState = shouldBeOn;
  148. repaint();
  149. if (clickNotification != dontSendNotification)
  150. {
  151. // async callbacks aren't possible here
  152. jassert (clickNotification != sendNotificationAsync);
  153. sendClickMessage (ModifierKeys::currentModifiers);
  154. if (deletionWatcher == nullptr)
  155. return;
  156. }
  157. if (stateNotification != dontSendNotification)
  158. sendStateMessage();
  159. else
  160. buttonStateChanged();
  161. if (auto* handler = getAccessibilityHandler())
  162. handler->notifyAccessibilityEvent (AccessibilityEvent::valueChanged);
  163. }
  164. }
  165. void Button::setToggleState (bool shouldBeOn, bool sendChange)
  166. {
  167. setToggleState (shouldBeOn, sendChange ? sendNotification : dontSendNotification);
  168. }
  169. void Button::setClickingTogglesState (bool shouldToggle) noexcept
  170. {
  171. const auto wasToggleable = isToggleable();
  172. clickTogglesState = shouldToggle;
  173. checkToggleableState (wasToggleable);
  174. // if you've got clickTogglesState turned on, you shouldn't also connect the button
  175. // up to be a command invoker. Instead, your command handler must flip the state of whatever
  176. // it is that this button represents, and the button will update its state to reflect this
  177. // in the applicationCommandListChanged() method.
  178. jassert (commandManagerToUse == nullptr || ! clickTogglesState);
  179. }
  180. void Button::setRadioGroupId (int newGroupId, NotificationType notification)
  181. {
  182. if (radioGroupId != newGroupId)
  183. {
  184. radioGroupId = newGroupId;
  185. if (lastToggleState)
  186. turnOffOtherButtonsInGroup (notification, notification);
  187. setToggleable (true);
  188. invalidateAccessibilityHandler();
  189. }
  190. }
  191. void Button::turnOffOtherButtonsInGroup (NotificationType clickNotification, NotificationType stateNotification)
  192. {
  193. if (auto* p = getParentComponent())
  194. {
  195. if (radioGroupId != 0)
  196. {
  197. WeakReference<Component> deletionWatcher (this);
  198. for (auto* c : p->getChildren())
  199. {
  200. if (c != this)
  201. {
  202. if (auto b = dynamic_cast<Button*> (c))
  203. {
  204. if (b->getRadioGroupId() == radioGroupId)
  205. {
  206. b->setToggleState (false, clickNotification, stateNotification);
  207. if (deletionWatcher == nullptr)
  208. return;
  209. }
  210. }
  211. }
  212. }
  213. }
  214. }
  215. }
  216. //==============================================================================
  217. void Button::enablementChanged()
  218. {
  219. updateState();
  220. repaint();
  221. }
  222. Button::ButtonState Button::updateState()
  223. {
  224. return updateState (isMouseOver (true), isMouseButtonDown());
  225. }
  226. Button::ButtonState Button::updateState (bool over, bool down)
  227. {
  228. ButtonState newState = buttonNormal;
  229. if (isEnabled() && isVisible() && ! isCurrentlyBlockedByAnotherModalComponent())
  230. {
  231. if ((down && (over || (triggerOnMouseDown && buttonState == buttonDown))) || isKeyDown)
  232. newState = buttonDown;
  233. else if (over)
  234. newState = buttonOver;
  235. }
  236. setState (newState);
  237. return newState;
  238. }
  239. void Button::setState (ButtonState newState)
  240. {
  241. if (buttonState != newState)
  242. {
  243. buttonState = newState;
  244. repaint();
  245. if (buttonState == buttonDown)
  246. {
  247. buttonPressTime = Time::getApproximateMillisecondCounter();
  248. lastRepeatTime = 0;
  249. }
  250. sendStateMessage();
  251. }
  252. }
  253. bool Button::isDown() const noexcept { return buttonState == buttonDown; }
  254. bool Button::isOver() const noexcept { return buttonState != buttonNormal; }
  255. void Button::buttonStateChanged() {}
  256. uint32 Button::getMillisecondsSinceButtonDown() const noexcept
  257. {
  258. auto now = Time::getApproximateMillisecondCounter();
  259. return now > buttonPressTime ? now - buttonPressTime : 0;
  260. }
  261. void Button::setTriggeredOnMouseDown (bool isTriggeredOnMouseDown) noexcept
  262. {
  263. triggerOnMouseDown = isTriggeredOnMouseDown;
  264. }
  265. bool Button::getTriggeredOnMouseDown() const noexcept
  266. {
  267. return triggerOnMouseDown;
  268. }
  269. //==============================================================================
  270. void Button::clicked()
  271. {
  272. }
  273. void Button::clicked (const ModifierKeys&)
  274. {
  275. clicked();
  276. }
  277. enum { clickMessageId = 0x2f3f4f99 };
  278. void Button::triggerClick()
  279. {
  280. postCommandMessage (clickMessageId);
  281. }
  282. void Button::internalClickCallback (const ModifierKeys& modifiers)
  283. {
  284. if (clickTogglesState)
  285. {
  286. const bool shouldBeOn = (radioGroupId != 0 || ! lastToggleState);
  287. if (shouldBeOn != getToggleState())
  288. {
  289. setToggleState (shouldBeOn, sendNotification);
  290. return;
  291. }
  292. }
  293. sendClickMessage (modifiers);
  294. }
  295. void Button::flashButtonState()
  296. {
  297. if (isEnabled())
  298. {
  299. needsToRelease = true;
  300. setState (buttonDown);
  301. callbackHelper->startTimer (100);
  302. }
  303. }
  304. void Button::handleCommandMessage (int commandId)
  305. {
  306. if (commandId == clickMessageId)
  307. {
  308. if (isEnabled())
  309. {
  310. flashButtonState();
  311. internalClickCallback (ModifierKeys::currentModifiers);
  312. }
  313. }
  314. else
  315. {
  316. Component::handleCommandMessage (commandId);
  317. }
  318. }
  319. //==============================================================================
  320. void Button::addListener (Listener* l) { buttonListeners.add (l); }
  321. void Button::removeListener (Listener* l) { buttonListeners.remove (l); }
  322. void Button::sendClickMessage (const ModifierKeys& modifiers)
  323. {
  324. Component::BailOutChecker checker (this);
  325. if (commandManagerToUse != nullptr && commandID != 0)
  326. {
  327. ApplicationCommandTarget::InvocationInfo info (commandID);
  328. info.invocationMethod = ApplicationCommandTarget::InvocationInfo::fromButton;
  329. info.originatingComponent = this;
  330. commandManagerToUse->invoke (info, true);
  331. }
  332. clicked (modifiers);
  333. if (checker.shouldBailOut())
  334. return;
  335. buttonListeners.callChecked (checker, [this] (Listener& l) { l.buttonClicked (this); });
  336. if (checker.shouldBailOut())
  337. return;
  338. NullCheckedInvocation::invoke (onClick);
  339. }
  340. void Button::sendStateMessage()
  341. {
  342. Component::BailOutChecker checker (this);
  343. buttonStateChanged();
  344. if (checker.shouldBailOut())
  345. return;
  346. buttonListeners.callChecked (checker, [this] (Listener& l) { l.buttonStateChanged (this); });
  347. if (checker.shouldBailOut())
  348. return;
  349. NullCheckedInvocation::invoke (onStateChange);
  350. }
  351. //==============================================================================
  352. void Button::paint (Graphics& g)
  353. {
  354. if (needsToRelease && isEnabled())
  355. {
  356. needsToRelease = false;
  357. needsRepainting = true;
  358. }
  359. paintButton (g, isOver(), isDown());
  360. lastStatePainted = buttonState;
  361. }
  362. //==============================================================================
  363. void Button::mouseEnter (const MouseEvent&) { updateState (true, false); }
  364. void Button::mouseExit (const MouseEvent&) { updateState (false, false); }
  365. void Button::mouseDown (const MouseEvent& e)
  366. {
  367. updateState (true, true);
  368. if (isDown())
  369. {
  370. if (autoRepeatDelay >= 0)
  371. callbackHelper->startTimer (autoRepeatDelay);
  372. if (triggerOnMouseDown)
  373. internalClickCallback (e.mods);
  374. }
  375. }
  376. void Button::mouseUp (const MouseEvent& e)
  377. {
  378. const auto wasDown = isDown();
  379. const auto wasOver = isOver();
  380. updateState (isMouseSourceOver (e), false);
  381. if (wasDown && wasOver && ! triggerOnMouseDown)
  382. {
  383. if (lastStatePainted != buttonDown)
  384. flashButtonState();
  385. WeakReference<Component> deletionWatcher (this);
  386. internalClickCallback (e.mods);
  387. if (deletionWatcher != nullptr)
  388. updateState (isMouseSourceOver (e), false);
  389. }
  390. }
  391. void Button::mouseDrag (const MouseEvent& e)
  392. {
  393. auto oldState = buttonState;
  394. updateState (isMouseSourceOver (e), true);
  395. if (autoRepeatDelay >= 0 && buttonState != oldState && isDown())
  396. callbackHelper->startTimer (autoRepeatSpeed);
  397. }
  398. bool Button::isMouseSourceOver (const MouseEvent& e)
  399. {
  400. if (e.source.isTouch() || e.source.isPen())
  401. return getLocalBounds().toFloat().contains (e.position);
  402. return isMouseOver();
  403. }
  404. void Button::focusGained (FocusChangeType)
  405. {
  406. updateState();
  407. repaint();
  408. }
  409. void Button::focusLost (FocusChangeType)
  410. {
  411. updateState();
  412. repaint();
  413. }
  414. void Button::visibilityChanged()
  415. {
  416. needsToRelease = false;
  417. updateState();
  418. }
  419. void Button::parentHierarchyChanged()
  420. {
  421. auto* newKeySource = shortcuts.isEmpty() ? nullptr : getTopLevelComponent();
  422. if (newKeySource != keySource.get())
  423. {
  424. if (keySource != nullptr)
  425. keySource->removeKeyListener (callbackHelper.get());
  426. keySource = newKeySource;
  427. if (keySource != nullptr)
  428. keySource->addKeyListener (callbackHelper.get());
  429. }
  430. }
  431. //==============================================================================
  432. void Button::setCommandToTrigger (ApplicationCommandManager* newCommandManager,
  433. CommandID newCommandID, bool generateTip)
  434. {
  435. commandID = newCommandID;
  436. generateTooltip = generateTip;
  437. if (commandManagerToUse != newCommandManager)
  438. {
  439. if (commandManagerToUse != nullptr)
  440. commandManagerToUse->removeListener (callbackHelper.get());
  441. commandManagerToUse = newCommandManager;
  442. if (commandManagerToUse != nullptr)
  443. commandManagerToUse->addListener (callbackHelper.get());
  444. // if you've got clickTogglesState turned on, you shouldn't also connect the button
  445. // up to be a command invoker. Instead, your command handler must flip the state of whatever
  446. // it is that this button represents, and the button will update its state to reflect this
  447. // in the applicationCommandListChanged() method.
  448. jassert (commandManagerToUse == nullptr || ! clickTogglesState);
  449. }
  450. if (commandManagerToUse != nullptr)
  451. applicationCommandListChangeCallback();
  452. else
  453. setEnabled (true);
  454. }
  455. void Button::applicationCommandListChangeCallback()
  456. {
  457. if (commandManagerToUse != nullptr)
  458. {
  459. ApplicationCommandInfo info (0);
  460. if (commandManagerToUse->getTargetForCommand (commandID, info) != nullptr)
  461. {
  462. updateAutomaticTooltip (info);
  463. setEnabled ((info.flags & ApplicationCommandInfo::isDisabled) == 0);
  464. setToggleState ((info.flags & ApplicationCommandInfo::isTicked) != 0, dontSendNotification);
  465. }
  466. else
  467. {
  468. setEnabled (false);
  469. }
  470. }
  471. }
  472. //==============================================================================
  473. void Button::addShortcut (const KeyPress& key)
  474. {
  475. if (key.isValid())
  476. {
  477. jassert (! isRegisteredForShortcut (key)); // already registered!
  478. shortcuts.add (key);
  479. parentHierarchyChanged();
  480. }
  481. }
  482. void Button::clearShortcuts()
  483. {
  484. shortcuts.clear();
  485. parentHierarchyChanged();
  486. }
  487. bool Button::isShortcutPressed() const
  488. {
  489. if (isShowing() && ! isCurrentlyBlockedByAnotherModalComponent())
  490. for (auto& s : shortcuts)
  491. if (s.isCurrentlyDown())
  492. return true;
  493. return false;
  494. }
  495. bool Button::isRegisteredForShortcut (const KeyPress& key) const
  496. {
  497. for (auto& s : shortcuts)
  498. if (key == s)
  499. return true;
  500. return false;
  501. }
  502. bool Button::keyStateChangedCallback()
  503. {
  504. if (! isEnabled())
  505. return false;
  506. const bool wasDown = isKeyDown;
  507. isKeyDown = isShortcutPressed();
  508. if (autoRepeatDelay >= 0 && (isKeyDown && ! wasDown))
  509. callbackHelper->startTimer (autoRepeatDelay);
  510. updateState();
  511. if (isEnabled() && wasDown && ! isKeyDown)
  512. {
  513. internalClickCallback (ModifierKeys::currentModifiers);
  514. // (return immediately - this button may now have been deleted)
  515. return true;
  516. }
  517. return wasDown || isKeyDown;
  518. }
  519. bool Button::keyPressed (const KeyPress& key)
  520. {
  521. if (isEnabled() && key.isKeyCode (KeyPress::returnKey))
  522. {
  523. triggerClick();
  524. return true;
  525. }
  526. return false;
  527. }
  528. //==============================================================================
  529. void Button::setRepeatSpeed (int initialDelayMillisecs,
  530. int repeatMillisecs,
  531. int minimumDelayInMillisecs) noexcept
  532. {
  533. autoRepeatDelay = initialDelayMillisecs;
  534. autoRepeatSpeed = repeatMillisecs;
  535. autoRepeatMinimumDelay = jmin (autoRepeatSpeed, minimumDelayInMillisecs);
  536. }
  537. void Button::repeatTimerCallback()
  538. {
  539. if (needsRepainting)
  540. {
  541. callbackHelper->stopTimer();
  542. updateState();
  543. needsRepainting = false;
  544. }
  545. else if (autoRepeatSpeed > 0 && (isKeyDown || (updateState() == buttonDown)))
  546. {
  547. auto repeatSpeed = autoRepeatSpeed;
  548. if (autoRepeatMinimumDelay >= 0)
  549. {
  550. auto timeHeldDown = jmin (1.0, getMillisecondsSinceButtonDown() / 4000.0);
  551. timeHeldDown *= timeHeldDown;
  552. repeatSpeed = repeatSpeed + (int) (timeHeldDown * (autoRepeatMinimumDelay - repeatSpeed));
  553. }
  554. repeatSpeed = jmax (1, repeatSpeed);
  555. auto now = Time::getMillisecondCounter();
  556. // if we've been blocked from repeating often enough, speed up the repeat timer to compensate..
  557. if (lastRepeatTime != 0 && (int) (now - lastRepeatTime) > repeatSpeed * 2)
  558. repeatSpeed = jmax (1, repeatSpeed / 2);
  559. lastRepeatTime = now;
  560. callbackHelper->startTimer (repeatSpeed);
  561. internalClickCallback (ModifierKeys::currentModifiers);
  562. }
  563. else if (! needsToRelease)
  564. {
  565. callbackHelper->stopTimer();
  566. }
  567. }
  568. std::unique_ptr<AccessibilityHandler> Button::createAccessibilityHandler()
  569. {
  570. return std::make_unique<detail::ButtonAccessibilityHandler> (*this, AccessibilityRole::button);
  571. }
  572. } // namespace juce