Mac環境で安トラックボールを快適に使う

IYHじゃなくて雑記です

ELECOM HUGE

手首がダメになりかけてきたのと、MIDI鍵の上に乗っけて使うポインティングデバイスが欲しかったのでトラックボールを買うことにしました。買ったのはこれ

選定基準は以下

・安い
・ボールが大きい
・それなりにボタン数がある

本当はみんな大好きケンジントンのExpertMouseが欲しかったのですが、トラックボール初めてなので投げ捨てるリスクも鑑みて、安めでボールの大きいトラックボール、かつMagic Mouseぐらいの気持ちで使えるようにショートカットを割り当てられるボタン数が多い物をということでエレコムのHUGEにしました。エレコム製というところで抵抗感は大きかったものの、他にあまり選択肢がなかったのでこれに

使い心地とか

まずすでに色々なところで言われているかなと思いますが、トラックボールの回りはあまりスムーズではありません。しばらく使ってるとよくなるとの声もありますが、メンテのたびにリセットです。耐久性に関してはまだ使って浅いのでなんとも言えませんが、元気に使いすぎるとすぐにダメになりそうな気配はあります。Magic Mouseに調教されている体なのでスクロールホイールも嫌い。あとこのトラックボールで一番致命的なのは、エルゴノミクスっぽい形状してるのに、全然手首を労ってくれないところです。そのまま使うと腱鞘炎が悪化するのでやめた方がいいです。3Dプリンターで傾ける土台を作っている人もいますが、3Dプリンターがないと結構用意するコストがバカにならないので、僕はキーボード用の足をつけました。

中華感あふれるこれ。まだこれも試行錯誤中ですが、角度はかなり付けれるようになったので概ね良い感じです。

エレコムマウスアシスタント

悪名高き公式ユーティリティー。実際その使い勝手はあまりよくなく、再起動のたびに適用し直さないと効かなくなるという厳しさをもつ。また公式ユーティリティーの割にOSの上のレイヤーで動作しており、KVMソフトなどとの併用でうまく動作しなかったりなどがある。海外勢はまずこれを使うのを嫌がり、他の有料ユーティリティーを使ったりなどを試している模様。リマップソフトとしてはKarabiner-Elementsなどが有名ですが、こちらではそのままだとHUGEのいくつかのキーが認識されず、完璧には使えない状態でした。今現在はとりあえず公式ユーティリティーを利用しているものの使い勝手はよくないので別のソフトで代用できないか試行錯誤中です。Karabiner-DriverKit-VirtualHIDDeviceを利用してHUGEのリマップを行なっているgitリポジトリを発見したので、これをちょっと拝借していじっている最中ではあります。上のレイヤーでの動作でも良いというのであれば、後述するHammerspoonでのカスタマイズが割と手っ取り早いかなとは思います。

せっかくのトラックボール、ボールでスクロールしよう

使い心地でスクロールホイールが嫌いと書きましたが、せっかくトラックボールを使ってるのでどうせならこのボールをゴロゴロしてスクロールしたいと思うのです。HUGEのスクロールホイールは親指への負担もかなり大きいのでこれもなんとかしたい。ということでトラックボールを転がしてスクロールできないものか色々調べてみました。Macだと有料ソフトでSmart Scrollというものがあったり、また先ほど紹介したKarabiner-Elementsを使う方法などがありますが、前述したようにKarabiner-Elementsは一部ボタンが効かないので一旦Smart Scrollを試用で使ってはみたものの悪くはないが有料がやはりネック。ということで調べてる途中で出会ったHammerspoonというソフトを利用することに。

Hammerspoonはキーボードやマウスなどの挙動をカスタマイズしたり、ウィンドウの配置を制御したりなどさまざまな動作をオートメーション化できるソフトです。記述に用いる言語はLuaです。なのでちょっとした条件文を書いて制御したりみたいなことができ、動作するアプリケーションウィンドウを縛ったりもできます。Hammerspoonも性質上結構上の方のレイヤーで動作してしまうのでKVMアプリなどに飛ばしたりといった使い方はできませんが、そういった使い方でなければいい感じに使えます。ということで一旦こちらを使ってトラックボールスクロールを実現することに。

トラックボールでのスクロールさせるがgithubのissueにあったのでこれをちょっといじってみました。

local scrollVerticalButton = 4
local scrollHorizontalButton = 3
local toggleVerticalScrollButton = 4
local toggleHorizontalScrollButton = 3
local deferred = false
local verticalScrollToggle = false
local horizontalScrollToggle = false

OverrideOtherMouseDown = hs.eventtap.new({
   hs.eventtap.event.types.otherMouseDown
}, function(e)
   local pressedMouseButton = e:getProperty(
                                  hs.eventtap.event.properties['mouseEventButtonNumber'])
   if scrollVerticalButton == pressedMouseButton or scrollHorizontalButton ==
       pressedMouseButton then
       deferred = true
       return true
   end
end)

