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.

762 lines
23KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library - "Jules' Utility Class Extensions"
  4. Copyright 2004-11 by Raw Material Software Ltd.
  5. ------------------------------------------------------------------------------
  6. JUCE can be redistributed and/or modified under the terms of the GNU General
  7. Public License (Version 2), as published by the Free Software Foundation.
  8. A copy of the license is included in the JUCE distribution, or can be found
  9. online at www.gnu.org/licenses.
  10. JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
  11. WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
  12. A PARTICULAR PURPOSE. See the GNU General Public License for more details.
  13. ------------------------------------------------------------------------------
  14. To release a closed-source product which uses JUCE, commercial licenses are
  15. available: visit www.rawmaterialsoftware.com/juce for more information.
  16. ==============================================================================
  17. */
  18. BEGIN_JUCE_NAMESPACE
  19. //==============================================================================
  20. class AlertWindowTextEditor : public TextEditor
  21. {
  22. public:
  23. AlertWindowTextEditor (const String& name, const bool isPasswordBox)
  24. : TextEditor (name, isPasswordBox ? getDefaultPasswordChar() : 0)
  25. {
  26. setSelectAllWhenFocused (true);
  27. }
  28. void returnPressed()
  29. {
  30. // pass these up the component hierarchy to be trigger the buttons
  31. getParentComponent()->keyPressed (KeyPress (KeyPress::returnKey, 0, '\n'));
  32. }
  33. void escapePressed()
  34. {
  35. // pass these up the component hierarchy to be trigger the buttons
  36. getParentComponent()->keyPressed (KeyPress (KeyPress::escapeKey, 0, 0));
  37. }
  38. private:
  39. JUCE_DECLARE_NON_COPYABLE (AlertWindowTextEditor);
  40. static juce_wchar getDefaultPasswordChar() noexcept
  41. {
  42. #if JUCE_LINUX
  43. return 0x2022;
  44. #else
  45. return 0x25cf;
  46. #endif
  47. }
  48. };
  49. //==============================================================================
  50. AlertWindow::AlertWindow (const String& title,
  51. const String& message,
  52. AlertIconType iconType,
  53. Component* associatedComponent_)
  54. : TopLevelWindow (title, true),
  55. alertIconType (iconType),
  56. associatedComponent (associatedComponent_),
  57. escapeKeyCancels (true)
  58. {
  59. if (message.isEmpty())
  60. text = " "; // to force an update if the message is empty
  61. setMessage (message);
  62. for (int i = Desktop::getInstance().getNumComponents(); --i >= 0;)
  63. {
  64. Component* const c = Desktop::getInstance().getComponent (i);
  65. if (c != nullptr && c->isAlwaysOnTop() && c->isShowing())
  66. {
  67. setAlwaysOnTop (true);
  68. break;
  69. }
  70. }
  71. if (! JUCEApplication::isStandaloneApp())
  72. setAlwaysOnTop (true); // for a plugin, make it always-on-top because the host windows are often top-level
  73. AlertWindow::lookAndFeelChanged();
  74. constrainer.setMinimumOnscreenAmounts (0x10000, 0x10000, 0x10000, 0x10000);
  75. }
  76. AlertWindow::~AlertWindow()
  77. {
  78. removeAllChildren();
  79. }
  80. void AlertWindow::userTriedToCloseWindow()
  81. {
  82. if (escapeKeyCancels || buttons.size() > 0)
  83. exitModalState (0);
  84. }
  85. //==============================================================================
  86. void AlertWindow::setMessage (const String& message)
  87. {
  88. const String newMessage (message.substring (0, 2048));
  89. if (text != newMessage)
  90. {
  91. text = newMessage;
  92. font = getLookAndFeel().getAlertWindowMessageFont();
  93. AttributedString newText;
  94. newText.append (getName() + "\n\n", Font (font.getHeight() * 1.1f, Font::bold));
  95. newText.append (text, font);
  96. attributedText = newText;
  97. updateLayout (true);
  98. repaint();
  99. }
  100. }
  101. //==============================================================================
  102. void AlertWindow::buttonClicked (Button* button)
  103. {
  104. if (button->getParentComponent() != nullptr)
  105. button->getParentComponent()->exitModalState (button->getCommandID());
  106. }
  107. //==============================================================================
  108. void AlertWindow::addButton (const String& name,
  109. const int returnValue,
  110. const KeyPress& shortcutKey1,
  111. const KeyPress& shortcutKey2)
  112. {
  113. TextButton* const b = new TextButton (name, String::empty);
  114. buttons.add (b);
  115. b->setWantsKeyboardFocus (true);
  116. b->setMouseClickGrabsKeyboardFocus (false);
  117. b->setCommandToTrigger (0, returnValue, false);
  118. b->addShortcut (shortcutKey1);
  119. b->addShortcut (shortcutKey2);
  120. b->addListener (this);
  121. b->changeWidthToFitText (getLookAndFeel().getAlertWindowButtonHeight());
  122. addAndMakeVisible (b, 0);
  123. updateLayout (false);
  124. }
  125. int AlertWindow::getNumButtons() const
  126. {
  127. return buttons.size();
  128. }
  129. void AlertWindow::triggerButtonClick (const String& buttonName)
  130. {
  131. for (int i = buttons.size(); --i >= 0;)
  132. {
  133. TextButton* const b = buttons.getUnchecked(i);
  134. if (buttonName == b->getName())
  135. {
  136. b->triggerClick();
  137. break;
  138. }
  139. }
  140. }
  141. void AlertWindow::setEscapeKeyCancels (bool shouldEscapeKeyCancel)
  142. {
  143. escapeKeyCancels = shouldEscapeKeyCancel;
  144. }
  145. //==============================================================================
  146. void AlertWindow::addTextEditor (const String& name,
  147. const String& initialContents,
  148. const String& onScreenLabel,
  149. const bool isPasswordBox)
  150. {
  151. AlertWindowTextEditor* const tc = new AlertWindowTextEditor (name, isPasswordBox);
  152. textBoxes.add (tc);
  153. allComps.add (tc);
  154. tc->setColour (TextEditor::outlineColourId, findColour (ComboBox::outlineColourId));
  155. tc->setFont (font);
  156. tc->setText (initialContents);
  157. tc->setCaretPosition (initialContents.length());
  158. addAndMakeVisible (tc);
  159. textboxNames.add (onScreenLabel);
  160. updateLayout (false);
  161. }
  162. TextEditor* AlertWindow::getTextEditor (const String& nameOfTextEditor) const
  163. {
  164. for (int i = textBoxes.size(); --i >= 0;)
  165. if (textBoxes.getUnchecked(i)->getName() == nameOfTextEditor)
  166. return textBoxes.getUnchecked(i);
  167. return nullptr;
  168. }
  169. String AlertWindow::getTextEditorContents (const String& nameOfTextEditor) const
  170. {
  171. TextEditor* const t = getTextEditor (nameOfTextEditor);
  172. return t != nullptr ? t->getText() : String::empty;
  173. }
  174. //==============================================================================
  175. void AlertWindow::addComboBox (const String& name,
  176. const StringArray& items,
  177. const String& onScreenLabel)
  178. {
  179. ComboBox* const cb = new ComboBox (name);
  180. comboBoxes.add (cb);
  181. allComps.add (cb);
  182. cb->addItemList (items, 1);
  183. addAndMakeVisible (cb);
  184. cb->setSelectedItemIndex (0);
  185. comboBoxNames.add (onScreenLabel);
  186. updateLayout (false);
  187. }
  188. ComboBox* AlertWindow::getComboBoxComponent (const String& nameOfList) const
  189. {
  190. for (int i = comboBoxes.size(); --i >= 0;)
  191. if (comboBoxes.getUnchecked(i)->getName() == nameOfList)
  192. return comboBoxes.getUnchecked(i);
  193. return nullptr;
  194. }
  195. //==============================================================================
  196. class AlertTextComp : public TextEditor
  197. {
  198. public:
  199. AlertTextComp (const String& message,
  200. const Font& font)
  201. {
  202. setReadOnly (true);
  203. setMultiLine (true, true);
  204. setCaretVisible (false);
  205. setScrollbarsShown (true);
  206. lookAndFeelChanged();
  207. setWantsKeyboardFocus (false);
  208. setFont (font);
  209. setText (message, false);
  210. bestWidth = 2 * (int) std::sqrt (font.getHeight() * font.getStringWidth (message));
  211. setColour (TextEditor::backgroundColourId, Colours::transparentBlack);
  212. setColour (TextEditor::outlineColourId, Colours::transparentBlack);
  213. setColour (TextEditor::shadowColourId, Colours::transparentBlack);
  214. }
  215. int getPreferredWidth() const noexcept { return bestWidth; }
  216. void updateLayout (const int width)
  217. {
  218. AttributedString s;
  219. s.setJustification (Justification::topLeft);
  220. s.append (getName(), getFont());
  221. TextLayout text;
  222. text.createLayoutWithBalancedLineLengths (s, width - 8.0f);
  223. setSize (width, jmin (width, (int) (text.getHeight() + getFont().getHeight())));
  224. }
  225. private:
  226. int bestWidth;
  227. JUCE_DECLARE_NON_COPYABLE (AlertTextComp);
  228. };
  229. void AlertWindow::addTextBlock (const String& textBlock)
  230. {
  231. AlertTextComp* const c = new AlertTextComp (textBlock, font);
  232. textBlocks.add (c);
  233. allComps.add (c);
  234. addAndMakeVisible (c);
  235. updateLayout (false);
  236. }
  237. //==============================================================================
  238. void AlertWindow::addProgressBarComponent (double& progressValue)
  239. {
  240. ProgressBar* const pb = new ProgressBar (progressValue);
  241. progressBars.add (pb);
  242. allComps.add (pb);
  243. addAndMakeVisible (pb);
  244. updateLayout (false);
  245. }
  246. //==============================================================================
  247. void AlertWindow::addCustomComponent (Component* const component)
  248. {
  249. customComps.add (component);
  250. allComps.add (component);
  251. addAndMakeVisible (component);
  252. updateLayout (false);
  253. }
  254. int AlertWindow::getNumCustomComponents() const
  255. {
  256. return customComps.size();
  257. }
  258. Component* AlertWindow::getCustomComponent (const int index) const
  259. {
  260. return customComps [index];
  261. }
  262. Component* AlertWindow::removeCustomComponent (const int index)
  263. {
  264. Component* const c = getCustomComponent (index);
  265. if (c != nullptr)
  266. {
  267. customComps.removeValue (c);
  268. allComps.removeValue (c);
  269. removeChildComponent (c);
  270. updateLayout (false);
  271. }
  272. return c;
  273. }
  274. //==============================================================================
  275. void AlertWindow::paint (Graphics& g)
  276. {
  277. getLookAndFeel().drawAlertBox (g, *this, textArea, textLayout);
  278. g.setColour (findColour (textColourId));
  279. g.setFont (getLookAndFeel().getAlertWindowFont());
  280. int i;
  281. for (i = textBoxes.size(); --i >= 0;)
  282. {
  283. const TextEditor* const te = textBoxes.getUnchecked(i);
  284. g.drawFittedText (textboxNames[i],
  285. te->getX(), te->getY() - 14,
  286. te->getWidth(), 14,
  287. Justification::centredLeft, 1);
  288. }
  289. for (i = comboBoxNames.size(); --i >= 0;)
  290. {
  291. const ComboBox* const cb = comboBoxes.getUnchecked(i);
  292. g.drawFittedText (comboBoxNames[i],
  293. cb->getX(), cb->getY() - 14,
  294. cb->getWidth(), 14,
  295. Justification::centredLeft, 1);
  296. }
  297. for (i = customComps.size(); --i >= 0;)
  298. {
  299. const Component* const c = customComps.getUnchecked(i);
  300. g.drawFittedText (c->getName(),
  301. c->getX(), c->getY() - 14,
  302. c->getWidth(), 14,
  303. Justification::centredLeft, 1);
  304. }
  305. }
  306. void AlertWindow::updateLayout (const bool onlyIncreaseSize)
  307. {
  308. const int titleH = 24;
  309. const int iconWidth = 80;
  310. const int wid = jmax (font.getStringWidth (text),
  311. font.getStringWidth (getName()));
  312. const int sw = (int) std::sqrt (font.getHeight() * wid);
  313. int w = jmin (300 + sw * 2, (int) (getParentWidth() * 0.7f));
  314. const int edgeGap = 10;
  315. const int labelHeight = 18;
  316. int iconSpace = 0;
  317. if (alertIconType == NoIcon)
  318. {
  319. attributedText.setJustification (Justification::centredTop);
  320. textLayout.createLayoutWithBalancedLineLengths (attributedText, (float) w);
  321. }
  322. else
  323. {
  324. attributedText.setJustification (Justification::topLeft);
  325. textLayout.createLayoutWithBalancedLineLengths (attributedText, (float) w);
  326. iconSpace = iconWidth;
  327. }
  328. w = jmax (350, (int) textLayout.getWidth() + iconSpace + edgeGap * 4);
  329. w = jmin (w, (int) (getParentWidth() * 0.7f));
  330. const int textLayoutH = (int) textLayout.getHeight();
  331. const int textBottom = 16 + titleH + textLayoutH;
  332. int h = textBottom;
  333. int buttonW = 40;
  334. int i;
  335. for (i = 0; i < buttons.size(); ++i)
  336. buttonW += 16 + buttons.getUnchecked(i)->getWidth();
  337. w = jmax (buttonW, w);
  338. h += (textBoxes.size() + comboBoxes.size() + progressBars.size()) * 50;
  339. if (buttons.size() > 0)
  340. h += 20 + buttons.getUnchecked(0)->getHeight();
  341. for (i = customComps.size(); --i >= 0;)
  342. {
  343. Component* c = customComps.getUnchecked(i);
  344. w = jmax (w, (c->getWidth() * 100) / 80);
  345. h += 10 + c->getHeight();
  346. if (c->getName().isNotEmpty())
  347. h += labelHeight;
  348. }
  349. for (i = textBlocks.size(); --i >= 0;)
  350. {
  351. const AlertTextComp* const ac = static_cast <const AlertTextComp*> (textBlocks.getUnchecked(i));
  352. w = jmax (w, ac->getPreferredWidth());
  353. }
  354. w = jmin (w, (int) (getParentWidth() * 0.7f));
  355. for (i = textBlocks.size(); --i >= 0;)
  356. {
  357. AlertTextComp* const ac = static_cast <AlertTextComp*> (textBlocks.getUnchecked(i));
  358. ac->updateLayout ((int) (w * 0.8f));
  359. h += ac->getHeight() + 10;
  360. }
  361. h = jmin (getParentHeight() - 50, h);
  362. if (onlyIncreaseSize)
  363. {
  364. w = jmax (w, getWidth());
  365. h = jmax (h, getHeight());
  366. }
  367. if (! isVisible())
  368. {
  369. centreAroundComponent (associatedComponent, w, h);
  370. }
  371. else
  372. {
  373. const int cx = getX() + getWidth() / 2;
  374. const int cy = getY() + getHeight() / 2;
  375. setBounds (cx - w / 2,
  376. cy - h / 2,
  377. w, h);
  378. }
  379. textArea.setBounds (edgeGap, edgeGap, w - (edgeGap * 2), h - edgeGap);
  380. const int spacer = 16;
  381. int totalWidth = -spacer;
  382. for (i = buttons.size(); --i >= 0;)
  383. totalWidth += buttons.getUnchecked(i)->getWidth() + spacer;
  384. int x = (w - totalWidth) / 2;
  385. int y = (int) (getHeight() * 0.95f);
  386. for (i = 0; i < buttons.size(); ++i)
  387. {
  388. TextButton* const c = buttons.getUnchecked(i);
  389. int ny = proportionOfHeight (0.95f) - c->getHeight();
  390. c->setTopLeftPosition (x, ny);
  391. if (ny < y)
  392. y = ny;
  393. x += c->getWidth() + spacer;
  394. c->toFront (false);
  395. }
  396. y = textBottom;
  397. for (i = 0; i < allComps.size(); ++i)
  398. {
  399. Component* const c = allComps.getUnchecked(i);
  400. h = 22;
  401. const int comboIndex = comboBoxes.indexOf (dynamic_cast <ComboBox*> (c));
  402. if (comboIndex >= 0 && comboBoxNames [comboIndex].isNotEmpty())
  403. y += labelHeight;
  404. const int tbIndex = textBoxes.indexOf (dynamic_cast <TextEditor*> (c));
  405. if (tbIndex >= 0 && textboxNames[tbIndex].isNotEmpty())
  406. y += labelHeight;
  407. if (customComps.contains (c))
  408. {
  409. if (c->getName().isNotEmpty())
  410. y += labelHeight;
  411. c->setTopLeftPosition (proportionOfWidth (0.1f), y);
  412. h = c->getHeight();
  413. }
  414. else if (textBlocks.contains (c))
  415. {
  416. c->setTopLeftPosition ((getWidth() - c->getWidth()) / 2, y);
  417. h = c->getHeight();
  418. }
  419. else
  420. {
  421. c->setBounds (proportionOfWidth (0.1f), y, proportionOfWidth (0.8f), h);
  422. }
  423. y += h + 10;
  424. }
  425. setWantsKeyboardFocus (getNumChildComponents() == 0);
  426. }
  427. bool AlertWindow::containsAnyExtraComponents() const
  428. {
  429. return allComps.size() > 0;
  430. }
  431. //==============================================================================
  432. void AlertWindow::mouseDown (const MouseEvent& e)
  433. {
  434. dragger.startDraggingComponent (this, e);
  435. }
  436. void AlertWindow::mouseDrag (const MouseEvent& e)
  437. {
  438. dragger.dragComponent (this, e, &constrainer);
  439. }
  440. bool AlertWindow::keyPressed (const KeyPress& key)
  441. {
  442. for (int i = buttons.size(); --i >= 0;)
  443. {
  444. TextButton* const b = buttons.getUnchecked(i);
  445. if (b->isRegisteredForShortcut (key))
  446. {
  447. b->triggerClick();
  448. return true;
  449. }
  450. }
  451. if (key.isKeyCode (KeyPress::escapeKey) && escapeKeyCancels && buttons.size() == 0)
  452. {
  453. exitModalState (0);
  454. return true;
  455. }
  456. else if (key.isKeyCode (KeyPress::returnKey) && buttons.size() == 1)
  457. {
  458. buttons.getUnchecked(0)->triggerClick();
  459. return true;
  460. }
  461. return false;
  462. }
  463. void AlertWindow::lookAndFeelChanged()
  464. {
  465. const int newFlags = getLookAndFeel().getAlertBoxWindowFlags();
  466. setUsingNativeTitleBar ((newFlags & ComponentPeer::windowHasTitleBar) != 0);
  467. setDropShadowEnabled (isOpaque() && (newFlags & ComponentPeer::windowHasDropShadow) != 0);
  468. }
  469. int AlertWindow::getDesktopWindowStyleFlags() const
  470. {
  471. return getLookAndFeel().getAlertBoxWindowFlags();
  472. }
  473. //==============================================================================
  474. class AlertWindowInfo
  475. {
  476. public:
  477. AlertWindowInfo (const String& title_, const String& message_, Component* component,
  478. AlertWindow::AlertIconType iconType_, int numButtons_,
  479. ModalComponentManager::Callback* callback_, bool modal_)
  480. : title (title_), message (message_), iconType (iconType_),
  481. numButtons (numButtons_), returnValue (0), associatedComponent (component),
  482. callback (callback_), modal (modal_)
  483. {
  484. }
  485. String title, message, button1, button2, button3;
  486. int invoke() const
  487. {
  488. MessageManager::getInstance()->callFunctionOnMessageThread (showCallback, (void*) this);
  489. return returnValue;
  490. }
  491. private:
  492. AlertWindow::AlertIconType iconType;
  493. int numButtons, returnValue;
  494. WeakReference<Component> associatedComponent;
  495. ModalComponentManager::Callback* callback;
  496. bool modal;
  497. void show()
  498. {
  499. LookAndFeel& lf = associatedComponent != nullptr ? associatedComponent->getLookAndFeel()
  500. : LookAndFeel::getDefaultLookAndFeel();
  501. ScopedPointer <Component> alertBox (lf.createAlertWindow (title, message, button1, button2, button3,
  502. iconType, numButtons, associatedComponent));
  503. jassert (alertBox != nullptr); // you have to return one of these!
  504. #if JUCE_MODAL_LOOPS_PERMITTED
  505. if (modal)
  506. {
  507. returnValue = alertBox->runModalLoop();
  508. }
  509. else
  510. #endif
  511. {
  512. alertBox->enterModalState (true, callback, true);
  513. alertBox.release();
  514. }
  515. }
  516. static void* showCallback (void* userData)
  517. {
  518. static_cast <AlertWindowInfo*> (userData)->show();
  519. return nullptr;
  520. }
  521. };
  522. #if JUCE_MODAL_LOOPS_PERMITTED
  523. void AlertWindow::showMessageBox (AlertIconType iconType,
  524. const String& title,
  525. const String& message,
  526. const String& buttonText,
  527. Component* associatedComponent)
  528. {
  529. if (LookAndFeel::getDefaultLookAndFeel().isUsingNativeAlertWindows())
  530. {
  531. NativeMessageBox::showMessageBox (iconType, title, message, associatedComponent);
  532. }
  533. else
  534. {
  535. AlertWindowInfo info (title, message, associatedComponent, iconType, 1, 0, true);
  536. info.button1 = buttonText.isEmpty() ? TRANS("ok") : buttonText;
  537. info.invoke();
  538. }
  539. }
  540. #endif
  541. void AlertWindow::showMessageBoxAsync (AlertIconType iconType,
  542. const String& title,
  543. const String& message,
  544. const String& buttonText,
  545. Component* associatedComponent)
  546. {
  547. if (LookAndFeel::getDefaultLookAndFeel().isUsingNativeAlertWindows())
  548. {
  549. return NativeMessageBox::showMessageBoxAsync (iconType, title, message, associatedComponent);
  550. }
  551. else
  552. {
  553. AlertWindowInfo info (title, message, associatedComponent, iconType, 1, 0, false);
  554. info.button1 = buttonText.isEmpty() ? TRANS("ok") : buttonText;
  555. info.invoke();
  556. }
  557. }
  558. bool AlertWindow::showOkCancelBox (AlertIconType iconType,
  559. const String& title,
  560. const String& message,
  561. const String& button1Text,
  562. const String& button2Text,
  563. Component* associatedComponent,
  564. ModalComponentManager::Callback* callback)
  565. {
  566. if (LookAndFeel::getDefaultLookAndFeel().isUsingNativeAlertWindows())
  567. {
  568. return NativeMessageBox::showOkCancelBox (iconType, title, message, associatedComponent, callback);
  569. }
  570. else
  571. {
  572. AlertWindowInfo info (title, message, associatedComponent, iconType, 2, callback, callback == nullptr);
  573. info.button1 = button1Text.isEmpty() ? TRANS("ok") : button1Text;
  574. info.button2 = button2Text.isEmpty() ? TRANS("cancel") : button2Text;
  575. return info.invoke() != 0;
  576. }
  577. }
  578. int AlertWindow::showYesNoCancelBox (AlertIconType iconType,
  579. const String& title,
  580. const String& message,
  581. const String& button1Text,
  582. const String& button2Text,
  583. const String& button3Text,
  584. Component* associatedComponent,
  585. ModalComponentManager::Callback* callback)
  586. {
  587. if (LookAndFeel::getDefaultLookAndFeel().isUsingNativeAlertWindows())
  588. {
  589. return NativeMessageBox::showYesNoCancelBox (iconType, title, message, associatedComponent, callback);
  590. }
  591. else
  592. {
  593. AlertWindowInfo info (title, message, associatedComponent, iconType, 3, callback, callback == nullptr);
  594. info.button1 = button1Text.isEmpty() ? TRANS("yes") : button1Text;
  595. info.button2 = button2Text.isEmpty() ? TRANS("no") : button2Text;
  596. info.button3 = button3Text.isEmpty() ? TRANS("cancel") : button3Text;
  597. return info.invoke();
  598. }
  599. }
  600. #if JUCE_MODAL_LOOPS_PERMITTED
  601. bool AlertWindow::showNativeDialogBox (const String& title,
  602. const String& bodyText,
  603. bool isOkCancel)
  604. {
  605. if (isOkCancel)
  606. {
  607. return NativeMessageBox::showOkCancelBox (AlertWindow::NoIcon, title, bodyText);
  608. }
  609. else
  610. {
  611. NativeMessageBox::showMessageBox (AlertWindow::NoIcon, title, bodyText);
  612. return true;
  613. }
  614. }
  615. #endif
  616. END_JUCE_NAMESPACE