Featured image of post UOS20文管dde-file-manager右键菜单实践

UOS20文管dde-file-manager右键菜单实践

UOS文件资源管理器dde-file-manager右键菜单实现。

本文主要总结了本人在研究实现 dde-file-manager 文件管理器右键菜单方案中,收集到的一些有用的信息。值得一提的是,在本文中dde-file-manager版本信息是至关重要的,甚至大于方案本身的重要性,请仔细比对之。

综合考虑自定义性、接口维护性、dfm版本等多重因素,我姑且认为以下接口的排序应当是这样的:

1
2
DFMExtMenuPlugin > oem > menuinterface > json
Gtihub: https://github.com/pinkkmin/dfm-examples

oem 的方式

文件管理上下文右键菜单规范 - deepin开发者平台

oem的方式有支持两种格式,分别是.desktop.conf.

对于.desktop格式,将菜单文件移至目标目录/usr/share/deepin/dde-file-manager/oem-extensions/,重启文管以重新载入菜单。以下是示例.

 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
[Desktop Entry]
Version=1.0
Type=Application
Actions=Foo1;Foo2;
Name=Foo
Icon=Foo

#文件类型
#MimeType=image/x-foo

#支持xxx.foo文件
X-DDE-FileManager-SupportSuffix=foo

#不在桌面显示
X-DDE-FileManager-NotShowIn=Desktop

#file://
X-DDE-FileManager-SupportSchemes=file

#不支持的文件类型
#X-DDE-FileManager-ExcludeMimeTypes=image/x-foo2

#仅支持单选文件
X-DDE-FileManager-MenuTypes=SingleFile;

[Desktop Action Foo1]
Exec=/usr/bin/foo --foo1 %f
Icon=foo1
Name=Open as foo1
Name[zh_CN]=用foo1打开

[Desktop Action Foo2]
Exec=/usr/bin/foo --foo2 %f
Name=Open as foo2
Name[zh_CN]=用foo2打开
Icon=foo2

对于.conf格式,目标目录为/usr/share/applications/context-menus,以下是示例:

 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
61
62
63
64
65
66
67
#右键菜单入口
[Menu Entry]
#该配置文件采用的规范协议版本
Version=1.0
#描述
Comment=This is a test file!!!
#本地化描述
Comment[zh_CN]=这是一个测试文件
#在[Menu Entry]下包含另外有2个分组,入口名对应为ActionOne和ActionTwo,作为一级菜单
Actions=ActionOne:ActionTwo

[Menu Action ActionOne]                                                                       
#菜单名
Name=Menu one %b
Name[zh_CN]=菜单1 %b
#选中单个文件和多个文件时生效
X-DDE-FileManager-MenuTypes=SingleFile:MultiFiles
#支持文件类型
MimeType=image/x-foo:image/x-foo1;
#不支持的文件类型
#X-DDE-FileManager-ExcludeMimeTypes=image/x-foo2:image/x-foo3

#支持file://
X-DDE-FileManager-SupportSchemes=file
#不支持桌面
X-DDE-FileManager-NotShowIn=Desktop

#支持xxx.foo文件
X-DDE-FileManager-SupportSuffix=foo

#默认在右键菜单的一级菜单的第二个位置插入该项
X-DDE-FileManager-PosNum=1
#选中多个文件时在右键菜单的输入第三个位置
X-DDE-FileManager-PosNum-MultiFiles=3
#在该项的上方插入分割线
X-DDE-FileManager-Separator=Top
#菜单执行命令,该一级菜单为执行动作的菜单,无子菜单
Exec=/usr/bin/foo %F

[Menu Action ActionTwo]                                                                       
#菜单名
Name=Menu two %d
Name[zh_CN]=菜单2 %d
#在空白区时生效
X-DDE-FileManager-MenuTypes=BlankSpace
#不支持文件管理器
X-DDE-FileManager-NotShowIn=Filemanager
#默认在右键菜单的一级菜单的第三个位置插入该项
X-DDE-FileManager-PosNum=3
#在该项的上下方插入分割线
X-DDE-FileManager-Separator=Both
#菜单包含两个子菜单
Actions=ActionThree:ActionFour

