[software development] 自实现dde-dock插件接收器解决方案(补充)
Tofloor
poster avatar
曾子康 TIM
deepin
10 hours ago
Author

自实现dde-dock插件接收器解决方案

上次写的不全面,太复杂,在这里补一份

前提

这里的接收器是简单的接收器,dde-dock的接收器请看对应的仓库(分散在dde-dock的仓库中)

dde-dock插件编写指导中有dde-dock插件的编写教程

0.插件接收器结构构造

#include 
#include 
#include 
class Plugin_Root;
class PluginController : public QObject, public PluginProxyInterface
{
    Q_OBJECT
public:
    explicit PluginController(QObject *parent = nullptr, Plugin_Root *plugin_root = nullptr);
    ~PluginController() override;
    virtual void itemAdded(PluginsItemInterface * const itemInter, const QString &itemKey) override;
    virtual void itemUpdate(PluginsItemInterface * const itemInter, const QString &itemKey) override;
    virtual void itemRemoved(PluginsItemInterface * const itemInter, const QString &itemKey) override;
    virtual void requestWindowAutoHide(PluginsItemInterface * const itemInter, const QString &itemKey, const bool autoHide) override;
    virtual void requestRefreshWindowVisible(PluginsItemInterface * const itemInter, const QString &itemKey) override;
    virtual void requestSetAppletVisible(PluginsItemInterface * const itemInter, const QString &itemKey, const bool visible) override;
    virtual void saveValue(PluginsItemInterface * const itemInter, const QString &key, const QVariant &value) override;
    virtual const QVariant getValue(PluginsItemInterface *const itemInter, const QString &key, const QVariant& fallback = QVariant()) override;
    virtual void removeValue(PluginsItemInterface *const itemInter, const QStringList &keyList) override;
private:
    Plugin_Root *root = nullptr;
    QSettings *m_settings = nullptr;
    QString buildKey(PluginsItemInterface *itemInter, const QString &key) const;
};
class Plugin_Root : public QObject
{
    Q_OBJECT
public:
    explicit Plugin_Root(QObject *parent);
    ~Plugin_Root();
private:
    QPluginLoader *plugin_loader = new QPluginLoader(this);
    PluginController *plugin_controller = new PluginController(this, this);
}

其中interfances文件夹是从/usr/include/dde-dock中复制的,该文件夹不会默认安装,需要执行如下命令安装

sudo apt install dde-dock-dev

1.定义PluginController

PluginController::PluginController(QObject *parent, Plugin_Root *plugin_root)
    :QObject(parent)
    ,root(plugin_root)
{
    m_settings = new QSettings("Program", "plugins", this);
}
PluginController::~PluginController()
{
    m_settings->sync();
}
QString PluginController::buildKey(PluginsItemInterface *itemInter, const QString &key) const
{
    if (itemInter)
    {
        return QString("%1/%2").arg(itemInter->pluginName()).arg(key);
    }
    else
    {
        return QString("unknown_plugin/%1").arg(key);
    }
}
void PluginController::itemAdded(PluginsItemInterface * const itemInter, const QString &itemKey)
{
    //通过root对UI进行控制,当然,也可以emit出去
}
void PluginController::itemUpdate(PluginsItemInterface * const itemInter, const QString &itemKey)
{}
void PluginController::itemRemoved(PluginsItemInterface * const itemInter, const QString &itemKey)
{}
void PluginController::requestWindowAutoHide(PluginsItemInterface * const itemInter, const QString &itemKey, const bool autoHide)
{}
void PluginController::requestRefreshWindowVisible(PluginsItemInterface * const itemInter, const QString &itemKey)
{}
void PluginController::requestSetAppletVisible(PluginsItemInterface * const itemInter, const QString &itemKey, const bool visible)
{}
void PluginController::saveValue(PluginsItemInterface * const itemInter, const QString &key, const QVariant &value)
{
    QString fullKey = buildKey(itemInter, key);
    m_settings->setValue(fullKey, value);
}
const QVariant PluginController::getValue(PluginsItemInterface * const itemInter, const QString &key, const QVariant &fallback)
{
    QString fullKey = buildKey(itemInter, key);
    QVariant value = m_settings->value(fullKey, fallback);
    return value;
}
void PluginController::removeValue(PluginsItemInterface * const itemInter, const QStringList &keyList)
{
    for (const QString &key : keyList)
    {
        QString fullKey = buildKey(itemInter, key);
        m_settings->remove(fullKey);
    }
}

此处用PluginsItemInterface的方法

QWidget *itemWidget(const QString &itemKey);
QWidget *itemTipsWidget(const QString &itemKey);
QWidget *itemPopupApplet(const QString &itemKey);

获取对应的控件,并放到UI上

2.使用QPluginLoader导入与卸载插件

