Ubuntu Weekly Recipe

第492回GNOME Shellの拡張機能を作ってみよう

今月の19日ぐらいにUbuntu 17.10がリリースされます。17.10の最大の変更点はUnityからGNOME Shellへの移行でしょう。そのGNOME Shell、実はUIコンポーネントの多くをJavaScriptで実装しています。JavaScriptの知識さえあれば容易に機能拡張できるようになっているのです。今回はそんなGNOME Shellの拡張機能の作り方を紹介します。

GNOME ShellとJavaScript

今回のリリースはGNOME Shellを抜きにしても、ここ数年で最大の変更点がごろごろ転がっています。ただ、いかんせん「GNOME Shellへの移行」のインパクトが大きすぎて、デスクトップ関連だと「まずGNOME Shellの話」をするだけでお腹いっぱいになってしまう状況になっています。もっとも影響が大きいであろうGNOME Shellに関しては、細かい問題はいくつかあるものの、なんとか今回のリリースで移行を完了できそうな見込みです。おそらく18.04の開発期間は、移行後のブラッシュアップに費やされることでしょう。

さてそんなGNOME Shellについて。これまで採用してきたUnityに対する最大のアドバンテージはその「拡張機能」でしょう。ウェブブラウザーのようにユーザーは自由に拡張機能を追加することで、ただのテーマを超えたUIのカスタマイズが可能になっているのです。いわゆるGNOME 2時代のアプレットやウィジェットがひとつにまとまったようなものだと思ってください。GNOME Shellの拡張機能については過去にも第254回第258回などで、さまざまな拡張機能を紹介していますし、最近だと第490回にてUbuntuのUIを実現するためにUbuntu 17.10で採用される標準の拡張機能の解説が掲載されています。

GNOME Shellの拡張機能はJavaScriptとCSSで構成されています。つまりウェブ開発の経験があれば比較的かんたんに拡張機能を作成できるのです。実はGNOME Shell本体も、そのシェル部分の多くがJavaScriptとCSSで構築されています。/usr/lib/gnome-shell/libgnome-shell.soの中身も、ほぼJavaScriptの関数だったりします。

ちなみにGNOME ShellではJavaScriptエンジンとして、Firefoxでも利用されているSpiderMonkeyを採用しています。MozillaのソースツリーからSpiderMonkey部分のみがmozjsパッケージとして切り出されているので、それをベースにしたGObject introspectionのJavaScriptバインディングがgjsです。GHOME Shellでは、gjsを用いてUIエレメントを操作できるようになっています。

たとえばGNOME Shell上で「Alt+F2」を押すと、コマンド実行ダイアログが表示されます。そこで「lg」を入力すると「Looking Glass」と呼ばれるデバッグシェルが起動します。Looking GlassのシェルではJavaScriptの文法に従ったコードを書くことでUIの要素を調査したり、コードスニペットの動作確認を行えるのです。

図1 Looking Glassのダイアログ
画像

GNOME Shell Extension

さっそくGNOME Shellの拡張機能を作ってみましょう。今回はまだ開発版であるUbuntu 17.10で作業をしていますが、GNOME Shell環境であればUbuntu GNOMEの古いリリースや他のディストリビューションでも同じような結果になるはずです。

GNOME Shellには拡張機能の管理コマンドであるgnome-shell-extension-toolが同梱されています。このコマンドを使うと、拡張機能の雛形も生成してくれるのです。

適当なディレクトリで--create-extensionオプションを付けて実行し、いくつかの質問に答えていきます。

$ gnome-shell-extension-tool --create-extension

Name should be a very short (ideally descriptive) string.
Examples are: "Click To Focus",  "Adblock", "Shell Window Shrinker".

Name: Sample

Description is a single-sentence explanation of what your extension does.
Examples are: "Make windows visible on click", "Block advertisement popups"
              "Animate windows shrinking on minimize"

Description: Sample Extension for Recipe

