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.

547 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. messagesListComponent.resized();
  66. }
  67. private:
  68. //==============================================================================
  69. class MessagesListComponent : public Component,
  70. private ValueTree::Listener,
  71. private AsyncUpdater
  72. {
  73. public:
  74. MessagesListComponent (MessagesPopupWindow& o, Project& currentProject)
  75. : owner (o),
  76. project (currentProject)
  77. {
  78. messagesTree = project.getProjectMessages();
  79. messagesTree.addListener (this);
  80. setOpaque (true);
  81. messagesChanged();
  82. }
  83. void resized() override
  84. {
  85. auto bounds = getLocalBounds();
  86. auto numMessages = messages.size();
  87. for (size_t i = 0; i < numMessages; ++i)
  88. {
  89. messages[i]->setBounds (bounds.removeFromTop (messageHeight));
  90. if (numMessages > 1 && i != (numMessages - 1))
  91. bounds.removeFromTop (messageSpacing);
  92. }
  93. }
  94. void paint (Graphics& g) override
  95. {
  96. g.fillAll (findColour (backgroundColourId).contrasting (0.2f));
  97. }
  98. int getRequiredHeight() const
  99. {
  100. auto numMessages = (int) messages.size();
  101. if (numMessages > 0)
  102. return (numMessages * messageHeight) + ((numMessages - 1) * messageSpacing);
  103. return 0;
  104. }
  105. void updateSize (int parentWidth)
  106. {
  107. setSize (parentWidth, getRequiredHeight());
  108. }
  109. private:
  110. static constexpr int messageHeight = 65;
  111. static constexpr int messageSpacing = 2;
  112. //==============================================================================
  113. struct MessageComponent : public Component
  114. {
  115. MessageComponent (MessagesListComponent& listComponent,
  116. const Identifier& messageToDisplay,
  117. std::vector<ProjectMessages::MessageAction> messageActions)
  118. : message (messageToDisplay)
  119. {
  120. for (auto& action : messageActions)
  121. {
  122. auto button = std::make_unique<TextButton> (action.first);
  123. addAndMakeVisible (*button);
  124. button->onClick = action.second;
  125. buttons.push_back (std::move (button));
  126. }
  127. icon = (ProjectMessages::getTypeForMessage (message) == ProjectMessages::Ids::warning ? getIcons().warning : getIcons().info);
  128. messageTitleLabel.setText (ProjectMessages::getTitleForMessage (message), dontSendNotification);
  129. messageTitleLabel.setFont (Font (11.0f).boldened());
  130. addAndMakeVisible (messageTitleLabel);
  131. messageDescriptionLabel.setText (ProjectMessages::getDescriptionForMessage (message), dontSendNotification);
  132. messageDescriptionLabel.setFont (Font (11.0f));
  133. messageDescriptionLabel.setJustificationType (Justification::topLeft);
  134. addAndMakeVisible (messageDescriptionLabel);
  135. dismissButton.setShape (getLookAndFeel().getCrossShape (1.0f), false, true, false);
  136. addAndMakeVisible (dismissButton);
  137. dismissButton.onClick = [this, &listComponent]
  138. {
  139. listComponent.messagesTree.getChildWithName (ProjectMessages::getTypeForMessage (message))
  140. .getChildWithName (message)
  141. .setProperty (ProjectMessages::Ids::isVisible, false, nullptr);
  142. };
  143. }
  144. void paint (Graphics& g) override
  145. {
  146. g.fillAll (findColour (secondaryBackgroundColourId).contrasting (0.1f));
  147. auto bounds = getLocalBounds().reduced (5);
  148. g.setColour (findColour (defaultIconColourId));
  149. g.fillPath (icon, icon.getTransformToScaleToFit (bounds.removeFromTop (messageTitleHeight)
  150. .removeFromLeft (messageTitleHeight).toFloat(), true));
  151. }
  152. void resized() override
  153. {
  154. auto bounds = getLocalBounds().reduced (5);
  155. auto topSlice = bounds.removeFromTop (messageTitleHeight);
  156. topSlice.removeFromLeft (messageTitleHeight + 5);
  157. topSlice.removeFromRight (5);
  158. dismissButton.setBounds (topSlice.removeFromRight (messageTitleHeight));
  159. messageTitleLabel.setBounds (topSlice);
  160. bounds.removeFromTop (5);
  161. auto numButtons = (int) buttons.size();
  162. if (numButtons > 0)
  163. {
  164. auto buttonBounds = bounds.removeFromBottom (buttonHeight);
  165. auto buttonWidth = roundToInt (buttonBounds.getWidth() / 3.5f);
  166. auto requiredWidth = (numButtons * buttonWidth) + ((numButtons - 1) * buttonSpacing);
  167. buttonBounds.reduce ((buttonBounds.getWidth() - requiredWidth) / 2, 0);
  168. for (auto& b : buttons)
  169. {
  170. b->setBounds (buttonBounds.removeFromLeft (buttonWidth));
  171. buttonBounds.removeFromLeft (buttonSpacing);
  172. }
  173. bounds.removeFromBottom (5);
  174. }
  175. messageDescriptionLabel.setBounds (bounds);
  176. }
  177. static constexpr int messageTitleHeight = 11;
  178. static constexpr int buttonHeight = messageHeight / 4;
  179. static constexpr int buttonSpacing = 5;
  180. Identifier message;
  181. Path icon;
  182. Label messageTitleLabel, messageDescriptionLabel;
  183. std::vector<std::unique_ptr<TextButton>> buttons;
  184. ShapeButton dismissButton { {},
  185. findColour (treeIconColourId),
  186. findColour (treeIconColourId).overlaidWith (findColour (defaultHighlightedTextColourId).withAlpha (0.2f)),
  187. findColour (treeIconColourId).overlaidWith (findColour (defaultHighlightedTextColourId).withAlpha (0.4f)) };
  188. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MessageComponent)
  189. };
  190. //==============================================================================
  191. void valueTreePropertyChanged (ValueTree&, const Identifier&) override { triggerAsyncUpdate(); }
  192. void valueTreeChildAdded (ValueTree&, ValueTree&) override { triggerAsyncUpdate(); }
  193. void valueTreeChildRemoved (ValueTree&, ValueTree&, int) override { triggerAsyncUpdate(); }
  194. void valueTreeChildOrderChanged (ValueTree&, int, int) override { triggerAsyncUpdate(); }
  195. void valueTreeParentChanged (ValueTree&) override { triggerAsyncUpdate(); }
  196. void valueTreeRedirected (ValueTree&) override { triggerAsyncUpdate(); }
  197. void handleAsyncUpdate() override
  198. {
  199. messagesChanged();
  200. }
  201. void messagesChanged()
  202. {
  203. auto listWasShowing = (getHeight() > 0);
  204. auto warningsTree = messagesTree.getChildWithName (ProjectMessages::Ids::warning);
  205. auto notificationsTree = messagesTree.getChildWithName (ProjectMessages::Ids::notification);
  206. auto removePredicate = [warningsTree, notificationsTree] (std::unique_ptr<MessageComponent>& messageComponent)
  207. {
  208. for (int i = 0; i < warningsTree.getNumChildren(); ++i)
  209. {
  210. auto child = warningsTree.getChild (i);
  211. if (child.getType() == messageComponent->message
  212. && child.getProperty (ProjectMessages::Ids::isVisible))
  213. {
  214. return false;
  215. }
  216. }
  217. for (int i = 0; i < notificationsTree.getNumChildren(); ++i)
  218. {
  219. auto child = notificationsTree.getChild (i);
  220. if (child.getType() == messageComponent->message
  221. && child.getProperty (ProjectMessages::Ids::isVisible))
  222. {
  223. return false;
  224. }
  225. }
  226. return true;
  227. };
  228. messages.erase (std::remove_if (std::begin (messages), std::end (messages), removePredicate),
  229. std::end (messages));
  230. for (int i = 0; i < warningsTree.getNumChildren(); ++i)
  231. {
  232. auto child = warningsTree.getChild (i);
  233. if (! child.getProperty (ProjectMessages::Ids::isVisible))
  234. continue;
  235. if (std::find_if (std::begin (messages), std::end (messages),
  236. [child] (const std::unique_ptr<MessageComponent>& messageComponent) { return messageComponent->message == child.getType(); })
  237. == std::end (messages))
  238. {
  239. messages.push_back (std::make_unique<MessageComponent> (*this, child.getType(), project.getMessageActions (child.getType())));
  240. addAndMakeVisible (*messages.back());
  241. }
  242. }
  243. for (int i = 0; i < notificationsTree.getNumChildren(); ++i)
  244. {
  245. auto child = notificationsTree.getChild (i);
  246. if (! child.getProperty (ProjectMessages::Ids::isVisible))
  247. continue;
  248. if (std::find_if (std::begin (messages), std::end (messages),
  249. [child] (const std::unique_ptr<MessageComponent>& messageComponent) { return messageComponent->message == child.getType(); })
  250. == std::end (messages))
  251. {
  252. messages.push_back (std::make_unique<MessageComponent> (*this, child.getType(), project.getMessageActions (child.getType())));
  253. addAndMakeVisible (*messages.back());
  254. }
  255. }
  256. 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 : public Component
  296. {
  297. public:
  298. ProjectMessagesComponent()
  299. {
  300. addAndMakeVisible (warningsComponent);
  301. addAndMakeVisible (notificationsComponent);
  302. warningsComponent.addMouseListener (this, true);
  303. notificationsComponent.addMouseListener (this, true);
  304. setOpaque (true);
  305. }
  306. //==============================================================================
  307. void resized() override
  308. {
  309. auto b = getLocalBounds();
  310. warningsComponent.setBounds (b.removeFromLeft (b.getWidth() / 2).reduced (5));
  311. notificationsComponent.setBounds (b.reduced (5));
  312. }
  313. void paint (Graphics& g) override
  314. {
  315. auto backgroundColour = findColour (backgroundColourId);
  316. if (isMouseDown || isMouseOver)
  317. backgroundColour = backgroundColour.overlaidWith (findColour (defaultHighlightColourId)
  318. .withAlpha (isMouseDown ? 1.0f : 0.8f));
  319. g.fillAll (backgroundColour);
  320. }
  321. //==============================================================================
  322. void mouseEnter (const MouseEvent&) override
  323. {
  324. isMouseOver = true;
  325. repaint();
  326. }
  327. void mouseExit (const MouseEvent&) override
  328. {
  329. isMouseOver = false;
  330. repaint();
  331. }
  332. void mouseDown (const MouseEvent&) override
  333. {
  334. isMouseDown = true;
  335. repaint();
  336. }
  337. void mouseUp (const MouseEvent&) override
  338. {
  339. isMouseDown = false;
  340. repaint();
  341. if (messagesWindow != nullptr)
  342. showOrHideAllMessages (! messagesWindow->isListShowing());
  343. }
  344. //==============================================================================
  345. void setProject (Project* newProject)
  346. {
  347. if (currentProject != newProject)
  348. {
  349. currentProject = newProject;
  350. if (currentProject != nullptr)
  351. {
  352. auto* projectWindow = ProjucerApplication::getApp().mainWindowList.getMainWindowForFile (currentProject->getFile());
  353. jassert (projectWindow != nullptr);
  354. messagesWindow = std::make_unique<MessagesPopupWindow> (*this, *projectWindow, *currentProject);
  355. auto projectMessagesTree = currentProject->getProjectMessages();
  356. warningsComponent.setTree (projectMessagesTree.getChildWithName (ProjectMessages::Ids::warning));
  357. notificationsComponent.setTree (projectMessagesTree.getChildWithName (ProjectMessages::Ids::notification));
  358. }
  359. else
  360. {
  361. warningsComponent.setTree ({});
  362. notificationsComponent.setTree ({});
  363. }
  364. }
  365. }
  366. private:
  367. //==============================================================================
  368. struct MessageCountComponent : public Component,
  369. public ValueTree::Listener
  370. {
  371. MessageCountComponent (ProjectMessagesComponent& o, Path pathToUse)
  372. : owner (o),
  373. path (pathToUse)
  374. {
  375. setInterceptsMouseClicks (false, false);
  376. }
  377. void paint (Graphics& g) override
  378. {
  379. auto b = getLocalBounds().toFloat();
  380. g.setColour (findColour ((owner.isMouseDown || owner.isMouseOver) ? defaultHighlightedTextColourId : treeIconColourId));
  381. g.fillPath (path, path.getTransformToScaleToFit (b.removeFromLeft (b.getWidth() / 2.0f), true));
  382. b.removeFromLeft (5);
  383. g.drawFittedText (String (numMessages), b.getSmallestIntegerContainer(), Justification::centredLeft, 1);
  384. }
  385. void setTree (ValueTree tree)
  386. {
  387. messagesTree = tree;
  388. if (messagesTree.isValid())
  389. messagesTree.addListener (this);
  390. updateNumMessages();
  391. }
  392. void valueTreeChildAdded (ValueTree&, ValueTree&) override { updateNumMessages(); }
  393. void valueTreeChildRemoved (ValueTree&, ValueTree&, int) override { updateNumMessages(); }
  394. void updateNumMessages()
  395. {
  396. numMessages = messagesTree.getNumChildren();
  397. repaint();
  398. }
  399. ProjectMessagesComponent& owner;
  400. ValueTree messagesTree;
  401. Path path;
  402. int numMessages = 0;
  403. };
  404. void showOrHideAllMessages (bool shouldBeVisible)
  405. {
  406. if (currentProject != nullptr)
  407. {
  408. auto messagesTree = currentProject->getProjectMessages();
  409. auto setVisible = [shouldBeVisible] (ValueTree subTree)
  410. {
  411. for (int i = 0; i < subTree.getNumChildren(); ++i)
  412. subTree.getChild (i).setProperty (ProjectMessages::Ids::isVisible, shouldBeVisible, nullptr);
  413. };
  414. setVisible (messagesTree.getChildWithName (ProjectMessages::Ids::warning));
  415. setVisible (messagesTree.getChildWithName (ProjectMessages::Ids::notification));
  416. }
  417. }
  418. //==============================================================================
  419. Project* currentProject = nullptr;
  420. bool isMouseOver = false, isMouseDown = false;
  421. MessageCountComponent warningsComponent { *this, getIcons().warning },
  422. notificationsComponent { *this, getIcons().info };
  423. std::unique_ptr<MessagesPopupWindow> messagesWindow;
  424. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ProjectMessagesComponent)
  425. };