Super キーで「全てのアプリケーション」の表示・非表示のみをトグルして Activities Overview 画面を封じる GNOME Shell 拡張機能を書いた

Ubuntu 18.04 にて。既存の GNOME Shell 拡張機能を書き換えることで、自分が求める動作を実現させた。

目次

何が問題だったか

Ubuntu のデフォルトだと、画面左上の「アクティビティ」を押下すると表示される、ウィンドウとワークスペースの一覧画面。MacOS の Mission Control みたいなヤツ。この画面を「Activities Overview」画面と呼ぶそうなのだが、存在から抹消したい。

自分はキーボード・ショートカットを色々変更して、アクティビティ系の表示設定は全て無効化し、PrintScreen キーに Show all applications を割り当てている (デフォルトは Super + A)。コレで PrintScreen キーを押すと全てのアプリケーション一覧が表示されるのだが、もう一度 PrintScreen を押すと、アプリケーション一覧が閉じるのではなく、「Activities Overview」画面が開いてしまうのだ。「All Applications」と「Activities Overview」を行き来してしまうので、閉じるには Esc キーを押さないといけない。

「アクティビティ」アイコンを消す系の GNOME Shell 拡張機能は多数あるのだが、この「Show all applications」からの「Activities Overview」画面への移動を抑止するような仕組みは、探しても見つからなかった。できれば Windows における Win キー単独の押下みたいな感じで、PrintScreen キーの押下で「全てのアプリケーション一覧」の表示 ⇔ 非表示をトグル切り替えする動きをしてほしいと思っている。

ところで、ESC to close overview from applications list という GNOME Shell 拡張機能がある。「Activities Overview」か「All Applications」画面で Esc キーを押すと、この画面を閉じられるというモノだ。

この GNOME Shell 拡張機能を参考に、やりたいことが実現できるのではないかと思い、書いてみることにした。

既存の GNOME Shell 拡張機能を書き換える

というワケで実装。

GNOME Shell 拡張機能は JavaScript で書かれており、インストールした拡張機能は以下のディレクトリに存在した。

この配下に extension.js というファイルがある。

extension.js を次のように直す

この extension.js を次のように修正した。全文を貼り付ける。

const Clutter = imports.gi.Clutter;
const Main = imports.ui.main;
const Lang = imports.lang;

// 以下2行を追加
const { Gio, Meta, Shell } = imports.gi;
const SHELL_KEYBINDINGS_SCHEMA = 'org.gnome.shell.keybindings';

let _this;
let escAction;

//based on https://github.com/GNOME/gnome-shell/blob/gnome-3-20/js/ui/viewSelector.js
function esc(actor, event) {
        _this = this;
        if (Main.modalCount > 1)
            return Clutter.EVENT_PROPAGATE;

        let modifiers = event.get_state();
        let symbol = event.get_key_symbol();

        // 以下条件を追加
        if (symbol == Clutter.Escape || symbol == Clutter.Print || symbol == Clutter.KEY_3270_PrintScreen || symbol == Clutter.Super_L || symbol == Clutter.Super_R) {
            return escAction();
        } else if (this._shouldTriggerSearch(symbol)) {
            this.startSearch(event);
        } else if (!this._searchActive && !global.stage.key_focus) {
            if (symbol == Clutter.Tab || symbol == Clutter.Down) {
                this._activePage.navigate_focus(null, Gtk.DirectionType.TAB_FORWARD, false);
                return Clutter.EVENT_STOP;
            } else if (symbol == Clutter.ISO_Left_Tab) {
                this._activePage.navigate_focus(null, Gtk.DirectionType.TAB_BACKWARD, false);
                return Clutter.EVENT_STOP;
            }
        }
        return Clutter.EVENT_PROPAGATE;
}

