text:
/*
  ==============================================================================

    ColourLibraryComponent.cpp
    Created: 18 Sep 2020 9:56:16pm
    Author:  Vincenzo

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

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

//==============================================================================
ColourLibraryComponent::ColourLibraryComponent(StringArray rowNamesIn, Array<Array<Colour>*> rowColoursIn, bool showLabelsIn, bool onlyOpaqueIn)
    : rowColours(rowColoursIn), showLabels(showLabelsIn), onlyOpaque(onlyOpaqueIn)
{
    for (auto rowName : rowNamesIn)
    {
        Label* label = new Label(rowName + "_Label", rowName);
        rowLabels.add(label);
        addAndMakeVisible(label);
    }

    if (rowColoursIn.size() > 0)
        setupFlex();

    setColour(ColourLibraryComponent::ColourIds::backgroundColourId, Colour());
    setColour(ColourLibraryComponent::ColourIds::outLineColourId, Colour());
    setColour(ColourLibraryComponent::ColourIds::rowBackgroundColourId, Colour());
    setColour(ColourLibraryComponent::ColourIds::rowOutlineColourId, Colour());
}

ColourLibraryComponent::~ColourLibraryComponent()
{
}

void ColourLibraryComponent::paint (juce::Graphics& g)
{
    g.fillAll(findColour(ColourLibraryComponent::ColourIds::backgroundColourId));

    g.setColour(findColour(ColourLibraryComponent::ColourIds::outLineColourId));
    g.drawRect(getLocalBounds());

    for (int i = 0; i < flexBox.items.size() / 2; i++)
    {
        Component* label = flexBox.items.getReference(i * 2).associatedComponent;
        FlexItem& item = flexBox.items.getReference(i * 2 + 1);
        Rectangle<int> rowRect = { label->getX(), label->getY(), jmax(label->getWidth(), (int)item.width), label->getHeight() + (int)item.height };

        g.setColour(findColour(ColourLibraryComponent::ColourIds::rowBackgroundColourId));
        g.fillRect(rowRect);

        g.setColour(findColour(ColourLibraryComponent::ColourIds::rowOutlineColourId));
        g.drawRect(rowRect);
    }
}

void ColourLibraryComponent::resized()
{
    for (int i = 0; i < flexBox.items.size(); i++)
    {
        int maxWidth = getWidth();

        if (maxSwatchColumns > 0)
            maxWidth = 25 * maxSwatchColumns + 8; // default width plus margins

        flexBox.items.getReference(i).width = maxWidth;
        flexBox.items.getReference(i).height = 24;
    }

    flexBox.performLayout(getLocalBounds());
}

void ColourLibraryComponent::showSwatchLabels(bool showLabelsIn)
{
    showLabels = showLabelsIn;
}

void ColourLibraryComponent::onlyShowOpaque(bool onlyOpaqueIn)
{
    if ((int)onlyOpaque + (int)onlyOpaqueIn == 1)
    {
        onlyOpaque = onlyOpaqueIn;
        setupFlex();
        return;
    }

    onlyOpaque = onlyOpaqueIn;
}

void ColourLibraryComponent::setNewRows(Array<Array<Colour>*> arraysToControl)
{
    rowColours = arraysToControl;
    refreshSwatches();
}

void ColourLibraryComponent::refreshSwatches()
{
    swatches.clear();
    for (int r = 0; r < rowBoxes.size(); r++)
    {
        FlexBox& rowBox = rowBoxes.getReference(r);
        Array<Colour>* colorRow = rowColours.getReference(r);

        rowBox.items.clear();

        for (int i = 0; i < colorRow->size(); i++)
        {
            Colour& c = colorRow->getReference(i);

            if (onlyOpaque && c.getAlpha() < 0xff)
                continue;

            PaintSwatch* swatch = new PaintSwatch(*colorRow, i, String(i + 1), showLabels);
            addAndMakeVisible(swatch);
            swatches.add(swatch);

            FlexItem swatchItem(20, 20, *swatch);
            swatchItem.margin = FlexItem::Margin(2);
            rowBox.items.add(swatchItem);
        }
    }

    resized();
}

void ColourLibraryComponent::forceSwatchColumns(int maxNumColumns)
{
    maxSwatchColumns = maxNumColumns;
}

void ColourLibraryComponent::setupFlex()
{
    flexBox = FlexBox(
        FlexBox::Direction::column,
        FlexBox::Wrap::noWrap,
        FlexBox::AlignContent::flexStart,
        FlexBox::AlignItems::stretch,
        FlexBox::JustifyContent::flexStart
    );

    rowBoxes.clear();
    for (int r = 0; r < rowLabels.size(); r++)
    {
        rowBoxes.add(FlexBox(
            FlexBox::Direction::row,
            FlexBox::Wrap::wrap,
            FlexBox::AlignContent::spaceAround,
            FlexBox::AlignItems::stretch,
            FlexBox::JustifyContent::flexStart
        ));

        Label* row = rowLabels[r];

        FlexItem labelAsItem;
        labelAsItem.associatedComponent = row;
        labelAsItem.minHeight = 24;
        labelAsItem.minWidth = Font().getStringWidth(row->getText());
        flexBox.items.add(labelAsItem);

        FlexItem boxAsItem;
        boxAsItem.associatedFlexBox = &rowBoxes.getReference(r);
        boxAsItem.alignSelf = FlexItem::AlignSelf::stretch;
        boxAsItem.margin = FlexItem::Margin(2, 0, 2, 8);
        flexBox.items.add(boxAsItem);
    }

    refreshSwatches();
}


text:
/*
  ==============================================================================

    NoteMapEditor.cpp
    Created: 18 Oct 2020 5:13:12pm
    Author:  Vincenzo

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

#include "NoteMapEditor.h"

NoteMapEditor::NoteMapEditor(NoteMap noteMapIn)
{
    header = &table.getHeader();
    header->addColumn("Note In", ColumnType::MidiNoteIn,font.getStringWidth("128"), font.getStringWidth("Note In"));
    header->addColumn("Key", ColumnType::KeyNumber, font.getStringWidth("128"), font.getStringWidth("Key"));
    header->addColumn("Note Out", ColumnType::MidiNoteOut, font.getStringWidth("128"), font.getStringWidth("Note Out"));
    header->addColumn("+/-", ColumnType::ToggleMapping, font.getStringWidth("+/-"), font.getStringWidth("+/-"));
    header->setStretchToFitActive(true);

    table.setColour(ListBox::outlineColourId, Colours::darkgrey);
    table.setOutlineThickness(1);

    addAndMakeVisible(table);

    resetMapping(noteMapIn, false);
}

NoteMapEditor::~NoteMapEditor()
{

}

void NoteMapEditor::resized()
{
    table.setBoundsInset(BorderSize<int>(1));
    header->resizeAllColumnsToFit(table.getWidth() - table.getVerticalScrollBar().getWidth());
}

//=============================================================================

int NoteMapEditor::getNumRows()
{
    return currentNoteMapNode.getNumChildren() + 1;
}

void NoteMapEditor::paintRowBackground(Graphics& g, int rowNumber, int w, int h, bool rowIsSelected)
{
    g.setColour(rowNumber % 2 == 0 ? Colours::grey : Colours::lightgrey);
    g.fillAll();

    if (rowNumber == currentNoteMapNode.getNumChildren())
    {
        g.setColour(Colours::black);
        g.drawText(newMappingTrans, 0, 0, w, h, Justification::left);
    }
}

void NoteMapEditor::paintCell(Graphics& g, int rowNumber, int columnId, int w, int h, bool isSelected)
{
}

Component* NoteMapEditor::refreshComponentForCell(int rowNumber, int columnId, bool isRowSelected, Component* existingComponentToUpdate)
{
    if (columnId < ColumnType::ToggleMapping)
    {
        if (rowNumber < currentNoteMapNode.getNumChildren())
        {
            ValueTree node = currentNoteMapNode.getChild(rowNumber);
            var value;

            switch (columnId)
            {
            case ColumnType::MidiNoteIn:

                if (node.hasProperty(IDs::mappingMidiNoteIn))
                    value = node[IDs::mappingMidiNoteIn];
                else
                    value = noneTrans;

                break;

            case ColumnType::KeyNumber:

                if (node.hasProperty(IDs::pianoKeyNumber))
                    value = node[IDs::pianoKeyNumber];
                else
                    value = noneTrans;

                break;

            case ColumnType::MidiNoteOut:

                // TODO: note out mapping implementation
                if (node.hasProperty(IDs::pianoKeyNumber))
                    value = node[IDs::pianoKeyNumber];
                else
                    value = noneTrans;

                break;
            }

            if ((int)value < 0)
                value = belowThreshTrans;

            else if ((int)value > 127)
                value = aboveThreshTrans;

            if (value.isVoid())
                value = 0;

            auto* valueLabel = static_cast<TableLabel*> (existingComponentToUpdate);
            if (valueLabel == nullptr)
            {
                valueLabel = new TableLabel();
                valueLabel->setColour(Label::ColourIds::textColourId, Colours::black);
                valueLabel->setColour(Label::ColourIds::outlineColourId, Colours::black);
                valueLabel->setJustificationType(Justification::centred);

                // TODO: note out mapping implementation
                if (columnId == ColumnType::KeyNumber || (columnId != ColumnType::MidiNoteOut && value == noneTrans))
                {
                    valueLabel->setEditable(true);
                    valueLabel->addListener(this);
                }
            }

            valueLabel->setRowNumber(rowNumber);
            valueLabel->setColumnId(columnId);
            valueLabel->setText(value.toString(), dontSendNotification);

            return valueLabel;
        }
        else
        {
            // don't understand why I have to do this
            delete existingComponentToUpdate;
        }
    }

    // Add / Delete Column
    else
    {
        auto* button = static_cast<TableButton*>(existingComponentToUpdate);

        if (existingComponentToUpdate == nullptr)
        {
            button = new TableButton();
            button->addListener(this);
        }

        button->setRowNumber(rowNumber);
        button->setColumnId(columnId);

        if (rowNumber < currentNoteMapNode.getNumChildren())
            button->setButtonText(deleteTrans);
        else
            button->setButtonText("+");

        return button;
    }

    return nullptr;
}

int NoteMapEditor::getColumnAutoSizeWidth(int columnId)
{
    switch (ColumnType(columnId))
    {
    case ColumnType::ToggleMapping:
        return font.getStringWidth("+");

    default:
        return font.getStringWidth("128");
    }
}

String NoteMapEditor::getCellTooltip(int rowNumber, int columnId)
{
    return String();
}

void NoteMapEditor::deleteKeyPressed(int lastRowSelected)
{

}

void NoteMapEditor::returnKeyPressed(int lastRowSelected)
{

}

//=============================================================================

NoteMap NoteMapEditor::getCurrentNoteMapping() const
{
    return currentNoteMap;
}

void NoteMapEditor::mapMidiNoteToKey(int midiNoteIn, int keyNumberOut)
{
    currentNoteMap.setValue(midiNoteIn, keyNumberOut);
    listeners.call(&MappingEditor::Listener::mappingEditorChanged, currentNoteMap);
    resetMapping(currentNoteMap, false);
}

void NoteMapEditor::resetMapping(NoteMap mappingIn, bool sendMessage)
{
    currentNoteMap = mappingIn;
    currentNoteMapNode = currentNoteMap.getAsValueTree(IDs::midiInputRemap);

    if (sendMessage)
        listeners.call(&MappingEditor::Listener::mappingEditorChanged, currentNoteMap);

    table.updateContent();
}

//=============================================================================

void NoteMapEditor::editorShown(Label* label, TextEditor& editor)
{
    TableLabel* tl = static_cast<TableLabel*>(label);

    ValueTree mapNode = currentNoteMapNode.getChild(tl->getRowNumber());

    editor.setInputRestrictions(4, "-0123456789");

    switch (tl->getColumnId())
    {
    case ColumnType::KeyNumber:
        editor.setText(mapNode[IDs::pianoKeyNumber]);
        break;

    // TODO: implement actual midi channel out
    case ColumnType::MidiChannelOut:
        editor.setText(mapNode[IDs::pianoKeyNumber]);
        break;
    }
}

void NoteMapEditor::labelTextChanged(Label* label)
{
    TableLabel* tl = static_cast<TableLabel*>(label);

    switch (tl->getColumnId())
    {
    case ColumnType::KeyNumber:

        if (label->getText().length() > 0)
        {
            int newKeyNum = label->getText().getIntValue();

            if (newKeyNum < 0)
                label->setText(belowThreshTrans, dontSendNotification);

            else if (newKeyNum > 127)
                label->setText(aboveThreshTrans, dontSendNotification);

            else
                editMapping(tl->getRowNumber(), newKeyNum, false);
        }
        else
        {
            label->setText(noneTrans, dontSendNotification);
            currentNoteMapNode.getChild(tl->getRowNumber()).removeProperty(IDs::pianoKeyNumber, nullptr);
        }

        listeners.call(&MappingEditor::Listener::mappingEditorChanged, currentNoteMap);
        break;

    case ColumnType::MidiNoteIn:

        if (tl->getText().length() > 0)
        {
            int newNoteIn = tl->getText().getIntValue();
            int keyMappedToNote = currentNoteMap.getValue(newNoteIn);

            // Midi Note In mapping already exists
            if (keyMappedToNote != newNoteIn)
            {
                AlertWindow::showMessageBox(
                    AlertWindow::AlertIconType::WarningIcon,
                    mappingErrorTrans,
                    getAlreadyMappedMessage(newNoteIn, keyMappedToNote),
                    okTrans
                );

                label->setText(noneTrans, dontSendNotification);
            }
            else
            {
                currentNoteMapNode.getChild(tl->getRowNumber()).setProperty(IDs::mappingMidiNoteIn, newNoteIn, nullptr);
            }
        }
        else
        {
            label->setText(noneTrans, dontSendNotification);
            currentNoteMapNode.getChild(tl->getRowNumber()).removeProperty(IDs::mappingMidiNoteIn, nullptr);
        }

        break;
    }
}

void NoteMapEditor::buttonClicked(Button* button)
{
    TableButton* tb = static_cast<TableButton*>(button);
    int rowNum = tb->getRowNumber();

    // Delete mapping
    if (rowNum < currentNoteMapNode.getNumChildren())
    {
        removeMapping(rowNum);
    }

    // Add new mapping
    else
    {
        addNewMapping();
    }
}

//=============================================================================

void NoteMapEditor::addNewMapping()
{
    currentNoteMapNode.appendChild(ValueTree(IDs::noteMapNode), nullptr);
    table.updateContent();
}

void NoteMapEditor::editMapping(int rowNumber, int keyNumberOut, bool sendMappingChangedMessage)
{
    currentNoteMapNode.getChild(rowNumber).setProperty(IDs::pianoKeyNumber, keyNumberOut, nullptr);
    currentNoteMap = NoteMap(currentNoteMapNode);
    resetMapping(currentNoteMap, sendMappingChangedMessage);
}

void NoteMapEditor::removeMapping(int rowNumber)
{
    currentNoteMapNode.removeChild(rowNumber, nullptr);
    currentNoteMap = NoteMap(currentNoteMapNode);
    resetMapping(currentNoteMap);
}

String NoteMapEditor::getAlreadyMappedMessage(int noteIn, int keyNumOut)
{
    return "Note " + String(noteIn) + " is already mapped to key " + String(keyNumOut) + ". Edit key number instead.";
}

repo_name: vsicurella/SuperVirtualKeyboard

mime_type: text/x-c


text:
/*
  ==============================================================================

    PresetLibraryBox.h
    Created: 27 May 2019 12:02:43pm
    Author:  Vincenzo

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

#pragma once

#include "../../../JuceLibraryCode/JuceHeader.h"

class ReferencedComboBox : public ComboBox
{
    PopupMenu rootMenu;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ReferencedComboBox)

public:

    ReferencedComboBox(const String& nameIn={}, const PopupMenu* menuIn=nullptr);
    ~ReferencedComboBox();

    void setMenu(const PopupMenu& menuIn);

};


text:
/*
  ==============================================================================

    NoteMapEditor.h
    Created: 18 Oct 2020 5:13:12pm
    Author:  Vincenzo

    A component for viewing and editing note mappings.

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

#pragma once
#include <JuceHeader.h>
#include "../../Structures/MappingEditorBase.h"
#include "../../CommonFunctions.h"

// Table Helpers

class TableComponent
{
    int rowNumber;
    int columnId;

public:

    void setRowNumber(int rowNumberIn)
    {
        rowNumber = rowNumberIn;
    }

    void setColumnId(int columnIdIn)
    {
        columnId = columnIdIn;
    }

    int getRowNumber() const
    {
        return rowNumber;
    }

    int getColumnId() const
    {
        return columnId;
    }
};

class TableLabel : public TableComponent, public Label {};
class TableButton : public TableComponent, public TextButton {};
class TableSlider : public TableComponent, public Slider {};

class NoteMapEditor : public Component,
                      public TableListBoxModel,
                      public MappingEditor,
                      private Button::Listener,
                      private Label::Listener
{
public:

    enum ColumnType
    {
        MidiNoteIn = 1,
        MidiChannelIn,
        KeyNumber,
        MidiNoteOut,
        MidiChannelOut,
        ToggleMapping
    };

public:

    NoteMapEditor(NoteMap noteMapIn);
    ~NoteMapEditor();

    void resized() override;

    //=============================================================================

    // TableListBoxModel implementation

    int getNumRows() override;

    void paintRowBackground(Graphics&, int rowNumber, int w, int h, bool rowIsSelected) override;

    void paintCell(Graphics&, int rowNumber, int columnId, int w, int h, bool isSelected) override;

    Component* refreshComponentForCell(int rowNumber, int columnId, bool isRowSelected, Component*) override;

    int getColumnAutoSizeWidth(int columnId) override;

    String getCellTooltip(int rowNumber, int columnId) override;

    void deleteKeyPressed(int lastRowSelected) override;

    void returnKeyPressed(int lastRowSelected) override;

    //=============================================================================

    // MappingEditor implementation

    NoteMap getCurrentNoteMapping() const override;

    void mapMidiNoteToKey(int midiNoteIn, int keyNumberOut) override;

    void resetMapping(NoteMap mappingIn = NoteMap(), bool sendMessage = true) override;

    //=============================================================================

    // Label::Listener implementation

    void editorShown(Label*, TextEditor&) override;

    void labelTextChanged(Label*) override;

    // Button::Listener implementation

    void buttonClicked(Button*) override;

    //=============================================================================

    void addNewMapping();

    // TODO: add midi note out & channel editing
    void editMapping(int rowNumber, int keyNumOut, bool sendMappingChangedMessage = true);

    void removeMapping(int rowNumber);

private:

    String getAlreadyMappedMessage(int noteIn, int keyNumOut);

private:

    NoteMap currentNoteMap;
    ValueTree currentNoteMapNode;

    Font font = Font(11, Font::plain);
    TableListBox table = { {}, this };
    TableHeaderComponent* header;

    String newMappingTrans = translate("  Add new mapping");
    String belowThreshTrans = translate("Too low");
    String aboveThreshTrans = translate("Too high");
    String mappingErrorTrans = translate("Mapping Error");
    String noneTrans = translate("None");
    String deleteTrans = translate("Delete");
    String okTrans = translate("Ok");
};


text:
/*
  ==============================================================================

    FocussedComponent.h
    Created: 17 Sep 2020 6:31:58pm
    Author:  Vincenzo

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

#pragma once
#include <JuceHeader.h>

class FocussedComponent;

class FocusableComponent
{
public:

    FocusableComponent(FocussedComponent* focusserIn = nullptr, std::function<void(var)> callbackIn = {})
        : currentFocusser(focusserIn)
    {

    }

    virtual void setNewFocusser(FocussedComponent* newFocusserIn)
    {
        currentFocusser = newFocusserIn;
    }

    bool isFocussedOn() const
    {
        return currentFocusser != nullptr;
    }

    virtual void performFocusFunction(var dataIn)
    {
        focusFunctionCallback(dataIn);
    }

protected:

    FocussedComponent* currentFocusser;

    std::function<void(var)> focusFunctionCallback;

};

class FocussedComponent
{

public:

    FocussedComponent(Component* initialComponentToFocus = nullptr, std::function<void(Component*, var)> focusserCallbackIn = {})
        : currentFocus(initialComponentToFocus), focusserCallback(focusserCallbackIn)
    {

    }

    virtual void setComponentToFocusOn(Component* componentPtr)
    {
        FocusableComponent* focusable = dynamic_cast<FocusableComponent*>(currentFocus);
        if (focusable)
            focusable->setNewFocusser(nullptr);

        currentFocus = componentPtr;

        focusable = dynamic_cast<FocusableComponent*>(currentFocus);
        if (focusable)
            focusable->setNewFocusser(this);
    }

    void setFocusserCallback(std::function<void(Component*, var)> focusserCallbackIn)
    {
        focusserCallback = focusserCallbackIn;
    }

protected:

    Component* currentFocus;
    std::function<void(Component*, var)> focusserCallback;
};


text:
/*
  ==============================================================================

    LabelledComponent.h
    Created: 6 Sep 2020 4:43:16pm
    Author:  Vincenzo

    A container for a component that allows a offsetLabel to be attached a certain way.

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

#pragma once
#include <JuceHeader.h>

class LabelledComponent : public Component
{

public:

    enum LabelPosition
    {
        LeftOfComponent = 0,
        TopOfComponent,
        RightOfComponent,
        BottomOfComponent
    };

    enum LabelJustification
    {
        LeftJustify = 0,
        CentredJustify,
        RightJustify
    };

public:

    LabelledComponent(
        Component&          componentIn,
        String              labelTextIn,
        bool                ownsComponent = true,
        LabelPosition       positionIn = LabelPosition::LeftOfComponent,
        LabelJustification  justificationIn = LabelJustification::LeftJustify,
        Font                fontIn = Font()
    ) :
        componentReference  (componentIn),
        deleteComponent     (ownsComponent),
        position            (positionIn),
        justification       (justificationIn),
        font                (fontIn)
    {
        (Component*)&componentReference;

        if (deleteComponent)
        {
            component.reset(&componentReference);
        }

        addAndMakeVisible(&componentReference);

        label.reset(new Label(componentReference.getName() + "_Label"));
        label->setText(labelTextIn, dontSendNotification);
        label->setFont(font);
        label->setJustificationType(Justification::verticallyCentred);
        addAndMakeVisible(label.get());

        setLabelSize(font.getStringWidth(label->getText()) + 8, 24);

        // Default Size
        component->setSize(225, 24);
    }

    ~LabelledComponent()
    {
        if (deleteComponent)
            component = nullptr;

        label = nullptr;
    }

    void resized() override
    {
        //if ((expectedWidth > 0 && expectedHeight > 0)
        //    && (expectedWidth != getWidth() || expectedHeight != getHeight())
        //   )
        //{
        //    float widthRatio = getWidth() / expectedWidth;
        //    float heightRatio = getHeight() / expectedHeight;

        //    componentReference.setSize(componentReference.getWidth() * widthRatio, componentReference.getHeight() * heightRatio);
        //    offsetLabel->setSize(offsetLabel->getWidth() * widthRatio, offsetLabel->getHeight() * heightRatio);
        //}

        int labelX = 0, labelY = 0, compX = 0, compY = 0;

        if (position % 2 == 0)
        {
            if (position == LabelPosition::LeftOfComponent)
                compX = label->getWidth();

            else
                labelX = componentReference.getWidth();
        }
        else
        {
            if (position == LabelPosition::TopOfComponent)
                compY = label->getHeight();

            else
                labelY = componentReference.getHeight();

            if (justification == LabelJustification::CentredJustify)
                labelX = (getWidth() - label->getWidth()) / 2;

            else if (justification == LabelJustification::RightJustify)
                labelX = getWidth() - label->getWidth();
        }

        label->setTopLeftPosition(labelX, labelY);
        componentReference.setTopLeftPosition(compX, compY);
    }

    void paintOverChildren(Graphics& g) override
    {
        //g.setColour(Colours::red);
        //g.drawRect(getLocalBounds(), 1);
    }

    void setComponentSize(int widthIn, int heightIn)
    {
        componentReference.setSize(widthIn, heightIn);

        if (position % 2 == 0)
        {
            expectedWidth = labelWidth + componentReference.getWidth();
            expectedHeight = jmax(labelHeight, componentReference.getHeight());
        }
        else
        {
            expectedWidth = jmax(labelWidth, componentReference.getWidth());
            expectedHeight = labelHeight + componentReference.getHeight();
        }

        setSize(expectedWidth, expectedHeight);
    }

    void setLabelSize(int widthIn, int heightIn)
    {
        labelWidth = widthIn;
        labelHeight = heightIn;
        label->setSize(labelWidth, labelHeight);
        setComponentSize(componentReference.getWidth(), componentReference.getHeight());
    }

    void setLabelPosition(LabelPosition positionIn)
    {
        position = positionIn;
        setComponentSize(componentReference.getWidth(), componentReference.getHeight());
    }

    void setLabelJustification(LabelJustification justificationIn)
    {
        justification = justificationIn;
        resized();
    }

    Component* get()
    {
        return &componentReference;
    }

    Label* getLabel() const
    {
        return label.get();
    }

    String getLabelText() const
    {
        return label->getText();
    }

    bool hasOwnership() const
    {
        return deleteComponent;
    }

    LabelPosition getLabelPosition() const
    {
        return position;
    }

public:

    template <class ComponentType>
    static ComponentType* getComponentPointer(LabelledComponent* labelledComponentIn)
    {
        return dynamic_cast<ComponentType*>(labelledComponentIn->get());
    }

private:

    std::unique_ptr<Component> component;
    Component& componentReference;
    bool deleteComponent;
    LabelPosition position;
    LabelJustification justification;
    Font font;

    float expectedWidth, expectedHeight;

    int labelWidth, labelHeight;
    std::unique_ptr<Label> label;
};


text:
/*
  ==============================================================================

    PresetLibraryBox.cpp
    Created: 27 May 2019 12:02:43pm
    Author:  Vincenzo

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

#include "ReferencedComboBox.h"

ReferencedComboBox::ReferencedComboBox(const String& nameIn, const PopupMenu* menuIn)
    : ComboBox(nameIn), rootMenu(PopupMenu())
{
    if (menuIn)
        setMenu(*menuIn);
}

ReferencedComboBox::~ReferencedComboBox()
{
}

void ReferencedComboBox::setMenu(const PopupMenu& menuIn)
{
    rootMenu = menuIn;

    PopupMenu& currentMenu = *getRootMenu();
    currentMenu = rootMenu;
}


text:
/*
  ==============================================================================

    ColourLibraryComponent.h
    Created: 18 Sep 2020 9:56:16pm
    Author:  Vincenzo

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

#pragma once

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

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

class PaintSwatch : public Component, public FocusableComponent
{
public:

    PaintSwatch(Array<Colour>& parentPaletteIn, int indexIn, String swatchLabelIn = "", bool showLabelIn = false, std::function<void(var)> callback = {})
        : parentPalette(parentPaletteIn), index(indexIn), swatchLabel(swatchLabelIn), showLabel(showLabelIn), FocusableComponent(nullptr, callback)
    {
        setName("Swatch_" + swatchLabelIn);
    };

    ~PaintSwatch() {};

    void paint(Graphics& g) override
    {
        g.fillAll(getCurrentColour());

        if (getCurrentColour().isTransparent())
        {
            g.setColour(Colours::darkgrey.darker());
            g.drawRect(getLocalBounds());
        }

        g.setColour(getCurrentColour().contrasting());

        if (showLabel)
        {
            g.drawFittedText(swatchLabel, getLocalBounds(), Justification::centred, 1, 0.1f);
        }

        if (isFocussedOn())
        {
            g.drawRect(getLocalBounds(), 2.0f);
        }
    }

    void setNewFocusser(FocussedComponent* focusserIn) override
    {
        FocusableComponent::setNewFocusser(focusserIn);
        repaint();
    }

    void setLabelText(String labelTextIn)
    {
        swatchLabel = labelTextIn;
    }

    void performFocusFunction(var dataIn) override
    {
        parentPalette.set(index, Colour::fromString(dataIn.toString()));
        repaint();
    }

    Colour getCurrentColour() const
    {
        return parentPalette[index];
    }

private:

    Array<Colour>& parentPalette;
    const int index;

    String swatchLabel;

    bool showLabel;
};

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

class ColourLibraryComponent : public juce::Component
{

public:

    enum ColourIds
    {
        backgroundColourId        = 0x00010100,
        rowBackgroundColourId    = 0x00010101,
        outLineColourId            = 0x00010200,
        rowOutlineColourId        = 0x00010201
    };

public:
    ColourLibraryComponent(StringArray rowNamesIn, Array<Array<Colour>*> arraysToControl = {}, bool showLabelsIn = false, bool onlyOpaqueIn = false);
    ~ColourLibraryComponent() override;

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

    void showSwatchLabels(bool toShowLabels);

    void onlyShowOpaque(bool onlyOpaque);

    void setNewRows(Array<Array<Colour>*> arraysToControl);

    void refreshSwatches();

    void forceSwatchColumns(int maxNumColumns);

private:

    void setupFlex();

private:

    Array<Array<Colour>*> rowColours;

    FlexBox flexBox;
    Array<FlexBox> rowBoxes;

    OwnedArray<Label> rowLabels;
    OwnedArray<PaintSwatch> swatches;

    bool showLabels;
    bool onlyOpaque;

    int maxSwatchColumns = 0;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ColourLibraryComponent)
};


text:
/*
  ==============================================================================

    PluginControlComponent.h
    Created: 8 Jul 2019
    Author:  Vincenzo Sicurella

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

#pragma once

#include "../JuceLibraryCode/JuceHeader.h"

#include "../../PluginState.h"
#include "../VectorResources.h"
#include "ReferencedComboBox.h"
#include "VirtualKeyboard/KeyboardComponent.h"
#include "VirtualKeyboard/KeyboardViewport.h"
#include "../../Structures/MappingHelper.h"

#include "Settings/SettingsContainer.h"

#include "../Dialogs/ModeInfoDialog.h"
#include "../Dialogs/MapByOrderDialog.h"
#include "../Dialogs/ExportKbmDialog.h"

#include "../../File IO/ReaperWriter.h"
#include "../../File IO/AbletonMidiWriter.h"
#include "../../File IO/KbmWriter.h"

class PluginControlComponent  : public Component,
                                public TextEditor::Listener,
                                public ComboBox::Listener,
                                public Slider::Listener,
                                public Button::Listener,
                                public ScrollBar::Listener,
                                public ChangeListener,
                                private SvkPluginState::Listener,
                                private SettingsContainer::Listener,
                                private MappingHelper::Listener,
                                private Timer
{
public:

    enum SaveMenuOptions
    {
        SaveMode = 1,
        SavePreset,
        ExportKBM,
        ExportReaper,
        ExportAbleton
    };

    enum LoadMenuOptions
    {
        LoadMode = 1,
        LoadPreset,
        LoadKBM
    };

public:
    //==============================================================================
    PluginControlComponent (SvkPluginState* pluginStateIn);

    ~PluginControlComponent();

    //==============================================================================

    void paint(Graphics& g) override;
    void resized() override;

    void mouseDown(const MouseEvent& e) override;

    void comboBoxChanged(ComboBox* comboBoxThatHasChanged) override;
    void sliderValueChanged(Slider* sliderThatWasMoved) override;
    void buttonClicked(Button* buttonThatWasClicked) override;

    void textEditorTextChanged(TextEditor& textEditor) override {};
    void textEditorEscapeKeyPressed(TextEditor& textEditor) override {};
    void textEditorReturnKeyPressed(TextEditor& textEditor) override;
    void textEditorFocusLost(TextEditor& textEditor) override {};

    void scrollBarMoved(ScrollBar* bar, double newRangeStart) override;

    void changeListenerCallback(ChangeBroadcaster* source) override;

    void timerCallback() override;

    //==============================================================================

    void resyncPresetNode(ValueTree presetNode) override;

    void modeViewedChanged(Mode* modeIn, int selectorNumber, int slotNumber) override;

    void mappingModeChanged(int mappingModeId) override;

    void inputMappingChanged(NoteMap& inputNoteMap) override;

    void customModeChanged(Mode* newCustomMode) override;

    void modeInfoChanged(Mode* modeEdited) override;

    void settingsTabChanged(int tabIndex, const String& tabName, SvkSettingsPanel* panelChangedTo) override;

    void keyMappingStatusChanged(int keyNumber, bool preparedToMap) override;

    void keyMapConfirmed(int keyNumber, int midiNote) override;

    void mappingEditorChanged(NoteMap&) override;

    //==============================================================================

    String getScaleEntryText();
    void setScaleEntryText(String textIn, NotificationType notify = NotificationType::dontSendNotification);

    VirtualKeyboard::Keyboard* getKeyboard();

    TextEditor* getScaleTextEditor();
    Viewport* getViewport();
    ComboBox* getMappingStyleBox();

    ReferencedComboBox* getMode1Box();
    ReferencedComboBox* getMode2Box();
    void updateModeBoxMenus();

    ImageButton* getSettingsButton();
    TextButton* getModeInfoButton();

    //==============================================================================

    /*
        Sets controls to plugin state
    */
    void loadPresetNode(ValueTree presetNodeIn);

    /*
        Updates UI to the new mode
    */
    void onModeViewedChange(Mode* modeViewed);

    int getModeSelectorViewed();

    void setMapModeBoxId(int mappingModeId, NotificationType notify = NotificationType::sendNotification);

    void setMappingStyleId(int idIn, NotificationType notify = NotificationType::sendNotification);

    void setMode1Root(int rootIn, NotificationType notify = NotificationType::dontSendNotification);

    void setMode2Root(int rootIn, NotificationType notify = NotificationType::dontSendNotification);

    void submitCustomScale();

    void updateRootNoteLabels();

    //==============================================================================

    void showModeInfo();

    void showMapOrderEditDialog();

    void showSettingsDialog();

    void hideSettings();

    void beginColorEditing();

    void endColorEditing();

    void beginManualMapping();

    //==============================================================================

    bool browseForModeToOpen();

    bool browseForPresetToOpen();

    bool exportModeViewedForReaper();

    bool exportModeViewedForAbleton();

    void exportKbmMapping();

