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.

704 lines
23KB

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