Author Archives: AKIYAMA

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ライブラリ内の構造が変わると動かなくなるかもしれません。

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

2021-09-06

Emacs用のSVG実装のカラーピッカー

先日からEmacsの中で動く作図ツールを作っています。

https://github.com/misohena/el-easydraw

その一環として今日はカラーピッカーを作りました。この手のソフトには必ずあるアレです。

Emacs上での先行事例はいくつかあるようでしたがSVGでの実装は見当たりませんでしたし、まぁ、自分で作りたいじゃないですか。こういうの作るの楽しいですし。

というわけで出来たのがこちら。

https://github.com/misohena/el-easydraw/blob/master/edraw-color-picker.el

一応ライブラリとして他で使い回すことを考えています。

応用としてとりあえず作ったコマンドがいくつか。

edraw-color-picker-read-colorread-colorの代わりを意識して作った色入力コマンドです。ミニバッファ内にカラーピッカーを配置してみました。文字からでもカラーピッカーからでも色を選択できます。

ミニバッファ内に表示されるカラーピッカー
図1: ミニバッファ内に表示されるカラーピッカー

edraw-color-picker-insert-colorは選択した色をそのままバッファに挿入するコマンドです。またedraw-color-picker-replace-color-at-pointは現在のポイントの位置にある色表記を読み取ってカラーピッカーで選択した色に置き換えるコマンドです。これらはバッファ内にインラインでカラーピッカーが表示されます。

インラインで表示されるカラーピッカー。
図2: インラインで表示されるカラーピッカー。

SVGは本当に簡単でこういう絵を出すのはサクッとすぐに出来ます。いや、本当は少し調査が必要だったのが二点。meshGradientの対応状況と色相バーの仕様です。こういう二次元のグラデーションで連想するのがメッシュ状の図形(いわゆるポリゴンの組み合わせ)で頂点に色を設定する仕組みです。3Dグラフィックスではおなじみですよね。SVGにも似たような仕組みがあったようなと調べてみたのですが、残念ながらEmacs(というかlibrsvg)では対応していないようですね(そもそもmeshGradient自体がSVG2から先送りされてたんですね)。幸い二枚のグラデーションを重ねれば良いとすぐに気がつけました。もう一点の色相バーですが、これ6色(赤、黄、緑、水、青、紫)を均等に並べて線形補間すればいいだけみたいですね。というわけでそれさえ分かればこういうカラーピッカーの絵自体はすぐに作れました。

画像内の各パーツは「領域」というオブジェクトで構成されています。指定に基づいて「領域」をレイアウト処理することで、いくらか柔軟に構成要素を増やしたり減らしたりできるようになっています。

領域にマウスイベントを配信(ディスパッチ)する仕組みを作り、それに反応して各部が動くようにしていきます。マウスイベントを受けて関連付けられた値が変わり、値の変更が通知されて表示が変わります。

各領域で選択した場所から最終的な色を求める方法ですが、色相バー(1次元)→彩度明度領域(2次元)→不透明度バー(1次元)の順に処理していきます。色相バーで選んだ色が彩度明度領域の右上の色になります。彩度明度領域は水平のグラデーション(左:白から右:色相選択色)の上に垂直グラデーション(上:透明黒から下:不透明黒)が重なっています。選んだ位置によって線形補間で水平、垂直の順で色を求めていきます。その色が今度は不透明度バーの上の色になって、最後に不透明度バーで選択した不透明度をその色に適用して完了です。仕組み自体はグラデーションの内容に依存していないので、色相、彩度、明度の軸を入れ替えても機能すると思いますし、1次元バーを緑、2次元領域を青と赤、などとしても大丈夫だと思います。(そのための切り替えUIを作るのが面倒くさかったのでやってませんが)

こうして出来たSVG画像はテキストプロパティまたはオーバーレイを使ってEmacs内に表示されます。というかEmacsではそれしか画像を表示する方法はありません(環境依存性の強い方法を除けば)。テキストプロパティでも良いのですが、オーバーレイの方がこういう用途では使いやすいでしょう。

オーバーレイにも画像を表示できる場所が三つあります。before-stringとdisplayとafter-stringです。displayはいいとして、before-stringとafter-stringですが、これはオーバーレイの前後に文字列を挿入するためのプロパティです。一見画像とは関係なさそうですが、この文字列にpropertize関数でdisplayテキストプロパティを適用するとオーバーレイの前後にさらに画像を表示することが可能になります。

これら色々ある表示方法を都合に合わせて選択できるように構造には柔軟性を持たせてあります。

先行事例を見てみると表示にフレームを使っているものがありました。普通のフレームを使っているものやchild frameという比較的最近のEmacsで使えるようになった仕組みを使っているものもありました。child frameを使うとフレーム内の好きな位置に重ねることが出来るのでポップアップで情報を出すパッケージで使われているようです。

とはいえフレームは扱いが難しく勉強が必要なので、とりあえずオーバーレイだけで出来る範囲で実装しています。

