juce::PreferencePanelを使ってみる


https://docs.juce.com/master/classPreferencesPanel.html#a8ab89869d859f9ef7493f5bdf10ee9aa

JUCEドキュメントサーフィンをしていてjuce::PreferencePanelというのを見つけて使ってみたメモになります。

# 概要

Macネイティブアプリケーションの環境設定パネルのように、上部にアイコンボタンがありそれをクリックすることでページを切り替えられるコンポーネントです。
また名前の通り設定パネルで使われることを想定してか、ウインドウとして表示する関数を持っているため、juce::Component::addAndMakeVisible()をしなくても使用可能です。

# サンプルコード

https://github.com/migizo/PreferencePanelTest

# 簡易的に使いたい場合
![スクリーンショット 2022-11-26 23.24.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136558/da3ad9b5-fa2b-31bf-ef50-2bada5ec5c5b.png)

createComponentForPage()という純粋仮想関数を持つため継承クラスを用意する必要があります。
このcreateComponentForPage()にはページ毎に表示したいコンポーネントのポインタを返すよう実装します。
addSettingsPanel()でページ名とアイコン情報を指定することが可能です。

```c++
class SimplePreferencePanel : public juce::PreferencesPanel
{
public:
    // 各ページを追加
    SimplePreferencePanel()
    {
        addSettingsPage("Page 1", image1, image1Size);
        addSettingsPage("Page 2", image2, image2Size);
        addSettingsPage("Page 3", image3, image3Size);
    }
    
    // ページ名に対応するコンポーネントを返す
    juce::Component* createComponentForPage (const juce::String& pageName) override
    {
        if (pageName == "Page 1")       return new juce::Label("Page 1", "Page 1");
        else if (pageName == "Page 2")  return new juce::Label("Page 2", "Page 2");
        else if (pageName == "Page 3")  return new juce::Label("Page 3", "Page 3");
        return nullptr;
    }
    //==============================================================================
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SimplePreferencePanel)
};
```
使用側ではaddAndMakeVisible()で追加するか
showInDialogBox(ウインドウ名,ウインドウ横幅,ウインドウ縦幅)でウインドウを表示します。
showInDialogBoxはstatic関数ではないので注意。(独自にstaticな関数で用意しても良いかもしれません)

# 各種カスタマイズしたい場合の例
簡易的な部分しか用意されていないため、おそらくデフォルト状態でそのまま使う状況は少なく
各種カスタマイズ使用することになるかと思います。
![スクリーンショット 2022-11-26 23.26.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136558/3c3f9c68-2b31-9ec1-3718-700aba8c41c8.png)

## 例:ウインドウ
showInDialogBox()でウインドウを表示したい場合、
ウインドウ情報はこの関数内で定義されているため
例えばネイティブのタイトルバーでの表示したいといった場合は別の関数を用意する必要があります。

```c++
// 一部変更箇所以外はjuce::PreferencePanel::showInDialogBox()と同じ
void showInNativeDialogBox(const juce::String &dialogTitle, int dialogWidth, int dialogHeight, juce::Colour backgroundColour=juce::Colours::white)
{
    setSize (dialogWidth, dialogHeight);

    juce::DialogWindow::LaunchOptions o;
    o.content.setNonOwned (this);
    o.dialogTitle                   = dialogTitle;
    o.dialogBackgroundColour        = backgroundColour;
    o.escapeKeyTriggersCloseButton  = false;
    o.useNativeTitleBar             = true; // ここをtrueに変更
    o.resizable                     = false;

    o.launchAsync();
}
```

## 例:ボタン
### ボタン選択時
juce::PreferencePanel::addSettingsPage()関数では
通常時,hover時,選択時でそれぞれ別の画像表示を指定することが可能です。
ですが背景に関しての処理ではないため
そのまま使うと選択時にウィンドウ背景が反映されていない黒色になります。
これをウインドウ背景が反映されるようにするにはボタン選択時色に透明の黒を指定するようにします。

