JUCE 小ネタ 〜 ComboBox 用ユーティリティ 〜

# はじめに

　JUCE の ComboBox クラスを利用すると、いくつかの選択肢をドロップダウンリストで表示して、それをユーザーに選択させることができます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/944/b8d3021c-c7c3-80d2-b2ba-e4e415cfb7b9.png" width="400px" alt="ComboBoxImage"></img>

　ただし、JUCE の ComboBox クラスは、そのリストの各エントリが何のデータを表すかについては関知せず、単に文字列としてのデータしか扱うことができません。実際のアプリケーションでは、 ComboBox で選択したエントリとデータを紐付けて扱いたいことがあるため、そのための仕組みをプログラマが用意する必要があります。

　そのような仕組みを何度も実装するのは面倒なので、僕は最近 `ComboBoxItemList` というユーティリティクラスを作って、その問題に対処しています。今回はそれについて紹介します。

## ComboBoxItemList クラスについて

ComboBoxItemList クラスはクラステンプレートになっていて、下のような API を持っています。（完全なクラス定義はこのページ下部のソースコードに書いてあります）

```cpp
/** ComboBox に設定するエントリを管理するクラス

    @tparam T ComboBox 上のエントリと紐付けて扱いたいデータの型
*/
template <class T>
struct ComboBoxItemList
{
    struct Entry
    {
        T value;            // データの値
        juce::String name;  //  ComboBox 上で表示する文字列
    };

    /** デフォルトコンストラクタ
    */
    ComboBoxItemList();

    /** コンストラクタ

        @param entries ComboBox に設定したいエントリ列
    */
    ComboBoxItemList (std::initializer_list<Entry> entries);

    /** 指定した ComboBox にエントリ列を適用する。

        @note この関数呼び出し後に ComboBox のエントリを追加／削除／入れ替えなどを行わないこと。
        それをした場合、 ComboBoxItemList で管理しているエントリの情報と整合性が取れなくなって、正しい結果を取得できなくなる。
    */
    void applyTo (juce::ComboBox& cb) const;

    /** 指定された値に対応する ComboBox 上での id を返す
    */
    int valueToId (const T& value) const;

    /** ComboBox 上での id に対応する値を返す
    */
    T idToValue (int id) const;
};
```

　ComboBoxItemList クラスのコンストラクタに Entry の配列を渡してオブジェクトを構築し、 applyTo() メンバ関数に ComboBox を渡すと、その ComboBox に対して、コンストラクタに渡したエントリが設定されます。 idToValue()／valueToId() メンバ関数を呼び出すと、 ComboBox 上での id とデータを相互に変換できます。

## 利用例

　このクラスの利用例を見てみます。以下のような enum 型があって、この enum 型の値を ComboBox と紐付けて扱いたい状況だとします。

```cpp
/** 何らかのデータ型
*/
enum class FilterType
{
    lowPass,
    highPass,
    lowShelf,
    highShelf
};
```

　このとき、 ComboBoxItemList は以下のように初期化します。つまり、 `ComboBoxItemList` のテンプレート引数には扱いたいデータの型を渡しておき、コンストラクタには扱いたいデータの値とそれの表示用の文字列のペアの配列にしたものを渡します。

```cpp
ComboBoxItemList<FilterType> filterTypes {
   { FilterType::lowPass, "LowPass" },
   { FilterType::highPass, "HighPass" },
   { FilterType::lowShelf, "LowShelf" },
   { FilterType::highShelf, "HighShelf" },
};
```

　コンポーネント側では、上で構築した filterTypes を使用して ComboBox にエントリを設定したり、 ComboBox で選択している要素の id から FilterType 型のデータを習得したりできます。

```cpp
class MyComponent
:   public juce::Component
{
public:
    /** ComboBox で最初に選択された状態にしておくフィルタタイプ
    */
    static
    FilterType getInitialFilterType() { return FilterType::lowPass; }

    /** コンストラクタ
    */
    MainComponent()
    {
        addAndMakeVisible(cbFilterType);

        // ComboBoxItemList<T>::applyTo() メンバ関数に ComboBox を渡して、
        // 事前に登録したエントリを ComboBox に設定する。
        filterTypes.applyTo(cbFilterType);

        // ComboBox の選択状態が変わったときに呼ばれるコールバック
        cbFilterType.onChange = [=] {
            // ComboBox の現在の選択状態をもとに、それに紐付いているデータを取得する。
            FilterType ft = filterTypes.idToValue(cbFilterType.getSelectedId());

            // 取得したデータをもとになにかする。
            doSomething(ft);
        };

        // データをもとに、ComboBox の選択状態を設定する。
        cbFilterType.setSelectedId(filterTypes.valueToId(getInitialFilterType()));

        setSize(600, 400);
    }

private:
    juce::ComboBox cbFilterType;

    // ...
};
```

## クラス定義とサンプルコード

　`ComboBoxItemList` クラスの完全な定義を含むサンプルコードを以下に載せます。このコードは JUCE の PIP 形式になっているため、このコードを `*.h` ファイルとして保存し Projucer で開けば、このプログラムをビルドするためのプロジェクトファイルが生成できます。（JUCE 6.0.4 で動作確認しています）

