Chienomi

テキストをスクロール表示させる

webengine

プログラムについて

諸兄も一度はYouTubveで見たことがあるのではないだろうか。

文字がただ流れるだけの動画を。

自分のペースで読めない上に内容に乏しく、非常にイライラするものだが、 作業ウィンドウ以外で流しておくことでウィンドウフォーカスを切り替えなくて済む、というメリットもある。

特にmpvを使うと、右クリックで再生/停止ができるし、再生速度もコントロールできてまあまあ便利だ。

ただ、検索性がないのでやっぱりストレスである。

そこで、テキストリーダーに自動スクロール機能が欲しい、と思った。

ありそうだが、見当たらなかった。

もちろん、一行単位であればsleepreadでも組み合わせればいいのだが、これはパッと動いてしまうため目の追従が難しく、読みづらい。 1ピクセルずつ動かしたいのだ。

xdotoolを使う、という方法も考えたのだが、意図したようにはうまくいかなかった。

JavaScriptを使えばすごく簡単なのに…というわけで、JavaScriptを使う方向でがんばることにした。

すごーーーーく単純に一時ファイルでHTMLを作ってQtWebengineで表示すればいいと思って、Pythonで作ったのだが…

…Surfでよかった。

まぁ、配布するならSurfみたいなマイナーなものは入れたくないなんていう天邪鬼さん1や、Surfは入っていないなんていう弱小ディストリさんはいっぱいいると思うので良しとしよう。

そんなわけで作った。

珍しく日本語READMEもある。 BGM機能があるのは、YouTubeでよく見るものをリスペクトしたものであり、背景画像がほしければCSS編集でOKだ。

やっていることはごく単純で、Pandocで処理することを前提として一時ファイルを活用している。

プレーンテキストの場合はsedを使ってラインブロック化している。 この場合改行されなくなってしまうので、pre-wrapするようにデフォルトのスタイルシートで変更している。

スクロール制御はJavaScriptでものすごく入門的なコードだ。 右クリックでも再生・停止できるようにしているのは、コントロールをウィンドウフォーカスしてからでなくても行えるようにするためである。

ちなみに、今回のJavaScriptは互換性をあまり気にしない贅沢なコードでもある。 これはかなり新しいGtkWebkit、あるいはQtWebengineを使うという前提が成り立っているためだ。

解説

それでは恒例の初心者向けコード

JavaScript部分

せっかくなので、入門的なJavaScriptコードを解説しよう。

スクロール部分

まずスクロール自体はwindow.scroolByによって実現できる。

自動スクロールをするためにはこれを繰り返すようにしなくてはいけない。 もちろん、単純なループでもできるのだが、JavaScriptはシングルスレッドなので操作不能になってしまう。

このような反復はJavaScriptではタイマーイベントで行う。 タイマーにコールバック関数を登録することで、タイマー起動時にコールバック関数が実行される。 もし他の処理が実行中の場合は処理をキューに入れ、順番がきたら実行される。

反復の場合setIntervalのほうが簡単で、初心者向けの解説ではよくこちらが使われるが、 どちらかといえばタイマーイベントを都度登録するsetTimeoutのほうがコントロールしやすい。

関数オブジェクト、クロージャという考え方になれていないとそもそもコールバック関数が使えないので、ここはしっかりと理解しておく必要がある。

関数オブジェクトは実行可能なコード群をオブジェクト化したものである。 スイッチを押せば実行される物体になっているとでも思えばいい。 コールバック関数は、それを呼び出すタイミングでそのスイッチを押すように動作する。

setTimeout は指定した時間が経過したときにコールバックを行う。 コールバック関数の中で自身をコールバック関数とする setTimeout を呼ぶことでループさせることができる。

「スクロールの開始」はその関数を呼べば良い。 setTimeout で呼ばなくても一度呼べば setTimeout によってループする。

「スクロールの停止」は setTimeout をしなければ次回の実行がなされないため、停止する。 停止方法はタイマーをキャンセルするのではなく、次回のタイマーセットを行わないというものである。 このため、タイマー動作状態がonの場合のみ setTimeout を行う。

setInterval を使用した場合はタイマーをセット/キャンセルして制御する。

キーイベント部分

キーボードのキー入力は document あるいは window に対してイベントリスナーを設定する。

DOM Level1 のonKeydown を設定する場合の情報は割とあるのだが、 DOM Level3 の addEventListener を設定する情報はやや少ない。

コールバック関数の引数としては KeyboardEvent オブジェクトが渡される。 特定のキーをキャプチャするわけではなく、キーボード入力全てをキャプチャしてコールバック関数が呼び出される。 コールバック関数内でキーを判別する。

なお、コールバック関数が時間がかかるとフリーズしていると感じることになるため注意が必要。

