#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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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(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( 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( 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(reinterpret_cast(message))); item->setData(1, kFieldRole, QVariant::fromValue(reinterpret_cast(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(); 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( pointerValue(item->data(1, kMessageRole))); const auto *field = reinterpret_cast( 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(number)) : reflection->SetInt32(message, field, static_cast(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(number)) : reflection->SetUInt32(message, field, static_cast(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 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; }