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 #pragma once
27 
28 
29 //==============================================================================
30 struct ContentViewHeader    : public Component
31 {
ContentViewHeaderContentViewHeader32     ContentViewHeader (String headerName, Icon headerIcon)
33         : name (headerName), icon (headerIcon)
34     {
35     }
36 
paintContentViewHeader37     void paint (Graphics& g) override
38     {
39         g.fillAll (findColour (contentHeaderBackgroundColourId));
40 
41         auto bounds = getLocalBounds().reduced (20, 0);
42 
43         icon.withColour (Colours::white).draw (g, bounds.toFloat().removeFromRight (30), false);
44 
45         g.setColour (Colours::white);
46         g.setFont (Font (18.0f));
47         g.drawFittedText (name, bounds, Justification::centredLeft, 1);
48     }
49 
50     String name;
51     Icon icon;
52 };
53 
54 //==============================================================================
55 class ListBoxHeader    : public Component
56 {
57 public:
ListBoxHeader(Array<String> columnHeaders)58     ListBoxHeader (Array<String> columnHeaders)
59     {
60         for (auto s : columnHeaders)
61         {
62             addAndMakeVisible (headers.add (new Label (s, s)));
63             widths.add (1.0f / (float) columnHeaders.size());
64         }
65 
66         setSize (200, 40);
67     }
68 
ListBoxHeader(Array<String> columnHeaders,Array<float> columnWidths)69     ListBoxHeader (Array<String> columnHeaders, Array<float> columnWidths)
70     {
71         jassert (columnHeaders.size() == columnWidths.size());
72 
73         auto index = 0;
74         for (auto s : columnHeaders)
75         {
76             addAndMakeVisible (headers.add (new Label (s, s)));
77             widths.add (columnWidths.getUnchecked (index++));
78         }
79 
80         recalculateWidths();
81 
82         setSize (200, 40);
83     }
84 
resized()85     void resized() override
86     {
87         auto bounds = getLocalBounds();
88         auto width = bounds.getWidth();
89 
90         auto index = 0;
91         for (auto h : headers)
92         {
93             auto headerWidth = roundToInt ((float) width * widths.getUnchecked (index));
94             h->setBounds (bounds.removeFromLeft (headerWidth));
95             ++index;
96         }
97     }
98 
setColumnHeaderWidth(int index,float proportionOfWidth)99     void setColumnHeaderWidth (int index, float proportionOfWidth)
100     {
101         if (! (isPositiveAndBelow (index, headers.size()) && isPositiveAndNotGreaterThan (proportionOfWidth, 1.0f)))
102         {
103             jassertfalse;
104             return;
105         }
106 
107         widths.set (index, proportionOfWidth);
108         recalculateWidths (index);
109     }
110 
getColumnX(int index)111     int getColumnX (int index)
112     {
113         auto prop = 0.0f;
114         for (int i = 0; i < index; ++i)
115             prop += widths.getUnchecked (i);
116 
117         return roundToInt (prop * (float) getWidth());
118     }
119 
getProportionAtIndex(int index)120     float getProportionAtIndex (int index)
121     {
122         jassert (isPositiveAndBelow (index, widths.size()));
123         return widths.getUnchecked (index);
124     }
125 
126 private:
127     OwnedArray<Label> headers;
128     Array<float> widths;
129 
130     void recalculateWidths (int indexToIgnore = -1)
131     {
132         auto total = 0.0f;
133 
134         for (auto w : widths)
135             total += w;
136 
137         if (total == 1.0f)
138             return;
139 
140         auto diff = 1.0f - total;
141         auto amount = diff / static_cast<float> (indexToIgnore == -1 ? widths.size() : widths.size() - 1);
142 
143         for (int i = 0; i < widths.size(); ++i)
144         {
145             if (i != indexToIgnore)
146             {
147                 auto val = widths.getUnchecked (i);
148                 widths.set (i, val + amount);
149             }
150         }
151     }
152 };
153 
154 //==============================================================================
155 class InfoButton    : public Button
156 {
157 public:
158     InfoButton (const String& infoToDisplay = {})
159         : Button ({})
160     {
161         if (infoToDisplay.isNotEmpty())
162             setInfoToDisplay (infoToDisplay);
163     }
164 
paintButton(Graphics & g,bool isMouseOverButton,bool isButtonDown)165     void paintButton (Graphics& g, bool isMouseOverButton, bool isButtonDown) override
166     {
167         auto bounds = getLocalBounds().toFloat().reduced (2);
168         auto& icon = getIcons().info;
169 
170         g.setColour (findColour (treeIconColourId).withMultipliedAlpha (isMouseOverButton || isButtonDown ? 1.0f : 0.5f));
171 
172         if (isButtonDown)
173             g.fillEllipse (bounds);
174         else
175             g.fillPath (icon, RectanglePlacement (RectanglePlacement::centred)
176                         .getTransformToFit (icon.getBounds(), bounds));
177     }
178 
clicked()179     void clicked() override
180     {
181         auto w = std::make_unique<InfoWindow> (info);
182         w->setSize (width, w->getHeight() * numLines + 10);
183 
184         CallOutBox::launchAsynchronously (std::move (w), getScreenBounds(), nullptr);
185     }
186 
187     using Button::clicked;
188 
setInfoToDisplay(const String & infoToDisplay)189     void setInfoToDisplay (const String& infoToDisplay)
190     {
191         if (infoToDisplay.isNotEmpty())
192         {
193             info = infoToDisplay;
194 
195             auto stringWidth = roundToInt (Font (14.0f).getStringWidthFloat (info));
196             width = jmin (300, stringWidth);
197 
198             numLines += static_cast<int> (stringWidth / width);
199         }
200     }
201 
setAssociatedComponent(Component * comp)202     void setAssociatedComponent (Component* comp)    { associatedComponent = comp; }
getAssociatedComponent()203     Component* getAssociatedComponent()              { return associatedComponent; }
204 
205 private:
206     String info;
207     Component* associatedComponent = nullptr;
208     int width;
209     int numLines = 1;
210 
211     //==============================================================================
212     struct InfoWindow    : public Component
213     {
InfoWindowInfoWindow214         InfoWindow (const String& s)
215             : stringToDisplay (s)
216         {
217             setSize (150, 14);
218         }
219 
paintInfoWindow220         void paint (Graphics& g) override
221         {
222             g.fillAll (findColour (secondaryBackgroundColourId));
223 
224             g.setColour (findColour (defaultTextColourId));
225             g.setFont (Font (14.0f));
226             g.drawFittedText (stringToDisplay, getLocalBounds(), Justification::centred, 15, 0.75f);
227         }
228 
229         String stringToDisplay;
230     };
231 
232     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (InfoButton)
233 };
234 
235 //==============================================================================
236 class PropertyGroupComponent  : public Component,
237                                 private TextPropertyComponent::Listener
238 {
239 public:
240     PropertyGroupComponent (String name, Icon icon, String desc = {})
header(name,icon)241         : header (name, icon),
242           description (desc)
243     {
244         addAndMakeVisible (header);
245 
246         description.setFont ({ 16.0f });
247         description.setColour (getLookAndFeel().findColour (defaultTextColourId));
248         description.setLineSpacing (5.0f);
249         description.setJustification (Justification::centredLeft);
250     }
251 
setProperties(const PropertyListBuilder & newProps)252     void setProperties (const PropertyListBuilder& newProps)
253     {
254         infoButtons.clear();
255         properties.clear();
256         properties.addArray (newProps.components);
257 
258         for (auto* prop : properties)
259         {
260             addAndMakeVisible (prop);
261 
262             if (! prop->getTooltip().isEmpty())
263             {
264                 addAndMakeVisible (infoButtons.add (new InfoButton (prop->getTooltip())));
265                 infoButtons.getLast()->setAssociatedComponent (prop);
266                 prop->setTooltip ({}); // set the tooltip to empty so it only displays when its button is clicked
267             }
268 
269             if (auto* multiChoice = dynamic_cast<MultiChoicePropertyComponent*> (prop))
270                 multiChoice->onHeightChange = [this] { updateSize(); };
271 
272             if (auto* text = dynamic_cast<TextPropertyComponent*> (prop))
273                 if (text->isTextEditorMultiLine())
274                     text->addListener (this);
275 
276         }
277     }
278 
updateSize(int x,int y,int width)279     int updateSize (int x, int y, int width)
280     {
281         header.setBounds (0, 0, width, headerSize);
282         auto height = header.getBottom() + 10;
283 
284         descriptionLayout.createLayout (description, (float) (width - 40));
285         auto descriptionHeight = (int) descriptionLayout.getHeight();
286 
287         if (descriptionHeight > 0)
288             height += (int) descriptionLayout.getHeight() + 25;
289 
290         for (auto* pp : properties)
291         {
292             auto propertyHeight = pp->getPreferredHeight() + (getHeightMultiplier (pp) * pp->getPreferredHeight());
293 
294             InfoButton* buttonToUse = nullptr;
295             for (auto* b : infoButtons)
296                 if (b->getAssociatedComponent() == pp)
297                     buttonToUse = b;
298 
299             if (buttonToUse != nullptr)
300             {
301                 buttonToUse->setSize (20, 20);
302                 buttonToUse->setCentrePosition (20, height + (propertyHeight / 2));
303             }
304 
305             pp->setBounds (40, height, width - 50, propertyHeight);
306 
307             if (shouldResizePropertyComponent (pp))
308                 resizePropertyComponent (pp);
309 
310             height += pp->getHeight() + 10;
311         }
312 
313         height += 16;
314 
315         setBounds (x, y, width, jmax (height, getParentHeight()));
316 
317         return height;
318     }
319 
paint(Graphics & g)320     void paint (Graphics& g) override
321     {
322         g.setColour (findColour (secondaryBackgroundColourId));
323         g.fillRect (getLocalBounds());
324 
325         auto textArea = getLocalBounds().toFloat()
326                                         .withTop ((float) headerSize)
327                                         .reduced (20.0f, 10.0f)
328                                         .withHeight (descriptionLayout.getHeight());
329         descriptionLayout.draw (g, textArea);
330     }
331 
332     OwnedArray<PropertyComponent> properties;
333 
334 private:
335     //==============================================================================
textPropertyComponentChanged(TextPropertyComponent * comp)336     void textPropertyComponentChanged (TextPropertyComponent* comp) override
337     {
338         auto fontHeight = [comp]
339         {
340             Label tmpLabel;
341             return comp->getLookAndFeel().getLabelFont (tmpLabel).getHeight();
342         }();
343 
344         auto lines = StringArray::fromLines (comp->getText());
345 
346         comp->setPreferredHeight (jmax (100, 10 + roundToInt (fontHeight * (float) lines.size())));
347 
348         updateSize();
349     }
350 
351     //==============================================================================
updateSize()352     void updateSize()
353     {
354         updateSize (getX(), getY(), getWidth());
355 
356         if (auto* parent = getParentComponent())
357             parent->parentSizeChanged();
358     }
359 
360     //==============================================================================
shouldResizePropertyComponent(PropertyComponent * p)361     bool shouldResizePropertyComponent (PropertyComponent* p)
362     {
363         if (auto* textComp = dynamic_cast<TextPropertyComponent*> (p))
364             return ! textComp->isTextEditorMultiLine();
365 
366         return (dynamic_cast<ChoicePropertyComponent*>  (p) != nullptr
367              || dynamic_cast<ButtonPropertyComponent*>  (p) != nullptr
368              || dynamic_cast<BooleanPropertyComponent*> (p) != nullptr);
369     }
370 
resizePropertyComponent(PropertyComponent * pp)371     void resizePropertyComponent (PropertyComponent* pp)
372     {
373         for (auto i = pp->getNumChildComponents() - 1; i >= 0; --i)
374         {
375             auto* child = pp->getChildComponent (i);
376 
377             auto bounds = child->getBounds();
378             child->setBounds (bounds.withSizeKeepingCentre (child->getWidth(), pp->getPreferredHeight()));
379         }
380     }
381 
getHeightMultiplier(PropertyComponent * pp)382     int getHeightMultiplier (PropertyComponent* pp)
383     {
384         auto availableTextWidth = ProjucerLookAndFeel::getTextWidthForPropertyComponent (pp);
385 
386         auto font = ProjucerLookAndFeel::getPropertyComponentFont();
387         auto nameWidth = font.getStringWidthFloat (pp->getName());
388 
389         if (availableTextWidth == 0)
390             return 0;
391 
392         return static_cast<int> (nameWidth / (float) availableTextWidth);
393     }
394 
395     OwnedArray<InfoButton> infoButtons;
396     ContentViewHeader header;
397     AttributedString description;
398     TextLayout descriptionLayout;
399     int headerSize = 40;
400 
401     //==============================================================================
402     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PropertyGroupComponent)
403 };
404