ValueTree, undoManager, Property ... Beginner's questions

Hi,
How implement a simple valueTree to save a component value in app ?

For exemple a simple app with a slider

        class MainComponent  : public Component
    {
    public:
        MainComponent()
        {
            setOpaque (true);
            setSize (640, 480);
            addAndMakeVisible(mySlider);
            mySlider.setSliderStyle(Slider::SliderStyle::LinearVertical);
            
            
        }

        void paint (Graphics& g) override
        {
            g.fillAll (findColour (ResizableWindow::backgroundColourId));
        }

        void resized() override
        {
            mySlider.setBounds(10, 10, 64, getHeight()-20);
            
        }

    private:
        Slider   mySlider;

It might depend on what you’re trying to accomplish… but a thing I find really cool is that you can make their Value refer to the ValueTree .

so you could make something like:

auto value = valueTree.getPropertyAsValue (“myProperty”, nullptr);
slider.getValueObject().referToSameSourceAs (value);

This way your ValueTree will be in sync with the slider value.
But you have to be aware if you create a new value tree, you’ll have to update the references.

1 Like

Thanks for your answer. Like this ? (tree is my ValueTree)
How restore value when my app opening ? I need methods to save and load my valueTree, isn’t it ?

MainComponent()
{
    setOpaque (true);
    setSize (640, 480);
    addAndMakeVisible(mySlider);
    mySlider.setSliderStyle(Slider::SliderStyle::LinearVertical);
    auto value = tree.getPropertyAsValue("mySlider", nullptr);
    mySlider.getValueObject().refersToSameSourceAs (value);
    
    
}

I’m trying something like this to save, but it seems my xml is empty !?!

XmlElement *xml (tree.createXml());
    File myFile {(File::getSpecialLocation(File::SpecialLocationType::userApplicationDataDirectory).getFullPathName() + "/Property.xml")};
    xml->writeToFile(myFile, "Property");

Should be slider.getValueObject().referTo (value);

Use getChildFile() instead of concatenating the paths, and check whether writeToFile returned true.

Yes you are right about the getChildFild but that doesn’t solve my problem about save and restore a slider value by an Xml properties file…

File myFile {(File::getSpecialLocation(File::SpecialLocationType::userApplicationDataDirectory).getChildFile("Property.xml").getFullPathName()) };

To save defaults from your app for the next time, there is the PropertiesFile class.
It inherits a PropertiesSet, so you can add a property for everything you want to save (it ends up in an XML).
You will have to propagate the value changes yourself, either by overriding the propertyChanged() method, or add a ChangeListener.

I am not sure, there might exist an automatic way to link that as well, and @matkatmusic once posted his own solution for that, if you want to use the forum search.
(here it is: ScopedValueSaver - never worry about App State again)

3 Likes

yep, you’re right… thanks for correcting it :wink:

@nseaprotector,
You have to make sure the property already exists in the valueTree before trying to assign it to the slider.

1 Like

Thanks for your answers.
Thanks Daniel, I’m a beginner and English is not my mother tongue which does not make it easy for me to find and understand the good way.
jrrossi: Do you talk about to this ?

Work fine

//==============================================================================
    class MainComponent  : public Component
    {
    public:
        MainComponent()
        {
            setOpaque (true);
            setSize (640, 480);
            addAndMakeVisible(mySlider);
            mySlider.setSliderStyle(Slider::SliderStyle::LinearVertical);
            initProperties();
            loadParams();
            
            
        }
        
        ~MainComponent()
        {
            saveParams();
            
        }


        void initProperties()
        {
            PropertiesFile::Options options;
            options.applicationName = ProjectInfo::projectName;
            options.filenameSuffix = ".settings";
            options.osxLibrarySubFolder = "Application Support";
            options.folderName = File::getSpecialLocation(File::SpecialLocationType::userApplicationDataDirectory).getChildFile("myApp").getFullPathName();
            options.storageFormat = PropertiesFile::storeAsXML;
            
            props.setStorageParameters(options);
        }
        void loadParams()
        {
           
            mySlider.setValue( props.getUserSettings()->getIntValue("mySlider"));


        }
        void saveParams()
        {
            props.getUserSettings()->setValue("mySlider", mySlider.getValue());
        }
        
        void paint (Graphics& g) override
        {
            g.fillAll (findColour (ResizableWindow::backgroundColourId));
        }

        void resized() override
        {
            mySlider.setBounds(10, 10, 100, getHeight()-20);
            
        }

    private:
        Slider   mySlider;
        ApplicationProperties props;

        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
    };
1 Like

I’m trying to add an UndoManager to see how implement it, doesn’t work for the moment…

addAndMakeVisible (undoButton);
addAndMakeVisible (redoButton);
undoButton.setButtonText("Undo");
redoButton.setButtonText ("Redo");
undoButton.onClick = [this] { undoManager.undo(); };
redoButton.onClick = [this] { undoManager.redo(); };
ValueTree myValueTree;
mySlider.getValueObject().refersToSameSourceAs(myValueTree.getPropertyAsValue ("mySlider", &undoManager));
startTimer (500);
void timerCallback() override
{
undoManager.beginNewTransaction();
}

Yes I finally resolve it by myself, happy :slight_smile:

class MainComponent  : public Component,private ValueTree::Listener, private Timer
{
public:
    MainComponent()
    {
        setOpaque (true);
        setSize (640, 480);
        addAndMakeVisible(mySlider);
        mySlider.setSliderStyle(Slider::SliderStyle::LinearVertical);
        initProperties();
        loadParams();
        addAndMakeVisible (undoButton);
        addAndMakeVisible (redoButton);
        undoButton.setButtonText("Undo");
        redoButton.setButtonText ("Redo");
        undoButton.onClick = [this] { undoManager.undo(); };
        redoButton.onClick = [this] { undoManager.redo(); };
        ValueTree myValueTree ("myApp");
        myValueTree.setProperty("mySlider", mySlider.getValue(), &undoManager);
        Value valueTreeObj = myValueTree.getPropertyAsValue("mySlider", &undoManager);
        mySlider.getValueObject().referTo(valueTreeObj);
        myValueTree.addListener(this);
        
        startTimer (500);
    }
    
    ~MainComponent()
    {
        saveParams();
        
    }
    void valueTreePropertyChanged (ValueTree&, const Identifier&) override
    {
        repaint();
    }
    void valueTreeChildAdded (ValueTree& parentTree,
                              ValueTree& childWhichHasBeenAdded) override
    {
    }
    

    void valueTreeChildRemoved (ValueTree& parentTree,
                                        ValueTree& childWhichHasBeenRemoved,
                                        int indexFromWhichChildWasRemoved) override
    {
        
    }
   
    virtual void valueTreeChildOrderChanged (ValueTree& parentTreeWhoseChildrenHaveMoved,
                                             int oldIndex, int newIndex) override
    {
        
    }
    
    void valueTreeParentChanged (ValueTree& treeWhoseParentHasChanged) override
    {
        
    }
    

    void initProperties()
    {
        PropertiesFile::Options options;
        options.applicationName = ProjectInfo::projectName;
        options.filenameSuffix = ".settings";
        options.osxLibrarySubFolder = "Application Support";
        options.folderName = File::getSpecialLocation(File::SpecialLocationType::userApplicationDataDirectory).getChildFile("myApp").getFullPathName();
        options.storageFormat = PropertiesFile::storeAsXML;
        
        props.setStorageParameters(options);
    }
    void loadParams()
    {
       
        mySlider.setValue( props.getUserSettings()->getIntValue("mySlider"));


    }
    void saveParams()
    {
        props.getUserSettings()->setValue("mySlider", mySlider.getValue());
    }
    
    void paint (Graphics& g) override
    {
        g.fillAll (findColour (ResizableWindow::backgroundColourId));
    }

    void resized() override
    {
        
        auto r = getLocalBounds();
        
        auto buttons = r.removeFromBottom (20);
        undoButton.setBounds (buttons.removeFromLeft (100));
        redoButton.setBounds (buttons.removeFromLeft (100));
        mySlider.setBounds(10, 10, 100, getHeight()-80);
        
    }

private:
    Slider   mySlider;
        TextButton undoButton, redoButton;
    ApplicationProperties props;
  UndoManager undoManager;
  
    void timerCallback() override
    {
        undoManager.beginNewTransaction();
    }
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
};
1 Like

Another possibility, it seems to me, is to create components that include functionalized properties. But the init function doesn’t seem to me to be Windows and Linux compatible, what do you think about that?

class Xslider  : public Slider //,private ValueTree::Listener, private Timer, public KeyListener
{
public:
    Xslider()
    {
        initProperties();

    }
    ~Xslider()
    {
        if(stringPropertyName.isNotEmpty())
            saveParams();
    }
    void setPropertyName (const String& newName)
    {
        stringPropertyName = newName;
        loadParams();
    }
    void initProperties()
    {
        PropertiesFile::Options options;
        options.applicationName = ProjectInfo::projectName;
        options.filenameSuffix = ".settings";
        options.osxLibrarySubFolder = "Application Support";
        options.folderName = File::getSpecialLocation(File::SpecialLocationType::userApplicationDataDirectory).getChildFile(ProjectInfo::projectName).getFullPathName();
        options.storageFormat = PropertiesFile::storeAsXML;
        
        props.setStorageParameters(options);
    }
    void loadParams()
    {
        
        setValue( props.getUserSettings()->getIntValue(stringPropertyName));
        Logger::writeToLog(getName());
        
    }
    void saveParams()
    {
        props.getUserSettings()->setValue(stringPropertyName, getValue());
             Logger::writeToLog(getName());
    }
    
private:
    //==============================================================================
    // Your private member variables go here...
    String stringPropertyName;
    ApplicationProperties props;
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Xslider)
};

