From 1b962aa2f2bc85792f2111037800ca62f2a84833 Mon Sep 17 00:00:00 2001 From: ed Date: Thu, 9 Nov 2017 17:06:28 +0000 Subject: [PATCH] GUI: Added a SidePanel component class which is useful for auxiliary UI components on mobile --- modules/juce_gui_basics/juce_gui_basics.cpp | 1 + modules/juce_gui_basics/juce_gui_basics.h | 1 + .../juce_gui_basics/layout/juce_SidePanel.cpp | 233 ++++++++++++++++++ .../juce_gui_basics/layout/juce_SidePanel.h | 188 ++++++++++++++ .../lookandfeel/juce_LookAndFeel.h | 3 +- .../lookandfeel/juce_LookAndFeel_V2.cpp | 38 +++ .../lookandfeel/juce_LookAndFeel_V2.h | 5 + .../lookandfeel/juce_LookAndFeel_V4.cpp | 7 + 8 files changed, 475 insertions(+), 1 deletion(-) create mode 100644 modules/juce_gui_basics/layout/juce_SidePanel.cpp create mode 100644 modules/juce_gui_basics/layout/juce_SidePanel.h diff --git a/modules/juce_gui_basics/juce_gui_basics.cpp b/modules/juce_gui_basics/juce_gui_basics.cpp index dbc7ee8c21..ad1e87bfdd 100644 --- a/modules/juce_gui_basics/juce_gui_basics.cpp +++ b/modules/juce_gui_basics/juce_gui_basics.cpp @@ -202,6 +202,7 @@ namespace juce #include "layout/juce_ResizableCornerComponent.cpp" #include "layout/juce_ResizableEdgeComponent.cpp" #include "layout/juce_ScrollBar.cpp" +#include "layout/juce_SidePanel.cpp" #include "layout/juce_StretchableLayoutManager.cpp" #include "layout/juce_StretchableLayoutResizerBar.cpp" #include "layout/juce_StretchableObjectResizer.cpp" diff --git a/modules/juce_gui_basics/juce_gui_basics.h b/modules/juce_gui_basics/juce_gui_basics.h index c5483f5fd7..7a4070ac7d 100644 --- a/modules/juce_gui_basics/juce_gui_basics.h +++ b/modules/juce_gui_basics/juce_gui_basics.h @@ -263,6 +263,7 @@ namespace juce #include "windows/juce_ThreadWithProgressWindow.h" #include "windows/juce_TooltipWindow.h" #include "layout/juce_MultiDocumentPanel.h" +#include "layout/juce_SidePanel.h" #include "filebrowser/juce_FileBrowserListener.h" #include "filebrowser/juce_DirectoryContentsList.h" #include "filebrowser/juce_DirectoryContentsDisplayComponent.h" diff --git a/modules/juce_gui_basics/layout/juce_SidePanel.cpp b/modules/juce_gui_basics/layout/juce_SidePanel.cpp new file mode 100644 index 0000000000..40fdb8465f --- /dev/null +++ b/modules/juce_gui_basics/layout/juce_SidePanel.cpp @@ -0,0 +1,233 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + 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 5 End-User License + Agreement and JUCE 5 Privacy Policy (both updated and effective as of the + 27th April 2017). + + End User License Agreement: www.juce.com/juce-5-licence + Privacy Policy: www.juce.com/juce-5-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. + + ============================================================================== +*/ + +namespace juce +{ + +SidePanel::SidePanel (StringRef title, int width, bool positionOnLeft, + Component* contentToDisplay, bool deleteComponentWhenNoLongerNeeded) + : titleLabel ("titleLabel", title), + isOnLeft (positionOnLeft), + panelWidth (width) +{ + lookAndFeelChanged(); + + addAndMakeVisible (titleLabel); + + dismissButton.addListener (this); + addAndMakeVisible (dismissButton); + + Desktop::getInstance().addGlobalMouseListener (this); + + if (contentToDisplay != nullptr) + setContent (contentToDisplay, deleteComponentWhenNoLongerNeeded); + + setOpaque (false); +} + +SidePanel::~SidePanel() +{ + if (parent != nullptr) + parent->removeComponentListener (this); +} + +void SidePanel::setContent (Component* newContent, bool deleteComponentWhenNoLongerNeeded) +{ + if (contentComponent.get() != newContent) + { + if (deleteComponentWhenNoLongerNeeded) + contentComponent.setOwned (newContent); + else + contentComponent.setNonOwned (newContent); + + addAndMakeVisible (contentComponent); + } +} + +void SidePanel::showOrHide (bool show) +{ + if (parent != nullptr) + { + isShowing = show; + + Desktop::getInstance().getAnimator().animateComponent (this, calculateBoundsInParent (*parent), + 1.0f, 250, true, 1.0, 0.0); + } +} + +void SidePanel::resized() +{ + auto bounds = getLocalBounds(); + + calculateAndRemoveShadowBounds (bounds); + + auto titleBounds = bounds.removeFromTop (titleBarHeight); + + dismissButton.setBounds (isOnLeft ? titleBounds.removeFromRight (30).withTrimmedRight (10) + : titleBounds.removeFromLeft (30).withTrimmedLeft (10)); + + titleLabel.setBounds (isOnLeft ? titleBounds.withTrimmedRight (40) + : titleBounds.withTrimmedLeft (40)); + + if (contentComponent != nullptr) + contentComponent->setBounds (bounds); +} + +void SidePanel::paint (Graphics& g) +{ + auto& lf = getLookAndFeel(); + + auto bgColour = lf.findColour (SidePanel::backgroundColour); + auto shadowColour = lf.findColour (SidePanel::shadowBaseColour); + + g.setGradientFill (ColourGradient (shadowColour.withAlpha (0.7f), (isOnLeft ? shadowArea.getTopLeft() + : shadowArea.getTopRight()).toFloat(), + shadowColour.withAlpha (0.0f), (isOnLeft ? shadowArea.getTopRight() + : shadowArea.getTopLeft()).toFloat(), false)); + g.fillRect (shadowArea); + + g.excludeClipRegion (shadowArea); + g.fillAll (bgColour); +} + +void SidePanel::parentHierarchyChanged() +{ + auto* newParent = getParentComponent(); + + if ((newParent != nullptr) && (parent != newParent)) + { + if (parent != nullptr) + parent->removeComponentListener (this); + + parent = newParent; + parent->addComponentListener (this); + } +} + +void SidePanel::mouseDrag (const MouseEvent& e) +{ + if (shouldResize) + { + auto currentMouseDragX = static_cast (e.position.x); + + if (isOnLeft) + { + amountMoved = startingBounds.getRight() - currentMouseDragX; + setBounds (getBounds().withX (startingBounds.getX() - jmax (amountMoved, 0))); + } + else + { + amountMoved = currentMouseDragX - startingBounds.getX(); + setBounds (getBounds().withX (startingBounds.getX() + jmax (amountMoved, 0))); + } + } + else + { + auto relativeMouseDownPosition = getLocalPoint (e.eventComponent, e.getMouseDownPosition()); + auto relativeMouseDragPosition = getLocalPoint (e.eventComponent, e.getPosition()); + + if (! getLocalBounds().contains (relativeMouseDownPosition) + && getLocalBounds().contains (relativeMouseDragPosition)) + { + shouldResize = true; + startingBounds = getBounds(); + } + } +} + +void SidePanel::mouseUp (const MouseEvent&) +{ + if (shouldResize) + { + showOrHide (amountMoved < (panelWidth / 2)); + + amountMoved = 0; + shouldResize = false; + } +} + +//========================================================================== +void SidePanel::lookAndFeelChanged() +{ + auto& lf = getLookAndFeel(); + + dismissButton.setShape (lf.getSidePanelDismissButtonShape (*this), false, true, false); + + dismissButton.setColours (lf.findColour (SidePanel::dismissButtonNormalColour), + lf.findColour (SidePanel::dismissButtonOverColour), + lf.findColour (SidePanel::dismissButtonDownColour)); + + titleLabel.setFont (lf.getSidePanelTitleFont (*this)); + titleLabel.setColour (Label::textColourId, findColour (SidePanel::titleTextColour)); + titleLabel.setJustificationType (lf.getSidePanelTitleJustification (*this)); +} + +void SidePanel::componentMovedOrResized (Component& component, bool wasMoved, bool wasResized) +{ + ignoreUnused (wasMoved); + + if (wasResized && (&component == parent)) + setBounds (calculateBoundsInParent (component)); +} + +void SidePanel::buttonClicked (Button*) +{ + showOrHide (false); +} + +Rectangle SidePanel::calculateBoundsInParent (Component& parentComp) const +{ + auto parentBounds = parentComp.getBounds(); + + if (isOnLeft) + { + return isShowing ? parentBounds.removeFromLeft (panelWidth) + : parentBounds.withX (parentBounds.getX() - panelWidth).withWidth (panelWidth); + } + + return isShowing ? parentBounds.removeFromRight (panelWidth) + : parentBounds.withX (parentBounds.getRight()).withWidth (panelWidth); +} + +void SidePanel::calculateAndRemoveShadowBounds (Rectangle& bounds) +{ + shadowArea = isOnLeft ? bounds.removeFromRight (shadowWidth) + : bounds.removeFromLeft (shadowWidth); +} + +bool SidePanel::isMouseEventInThisOrChildren (Component* eventComponent) +{ + if (eventComponent == this) + return true; + + for (auto& child : getChildren()) + if (eventComponent == child) + return true; + + return false; +} + +} // namespace juce diff --git a/modules/juce_gui_basics/layout/juce_SidePanel.h b/modules/juce_gui_basics/layout/juce_SidePanel.h new file mode 100644 index 0000000000..a15a926a1e --- /dev/null +++ b/modules/juce_gui_basics/layout/juce_SidePanel.h @@ -0,0 +1,188 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + 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 5 End-User License + Agreement and JUCE 5 Privacy Policy (both updated and effective as of the + 27th April 2017). + + End User License Agreement: www.juce.com/juce-5-licence + Privacy Policy: www.juce.com/juce-5-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. + + ============================================================================== +*/ + +namespace juce +{ + +//============================================================================== +/** + A component that is positioned on either the left- or right-hand side of its parent, + containing a header and some content. This sort of component is typically used for + navigation and forms in mobile applications. + + When triggered with the showOrHide() method, the SidePanel will animate itself to its + new position. This component also contains some logic to reactively resize and dismiss + itself when the user drags it. + */ +//============================================================================== +class SidePanel : public Component, + private ComponentListener, + private Button::Listener +{ +public: + //============================================================================== + /** Creates a SidePanel component. + + @param title the text to use for the SidePanel's title bar + @param width the width of the SidePanel + @param positionOnLeft if true, the SidePanel will be positioned on the left of its parent component and + if false, the SidePanel will be positioned on the right of its parent component + @param contentComponent the component to add to this SidePanel - this content will take up the full + size of the SidePanel, minus the height of the title bar. You can pass nullptr + to this if you like and set the content component later using the setContent() method + @param deleteComponentWhenNoLongerNeeded if true, the component will be deleted automatically when + the SidePanel is deleted or when a different component is added. If false, + the caller must manage the lifetime of the component + */ + SidePanel (StringRef title, int width, bool positionOnLeft, + Component* contentComponent = nullptr, bool deleteComponentWhenNoLongerNeeded = true); + + /** Destructor */ + ~SidePanel(); + + //============================================================================== + /** Sets the component that this SidePanel will contain. + + This will add the given component to this SidePanel and position it below the title bar. + + (Don't add or remove any child components directly using the normal + Component::addChildComponent() methods). + + @param newViewedComponent the component to add to this SidePanel, or null to remove + the current component. + @param deleteComponentWhenNoLongerNeeded if true, the component will be deleted automatically when + the SidePanel is deleted or when a different component is added. If false, + the caller must manage the lifetime of the component + + @see getContent + */ + void setContent (Component* newContentComponent, + bool deleteComponentWhenNoLongerNeeded = true); + + /** Returns the component that's currently being used inside the SidePanel. + + @see setViewedComponent + */ + Component* getContent() { return contentComponent.get(); } + + /** Shows or hides the SidePanel. + + This will animate the SidePanel to either its full width or to be hidden on the + left- or right-hand side of its parent component depending on the value of positionOnLeft + that was passed to the constructor. + + @param show if true, this will show the SidePanel and if false the SidePanel will be hidden + */ + void showOrHide (bool show); + + //============================================================================== + /** Returns true if the SidePanel is currently showing. */ + bool isPanelShowing() const noexcept { return isShowing; } + + /** Returns true if the SidePanel is positioned on the left of its parent. */ + bool isPanelOnLeft() const noexcept { return isOnLeft; } + + /** Sets the width of the shadow that will be drawn on the side of the panel. */ + void setShadowWidth (int newWidth) noexcept { shadowWidth = newWidth; } + + /** Sets the height of the title bar at the top of the SidePanel. */ + void setTitleBarHeight (int newHeight) noexcept { titleBarHeight = newHeight; } + + //============================================================================== + void resized() override; + void paint (Graphics& g) override; + + void parentHierarchyChanged() override; + + void mouseDrag (const MouseEvent&) override; + void mouseUp (const MouseEvent&) override; + + //============================================================================== + /** This abstract base class is implemented by LookAndFeel classes to provide + SidePanel drawing functionality. + */ + struct JUCE_API LookAndFeelMethods + { + virtual ~LookAndFeelMethods() {} + + virtual Font getSidePanelTitleFont (SidePanel&) = 0; + virtual Justification getSidePanelTitleJustification (SidePanel&) = 0; + virtual Path getSidePanelDismissButtonShape (SidePanel&) = 0; + }; + + /** A set of colour IDs to use to change the colour of various aspects of the SidePanel. + + These constants can be used either via the Component::setColour(), or LookAndFeel::setColour() + methods. + + @see Component::setColour, Component::findColour, LookAndFeel::setColour, LookAndFeel::findColour + */ + enum ColourIds + { + backgroundColour = 0x100f001, + titleTextColour = 0x100f002, + shadowBaseColour = 0x100f003, + dismissButtonNormalColour = 0x100f004, + dismissButtonOverColour = 0x100f004, + dismissButtonDownColour = 0x100f005 + }; + +private: + //========================================================================== + Component* parent = nullptr; + OptionalScopedPointer contentComponent; + + Label titleLabel; + ShapeButton dismissButton {"dismissButton", Colours::lightgrey, Colours::lightgrey, Colours::white}; + + Rectangle shadowArea; + + bool isOnLeft = false; + bool isShowing = false; + + int panelWidth = 0; + int shadowWidth = 15; + int titleBarHeight = 40; + + Rectangle startingBounds; + bool shouldResize = false; + int amountMoved = 0; + + //========================================================================== + void lookAndFeelChanged() override; + void componentMovedOrResized (Component&, bool wasMoved, bool wasResized) override; + void buttonClicked (Button*) override; + + Rectangle calculateBoundsInParent (Component&) const; + void calculateAndRemoveShadowBounds (Rectangle& bounds); + + bool isMouseEventInThisOrChildren (Component*); + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SidePanel) +}; + +} // namespace juce diff --git a/modules/juce_gui_basics/lookandfeel/juce_LookAndFeel.h b/modules/juce_gui_basics/lookandfeel/juce_LookAndFeel.h index 020c3991cf..31a0bad15f 100644 --- a/modules/juce_gui_basics/lookandfeel/juce_LookAndFeel.h +++ b/modules/juce_gui_basics/lookandfeel/juce_LookAndFeel.h @@ -100,7 +100,8 @@ class JUCE_API LookAndFeel : public ScrollBar::LookAndFeelMethods, public StretchableLayoutResizerBar::LookAndFeelMethods, public ExtraLookAndFeelBaseClasses::KeyMappingEditorComponentMethods, public ExtraLookAndFeelBaseClasses::AudioDeviceSelectorComponentMethods, - public ExtraLookAndFeelBaseClasses::LassoComponentMethods + public ExtraLookAndFeelBaseClasses::LassoComponentMethods, + public SidePanel::LookAndFeelMethods { public: //============================================================================== diff --git a/modules/juce_gui_basics/lookandfeel/juce_LookAndFeel_V2.cpp b/modules/juce_gui_basics/lookandfeel/juce_LookAndFeel_V2.cpp index 4dde3a2232..af22085e56 100644 --- a/modules/juce_gui_basics/lookandfeel/juce_LookAndFeel_V2.cpp +++ b/modules/juce_gui_basics/lookandfeel/juce_LookAndFeel_V2.cpp @@ -212,6 +212,13 @@ LookAndFeel_V2::LookAndFeel_V2() FileSearchPathListComponent::backgroundColourId, 0xffffffff, FileChooserDialogBox::titleTextColourId, 0xff000000, + + SidePanel::backgroundColour, 0xffffffff, + SidePanel::titleTextColour, 0xff000000, + SidePanel::shadowBaseColour, 0xff000000, + SidePanel::dismissButtonNormalColour, textButtonColour, + SidePanel::dismissButtonOverColour, textButtonColour, + SidePanel::dismissButtonDownColour, 0xff4444ff, }; for (int i = 0; i < numElementsInArray (standardColours); i += 2) @@ -2775,6 +2782,37 @@ void LookAndFeel_V2::drawKeymapChangeButton (Graphics& g, int width, int height, g.drawRect (0, 0, width, height); } } +//============================================================================== +Font LookAndFeel_V2::getSidePanelTitleFont (SidePanel&) +{ + return Font (18.0f); +} + +Justification LookAndFeel_V2::getSidePanelTitleJustification (SidePanel& panel) +{ + return panel.isPanelOnLeft() ? Justification::centredRight + : Justification::centredLeft; +} + +Path LookAndFeel_V2::getSidePanelDismissButtonShape (SidePanel& panel) +{ + Path p; + + if (panel.isPanelOnLeft()) + { + p.startNewSubPath (10, 0); + p.lineTo (0, 5); + p.lineTo (10, 10); + } + else + { + p.startNewSubPath (0, 10); + p.lineTo (10, 5); + p.lineTo (0, 0); + } + + return p; +} //============================================================================== void LookAndFeel_V2::drawBevel (Graphics& g, const int x, const int y, const int width, const int height, diff --git a/modules/juce_gui_basics/lookandfeel/juce_LookAndFeel_V2.h b/modules/juce_gui_basics/lookandfeel/juce_LookAndFeel_V2.h index af0b365888..9e99571a5e 100644 --- a/modules/juce_gui_basics/lookandfeel/juce_LookAndFeel_V2.h +++ b/modules/juce_gui_basics/lookandfeel/juce_LookAndFeel_V2.h @@ -323,6 +323,11 @@ public: void drawKeymapChangeButton (Graphics&, int width, int height, Button&, const String& keyDescription) override; + //============================================================================== + Font getSidePanelTitleFont (SidePanel&) override; + Justification getSidePanelTitleJustification (SidePanel&) override; + Path getSidePanelDismissButtonShape (SidePanel&) override; + //============================================================================== /** Draws a 3D raised (or indented) bevel using two colours. diff --git a/modules/juce_gui_basics/lookandfeel/juce_LookAndFeel_V4.cpp b/modules/juce_gui_basics/lookandfeel/juce_LookAndFeel_V4.cpp index fe8729001b..411b41f711 100644 --- a/modules/juce_gui_basics/lookandfeel/juce_LookAndFeel_V4.cpp +++ b/modules/juce_gui_basics/lookandfeel/juce_LookAndFeel_V4.cpp @@ -1439,6 +1439,13 @@ void LookAndFeel_V4::initialiseColours() FileSearchPathListComponent::backgroundColourId, currentColourScheme.getUIColour (ColourScheme::UIColour::menuBackground).getARGB(), FileChooserDialogBox::titleTextColourId, currentColourScheme.getUIColour (ColourScheme::UIColour::defaultText).getARGB(), + + SidePanel::backgroundColour, currentColourScheme.getUIColour (ColourScheme::UIColour::widgetBackground).getARGB(), + SidePanel::titleTextColour, currentColourScheme.getUIColour (ColourScheme::UIColour::defaultText).getARGB(), + SidePanel::shadowBaseColour, currentColourScheme.getUIColour (ColourScheme::UIColour::widgetBackground).darker().getARGB(), + SidePanel::dismissButtonNormalColour, currentColourScheme.getUIColour (ColourScheme::UIColour::defaultFill).getARGB(), + SidePanel::dismissButtonOverColour, currentColourScheme.getUIColour (ColourScheme::UIColour::defaultFill).darker().getARGB(), + SidePanel::dismissButtonDownColour, currentColourScheme.getUIColour (ColourScheme::UIColour::defaultFill).brighter().getARGB(), }; for (int i = 0; i < numElementsInArray (coloursToUse); i += 2)