Reproducing the UI of MOD-D3 with JUCE (2) : NumberBox

Introduction

In my last article I explained the implementation of the RangeFader part of MOD-D3. This time, I will explain the implementation of the NumberBox part.

The final appearance and functionality to be created in this article will be as follows.

  • Corresponding to the maximum and minimum values of RangeFader.
  • Dragging up and down changes the value.
  • The unique lock-on mark when active.
  • Calling TextEditor in response to keystrokes

Prerequisites

Creating the basic NumberBox

Adding New CPP & Header File

First, create a CPP and a Header file for the NumberBox to avoid cluttering up the PluginEditor.h file. We can easily create it with Projucer by following the steps below.

Customizing the Slider Class

At first I used the Label class to implement the NumberBox, but it was much easier to use the Slider class. Here’s how to do it:

class NumberBox  : public juce::Slider
{
public:
    NumberBox();
    ~NumberBox() override;

    void paint (juce::Graphics&) override;
    
    bool getLockedOnState ();
    void setLockedOnState (bool state);

    void mouseDown (const juce::MouseEvent& event) override;
    void mouseDrag (const juce::MouseEvent& event) override;
    void mouseUp (const juce::MouseEvent& event) override;

private:
    bool isLockedOn = false;
    juce::Colour grey = juce::Colour::fromFloatRGBA (0.42, 0.42, 0.42, 1.0);
    
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NumberBox)
};
/*
  ==============================================================================

    NumberBox.cpp
    Created: 8 May 2021 3:05:17pm
    Author:  Kengo Suzuki

  ==============================================================================
*/

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

//==============================================================================
NumberBox::NumberBox()
{
    setSliderStyle (juce::Slider::LinearBarVertical);
    setColour (juce::Slider::textBoxTextColourId, grey);
    setColour (juce::Slider::textBoxOutlineColourId, juce::Colours::transparentWhite);
    setColour (juce::Slider::trackColourId, juce::Colours::transparentWhite);
    setTextBoxIsEditable (true);
    setVelocityBasedMode (true);
    setVelocityModeParameters (0.5, 1, 0.09, false);
    setRange (0.0, 100.0, 0.5);
    setTextValueSuffix (" %");
}

NumberBox::~NumberBox()
{
}

void NumberBox::paint (juce::Graphics& g)
{
    if (isLockedOn)
    {
        //            RangeFader's NumberBox : Cube's NumberBox
        auto length = getHeight() > 15 ? 4   : 3;
        auto thick  = getHeight() > 15 ? 3.0 : 2.5;
        
        g.setColour (findColour (juce::Slider::textBoxTextColourId));
        //          fromX       fromY        toX                  toY
        g.drawLine (0,          0,           0,                   length,               thick);
        g.drawLine (0,          0,           length,              0,                    thick);
        g.drawLine (0,          getHeight(), 0,                   getHeight() - length, thick);
        g.drawLine (0,          getHeight(), length,              getHeight(),          thick);
        g.drawLine (getWidth(), getHeight(), getWidth() - length, getHeight(),          thick);
        g.drawLine (getWidth(), getHeight(), getWidth(),          getHeight() - length, thick);
        g.drawLine (getWidth(), 0,           getWidth() - length, 0,                    thick);
        g.drawLine (getWidth(), 0,           getWidth(),          length,               thick);
    }
}

bool NumberBox::getLockedOnState ()
{
    return isLockedOn;
}

void NumberBox::setLockedOnState (bool state)
{
    isLockedOn = state;
}

void NumberBox::mouseDown (const juce::MouseEvent& event)
{
    juce::Slider::mouseDown (event);
    setTextBoxIsEditable(false);
    isLockedOn = true;
    repaint();
}
    
void NumberBox::mouseDrag (const juce::MouseEvent& event)
{
    juce::Slider::mouseDrag (event);
    isLockedOn = true;
    event.source.enableUnboundedMouseMovement (true);
}
    
void NumberBox::mouseUp (const juce::MouseEvent& event)
{
    juce::Slider::mouseUp (event);
    juce::Desktop::getInstance().getMainMouseSource().setScreenPosition (event.source.getLastMouseDownPosition());
}

When the NumberBox is clicked or dragged, the LockedOn flag is set to true, and the lock-on mark is displayed accordingly. Also, We set false to setTextBoxIsEditable() in mouseDown() to prevent it from being called when clicked, since we want to call TextEditror when keyed in.

Setting the properties of the NumberBox

