自定义Shell

自定义Shell展示了如何实现自定义shell扩展。

Shell扩展到Wayland是用于管理窗口状态、位置和大小的协议。大多数合成器将支持一个或多个内建的扩展,但在某些情况下,编写一个包含应用程序所需的特定功能的自定义扩展可能是有用的。

这需要在Wayland连接的服务器和客户端两端实现shell扩展,因此它主要在构建平台并且控制合成器和其客户端应用程序时有用。

自定义Shell示例展示了简单shell扩展的实现。它分为三个部分

  • 自定义shell接口的协议描述。
  • 在客户端应用程序中连接到该接口的插件。
  • 包含接口服务器端实现的示例合成器。

协议描述遵循由wayland-scanner读取的标准XML格式。在此不详细说明,但它涵盖了以下功能

  • 创建wl_surface的shell表面的接口。这允许协议在现有的wl_surface API之上添加功能。
  • 在shell表面上设置窗口标题的请求。
  • 最小化/取消最小化shell表面的请求。
  • 事件通知客户端shell表面的当前最小化状态。

为了使qtwaylandscanner在构建过程中自动运行,我们使用CMake函数qt_generate_wayland_protocol_server_sources()qt_generate_wayland_protocol_client_sources()分别生成服务器端和客户端的粘合代码。(当使用qmake时,WAYLANDSERVERSOURCESWAYLANDCLIENTSOURCES变量达到同样的效果。)

客户端插件

为了使Qt客户端能够发现shell集成,我们必须重新实现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();
}

这将为shell集成附加一个"example-shell"密钥,并提供在客户端连接到接口时实例化ExampleShellIntegration类的方法。

创建shell扩展的API在头文件qwaylandclientshellapi_p.h中可用。

#include <QtWaylandClient/private/qwaylandclientshellapi_p.h>

此头文件需要包括私有API,因为与公共Qt API不同,它不提供二进制兼容性保证。API仍然被认为是稳定的,并将保持源兼容性,并在这方面类似于Qt中的其他插件API。

ExampleShellIntegration是创建shell表面的客户端入口点,如上所述。它扩展了QWaylandShellIntegrationTemplate类,使用了Curiously Recurring Template Pattern

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

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

它还继承自QtWayland::qt_example_shell类,该类基于协议的XML描述由qtwaylandscanner生成。

构造函数指定了我们支持的协议版本

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

示例_shell协议目前处于版本一,因此我们将1传递给父类。这用于协议协商,并确保如果合成器使用新版本的协议,旧客户端将继续工作。

ExampleShellIntegration初始化时,应用程序连接到服务器,并接收到了合成器支持的全球接口的广播。如果成功,它可以发出对接口的请求。在这种情况下,只有一个支持的请求:创建shell表面。它使用内置函数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扩展了两个类。

class ExampleShellSurface : public QWaylandShellSurface
        , public QtWayland::qt_example_shell_surface

第一个是QtWayland::qt_example_shell_surface类,它是根据协议的XML描述生成的。这提供了事件虚拟函数和请求的常规成员函数。

QtWayland::qt_example_shell_surface类只有一个事件。

    void example_shell_surface_minimize(uint32_t minimized) override;

ExampleShellSurface重新实现了这个事件以更新其内部窗口状态。当窗口状态发生更改时,它将挂起状态存储起来,稍后调用QWaylandShellSurface中的applyConfigureWhenPossible()。状态、大小和位置更改应该是这样的。这样一来,我们确保更改不干扰表面渲染,并且可以将多个相关更改轻松地作为一个整体应用。

当安全重新配置表面时,将调用虚拟函数applyConfigure()

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

这里是我们在窗口中实际提交新的(最小化或取消最小化)状态的地方。

第二个超类是QWaylandShellSurface。这是Wayland的QPA插件和QWaylandWindow与shell通信所使用的接口。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中,这反过来会转换成对自定义shell表面接口的请求。

合成器

示例的最后一部分是合成器本身,其结构与其它合成器例子相同。有关最小QML示例中构建块更多细节,请参阅。

与Custom Shell合成器的一个显著不同是shell扩展的实例化。在最小QML示例中实例化了shell扩展IviApplicationXdgShellWlShell,而Custom Shell示例仅创建了一个ExampleShell扩展的实例。

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

我们创建shell扩展的实例作为WaylandCompositor的直接子类,以便将其注册为一个全局接口。这将作为客户端连接时广播出去,他们会像上一节中概述的那样连接到该接口。

ExampleShell 是由协议 XML 中定义的 API 生成的 QtWaylandServer::qt_example_shell 接口的一个子类,它也继承了 QWaylandCompositorExtensionTemplate 类,确保这些对象可以被 QWaylandCompositor 识别为扩展。

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

这种双重继承是构建扩展时 Qt Wayland Compositor 中的一个典型模式。类 QWaylandCompositorExtensionTemplate 创建了 QWaylandCompositorExtension 和由 qtwaylandscanner 生成 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

这就是全部内容。自定义Shell示例是一个功能有限的扩展壳,但只有非常少的特性,但它可以作为构建专用扩展的起始点。

示例项目 @ code.qt.io

© 2024 Qt公司。本文件中包含的文档贡献均为各自所有者的版权。本文件提供的文档根据自由软件开发基金会发布的GNU自由文档许可证版本1.3进行许可。Qt及其相关标志是Qt公司在芬兰以及世界其他国家的商标。所有其他商标均为各自所有者的财产。