カラーピッカーはどこに表示すれば良いのでしょうか。だいたいポイント(やマウスカーソル)の近くですよね。ただ、ポイントの左や右にカラーピッカーのオーバーレイを挿入すると行が大きく膨らんで見づらくなってしまいますし、ウィンドウの左右からはみ出して見えなくなってしまうこともあります。となれば上の行か下の行に表示してはどうでしょう。上の行や下の行に適切な表示カラムが無いならどうすればいいでしょうか。幸いオーバーレイは行を新たに作ることが出来ます。before-string、display、after-stringの各プロパティにはそれぞれ文字列が指定できますが、いずれでも"\n"を入れることで改行が出来ます。例えば現在ポイントが20カラム目にあるとして、その真下付近に画像を表示するには、行末の改行文字にオーバーレイをかけて、before-stringを "\n " (←空白20個 - 画像の幅/2くらい)、displayに画像、after-stringに "\n" を指定してやればOKです。水平スクロールしている可能性があるのでwindow-hscrollの値を考慮するのを忘れずに。

また、ミニバッファに色を入力するようなシチュエーションでは画面の下の方にカラーピッカーを表示して欲しいです。いや、いっそのことミニバッファ内にカラーピッカーを入れたらどうでしょう。ここでもオーバーレイに改行させれば上をカラーピッカー、下を入力欄とすることが出来ます。ただし一つ注意点が。バッファの先頭にオーバーレイを入れたいわけですが、先頭の1文字にかけるとbefore-stringに画像を設定しなければならずその後に改行が入れられません。displayで改行すると1文字目が(改行に置き換わって)表示されなくなってしまいますし、after-stringで改行すると1文字目の後で改行されてしまいます。なので先頭の空の範囲((point-min)から(point-min)まで)にオーバーレイを設定します。注意点が二つ。evaporateプロパティがtだと空の範囲になった段階でオーバーレイが消えてしまいます。なのでevaporateは使わずちゃんとオーバーレイを管理する必要があります。もう一点は空の範囲のオーバーレイではdisplayプロパティが機能しないということ。before-stringとafter-stringは表示されるのですが、なぜかdisplayプロパティは空の範囲だと表示されません。なのでbefore-stringに画像を、after-stringに"\n"を指定することになります。(2021-09-07追記:よく考えたらbefore-stringの中で文字列の一部にだけdisplayプロパティをかけて、残りで改行すれば済むような気もします。未確認ですが、多分動くのではないかと)

他にもEmacsの色名とWeb(HTMLやCSS)の色名に対応するとか、色の表現形式とか、画像のスケーリングについてとか(上のスクリーンショット、ピッカーの大きさが違うのに気がつきましたか?)、色々書きたいことは山ほどありますが、この辺にしておきます。

(追記: Emacsのcss-modeやcustomize-face等でカラーピッカーを使う設定)

2021-08-29 ,

org-modeのアジェンダで土曜日と日本の祝日を色づけする

土曜日は青で、日曜と祝日は 'org-agenda-date-weekend のfaceで表示するには次のように設定します。

(setq org-agenda-day-face-function
      (lambda (date)
        (let ((face (cond
                     ;; 土曜日
                     ((= (calendar-day-of-week date) 6)
                      '(:inherit org-agenda-date :foreground "#0df"))
                     ;; 日曜日か日本の祝日
                     ((or (= (calendar-day-of-week date) 0)
                          (let ((calendar-holidays japanese-holidays))
                            (calendar-check-holidays date)))
                      'org-agenda-date-weekend)
                     ;; 普通の日
                     (t 'org-agenda-date))))
          ;; 今日は色を反転
          (if (org-agenda-today-p date) (list :inherit face :inverse-video t) face))))
土曜日が青、日曜日と祝日が赤で色づけされたorg-agenda
図1: 土曜日が青、日曜日と祝日が赤で色づけされたorg-agenda

素のcalendarパッケージでもそうなのですが土曜日が青色で表示されないんですよね。日本のカレンダーでは当たり前で、もはや青くないと違和感を感じるレベルなのですが、きっと海外では違うのでしょうね? 今ちょっと検索してみましたがいろんな文化があって面白そうです。

日付の色づけは org-agenda-day-face-function 変数で変更できます。

引数の date には (month day year) という形式のリストが渡されてきます。これはcalendarパッケージが日付を扱うときの形式に合わせているようです。

土曜日用に新しいfaceを定義するのも面倒なのでanonymousフェイスで返しています。フェイス属性:inherit は先頭にないとダメなようです。

祝日の判定は calendar-check-holidays 関数で行っています。ただ、私は calendar-holidays 変数に日本の祝日以外も入れてしまっているので、一時的に calendar-holidays 変数シンボルを japanese-holidays の示すリストに束縛してからチェックしています。

「今日」のfaceを変えたいのですが土、日祝、普通の三種類があるので今日か否かで変えるとなると組み合わせで六種類必要になってしまいます。なのでここでもanonymousフェイスを生成して :inherit が指す先を日によって変えることにしました。 :inherit にはシンボルだけでなくanonymousフェイスも指定できるようです。foregroundはそのままにbackgroundだけちょっと色を付けるといったくらいならこの方法が楽でしょう。全ての組み合わせを細かく調整したいのであればいっそ次のような感じの方が清々しいかもしれません。

(setq org-agenda-day-face-function
      (lambda (date)
        (pcase (+ (if (org-agenda-today-p date) 3 0)
                  (cond
                   ((= (calendar-day-of-week date) 6)
                    0)
                   ((or (= (calendar-day-of-week date) 0)
                        (let ((calendar-holidays japanese-holidays))
                          (calendar-check-holidays date)))
                    1)
                   (t
                    2)))
          (0 (list :foreground "#08a"))
          (1 (list :foreground "#a00"))
          (2 (list :foreground "#aaa"))
          (3 (list :foreground "#0cf"))
          (4 (list :foreground "#f00"))
          (5 (list :foreground "#fff")))))