BetterTouchToolのマウスホイールトリガーでハマった優先順位の罠

今日は BetterTouchTool のなかなか面白い境界ケースを調べた。同じ「マウスが画面下部にあるとき、ホイールで SoundSource の音量を調整する」設定が、QQ や Finder などが最前面のときは正常に動くのに、Chrome が最前面だと発火しない。

現象

  • Global に Normal Mouse トリガーが2つある:996 / 997
  • BTT では、この2つのコードはそれぞれマウスホイール下方向 / 上方向を表す。
  • この2つの Global トリガーの条件は mouse_pos_percent_y < -98、つまりポインタが画面下部にあること。
  • Chrome には別に、アプリ固有(app-specific)の 996 / 997 がいくつかある:
    • Cmd+スクロールでページを拡大・縮小
    • 上部でのホイールでタブ切り替え
    • 左端でのホイールで Page Up / Page Down
  • QQ には app-specific の 996 / 997 はない。

直感的には、Chrome 側のそれらのトリガーと下部の音量トリガーは競合しない。上部、左端、Cmd 修飾キー、下部という位置条件はどれも重なっていない。ところが実際には、Chrome が最前面のときは Global の下部ホイールトリガーが不安定、または発火せず、QQ が最前面のときは正常に動く。

推測

これは Chrome がホイールを横取りしているわけでも、位置条件が本当に競合しているわけでもなく、BTT の高頻度 mouse trigger に対するマッチング戦略の問題だと思われる。

現在の app 配下に同種の Normal Mouse / Mouse Wheel trigger がすでにある場合、BTT はまず現在の app の app-specific ルール層に入る。たとえそれらの app-specific ルールの条件に一致しなくても、BTT が Global 内の同種 trigger をさらに走査するとは限らない。

BTT の作者 Andreas も、似た問題について言及している。mouse triggers は、条件判定のために app をまたいで、命中する可能性のあるすべての trigger を総当たりで探すわけではない。主な理由はパフォーマンスとのこと。類似の問題はこちら:

修正方法

最小限の修正は、Chrome にも下部音量トリガーを1セット複製して、Chrome 自身の 996 / 997 ルール層に入れること。

つまり:

  • Global には下部ホイール音量トリガーを残し、app-specific のホイール設定がないアプリ向けに使う。
  • Chrome には同じ下部ホイール音量トリガーを別途置き、Chrome の上部タブ切り替え、左端ページ移動、Cmd+ホイール拡大・縮小と同じ層に置く。

長期的によりきれいな構造にするなら、ある種の入力が高頻度の mouse wheel trigger である場合、app-specific ルールと Global fallback に混在して依存するのはできるだけ避け、関連するルールを同じ層に置いて、位置・修飾キー・app を条件で切り分けるのがよい。

感想

BTT は今ではかなり高機能だが、そのぶんどんどん肥大化している。この種の挙動は本質的にはパフォーマンス上のトレードオフだ。ホイールイベントのたびに大量の app/global 条件を走査するのを避けるために、「継承式設定」に対する直感的な一貫性の一部を犠牲にしている。

この落とし穴はかなり見えにくい。条件ロジックだけを見るとまったく競合していないのに、BTT のマッチング階層から見ると、すでに競合しているからだ。