private:

    SvkPluginState* pluginState;
    SvkPresetManager* presetManager;

    std::unique_ptr<Image> saveIcon;
    std::unique_ptr<Image> openIcon;
    std::unique_ptr<Image> settingsIcon;

    std::unique_ptr<PopupMenu> saveMenu;
    Array<PopupMenu::Item> saveMenuItems;

    std::unique_ptr<PopupMenu> loadMenu;
    Array<PopupMenu::Item> loadMenuItems;

    MappingHelper* manualMappingHelper;
    std::unique_ptr<SettingsContainer> settingsContainer;

    ScrollBar* viewportScrollBar;
    ModeInfoDialog* modeInfo;
    MapByOrderDialog* mapByOrderDialog;

    Array<Component*> mappingComponents;

    bool settingsPanelOpen = false;
    bool isColorEditing = false;
    bool inMappingMode = false;
    bool mappingSettingsOpen = false;

    String noKeySelectedTrans = "No key selected.";
    String waitingForTrans = "Waiting for input to map to key ";

    String inputModeTooltip = "The input scale that represents your physical keyboard or DAW piano roll.";
    String outputModeTooltip = "The output scale that you want to play or reference.";

    String mapModeTooltip =
        "Maps MIDI note input to notes in the selected scale (Mode 2).\n"
        "Auto Map: automatically maps notes based on Mode 1 and Mode 2.\n"
        "Manual: create a custom mapping by clicking notes on the virtual keyboard.";

    String mapStyleTooltip =
        "The way notes are mapped from the input to output.\n"
        "Mode To Mode: natural notes from Mode 1 are mapped to natural Notes in Mode 2.\n"
        "Scale To Mode: each note from Mode 1 is mapped to natural notes in Mode 2.\n"
        "by Layer: Notes from a layer in Mode 1 are mapped to a layer in Mode 2.";

    float centerKeyPos = 60.5f;

    int defaultHeight = 210;
    int barHeight = 24;
    int gap = 8;

    //==============================================================================
    std::unique_ptr<TextEditor> scaleTextBox;
    std::unique_ptr<ReferencedComboBox> mode1Box;
    std::unique_ptr<ReferencedComboBox> mode2Box;
    std::unique_ptr<Slider> mode1RootSld;
    std::unique_ptr<Slider> mode2RootSld;
    std::unique_ptr<TextButton> scaleEntryBtn;
    std::unique_ptr<TextButton> modeInfoButton;
    std::unique_ptr<ToggleButton> mode1ViewBtn;
    std::unique_ptr<ToggleButton> mode2ViewBtn;
    std::unique_ptr<Label> mode1RootLbl;
    std::unique_ptr<Label> mode2RootLbl;
    std::unique_ptr<ComboBox> mapStyleBox;
    std::unique_ptr<Label> mapStyleLbl;
    std::unique_ptr<TextButton> mapOrderEditBtn;
    std::unique_ptr<ComboBox> mapModeBox;
    std::unique_ptr<TextButton> mapApplyBtn;
    std::unique_ptr<Label> mapManualTip;
    std::unique_ptr<Label> mapManualStatus;
    std::unique_ptr<TextButton> mapManualCancel;
    std::unique_ptr<TextButton> mapManualResetBtn;
    std::unique_ptr<TextButton> mapCopyToManualBtn;
    std::unique_ptr<VirtualKeyboard::Keyboard> keyboard;
    std::unique_ptr<KeyboardViewport> keyboardViewport;
    std::unique_ptr<ImageButton> saveButton;
    std::unique_ptr<ImageButton> openButton;
    std::unique_ptr<ImageButton> settingsButton;

    //==============================================================================
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PluginControlComponent)
};