Once this is done, declare a NumberBox object with the number corresponding to the XYZ RangeFader as follows. Also, don’t forget to include the NumberBox.h file.

#pragma once

#include <JuceHeader.h>
#include "PluginProcessor.h"
#include "NumberBox.h"
・・・
class ModD3TutorialAudioProcessorEditor  : public juce::AudioProcessorEditor
{
・・・
private:
    RangeFader xRangeFader;
    RangeFader yRangeFader;
    RangeFader zRangeFader;
    
    NumberBox xMinNumberBox;
    NumberBox xMaxNumberBox;
    Numberbox yMinNumberBox;
    NumberBox yMaxNumberBox;
    Numberbox zMinNumberBox;
    NumberBox zMaxNumberBox;
    
    CustomLAF customLAF;

    ModD3TutorialAudioProcessor& audioProcessor;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ModD3TutorialAudioProcessorEditor)
};

Then, call the following member function:

ModD3TutorialAudioProcessorEditor::ModD3TutorialAudioProcessorEditor (ModD3TutorialAudioProcessor& p)
    : AudioProcessorEditor (&p), audioProcessor (p)
{
・・・
    addAndMakeVisible (&xMinNumberBox);
    xMinNumberBox.getValueObject().referTo (xRangeFader.getMinValueObject());
    xMinNumberBox.setDoubleClickReturnValue (true, 0);
    
    addAndMakeVisible (&xMaxNumberBox);
    xMaxNumberBox.getValueObject().referTo (xRangeFader.getMaxValueObject());
    xMaxNumberBox.setDoubleClickReturnValue (true, 100);
    
    addAndMakeVisible (&yMinNumberBox);
    yMinNumberBox.getValueObject().referTo (yRangeFader.getMinValueObject());
    yMinNumberBox.setDoubleClickReturnValue (true, 0);
    
    addAndMakeVisible (&yMaxNumberBox);
    yMaxNumberBox.getValueObject().referTo (yRangeFader.getMaxValueObject());
    yMaxNumberBox.setDoubleClickReturnValue (true, 100);
    
    addAndMakeVisible (&zMinNumberBox);
    zMinNumberBox.getValueObject().referTo (zRangeFader.getMinValueObject());
    zMinNumberBox.setDoubleClickReturnValue (true, 0);
    
    addAndMakeVisible (&zMaxNumberBox);
    zMaxNumberBox.getValueObject().referTo (zRangeFader.getMaxValueObject());
    zMaxNumberBox.setDoubleClickReturnValue (true, 100);
}

We used referTo() so that each NumberBox value corresponds to a RangeFaders. Also, We called setDoubleClickReturnValue() to reset the value to the initial value when double-clicked.

Then, set the placement and size of the NumberBox as follows:

void ModD3TutorialAudioProcessorEditor::resized()
{
    xRangeFader.setBounds (5, getHeight() / 2 - 90, getWidth() - 10, 30);
    yRangeFader.setBounds (5, getHeight() / 2,      getWidth() - 10, 30);
    zRangeFader.setBounds (5, getHeight() / 2 + 90, getWidth() - 10, 30);
    
    xMinNumberBox.setBounds (xRangeFader.getX() + 30,  xRangeFader.getY() + 40, 55, 15);
    xMaxNumberBox.setBounds (xRangeFader.getX() + 110, xRangeFader.getY() + 40, 55, 15);
    yMinNumberBox.setBounds (yRangeFader.getX() + 30,  yRangeFader.getY() + 40, 55, 15);
    yMaxNumberBox.setBounds (yRangeFader.getX() + 110, yRangeFader.getY() + 40, 55, 15);
    zMinNumberBox.setBounds (zRangeFader.getX() + 30,  zRangeFader.getY() + 40, 55, 15);
    zMaxNumberBox.setBounds (zRangeFader.getX() + 110, zRangeFader.getY() + 40, 55, 15);
}

Let’s try to build it at this point. It should look like this.

Making the lock-on mark unique.

As you can see, multiple lock-on marks coexist at the moment, so we’ll make them unique.

To do so, we need to override mouseDown():

class ModD3TutorialAudioProcessorEditor  : public juce::AudioProcessorEditor
{
public:
    ModD3TutorialAudioProcessorEditor (ModD3TutorialAudioProcessor&);
    ~ModD3TutorialAudioProcessorEditor() override;

    //==============================================================================
    void paint (juce::Graphics&) override;
    void resized() override;
    
