Audio plugin host https://kx.studio/carla
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.

juce_Button.cpp 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695
  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2020 - Raw Material Software Limited
  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 6 End-User License
  8. Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
  9. End User License Agreement: www.juce.com/juce-6-licence
  10. Privacy Policy: www.juce.com/juce-privacy-policy
  11. Or: You may also use this code under the terms of the GPL v3 (see
  12. www.gnu.org/licenses).
  13. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  14. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  15. DISCLAIMED.
  16. ==============================================================================
  17. */
  18. namespace juce
  19. {
  20. struct Button::CallbackHelper : public Timer,
  21. public ApplicationCommandManagerListener,
  22. public Value::Listener,
  23. public KeyListener
  24. {
  25. CallbackHelper (Button& b) : button (b) {}
  26. void timerCallback() override
  27. {
  28. button.repeatTimerCallback();
  29. }
  30. bool keyStateChanged (bool, Component*) override
  31. {
  32. return button.keyStateChangedCallback();
  33. }
  34. void valueChanged (Value& value) override
  35. {
  36. if (value.refersToSameSourceAs (button.isOn))
  37. button.setToggleState (button.isOn.getValue(), dontSendNotification, sendNotification);
  38. }
  39. bool keyPressed (const KeyPress&, Component*) override
  40. {
  41. // returning true will avoid forwarding events for keys that we're using as shortcuts
  42. return button.isShortcutPressed();
  43. }
  44. void applicationCommandInvoked (const ApplicationCommandTarget::InvocationInfo& info) override
  45. {
  46. if (info.commandID == button.commandID
  47. && (info.commandFlags & ApplicationCommandInfo::dontTriggerVisualFeedback) == 0)
  48. button.flashButtonState();
  49. }
  50. void applicationCommandListChanged() override
  51. {
  52. button.applicationCommandListChangeCallback();
  53. }
  54. Button& button;
  55. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CallbackHelper)
  56. };
  57. //==============================================================================
  58. Button::Button (const String& name) : Component (name), text (name)
  59. {
  60. callbackHelper.reset (new CallbackHelper (*this));
  61. setWantsKeyboardFocus (true);
  62. isOn.addListener (callbackHelper.get());
  63. }
  64. Button::~Button()
  65. {
  66. clearShortcuts();
  67. if (commandManagerToUse != nullptr)
  68. commandManagerToUse->removeListener (callbackHelper.get());
  69. isOn.removeListener (callbackHelper.get());
  70. callbackHelper.reset();
  71. }
  72. //==============================================================================
  73. void Button::setButtonText (const String& newText)
  74. {
  75. if (text != newText)
  76. {
  77. text = newText;
  78. repaint();
  79. }
  80. }
  81. void Button::setTooltip (const String& newTooltip)
  82. {
  83. SettableTooltipClient::setTooltip (newTooltip);
  84. generateTooltip = false;
  85. }
  86. void Button::updateAutomaticTooltip (const ApplicationCommandInfo& info)
  87. {
  88. if (generateTooltip && commandManagerToUse != nullptr)
  89. {
  90. auto tt = info.description.isNotEmpty() ? info.description
  91. : info.shortName;
  92. for (auto& kp : commandManagerToUse->getKeyMappings()->getKeyPressesAssignedToCommand (commandID))
  93. {
  94. auto key = kp.getTextDescription();
  95. tt << " [";
  96. if (key.length() == 1)
  97. tt << TRANS("shortcut") << ": '" << key << "']";
  98. else
  99. tt << key << ']';
  100. }
  101. SettableTooltipClient::setTooltip (tt);
  102. }
  103. }
  104. void Button::setConnectedEdges (int newFlags)
  105. {
  106. if (connectedEdgeFlags != newFlags)
  107. {
  108. connectedEdgeFlags = newFlags;
  109. repaint();
  110. }
  111. }
  112. //==============================================================================
  113. void Button::setToggleState (bool shouldBeOn, NotificationType notification)
  114. {
  115. setToggleState (shouldBeOn, notification, notification);
  116. }
  117. void Button::setToggleState (bool shouldBeOn, NotificationType clickNotification, NotificationType stateNotification)
  118. {
  119. if (shouldBeOn != lastToggleState)
  120. {
  121. WeakReference<Component> deletionWatcher (this);
  122. if (shouldBeOn)
  123. {
  124. turnOffOtherButtonsInGroup (clickNotification, stateNotification);
  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 (clickNotification != dontSendNotification)
  139. {
  140. // async callbacks aren't possible here
  141. jassert (clickNotification != sendNotificationAsync);
  142. sendClickMessage (ModifierKeys::currentModifiers);
  143. if (deletionWatcher == nullptr)
  144. return;
  145. }
  146. if (stateNotification != dontSendNotification)
  147. sendStateMessage();
  148. else
  149. buttonStateChanged();
  150. }
  151. }
  152. void Button::setToggleState (bool shouldBeOn, bool sendChange)
  153. {
  154. setToggleState (shouldBeOn, sendChange ? sendNotification : dontSendNotification);
  155. }
  156. void Button::setClickingTogglesState (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 (int newGroupId, NotificationType notification)
  170. {
  171. if (radioGroupId != newGroupId)
  172. {
  173. radioGroupId = newGroupId;
  174. if (lastToggleState)
  175. turnOffOtherButtonsInGroup (notification, notification);
  176. }
  177. }
  178. void Button::turnOffOtherButtonsInGroup (NotificationType clickNotification, NotificationType stateNotification)
  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, clickNotification, stateNotification);
  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 (bool over, 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 (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. auto now = Time::getApproximateMillisecondCounter();
  246. return now > buttonPressTime ? now - buttonPressTime : 0;
  247. }
  248. void Button::setTriggeredOnMouseDown (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::currentModifiers);
  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. return;
  322. buttonListeners.callChecked (checker, [this] (Listener& l) { l.buttonClicked (this); });
  323. if (checker.shouldBailOut())
  324. return;
  325. if (onClick != nullptr)
  326. onClick();
  327. }
  328. void Button::sendStateMessage()
  329. {
  330. Component::BailOutChecker checker (this);
  331. buttonStateChanged();
  332. if (checker.shouldBailOut())
  333. return;
  334. buttonListeners.callChecked (checker, [this] (Listener& l) { l.buttonStateChanged (this); });
  335. if (checker.shouldBailOut())
  336. return;
  337. if (onStateChange != nullptr)
  338. onStateChange();
  339. }
  340. //==============================================================================
  341. void Button::paint (Graphics& g)
  342. {
  343. if (needsToRelease && isEnabled())
  344. {
  345. needsToRelease = false;
  346. needsRepainting = true;
  347. }
  348. paintButton (g, isOver(), isDown());
  349. lastStatePainted = buttonState;
  350. }
  351. //==============================================================================
  352. void Button::mouseEnter (const MouseEvent&) { updateState (true, false); }
  353. void Button::mouseExit (const MouseEvent&) { updateState (false, false); }
  354. void Button::mouseDown (const MouseEvent& e)
  355. {
  356. updateState (true, true);
  357. if (isDown())
  358. {
  359. if (autoRepeatDelay >= 0)
  360. callbackHelper->startTimer (autoRepeatDelay);
  361. if (triggerOnMouseDown)
  362. internalClickCallback (e.mods);
  363. }
  364. }
  365. void Button::mouseUp (const MouseEvent& e)
  366. {
  367. const bool wasDown = isDown();
  368. const bool wasOver = isOver();
  369. updateState (isMouseSourceOver (e), false);
  370. if (wasDown && wasOver && ! triggerOnMouseDown)
  371. {
  372. if (lastStatePainted != buttonDown)
  373. flashButtonState();
  374. internalClickCallback (e.mods);
  375. }
  376. }
  377. void Button::mouseDrag (const MouseEvent& e)
  378. {
  379. auto oldState = buttonState;
  380. updateState (isMouseSourceOver (e), true);
  381. if (autoRepeatDelay >= 0 && buttonState != oldState && isDown())
  382. callbackHelper->startTimer (autoRepeatSpeed);
  383. }
  384. bool Button::isMouseSourceOver (const MouseEvent& e)
  385. {
  386. if (e.source.isTouch() || e.source.isPen())
  387. return getLocalBounds().toFloat().contains (e.position);
  388. return isMouseOver();
  389. }
  390. void Button::focusGained (FocusChangeType)
  391. {
  392. updateState();
  393. repaint();
  394. }
  395. void Button::focusLost (FocusChangeType)
  396. {
  397. updateState();
  398. repaint();
  399. }
  400. void Button::visibilityChanged()
  401. {
  402. needsToRelease = false;
  403. updateState();
  404. }
  405. void Button::parentHierarchyChanged()
  406. {
  407. auto* newKeySource = shortcuts.isEmpty() ? nullptr : getTopLevelComponent();
  408. if (newKeySource != keySource.get())
  409. {
  410. if (keySource != nullptr)
  411. keySource->removeKeyListener (callbackHelper.get());
  412. keySource = newKeySource;
  413. if (keySource != nullptr)
  414. keySource->addKeyListener (callbackHelper.get());
  415. }
  416. }
  417. //==============================================================================
  418. void Button::setCommandToTrigger (ApplicationCommandManager* newCommandManager,
  419. CommandID newCommandID, bool generateTip)
  420. {
  421. commandID = newCommandID;
  422. generateTooltip = generateTip;
  423. if (commandManagerToUse != newCommandManager)
  424. {
  425. if (commandManagerToUse != nullptr)
  426. commandManagerToUse->removeListener (callbackHelper.get());
  427. commandManagerToUse = newCommandManager;
  428. if (commandManagerToUse != nullptr)
  429. commandManagerToUse->addListener (callbackHelper.get());
  430. // if you've got clickTogglesState turned on, you shouldn't also connect the button
  431. // up to be a command invoker. Instead, your command handler must flip the state of whatever
  432. // it is that this button represents, and the button will update its state to reflect this
  433. // in the applicationCommandListChanged() method.
  434. jassert (commandManagerToUse == nullptr || ! clickTogglesState);
  435. }
  436. if (commandManagerToUse != nullptr)
  437. applicationCommandListChangeCallback();
  438. else
  439. setEnabled (true);
  440. }
  441. void Button::applicationCommandListChangeCallback()
  442. {
  443. if (commandManagerToUse != nullptr)
  444. {
  445. ApplicationCommandInfo info (0);
  446. if (commandManagerToUse->getTargetForCommand (commandID, info) != nullptr)
  447. {
  448. updateAutomaticTooltip (info);
  449. setEnabled ((info.flags & ApplicationCommandInfo::isDisabled) == 0);
  450. setToggleState ((info.flags & ApplicationCommandInfo::isTicked) != 0, dontSendNotification);
  451. }
  452. else
  453. {
  454. setEnabled (false);
  455. }
  456. }
  457. }
  458. //==============================================================================
  459. void Button::addShortcut (const KeyPress& key)
  460. {
  461. if (key.isValid())
  462. {
  463. jassert (! isRegisteredForShortcut (key)); // already registered!
  464. shortcuts.add (key);
  465. parentHierarchyChanged();
  466. }
  467. }
  468. void Button::clearShortcuts()
  469. {
  470. shortcuts.clear();
  471. parentHierarchyChanged();
  472. }
  473. bool Button::isShortcutPressed() const
  474. {
  475. if (isShowing() && ! isCurrentlyBlockedByAnotherModalComponent())
  476. for (auto& s : shortcuts)
  477. if (s.isCurrentlyDown())
  478. return true;
  479. return false;
  480. }
  481. bool Button::isRegisteredForShortcut (const KeyPress& key) const
  482. {
  483. for (auto& s : shortcuts)
  484. if (key == s)
  485. return true;
  486. return false;
  487. }
  488. bool Button::keyStateChangedCallback()
  489. {
  490. if (! isEnabled())
  491. return false;
  492. const bool wasDown = isKeyDown;
  493. isKeyDown = isShortcutPressed();
  494. if (autoRepeatDelay >= 0 && (isKeyDown && ! wasDown))
  495. callbackHelper->startTimer (autoRepeatDelay);
  496. updateState();
  497. if (isEnabled() && wasDown && ! isKeyDown)
  498. {
  499. internalClickCallback (ModifierKeys::currentModifiers);
  500. // (return immediately - this button may now have been deleted)
  501. return true;
  502. }
  503. return wasDown || isKeyDown;
  504. }
  505. bool Button::keyPressed (const KeyPress& key)
  506. {
  507. if (isEnabled() && key.isKeyCode (KeyPress::returnKey))
  508. {
  509. triggerClick();
  510. return true;
  511. }
  512. return false;
  513. }
  514. //==============================================================================
  515. void Button::setRepeatSpeed (int initialDelayMillisecs,
  516. int repeatMillisecs,
  517. int minimumDelayInMillisecs) noexcept
  518. {
  519. autoRepeatDelay = initialDelayMillisecs;
  520. autoRepeatSpeed = repeatMillisecs;
  521. autoRepeatMinimumDelay = jmin (autoRepeatSpeed, minimumDelayInMillisecs);
  522. }
  523. void Button::repeatTimerCallback()
  524. {
  525. if (needsRepainting)
  526. {
  527. callbackHelper->stopTimer();
  528. updateState();
  529. needsRepainting = false;
  530. }
  531. else if (autoRepeatSpeed > 0 && (isKeyDown || (updateState() == buttonDown)))
  532. {
  533. auto repeatSpeed = autoRepeatSpeed;
  534. if (autoRepeatMinimumDelay >= 0)
  535. {
  536. auto timeHeldDown = jmin (1.0, getMillisecondsSinceButtonDown() / 4000.0);
  537. timeHeldDown *= timeHeldDown;
  538. repeatSpeed = repeatSpeed + (int) (timeHeldDown * (autoRepeatMinimumDelay - repeatSpeed));
  539. }
  540. repeatSpeed = jmax (1, repeatSpeed);
  541. auto now = Time::getMillisecondCounter();
  542. // if we've been blocked from repeating often enough, speed up the repeat timer to compensate..
  543. if (lastRepeatTime != 0 && (int) (now - lastRepeatTime) > repeatSpeed * 2)
  544. repeatSpeed = jmax (1, repeatSpeed / 2);
  545. lastRepeatTime = now;
  546. callbackHelper->startTimer (repeatSpeed);
  547. internalClickCallback (ModifierKeys::currentModifiers);
  548. }
  549. else if (! needsToRelease)
  550. {
  551. callbackHelper->stopTimer();
  552. }
  553. }
  554. } // namespace juce