text:
/*
  ==============================================================================

    PluginControlComponent.cpp
    Created: 8 Jul 2019
    Author:  Vincenzo Sicurella

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

#include "PluginControlComponent.h"

PluginControlComponent::PluginControlComponent(SvkPluginState* pluginStateIn)
    : pluginState(pluginStateIn)
{
    pluginState->addListener(this);
    presetManager = pluginState->getPresetManager();

    scaleTextBox.reset(new TextEditor("Scale Text Box"));
    addAndMakeVisible(scaleTextBox.get());
    scaleTextBox->setMultiLine(false);
    scaleTextBox->setReturnKeyStartsNewLine(false);
    scaleTextBox->setReadOnly(false);
    scaleTextBox->setScrollbarsShown(true);
    scaleTextBox->setCaretVisible(true);
    scaleTextBox->setPopupMenuEnabled(true);
    scaleTextBox->setText(String());

    mode1Box.reset(new ReferencedComboBox("Mode1 Box"));
    addAndMakeVisible(mode1Box.get());
    mode1Box->setEditableText(false);
    mode1Box->setJustificationType(Justification::centredLeft);
    mode1Box->setTextWhenNothingSelected(String());
    mode1Box->setTextWhenNoChoicesAvailable(TRANS("(no choices)"));
    mode1Box->setTooltip(inputModeTooltip);
    mode1Box->addListener(this);

    mode2Box.reset(new ReferencedComboBox("Mode2 Box"));
    addAndMakeVisible(mode2Box.get());
    mode2Box->setEditableText(false);
    mode2Box->setJustificationType(Justification::centredLeft);
    mode2Box->setTextWhenNothingSelected(String());
    mode2Box->setTextWhenNoChoicesAvailable(TRANS("(no choices)"));
    mode2Box->setTooltip(outputModeTooltip);
    mode2Box->addListener(this);

    mode1RootSld.reset(new Slider("Mode1 Root Slider"));
    addAndMakeVisible(mode1RootSld.get());
    mode1RootSld->setRange(0, 127, 1);
    mode1RootSld->setSliderStyle(Slider::IncDecButtons);
    mode1RootSld->setTextBoxStyle(Slider::TextBoxLeft, false, 40, 20);
    mode1RootSld->addListener(this);

    mode2RootSld.reset(new Slider("mode2RootSld"));
    addAndMakeVisible(mode2RootSld.get());
    mode2RootSld->setRange(0, 127, 1);
    mode2RootSld->setSliderStyle(Slider::IncDecButtons);
    mode2RootSld->setTextBoxStyle(Slider::TextBoxLeft, false, 40, 20);
    mode2RootSld->addListener(this);

    scaleEntryBtn.reset(new TextButton("Scale Entry Button"));
    addAndMakeVisible(scaleEntryBtn.get());
    scaleEntryBtn->setButtonText(TRANS("OK"));
    scaleEntryBtn->addListener(this);

    modeInfoButton.reset(new TextButton("Mode Info Button"));
    addAndMakeVisible(modeInfoButton.get());
    modeInfoButton->setButtonText(TRANS("i"));
    modeInfoButton->addListener(this);

    mode1ViewBtn.reset(new ToggleButton("Mode1 View Button"));
    addChildComponent(mode1ViewBtn.get());
    mode1ViewBtn->setButtonText(String());
    mode1ViewBtn->setRadioGroupId(10);
    mode1ViewBtn->addListener(this);
    mode1ViewBtn->setToggleState(true, dontSendNotification);

    mode2ViewBtn.reset(new ToggleButton("Mode2 View Button"));
    addChildComponent(mode2ViewBtn.get());
    mode2ViewBtn->setButtonText(String());
    mode2ViewBtn->setRadioGroupId(10);
    mode2ViewBtn->addListener(this);

    mode1RootLbl.reset(new Label("Mode1 Root Label",
        TRANS("C4")));
    addAndMakeVisible(mode1RootLbl.get());
    mode1RootLbl->setFont(Font(15.00f, Font::plain).withTypefaceStyle("Regular"));
    mode1RootLbl->setJustificationType(Justification::centredLeft);
    mode1RootLbl->setEditable(false, false, false);
    mode1RootLbl->setColour(TextEditor::textColourId, Colours::black);
    mode1RootLbl->setColour(TextEditor::backgroundColourId, Colour(0x00000000));

    mode2RootLbl.reset(new Label("Mode 2 Root Label",
        TRANS("F5")));
    addAndMakeVisible(mode2RootLbl.get());
    mode2RootLbl->setFont(Font(15.00f, Font::plain).withTypefaceStyle("Regular"));
    mode2RootLbl->setJustificationType(Justification::centredLeft);
    mode2RootLbl->setEditable(false, false, false);
    mode2RootLbl->setColour(TextEditor::textColourId, Colours::black);
    mode2RootLbl->setColour(TextEditor::backgroundColourId, Colour(0x00000000));

    mapStyleBox.reset(new ComboBox("Mapping Style Box"));
    addAndMakeVisible(mapStyleBox.get());
    mapStyleBox->setEditableText(false);
    mapStyleBox->setJustificationType(Justification::centredLeft);
    mapStyleBox->setTextWhenNothingSelected(TRANS("Mode To Mode"));
    mapStyleBox->setTextWhenNoChoicesAvailable(TRANS("(no choices)"));
    mapStyleBox->addItem(TRANS("Mode To Mode"), 1);
    mapStyleBox->addItem(TRANS("Scale To Mode"), 2);
    mapStyleBox->addItem(TRANS("By Key Layers"), 3);
    mapStyleBox->setTooltip(mapStyleTooltip);
    mapStyleBox->addListener(this);

    mapStyleLbl.reset(new Label("Mapping Style Label",
        TRANS("Mapping Style:")));
    addAndMakeVisible(mapStyleLbl.get());
    mapStyleLbl->setFont(Font(15.00f, Font::plain).withTypefaceStyle("Regular"));
    mapStyleLbl->setJustificationType(Justification::centredLeft);
    mapStyleLbl->setEditable(false, false, false);
    mapStyleLbl->setColour(TextEditor::textColourId, Colours::black);
    mapStyleLbl->setColour(TextEditor::backgroundColourId, Colour(0x00000000));

    mapOrderEditBtn.reset(new TextButton("Map Order Edit Button", TRANS("Set layer mappings")));
    addChildComponent(mapOrderEditBtn.get());
    mapOrderEditBtn->setButtonText(TRANS("Layers"));
    mapOrderEditBtn->addListener(this);

    mapModeBox.reset(new ComboBox("Mapping Mode Box"));
    addAndMakeVisible(mapModeBox.get());
    mapModeBox->setEditableText(false);
    mapModeBox->setJustificationType(Justification::centredLeft);
    mapModeBox->setTextWhenNothingSelected(TRANS("Mapping Off"));
    mapModeBox->setTextWhenNoChoicesAvailable(TRANS("Mapping Off"));
    mapModeBox->addItem(TRANS("Mapping Off"), 1);
    mapModeBox->addItem(TRANS("Auto Map"), 2);
    mapModeBox->addItem(TRANS("Manual Map"), 3);
    mapModeBox->setTooltip(mapModeTooltip);
    mapModeBox->addListener(this);

    mapApplyBtn.reset(new TextButton("Apply Layer Map Button"));
    addChildComponent(mapApplyBtn.get());
    mapApplyBtn->setButtonText(TRANS("Apply"));
    mapApplyBtn->addListener(this);

    mapCopyToManualBtn.reset(new TextButton("Copy Mapping to Manual Button", TRANS("Edit this mapping in Manual mode.")));
    addChildComponent(mapCopyToManualBtn.get());
    mapCopyToManualBtn->setButtonText(TRANS("Edit"));
    mapCopyToManualBtn->setSize(40, barHeight);
    mapCopyToManualBtn->addListener(this);

    mapManualTip.reset(new Label("Manual Mapping Tip", TRANS("Right-click output key, then trigger input MIDI note.")));
    addChildComponent(mapManualTip.get());

    mapManualStatus.reset(new Label("Manual Mapping Status", noKeySelectedTrans));
    addChildComponent(mapManualStatus.get());

    mapManualCancel.reset(new TextButton("Manual Mapping Cancel", TRANS("Cancel key mapping.")));
    addChildComponent(mapManualCancel.get());
    mapManualCancel->setButtonText(juce::CharPointer_UTF8("\xc3\x97"));
    mapManualCancel->addListener(this);
    mapManualCancel->setSize(barHeight * 0.75f, barHeight * 0.75f);

    mapManualResetBtn.reset(new TextButton("Reset Manual Mapping Button", TRANS("Reset mapping.")));
    addChildComponent(mapManualResetBtn.get());
    mapManualResetBtn->setButtonText(TRANS("Reset"));
    mapManualResetBtn->setSize(48, barHeight);
    mapManualResetBtn->addListener(this);

    keyboard.reset(new VirtualKeyboard::Keyboard());
    keyboard->addListener(pluginState->getMidiProcessor());

    keyboardViewport.reset(new KeyboardViewport(keyboard.get(), "Keyboard Viewport"));
    addAndMakeVisible(keyboardViewport.get());
    viewportScrollBar = &keyboardViewport->getHorizontalScrollBar();

    saveButton.reset(new ImageButton("Save Button"));
    addAndMakeVisible(saveButton.get());
    saveButton->setButtonText(TRANS("Save"));
    saveButton->addListener(this);
    saveButton->setImages(false, true, true,
        Image(), 1.000f, Colour(0x00000000),
        Image(), 1.000f, Colour(0x00000000),
        Image(), 1.000f, Colour(0x00000000));
    saveButton->setSize(28, 28);

    openButton.reset(new ImageButton("Open Button"));
    addAndMakeVisible(openButton.get());
    openButton->setButtonText(TRANS("Open"));
    openButton->addListener(this);
    openButton->setImages(false, true, true,
        Image(), 1.000f, Colour(0x00000000),
        Image(), 1.000f, Colour(0x00000000),
        Image(), 1.000f, Colour(0x00000000));
    openButton->setSize(28, 28);

    settingsButton.reset(new ImageButton("Settings Button"));
    addAndMakeVisible(settingsButton.get());
    settingsButton->setButtonText(TRANS("Settings"));
    settingsButton->addListener(this);
    settingsButton->setImages(false, true, true,
        Image(), 1.000f, Colour(0x00000000),
        Image(), 1.000f, Colour(0x00000000),
        Image(), 1.000f, Colour(0x00000000));
    settingsButton->setSize(28, 28);

    saveIcon.reset(new Image(Image::PixelFormat::RGB, saveButton->getWidth(), saveButton->getHeight(), true));
    openIcon.reset(new Image(Image::PixelFormat::RGB, openButton->getWidth(), openButton->getHeight(), true));

    saveButton->setImages(true, true, true, *saveIcon.get(), 0.0f, Colour(), *saveIcon.get(), 0.0f, Colours::white.withAlpha(0.25f), *saveIcon.get(), 0.0f, Colours::white.withAlpha(0.5f));
    openButton->setImages(true, true, true, *openIcon.get(), 0.0f, Colour(), *openIcon.get(), 0.0f, Colours::white.withAlpha(0.25f), *openIcon.get(), 0.0f, Colours::white.withAlpha(0.5f));

    scaleTextBox->addListener(this);
    scaleTextBox->setInputRestrictions(0, " 0123456789");

    //keyboardViewport->setScrollingMode(3);

    // allows for implementing mouseDown() to update the menus
    mode1Box->setInterceptsMouseClicks(false, false);
    mode2Box->setInterceptsMouseClicks(false, false);

    saveMenu.reset(new PopupMenu());
    saveMenuItems =
    {
        PopupMenu::Item("Save Mode").setID(SaveMenuOptions::SaveMode).setAction([&]() { pluginState->saveModeViewedToFile(); }),
        PopupMenu::Item("Save Preset").setID(SaveMenuOptions::SavePreset).setAction([&]() { pluginState->savePresetToFile(); }),
        PopupMenu::Item("Export Mapping as KBM").setID(SaveMenuOptions::ExportKBM).setAction([&]() { exportKbmMapping(); }),
        PopupMenu::Item("Export for Reaper Note Names").setID(SaveMenuOptions::ExportReaper).setAction([&]() { exportModeViewedForReaper(); }),
        PopupMenu::Item("Export for Ableton Folding").setID(SaveMenuOptions::ExportAbleton).setAction([&]() { exportModeViewedForAbleton(); })
    };
    for (auto item : saveMenuItems)
        saveMenu->addItem(item);

    loadMenu.reset(new PopupMenu());
    loadMenuItems =
    {
        PopupMenu::Item("Load Mode").setID(LoadMenuOptions::LoadMode).setAction([&]() { browseForModeToOpen(); }),
        PopupMenu::Item("Load Preset").setID(LoadMenuOptions::LoadPreset).setAction([&]() { browseForPresetToOpen(); })
    };
    for (auto item : loadMenuItems)
        loadMenu->addItem(item);

    ValueTree pluginWindowNode = pluginState->getPluginEditorNode();
    int recallWidth = pluginWindowNode[IDs::windowBoundsW];
    int recallHeight = pluginWindowNode[IDs::windowBoundsH];
    centerKeyPos = pluginWindowNode[IDs::viewportPosition];

    setSize(
        recallWidth > 0 ? recallWidth : 1000,
        recallHeight > 0 ? recallHeight : defaultHeight
    );

    settingsIcon.reset(new Image(Image::PixelFormat::RGB, settingsButton->getWidth(), settingsButton->getHeight(), true));
    settingsButton->setImages(true, true, true, *settingsIcon.get(), 0.0f, Colour(), *settingsIcon.get(), 0.0f, Colours::white.withAlpha(0.25f), *settingsIcon.get(), 0.0f, Colours::white.withAlpha(0.5f));
    settingsButton->setClickingTogglesState(true);

    mappingComponents =
    {
        mapStyleLbl.get(),
        mapStyleBox.get(),
        mapOrderEditBtn.get(),
        mode1RootLbl.get(),
        mode1RootSld.get(),
        mode1Box.get(),
        mode1ViewBtn.get(),
        mode2ViewBtn.get(),
        mapCopyToManualBtn.get()
    };

    loadPresetNode(pluginState->getPresetNode());

    viewportScrollBar->addListener(this);

    // Update keyboard to show external midi input
#if (!JUCE_ANDROID && !JUCE_IOS)
    startTimerHz(30);
#endif
}

PluginControlComponent::~PluginControlComponent()
{
    pluginState->removeListener(this);

    if (settingsPanelOpen)
    {
        pluginState->removeListener(settingsContainer.get());
        settingsContainer = nullptr;
    }

    scaleTextBox = nullptr;
    mode1Box = nullptr;
    mode2Box = nullptr;
    mode1RootSld = nullptr;
    mode2RootSld = nullptr;
    scaleEntryBtn = nullptr;
    modeInfoButton = nullptr;
    mode1ViewBtn = nullptr;
    mode2ViewBtn = nullptr;
    mode1RootLbl = nullptr;
    mode2RootLbl = nullptr;
    mapStyleBox = nullptr;
    mapStyleLbl = nullptr;
    mapManualTip = nullptr;
    mapManualStatus = nullptr;
    mapOrderEditBtn = nullptr;
    mapManualCancel = nullptr;
    mapModeBox = nullptr;
    mapApplyBtn = nullptr;
    keyboard = nullptr;
    keyboardViewport = nullptr;
    saveButton = nullptr;
    openButton = nullptr;
    settingsButton = nullptr;
}

//==============================================================================

void PluginControlComponent::loadPresetNode(ValueTree presetNodeIn)
{
    if (presetNodeIn.hasType(IDs::presetNode) && (float)presetNodeIn[IDs::pluginPresetVersion] == SVK_PRESET_VERSION)
    {
        ValueTree presetNode = presetNodeIn;

        ValueTree properties = presetNode.getChildWithName(IDs::presetProperties);
        if (properties.isValid())
        {
            mode2ViewBtn->setToggleState((int)properties[IDs::modeSelectorViewed], dontSendNotification);
        }

        ValueTree keyboardSettings = presetNode.getChildWithName(IDs::pianoNode);
        if (keyboardSettings.isValid())
        {
            keyboard->restoreNode(keyboardSettings);
        }

        ValueTree mapping = presetNode.getChildWithName(IDs::midiMapNode);
        if (mapping.isValid())
        {
            setMapModeBoxId(mapping[IDs::mappingMode]);
            setMappingStyleId(mapping[IDs::autoMappingStyle]);
        }

        ValueTree modeSelectors = presetNode.getChildWithName(IDs::modeSelectorsNode);
        if (modeSelectors.isValid())
        {
            for (int num = 0; num < modeSelectors.getNumChildren(); num++)
            {
                ValueTree selector = modeSelectors.getChild(num);
                Mode* mode = presetManager->getModeBySelector(num);

                // TODO: generalize
                if (num == 0)
                {
                    mode1RootSld->setValue(selector[IDs::modeSelectorRootNote], dontSendNotification);
                    mode1Box->setText(mode->getName(), dontSendNotification);
                }
                else if (num == 1)
                {
                    mode2RootSld->setValue(selector[IDs::modeSelectorRootNote], dontSendNotification);
                    mode2Box->setText(mode->getName(), dontSendNotification);
                }

                updateRootNoteLabels();

                if (num == (int)presetNode.getChildWithName(IDs::presetProperties)[IDs::modeSelectorViewed])
                {
                    modeViewedChanged(mode, num, 0 /*unused*/);
                }
            }
        }
    }
}

