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.

709 lines
19KB

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