キーの種別を取るための方法は色々あるのだが、 code が推奨される。 プリンタブルなキーに限定するのであれば key でも良い。 charcharCode はあまりうまく動作しないし、 keyCodewhich は廃止されている上に、プラットフォームに依存する。

それぞれどのような値になるのかは次のようなコードを書いて確認すれば良い。

document.addEventListener('keydown', (event) => {
    alert('char:' + event.char)
    alert('charCode:' + event.charCode)
    alert('code:' + event.code)
    alert('key:' + event.key)
    alert('(obsolete) keycode:' + event.keyCode)
    alert('(obsolete) which:' + event.which)
}, false)

元のキーの動作を無効にする場合は、第三引数を false として先にキーを捕捉してから Event.preventDefault によってバブリングを停止する。

右クリック禁止2、でお馴染み右クリックは contextmenu イベントになる3

スクロールスピード

スクロールは

  • スクロールする時間間隔が短くなると速くなる
  • 一度にスクロールする量が増えると速くなる

量が増えるとスムーズさが書けるため、時間を短くするほうが優先。 間隔が1(1ミリ秒)になった場合、加速はスクロール量によって行う。

逆に遅くするときはスクロール量が増えているならそちらを先に減らす。スクロール量が1であれば時間間隔を増やす。

1ミリ秒刻みでは使いにくかろうと思ったので、5ミリ秒刻み、ただし値が小さいほうが変化量は大きいため、5ミリ秒からは1ミリ秒で調整されるようにしている。 (50ミリ秒から+5された場合は10%遅くなるが、10ミリ秒から-5された場合は100%速くなる)

割合で増減させてもいいのだが、どちらかといえばキーリピートが効くため一定間隔にしたほうが良いUIであると考えられる。

今回は50ミリ秒をデフォルトとしたため、10%の5ミリ秒を増減単位とした。 なお、この速度感は画面のピクセル数によって異なり、特にピクセル数が多く高精細なディスプレイの場合は遅く、ディスプレイサイズが大きくピクセル数が少ない場合は速く感じることになる。

設計

基本的には「得意なことは得意な方法で」だ。

このようにスクロールやキーイベントなどはウェブブラウザとJavaScriptが簡単に書ける。 だから無理せずウェブブラウザとJavaScriptで書こうと考えたわけだ。

ただし余計な機能や情報があると使いにくい。 最低限のウェブブラウザが欲しいのだが、既にそのようなものはSurfがあるのでこれを使う。

もっとも、レンダリング部分はQtwebengineやGtkWebkitがあるのだから書くのは非常に簡単である。

もちろん、そのためにはテキストをHTMLにする必要がある。

テキストを正確にHTMLで表現するのはちょっと大変だが、CSSで white-space: pre-wrap にしてしまえばタグ要素と & をエスケープしてしまえば元のテキストを表現できる。 このようなことはSedでもできる。 ただし、 & を先にエスケープしなくてはならない。エスケープがエスケープされることを防ぐためだ。

テキスト処理するためのツールはLinux上に豊富にある。このようなことはシェルが得意とする部分だ。 文字列を埋め込むだけならば特別なツール(例えばeRuby)を使わなくても、ヒアドキュメントで十分だろう。 プログラムはシェルスクリプトで構築する、という方針は簡単に決められる。

中間的なファイルや生成に必要なファイルは、もちろん予め用意してリンクさせることもできるが、ディレクトリ設計に関する障害を増やすことになる。 今回はローカルディレクトリにインストールする、という前提を与えてはいるが、それでもできれば避けたいところだ。 それにバージョンアップの手間も考えれば、一時ファイルを作る方針にした。 これも簡単なものなのでヒアドキュメントで処理する。

ブラウザを作るにあたっては、私の得意なRubyではなく、割と苦手なPythonにした。 Rubyにもqml Rubyバインディングは存在するし、動作もするが、メンテナンスされておらず(3年間放置されている)、情報も非常に少ない。 また、ruby-qmlよりもpyqtのほうがqml自体もシンプルに書けるので、Pythonを採用した。

表示はウェブブラウザ(作ったもの的にはPython+qml+Webengine)で、スクロールはJavaScriptで、変換と橋渡しはシェルスクリプトで。 コンポーネントを分けて、それぞれが得意なことをシンプルに行う。 問題を簡単にし、ミスを減らすポイントでもある。


  1. もっとも、Unsurfを動かすためにはPyQt5とpython-openglが必要なので、ハードルの高さはどちらが上やら↩︎

  2. だいぶ懐かしい響きだが、今でもしているところはある。あまり意味はない。↩︎

  3. より正確にいえば、これはコンテキストメニューを表示させたときに発生するイベントで、右クリックと一対一ではない。キーボードのメニューキーを押した場合や、右クリック以外をコンテキストメニューキーにしている場合も同様にも発生するし、右クリックがコンテキストメニューでないのならば発生しない。↩︎