[Menu Action ActionThree] 
Name=Menu three
Name[zh_CN]=菜单3
X-DDE-FileManager-PosNum=1
#菜单执行命令
Exec=/usr/bin/foo --xxx %p

[Menu Action ActionFour] 
Name=Menu four
Name[zh_CN]=菜单4
X-DDE-FileManager-PosNum=2
#菜单执行命令
Exec=/usr/bin/foo --yyy %p

在菜单方案中,我采用的是.conf的格式。但是,.conf的方式不支持菜单带图标。

插件的方式之 DFMExtMenuPlugin

dde-file-manager >= 5.5.10 (10 Dec 2021)

depends: libdfm-extension-dev

👀 结合tag和commit以及系统的发布日志,发现此接口从5.5.10开始支持。

History for src/dde-file-manager-extension - linuxdeepin/dde-file-manager

👊 我实践此接口时(2022-09-02)并没有找到可参考的文档或示例,所以我邮件请教了dfm的开发者。他提供了一份有用的文档(见docs/dfm-extension-doc(answerofdeveloper).md)可以参考。

⚠️不过应当小心此文档的时效性,还是以官方的最新文档以及github仓库信息优先。

1
2
3
4
extern "C" void dfm_extension_initiliaze();
extern "C" void dfm_extension_shutdown();
extern "C" DFMExtMenuPlugin *dfm_extension_menu();
extern "C" DFMExtEmblemIconPlugin *dfm_extension_emblem();
 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
class DFMExtMenuPlugin
{
    DFM_DISABLE_COPY(DFMExtMenuPlugin)
public:
    using InitializeFunc = std::function<void(DFMEXT::DFMExtMenuProxy *proxy)>;
    using BuildNormalMenuFunc = std::function<bool(DFMExtMenu *,
                                                   const std::string &,
                                                   const std::string &,
                                                   const std::list<std::string> &, bool)>;
    using BuildEmptyAreaMenuFunc = std::function<bool(DFMEXT::DFMExtMenu *, const std::string &, bool)>;

public:
    DFMExtMenuPlugin();
    ~DFMExtMenuPlugin();

    DFM_FAKE_VIRTUAL void initialize(DFMEXT::DFMExtMenuProxy *proxy);
    DFM_FAKE_VIRTUAL bool buildNormalMenu(DFMEXT::DFMExtMenu *main,
                                          const std::string &currentUrl,
                                          const std::string &focusUrl,
                                          const std::list<std::string> &urlList,
                                          bool onDesktop);
    DFM_FAKE_VIRTUAL bool buildEmptyAreaMenu(DFMEXT::DFMExtMenu *main, const std::string &currentUrl, bool onDesktop);

public:
    void registerInitialize(const InitializeFunc &func);
    void registerBuildNormalMenu(const BuildNormalMenuFunc &func);
    void registerBuildEmptyAreaMenu(const BuildEmptyAreaMenuFunc &func);

private:
    DFMExtMenuPluginPrivate *d;
};

阅读dde-file-manager源码,之后:

  1. extension插件的管理在于类DFMExtPluginManager, 而动态库的加载在DFMExtPluginLoader中,可以看到对dfm_extension_menu的转换.

  2. 如上类DFMExtMenuPlugin的定义可见,要实现右键菜单,我们需要继承此类。而创建一个我们的实例,并为之register绑定处理函数。

  3. 可见DFMExtMenuPlugin.cpp,如果没有register对应的函数也是没关系的。

  4. 因此,基于此接口,我们需要做的事情就是:

    1. 先实现三个dfm_extension_接口,在dfm_extension_menu返回我们的实例;

    2. 为我们的实例注册函数,菜单的展示主要是BuildNormalMenuFunc

  5. 以下,展示了一份简单的菜单示例,获取工程请参见examples目录。

  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
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
//dfmctxmenuplugin.cpp
#include "dfm-extension.h"
#include "menu/dfmextmenu.h"
#include "menu/dfmextaction.h"
#include "menu/dfmextmenuplugin.h"

