/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2020 - Raw Material Software Limited JUCE is an open source library subject to commercial or open-source licensing. By using JUCE, you agree to the terms of both the JUCE 6 End-User License Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). End User License Agreement: www.juce.com/juce-6-licence Privacy Policy: www.juce.com/juce-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ #pragma once #include "../../Application/jucer_CommonHeaders.h" #include "../../Application/jucer_Application.h" //============================================================================== class MessagesPopupWindow : public Component, private ComponentMovementWatcher { public: MessagesPopupWindow (Component& target, Component& parent, Project& project) : ComponentMovementWatcher (&parent), targetComponent (target), parentComponent (parent), messagesListComponent (*this, project) { parentComponent.addAndMakeVisible (this); setAlwaysOnTop (true); addAndMakeVisible (viewport); viewport.setScrollBarsShown (true, false); viewport.setViewedComponent (&messagesListComponent, false); viewport.setWantsKeyboardFocus (false); setOpaque (true); } void paint (Graphics& g) override { g.fillAll (findColour (secondaryBackgroundColourId)); } void resized() override { viewport.setBounds (getLocalBounds()); } bool isListShowing() const { return messagesListComponent.getRequiredHeight() > 0; } void updateBounds (bool animate) { auto targetBounds = parentComponent.getLocalArea (&targetComponent, targetComponent.getLocalBounds()); auto height = jmin (messagesListComponent.getRequiredHeight(), maxHeight); auto yPos = jmax (indent, targetBounds.getY() - height); Rectangle bounds (targetBounds.getX(), yPos, jmin (width, parentComponent.getWidth() - targetBounds.getX() - indent), targetBounds.getY() - yPos); auto& animator = Desktop::getInstance().getAnimator(); if (animate) { setBounds (bounds.withY (targetBounds.getY())); animator.animateComponent (this, bounds, 1.0f, 150, false, 1.0, 1.0); } else { if (animator.isAnimating (this)) animator.cancelAnimation (this, false); setBounds (bounds); } messagesListComponent.resized(); } private: //============================================================================== class MessagesListComponent : public Component, private ValueTree::Listener, private AsyncUpdater { public: MessagesListComponent (MessagesPopupWindow& o, Project& currentProject) : owner (o), project (currentProject) { messagesTree = project.getProjectMessages(); messagesTree.addListener (this); setOpaque (true); messagesChanged(); } void resized() override { auto bounds = getLocalBounds(); auto numMessages = messages.size(); for (size_t i = 0; i < numMessages; ++i) { messages[i]->setBounds (bounds.removeFromTop (messageHeight)); if (numMessages > 1 && i != (numMessages - 1)) bounds.removeFromTop (messageSpacing); } } void paint (Graphics& g) override { g.fillAll (findColour (backgroundColourId).contrasting (0.2f)); } int getRequiredHeight() const { auto numMessages = (int) messages.size(); if (numMessages > 0) return (numMessages * messageHeight) + ((numMessages - 1) * messageSpacing); return 0; } void updateSize (int parentWidth) { setSize (parentWidth, getRequiredHeight()); } private: static constexpr int messageHeight = 65; static constexpr int messageSpacing = 2; //============================================================================== struct MessageComponent : public Component { MessageComponent (MessagesListComponent& listComponent, const Identifier& messageToDisplay, std::vector messageActions) : message (messageToDisplay) { for (auto& action : messageActions) { auto button = std::make_unique (action.first); addAndMakeVisible (*button); button->onClick = action.second; buttons.push_back (std::move (button)); } icon = (ProjectMessages::getTypeForMessage (message) == ProjectMessages::Ids::warning ? getIcons().warning : getIcons().info); messageTitleLabel.setText (ProjectMessages::getTitleForMessage (message), dontSendNotification); messageTitleLabel.setFont (Font (11.0f).boldened()); addAndMakeVisible (messageTitleLabel); messageDescriptionLabel.setText (ProjectMessages::getDescriptionForMessage (message), dontSendNotification); messageDescriptionLabel.setFont (Font (11.0f)); messageDescriptionLabel.setJustificationType (Justification::topLeft); addAndMakeVisible (messageDescriptionLabel); dismissButton.setShape (getLookAndFeel().getCrossShape (1.0f), false, true, false); addAndMakeVisible (dismissButton); dismissButton.onClick = [this, &listComponent] { listComponent.messagesTree.getChildWithName (ProjectMessages::getTypeForMessage (message)) .getChildWithName (message) .setProperty (ProjectMessages::Ids::isVisible, false, nullptr); }; } void paint (Graphics& g) override { g.fillAll (findColour (secondaryBackgroundColourId).contrasting (0.1f)); auto bounds = getLocalBounds().reduced (5); g.setColour (findColour (defaultIconColourId)); g.fillPath (icon, icon.getTransformToScaleToFit (bounds.removeFromTop (messageTitleHeight) .removeFromLeft (messageTitleHeight).toFloat(), true)); } void resized() override { auto bounds = getLocalBounds().reduced (5); auto topSlice = bounds.removeFromTop (messageTitleHeight); topSlice.removeFromLeft (messageTitleHeight + 5); topSlice.removeFromRight (5); dismissButton.setBounds (topSlice.removeFromRight (messageTitleHeight)); messageTitleLabel.setBounds (topSlice); bounds.removeFromTop (5); auto numButtons = (int) buttons.size(); if (numButtons > 0) { auto buttonBounds = bounds.removeFromBottom (buttonHeight); auto buttonWidth = roundToInt ((float) buttonBounds.getWidth() / 3.5f); auto requiredWidth = (numButtons * buttonWidth) + ((numButtons - 1) * buttonSpacing); buttonBounds.reduce ((buttonBounds.getWidth() - requiredWidth) / 2, 0); for (auto& b : buttons) { b->setBounds (buttonBounds.removeFromLeft (buttonWidth)); buttonBounds.removeFromLeft (buttonSpacing); } bounds.removeFromBottom (5); } messageDescriptionLabel.setBounds (bounds); } static constexpr int messageTitleHeight = 11; static constexpr int buttonHeight = messageHeight / 4; static constexpr int buttonSpacing = 5; Identifier message; Path icon; Label messageTitleLabel, messageDescriptionLabel; std::vector> buttons; ShapeButton dismissButton { {}, findColour (treeIconColourId), findColour (treeIconColourId).overlaidWith (findColour (defaultHighlightedTextColourId).withAlpha (0.2f)), findColour (treeIconColourId).overlaidWith (findColour (defaultHighlightedTextColourId).withAlpha (0.4f)) }; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MessageComponent) }; //============================================================================== void valueTreePropertyChanged (ValueTree&, const Identifier&) override { messagesChanged(); } void valueTreeChildAdded (ValueTree&, ValueTree&) override { messagesChanged(); } void valueTreeChildRemoved (ValueTree&, ValueTree&, int) override { messagesChanged(); } void valueTreeChildOrderChanged (ValueTree&, int, int) override { messagesChanged(); } void valueTreeParentChanged (ValueTree&) override { messagesChanged(); } void valueTreeRedirected (ValueTree&) override { messagesChanged(); } void handleAsyncUpdate() override { messagesChanged(); } void messagesChanged() { auto listWasShowing = (getHeight() > 0); auto warningsTree = messagesTree.getChildWithName (ProjectMessages::Ids::warning); auto notificationsTree = messagesTree.getChildWithName (ProjectMessages::Ids::notification); auto removePredicate = [warningsTree, notificationsTree] (std::unique_ptr& messageComponent) { for (int i = 0; i < warningsTree.getNumChildren(); ++i) { auto child = warningsTree.getChild (i); if (child.getType() == messageComponent->message && child.getProperty (ProjectMessages::Ids::isVisible)) { return false; } } for (int i = 0; i < notificationsTree.getNumChildren(); ++i) { auto child = notificationsTree.getChild (i); if (child.getType() == messageComponent->message && child.getProperty (ProjectMessages::Ids::isVisible)) { return false; } } return true; }; messages.erase (std::remove_if (std::begin (messages), std::end (messages), removePredicate), std::end (messages)); for (int i = 0; i < warningsTree.getNumChildren(); ++i) { auto child = warningsTree.getChild (i); if (! child.getProperty (ProjectMessages::Ids::isVisible)) continue; if (std::find_if (std::begin (messages), std::end (messages), [child] (const std::unique_ptr& messageComponent) { return messageComponent->message == child.getType(); }) == std::end (messages)) { messages.push_back (std::make_unique (*this, child.getType(), project.getMessageActions (child.getType()))); addAndMakeVisible (*messages.back()); } } for (int i = 0; i < notificationsTree.getNumChildren(); ++i) { auto child = notificationsTree.getChild (i); if (! child.getProperty (ProjectMessages::Ids::isVisible)) continue; if (std::find_if (std::begin (messages), std::end (messages), [child] (const std::unique_ptr& messageComponent) { return messageComponent->message == child.getType(); }) == std::end (messages)) { messages.push_back (std::make_unique (*this, child.getType(), project.getMessageActions (child.getType()))); addAndMakeVisible (*messages.back()); } } auto isNowShowing = (messages.size() > 0); owner.updateBounds (isNowShowing != listWasShowing); updateSize (owner.getWidth()); } //============================================================================== MessagesPopupWindow& owner; Project& project; ValueTree messagesTree; std::vector> messages; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MessagesListComponent) }; //============================================================================== void componentMovedOrResized (bool, bool) override { if (isListShowing()) updateBounds (false); } using ComponentMovementWatcher::componentMovedOrResized; void componentPeerChanged() override { if (isListShowing()) updateBounds (false); } void componentVisibilityChanged() override { if (isListShowing()) updateBounds (false); } using ComponentMovementWatcher::componentVisibilityChanged; //============================================================================== static constexpr int maxHeight = 500, width = 350, indent = 20; Component& targetComponent; Component& parentComponent; Viewport viewport; MessagesListComponent messagesListComponent; //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MessagesPopupWindow) }; //============================================================================== class ProjectMessagesComponent : public Component { public: ProjectMessagesComponent() { setFocusContainerType (FocusContainerType::focusContainer); setTitle ("Project Messages"); addAndMakeVisible (warningsComponent); addAndMakeVisible (notificationsComponent); warningsComponent.addMouseListener (this, true); notificationsComponent.addMouseListener (this, true); setOpaque (true); } //============================================================================== void resized() override { auto b = getLocalBounds(); warningsComponent.setBounds (b.removeFromLeft (b.getWidth() / 2).reduced (5)); notificationsComponent.setBounds (b.reduced (5)); } void paint (Graphics& g) override { auto backgroundColour = findColour (backgroundColourId); if (isMouseDown || isMouseOver) backgroundColour = backgroundColour.overlaidWith (findColour (defaultHighlightColourId) .withAlpha (isMouseDown ? 1.0f : 0.8f)); g.fillAll (backgroundColour); } //============================================================================== void mouseEnter (const MouseEvent&) override { isMouseOver = true; repaint(); } void mouseExit (const MouseEvent&) override { isMouseOver = false; repaint(); } void mouseDown (const MouseEvent&) override { isMouseDown = true; repaint(); } void mouseUp (const MouseEvent&) override { isMouseDown = false; repaint(); showOrHideMessagesWindow(); } std::unique_ptr createAccessibilityHandler() override { return std::make_unique (*this, AccessibilityRole::button, AccessibilityActions().addAction (AccessibilityActionType::press, [this] { showOrHideMessagesWindow(); })); } //============================================================================== void setProject (Project* newProject) { if (currentProject != newProject) { currentProject = newProject; if (currentProject != nullptr) { if (auto* projectWindow = ProjucerApplication::getApp().mainWindowList.getMainWindowForFile (currentProject->getFile())) messagesWindow = std::make_unique (*this, *projectWindow, *currentProject); auto projectMessagesTree = currentProject->getProjectMessages(); warningsComponent.setTree (projectMessagesTree.getChildWithName (ProjectMessages::Ids::warning)); notificationsComponent.setTree (projectMessagesTree.getChildWithName (ProjectMessages::Ids::notification)); } else { warningsComponent.setTree ({}); notificationsComponent.setTree ({}); } } } void numMessagesChanged() { const auto total = warningsComponent.getNumMessages() + notificationsComponent.getNumMessages(); setHelpText (String (total) + (total == 1 ? " message" : " messages")); } void showOrHideMessagesWindow() { if (messagesWindow != nullptr) showOrHideAllMessages (! messagesWindow->isListShowing()); } private: //============================================================================== struct MessageCountComponent : public Component, private ValueTree::Listener { MessageCountComponent (ProjectMessagesComponent& o, Path pathToUse) : owner (o), path (pathToUse) { setInterceptsMouseClicks (false, false); } void paint (Graphics& g) override { auto b = getLocalBounds().toFloat(); g.setColour (findColour ((owner.isMouseDown || owner.isMouseOver) ? defaultHighlightedTextColourId : treeIconColourId)); g.fillPath (path, path.getTransformToScaleToFit (b.removeFromLeft (b.getWidth() / 2.0f), true)); b.removeFromLeft (5); g.drawFittedText (String (numMessages), b.getSmallestIntegerContainer(), Justification::centredLeft, 1); } void setTree (ValueTree tree) { messagesTree = tree; if (messagesTree.isValid()) messagesTree.addListener (this); updateNumMessages(); } void updateNumMessages() { numMessages = messagesTree.getNumChildren(); owner.numMessagesChanged(); repaint(); } int getNumMessages() const noexcept { return numMessages; } private: void valueTreeChildAdded (ValueTree&, ValueTree&) override { updateNumMessages(); } void valueTreeChildRemoved (ValueTree&, ValueTree&, int) override { updateNumMessages(); } ProjectMessagesComponent& owner; ValueTree messagesTree; Path path; int numMessages = 0; }; void showOrHideAllMessages (bool shouldBeVisible) { if (currentProject != nullptr) { auto messagesTree = currentProject->getProjectMessages(); auto setVisible = [shouldBeVisible] (ValueTree subTree) { for (int i = 0; i < subTree.getNumChildren(); ++i) subTree.getChild (i).setProperty (ProjectMessages::Ids::isVisible, shouldBeVisible, nullptr); }; setVisible (messagesTree.getChildWithName (ProjectMessages::Ids::warning)); setVisible (messagesTree.getChildWithName (ProjectMessages::Ids::notification)); } } //============================================================================== Project* currentProject = nullptr; bool isMouseOver = false, isMouseDown = false; MessageCountComponent warningsComponent { *this, getIcons().warning }, notificationsComponent { *this, getIcons().info }; std::unique_ptr messagesWindow; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ProjectMessagesComponent) };