void Plugin_Root::load_plugin(QString filepath)
{
    unload_plugin();
    if (filepath.isEmpty()) return;
    plugin_loader->setFileName(filepath);
    if (!plugin_loader->load())
    {
        qDebug() << "插件导入失败:" << plugin_loader->errorString();
        return;
    }
    QObject *pluginInstance = plugin_loader->instance();
    if (!pluginInstance)
    {
        plugin_loader->unload();
        return;
    }
    PluginsItemInterface *plugin_interface = qobject_cast(pluginInstance);
    if (plugin_interface)
    {
        plugin_interface->init(plugin_controller);
        plugin_controller->itemUpdate(plugin_interface, "");//强制刷新插件
        plugin_interface->positionChanged(plugin_position);//设置插件位置(针对插件套插件的情况)
    }
}
void Plugin_Root::unload_plugin()
{
    if (!plugin_loader->isLoaded()) return;
    QObject *pluginInstance = plugin_loader->instance();
    if (pluginInstance)
    {
        PluginsItemInterface *plugin_interface = qobject_cast(pluginInstance);
        if (plugin_interface)
        {
            if (plugin_interface->pluginIsAllowDisable() && !plugin_interface->pluginIsDisable())
            {
                plugin_interface->pluginStateSwitched();
            }
        }
    }
    plugin_loader->unload();
}

3.动态卸载插件的方案

dde-dock插件没有定义怎么卸载,故卸载的时候大概会打出SIGSEGV

故动态卸载的方式只能是禁用插件+隐藏插件的QWidget载体

Plugin_Root::~Plugin_Root()
{
    only_hide();
    return;
}
void Plugin_Root::only_hide()
{
    this->setParent(nullptr);
    if (!plugin_loader->isLoaded())
    {
        unload_plugin();
        this->deleteLater();
        return;
    }
    if (has_been_closed) return;
    has_been_closed = true;
    QObject *pluginInstance = plugin_loader->instance();
    if (pluginInstance)
    {
        PluginsItemInterface *plugin_interface = qobject_cast(pluginInstance);
        if (plugin_interface)
        {
            if (plugin_interface->pluginIsAllowDisable())
            {
                if (!plugin_interface->pluginIsDisable())
                {
                    plugin_interface->pluginStateSwitched();
                }
            }
        }
    }
    //实现隐藏UI
}

4.实现右键菜单

Plugin_Item_Widget是插件的Item主控件的载体

