Category Archives: 未分類

2021-07-23

neotreeで(setq neo-smart-open t)すると固まる(Windows)

neotreeを試してみたのですが、neo-smart-openをtにするとf8に割り当てたneotree-toggleを押したときに固まることがありました。普通のファイルを開いているときは固まらず、scratchやdiredでディレクトリを開いているときに固まるようです。

コードを追ってみたところ neo-buffer--select-file-node 関数の中で無限ループに陥ってしまう場所を見つけました。

https://github.com/jaypei/emacs-neotree/blob/98fe21334affaffe2334bf7c987edaf1980d2d0b/neotree.el#L1632

親ディレクトリへ移動するループで、ルートに到達したかの判定を"/"と比較することで行っています。Windowsでは"c:/"等がルートで何度neo-path--updirを適用しても決して"/"になりませんからいつまで経っても終わりません。

とりあえず"/"を"c:/"にしたら直ったのですが、それではあんまりなのでupdirしたときにパスが変化しなかったら終わらせるようにしてみました。

うまくadviceもかけられないしneotree.el読み込み後に関数まるごと再定義。neotreeはそんなに頻繁に更新されていないみたいなのでまあいいか。

(with-eval-after-load "neotree"
  (defun neo-buffer--select-file-node (file &optional recursive-p)
    "Select the node that corresponds to the FILE.
If RECURSIVE-P is non nil, find files will recursively."
    (let ((efile file)
          (iter-prev-dir nil) ;;ADD
          (iter-curr-dir nil)
          (file-node-find-p nil)
          (file-node-list nil))
      (unless (file-name-absolute-p efile)
        (setq efile (expand-file-name efile)))
      (setq iter-curr-dir efile)
      (catch 'return
        (while t
          (setq iter-prev-dir iter-curr-dir) ;;ADD
          (setq iter-curr-dir (neo-path--updir iter-curr-dir))
          (push iter-curr-dir file-node-list)
          (when (neo-path--file-equal-p iter-curr-dir neo-buffer--start-node)
            (setq file-node-find-p t)
            (throw 'return nil))
          (let ((niter-curr-dir (file-remote-p iter-curr-dir 'localname)))
            (unless niter-curr-dir
              (setq niter-curr-dir iter-curr-dir))
            (when (or (string= iter-curr-dir iter-prev-dir) ;;ADD
                      (neo-path--file-equal-p niter-curr-dir "/"))
              (setq file-node-find-p nil)
              (throw 'return nil)))))
      (when file-node-find-p
        (dolist (p file-node-list)
          (neo-buffer--set-expand p t))
        (neo-buffer--save-cursor-pos file)
        (neo-buffer--refresh nil)))))
2021-07-22 ,

Feedly Open All Unread Button Jul.2021

久しぶりにFeedlyを使ってみているのですが、未読のページ(全文)を一括で開く機能が無いととても使う気になれないので以前作っていたスクリプトを修正しました。

ChromeのTampermonkeyで確認。

// ==UserScript==
// @name         Feedly Open All Unread
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  try to take over the world!
// @author       You
// @match        https://feedly.com/i/*
// @icon         https://www.google.com/s2/favicons?domain=feedly.com
// @grant        GM_openInTab
// ==/UserScript==

(function() {
    'use strict';

    function openUnreadEntries(limit){
        const unreads = document.querySelectorAll(".entry--unread");
        const count = Math.min(unreads.length, limit || 5);
        console.log("count="+count);
        for(let i = 0; i < count; ++i){
            const entry = unreads[i];
            const url = entry && entry.dataset.alternateLink;
            //window.open(url, "_blank");
            //browser.tabs.create({url, active: false});
            GM_openInTab(url, {active:false});
            console.log("open " + url);

            // mark as read and hide
            const readAndHideButton = entry && (
                entry.querySelector(".EntryHideButton") || //Title Only
                    entry.querySelector(".EntryMarkAsReadButton") //Magazine, Card
            );
            if(readAndHideButton){
                readAndHideButton.click();
            }
        };
    }
    function createButton(){
        const div = document.createElement("div");
        div.style.display = "inline-block";
        div.style.verticalAlign = "top";
        div.className = "button-dropdown OpenUnreadButton";

        const button = document.createElement("button");
        div.appendChild(button);
        button.type = "button";
        button.innerText = "Open Unread";
        button.style = "padding-right:0px; margin-right:0px;";
        button.addEventListener("click", function(e){
            openUnreadEntries(parseInt(inputCount.value, 10));
        }, false);

        const inputCount = document.createElement("input");
        div.appendChild(inputCount);
        inputCount.type = "number";
        inputCount.value = "5";
        inputCount.style = "width:4em; padding:12px 5px 10px 5px; border:1px solid transparent; appearance: normal;";

        return div;
    }
    function updatePageUI(){
        for(const bar of document.getElementsByClassName("actions-container")){
            if(!bar.querySelector(".OpenUnreadButton")){
                bar.insertBefore(createButton(), bar.firstChild);
            }
        }
    }
    setInterval(updatePageUI, 1000);

})();

Inoreaderというのも使ってみたのですが、どっちみち全文を一気に読むには拡張機能が必要なことに変わりないのでとりあえずFeedlyでいいやと思いました。

2021-07-18 , ,

FontForgeでMeiryoKe_ConsoleとInconsolataをくっつける

これまでEmacsのフォント設定でASCIIはInconsolata、それ以外はMeiryoKe_Consoleを使うように設定していたのだけど、細かい調整が出来ないのでそれならばとFontForgeで一つのフォントに合成してしまうことにした。そうすれば細かい調整はFontForgeの方である程度行えるからだ。また、フォントサイズ(奇数サイズ)によって全角の幅が半角の幅のちょうど二倍にならない場合があるのもフォントを一つにしてしまえば解決すると思われた。ちなみにInconsolataは0に斜線が入っていて細身で綺麗なので長年使っている。MeiryoKe_Consoleを使っているのはぼやけたフォントが嫌いなのでヒンティングがしっかりしているメイリオでかつ等幅にしたものが欲しいからだ(ヒンティングには賛否両論あるとは思うが私はどちらかと言えば下手にアンチエイリアスしてぼやけたものよりもシャープな文字の方が好きだ。低解像度環境で細いフォントならば)。

ダウンロード先:

  • Inconsolata : Boldもダウンロードしておくと良い。さらにFontForge等で斜体版を作っておくとなお良い。
  • MeiryoKe : こちらもFontForgeで斜体版を作っておくと良い。
  • FontForge : 最新版にしたら常時カナロックがかかってしまう現象が発生した。IMEで半角変換して入力すればなんとか使える。最新の開発版では問題なかった。

合成する手順:

  1. Inconsolata側:
    1. FontForgeでフォントを開く。
    2. EMサイズを2048にする。「エレメント」→「フォント情報」→「一般情報」→「EMの大きさ」を「2048」にしてOKを押す。すると(「輪郭を拡大/縮小」にチェックが入っていれば)自動的にグリフが拡大縮小される。InconsolataのEMサイズは1000、MeiryoKe_Consoleは2048なので合成する前に合わせなければならない。
    3. ヒント情報・ヒント命令を削除する。「編集」→「選択」→「出力に値するグリフ」, 「ヒント」→「ヒントを削除」, 「ヒント→ヒント命令を削除」。MeiryoKe側に移したときにヒント命令は単純には引き継げないので削除する。
    4. ASCII文字(等MeiryoKe側へコピーしたい文字)のグリフを選択して右クリックし「参照を解除」。iとjがuni0307を参照していたりするのでこれを解除する。しないとコピーしたときにiやjの上の点がメイリオのものになってしまう。
    5. ASCII文字(等MeiryoKe側へコピーしたい文字)のグリフを選択して右クリックし「コピー」する。
  2. MeiryoKe_Console側:
    1. FontForgeでフォントを開く。
    2. フォント情報を書き替える。
      • PS Names
        • フォント名は「MeiryoKe_Inconsolata_スタイル名」等とする。
        • ファミリー名は「MeiryoKe_Inconsolata」とする。
      • 一般情報
        • (斜体なら)イタリックの傾きを傾けた角度にする。
      • OS/2
        • その他/Style MapをRegular, Bold, Italic, Bold Italicのいずれかに。
        • メトリック/WinDescentを360へ増やす(jの下が切れてしまうので。値はjやyの一番下の座標を見て決める)。
        • (斜体なら)Panose/文字の形状を「斜体/箱入」に。
        • TTF名のファミリー名、スタイル名、フルネーム等を適切に変更。
    3. ASCII文字(等Inconsolataから持ってきたい範囲)のグリフをカットして削除する。
    4. Inconsolata側でコピーしたグリフを貼り付ける(または、「エレメント」→「フォントの統合」で持ってきた方がトラブルが少ないかもしれない。少なくともアンカー情報はコピーだと引き継がれない模様。どちらを使うにせよすでにあるグリフには上書きされないので統合・コピーする前に取りこみたい箇所を消しておくこと)。
    5. フォントを出力する。「ファイル」→「フォントを出力」。 オプションはほとんどデフォルトだが、一応私が出力したときのを書いておく:
      • ヒント
        • Flexヒント
      • TrueType ヒント
      • PostScriptグリフ名
      • TrueType
      • オプション
        • OpenTypeの仕様
        • 旧来のkernテーブル(不要かも?)
        • Windows Compatible kern(不要かも?)
        • Prefer native kerning(不要かも?)
  3. 出力したフォントの平均文字幅情報を書き替える。平均文字幅(xAveCharWidth)はFontForgeが勝手に計算してしまうので設定で変更できない。半角文字の幅である1024にしたいのでバイナリエディタで書き替える。
    1. バイナリエディタで開いて先頭付近のOS/2という文字列を見つける。
    2. OS/2の後の4バイトはチェックサムなので無視し、次の4バイトが示すオフセットを読み取り(ビックエンディアン)、ファイル先頭からそのオフセットの場所に飛ぶ。
    3. 最初の2バイトはバージョン番号なので無視し、次の2バイトを1024(04 00)に書き替える。

メモ:

  • ○や■の記号などInconsolataから持ってきたくないものもあるのでASCIIのグリフだけInconsolataからMeiryoKe_Consolasにコピーした。
  • 変形は極力しない方が良い(特にMeiryoKe側は)。ヒント情報がどうなるか分からないので。
  • Inconsolataのiやjは他のグリフを参照しているので、「参照を解除」してから移すこと。でないとiやjの上ポチがずれた位置に出てしまう。参照先であるuni0307もコピーする手もあるかもしれない。
  • WinDescentを少し増やさないとjの下が切れてしまう。座標を見て 360 400にした。一般情報の「深さ」は変えなくて大丈夫。
  • 「エレメント」→「フォント情報」→「OS/2」→「Panose」→「幅の比率」が「等幅」だと全角文字の後に空白が空いてしまう。FontForgeのバグだそう(FontForgeで生成した日本語TrueTypeフォント文字幅広すぎ対策 - itouhiroはてなブログ)なので「等幅」のまま出力してxAvgCharWidthを別途書き替える。「任意」にすると一見問題が解決するように見えるが、フォントサイズが奇数のときに全角の幅が半角のぴったり二倍にならなくなる。例えば全角の幅が19pxにときに半角の幅が10pxになってしまう。「等幅」で出力して後でxAvgCharWidthを書き替えるとこの問題は起きない(フォントサイズ19pxのとき全角の幅が20pxになる)。
  • Emacsで使うとフレームの横幅が異様に大きくなってしまうことがある。(default-font-width)が大きな値を返してくる。これもxAvgCharWidthの問題。OS/2テーブルのxAvgCharWidthを書き替える必要がある。FontForgeは勝手に計算して出力するので設定で変更できない。
  • xAvgCharWidthの書き替え方:

    1. TTFファイルの中でOS/2テーブルの位置は、ファイル先頭付近の「OS/2」と書いてある所の直後を見れば分かる。「OS/2」の直後には32ビット整数(ビックエンディアン)でチェックサム、オフセット、サイズと続く。手元のファイルでは、オフセットは1D8hや1C8h等であった。このオフセットはファイル先頭からOS/2テーブル先頭までのバイト数。
    2. OS/2テーブル先頭からバージョン(uint16)、xAvgCharWidth(int16)と続く。手元のファイルでは xAveCharWidthは1959(7a7h)だった。これを1024(400h)に書き替える。

    (参考: OpenTypeフォント: vanillaの日記, OpenTypeフォントの続き(5)・・・OS/2テーブル: vanillaの日記, OS/2 - OS/2 and Windows metrics table (OpenType 1.8.4) - Typography | Microsoft Docs)

  • ヒント命令はコピーでもフォントの統合でも引き継げないのでInconsolataの分は諦める。大人しく消してしまった方が良い。各グリフのヒント命令(glyfテーブル)はフォント全体(fpgmテーブル)で定義している関数を呼び出したりしているので単純にグリフをコピーしても正しく動作しない。頑張って解析すれば統合できるかもしれないが大変。FDEF命令で定義する関数の番号をずらせれば統合できるかもしれない。CALLの前が必ずしも番号のPUSHとは限らないみたいなので慎重に追っていく必要がありそう。関連する他のテーブルにも注意を払う必要がありそう。(参考: OpenTypeフォントの続き(9)・・・インストラクション: vanillaの日記, TrueType Instruction Set (OpenType 1.8.4) - Typography | Microsoft Docs)
  • ハイフンマイナス(U+002D)がなぜか太く表示されてしまうようなので 縦110%拡大したら小さなフォントサイズでチルダの波形が波に見えない場合があったので、そのグリフだけはヒント情報とヒント命令を生成した。他の文字はヒント情報をFontForgeで自動生成すると文字の形や幅がおかしくなることがあった。
  • ベースラインが揃っていないのが気になる。元々メイリオ自体漢字や仮名は少しベースラインより下にはみ出してる。調整したいがメイリオ側を変形するのはヒンティングが大丈夫が不安。Inconsolataをベースラインより下げてしまうのも手かも?
  • なぜかInconsolata部分の高さが微妙に低い(ヒントがないせい?)ので縦方向に少し(110%ほど)拡大した。
2021-07-17 ,

指定した任意のorgファイルへエントリー(サブツリー)を移動する方法

org-refile (C-c C-w) でorg-modeのエントリー(サブツリー)を他の場所へ移動できるのだけど、デフォルトだとカレントバッファ内の第1レベルのエントリ内へしか移動できない。

移動先の候補は org-refile-targets 変数で変更できるのだけど、その場で好きなファイルを指定してその中へ移動というのは出来ないように見える。これまで org-refile-targets には (org-agenda-files :maxlevel . 2) を指定していたのでアジェンダ処理対象ファイルへは移動できるのだけど、特定のプロジェクト専用のorgファイル(あちこちのディレクトリにある)へ移動したい場合には困る。

やりたいことはシンプルに、C-c C-wの後にファイルを指定して、次に移動先のエントリーを指定したいだけなのだけど。

変数 org-refile-use-outline-path や org-outline-path-complete-in-steps を使うと最初にファイルを指定できるようにもなるけれど、やっぱり org-refile-targets の制限を受けてしまう。

これまではエントリーをカット&ペーストしていたのだけど、やはり C-c C-w で出来た方が便利だ。ちなみにペーストは任意のエントリーの子としてペーストする機能が見当たらずペーストしてからレベルを調整する必要があって手間がかかる。C-c C-x C-y (org-paste-special) は現在の場所と同一レベルにペーストするので M-right で一段下げなければならない。

どうにもならないのかと諦めかけたところ、次の記事を見た。

目にとまったのはこの部分。

org-refile-targets lets you control which files to consider, e.g.:

nil  ;; only the current file
'((org-agenda-files :maxlevel . 2)) ;; all agenda files, 1st/2nd level
'((org-files-list :maxlevel . 4)) ;; all agenda and all open files
'((my-org-files-list :maxlevel . 4)) ;; all files returned by `my-org-files-list'

org-refile-targets には任意の関数を指定できるのであった。その関数は移動候補となるファイル名のリストを返すことになっている。

(org-files-list) 関数はアジェンダファイルと現在開いているorgファイルを返すので、それを指定すればアジェンダファイルの他に現在開いているorgファイルへも移動できるようになる。

完全に任意のファイルとはいかないが、先にファイルを開いておけばいいだけなので当分これで我慢しておこうと思う。

2021-05-01

2021年春の新番組と視聴環境

さて、今期はどうでしょうね。

03/27(土) 17:30~ 日本テレビ 僕のヒーローアカデミア 第5期
03/28(土) 24:40~ NHK総合 赤ちゃん本部長
03/31(水) 24:00~ ニコニコ動画 ヘタリア World★Stars
04/01(木) 17:55~ テレビ東京 シャーマンキング 新作
04/01(木) 22:30~ TOKYO MX ゴジラ シンギュラポイント(Godzilla Singular Point)
04/01(木) 24:00~   天地創造デザイン部【特別編】
04/01(木) 27:00~ BS-TBS 歌うサッカーパンダ ミファンダ
04/02(金) 18:55~ NHK Eテレ もっと!まじめにふまじめ かいけつゾロリ 第2シリーズ
04/02(金) 22:00~ TOKYO MX SSSS.DYNAZENON(ダイナゼノン)
04/02(金) 25:23~ テレビ東京 灼熱カバディ
04/02(金) 26:25~ TBS ましろのおと
04/03(土) 08:00~ テレビ東京 カードファイト!! ヴァンガード overDress
04/03(土) 09:00~ NHK Eテレ おしりたんてい 新シリーズ
04/03(土) 12:00~   どすこい すしずもう
04/03(土) 22:30~ TOKYO MX Thunderbolt Fantasy 東離劍遊紀3
04/03(土) 23:30~ TOKYO MX Vivy -Fluorite Eye's Song-
× 04/03(土) 24:57~ TOKYO MX ダイナ荘びより
04/04(日) 09:30~ テレビ東京 マジカパーティ
04/04(日) 22:00~ TOKYO MX ドラゴン、家を買う。
04/04(日) 22:30~ TOKYO MX 憂国のモリアーティ 2クール目
04/04(日) 23:00~ TOKYO MX NOMAD メガロボクス2
04/04(日) 23:30~ TOKYO MX さよなら私のクラマー
04/04(日) 24:00~ TOKYO MX セブンナイツレボリューション -英雄の継承者-
04/04(日) 24:10~ NHK総合 キングダム 第3シリーズ ※第1話から放送再開
04/04(日) 24:30~ TOKYO MX 戦闘員、派遣します!
04/04(日) 25:00~ TOKYO MX 黒ギャルになったから親友とヤってみた。
04/05(月) 22:30~ TOKYO MX やくならマグカップも
04/05(月) 23:00~ TOKYO MX 恋と呼ぶには気持ち悪い
04/05(月) 24:00~ TOKYO MX ひげを剃る。そして女子高生を拾う。
04/05(月) 25:30~ テレビ東京 フルーツバスケット The Final
04/05(月) 26:00~ テレビ東京 オッドタクシー
04/06(火) 07:30~ テレビ東京 iiiあいすくりん 
04/06(火) 18:45~ NHK Eテレ ふしぎ駄菓子屋 銭天堂 新シリーズ
04/06(火) 23:00~ TOKYO MX 転スラ日記
04/06(火) 24:30~ TOKYO MX 聖女の魔力は万能です
× 04/06(火) 25:29~ 日本テレビ 擾乱 THE PRINCESS OF SNOW AND BLOOD
× 04/07(水) 18:45~ NHK Eテレ 宇宙なんちゃら こてつくん
04/07(水) 22:00~ TOKYO MX MARS RED
04/07(水) 23:30~ TOKYO MX 究極進化したフルダイブRPGが現実よりもクソゲ―だったら
04/07(水) 25:35~ TOKYO MX スーパーカブ
× 04/08(木) 23:30~ TOKYO MX Fairy蘭丸~あなたの心お助けします~
04/08(木) 24:00~ TOKYO MX ゾンビランドサガリベンジ
× 04/08(木) 24:55~ フジテレビ バクテン!!
04/08(木) 25:28~ TBS 異世界魔王と召喚少女の奴隷魔術Ω (第2期)
04/08(木) ~ Netflix 極主夫道
04/09(金) 18:25~ テレビ東京 妖怪ウォッチ♪(新シリーズ)
04/09(金) 19:25~ テレビ東京 新幹線変形ロボ シンカリオンZ
× 04/09(金) 25:25~ TBS すばらしきこのせかい The Animation
04/09(金) 25:50頃~ TBS 結城友奈は勇者である ちゅるっと! (ショートアニメ)
× 04/09(金) 25:55~ TBS BLUE REFLECTION RAY/澪
04/10(土) 22:00~ TOKYO MX スライム倒して300年、知らないうちにレベルMAXになってました
04/10(土) 24:00~ TOKYO MX 86―エイティシックス―
04/10(土) 24:30~ TOKYO MX シャドーハウス
× 04/10(土) 24:55~ 日本テレビ EDENS ZERO
× 04/10(土) 25:00~ TOKYO MX イジらないで、長瀞さん
× 04/10(土) 25:30~ テレビ朝日 バトルアスリーテス 大運動会ReSTART!
04/10(土) 26:00~ テレビ朝日 美少年探偵団
  04/11(日) 10:30~ テレビ東京 ミュークルドリーミー みっくす!
× 04/11(日) 25:35~ テレビ東京 東京リベンジャーズ
04/12(月) 22:50~ NHK Eテレ 不滅のあなたへ
× 04/12(月) 23:57~ TOKYO MX いたずらぐまのグル~ミ~
04/13(火) 22:29~ TOKYO MX SDガンダムワールド ヒーローズ
× 04/14(水) 24:55~ フジテレビ セスタス -The Roman Fighter-
× 04/14(水) 25:05~ TOKYO MX 幼なじみが絶対に負けないラブコメ
04/17(土) U-NEXT 新テニスの王子様 氷帝vs立海 Game of Future 後篇
04/17(土) 17:35~ NHK Eテレ 魔入りました!入間くん 第2シリーズ
04/28(水) ~ Netflix YASUKE -ヤスケ-

今期一押しはやっぱりスーパーカブ。とても丁寧な作りで大変好感が持てますね。私好みの話でもあります。ただ、退屈に思う人もいるでしょうね。幸薄そうな女の子が鼻につく人もいるかもしれません。個人的にはこの手の現実への適応、ちょっとした心の成長を描く話はもう少し男の子が主人公のものがあっても良いのではと常々。

他に数話見た段階で気になるのは:

他にもいくつか視聴を継続しているものはあります。

ところで今期から視聴の大半をネット配信に移行しました。というのもほとんどテレビが映らなくなってしまったからです。チューナーを変えてもダメなのでおそらくアンテナから部屋までのどこかに問題があるのではないかと思います。最近はほぼ全ての作品がネット配信されているのでほとんど困ることはありません。Abemaやニコニコの無料視聴もありますし、Prime Videoは元々見られますし、dアニメストアにも入ったのでそれでほぼ全て網羅できます。良い時代になったものです。

dアニメストアの月額440円は安すぎです。見逃してしまったりまた見たいと思っていた過去の作品もいくらでも見られます。以前カノープスのチューナーカードMTV2000が壊れたときに途中から見られなかった(続きが気になっていた)ファンタジックチルドレンと巌窟王をようやく見ることが出来ました。また見たいなと思っていた作品も色々見ています。真っ先に見たのはスケッチブック~full color's~でした。最終回付近で面白さに気がついて録画を残していなかったパターンです。まぁ、私好みですよね。

現状でいくつか見られないものもあるのですが、一番大きいのはNetflix独占ものです。正直1~2本の作品を見るためだけに入るのはコスパが悪い気がします。今期ならゴジラはMXで3話くらいまでかろうじて見られたので続きが気になっています。前期のBEASTARSも続きがみたいのですが。短い期間だけ入って後でまとめて見た方が良いかもしれません。

2021-02-01

2021年冬の新番組

さて、今期も一通り見終わりました。

× 01/01(金) ネット配信 幼女社長
01/03(日) 25:00~ TOKYO MX じみへんっ!!~地味子を変えちゃう純異性交遊~
01/04(月) 21:00~ TOKYO MX ゲキドル ACTIDOL SCHOOL 新春スペシャル
01/04(月) 23:00~ TOKYO MX たとえばラストダンジョン前の村の少年が序盤の街で暮らすような物語
01/04(月) 22:30~ TOKYO MX 7SEEDS 第2期
01/04(月) 24:30~ TOKYO MX 裏世界ピクニック
01/04(月) 24:00~ TOKYO MX ウマ娘 プリティーダービー Season2
01/05(水) 07:30~ テレビ東京系 PUI PUI モルカー
01/05(火) 21:00~ AT-X ゲキドル ACTIDOL SCHOOL
01/05(火) 23:00~ TOKYO MX 転生したらスライムだった件 特別話数「閑話:ヒナタ・サカグチ」
01/06(水) 17:55~ テレビ東京系 七つの大罪 憤怒の審判
01/06(水) 23:30~ TOKYO MX Re:ゼロから始める異世界生活(第2期 後半クール)
× 01/06(水) 23:00~ TOKYO MX アイ★チュウ
× 01/06(水) 24:00~ TOKYO MX 装甲娘戦機
× 01/06(水) 24:30~ TOKYO MX オルタンシア・サーガ
01/06(水) 24:55~ フジテレビ BEASTARS 第2期
01/06(水) 25:57~ ABCテレビ エビシー修業日記
01/07(木) 22:00~ TOKYO MX SHOW BY ROCK!!STARS!!
01/07(木) 23:30~ TOKYO MX ゆるキャン△ SEASON2
01/07(木) 24:00~ TOKYO MX 天地創造デザイン部
01/07(木) 24:30~ TOKYO MX ひぐらしのなく頃に 業(新作) 14話以降
01/07(木) 24:55~ フジテレビ 2.43 清陰高校男子バレー部
01/07(木) 26:10~ フジテレビ 約束のネバーランド 第2期
01/07(木) 25:28~ TBS 五等分の花嫁∬
× 01/08(金) 22:00~ TOKYO MX 弱キャラ友崎くん
× 01/08(金) 22:30~ TOKYO MX 蜘蛛ですが、なにか?
01/08(金) 23:00~ BS朝日 アイドールズ!-IDOL Survival-
01/08(金) 24:00~ TOKYO MX バック・アロウ
01/08(金) 24:30~ TOKYO MX WIXOSS DIVA(A)LIVE
01/08(金) 25:00~ TOKYO MX おとなの防具屋さん 第2期
× 01/08(金) 25:55~ TBS プレイタの傷 PROJECT SCARD
01/08(金) 26:55~ TBS 俺だけ入れる隠しダンジョン
× 01/09(土) 23:30~ TOKYO MX はたらく細胞!!
× 01/09(土) 24:00~ TOKYO MX はたらく細胞 BLACK
× 01/09(土) 24:30~ TOKYO MX ホリミヤ
01/09(土) 25:00~ TOKYO MX Levius レビウス
01/09(土) 25:30~ TOKYO MX 怪病医ラムネ
01/09(土) 25:30~ テレビ朝日系 ワールドトリガー 2ndシーズン
01/09(土) 26:00~ テレビ朝日系 SK∞ エスケーエイト
× 01/10(日) 07:00~ テレビ東京系 アイカツプラネット!
01/10(日) 21:30~ TOKYO MX 魔道祖師(日本語吹替版)
01/10(日) 21:54~ テレビ愛知 八十亀ちゃんかんさつにっき 3さつめ
01/10(日) 22:00~ TOKYO MX 怪物事変
× 01/10(日) 22:30~ TOKYO MX スケートリーディング☆スターズ
× 01/10(日) 23:30~ TOKYO MX IDOLY PRIDE
01/10(日) 24:00~ TOKYO MX 無職転生-異世界行ったら本気だす-
01/10(日) 25:05~ TOKYO MX EX-ARMエクスアーム
01/10(日) 25:50~ テレビ東京 のんのんびより のんすとっぷ
01/10(日) 27:20~ テレビ東京 闇芝居 第8期
01/11(月) 25:00~ TOKYO MX アズールレーン びそくぜんしんっ!
01/11(月) 25:10~ TOKYO MX 真・中華一番! 第二期
01/11(月) 26:00~ テレビ東京 WAVE!!~サーフィンやっぺ!!~
01/12(火) 23:00~ TOKYO MX 転生したらスライムだった件 第2期 第1部
01/12(火) 24:30~ TOKYO MX 文豪ストレイドッグス わん!
× 01/12(火) 24:45~ TOKYO MX ワールドウィッチーズ発進しますっ!
01/12(火) 25:35~ 日本テレビ ワンダーエッグ・プライオリティ
01/13(水) 19:25~ NHK Eテレ ログ・ホライズン -円卓崩壊-
01/13(水) 25:05~ TOKYO MX 回復術士のやり直し
01/13(水) 28:00~ AT-X 回復術士のやり直し(完全《回復》ver.)
01/14(木) 22:30~ TOKYO MX Dr.STONE(ドクターストーン) 第2期
01/14(木) ~ ABCテレビ 開運 キンタマーニドッグ
01/20(水) 22:00~ TOKYO MX 魔術士オーフェンはぐれ旅 キムラック編

一番よく出来ていると思ったのは無職転生でしょうか。なろうのくせに、異世界転生俺ツエーもののくせに面白いぞと心中複雑な感じです。何がいいんでしょうね。丁寧に作られている感はありますね。話運びもスムーズだし、美術面も綺麗ですし。

次に気になるのがエスケーエイト。スケボーレースに興じる青少年たちの話。何でもありのレースと癖のあるキャラクターたちでこれまでのところなかなか楽しめる作品になっています。

前々から期待されていた作品と言えばゆるキャン△とのんのんびよりでしょうか。

ゆるキャン△は前期に比べるとだいぶ見劣りがするスタート。特に1話は疑問に思うところが多かったのですが、後で聞いたところによるとそういった点はアニメオリジナル要素だと聞いて納得。へやキャン△もイマイチでしたからね。現在4話まで進んで少しずつ良くなっている気もするので今後に期待します。

のんのんびよりは、まぁ、元々常に面白いわけではないので、時々面白い話があればOKです。

2021-01-31

Emacs Lispから音を鳴らす

ふとEmacs Lispマニュアルの Sound Output のところが目に入ったので試してみました。サウンドまわりは大昔に試してあまり良くなかった記憶がありますがその後どれだけ改善されたかな。

マニュアルを読んでみると (play-sound '(sound :file "ファイルへのパス"))(play-sound '(sound :data "サウンドデータ")) で鳴るみたいです。

試しにサイン波でも生成して鳴らしてみましょうか。

対応形式は.wav(RIFF)または.auと書いてあります。.auの方がヘッダーがシンプルなのでそれにしましょう。

44.1KHzモノラルの16-bit PCMのヘッダーは文字列にすると ".snd\x00\x00\x00\x18\xff\xff\xff\xff\x00\x00\x00\x03\x00\x00\xac\x44\x00\x00\x00\x01" の24バイトですね(長さは不明ffffffffとしてあります)。各要素はビッグエンディアンな32-bit整数です。

16-bit PCMの各要素は負号付き整数みたいですね。-32767~0~32768。

うーん、どうしましょう。文字列バッファを先に確保して、そこにバイト列を埋めていく方が速度的に良さそうでしょうか? (適当に文字列結合しまくる方が作るのは楽そうですが)

ひとまず16-bit, 32-bit整数を文字列化する関数を作り……

(defun my-snd-aset-i32 (bytearray index x)
  (aset bytearray index (logand 255 (ash x -24)))
  (aset bytearray (+ index 1) (logand 255 (ash x -16)))
  (aset bytearray (+ index 2) (logand 255 (ash x -8)))
  (aset bytearray (+ index 3) (logand 255 x)))

(defun my-snd-aset-i16 (bytearray index x)
  (aset bytearray index (logand 255 (ash x -8)))
  (aset bytearray (+ index 1) (logand 255 x)))

サイン波を生成する関数を作り……

(defconst my-snd-byte-size-i16 2)
(defconst my-snd-amplitude-i16 32767)

(defun my-snd-aset-sin-wave (dst-buffer dst-index num-samples wavelength)
  (let ((2pi/wavelength (/ (* 2 pi) wavelength))
        (i 0))
    (while (< i num-samples)
      (my-snd-aset-i16 dst-buffer dst-index
                       (truncate (* my-snd-amplitude-i16
                                    ;;周期の誤差をどこまで許容するかよく分からないよね
                                    (sin (* i 2pi/wavelength)))))
      (setq dst-index (+ dst-index my-snd-byte-size-i16))
      (setq i (1+ i))))
  dst-buffer)

試しに440Hzのサイン波を含むauデータを生成する関数を作る。

(defun my-snd-generate-au-sin-wave-440hz-1sec ()
  (let* ((samples-per-sec 44100)
         (seconds 1)
         (num-samples (* seconds samples-per-sec))
         (data-size (* my-snd-byte-size-i16 num-samples))
         (data (make-string (+ 24 data-size) 0)))
    ;; header
    (my-snd-aset-i32 data 0 #x2e736e64)
    (my-snd-aset-i32 data 4 24) ;;Data offset
    (my-snd-aset-i32 data 8 data-size)
    (my-snd-aset-i32 data 12 3) ;;16-bit linear PCM
    (my-snd-aset-i32 data 16 samples-per-sec)
    (my-snd-aset-i32 data 20 1) ;;Channels
    ;; data
    (my-snd-aset-sin-wave data 24 num-samples (/ samples-per-sec 440.0))

    data))

鳴らしてみましょぅ。

(play-sound (list 'sound :data (string-as-unibyte ;;string-as-unibyte必要?
                                (my-snd-generate-au-sin-wave-440hz-1sec))))

あれ、結果は (error "Invalid sound specification") だって。

おかしいなデータが壊れているのかな。試しにファイルに書き出してみる。

(with-temp-file "~/tmp/sin440.au"
  (set-buffer-file-coding-system 'no-conversion)
  (insert (string-as-unibyte (my-snd-generate-au-sin-wave-440hz-1sec))))

プレイヤーで鳴らしたら「ポー」っとちゃんと鳴ります。

それならこのファイルをplay-soundで鳴らしてみる。

(play-sound '(sound :file "~/tmp/sin440.au"))

あれ鳴らない、と思ったらプレイヤーで開いていたからでした。プレイヤーを閉じたらEmacsからちゃんと鳴りました。

play-sound関数はsubr.elにあって、Emacsのサウンドサポートを確認してからplay-sound-internal関数を呼び出しています。play-sound-internal関数はsound.cにあって、最初の方でparse_sound関数を呼び出しています。parse_sound関数を見ると……

#else /* WINDOWSNT */
  /*
    Data is not supported in Windows.  Therefore a
    File name MUST be supplied.
  */
  if (!STRINGP (attrs[SOUND_FILE]))
    {
      return 0;
    }
#endif /* WINDOWSNT */

Windowsでは :data をサポートしていないんだって!


そういえば昔Emacsから音を鳴らしたくて↓こういうのを作ったのでした。

これは何万個もある音声ファイルをテキストファイルと付き合わせて確認する作業で活躍したのでした。今でも動くのかは不明。

2021-01-02 , ,

org-mode文書の中に囲碁の碁盤を埋め込む

Emacsの中で囲碁の棋譜(SGF)を編集するツールを作りました。

2020-01-02-el-igo-screenshot.gif

この通り、org-mode文書の中にSGF形式の棋譜を埋め込んで編集できます。エクスポートも可能。この文書自体org-modeで書いているのでほらこの通り(隅の七目)。

作ったのは碁の勉強をするにあたって普段使っているorg-modeでノートが取れたら便利そうだなと思ったことがきっかけです。

ちょうど先日EmacsでのSVG使用例を見かけてもっと自由に使っていいんだと感銘を受けたところでした。

SVGといえば少し前にJavaScriptでSVGを使った碁盤を作ったところだったので、SVGでの碁盤の表現方法はすでに分かっていました。なので基本的な考え方はこれを踏襲してEmacs Lisp上で実装し直すことにしました。

最近書いていたのはそのための要素技術なのでした。

思いのほかよく出来て、機能的にも内部構造的にもJavaScript版より良くなった気がします。

私は以前からEmacsやorg-modeにはもっとビジュアルで埋め込まれた要素が沢山あって良いと思っていたので迷わずorg-mode文書の中に碁盤を埋め込んで表示してビジュアルに編集できる仕様を思いつきました。

最初はソースブロック(#+begin_src sgf ~ #+end_src)を盤面のオーバーレイに置き換えてみたのですがソースブロックは評価するとかしないとか結果が何だとか色々としっくり来ないところが多かったので(棋譜を評価した結果って何!?)最終的に専用の特殊ブロック(#+begin_igo ~ #+end_igo)を盤面にすることにしました。

みんなもっと自分の好きなものを埋め込みましょう。私が音楽が好きだったら譜面入力インタフェースを作成することでしょう。簡単な作図くらいorg-modeの中で完結しても罰は当たらないと思いますよ。XWidgets? 日和ってんじゃねーよ! とはいえ現状のSVG一本槍では複雑な用途には厳しいものがありますね。更新速度と当たり判定的に。

2020-12-19 ,

個別のorg-modeファイルにエクスポート時のフィルターを設定する

Defining filters for individual files(個別のファイルにフィルタを定義する)に書かれている例を試してみました。

#+BIND: org-export-filter-timestamp-functions (tmp-f-timestamp)
#+BIND: org-export-filter-strike-through-functions (tmp-f-strike-through)
#+BEGIN_SRC emacs-lisp :exports results :results none
  (defun tmp-f-timestamp (s backend info)
    (replace-regexp-in-string "&[lg]t;\\|[][]" "" s))
  (defun tmp-f-strike-through (s backend info) "")
#+END_SRC

time stamp [2020-12-19 Sat 14:06]

abc +def+ ghi

この例では二つのフィルタ関数を作成しています。一つはタイムスタンプのブラケット([]や<>)を消去する tmp-f-timestamp 関数、もう一つは取り消し線のテキストをまるまる消去する tmp-f-strike-through 関数です。一般的にフィルタ関数は、引数sの文字列(バックエンドによって変換済みの文字列)を再加工して返すことになっています。

この二つの関数をソースブロック( #+BEGIN_SRC emacs-lisp#+END_SRC )内に書くことでエクスポートするたびに評価させ関数を定義しています。ヘッダーオプション :exports results :results: none の指定によって、エクスポートするたびに毎回評価させつつ実際には何も(リスティングや結果等を)エクスポートしないようにしています。

さらに #+BIND: でそれらの関数を変数 org-export-filter-timestamp-functionsorg-export-filter-strike-through-functions に設定しています。

これであとはエクスポートすれば time stamp [2020-12-19 Sat 14:06][] が削除され、 abc +def+ ghi の部分が abc ghi になるはず……

あれれ、HTMLでエクスポートしてみましたがフィルタが適用されていません。

ソースブロック(#+BEGIN_SRC#+END_SRC)の部分はエクスポート時にyes/noの確認がありyesを選択しました。 tmp-f-timestamptmp-f-strike-through はちゃんとEmacs内に登録されているので評価されていることは間違いありません(エクスポートが終わっても tmp- で始まる関数が残っているのは不愉快ではありますが)。

変数 org-export-filter-timestamp-functionsorg-export-filter-strike-through-functionsnil 。となるとBIND( #+BIND: )の部分が機能していない? しかし調べたところ #+BIND: はエクスポートの間だけバッファローカル変数になると書かれています。なら今nilでも当然ですが……ははぁ、これはセキュリティがらみですね。BIND部分を無条件で評価してしまうと他人から貰ったorg文書をエクスポートしたときにイタズラされる可能性があります。

探してみると org-export-allow-bind-keywords という変数があって nil になっていました。試しにバッファ内で M-: (setq-local org-export-allow-bind-keywords t) してみたらちゃんとフィルタが機能しました。

うーんしかし org-export-allow-bind-keywords を常に t にするのは少々不安ですし、どうするべきなんでしょう。

試しに (setq-local org-export-allow-bind-keywords t) をソースブロックの中に入れてみました。

#+BIND: org-export-filter-timestamp-functions (tmp-f-timestamp)
#+BIND: org-export-filter-strike-through-functions (tmp-f-strike-through)
#+BEGIN_SRC emacs-lisp :exports results :results none

  ;;; 追加!!!
  (setq-local org-export-allow-bind-keywords t)

  (defun tmp-f-timestamp (s backend info)
    (replace-regexp-in-string "&[lg]t;\\|[][]" "" s))
  (defun tmp-f-strike-through (s backend info) "")
#+END_SRC

time stamp [2020-12-19 Sat 14:06]

abc +def+ ghi

ファイルを開き直してエクスポート(ソースブロックの評価はyes/no確認あり)してみるとちゃんとフィルタが機能してブラケットや取り消し線部分が消去されていました。つまりBINDとソースブロックを評価するセキュリティリスクがソースブロックに一本化されたわけです。どうなんだろうこれ。

最初からBINDなんか使わなければいいんじゃないでしょうか。

#+BEGIN_SRC emacs-lisp :exports results :results none
  (setq-local org-export-filter-timestamp-functions '(tmp-f-timestamp))
  (setq-local org-export-filter-strike-through-functions '(tmp-f-strike-through))

  (defun tmp-f-timestamp (s backend info)
    (replace-regexp-in-string "&[lg]t;\\|[][]" "" s))
  (defun tmp-f-strike-through (s backend info) "")
#+END_SRC

time stamp [2020-12-19 Sat 14:06]

abc +def+ ghi

うん、これでもちゃんとフィルタされます。

逆にBINDだけにするなら次のようにすれば良いでしょう。

#+BIND: org-export-filter-timestamp-functions ((lambda (s backend info) (replace-regexp-in-string "&[lg]t;\\|[][]" "" s)))
#+BIND: org-export-filter-strike-through-functions ((lambda (s backend info) ""))

time stamp [2020-12-19 Sat 14:06]

abc +def+ ghi

この場合 org-export-allow-bind-keywords 変数が t ならフィルタされ nil ならされません。 org-confirm-babel-evaluate 変数のようにBINDを評価するときにyes/no確認が出せれば良いのかもしれないが、そういう設定は見当たりません。まぁ、そこまでするならソースブロックで良いでしょう。

ちなみにソースブロックを使う方法で tmp-* 関数が残ってしまうのが気に入らなければ次のようにlambdaにしてしまえば良いのでしょうきっと。

#+BEGIN_SRC emacs-lisp :exports results :results none
(setq-local org-export-filter-timestamp-functions
            (list (lambda (s backend info)
                    (replace-regexp-in-string "&[lg]t;\\|[][]" "" s))))
(setq-local org-export-filter-strike-through-functions
            (list (lambda (s backend info)
                    "")))
#+END_SRC

time stamp [2020-12-19 Sat 14:06]

abc +def+ ghi

評価するかいちいち確認するのが嫌ならば(かつ org-confirm-babel-evaluatenil にするのが嫌ならば)、あらかじめ何らかの条件で自動的にフィルタが設定されるようにどこかに仕込んでおくことになりそうです。

2020-12-13

Emacs Lisp でマウスの動きを追跡する

(Emacs27.1, Windows 10で確認)

まず普段マウスに関してどんなイベントが来るのか調べる。

次のコードを実行した後ウィンドウの中でマウスを色々操作するとイベント(発生した出来事の詳細を表すリスト)の内容が出力される。

(while t
  (message "%s" (read-event)))
(down-mouse-1 (#<window 26 on *scratch*> 186 (400 . 236) 979458359 nil 186 (50 . 6) nil (400 . 104) (8 . 22)))
(mouse-1 (#<window 26 on *scratch*> 186 (400 . 236) 979458625 nil 186 (50 . 6) nil (400 . 104) (8 . 22)))
(down-mouse-1 (#<window 26 on *scratch*> 186 (213 . 271) 979474031 nil 186 (26 . 6) nil (213 . 139) (8 . 22)))
(drag-mouse-1 (#<window 26 on *scratch*> 186 (213 . 271) 979474031 nil 186 (26 . 6) nil (213 . 139) (8 . 22)) (#<window 26 on *scratch*> 186 (271 . 232) 979475453 nil 186 (33 . 6) nil (271 . 100) (8 . 22)))
(down-mouse-1 (#<window 26 on *scratch*> 186 (113 . 250) 979478593 nil 186 (14 . 6) nil (113 . 118) (8 . 22)))
(mouse-1 (#<window 26 on *scratch*> 186 (113 . 250) 979478687 nil 186 (14 . 6) nil (113 . 118) (8 . 22)))
(down-mouse-1 (#<window 26 on *scratch*> 186 (102 . 250) 979479796 nil 186 (12 . 6) nil (102 . 118) (8 . 22)))
(drag-mouse-1 (#<window 26 on *scratch*> 186 (102 . 250) 979479796 nil 186 (12 . 6) nil (102 . 118) (8 . 22)) (#<window 26 on *scratch*> 186 (161 . 244) 979480796 nil 186 (20 . 6) nil (161 . 112) (8 . 22)))
(down-mouse-3 (#<window 26 on *scratch*> 186 (252 . 245) 979484453 nil 186 (31 . 6) nil (252 . 113) (8 . 22)))
(mouse-3 (#<window 26 on *scratch*> 186 (252 . 245) 979484578 nil 186 (31 . 6) nil (252 . 113) (8 . 22)))

リストの各要素の意味はInput Events - Emacs Lisp Reference Manualを参照のこと。

ボタンの押し離しについてはイベントが来るが動きについてはイベントが来ていないことが分かる。

マウスの動きを得るには track-mouse 変数か (track-mouse body ) マクロを使う。あるコマンドで track-mouse 変数を t にして別のコマンドでnilにするやり方もあり得るが、一つのコマンドの中で (track-mouse body ) マクロを使う方がnilに戻し忘れる心配が無くて無難。(Mouse Tracking - Emacs Lisp Reference Manual)

実際にtrack-mouseを使ったときにどんなイベントが来るのか調べる。

(track-mouse
  (while t
    (let ((ev (read-event)))
      (message "basic type=%s modifiers=%s event=%s" (event-basic-type ev) (event-modifiers ev) ev))))
basic type=mouse-movement modifiers=nil event=(mouse-movement (#<window 26 on *scratch*> 301 (265 . 379) 1465867125 nil 301 (32 . 7) nil (89 . 239) (8 . 20)))
basic type=mouse-movement modifiers=nil event=(mouse-movement (#<window 26 on *scratch*> 301 (272 . 361) 1465867531 nil 301 (33 . 7) nil (96 . 221) (8 . 20)))
basic type=mouse-movement modifiers=nil event=(mouse-movement (#<window 26 on *scratch*> 301 (276 . 359) 1465867640 nil 301 (33 . 7) nil (100 . 219) (8 . 20)))
basic type=mouse-movement modifiers=nil event=(mouse-movement (#<window 26 on *scratch*> 301 (280 . 358) 1465867734 nil 301 (34 . 7) nil (104 . 218) (8 . 20)))
basic type=mouse-movement modifiers=nil event=(mouse-movement (#<window 26 on *scratch*> 301 (288 . 358) 1465867859 nil 301 (35 . 7) nil (112 . 218) (8 . 20)))
basic type=mouse-movement modifiers=nil event=(mouse-movement (#<window 26 on *scratch*> 301 (297 . 358) 1465868015 nil 301 (36 . 7) nil (121 . 218) (8 . 20)))
basic type=mouse-movement modifiers=nil event=(mouse-movement (#<window 26 on *scratch*> 301 (305 . 360) 1465868078 nil 301 (37 . 7) nil (129 . 220) (8 . 20)))
basic type=mouse-movement modifiers=nil event=(mouse-movement (#<window 26 on *scratch*> 301 (308 . 380) 1465868828 nil 301 (37 . 7) nil (132 . 240) (8 . 20)))
basic type=mouse-movement modifiers=nil event=(mouse-movement (#<window 26 on *scratch*> 301 (303 . 389) 1465869062 nil 301 (36 . 7) nil (127 . 249) (8 . 20)))
basic type=mouse-movement modifiers=nil event=(mouse-movement (#<window 26 on *scratch*> 301 (295 . 393) 1465869281 nil 301 (35 . 7) nil (119 . 253) (8 . 20)))

マウスを動かしただけで次々とmouse-movementイベントが来る。

マウスイベントには次の種類がある。

  • Click (例: mouse-1, wheel-up)
  • Drag (例: drag-mouse-1)
  • Button Down (例: down-mouse-1)
  • Repeat (例: double-mouse-1)(いわゆるダブルクリックやトリプルクリック)
  • Motion (例: mouse-movement)

主な操作に対して発生する一連のイベントの流れはだいたい次のようになる。

  • クリック時に来るイベントの流れ:

    1. down-mouse-1
    2. mouse-movement (0回以上)
    3. mouse-1

    基本的にはDown→Clickの順に発生する。まれにこの間にMotionが挟まることがある。押し下げと解放の間にわずかに動いた場合mouse-movementが来る場合がある。閾値以下の動きならドラッグではなくクリックと判定される。

    up-mouse-1というのは無い。Click(mouse-1)かDrag(drag-mouse-1)のどちらかのみが来る。

    もちろんマウスボタン押し下げ中にキーボードを押した場合は、そのキーイベントが挟まる。

  • ドラッグ時に来るイベントの流れ:

    1. down-mouse-1
    2. mouse-movement (0回以上)
    3. drag-mouse-1

    ドラッグなのでほとんどの場合mouse-movementは来ることになるが、指し示す位置がスクロールによってのみ変わった場合は来ないこともある。つまりマウスボタンを押し下げて動かさずに何らかの方法でスクロールしてから離した場合。これもきちんとDragと判定される。

  • シフトキーを押したままクリックした時に来るイベントの流れ:

    1. S-down-mouse-1
    2. mouse-movement (0回以上)
    3. S-mouse-1

    シンボルにS-といった部分が付く。(21.7.4 Click Events - Emacs Lisp Reference Manual)

  • クリックの途中でシフトキーを押した場合(mouse down, shift down, mouse up):

    1. down-mouse-1
    2. mouse-movement (0回以上)
    3. S-mouse-1

    つまり、down-mouse-1に対応するmouse upがmouse-1やdrag-mouse-1とは限らない。S-mouse-1とかC-S-drag-mouse-1とかになることもある。逆にS-down-mouse-1(シフトを押しながらマウスボタン押し下げ)に対してmouse upが(S-が付かない)mouse-1になることもある(途中でシフトキーが離された場合)。

    判別にはevent-basic-typeとevent-modifiersを使うと良い。

    • (event-basic-type event) で 'mouse-1 や 'mouse-3 等の(S-等が付かない)ボタンの種類が返ってくる。
    • (event-modifiers event) で '(click) や '(drag) が返ってくる。

    (21.7.12 Classifying Events - Emacs Lisp Reference Manual)

  • mouse-movement中にボタンの状態を取得する方法は見当たらない。イベントの中にも無いし、位置における (mouse-position) のような現在の状態を取得する関数も見当たらない。
  • フレームの外に出た場合の挙動はOSによって違うかもしれないが、とりあえずWindows 10での動作:
    • フレーム外に出ても特に何かイベントは来ない。出たことを知る確実な方法は見当たらない。
    • ドラッグ中にフレームの外に出た場合、フレームがアクティブな間はマウスイベントはモーションも含めて来続ける。フレームの外でボタンを離したときもdrag-mouse-1が発生する。
    • ドラッグ中にフレームの外に出て、ドラッグしたままで(Alt-Tab等で)フレームが非アクティブになったとき、マウスイベントは来なくなる。この後マウスボタンが離されてもイベントは発生せず、離されたことを検出する方法は見当たらない。マウスカーソルがフレーム内に戻ってきたときにモーションイベントは発生するが、ボタンがすでに離されていることに気がつけない。
    • フレームが非アクティブの時にフレームの上をマウスが通過した場合、モーションイベントが来る。

以上の点に注意しながらプログラムを組んでいくことになる。

ひとまずtrack-mouseマクロとread-eventを使うとして、マウスイベント以外はどう処理したら良いのか。read-eventを使ってイベントを読み込むと当然マウス以外のイベントも来る。マウス入力中の不正入力として破棄しても問題ないのだろうけれど、一応デフォルトの処理をさせてみたい。イベントをデフォルト処理にdispatchする関数は見当たらない。 unread-command-events 変数を使うと、まだ処理していないイベントを先読みしたり、戻ししたり出来る。使わないイベントをread-eventで読んでしまった場合、 (push (cons t event) unread-command-events) で戻せる。実質的にこの操作がdispatchになる。余談だけど全く関係ないイベントを生成してポストすることも出来る。 (push (cons t ?a) unread-command-events) とすれば現在のポイントでaを押したときのキーバインドが呼び出される。その気になればこの変数だけからpeek-eventとかpost-eventとか作れそう。(21.8.6 Miscellaneous Event Input Features - Emacs Lisp Reference Manual)

ちなみにイベントを待つにはread-event等を使っても良いが、 (sit-for seconds) を使っても良い。時間またはイベントの到着を待つ。再描画(redisplay)もしてくれる。(21.10 Waiting for Elapsed Time or Input - Emacs Lisp Reference Manual)

以上の調査を元に、試しに画像(オブジェクト)に対してマウスの押し下げから移動、解放までを追跡してコールバックを呼び出す関数を作ってみた。

(defun track-dragging-on (down-event on-move &optional on-up on-leave)
  "マウスdownイベント(down-mouse-1やdown-mouse-3等)が発生したときにこの関数を呼び出すと、押したところにある画像上で発生するモーションイベントをON-MOVEにコールバックし続ける。ボタンが離されたり画像の外に出たり何かマウスイベント以外が発生したとき関数は終了する。"
  (if (not (memq 'down (event-modifiers down-event)))
      (error "down-event is not a down event. %s" (event-modifiers down-event)))
  (let* ((down-basic-type (event-basic-type down-event))
         (down-position (event-start down-event))
         (target-window (posn-window down-position))
         (target-point (posn-point down-position))
         (target-object (posn-object down-position)))

    (track-mouse
      (let (result)
        (while (null result)
          (let ((event (read-event)))
            (cond
             ;; mouse move
             ((mouse-movement-p event)
              ;; クリックしたときと同じオブジェクト上であることを確認する。
              ;; 単純にobjectをeqで判定できない。
              ;; displayプロパティの値(リスト)は毎回コピーされているようなので。
              ;; SVGを含んだ結構大きなリストになるのでequalを使うのもためらわれるし、一致したところで同じオブジェクトとは限らない。
              ;; 指しているウィンドウとポイントが同じでobjectの最初の要素(おそらく'image)が一致していることで確認している。
              (if (and (eq (posn-window (event-start event))
                           target-window)
                       (= (posn-point (event-start event))
                          target-point)
                       (eq (car (posn-object (event-start event))) ;;ex: 'image
                           (car target-object))) ;;ex: 'image
                  (if on-move (funcall on-move event))
                ;; out of target
                (if on-up (funcall on-leave event))
                (setq result 'leave)))
             ;; mouse up
             ((and (eq (event-basic-type event) down-basic-type)
                   (or (memq 'click (event-modifiers event))
                       (memq 'drag (event-modifiers event))))
              (if on-up (funcall on-up event))
              (setq result 'up))
             ;; otherwise
             (t
              (if on-up (funcall on-up event))
              (setq result 'unknown-event)
              (push (cons t event) unread-command-events)))))
        result))))

この関数を使って試しにSVG画像を作ってその中でドラッグしたときのイベントを表示させてみる。(画像の表示方法については先日の記事を参照のこと)

(let (;;適当な文字列("A")を追加しその範囲をstart,endとする。
      (start (point))
      (end (progn (insert "A") (point))))

  ;; start~endのdisplayプロパティにSVG画像を設定する。
  (put-text-property start end 'display
                     (let ((svg (svg-create 400 300)))
                       (svg-rectangle svg 0 0 400 300 :fill "#333")
                       (svg-image svg)))

  ;; start~endのkeymapプロパティに down-mouse-1 に反応するキーマップを設定する。
  (put-text-property
   start end 'keymap
   (let ((km (make-sparse-keymap)))
     (define-key km [down-mouse-1]
       (lambda (down-event)
         (interactive "e")
         (message "mouse down %s" down-event)

         (track-dragging-on
          down-event
          (lambda (motion-event)
            (message "mouse move %s" motion-event))
          (lambda (up-event)
            (message "mouse up %s" up-event))
          (lambda (leave-event)
            (message "mouse leave %s" leave-event)))

         (message "finish")))
     km))
  )