```cpp:ComboBoxItemListDemo.h
/*******************************************************************************
 The block below describes the properties of this PIP. A PIP is a short snippet
 of code that can be read by the Projucer and used to generate a JUCE project.

 BEGIN_JUCE_PIP_METADATA

 name:             ComboBoxItemListDemo
 version:          1.0.0
 vendor:           @hotwatermorning
 website:          https://diatonic.jp
 description:      Demo application of ComboBoxItemList.

 dependencies:     juce_core, juce_data_structures, juce_events, juce_graphics,
                   juce_gui_basics
 exporters:        xcode_mac, vs2019, linux_make, xcode_iphone

 type:             Component
 mainClass:        MainComponent

 useLocalCopy:     0

 END_JUCE_PIP_METADATA

*******************************************************************************/

#pragma once

#include <algorithm>
#include <vector>

/** 何らかのデータ型
*/
enum class FilterType
{
    lowPass,
    highPass,
    lowShelf,
    highShelf
};

/** ComboBox に設定するエントリを管理するクラス

    @tparam T ComboBox 上のエントリと紐付けて扱いたいデータの型
*/
template <class T>
struct ComboBoxItemList
{
    struct Entry
    {
        T value;            // データの値
        juce::String name;  // ComboBox 上で表示する文字列
    };

    /** デフォルトコンストラクタ
    */
    ComboBoxItemList()
    {
    }

    /** コンストラクタ

        @param entries ComboBox　に設定したいエントリ列
    */
    ComboBoxItemList (std::initializer_list<Entry> entries)
        : entries (entries)
    {
#if JUCE_DEBUG
        auto tmp = this->entries;
        std::sort (tmp.begin(), tmp.end(), [] (const auto& lhs, const auto& rhs) { return lhs.value < rhs.value; });
        // 誤って同じ値を持つ Entry が追加されていないかどうかをチェック。
        // 同じ値を持つ Entry が存在すると valueToId() の意味が曖昧になるため、そのようなデータは許容しない。
        auto it = std::unique (tmp.begin(), tmp.end(), [] (const auto& lhs, const auto& rhs) { return lhs.value == rhs.value; });
        jassert (it == tmp.end());
#endif
    }

    /** 指定した ComboBox にエントリ列を適用する。

        @note この関数呼び出し後に `cb` のエントリを追加／削除／入れ替えなどしないこと。
        それをした場合、 ComboBoxItemList で管理しているエントリの情報と整合性が取れなくなって、 ComboBoxItemList から正しい結果を取得できなくなる。
    */
    void applyTo (juce::ComboBox& cb) const
    {
        cb.clear();

        int id = 1;
        for (auto&& entry : entries)
        {
            cb.addItem (entry.name, id);
            id++;
        }
    }

    /** 指定された値に対応する ComboBox 上での id を返す

        @return いずれかのエントリが選択されている場合はその id を返す。（id の値は 1　以上になる）
        未選択状態の場合は 0 が返る。
    */
    int valueToId (const T& value) const
    {
        auto found = std::find_if (entries.begin(), entries.end(), [value] (const auto& entry) { return entry.value == value; });
        if (found == entries.end())
        {
            return 0;
        }

        return static_cast<int> (found - entries.begin() + 1);
    }

    /** ComboBox 上での id に対応する値を返す
    */
    T idToValue (int id) const
    {
        const auto index = id - 1;
        jassert (0 <= index && index < entries.size());

        return entries[index].value;
    }

private:
    std::vector<Entry> entries;
};

ComboBoxItemList<FilterType> filterTypes {
   { FilterType::lowPass, "LowPass" },
   { FilterType::highPass, "HighPass" },
   { FilterType::lowShelf, "LowShelf" },
   { FilterType::highShelf, "HighShelf" },
};

class MainComponent
:   public juce::Component
{
public:
    /** ComboBox で最初に選択された状態にしておくフィルタタイプ
    */
    static
    FilterType getInitialFilterType() { return FilterType::lowPass; }

    /** コンストラクタ
    */
    MainComponent()
    {
        addAndMakeVisible(cbFilterType);

        // ComboBoxItemList<T>::applyTo() メンバ関数に ComboBox を渡して、
        // 事前に登録したエントリを ComboBox に設定する。
        filterTypes.applyTo(cbFilterType);

        // ComboBox の選択状態が変わったときに呼ばれるコールバック
        cbFilterType.onChange = [this] {
            // ComboBox の現在の選択状態をもとに、それに紐付いているデータを取得する。
            FilterType ft = filterTypes.idToValue(cbFilterType.getSelectedId());

            // 取得したデータをもとになにかする。
            doSomething(ft);
        };

        // データをもとに、 ComboBox の選択状態を設定する。
        cbFilterType.setSelectedId(filterTypes.valueToId(getInitialFilterType()));

        setSize(600, 400);
    }

private:
    juce::ComboBox cbFilterType;

    void resized() override
    {
        auto b = juce::Rectangle<int>(0, 0, 200, 30).withCentre(getBounds().getCentre());
        cbFilterType.setBounds(b);
    }

    void doSomething(FilterType ft)
    {
        // デバッグ出力に ComboBox の選択結果を表示する。
        switch(ft) {
            case FilterType::lowPass:   DBG("LowPass is selected."); return;
            case FilterType::highPass:  DBG("HighPass is selected."); return;
            case FilterType::lowShelf:  DBG("LowShelf is selected."); return;
            case FilterType::highShelf: DBG("HighShelf is selected."); return;
        }
    }

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
};
```