m13o

2020-06-05 Fri 23:21
モニタディスプレイのサイズに合わせてEmacsのフレームサイズと位置を調整するEmacs

これまでは, 日常の作業は公私共にMBPを使っている都合上そのディスプレイのみを利用していたため, Emacsのfaceにαを適用させた状態でフレームをフルスクリーン表示にしていましたが, 昨今COVID-19関連でリモートワーク全盛となった事もあり, 4Kモニタディスプレイを利用するようになりました. そこで1つ問題となっていたのが, カレントディスプレイが4Kモニタディスプレイの時とMBPのモニタディスプレイの時とでフルスクリーン時に(当然)フレームの高さと幅が変わってしまい, うっかり4Kモニタディスプレイ側がカレントになっている時にEmacsを起動し, やたら大きいフレームのEmacsが存在するという事象が多発してしまう事です.

そもそもは, 1枚のモニタディスプレイ環境で他の資料を眺めつつ別の資料を書いたり実装したりするためのフルスクリーン&透過処理だったわけですが, 4Kモニタディスプレイを手に入れてしまった事で, 4Kモニタディスプレイ側に色々表示する事が可能になったため, その意義が失われてしまいました.

そこで, Emacsのフレームをフルスクリーンにする事をやめ, カレントディスプレイの解像度やサイズによって起動フレームの大きさをいい感じになるようにしました.

モニタディスプレイの情報を取得する

何はなくとも, モニタディスプレイの情報を取得できない事には始まりません. Emacsのマニュアルを読むと display-monitor-attributes-list と frame-monitor-attributes という関数がある事がわかります. これらの関数は, 接続されているモニタディスプレイの名前や解像度などの情報をkey-value形式のリストとして返す物で, 前者は接続(認識)可能な全てのモニタディスプレイ, 後者はカレントモニタディスプレイの情報をそれぞれ返します.

例えば, 私のEmacsを起動している環境で display-monitor-attributes-list を評価すると以下のようなリストが返ります.

(display-monitor-attributes-list)
;; 以下は評価結果
(((geometry 0 0 3840 2160) (workarea 0 23 3840 2137) (mm-size 598 337) (frames #<frame GNU Emacs 26.3 (build 1, x86_64-apple-darwin18.7.0, Carbon Version 158 AppKit 1671.6)
 of 2019-09-16 0x1078b9b68>) (name . "PHL 278E1") (metal-device-name . "Intel(R) Iris(TM) Graphics 550") (backing-scale-factor . 1)) ((geometry 3840 959 1680 1050) (workarea 3840 982 1680 1027) (mm-size 286 179) (frames) (name . "カラーLCD") (metal-device-name . "Intel(R) Iris(TM) Graphics 550") (backing-scale-factor . 2)))

各key-valuesの組の意味合いは概ね以下のようです.

geometry
カレントモニタディスプレイの左上を起点(0)として, 最初の2要素は座標, 次の2要素はサイズ(解像度)です. 注意しなければならないのは, 複数枚モニタディスプレイを接続している環境の場合, カレント以外のモニタディスプレイはカレントモニタディスプレイの起点からの位置が最初の2要素の値になるため, 負数になったり, 解像度を超える値になったりします.
workarea
実際に利用可能なモニタディスプレイ内の空間を表します. 最初の2要素が位置で, 次の2要素はサイズ(解像度)です. 実際に利用可能な領域なので, 上記の私の例で言うと, macOSが表示するメニューバー分のheightが加味されています.
mm-size
モニタディスプレイの表示領域をミリメートル単位のサイズで表したものです.
frames
そのオニタディスプレイ内にあるフレームのリストです.
name
モニタディスプレイの名前です. 私が接続している4Kモニタが何かバレてしまった……

ここまでは恐らく display-graphic-p が t となる環境であれば共通している要素ですが, その他, 各OS毎に固有のディスプレイ属性情報を返す可能性があります.

例えば, macOS Catalinaを載せたMBPの場合だと, metal-device-name と backing-scale-factor というkeyが存在します.

