feat: Add configuration manager (#270)

This commit is contained in:
Petr Mironychev
2025-11-20 17:31:13 +01:00
committed by GitHub
parent 6f7d8a0987
commit 1e3b1997cc
16 changed files with 880 additions and 9 deletions

View File

@ -1,6 +1,7 @@
add_library(QodeAssistSettings STATIC
GeneralSettings.hpp GeneralSettings.cpp
CustomPromptSettings.hpp CustomPromptSettings.cpp
ConfigurationManager.hpp ConfigurationManager.cpp
SettingsUtils.hpp
SettingsConstants.hpp
ButtonAspect.hpp

View File

@ -0,0 +1,237 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "ConfigurationManager.hpp"
#include <QDir>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QRegularExpression>
#include <QUuid>
#include <coreplugin/icore.h>
#include "Logger.hpp"
namespace QodeAssist::Settings {
ConfigurationManager::ConfigurationManager(QObject *parent)
: QObject(parent)
{}
ConfigurationManager &ConfigurationManager::instance()
{
static ConfigurationManager instance;
return instance;
}
QString ConfigurationManager::configurationTypeToString(ConfigurationType type) const
{
switch (type) {
case ConfigurationType::CodeCompletion:
return "code_completion";
case ConfigurationType::Chat:
return "chat";
case ConfigurationType::QuickRefactor:
return "quick_refactor";
}
return "unknown";
}
QString ConfigurationManager::getConfigurationDirectory(ConfigurationType type) const
{
QString path = QString("%1/qodeassist/configurations/%2")
.arg(Core::ICore::userResourcePath().toFSPathString(),
configurationTypeToString(type));
return path;
}
bool ConfigurationManager::ensureDirectoryExists(ConfigurationType type) const
{
QDir dir(getConfigurationDirectory(type));
if (!dir.exists()) {
return dir.mkpath(".");
}
return true;
}
bool ConfigurationManager::loadConfigurations(ConfigurationType type)
{
QVector<AIConfiguration> *configs = nullptr;
switch (type) {
case ConfigurationType::CodeCompletion:
configs = &m_ccConfigurations;
break;
case ConfigurationType::Chat:
configs = &m_caConfigurations;
break;
case ConfigurationType::QuickRefactor:
configs = &m_qrConfigurations;
break;
}
if (!configs) {
return false;
}
configs->clear();
if (!ensureDirectoryExists(type)) {
LOG_MESSAGE("Failed to create configuration directory");
return false;
}
QDir dir(getConfigurationDirectory(type));
QStringList filters;
filters << "*.json";
QFileInfoList files = dir.entryInfoList(filters, QDir::Files);
for (const QFileInfo &fileInfo : files) {
QFile file(fileInfo.absoluteFilePath());
if (!file.open(QIODevice::ReadOnly)) {
LOG_MESSAGE(QString("Failed to open configuration file: %1").arg(fileInfo.fileName()));
continue;
}
QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
file.close();
if (!doc.isObject()) {
LOG_MESSAGE(QString("Invalid configuration file: %1").arg(fileInfo.fileName()));
continue;
}
QJsonObject obj = doc.object();
AIConfiguration config;
config.id = obj["id"].toString();
config.name = obj["name"].toString();
config.provider = obj["provider"].toString();
config.model = obj["model"].toString();
config.templateName = obj["template"].toString();
config.url = obj["url"].toString();
config.endpointMode = obj["endpointMode"].toString();
config.customEndpoint = obj["customEndpoint"].toString();
config.type = type;
config.formatVersion = obj.value("formatVersion").toInt(1);
if (config.id.isEmpty() || config.name.isEmpty()) {
LOG_MESSAGE(QString("Invalid configuration data in file: %1").arg(fileInfo.fileName()));
continue;
}
configs->append(config);
}
emit configurationsChanged(type);
return true;
}
bool ConfigurationManager::saveConfiguration(const AIConfiguration &config)
{
if (!ensureDirectoryExists(config.type)) {
LOG_MESSAGE("Failed to create configuration directory");
return false;
}
QJsonObject obj;
obj["formatVersion"] = config.formatVersion;
obj["id"] = config.id;
obj["name"] = config.name;
obj["provider"] = config.provider;
obj["model"] = config.model;
obj["template"] = config.templateName;
obj["url"] = config.url;
obj["endpointMode"] = config.endpointMode;
obj["customEndpoint"] = config.customEndpoint;
QString sanitizedName = config.name;
sanitizedName.replace(" ", "_");
sanitizedName.replace(QRegularExpression("[^a-zA-Z0-9_-]"), "");
QString fileName = QString("%1/%2_%3.json")
.arg(getConfigurationDirectory(config.type), sanitizedName, config.id);
QFile file(fileName);
if (!file.open(QIODevice::WriteOnly)) {
LOG_MESSAGE(QString("Failed to create configuration file: %1").arg(fileName));
return false;
}
QJsonDocument doc(obj);
file.write(doc.toJson(QJsonDocument::Indented));
file.close();
loadConfigurations(config.type);
return true;
}
bool ConfigurationManager::deleteConfiguration(const QString &id, ConfigurationType type)
{
QDir dir(getConfigurationDirectory(type));
QStringList filters;
filters << QString("*_%1.json").arg(id);
QFileInfoList files = dir.entryInfoList(filters, QDir::Files);
if (files.isEmpty()) {
LOG_MESSAGE(QString("Configuration file not found for id: %1").arg(id));
return false;
}
for (const QFileInfo &fileInfo : files) {
QFile file(fileInfo.absoluteFilePath());
if (!file.remove()) {
LOG_MESSAGE(QString("Failed to delete configuration file: %1")
.arg(fileInfo.absoluteFilePath()));
return false;
}
}
loadConfigurations(type);
return true;
}
QVector<AIConfiguration> ConfigurationManager::configurations(ConfigurationType type) const
{
switch (type) {
case ConfigurationType::CodeCompletion:
return m_ccConfigurations;
case ConfigurationType::Chat:
return m_caConfigurations;
case ConfigurationType::QuickRefactor:
return m_qrConfigurations;
}
return {};
}
AIConfiguration ConfigurationManager::getConfigurationById(const QString &id,
ConfigurationType type) const
{
const QVector<AIConfiguration> &configs = configurations(type);
for (const AIConfiguration &config : configs) {
if (config.id == id) {
return config;
}
}
return AIConfiguration();
}
} // namespace QodeAssist::Settings

View File

@ -0,0 +1,78 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QObject>
#include <QString>
#include <QVector>
namespace QodeAssist::Settings {
enum class ConfigurationType { CodeCompletion, Chat, QuickRefactor };
inline constexpr int CONFIGURATION_FORMAT_VERSION = 1;
struct AIConfiguration
{
QString id;
QString name;
QString provider;
QString model;
QString templateName;
QString url;
QString endpointMode;
QString customEndpoint;
ConfigurationType type;
int formatVersion = CONFIGURATION_FORMAT_VERSION;
};
class ConfigurationManager : public QObject
{
Q_OBJECT
public:
static ConfigurationManager &instance();
bool loadConfigurations(ConfigurationType type);
bool saveConfiguration(const AIConfiguration &config);
bool deleteConfiguration(const QString &id, ConfigurationType type);
QVector<AIConfiguration> configurations(ConfigurationType type) const;
AIConfiguration getConfigurationById(const QString &id, ConfigurationType type) const;
QString getConfigurationDirectory(ConfigurationType type) const;
signals:
void configurationsChanged(ConfigurationType type);
private:
explicit ConfigurationManager(QObject *parent = nullptr);
~ConfigurationManager() override = default;
bool ensureDirectoryExists(ConfigurationType type) const;
QString configurationTypeToString(ConfigurationType type) const;
QVector<AIConfiguration> m_ccConfigurations;
QVector<AIConfiguration> m_caConfigurations;
QVector<AIConfiguration> m_qrConfigurations;
};
} // namespace QodeAssist::Settings

View File

@ -23,11 +23,16 @@
#include <coreplugin/icore.h>
#include <utils/layoutbuilder.h>
#include <utils/utilsicons.h>
#include <QDesktopServices>
#include <QDir>
#include <QInputDialog>
#include <QLabel>
#include <QMessageBox>
#include <QPushButton>
#include <QTextEdit>
#include <QTimer>
#include <QUrl>
#include <QUuid>
#include <QtWidgets/qboxlayout.h>
#include <QtWidgets/qcompleter.h>
#include <QtWidgets/qgroupbox.h>
@ -35,6 +40,7 @@
#include <QtWidgets/qstackedwidget.h>
#include "../Version.hpp"
#include "ConfigurationManager.hpp"
#include "Logger.hpp"
#include "SettingsConstants.hpp"
#include "SettingsDialog.hpp"
@ -119,6 +125,12 @@ GeneralSettings::GeneralSettings()
ccTemplateDescription.setReadOnly(true);
ccTemplateDescription.setDefaultValue("");
ccSaveConfig.m_buttonText = TrConstants::SAVE_CONFIG;
ccLoadConfig.m_buttonText = TrConstants::LOAD_CONFIG;
ccOpenConfigFolder.m_buttonText = TrConstants::OPEN_CONFIG_FOLDER;
ccOpenConfigFolder.m_icon = Utils::Icons::OPENFILE.icon();
ccOpenConfigFolder.m_isCompact = true;
// preset1
specifyPreset1.setSettingsKey(Constants::CC_SPECIFY_PRESET1);
specifyPreset1.setLabelText(TrConstants::ADD_NEW_PRESET);
@ -204,6 +216,12 @@ GeneralSettings::GeneralSettings()
caTemplateDescription.setReadOnly(true);
caTemplateDescription.setDefaultValue("");
caSaveConfig.m_buttonText = TrConstants::SAVE_CONFIG;
caLoadConfig.m_buttonText = TrConstants::LOAD_CONFIG;
caOpenConfigFolder.m_buttonText = TrConstants::OPEN_CONFIG_FOLDER;
caOpenConfigFolder.m_icon = Utils::Icons::OPENFILE.icon();
caOpenConfigFolder.m_isCompact = true;
// quick refactor settings
initStringAspect(qrProvider, Constants::QR_PROVIDER, TrConstants::PROVIDER, "Ollama");
qrProvider.setReadOnly(true);
@ -242,6 +260,12 @@ GeneralSettings::GeneralSettings()
qrTemplateDescription.setReadOnly(true);
qrTemplateDescription.setDefaultValue("");
qrSaveConfig.m_buttonText = TrConstants::SAVE_CONFIG;
qrLoadConfig.m_buttonText = TrConstants::LOAD_CONFIG;
qrOpenConfigFolder.m_buttonText = TrConstants::OPEN_CONFIG_FOLDER;
qrOpenConfigFolder.m_icon = Utils::Icons::OPENFILE.icon();
qrOpenConfigFolder.m_isCompact = true;
ccShowTemplateInfo.m_icon = Utils::Icons::INFO.icon();
ccShowTemplateInfo.m_tooltip = Tr::tr("Show template information");
ccShowTemplateInfo.m_isCompact = true;
@ -300,15 +324,18 @@ GeneralSettings::GeneralSettings()
auto ccGroup = Group{
title(TrConstants::CODE_COMPLETION),
Column{
Row{ccSaveConfig, ccLoadConfig, ccOpenConfigFolder, Stretch{1}},
ccGrid,
Row{specifyPreset1, preset1Language, Stretch{1}},
ccPreset1Grid}};
auto caGroup = Group{
title(TrConstants::CHAT_ASSISTANT), Column{caGrid}};
title(TrConstants::CHAT_ASSISTANT),
Column{Row{caSaveConfig, caLoadConfig, caOpenConfigFolder, Stretch{1}}, caGrid}};
auto qrGroup = Group{
title(TrConstants::QUICK_REFACTOR), Column{qrGrid}};
title(TrConstants::QUICK_REFACTOR),
Column{Row{qrSaveConfig, qrLoadConfig, qrOpenConfigFolder, Stretch{1}}, qrGrid}};
auto rootLayout = Column{
Row{enableQodeAssist, Stretch{1}, Row{checkUpdate, resetToDefaults}},
@ -417,7 +444,7 @@ void GeneralSettings::showModelsNotSupportedDialog(Utils::StringAspect &aspect)
QString key = QString("CompleterHistory/")
.append(
(&aspect == &ccModel) ? Constants::CC_MODEL_HISTORY
(&aspect == &ccModel) ? Constants::CC_MODEL_HISTORY
: (&aspect == &caModel) ? Constants::CA_MODEL_HISTORY
: Constants::QR_MODEL_HISTORY);
#if QODEASSIST_QT_CREATOR_VERSION >= QT_VERSION_CHECK(18, 0, 0)
@ -494,7 +521,8 @@ void GeneralSettings::showUrlSelectionDialog(
dialog.exec();
}
void GeneralSettings::showTemplateInfoDialog(const Utils::StringAspect &descriptionAspect, const QString &templateName)
void GeneralSettings::showTemplateInfoDialog(
const Utils::StringAspect &descriptionAspect, const QString &templateName)
{
SettingsDialog dialog(Tr::tr("Template Information"));
dialog.addLabel(QString("<b>%1:</b> %2").arg(Tr::tr("Template"), templateName));
@ -575,6 +603,48 @@ void GeneralSettings::setupConnections()
connect(&qrShowTemplateInfo, &ButtonAspect::clicked, this, [this]() {
showTemplateInfoDialog(qrTemplateDescription, qrTemplate.value());
});
connect(&ccSaveConfig, &ButtonAspect::clicked, this, [this]() { onSaveConfiguration("cc"); });
connect(&ccLoadConfig, &ButtonAspect::clicked, this, [this]() { onLoadConfiguration("cc"); });
connect(&caSaveConfig, &ButtonAspect::clicked, this, [this]() { onSaveConfiguration("ca"); });
connect(&caLoadConfig, &ButtonAspect::clicked, this, [this]() { onLoadConfiguration("ca"); });
connect(&qrSaveConfig, &ButtonAspect::clicked, this, [this]() { onSaveConfiguration("qr"); });
connect(&qrLoadConfig, &ButtonAspect::clicked, this, [this]() { onLoadConfiguration("qr"); });
connect(&ccOpenConfigFolder, &ButtonAspect::clicked, this, [this]() {
auto &manager = ConfigurationManager::instance();
QString path = manager.getConfigurationDirectory(ConfigurationType::CodeCompletion);
QDir dir(path);
if (!dir.exists()) {
dir.mkpath(".");
}
QUrl url = QUrl::fromLocalFile(dir.absolutePath());
QDesktopServices::openUrl(url);
});
connect(&caOpenConfigFolder, &ButtonAspect::clicked, this, [this]() {
auto &manager = ConfigurationManager::instance();
QString path = manager.getConfigurationDirectory(ConfigurationType::Chat);
QDir dir(path);
if (!dir.exists()) {
dir.mkpath(".");
}
QUrl url = QUrl::fromLocalFile(dir.absolutePath());
QDesktopServices::openUrl(url);
});
connect(&qrOpenConfigFolder, &ButtonAspect::clicked, this, [this]() {
auto &manager = ConfigurationManager::instance();
QString path = manager.getConfigurationDirectory(ConfigurationType::QuickRefactor);
QDir dir(path);
if (!dir.exists()) {
dir.mkpath(".");
}
QUrl url = QUrl::fromLocalFile(dir.absolutePath());
QDesktopServices::openUrl(url);
});
}
void GeneralSettings::resetPageToDefaults()
@ -620,6 +690,176 @@ void GeneralSettings::resetPageToDefaults()
}
}
void GeneralSettings::onSaveConfiguration(const QString &prefix)
{
bool ok;
QString configName = QInputDialog::getText(
Core::ICore::dialogParent(),
TrConstants::SAVE_CONFIGURATION,
TrConstants::CONFIGURATION_NAME,
QLineEdit::Normal,
QString(),
&ok);
if (!ok || configName.trimmed().isEmpty()) {
return;
}
AIConfiguration config;
config.id = QUuid::createUuid().toString(QUuid::WithoutBraces);
config.name = configName.trimmed();
if (prefix == "cc") {
config.provider = ccProvider.value();
config.model = ccModel.value();
config.templateName = ccTemplate.value();
config.url = ccUrl.value();
config.endpointMode = ccEndpointMode.stringValue();
config.customEndpoint = ccCustomEndpoint.value();
config.type = ConfigurationType::CodeCompletion;
} else if (prefix == "ca") {
config.provider = caProvider.value();
config.model = caModel.value();
config.templateName = caTemplate.value();
config.url = caUrl.value();
config.endpointMode = caEndpointMode.stringValue();
config.customEndpoint = caCustomEndpoint.value();
config.type = ConfigurationType::Chat;
} else if (prefix == "qr") {
config.provider = qrProvider.value();
config.model = qrModel.value();
config.templateName = qrTemplate.value();
config.url = qrUrl.value();
config.endpointMode = qrEndpointMode.stringValue();
config.customEndpoint = qrCustomEndpoint.value();
config.type = ConfigurationType::QuickRefactor;
}
auto &manager = ConfigurationManager::instance();
if (manager.saveConfiguration(config)) {
QMessageBox::information(
Core::ICore::dialogParent(),
TrConstants::SAVE_CONFIGURATION,
TrConstants::CONFIGURATION_SAVED);
} else {
QMessageBox::warning(
Core::ICore::dialogParent(),
TrConstants::SAVE_CONFIGURATION,
Tr::tr("Failed to save configuration. Check logs for details."));
}
}
void GeneralSettings::onLoadConfiguration(const QString &prefix)
{
ConfigurationType type;
if (prefix == "cc") {
type = ConfigurationType::CodeCompletion;
} else if (prefix == "ca") {
type = ConfigurationType::Chat;
} else if (prefix == "qr") {
type = ConfigurationType::QuickRefactor;
} else {
return;
}
auto &manager = ConfigurationManager::instance();
manager.loadConfigurations(type);
QVector<AIConfiguration> configs = manager.configurations(type);
if (configs.isEmpty()) {
QMessageBox::information(
Core::ICore::dialogParent(),
TrConstants::LOAD_CONFIGURATION,
TrConstants::NO_CONFIGURATIONS_FOUND);
return;
}
SettingsDialog dialog(TrConstants::LOAD_CONFIGURATION);
dialog.addLabel(TrConstants::SELECT_CONFIGURATION);
dialog.addSpacing();
QStringList configNames;
for (const AIConfiguration &config : configs) {
configNames.append(config.name);
}
auto configList = dialog.addComboBox(configNames, QString());
dialog.addSpacing();
auto *deleteButton = new QPushButton(TrConstants::DELETE_CONFIGURATION);
auto *okButton = new QPushButton(TrConstants::OK);
auto *cancelButton = new QPushButton(TrConstants::CANCEL);
connect(deleteButton, &QPushButton::clicked, &dialog, [&]() {
int currentIndex = configList->currentIndex();
if (currentIndex >= 0 && currentIndex < configs.size()) {
QMessageBox::StandardButton reply = QMessageBox::question(
&dialog,
TrConstants::DELETE_CONFIGURATION,
TrConstants::CONFIRM_DELETE_CONFIG,
QMessageBox::Yes | QMessageBox::No);
if (reply == QMessageBox::Yes) {
const AIConfiguration &configToDelete = configs[currentIndex];
if (manager.deleteConfiguration(configToDelete.id, type)) {
dialog.accept();
onLoadConfiguration(prefix);
} else {
QMessageBox::warning(
&dialog,
TrConstants::DELETE_CONFIGURATION,
Tr::tr("Failed to delete configuration."));
}
}
}
});
connect(okButton, &QPushButton::clicked, &dialog, [&]() {
int currentIndex = configList->currentIndex();
if (currentIndex >= 0 && currentIndex < configs.size()) {
const AIConfiguration &config = configs[currentIndex];
if (prefix == "cc") {
ccProvider.setValue(config.provider);
ccModel.setValue(config.model);
ccTemplate.setValue(config.templateName);
ccUrl.setValue(config.url);
ccEndpointMode.setValue(ccEndpointMode.indexForDisplay(config.endpointMode));
ccCustomEndpoint.setValue(config.customEndpoint);
} else if (prefix == "ca") {
caProvider.setValue(config.provider);
caModel.setValue(config.model);
caTemplate.setValue(config.templateName);
caUrl.setValue(config.url);
caEndpointMode.setValue(caEndpointMode.indexForDisplay(config.endpointMode));
caCustomEndpoint.setValue(config.customEndpoint);
} else if (prefix == "qr") {
qrProvider.setValue(config.provider);
qrModel.setValue(config.model);
qrTemplate.setValue(config.templateName);
qrUrl.setValue(config.url);
qrEndpointMode.setValue(qrEndpointMode.indexForDisplay(config.endpointMode));
qrCustomEndpoint.setValue(config.customEndpoint);
}
writeSettings();
QMessageBox::information(
Core::ICore::dialogParent(),
TrConstants::LOAD_CONFIGURATION,
TrConstants::CONFIGURATION_LOADED);
dialog.accept();
}
});
connect(cancelButton, &QPushButton::clicked, &dialog, &QDialog::reject);
dialog.buttonLayout()->addWidget(deleteButton);
addDialogButtons(dialog.buttonLayout(), okButton, cancelButton);
configList->setFocus();
dialog.exec();
}
class GeneralSettingsPage : public Core::IOptionsPage
{
public:

View File

@ -66,6 +66,10 @@ public:
Utils::StringAspect ccTemplateDescription{this};
ButtonAspect ccSaveConfig{this};
ButtonAspect ccLoadConfig{this};
ButtonAspect ccOpenConfigFolder{this};
// TODO create dynamic presets system
// preset1 for code completion settings
Utils::BoolAspect specifyPreset1{this};
@ -107,6 +111,10 @@ public:
Utils::StringAspect caTemplateDescription{this};
ButtonAspect caSaveConfig{this};
ButtonAspect caLoadConfig{this};
ButtonAspect caOpenConfigFolder{this};
// quick refactor settings
Utils::StringAspect qrProvider{this};
ButtonAspect qrSelectProvider{this};
@ -128,6 +136,10 @@ public:
Utils::StringAspect qrTemplateDescription{this};
ButtonAspect qrSaveConfig{this};
ButtonAspect qrLoadConfig{this};
ButtonAspect qrOpenConfigFolder{this};
ButtonAspect ccShowTemplateInfo{this};
ButtonAspect caShowTemplateInfo{this};
ButtonAspect qrShowTemplateInfo{this};
@ -148,6 +160,9 @@ public:
void updatePreset1Visiblity(bool state);
void onSaveConfiguration(const QString &prefix);
void onLoadConfiguration(const QString &prefix);
private:
void setupConnections();
void resetPageToDefaults();

View File

@ -99,6 +99,19 @@ inline const char AUTO_COMPLETION_SETTINGS[]
inline const char ADD_NEW_PRESET[]
= QT_TRANSLATE_NOOP("QtC::QodeAssist", "Add new preset for language");
inline const char SAVE_CONFIG[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Save Config...");
inline const char LOAD_CONFIG[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Load Config...");
inline const char OPEN_CONFIG_FOLDER[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Open Folder");
inline const char SAVE_CONFIGURATION[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Save Configuration");
inline const char LOAD_CONFIGURATION[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Load Configuration");
inline const char CONFIGURATION_NAME[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Configuration name:");
inline const char SELECT_CONFIGURATION[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Select Configuration");
inline const char NO_CONFIGURATIONS_FOUND[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "No saved configurations found.");
inline const char CONFIGURATION_SAVED[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Configuration saved successfully.");
inline const char CONFIGURATION_LOADED[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Configuration loaded successfully.");
inline const char DELETE_CONFIGURATION[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Delete");
inline const char CONFIRM_DELETE_CONFIG[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Are you sure you want to delete this configuration?");
} // namespace TrConstants
struct Tr