2021-09-11

The Emacs Widget Libraryのプッシュボタンを連打できない問題を解決する

プッシュボタンが連打できなくて修正しようとしたらドハマリして時間を大量に無駄にしたのでメモ。

Widgetライブラリのpush-buttonを連打しようとするとダブルクリックやトリプルクリックと判定されてボタンが押せなくなります。そしてなぜか範囲選択が始まったりします。「+」「-」とか「Previous」「Next」等をプッシュボタンにすると連打できないのはとても気になるので何とかしました。

次のコードは何とかしたカウンターアプリケーションの例です。 M-x my-count で単純なカウンターが動きます。

(require 'widget)

;; ダブルクリック、トリプルクリックに対応した `widget-button-click' の代わりです。
(defun my-widget-button-click (event)
  (interactive "e")
  ;; Add double and triple click support to widget-button-release-event-p
  (cl-letf (((symbol-function 'widget-button-release-event-p)
             'my-widget-button-release-event-p))
    (widget-button-click event)))

;; ダブルクリック、トリプルクリックに対応した `my-widget-button-release-event-p' の代わりです。
(defun my-widget-button-release-event-p (event)
  (and (eventp event)
       (memq (event-basic-type event) '(mouse-1 mouse-2 mouse-3))
       (or (and (or (memq 'double (event-modifiers event)) ;;double click
                    (memq 'triple (event-modifiers event))) ;;triple click
                (null (memq 'down (event-modifiers event))))
           (memq 'click (event-modifiers event))
           (memq 'drag (event-modifiers event)))))

(defvar my-push-button-map
  (let ((km (make-sparse-keymap)))
    (define-key km [drag-mouse-1] 'ignore)
    (define-key km [double-down-mouse-1] 'my-widget-button-click)
    (define-key km [triple-down-mouse-1] 'my-widget-button-click)
    km))


(defvar-local my-number-widget nil)
(defun my-increase (delta)
  (widget-value-set
   my-number-widget
   (+ (widget-value my-number-widget) delta)))
(defun my-inc (&rest _) (my-increase 1))
(defun my-dec (&rest _) (my-increase -1))

(defun my-counter ()
  (interactive)
  (pop-to-buffer "*my counter*")
  (kill-all-local-variables)
  (let ((inhibit-read-only t))
    (erase-buffer))
  (remove-overlays)
  (setq-local my-number-widget
              (widget-create 'number
                             :size 10
                             :value 0))
  (widget-insert " ")
  (widget-create 'push-button
                 :notify 'my-dec
                 :keymap my-push-button-map ;;support double and triple down
                 "Decrement")
  (widget-insert " ")
  (widget-create 'push-button
                 :notify 'my-inc
                 :keymap my-push-button-map ;;support double and triple down
                 "Increment")
  (widget-insert "\n")
  (use-local-map widget-keymap)
  (widget-setup)
  (widget-forward 1))

問題の原因は次の二点にありました。

  • ダブル、トリプルクリックのキーマップが割り当てられていないこと
  • マウスのボタンが離されたことを判定する関数がダブル、トリプルクリックに対応していないこと

プッシュボタンの処理はローカルマップのdown-mouse-1に割り当てられたwidget-button-click関数によって行われています。その中では押された位置のボタンを割り出して、ボタンが離されるまで追跡し、ボタンに関連付けられたアクションを実行します。途中でキャンセルされたらグローバルマップのマウスイベントに割り当てられたコマンドを実行します。

ローカルマップのdouble-down-mouse-1とtriple-down-mouse-1にwidget-button-clickが割り当てられていないのが問題なのだと思い割り当てたのですが変わりませんでした。

widget-button-click関数の中ではwidget-button-release-event-pという関数を呼んでいてイベントがマウスボタンを離したものかを判定しています。そこがclickとdragのみを考慮していてdoubleやtripleを考慮していません。なのでボタンを離しても離されていないものとしてずっと追跡を続けてしまっていました。

上の例ではcl-letfを使用してダブルクリック、トリプルクリックが発生したときだけwidget-button-release-event-pの中身を入れ替えることで対処しています。従ってwidgetライブラリ内の構造が変わると動かなくなるかもしれません。

以上ですが、誰得情報なんだこれ。