自定义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
时,WAYLANDSERVERSOURCES
和WAYLANDCLIENTSOURCES
变量达到同样的效果。)
客户端插件
为了使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 ¶mList) override; }; QWaylandShellIntegration *QWaylandExampleShellIntegrationPlugin::create(const QString &key, const QStringList ¶mList) { 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扩展IviApplication、XdgShell和WlShell,而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示例是一个功能有限的扩展壳,但只有非常少的特性,但它可以作为构建专用扩展的起始点。
© 2024 Qt公司。本文件中包含的文档贡献均为各自所有者的版权。本文件提供的文档根据自由软件开发基金会发布的GNU自由文档许可证版本1.3进行许可。Qt及其相关标志是Qt公司在芬兰以及世界其他国家的商标。所有其他商标均为各自所有者的财产。