diff --git a/examples/GUI/PropertiesDemo.h b/examples/GUI/PropertiesDemo.h index a521c5985f..01ca26df61 100644 --- a/examples/GUI/PropertiesDemo.h +++ b/examples/GUI/PropertiesDemo.h @@ -141,58 +141,27 @@ static Array createChoices (int howMany) StringArray choices; Array choiceVars; - for (int i = 0; i < howMany; ++i) + for (int i = 0; i < 12; ++i) { choices.add ("Item " + String (i)); choiceVars.add (i); } for (int i = 0; i < howMany; ++i) - comps.add (new ChoicePropertyComponent (Value (Random::getSystemRandom().nextInt (6)), "Choice Property " + String (i + 1), choices, choiceVars)); + comps.add (new ChoicePropertyComponent (Value (Random::getSystemRandom().nextInt (12)), "Choice Property " + String (i + 1), choices, choiceVars)); + + for (int i = 0; i < howMany; ++i) + comps.add (new MultiChoicePropertyComponent (Value (Array()), "Multi-Choice Property " + String (i + 1), choices, choiceVars)); return comps; } //============================================================================== -class PropertiesDemo : public Component -{ -public: - PropertiesDemo() - { - setOpaque (true); - addAndMakeVisible (propertyPanel); - - propertyPanel.addSection ("Text Editors", createTextEditors()); - propertyPanel.addSection ("Sliders", createSliders (3)); - propertyPanel.addSection ("Choice Properties", createChoices (6)); - propertyPanel.addSection ("Buttons & Toggles", createButtons (3)); - - setSize (750, 650); - } - - void paint (Graphics& g) override - { - g.fillAll (getUIColourIfAvailable (LookAndFeel_V4::ColourScheme::UIColour::windowBackground, - Colour::greyLevel (0.8f))); - } - - void resized() override - { - propertyPanel.setBounds (getLocalBounds().reduced (4)); - } - -private: - PropertyPanel propertyPanel; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PropertiesDemo) -}; - -//============================================================================== -class ConcertinaDemo : public Component, +class PropertiesDemo : public Component, private Timer { public: - ConcertinaDemo() + PropertiesDemo() { setOpaque (true); addAndMakeVisible (concertinaPanel); @@ -212,7 +181,7 @@ public: { auto* panel = new PropertyPanel ("Choice Properties"); - panel->addProperties (createChoices (12)); + panel->addProperties (createChoices (3)); addPanel (panel); } @@ -222,6 +191,8 @@ public: addPanel (panel); } + + setSize (750, 650); startTimer (300); } @@ -251,5 +222,5 @@ private: concertinaPanel.setMaximumPanelSize (panel, panel->getTotalContentHeight()); } - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ConcertinaDemo) + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PropertiesDemo) }; diff --git a/modules/juce_gui_basics/juce_gui_basics.cpp b/modules/juce_gui_basics/juce_gui_basics.cpp index 3645f52222..b8148e76f6 100644 --- a/modules/juce_gui_basics/juce_gui_basics.cpp +++ b/modules/juce_gui_basics/juce_gui_basics.cpp @@ -235,6 +235,7 @@ namespace juce #include "properties/juce_PropertyPanel.cpp" #include "properties/juce_SliderPropertyComponent.cpp" #include "properties/juce_TextPropertyComponent.cpp" +#include "properties/juce_MultiChoicePropertyComponent.cpp" #include "widgets/juce_ComboBox.cpp" #include "widgets/juce_ImageComponent.cpp" #include "widgets/juce_Label.cpp" diff --git a/modules/juce_gui_basics/juce_gui_basics.h b/modules/juce_gui_basics/juce_gui_basics.h index afaa0caccf..d0677a7235 100644 --- a/modules/juce_gui_basics/juce_gui_basics.h +++ b/modules/juce_gui_basics/juce_gui_basics.h @@ -280,6 +280,7 @@ namespace juce #include "properties/juce_PropertyPanel.h" #include "properties/juce_SliderPropertyComponent.h" #include "properties/juce_TextPropertyComponent.h" +#include "properties/juce_MultiChoicePropertyComponent.h" #include "application/juce_Application.h" #include "misc/juce_BubbleComponent.h" #include "lookandfeel/juce_LookAndFeel.h" diff --git a/modules/juce_gui_basics/properties/juce_MultiChoicePropertyComponent.cpp b/modules/juce_gui_basics/properties/juce_MultiChoicePropertyComponent.cpp new file mode 100644 index 0000000000..6c41493ef7 --- /dev/null +++ b/modules/juce_gui_basics/properties/juce_MultiChoicePropertyComponent.cpp @@ -0,0 +1,244 @@ +/* + ============================================================================== + + 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 +{ + +//============================================================================== +class MultiChoicePropertyComponent::MultiChoiceRemapperSource : public Value::ValueSource, + private Value::Listener +{ +public: + MultiChoiceRemapperSource (const Value& source, var v) + : sourceValue (source), + varToControl (v) + { + sourceValue.addListener (this); + } + + var getValue() const override + { + if (auto* arr = sourceValue.getValue().getArray()) + if (arr->contains (varToControl)) + return 1; + + return 0; + } + + void setValue (const var& newValue) override + { + if (auto* arr = sourceValue.getValue().getArray()) + { + auto newValueInt = static_cast (newValue); + + if (newValueInt == 1) + arr->addIfNotAlreadyThere (varToControl); + else + arr->remove (arr->indexOf (varToControl)); + } + } + +private: + Value sourceValue; + var varToControl; + + void valueChanged (Value&) override { sendChangeMessage (true); } + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MultiChoiceRemapperSource) +}; + +//============================================================================== +class MultiChoicePropertyComponent::MultiChoiceRemapperSourceWithDefault : public Value::ValueSource, + private Value::Listener +{ +public: + MultiChoiceRemapperSourceWithDefault (const ValueWithDefault& vwd, var v) + : valueWithDefault (vwd), + sourceValue (valueWithDefault.getPropertyAsValue()), + varToControl (v) + { + sourceValue.addListener (this); + } + + var getValue() const override + { + if (auto* arr = valueWithDefault.get().getArray()) + if (arr->contains (varToControl)) + return 1; + + return 0; + } + + void setValue (const var& newValue) override + { + if (auto* arr = valueWithDefault.get().getArray()) + { + auto newValueInt = static_cast (newValue); + + if (newValueInt == 1) + arr->addIfNotAlreadyThere (varToControl); + else + arr->remove (arr->indexOf (varToControl)); + } + } + +private: + ValueWithDefault valueWithDefault; + Value sourceValue; + var varToControl; + + void valueChanged (Value&) override { sendChangeMessage (true); } + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MultiChoiceRemapperSourceWithDefault) +}; + +//============================================================================== +MultiChoicePropertyComponent::MultiChoicePropertyComponent (const String& propertyName, + const StringArray& choices, + const Array& correspondingValues) + : PropertyComponent (propertyName, 70) +{ + // The array of corresponding values must contain one value for each of the items in + // the choices array! + jassert (choices.size() == correspondingValues.size()); + + ignoreUnused (correspondingValues); + + for (auto choice : choices) + addAndMakeVisible (choiceButtons.add (new ToggleButton (choice))); + + maxHeight = (choiceButtons.size() * 25) + 20; + + { + Path expandShape; + expandShape.addTriangle ({ 0, 0 }, { 5, 10 }, { 10, 0}); + expandButton.setShape (expandShape, true, true, false); + } + + expandButton.onClick = [this] { setExpanded (! expanded); }; + addAndMakeVisible (expandButton); + + lookAndFeelChanged(); +} + +MultiChoicePropertyComponent::MultiChoicePropertyComponent (const Value& valueToControl, + const String& propertyName, + const StringArray& choices, + const Array& correspondingValues) + : MultiChoicePropertyComponent (propertyName, choices, correspondingValues) +{ + // The value to control must be an array! + jassert (valueToControl.getValue().isArray()); + + for (int i = 0; i < choiceButtons.size(); ++i) + choiceButtons[i]->getToggleStateValue().referTo (Value (new MultiChoiceRemapperSource (valueToControl, + correspondingValues[i]))); +} + +MultiChoicePropertyComponent::MultiChoicePropertyComponent (const ValueWithDefault& valueToControl, + const String& propertyName, + const StringArray& choices, + const Array& correspondingValues) + : MultiChoicePropertyComponent (propertyName, choices, correspondingValues) +{ + // The value to control must be an array! + jassert (valueToControl.get().isArray()); + + for (int i = 0; i < choiceButtons.size(); ++i) + choiceButtons[i]->getToggleStateValue().referTo (Value (new MultiChoiceRemapperSourceWithDefault (valueToControl, + correspondingValues[i]))); +} + +void MultiChoicePropertyComponent::paint (Graphics& g) +{ + g.setColour (findColour (TextEditor::backgroundColourId)); + g.fillRect (getLookAndFeel().getPropertyComponentContentPosition (*this)); + + if (! expanded) + { + g.setColour (findColour (PropertyComponent::labelTextColourId).withAlpha (0.4f)); + g.drawFittedText ("+ " + String (numHidden) + " more", getLookAndFeel().getPropertyComponentContentPosition (*this) + .removeFromBottom (20).withTrimmedLeft (10), + Justification::centredLeft, 1); + } + + PropertyComponent::paint (g); +} + +void MultiChoicePropertyComponent::resized() +{ + auto bounds = getLookAndFeel().getPropertyComponentContentPosition (*this); + + bounds.removeFromBottom (5); + expandButton.setBounds (bounds.removeFromBottom (10)); + + numHidden = 0; + + for (auto* b : choiceButtons) + { + if (bounds.getHeight() >= 25) + { + b->setVisible (true); + b->setBounds (bounds.removeFromTop (25).reduced (5, 2)); + } + else + { + b->setVisible (false); + ++numHidden; + } + } +} + +void MultiChoicePropertyComponent::setExpanded (bool isExpanded) noexcept +{ + if (expanded == isExpanded) + return; + + expanded = isExpanded; + preferredHeight = expanded ? maxHeight : 70; + + if (auto* propertyPanel = findParentComponentOfClass()) + propertyPanel->resized(); + + if (onHeightChange != nullptr) + onHeightChange(); + + expandButton.setTransform (AffineTransform::rotation (expanded ? MathConstants::pi : MathConstants::twoPi, + (float) expandButton.getBounds().getCentreX(), + (float) expandButton.getBounds().getCentreY())); +} + +//============================================================================== +void MultiChoicePropertyComponent::lookAndFeelChanged() +{ + auto iconColour = findColour (PropertyComponent::labelTextColourId); + expandButton.setColours (iconColour, iconColour.darker(), iconColour.darker()); +} + +} // namespace juce diff --git a/modules/juce_gui_basics/properties/juce_MultiChoicePropertyComponent.h b/modules/juce_gui_basics/properties/juce_MultiChoicePropertyComponent.h new file mode 100644 index 0000000000..b44805517e --- /dev/null +++ b/modules/juce_gui_basics/properties/juce_MultiChoicePropertyComponent.h @@ -0,0 +1,121 @@ +/* + ============================================================================== + + 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 PropertyComponent that shows its value as an expandable list of ToggleButtons. + + This type of property component contains a list of options where multiple options + can be selected at once. + + @see PropertyComponent, PropertyPanel + + @tags{GUI} +*/ +class MultiChoicePropertyComponent : public PropertyComponent +{ +public: + /** Creates the component. Note that the underlying var object that the Value refers to must be an array. + + @param valueToControl the value that the ToggleButtons will read and control + @param propertyName the name of the property + @param choices the list of possible values that will be represented + @param correspondingValues a list of values corresponding to each item in the 'choices' StringArray. + These are the values that will be read and written to the + valueToControl value. This array must contain the same number of items + as the choices array + */ + MultiChoicePropertyComponent (const Value& valueToControl, + const String& propertyName, + const StringArray& choices, + const Array& correspondingValues); + + /** Creates the component using a ValueWithDefault object. This will select the default options. + + @param valueToControl the ValueWithDefault object that contains the Value object that the ToggleButtons will read and control + @param propertyName the name of the property + @param choices the list of possible values that will be represented + @param correspondingValues a list of values corresponding to each item in the 'choices' StringArray. + These are the values that will be read and written to the + valueToControl value. This array must contain the same number of items + as the choices array + */ + MultiChoicePropertyComponent (const ValueWithDefault& valueToControl, + const String& propertyName, + const StringArray& choices, + const Array& correspondingValues); + + //============================================================================== + /** Returns true if the list of options is expanded. */ + bool isExpanded() const noexcept { return expanded; } + + /** Expands or shrinks the list of options. + + N.B. This will just set the preferredHeight value of the PropertyComponent and attempt to + call PropertyPanel::resized(), so if you are not displaying this object in a PropertyPanel + then you should use the onHeightChange callback to resize it when the height changes. + + @see onHeightChange + */ + void setExpanded (bool expanded) noexcept; + + /** You can assign a lambda to this callback object to have it called when the MultiChoicePropertyComponent height changes. */ + std::function onHeightChange; + + //============================================================================== + /** @internal */ + void paint (Graphics& g) override; + /** @internal */ + void resized() override; + /** @internal */ + void refresh() override {} + +private: + MultiChoicePropertyComponent (const String&, const StringArray&, const Array&); + + class MultiChoiceRemapperSource; + class MultiChoiceRemapperSourceWithDefault; + + //============================================================================== + void lookAndFeelChanged() override; + + //============================================================================== + int maxHeight = 0; + int numHidden = 0; + bool expanded = false; + + OwnedArray choiceButtons; + ShapeButton expandButton { "Expand", Colours::transparentBlack, Colours::transparentBlack, Colours::transparentBlack }; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MultiChoicePropertyComponent) +}; + +} // namespace juce