function init() {
  // ↓ 追加ココカラ
  Main.wm.removeKeybinding('toggle-application-view');
  Main.overview.viewSelector._toggleAppsPage = function() {
    Main.overview.viewSelector._showAppsButton.checked = true;
    if(Main.modalCount >= 1) {
      Main.overview.hide();
    }
    else {
      Main.overview.show();
    }
  };
  Main.wm.addKeybinding('toggle-application-view',
                        new Gio.Settings({ schema_id: SHELL_KEYBINDINGS_SCHEMA }),
                        Meta.KeyBindingFlags.NONE,
                        Shell.ActionMode.NORMAL |
                        Shell.ActionMode.OVERVIEW,
                        Main.overview.viewSelector._toggleAppsPage.bind(Main.overview.viewSelector));
  // ↑ 追加ココマデ
  
    Main.overview.viewSelector._onStageKeyPress = esc;
}

function originalEscAction() {
    if (_this._searchActive)
        _this.reset();
    else if (_this._showAppsButton.checked)
        _this._showAppsButton.checked = false;
    else
        Main.overview.hide();
    return Clutter.EVENT_STOP;
}

function modifiedEscAction() {
    if (_this._searchActive)
        _this.reset();
    else
        Main.overview.hide();
    return Clutter.EVENT_STOP;
}

function enable() {
    escAction = modifiedEscAction;
}

function disable() {
    escAction = originalEscAction;
}

Diff で表現

diff で表現すると次のとおり。

$ diff --color=auto ORIG-extension.js extension.js

