1 /*
2   ==============================================================================
3 
4    This file is part of the JUCE library.
5    Copyright (c) 2020 - Raw Material Software Limited
6 
7    JUCE is an open source library subject to commercial or open-source
8    licensing.
9 
10    By using JUCE, you agree to the terms of both the JUCE 6 End-User License
11    Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
12 
13    End User License Agreement: www.juce.com/juce-6-licence
14    Privacy Policy: www.juce.com/juce-privacy-policy
15 
16    Or: You may also use this code under the terms of the GPL v3 (see
17    www.gnu.org/licenses).
18 
19    JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
20    EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
21    DISCLAIMED.
22 
23   ==============================================================================
24 */
25 
26 namespace juce
27 {
28 
29 struct ConcertinaPanel::PanelSizes
30 {
31     struct Panel
32     {
33         Panel() = default;
34 
Paneljuce::ConcertinaPanel::PanelSizes::Panel35         Panel (int sz, int mn, int mx) noexcept
36             : size (sz), minSize (mn), maxSize (mx) {}
37 
setSizejuce::ConcertinaPanel::PanelSizes::Panel38         int setSize (int newSize) noexcept
39         {
40             jassert (minSize <= maxSize);
41             auto oldSize = size;
42             size = jlimit (minSize, maxSize, newSize);
43             return size - oldSize;
44         }
45 
expandjuce::ConcertinaPanel::PanelSizes::Panel46         int expand (int amount) noexcept
47         {
48             amount = jmin (amount, maxSize - size);
49             size += amount;
50             return amount;
51         }
52 
reducejuce::ConcertinaPanel::PanelSizes::Panel53         int reduce (int amount) noexcept
54         {
55             amount = jmin (amount, size - minSize);
56             size -= amount;
57             return amount;
58         }
59 
canExpandjuce::ConcertinaPanel::PanelSizes::Panel60         bool canExpand() const noexcept     { return size < maxSize; }
isMinimisedjuce::ConcertinaPanel::PanelSizes::Panel61         bool isMinimised() const noexcept   { return size <= minSize; }
62 
63         int size, minSize, maxSize;
64     };
65 
66     Array<Panel> sizes;
67 
getjuce::ConcertinaPanel::PanelSizes68     Panel& get (int index) noexcept               { return sizes.getReference (index); }
getjuce::ConcertinaPanel::PanelSizes69     const Panel& get (int index) const noexcept   { return sizes.getReference (index); }
70 
withMovedPaneljuce::ConcertinaPanel::PanelSizes71     PanelSizes withMovedPanel (int index, int targetPosition, int totalSpace) const
72     {
73         auto num = sizes.size();
74         totalSpace = jmax (totalSpace, getMinimumSize (0, num));
75         targetPosition = jmax (targetPosition, totalSpace - getMaximumSize (index, num));
76 
77         PanelSizes newSizes (*this);
78         newSizes.stretchRange (0, index, targetPosition - newSizes.getTotalSize (0, index), stretchLast);
79         newSizes.stretchRange (index, num, totalSpace - newSizes.getTotalSize (0, index) - newSizes.getTotalSize (index, num), stretchFirst);
80         return newSizes;
81     }
82 
fittedIntojuce::ConcertinaPanel::PanelSizes83     PanelSizes fittedInto (int totalSpace) const
84     {
85         auto newSizes (*this);
86         auto num = newSizes.sizes.size();
87         totalSpace = jmax (totalSpace, getMinimumSize (0, num));
88         newSizes.stretchRange (0, num, totalSpace - newSizes.getTotalSize (0, num), stretchAll);
89         return newSizes;
90     }
91 
withResizedPaneljuce::ConcertinaPanel::PanelSizes92     PanelSizes withResizedPanel (int index, int panelHeight, int totalSpace) const
93     {
94         PanelSizes newSizes (*this);
95 
96         if (totalSpace <= 0)
97         {
98             newSizes.get(index).size = panelHeight;
99         }
100         else
101         {
102             auto num = sizes.size();
103             auto minSize = getMinimumSize (0, num);
104             totalSpace = jmax (totalSpace, minSize);
105 
106             newSizes.get(index).setSize (panelHeight);
107             newSizes.stretchRange (0, index,   totalSpace - newSizes.getTotalSize (0, num), stretchLast);
108             newSizes.stretchRange (index, num, totalSpace - newSizes.getTotalSize (0, num), stretchLast);
109             newSizes = newSizes.fittedInto (totalSpace);
110         }
111 
112         return newSizes;
113     }
114 
115 private:
116     enum ExpandMode
117     {
118         stretchAll,
119         stretchFirst,
120         stretchLast
121     };
122 
growRangeFirstjuce::ConcertinaPanel::PanelSizes123     void growRangeFirst (int start, int end, int spaceDiff) noexcept
124     {
125         for (int attempts = 4; --attempts >= 0 && spaceDiff > 0;)
126             for (int i = start; i < end && spaceDiff > 0; ++i)
127                 spaceDiff -= get (i).expand (spaceDiff);
128     }
129 
growRangeLastjuce::ConcertinaPanel::PanelSizes130     void growRangeLast (int start, int end, int spaceDiff) noexcept
131     {
132         for (int attempts = 4; --attempts >= 0 && spaceDiff > 0;)
133             for (int i = end; --i >= start && spaceDiff > 0;)
134                 spaceDiff -= get (i).expand (spaceDiff);
135     }
136 
growRangeAlljuce::ConcertinaPanel::PanelSizes137     void growRangeAll (int start, int end, int spaceDiff) noexcept
138     {
139         Array<Panel*> expandableItems;
140 
141         for (int i = start; i < end; ++i)
142             if (get(i).canExpand() && ! get(i).isMinimised())
143                 expandableItems.add (& get(i));
144 
145         for (int attempts = 4; --attempts >= 0 && spaceDiff > 0;)
146             for (int i = expandableItems.size(); --i >= 0 && spaceDiff > 0;)
147                 spaceDiff -= expandableItems.getUnchecked(i)->expand (spaceDiff / (i + 1));
148 
149         growRangeLast (start, end, spaceDiff);
150     }
151 
shrinkRangeFirstjuce::ConcertinaPanel::PanelSizes152     void shrinkRangeFirst (int start, int end, int spaceDiff) noexcept
153     {
154         for (int i = start; i < end && spaceDiff > 0; ++i)
155             spaceDiff -= get(i).reduce (spaceDiff);
156     }
157 
shrinkRangeLastjuce::ConcertinaPanel::PanelSizes158     void shrinkRangeLast (int start, int end, int spaceDiff) noexcept
159     {
160         for (int i = end; --i >= start && spaceDiff > 0;)
161             spaceDiff -= get(i).reduce (spaceDiff);
162     }
163 
stretchRangejuce::ConcertinaPanel::PanelSizes164     void stretchRange (int start, int end, int amountToAdd, ExpandMode expandMode) noexcept
165     {
166         if (end > start)
167         {
168             if (amountToAdd > 0)
169             {
170                 if (expandMode == stretchAll)        growRangeAll   (start, end, amountToAdd);
171                 else if (expandMode == stretchFirst) growRangeFirst (start, end, amountToAdd);
172                 else if (expandMode == stretchLast)  growRangeLast  (start, end, amountToAdd);
173             }
174             else
175             {
176                 if (expandMode == stretchFirst)  shrinkRangeFirst (start, end, -amountToAdd);
177                 else                             shrinkRangeLast  (start, end, -amountToAdd);
178             }
179         }
180     }
181 
getTotalSizejuce::ConcertinaPanel::PanelSizes182     int getTotalSize (int start, int end) const noexcept
183     {
184         int tot = 0;
185         while (start < end)  tot += get (start++).size;
186         return tot;
187     }
188 
getMinimumSizejuce::ConcertinaPanel::PanelSizes189     int getMinimumSize (int start, int end) const noexcept
190     {
191         int tot = 0;
192         while (start < end)  tot += get (start++).minSize;
193         return tot;
194     }
195 
getMaximumSizejuce::ConcertinaPanel::PanelSizes196     int getMaximumSize (int start, int end) const noexcept
197     {
198         int tot = 0;
199 
200         while (start < end)
201         {
202             auto mx = get (start++).maxSize;
203 
204             if (mx > 0x100000)
205                 return mx;
206 
207             tot += mx;
208         }
209 
210         return tot;
211     }
212 };
213 
214 //==============================================================================
215 class ConcertinaPanel::PanelHolder  : public Component
216 {
217 public:
PanelHolder(Component * comp,bool takeOwnership)218     PanelHolder (Component* comp, bool takeOwnership)
219         : component (comp, takeOwnership)
220     {
221         setRepaintsOnMouseActivity (true);
222         setWantsKeyboardFocus (false);
223         addAndMakeVisible (comp);
224     }
225 
paint(Graphics & g)226     void paint (Graphics& g) override
227     {
228         if (customHeaderComponent == nullptr)
229         {
230             const Rectangle<int> area (getWidth(), getHeaderSize());
231             g.reduceClipRegion (area);
232 
233             getLookAndFeel().drawConcertinaPanelHeader (g, area, isMouseOver(), isMouseButtonDown(),
234                                                         getPanel(), *component);
235         }
236     }
237 
resized()238     void resized() override
239     {
240         auto bounds = getLocalBounds();
241         auto headerBounds = bounds.removeFromTop (getHeaderSize());
242 
243         if (customHeaderComponent != nullptr)
244             customHeaderComponent->setBounds (headerBounds);
245 
246         component->setBounds (bounds);
247     }
248 
mouseDown(const MouseEvent &)249     void mouseDown (const MouseEvent&) override
250     {
251         mouseDownY = getY();
252         dragStartSizes = getPanel().getFittedSizes();
253     }
254 
mouseDrag(const MouseEvent & e)255     void mouseDrag (const MouseEvent& e) override
256     {
257         if (e.mouseWasDraggedSinceMouseDown())
258         {
259             auto& panel = getPanel();
260             panel.setLayout (dragStartSizes.withMovedPanel (panel.holders.indexOf (this),
261                                                             mouseDownY + e.getDistanceFromDragStartY(),
262                                                             panel.getHeight()), false);
263         }
264     }
265 
mouseDoubleClick(const MouseEvent &)266     void mouseDoubleClick (const MouseEvent&) override
267     {
268         getPanel().panelHeaderDoubleClicked (component);
269     }
270 
setCustomHeaderComponent(Component * headerComponent,bool shouldTakeOwnership)271     void setCustomHeaderComponent (Component* headerComponent, bool shouldTakeOwnership)
272     {
273         customHeaderComponent.set (headerComponent, shouldTakeOwnership);
274 
275         if (headerComponent != nullptr)
276         {
277             addAndMakeVisible (customHeaderComponent);
278             customHeaderComponent->addMouseListener (this, false);
279         }
280     }
281 
282     OptionalScopedPointer<Component> component;
283 
284 private:
285     PanelSizes dragStartSizes;
286     int mouseDownY;
287     OptionalScopedPointer<Component> customHeaderComponent;
288 
getHeaderSize() const289     int getHeaderSize() const noexcept
290     {
291         ConcertinaPanel& panel = getPanel();
292         auto ourIndex = panel.holders.indexOf (this);
293         return panel.currentSizes->get(ourIndex).minSize;
294     }
295 
getPanel() const296     ConcertinaPanel& getPanel() const
297     {
298         auto panel = dynamic_cast<ConcertinaPanel*> (getParentComponent());
299         jassert (panel != nullptr);
300         return *panel;
301     }
302 
303     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PanelHolder)
304 };
305 
306 //==============================================================================
ConcertinaPanel()307 ConcertinaPanel::ConcertinaPanel()
308     : currentSizes (new PanelSizes()),
309       headerHeight (20)
310 {
311 }
312 
~ConcertinaPanel()313 ConcertinaPanel::~ConcertinaPanel() {}
314 
getNumPanels() const315 int ConcertinaPanel::getNumPanels() const noexcept
316 {
317     return holders.size();
318 }
319 
getPanel(int index) const320 Component* ConcertinaPanel::getPanel (int index) const noexcept
321 {
322     if (PanelHolder* h = holders[index])
323         return h->component;
324 
325     return nullptr;
326 }
327 
addPanel(int insertIndex,Component * component,bool takeOwnership)328 void ConcertinaPanel::addPanel (int insertIndex, Component* component, bool takeOwnership)
329 {
330     jassert (component != nullptr); // can't use a null pointer here!
331     jassert (indexOfComp (component) < 0); // You can't add the same component more than once!
332 
333     auto holder = new PanelHolder (component, takeOwnership);
334     holders.insert (insertIndex, holder);
335     currentSizes->sizes.insert (insertIndex, PanelSizes::Panel (headerHeight, headerHeight, std::numeric_limits<int>::max()));
336     addAndMakeVisible (holder);
337     resized();
338 }
339 
removePanel(Component * component)340 void ConcertinaPanel::removePanel (Component* component)
341 {
342     auto index = indexOfComp (component);
343 
344     if (index >= 0)
345     {
346         currentSizes->sizes.remove (index);
347         holders.remove (index);
348         resized();
349     }
350 }
351 
setPanelSize(Component * panelComponent,int height,bool animate)352 bool ConcertinaPanel::setPanelSize (Component* panelComponent, int height, bool animate)
353 {
354     auto index = indexOfComp (panelComponent);
355     jassert (index >= 0); // The specified component doesn't seem to have been added!
356 
357     height += currentSizes->get(index).minSize;
358     auto oldSize = currentSizes->get(index).size;
359     setLayout (currentSizes->withResizedPanel (index, height, getHeight()), animate);
360     return oldSize != currentSizes->get(index).size;
361 }
362 
expandPanelFully(Component * component,bool animate)363 bool ConcertinaPanel::expandPanelFully (Component* component, bool animate)
364 {
365     return setPanelSize (component, getHeight(), animate);
366 }
367 
setMaximumPanelSize(Component * component,int maximumSize)368 void ConcertinaPanel::setMaximumPanelSize (Component* component, int maximumSize)
369 {
370     auto index = indexOfComp (component);
371     jassert (index >= 0); // The specified component doesn't seem to have been added!
372 
373     if (index >= 0)
374     {
375         currentSizes->get(index).maxSize = currentSizes->get(index).minSize + maximumSize;
376         resized();
377     }
378 }
379 
setPanelHeaderSize(Component * component,int headerSize)380 void ConcertinaPanel::setPanelHeaderSize (Component* component, int headerSize)
381 {
382     auto index = indexOfComp (component);
383     jassert (index >= 0); // The specified component doesn't seem to have been added!
384 
385     if (index >= 0)
386     {
387         auto oldMin = currentSizes->get (index).minSize;
388 
389         currentSizes->get (index).minSize = headerSize;
390         currentSizes->get (index).size += headerSize - oldMin;
391         resized();
392     }
393 }
394 
setCustomPanelHeader(Component * component,Component * customComponent,bool takeOwnership)395 void ConcertinaPanel::setCustomPanelHeader (Component* component, Component* customComponent, bool takeOwnership)
396 {
397     OptionalScopedPointer<Component> optional (customComponent, takeOwnership);
398 
399     auto index = indexOfComp (component);
400     jassert (index >= 0); // The specified component doesn't seem to have been added!
401 
402     if (index >= 0)
403         holders.getUnchecked (index)->setCustomHeaderComponent (optional.release(), takeOwnership);
404 }
405 
resized()406 void ConcertinaPanel::resized()
407 {
408     applyLayout (getFittedSizes(), false);
409 }
410 
indexOfComp(Component * comp) const411 int ConcertinaPanel::indexOfComp (Component* comp) const noexcept
412 {
413     for (int i = 0; i < holders.size(); ++i)
414         if (holders.getUnchecked(i)->component == comp)
415             return i;
416 
417     return -1;
418 }
419 
getFittedSizes() const420 ConcertinaPanel::PanelSizes ConcertinaPanel::getFittedSizes() const
421 {
422     return currentSizes->fittedInto (getHeight());
423 }
424 
applyLayout(const PanelSizes & sizes,bool animate)425 void ConcertinaPanel::applyLayout (const PanelSizes& sizes, bool animate)
426 {
427     if (! animate)
428         animator.cancelAllAnimations (false);
429 
430     const int animationDuration = 150;
431     auto w = getWidth();
432     int y = 0;
433 
434     for (int i = 0; i < holders.size(); ++i)
435     {
436         PanelHolder& p = *holders.getUnchecked (i);
437 
438         auto h = sizes.get (i).size;
439         const Rectangle<int> pos (0, y, w, h);
440 
441         if (animate)
442             animator.animateComponent (&p, pos, 1.0f, animationDuration, false, 1.0, 1.0);
443         else
444             p.setBounds (pos);
445 
446         y += h;
447     }
448 }
449 
setLayout(const PanelSizes & sizes,bool animate)450 void ConcertinaPanel::setLayout (const PanelSizes& sizes, bool animate)
451 {
452     *currentSizes = sizes;
453     applyLayout (getFittedSizes(), animate);
454 }
455 
panelHeaderDoubleClicked(Component * component)456 void ConcertinaPanel::panelHeaderDoubleClicked (Component* component)
457 {
458     if (! expandPanelFully (component, true))
459         setPanelSize (component, 0, true);
460 }
461 
462 } // namespace juce
463