Uuid is a globally-unique identifier for your extension.
This should be in the format of an email address ([email protected]), but
need not be an actual email address, though it's a good idea to base the uuid on your
email address.  For example, if your email address is [email protected], you might
use an extension title [email protected].
Uuid [Sample@ubuntu-ax2]: [email protected]
Created extension in '/home/shibata/.local/share/gnome-shell/extensions/[email protected]'
This tool has been deprecated, use 'gio open' instead.
See 'gio help open' for more info.

決めなければならない項目は次の3つです。

  • 名前(Name)
  • 概要(Description)
  • UUID

名前と概要は自由に設定してください。UUIDはユニークなIDをつけますが、何か決まった指定方法があるわけではありません。他の拡張機能と名前がかぶらないことが重要です。コマンドの説明にもあるように、⁠拡張機能の名前)@(作成者のメールアドレスで@を.に変更したもの⁠⁠」あたりが無難でしょう。

設定が完了するとエディターが起動し、メインコードであるextension.jsが表示されます[1]⁠。実際に作られるファイルは次の3つです。

  • extension.js
  • metadata.json
  • stylesheet.css

extension.jsが実際にロジックを記述し、metadata.jsonには拡張機能の情報を記録します。stylesheet.cssは作成するUI部分のレイアウトを記述するCSSファイルです。

作成されたファイルはユーザー固有の拡張機能ディレクトリ~/.local/share/gnome-shell/extensions/に保存されます。

$ ls -la ~/.local/share/gnome-shell/extensions/[email protected]/
合計 20
drwxrwxr-x 2 shibata shibata 4096 10月  7 20:32 .
drwxrwxr-x 3 shibata shibata 4096 10月  7 20:32 ..
-rw-rw-r-- 1 shibata shibata 1468 10月  7 20:32 extension.js
-rw-rw-r-- 1 shibata shibata  133 10月  7 20:32 metadata.json
-rw-rw-r-- 1 shibata shibata  172 10月  7 20:32 stylesheet.css

$ cat ~/.local/share/gnome-shell/extensions/[email protected]/metadata.json
{"name": "Sample", "description": "Sample Extension for Recipe", "uuid": "[email protected]", "shell-version": ["3.26.1"]}

$ cat ~/.local/share/gnome-shell/extensions/[email protected]/stylesheet.css
.helloworld-label {
    font-size: 36px;
    font-weight: bold;
    color: #ffffff;
    background-color: rgba(10,10,10,0.7);
    border-radius: 5px;
    padding: .5em;
}

一般的にGNOME Shellの拡張機能をインストールしたときは、このディレクトリに展開されます。Ubuntu DockやAppIndicatorsのようなシステムグローバルな拡張機能は/usr/share/gnome-shell/extensionsに保存されます。

サンプル拡張機能の有効化

拡張機能をインストールしただけでは機能は有効になっていません。第490回でも説明しているように、ウェブブラウザーのアドオンやGNOME Tweak Toolなどとして実装されている拡張機能の管理ツールを使って有効化する必要があります。

余計なものをインストールしたくないのであれば、標準でインストールされているgnome-shell-extension-prefsコマンドを使うと良いでしょう。

図2 gnome-shell-extension-prefs
画像

このコマンドはインストール済みの拡張機能をリストアップして個別に有効化・無効化を設定できます。注意しなくてはならないのは、システムグローバルな拡張機能自体は表示するものの、有効化・無効化の状態は正しく取得できないということです。有効化・無効化の状態はgsettings側の設定を読んでいるらしく、Ubuntu DockやAppIndicatorsのようなシステムグローバルに有効化されている拡張機能は「無効化されている」と誤判定してしまっています。

よって有効化・無効化する際は誤って他の拡張機能を操作しないように気をつけましょう。ちなみにgsettingsコマンドで有効になっている拡張機能のリストを確認したい場合は、次のように実行します。

$ gsettings get org.gnome.shell enabled-extensions
['[email protected]']

環境によっては有効化前もしくは有効化後にGNOME Shellを再読込する必要があるようです。⁠Alt+F2」でコマンド実行ダイアログを表示したあとに「r」を入力という方法もありますが、この方法はWaylandセッションでは動作しないので注意してください。もっとも確実な方法は一度ログアウトすることです。

サンプルの拡張機能が有効化されたら、トップパネルに歯車のアイコンが表示されます。それをクリックすると画面中央に「Hello, world!」とメッセージが表示されます。

図3 表示されるメッセージ
画像

extension.jsの中身

拡張機能の本体であるextension.jsの中身を見ていきましょう。基本的に「拡張機能ロード時に呼び出されるinit()関数」⁠拡張機能有効化時のenable()⁠拡張機能無効化時のdisable()が存在し、拡張機能ごとにそれ以外の関数を実装・呼び出していくことになります。

const St = imports.gi.St;
    const Main = imports.ui.main;
    const Tweener = imports.ui.tweener;
    
    let text, button;

ファイルの冒頭は必要な機能のインポートとグローバル変数の設定です。St「Shell Toolkit」と呼ばれるUIエレメントを操作するツールキットです[2]⁠。実際はClutterのJavaScriptバインディングなので、Clutterのドキュメントも参考になるでしょう。MainはStで作成したUIエレメントを登録し管理するインスタンスです。Tweenerはアニメーションフレームワークです。UIエレメントを動的に変化させたい場合に使います。

textやbuttonはこのサンプル拡張機能で使用しているブロックスコープの変数です。

先にinit()の中を見てみましょう。前述したとおり、この関数はロード時に一度だけ呼び出されます。拡張機能としては必須の関数です。有効無効に関わらずロード時に呼び出されるため、この関数の中でUIの変更(つまりMainオブジェクトに対する変更)を行ってはいけません。

今回のサンプルではトップパネルに表示するボタン(UIコンテナー)とアイコンを作成し、それらを紐付けた上で、ボタン上でマウスを押した時のイベントハンドラーを登録しています。

function init() {
    /*
     * トップパネルに表示するボタン(UIコンテナー)を作成し、
     * プロパティを設定しています。
     *
     *   style_class: スタイルシートのクラス名
     *   reactive: マウス操作などに応答するかどうか
     *   can_focus: キーボード操作によるフォーカスが合うかどうか
     *   x_fill/y_fill: 子要素がx/y方向それぞれに親要素にあわせて
     *                  サイズ調整されるかどうか
     *   track_hover: マウスカーソルでhoverステートになるかどうか
     *
     * 今回はボタンとして使うため、reactiveをtrueにしています。
     */
    button = new St.Bin({ style_class: 'panel-button',
                          reactive: true,
                          can_focus: true,
                          x_fill: true,
                          y_fill: false,
                          track_hover: true });

    /*
     * アイコンを作成しています。
     *
     * "system-run-symbolic"はシステムに最初から入っているアイコン名です。
     * /usr/share/icons/Adwaita/scalable/actions/system-run-symbolic.svg
     */
    let icon = new St.Icon({ icon_name: 'system-run-symbolic',
                             style_class: 'system-status-icon' });

    /*
     * buttonの子要素として先ほど作成したアイコンを設定しています。
     */
    button.set_child(icon);

    /*
     * button-press-eventで応答するハンドラーとして
     * _showHello()を設定しています。
     *
     * サポートしているシグナルは以下のURLで確認できます。
     * https://developer.gnome.org/clutter/stable/ClutterActor.html#ClutterActor.signals
     */
    button.connect('button-press-event', _showHello);
}

init()以外に必須の関数が、拡張機能を有効化・無効化されたときに呼び出されるenable()disable()です。

function enable() {
    /* ボタンをトップパネルの右側に追加します。
     *
     * トップパネルは右端の_leftBox、_centerBox、_rightBoxの
     * 3つのコンテナーが存在します。
     *   _leftBox: 左端のアクティビティやカレントタスクを表示している領域
     *   _centerBOx: 中央の日時を表示している領域
     *   _rightBox: 右端のインジケーター領域
     * Looking Glassで「Main.panel._rightBox」を実行すれば
     * どれがどこかわかりやすいでしょう。
     *
     * 今回は_rightBoxにbuttonを追加しています。
     * 第二引数で追加順が最初か末尾かを変更できます。
     */
    Main.panel._rightBox.insert_child_at_index(button, 0);
}

function disable() {
    /* 無効化時にトップパネルからボタンを削除します。 */
    Main.panel._rightBox.remove_child(button);
}

ここまででボタンの作成、ボタンを押された時のイベントハンドラーの登録までを行えました。あとはイベントハンドラーを実装するだけです。

今回のハンドラーは、メインディスプレイの中央に一定期間メッセージダイアログを表示するというものです。

function _hideHello() {
    /*
     * Mainオブジェクトから、_showHello()が追加した
     * ラベルオブジェクトを削除します。
     */
    Main.uiGroup.remove_actor(text);
    text = null;
}

function _showHello() {
    /*
     * ディスプレイ中央に表示するラベルオブジェクトを作成します。
     *
     * style_classに指定してるhelloworld-labelクラスは、
     * stylesheet.cssで設定しています。
     * つまりこのCSSファイルを変更するだけで
     * ラベルの見た目を変更できるわけです。
     *
     * textに表示するテキストを設定し、Mainオブジェクトに追加しています。
     */
    if (!text) {
        text = new St.Label({ style_class: 'helloworld-label', text: "Hello, world!" });
        Main.uiGroup.add_actor(text);
    }

    /* ラベルオブジェクトを不透明に設定しています。 */
    text.opacity = 255;

    /* プライマリーモニターのインスタンスを探しています。 */
    let monitor = Main.layoutManager.primaryMonitor;

    /* ラベルの表示位置を、プライマリーモニターの中央に設定しています。 */
    text.set_position(monitor.x + Math.floor(monitor.width / 2 - text.width / 2),
                      monitor.y + Math.floor(monitor.height / 2 - text.height / 2));

    /*
     * Tweenerを用いてラベルオブジェクトのアニメーションを設定しています。
     *
     * 2秒(time)かけて不透明度(opacity)を0(透明)にしています。
     * opacityの変化の方法はeaseOutQuadを指定しています。
     * 参考: http://hosted.zeh.com.br/tweener/docs/en-us/misc/transitions.html
     * 変化が完了したら_hideHello()を呼び出し、
     * ラベルオブジェクトを削除します。
     */
    Tweener.addTween(text,
                     { opacity: 0,
                       time: 2,
                       transition: 'easeOutQuad',
                       onComplete: _hideHello });
}

これがサンプルコードのすべてです。

拡張機能のデバッグ方法

拡張機能の中でもlog()関数を使ってデバッグログを記録できます。これがどこに出力されるかと言うと、GNOME Shell本体のログ出力先です。Ubuntu 17.10の場合、journalctlコマンドを使えば特定のプロセスのログを取得できます。よって次のコマンドを実行するとGNOME Shell本体のログを表示できることでしょう。

$ journalctl /usr/bin/gnome-shell -f

-fオプションを付けることで、ログが追加されるたびに表示が更新されます。過去も遡って見たい場合-fを外してください。

標準設定だとページャーとしてlessを使うのですが、-Sオプションが付いているため長い行は端末の横幅サイズで見えないようになっています。表示する場合はカーソルキーで左右を移動しましょう。

単に右端で折り返したいなら、以下の方法が使えます。

単に出力をlessに渡す(カラーコードなどは設定されなくなる)
$ journalctl /usr/bin/gnome-shell --no-pager | less

lessのオプションを変更する(一部カラーコードが表示されない)
$ SYSTEMD_LESS="FRMXK" journalctl /usr/bin/gnome-shell

別のページャーを使う
$ SYSTEMD_PAGER=foo journalctl /usr/bin/gnome-shell

好みに合わせて選択すると良いでしょう。

Looking Glassを開いて、左上のスポイトをクリックすると、マウスを用いて個々のUIエレメントのオブジェクトを選択できます。あとは選択したオブジェクトをLooking Glass上で評価すれば、より深いデバッグが可能になることでしょう。

おすすめ記事

記事・ニュース一覧