```c++
setColour(juce::DrawableButton::backgroundOnColourId, juce::Colours::black.withAlpha(0.5f));
```

juce::PreferencePanel派生クラスに対してsetColourを指定しても子として管理されているボタン自体には反映されません。
そのため子のボタンコンポーネントに対して指定します。

```c++:juce::PreferencePanel派生クラス内
void colourChanged() override
{
    auto target = juce::DrawableButton::backgroundOnColourId;
    auto colour = findColour(target);
    for (auto* child : getChildren())
    {
        child->setColour(target, colour);
    }
}
```
これで背景色が反映されるようになります。

### ボタンhover時
ちなみにhover時にもボタン背景を暗くしたい場合などはLookAndFeelで指定します。
juce::LookAndFeel_V2::DrawableButton()でボタン描画処理が行われているのでこちらを変更します。
(選択時背景色も上記の方法ではなく、こちらでまとめても良いかもしれません)
```c++:例: juce::LookAndFeel_V2派生クラス::DrawableButton()内
g.fillAll (button.findColour (toggleState ? juce::DrawableButton::backgroundOnColourId :
                                            juce::DrawableButton::backgroundColourId));
         
// hover追加
if(toggleState == false && shouldDrawButtonAsHighlighted)
    g.fillAll(juce::Colours::black.withAlpha(0.25f));

```

## 例:アニメーション
![preferencePanelAnimTest.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136558/06c5d4e0-ff50-718b-a1d3-b8b777a1b856.gif)
juce::ComponentAnimatorを使用して選択状態の半透明黒をアニメーションさせる場合以下のように実装します。

### hover時&選択時の描画状態を通常時と同じにする
```c++:例: juce::PreferencePanel派生クラスコンストラクタ内
// 独自に作成したaddCustomSettingsPage()を使用
addCustomSettingsPage("Page 1", image1, image1Size);
addCustomSettingsPage("Page 2", image2, image2Size);
addCustomSettingsPage("Page 3", image3, image3Size);

// 選択時の色を通常時の色と同じにする
setColour(juce::DrawableButton::backgroundOnColourId, findColour(juce::DrawableButton::backgroundColourId));
```

```c++
// hover,選択に関わらず同じ表示にする
void addCustomSettingsPage(const juce::String& title, const void* imageData, int imageDataSize)
{
    juce::DrawableImage icon;
    icon.setImage (juce::ImageCache::getFromMemory (imageData, imageDataSize));
    addSettingsPage (title, &icon, &icon, &icon);
}
```

### juce::Button::Listenerを継承し、クリック通知を受け取るようにする
```c++:例: juce::PreferencePanel派生クラスコンストラクタ内
// ボタンリスナーを登録
for (auto* child: getChildren())
{
    if (auto* button = dynamic_cast<juce::Button*>(child))
        button->addListener(this);
}
```

### 半透明黒を描画するコンポーネントを用意し、クリック時にアニメーションを開始する
```c++
// 半透明黒を描画するだけのコンポーネント
class AnimComp : public juce::Component
{
public:
    void paint(juce::Graphics& g) override
    {
        g.fillAll(juce::Colours::black.withAlpha(0.5f));
    }
};

// juce::PreferencePanel派生クラスのメンバとして定義
AnimComp animComp;
```

```c++
void buttonClicked (juce::Button* button) override
{
    // 対象コンポーネントの境界をクリックされたボタンの境界にアニメーションさせて変化させる
    juce::Desktop::getInstance().getAnimator().animateComponent(&animComp,
                                                                button->getBounds(),
                                                                1.0,    // finalAlpha
                                                                150,    // ms
                                                                false,  // アニメーション時に代理コンポーネントを使用するかどうか
                                                                        // trueにすると元のコンポーネント自体をペイントする必要が無くなるが、管理がややこしそうな場合はfalseに
                                                                1.0f,   // startSpeed,値を大きくするほど前半早くなる
                                                                1.0f);  // endSpeed,値を大きくするほど後半早くなる
}
```