dde-control-center
dde-control-center
接口变更记录
时间 | 版本 | 说明 | 控制中心版本号 |
2022.4.13 | 1.0 | 创建 | 6.0.0.0 |
2022.6.1 | 1.1 | 1.接口增加版本号控制
2.增加布局类LayoutBase | 6.0.0.1+u013 |
2022.8.8 | 1.2 | 1.PluginInterface类里location返回值由int型改为QString,返回插件位置索引或前一个ModuleObject的name
2.去掉了ModuleObject类里的setChildType等函数。替代方案是用PageModule、VListModule、HListModule或其继承类
3.ModuleObject里的icon支持DDciIcon(DTK>5.6.0),icon接口兼容QIcon类型,同时可设置DDciIcon或QString。当为QString时,会在资源里查找对应图标
4.去掉了LayoutBase类,相关静态函数移到ModuleObject中,用PageModule等类替代其功能 | 6.0.3+u021 |
2022.10.8 | 1.3 | 1.修改hidden拼写错误
2.扩展添加一些ModuleObject类 | 6.0.3+u043 |
2022.12.12 | 1.4 | 1.规范插件IId名
2.扩展ModuleObject类不参与搜索接口 | 6.0.4.1+u039 |
V23控制中心新特性
- V23控制中心只负责框架设计,具体功能全部由插件实现
- V23控制中心支持多级插件系统,支持插件插入到任意位置中
- 更方便、更精确的搜索功能
- 高度可定制,可定制任意插件是否显示,若插件支持,可定制任意插件内容是否显示
V23控制中心插件安装路径必要说明
- 控制中心会自动加载翻译,翻译目录需要严格放置在
/${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATAROOTDIR}/dde-control-center/translations
下,控制中心会自动加载,同时,插件的翻译和名称也有要求,命名为${Plugin_name}_{locale}.ts
,locale 就是多语言的翻译,翻译文件必须控制和插件名称相同
- 控制中心的so应该放置在
/${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}/dde-control-center/modules
下,请使用构建系统的提供的gnuinstall路径,上面举的例子是cmake, mesonbuild也有自己的逻辑
V23控制中心插件开发必要说明
控制中心有一个option,可以用来加载一个文件夹下的插件,比如一般插件会放置到build
文件夹下,这时候可以
dde-control-center --spec ./build
来加载单独一个插件进行调试。另外提醒,调试时候不要使用asan,因为没有使用asan的控制中心无法加载使用了asan编译的插件
V23控制中心开发接口说明
- ModuleObject类用于构建每个页面元素,其是插件的核心
- PluginInterface类用于规范插件信息,每个插件必须提供一个ModuleObject对象。
ModuleObject基本信息说明
名称 | 数据类型 | 说明 |
name | QString | 名称,作为每个模块的唯一标识,**必须设置** |
displayName | QString | 显示名称,如菜单的名称,页面的标题等,为空则不显示 |
description | QString | 描述,如主菜单的描述信息 |
contentText | QStringList | 上下文数据,参与搜索,只可用于终结点 |
icon | QVariant | 图标,如主菜单的图标,可以为QIcon、DDciIcon、QString(会根据资源路径去找) |
badge | int | 主菜单中的角标, 默认为0不显示,大于0显示 |
基本信息都有对应的set函数,修改时有moduleDataChanged信号
ModuleObject接口说明
名称 | 说明 |
active() | 模块激活时被调用,可重写此方法实现后端数据获取 |
deactive() | 离开模块时被调用,可用于释放资源 |
page() | 终结点的模块必须实现此方法,用于显示页面,并且每次调用需new新的Widget 注意: page返回的widget生命周期只是对应窗口显示的时候,即模块切换时就会被析构。ModuleObject的生命周期是从控制中心启动到关闭 |
activePage(autoActive) | 激活并返回page,控制激活流程,不建议重写 |
名称 | 说明 |
trigger() | 触发该ModuleObject,该函数会触发triggered信号,框架收到信号会切换到该ModuleObject页 |
currentModule() | 当前激活的子项 |
setCurrentModule(child) | 设置当前激活项,由框架调用。子项变化时会触发currentModuleChanged信号 |
defultModule() | 默认激活的子项(如第二级激活时,会根据该值展开到第三级、第四级),如果返回为nullptr则不向下展开 |
isHidden() | 是否为隐藏,默认不隐藏 |
setHidden(hidden) | 设置为隐藏,对应ModuleObject隐藏应通过该函数设置,不要自行设置QWidget的隐藏 |
isDisabled() | 是否为禁用,默认为启用 |
setDisabled(disabled) | 设置为禁用,对应ModuleObject禁用应通过该函数设置,不要自行设置QWidget的禁用 |
findChild(child) | 查找子项,广度搜索优先,返回子项相对于当前模块所在的层级,-1为未找到,0为自己,>0为子项层级 |
hasChildrens() | 是否拥有子项 |
childrens() | 子项列表,由ModuleObject组成 |
getChildrenSize() | 获取子项列表大小 |
appendChild(child) | 添加子项 |
removeChild(child/index) | 删除子项 |
insertChild(before/index, child) | 插入子项 |
getFlagState/getFlag
/*setFlagState* | 处理状态标志,状态标志为uint32_t型,高16位(0xFFFF0000)为控制中心定义,低16位(0x0000FFFF)可由用户设置 |
extra/setExtra | 扩展标志,在VList和Page布局中放在最下面,横向排列 |
noSearch/setNoSearch | 是否参与搜索,默认参与搜索 |
名称 | 说明 |
moduleDataChanged() | 基本信息改变后发送此信号 |
displayNameChanged(const QString &displayName) | displayName改变后发送此信号 |
stateChanged(uint32_t flag, bool state) | 状态标志变化 |
childStateChanged(ModuleObject *const child, uint32_t flag, bool state) | 子项状态标志变化 |
removedChild(ModuleObject *const module) | 删除child前触发 |
insertedChild(ModuleObject *const module) | 插入child后触发 |
childrenSizeChanged(const int size) | childrens改变后必须发送此信号 |
triggered() | trigger触发该信号,框架收到信号会切换到该ModuleObject页 |
currentModuleChanged(ModuleObject *currentModule) | 当前激活的子项改变时会触发此信号 |
名称 | 说明 |
IsHidden | 返回module是否显示,判断了配置项和程序设置项 |
IsHiddenFlag | 判断标志是否为隐藏标志 |
IsDisabled | 返回module是否可用,判断了配置项和程序设置项 |
IsDisabledFlag | 判断标志是否为禁用标志 |
小提示:当模块无需实现虚函数时,可不用继承,直接设置其基本信息即可。
PluginInterface接口说明
名称 | 说明 |
module() | 模块对象,每个插件必须提供一个根模块,该模块管理插件中所有子项 |
name() | 插件名称,插件标识,需具有唯一性 |
follow() | 插件必须知道其需要跟随的父ModuleObject的url ,默认为空则为一级插件 |
location() | 插件位置索引或前一个ModuleObject的name,相同索引则按加载顺序进行排序,先加载的往后顺延,默认追加到最后 |
一个标准的插件开发流程:
- 继承PluginInterface,实现其虚函数。
- 实例化一个根模块,根模块在初始化时不允许有耗时操作,若有耗时操作,应继承ModuleObject然后实现active方法,将耗时操作放入其中。
- 若根模块的子项是横向菜单列表,则可使用List储存其基础信息,继承或使用HListModule类,然后循环使用appendChild方法将菜单添加到根模块中。
- 若根模块的子项是纵向菜单列表,则可使用List储存其基础信息,继承或使用VListModule类,然后循环使用appendChild方法将菜单添加到根模块中。
- 以此类推,具体的某个子项菜单同样再次添加菜单列表,直到菜单列表的子项为PageModule时为止。
- 准备一个以上的Module继承自ModuleObject,并实现其page()方法,然后添加到PageModule中,注意,page()方法中需返回新的QWidget对象。
- 当某个菜单为PageModule时,使用其appendChild方法将上方的Module添加到其子项中,此时,控制中心会根据page的大小添加滚动条,并将多个page进行垂直排列进行显示。PageModule持支嵌套,并且其有默认边距,如果嵌套使用,嵌套的PageModule边距建议设置为0( getContentsMargins(0, 0, 0, 0))
- 若某个VListModule或PageModule页面需要附加按钮时,可调其子项ModuleObject的setExtra,该ModuleObject的page提供按钮,这样该ModuleObject将显示在VListModule或PageModule页面的最下方。 注意:插件加载是在线程中进行的,在加载完成后会随ModuleObject移到主线程中。加载时(ModuleObject的构造函数中)创建的对象******必须******将ModuleObject设置为父对象,否则会导致没有父对象的对象不会被移到主线程中,其中的信号槽等不到对应的线程而一直不执行。
代码示例:
- 准备Page,LabelModule继承自ModuleObject
QWidget *LabelModule::page()
{
return new QLabel(text());
}
void LabelModule::setText(const QString &text)
{
m_text = text;
}
- 准备附加按钮,ButtonModule继承自ModuleObject
QWidget *ButtonModule::page()
{
QPushButton *button = new QPushButton(text());
button->setMaximumWidth(200);
connect(button, &QPushButton::clicked, this, &ButtonModule::onButtonClicked);
return button;
}
void ButtonModule::setText(const QString &text)
{
m_text = text;
}
- 实现PluginInterface接口
class Plugin : public PluginInterface
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "com.deepin.dde.ControlCenter.Plugin_test" FILE "plugin-test.json")
Q_INTERFACES(DCC_NAMESPACE::PluginInterface)
public:
virtual QString name() const override;
virtual ModuleObject *module() override;
};
QString Plugin::name() const
{
return QStringLiteral("plugin1");
}
ModuleObject *Test1Plugin::module()
{
return new Test1ModuleObject();
}
Test1ModuleObject::Test1ModuleObject()
: HListModule("firstmenu", tr("主菜单"), tr("我是主菜单"), DIconTheme::findQIcon("preferences-system"))
{
{
int i = 1;
ModuleObject *module = new PageModule(QString("menu%1").arg(i), tr("菜单%1").arg(i), this);
for (int j = 0; j < 5; j++) {
LabelModule *labelModule = new LabelModule(QString("main%1menu%2").arg(i).arg(j), QString("具体页面%1的第%2个page的标题").arg(i).arg(j), module);
labelModule->setText(QString("我是具体页面%1的第%2个page").arg(i).arg(j));
module->appendChild(labelModule);
}
appendChild(module);
}
{
int i = 2;
ModuleObject *module = new PageModule(QString("menu%1").arg(i), tr("菜单%1").arg(i), this);
for (int j = 0; j < 30; j++) {
ButtonModule *buttonModule = new ButtonModule(QString("main%1menu%2").arg(i).arg(j), QString("具体页面%1的第%2个page的标题").arg(i).arg(j), module);
buttonModule->setText(QString("我是具体页面%1的第%2个page").arg(i).arg(j));
module->appendChild(buttonModule);
}
appendChild(module);
}
{
int i = 4;
ModuleObject *module = new PageModule(QString("menu%1").arg(i), tr("菜单%1").arg(i), this);
for (int j = 0; j < 30; j++) {
ButtonModule *buttonModule = new ButtonModule(QString("main%1menu%2").arg(i).arg(j), QString("自定义布局页面%1的第%2个page的标题").arg(i).arg(j), module);
buttonModule->setText(QString("我是页面%1的第%2个按钮").arg(i).arg(j));
module->appendChild(buttonModule);
}
module->children(1)->setHidden(true);
module->children(2)->setDisabled(true);
appendChild(module);
}
VListModule *module = new VListModule(QString("menu%1").arg(5), tr("菜单%1").arg(5));
appendChild(module);
ModuleObject *lstModule1 = new PageModule(QString("menuSpeci1"), tr("特殊菜单1"), module);
module->appendChild(lstModule1);
LabelModule *labelModule1 = new LabelModule(QString("pageSpeci1"), QString("特殊页面1"), lstModule1);
labelModule1->setText("特殊页面内容1");
lstModule1->appendChild(labelModule1);
PageModule *lstModule2 = new PageModule("menuSpeci2", "特殊菜单2", module);
module->appendChild(lstModule2);
LabelModule *module2_1 = new LabelModule(QString("pageSpeci2"), QString("特殊页面2"), lstModule2);
module2_1->setText("特殊页面内容2");
lstModule2->appendChild(module2_1);
ButtonModule *module2_2 = new ButtonModule(QString("pageSpeci2"), QString("特殊页面2"), lstModule2);
module2_2->setText("Page中的测试按钮");
module2_2->setExtra();
lstModule2->appendChild(module2_2);
ButtonModule *ButtonModule3 = new ButtonModule(QString("pageSpeci3"), QString("特殊页面3"), lstModule2);
ButtonModule3->setText("测试按钮");
ButtonModule3->setExtra();
module->appendChild(ButtonModule3);
connect(ButtonModule3, &ButtonModule::onButtonClicked, ButtonModule3, &ModuleObject::triggered);
PageModule *page3 = new PageModule(QString("pageSpeci3"), QString("特殊页面3"), ButtonModule3);
ButtonModule3->appendChild(page3);
ButtonModule *module3_1 = new ButtonModule("testPage", "测试页面", module);
module3_1->setText("附加按钮测试页面");
page3->appendChild(module3_1);
ButtonModule *module3_2 = new ButtonModule("buttonClose", "关闭", module);
module3_2->setText("关闭");
module3_2->setExtra();
page3->appendChild(module3_2);
connect(module3_2, &ButtonModule::onButtonClicked, lstModule1, &ModuleObject::triggered);
}
- 二级插件与一级插件不同的是,需要实现其follow方法
QString Plugin::name() const
{
return QStringLiteral("plugin-test2");
}
ModuleObject* Plugin::module()
{
ModuleObject *moduleRoot = new ModuleObject("menu3", tr("菜单3"), tr("我是菜单3"), DIconTheme::findQIcon("preferences-system"), this);
moduleRoot->setChildType(ModuleObject::ChildType::Page);
for (int j = 0; j < 4; j++) {
LabelModule *labelModule = new LabelModule(QString("main%1menu%2").arg(3).arg(j), QString("具体页面%1的第%2个page的标题").arg(3).arg(j), moduleRoot);
labelModule->setText(QString("我是具体页面%1的第%2个page").arg(3).arg(j));
moduleRoot->appendChild(labelModule);
}
return moduleRoot;
}
QString Plugin::follow() const
{
return QStringLiteral("firstmenu");
}
QString Plugin::location() const
{
return "2";
}
- 自定义布局 要实现类似PageModule的自定义布局,需继承ModuleObject类实现其page函数
#include "interface/moduleobject.h"
class QFormLayout;
class QScrollArea;
class FormModule : public DCC_NAMESPACE::ModuleObject
{
Q_OBJECT
public:
explicit FormModule(const QString &name, const QString &displayName = {}, QObject *parent = nullptr);
QWidget *page() override;
private Q_SLOTS:
void onCurrentModuleChanged(ModuleObject *child);
private:
void onAddChild(DCC_NAMESPACE::ModuleObject *const childModule);
void onRemoveChild(DCC_NAMESPACE::ModuleObject *const childModule);
void clearData();
private:
QMap<ModuleObject *, QWidget *> m_mapWidget;
QScrollArea *m_area;
QFormLayout *m_layout;
};
FormModule::FormModule(const QString &name, const QString &displayName, QObject *parent)
: ModuleObject(name, displayName, parent)
, m_area(nullptr)
, m_layout(nullptr)
{
connect(this, &FormModule::currentModuleChanged, this, &FormModule::onCurrentModuleChanged);
}
QWidget *FormModule::page()
{
QWidget *parentWidget = new QWidget();
QVBoxLayout *mainLayout = new QVBoxLayout;
mainLayout->setContentsMargins(0, 0, 0, 0);
parentWidget->setLayout(mainLayout);
m_layout = new QFormLayout();
connect(parentWidget, &QObject::destroyed, this, [this]() { clearData(); });
QWidget *areaWidget = new QWidget();
m_area = new QScrollArea(parentWidget);
m_area->setFrameShape(QFrame::NoFrame);
m_area->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_area->setWidgetResizable(true);
areaWidget->setParent(m_area);
m_area->setWidget(areaWidget);
mainLayout->addWidget(m_area);
areaWidget->setLayout(m_layout);
for (auto &&tmpChild : childrens()) {
auto page = tmpChild->activePage();
if (page) {
m_layout->addRow(tmpChild->displayName(), page);
m_mapWidget.insert(tmpChild, page);
}
}
auto addModuleSlot = [this](ModuleObject *const tmpChild) {
onAddChild(tmpChild);
};
connect(this, &ModuleObject::insertedChild, areaWidget, addModuleSlot);
connect(this, &ModuleObject::appendedChild, areaWidget, addModuleSlot);
connect(this, &ModuleObject::removedChild, areaWidget, [this](ModuleObject *const childModule) { onRemoveChild(childModule); });
connect(this, &ModuleObject::childStateChanged, areaWidget, [this](ModuleObject *const tmpChild, uint32_t flag, bool state) {
if (ModuleObject::IsHiddenFlag(flag)) {
if (state)
onRemoveChild(tmpChild);
else
onAddChild(tmpChild);
}
});
onCurrentModuleChanged(currentModule());
return parentWidget;
}
void FormModule::onCurrentModuleChanged(dccV23::ModuleObject *child)
{
QTimer::singleShot(10, m_area, [this, child]() {
if (m_area && m_mapWidget.contains(child)) {
QWidget *w = m_mapWidget.value(child);
if (-1 != m_layout->indexOf(w)) {
QPoint p = w->mapTo(w->parentWidget(), QPoint());
m_area->verticalScrollBar()->setSliderPosition(p.y());
}
}
});
}
void FormModule::onAddChild(dccV23::ModuleObject *const childModule)
{
if (ModuleObject::IsHidden(childModule) || m_mapWidget.contains(childModule))
return;
int index = 0;
for (auto &&child : childrens()) {
if (child == childModule)
break;
if (!ModuleObject::IsHidden(child))
index++;
}
auto newPage = childModule->activePage();
if (newPage) {
m_layout->insertRow(index, childModule->displayName(), newPage);
m_mapWidget.insert(childModule, newPage);
}
}
void FormModule::onRemoveChild(dccV23::ModuleObject *const childModule)
{
if (m_mapWidget.contains(childModule)) {
QWidget *w = m_mapWidget.value(childModule);
int index = m_layout->indexOf(w);
if (-1 != index) {
w->deleteLater();
delete m_layout->takeAt(index);
m_mapWidget.remove(childModule);
return;
}
}
}
void FormModule::clearData()
{
m_layout = nullptr;
m_area = nullptr;
m_mapWidget.clear();
}
- 扩展ModuleObject 为了方便开发,**dcc-widgets里提供了一些实用的ModuleObject**,具体可以参考对应的头文件
void Test1ModuleObject::addTestModule(ModuleObject *parent)
{
ItemModule *item = new ItemModule("item", tr("Title"));
item->setRightWidget([](ModuleObject *item) {
return new QPushButton(Dtk::Widget::DStyle::standardIcon(qApp->style(), Dtk::Widget::DStyle::SP_EditElement), "");
});
parent->appendChild(item);
ItemModule *itemButton = new ItemModule("itemButton", tr("Button:"), false);
itemButton->setRightWidget(this, &Test1ModuleObject::initButton);
itemButton->setBackground(true);
parent->appendChild(itemButton);
SettingsGroupModule *groupModule = new SettingsGroupModule("group", tr("group Module"));
groupModule->appendChild(new ItemModule("groupItem1", tr("group PushButton"), [](ModuleObject *module) { return new QPushButton(); }));
groupModule->appendChild(new ItemModule("groupItem2", tr("group LineEdit"), [](ModuleObject *module) { return new QLineEdit(); }));
groupModule->appendChild(new ItemModule("groupItem3", tr("group ComboBox"), [](ModuleObject *module) { return new QComboBox(); }));
parent->appendChild(groupModule);
HorizontalModule *hor = new HorizontalModule("hor", tr("Horizontal Module"));
hor->setStretchType(HorizontalModule::AllStretch);
parent->appendChild(hor);
ItemModule *hlabel = new ItemModule(
"hlabel", tr("Horizontal Edit"), [](ModuleObject *module) {
QLabel *label = new QLabel(module->displayName());
connect(module, &ModuleObject::displayNameChanged, label, &QLabel::setText);
return label;
},
false);
connect(hlabel, &ModuleObject::displayNameChanged, hlabel, [hlabel]() {
hlabel->setHidden(false);
});
ItemModule *hedit = new ItemModule(
"hedit", tr("Horizontal Edit"), [hlabel](ModuleObject *module) {
QLineEdit *edit = new QLineEdit(module->displayName());
edit->setFixedHeight(32);
connect(module, &ModuleObject::displayNameChanged, edit, &QLineEdit::setText);
connect(edit, &QLineEdit::editingFinished, [edit, hlabel, module]() {
QString text = edit->text();
if (!text.isEmpty()) {
hlabel->setDisplayName(text);
module->setDisplayName(text);
}
module->setHidden(true);
});
return edit;
},
false);
hedit->setHidden(true);
ItemModule *hbutton = new ItemModule(
"hbutton", tr("Horizontal QPushButton"), [hlabel, hedit](ModuleObject *module) {
QPushButton *but = new QPushButton();
but->setIcon(Dtk::Widget::DStyle::standardIcon(qApp->style(), Dtk::Widget::DStyle::SP_EditElement));
but->setFixedSize(32, 32);
connect(but, &QPushButton::clicked, module, [hlabel, hedit, module]() {
hlabel->setHidden(true);
hedit->setHidden(false);
module->setHidden(true);
});
return but;
},
false);
connect(hedit, &ModuleObject::stateChanged, hedit, [hlabel, hbutton, hedit]() {
hlabel->setHidden(!hedit->isHidden());
hbutton->setHidden(!hedit->isHidden());
});
hor->appendChild(hlabel);
hor->appendChild(hedit);
hor->appendChild(hbutton);
ListViewModule *listmodule = new ListViewModule("listView", tr("List View"));
connect(listmodule, &ListViewModule::clicked, this, [](ModuleObject *module) {
qInfo() << __FILE__ << __LINE__ << "clicked:" << module->displayName();
QString display = module->displayName();
if (display.contains("click")) {
display.remove("click");
module->setDisplayName(display);
} else {
module->setDisplayName(display + "click");
}
});
parent->appendChild(listmodule);
ModuleObjectItem *listitem0 = new ModuleObjectItem("item0", tr("listitem 0"));
listmodule->appendChild(listitem0);
ModuleObjectItem *item1 = new ModuleObjectItem("item1", tr("listitem 1"));
item1->setRightIcon(Dtk::Widget::DStyle::SP_ArrowEnter);
listmodule->appendChild(item1);
ModuleObjectItem *item2 = new ModuleObjectItem("item2", tr("listitem 2"));
item2->setRightText("right Text", -2);
item2->setRightIcon(Dtk::Widget::DStyle::SP_ArrowEnter);
listmodule->appendChild(item2);
connect(item2, &ModuleObjectItem::clicked, this, [item1]() {
qInfo() << __FILE__ << __LINE__ << "clicked ModuleObjectItem:" << tr("listitem 2");
item1->setHidden(!item1->isHidden());
static int pix = Dtk::Widget::DStyle::SP_ForkElement;
item1->setRightIcon((Dtk::Widget::DStyle::StandardPixmap)(pix));
pix++;
if (pix >= (int)(Dtk::Widget::DStyle::SP_Title_SS_ShowNormalButton))
pix = Dtk::Widget::DStyle::SP_ForkElement;
});
ModuleObject *listitem03 = new ModuleObject("listitem03", tr("listitem 3"));
listmodule->appendChild(listitem03);
}
Debug 说明
控制中心有一个参数 --spec
,这个参数接受一个path的变量,用于加载当前文件夹下所有插件,控制中心此时为一个runtime。如果是加载一个子插件,需要将父插件软连接到这个该目录,之后可以调试。
CMake
控制中心导出两个target Dde::DCCWidget
和Dde::DCCInterface
,在find_package(DdeControlCenter)
后直接在target_link_libraries
中连接这两个target就可以使用控制中心的库和头文件了。