void PluginControlComponent::onModeViewedChange(Mode* modeViewed)
{
    setScaleEntryText(modeViewed->getStepsString());
    keyboard->applyMode(modeViewed, true);
    resized();
}

void PluginControlComponent::paint (Graphics& g)
{
    g.fillAll (Colour (0xff323e44));

    Colour buttonFill = getLookAndFeel().findColour(TextButton::ColourIds::buttonColourId);
    Colour buttonOutline = getLookAndFeel().findColour(TextEditor::ColourIds::outlineColourId);

    drawSaveIcon(g, saveButton->getBoundsInParent(), buttonFill, buttonOutline);
    drawLoadIcon(g, openButton->getBoundsInParent(), buttonFill, buttonOutline);
    drawSettingsIcon(g, settingsButton->getBoundsInParent(), buttonFill, buttonOutline);
}

void PluginControlComponent::resized()
{
    int basicHeight = getHeight();

    if (settingsPanelOpen)
    {
        basicHeight -= defaultHeight;
        settingsContainer->setBounds(0, basicHeight, getWidth(), defaultHeight);
        settingsContainer->resized();
    }

    scaleTextBox->setSize(proportionOfWidth(0.2f), barHeight);
    scaleTextBox->setCentrePosition(proportionOfWidth(0.5f), barHeight / 2 + gap);

    scaleEntryBtn->setBounds(scaleTextBox->getRight() + 8, scaleTextBox->getY(), 31, 24);
    modeInfoButton->setBounds(scaleTextBox->getX() - 32, scaleTextBox->getY(), 24, 24);

    saveButton->setSize(barHeight, barHeight);
    saveButton->setTopLeftPosition(gap, scaleTextBox->getY());

    openButton->setSize(barHeight, barHeight);
    openButton->setTopLeftPosition(saveButton->getRight() + gap, saveButton->getY());

    settingsButton->setSize(barHeight, barHeight);
    settingsButton->setTopLeftPosition(openButton->getRight() + gap, saveButton->getY());

    mapModeBox->setTopLeftPosition(settingsButton->getRight() + gap, gap);
    mapModeBox->setSize(jmin(proportionOfWidth(0.15f), modeInfoButton->getX() - mapModeBox->getX() - gap), barHeight);

    mode1ViewBtn->setBounds(getWidth() - 32, gap, 32, barHeight);
    mode2ViewBtn->setBounds(getWidth() - 32, mode1ViewBtn->getBottom() + gap, 32, barHeight);

    int keyboardY = barHeight + gap * 2;

    if (inMappingMode)
    {
        mode1Box->setSize(proportionOfWidth(0.15f), barHeight);
        mode1Box->setTopLeftPosition(mode1ViewBtn->getX() - mode1Box->getWidth() - gap / 2, gap);

        mode2Box->setSize(proportionOfWidth(0.15f), barHeight);
        mode2Box->setTopLeftPosition(mode1Box->getX(), mode2ViewBtn->getY());

        mapStyleLbl->setTopLeftPosition(gap / 2, scaleTextBox->getBottom() + gap);
        mapStyleLbl->setSize(mapModeBox->getX() - mapStyleLbl->getX(), barHeight);
        mapStyleBox->setBounds(mapStyleLbl->getRight(), mapStyleLbl->getY(), mapModeBox->getWidth(), barHeight);

        mapManualTip->setBounds(mapStyleLbl->getX(), mapStyleLbl->getY(), mapManualTip->getFont().getStringWidth(mapManualTip->getText()), barHeight);
        mapManualStatus->setBounds(mapManualTip->getRight(), mapManualTip->getY(), mapManualStatus->getFont().getStringWidth(mapManualStatus->getText()) + 8, barHeight);

        mapOrderEditBtn->setBounds(mapStyleBox->getRight() + gap, mapStyleBox->getY(), 55, barHeight);
        mapApplyBtn->setBounds(mapOrderEditBtn->getRight() + gap, mapOrderEditBtn->getY(), 55, barHeight);

        if (mapApplyBtn->isVisible())
            mapCopyToManualBtn->setTopLeftPosition(mapApplyBtn->getRight() + gap, mapApplyBtn->getY());
        else
            mapCopyToManualBtn->setTopLeftPosition(mapOrderEditBtn->getPosition());

        mapManualCancel->setTopLeftPosition(mapManualStatus->getRight() + gap / 2, mapManualStatus->getY() + mapManualCancel->getHeight() / 4);

        if (mapModeBox->getSelectedId() == 3)
        {
            if (mapManualCancel->isVisible())
                mapManualResetBtn->setTopLeftPosition(mapManualCancel->getRight() + gap, mapManualStatus->getY());
            else
                mapManualResetBtn->setTopLeftPosition(mapManualStatus->getRight() + gap, mapManualStatus->getY());
        }

        keyboardY = (keyboardY * 2) - gap;
    }
    else
    {
        mode2Box->setSize(proportionOfWidth(0.15f) + mode2ViewBtn->getWidth(), barHeight);
        mode2Box->setTopLeftPosition(getWidth() - mode2Box->getWidth() - gap, gap);
    }

    mode1RootSld->setBounds(mode1Box->getX() - 80 - gap, mode1Box->getY(), 80, barHeight);
    mode2RootSld->setBounds(mode2Box->getX() - 80 - gap, mode2Box->getY(), 80, barHeight);

    mode1RootLbl->setBounds(mode1RootSld->getX() - 32, mode1Box->getY(), 32, barHeight);
    mode2RootLbl->setBounds(mode2RootSld->getX() - 32, mode2Box->getY(), 32, barHeight);

    viewportScrollBar->removeListener(this);

    int scrollBarThickness = basicHeight / 28;
    int keyboardViewWidth = getWidth() - gap * 2;
    int keyboardViewHeight = jmax(basicHeight - keyboardY - gap, 1);

    keyboardViewport->setBounds(gap, keyboardY, keyboardViewWidth, keyboardViewHeight);
    keyboardViewport->setScrollBarThickness(scrollBarThickness);
    keyboardViewport->resizeKeyboard();
    keyboardViewport->centerOnKey((int)centerKeyPos);

    if (keyboard->getWidth() < keyboardViewWidth)
    {
        keyboardViewport->setSize(keyboard->getWidth(), keyboardViewHeight);
        keyboardViewport->setTopLeftPosition((getWidth() - keyboard->getWidth()) / 2, keyboardY);
    }

    viewportScrollBar->addListener(this);

    pluginState->getPluginEditorNode().setProperty(IDs::windowBoundsW, getWidth(), nullptr);
    pluginState->getPluginEditorNode().setProperty(IDs::windowBoundsH, basicHeight, nullptr);
}