#include <QDebug>
#include <QWidget>
#include <QMessageLogger>

USING_DFMEXT_NAMESPACE

DFMExtMenuPlugin dsm_inst;
DFMEXT::DFMExtMenuProxy *menuProxy = nullptr;

DFMExtMenuPlugin::InitializeFunc initFunc;
DFMExtMenuPlugin::BuildNormalMenuFunc buildnFunc;
DFMExtAction::TriggeredFunc triggerFunc;

void DSM_ActionTrigger(DFMExtAction *, bool check)
{
    QWidget *dlg = new QWidget();
    dlg->show();
}

void DSM_Initialize(DFMEXT::DFMExtMenuProxy *proxy)
{
    qInfo()<<"########################## DSM_Initialize proxy.......";
    menuProxy = proxy;
}

bool DSM_BuildNormalMenu(DFMEXT::DFMExtMenu *main,
                         const std::string &currentUrl,
                         const std::string &focusUrl,
                         const std::list<std::string> &urlList,
                         bool onDesktop)
{
    qInfo()<<"########################## DSM_BuildNormalMenu begin";
    if (main == nullptr)
            return false;

    // top menu1
    DFMExtAction *action1 =  menuProxy->createAction();
    action1->setText("top-menu1");
    action1->registerTriggered(triggerFunc);
    action1->setIcon("://Dame.png");

    // sub menus
    DFMExtMenu *menu = menuProxy->createMenu();

    {
        // sub menu1
        DFMExtAction *action1 =  menuProxy->createAction();
        action1->setText("sub-menu1");
        action1->registerTriggered(triggerFunc);

        // sub menu2
        DFMExtAction *action2 =  menuProxy->createAction();
        action2->setText("sub-menu2");
        action2->registerTriggered(triggerFunc);

        menu->addAction(action1);
        menu->addAction(action2);
    }

    action1->setMenu(menu);

    // top menu2
    DFMExtAction *action2 =  menuProxy->createAction();
    action2->setText("top-menu2");
    action2->registerTriggered(triggerFunc);
    action2->setIcon("://Dame.png");

    std::list<DFMExtAction *> mainActions = main->actions();
    if(mainActions.size() > 0)
    {
        DFMExtAction* posAction = mainActions.front();
        main->insertAction(posAction, action1);
        main->insertAction(action1, action2);
    }
    else
    {
        main->addAction(action1);
        main->addAction(action2);
    }
//    DFMExtAction *menuAction();
//    std::list<DFMExtAction *> actions();
    return true;
}

extern "C" void dfm_extension_initiliaze()
{
    qInfo()<<"########################## dfm_extension_initiliaze begin";

    initFunc = DSM_Initialize;
    buildnFunc = DSM_BuildNormalMenu;
    triggerFunc = DSM_ActionTrigger;

    dsm_inst.registerInitialize(initFunc);
    dsm_inst.registerBuildNormalMenu(buildnFunc);
    qInfo()<<"########################## dfm_extension_initiliaze end";
}

extern "C"  void dfm_extension_shutdown()
{
    qInfo()<<"########################## dfm_extension_shutdown ";
}

extern "C"  DFMExtMenuPlugin *dfm_extension_menu()
{
    qInfo()<<"########################## dfm_extesion_menu-----------";

    return &dsm_inst;
}

将编译完成的动态库放置/usr/lib/$${QMAKE_HOST.arch}-linux-gnu/dde-file-manager/plugins/extensions,在支持extensions版本的dde-file-manager此目录已自动创建。

杀死dde-file-manager进程,并重启之,即可加载我们的插件.

插件方式之 MenuInterface

dde-file-manager < 5.0.3

