カスタムシェル

Custom Shellはカスタムシェル拡張機能の実装方法を示します。

Waylandのシェル拡張はウィンドウの状態、位置、サイズを管理するプロトコルです。ほとんどのコンポジターは1つまたは複数の組み込み拡張機能をサポートしていますが、アプリケーションに必要な機能を正確に含むカスタム拡張機能を記述できると便利な場合もあります。

この場合、Wayland接続のサーバー側とクライアント側の両方でシェル拡張機能を実装する必要があります。

カスタムシェルの例ではシンプルなシェル拡張の実装を示しています。これは3つの部分に分かれています:

  • カスタムシェルインターフェースのプロトコル記述。
  • クライアントアプリケーションでインターフェースに接続するためのプラグイン。
  • インターフェイスのサーバー側実装を持つコンポジターの例。

プロトコルの記述はwayland-scanner で読み込まれる標準的な XML フォーマットに従っています。ここでは詳しく説明しませんが、以下の機能をカバーしています:

  • wl_surface のシェルサーフェスを作成するためのインターフェイス。 これにより、プロトコルは既存のwl_surface API の上に機能を追加することができる。
  • シェルサーフェスにウィンドウタイトルを設定するリクエスト。
  • シェルサーフェスを最小化/非最小化するリクエスト。
  • シェルサーフェスの現在の最小化状態をクライアントに通知するイベント。

qtwaylandscanner をビルドの一部として自動的に実行させるために、CMake 関数のqt_generate_wayland_protocol_server_sources()qt_generate_wayland_protocol_client_sources()を使用して、それぞれサーバー側とクライアント側のグルー・コードを生成します。(qmake を使う場合は、WAYLANDSERVERSOURCESWAYLANDCLIENTSOURCES 変数で同じことができます)。

クライアント・プラグイン

シェル統合をQtクライアントから検出するためには、QWaylandShellIntegrationPluginを再実装する必要があります。

class QWaylandExampleShellIntegrationPlugin : public QWaylandShellIntegrationPlugin
{
    Q_OBJECT
    Q_PLUGIN_METADATA(IID QWaylandShellIntegrationFactoryInterface_iid FILE "example-shell.json")

public:
    QWaylandShellIntegration *create(const QString &key, const QStringList &paramList) override;
};

QWaylandShellIntegration *QWaylandExampleShellIntegrationPlugin::create(const QString &key, const QStringList &paramList)
{
    Q_UNUSED(key);
    Q_UNUSED(paramList);
    return new ExampleShellIntegration();
}

これはシェル統合に "example-shell "キーをアタッチし、クライアントがインターフェイスに接続したときにExampleShellIntegration クラスがインスタンス化される方法を提供します。

シェル拡張を作成するためのAPIは、ヘッダーqwaylandclientshellapi_p.h

#include <QtWaylandClient/private/qwaylandclientshellapi_p.h>

このヘッダにはプライベートAPIを含める必要があります。なぜなら、パブリックなQt APIとは異なり、バイナリ互換性が保証されていないからです。このAPIはまだ安定していると考えられており、ソース互換性が保たれ、この点ではQtの他のプラグインAPIと似ています。

ExampleShellIntegration は、上記のようにシェルサーフェスを作成するためのクライアントサイドのエントリーポイントです。これは、Curiously Recurring Templateパターンを使用して、QWaylandShellIntegrationTemplateクラスを継承しています。

class Q_WAYLANDCLIENT_EXPORT ExampleShellIntegration
        : public QWaylandShellIntegrationTemplate<ExampleShellIntegration>
        , public QtWayland::qt_example_shell
{
public:
    ExampleShellIntegration();

    QWaylandShellSurface *createShellSurface(QWaylandWindow *window) override;
};

また、プロトコルの XML 記述に基づいてqtwaylandscanner によって生成されるQtWayland::qt_example_shell クラスも継承しています。

コンストラクタは、サポートするプロトコルのバージョンを指定します:

ExampleShellIntegration::ExampleShellIntegration()
    : QWaylandShellIntegrationTemplate(/* Supported protocol version */ 1)
{
}

example_shell プロトコルは現在バージョン1なので、親クラスに1 を渡します。これはプロトコルのネゴシエーションで使用され、コンポジターが新しいバージョンのプロトコルを使用しても古いクライアントが動作し続けるようにします。

ExampleShellIntegration が初期化されると、アプリケーションはサーバーに接続され、コンポジターがサポートするグローバル・インターフェースのブロードキャストを受け取ります。成功すれば、インターフェイスに対するリクエストを発行できる。この場合、サポートするリクエストは1つだけです:シェルサーフェスの作成です。組み込み関数wlSurfaceForWindow() を使って QWaylandWindow をwl_surface に変換し、リクエストを発行します。そして返されたサーフェスをExampleShellSurface オブジェクトで拡張し、qt_example_shell_surface インターフェースのリクエストとイベントを処理します。

