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.

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