NumberBox – Edit Mode –

Introduction

In this article, I will explain how to implement the insert mode in NumberBox. As shown below, click on the NumberBox and press any of the number keys to change to insert mode. By entering any number while in this mode, you can quickly set a more precise value than by dragging. After entering the value, you can exit this mode by pressing the return key.

Since I will focus on explaining the insert mode implementation, I will not explain the basic NumberBox implementation.

GitHub - szkkng/NumberBox: This is a number box made with JUCE.
This is a number box made with JUCE. Contribute to szkkng/NumberBox development by creating an account on GitHub.

Prerequisites

Let’s prepare a basic NumberBox in advance in this section.

First, create a new project named NumberBox, and create this header and cpp file.

The basic implementation of NumberBox is as follows:

#pragma once

#include <JuceHeader.h>

//==============================================================================
class CustomLookAndFeel : public juce::LookAndFeel_V4
{
public:
    CustomLookAndFeel();
    
    juce::Label* createSliderTextBox (juce::Slider& slider) override;
};

//==============================================================================
class NumberBox  : public juce::Slider
{
public:
    NumberBox();
    
    void paint (juce::Graphics& g) override;
    void mouseDown (const juce::MouseEvent& event) override;
    void mouseUp (const juce::MouseEvent& event) override;
    
private:
    CustomLookAndFeel customLookAndFeel;
    
    juce::Colour grey = juce::Colour::fromFloatRGBA (0.42f, 0.42f, 0.42f, 1.f);
    
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NumberBox)
};
#include "NumberBox.h"

//==============================================================================
CustomLookAndFeel::CustomLookAndFeel(){}

juce::Label* CustomLookAndFeel::createSliderTextBox (juce::Slider& slider)
{
    auto* l = new juce::Label();
    
    l->setJustificationType (juce::Justification::centred);
    l->setColour (juce::Label::textColourId, slider.findColour(juce::Slider::textBoxTextColourId));
    l->setColour (juce::Label::textWhenEditingColourId, slider.findColour(juce::Slider::textBoxTextColourId));
    l->setColour (juce::Label::outlineWhenEditingColourId, juce::Colours::transparentWhite);
    l->setInterceptsMouseClicks (false, false);
    l->setFont (18);
                
    return l;
}

//==============================================================================
NumberBox::NumberBox()
{
    setSliderStyle (juce::Slider::LinearBarVertical);
    setColour (juce::Slider::trackColourId, juce::Colours::transparentWhite);
    setLookAndFeel (&customLookAndFeel);
    setTextBoxIsEditable (false);
    setVelocityBasedMode (true);
    setVelocityModeParameters (0.5, 1, 0.09, false);
    setRange (0, 100, 0.01);
    setValue (50.0);
    setDoubleClickReturnValue (true, 50.0);
    setTextValueSuffix (" %");
    setWantsKeyboardFocus (true);
    onValueChange = [&]()
    {
        if (getValue() < 10)
            setNumDecimalPlacesToDisplay(2);
        else if (10 <= getValue() && getValue() < 100)
            setNumDecimalPlacesToDisplay(1);
        else
            setNumDecimalPlacesToDisplay(0);
    };
}

void NumberBox::paint (juce::Graphics& g)
{
    if (hasKeyboardFocus (false))
    {
        auto bounds = getLocalBounds().toFloat();
        auto h = bounds.getHeight();
        auto w = bounds.getWidth();
        auto len = juce::jmin (h, w) * 0.15f;
        auto thick  = len / 1.8f;
        
        g.setColour (findColour (juce::Slider::textBoxOutlineColourId));
        
        // Left top
        g.drawLine (0.0f, 0.0f, 0.0f, len, thick);
        g.drawLine (0.0f, 0.0f, len, 0.0f, thick);
        
        // Left bottom
        g.drawLine (0.0f, h, 0.0f, h - len, thick);
        g.drawLine (0.0f, h, len, h, thick);
        
        // Right top
        g.drawLine (w, 0.0f, w, len, thick);
        g.drawLine (w, 0.0f, w - len, 0.0f, thick);
        
        // Right bottom
        g.drawLine (w, h, w, h - len, thick);
        g.drawLine (w, h, w - len, h, thick);
    }
}