depends: libdde-file-manager-dev

⚠️ 此接口在5.0.3已经被移除,具体可查看dde-file-manager仓库的pluginmanager.cpp文件的commit记录。

⚠️ 此链接的内容可能已过期。Context Menu Extension (zh) | Deepin File Manager

 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
/*
 * 文件管理器的右键菜单插件接口
*/
class MenuInterface : public QObject
{
public:
    explicit MenuInterface(QObject *parent = nullptr)
        : QObject(parent) {}
    virtual ~MenuInterface() {}
    /*!
     * \brief additionalMenu 选中文件菜单接口
     * \param files: 选中的文件路径列表
     * \param currentDir:右键所在的文件目录
     * \return QList<QAction*>:返回一组QAction指针列表,每个action对应菜单的一个菜单项
     */
    virtual QList<QAction*> additionalMenu(const QStringList &files, const QString& currentDir){
        Q_UNUSED(files)
        Q_UNUSED(currentDir)
        QList<QAction*> actions;
        return actions;
    }
    /*!
     * \brief additionalEmptyMenu 空白菜单接口
     * \param currentDir: 空白菜单所在的文件目录
     * \param onDesktop: 空白菜单是否为在桌面上触发的
     * \return QList<QAction*>:返回一组QAction指针列表,每个action对应菜单的一个菜单项
     */
    virtual QList<QAction*> additionalEmptyMenu(const QString &currentDir, bool onDesktop = false){
        Q_UNUSED(currentDir)
        Q_UNUSED(onDesktop)
        QList<QAction*> actions;
        return actions;
    }
};

以下是基于此接口实现的右键菜单插件,和peony的菜单插件类似。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// dfmctxmenuplugin.h
#ifndef DFMCTXMENUPLUGIN_H
#define DFMCTXMENUPLUGIN_H
#include "dde-file-manager-plugins/menuinterface.h"
class DFMCtxMenuPlugin : public MenuInterface
 {
 Q_OBJECT
 Q_INTERFACES(MenuInterface)
 Q_PLUGIN_METADATA(IID MenuInterface_iid)
 public:
 DFMCtxMenuPlugin();
QList<QAction*> additionalMenu(const QStringList &files, const QString& currentDir) override;
};
#endif // DFMCTXMENUPLUGIN_H
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// dfmctxmenuplugin.cpp
#include "dfmctxmenuplugin.h"
DFMCtxMenuPlugin::DFMCtxMenuPlugin()
{
}
QList<QAction *> DFMCtxMenuPlugin::additionalMenu(const QStringList &files, const QString &currentDir)
{
 QList<QAction *> actionPtrList;
 actionPtrList.append(new QAction("Sample Text"));
 return actionPtrList;
}

将编译完成的动态库放置目录/usr/lib/$${QMAKE_HOST.arch}-linux-gnu/dde-file-manager/plugins/menu.

然后,杀死 dde-file-manager进程,并重启之便会加载我们的插件.

Json的方式(类似oem)

dde-file-manager < 5.0

⚠️ 此接口在5.0.3已经被移除,具体可查看dde-file-manager仓库的pluginmanager.cpp文件的commit记录。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
[
    {
        "MenuType": "EmptyArea",
        "Icon": "Files",
        "Text[zh_CN]": "在 VSCode 中打开",
        "Text[en_US]": "Open with VSCode",
        "Exec": "code",
        "Args": ["."],
        "NotShowIn": ["Desktop"],
    }
]

Demo

oem

DFMExtMenuPlugin

desktop

file-manager

无可演示的合适版本DFM.

Json

由于我没有低于5.0版本的dfm,因此此部分缺失。

目前选定方案-oem(.conf)

dde-file-manager 5.2.45

date: 2022年12月29日16:36:30

由于目前适配的dfm版本相对略低,因此插件动态库的方式并不支持,无奈只能采取oem的方式生成菜单了。

哦吼是一首歌。
Built with Hugo
Theme Stack designed by Jimmy