新增数据映射页面,表映射、函数映射;新增版本更新提示功能

This commit is contained in:
taocong
2026-07-02 19:54:22 +08:00
parent d4d607dc6a
commit 1f33f0fbaa
31 changed files with 1506 additions and 214 deletions
+2 -1
View File
@@ -1,4 +1,5 @@
#include "aboutdialog.h"
#include "utils/Constants.h"
#include "ElaImageCard.h"
#include "ElaText.h"
@@ -54,7 +55,7 @@ AboutDialog::AboutDialog(QWidget* parent) : InterDialog(parent)
auto* downloadBtn = new ElaPushButton("下载最新", this);
connect(downloadBtn, &ElaPushButton::clicked, []
{
QDesktopServices::openUrl(QUrl("http://10.20.163.105:6045/uft3changecode/下载.html"));
QDesktopServices::openUrl(QUrl(Constants::Urls::downloadPage()));
});
addFirstButton(downloadBtn);
+149 -111
View File
@@ -1,9 +1,16 @@
#include "batchconvertpage.h"
#include "src/utils/Constants.h"
#include "src/utils/uf2configreader.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGroupBox>
#include <QFile>
#include <QFileInfo>
#include <QDir>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QRegularExpression>
#include <QCoreApplication>
#include <QHeaderView>
#include <QMessageBox>
@@ -48,19 +55,24 @@ BatchConvertPage::BatchConvertPage(QWidget *parent)
{
initUI();
loadFromCustJson();
m_configWatcher = new QFileSystemWatcher(this);
QString configPath = getConfigFilePath();
if (!configPath.isEmpty() && QFile::exists(configPath)) {
m_configWatcher->addPath(configPath);
connect(m_configWatcher, &QFileSystemWatcher::fileChanged, this, &BatchConvertPage::onConfigFileChanged);
}
// 防抖定时器:编辑器保存可能触发多次信号,合并为一次重载
m_reloadTimer = new QTimer(this);
m_reloadTimer->setSingleShot(true);
m_reloadTimer->setInterval(300);
connect(m_reloadTimer, &QTimer::timeout, this, &BatchConvertPage::onReloadTimeout);
// 记录初始 mtime
m_lastFileMtime = currentFileMtime();
setupConfigWatcher();
}
BatchConvertPage::~BatchConvertPage()
{
if (m_configWatcher) {
m_configWatcher->removePaths(m_configWatcher->files());
m_configWatcher->removePaths(m_configWatcher->directories());
}
}
@@ -366,140 +378,112 @@ void BatchConvertPage::onClearTable()
bool BatchConvertPage::saveToCustJson(const QStringList &funcList)
{
QString jsonPath = QCoreApplication::applicationDirPath() + "/uf2touft3/cust.json";
QString jsonPath = Constants::custJson();
// 读取原始文件内容(保留全部格式不重序列化)
QFile file(jsonPath);
if (!file.open(QIODevice::ReadOnly)) {
LogManager::instance()->logWarning(
QString("业务转码 - 保存配置失败,无法打开: %1").arg(jsonPath));
return false;
}
QByteArray data = file.readAll();
QString content = QString::fromUtf8(file.readAll());
file.close();
QString content = QString::fromUtf8(data);
int convertStart = content.indexOf("\"convert\":");
if (convertStart < 0) {
convertStart = content.indexOf("\"convert\" :");
}
if (convertStart < 0) {
// 用正则定位 "convert" 节点(兼容任意空格)
static QRegularExpression convertRe(R"("convert"\s*:)");
auto convertMatch = convertRe.match(content);
if (!convertMatch.hasMatch()) {
LogManager::instance()->logWarning("业务转码 - 保存配置失败,未找到 convert 节点");
return false;
}
int dirStart = content.indexOf("\"dir\":[", convertStart);
if (dirStart < 0) {
dirStart = content.indexOf("\"dir\" : [", convertStart);
}
if (dirStart < 0) {
// 在 convert 之后定位 "dir" 数组起始 [
static QRegularExpression dirRe(R"("dir"\s*:\s*\[)");
auto dirMatch = dirRe.match(content, convertMatch.capturedEnd());
if (!dirMatch.hasMatch()) {
LogManager::instance()->logWarning("业务转码 - 保存配置失败,未找到 dir 数组");
return false;
}
int arrayStart = content.indexOf('[', dirStart);
int bracketStart = dirMatch.capturedEnd() - 1; // 指向 '['
// 括号计数找到匹配的 ']'
int bracketCount = 1;
int pos = arrayStart + 1;
int pos = bracketStart + 1;
while (pos < content.length() && bracketCount > 0) {
if (content[pos] == '[') bracketCount++;
else if (content[pos] == ']') bracketCount--;
QChar ch = content[pos];
if (ch == '[') bracketCount++;
else if (ch == ']') bracketCount--;
pos++;
}
int arrayEnd = pos;
QString newDirArray = "[";
int bracketEnd = pos; // 指向 ']' 之后一位
// 构建新数组(保持原始缩进风格:项 12 空格,闭合 ] 8 空格)
QString newArray = "[";
for (int i = 0; i < funcList.size(); ++i) {
if (i > 0) newDirArray += ",";
newDirArray += "\n \"" + funcList[i] + ".service_design\"";
if (i > 0) newArray += ",";
newArray += "\n \"" + funcList[i] + ".service_design\"";
}
newDirArray += "\n ]";
content.replace(arrayStart, arrayEnd - arrayStart, newDirArray);
if (!funcList.isEmpty()) {
newArray += "\n ";
}
newArray += "]";
// 只替换 [bracketStart, bracketEnd) 区间,其余内容原样保留
content.replace(bracketStart, bracketEnd - bracketStart, newArray);
// 写回
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
LogManager::instance()->logWarning(
QString("业务转码 - 保存配置失败,无法写入: %1").arg(jsonPath));
return false;
}
file.write(content.toUtf8());
file.close();
// 程序自身写入后更新 mtime,避免触发无谓的重载
m_lastFileMtime = currentFileMtime();
return true;
}
void BatchConvertPage::loadFromCustJson()
{
QString jsonPath = QCoreApplication::applicationDirPath() + "/uf2touft3/cust.json";
QString jsonPath = Constants::custJson();
QFile file(jsonPath);
int retryCount = 0;
const int maxRetries = 3;
const int retryDelayMs = 100;
while (!file.open(QIODevice::ReadOnly) && retryCount < maxRetries) {
retryCount++;
QThread::msleep(retryDelayMs);
}
if (!file.isOpen()) {
if (!file.open(QIODevice::ReadOnly)) {
LogManager::instance()->logWarning(
QString("业务转码 - 加载配置失败,无法打开: %1").arg(jsonPath));
return;
}
QByteArray data = file.readAll();
file.close();
QString content = QString::fromUtf8(data);
int convertStart = content.indexOf("\"convert\":");
if (convertStart < 0) {
convertStart = content.indexOf("\"convert\" :");
}
if (convertStart < 0) {
QJsonParseError parseError;
QJsonDocument doc = QJsonDocument::fromJson(data, &parseError);
if (parseError.error != QJsonParseError::NoError) {
LogManager::instance()->logWarning(
QString("业务转码 - 加载配置 JSON 解析失败: %1").arg(parseError.errorString()));
return;
}
int dirStart = content.indexOf("\"dir\":[", convertStart);
if (dirStart < 0) {
dirStart = content.indexOf("\"dir\" : [", convertStart);
}
if (dirStart < 0) {
return;
}
int arrayStart = content.indexOf('[', dirStart);
int bracketCount = 1;
int pos = arrayStart + 1;
while (pos < content.length() && bracketCount > 0) {
if (content[pos] == '[') bracketCount++;
else if (content[pos] == ']') bracketCount--;
pos++;
}
int arrayEnd = pos;
QString arrayContent = content.mid(arrayStart + 1, arrayEnd - arrayStart - 1).trimmed();
if (arrayContent.isEmpty() || arrayContent == "null") {
return;
}
QJsonObject root = doc.object();
QJsonArray dir = root["convert"].toObject()["dir"].toArray();
QStringList items;
int itemPos = 0;
while (itemPos < arrayContent.length()) {
int itemStart = arrayContent.indexOf('"', itemPos);
if (itemStart < 0) break;
int itemEnd = arrayContent.indexOf('"', itemStart + 1);
if (itemEnd < 0) break;
QString item = arrayContent.mid(itemStart + 1, itemEnd - itemStart - 1);
items.append(item);
itemPos = itemEnd + 1;
}
for (QString &item : items) {
for (const QJsonValue &val : dir) {
QString item = val.toString();
if (item.endsWith(".service_design")) {
item = item.left(item.length() - static_cast<int>(strlen(".service_design")));
item.chop(static_cast<int>(strlen(".service_design")));
}
items.append(item);
}
m_funcList = items;
updateTable();
}
@@ -566,17 +550,71 @@ void BatchConvertPage::addFunction(const QString &funcName)
}
}
void BatchConvertPage::setupConfigWatcher()
{
QString configPath = getConfigFilePath();
if (configPath.isEmpty()) {
return;
}
m_configWatcher = new QFileSystemWatcher(this);
// 1. 监视文件本身(覆盖直接编辑场景)
if (QFile::exists(configPath)) {
m_configWatcher->addPath(configPath);
}
// 2. 监视文件所在目录(覆盖编辑器原子替换:写临时文件→删除→重命名)
// Windows 上 fileChanged 在文件被替换后常失效,directoryChanged 更可靠
QString dir = QFileInfo(configPath).absolutePath();
if (QDir(dir).exists()) {
m_configWatcher->addPath(dir);
}
connect(m_configWatcher, &QFileSystemWatcher::fileChanged,
this, &BatchConvertPage::onConfigFileChanged);
connect(m_configWatcher, &QFileSystemWatcher::directoryChanged,
this, &BatchConvertPage::onConfigDirChanged);
}
QDateTime BatchConvertPage::currentFileMtime() const
{
QFileInfo info(getConfigFilePath());
return info.exists() ? info.lastModified() : QDateTime();
}
void BatchConvertPage::onConfigFileChanged(const QString &path)
{
QStringList oldList = m_funcList;
loadFromCustJson();
if (m_configWatcher && !m_configWatcher->files().contains(path)) {
{
// 文件可能被编辑器替换导致监视失效,尝试重新添加
if (m_configWatcher && !m_configWatcher->files().contains(path)
&& QFile::exists(path)) {
m_configWatcher->addPath(path);
}
m_reloadTimer->start(); // 防抖
}
void BatchConvertPage::onConfigDirChanged(const QString & /*dirPath*/)
{
// 目录变化时重新监视文件(可能之前文件不存在/被替换)
QString configPath = getConfigFilePath();
if (m_configWatcher && QFile::exists(configPath)
&& !m_configWatcher->files().contains(configPath)) {
m_configWatcher->addPath(configPath);
}
m_reloadTimer->start(); // 防抖
}
void BatchConvertPage::onReloadTimeout()
{
// 比较 mtime,避免无谓重载
QDateTime now = currentFileMtime();
if (now.isValid() && now != m_lastFileMtime) {
m_lastFileMtime = now;
loadFromCustJson();
}
}
QString BatchConvertPage::getConfigFilePath() const
{
return QCoreApplication::applicationDirPath() + "/uf2touft3/cust.json";
return Constants::custJson();
}
+8 -1
View File
@@ -5,7 +5,8 @@
#include <QStandardItemModel>
#include <QPushButton>
#include <QFileSystemWatcher>
#include <QThread>
#include <QTimer>
#include <QDateTime>
#include "ElaLineEdit.h"
#include "ElaTableView.h"
#include "ElaProgressBar.h"
@@ -39,6 +40,8 @@ private slots:
void onClearTable();
void onEditRow(int row);
void onConfigFileChanged(const QString &path);
void onConfigDirChanged(const QString &path);
void onReloadTimeout();
private:
void initUI();
@@ -48,6 +51,8 @@ private:
void updateTable();
void resizeEvent(QResizeEvent *event) override;
QString getConfigFilePath() const;
void setupConfigWatcher();
QDateTime currentFileMtime() const;
ElaTableView *m_funcTable;
QStandardItemModel *m_tableModel;
@@ -56,6 +61,8 @@ private:
ElaText *m_progressLabel;
QPushButton *m_startBtn;
QFileSystemWatcher *m_configWatcher;
QTimer *m_reloadTimer;
QDateTime m_lastFileMtime;
};
#endif
@@ -0,0 +1,449 @@
#include "functionmappingpage.h"
#include "utils/Constants.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QMessageBox>
#include <QCoreApplication>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QDialog>
#include <QStandardItem>
#include <QRegularExpression>
#include <QTextStream>
#include <QJsonValue>
#include <QLabel>
#include <algorithm>
#include <QTimer>
#include "src/utils/logmanager.h"
#include "ElaPushButton.h"
#include "ElaLineEdit.h"
#include "ElaText.h"
// 自定义代理模型:支持两列同时过滤
class FuncFilterProxyModel : public QSortFilterProxyModel
{
public:
explicit FuncFilterProxyModel(QObject *parent = nullptr) : QSortFilterProxyModel(parent) {}
void setFilterText(const QString &text)
{
m_filterText = text;
invalidateFilter();
}
protected:
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override
{
if (m_filterText.isEmpty()) return true;
QString t0 = sourceModel()->data(sourceModel()->index(sourceRow, 0, sourceParent)).toString();
QString t1 = sourceModel()->data(sourceModel()->index(sourceRow, 1, sourceParent)).toString();
return t0.contains(m_filterText, Qt::CaseInsensitive) || t1.contains(m_filterText, Qt::CaseInsensitive);
}
private:
QString m_filterText;
};
FunctionMappingPage::FunctionMappingPage(QWidget *parent)
: QWidget(parent)
, m_isLoading(false)
, m_isSaving(false)
{
initUI();
loadData();
}
FunctionMappingPage::~FunctionMappingPage()
{
}
void FunctionMappingPage::initUI()
{
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(20, 20, 20, 20);
mainLayout->setSpacing(10);
// 搜索框
QHBoxLayout *searchLayout = new QHBoxLayout;
ElaLineEdit *searchEdit = new ElaLineEdit(this);
searchEdit->setPlaceholderText("搜索 UF20 或 UFT3 函数名...");
searchEdit->setClearButtonEnabled(true);
connect(searchEdit, &ElaLineEdit::textChanged, this, &FunctionMappingPage::onSearchTextChanged);
searchLayout->addWidget(searchEdit);
mainLayout->addLayout(searchLayout);
m_model = new QStandardItemModel(this);
m_model->setColumnCount(2);
m_model->setHorizontalHeaderLabels({"UF20", "UFT3"});
connect(m_model, &QStandardItemModel::itemChanged, this, &FunctionMappingPage::onModelItemChanged);
m_proxyModel = new FuncFilterProxyModel(this);
m_proxyModel->setSourceModel(m_model);
m_tableView = new ElaTableView;
m_tableView->setModel(m_proxyModel);
m_tableView->setSelectionBehavior(QAbstractItemView::SelectRows);
m_tableView->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_tableView->setEditTriggers(QAbstractItemView::DoubleClicked | QAbstractItemView::EditKeyPressed);
m_tableView->verticalHeader()->setVisible(false);
m_tableView->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch);
m_tableView->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch);
mainLayout->addWidget(m_tableView);
QHBoxLayout *btnLayout = new QHBoxLayout;
ElaPushButton *addBtn = new ElaPushButton("添加", this);
ElaPushButton *delBtn = new ElaPushButton("删除选中", this);
ElaPushButton *saveBtn = new ElaPushButton("保存", this);
connect(addBtn, &ElaPushButton::clicked, this, &FunctionMappingPage::onAddRow);
connect(delBtn, &ElaPushButton::clicked, this, &FunctionMappingPage::onDeleteRow);
connect(saveBtn, &ElaPushButton::clicked, this, &FunctionMappingPage::onSave);
btnLayout->addWidget(addBtn);
btnLayout->addWidget(delBtn);
btnLayout->addStretch();
btnLayout->addWidget(saveBtn);
mainLayout->addLayout(btnLayout);
QHBoxLayout *tipLayout = new QHBoxLayout;
QLabel *tipLabel = new QLabel("💡 双击表格修改数据");
tipLabel->setStyleSheet("color: #666; font-size: 13px;");
tipLayout->addWidget(tipLabel);
tipLayout->addStretch();
mainLayout->addLayout(tipLayout);
// 文件监控:检测 cust.json 外部修改后自动刷新
m_fileWatcher = new QFileSystemWatcher(this);
connect(m_fileWatcher, &QFileSystemWatcher::fileChanged, this, &FunctionMappingPage::onFileChanged);
}
void FunctionMappingPage::loadData()
{
m_isLoading = true;
m_model->removeRows(0, m_model->rowCount());
QJsonObject custJson = readCustJson();
if (custJson.contains("functionReplace")) {
QJsonObject dataObj = custJson["functionReplace"].toObject();
QStringList keys = dataObj.keys();
for (const QString &key : keys) {
QList<QStandardItem*> rowItems;
rowItems.append(new QStandardItem(key));
rowItems.append(new QStandardItem(dataObj[key].toString()));
m_model->appendRow(rowItems);
}
}
m_isLoading = false;
// 设置文件监控
QString jsonPath = getCustJsonPath();
if (!m_fileWatcher->files().contains(jsonPath))
m_fileWatcher->addPath(jsonPath);
}
QString FunctionMappingPage::getCustJsonPath() const
{
return Constants::custJson();
}
QJsonObject FunctionMappingPage::readCustJson()
{
QString jsonPath = getCustJsonPath();
QFile file(jsonPath);
if (!file.open(QIODevice::ReadOnly)) {
LogManager::instance()->logError("无法读取 cust.json: " + jsonPath);
return QJsonObject();
}
QByteArray data = file.readAll();
file.close();
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError) {
LogManager::instance()->logError("cust.json 解析失败: " + error.errorString());
return QJsonObject();
}
return doc.object();
}
// 从原始 JSON 文本中提取顶层 key 的顺序
static QStringList extractTopLevelKeyOrder(const QString &rawJson)
{
QStringList keys;
QRegularExpression re("\"(\\w+)\"\\s*:");
auto it = re.globalMatch(rawJson);
while (it.hasNext()) {
QString key = it.next().captured(1);
if (!keys.contains(key))
keys.append(key);
}
return keys;
}
// 对 JSON 字符串进行缩进处理
static QString indentJsonValue(const QString &json)
{
if (json.isEmpty()) return json;
QStringList lines = json.split('\n');
QString result;
for (const QString &line : lines) {
if (!line.trimmed().isEmpty())
result += " " + line + "\n";
}
return result;
}
// 写入 JSON 值,根据类型选择合适的格式
static QString formatJsonValue(const QJsonValue &value)
{
if (value.isObject()) {
QJsonDocument doc(value.toObject());
QString raw = QString::fromUtf8(doc.toJson(QJsonDocument::Indented));
return indentJsonValue(raw);
} else if (value.isArray()) {
QJsonDocument doc(value.toArray());
QString raw = QString::fromUtf8(doc.toJson(QJsonDocument::Indented));
return indentJsonValue(raw);
} else if (value.isString()) {
QString escaped = value.toString();
escaped.replace("\\", "\\\\");
escaped.replace("\"", "\\\"");
escaped.replace("\n", "\\n");
escaped.replace("\r", "\\r");
escaped.replace("\t", "\\t");
return "\"" + escaped + "\"";
} else if (value.isBool()) {
return value.toBool() ? "true" : "false";
} else if (value.isDouble()) {
double d = value.toDouble();
if (d == static_cast<int>(d))
return QString::number(static_cast<int>(d));
return QString::number(d, 'f');
}
return "null";
}
// 按顺序格式化一个 string->string 键值对的嵌套 JSON 对象
static QString formatOrderedJsonObject(const QList<QPair<QString, QString>> &pairs)
{
QString result = "{\n";
bool first = true;
for (const auto &pair : pairs) {
if (!first) result += ",\n";
first = false;
QString ek = pair.first;
ek.replace("\\", "\\\\"); ek.replace("\"", "\\\"");
QString ev = pair.second;
ev.replace("\\", "\\\\"); ev.replace("\"", "\\\"");
ev.replace("\n", "\\n"); ev.replace("\r", "\\r"); ev.replace("\t", "\\t");
result += " \"" + ek + "\": \"" + ev + "\"";
}
result += "\n }";
return result;
}
bool FunctionMappingPage::writeCustJson(const QJsonObject &json, const QStringList &keyOrder,
const QString &orderedKey, const QList<QPair<QString, QString>> &orderedData)
{
QString jsonPath = getCustJsonPath();
QFile file(jsonPath);
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
LogManager::instance()->logError("无法写入 cust.json: " + jsonPath);
return false;
}
QTextStream out(&file);
out << "{\n";
bool first = true;
for (const QString &key : keyOrder) {
if (!json.contains(key)) continue;
if (!first) out << ",\n";
first = false;
out << " \"" << key << "\": ";
if (key == orderedKey) {
out << formatOrderedJsonObject(orderedData);
} else {
out << formatJsonValue(json[key]);
}
}
out << "\n}\n";
file.close();
return true;
}
void FunctionMappingPage::onAddRow()
{
QDialog dialog(this);
dialog.setWindowTitle("添加函数映射");
dialog.resize(450, 180);
QVBoxLayout *layout = new QVBoxLayout(&dialog);
layout->setSpacing(12);
QHBoxLayout *keyLayout = new QHBoxLayout;
ElaText *keyLabel = new ElaText("UF20:", &dialog);
keyLabel->setTextPointSize(10);
keyLayout->addWidget(keyLabel);
ElaLineEdit *keyEdit = new ElaLineEdit(&dialog);
keyEdit->setPlaceholderText("输入UF20函数名");
keyLayout->addWidget(keyEdit);
layout->addLayout(keyLayout);
QHBoxLayout *valLayout = new QHBoxLayout;
ElaText *valLabel = new ElaText("UFT3:", &dialog);
valLabel->setTextPointSize(10);
valLayout->addWidget(valLabel);
ElaLineEdit *valEdit = new ElaLineEdit(&dialog);
valEdit->setPlaceholderText("输入UFT3函数名");
valLayout->addWidget(valEdit);
layout->addLayout(valLayout);
QHBoxLayout *btnLayout = new QHBoxLayout;
ElaPushButton *okBtn = new ElaPushButton("确定", &dialog);
ElaPushButton *cancelBtn = new ElaPushButton("取消", &dialog);
btnLayout->addStretch();
btnLayout->addWidget(okBtn);
btnLayout->addWidget(cancelBtn);
layout->addLayout(btnLayout);
connect(okBtn, &ElaPushButton::clicked, &dialog, &QDialog::accept);
connect(cancelBtn, &ElaPushButton::clicked, &dialog, &QDialog::reject);
if (dialog.exec() != QDialog::Accepted) return;
QString key = keyEdit->text().trimmed();
QString val = valEdit->text().trimmed();
if (key.isEmpty()) {
QMessageBox::warning(this, "提示", "UF20 函数名不能为空!");
return;
}
for (int row = 0; row < m_model->rowCount(); ++row) {
if (m_model->item(row, 0) && m_model->item(row, 0)->text() == key) {
QMessageBox::warning(this, "提示", QString("UF20 函数名 \"%1\" 已存在!").arg(key));
return;
}
}
QList<QStandardItem*> rowItems;
rowItems.append(new QStandardItem(key));
rowItems.append(new QStandardItem(val));
m_model->appendRow(rowItems);
}
void FunctionMappingPage::onDeleteRow()
{
QModelIndexList selectedRows = m_tableView->selectionModel()->selectedRows();
if (selectedRows.isEmpty()) {
QMessageBox::warning(this, "提示", "请先选中要删除的行!");
return;
}
QStringList keyNames;
QList<int> sourceRows;
for (const QModelIndex &idx : selectedRows) {
QModelIndex srcIdx = m_proxyModel->mapToSource(idx);
QStandardItem *keyItem = m_model->item(srcIdx.row(), 0);
if (keyItem) keyNames.append(keyItem->text());
sourceRows.append(srcIdx.row());
}
QString confirmMsg;
if (selectedRows.size() == 1) {
confirmMsg = QString("确定要删除 \"%1\" 这条映射吗?").arg(keyNames.first());
} else {
confirmMsg = QString("确定要删除选中的 %1 条映射吗?").arg(selectedRows.size());
}
QMessageBox::StandardButton reply = QMessageBox::question(this, "确认删除",
confirmMsg, QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes) return;
// 从高行号到低行号删除,避免索引偏移问题
std::sort(sourceRows.begin(), sourceRows.end(), std::greater<int>());
for (int row : sourceRows) {
m_model->removeRow(row);
}
}
void FunctionMappingPage::onModelItemChanged(QStandardItem *item)
{
if (m_isLoading) return;
Q_UNUSED(item);
}
void FunctionMappingPage::onSave()
{
// 标记正在保存,忽略文件监控事件
m_isSaving = true;
// 读取原始文件内容以保留顶层 key 顺序
QString jsonPath = getCustJsonPath();
QFile file(jsonPath);
if (!file.open(QIODevice::ReadOnly)) {
QMessageBox::critical(this, "错误", "无法读取 cust.json!");
return;
}
QString rawJson = QString::fromUtf8(file.readAll());
file.close();
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(rawJson.toUtf8(), &error);
if (error.error != QJsonParseError::NoError) {
QMessageBox::critical(this, "错误", "cust.json 解析失败: " + error.errorString());
return;
}
QJsonObject custJson = doc.object();
// 提取原始 key 顺序
QStringList keyOrder = extractTopLevelKeyOrder(rawJson);
// 构建有序的函数替换数据
QJsonObject functionReplaceObj;
QList<QPair<QString, QString>> funcData;
for (int row = 0; row < m_model->rowCount(); ++row) {
QStandardItem *keyItem = m_model->item(row, 0);
QStandardItem *valItem = m_model->item(row, 1);
if (keyItem && !keyItem->text().trimmed().isEmpty()) {
QString k = keyItem->text().trimmed();
QString v = valItem ? valItem->text().trimmed() : "";
functionReplaceObj[k] = v;
funcData.append({k, v});
}
}
custJson["functionReplace"] = functionReplaceObj;
if (writeCustJson(custJson, keyOrder, "functionReplace", funcData)) {
LogManager::instance()->logInfo("函数映射配置已保存到 cust.json");
QMessageBox::information(this, "提示", "保存成功!");
} else {
QMessageBox::critical(this, "错误", "保存到 cust.json 失败!");
m_isSaving = false;
}
// 延迟重置保存标记,让文件监控忽略本次写入事件
QTimer::singleShot(2000, this, [this]() {
m_isSaving = false;
// 重新添加监控路径
QString path = getCustJsonPath();
if (!m_fileWatcher->files().contains(path))
m_fileWatcher->addPath(path);
});
}
void FunctionMappingPage::onFileChanged(const QString &path)
{
Q_UNUSED(path);
if (m_isSaving) return;
LogManager::instance()->logInfo("检测到 cust.json 外部修改,自动刷新数据");
loadData();
}
void FunctionMappingPage::onSearchTextChanged(const QString &text)
{
static_cast<FuncFilterProxyModel*>(m_proxyModel)->setFilterText(text);
}
@@ -0,0 +1,46 @@
#ifndef FUNCTIONMAPPINGPAGE_H
#define FUNCTIONMAPPINGPAGE_H
#include <QWidget>
#include <QStandardItemModel>
#include <QFileSystemWatcher>
#include <QSortFilterProxyModel>
#include <QPair>
#include <QList>
#include "ElaTableView.h"
class ElaLineEdit;
class FunctionMappingPage : public QWidget
{
Q_OBJECT
public:
explicit FunctionMappingPage(QWidget *parent = nullptr);
~FunctionMappingPage();
private slots:
void onAddRow();
void onDeleteRow();
void onModelItemChanged(QStandardItem *item);
void onSave();
void onFileChanged(const QString &path);
void onSearchTextChanged(const QString &text);
private:
void initUI();
void loadData();
QJsonObject readCustJson();
bool writeCustJson(const QJsonObject &json, const QStringList &keyOrder,
const QString &orderedKey, const QList<QPair<QString, QString>> &orderedData);
QString getCustJsonPath() const;
ElaTableView *m_tableView;
QStandardItemModel *m_model;
QSortFilterProxyModel *m_proxyModel;
QFileSystemWatcher *m_fileWatcher;
bool m_isLoading;
bool m_isSaving;
};
#endif // FUNCTIONMAPPINGPAGE_H
+449
View File
@@ -0,0 +1,449 @@
#include "tablemappingpage.h"
#include "utils/Constants.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QMessageBox>
#include <QCoreApplication>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QDialog>
#include <QStandardItem>
#include <QRegularExpression>
#include <QTextStream>
#include <QJsonValue>
#include <QLabel>
#include <algorithm>
#include <QTimer>
#include "src/utils/logmanager.h"
#include "ElaPushButton.h"
#include "ElaLineEdit.h"
#include "ElaText.h"
// 自定义代理模型:支持两列同时过滤
class TableFilterProxyModel : public QSortFilterProxyModel
{
public:
explicit TableFilterProxyModel(QObject *parent = nullptr) : QSortFilterProxyModel(parent) {}
void setFilterText(const QString &text)
{
m_filterText = text;
invalidateFilter();
}
protected:
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override
{
if (m_filterText.isEmpty()) return true;
QString t0 = sourceModel()->data(sourceModel()->index(sourceRow, 0, sourceParent)).toString();
QString t1 = sourceModel()->data(sourceModel()->index(sourceRow, 1, sourceParent)).toString();
return t0.contains(m_filterText, Qt::CaseInsensitive) || t1.contains(m_filterText, Qt::CaseInsensitive);
}
private:
QString m_filterText;
};
TableMappingPage::TableMappingPage(QWidget *parent)
: QWidget(parent)
, m_isLoading(false)
, m_isSaving(false)
{
initUI();
loadData();
}
TableMappingPage::~TableMappingPage()
{
}
void TableMappingPage::initUI()
{
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(20, 20, 20, 20);
mainLayout->setSpacing(10);
// 搜索框
QHBoxLayout *searchLayout = new QHBoxLayout;
ElaLineEdit *searchEdit = new ElaLineEdit(this);
searchEdit->setPlaceholderText("搜索 UF20 或 UFT3 表名...");
searchEdit->setClearButtonEnabled(true);
connect(searchEdit, &ElaLineEdit::textChanged, this, &TableMappingPage::onSearchTextChanged);
searchLayout->addWidget(searchEdit);
mainLayout->addLayout(searchLayout);
m_model = new QStandardItemModel(this);
m_model->setColumnCount(2);
m_model->setHorizontalHeaderLabels({"UF20", "UFT3"});
connect(m_model, &QStandardItemModel::itemChanged, this, &TableMappingPage::onModelItemChanged);
m_proxyModel = new TableFilterProxyModel(this);
m_proxyModel->setSourceModel(m_model);
m_tableView = new ElaTableView;
m_tableView->setModel(m_proxyModel);
m_tableView->setSelectionBehavior(QAbstractItemView::SelectRows);
m_tableView->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_tableView->setEditTriggers(QAbstractItemView::DoubleClicked | QAbstractItemView::EditKeyPressed);
m_tableView->verticalHeader()->setVisible(false);
m_tableView->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch);
m_tableView->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch);
mainLayout->addWidget(m_tableView);
QHBoxLayout *btnLayout = new QHBoxLayout;
ElaPushButton *addBtn = new ElaPushButton("添加", this);
ElaPushButton *delBtn = new ElaPushButton("删除选中", this);
ElaPushButton *saveBtn = new ElaPushButton("保存", this);
connect(addBtn, &ElaPushButton::clicked, this, &TableMappingPage::onAddRow);
connect(delBtn, &ElaPushButton::clicked, this, &TableMappingPage::onDeleteRow);
connect(saveBtn, &ElaPushButton::clicked, this, &TableMappingPage::onSave);
btnLayout->addWidget(addBtn);
btnLayout->addWidget(delBtn);
btnLayout->addStretch();
btnLayout->addWidget(saveBtn);
mainLayout->addLayout(btnLayout);
QHBoxLayout *tipLayout = new QHBoxLayout;
QLabel *tipLabel = new QLabel("💡 双击表格修改数据");
tipLabel->setStyleSheet("color: #666; font-size: 13px;");
tipLayout->addWidget(tipLabel);
tipLayout->addStretch();
mainLayout->addLayout(tipLayout);
// 文件监控:检测 cust.json 外部修改后自动刷新
m_fileWatcher = new QFileSystemWatcher(this);
connect(m_fileWatcher, &QFileSystemWatcher::fileChanged, this, &TableMappingPage::onFileChanged);
}
void TableMappingPage::loadData()
{
m_isLoading = true;
m_model->removeRows(0, m_model->rowCount());
QJsonObject custJson = readCustJson();
if (custJson.contains("tableCompareUf2_Uf3")) {
QJsonObject dataObj = custJson["tableCompareUf2_Uf3"].toObject();
QStringList keys = dataObj.keys();
for (const QString &key : keys) {
QList<QStandardItem*> rowItems;
rowItems.append(new QStandardItem(key));
rowItems.append(new QStandardItem(dataObj[key].toString()));
m_model->appendRow(rowItems);
}
}
m_isLoading = false;
// 设置文件监控
QString jsonPath = getCustJsonPath();
if (!m_fileWatcher->files().contains(jsonPath))
m_fileWatcher->addPath(jsonPath);
}
QString TableMappingPage::getCustJsonPath() const
{
return Constants::custJson();
}
QJsonObject TableMappingPage::readCustJson()
{
QString jsonPath = getCustJsonPath();
QFile file(jsonPath);
if (!file.open(QIODevice::ReadOnly)) {
LogManager::instance()->logError("无法读取 cust.json: " + jsonPath);
return QJsonObject();
}
QByteArray data = file.readAll();
file.close();
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError) {
LogManager::instance()->logError("cust.json 解析失败: " + error.errorString());
return QJsonObject();
}
return doc.object();
}
// 从原始 JSON 文本中提取顶层 key 的顺序
static QStringList extractTopLevelKeyOrder(const QString &rawJson)
{
QStringList keys;
QRegularExpression re("\"(\\w+)\"\\s*:");
auto it = re.globalMatch(rawJson);
while (it.hasNext()) {
QString key = it.next().captured(1);
if (!keys.contains(key))
keys.append(key);
}
return keys;
}
// 对 JSON 字符串进行缩进处理
static QString indentJsonValue(const QString &json)
{
if (json.isEmpty()) return json;
QStringList lines = json.split('\n');
QString result;
for (const QString &line : lines) {
if (!line.trimmed().isEmpty())
result += " " + line + "\n";
}
return result;
}
// 写入 JSON 值,根据类型选择合适的格式
static QString formatJsonValue(const QJsonValue &value)
{
if (value.isObject()) {
QJsonDocument doc(value.toObject());
QString raw = QString::fromUtf8(doc.toJson(QJsonDocument::Indented));
return indentJsonValue(raw);
} else if (value.isArray()) {
QJsonDocument doc(value.toArray());
QString raw = QString::fromUtf8(doc.toJson(QJsonDocument::Indented));
return indentJsonValue(raw);
} else if (value.isString()) {
QString escaped = value.toString();
escaped.replace("\\", "\\\\");
escaped.replace("\"", "\\\"");
escaped.replace("\n", "\\n");
escaped.replace("\r", "\\r");
escaped.replace("\t", "\\t");
return "\"" + escaped + "\"";
} else if (value.isBool()) {
return value.toBool() ? "true" : "false";
} else if (value.isDouble()) {
double d = value.toDouble();
if (d == static_cast<int>(d))
return QString::number(static_cast<int>(d));
return QString::number(d, 'f');
}
return "null";
}
// 按顺序格式化一个 string->string 键值对的嵌套 JSON 对象
static QString formatOrderedJsonObject(const QList<QPair<QString, QString>> &pairs)
{
QString result = "{\n";
bool first = true;
for (const auto &pair : pairs) {
if (!first) result += ",\n";
first = false;
QString ek = pair.first;
ek.replace("\\", "\\\\"); ek.replace("\"", "\\\"");
QString ev = pair.second;
ev.replace("\\", "\\\\"); ev.replace("\"", "\\\"");
ev.replace("\n", "\\n"); ev.replace("\r", "\\r"); ev.replace("\t", "\\t");
result += " \"" + ek + "\": \"" + ev + "\"";
}
result += "\n }";
return result;
}
bool TableMappingPage::writeCustJson(const QJsonObject &json, const QStringList &keyOrder,
const QString &orderedKey, const QList<QPair<QString, QString>> &orderedData)
{
QString jsonPath = getCustJsonPath();
QFile file(jsonPath);
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
LogManager::instance()->logError("无法写入 cust.json: " + jsonPath);
return false;
}
QTextStream out(&file);
out << "{\n";
bool first = true;
for (const QString &key : keyOrder) {
if (!json.contains(key)) continue;
if (!first) out << ",\n";
first = false;
out << " \"" << key << "\": ";
if (key == orderedKey) {
out << formatOrderedJsonObject(orderedData);
} else {
out << formatJsonValue(json[key]);
}
}
out << "\n}\n";
file.close();
return true;
}
void TableMappingPage::onAddRow()
{
QDialog dialog(this);
dialog.setWindowTitle("添加表映射");
dialog.resize(450, 180);
QVBoxLayout *layout = new QVBoxLayout(&dialog);
layout->setSpacing(12);
QHBoxLayout *keyLayout = new QHBoxLayout;
ElaText *keyLabel = new ElaText("UF20:", &dialog);
keyLabel->setTextPointSize(10);
keyLayout->addWidget(keyLabel);
ElaLineEdit *keyEdit = new ElaLineEdit(&dialog);
keyEdit->setPlaceholderText("输入UF20表名");
keyLayout->addWidget(keyEdit);
layout->addLayout(keyLayout);
QHBoxLayout *valLayout = new QHBoxLayout;
ElaText *valLabel = new ElaText("UFT3:", &dialog);
valLabel->setTextPointSize(10);
valLayout->addWidget(valLabel);
ElaLineEdit *valEdit = new ElaLineEdit(&dialog);
valEdit->setPlaceholderText("输入UFT3表名");
valLayout->addWidget(valEdit);
layout->addLayout(valLayout);
QHBoxLayout *btnLayout = new QHBoxLayout;
ElaPushButton *okBtn = new ElaPushButton("确定", &dialog);
ElaPushButton *cancelBtn = new ElaPushButton("取消", &dialog);
btnLayout->addStretch();
btnLayout->addWidget(okBtn);
btnLayout->addWidget(cancelBtn);
layout->addLayout(btnLayout);
connect(okBtn, &ElaPushButton::clicked, &dialog, &QDialog::accept);
connect(cancelBtn, &ElaPushButton::clicked, &dialog, &QDialog::reject);
if (dialog.exec() != QDialog::Accepted) return;
QString key = keyEdit->text().trimmed();
QString val = valEdit->text().trimmed();
if (key.isEmpty()) {
QMessageBox::warning(this, "提示", "UF20 表名不能为空!");
return;
}
for (int row = 0; row < m_model->rowCount(); ++row) {
if (m_model->item(row, 0) && m_model->item(row, 0)->text() == key) {
QMessageBox::warning(this, "提示", QString("UF20 表名 \"%1\" 已存在!").arg(key));
return;
}
}
QList<QStandardItem*> rowItems;
rowItems.append(new QStandardItem(key));
rowItems.append(new QStandardItem(val));
m_model->appendRow(rowItems);
}
void TableMappingPage::onDeleteRow()
{
QModelIndexList selectedRows = m_tableView->selectionModel()->selectedRows();
if (selectedRows.isEmpty()) {
QMessageBox::warning(this, "提示", "请先选中要删除的行!");
return;
}
QStringList keyNames;
QList<int> sourceRows;
for (const QModelIndex &idx : selectedRows) {
QModelIndex srcIdx = m_proxyModel->mapToSource(idx);
QStandardItem *keyItem = m_model->item(srcIdx.row(), 0);
if (keyItem) keyNames.append(keyItem->text());
sourceRows.append(srcIdx.row());
}
QString confirmMsg;
if (selectedRows.size() == 1) {
confirmMsg = QString("确定要删除 \"%1\" 这条映射吗?").arg(keyNames.first());
} else {
confirmMsg = QString("确定要删除选中的 %1 条映射吗?").arg(selectedRows.size());
}
QMessageBox::StandardButton reply = QMessageBox::question(this, "确认删除",
confirmMsg, QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes) return;
// 从高行号到低行号删除,避免索引偏移问题
std::sort(sourceRows.begin(), sourceRows.end(), std::greater<int>());
for (int row : sourceRows) {
m_model->removeRow(row);
}
}
void TableMappingPage::onModelItemChanged(QStandardItem *item)
{
if (m_isLoading) return;
Q_UNUSED(item);
}
void TableMappingPage::onSave()
{
// 标记正在保存,忽略文件监控事件
m_isSaving = true;
// 读取原始文件内容以保留顶层 key 顺序
QString jsonPath = getCustJsonPath();
QFile file(jsonPath);
if (!file.open(QIODevice::ReadOnly)) {
QMessageBox::critical(this, "错误", "无法读取 cust.json!");
return;
}
QString rawJson = QString::fromUtf8(file.readAll());
file.close();
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(rawJson.toUtf8(), &error);
if (error.error != QJsonParseError::NoError) {
QMessageBox::critical(this, "错误", "cust.json 解析失败: " + error.errorString());
return;
}
QJsonObject custJson = doc.object();
// 提取原始 key 顺序
QStringList keyOrder = extractTopLevelKeyOrder(rawJson);
// 构建有序的表替换数据
QJsonObject tableCompareObj;
QList<QPair<QString, QString>> tableData;
for (int row = 0; row < m_model->rowCount(); ++row) {
QStandardItem *keyItem = m_model->item(row, 0);
QStandardItem *valItem = m_model->item(row, 1);
if (keyItem && !keyItem->text().trimmed().isEmpty()) {
QString k = keyItem->text().trimmed();
QString v = valItem ? valItem->text().trimmed() : "";
tableCompareObj[k] = v;
tableData.append({k, v});
}
}
custJson["tableCompareUf2_Uf3"] = tableCompareObj;
if (writeCustJson(custJson, keyOrder, "tableCompareUf2_Uf3", tableData)) {
LogManager::instance()->logInfo("表映射配置已保存到 cust.json");
QMessageBox::information(this, "提示", "保存成功!");
} else {
QMessageBox::critical(this, "错误", "保存到 cust.json 失败!");
m_isSaving = false;
}
// 延迟重置保存标记,让文件监控忽略本次写入事件
QTimer::singleShot(2000, this, [this]() {
m_isSaving = false;
// 重新添加监控路径
QString path = getCustJsonPath();
if (!m_fileWatcher->files().contains(path))
m_fileWatcher->addPath(path);
});
}
void TableMappingPage::onFileChanged(const QString &path)
{
Q_UNUSED(path);
if (m_isSaving) return;
LogManager::instance()->logInfo("检测到 cust.json 外部修改,自动刷新数据");
loadData();
}
void TableMappingPage::onSearchTextChanged(const QString &text)
{
static_cast<TableFilterProxyModel*>(m_proxyModel)->setFilterText(text);
}
+46
View File
@@ -0,0 +1,46 @@
#ifndef TABLEMAPPINGPAGE_H
#define TABLEMAPPINGPAGE_H
#include <QWidget>
#include <QStandardItemModel>
#include <QFileSystemWatcher>
#include <QSortFilterProxyModel>
#include <QPair>
#include <QList>
#include "ElaTableView.h"
class ElaLineEdit;
class TableMappingPage : public QWidget
{
Q_OBJECT
public:
explicit TableMappingPage(QWidget *parent = nullptr);
~TableMappingPage();
private slots:
void onAddRow();
void onDeleteRow();
void onModelItemChanged(QStandardItem *item);
void onSave();
void onFileChanged(const QString &path);
void onSearchTextChanged(const QString &text);
private:
void initUI();
void loadData();
QJsonObject readCustJson();
bool writeCustJson(const QJsonObject &json, const QStringList &keyOrder,
const QString &orderedKey, const QList<QPair<QString, QString>> &orderedData);
QString getCustJsonPath() const;
ElaTableView *m_tableView;
QStandardItemModel *m_model;
QSortFilterProxyModel *m_proxyModel;
QFileSystemWatcher *m_fileWatcher;
bool m_isLoading;
bool m_isSaving;
};
#endif // TABLEMAPPINGPAGE_H
-26
View File
@@ -1,26 +0,0 @@
#include "helpdocpage.h"
#include <QVBoxLayout>
#include <QLabel>
#include <QDesktopServices>
#include <QUrl>
HelpDocPage::HelpDocPage(QWidget *parent)
: QWidget(parent)
{
initUI();
}
HelpDocPage::~HelpDocPage()
{
}
void HelpDocPage::initUI()
{
QVBoxLayout *layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
QLabel *label = new QLabel("<a href='https://github.com'>点击此处跳转帮助文档</a>");
label->setStyleSheet("font-size: 14px; color: #4a90d9;");
label->setOpenExternalLinks(true);
layout->addWidget(label, 0, Qt::AlignCenter);
}
-34
View File
@@ -1,34 +0,0 @@
#ifndef HELPDOCPAGE_H
#define HELPDOCPAGE_H
#include <QWidget>
#include <QStackedWidget>
#include <QLabel>
#include <QTextBrowser>
#include "ElaTreeView.h"
class HelpDocPage : public QWidget
{
Q_OBJECT
public:
explicit HelpDocPage(QWidget *parent = nullptr);
~HelpDocPage();
private:
void initUI();
void initTreeView();
void initContentPages();
void loadHelpFile();
void updateContent(const QString &section);
QString extractSection(const QString &sectionName);
QString markdownToHtml(const QString &markdown);
ElaTreeView* m_treeView;
QStackedWidget* m_stackedWidget;
QLabel* m_titleLabel;
QTextBrowser* m_contentBrowser;
QString m_helpContent;
};
#endif // HELPDOCPAGE_H
@@ -1,4 +1,5 @@
#include "metadatupdatepage.h"
#include "utils/Constants.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGridLayout>
@@ -15,8 +16,7 @@ MetadataUpdatePage::MetadataUpdatePage(QWidget *parent)
{
initUI();
_processor = new MetadataProcessor(this);
QString basePath = QCoreApplication::applicationDirPath();
_processor->initConfig(basePath);
_processor->initConfig(Constants::toolDir());
}
MetadataUpdatePage::~MetadataUpdatePage()
@@ -261,8 +261,7 @@ void MetadataUpdatePage::updateUf2Interfaces(const QStringList& items)
} else if (item == "标准错误号") {
processUf2ErrNumber();
} else if (item == "接口数据") {
QString basePath = QCoreApplication::applicationDirPath();
processUf2Interface(basePath);
processUf2Interface(Constants::toolDir());
interfaceUpdated = true;
}
}
@@ -298,8 +297,7 @@ void MetadataUpdatePage::updateUft3Interfaces(const QStringList& items)
} else if (item == "表结构") {
processUft3Table();
} else if (item == "接口数据") {
QString basePath = QCoreApplication::applicationDirPath();
processUft3Interface(basePath);
processUft3Interface(Constants::toolDir());
interfaceUpdated = true;
}
}
+2 -1
View File
@@ -1,4 +1,5 @@
#include "settingspage.h"
#include "utils/Constants.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
@@ -142,7 +143,7 @@ void SettingsPage::loadSettings()
QString SettingsPage::getConfigIniPath()
{
return QCoreApplication::applicationDirPath() + "/uf2touft3/config.ini";
return Constants::configIni();
}
bool SettingsPage::isValidUtf8(const QByteArray &data)