void PluginControlComponent::mouseDown(const MouseEvent& e)
{
    if (mode1Box->isVisible() && mode1Box->getBounds().contains(e.getPosition()))
    {
        String display = mode1Box->getText();
        presetManager->requestModeMenu(mode1Box->getRootMenu());
        mode1Box->setText(display, dontSendNotification);
        mode1Box->showPopup();
    }

    if (mode2Box->isVisible() && mode2Box->getBounds().contains(e.getPosition()))
    {
        String display = mode2Box->getText();
        presetManager->requestModeMenu(mode2Box->getRootMenu());
        mode2Box->setText(display, dontSendNotification);
        mode2Box->showPopup();
    }
}

void PluginControlComponent::comboBoxChanged (ComboBox* comboBoxThatHasChanged)
{
    if (comboBoxThatHasChanged == mode1Box.get())
    {
        pluginState->handleModeSelection(0, mode1Box->getSelectedId());
    }
    else if (comboBoxThatHasChanged == mode2Box.get())
    {
        pluginState->handleModeSelection(1, mode2Box->getSelectedId());
    }
    else if (comboBoxThatHasChanged == mapModeBox.get())
    {
        setMapModeBoxId(mapModeBox->getSelectedId());
    }
    else if (comboBoxThatHasChanged == mapStyleBox.get())
    {
        setMappingStyleId(mapStyleBox->getSelectedId());
    }
}

