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.

545 lines
20KB

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