CmvrControlStation/src/configeditorwidget.cpp

852 lines
34 KiB
C++
Raw Normal View History

2026-09-18 16:22:20 +08:00
#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;
}