void PluginControlComponent::sliderValueChanged (Slider* sliderThatWasMoved)
{
    if (sliderThatWasMoved == mode1RootSld.get())
    {
        pluginState->setModeSelectorRoot(0, mode1RootSld->getValue());
        updateRootNoteLabels();
    }
    else if (sliderThatWasMoved == mode2RootSld.get())
    {
        pluginState->setModeSelectorRoot(1, mode2RootSld->getValue());
        updateRootNoteLabels();
    }
}

void PluginControlComponent::buttonClicked (Button* buttonThatWasClicked)
{
    if (buttonThatWasClicked == scaleEntryBtn.get())
    {
        submitCustomScale();
    }
    else if (buttonThatWasClicked == modeInfoButton.get())
    {
        showModeInfo();
    }
    else if (buttonThatWasClicked == mode1ViewBtn.get() || buttonThatWasClicked == mode2ViewBtn.get())
    {
        pluginState->setModeSelectorViewed(mode2ViewBtn->getToggleState());
        scaleTextBox->setText(pluginState->getModeViewed()->getStepsString(), false);
    }
    else if (buttonThatWasClicked == mapCopyToManualBtn.get())
    {
        NoteMap autoMap = *pluginState->getMidiInputFilterMap();
        mapModeBox->setSelectedId(3, sendNotificationSync);
        manualMappingHelper->resetMapping(autoMap);
    }
    else if (buttonThatWasClicked == mapOrderEditBtn.get())
    {
        showMapOrderEditDialog();
    }
    else if (buttonThatWasClicked == mapApplyBtn.get())
    {
        pluginState->doAutoMapping();
    }
    else if (buttonThatWasClicked == saveButton.get())
    {
        saveMenu->showAt(saveButton.get());
    }
    else if (buttonThatWasClicked == openButton.get())
    {
        loadMenu->showAt(openButton.get());
    }
    else if (buttonThatWasClicked == settingsButton.get())
    {
        showSettingsDialog();
    }
    else if (buttonThatWasClicked == mapManualCancel.get())
    {
        manualMappingHelper->cancelKeyMap();
    }
    else if (buttonThatWasClicked == mapManualResetBtn.get())
    {
        manualMappingHelper->resetMapping();
        keyMappingStatusChanged(-1, false);
    }
}

void PluginControlComponent::textEditorReturnKeyPressed(TextEditor& textEditor)
{
    submitCustomScale();
}

void PluginControlComponent::scrollBarMoved(ScrollBar* bar, double newRangeStart)
{
    centerKeyPos = keyboardViewport->getCenterKeyProportion();
    pluginState->getPluginEditorNode().setProperty(IDs::viewportPosition, centerKeyPos, nullptr);
}

void PluginControlComponent::changeListenerCallback(ChangeBroadcaster* source)
{
    // Mode Info Changed
    if (source == modeInfo)
    {
        pluginState->commitModeInfo();
        onModeViewedChange(pluginState->getModeViewed());
    }
}

void PluginControlComponent::timerCallback()
{
    keyboard->repaint();
}

//==============================================================================

void PluginControlComponent::resyncPresetNode(ValueTree presetNode)
{
    keyboard->restoreNode(presetNode.getChildWithName(IDs::pianoNode), false);
}

void PluginControlComponent::modeViewedChanged(Mode* modeIn, int selectorNumber, int slotNumber)
{
    MidiKeyboardState* displayState = selectorNumber == 0
        ? pluginState->getMidiProcessor()->getOriginalKeyboardState()
        : pluginState->getMidiProcessor()->getRemappedKeyboardState();

    keyboard->displayKeyboardState(displayState);
    onModeViewedChange(modeIn);
}

void PluginControlComponent::mappingModeChanged(int mappingModeId)
{
    inMappingMode = mappingModeId > 1;

    for (auto c : mappingComponents)
    {
        c->setVisible(inMappingMode);
        c->setEnabled(inMappingMode);
    }

    if (inMappingMode)
    {
        // this is a fallback
        if (mappingModeId == 2)
            mapStyleBox->setSelectedId(pluginState->getMappingNode()[IDs::autoMappingStyle], dontSendNotification);

        if (mappingModeId == 2 && mapStyleBox->getSelectedId() == 3)
        {
            mapOrderEditBtn->setVisible(true);
            mapApplyBtn->setVisible(true);
        }
        else
        {
            mapOrderEditBtn->setVisible(false);
            mapApplyBtn->setVisible(false);
        }

        mode2ViewBtn->setToggleState(true, sendNotification);
        toggleMenuItemEnablement(*saveMenu, SaveMenuOptions::ExportKBM, true);
    }
    else
    {
        toggleMenuItemEnablement(*saveMenu, SaveMenuOptions::ExportKBM, false);
    }

    if (mappingModeId == 3)
    {
        beginManualMapping();
    }
    else
    {
        mapManualTip->setVisible(false);
        mapManualStatus->setVisible(false);
        mapManualCancel->setVisible(false);
        mapManualResetBtn->setVisible(false);

        keyboard->setUIMode(VirtualKeyboard::UIMode::playMode);
        mapManualStatus->setText(noKeySelectedTrans, sendNotification);
    }

    resized();
}

void PluginControlComponent::inputMappingChanged(NoteMap& inputNoteMap)
{
    keyboard->setInputNoteMap(inputNoteMap);
}

void PluginControlComponent::customModeChanged(Mode* newCustomMode)
{
    // This is a hack and should be handled better

    String customModeName = newCustomMode->getName();

    if (getModeSelectorViewed() == 1)
        mode2Box->setText(customModeName, dontSendNotification);
    else
        mode1Box->setText(customModeName, dontSendNotification);

}

void PluginControlComponent::modeInfoChanged(Mode* modeEdited)
{
    if (getModeSelectorViewed() == 1)
        mode2Box->setText(modeEdited->getName(), dontSendNotification);
    else
        mode1Box->setText(modeEdited->getName(), dontSendNotification);
}

void PluginControlComponent::settingsTabChanged(int tabIndex, const String& tabName, SvkSettingsPanel* panelChangedTo)
{
    DBG("Settings changed to " + tabName + ", tab: " + String(tabIndex));

    if (panelChangedTo->getName() == "ColourSettingsPanel")
    {
        // TODO clean up
        beginColorEditing();
    }
    else if (isColorEditing)
    {
        endColorEditing();
    }

    if (tabName == "Mapping")
    {
        mappingSettingsOpen = true;
        static_cast<MappingSettingsPanel*>(panelChangedTo)->registerEditorListener(this);
    }

    else if (mappingSettingsOpen)
    {
        mappingSettingsOpen = false;

        // MappingSettingsPanel removes listeners from NoteMapEditor
    }

    pluginState->getPluginEditorNode().setProperty(IDs::settingsTabName, tabName, nullptr);
}

void PluginControlComponent::keyMappingStatusChanged(int keyNumber, bool preparedToMap)
{
    if (preparedToMap)
    {
        mapManualStatus->setText(waitingForTrans + String(keyNumber), sendNotification);
        mapManualCancel->setVisible(true);
    }
    else
    {
        mapManualStatus->setText(noKeySelectedTrans, sendNotification);
        mapManualCancel->setVisible(false);
    }

    mapManualStatus->setSize(mapManualStatus->getFont().getStringWidth(mapManualStatus->getText()) + 8, mapManualStatus->getHeight());
    resized();
}

void PluginControlComponent::keyMapConfirmed(int keyNumber, int midiNote)
{
    mapManualStatus->setText("Key " + String(keyNumber) + " mapped to MIDI Note " + String(midiNote), sendNotification);
    mapManualCancel->setVisible(false);
    resized();
}

void PluginControlComponent::mappingEditorChanged(NoteMap& newMapping)
{
    DBG("MAPPING CHANGED");
    pluginState->setMidiInputMap(newMapping, mapModeBox->getSelectedId() == 3);
}

//==============================================================================

VirtualKeyboard::Keyboard* PluginControlComponent::getKeyboard()
{
    return keyboard.get();
}

Viewport* PluginControlComponent::getViewport()
{
    return  keyboardViewport.get();
}

TextEditor* PluginControlComponent::getScaleTextEditor()
{
    return scaleTextBox.get();
}

ComboBox* PluginControlComponent::getMappingStyleBox()
{
    return mapStyleBox.get();
}

String PluginControlComponent::getScaleEntryText()
{
    return scaleTextBox->getText();
}

void PluginControlComponent::setScaleEntryText(String textIn, NotificationType notify)
{
    scaleTextBox->setText(textIn, notify == sendNotification);
}

ReferencedComboBox* PluginControlComponent::getMode1Box()
{
    return mode1Box.get();
}

ReferencedComboBox* PluginControlComponent::getMode2Box()
{
    return mode2Box.get();
}

void PluginControlComponent::updateModeBoxMenus()
{

}

ImageButton* PluginControlComponent::getSettingsButton()
{
    return settingsButton.get();
}

TextButton* PluginControlComponent::getModeInfoButton()
{
    return modeInfoButton.get();
}