    void mouseDown (const juce::MouseEvent& event) override;
void ModD3AudioProcessorEditor::mouseDown(const juce::MouseEvent& event)
{
    if (xMinNumberBox.getLockedOnState() || xMaxNumberBox.getLockedOnState()
        || yMinNumberBox.getLockedOnState() || yMaxNumberBox.getLockedOnState()
        || zMinNumberBox.getLockedOnState() || zMaxNumberBox.getLockedOnState())
    {
        if (xMinNumberBox.getLockedOnState())
        {
            xMinNumberBox.setLockedOnState(false);
            xMinNumberBox.repaint();
        }
        else if (xMaxNumberBox.getLockedOnState())
        {
            xMaxNumberBox.setLockedOnState(false);
            xMaxNumberBox.repaint();
        }
        else if (yMinNumberBox.getLockedOnState())
        {
            yMinNumberBox.setLockedOnState(false);
            yMinNumberBox.repaint();
        }
        else if (yMaxNumberBox.getLockedOnState())
        {
            yMaxNumberBox.setLockedOnState(false);
            yMaxNumberBox.repaint();
        }
        else if (zMinNumberBox.getLockedOnState())
        {
            zMinNumberBox.setLockedOnState(false);
            zMinNumberBox.repaint();
        }
        else if (zMaxNumberBox.getLockedOnState())
        {
            zMaxNumberBox.setLockedOnState(false);
            zMaxNumberBox.repaint();
        }
    }
}

Next, call addMouseListener() to the PluginEditor in the constructor below to register the listener. Note that if you don’t do this, the lock-on mark will remain on the component when you click from one component to the next, for example.

ModD3TutorialAudioProcessorEditor::ModD3TutorialAudioProcessorEditor (ModD3TutorialAudioProcessor& p)
    : AudioProcessorEditor (&p), audioProcessor (p)
{
    setSize (200, 300);
    addMouseListener (this, true);
・・・

So let’s build it and see what happens.

As you can see, the lock-on mark can now be kept unique.

Response to keystrokes

Of course, at the moment, the NumberBox does not change to a TextEditor when keyed in. I will explain the steps to make it possible in this chapter.

Inheriting the KeyListener class

If you want to trigger some event by key input, you need to inherit the KeyListener class as follows:

class ModD3TutorialAudioProcessorEditor  : public juce::AudioProcessorEditor, public juce::KeyListener
{
public:
    ModD3TutorialAudioProcessorEditor (ModD3TutorialAudioProcessor&);
    ~ModD3TutorialAudioProcessorEditor() override;

    //==============================================================================
    void paint (juce::Graphics&) override;
    void resized() override;
    
    void mouseDown (const juce::MouseEvent& event) override;
    
    bool keyPressed (const juce::KeyPress& key, Component* c) override;
・・・

Then, call addKeyListener() in the constructor in the PluginEditor:

ModD3TutorialAudioProcessorEditor::ModD3TutorialAudioProcessorEditor (ModD3TutorialAudioProcessor& p)
    : AudioProcessorEditor (&p), audioProcessor (p)
{
    setSize (200, 300);
    addMouseListener(this, true);
    addKeyListener(this);
・・・

Overriding keyPressed()

The implementation of keyPress() to override is as follows(I’m only going to describe the X RangeFaders part here because it’s very long):

bool ModD3TutorialAudioProcessorEditor::keyPressed (const juce::KeyPress& key, Component* c)
{
    if (xMinNumberBox.getClickState())
    {
        xMinNumberBox.setTextBoxIsEditable (true);
        xMinNumberBox.showTextBox();
        xMinNumberBox.setTextBoxIsEditable (false);
        return true;
    }

    if (xMaxNumberBox.getClickState())
    {
        xMaxNumberBox.setTextBoxIsEditable (true);
        xMaxNumberBox.showTextBox();
        xMaxNumberBox.setTextBoxIsEditable (false);
        return true;
    }
・・・
    return false;
}

If a key is entered and a NumberBox with a lock-on mark exists, the TextEditor will be displayed. Also, by setting setTextBoxIsEditable() to false as soon as it is displayed, the TextEditor will not be displayed even if the user clicks on it immediately after finishing keystrokes (pressing the return key).

Now let’s build and see.

Customizing look-and-feel

As you can see, the TextEditor does not look cool, so we will improve the LookAndFeel class that we customized in the last article. Sorry, the class name is a little different, and it is divided into cpp and header files, although I didn’t do it in the last article.

Before
After

The main changes are as follows:

  • No outer border line.
  • No change in text color or background color.
  • No misalignment of text.
  • No cursor.

Creating EditableLabel

You can fine-tune the Label by setting the return value of createSliderTextBox() to the pointer to the customized Label object instead of the regular Label object.

So we will define a customized Label class called EditableLabel, and override createEditorComponent() to configure the details of the TextEditor as shown below:

class CustomLookAndFeel  : public juce::LookAndFeel_V4
{
public:
    struct EditableLabel : public juce::Label
    {
        EditableLabel() {};
        ~EditableLabel() {};
        
        juce::TextEditor* createEditorComponent() override
        {
            auto* ed = juce::Label::createEditorComponent();
            ed->setJustification (juce::Justification::centred);
            ed->setColour (juce::TextEditor::backgroundColourId, juce::Colour::fromFloatRGBA (0.078, 0.078, 0.078, 1.0));
            ed->setMouseCursor (juce::MouseCursor::NoCursor);
            ed->setInputRestrictions (5, "0123456789.");
            ed->setIndents (4, -1);
            return ed;
        }
        
        void editorShown (juce::TextEditor* editor) override
        {
            editor->clear();
        }
    };
    
    CustomLookAndFeel();
    ~CustomLookAndFeel() override;
    
    void drawLinearSlider (juce::Graphics& g, int x, int y, int width, int height,
                           float sliderPos,
                           float minSliderPos,
                           float maxSliderPos,
                           const juce::Slider::SliderStyle style, juce::Slider& slider) override;
    
    EditableLabel* createSliderTextBox (juce::Slider& slider) override;

private:

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CustomLookAndFeel)
};

Overriding createSliderTextBox()

Then override it so that createSliderTextBox() returns a pointer to the customized EditableLabel object as shown below:

CustomLookAndFeel::EditableLabel* CustomLookAndFeel::createSliderTextBox (juce::Slider& slider)
{
    auto* l = new EditableLabel();
    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, slider.findColour (juce::Slider::textBoxOutlineColourId));

    // For Cube's NumberBox. Not relevant this time.
    if (slider.getHeight() > 15)
        l->setFont (18);

    return l;
}

Settting CustomLookAndFeel

Now that we have completed the customization of the LookAndFeel class, let’s apply it to the NumberBox.

Declare an object of the CustomLookAndFeel class and call setLookAndFeel() in the constructor of the NumberBox.

class NumberBox  : public juce::Slider
{
・・・
private:
    CustomLookAndFeel customLookAndFeel;

・・・
};
NumberBox::NumberBox()
{
    setSliderStyle (juce::Slider::LinearBarVertical);
    setLookAndFeel (&customLookAndFeel);
    setColour (juce::Slider::textBoxTextColourId, grey);
    setColour (juce::Slider::textBoxOutlineColourId, juce::Colours::transparentWhite);
    setColour (juce::Slider::trackColourId, juce::Colours::transparentWhite);
    setTextBoxIsEditable (false);
    setVelocityBasedMode (true);
    setVelocityModeParameters (0.5, 1, 0.09, false);
    setRange (0, 100, 0.5);
    setTextValueSuffix (" %");
}

Now let’s build and see.

Customizing CaretComponent

Finally, we will change the color of the caret from blue to red.

Before
After

Overriding createCaretComponent()

To do this is simple, just override createCaretComponent() to return a pointer to the caret object that has been changed to red.

class CustomLookAndFeel : public juce::LookAndFeel_V4
{
public:
・・・
    juce::CaretComponent* createCaretComponent (juce::Component* keyFocusOwner) override;
juce::CaretComponent* CustomLookAndFeel::createCaretComponent (juce::Component* keyFocusOwner)
{
    auto caret = new juce::CaretComponent (keyFocusOwner);
    caret->setColour (juce::CaretComponent::caretColourId, juce::Colours::red);
    return caret;
}

Let’s build it and see. This is the goal of this article.

Conclusion

In this article, I explained how to implement the multifunctional NumberBox. There are some really small details that I don’t like, so I’m going to try to improve those and evolve it into a cooler NumberBox.

In the next article, we will explain how to implement XYZ MapButtons. These allow you to switch the RangeFader to a ThreeValue.

Finally, it is really difficult for me to write articles in English. I may have confused the readers with my awkward expressions. So I will continue to study English, and of course, JUCE too. Thank you for reading to the end.

Comments

Copied title and URL