void NumberBox::mouseDown (const juce::MouseEvent& event)
{
    juce::Slider::mouseDown (event);

    setMouseCursor (juce::MouseCursor::NoCursor);
}

void NumberBox::mouseUp (const juce::MouseEvent& event)
{
    juce::Slider::mouseUp (event);

    juce::Desktop::getInstance().getMainMouseSource().setScreenPosition(event.source.getLastMouseDownPosition());

    setMouseCursor (juce::MouseCursor::NormalCursor);
}

Then, include this header file and create this object.

#pragma once

#include <JuceHeader.h>
#include "NumberBox.h"

//==============================================================================
class MainComponent  : public juce::Component
{
public:
    //==============================================================================
    MainComponent();
    ~MainComponent() override;

    //==============================================================================
    void paint (juce::Graphics&) override;
    void resized() override;

private:
    //==============================================================================
    NumberBox blueBox, greenBox, yellowBox;
    
    juce::Colour blue   = juce::Colour::fromFloatRGBA (0.43f, 0.83f, 1.0f,  1.0f);
    juce::Colour green  = juce::Colour::fromFloatRGBA (0.34f, 0.74f, 0.66f, 1.0f);
    juce::Colour yellow = juce::Colour::fromFloatRGBA (1.0f,  0.71f, 0.2f,  1.0f);
    juce::Colour black  = juce::Colour::fromFloatRGBA (0.08f, 0.08f, 0.08f, 1.0f);

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
};

Finally, call various member functions to adjust the appearance of the editor.

#include "MainComponent.h"

//==============================================================================
MainComponent::MainComponent()
{
    setSize (500, 300);
    setWantsKeyboardFocus (true);
    
    blueBox.setColour (juce::Slider::textBoxTextColourId, blue);
    blueBox.setColour (juce::Slider::textBoxOutlineColourId, blue);
    
    greenBox.setColour (juce::Slider::textBoxTextColourId, green);
    greenBox.setColour (juce::Slider::textBoxOutlineColourId, green);
    
    yellowBox.setColour (juce::Slider::textBoxTextColourId, yellow);
    yellowBox.setColour (juce::Slider::textBoxOutlineColourId, yellow);
    
    addAndMakeVisible (blueBox);
    addAndMakeVisible (greenBox);
    addAndMakeVisible (yellowBox);
}

MainComponent::~MainComponent()
{
}

//==============================================================================
void MainComponent::paint (juce::Graphics& g)
{
    g.fillAll (black);
}

void MainComponent::resized()
{
    auto bounds = getLocalBounds().withSizeKeepingCentre (80, 30);
    
    blueBox.setBounds (bounds.withX (50));
    greenBox.setBounds (bounds.withX (205));
    yellowBox.setBounds (bounds.withX (360));
}

Now let’s build it. It should look like the following.

That’s all the preparation you need to do! Let’s get started!

Customizing the label class

The createSliderTextBox() overridden by the CustomLookAndFeel class returns a juce::Label object. So, you can override it to return an object of CustomLabel class, which is customized by inheriting from juce::Label class, for more detailed settings. In this chapter, I will explain how to customize this class.

The declaration part of the CustomLabel class looks like the following:

class CustomLabel : public juce::Label
{
public:
    static juce::String initialPressedKey;

    juce::TextEditor* createEditorComponent() override;
    void editorShown (juce::TextEditor* editor) override;
};

Overriding createEditorComponent()

By overriding createEditorComponent(), you can configure the details of the TextEditor that will be called when editing a label.

juce::TextEditor* CustomLabel::createEditorComponent()
{
    auto* ed = juce::Label::createEditorComponent();

    ed->setJustification (juce::Justification::centred);
    ed->setColour (juce::TextEditor::backgroundColourId, juce::Colours::transparentWhite);
    ed->setInputRestrictions (5, "0123456789.");
    ed->setIndents (4, -1);

    return ed;
}

When setInputRestrictions() is called with arguments as shown above, it accepts input up to 5 characters and limits the characters allowed to be input to numbers from 0 to 9 and a decimal point.

Then, the reason why I’m passing this argument to setIndents() is that the position of the text will be shifted quite a bit when TextEditor is called. These were the appropriate values that would not change the position before and after the call.

