diff --git a/modules/juce_data_structures/juce_data_structures.h b/modules/juce_data_structures/juce_data_structures.h index f4b74adbc9..4eaf00cadd 100644 --- a/modules/juce_data_structures/juce_data_structures.h +++ b/modules/juce_data_structures/juce_data_structures.h @@ -60,5 +60,6 @@ #include "values/juce_ValueTree.h" #include "values/juce_ValueTreeSynchroniser.h" #include "values/juce_CachedValue.h" +#include "values/juce_ValueWithDefault.h" #include "app_properties/juce_PropertiesFile.h" #include "app_properties/juce_ApplicationProperties.h" diff --git a/modules/juce_data_structures/values/juce_ValueWithDefault.h b/modules/juce_data_structures/values/juce_ValueWithDefault.h new file mode 100644 index 0000000000..72f8e29fa8 --- /dev/null +++ b/modules/juce_data_structures/values/juce_ValueWithDefault.h @@ -0,0 +1,163 @@ +/* + ============================================================================== + + 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 +{ + +//============================================================================== +/** + This class acts as a wrapper around a property inside a ValueTree. + + If the property inside the ValueTree is missing or empty the ValueWithDefault will automatically + return a default value, which can be specified when initialising the ValueWithDefault. +*/ +class ValueWithDefault +{ +public: + //============================================================================== + /** Creates an unitialised ValueWithDefault. Initialise it using one of the referTo() methods. */ + ValueWithDefault() : undoManager (nullptr) {} + + /** Creates an ValueWithDefault object. The default value will be an empty var. */ + ValueWithDefault (ValueTree& tree, const Identifier& propertyID, + UndoManager* um) + : targetTree (tree), + targetProperty (propertyID), + undoManager (um), + defaultValue() + { + } + + /** Creates an ValueWithDefault object. The default value will be defaultToUse. */ + ValueWithDefault (ValueTree& tree, const Identifier& propertyID, + UndoManager* um, const var& defaultToUse) + : targetTree (tree), + targetProperty (propertyID), + undoManager (um), + defaultValue (defaultToUse) + { + } + + /** Creates a ValueWithDefault object from another ValueWithDefault object. */ + ValueWithDefault (const ValueWithDefault& other) + : targetTree (other.targetTree), + targetProperty (other.targetProperty), + undoManager (other.undoManager), + defaultValue (other.defaultValue) + { + } + + //============================================================================== + /** Returns the current value of the property. If the property does not exist or is empty, + returns the default value. + */ + var get() const noexcept + { + if (isUsingDefault()) + return defaultValue; + + return targetTree[targetProperty]; + } + + /** Returns the current property as a Value object. */ + Value getPropertyAsValue() { return targetTree.getPropertyAsValue (targetProperty, undoManager); } + + /** Returns the current default value. */ + var getDefault() const { return defaultValue; } + + /** Sets the default value to a new var. */ + void setDefault (const var& newDefault) + { + if (defaultValue != newDefault) + defaultValue = newDefault; + } + + /** Returns true if the property does not exist or is empty. */ + bool isUsingDefault() const + { + return ! targetTree.hasProperty (targetProperty); + } + + /** Resets the property to an empty var. */ + void resetToDefault() noexcept + { + targetTree.removeProperty (targetProperty, nullptr); + } + + //============================================================================== + /** Sets the property and returns the new ValueWithDefault. This will modify the property in the referenced ValueTree. */ + ValueWithDefault& operator= (const var& newValue) + { + setValue (newValue, undoManager); + return *this; + } + + /** Sets the property. This will actually modify the property in the referenced ValueTree. */ + void setValue (const var& newValue, UndoManager* undoManagerToUse) + { + targetTree.setProperty (targetProperty, newValue, undoManagerToUse); + } + + //============================================================================== + /** Makes the ValueWithDefault refer to the specified property inside the given ValueTree. */ + void referTo (ValueTree& tree, const Identifier& property, UndoManager* um) + { + referToWithDefault (tree, property, um, var()); + } + + /** Makes the ValueWithDefault refer to the specified property inside the given ValueTree, + and specifies a default value to use. + */ + void referTo (ValueTree& tree, const Identifier& property, UndoManager* um, const var& defaultVal) + { + referToWithDefault (tree, property, um, defaultVal); + } + + //============================================================================== + /** Returns a reference to the ValueTree containing the referenced property. */ + ValueTree& getValueTree() noexcept { return targetTree; } + + /** Returns the property ID of the referenced property. */ + Identifier& getPropertyID() noexcept { return targetProperty; } + +private: + //============================================================================== + ValueTree targetTree; + Identifier targetProperty; + UndoManager* undoManager; + var defaultValue; + + //============================================================================== + void referToWithDefault (ValueTree& v, const Identifier& i, UndoManager* um, const var& defaultVal) + { + targetTree = v; + targetProperty = i; + undoManager = um; + defaultValue = defaultVal; + } +}; + +} // namespace juce diff --git a/modules/juce_gui_basics/properties/juce_ChoicePropertyComponent.cpp b/modules/juce_gui_basics/properties/juce_ChoicePropertyComponent.cpp index c788ae3920..18e15bb8ef 100644 --- a/modules/juce_gui_basics/properties/juce_ChoicePropertyComponent.cpp +++ b/modules/juce_gui_basics/properties/juce_ChoicePropertyComponent.cpp @@ -27,30 +27,32 @@ namespace juce { +//============================================================================== class ChoicePropertyComponent::RemapperValueSource : public Value::ValueSource, private Value::Listener { public: RemapperValueSource (const Value& source, const Array& map) - : sourceValue (source), mappings (map) + : sourceValue (source), + mappings (map) { sourceValue.addListener (this); } - var getValue() const + var getValue() const override { - const var targetValue (sourceValue.getValue()); + auto targetValue = sourceValue.getValue(); - for (int i = 0; i < mappings.size(); ++i) - if (mappings.getReference(i).equalsWithSameType (targetValue)) - return i + 1; + for (auto& map : mappings) + if (map.equalsWithSameType (targetValue)) + return mappings.indexOf (map) + 1; return mappings.indexOf (targetValue) + 1; } - void setValue (const var& newValue) + void setValue (const var& newValue) override { - const var remappedVal (mappings [static_cast (newValue) - 1]); + auto remappedVal = mappings [static_cast (newValue) - 1]; if (! remappedVal.equalsWithSameType (sourceValue)) sourceValue = remappedVal; @@ -60,14 +62,66 @@ protected: Value sourceValue; Array mappings; - void valueChanged (Value&) - { - sendChangeMessage (true); - } + void valueChanged (Value&) override { sendChangeMessage (true); } + //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (RemapperValueSource) }; +//============================================================================== +class ChoicePropertyComponent::RemapperValueSourceWithDefault : public Value::ValueSource, + private Value::Listener +{ +public: + RemapperValueSourceWithDefault (const ValueWithDefault& vwd, const Array& map) + : valueWithDefault (vwd), + sourceValue (valueWithDefault.getPropertyAsValue()), + mappings (map) + { + sourceValue.addListener (this); + } + + var getValue() const override + { + if (valueWithDefault.isUsingDefault()) + return -1; + + auto targetValue = sourceValue.getValue(); + + for (auto map : mappings) + if (map.equalsWithSameType (targetValue)) + return mappings.indexOf (map) + 1; + + return mappings.indexOf (targetValue) + 1; + } + + void setValue (const var& newValue) override + { + auto newValueInt = static_cast (newValue); + + if (newValueInt == -1) + { + valueWithDefault.resetToDefault(); + } + else + { + auto remappedVal = mappings [newValueInt - 1]; + + if (! remappedVal.equalsWithSameType (sourceValue)) + valueWithDefault = remappedVal; + } + } + +private: + ValueWithDefault valueWithDefault; + Value sourceValue; + Array mappings; + + void valueChanged (Value&) override { sendChangeMessage (true); } + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (RemapperValueSourceWithDefault) +}; //============================================================================== ChoicePropertyComponent::ChoicePropertyComponent (const String& name) @@ -76,24 +130,54 @@ ChoicePropertyComponent::ChoicePropertyComponent (const String& name) { } -ChoicePropertyComponent::ChoicePropertyComponent (const Value& valueToControl, - const String& name, +ChoicePropertyComponent::ChoicePropertyComponent (const String& name, const StringArray& choiceList, const Array& correspondingValues) : PropertyComponent (name), - choices (choiceList), - isCustomClass (false) + choices (choiceList) { // The array of corresponding values must contain one value for each of the items in // the choices array! jassert (correspondingValues.size() == choices.size()); + ignoreUnused (correspondingValues); +} + +ChoicePropertyComponent::ChoicePropertyComponent (const Value& valueToControl, + const String& name, + const StringArray& choiceList, + const Array& correspondingValues) + : ChoicePropertyComponent (name, choiceList, correspondingValues) +{ createComboBox(); comboBox.getSelectedIdAsValue().referTo (Value (new RemapperValueSource (valueToControl, correspondingValues))); } +ChoicePropertyComponent::ChoicePropertyComponent (ValueWithDefault valueToControl, + const String& name, + const StringArray& choiceList, + const Array& correspondingValues) + : ChoicePropertyComponent (name, choiceList, correspondingValues) +{ + createComboBoxWithDefault (choiceList [correspondingValues.indexOf (valueToControl.getDefault())]); + + comboBox.getSelectedIdAsValue().referTo (Value (new RemapperValueSourceWithDefault (valueToControl, + correspondingValues))); +} + +ChoicePropertyComponent::ChoicePropertyComponent (ValueWithDefault valueToControl, + const String& name) + : PropertyComponent (name), + choices ({ "Enabled", "Disabled" }) +{ + createComboBoxWithDefault (valueToControl.getDefault() ? "Enabled" : "Disabled"); + + comboBox.getSelectedIdAsValue().referTo (Value (new RemapperValueSourceWithDefault (valueToControl, + { true, false }))); +} + ChoicePropertyComponent::~ChoicePropertyComponent() { } @@ -103,10 +187,27 @@ void ChoicePropertyComponent::createComboBox() { addAndMakeVisible (comboBox); - for (int i = 0; i < choices.size(); ++i) + for (auto choice : choices) + { + if (choice.isNotEmpty()) + comboBox.addItem (choice, choices.indexOf (choice) + 1); + else + comboBox.addSeparator(); + } + + comboBox.setEditableText (false); +} + +void ChoicePropertyComponent::createComboBoxWithDefault (const String& defaultString) +{ + addAndMakeVisible (comboBox); + + comboBox.addItem ("Default" + (defaultString.isNotEmpty() ? " (" + defaultString + ")" : ""), -1); + + for (auto choice : choices) { - if (choices[i].isNotEmpty()) - comboBox.addItem (choices[i], i + 1); + if (choice.isNotEmpty()) + comboBox.addItem (choice, choices.indexOf (choice) + 1); else comboBox.addSeparator(); } @@ -149,7 +250,7 @@ void ChoicePropertyComponent::comboBoxChanged (ComboBox*) { if (isCustomClass) { - const int newIndex = comboBox.getSelectedId() - 1; + auto newIndex = comboBox.getSelectedId() - 1; if (newIndex != getIndex()) setIndex (newIndex); diff --git a/modules/juce_gui_basics/properties/juce_ChoicePropertyComponent.h b/modules/juce_gui_basics/properties/juce_ChoicePropertyComponent.h index 600a23cc7f..02611ba912 100644 --- a/modules/juce_gui_basics/properties/juce_ChoicePropertyComponent.h +++ b/modules/juce_gui_basics/properties/juce_ChoicePropertyComponent.h @@ -75,6 +75,31 @@ public: const StringArray& choices, const Array& correspondingValues); + /** Creates the component using a ValueWithDefault object. This will add an item to the ComboBox for the + default value with an ID of -1. + + @param valueToControl the ValueWithDefault object that contains the Value object that the combo box will read and control + @param propertyName the name of the property + @param choices the list of possible values that the drop-down list will contain + @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 + + */ + ChoicePropertyComponent (ValueWithDefault valueToControl, + const String& propertyName, + const StringArray& choices, + const Array& correspondingValues); + + /** Creates the component using a ValueWithDefault object, adding an item to the ComboBox for the + default value with an ID of -1 as well as adding separate "Enabled" and "Disabled" options. + + This is useful for simple on/off choices that also need a default value. + */ + ChoicePropertyComponent (ValueWithDefault valueToControl, + const String& propertyName); + /** Destructor. */ ~ChoicePropertyComponent(); @@ -95,10 +120,9 @@ public: /** Returns the list of options. */ const StringArray& getChoices() const; - //============================================================================== /** @internal */ - void refresh(); + void refresh() override; protected: /** The list of options that will be shown in the combo box. @@ -110,12 +134,19 @@ protected: StringArray choices; private: + /** Delegating constructor. */ + ChoicePropertyComponent (const String&, const StringArray&, const Array&); + ComboBox comboBox; - bool isCustomClass; + bool isCustomClass = false; class RemapperValueSource; + class RemapperValueSourceWithDefault; + void createComboBox(); - void comboBoxChanged (ComboBox*); + void createComboBoxWithDefault (const String&); + + void comboBoxChanged (ComboBox*) override; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ChoicePropertyComponent) }; diff --git a/modules/juce_gui_basics/properties/juce_TextPropertyComponent.cpp b/modules/juce_gui_basics/properties/juce_TextPropertyComponent.cpp index 0ddfd9524e..d41bf7a8e1 100644 --- a/modules/juce_gui_basics/properties/juce_TextPropertyComponent.cpp +++ b/modules/juce_gui_basics/properties/juce_TextPropertyComponent.cpp @@ -27,6 +27,7 @@ namespace juce { +//============================================================================== class TextPropertyComponent::LabelComp : public Label, public FileDragAndDropTarget { @@ -85,11 +86,66 @@ public: interestedInFileDrag = isInterested; } + void setTextToDisplayWhenEmpty (const String& text, float alpha) + { + textToDisplayWhenEmpty = text; + alphaToUseForEmptyText = alpha; + } + + void paintOverChildren (Graphics& g) override + { + if (getText().isEmpty() && ! isBeingEdited()) + { + auto textArea = getBorderSize().subtractedFrom (getLocalBounds()); + auto labelFont = owner.getLookAndFeel().getLabelFont (*this); + + g.setColour (owner.findColour (TextPropertyComponent::textColourId).withAlpha (alphaToUseForEmptyText)); + g.setFont (labelFont); + + g.drawFittedText (textToDisplayWhenEmpty, textArea, getJustificationType(), + jmax (1, (int) (textArea.getHeight() / labelFont.getHeight())), + getMinimumHorizontalScale()); + } + } + private: TextPropertyComponent& owner; + int maxChars; bool isMultiline; bool interestedInFileDrag = true; + + String textToDisplayWhenEmpty; + float alphaToUseForEmptyText = 0.0f; +}; + +//============================================================================== +class TextPropertyComponent::RemapperValueSourceWithDefault : public Value::ValueSource +{ +public: + RemapperValueSourceWithDefault (const ValueWithDefault& vwd) + : valueWithDefault (vwd) + { + } + + var getValue() const override + { + return valueWithDefault.isUsingDefault() ? var() : valueWithDefault.get(); + } + + void setValue (const var& newValue) override + { + if (newValue.toString().isEmpty()) + valueWithDefault.resetToDefault(); + else + valueWithDefault = newValue; + } + +private: + ValueWithDefault valueWithDefault; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (RemapperValueSourceWithDefault) }; //============================================================================== @@ -107,13 +163,22 @@ TextPropertyComponent::TextPropertyComponent (const Value& valueToControl, int maxNumChars, bool isMultiLine, bool isEditable) - : PropertyComponent (name) + : TextPropertyComponent (name, maxNumChars, isMultiLine, isEditable) { - createEditor (maxNumChars, isMultiLine, isEditable); - textEditor->getTextValue().referTo (valueToControl); } +TextPropertyComponent::TextPropertyComponent (const ValueWithDefault& valueToControl, + const String& name, + int maxNumChars, + bool isMultiLine, + bool isEditable) + : TextPropertyComponent (name, maxNumChars, isMultiLine, isEditable) +{ + textEditor->getTextValue().referTo (Value (new RemapperValueSourceWithDefault (valueToControl))); + textEditor->setTextToDisplayWhenEmpty (valueToControl.getDefault(), 0.5f); +} + TextPropertyComponent::~TextPropertyComponent() { } diff --git a/modules/juce_gui_basics/properties/juce_TextPropertyComponent.h b/modules/juce_gui_basics/properties/juce_TextPropertyComponent.h index cb67290c45..2b2e62e699 100644 --- a/modules/juce_gui_basics/properties/juce_TextPropertyComponent.h +++ b/modules/juce_gui_basics/properties/juce_TextPropertyComponent.h @@ -70,6 +70,23 @@ public: bool isMultiLine, bool isEditable = true); + /** Creates a text property component with a default value. + + @param valueToControl The ValueWithDefault that is controlled by the TextPropertyComponent + @param propertyName The name of the property + @param maxNumChars If not zero, then this specifies the maximum allowable length of + the string. If zero, then the string will have no length limit. + @param isMultiLine Sets whether the text editor allows carriage returns. + @param isEditable Sets whether the text editor is editable. The default is true. + + @see TextEditor, setEditable + */ + TextPropertyComponent (const ValueWithDefault& valueToControl, + const String& propertyName, + int maxNumChars, + bool isMultiLine, + bool isEditable = true); + /** Destructor. */ ~TextPropertyComponent(); @@ -144,6 +161,8 @@ public: virtual void textWasEdited(); private: + class RemapperValueSourceWithDefault; + class LabelComp; friend class LabelComp;