void Plugin_Item_Widget::set_extra_menu(QString data)
{
    for (auto child : plugin_extra_context_menu->children())
    {
        child->disconnect();
        child->deleteLater();
    }
    if (data.isEmpty())
    {
        use_plugin_context = false;
        return;
    }
    else
    {
        use_plugin_context = true;
        QJsonParseError error;
        QJsonDocument doc = QJsonDocument::fromJson(data.toUtf8(), &error);
        if (error.error != QJsonParseError::NoError)
        {
            return;
        }
        if (!doc.isObject())
        {
            return;
        }
        //(O_O)!好像没有详细定义
        QJsonObject menuObj = doc.object();
        bool checkableMenu = menuObj["checkableMenu"].toBool(false);
        bool singleCheck = menuObj["singleCheck"].toBool(false);
        if (singleCheck && checkableMenu)
        {
            QActionGroup *exclusiveGroup = new QActionGroup(plugin_extra_context_menu);
            exclusiveGroup->setExclusive(true);
        }
        if (!menuObj.contains("items") || !menuObj["items"].isArray())
        {
            return;
        }
        QJsonArray itemsArray = menuObj["items"].toArray();
        parseMenuItemsArray(itemsArray, checkableMenu, singleCheck);
    }
}
void Plugin_Item_Widget::parseMenuItemsArray(const QJsonArray &itemsArray, bool checkableMenu, bool singleCheck)
{
    QActionGroup *exclusiveGroup = nullptr;
    if (singleCheck && checkableMenu)
    {
        exclusiveGroup = new QActionGroup(plugin_extra_context_menu);
        exclusiveGroup->setExclusive(true);
    }
    for (const QJsonValue &itemValue : itemsArray)
    {
        if (!itemValue.isObject())
        {
            continue;
        }
        QJsonObject itemObj = itemValue.toObject();
        QString itemId = itemObj["itemId"].toString();
        QString itemText = itemObj["itemText"].toString();
        bool isActive = itemObj["isActive"].toBool(true);
        if (itemText.isEmpty())
        {
            continue;
        }
        QAction *action = plugin_extra_context_menu->addAction(itemText);
        if (!itemId.isEmpty())
        {
            action->setData(itemId);
        }
        action->setEnabled(isActive);
        if (checkableMenu)
        {
            action->setCheckable(true);
            if (itemObj.contains("checked"))
            {
                bool checked = itemObj["checked"].toBool(false);
                action->setChecked(checked);
            }
            if (singleCheck && exclusiveGroup)
            {
                exclusiveGroup->addAction(action);
            }
        }
        if (itemObj.contains("items") && itemObj["items"].isArray())
        {
            QMenu *subMenu = new QMenu(itemText, plugin_extra_context_menu);
            action->setMenu(subMenu);
            QJsonArray subItemsArray = itemObj["items"].toArray();
            parseMenuItemsArray(subMenu, subItemsArray, checkableMenu, singleCheck);
        }
        connect(action, &QAction::triggered, this, [this, action, itemId]
        {
            emit this->extra_menu_call(itemId, action->isChecked());
        });
    }
}
void Plugin_Item_Widget::parseMenuItemsArray(QMenu* parentMenu, const QJsonArray& itemsArray, bool checkableMenu, bool singleCheck)
{
    QActionGroup *exclusiveGroup = nullptr;

    if (singleCheck && checkableMenu)
    {
        exclusiveGroup = new QActionGroup(parentMenu);
        exclusiveGroup->setExclusive(true);
    }
    for (const QJsonValue &itemValue : itemsArray)
    {
        if (!itemValue.isObject())
        {
            continue;
        }
        QJsonObject itemObj = itemValue.toObject();
        QString itemId = itemObj["itemId"].toString();
        QString itemText = itemObj["itemText"].toString();
        bool isActive = itemObj["isActive"].toBool(true);
        if (itemText.isEmpty())
        {
            continue;
        }
        QAction *action = parentMenu->addAction(itemText);
        if (!itemId.isEmpty())
        {
            action->setData(itemId);
        }
        action->setEnabled(isActive);
        if (checkableMenu)
        {
            action->setCheckable(true);
            if (itemObj.contains("checked"))
            {
                bool checked = itemObj["checked"].toBool(false);
                action->setChecked(checked);
            }
            if (singleCheck && exclusiveGroup)
            {
                exclusiveGroup->addAction(action);
            }
        }
        if (itemObj.contains("items") && itemObj["items"].isArray())
        {
            QMenu *subMenu = new QMenu(itemText, parentMenu);
            action->setMenu(subMenu);
            QJsonArray subItemsArray = itemObj["items"].toArray();
            parseMenuItemsArray(subMenu, subItemsArray, checkableMenu, singleCheck);
        }
        connect(action, &QAction::triggered, this, [this, action, itemId]
        {
            emit this->extra_menu_call(itemId, action->isChecked());
        });
    }
}
void Plugin_Item_Widget::contextMenuEvent(QContextMenuEvent *event)
{
    plugin_extra_context_menu->exec(mapToGlobal(event->pos()));
}

通过

set_extra_menu(itemInter->itemContextMenu(itemKey));

设置右键菜单,注意,itemInter->itemContextMenu(itemKey);只能调用1次

5.实现左键Command

void Plugin_Root::click_call()
{
    QObject *pluginInstance = plugin_loader->instance();
    if (pluginInstance)
    {
        PluginsItemInterface *plugin_interface = qobject_cast(pluginInstance);
        if (plugin_interface)
        {
            QString command = plugin_interface->itemCommand(plugin_itemKey);
            if (command.isNull() || command.isEmpty()) return;
            QProcess process;
            process.setProgram("/bin/bash");
            process.setArguments(QStringList() << "-c" << command);
            process.setStandardOutputFile("/dev/null");
            process.setStandardErrorFile("/dev/null");
            process.startDetached();
        }
    }
}

在单击的时候触发Plugin_Root::click_call();即可

6.杂项

通过

virtual void contextMenuEvent(QContextMenuEvent *event);
virtual void mousePressEvent(QMouseEvent *event);
virtual void mouseReleaseEvent(QMouseEvent *event);
virtual void enterEvent(QEvent *event);
virtual void leaveEvent(QEvent *event);

实现tips,popup窗口的显示与隐藏

dde-dock插件编写指导中有dde-dock插件的编写教程

DTK插件的特殊解决方案

对于某些插件,一般是使用了DTK的插件,导入插件时会崩溃,下图为崩溃时候的Debug信息

截图_选择区域_20260216144944.png

解决方案是将main.cpp的QApplication改为DApplication

QT += dtkcore dtkgui dtkwidget
...
#include 
int main(int argc, char* argv[])
{
    Dtk::Widget::DApplication app(argc, argv);
    ...
}

至于CMake,我一直用的是QMake,不知道CMake怎么写

该更改已经提交的github上

题外话

不过还有一个点.我不明白,之前的灰色字体挺好的,为什么要改成黑色?

以及dde-file-manager的搜索框的图像有偏移,QMenu的Checked的QAction好像也有类似的问题

在更新包的时候出现的这样的问题,不知道是只有20.9有问题,还是其他版本也有问题
截图_deepin-editor_20260216144729.png

Reply Favorite View the author
All Replies
wcs4221
deepin
8 hours ago
#1

hi支持一下!

Reply View the author