int PluginControlComponent::getModeSelectorViewed()
{
    return mode2ViewBtn->getToggleState();
}

void PluginControlComponent::setMapModeBoxId(int mappingModeId, NotificationType notify)
{
    mapModeBox->setSelectedId(mappingModeId, dontSendNotification);

    if (notify != dontSendNotification)
        pluginState->setMapMode(mapModeBox->getSelectedId());
}

void PluginControlComponent::setMappingStyleId(int idIn, NotificationType notify)
{
    mapStyleBox->setSelectedId(idIn, dontSendNotification);

    if (idIn == 3 && mapModeBox->getSelectedId() == 2)
    {
        mapOrderEditBtn->setVisible(true);
        mapApplyBtn->setVisible(true);
    }
    else
    {
        mapOrderEditBtn->setVisible(false);
        mapApplyBtn->setVisible(false);
    }

    if (notify != dontSendNotification)
        pluginState->setAutoMapStyle(mapStyleBox->getSelectedId());

    resized();
}

void PluginControlComponent::submitCustomScale()
{
    pluginState->setModeCustom(scaleTextBox->getText());
}

void PluginControlComponent::updateRootNoteLabels()
{
    mode1RootLbl->setText(MidiMessage::getMidiNoteName(mode1RootSld->getValue(), true, true, 4), dontSendNotification);
    mode2RootLbl->setText(MidiMessage::getMidiNoteName(mode2RootSld->getValue(), true, true, 4), dontSendNotification);
}

//==============================================================================

void PluginControlComponent::showModeInfo()
{
    modeInfo  = new ModeInfoDialog(pluginState->getModeViewed());
    modeInfo->addChangeListener(this);
    CallOutBox::launchAsynchronously(std::unique_ptr<Component>(modeInfo), scaleTextBox->getScreenBounds(), nullptr);
}

void PluginControlComponent::showMapOrderEditDialog()
{
    mapByOrderDialog = new MapByOrderDialog(pluginState->getModeMapper(), pluginState->getMode1(), pluginState->getMode2());
    CallOutBox::launchAsynchronously(std::unique_ptr<Component>(mapByOrderDialog), mapStyleBox->getScreenBounds(), nullptr);
}

void PluginControlComponent::showSettingsDialog()
{
    if (!settingsPanelOpen)
    {
        settingsContainer.reset(new SettingsContainer(pluginState));
        settingsContainer->setKeyboardPointer(keyboard.get());
        settingsContainer->addListener(this);
        pluginState->addListener(settingsContainer.get());

        settingsPanelOpen = true;
        pluginState->getPluginEditorNode().setProperty(IDs::settingsOpen, true, nullptr);
        settingsButton->setToggleState(true, dontSendNotification);

        addAndMakeVisible(settingsContainer.get());
        getParentComponent()->setSize(getWidth(), getHeight() + defaultHeight);

        int setToTab = 0;
        String lastTab = pluginState->getPluginEditorNode()[IDs::settingsTabName].toString();
        DBG("Last tab: " + lastTab);
        if (lastTab.length() > 0)
            setToTab = settingsContainer->getTabNames().indexOf(lastTab);

        settingsContainer->setCurrentTabIndex(setToTab, true);
    }
    else
    {
        hideSettings();
    }
}

void PluginControlComponent::hideSettings()
{
    settingsContainer->removeListener(this);
    settingsContainer->setVisible(false);
    pluginState->removeListener(settingsContainer.get());
    settingsContainer = nullptr;

    settingsPanelOpen = false;
    pluginState->getPluginEditorNode().setProperty(IDs::settingsOpen, false, nullptr);

    if (isColorEditing)
        endColorEditing();

    getParentComponent()->setSize(getWidth(), getHeight() - defaultHeight);
}

void PluginControlComponent::beginColorEditing()
{
    keyboard->setUIMode(VirtualKeyboard::UIMode::editMode);
    isColorEditing = true;

    keyMappingStatusChanged(-1, false);
    mapModeBox->setEnabled(false);
    mapStyleBox->setEnabled(false);
    mapManualResetBtn->setEnabled(false);
    mapOrderEditBtn->setEnabled(false);
    mapApplyBtn->setEnabled(false);
    mapCopyToManualBtn->setEnabled(false);
}

void PluginControlComponent::endColorEditing()
{
    keyboard->setUIMode(VirtualKeyboard::UIMode::playMode);
    isColorEditing = false;

    mapModeBox->setEnabled(true);
    mapStyleBox->setEnabled(true);
    mapManualResetBtn->setEnabled(true);
    mapOrderEditBtn->setEnabled(true);
    mapApplyBtn->setEnabled(true);
    mapCopyToManualBtn->setEnabled(true);

    // Restore previous UI state
    pluginState->setMapMode(mapModeBox->getSelectedId());
}

void PluginControlComponent::beginManualMapping()
{
    mapManualStatus->setText(noKeySelectedTrans, sendNotification);

    mapStyleBox->setVisible(false);
    mapStyleLbl->setVisible(false);
    mapCopyToManualBtn->setVisible(false);

    mapManualTip->setVisible(true);
    mapManualStatus->setVisible(true);
    mapManualResetBtn->setVisible(true);

    manualMappingHelper = pluginState->getManualMappingHelper();
    manualMappingHelper->addListener(this);

    keyboard->setUIMode(VirtualKeyboard::UIMode::mapMode);

    // Selects the key to map a MIDI note to
    keyboard->setMappingHelper(manualMappingHelper);
}

void PluginControlComponent::setMode1Root(int rootIn, NotificationType notify)
{
    mode1RootSld->setValue(rootIn, notify);
    mode1RootLbl->setText(MidiMessage::getMidiNoteName(rootIn, true, true, 4), dontSendNotification);
}

void PluginControlComponent::setMode2Root(int rootIn, NotificationType notify)
{
    mode2RootSld->setValue(rootIn, notify);
    mode2RootLbl->setText(MidiMessage::getMidiNoteName(rootIn, true, true, 4), dontSendNotification);
}

bool PluginControlComponent::browseForModeToOpen()
{
    ValueTree modeNode = presetManager->nodeFromFile("Open Mode", "*.svk", pluginState->getPluginSettings()->getModePath());

    if (Mode::isValidMode(modeNode))
    {
        presetManager->addSlotAndSetSelection(getModeSelectorViewed(), modeNode);
        onModeViewedChange(pluginState->getModeViewed());
        return true;
    }

    return false;
}

bool PluginControlComponent::browseForPresetToOpen()
{
    ValueTree presetLoaded = presetManager->presetFromFile(pluginState->getPluginSettings()->getPresetPath());
    pluginState->recallState(presetLoaded);
    return true;
}

bool PluginControlComponent::exportModeViewedForReaper()
{
    ReaperWriter rpp = ReaperWriter(pluginState->getModeViewed());
    return rpp.write();
}

bool PluginControlComponent::exportModeViewedForAbleton()
{
    AbletonMidiWriter amw(*pluginState->getModeViewed());
    return amw.write();
}

void PluginControlComponent::exportKbmMapping()
{
    DialogWindow::LaunchOptions options;
    options.dialogTitle = "Export KBM file";
    options.escapeKeyTriggersCloseButton = true;
    options.componentToCentreAround = this;
    options.content = OptionalScopedPointer<Component>(new ExportKbmDialog(
        pluginState->getMappingNode(),
        *pluginState->getMode1(), *pluginState->getMode2(), *pluginState->getModeMapper()), true
        );
    options.content->setSize(316, 112);

    if (JUCEApplication::isStandaloneApp())
        options.runModal();
    else
        options.launchAsync();
}


text:
/*
  ==============================================================================

    DirectoryBrowserComponent.h
    Created: 5 Sep 2020 9:59:39pm
    Author:  Vincenzo

    Simple combination of a text editor and button to trigger browsing for a
    directory and then displaying the full directory path.

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

#pragma once
#include <JuceHeader.h>

class DirectoryBrowserComponent : public Component, public Button::Listener
{
public:

    DirectoryBrowserComponent(const String dialogBoxTitle, const File& directoryIn = File())
        : chooser(dialogBoxTitle, directoryIn)
    {
        setName(dialogBoxTitle);

        editor.reset(new TextEditor(dialogBoxTitle + "Editor"));
        editor->setMultiLine(false, false);
        editor->setReadOnly(true);
        addAndMakeVisible(editor.get());

        openButton.reset(new TextButton(dialogBoxTitle + "Button", "Choose directory for " + dialogBoxTitle));
        openButton->setButtonText("...");
        openButton->setConnectedEdges(Button::ConnectedEdgeFlags::ConnectedOnLeft);
        addAndMakeVisible(openButton.get());
        openButton->addListener(this);
    }

    ~DirectoryBrowserComponent()
    {
        editor = nullptr;
        openButton = nullptr;
    }

    void resized() override
    {
        openButton->setSize(getHeight(), getHeight());
        openButton->setTopRightPosition(getWidth() - 1, 0);
        editor->setBounds(0, 0, getWidth() - getHeight(), getHeight());
    }

    void buttonClicked(Button* buttonThatWasClicked) override
    {
        if (chooser.browseForDirectory())
        {
            editor->setText(chooser.getResult().getFullPathName(), false);
            listeners.call(&DirectoryBrowserComponent::Listener::directoryChanged, this, chooser.getResult());
        }
    }

    void setText(String textIn)
    {
        editor->setText(textIn);
    }

    class Listener
    {
    public:

        Listener() {};
        virtual ~Listener() {};

        virtual void directoryChanged(DirectoryBrowserComponent* source, File directorySelected) = 0;
    };

    void addListener(DirectoryBrowserComponent::Listener* listenerIn)
    {
        listeners.add(listenerIn);
    }

    void removeListener(DirectoryBrowserComponent::Listener* listenerIn)
    {
        listeners.remove(listenerIn);
    }

protected:

    ListenerList<DirectoryBrowserComponent::Listener> listeners;

private:

    FileChooser chooser;

    std::unique_ptr<TextEditor> editor;
    std::unique_ptr<TextButton> openButton;

    std::function<void(const File&)> directorySetCallback;
};


text:
/*
  ==============================================================================

    MappingSettingsPanel.h
    Created: 7 Oct 2020 10:00:18pm
    Author:  Vincenzo

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

#pragma once

#include "SvkSettingsPanel.h"
#include "../NoteMapEditor.h"

class MappingSettingsPanel : public SvkSettingsPanel,
                             public SvkPluginState::Listener,
                             public MappingEditor::Listener
{
public:

    MappingSettingsPanel(SvkPluginState* pluginStateIn);
    ~MappingSettingsPanel();

    void visibilityChanged() override;

    void mappingModeChanged(int mappingModeId) override;

    void inputMappingChanged(NoteMap&) override;

    void mappingEditorChanged(NoteMap&) override;

    void registerEditorListener(MappingEditor::Listener* listenerIn);

private:

    Label* currentMappingLabel;
    NoteMapEditor* noteMapEditor;
};


text:
/*
  ==============================================================================

    SettingsContainer.h
    Created: 13 Nov 2019 7:13:39pm
    Author:  Vincenzo Sicurella

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

#pragma once
#include "../../../PluginState.h"

#include "GeneralSettingsPanel.h"
#include "MidiSettingsPanel.h"
#include "KeyboardSettingsPanel.h"
#include "MappingSettingsPanel.h"
#include "ColourSettingsPanel.h"
//#include "ControlSettingsPanel.h"
#include "DebugSettingsPanel.h"

//==============================================================================
/*
*/
class SettingsContainer : public TabbedComponent, public SvkPluginState::Listener
{
public:

    SettingsContainer(SvkPluginState* pluginStateIn)
    : TabbedComponent(TabbedButtonBar::Orientation::TabsAtTop),
        pluginState(pluginStateIn)
    {
        tabColour = Colour(0xff323e44);
        backgroundColour = tabColour.darker();
        textColour = tabColour.contrasting();

        setColour(backgroundColourId, backgroundColour);

        //view.reset(new Viewport("SettingsViewport"));
        //view->setScrollOnDragEnabled(true);
        //view->setScrollBarsShown(true, false);
        //addAndMakeVisible(view.get());

        for (auto panelName : panelNames)
        {
            if (panelName == "General")
                panels.add(new GeneralSettingsPanel(pluginState));

            else if (panelName == "Midi")
                panels.add(new MidiSettingsPanel(pluginState));

            else if (panelName == "Keyboard")
                panels.add(new KeyboardSettingsPanel(pluginState));

            else if (panelName == "Mapping")
                panels.add(new MappingSettingsPanel(pluginState));

            else if (panelName == "Colors")
                panels.add(new ColourSettingsPanel(pluginState));

            else if (panelName == "Debug")
                panels.add(new DebugSettingsPanel(pluginState));
        }

        for (int i = 0; i < panels.size(); i++)
        {
            addTab(panelNames[i], tabColour, panels[i], true);
        }

        setSize(100, 100);
    }

    ~SettingsContainer()
    {
        view = nullptr;
    }

    SvkSettingsPanel* getComponentViewed()
    {
        return componentViewed;
    }

    void currentTabChanged (int newCurrentTabIndex, const String &newCurrentTabName) override
    {
        componentViewed = panels.getUnchecked(newCurrentTabIndex);
        componentViewed->resized();

        listeners.call(&SettingsContainer::Listener::settingsTabChanged,
            newCurrentTabIndex,
            newCurrentTabName,
            componentViewed
        );
    }

    void setKeyboardPointer(VirtualKeyboard::Keyboard* keyboardPtrIn)
    {
        for (int i = 0; i < panelNames.size(); i++)
        {
            if (keyboardPanels.contains(panelNames[i]))
                panels[i]->setKeyboardPointer(keyboardPtrIn);
        }
    }

    void modeViewedChanged(Mode* modeIn, int selectorNumber, int slotNumber) override
    {
        for (auto p : panels)
        {
            if (p->getName() == "ColourSettingsPanel")
                p->refreshPanel();
        }
    }

public:

    class Listener
    {
    public:
        virtual void settingsTabChanged(int newTabIndex, const String& tabName, SvkSettingsPanel* panelChangedTo) = 0;
    };

    void addListener(SettingsContainer::Listener* listenerIn) { listeners.add(listenerIn); }

    void removeListener(SettingsContainer::Listener* listenerIn) { listeners.remove(listenerIn); }

protected:

    ListenerList<Listener> listeners;

private:

    SvkPluginState* pluginState;

    Array<SvkSettingsPanel*> panels;
    std::unique_ptr<Viewport> view;

    SvkSettingsPanel* componentViewed = nullptr;

    int defaultControlHeight = 50;

    Colour backgroundColour;
    Colour tabColour;
    Colour textColour;

    StringArray panelNames =
    {
        "General"
        , "Midi"
        , "Keyboard"
        , "Mapping"
        , "Colors"
//#if JUCE_DEBUG
//        , "Debug"
//#endif
    };

    StringArray keyboardPanels = { "Keyboard", "Colors", "Debug" };

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SettingsContainer)
};


text:
/*
  ==============================================================================

    ColourSettingsPanel.cpp
    Created: 13 Sep 2020 2:41:09pm
    Author:  Vincenzo

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

#include "ColourSettingsPanel.h"

ColourSettingsPanel::ColourSettingsPanel(SvkPluginState* stateIn)
    : SvkSettingsPanel("ColourSettingsPanel", stateIn,
        {
            "",
            "Choose settings then click keys to paint:",
            "Select a box to adjust the color:",
            ""
        },
        {
            IDs::pianoKeyColor,
            IDs::pianoKeyColorsDegree,
            IDs::pianoKeyColorsLayer,
            IDs::pianoKeyColorsIndividual,
            IDs::pianoKeyColorReset,
            Identifier("KeyColourLibrary"),
            Identifier("NoteOnColourLibrary")
        },
        {
            SvkControlProperties(ControlTypeNames::GenericControl, "Color Selector"),
            SvkControlProperties(ControlTypeNames::ToggleControl, "Paint by Scale Degree", false, 1),
            SvkControlProperties(ControlTypeNames::ToggleControl, "Paint by Layer (hold Shift)", false, 1),
            SvkControlProperties(ControlTypeNames::ToggleControl, "Paint by Individual Key (hold Ctrl)", false, 1),
            SvkControlProperties(ControlTypeNames::ToggleControl, "Reset Color to Default (Right-click)", false, 1),
            SvkControlProperties(ControlTypeNames::GenericControl, "Key Colors", false, 2),
            SvkControlProperties(ControlTypeNames::GenericControl, "Note On Colors", false, 3)
        },
        FlexBox(), {}, { FlexItem() }
    )
{
    // Set up Color Selector and replace generic control
    colourSelector = new FocussedColourSelector();
    addAndMakeVisible(colourSelector);
    controls.set(0, colourSelector, true);
    getSectionFlexBox(0)->items.getReference(0).associatedComponent = colourSelector;

    degreePaintButton = static_cast<TextButton*>(idToControl[IDs::pianoKeyColorsDegree]);
    degreePaintButton->setRadioGroupId(paintTypeRadioGroup, dontSendNotification);
    degreePaintButton->setToggleState(true, dontSendNotification);
    degreePaintButton->addListener(this);

    layerPaintButton = static_cast<TextButton*>(idToControl[IDs::pianoKeyColorsLayer]);
    layerPaintButton->setRadioGroupId(paintTypeRadioGroup, dontSendNotification);
    layerPaintButton->addListener(this);

    keyPaintButton = static_cast<TextButton*>(idToControl[IDs::pianoKeyColorsIndividual]);
    keyPaintButton->setRadioGroupId(paintTypeRadioGroup, dontSendNotification);
    keyPaintButton->addListener(this);

    resetColourToggle = static_cast<TextButton*>(idToControl[IDs::pianoKeyColorReset]);
    resetColourToggle->addListener(this);

    flexParent.items.getReference(numSections - 1).margin = FlexItem::Margin(0, 5, 0, 0);

    addMouseListener(this, true);

    setSize(100, 100);
}

ColourSettingsPanel::~ColourSettingsPanel()
{
    colourSelector = nullptr;

    if (virtualKeyboard)
        virtualKeyboard->removeMouseListener(this);
}

void ColourSettingsPanel::resized()
{
    getSectionFlexBox(0)->items.getReference(0).height = getHeight();
    SvkSettingsPanel::resized();
}

void ColourSettingsPanel::buttonClicked(Button* buttonThatWasClicked)
{
    if (buttonThatWasClicked == degreePaintButton)
    {
        virtualKeyboard->getProperties().set(IDs::pianoKeyPaintType, 0);
    }
    else if (buttonThatWasClicked == layerPaintButton)
    {
        virtualKeyboard->getProperties().set(IDs::pianoKeyPaintType, 1);
    }
    else if (buttonThatWasClicked == keyPaintButton)
    {
        virtualKeyboard->getProperties().set(IDs::pianoKeyPaintType, 2);
    }

    if (buttonThatWasClicked == resetColourToggle)
    {
        virtualKeyboard->getProperties().set(IDs::pianoKeyColorReset, buttonThatWasClicked->getToggleState());
        userClickedReset = buttonThatWasClicked->getToggleState();
    }
}

void ColourSettingsPanel::modifierKeysChanged(const ModifierKeys& modifiers)
{
    checkModifiers(modifiers);
}

void ColourSettingsPanel::mouseMove(const MouseEvent& event)
{
    if (!virtualKeyboard->isMouseOver())
        checkModifiers(event.mods);
}

void ColourSettingsPanel::mouseDown(const MouseEvent& event)
{
    if (event.mods.isRightButtonDown())
    {
        resetColourToggle->setToggleState(true, dontSendNotification);
        userClickedReset = false;
    }

    // TODO: allow one click on virtual keyboard to set focus before painting?
    if (event.eventComponent->getParentComponent() == virtualKeyboard)
    {
        colourSelector->setComponentToFocusOn(virtualKeyboard);

        // TODO: better messaging for VirtualKeyboard changes
        virtualKeyboard->updateKeyColors(false);
        keyColourLibrary->refreshSwatches();
    }

    else if (colourSelector->isMouseOver() || event.eventComponent->getParentComponent() == colourSelector)
        return;

    else if (dynamic_cast<FocusableComponent*>(event.eventComponent))
    {
        colourSelector->setComponentToFocusOn(event.eventComponent);

        // TODO: find out why this isn't being done in ColourSelector focusser callback
        virtualKeyboard->getProperties().set(IDs::colorSelected, colourSelector->getCurrentColour().toString());
    }

    else
        colourSelector->setComponentToFocusOn(nullptr);
}

void ColourSettingsPanel::mouseUp(const MouseEvent& event)
{
    if (!userClickedReset && resetColourToggle->getToggleState())
        resetColourToggle->setToggleState(false, dontSendNotification);
}

void ColourSettingsPanel::setKeyboardPointer(VirtualKeyboard::Keyboard* keyboardPointer)
{
    virtualKeyboard = keyboardPointer;

    Colour init = Colour::fromString(virtualKeyboard->getProperties()[IDs::colorSelected].toString());
    if (init.isTransparent())
        init = virtualKeyboard->getKeyLayerColor(0);
    colourSelector->setCurrentColour(init, dontSendNotification);

    // fill key color swatches
    keyColourLibrary = new ColourLibraryComponent(
        { "Key Layer Colors:",
            "Scale Degree Colors:",
            "Individual Key Colors:" },
        { virtualKeyboard->getKeyLayerColours(),
          virtualKeyboard->getKeyDegreeColours(),
          virtualKeyboard->getKeyIndividualColours() }
        , true, true
    );
    keyColourLibrary->setName("KeyColourLibrary");
    keyColourLibrary->setColour(ColourLibraryComponent::ColourIds::backgroundColourId, Colour(Colour(0xff323e44).brighter(0.125f)));
    controls.set(5, keyColourLibrary, true);
    addAndMakeVisible(keyColourLibrary);
    getSectionFlexBox(2)->items.getReference(1).associatedComponent = keyColourLibrary;

    // fill note on color swatches
    noteOnColourLibrary = new ColourLibraryComponent({ "Midi Channel Note On Colors:" }, { virtualKeyboard->getKeyNoteOnColours() }, true);
    noteOnColourLibrary->setName("NoteOnColourLibrary");
    noteOnColourLibrary->setColour(ColourLibraryComponent::ColourIds::backgroundColourId, Colour(Colour(0xff323e44).brighter(0.125f)));
    noteOnColourLibrary->forceSwatchColumns(8);
    controls.set(6, noteOnColourLibrary, true);
    addAndMakeVisible(noteOnColourLibrary);
    getSectionFlexBox(3)->items.getReference(0).associatedComponent = noteOnColourLibrary;

    // callback for setting color in keyboard
    colourSelector->setFocusserCallback([&](Component* c, var dataIn)
    {
        if (dynamic_cast<PaintSwatch*>(c))
            virtualKeyboard->updateKeyColors();
        else
            virtualKeyboard->getProperties().set(IDs::colorSelected, dataIn);
    });

    virtualKeyboard->addMouseListener(this, true);
}

void ColourSettingsPanel::refreshPanel()
{
    if (keyColourLibrary)
        keyColourLibrary->refreshSwatches();
}

ColourSelector* ColourSettingsPanel::getColourSelector()
{
    return colourSelector;
}

void ColourSettingsPanel::checkModifiers(const ModifierKeys& modifiers)
{
    bool valueChanged = true;

    if (modifiers.isCtrlDown() && !ctrlHeld)
        ctrlHeld = true;

    else if (ctrlHeld && !modifiers.isCtrlDown())
        ctrlHeld = false;

    else if (modifiers.isShiftDown() && !shiftHeld)
        shiftHeld = true;

    else if (shiftHeld && !modifiers.isShiftDown())
        shiftHeld = false;

    else
        valueChanged = false;

    if (valueChanged)
    {
        if (ctrlHeld)
            keyPaintButton->setToggleState(true, sendNotification);

        else if (shiftHeld)
            layerPaintButton->setToggleState(true, sendNotification);

        else
            degreePaintButton->setToggleState(true, sendNotification);
    }
}