GNOME Shell Extension を調べてみた

どうも、GNOME Seed の開発が停止し、 Gjs (GNOME JavaScript) のほうが活発に活動しているようです。GNOME Shell では Gjs が基本だし、拡張するためツールも Gjs だから、仕方がないのかな?
ということで、流れに逆らわないように Gjs の調査も兼ねて、 GNOME Shell Extension の作り方を調べてみました。

最初に、簡単なサンプルを作るコマンド gnome-shell-extension-tool を実行します。

$ gnome-shell-extension-tool --help
Usage: gnome-shell-extension-tool [options]

Options:
  -h, --help            show this help message and exit
  -d DISABLE, --disable-extension=DISABLE
                        Disable a GNOME Shell extension
  -e ENABLE, --enable-extension=ENABLE
                        Enable a GNOME Shell extension
  -c, --create-extension
                        Create a new GNOME Shell extension

 -c オプションを付けると作成できるみたいです。

$ gnome-shell-extension-tool -c

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: This is my first sample extension.

Uuid is a globally-unique identifier for your extension.
This should be in the format of an email address (foo.bar@extensions.example.com), 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 janedoe@example.com, you might
use an extension title clicktofocus@janedoe.example.com.
Uuid [Sample@your.pc.name]: sample@example.com
Created extension in '/home/yourname/.local/share/gnome-shell/extensions/sample@example.com'

面倒なので、Sample という名前にし、 UUID を sample@example.com にしました。すると、$HOME/.local/share/gnome-shell/extensions 以下に sample@example.com というディレクトリができ、その中にファイルが3つ作成され、 gedit で extension.js が開かれます。

$ ls
extension.js  metadata.json  stylesheet.css

中身を見る前に、どんなプログラムになるのか、試してみましょう。Alt + F2 でコマンド実行窓を開き、 "r" の一文字を入力して Enter キーを押します。すると、GNOME Shell が再起動し、上のパネルの時計の右側に、歯車のアイコンが表示されます。これをクリックすると、モニター中央に「Hello, world!」と表示され、徐々に薄くなっていくのが分かります。

ついでに、 LookingGlass https://live.gnome.org/GnomeShell/LookingGlass で確認しましょう。 Alt + F2 でコマンド実行窓を開き、"lg"の二文字を入力して Enter キーを押します。窓の左上に「Extensions」というのがあるので、それをクリックすると、今回作った Sample というのが見つかると思います。 LookingGlass の使い方は上のWebなどで調べて下さい。

では、extension.jsの中身を見て行きましょう。

const St = imports.gi.St;
const Main = imports.ui.main;
const Tweener = imports.ui.tweener;

最初の3行は GNOME Shell で利用されているオブジェクトを imports しています。
St を imports することで、/usr/lib/gnome-shell/St-1.0.typelib の中身が利用できるようになります。C言語のドキュメントは、 devhelp を使って確認できます。目次タブで「St Reference Manual」を見てください。本当なら、XML形式のgirファイルを調べれば良いのですが、gnome-shellをビルドするときに内部で作成されて、インストールはされないので、自分で作るしかないようです。

次の二行で、GNOME Shell の JavaScript で作成したライブラリを利用できるようにしています。 main.js と tweener.js は /usr/share/gnome-shell/js 以下に ui/main.js と tweener.js として存在しています。これらのマニュアルは無さそうなので、ソースを読まなきゃダメみたいですね。

さて、次の行を見ていきましょう。

let text, button;

この extension で利用するオブジェクトとして、 text と button を宣言しています。Gjs では var ではなく let を基本的に使ったほうが良いみたいです。text は、「Hello, world!」を表示する StLabel 型のオブジェクトです。button は、パネルに表示されている歯車アイコンのある StBin 型のオブジェクトです。実際に StBin はコンテナで、中にアイコンが配置されています。

function _hideHello() {
    Main.uiGroup.remove_actor(text);
    text = null;
}

これは、 text という StLabel 型のオブジェクトを消す処理で、後で説明する _showHello() 関数の最後のアニメーション終了時に呼び出されます。最後に null を代入しているので、ガベージコレクションによってメモリからいずれ削除されます。

function _showHello() {
    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(Math.floor(monitor.width / 2 - text.width / 2),
                      Math.floor(monitor.height / 2 - text.height / 2));

    Tweener.addTween(text,
                     { opacity: 0,
                       time: 2,
                       transition: 'easeOutQuad',
                       onComplete: _hideHello });
}

これは、 button という StBin 型のオブジェクトがクリックされた時に呼び出される _showHello() という名前のコールバック関数です。 text オブジェクトが null の場合、 text という名前の StLabel 型オブジェクトを作成し、Main.uiGroup の Actor として追加されます。 Actor というのは Clutter で使われるオブジェクトです。詳しくは Clutter のマニュアルを参照してください。

最初に text の不透明度を 255 にし、 Main.layoutManager.primaryMonitor オブジェクトを moniter 変数に割り当て、 text の一を monitor の中心に設定します。

そして、 Tweener を使ってアニメーションを実行します。内容は、 text オブジェクトの不透明度を徐々に透明にし、不透明度が0になったら _hideHello() 関数を呼び出します。

function init() {
    button = new St.Bin({ style_class: 'panel-button',
                          reactive: true,
                          can_focus: true,
                          x_fill: true,
                          y_fill: false,
                          track_hover: true });
    let icon = new St.Icon({ icon_name: 'system-run',
                             icon_type: St.IconType.SYMBOLIC,
                             style_class: 'system-status-icon' });

    button.set_child(icon);
    button.connect('button-press-event', _showHello);
}

これは、 GNOME Shell が起動時に呼び出す初期化関数 init() です。最初に button という StBin 型のオブジェクトを作成し、次に icon という StIcon 型のオブジェクトを作成し、 button に icon を乗せ (子オブジェクトとして指定し)、button がクリックされた時のイベントハンドラ関数として _showHello() を指定しています。
注意するのは、 St.Bin のコンストラクタで、 reactive: true を指定しているところです。reactive は、St.Binが継承している親の StWidget の親にあたる ClutterActor クラスの property で、button がイベントに反応するかどうかを指定します。 true にすることで反応しますが、デフォルトは false なので必ず true にする必要があります。

function enable() {
    Main.panel._rightBox.insert_child_at_index(button, 0);
}

function disable() {
    Main.panel._rightBox.remove_child(button);
}

これは、 gnome-tweak-tool の 「GNOME Shell 拡張」で ON/OFF された時に呼び出される関数です。GNOME Shell 起動時には必ず init() が呼び出されているので、 enable で button を追加し、 disable で button を削除します。

これを見る限り、有効にしない extension はメモリを利用し、スレッドを消費しているようなので、可能な限り削除したほうが良さそうですね。

ということで、GNOME Shell Extension を調べて、デフォルトで作成される extension.js のソースを見てきました。残りの2つのファイル metadata.json と stylesheet.css は、中を見れば簡単に解ると思います。なので、説明は省略します。

基本的に、 main.js で作成されているオブジェクトに、何らかの Actor を追加したり、イベントに対応して StWidget を用いて 独自 UI を作成したり、 tweener.js で記述されているアニメーション効果を利用する感じで作られているようです。後は、Gjs で記述できる処理なら何でもできそうな感じですね。

特に作りたい extension が無いので今回はここまでで終わりですが、今後のことも考えて Gjs の調査も進めていく予定です。