QWaylandShellSurface *ExampleShellIntegration::createShellSurface(QWaylandWindow *window)
{
    if (!isActive())
        return nullptr;
    auto *surface = surface_create(wlSurfaceForWindow(window));
    return new ExampleShellSurface(surface, window);
}

ExampleShellSurface は2つのクラスを拡張します。

class ExampleShellSurface : public QWaylandShellSurface
        , public QtWayland::qt_example_shell_surface

一つは、プロトコルのXML記述に基づいて生成されるQtWayland::qt_example_shell_surface クラスである。これはイベント用の仮想関数と、プロトコルのリクエスト用の通常のメンバ関数を提供する。

QtWayland::qt_example_shell_surface クラスは一つのイベントしか持たない。

    void example_shell_surface_minimize(uint32_t minimized) override;

ExampleShellSurface はこれを再インプリメントして、内部のウィンドウの状態を更新する。ウィンドウの状態が変更されると、保留中の状態を後まで保存し、QWaylandShellSurfaceapplyConfigureWhenPossible() を呼び出す。状態、サイズ、位置の変更はこのように整理する必要がある。そうすることで、変更がサーフェスへのレンダリングに干渉しないようにし、関連する複数の変更を1つの変更として簡単に適用できるようにする。

サーフェスの再構成が安全になったら、仮想applyConfigure() 関数が呼び出される。

void ExampleShellSurface::applyConfigure()
{
    if (m_stateChanged)
        QWindowSystemInterface::handleWindowStateChanged(platformWindow()->window(), m_pendingStates);
    m_stateChanged = false;
}

ここで実際に新しい(最小化または最小化解除された)状態をウィンドウにコミットする。

つ目のスーパークラスはQWaylandShellSurface 。これはWaylandのQPAプラグインとQWaylandWindowがシェルとの通信に使用するインターフェースです。ExampleShellSurface はこのインターフェイスからいくつかの仮想関数も再実装しています。

    bool wantsDecorations() const override;
    void setTitle(const QString &) override;
    void requestWindowStates(Qt::WindowStates states) override;
    void applyConfigure() override;

例えば、Qtアプリケーションがウィンドウのタイトルを設定するとき、これは仮想関数setTitle() の呼び出しに変換されます。

void ExampleShellSurface::setTitle(const QString &windowTitle)
{
    set_window_title(windowTitle);
}

ExampleShellSurface では、これはカスタム・シェル表面インターフェースのリクエストに変換されます。

コンポジター

この例の最後の部分はコンポジターそのものです。これは他のコンポジターの例と同じ一般的な構造を持っています。Qt Waylandコンポジターの構成要素についての詳細はMinimal QML exampleを参照してください。

カスタムシェル・コンポジターで特筆すべき違いは、シェル拡張のインスタンス化です。Minimal QMLの例ではシェル拡張IviApplicationXdgShellWlShell のインスタンスを作成しますが、Custom Shellの例ではExampleShell のインスタンスのみを作成します。

ExampleShell {
    id: shell
    onShellSurfaceCreated: (shellSurface) => {
        shellSurfaces.append({shellSurface: shellSurface});
    }
}

グローバルインターフェースとして登録するために、WaylandCompositor の直接の子としてシェル拡張モジュールのインスタンスを作成します。これはクライアントが接続するときにブロードキャストされ、前のセクションで説明したようにクライアントはインターフェイスにアタッチできるようになります。

ExampleShell は生成されたQtWaylandServer::qt_example_shell インターフェイスのサブクラスであり、プロトコルの XML で定義された API を含んでいる。これはまた、QWaylandCompositorExtensionTemplate のサブクラスでもあり、QWaylandCompositor によってオブジェクトが拡張として認識されることを保証する。

class ExampleShell
        : public QWaylandCompositorExtensionTemplate<ExampleShell>
        , QtWaylandServer::qt_example_shell

この二重継承はQt Wayland Compositorで拡張機能を構築する際の典型的なパターンです。QWaylandCompositorExtensionTemplate クラスはQWaylandCompositorExtensionqtwaylandscanner によって生成されたqt_example_shell クラスの接続を作成します。

同様に、ExampleShellSurface クラスは生成されたQtWaylandServer::qt_example_shell_surface クラスとQWaylandShellSurfaceTemplate を継承し、ShellSurface クラスのサブクラスとなり、Qt Wayland Compositor と生成されたプロトコルコードとの接続を確立します。