Since the Slider class is already very heavy, I would avoid inheriting from.
My solution would be a little connector class, haven’t tried, but similar to this:

class SliderPropertyAttachment : private Value::Listener
{
public:
    SliderPropertyAttachment (Slider& slider, ApplicationProperties& p, Identifier& pname)
      : properties (p),
        propName (pname)
    {
        value.addListener (this);
        value.referTo (slider.getValueObject());
        ScopedValueSetter setter (updating, true);
        value = properties.getUserSettings()->getIntValue (propName);
    }

    void valueChanged (Value&) override
    {
        if (! updating)
        {
            ScopedValueSetter setter (updating, true);
            properties.getUserSettings()->setValue (propName, value);
        }
    }

private:
    ApplicationProperties& properties;
    Identifier             propName;
    Value                  value;
    bool                   updating = false;
};

You might need to play around with construction order of the ApplicationProperties object…

Ok, Daniel but you say Slider is already heavy and if I understand the code you save the property each time the slider value change ? is it true ?

Note: I was wondering if the addition of properties and valueTree (to have Undo/Redo) were not redundant?

I’m sorry to ask you again, but how I can use my valueTree and undoManager in my children views ? I try this but it doesn’t work for the second view ?

class ChildView : public Component
{
public:
    ChildView()
    {
        addAndMakeVisible(sliderFirst);
        addAndMakeVisible(sliderSecond);
    }
void setUndo (ValueTree& v, UndoManager& um, int intIndexChildren)
{
    sliderFirst.getValueObject().referTo(v.getChild(intIndexChildren).getPropertyAsValue("SliderFirst", &um));
    sliderSecond.getValueObject().referTo(v.getChild(intIndexChildren).getPropertyAsValue("SliderSecond", &um));
}
void paint (Graphics& g) override
    {
    }
void resized() override
    {
        
        sliderFirst.setBoundsRelative(0.01f, 0.01f, 0.48f, 0.98f);
        sliderSecond.setBoundsRelative(0.5f, 0.01f, 0.48f, 0.98f);
        
    }
private:
    Slider  sliderFirst;
    Slider  sliderSecond;
};
class MainComponent   : public Component,private ValueTree::Listener, private Timer, public KeyListener, private ComboBox::Listener
{
public:
    //==============================================================================
    MainComponent()
    {
      setOpaque (true);
    setSize (640, 480);
    ValueTree myValueTree ("myApp");
    addAndMakeVisible(viewNumber1);
    addAndMakeVisible(viewNumber2);
    ValueTree   valueTreeChildView1 ("View1");
    ValueTree   valueTreeChildView2 ("View2");
    myValueTree.addChild(valueTreeChildView1, 1, &undoManager);
    myValueTree.addChild(valueTreeChildView2, 2, &undoManager);
    viewNumber1.setUndo(myValueTree, undoManager, 1);
    viewNumber2.setUndo(myValueTree, undoManager, 2);
    
        
        comboBoxTest.addItem("View 1", 1);
        comboBoxTest.addItem("View 2", 2);
        addAndMakeVisible(comboBoxTest);
        addAndMakeVisible (undoButton);
        addAndMakeVisible (redoButton);
        undoButton.setButtonText("Undo");
        redoButton.setButtonText ("Redo");
        undoButton.onClick = [this] { undoManager.undo(); };
        redoButton.onClick = [this] { undoManager.redo(); };
        Value valueComboTest = myValueTree.getPropertyAsValue("comboTest", &undoManager);
        comboBoxTest.getSelectedIdAsValue().referTo(valueComboTest);
        myValueTree.addListener(this);
        addKeyListener(this);
        comboBoxTest.addListener(this);
        comboBoxTest.setSelectedId(1);
        resized();
        startTimer (500);
    }
    