Overriding editorShown()

editorShown() is a member function that is called when a TextEditor object appears. By default, the current value is already entered when it appears, but this function erases that value and sets the key that the user first entered to edit.

void CustomLabel::editorShown (juce::TextEditor* editor)
{
    editor->clear();
    editor->setText (initialPressedKey);
}

Initializing a static member variable

The reason why we prepared this static member variable is that the object of the NumberBox class and the CustomLabel class need to share the value that the user initially entered.

juce::String CustomLabel::initialPressedKey = "";

Updating createSliderTextBox()

Now let’s change the return value of createSliderTextBox() from juce::Label to CustomLabel:

class CustomLookAndFeel : public juce::LookAndFeel_V4
{
public:
    CustomLookAndFeel();
   
    CustomLabel* createSliderTextBox (juce::Slider& slider) override;
};
CustomLabel* CustomLookAndFeel::createSliderTextBox (juce::Slider& slider)
{
    auto* l = new CustomLabel();
    
    l->setJustificationType (juce::Justification::centred);
    l->setColour (juce::Label::textColourId, slider.findColour(juce::Slider::textBoxTextColourId));
    l->setColour (juce::Label::textWhenEditingColourId, slider.findColour(juce::Slider::textBoxTextColourId));
    l->setColour (juce::Label::outlineWhenEditingColourId, juce::Colours::transparentWhite);
    l->setInterceptsMouseClicks (false, false);
    l->setFont (18);
                
    return l;
}

Overriding keyPressed()

In order to accept key input from the users, we need to inherit from the KeyListener class and override keyPressed().

class NumberBox  : public juce::Slider, public juce::KeyListener
{
public:
    NumberBox();    
    
    void paint (juce::Graphics& g) override;
    void mouseDown (const juce::MouseEvent& event) override;
    void mouseUp (const juce::MouseEvent& event) override;
    bool keyPressed (const juce::KeyPress& k, juce::Component* c) override;
・・・

Don’t forget to call addKeyListener() in the constructor of NumberBox.

NumberBox::NumberBox()
{
・・・
    addKeyListener (this);  
}

The implementation of keyPressed() is as follows:

bool NumberBox::keyPressed (const juce::KeyPress& k, juce::Component* c)
{
    char numChars[] = "0123456789";

    for (auto numChar : numChars)
    {
        if (k.getTextCharacter() == numChar)
        {
            setTextBoxIsEditable (true);
            CustomLabel::initialPressedKey = juce::String::charToString (k.getTextCharacter());
            showTextBox();
            setTextBoxIsEditable (false);

            return true;
        }
    }

    return false;
}

Since we want the first value entered by the user to be only a number, we will prepare an array with char type elements. If the value entered by the user is one of those numbers, the value will be stored in a static member variable, and a text box with the value already entered will appear.

Now let’s build it and see if it works! You’ll have a great user experience!

Customizing the caret class

It’s cool enough as it is, but it would be even cooler if the color of the caret could be changed to match the color of the NumberBox. You can change the color of the caret by calling setColour(), which is very easy:

class CustomLookAndFeel : public juce::LookAndFeel_V4
{
public:
    CustomLookAndFeel();
    
    juce::CaretComponent* createCaretComponent (juce::Component* keyFocusOwner) override;
    CustomLabel* createSliderTextBox (juce::Slider& slider) override;
};
juce::CaretComponent* CustomLookAndFeel::createCaretComponent (juce::Component* keyFocusOwner)
{
    auto caret = new juce::CaretComponent (keyFocusOwner);

    caret->setColour(juce::CaretComponent::caretColourId, keyFocusOwner->findColour (juce::Label::textColourId));

    return caret;
}

That is all the implementation completed. Let’s build it!

Conclusion

In this article, I have explained how to implement the insert mode of NumberBox. The main operation of NumberBox is to change the value by dragging, but it will be more convenient if you can directly input and adjust the value in this way.

Finally, I would like to point out that there may be some inefficiencies in my implementation of this method. If you have a better way, please let me know in the comments or DM. Thank you for reading to the end. Happy coding!

References

Comments

Copied title and URL