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.

567 lines
21KB

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