    ~MainComponent()
    {
        
        
    }
    void comboBoxChanged (ComboBox* comboBoxThatHasChanged) override
    {
        if(comboBoxTest.getSelectedId() == 1)
        {
            viewNumber1.setVisible(true);
            viewNumber2.setVisible(false);
        }
        else
        {
            viewNumber1.setVisible(false);
            viewNumber2.setVisible(true);
        }
    }
    bool keyPressed (const KeyPress& key,
                     Component* originatingComponent) override
    {
        if(key.getKeyCode() == 90)
        {
            if(key.getModifiers() == ModifierKeys::commandModifier)
                undoManager.undo();
            
            if(key.getModifiers() == ModifierKeys::shiftModifier + ModifierKeys::commandModifier)
                undoManager.redo();
        }
        
        return 0;
    }
    
    
    void valueTreePropertyChanged (ValueTree&, const Identifier&) override
    {
        repaint();
    }
    void valueTreeChildAdded (ValueTree& parentTree,
                              ValueTree& childWhichHasBeenAdded) override
    {
    }
    
    
    void valueTreeChildRemoved (ValueTree& parentTree,
                                ValueTree& childWhichHasBeenRemoved,
                                int indexFromWhichChildWasRemoved) override
    {
        
    }
    
    virtual void valueTreeChildOrderChanged (ValueTree& parentTreeWhoseChildrenHaveMoved,
                                             int oldIndex, int newIndex) override
    {
        
    }
    