ToggleMouseDown = hs.eventtap.new({hs.eventtap.event.types.otherMouseDown},
                                 function(e)
   local pressedMouseButton = e:getProperty(
                                  hs.eventtap.event.properties['mouseEventButtonNumber'])
   if toggleVerticalScrollButton == pressedMouseButton then
       if (verticalScrollToggle) then
           verticalScrollToggle = false
           return true
       else
           verticalScrollToggle = true
           horizontalScrollToggle = false
           return true
       end
   end
   if toggleHorizontalScrollButton == pressedMouseButton then
       if (horizontalScrollToggle) then
           horizontalScrollToggle = false
           return true
       else
           horizontalScrollToggle = true
           verticalScrollToggle = false
           return true
       end
   end
end)

OverrideOtherMouseUp = hs.eventtap.new({hs.eventtap.event.types.otherMouseUp},
                                      function(e)
   local pressedMouseButton = e:getProperty(
                                  hs.eventtap.event.properties['mouseEventButtonNumber'])
   if scrollVerticalButton == pressedMouseButton or scrollHorizontalButton ==
       pressedMouseButton then
       if (deferred) then
           OverrideOtherMouseDown:stop()
           OverrideOtherMouseUp:stop()
           hs.eventtap.otherClick(e:location(), 0, pressedMouseButton)
           OverrideOtherMouseDown:start()
           OverrideOtherMouseUp:start()
           return true
       end
       return false
   end
   return false
end)

local scrollmult = -1 -- negative multiplier makes mouse work like traditional scrollwheel
local scrollmultHorizontal = -1
local moveX = 0
local moveY = 0

local function scrollFunction(x, y)
   local oldmousepos = hs.mouse.absolutePosition()
   local scroll = hs.eventtap.event.newScrollEvent({x, y}, {}, 'pixel')
   hs.mouse.absolutePosition(oldmousepos)
   return scroll
end

ToggleMouseButtonThenScroll = hs.eventtap.new({
   hs.eventtap.event.types.mouseMoved
}, function(e)
   if verticalScrollToggle then
       local dy = e:getProperty(
                      hs.eventtap.event.properties['mouseEventDeltaY'])
       moveY = -dy * scrollmult
       return true, {scrollFunction(0, moveY)}
   else
       if horizontalScrollToggle then
           local dx = e:getProperty(
                          hs.eventtap.event.properties['mouseEventDeltaX'])
           moveX = -dx * scrollmultHorizontal
           return true, {scrollFunction(moveX, 0)}
       else
           return false, {}
       end
   end
end)

DragOtherToScroll = hs.eventtap.new({hs.eventtap.event.types.otherMouseDragged},
                                   function(e)
   local pressedMouseButton = e:getProperty(
                                  hs.eventtap.event.properties['mouseEventButtonNumber'])
   if scrollVerticalButton == pressedMouseButton then
       deferred = false
       local dy = e:getProperty(
                      hs.eventtap.event.properties['mouseEventDeltaY'])
       moveY = -dy * scrollmult
       verticalScrollToggle = false
       return true, {scrollFunction(0, moveY)}

   else
       if scrollHorizontalButton == pressedMouseButton then
           deferred = false
           local dx = e:getProperty(
                          hs.eventtap.event.properties['mouseEventDeltaX'])
           moveX = -dx * scrollmultHorizontal
           horizontalScrollToggle = false
           return true, {scrollFunction(moveX, 0)}
       else
           return false, {}
       end
   end
end)
OverrideOtherMouseDown:start()
OverrideOtherMouseUp:start()
ToggleMouseDown:start()
ToggleMouseButtonThenScroll:start()
DragOtherToScroll:start()

改修の方針としては

・スクロール用ボタン押しっぱなしでちょっとだけスクロール。長距離スクロールするときはスクロールボタンをトグルで動作させてフリーハンドでスクロール
・垂直スクロールと水平スクロールを分けたい

というところです。スクロールボタンのトグルとホールドに関しては正直好みだと思うのであれですが、垂直スクロールと水平スクロールはきっちり分けてあげないとトラックボールだと制御が辛いかなと思ったのでここは分けました。ブラウジングならまだいいんですが、Cubaseのピアノロールとかでスクロールさせると上下と左右が一緒に動きまくって大変なことになっちゃうので・・・
大体今のでいい感じかなと思いますが、マウス位置の保持処理とかがなんかズレちゃったりするのが若干気になるのと、トラックボールでのスクロールがトグルで入ってるかどうかわかりにくいというのがあるので、スクロールトグル時はマウスのアイコン変えさせたりとか処理を追加した方がいいかもしれません。

あとはこれだけだと慣性スクロールが効かないので、mosとかを併用するとなかなかトラックパッドっぽくなっていい感じです。

まとめ

いろいろいじくり回していい感じにはなったものの、どこまで頑張ってもやはりマウスの速度・精度には及ばないというところもあるので、用途によって使い分けは必要かなと思いました。普段使い・(ギリギリ)DAWあたりはトラックボールでもいいかなと思いましたが、イラレみたいなソフトとか3D系のソフトとかはやはりマウスの方が良さそうに感じます。コーディングでも使えないことはないですが、長文のテキスト選択はめちゃくちゃやりづらいので工夫は必要そうです(コーディングだとそもそもキーボードだけで完結しちゃう人の方が多数派だと思いますが)。

今回組んだHammerspoonはHUGE以外のトラックボールでも使えるので、今後めちゃくちゃトラックボール気に入ったらExpertMouseとかGameBallに行きたいと思います。ポインティングデバイス、一番使うものなのでいいもの買った方がいいですよ

いいなと思ったら応援しよう!