• 首页
  • 加入
  • RSS
  • 开发一个 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();)

    } // namespace KWin

    #include "main.moc"

    在 Effect 类中有几个不同阶段的方法可以重载。

    在方法名称中可以看出,在场景及窗口绘制的过程中,分别可以在实际绘制的前后分别执行一些动作,圆角插件就是在 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