    void valueTreeParentChanged (ValueTree& treeWhoseParentHasChanged) override
    {
        
    }
    
    
    
    void paint (Graphics& g) override
    {
        g.fillAll (findColour (ResizableWindow::backgroundColourId));
    }
    
    void resized() override
    {
        comboBoxTest.setBoundsRelative(0, 0.02f, 1, 0.08f);
        undoButton.setBoundsRelative(0.02f, 0.88f, 0.4f, 0.1f);
        redoButton.setBoundsRelative(0.58f, 0.88f, 0.4f, 0.1f);
        viewNumber1.setBoundsRelative(0, 0.2f, 1.0f, 0.8f);
        viewNumber2.setBoundsRelative(0, 0.2f, 1.0f, 0.8f);
        
    }
    
private:
  
    ComboBox    comboBoxTest;
    ChildView   viewNumber1;
    ChildView   viewNumber2;
    
    TextButton undoButton, redoButton;
    UndoManager undoManager;

    void timerCallback() override
    {
        undoManager.beginNewTransaction();
    }
    
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
};

I am not sure, if that’s the only problem, but to start with, add DBG statements in your keyPressed callback. You don’t need a keyListener, because every component is already a keyListener. The override is different, that there is no originatingComponent argument (because that is this). But keypress events will only arrive at the component, that has the keyboardFocus. To control the KeyboardFocus better, you need to look into Component::setWantsKeyboardFocus().

A better approach is using the keyboard shortcut infrastructure from ApplicationCommandManager.

The next thing is the timer to call beginNewTransaction: You only want that to happen, if no mouse button is pressed, otherwise a user action is just taking place. You can check the mouse button status with the static function Component::isMouseButtonDownAnywhere().

Hope that gets you a few steps further…

Thanks for your answer Daniel !
You’re right for the KeyListener I like that <3

bool keyPressed (const KeyPress&amp; key) override

For the timer I understand and you are right it’s really better to call it when it’s needed, I’ll implement this.

void timerCallback() override
{
    if (Component::isMouseButtonDownAnywhere())
    undoManager.beginNewTransaction();
}

For the moment, the debugger send me an assert to the setPropertyExcludingListener call ???
And I think my implementation of treeView and undoManager in my childView class is not the good way to do, but I don’t know how making a better code ?

So I’m trying to move forward, if I understand correctly my ValueTree represents the M and C of the MVC and the property backup I was adding to my Slider has no place its value must come from the ValueTree. Saving a property as I did applies only to the view, for example the size of the window…