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.

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