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.

709 lines
22KB

  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. static juce_wchar getDefaultPasswordChar() noexcept
  22. {
  23. #if JUCE_LINUX
  24. return 0x2022;
  25. #else
  26. return 0x25cf;
  27. #endif
  28. }
  29. //==============================================================================
  30. AlertWindow::AlertWindow (const String& title,
  31. const String& message,
  32. AlertIconType iconType,
  33. Component* comp)
  34. : TopLevelWindow (title, true),
  35. alertIconType (iconType),
  36. associatedComponent (comp)
  37. {
  38. setAlwaysOnTop (juce_areThereAnyAlwaysOnTopWindows());
  39. if (message.isEmpty())
  40. text = " "; // to force an update if the message is empty
  41. setMessage (message);
  42. AlertWindow::lookAndFeelChanged();
  43. constrainer.setMinimumOnscreenAmounts (0x10000, 0x10000, 0x10000, 0x10000);
  44. }
  45. AlertWindow::~AlertWindow()
  46. {
  47. // Ensure that the focus does not jump to another TextEditor while we
  48. // remove children.
  49. for (auto* t : textBoxes)
  50. t->setWantsKeyboardFocus (false);
  51. // Giveaway focus before removing the editors, so that any TextEditor
  52. // with focus has a chance to dismiss native keyboard if shown.
  53. if (hasKeyboardFocus (true))
  54. Component::unfocusAllComponents();
  55. removeAllChildren();
  56. }
  57. void AlertWindow::userTriedToCloseWindow()
  58. {
  59. if (escapeKeyCancels || buttons.size() > 0)
  60. exitModalState (0);
  61. }
  62. //==============================================================================
  63. void AlertWindow::setMessage (const String& message)
  64. {
  65. auto newMessage = message.substring (0, 2048);
  66. if (text != newMessage)
  67. {
  68. text = newMessage;
  69. updateLayout (true);
  70. repaint();
  71. }
  72. }
  73. //==============================================================================
  74. void AlertWindow::exitAlert (Button* button)
  75. {
  76. if (auto* parent = button->getParentComponent())
  77. parent->exitModalState (button->getCommandID());
  78. }
  79. //==============================================================================
  80. void AlertWindow::addButton (const String& name,
  81. const int returnValue,
  82. const KeyPress& shortcutKey1,
  83. const KeyPress& shortcutKey2)
  84. {
  85. auto* b = new TextButton (name, {});
  86. buttons.add (b);
  87. b->setWantsKeyboardFocus (true);
  88. b->setMouseClickGrabsKeyboardFocus (false);
  89. b->setCommandToTrigger (nullptr, returnValue, false);
  90. b->addShortcut (shortcutKey1);
  91. b->addShortcut (shortcutKey2);
  92. b->onClick = [this, b] { exitAlert (b); };
  93. Array<TextButton*> buttonsArray (buttons.begin(), buttons.size());
  94. auto& lf = getLookAndFeel();
  95. auto buttonHeight = lf.getAlertWindowButtonHeight();
  96. auto buttonWidths = lf.getWidthsForTextButtons (*this, buttonsArray);
  97. jassert (buttonWidths.size() == buttons.size());
  98. int i = 0;
  99. for (auto* button : buttons)
  100. button->setSize (buttonWidths[i++], buttonHeight);
  101. addAndMakeVisible (b, 0);
  102. updateLayout (false);
  103. }
  104. int AlertWindow::getNumButtons() const
  105. {
  106. return buttons.size();
  107. }
  108. void AlertWindow::triggerButtonClick (const String& buttonName)
  109. {
  110. for (auto* b : buttons)
  111. {
  112. if (buttonName == b->getName())
  113. {
  114. b->triggerClick();
  115. break;
  116. }
  117. }
  118. }
  119. void AlertWindow::setEscapeKeyCancels (bool shouldEscapeKeyCancel)
  120. {
  121. escapeKeyCancels = shouldEscapeKeyCancel;
  122. }
  123. //==============================================================================
  124. void AlertWindow::addTextEditor (const String& name,
  125. const String& initialContents,
  126. const String& onScreenLabel,
  127. const bool isPasswordBox)
  128. {
  129. auto* ed = new TextEditor (name, isPasswordBox ? getDefaultPasswordChar() : 0);
  130. ed->setSelectAllWhenFocused (true);
  131. ed->setEscapeAndReturnKeysConsumed (false);
  132. textBoxes.add (ed);
  133. allComps.add (ed);
  134. ed->setColour (TextEditor::outlineColourId, findColour (ComboBox::outlineColourId));
  135. ed->setFont (getLookAndFeel().getAlertWindowMessageFont());
  136. addAndMakeVisible (ed);
  137. ed->setText (initialContents);
  138. ed->setCaretPosition (initialContents.length());
  139. textboxNames.add (onScreenLabel);
  140. updateLayout (false);
  141. }
  142. TextEditor* AlertWindow::getTextEditor (const String& nameOfTextEditor) const
  143. {
  144. for (auto* tb : textBoxes)
  145. if (tb->getName() == nameOfTextEditor)
  146. return tb;
  147. return nullptr;
  148. }
  149. String AlertWindow::getTextEditorContents (const String& nameOfTextEditor) const
  150. {
  151. if (auto* t = getTextEditor (nameOfTextEditor))
  152. return t->getText();
  153. return {};
  154. }
  155. //==============================================================================
  156. void AlertWindow::addComboBox (const String& name,
  157. const StringArray& items,
  158. const String& onScreenLabel)
  159. {
  160. auto* cb = new ComboBox (name);
  161. comboBoxes.add (cb);
  162. allComps.add (cb);
  163. cb->addItemList (items, 1);
  164. addAndMakeVisible (cb);
  165. cb->setSelectedItemIndex (0);
  166. comboBoxNames.add (onScreenLabel);
  167. updateLayout (false);
  168. }
  169. ComboBox* AlertWindow::getComboBoxComponent (const String& nameOfList) const
  170. {
  171. for (auto* cb : comboBoxes)
  172. if (cb->getName() == nameOfList)
  173. return cb;
  174. return nullptr;
  175. }
  176. //==============================================================================
  177. class AlertTextComp : public TextEditor
  178. {
  179. public:
  180. AlertTextComp (AlertWindow& owner, const String& message, const Font& font)
  181. {
  182. if (owner.isColourSpecified (AlertWindow::textColourId))
  183. setColour (TextEditor::textColourId, owner.findColour (AlertWindow::textColourId));
  184. setColour (TextEditor::backgroundColourId, Colours::transparentBlack);
  185. setColour (TextEditor::outlineColourId, Colours::transparentBlack);
  186. setColour (TextEditor::shadowColourId, Colours::transparentBlack);
  187. setReadOnly (true);
  188. setMultiLine (true, true);
  189. setCaretVisible (false);
  190. setScrollbarsShown (true);
  191. lookAndFeelChanged();
  192. setWantsKeyboardFocus (false);
  193. setFont (font);
  194. setText (message, false);
  195. bestWidth = 2 * (int) std::sqrt (font.getHeight() * font.getStringWidth (message));
  196. }
  197. void updateLayout (const int width)
  198. {
  199. AttributedString s;
  200. s.setJustification (Justification::topLeft);
  201. s.append (getText(), getFont());
  202. TextLayout text;
  203. text.createLayoutWithBalancedLineLengths (s, width - 8.0f);
  204. setSize (width, jmin (width, (int) (text.getHeight() + getFont().getHeight())));
  205. }
  206. int bestWidth;
  207. JUCE_DECLARE_NON_COPYABLE (AlertTextComp)
  208. };
  209. void AlertWindow::addTextBlock (const String& textBlock)
  210. {
  211. auto* c = new AlertTextComp (*this, textBlock, getLookAndFeel().getAlertWindowMessageFont());
  212. textBlocks.add (c);
  213. allComps.add (c);
  214. addAndMakeVisible (c);
  215. updateLayout (false);
  216. }
  217. //==============================================================================
  218. void AlertWindow::addProgressBarComponent (double& progressValue)
  219. {
  220. auto* pb = new ProgressBar (progressValue);
  221. progressBars.add (pb);
  222. allComps.add (pb);
  223. addAndMakeVisible (pb);
  224. updateLayout (false);
  225. }
  226. //==============================================================================
  227. void AlertWindow::addCustomComponent (Component* const component)
  228. {
  229. customComps.add (component);
  230. allComps.add (component);
  231. addAndMakeVisible (component);
  232. updateLayout (false);
  233. }
  234. int AlertWindow::getNumCustomComponents() const { return customComps.size(); }
  235. Component* AlertWindow::getCustomComponent (int index) const { return customComps [index]; }
  236. Component* AlertWindow::removeCustomComponent (const int index)
  237. {
  238. auto* c = getCustomComponent (index);
  239. if (c != nullptr)
  240. {
  241. customComps.removeFirstMatchingValue (c);
  242. allComps.removeFirstMatchingValue (c);
  243. removeChildComponent (c);
  244. updateLayout (false);
  245. }
  246. return c;
  247. }
  248. //==============================================================================
  249. void AlertWindow::paint (Graphics& g)
  250. {
  251. auto& lf = getLookAndFeel();
  252. lf.drawAlertBox (g, *this, textArea, textLayout);
  253. g.setColour (findColour (textColourId));
  254. g.setFont (lf.getAlertWindowFont());
  255. for (int i = textBoxes.size(); --i >= 0;)
  256. {
  257. auto* te = textBoxes.getUnchecked(i);
  258. g.drawFittedText (textboxNames[i],
  259. te->getX(), te->getY() - 14,
  260. te->getWidth(), 14,
  261. Justification::centredLeft, 1);
  262. }
  263. for (int i = comboBoxNames.size(); --i >= 0;)
  264. {
  265. auto* cb = comboBoxes.getUnchecked(i);
  266. g.drawFittedText (comboBoxNames[i],
  267. cb->getX(), cb->getY() - 14,
  268. cb->getWidth(), 14,
  269. Justification::centredLeft, 1);
  270. }
  271. for (auto* c : customComps)
  272. g.drawFittedText (c->getName(),
  273. c->getX(), c->getY() - 14,
  274. c->getWidth(), 14,
  275. Justification::centredLeft, 1);
  276. }
  277. void AlertWindow::updateLayout (const bool onlyIncreaseSize)
  278. {
  279. const int titleH = 24;
  280. const int iconWidth = 80;
  281. auto& lf = getLookAndFeel();
  282. auto messageFont (lf.getAlertWindowMessageFont());
  283. auto wid = jmax (messageFont.getStringWidth (text),
  284. messageFont.getStringWidth (getName()));
  285. auto sw = (int) std::sqrt (messageFont.getHeight() * wid);
  286. auto w = jmin (300 + sw * 2, (int) (getParentWidth() * 0.7f));
  287. const int edgeGap = 10;
  288. const int labelHeight = 18;
  289. int iconSpace = 0;
  290. AttributedString attributedText;
  291. attributedText.append (getName(), lf.getAlertWindowTitleFont());
  292. if (text.isNotEmpty())
  293. attributedText.append ("\n\n" + text, messageFont);
  294. attributedText.setColour (findColour (textColourId));
  295. if (alertIconType == NoIcon)
  296. {
  297. attributedText.setJustification (Justification::centredTop);
  298. textLayout.createLayoutWithBalancedLineLengths (attributedText, (float) w);
  299. }
  300. else
  301. {
  302. attributedText.setJustification (Justification::topLeft);
  303. textLayout.createLayoutWithBalancedLineLengths (attributedText, (float) w);
  304. iconSpace = iconWidth;
  305. }
  306. w = jmax (350, (int) textLayout.getWidth() + iconSpace + edgeGap * 4);
  307. w = jmin (w, (int) (getParentWidth() * 0.7f));
  308. auto textLayoutH = (int) textLayout.getHeight();
  309. auto textBottom = 16 + titleH + textLayoutH;
  310. int h = textBottom;
  311. int buttonW = 40;
  312. for (auto* b : buttons)
  313. buttonW += 16 + b->getWidth();
  314. w = jmax (buttonW, w);
  315. h += (textBoxes.size() + comboBoxes.size() + progressBars.size()) * 50;
  316. if (auto* b = buttons[0])
  317. h += 20 + b->getHeight();
  318. for (auto* c : customComps)
  319. {
  320. w = jmax (w, (c->getWidth() * 100) / 80);
  321. h += 10 + c->getHeight();
  322. if (c->getName().isNotEmpty())
  323. h += labelHeight;
  324. }
  325. for (auto* tb : textBlocks)
  326. w = jmax (w, static_cast<const AlertTextComp*> (tb)->bestWidth);
  327. w = jmin (w, (int) (getParentWidth() * 0.7f));
  328. for (auto* tb : textBlocks)
  329. {
  330. auto* ac = static_cast<AlertTextComp*> (tb);
  331. ac->updateLayout ((int) (w * 0.8f));
  332. h += ac->getHeight() + 10;
  333. }
  334. h = jmin (getParentHeight() - 50, h);
  335. if (onlyIncreaseSize)
  336. {
  337. w = jmax (w, getWidth());
  338. h = jmax (h, getHeight());
  339. }
  340. if (! isVisible())
  341. centreAroundComponent (associatedComponent, w, h);
  342. else
  343. setBounds (getBounds().withSizeKeepingCentre (w, h));
  344. textArea.setBounds (edgeGap, edgeGap, w - (edgeGap * 2), h - edgeGap);
  345. const int spacer = 16;
  346. int totalWidth = -spacer;
  347. for (auto* b : buttons)
  348. totalWidth += b->getWidth() + spacer;
  349. auto x = (w - totalWidth) / 2;
  350. auto y = (int) (getHeight() * 0.95f);
  351. for (auto* c : buttons)
  352. {
  353. int ny = proportionOfHeight (0.95f) - c->getHeight();
  354. c->setTopLeftPosition (x, ny);
  355. if (ny < y)
  356. y = ny;
  357. x += c->getWidth() + spacer;
  358. c->toFront (false);
  359. }
  360. y = textBottom;
  361. for (auto* c : allComps)
  362. {
  363. h = 22;
  364. const int comboIndex = comboBoxes.indexOf (dynamic_cast<ComboBox*> (c));
  365. if (comboIndex >= 0 && comboBoxNames [comboIndex].isNotEmpty())
  366. y += labelHeight;
  367. const int tbIndex = textBoxes.indexOf (dynamic_cast<TextEditor*> (c));
  368. if (tbIndex >= 0 && textboxNames[tbIndex].isNotEmpty())
  369. y += labelHeight;
  370. if (customComps.contains (c))
  371. {
  372. if (c->getName().isNotEmpty())
  373. y += labelHeight;
  374. c->setTopLeftPosition (proportionOfWidth (0.1f), y);
  375. h = c->getHeight();
  376. }
  377. else if (textBlocks.contains (c))
  378. {
  379. c->setTopLeftPosition ((getWidth() - c->getWidth()) / 2, y);
  380. h = c->getHeight();
  381. }
  382. else
  383. {
  384. c->setBounds (proportionOfWidth (0.1f), y, proportionOfWidth (0.8f), h);
  385. }
  386. y += h + 10;
  387. }
  388. setWantsKeyboardFocus (getNumChildComponents() == 0);
  389. }
  390. bool AlertWindow::containsAnyExtraComponents() const
  391. {
  392. return allComps.size() > 0;
  393. }
  394. //==============================================================================
  395. void AlertWindow::mouseDown (const MouseEvent& e)
  396. {
  397. dragger.startDraggingComponent (this, e);
  398. }
  399. void AlertWindow::mouseDrag (const MouseEvent& e)
  400. {
  401. dragger.dragComponent (this, e, &constrainer);
  402. }
  403. bool AlertWindow::keyPressed (const KeyPress& key)
  404. {
  405. for (auto* b : buttons)
  406. {
  407. if (b->isRegisteredForShortcut (key))
  408. {
  409. b->triggerClick();
  410. return true;
  411. }
  412. }
  413. if (key.isKeyCode (KeyPress::escapeKey) && escapeKeyCancels)
  414. {
  415. exitModalState (0);
  416. return true;
  417. }
  418. if (key.isKeyCode (KeyPress::returnKey) && buttons.size() == 1)
  419. {
  420. buttons.getUnchecked(0)->triggerClick();
  421. return true;
  422. }
  423. return false;
  424. }
  425. void AlertWindow::lookAndFeelChanged()
  426. {
  427. const int newFlags = getLookAndFeel().getAlertBoxWindowFlags();
  428. setUsingNativeTitleBar ((newFlags & ComponentPeer::windowHasTitleBar) != 0);
  429. setDropShadowEnabled (isOpaque() && (newFlags & ComponentPeer::windowHasDropShadow) != 0);
  430. updateLayout (false);
  431. }
  432. int AlertWindow::getDesktopWindowStyleFlags() const
  433. {
  434. return getLookAndFeel().getAlertBoxWindowFlags();
  435. }
  436. //==============================================================================
  437. class AlertWindowInfo
  438. {
  439. public:
  440. AlertWindowInfo (const String& t, const String& m, Component* component,
  441. AlertWindow::AlertIconType icon, int numButts,
  442. ModalComponentManager::Callback* cb, bool runModally)
  443. : title (t), message (m), iconType (icon), numButtons (numButts),
  444. associatedComponent (component), callback (cb), modal (runModally)
  445. {
  446. }
  447. String title, message, button1, button2, button3;
  448. int invoke() const
  449. {
  450. MessageManager::getInstance()->callFunctionOnMessageThread (showCallback, (void*) this);
  451. return returnValue;
  452. }
  453. private:
  454. AlertWindow::AlertIconType iconType;
  455. int numButtons, returnValue = 0;
  456. WeakReference<Component> associatedComponent;
  457. ModalComponentManager::Callback* callback;
  458. bool modal;
  459. void show()
  460. {
  461. auto& lf = associatedComponent != nullptr ? associatedComponent->getLookAndFeel()
  462. : LookAndFeel::getDefaultLookAndFeel();
  463. std::unique_ptr<Component> alertBox (lf.createAlertWindow (title, message, button1, button2, button3,
  464. iconType, numButtons, associatedComponent));
  465. jassert (alertBox != nullptr); // you have to return one of these!
  466. alertBox->setAlwaysOnTop (juce_areThereAnyAlwaysOnTopWindows());
  467. #if JUCE_MODAL_LOOPS_PERMITTED
  468. if (modal)
  469. {
  470. returnValue = alertBox->runModalLoop();
  471. }
  472. else
  473. #endif
  474. {
  475. ignoreUnused (modal);
  476. alertBox->enterModalState (true, callback, true);
  477. alertBox.release();
  478. }
  479. }
  480. static void* showCallback (void* userData)
  481. {
  482. static_cast<AlertWindowInfo*> (userData)->show();
  483. return nullptr;
  484. }
  485. };
  486. #if JUCE_MODAL_LOOPS_PERMITTED
  487. void AlertWindow::showMessageBox (AlertIconType iconType,
  488. const String& title,
  489. const String& message,
  490. const String& buttonText,
  491. Component* associatedComponent)
  492. {
  493. if (LookAndFeel::getDefaultLookAndFeel().isUsingNativeAlertWindows())
  494. {
  495. NativeMessageBox::showMessageBox (iconType, title, message, associatedComponent);
  496. }
  497. else
  498. {
  499. AlertWindowInfo info (title, message, associatedComponent, iconType, 1, nullptr, true);
  500. info.button1 = buttonText.isEmpty() ? TRANS("OK") : buttonText;
  501. info.invoke();
  502. }
  503. }
  504. #endif
  505. void AlertWindow::showMessageBoxAsync (AlertIconType iconType,
  506. const String& title,
  507. const String& message,
  508. const String& buttonText,
  509. Component* associatedComponent,
  510. ModalComponentManager::Callback* callback)
  511. {
  512. if (LookAndFeel::getDefaultLookAndFeel().isUsingNativeAlertWindows())
  513. {
  514. NativeMessageBox::showMessageBoxAsync (iconType, title, message, associatedComponent, callback);
  515. }
  516. else
  517. {
  518. AlertWindowInfo info (title, message, associatedComponent, iconType, 1, callback, false);
  519. info.button1 = buttonText.isEmpty() ? TRANS("OK") : buttonText;
  520. info.invoke();
  521. }
  522. }
  523. bool AlertWindow::showOkCancelBox (AlertIconType iconType,
  524. const String& title,
  525. const String& message,
  526. const String& button1Text,
  527. const String& button2Text,
  528. Component* associatedComponent,
  529. ModalComponentManager::Callback* callback)
  530. {
  531. if (LookAndFeel::getDefaultLookAndFeel().isUsingNativeAlertWindows())
  532. return NativeMessageBox::showOkCancelBox (iconType, title, message, associatedComponent, callback);
  533. AlertWindowInfo info (title, message, associatedComponent, iconType, 2, callback, callback == nullptr);
  534. info.button1 = button1Text.isEmpty() ? TRANS("OK") : button1Text;
  535. info.button2 = button2Text.isEmpty() ? TRANS("Cancel") : button2Text;
  536. return info.invoke() != 0;
  537. }
  538. int AlertWindow::showYesNoCancelBox (AlertIconType iconType,
  539. const String& title,
  540. const String& message,
  541. const String& button1Text,
  542. const String& button2Text,
  543. const String& button3Text,
  544. Component* associatedComponent,
  545. ModalComponentManager::Callback* callback)
  546. {
  547. if (LookAndFeel::getDefaultLookAndFeel().isUsingNativeAlertWindows())
  548. return NativeMessageBox::showYesNoCancelBox (iconType, title, message, associatedComponent, callback);
  549. AlertWindowInfo info (title, message, associatedComponent, iconType, 3, callback, callback == nullptr);
  550. info.button1 = button1Text.isEmpty() ? TRANS("Yes") : button1Text;
  551. info.button2 = button2Text.isEmpty() ? TRANS("No") : button2Text;
  552. info.button3 = button3Text.isEmpty() ? TRANS("Cancel") : button3Text;
  553. return info.invoke();
  554. }
  555. #if JUCE_MODAL_LOOPS_PERMITTED
  556. bool AlertWindow::showNativeDialogBox (const String& title,
  557. const String& bodyText,
  558. bool isOkCancel)
  559. {
  560. if (isOkCancel)
  561. return NativeMessageBox::showOkCancelBox (AlertWindow::NoIcon, title, bodyText);
  562. NativeMessageBox::showMessageBox (AlertWindow::NoIcon, title, bodyText);
  563. return true;
  564. }
  565. #endif
  566. } // namespace juce