この型をQt Quickで利用できるようにするために、Q_COMPOSITOR_DECLARE_QUICK_EXTENSION_CLASS プリプロセッサ・マクロを使用します。特に、Qt Quickのグラフに追加されたときに拡張機能を自動的に初期化する処理を行います。

void ExampleShell::initialize()
{
    QWaylandCompositorExtensionTemplate::initialize();

    QWaylandCompositor *compositor = static_cast<QWaylandCompositor *>(extensionContainer());
    if (!compositor) {
        qWarning() << "Failed to find QWaylandCompositor when initializing ExampleShell";
        return;
    }

    init(compositor->display(), 1);
}

initialize() 関数のデフォルトの実装では、拡張機能をコンポジターに登録します。これに加えて、プロトコル拡張自体を初期化します。これはQtWaylandServer::qt_example_shell_surface クラスで生成されたinit() 関数を呼び出すことで行います。

また、surface_create リクエスト用に生成された仮想関数を再実装します。

void ExampleShell::example_shell_surface_create(Resource *resource, wl_resource *surfaceResource, uint32_t id)
{
    QWaylandSurface *surface = QWaylandSurface::fromResource(surfaceResource);

    if (!surface->setRole(ExampleShellSurface::role(), resource->handle, QT_EXAMPLE_SHELL_ERROR_ROLE))
        return;

    QWaylandResource shellSurfaceResource(wl_resource_create(resource->client(), &::qt_example_shell_surface_interface,
                                                           wl_resource_get_version(resource->handle), id));

    auto *shellSurface = new ExampleShellSurface(this, surface, shellSurfaceResource);
    emit shellSurfaceCreated(shellSurface);
}

この仮想関数は、クライアントがコネクション上でリクエストを発行するたびに呼び出されます。

このシェル拡張は単一のQWaylandSurfaceRole しかサポートしませんが、シェルサーフェスを作成するときにQWaylandSurface に割り当てることは重要です。この主な理由は、同じサーフェスに競合するロールを割り当てることはプロトコルエラーとみなされ、このエラーが発生した場合、このエラーを発行するのはコンポジターの責任であるためです。サーフェスを採用するときにロールを設定することで、サーフェスが後で別のロールで再利用されたときにプロトコルエラーが発生することを防ぎます。

組み込み関数を使用してWaylandとQtの型を変換し、ExampleShellSurface オブジェクトを作成します。すべての準備が整ったら、shellSurfaceCreated() シグナルを発信します。このシグナルはQMLコードでインターセプトされ、シェルサーフェスのリストに追加されます。

ExampleShell {
    id: shell
    onShellSurfaceCreated: (shellSurface) => {
        shellSurfaces.append({shellSurface: shellSurface});
    }
}

ExampleShellSurface では、プロトコル拡張のシェルサーフェス部分を有効にします。

例の実行

クライアントが新しいシェル拡張にうまく接続できるようにするには、いくつかの設定を行う必要があります。

まず、クライアントがシェル拡張のプラグインを見つけられるようにしなければなりません。これを行う簡単な方法の一つは、QT_PLUGIN_PATH をプラグインのインストールディレクトリに設定することです。Qt はカテゴリごとにプラグインを検索するので、プラグインのパスはwayland-shell-integration というカテゴリのディレクトリを含む親ディレクトリを指すようにします。従って、インストールされたファイルが/path/to/build/plugins/wayland-shell-integration/libexampleshellplugin.so であれば、QT_PLUGIN_PATH を次のように設定します:

export QT_PLUGIN_PATH=/path/to/build/plugins

プラグインディレクトリを設定する他の方法については、プラグインのドキュメントを参照してください。

最後のステップは、クライアントが実際に正しいシェル拡張子にアタッチすることを確認することです。Qtクライアントは自動的に組み込みのシェル拡張にアタッチしようとしますが、これはQT_WAYLAND_SHELL_INTEGRATION 環境変数にロードする拡張の名前を設定することで上書きできます。

export QT_WAYLAND_SHELL_INTEGRATION=example-shell

これで全てです。カスタムシェルの例は、ごくわずかな機能しか持たない限定的なシェル拡張ですが、特殊な拡張を構築するための出発点として使うことができます。

サンプルプロジェクト @ code.qt.io

©2024 The Qt Company Ltd. ここに含まれるドキュメントの著作権はそれぞれの所有者に帰属します。 ここで提供されるドキュメントは、Free Software Foundation が発行したGNU Free Documentation License version 1.3に基づいてライセンスされています。 Qtおよびそれぞれのロゴは、フィンランドおよびその他の国におけるThe Qt Company Ltd.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。