开发一个 KWin 特效插件
Sunday, June 25, 2023
justforlxz
@lxz:mkacg.chat
KWin 是 KDE 开发的窗口管理器,提供了非常丰富的插件,可以对功能进行大量的定制。
本篇文章是对窗口特效插件的开发介绍。
插件开发
插件定义
KWin 的插件通常可以使用一些宏辅助生成代码,例如使用 KPluginFactory
进行插件的定义,内容是用来生成插件的入口类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| #define EffectPluginFactory_iid "org.kde.kwin.EffectPluginFactory" KWIN_PLUGIN_VERSION_STRING #define KWIN_PLUGIN_FACTORY_NAME KPLUGINFACTORY_PLUGIN_CLASS_INTERNAL_NAME #define KWIN_EFFECT_FACTORY_SUPPORTED_ENABLED(className, jsonFile, supported, enabled ) \ class KWIN_PLUGIN_FACTORY_NAME : public KWin::EffectPluginFactory \ { \ Q_OBJECT \ Q_PLUGIN_METADATA(IID EffectPluginFactory_iid FILE jsonFile) \ Q_INTERFACES(KPluginFactory) \ public: \ explicit KWIN_PLUGIN_FACTORY_NAME() {} \ ~KWIN_PLUGIN_FACTORY_NAME() {} \ bool isSupported() const override { \ supported \ } \ bool enabledByDefault() const override { \ enabled \ } \ KWin::Effect *createEffect() const override { \ return new className(); \ } \ };
#define KWIN_EFFECT_FACTORY_ENABLED(className, jsonFile, enabled ) \ KWIN_EFFECT_FACTORY_SUPPORTED_ENABLED(className, jsonFile, return true;, enabled )
#define KWIN_EFFECT_FACTORY_SUPPORTED(className, jsonFile, supported ) \ KWIN_EFFECT_FACTORY_SUPPORTED_ENABLED(className, jsonFile, supported, return true; )
#define KWIN_EFFECT_FACTORY(className, jsonFile ) \ KWIN_EFFECT_FACTORY_SUPPORTED_ENABLED(className, jsonFile, return true;, return true; )
|
大部分宏只是为了方便结构修改,我们只需要使用 K_PLUGIN_FACTORY
进行插件定义即可。
假设我们开发了一个插件,名字叫 demo,我们只需要在 main.cpp 中使用 KWIN_EFFECT_FACTORY_SUPPORTED
定义
1 2 3 4 5
| KWIN_EFFECT_FACTORY_SUPPORTED( Demo, "metadata.json", return true; )
|
代码展开后是这样的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| class KPLUGINFACTORY_PLUGIN_CLASS_INTERNAL_NAME : public KWin::EffectPluginFactory \ { Q_OBJECT Q_PLUGIN_METADATA(IID EffectPluginFactory_iid FILE "metadata.json") Q_INTERFACES(KPluginFactory) public: \ explicit KPLUGINFACTORY_PLUGIN_CLASS_INTERNAL_NAME() {} ~KPLUGINFACTORY_PLUGIN_CLASS_INTERNAL_NAME() {} bool isSupported() const override { return true; } bool enabledByDefault() const override { return true; } KWin::Effect *createEffect() const override { return new Demo(); } };
|
可以看到,其实 KWIN_EFFECT_FACTORY_SUPPORTED
只是为我们生成了工厂函数,辅助生成了一些必要的重载。
metadata.json
文件是用来作为插件的描述信息使用的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| { "KPlugin": { "Category": "Accessibility", "Description": "Allow clip of window content", "EnabledByDefault": true, "Id": "scissor", "License": "GPL", "Name": "ScissorWindow", "Name[zh_CN]": "窗口圆角" }, "org.kde.kwin.effect": { "enabledByDefaultMethod": true } }
|
特效插件
特效插件是一类可以改变窗口画面的插件,例如我们可以在插件里对窗口进行贴图、变形和裁切,在 DDE 中,就使用特效插件完成了圆角裁切和窗口模糊。
这里使用圆角裁切插件作为例子,首先使用 KWIN_EFFECT_FACTORY_SUPPORTED
宏对插件进行定义, KWIN_EFFECT_FACTORY_SUPPORTED
接受一个 class 作为返回的接口类,它需要继承自 Effect
,第二个参数是元信息的 json 文件,第三个参数是返回是否支持,在启用插件时可对当前环境进行判断,例如插件需要使用 OpenGL
对图形进行一些操作,但是当前环境不支持 OpenGL
,那么插件就不会启用。
1 2 3 4 5 6 7 8 9 10 11 12
| #include "scissorwindow.h"
namespace KWin {
KWIN_EFFECT_FACTORY_SUPPORTED(ScissorWindow, "metadata.json.stripped", return ScissorWindow::supported();)
}
#include "main.moc"
|
在 Effect 类中有几个不同阶段的方法可以重载。
- prePaintScreen
- 设置是否变换窗口或整个屏幕
- 更改将要绘制的屏幕区域
- 做各种内务处理任务,比如初始化你的效果变量
用于即将到来的绘画过程或更新动画的进度
- paintScreen
- 在窗口上画东西(调用后画 effect->paintScreen())
- 绘制多个桌面和/或同一桌面的多个副本
- postPaintScreen
- 在动画的情况下安排下一次重绘,不应该在这里画任何东西。
- prePaintWindow
- 启用或禁用窗口的绘制(例如启用最小化窗口的绘制)
- 将窗口设置为半透明
- 设置要转换的窗口
- 请求将窗口分成多个部分
- paintWindow
- 做各种转换
- 改变窗口的不透明度
- 改变亮度和/或饱和度,如果支持的话
- postPaintWindow
- 在动画的情况下为单个窗口安排下一次重绘
不应该在这里画任何东西。
- paintEffectFrame
- 在绘制 EffectFrame 之前直接调用此方法。
- 如果需要绑定shader或者执行,可以实现这个方法帧渲染前的其他操作。
- drawWindow
- 可以调用以绘制一个窗口的多个副本(例如缩略图)。
- 可以在这里改变窗口的不透明度/亮度/等,但不能做任何转换。
- 在基于 OpenGL 的合成中,框架确保上下文是最新的
在方法名称中可以看出,在场景及窗口绘制的过程中,分别可以在实际绘制的前后分别执行一些动作,圆角插件就是在 drawWindow
函数中,使用 OpenGL
对窗口使用着色器进行窗口裁切,并绘制到屏幕上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| void ScissorWindow::drawWindow(EffectWindow *w, int mask, const QRegion& region, WindowPaintData &data) { if (w->isDesktop() || isMaximized(w)) { return effects->drawWindow(w, mask, region, data); }
QPointF cornerRadius; const QVariant valueRadius = w->data(WindowRadiusRole); if (valueRadius.isValid()) { cornerRadius = w->data(WindowRadiusRole).toPointF(); const qreal xMin{ std::min(cornerRadius.x(), w->width() / 2.0) }; const qreal yMin{ std::min(cornerRadius.y(), w->height() / 2.0) }; const qreal minRadius{ std::min(xMin, yMin) }; cornerRadius = QPointF(minRadius, minRadius); }
if (cornerRadius.x() < 2 && cornerRadius.y() < 2) { return effects->drawWindow(w, mask, region, data); }
const QString& key = QString("%1+%2").arg(cornerRadius.toPoint().x()).arg(cornerRadius.toPoint().y() ); if (!m_texMaskMap.count(key)) { QImage img(QSize(radius.x() * 2, radius.y() * 2), QImage::Format_RGBA8888); img.fill(QColor(0, 0, 0, 0)); QPainter painter(&img); painter.setPen(Qt::NoPen); painter.setBrush(QColor(255, 255, 255, 255)); painter.setRenderHint(QPainter::Antialiasing); painter.drawEllipse(0, 0, radius.x() * 2, radius.y() * 2); painter.end();
m_texMaskMap[key] = new GLTexture(img.copy(0, 0, radius.x(), radius.y())); m_texMaskMap[key]->setFilter(GL_LINEAR); m_texMaskMap[key]->setWrapMode(GL_CLAMP_TO_EDGE); }
ShaderManager::instance()->pushShader(m_filletOptimizeShader); m_filletOptimizeShader->setUniform("typ1", 1); m_filletOptimizeShader->setUniform("sampler", 0); m_filletOptimizeShader->setUniform("msk1", 1); m_filletOptimizeShader->setUniform("k", QVector2D(w->width() / cornerRadius.x(), w->height() / cornerRadius.y())); if (w->hasDecoration()) { m_filletOptimizeShader->setUniform("typ2", 0); } else { m_filletOptimizeShader->setUniform("typ2", 1); } auto old_shader = data.shader; data.shader = m_filletOptimizeShader;
glActiveTexture(GL_TEXTURE1); m_texMaskMap[key]->bind(); glActiveTexture(GL_TEXTURE0); effects->drawWindow(w, mask, region, data); ShaderManager::instance()->popShader(); data.shader = old_shader; glActiveTexture(GL_TEXTURE1); m_texMaskMap[key]->unbind(); glActiveTexture(GL_TEXTURE0); return; }
|
如果窗口是桌面类型,或者已经最大化了,则无需处理,直接返回 Effect 原本的处理函数。
之后尝试从窗口属性中取出圆角大小的值,如果没有设置圆角大小,或者值小于2,则无需处理。
尝试查询缓存,在这里为窗口的四个角构建一份遮罩对象并缓存,使用 OpenGL
将遮罩和着色器进行关联,激活两个材质分别绘制窗口内容和四个角的遮罩,在着色器中完成窗口圆角的半透明效果。
限于篇幅,本文不展开介绍如何实现圆角插件的全部实现过程,仅挑选关键步骤。
安装
将动态库复制到 /usr/share/kwin/effects/plugins
,并使用 DBus
激活插件。
1
| qdbus --literal org.kde.KWin /Effects org.kde.kwin.Effects.loadEffect scissor
|