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.

574 lines
21KB

  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. #pragma once
  19. #include "../../Application/jucer_CommonHeaders.h"
  20. #include "../../Application/jucer_Application.h"
  21. //==============================================================================
  22. class MessagesPopupWindow final : public Component,
  23. private ComponentMovementWatcher
  24. {
  25. public:
  26. MessagesPopupWindow (Component& target, Component& parent, Project& project)
  27. : ComponentMovementWatcher (&parent),
  28. targetComponent (target),
  29. parentComponent (parent),
  30. messagesListComponent (*this, project)
  31. {
  32. parentComponent.addAndMakeVisible (this);
  33. setAlwaysOnTop (true);
  34. addAndMakeVisible (viewport);
  35. viewport.setScrollBarsShown (true, false);
  36. viewport.setViewedComponent (&messagesListComponent, false);
  37. viewport.setWantsKeyboardFocus (false);
  38. setOpaque (true);
  39. }
  40. void paint (Graphics& g) override
  41. {
  42. g.fillAll (findColour (secondaryBackgroundColourId));
  43. }
  44. void resized() override
  45. {
  46. viewport.setBounds (getLocalBounds());
  47. }
  48. bool isListShowing() const
  49. {
  50. return messagesListComponent.getRequiredHeight() > 0;
  51. }
  52. void updateBounds (bool animate)
  53. {
  54. auto targetBounds = parentComponent.getLocalArea (&targetComponent, targetComponent.getLocalBounds());
  55. auto height = jmin (messagesListComponent.getRequiredHeight(), maxHeight);
  56. auto yPos = jmax (indent, targetBounds.getY() - height);
  57. Rectangle<int> bounds (targetBounds.getX(), yPos,
  58. jmin (width, parentComponent.getWidth() - targetBounds.getX() - indent), targetBounds.getY() - yPos);
  59. auto& animator = Desktop::getInstance().getAnimator();
  60. if (animate)
  61. {
  62. setBounds (bounds.withY (targetBounds.getY()));
  63. animator.animateComponent (this, bounds, 1.0f, 150, false, 1.0, 1.0);
  64. }
  65. else
  66. {
  67. if (animator.isAnimating (this))
  68. animator.cancelAnimation (this, false);
  69. setBounds (bounds);
  70. }
  71. messagesListComponent.resized();
  72. }
  73. private:
  74. //==============================================================================
  75. class MessagesListComponent final : public Component,
  76. private ValueTree::Listener,
  77. private AsyncUpdater
  78. {
  79. public:
  80. MessagesListComponent (MessagesPopupWindow& o, Project& currentProject)
  81. : owner (o),
  82. project (currentProject)
  83. {
  84. messagesTree = project.getProjectMessages();
  85. messagesTree.addListener (this);
  86. setOpaque (true);
  87. messagesChanged();
  88. }
  89. void resized() override
  90. {
  91. auto bounds = getLocalBounds();
  92. auto numMessages = messages.size();
  93. for (size_t i = 0; i < numMessages; ++i)
  94. {
  95. messages[i]->setBounds (bounds.removeFromTop (messageHeight));
  96. if (numMessages > 1 && i != (numMessages - 1))
  97. bounds.removeFromTop (messageSpacing);
  98. }
  99. }
  100. void paint (Graphics& g) override
  101. {
  102. g.fillAll (findColour (backgroundColourId).contrasting (0.2f));
  103. }
  104. int getRequiredHeight() const
  105. {
  106. auto numMessages = (int) messages.size();
  107. if (numMessages > 0)
  108. return (numMessages * messageHeight) + ((numMessages - 1) * messageSpacing);
  109. return 0;
  110. }
  111. void updateSize (int parentWidth)
  112. {
  113. setSize (parentWidth, getRequiredHeight());
  114. }
  115. private:
  116. static constexpr int messageHeight = 65;
  117. static constexpr int messageSpacing = 2;
  118. //==============================================================================
  119. struct MessageComponent final : public Component
  120. {
  121. MessageComponent (MessagesListComponent& listComponent,
  122. const Identifier& messageToDisplay,
  123. std::vector<ProjectMessages::MessageAction> messageActions)
  124. : message (messageToDisplay)
  125. {
  126. for (auto& action : messageActions)
  127. {
  128. auto button = std::make_unique<TextButton> (action.first);
  129. addAndMakeVisible (*button);
  130. button->onClick = action.second;
  131. buttons.push_back (std::move (button));
  132. }
  133. icon = (ProjectMessages::getTypeForMessage (message) == ProjectMessages::Ids::warning ? getIcons().warning : getIcons().info);
  134. messageTitleLabel.setText (ProjectMessages::getTitleForMessage (message), dontSendNotification);
  135. messageTitleLabel.setFont (Font (11.0f).boldened());
  136. addAndMakeVisible (messageTitleLabel);
  137. messageDescriptionLabel.setText (ProjectMessages::getDescriptionForMessage (message), dontSendNotification);
  138. messageDescriptionLabel.setFont (Font (11.0f));
  139. messageDescriptionLabel.setJustificationType (Justification::topLeft);
  140. addAndMakeVisible (messageDescriptionLabel);
  141. dismissButton.setShape (getLookAndFeel().getCrossShape (1.0f), false, true, false);
  142. addAndMakeVisible (dismissButton);
  143. dismissButton.onClick = [this, &listComponent]
  144. {
  145. listComponent.messagesTree.getChildWithName (ProjectMessages::getTypeForMessage (message))
  146. .getChildWithName (message)
  147. .setProperty (ProjectMessages::Ids::isVisible, false, nullptr);
  148. };
  149. }
  150. void paint (Graphics& g) override
  151. {
  152. g.fillAll (findColour (secondaryBackgroundColourId).contrasting (0.1f));
  153. auto bounds = getLocalBounds().reduced (5);
  154. g.setColour (findColour (defaultIconColourId));
  155. g.fillPath (icon, icon.getTransformToScaleToFit (bounds.removeFromTop (messageTitleHeight)
  156. .removeFromLeft (messageTitleHeight).toFloat(), true));
  157. }
  158. void resized() override
  159. {
  160. auto bounds = getLocalBounds().reduced (5);
  161. auto topSlice = bounds.removeFromTop (messageTitleHeight);
  162. topSlice.removeFromLeft (messageTitleHeight + 5);
  163. topSlice.removeFromRight (5);
  164. dismissButton.setBounds (topSlice.removeFromRight (messageTitleHeight));
  165. messageTitleLabel.setBounds (topSlice);
  166. bounds.removeFromTop (5);
  167. auto numButtons = (int) buttons.size();
  168. if (numButtons > 0)
  169. {
  170. auto buttonBounds = bounds.removeFromBottom (buttonHeight);
  171. auto buttonWidth = roundToInt ((float) buttonBounds.getWidth() / 3.5f);
  172. auto requiredWidth = (numButtons * buttonWidth) + ((numButtons - 1) * buttonSpacing);
  173. buttonBounds.reduce ((buttonBounds.getWidth() - requiredWidth) / 2, 0);
  174. for (auto& b : buttons)
  175. {
  176. b->setBounds (buttonBounds.removeFromLeft (buttonWidth));
  177. buttonBounds.removeFromLeft (buttonSpacing);
  178. }
  179. bounds.removeFromBottom (5);
  180. }
  181. messageDescriptionLabel.setBounds (bounds);
  182. }
  183. static constexpr int messageTitleHeight = 11;
  184. static constexpr int buttonHeight = messageHeight / 4;
  185. static constexpr int buttonSpacing = 5;
  186. Identifier message;
  187. Path icon;
  188. Label messageTitleLabel, messageDescriptionLabel;
  189. std::vector<std::unique_ptr<TextButton>> buttons;
  190. ShapeButton dismissButton { {},
  191. findColour (treeIconColourId),
  192. findColour (treeIconColourId).overlaidWith (findColour (defaultHighlightedTextColourId).withAlpha (0.2f)),
  193. findColour (treeIconColourId).overlaidWith (findColour (defaultHighlightedTextColourId).withAlpha (0.4f)) };
  194. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MessageComponent)
  195. };
  196. //==============================================================================
  197. void valueTreePropertyChanged (ValueTree&, const Identifier&) override { messagesChanged(); }
  198. void valueTreeChildAdded (ValueTree&, ValueTree&) override { messagesChanged(); }
  199. void valueTreeChildRemoved (ValueTree&, ValueTree&, int) override { messagesChanged(); }
  200. void valueTreeChildOrderChanged (ValueTree&, int, int) override { messagesChanged(); }
  201. void valueTreeParentChanged (ValueTree&) override { messagesChanged(); }
  202. void valueTreeRedirected (ValueTree&) override { messagesChanged(); }
  203. void handleAsyncUpdate() override
  204. {
  205. messagesChanged();
  206. }
  207. void messagesChanged()
  208. {
  209. auto listWasShowing = (getHeight() > 0);
  210. auto warningsTree = messagesTree.getChildWithName (ProjectMessages::Ids::warning);
  211. auto notificationsTree = messagesTree.getChildWithName (ProjectMessages::Ids::notification);
  212. auto removePredicate = [warningsTree, notificationsTree] (std::unique_ptr<MessageComponent>& messageComponent)
  213. {
  214. for (int i = 0; i < warningsTree.getNumChildren(); ++i)
  215. {
  216. auto child = warningsTree.getChild (i);
  217. if (child.getType() == messageComponent->message
  218. && child.getProperty (ProjectMessages::Ids::isVisible))
  219. {
  220. return false;
  221. }
  222. }
  223. for (int i = 0; i < notificationsTree.getNumChildren(); ++i)
  224. {
  225. auto child = notificationsTree.getChild (i);
  226. if (child.getType() == messageComponent->message
  227. && child.getProperty (ProjectMessages::Ids::isVisible))
  228. {
  229. return false;
  230. }
  231. }
  232. return true;
  233. };
  234. messages.erase (std::remove_if (messages.begin(), messages.end(), removePredicate),
  235. messages.end());
  236. for (auto* tree : { &warningsTree, &notificationsTree })
  237. {
  238. for (int i = 0; i < tree->getNumChildren(); ++i)
  239. {
  240. auto child = tree->getChild (i);
  241. if (! child.getProperty (ProjectMessages::Ids::isVisible))
  242. continue;
  243. const auto messageMatchesType = [&child] (const auto& messageComponent)
  244. {
  245. return messageComponent->message == child.getType();
  246. };
  247. if (std::none_of (messages.begin(), messages.end(), messageMatchesType))
  248. {
  249. messages.push_back (std::make_unique<MessageComponent> (*this,
  250. child.getType(),
  251. project.getMessageActions (child.getType())));
  252. addAndMakeVisible (*messages.back());
  253. }
  254. }
  255. }
  256. const auto isNowShowing = (messages.size() > 0);
  257. owner.updateBounds (isNowShowing != listWasShowing);
  258. updateSize (owner.getWidth());
  259. }
  260. //==============================================================================
  261. MessagesPopupWindow& owner;
  262. Project& project;
  263. ValueTree messagesTree;
  264. std::vector<std::unique_ptr<MessageComponent>> messages;
  265. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MessagesListComponent)
  266. };
  267. //==============================================================================
  268. void componentMovedOrResized (bool, bool) override
  269. {
  270. if (isListShowing())
  271. updateBounds (false);
  272. }
  273. using ComponentMovementWatcher::componentMovedOrResized;
  274. void componentPeerChanged() override
  275. {
  276. if (isListShowing())
  277. updateBounds (false);
  278. }
  279. void componentVisibilityChanged() override
  280. {
  281. if (isListShowing())
  282. updateBounds (false);
  283. }
  284. using ComponentMovementWatcher::componentVisibilityChanged;
  285. //==============================================================================
  286. static constexpr int maxHeight = 500, width = 350, indent = 20;
  287. Component& targetComponent;
  288. Component& parentComponent;
  289. Viewport viewport;
  290. MessagesListComponent messagesListComponent;
  291. //==============================================================================
  292. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MessagesPopupWindow)
  293. };
  294. //==============================================================================
  295. class ProjectMessagesComponent final : public Component
  296. {
  297. public:
  298. ProjectMessagesComponent()
  299. {
  300. setFocusContainerType (FocusContainerType::focusContainer);
  301. setTitle ("Project Messages");
  302. addAndMakeVisible (warningsComponent);
  303. addAndMakeVisible (notificationsComponent);
  304. warningsComponent.addMouseListener (this, true);
  305. notificationsComponent.addMouseListener (this, true);
  306. setOpaque (true);
  307. }
  308. //==============================================================================
  309. void resized() override
  310. {
  311. auto b = getLocalBounds();
  312. warningsComponent.setBounds (b.removeFromLeft (b.getWidth() / 2).reduced (5));
  313. notificationsComponent.setBounds (b.reduced (5));
  314. }
  315. void paint (Graphics& g) override
  316. {
  317. auto backgroundColour = findColour (backgroundColourId);
  318. if (isMouseDown || isMouseOver)
  319. backgroundColour = backgroundColour.overlaidWith (findColour (defaultHighlightColourId)
  320. .withAlpha (isMouseDown ? 1.0f : 0.8f));
  321. g.fillAll (backgroundColour);
  322. }
  323. //==============================================================================
  324. void mouseEnter (const MouseEvent&) override
  325. {
  326. isMouseOver = true;
  327. repaint();
  328. }
  329. void mouseExit (const MouseEvent&) override
  330. {
  331. isMouseOver = false;
  332. repaint();
  333. }
  334. void mouseDown (const MouseEvent&) override
  335. {
  336. isMouseDown = true;
  337. repaint();
  338. }
  339. void mouseUp (const MouseEvent&) override
  340. {
  341. isMouseDown = false;
  342. repaint();
  343. showOrHideMessagesWindow();
  344. }
  345. std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override
  346. {
  347. return std::make_unique<AccessibilityHandler> (*this,
  348. AccessibilityRole::button,
  349. AccessibilityActions().addAction (AccessibilityActionType::press,
  350. [this] { showOrHideMessagesWindow(); }));
  351. }
  352. //==============================================================================
  353. void setProject (Project* newProject)
  354. {
  355. if (currentProject != newProject)
  356. {
  357. currentProject = newProject;
  358. if (currentProject != nullptr)
  359. {
  360. if (auto* projectWindow = ProjucerApplication::getApp().mainWindowList.getMainWindowForFile (currentProject->getFile()))
  361. messagesWindow = std::make_unique<MessagesPopupWindow> (*this, *projectWindow, *currentProject);
  362. auto projectMessagesTree = currentProject->getProjectMessages();
  363. warningsComponent.setTree (projectMessagesTree.getChildWithName (ProjectMessages::Ids::warning));
  364. notificationsComponent.setTree (projectMessagesTree.getChildWithName (ProjectMessages::Ids::notification));
  365. }
  366. else
  367. {
  368. warningsComponent.setTree ({});
  369. notificationsComponent.setTree ({});
  370. }
  371. }
  372. }
  373. void numMessagesChanged()
  374. {
  375. const auto total = warningsComponent.getNumMessages()
  376. + notificationsComponent.getNumMessages();
  377. setHelpText (String (total) + (total == 1 ? " message" : " messages"));
  378. }
  379. void showOrHideMessagesWindow()
  380. {
  381. if (messagesWindow != nullptr)
  382. showOrHideAllMessages (! messagesWindow->isListShowing());
  383. }
  384. private:
  385. //==============================================================================
  386. struct MessageCountComponent final : public Component,
  387. private ValueTree::Listener
  388. {
  389. MessageCountComponent (ProjectMessagesComponent& o, Path pathToUse)
  390. : owner (o),
  391. path (pathToUse)
  392. {
  393. setInterceptsMouseClicks (false, false);
  394. }
  395. void paint (Graphics& g) override
  396. {
  397. auto b = getLocalBounds().toFloat();
  398. g.setColour (findColour ((owner.isMouseDown || owner.isMouseOver) ? defaultHighlightedTextColourId : treeIconColourId));
  399. g.fillPath (path, path.getTransformToScaleToFit (b.removeFromLeft (b.getWidth() / 2.0f), true));
  400. b.removeFromLeft (5);
  401. g.drawFittedText (String (numMessages), b.getSmallestIntegerContainer(), Justification::centredLeft, 1);
  402. }
  403. void setTree (ValueTree tree)
  404. {
  405. messagesTree = tree;
  406. if (messagesTree.isValid())
  407. messagesTree.addListener (this);
  408. updateNumMessages();
  409. }
  410. void updateNumMessages()
  411. {
  412. numMessages = messagesTree.getNumChildren();
  413. owner.numMessagesChanged();
  414. repaint();
  415. }
  416. int getNumMessages() const noexcept { return numMessages; }
  417. private:
  418. void valueTreeChildAdded (ValueTree&, ValueTree&) override { updateNumMessages(); }
  419. void valueTreeChildRemoved (ValueTree&, ValueTree&, int) override { updateNumMessages(); }
  420. ProjectMessagesComponent& owner;
  421. ValueTree messagesTree;
  422. Path path;
  423. int numMessages = 0;
  424. };
  425. void showOrHideAllMessages (bool shouldBeVisible)
  426. {
  427. if (currentProject != nullptr)
  428. {
  429. auto messagesTree = currentProject->getProjectMessages();
  430. auto setVisible = [shouldBeVisible] (ValueTree subTree)
  431. {
  432. for (int i = 0; i < subTree.getNumChildren(); ++i)
  433. subTree.getChild (i).setProperty (ProjectMessages::Ids::isVisible, shouldBeVisible, nullptr);
  434. };
  435. setVisible (messagesTree.getChildWithName (ProjectMessages::Ids::warning));
  436. setVisible (messagesTree.getChildWithName (ProjectMessages::Ids::notification));
  437. }
  438. }
  439. //==============================================================================
  440. Project* currentProject = nullptr;
  441. bool isMouseOver = false, isMouseDown = false;
  442. MessageCountComponent warningsComponent { *this, getIcons().warning },
  443. notificationsComponent { *this, getIcons().info };
  444. std::unique_ptr<MessagesPopupWindow> messagesWindow;
  445. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ProjectMessagesComponent)
  446. };