/* ============================================================================== This file is part of the JUCE framework. Copyright (c) Raw Material Software Limited JUCE is an open source framework subject to commercial or open source licensing. By downloading, installing, or using the JUCE framework, or combining the JUCE framework with any other source code, object code, content or any other copyrightable work, you agree to the terms of the JUCE End User Licence Agreement, and all incorporated terms including the JUCE Privacy Policy and the JUCE Website Terms of Service, as applicable, which will bind you. If you do not agree to the terms of these agreements, we will not license the JUCE framework to you, and you must discontinue the installation or download process and cease use of the JUCE framework. JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ JUCE Privacy Policy: https://juce.com/juce-privacy-policy JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ Or: You may also use this code under the terms of the AGPLv3: https://www.gnu.org/licenses/agpl-3.0.en.html THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. ============================================================================== */ namespace juce { struct ConcertinaPanel::PanelSizes { struct Panel { Panel() = default; Panel (int sz, int mn, int mx) noexcept : size (sz), minSize (mn), maxSize (mx) {} int setSize (int newSize) noexcept { jassert (minSize <= maxSize); auto oldSize = size; size = jlimit (minSize, maxSize, newSize); return size - oldSize; } int expand (int amount) noexcept { amount = jmin (amount, maxSize - size); size += amount; return amount; } int reduce (int amount) noexcept { amount = jmin (amount, size - minSize); size -= amount; return amount; } bool canExpand() const noexcept { return size < maxSize; } bool isMinimised() const noexcept { return size <= minSize; } int size, minSize, maxSize; }; Array sizes; Panel& get (int index) noexcept { return sizes.getReference (index); } const Panel& get (int index) const noexcept { return sizes.getReference (index); } PanelSizes withMovedPanel (int index, int targetPosition, int totalSpace) const { auto num = sizes.size(); totalSpace = jmax (totalSpace, getMinimumSize (0, num)); targetPosition = jmax (targetPosition, totalSpace - getMaximumSize (index, num)); PanelSizes newSizes (*this); newSizes.stretchRange (0, index, targetPosition - newSizes.getTotalSize (0, index), stretchLast); newSizes.stretchRange (index, num, totalSpace - newSizes.getTotalSize (0, index) - newSizes.getTotalSize (index, num), stretchFirst); return newSizes; } PanelSizes fittedInto (int totalSpace) const { auto newSizes (*this); auto num = newSizes.sizes.size(); totalSpace = jmax (totalSpace, getMinimumSize (0, num)); newSizes.stretchRange (0, num, totalSpace - newSizes.getTotalSize (0, num), stretchAll); return newSizes; } PanelSizes withResizedPanel (int index, int panelHeight, int totalSpace) const { PanelSizes newSizes (*this); if (totalSpace <= 0) { newSizes.get (index).size = panelHeight; } else { auto num = sizes.size(); auto minSize = getMinimumSize (0, num); totalSpace = jmax (totalSpace, minSize); newSizes.get (index).setSize (panelHeight); newSizes.stretchRange (0, index, totalSpace - newSizes.getTotalSize (0, num), stretchLast); newSizes.stretchRange (index, num, totalSpace - newSizes.getTotalSize (0, num), stretchLast); newSizes = newSizes.fittedInto (totalSpace); } return newSizes; } private: enum ExpandMode { stretchAll, stretchFirst, stretchLast }; void growRangeFirst (int start, int end, int spaceDiff) noexcept { for (int attempts = 4; --attempts >= 0 && spaceDiff > 0;) for (int i = start; i < end && spaceDiff > 0; ++i) spaceDiff -= get (i).expand (spaceDiff); } void growRangeLast (int start, int end, int spaceDiff) noexcept { for (int attempts = 4; --attempts >= 0 && spaceDiff > 0;) for (int i = end; --i >= start && spaceDiff > 0;) spaceDiff -= get (i).expand (spaceDiff); } void growRangeAll (int start, int end, int spaceDiff) noexcept { Array expandableItems; for (int i = start; i < end; ++i) if (get (i).canExpand() && ! get (i).isMinimised()) expandableItems.add (& get (i)); for (int attempts = 4; --attempts >= 0 && spaceDiff > 0;) for (int i = expandableItems.size(); --i >= 0 && spaceDiff > 0;) spaceDiff -= expandableItems.getUnchecked (i)->expand (spaceDiff / (i + 1)); growRangeLast (start, end, spaceDiff); } void shrinkRangeFirst (int start, int end, int spaceDiff) noexcept { for (int i = start; i < end && spaceDiff > 0; ++i) spaceDiff -= get (i).reduce (spaceDiff); } void shrinkRangeLast (int start, int end, int spaceDiff) noexcept { for (int i = end; --i >= start && spaceDiff > 0;) spaceDiff -= get (i).reduce (spaceDiff); } void stretchRange (int start, int end, int amountToAdd, ExpandMode expandMode) noexcept { if (end > start) { if (amountToAdd > 0) { if (expandMode == stretchAll) growRangeAll (start, end, amountToAdd); else if (expandMode == stretchFirst) growRangeFirst (start, end, amountToAdd); else if (expandMode == stretchLast) growRangeLast (start, end, amountToAdd); } else { if (expandMode == stretchFirst) shrinkRangeFirst (start, end, -amountToAdd); else shrinkRangeLast (start, end, -amountToAdd); } } } int getTotalSize (int start, int end) const noexcept { int tot = 0; while (start < end) tot += get (start++).size; return tot; } int getMinimumSize (int start, int end) const noexcept { int tot = 0; while (start < end) tot += get (start++).minSize; return tot; } int getMaximumSize (int start, int end) const noexcept { int tot = 0; while (start < end) { auto mx = get (start++).maxSize; if (mx > 0x100000) return mx; tot += mx; } return tot; } }; //============================================================================== class ConcertinaPanel::PanelHolder final : public Component { public: PanelHolder (Component* comp, bool takeOwnership) : component (comp, takeOwnership) { setRepaintsOnMouseActivity (true); setWantsKeyboardFocus (false); addAndMakeVisible (comp); } void paint (Graphics& g) override { if (customHeader.get() != nullptr) return; const Rectangle area (getWidth(), getHeaderSize()); g.reduceClipRegion (area); getLookAndFeel().drawConcertinaPanelHeader (g, area, isMouseOver(), isMouseButtonDown(), getPanel(), *component); } void resized() override { auto bounds = getLocalBounds(); auto headerBounds = bounds.removeFromTop (getHeaderSize()); if (customHeader.get() != nullptr) customHeader.get()->setBounds (headerBounds); component->setBounds (bounds); } void mouseDown (const MouseEvent&) override { mouseDownY = getY(); dragStartSizes = getPanel().getFittedSizes(); } void mouseDrag (const MouseEvent& e) override { if (e.mouseWasDraggedSinceMouseDown()) { auto& panel = getPanel(); panel.setLayout (dragStartSizes.withMovedPanel (panel.holders.indexOf (this), mouseDownY + e.getDistanceFromDragStartY(), panel.getHeight()), false); } } void mouseDoubleClick (const MouseEvent&) override { getPanel().panelHeaderDoubleClicked (component); } void setCustomHeaderComponent (Component* headerComponent, bool shouldTakeOwnership) { customHeader = CustomHeader (this, OptionalScopedPointer (headerComponent, shouldTakeOwnership)); addAndMakeVisible (headerComponent); } OptionalScopedPointer component; private: PanelSizes dragStartSizes; int mouseDownY; struct CustomHeader { CustomHeader() = default; CustomHeader (MouseListener* l, OptionalScopedPointer c) : listener (l), customHeaderComponent (std::move (c)) { if (customHeaderComponent != nullptr) customHeaderComponent->addMouseListener (listener, false); } CustomHeader (CustomHeader&& other) noexcept : listener (std::exchange (other.listener, nullptr)), customHeaderComponent (std::exchange (other.customHeaderComponent, {})) {} CustomHeader& operator= (CustomHeader&& other) noexcept { std::swap (other.listener, listener); std::swap (other.customHeaderComponent, customHeaderComponent); return *this; } CustomHeader (const CustomHeader& other) = delete; CustomHeader& operator= (const CustomHeader& other) = delete; ~CustomHeader() noexcept { if (customHeaderComponent != nullptr) customHeaderComponent->removeMouseListener (listener); } Component* get() const { return customHeaderComponent.get(); } private: MouseListener* listener = nullptr; OptionalScopedPointer customHeaderComponent; }; CustomHeader customHeader; int getHeaderSize() const noexcept { ConcertinaPanel& panel = getPanel(); auto ourIndex = panel.holders.indexOf (this); return panel.currentSizes->get (ourIndex).minSize; } ConcertinaPanel& getPanel() const { auto panel = dynamic_cast (getParentComponent()); jassert (panel != nullptr); return *panel; } JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PanelHolder) }; //============================================================================== ConcertinaPanel::ConcertinaPanel() : currentSizes (new PanelSizes()), headerHeight (20) { } ConcertinaPanel::~ConcertinaPanel() = default; int ConcertinaPanel::getNumPanels() const noexcept { return holders.size(); } Component* ConcertinaPanel::getPanel (int index) const noexcept { if (PanelHolder* h = holders[index]) return h->component; return nullptr; } void ConcertinaPanel::addPanel (int insertIndex, Component* component, bool takeOwnership) { jassert (component != nullptr); // can't use a null pointer here! jassert (indexOfComp (component) < 0); // You can't add the same component more than once! auto holder = new PanelHolder (component, takeOwnership); holders.insert (insertIndex, holder); currentSizes->sizes.insert (insertIndex, PanelSizes::Panel (headerHeight, headerHeight, std::numeric_limits::max())); addAndMakeVisible (holder); resized(); } void ConcertinaPanel::removePanel (Component* component) { auto index = indexOfComp (component); if (index >= 0) { currentSizes->sizes.remove (index); holders.remove (index); resized(); } } bool ConcertinaPanel::setPanelSize (Component* panelComponent, int height, bool animate) { auto index = indexOfComp (panelComponent); jassert (index >= 0); // The specified component doesn't seem to have been added! height += currentSizes->get (index).minSize; auto oldSize = currentSizes->get (index).size; setLayout (currentSizes->withResizedPanel (index, height, getHeight()), animate); return oldSize != currentSizes->get (index).size; } bool ConcertinaPanel::expandPanelFully (Component* component, bool animate) { return setPanelSize (component, getHeight(), animate); } void ConcertinaPanel::setMaximumPanelSize (Component* component, int maximumSize) { auto index = indexOfComp (component); jassert (index >= 0); // The specified component doesn't seem to have been added! if (index >= 0) { currentSizes->get (index).maxSize = currentSizes->get (index).minSize + maximumSize; resized(); } } void ConcertinaPanel::setPanelHeaderSize (Component* component, int headerSize) { auto index = indexOfComp (component); jassert (index >= 0); // The specified component doesn't seem to have been added! if (index >= 0) { auto oldMin = currentSizes->get (index).minSize; currentSizes->get (index).minSize = headerSize; currentSizes->get (index).size += headerSize - oldMin; resized(); } } void ConcertinaPanel::setCustomPanelHeader (Component* component, Component* customComponent, bool takeOwnership) { OptionalScopedPointer optional (customComponent, takeOwnership); auto index = indexOfComp (component); jassert (index >= 0); // The specified component doesn't seem to have been added! if (index >= 0) holders.getUnchecked (index)->setCustomHeaderComponent (optional.release(), takeOwnership); } void ConcertinaPanel::resized() { applyLayout (getFittedSizes(), false); } int ConcertinaPanel::indexOfComp (Component* comp) const noexcept { for (int i = 0; i < holders.size(); ++i) if (holders.getUnchecked (i)->component == comp) return i; return -1; } ConcertinaPanel::PanelSizes ConcertinaPanel::getFittedSizes() const { return currentSizes->fittedInto (getHeight()); } void ConcertinaPanel::applyLayout (const PanelSizes& sizes, bool animate) { if (! animate) animator.cancelAllAnimations (false); const int animationDuration = 150; auto w = getWidth(); int y = 0; for (int i = 0; i < holders.size(); ++i) { PanelHolder& p = *holders.getUnchecked (i); auto h = sizes.get (i).size; const Rectangle pos (0, y, w, h); if (animate) animator.animateComponent (&p, pos, 1.0f, animationDuration, false, 1.0, 1.0); else p.setBounds (pos); y += h; } } void ConcertinaPanel::setLayout (const PanelSizes& sizes, bool animate) { *currentSizes = sizes; applyLayout (getFittedSizes(), animate); } void ConcertinaPanel::panelHeaderDoubleClicked (Component* component) { if (! expandPanelFully (component, true)) setPanelSize (component, 0, true); } //============================================================================== std::unique_ptr ConcertinaPanel::createAccessibilityHandler() { return std::make_unique (*this, AccessibilityRole::group); } } // namespace juce