juce_Button.cpp 19 KB

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