/* ============================================================================== This file is part of the JUCE 6 technical preview. Copyright (c) 2020 - Raw Material Software Limited You may use this code under the terms of the GPL v3 (see www.gnu.org/licenses). For this technical preview, this file is not subject to commercial licensing. 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 { 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 : public Component { public: PanelHolder (Component* comp, bool takeOwnership) : component (comp, takeOwnership) { setRepaintsOnMouseActivity (true); setWantsKeyboardFocus (false); addAndMakeVisible (comp); } void paint (Graphics& g) override { if (customHeaderComponent == nullptr) { 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 (customHeaderComponent != nullptr) customHeaderComponent->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) { customHeaderComponent.set (headerComponent, shouldTakeOwnership); if (headerComponent != nullptr) { addAndMakeVisible (customHeaderComponent); customHeaderComponent->addMouseListener (this, false); } } OptionalScopedPointer component; private: PanelSizes dragStartSizes; int mouseDownY; OptionalScopedPointer customHeaderComponent; 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() {} 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); } } // namespace juce