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.

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