852 lines
34 KiB
C++
852 lines
34 KiB
C++
|
|
#include "configeditorwidget.h"
|
||
|
|
|
||
|
|
#include "robotclient.h"
|
||
|
|
|
||
|
|
#include "cmvr/config/device_manager_config/device_manager_config.pb.h"
|
||
|
|
#include "cmvr/config/task_manager_config/task_manager_config.pb.h"
|
||
|
|
|
||
|
|
#include <google/protobuf/descriptor.h>
|
||
|
|
#include <google/protobuf/dynamic_message.h>
|
||
|
|
#include <google/protobuf/message.h>
|
||
|
|
#include <google/protobuf/reflection.h>
|
||
|
|
#include <google/protobuf/text_format.h>
|
||
|
|
|
||
|
|
#include <QCheckBox>
|
||
|
|
#include <QDateTime>
|
||
|
|
#include <QFormLayout>
|
||
|
|
#include <QFontDatabase>
|
||
|
|
#include <QHBoxLayout>
|
||
|
|
#include <QHeaderView>
|
||
|
|
#include <QLabel>
|
||
|
|
#include <QLineEdit>
|
||
|
|
#include <QListWidget>
|
||
|
|
#include <QMessageBox>
|
||
|
|
#include <QPlainTextEdit>
|
||
|
|
#include <QPushButton>
|
||
|
|
#include <QRegularExpression>
|
||
|
|
#include <QSettings>
|
||
|
|
#include <QSignalBlocker>
|
||
|
|
#include <QSpinBox>
|
||
|
|
#include <QSplitter>
|
||
|
|
#include <QTabWidget>
|
||
|
|
#include <QTableWidget>
|
||
|
|
#include <QTimer>
|
||
|
|
#include <QTextCursor>
|
||
|
|
#include <QTreeWidget>
|
||
|
|
#include <QTreeWidgetItem>
|
||
|
|
#include <QVBoxLayout>
|
||
|
|
|
||
|
|
#include <algorithm>
|
||
|
|
#include <cstdint>
|
||
|
|
|
||
|
|
namespace {
|
||
|
|
constexpr int kPathRole = Qt::UserRole;
|
||
|
|
constexpr int kMessageRole = Qt::UserRole + 1;
|
||
|
|
constexpr int kFieldRole = Qt::UserRole + 2;
|
||
|
|
constexpr int kIndexRole = Qt::UserRole + 3;
|
||
|
|
constexpr int kPreviousValueRole = Qt::UserRole + 4;
|
||
|
|
|
||
|
|
QTableWidgetItem *readOnlyItem(const QString &text)
|
||
|
|
{
|
||
|
|
auto *item = new QTableWidgetItem(text);
|
||
|
|
item->setFlags(item->flags() & ~Qt::ItemIsEditable);
|
||
|
|
return item;
|
||
|
|
}
|
||
|
|
|
||
|
|
quintptr pointerValue(const QVariant &value)
|
||
|
|
{
|
||
|
|
return static_cast<quintptr>(value.toULongLong());
|
||
|
|
}
|
||
|
|
|
||
|
|
QString fieldTypeName(const google::protobuf::FieldDescriptor *field)
|
||
|
|
{
|
||
|
|
using Field = google::protobuf::FieldDescriptor;
|
||
|
|
if (field->is_repeated()) {
|
||
|
|
if (field->cpp_type() == Field::CPPTYPE_MESSAGE)
|
||
|
|
return QStringLiteral("message[]");
|
||
|
|
return QString::fromLatin1(field->type_name()) + QStringLiteral("[]");
|
||
|
|
}
|
||
|
|
return QString::fromLatin1(field->type_name());
|
||
|
|
}
|
||
|
|
|
||
|
|
QString enumValueName(const google::protobuf::EnumValueDescriptor *value)
|
||
|
|
{
|
||
|
|
return value ? QString::fromStdString(value->name()) : QString();
|
||
|
|
}
|
||
|
|
|
||
|
|
QString scalarValue(const google::protobuf::Message *message,
|
||
|
|
const google::protobuf::FieldDescriptor *field,
|
||
|
|
int index)
|
||
|
|
{
|
||
|
|
const auto *reflection = message->GetReflection();
|
||
|
|
const bool repeated = field->is_repeated();
|
||
|
|
using Field = google::protobuf::FieldDescriptor;
|
||
|
|
switch (field->cpp_type()) {
|
||
|
|
case Field::CPPTYPE_BOOL:
|
||
|
|
return (repeated ? reflection->GetRepeatedBool(*message, field, index)
|
||
|
|
: reflection->GetBool(*message, field))
|
||
|
|
? QStringLiteral("true") : QStringLiteral("false");
|
||
|
|
case Field::CPPTYPE_INT32:
|
||
|
|
return QString::number(repeated ? reflection->GetRepeatedInt32(*message, field, index)
|
||
|
|
: reflection->GetInt32(*message, field));
|
||
|
|
case Field::CPPTYPE_INT64:
|
||
|
|
return QString::number(static_cast<qlonglong>(
|
||
|
|
repeated ? reflection->GetRepeatedInt64(*message, field, index)
|
||
|
|
: reflection->GetInt64(*message, field)));
|
||
|
|
case Field::CPPTYPE_UINT32:
|
||
|
|
return QString::number(repeated ? reflection->GetRepeatedUInt32(*message, field, index)
|
||
|
|
: reflection->GetUInt32(*message, field));
|
||
|
|
case Field::CPPTYPE_UINT64:
|
||
|
|
return QString::number(static_cast<qulonglong>(
|
||
|
|
repeated ? reflection->GetRepeatedUInt64(*message, field, index)
|
||
|
|
: reflection->GetUInt64(*message, field)));
|
||
|
|
case Field::CPPTYPE_FLOAT:
|
||
|
|
return QString::number(repeated ? reflection->GetRepeatedFloat(*message, field, index)
|
||
|
|
: reflection->GetFloat(*message, field), 'g', 9);
|
||
|
|
case Field::CPPTYPE_DOUBLE:
|
||
|
|
return QString::number(repeated ? reflection->GetRepeatedDouble(*message, field, index)
|
||
|
|
: reflection->GetDouble(*message, field), 'g', 17);
|
||
|
|
case Field::CPPTYPE_STRING:
|
||
|
|
return QString::fromUtf8((repeated ? reflection->GetRepeatedString(*message, field, index)
|
||
|
|
: reflection->GetString(*message, field)).data());
|
||
|
|
case Field::CPPTYPE_ENUM: {
|
||
|
|
const auto *value = repeated
|
||
|
|
? reflection->GetRepeatedEnum(*message, field, index)
|
||
|
|
: reflection->GetEnum(*message, field);
|
||
|
|
return enumValueName(value);
|
||
|
|
}
|
||
|
|
case Field::CPPTYPE_MESSAGE:
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
return QString();
|
||
|
|
}
|
||
|
|
|
||
|
|
void setItemMetadata(QTreeWidgetItem *item, google::protobuf::Message *message,
|
||
|
|
const google::protobuf::FieldDescriptor *field, int index,
|
||
|
|
const QString &value)
|
||
|
|
{
|
||
|
|
item->setData(1, kMessageRole,
|
||
|
|
QVariant::fromValue<qulonglong>(reinterpret_cast<quintptr>(message)));
|
||
|
|
item->setData(1, kFieldRole,
|
||
|
|
QVariant::fromValue<qulonglong>(reinterpret_cast<quintptr>(field)));
|
||
|
|
item->setData(1, kIndexRole, index);
|
||
|
|
item->setData(1, kPreviousValueRole, value);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
ConfigEditorWidget::~ConfigEditorWidget() = default;
|
||
|
|
|
||
|
|
ConfigEditorWidget::ConfigEditorWidget(RobotClient *client, QWidget *parent)
|
||
|
|
: QWidget(parent), m_client(client)
|
||
|
|
{
|
||
|
|
auto *layout = new QVBoxLayout(this);
|
||
|
|
layout->setContentsMargins(24, 18, 24, 18);
|
||
|
|
layout->setSpacing(12);
|
||
|
|
|
||
|
|
auto *connectionBar = new QWidget(this);
|
||
|
|
connectionBar->setObjectName(QStringLiteral("panel"));
|
||
|
|
auto *connectionLayout = new QHBoxLayout(connectionBar);
|
||
|
|
connectionLayout->setContentsMargins(16, 12, 16, 12);
|
||
|
|
connectionLayout->setSpacing(8);
|
||
|
|
auto *hostLabel = new QLabel(QStringLiteral("配置服务"), connectionBar);
|
||
|
|
hostLabel->setObjectName(QStringLiteral("sectionTitle"));
|
||
|
|
connectionLayout->addWidget(hostLabel);
|
||
|
|
m_hostEdit = new QLineEdit(connectionBar);
|
||
|
|
m_hostEdit->setPlaceholderText(QStringLiteral("设备地址"));
|
||
|
|
m_hostEdit->setMinimumWidth(180);
|
||
|
|
m_portSpin = new QSpinBox(connectionBar);
|
||
|
|
m_portSpin->setRange(1, 65535);
|
||
|
|
m_portSpin->setButtonSymbols(QAbstractSpinBox::NoButtons);
|
||
|
|
m_portSpin->setFixedWidth(76);
|
||
|
|
QSettings settings;
|
||
|
|
m_hostEdit->setText(settings.value(QStringLiteral("configuration/host"),
|
||
|
|
QStringLiteral("192.168.28.248")).toString());
|
||
|
|
const int savedConfigurationPort = settings.value(
|
||
|
|
QStringLiteral("configuration/port"), 50053).toInt();
|
||
|
|
m_portSpin->setValue(savedConfigurationPort == 50052 ? 50053 : savedConfigurationPort);
|
||
|
|
connectionLayout->addWidget(m_hostEdit, 1);
|
||
|
|
connectionLayout->addWidget(m_portSpin);
|
||
|
|
m_connectButton = new QPushButton(QStringLiteral("连接"), connectionBar);
|
||
|
|
m_connectButton->setObjectName(QStringLiteral("primaryButton"));
|
||
|
|
m_refreshButton = new QPushButton(QStringLiteral("刷新"), connectionBar);
|
||
|
|
m_refreshButton->setEnabled(false);
|
||
|
|
connectionLayout->addWidget(m_connectButton);
|
||
|
|
connectionLayout->addWidget(m_refreshButton);
|
||
|
|
layout->addWidget(connectionBar);
|
||
|
|
|
||
|
|
auto *splitter = new QSplitter(Qt::Horizontal, this);
|
||
|
|
splitter->setChildrenCollapsible(false);
|
||
|
|
m_fileList = new QListWidget(splitter);
|
||
|
|
m_fileList->setMinimumWidth(260);
|
||
|
|
m_fileList->setMaximumWidth(380);
|
||
|
|
m_fileList->setAlternatingRowColors(true);
|
||
|
|
|
||
|
|
auto *editorPanel = new QWidget(splitter);
|
||
|
|
auto *editorLayout = new QVBoxLayout(editorPanel);
|
||
|
|
editorLayout->setContentsMargins(12, 0, 0, 0);
|
||
|
|
editorLayout->setSpacing(8);
|
||
|
|
m_rootLabel = new QLabel(QStringLiteral("远端根目录:--"), editorPanel);
|
||
|
|
m_rootLabel->setObjectName(QStringLiteral("mutedLabel"));
|
||
|
|
m_rootLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||
|
|
m_pathLabel = new QLabel(QStringLiteral("未选择配置文件"), editorPanel);
|
||
|
|
m_pathLabel->setObjectName(QStringLiteral("sectionTitle"));
|
||
|
|
m_pathLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||
|
|
m_typeLabel = new QLabel(QStringLiteral("--"), editorPanel);
|
||
|
|
m_typeLabel->setObjectName(QStringLiteral("mutedLabel"));
|
||
|
|
editorLayout->addWidget(m_rootLabel);
|
||
|
|
editorLayout->addWidget(m_pathLabel);
|
||
|
|
editorLayout->addWidget(m_typeLabel);
|
||
|
|
|
||
|
|
m_editorTabs = new QTabWidget(editorPanel);
|
||
|
|
m_textEditor = new QPlainTextEdit(m_editorTabs);
|
||
|
|
m_textEditor->setLineWrapMode(QPlainTextEdit::NoWrap);
|
||
|
|
m_textEditor->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
|
||
|
|
m_textEditor->setPlaceholderText(QStringLiteral("选择左侧配置文件后在此编辑"));
|
||
|
|
m_textEditor->setEnabled(false);
|
||
|
|
m_editorTabs->addTab(m_textEditor, QStringLiteral("原始配置"));
|
||
|
|
m_fieldTree = new QTreeWidget(m_editorTabs);
|
||
|
|
m_fieldTree->setColumnCount(3);
|
||
|
|
m_fieldTree->setHeaderLabels({QStringLiteral("字段名"), QStringLiteral("值"), QStringLiteral("类型")});
|
||
|
|
m_fieldTree->setAlternatingRowColors(true);
|
||
|
|
m_fieldTree->setRootIsDecorated(true);
|
||
|
|
m_fieldTree->setUniformRowHeights(true);
|
||
|
|
m_fieldTree->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
|
||
|
|
m_fieldTree->header()->setSectionResizeMode(1, QHeaderView::Stretch);
|
||
|
|
m_fieldTree->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
|
||
|
|
m_fieldTree->setEditTriggers(QAbstractItemView::DoubleClicked |
|
||
|
|
QAbstractItemView::EditKeyPressed |
|
||
|
|
QAbstractItemView::SelectedClicked);
|
||
|
|
m_editorTabs->addTab(m_fieldTree, QStringLiteral("字段编辑"));
|
||
|
|
m_enableTable = new QTableWidget(m_editorTabs);
|
||
|
|
m_enableTable->setColumnCount(4);
|
||
|
|
m_enableTable->setHorizontalHeaderLabels(
|
||
|
|
{QStringLiteral("ID"), QStringLiteral("类型"), QStringLiteral("配置文件"),
|
||
|
|
QStringLiteral("启用")});
|
||
|
|
m_enableTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
|
||
|
|
m_enableTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
|
||
|
|
m_enableTable->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Stretch);
|
||
|
|
m_enableTable->horizontalHeader()->setSectionResizeMode(3, QHeaderView::ResizeToContents);
|
||
|
|
m_enableTable->verticalHeader()->setVisible(false);
|
||
|
|
m_enableTable->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||
|
|
m_enableTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||
|
|
m_editorTabs->addTab(m_enableTable, QStringLiteral("启用项"));
|
||
|
|
m_editorTabs->setTabEnabled(2, false);
|
||
|
|
editorLayout->addWidget(m_editorTabs, 1);
|
||
|
|
|
||
|
|
auto *actionLayout = new QHBoxLayout;
|
||
|
|
m_statusLabel = new QLabel(QStringLiteral("未连接配置服务"), editorPanel);
|
||
|
|
m_statusLabel->setObjectName(QStringLiteral("mutedLabel"));
|
||
|
|
m_statusLabel->setWordWrap(true);
|
||
|
|
actionLayout->addWidget(m_statusLabel, 1);
|
||
|
|
m_reloadButton = new QPushButton(QStringLiteral("重新加载"), editorPanel);
|
||
|
|
m_validateButton = new QPushButton(QStringLiteral("校验"), editorPanel);
|
||
|
|
m_saveButton = new QPushButton(QStringLiteral("保存到设备"), editorPanel);
|
||
|
|
m_saveButton->setObjectName(QStringLiteral("primaryButton"));
|
||
|
|
actionLayout->addWidget(m_reloadButton);
|
||
|
|
actionLayout->addWidget(m_validateButton);
|
||
|
|
actionLayout->addWidget(m_saveButton);
|
||
|
|
editorLayout->addLayout(actionLayout);
|
||
|
|
splitter->addWidget(m_fileList);
|
||
|
|
splitter->addWidget(editorPanel);
|
||
|
|
splitter->setStretchFactor(0, 0);
|
||
|
|
splitter->setStretchFactor(1, 1);
|
||
|
|
layout->addWidget(splitter, 1);
|
||
|
|
|
||
|
|
m_parseTimer = new QTimer(this);
|
||
|
|
m_parseTimer->setSingleShot(true);
|
||
|
|
m_parseTimer->setInterval(250);
|
||
|
|
connect(m_parseTimer, &QTimer::timeout, this, &ConfigEditorWidget::rebuildEnableTable);
|
||
|
|
connect(m_connectButton, &QPushButton::clicked, this, &ConfigEditorWidget::toggleConnection);
|
||
|
|
connect(m_refreshButton, &QPushButton::clicked, this, &ConfigEditorWidget::refreshFiles);
|
||
|
|
connect(m_fileList, &QListWidget::currentItemChanged,
|
||
|
|
this, &ConfigEditorWidget::selectFile);
|
||
|
|
connect(m_reloadButton, &QPushButton::clicked, this, &ConfigEditorWidget::reloadCurrentFile);
|
||
|
|
connect(m_validateButton, &QPushButton::clicked, this, &ConfigEditorWidget::validateCurrentFile);
|
||
|
|
connect(m_saveButton, &QPushButton::clicked, this, &ConfigEditorWidget::saveCurrentFile);
|
||
|
|
connect(m_textEditor, &QPlainTextEdit::textChanged,
|
||
|
|
this, &ConfigEditorWidget::handleEditorChanged);
|
||
|
|
connect(m_fieldTree, &QTreeWidget::itemChanged,
|
||
|
|
this, &ConfigEditorWidget::handleFieldItemChanged);
|
||
|
|
connect(m_client, &RobotClient::configurationConnectionChanged,
|
||
|
|
this, &ConfigEditorWidget::handleConnectionChanged);
|
||
|
|
connect(m_client, &RobotClient::configurationFilesReceived,
|
||
|
|
this, &ConfigEditorWidget::handleFilesReceived);
|
||
|
|
connect(m_client, &RobotClient::configurationFileReceived,
|
||
|
|
this, &ConfigEditorWidget::handleFileReceived);
|
||
|
|
connect(m_client, &RobotClient::configurationValidationFinished,
|
||
|
|
this, &ConfigEditorWidget::handleValidationFinished);
|
||
|
|
connect(m_client, &RobotClient::configurationSaveFinished,
|
||
|
|
this, &ConfigEditorWidget::handleSaveFinished);
|
||
|
|
updateActions();
|
||
|
|
}
|
||
|
|
|
||
|
|
void ConfigEditorWidget::toggleConnection()
|
||
|
|
{
|
||
|
|
if (m_connected) {
|
||
|
|
if (!confirmDiscardChanges())
|
||
|
|
return;
|
||
|
|
m_client->disconnectConfigurationServer();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (m_hostEdit->text().trimmed().isEmpty()) {
|
||
|
|
setStatus(QStringLiteral("设备地址不能为空"), true);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
QSettings settings;
|
||
|
|
settings.setValue(QStringLiteral("configuration/host"), m_hostEdit->text().trimmed());
|
||
|
|
settings.setValue(QStringLiteral("configuration/port"), m_portSpin->value());
|
||
|
|
setBusy(true);
|
||
|
|
setStatus(QStringLiteral("正在连接配置服务..."));
|
||
|
|
m_client->connectConfigurationServer(m_hostEdit->text(), m_portSpin->value());
|
||
|
|
}
|
||
|
|
|
||
|
|
void ConfigEditorWidget::refreshFiles()
|
||
|
|
{
|
||
|
|
if (!m_connected || !confirmDiscardChanges())
|
||
|
|
return;
|
||
|
|
setBusy(true);
|
||
|
|
setStatus(QStringLiteral("正在读取远端配置列表..."));
|
||
|
|
m_client->requestConfigurationFiles();
|
||
|
|
}
|
||
|
|
|
||
|
|
void ConfigEditorWidget::selectFile(QListWidgetItem *current, QListWidgetItem *previous)
|
||
|
|
{
|
||
|
|
if (!current)
|
||
|
|
return;
|
||
|
|
const QString nextPath = current->data(kPathRole).toString();
|
||
|
|
if (nextPath == m_currentPath)
|
||
|
|
return;
|
||
|
|
if (!confirmDiscardChanges()) {
|
||
|
|
const QSignalBlocker blocker(m_fileList);
|
||
|
|
m_fileList->setCurrentItem(previous);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
setBusy(true);
|
||
|
|
setStatus(QStringLiteral("正在读取 %1...").arg(nextPath));
|
||
|
|
m_client->readConfigurationFile(nextPath);
|
||
|
|
}
|
||
|
|
|
||
|
|
void ConfigEditorWidget::reloadCurrentFile()
|
||
|
|
{
|
||
|
|
if (m_currentPath.isEmpty() || !confirmDiscardChanges())
|
||
|
|
return;
|
||
|
|
setBusy(true);
|
||
|
|
m_client->readConfigurationFile(m_currentPath);
|
||
|
|
}
|
||
|
|
|
||
|
|
void ConfigEditorWidget::validateCurrentFile()
|
||
|
|
{
|
||
|
|
if (m_currentPath.isEmpty())
|
||
|
|
return;
|
||
|
|
setBusy(true);
|
||
|
|
setStatus(QStringLiteral("正在校验 %1...").arg(m_currentPath));
|
||
|
|
m_client->validateConfigurationFile(m_currentPath, m_textEditor->toPlainText());
|
||
|
|
}
|
||
|
|
|
||
|
|
void ConfigEditorWidget::saveCurrentFile()
|
||
|
|
{
|
||
|
|
if (m_currentPath.isEmpty() || m_revision.isEmpty())
|
||
|
|
return;
|
||
|
|
setBusy(true);
|
||
|
|
setStatus(QStringLiteral("正在保存 %1...").arg(m_currentPath));
|
||
|
|
m_client->saveConfigurationFile(m_currentPath, m_textEditor->toPlainText(), m_revision);
|
||
|
|
}
|
||
|
|
|
||
|
|
void ConfigEditorWidget::handleConnectionChanged(bool connected, const QString &detail)
|
||
|
|
{
|
||
|
|
m_connected = connected;
|
||
|
|
setBusy(false);
|
||
|
|
m_hostEdit->setEnabled(!connected);
|
||
|
|
m_portSpin->setEnabled(!connected);
|
||
|
|
m_connectButton->setText(connected ? QStringLiteral("断开") : QStringLiteral("连接"));
|
||
|
|
setStatus(detail, !connected && detail != QStringLiteral("配置服务已断开"));
|
||
|
|
if (connected)
|
||
|
|
refreshFiles();
|
||
|
|
else {
|
||
|
|
m_fileList->clear();
|
||
|
|
m_rootLabel->setText(QStringLiteral("远端根目录:--"));
|
||
|
|
clearCurrentFile();
|
||
|
|
}
|
||
|
|
updateActions();
|
||
|
|
}
|
||
|
|
|
||
|
|
void ConfigEditorWidget::handleFilesReceived(const QString &configRoot,
|
||
|
|
const QVariantList &files,
|
||
|
|
bool success, const QString &detail)
|
||
|
|
{
|
||
|
|
setBusy(false);
|
||
|
|
if (!success) {
|
||
|
|
setStatus(detail.isEmpty() ? QStringLiteral("读取配置列表失败") : detail, true);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const QSignalBlocker blocker(m_fileList);
|
||
|
|
m_fileList->clear();
|
||
|
|
clearCurrentFile();
|
||
|
|
for (const QVariant &value : files) {
|
||
|
|
const QVariantMap file = value.toMap();
|
||
|
|
const QString path = file.value(QStringLiteral("relativePath")).toString();
|
||
|
|
auto *item = new QListWidgetItem(path, m_fileList);
|
||
|
|
item->setData(kPathRole, path);
|
||
|
|
item->setToolTip(QStringLiteral("%1\n%2 bytes\n%3")
|
||
|
|
.arg(file.value(QStringLiteral("messageType")).toString())
|
||
|
|
.arg(file.value(QStringLiteral("sizeBytes")).toULongLong())
|
||
|
|
.arg(QDateTime::fromMSecsSinceEpoch(
|
||
|
|
file.value(QStringLiteral("modifiedUnixMs")).toLongLong()).toString(Qt::ISODate)));
|
||
|
|
}
|
||
|
|
m_rootLabel->setText(QStringLiteral("远端根目录:%1").arg(configRoot));
|
||
|
|
setStatus(QStringLiteral("已读取 %1 个配置文件").arg(files.size()));
|
||
|
|
updateActions();
|
||
|
|
}
|
||
|
|
|
||
|
|
void ConfigEditorWidget::handleFileReceived(const QString &relativePath,
|
||
|
|
const QString &content,
|
||
|
|
const QString &revision,
|
||
|
|
const QString &messageType,
|
||
|
|
bool success, const QString &detail)
|
||
|
|
{
|
||
|
|
setBusy(false);
|
||
|
|
if (!success) {
|
||
|
|
setStatus(detail.isEmpty() ? QStringLiteral("读取配置文件失败") : detail, true);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
m_loading = true;
|
||
|
|
m_currentPath = relativePath;
|
||
|
|
m_revision = revision;
|
||
|
|
m_messageType = messageType;
|
||
|
|
m_textEditor->setPlainText(content);
|
||
|
|
m_textEditor->document()->setModified(false);
|
||
|
|
m_loading = false;
|
||
|
|
m_dirty = false;
|
||
|
|
m_pathLabel->setText(relativePath);
|
||
|
|
m_typeLabel->setText(QStringLiteral("消息类型:%1 版本:%2")
|
||
|
|
.arg(messageType, revision));
|
||
|
|
rebuildFieldTree();
|
||
|
|
rebuildEnableTable();
|
||
|
|
setStatus(QStringLiteral("配置已加载"));
|
||
|
|
updateActions();
|
||
|
|
}
|
||
|
|
|
||
|
|
void ConfigEditorWidget::handleValidationFinished(const QString &relativePath,
|
||
|
|
bool success,
|
||
|
|
const QString &detail)
|
||
|
|
{
|
||
|
|
if (relativePath != m_currentPath)
|
||
|
|
return;
|
||
|
|
setBusy(false);
|
||
|
|
setStatus(detail.isEmpty() ? (success ? QStringLiteral("配置有效")
|
||
|
|
: QStringLiteral("配置无效")) : detail,
|
||
|
|
!success);
|
||
|
|
}
|
||
|
|
|
||
|
|
void ConfigEditorWidget::handleSaveFinished(const QString &relativePath,
|
||
|
|
const QString &revision,
|
||
|
|
bool success,
|
||
|
|
const QString &detail)
|
||
|
|
{
|
||
|
|
if (relativePath != m_currentPath)
|
||
|
|
return;
|
||
|
|
setBusy(false);
|
||
|
|
if (!success) {
|
||
|
|
setStatus(detail.isEmpty() ? QStringLiteral("保存失败") : detail, true);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
m_revision = revision;
|
||
|
|
m_dirty = false;
|
||
|
|
m_textEditor->document()->setModified(false);
|
||
|
|
m_typeLabel->setText(m_typeLabel->text().section(QStringLiteral(" 版本:"), 0, 0)
|
||
|
|
+ QStringLiteral(" 版本:") + revision);
|
||
|
|
setStatus(QStringLiteral("已保存到远端项目配置文件"));
|
||
|
|
updateActions();
|
||
|
|
}
|
||
|
|
|
||
|
|
void ConfigEditorWidget::handleEditorChanged()
|
||
|
|
{
|
||
|
|
if (m_loading || m_currentPath.isEmpty())
|
||
|
|
return;
|
||
|
|
m_dirty = true;
|
||
|
|
m_parseTimer->start();
|
||
|
|
updateActions();
|
||
|
|
}
|
||
|
|
|
||
|
|
bool ConfigEditorWidget::parseFieldDocument()
|
||
|
|
{
|
||
|
|
m_dynamicMessage.reset();
|
||
|
|
m_dynamicFactory.reset();
|
||
|
|
if (m_messageType.isEmpty() || m_textEditor->toPlainText().trimmed().isEmpty())
|
||
|
|
return false;
|
||
|
|
|
||
|
|
const auto *descriptor = google::protobuf::DescriptorPool::generated_pool()
|
||
|
|
->FindMessageTypeByName(m_messageType.toStdString());
|
||
|
|
if (!descriptor)
|
||
|
|
return false;
|
||
|
|
m_dynamicFactory = std::make_unique<google::protobuf::DynamicMessageFactory>();
|
||
|
|
const auto *prototype = m_dynamicFactory->GetPrototype(descriptor);
|
||
|
|
if (!prototype)
|
||
|
|
return false;
|
||
|
|
m_dynamicMessage.reset(prototype->New());
|
||
|
|
if (!google::protobuf::TextFormat::ParseFromString(
|
||
|
|
m_textEditor->toPlainText().toStdString(), m_dynamicMessage.get())) {
|
||
|
|
m_dynamicMessage.reset();
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
void ConfigEditorWidget::appendMessageFields(QTreeWidgetItem *parent,
|
||
|
|
google::protobuf::Message *message)
|
||
|
|
{
|
||
|
|
if (!parent || !message)
|
||
|
|
return;
|
||
|
|
const auto *descriptor = message->GetDescriptor();
|
||
|
|
const auto *reflection = message->GetReflection();
|
||
|
|
using Field = google::protobuf::FieldDescriptor;
|
||
|
|
|
||
|
|
for (int fieldIndex = 0; fieldIndex < descriptor->field_count(); ++fieldIndex) {
|
||
|
|
const auto *field = descriptor->field(fieldIndex);
|
||
|
|
const QString fieldName = QString::fromStdString(field->name());
|
||
|
|
if (field->is_repeated()) {
|
||
|
|
const int count = reflection->FieldSize(*message, field);
|
||
|
|
if (count == 0) {
|
||
|
|
auto *empty = new QTreeWidgetItem(parent);
|
||
|
|
empty->setText(0, fieldName + QStringLiteral(" []"));
|
||
|
|
empty->setText(1, QStringLiteral("<空>"));
|
||
|
|
empty->setText(2, fieldTypeName(field));
|
||
|
|
empty->setFlags(empty->flags() & ~Qt::ItemIsEditable);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
for (int index = 0; index < count; ++index) {
|
||
|
|
auto *item = new QTreeWidgetItem(parent);
|
||
|
|
item->setText(0, fieldName + QStringLiteral("[%1]").arg(index));
|
||
|
|
item->setText(2, fieldTypeName(field));
|
||
|
|
if (field->cpp_type() == Field::CPPTYPE_MESSAGE) {
|
||
|
|
item->setText(1, QStringLiteral("message"));
|
||
|
|
appendMessageFields(item,
|
||
|
|
reflection->MutableRepeatedMessage(message, field, index));
|
||
|
|
item->setExpanded(true);
|
||
|
|
} else {
|
||
|
|
const QString value = scalarValue(message, field, index);
|
||
|
|
item->setText(1, value);
|
||
|
|
setItemMetadata(item, message, field, index, value);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
auto *item = new QTreeWidgetItem(parent);
|
||
|
|
item->setText(0, fieldName);
|
||
|
|
item->setText(2, fieldTypeName(field));
|
||
|
|
if (field->cpp_type() == Field::CPPTYPE_MESSAGE) {
|
||
|
|
if (reflection->HasField(*message, field)) {
|
||
|
|
item->setText(1, QStringLiteral("message"));
|
||
|
|
appendMessageFields(item, reflection->MutableMessage(message, field));
|
||
|
|
item->setExpanded(true);
|
||
|
|
} else {
|
||
|
|
item->setText(1, QStringLiteral("<未设置>"));
|
||
|
|
item->setFlags(item->flags() & ~Qt::ItemIsEditable);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
const QString value = scalarValue(message, field, -1);
|
||
|
|
item->setText(1, value);
|
||
|
|
setItemMetadata(item, message, field, -1, value);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void ConfigEditorWidget::rebuildFieldTree()
|
||
|
|
{
|
||
|
|
const QSignalBlocker blocker(m_fieldTree);
|
||
|
|
m_fieldTree->clear();
|
||
|
|
if (!parseFieldDocument())
|
||
|
|
return;
|
||
|
|
|
||
|
|
auto *root = new QTreeWidgetItem(m_fieldTree);
|
||
|
|
root->setText(0, m_messageType);
|
||
|
|
root->setText(1, QStringLiteral("message"));
|
||
|
|
root->setText(2, QStringLiteral("message"));
|
||
|
|
root->setFlags(root->flags() & ~Qt::ItemIsEditable);
|
||
|
|
appendMessageFields(root, m_dynamicMessage.get());
|
||
|
|
root->setExpanded(true);
|
||
|
|
}
|
||
|
|
|
||
|
|
bool ConfigEditorWidget::applyFieldValue(QTreeWidgetItem *item, const QString &text)
|
||
|
|
{
|
||
|
|
if (!item)
|
||
|
|
return false;
|
||
|
|
auto *message = reinterpret_cast<google::protobuf::Message *>(
|
||
|
|
pointerValue(item->data(1, kMessageRole)));
|
||
|
|
const auto *field = reinterpret_cast<const google::protobuf::FieldDescriptor *>(
|
||
|
|
pointerValue(item->data(1, kFieldRole)));
|
||
|
|
if (!message || !field)
|
||
|
|
return false;
|
||
|
|
const int index = item->data(1, kIndexRole).toInt();
|
||
|
|
const QString value = text.trimmed();
|
||
|
|
const auto *reflection = message->GetReflection();
|
||
|
|
using Field = google::protobuf::FieldDescriptor;
|
||
|
|
bool ok = true;
|
||
|
|
|
||
|
|
auto setBool = [&](bool parsed) {
|
||
|
|
if (field->is_repeated()) reflection->SetRepeatedBool(message, field, index, parsed);
|
||
|
|
else reflection->SetBool(message, field, parsed);
|
||
|
|
};
|
||
|
|
switch (field->cpp_type()) {
|
||
|
|
case Field::CPPTYPE_BOOL:
|
||
|
|
if (value.compare(QStringLiteral("true"), Qt::CaseInsensitive) == 0 || value == QStringLiteral("1"))
|
||
|
|
setBool(true);
|
||
|
|
else if (value.compare(QStringLiteral("false"), Qt::CaseInsensitive) == 0 || value == QStringLiteral("0"))
|
||
|
|
setBool(false);
|
||
|
|
else ok = false;
|
||
|
|
break;
|
||
|
|
case Field::CPPTYPE_INT32: {
|
||
|
|
const qlonglong number = value.toLongLong(&ok);
|
||
|
|
if (ok) field->is_repeated() ? reflection->SetRepeatedInt32(message, field, index, static_cast<int32_t>(number))
|
||
|
|
: reflection->SetInt32(message, field, static_cast<int32_t>(number));
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
case Field::CPPTYPE_INT64: {
|
||
|
|
const qlonglong number = value.toLongLong(&ok);
|
||
|
|
if (ok) field->is_repeated() ? reflection->SetRepeatedInt64(message, field, index, number)
|
||
|
|
: reflection->SetInt64(message, field, number);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
case Field::CPPTYPE_UINT32: {
|
||
|
|
const qulonglong number = value.toULongLong(&ok);
|
||
|
|
if (ok) field->is_repeated() ? reflection->SetRepeatedUInt32(message, field, index, static_cast<uint32_t>(number))
|
||
|
|
: reflection->SetUInt32(message, field, static_cast<uint32_t>(number));
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
case Field::CPPTYPE_UINT64: {
|
||
|
|
const qulonglong number = value.toULongLong(&ok);
|
||
|
|
if (ok) field->is_repeated() ? reflection->SetRepeatedUInt64(message, field, index, number)
|
||
|
|
: reflection->SetUInt64(message, field, number);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
case Field::CPPTYPE_FLOAT: {
|
||
|
|
const float number = value.toFloat(&ok);
|
||
|
|
if (ok) field->is_repeated() ? reflection->SetRepeatedFloat(message, field, index, number)
|
||
|
|
: reflection->SetFloat(message, field, number);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
case Field::CPPTYPE_DOUBLE: {
|
||
|
|
const double number = value.toDouble(&ok);
|
||
|
|
if (ok) field->is_repeated() ? reflection->SetRepeatedDouble(message, field, index, number)
|
||
|
|
: reflection->SetDouble(message, field, number);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
case Field::CPPTYPE_STRING:
|
||
|
|
if (field->is_repeated()) reflection->SetRepeatedString(message, field, index, text.toUtf8().toStdString());
|
||
|
|
else reflection->SetString(message, field, text.toUtf8().toStdString());
|
||
|
|
break;
|
||
|
|
case Field::CPPTYPE_ENUM: {
|
||
|
|
const auto *enumValue = field->enum_type()->FindValueByName(value.toStdString());
|
||
|
|
if (!enumValue) {
|
||
|
|
int number = value.toInt(&ok);
|
||
|
|
enumValue = field->enum_type()->FindValueByNumber(number);
|
||
|
|
}
|
||
|
|
if (!enumValue) ok = false;
|
||
|
|
else if (field->is_repeated()) reflection->SetRepeatedEnum(message, field, index, enumValue);
|
||
|
|
else reflection->SetEnum(message, field, enumValue);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
case Field::CPPTYPE_MESSAGE:
|
||
|
|
ok = false;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
return ok;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool ConfigEditorWidget::syncFieldTreeToText()
|
||
|
|
{
|
||
|
|
if (!m_dynamicMessage)
|
||
|
|
return false;
|
||
|
|
google::protobuf::TextFormat::Printer printer;
|
||
|
|
std::string text;
|
||
|
|
if (!printer.PrintToString(*m_dynamicMessage, &text))
|
||
|
|
return false;
|
||
|
|
m_loading = true;
|
||
|
|
{
|
||
|
|
const QSignalBlocker blocker(m_textEditor);
|
||
|
|
m_textEditor->setPlainText(QString::fromStdString(text));
|
||
|
|
m_textEditor->document()->setModified(true);
|
||
|
|
}
|
||
|
|
m_loading = false;
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
void ConfigEditorWidget::handleFieldItemChanged(QTreeWidgetItem *item, int column)
|
||
|
|
{
|
||
|
|
if (m_loading || column != 1 || !item || !item->data(1, kFieldRole).isValid())
|
||
|
|
return;
|
||
|
|
const QString previous = item->data(1, kPreviousValueRole).toString();
|
||
|
|
if (!applyFieldValue(item, item->text(1)) || !syncFieldTreeToText()) {
|
||
|
|
const QSignalBlocker blocker(m_fieldTree);
|
||
|
|
item->setText(1, previous);
|
||
|
|
setStatus(QStringLiteral("字段值格式无效,修改未应用"), true);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
item->setData(1, kPreviousValueRole, item->text(1));
|
||
|
|
m_dirty = true;
|
||
|
|
updateActions();
|
||
|
|
m_parseTimer->start();
|
||
|
|
}
|
||
|
|
|
||
|
|
void ConfigEditorWidget::rebuildEnableTable()
|
||
|
|
{
|
||
|
|
rebuildFieldTree();
|
||
|
|
m_enableTable->setRowCount(0);
|
||
|
|
const ManagerKind kind = managerKindForPath(m_currentPath);
|
||
|
|
m_editorTabs->setTabEnabled(2, kind != ManagerKind::None);
|
||
|
|
if (kind == ManagerKind::None)
|
||
|
|
return;
|
||
|
|
|
||
|
|
const std::string text = m_textEditor->toPlainText().toStdString();
|
||
|
|
QStringList ids;
|
||
|
|
QStringList types;
|
||
|
|
QStringList configFiles;
|
||
|
|
QList<bool> enabledValues;
|
||
|
|
bool parsed = false;
|
||
|
|
if (kind == ManagerKind::Device) {
|
||
|
|
cmvr::config::DeviceManagerRootConfig root;
|
||
|
|
parsed = google::protobuf::TextFormat::ParseFromString(text, &root);
|
||
|
|
if (parsed) {
|
||
|
|
for (const auto &entry : root.device_manager().devices()) {
|
||
|
|
ids.append(QString::fromStdString(entry.id()));
|
||
|
|
types.append(QString::fromStdString(
|
||
|
|
cmvr::config::DeviceConfigEntry::DeviceType_Name(entry.type())));
|
||
|
|
configFiles.append(QString::fromStdString(entry.config_file()));
|
||
|
|
enabledValues.append(entry.enable());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
cmvr::config::TaskManagerRootConfig root;
|
||
|
|
parsed = google::protobuf::TextFormat::ParseFromString(text, &root);
|
||
|
|
if (parsed) {
|
||
|
|
for (const auto &entry : root.task_manager().tasks()) {
|
||
|
|
ids.append(QString::fromStdString(entry.id()));
|
||
|
|
types.append(QString::fromStdString(
|
||
|
|
cmvr::config::TaskConfigEntry::TaskType_Name(entry.type())));
|
||
|
|
configFiles.append(QString::fromStdString(entry.config_file()));
|
||
|
|
enabledValues.append(entry.enable());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (!parsed) {
|
||
|
|
setStatus(QStringLiteral("原始配置存在语法错误,启用项暂不可编辑"), true);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
m_enableTable->setRowCount(ids.size());
|
||
|
|
for (int row = 0; row < ids.size(); ++row) {
|
||
|
|
m_enableTable->setItem(row, 0, readOnlyItem(ids.at(row)));
|
||
|
|
m_enableTable->setItem(row, 1, readOnlyItem(types.at(row)));
|
||
|
|
m_enableTable->setItem(row, 2, readOnlyItem(configFiles.at(row)));
|
||
|
|
auto *container = new QWidget(m_enableTable);
|
||
|
|
auto *switchLayout = new QHBoxLayout(container);
|
||
|
|
switchLayout->setContentsMargins(8, 0, 8, 0);
|
||
|
|
switchLayout->setAlignment(Qt::AlignCenter);
|
||
|
|
auto *toggle = new QCheckBox(container);
|
||
|
|
toggle->setObjectName(QStringLiteral("configEnableSwitch"));
|
||
|
|
toggle->setChecked(enabledValues.at(row));
|
||
|
|
toggle->setToolTip(enabledValues.at(row) ? QStringLiteral("点击停用")
|
||
|
|
: QStringLiteral("点击启用"));
|
||
|
|
switchLayout->addWidget(toggle);
|
||
|
|
m_enableTable->setCellWidget(row, 3, container);
|
||
|
|
connect(toggle, &QCheckBox::toggled, this, [this, row, toggle](bool checked) {
|
||
|
|
if (!replaceEnableValue(row, checked)) {
|
||
|
|
const QSignalBlocker blocker(toggle);
|
||
|
|
toggle->setChecked(!checked);
|
||
|
|
setStatus(QStringLiteral("无法定位对应的 enable 字段"), true);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
bool ConfigEditorWidget::confirmDiscardChanges()
|
||
|
|
{
|
||
|
|
if (!m_dirty)
|
||
|
|
return true;
|
||
|
|
const auto answer = QMessageBox::question(
|
||
|
|
this, QStringLiteral("未保存的修改"),
|
||
|
|
QStringLiteral("当前配置尚未保存,确定放弃修改吗?"),
|
||
|
|
QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Cancel);
|
||
|
|
return answer == QMessageBox::Discard;
|
||
|
|
}
|
||
|
|
|
||
|
|
void ConfigEditorWidget::setBusy(bool busy)
|
||
|
|
{
|
||
|
|
m_busy = busy;
|
||
|
|
updateActions();
|
||
|
|
}
|
||
|
|
|
||
|
|
void ConfigEditorWidget::setStatus(const QString &text, bool error)
|
||
|
|
{
|
||
|
|
m_statusLabel->setText(text);
|
||
|
|
m_statusLabel->setStyleSheet(error ? QStringLiteral("color:#b42318;") : QString());
|
||
|
|
}
|
||
|
|
|
||
|
|
void ConfigEditorWidget::clearCurrentFile()
|
||
|
|
{
|
||
|
|
m_loading = true;
|
||
|
|
m_currentPath.clear();
|
||
|
|
m_revision.clear();
|
||
|
|
m_dirty = false;
|
||
|
|
m_textEditor->clear();
|
||
|
|
m_messageType.clear();
|
||
|
|
m_dynamicFactory.reset();
|
||
|
|
m_dynamicMessage.reset();
|
||
|
|
m_fieldTree->clear();
|
||
|
|
m_loading = false;
|
||
|
|
m_pathLabel->setText(QStringLiteral("未选择配置文件"));
|
||
|
|
m_typeLabel->setText(QStringLiteral("--"));
|
||
|
|
m_enableTable->setRowCount(0);
|
||
|
|
m_editorTabs->setTabEnabled(2, false);
|
||
|
|
updateActions();
|
||
|
|
}
|
||
|
|
|
||
|
|
void ConfigEditorWidget::updateActions()
|
||
|
|
{
|
||
|
|
const bool hasFile = !m_currentPath.isEmpty();
|
||
|
|
m_refreshButton->setEnabled(m_connected && !m_busy);
|
||
|
|
m_fileList->setEnabled(m_connected && !m_busy);
|
||
|
|
m_textEditor->setEnabled(hasFile && m_connected && !m_busy);
|
||
|
|
m_fieldTree->setEnabled(hasFile && m_connected && !m_busy);
|
||
|
|
m_enableTable->setEnabled(hasFile && m_connected && !m_busy);
|
||
|
|
m_reloadButton->setEnabled(hasFile && m_connected && !m_busy);
|
||
|
|
m_validateButton->setEnabled(hasFile && m_connected && !m_busy);
|
||
|
|
m_saveButton->setEnabled(hasFile && m_connected && m_dirty && !m_busy);
|
||
|
|
m_connectButton->setEnabled(!m_busy);
|
||
|
|
}
|
||
|
|
|
||
|
|
bool ConfigEditorWidget::replaceEnableValue(int index, bool enabled)
|
||
|
|
{
|
||
|
|
QString text = m_textEditor->toPlainText();
|
||
|
|
const QRegularExpression expression(QStringLiteral("\\benable\\s*:\\s*(true|false)"));
|
||
|
|
auto matches = expression.globalMatch(text);
|
||
|
|
QRegularExpressionMatch target;
|
||
|
|
int current = 0;
|
||
|
|
for (; current <= index && matches.hasNext(); ++current)
|
||
|
|
target = matches.next();
|
||
|
|
if (!target.hasMatch() || current <= index)
|
||
|
|
return false;
|
||
|
|
QTextCursor cursor(m_textEditor->document());
|
||
|
|
cursor.setPosition(target.capturedStart(1));
|
||
|
|
cursor.setPosition(target.capturedEnd(1), QTextCursor::KeepAnchor);
|
||
|
|
m_loading = true;
|
||
|
|
cursor.insertText(enabled ? QStringLiteral("true") : QStringLiteral("false"));
|
||
|
|
m_loading = false;
|
||
|
|
m_dirty = true;
|
||
|
|
m_textEditor->document()->setModified(true);
|
||
|
|
updateActions();
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
ConfigEditorWidget::ManagerKind ConfigEditorWidget::managerKindForPath(
|
||
|
|
const QString &path) const
|
||
|
|
{
|
||
|
|
const QString normalized = path.toLower();
|
||
|
|
if (normalized == QStringLiteral("manager/device_manager.pb.txt"))
|
||
|
|
return ManagerKind::Device;
|
||
|
|
if (normalized == QStringLiteral("manager/task_manager.pb.txt"))
|
||
|
|
return ManagerKind::Task;
|
||
|
|
return ManagerKind::None;
|
||
|
|
}
|