カレントモニタディスプレイのサイズ(解像度)に合わせてフレームの位置とサイズを変更する

いい感じの位置とサイズにしたいけど, いい感じというのも中々難しいので, とりあえずMBPのモニタディスプレイのサイズを超えない範囲で, カレントディスプレイの真ん中に幅80%, 高さ90%を埋めるくらいで一旦構築しようと思います.

まず, display-monitor-attributes-list を利用して, 接続されているモニタディスプレイの全ての workarea から最も小さい width, height をそれぞれ取得し, それを最大値としていい感じの width, height の組み合わせを計算します.

(let* ((workarea (mapcar (lambda (attr)
                          (cdddr (assoc 'workarea attr)))
                        (display-monitor-attributes-list)))
       (width (floor (* (seq-min (mapcar #'car workarea)) 0.8))) ;; widthは80%, heightは90%くらいにしておく
       (height (floor (* (seq-min (mapcar #'cadr workarea)) 0.9)))))

そして, カレントモニタディスプレイの座標を元に, 中央にEmacsのフレームがくるようにします.1 , 2

(let* ((current-workarea (frame-monitor-workarea))
       (current-frame (selected-frame))
       (x 0)
       (y 0))
  (when (>= (nth 0 current-workarea) 0)
      (setq x (- (+ (nth 0 current-workarea) (/ (nth 2 current-workarea) 2)) (/ width 2))))
  (when (>= (nth 1 current-workarea) 0)
      (setq y (+ (/ (- (nth 3 current-workarea) height) 2) (nth 1 current-workarea))))
  (set-frame-position current-frame x y)
  (set-frame-size current-frame width height t))

これでEmacsのフレームがいい感じのサイズ感で表示されるようになりました.

後は起動時にこれが実行されるようにしつつ, 適当なキーバインドを設定しておけば, 任意のタイミングでカレントフレームの位置とサイズを調整できるようになります.

コードの全景は以下のような感じになります.

(defun adjust-pos-and-size-of-current-frame-to-center ()
  (interactive)
  (when (display-graphic-p)
    (let* ((workarea (mapcar (lambda (attr)
                               (cdddr (assoc 'workarea attr)))
                             (display-monitor-attributes-list)))
           (width (floor (* (seq-min (mapcar #'car workarea)) 0.8)))
           (height (floor (* (seq-min (mapcar #'cadr workarea)) 0.9))))
      (let* ((current-workarea (frame-monitor-workarea))
             (current-frame (selected-frame))
             (x 0)
             (y 0))
        (when (>= (nth 0 current-workarea) 0)
          (setq x (- (+ (nth 0 current-workarea) (/ (nth 2 current-workarea) 2)) (/ width 2))))
        (when (>= (nth 1 current-workarea) 0)
          (setq y (+ (/ (- (nth 3 current-workarea) height) 2) (nth 1 current-workarea))))
        (set-frame-position current-frame x y)
        (set-frame-size current-frame width height t)))))

諸注意

私の環境は, Emacs 26.3 + hacker's patch on macOS Catalina という構成なのですが, この場合にモニタディスプレイがmacOS上で設定しているメインディスプレイよりも左であったり上であったりする場所に配置されていると, set-frame-position に設定する値が負数になります. 負数の場合, 画面下端, 右端を基準にしたオフセットになり, マルチディスプレイの場合は他のディスプレイに表示されるというような事がマニュアルには書いてあるのですが, 残念な事にそんな事にはならず, 正方向に周り込むようになり, 負数の方向にあるモニタディスプレイにフレームは移動しません. 私はMBPをメインディスプレイに, 4Kモニタディスプレイを左にくる形で設定しているため, 意図通りにならず, しばらく試行錯誤しましたが, どうすればうまく設定できるのかよくわからなかったため, 4KモニタディスプレイをmacOS上でメインディスプレイに設定し, 負数を発生させないようにするワークアラウンドで対応しました. また, 上記コードでは負数を回避して, 正数の場合のみ対応するよう制約がかかっています. 気が向いたらEmacs本体のソースコードを眺めてみようとは思いますが気が向いたらです.

脚注: