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.

676 lines
18KB

  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);
  64. }
  65. Button::~Button()
  66. {
  67. clearShortcuts();
  68. if (commandManagerToUse != nullptr)
  69. commandManagerToUse->removeListener (callbackHelper);
  70. isOn.removeListener (callbackHelper);
  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. buttonListeners.callChecked (checker, [this] (Listener& l) { l.buttonClicked (this); });
  319. }
  320. void Button::sendStateMessage()
  321. {
  322. Component::BailOutChecker checker (this);
  323. buttonStateChanged();
  324. if (! checker.shouldBailOut())
  325. buttonListeners.callChecked (checker, [this] (Listener& l) { l.buttonStateChanged (this); });
  326. }
  327. //==============================================================================
  328. void Button::paint (Graphics& g)
  329. {
  330. if (needsToRelease && isEnabled())
  331. {
  332. needsToRelease = false;
  333. needsRepainting = true;
  334. }
  335. paintButton (g, isOver(), isDown());
  336. lastStatePainted = buttonState;
  337. }
  338. //==============================================================================
  339. void Button::mouseEnter (const MouseEvent&) { updateState (true, false); }
  340. void Button::mouseExit (const MouseEvent&) { updateState (false, false); }
  341. void Button::mouseDown (const MouseEvent& e)
  342. {
  343. updateState (true, true);
  344. if (isDown())
  345. {
  346. if (autoRepeatDelay >= 0)
  347. callbackHelper->startTimer (autoRepeatDelay);
  348. if (triggerOnMouseDown)
  349. internalClickCallback (e.mods);
  350. }
  351. }
  352. void Button::mouseUp (const MouseEvent& e)
  353. {
  354. const bool wasDown = isDown();
  355. const bool wasOver = isOver();
  356. updateState (isMouseOrTouchOver (e), false);
  357. if (wasDown && wasOver && ! triggerOnMouseDown)
  358. {
  359. if (lastStatePainted != buttonDown)
  360. flashButtonState();
  361. internalClickCallback (e.mods);
  362. }
  363. }
  364. void Button::mouseDrag (const MouseEvent& e)
  365. {
  366. auto oldState = buttonState;
  367. updateState (isMouseOrTouchOver (e), true);
  368. if (autoRepeatDelay >= 0 && buttonState != oldState && isDown())
  369. callbackHelper->startTimer (autoRepeatSpeed);
  370. }
  371. bool Button::isMouseOrTouchOver (const MouseEvent& e)
  372. {
  373. if (e.source.isTouch())
  374. return getLocalBounds().toFloat().contains (e.position);
  375. return isMouseOver();
  376. }
  377. void Button::focusGained (FocusChangeType)
  378. {
  379. updateState();
  380. repaint();
  381. }
  382. void Button::focusLost (FocusChangeType)
  383. {
  384. updateState();
  385. repaint();
  386. }
  387. void Button::visibilityChanged()
  388. {
  389. needsToRelease = false;
  390. updateState();
  391. }
  392. void Button::parentHierarchyChanged()
  393. {
  394. auto* newKeySource = shortcuts.isEmpty() ? nullptr : getTopLevelComponent();
  395. if (newKeySource != keySource.get())
  396. {
  397. if (keySource != nullptr)
  398. keySource->removeKeyListener (callbackHelper);
  399. keySource = newKeySource;
  400. if (keySource != nullptr)
  401. keySource->addKeyListener (callbackHelper);
  402. }
  403. }
  404. //==============================================================================
  405. void Button::setCommandToTrigger (ApplicationCommandManager* const newCommandManager,
  406. const CommandID newCommandID, const bool generateTip)
  407. {
  408. commandID = newCommandID;
  409. generateTooltip = generateTip;
  410. if (commandManagerToUse != newCommandManager)
  411. {
  412. if (commandManagerToUse != nullptr)
  413. commandManagerToUse->removeListener (callbackHelper);
  414. commandManagerToUse = newCommandManager;
  415. if (commandManagerToUse != nullptr)
  416. commandManagerToUse->addListener (callbackHelper);
  417. // if you've got clickTogglesState turned on, you shouldn't also connect the button
  418. // up to be a command invoker. Instead, your command handler must flip the state of whatever
  419. // it is that this button represents, and the button will update its state to reflect this
  420. // in the applicationCommandListChanged() method.
  421. jassert (commandManagerToUse == nullptr || ! clickTogglesState);
  422. }
  423. if (commandManagerToUse != nullptr)
  424. applicationCommandListChangeCallback();
  425. else
  426. setEnabled (true);
  427. }
  428. void Button::applicationCommandListChangeCallback()
  429. {
  430. if (commandManagerToUse != nullptr)
  431. {
  432. ApplicationCommandInfo info (0);
  433. if (commandManagerToUse->getTargetForCommand (commandID, info) != nullptr)
  434. {
  435. updateAutomaticTooltip (info);
  436. setEnabled ((info.flags & ApplicationCommandInfo::isDisabled) == 0);
  437. setToggleState ((info.flags & ApplicationCommandInfo::isTicked) != 0, dontSendNotification);
  438. }
  439. else
  440. {
  441. setEnabled (false);
  442. }
  443. }
  444. }
  445. //==============================================================================
  446. void Button::addShortcut (const KeyPress& key)
  447. {
  448. if (key.isValid())
  449. {
  450. jassert (! isRegisteredForShortcut (key)); // already registered!
  451. shortcuts.add (key);
  452. parentHierarchyChanged();
  453. }
  454. }
  455. void Button::clearShortcuts()
  456. {
  457. shortcuts.clear();
  458. parentHierarchyChanged();
  459. }
  460. bool Button::isShortcutPressed() const
  461. {
  462. if (isShowing() && ! isCurrentlyBlockedByAnotherModalComponent())
  463. for (auto& s : shortcuts)
  464. if (s.isCurrentlyDown())
  465. return true;
  466. return false;
  467. }
  468. bool Button::isRegisteredForShortcut (const KeyPress& key) const
  469. {
  470. for (auto& s : shortcuts)
  471. if (key == s)
  472. return true;
  473. return false;
  474. }
  475. bool Button::keyStateChangedCallback()
  476. {
  477. if (! isEnabled())
  478. return false;
  479. const bool wasDown = isKeyDown;
  480. isKeyDown = isShortcutPressed();
  481. if (autoRepeatDelay >= 0 && (isKeyDown && ! wasDown))
  482. callbackHelper->startTimer (autoRepeatDelay);
  483. updateState();
  484. if (isEnabled() && wasDown && ! isKeyDown)
  485. {
  486. internalClickCallback (ModifierKeys::getCurrentModifiers());
  487. // (return immediately - this button may now have been deleted)
  488. return true;
  489. }
  490. return wasDown || isKeyDown;
  491. }
  492. bool Button::keyPressed (const KeyPress& key)
  493. {
  494. if (isEnabled() && key.isKeyCode (KeyPress::returnKey))
  495. {
  496. triggerClick();
  497. return true;
  498. }
  499. return false;
  500. }
  501. //==============================================================================
  502. void Button::setRepeatSpeed (const int initialDelayMillisecs,
  503. const int repeatMillisecs,
  504. const int minimumDelayInMillisecs) noexcept
  505. {
  506. autoRepeatDelay = initialDelayMillisecs;
  507. autoRepeatSpeed = repeatMillisecs;
  508. autoRepeatMinimumDelay = jmin (autoRepeatSpeed, minimumDelayInMillisecs);
  509. }
  510. void Button::repeatTimerCallback()
  511. {
  512. if (needsRepainting)
  513. {
  514. callbackHelper->stopTimer();
  515. updateState();
  516. needsRepainting = false;
  517. }
  518. else if (autoRepeatSpeed > 0 && (isKeyDown || (updateState() == buttonDown)))
  519. {
  520. auto repeatSpeed = autoRepeatSpeed;
  521. if (autoRepeatMinimumDelay >= 0)
  522. {
  523. auto timeHeldDown = jmin (1.0, getMillisecondsSinceButtonDown() / 4000.0);
  524. timeHeldDown *= timeHeldDown;
  525. repeatSpeed = repeatSpeed + (int) (timeHeldDown * (autoRepeatMinimumDelay - repeatSpeed));
  526. }
  527. repeatSpeed = jmax (1, repeatSpeed);
  528. auto now = Time::getMillisecondCounter();
  529. // if we've been blocked from repeating often enough, speed up the repeat timer to compensate..
  530. if (lastRepeatTime != 0 && (int) (now - lastRepeatTime) > repeatSpeed * 2)
  531. repeatSpeed = jmax (1, repeatSpeed / 2);
  532. lastRepeatTime = now;
  533. callbackHelper->startTimer (repeatSpeed);
  534. internalClickCallback (ModifierKeys::getCurrentModifiers());
  535. }
  536. else if (! needsToRelease)
  537. {
  538. callbackHelper->stopTimer();
  539. }
  540. }
  541. } // namespace juce