4a5,8
> // 以下2行を追加
> const { Gio, Meta, Shell } = imports.gi;
> const SHELL_KEYBINDINGS_SCHEMA = 'org.gnome.shell.keybindings';
> 
17c21,22
<         if (symbol == Clutter.Escape) {
---
>         // 以下条件を追加
>         if (symbol == Clutter.Escape || symbol == Clutter.Print || symbol == Clutter.KEY_3270_PrintScreen || symbol == Clutter.Super_L || symbol == Clutter.Super_R) {
33a39,57
>   // ↓ 追加ココカラ
>   Main.wm.removeKeybinding('toggle-application-view');
>   Main.overview.viewSelector._toggleAppsPage = function() {
>     Main.overview.viewSelector._showAppsButton.checked = true;
>     if(Main.modalCount >= 1) {
>       Main.overview.hide();
>     }
>     else {
>       Main.overview.show();
>     }
>   };
>   Main.wm.addKeybinding('toggle-application-view',
>                         new Gio.Settings({ schema_id: SHELL_KEYBINDINGS_SCHEMA }),
>                         Meta.KeyBindingFlags.NONE,
>                         Shell.ActionMode.NORMAL |
>                         Shell.ActionMode.OVERVIEW,
>                         Main.overview.viewSelector._toggleAppsPage.bind(Main.overview.viewSelector));
>   // ↑ 追加ココマデ
>   

どうやって反映する?

extension.js を書き換えただけではこのコードは反映されない。

Alt + F2 キーを押して「コマンドを入力」を表示し、r とのみ入力して実行する。

すると画面中央に「再起動中…」というポップアップが表示され、GNOME Shell 拡張機能を再読込できる。

コレで書き換えた extension.js が有効になる。PrintScreen キーを押せば「全てのアプリケーション一覧」が開き、もう一度 PrintScreen キーを押せば一覧が閉じる。閉じるキーには Esc キーの他に Super キーなども含めておいた。

何をどう実装しているのか

初めて GNOME Shell 拡張機能とやらを触ってみたワケだが、どうやって実装したかメモを残しておく。

まず、言語的には JavaScript がベースなので、処理はなんとなく追いやすかった。import しているソース群は、ググってみると以下にあったので、参考にした。

初めに、if(symbol == Clutter.Escape) 部分が気になったので、Clutter をググり、キーボードごとの定数があることを突き止め、条件を増やしてみた。しかし、PrintScreen キーを押下してもこの条件文には到達していないようだったので、もう少し調べてみた。

ViewSelector クラスの実装を見てみると、何やら Overview 絡みのキーイベントを定義しているっぽいところを見つけた。

Main.wm.addKeybinding('toggle-application-view',
                      new Gio.Settings({ schema_id: SHELL_KEYBINDINGS_SCHEMA }),
                      Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
                      Shell.ActionMode.NORMAL |
                      Shell.ActionMode.OVERVIEW,
                      this._toggleAppsPage.bind(this));

Main.wm.addKeybinding('toggle-overview',
                      new Gio.Settings({ schema_id: SHELL_KEYBINDINGS_SCHEMA }),
                      Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
                      Shell.ActionMode.NORMAL |
                      Shell.ActionMode.OVERVIEW,
                      Main.overview.toggle.bind(Main.overview));

addKeybinding() があるなら remove もあるかな?と思い調べてみたら、あった。

Main.wm.removeKeybinding('custom-switch-is-key');

というワケで、まずはイベントを remove だけして様子を見てみると、toggle-application-view イベントを消したらアプリケーション一覧が開かなくなったので、コイツがキモだと分かった。

コードを見ると、_toggleAppsPage() 関数で何やらチェックボックス的なモノを操作しつつ、Overview を表示しているっぽかった。

_toggleAppsPage() {
  this._showAppsButton.checked = !this._showAppsButton.checked;
  Main.overview.show();
}

_showAppsButtonchecked を切り替えることに意味があるのか?と思いコードを追うと、以下の行を発見。どうやらチェック状況によって別の関数を呼び出すイベントが定義されているようだった。

this._showAppsButton.connect('notify::checked', this._onShowAppsButtonToggled.bind(this));

_toggleAppsPage() 関数のすぐ下にある showApps() 関数も参考にすると、

ということが分かった。

そしたら、既存のイベントを一旦 remove し、オレオレイベントを再定義してやることにしよう。それが init() 関数に追記した以下のコードだ。

const { Gio, Meta, Shell } = imports.gi;
const SHELL_KEYBINDINGS_SCHEMA = 'org.gnome.shell.keybindings';

// 以下 init() 関数内

Main.wm.removeKeybinding('toggle-application-view');
Main.overview.viewSelector._toggleAppsPage = function() {
  Main.overview.viewSelector._showAppsButton.checked = true;
  if(Main.modalCount >= 1) {
    Main.overview.hide();
  }
  else {
    Main.overview.show();
  }
};
Main.wm.addKeybinding('toggle-application-view',
                      new Gio.Settings({ schema_id: SHELL_KEYBINDINGS_SCHEMA }),
                      Meta.KeyBindingFlags.NONE,
                      Shell.ActionMode.NORMAL |
                      Shell.ActionMode.OVERVIEW,
                      Main.overview.viewSelector._toggleAppsPage.bind(Main.overview.viewSelector));

import は以下を参考にした。

既存イベントを remove したら _toggleAppsPage() 関数をオレオレ関数に書き換える。Main.modalCount を見ると、「Activities Overview」か「アプリケーション一覧」が開いているかどうかが分かるので、開いていれば hide()、開いていなければ show() を呼ぶようにした。

addKeybiding() 中で、Meta.KeyBindingFlags を使用しているところがある。元々のコードでは Meta.KeyBindingFlags.IGNORE_AUTOREPEAT という定数を使用していたが、どうもこの定数が見当たらなかったので、代わりにテキトーに NONE を指定してみることにした。

init() 関数あたりに print('META : ' + JSON.stringify(Meta.KeyBindingFlags) ); といったコードを入れ、Alt + F2r で GNOME Shell を再起動、その後ターミナルで

$ journalctl /usr/bin/gnome-shell

の最後の方を見ると、次のようなログが見えるはずだ。IGNORE_AUTOREPEAT は存在しないみたい。

実装時は init() 関数内を try / catch で囲み、エラーを print(e) で出力することで、Meta.KeyBindingFlags.IGNORE_AUTOREPEAT が存在しないことを突き止められた。

 2月 20 12:32:44 Neos-ThinkPad org.gnome.Shell.desktop[1143]: Error: Expected type flags for Argument 'flags' but got type 'undefined'

こんな感じのエラーが出ていた。

テキトーに NONE を指定してやったら上手く行ったので、コードを整理して終わり。上で示した完成形となった。

まとめ

ということで、GNOME Shell 拡張機能を書いてみた。今回は既存のどこぞの拡張機能のソースコードを手直しする形で実装してみたけど、コマンドラインツールでゼロから作って、拡張機能ストアに登録することもできるようなので、今後試してみたい。