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.

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