【How to JUCE】juce::Component で自分以外の何かがクリックされた際に処理をしたい!


本記事はJUCE Advent Calendar 2021 の12月5日向けに投稿した記事です。

はじめに

こんにちは、Takacie です。
2021年も終わりに近づいてきましたね。
僕は先月、C++ のクロスプラットフォームアプリケーションのフレームワークである JUCE を用いたオーディオプラグイン "iR Reverb" を booth にてリリースしました。
その際に、パラメーター郡をプリセットとして読み書きするための独自のプリセット機能を作りました。

画像の "(default)" と書いてあるボタンをクリックすると、プリセット一覧が表示されるのですが、これを閉じるためにもう一度このボタンをクリックしなければならないのはユーザーエクスペリエンス的に良くないなと思いました。
そこで、ある juce::Component(以降 Component )を開いている状態で、その Component 以外をクリックしたときに処理(自分のケースだとプリセット一覧を閉じる処理)をしたいとき、どうすればよいのかを書いていこうと思います。

調べたこと

まず、当然と言えば当然なのですが、Component で定義されている Component::mouseDown(const MouseEvent&) などのマウスイベントには、自身以外をクリックしたときのイベントは存在しません。
どうしたものかと調べていると、JUCE のフォーラムで以下の投稿を見つけました。

[SOLVED] Hiding a component when a user clicks outside of its bounds -
https://forum.juce.com/t/solved-hiding-a-component-when-a-user-clicks-outside-of-its-bounds/41390

ここで回答として出されているのは「マウスリスナーの代わりに、該当のコンポーネントを表示する際に、その下にUI全体を覆う半透明のコンポーネントを一緒に表示し、後者をクリックした際に両方とも非表示にする」というものです。
確かに、僕が言っているような処理はできそうですが、そのようなコンポーネントを作る度に半透明のコンポーネントを用意するのも面倒なので、この案は却下しました。

他にも Desktop::addGlobalMouseListener(MouseListener*) を使うという手などもありましたが、該当コンポーネントがクリックされた際には処理しないなど、実装がややこしくなりそうなので、これも却下です。「UI のどこかがクリックされる度に該当コンポーネントの色を変化させたい」とか、そういうケースの場合は、この手法を使うといいのかなと思います。

Clicking outside component -
https://forum.juce.com/t/clicking-outside-component/18006

解決策

Component は Component::enterModalState(引数略) を実行することでモーダルな状態にすることができます。

juce::Component* some_component;
// ... //
some_component->enterModalState(); // some_componentはモーダルな状態になる

モーダルな状態になっているということは、親コンポーネントの操作を一切受け付けなくなるということです。
「え?一切受け付けないのならマウスイベントも拾えないじゃん!」となりそうですが大丈夫です。

該当コンポーネントがモーダルな状態で、親コンポーネント(自身以外のコンポーネント)をクリックした際に Component::inputAttemptWhenModal() が呼ばれます。デフォルトでは「他のコンポーネント触りたいならこのコンポーネントを閉じな!」と、システムのアラート音を鳴らすだけなのですが、これをオーバーライドしてやって、Component::exitModalState(int) でモーダルな状態を抜ける処理を含めると、タイトルを回収することができます。

class SomeComponent : public juce::Component {
public:
  // ... //

  void inputAttemptWhenModal() override
  {
    exitModalState(0); // モーダルな状態を抜ける(タイトル回収には必須)
    // 以下にやりたい処理を書いていく
    // 今回は自身を非表示にする処理を行う
    setVisible(false); // コンポーネントを非表示にする
  }

  // ... //
};

簡単です。

おわりに

様々なケースで使えそうですが、モーダルにすると都合の悪い場合もあります。その場合は、前述した Desktop::addGlobalMouseListener(MouseListener*) を使う必要があると思います(実装してみないと分からないので詳細略)。

【How to JUCE】、このくらいの規模だったらシリーズ化してみてもいいかもなー