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.

699 lines
19KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2017 - ROLI Ltd.
  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 5 End-User License
  8. Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
  9. 27th April 2017).
  10. End User License Agreement: www.juce.com/juce-5-licence
  11. Privacy Policy: www.juce.com/juce-5-privacy-policy
  12. Or: You may also use this code under the terms of the GPL v3 (see
  13. www.gnu.org/licenses).
  14. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  15. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  16. DISCLAIMED.
  17. ==============================================================================
  18. */
  19. class Button::CallbackHelper : public Timer,
  20. public ApplicationCommandManagerListener,
  21. public ValueListener,
  22. public KeyListener
  23. {
  24. public:
  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(), 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. private:
  55. Button& button;
  56. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CallbackHelper)
  57. };
  58. //==============================================================================
  59. Button::Button (const String& name)
  60. : Component (name),
  61. text (name),
  62. buttonPressTime (0),
  63. lastRepeatTime (0),
  64. commandManagerToUse (nullptr),
  65. autoRepeatDelay (-1),
  66. autoRepeatSpeed (0),
  67. autoRepeatMinimumDelay (-1),
  68. radioGroupId (0),
  69. connectedEdgeFlags (0),
  70. commandID(),
  71. buttonState (buttonNormal),
  72. lastStatePainted (buttonNormal),
  73. lastToggleState (false),
  74. clickTogglesState (false),
  75. needsToRelease (false),
  76. needsRepainting (false),
  77. isKeyDown (false),
  78. triggerOnMouseDown (false),
  79. generateTooltip (false)
  80. {
  81. callbackHelper = new CallbackHelper (*this);
  82. setWantsKeyboardFocus (true);
  83. isOn.addListener (callbackHelper);
  84. }
  85. Button::~Button()
  86. {
  87. clearShortcuts();
  88. if (commandManagerToUse != nullptr)
  89. commandManagerToUse->removeListener (callbackHelper);
  90. isOn.removeListener (callbackHelper);
  91. callbackHelper = nullptr;
  92. }
  93. //==============================================================================
  94. void Button::setButtonText (const String& newText)
  95. {
  96. if (text != newText)
  97. {
  98. text = newText;
  99. repaint();
  100. }
  101. }
  102. void Button::setTooltip (const String& newTooltip)
  103. {
  104. SettableTooltipClient::setTooltip (newTooltip);
  105. generateTooltip = false;
  106. }
  107. void Button::updateAutomaticTooltip (const ApplicationCommandInfo& info)
  108. {
  109. if (generateTooltip && commandManagerToUse != nullptr)
  110. {
  111. String tt (info.description.isNotEmpty() ? info.description
  112. : info.shortName);
  113. Array<KeyPress> keyPresses (commandManagerToUse->getKeyMappings()->getKeyPressesAssignedToCommand (commandID));
  114. for (int i = 0; i < keyPresses.size(); ++i)
  115. {
  116. const String key (keyPresses.getReference(i).getTextDescription());
  117. tt << " [";
  118. if (key.length() == 1)
  119. tt << TRANS("shortcut") << ": '" << key << "']";
  120. else
  121. tt << key << ']';
  122. }
  123. SettableTooltipClient::setTooltip (tt);
  124. }
  125. }
  126. void Button::setConnectedEdges (const int newFlags)
  127. {
  128. if (connectedEdgeFlags != newFlags)
  129. {
  130. connectedEdgeFlags = newFlags;
  131. repaint();
  132. }
  133. }
  134. //==============================================================================
  135. void Button::setToggleState (const bool shouldBeOn, const NotificationType notification)
  136. {
  137. if (shouldBeOn != lastToggleState)
  138. {
  139. WeakReference<Component> deletionWatcher (this);
  140. if (shouldBeOn)
  141. {
  142. turnOffOtherButtonsInGroup (notification);
  143. if (deletionWatcher == nullptr)
  144. return;
  145. }
  146. // This test is done so that if the value is void rather than explicitly set to
  147. // false, the value won't be changed unless the required value is true.
  148. if (getToggleState() != shouldBeOn)
  149. {
  150. isOn = shouldBeOn;
  151. if (deletionWatcher == nullptr)
  152. return;
  153. }
  154. lastToggleState = shouldBeOn;
  155. repaint();
  156. if (notification != dontSendNotification)
  157. {
  158. // async callbacks aren't possible here
  159. jassert (notification != sendNotificationAsync);
  160. sendClickMessage (ModifierKeys::getCurrentModifiers());
  161. if (deletionWatcher == nullptr)
  162. return;
  163. }
  164. if (notification != dontSendNotification)
  165. sendStateMessage();
  166. else
  167. buttonStateChanged();
  168. }
  169. }
  170. void Button::setToggleState (const bool shouldBeOn, bool sendChange)
  171. {
  172. setToggleState (shouldBeOn, sendChange ? sendNotification : dontSendNotification);
  173. }
  174. void Button::setClickingTogglesState (const bool shouldToggle) noexcept
  175. {
  176. clickTogglesState = shouldToggle;
  177. // if you've got clickTogglesState turned on, you shouldn't also connect the button
  178. // up to be a command invoker. Instead, your command handler must flip the state of whatever
  179. // it is that this button represents, and the button will update its state to reflect this
  180. // in the applicationCommandListChanged() method.
  181. jassert (commandManagerToUse == nullptr || ! clickTogglesState);
  182. }
  183. bool Button::getClickingTogglesState() const noexcept
  184. {
  185. return clickTogglesState;
  186. }
  187. void Button::setRadioGroupId (const int newGroupId, NotificationType notification)
  188. {
  189. if (radioGroupId != newGroupId)
  190. {
  191. radioGroupId = newGroupId;
  192. if (lastToggleState)
  193. turnOffOtherButtonsInGroup (notification);
  194. }
  195. }
  196. void Button::turnOffOtherButtonsInGroup (const NotificationType notification)
  197. {
  198. if (Component* const p = getParentComponent())
  199. {
  200. if (radioGroupId != 0)
  201. {
  202. WeakReference<Component> deletionWatcher (this);
  203. for (int i = p->getNumChildComponents(); --i >= 0;)
  204. {
  205. Component* const c = p->getChildComponent (i);
  206. if (c != this)
  207. {
  208. if (Button* const b = dynamic_cast<Button*> (c))
  209. {
  210. if (b->getRadioGroupId() == radioGroupId)
  211. {
  212. b->setToggleState (false, notification);
  213. if (deletionWatcher == nullptr)
  214. return;
  215. }
  216. }
  217. }
  218. }
  219. }
  220. }
  221. }
  222. //==============================================================================
  223. void Button::enablementChanged()
  224. {
  225. updateState();
  226. repaint();
  227. }
  228. Button::ButtonState Button::updateState()
  229. {
  230. return updateState (isMouseOver (true), isMouseButtonDown());
  231. }
  232. Button::ButtonState Button::updateState (const bool over, const bool down)
  233. {
  234. ButtonState newState = buttonNormal;
  235. if (isEnabled() && isVisible() && ! isCurrentlyBlockedByAnotherModalComponent())
  236. {
  237. if ((down && (over || (triggerOnMouseDown && buttonState == buttonDown))) || isKeyDown)
  238. newState = buttonDown;
  239. else if (over)
  240. newState = buttonOver;
  241. }
  242. setState (newState);
  243. return newState;
  244. }
  245. void Button::setState (const ButtonState newState)
  246. {
  247. if (buttonState != newState)
  248. {
  249. buttonState = newState;
  250. repaint();
  251. if (buttonState == buttonDown)
  252. {
  253. buttonPressTime = Time::getApproximateMillisecondCounter();
  254. lastRepeatTime = 0;
  255. }
  256. sendStateMessage();
  257. }
  258. }
  259. bool Button::isDown() const noexcept { return buttonState == buttonDown; }
  260. bool Button::isOver() const noexcept { return buttonState != buttonNormal; }
  261. void Button::buttonStateChanged() {}
  262. uint32 Button::getMillisecondsSinceButtonDown() const noexcept
  263. {
  264. const uint32 now = Time::getApproximateMillisecondCounter();
  265. return now > buttonPressTime ? now - buttonPressTime : 0;
  266. }
  267. void Button::setTriggeredOnMouseDown (const bool isTriggeredOnMouseDown) noexcept
  268. {
  269. triggerOnMouseDown = isTriggeredOnMouseDown;
  270. }
  271. //==============================================================================
  272. void Button::clicked()
  273. {
  274. }
  275. void Button::clicked (const ModifierKeys&)
  276. {
  277. clicked();
  278. }
  279. enum { clickMessageId = 0x2f3f4f99 };
  280. void Button::triggerClick()
  281. {
  282. postCommandMessage (clickMessageId);
  283. }
  284. void Button::internalClickCallback (const ModifierKeys& modifiers)
  285. {
  286. if (clickTogglesState)
  287. {
  288. const bool shouldBeOn = (radioGroupId != 0 || ! lastToggleState);
  289. if (shouldBeOn != getToggleState())
  290. {
  291. setToggleState (shouldBeOn, sendNotification);
  292. return;
  293. }
  294. }
  295. sendClickMessage (modifiers);
  296. }
  297. void Button::flashButtonState()
  298. {
  299. if (isEnabled())
  300. {
  301. needsToRelease = true;
  302. setState (buttonDown);
  303. callbackHelper->startTimer (100);
  304. }
  305. }
  306. void Button::handleCommandMessage (int commandId)
  307. {
  308. if (commandId == clickMessageId)
  309. {
  310. if (isEnabled())
  311. {
  312. flashButtonState();
  313. internalClickCallback (ModifierKeys::getCurrentModifiers());
  314. }
  315. }
  316. else
  317. {
  318. Component::handleCommandMessage (commandId);
  319. }
  320. }
  321. //==============================================================================
  322. void Button::addListener (ButtonListener* const newListener)
  323. {
  324. buttonListeners.add (newListener);
  325. }
  326. void Button::removeListener (ButtonListener* const listener)
  327. {
  328. buttonListeners.remove (listener);
  329. }
  330. void Button::sendClickMessage (const ModifierKeys& modifiers)
  331. {
  332. Component::BailOutChecker checker (this);
  333. if (commandManagerToUse != nullptr && commandID != 0)
  334. {
  335. ApplicationCommandTarget::InvocationInfo info (commandID);
  336. info.invocationMethod = ApplicationCommandTarget::InvocationInfo::fromButton;
  337. info.originatingComponent = this;
  338. commandManagerToUse->invoke (info, true);
  339. }
  340. clicked (modifiers);
  341. if (! checker.shouldBailOut())
  342. buttonListeners.callChecked (checker, &ButtonListener::buttonClicked, this); // (can't use Button::Listener due to idiotic VC2005 bug)
  343. }
  344. void Button::sendStateMessage()
  345. {
  346. Component::BailOutChecker checker (this);
  347. buttonStateChanged();
  348. if (! checker.shouldBailOut())
  349. buttonListeners.callChecked (checker, &ButtonListener::buttonStateChanged, this);
  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 bool wasDown = isDown();
  379. const bool wasOver = isOver();
  380. updateState (isMouseOrTouchOver (e), false);
  381. if (wasDown && wasOver && ! triggerOnMouseDown)
  382. {
  383. if (lastStatePainted != buttonDown)
  384. flashButtonState();
  385. internalClickCallback (e.mods);
  386. }
  387. }
  388. void Button::mouseDrag (const MouseEvent& e)
  389. {
  390. const ButtonState oldState = buttonState;
  391. updateState (isMouseOrTouchOver (e), true);
  392. if (autoRepeatDelay >= 0 && buttonState != oldState && isDown())
  393. callbackHelper->startTimer (autoRepeatSpeed);
  394. }
  395. bool Button::isMouseOrTouchOver (const MouseEvent& e)
  396. {
  397. if (e.source.isTouch())
  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. Component* const newKeySource = (shortcuts.size() == 0) ? nullptr : getTopLevelComponent();
  419. if (newKeySource != keySource.get())
  420. {
  421. if (keySource != nullptr)
  422. keySource->removeKeyListener (callbackHelper);
  423. keySource = newKeySource;
  424. if (keySource != nullptr)
  425. keySource->addKeyListener (callbackHelper);
  426. }
  427. }
  428. //==============================================================================
  429. void Button::setCommandToTrigger (ApplicationCommandManager* const newCommandManager,
  430. const CommandID newCommandID, const bool generateTip)
  431. {
  432. commandID = newCommandID;
  433. generateTooltip = generateTip;
  434. if (commandManagerToUse != newCommandManager)
  435. {
  436. if (commandManagerToUse != nullptr)
  437. commandManagerToUse->removeListener (callbackHelper);
  438. commandManagerToUse = newCommandManager;
  439. if (commandManagerToUse != nullptr)
  440. commandManagerToUse->addListener (callbackHelper);
  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 (int i = shortcuts.size(); --i >= 0;)
  488. if (shortcuts.getReference(i).isCurrentlyDown())
  489. return true;
  490. return false;
  491. }
  492. bool Button::isRegisteredForShortcut (const KeyPress& key) const
  493. {
  494. for (int i = shortcuts.size(); --i >= 0;)
  495. if (key == shortcuts.getReference(i))
  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::getCurrentModifiers());
  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 (const int initialDelayMillisecs,
  527. const int repeatMillisecs,
  528. const 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. int repeatSpeed = autoRepeatSpeed;
  545. if (autoRepeatMinimumDelay >= 0)
  546. {
  547. double timeHeldDown = jmin (1.0, getMillisecondsSinceButtonDown() / 4000.0);
  548. timeHeldDown *= timeHeldDown;
  549. repeatSpeed = repeatSpeed + (int) (timeHeldDown * (autoRepeatMinimumDelay - repeatSpeed));
  550. }
  551. repeatSpeed = jmax (1, repeatSpeed);
  552. const uint32 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::getCurrentModifiers());
  559. }
  560. else if (! needsToRelease)
  561. {
  562. callbackHelper->stopTimer();
  563. }
  564. }