diff --git a/adapters/describe.json b/adapters/describe.json index eee875ff..91b07ab6 100644 --- a/adapters/describe.json +++ b/adapters/describe.json @@ -12,6 +12,7 @@ { "id": 1, "ip": "127.0.0.1", + "name": "Connection 1", "persistent": true, "port": 502, "timeout": 1000, @@ -21,6 +22,7 @@ "baudrate": 115200, "databits": 8, "id": 2, + "name": "Connection 2", "parity": "N", "persistent": true, "portName": "COM1", @@ -147,6 +149,11 @@ "title": "Connection ID", "type": "integer" }, + "name": { + "description": "Human-readable connection name (default: \"Connection \")", + "title": "Connection Name", + "type": "string" + }, "persistent": { "description": "Keep connection open between reads", "title": "Keep Connection Persistent", @@ -258,5 +265,5 @@ }, "type": "object" }, - "version": "0.0.1-master" + "version": "0.0.1-master:ac7eec7" } diff --git a/adapters/dummymodbusadapter b/adapters/dummymodbusadapter index ebd45a28..75c0788d 100755 Binary files a/adapters/dummymodbusadapter and b/adapters/dummymodbusadapter differ diff --git a/adapters/dummymodbusadapter.exe b/adapters/dummymodbusadapter.exe index 0364adb6..6d02031b 100644 Binary files a/adapters/dummymodbusadapter.exe and b/adapters/dummymodbusadapter.exe differ diff --git a/adapters/json-rpc-spec.md b/adapters/json-rpc-spec.md index 08486cd4..cba7ed37 100644 --- a/adapters/json-rpc-spec.md +++ b/adapters/json-rpc-spec.md @@ -187,6 +187,7 @@ Adapter-wide settings. Currently empty; reserved for future use. | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | integer | yes | Connection index: `0`, `1`, or `2` | +| `name` | string | no | Human-readable label (default: `"Connection "`) | | `type` | string | yes | `"tcp"` or `"serial"` | | `timeout` | integer | no | Timeout in milliseconds (default: `1000`) | | `persistent` | boolean | no | Keep connection open between reads (default: `false`) | diff --git a/adapters/modbusadapter b/adapters/modbusadapter index a902a24e..e326628e 100755 Binary files a/adapters/modbusadapter and b/adapters/modbusadapter differ diff --git a/adapters/modbusadapter.exe b/adapters/modbusadapter.exe index d3a960af..fda4ef67 100644 Binary files a/adapters/modbusadapter.exe and b/adapters/modbusadapter.exe differ diff --git a/src/customwidgets/addabletabwidget.cpp b/src/customwidgets/addabletabwidget.cpp index c0078908..09eb9e1b 100644 --- a/src/customwidgets/addabletabwidget.cpp +++ b/src/customwidgets/addabletabwidget.cpp @@ -21,9 +21,8 @@ void AddableTabWidget::handleCloseTab(int index) return; } - emit tabClosed(index); - QWidget* page = widget(index); + emit tabClosed(page); removeTab(index); if (page) { diff --git a/src/customwidgets/addabletabwidget.h b/src/customwidgets/addabletabwidget.h index d73e48cc..d5631ba1 100644 --- a/src/customwidgets/addabletabwidget.h +++ b/src/customwidgets/addabletabwidget.h @@ -22,7 +22,7 @@ class AddableTabWidget : public QTabWidget QWidget* tabContent(int index) const; signals: - void tabClosed(int index); + void tabClosed(QWidget* widget); void addTabRequested(); // Emitted when user clicks the "+" button public slots: diff --git a/src/customwidgets/deviceconfigtab.cpp b/src/customwidgets/deviceconfigtab.cpp index 11ce6418..1c562c8e 100644 --- a/src/customwidgets/deviceconfigtab.cpp +++ b/src/customwidgets/deviceconfigtab.cpp @@ -17,14 +17,13 @@ DeviceConfigTab::DeviceConfigTab(SettingsModel* pSettingsModel, const QJsonObject& deviceValues, QWidget* parent) : QWidget(parent), - _pLayout(nullptr), + _pLayout(new QVBoxLayout(this)), _pNameEdit(new QLineEdit(this)), _pAdapterCombo(new QComboBox(this)), _pSchemaForm(nullptr), _pSettingsModel(pSettingsModel), _deviceId(deviceValues.value("id").toInt(-1)) { - _pLayout = new QVBoxLayout(this); setLayout(_pLayout); auto* nameRow = new QHBoxLayout; @@ -33,10 +32,9 @@ DeviceConfigTab::DeviceConfigTab(SettingsModel* pSettingsModel, nameRow->addStretch(); _pLayout->addLayout(nameRow); - int deviceId = deviceValues.value("id").toInt(-1); - if (deviceId >= 0 && pSettingsModel->deviceList().contains(static_cast(deviceId))) + if (_deviceId >= 0 && pSettingsModel->hasDevice(static_cast(_deviceId))) { - _pNameEdit->setText(pSettingsModel->deviceSettings(static_cast(deviceId))->name()); + _pNameEdit->setText(pSettingsModel->deviceSettings(static_cast(_deviceId))->name()); } auto* adapterRow = new QHBoxLayout; @@ -66,6 +64,7 @@ DeviceConfigTab::DeviceConfigTab(SettingsModel* pSettingsModel, _pAdapterCombo->setCurrentIndex(idx); } + connect(_pNameEdit, &QLineEdit::textChanged, this, &DeviceConfigTab::onNameChanged); connect(_pAdapterCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &DeviceConfigTab::onAdapterChanged); @@ -96,15 +95,29 @@ void DeviceConfigTab::onAdapterChanged(int index) } defaultValues["id"] = currentId; + if (_deviceId >= 0 && _pSettingsModel->hasDevice(static_cast(_deviceId))) + { + _pSettingsModel->deviceSettings(static_cast(_deviceId))->setAdapterId(newAdapterId); + } + rebuildSchemaForm(newAdapterId, defaultValues); } +void DeviceConfigTab::onNameChanged(const QString& name) +{ + if (_deviceId >= 0 && _pSettingsModel->hasDevice(static_cast(_deviceId))) + { + _pSettingsModel->deviceSettings(static_cast(_deviceId))->setName(name); + } + emit nameChanged(name); +} + void DeviceConfigTab::rebuildSchemaForm(const QString& adapterId, const QJsonObject& deviceValues) { if (_pSchemaForm) { _pLayout->removeWidget(_pSchemaForm); - delete _pSchemaForm; + _pSchemaForm->deleteLater(); _pSchemaForm = nullptr; } @@ -137,3 +150,8 @@ QString DeviceConfigTab::deviceName() const { return _pNameEdit->text(); } + +int DeviceConfigTab::deviceId() const +{ + return _deviceId; +} diff --git a/src/customwidgets/deviceconfigtab.h b/src/customwidgets/deviceconfigtab.h index 097c43e9..589b6c1c 100644 --- a/src/customwidgets/deviceconfigtab.h +++ b/src/customwidgets/deviceconfigtab.h @@ -46,8 +46,19 @@ class DeviceConfigTab : public QWidget */ QString deviceName() const; + /*! + * \brief Return the device ID assigned to this tab, or -1 if none. + * \return Device ID as int. + */ + int deviceId() const; + +signals: + //! Emitted when the device name field changes. + void nameChanged(const QString& name); + private slots: void onAdapterChanged(int index); + void onNameChanged(const QString& name); private: void rebuildSchemaForm(const QString& adapterId, const QJsonObject& deviceValues); diff --git a/src/customwidgets/deviceform.cpp b/src/customwidgets/deviceform.cpp index e74c01d5..9764bed2 100644 --- a/src/customwidgets/deviceform.cpp +++ b/src/customwidgets/deviceform.cpp @@ -1,8 +1,8 @@ #include "deviceform.h" #include "ui_deviceform.h" -DeviceForm::DeviceForm(SettingsModel* pSettingsModel, deviceId_t _deviceId, QWidget* parent) - : QWidget(parent), _pUi(new Ui::DeviceForm), _pSettingsModel(pSettingsModel), _deviceId(_deviceId) +DeviceForm::DeviceForm(SettingsModel* pSettingsModel, deviceId_t deviceId, QWidget* parent) + : QWidget(parent), _pUi(new Ui::DeviceForm), _pSettingsModel(pSettingsModel), _deviceId(deviceId) { _pUi->setupUi(this); @@ -11,11 +11,7 @@ DeviceForm::DeviceForm(SettingsModel* pSettingsModel, deviceId_t _deviceId, QWid _pUi->lineName->setText(dev->name()); _pUi->spinId->setValue(static_cast(this->_deviceId)); - connect(_pUi->lineName, &QLineEdit::textChanged, this, [this](const QString& newName) { - Device* device = _pSettingsModel->deviceSettings(this->_deviceId); - device->setName(newName); - emit deviceIdentifiersChanged(this->_deviceId); - }); + connect(_pUi->lineName, &QLineEdit::textChanged, this, &DeviceForm::onNameChanged); connect(_pUi->spinId, QOverload::of(&QSpinBox::valueChanged), this, [this](int newId) { _pSettingsModel->updateDeviceId(this->_deviceId, static_cast(newId)); this->_deviceId = newId; @@ -32,3 +28,10 @@ deviceId_t DeviceForm::deviceId() { return _deviceId; } + +void DeviceForm::onNameChanged(const QString& newName) +{ + Device* device = _pSettingsModel->deviceSettings(_deviceId); + device->setName(newName); + emit deviceIdentifiersChanged(_deviceId); +} diff --git a/src/customwidgets/deviceform.h b/src/customwidgets/deviceform.h index 9dea8f79..51433715 100644 --- a/src/customwidgets/deviceform.h +++ b/src/customwidgets/deviceform.h @@ -14,7 +14,7 @@ class DeviceForm : public QWidget Q_OBJECT public: - explicit DeviceForm(SettingsModel* pSettingsModel, deviceId_t _deviceId, QWidget* parent = nullptr); + explicit DeviceForm(SettingsModel* pSettingsModel, deviceId_t deviceId, QWidget* parent = nullptr); ~DeviceForm(); deviceId_t deviceId(); @@ -22,6 +22,9 @@ class DeviceForm : public QWidget signals: void deviceIdentifiersChanged(deviceId_t devId); +private slots: + void onNameChanged(const QString& newName); + private: Ui::DeviceForm* _pUi; diff --git a/src/customwidgets/schemaformwidget.cpp b/src/customwidgets/schemaformwidget.cpp index d7592211..7e0598f2 100644 --- a/src/customwidgets/schemaformwidget.cpp +++ b/src/customwidgets/schemaformwidget.cpp @@ -75,6 +75,7 @@ void SchemaFormWidget::setSchema(const QJsonObject& schema, const QJsonObject& v QString label = propSchema.value("title").toString(key); QWidget* widget = createWidgetForProperty(propSchema, currentValue); + wireFieldChanged(key, widget); _fields.append({ key, widget }); _pFormLayout->addRow(label + ":", widget); } @@ -90,6 +91,7 @@ void SchemaFormWidget::setSchema(const QJsonObject& schema, const QJsonObject& v QJsonObject propSchema = thenProps.value(key).toObject(); QString label = propSchema.value("title").toString(key); QWidget* widget = createWidgetForProperty(propSchema, values.value(key)); + wireFieldChanged(key, widget); _fields.append({ key, widget }); _pFormLayout->addRow(label + ":", widget); } @@ -100,6 +102,7 @@ void SchemaFormWidget::setSchema(const QJsonObject& schema, const QJsonObject& v QJsonObject propSchema = elseProps.value(key).toObject(); QString label = propSchema.value("title").toString(key); QWidget* widget = createWidgetForProperty(propSchema, values.value(key)); + wireFieldChanged(key, widget); _fields.append({ key, widget }); _pFormLayout->addRow(label + ":", widget); } @@ -212,6 +215,16 @@ QWidget* SchemaFormWidget::createWidgetForProperty(const QJsonObject& propSchema } } +void SchemaFormWidget::wireFieldChanged(const QString& key, QWidget* widget) +{ + auto* edit = qobject_cast(widget); + if (edit == nullptr) + { + return; + } + connect(edit, &QLineEdit::textChanged, this, [this, key](const QString& text) { emit fieldChanged(key, text); }); +} + bool SchemaFormWidget::parseConditional(const QJsonObject& schema) { const QJsonObject ifObj = schema.value("if").toObject(); @@ -223,19 +236,24 @@ bool SchemaFormWidget::parseConditional(const QJsonObject& schema) return false; } + const QJsonObject ifProperties = ifObj.value("properties").toObject(); + + QString triggerKey; const QJsonArray required = ifObj.value("required").toArray(); - if (required.size() != 1) + if (required.size() == 1) { - return false; + triggerKey = required.at(0).toString(); + } + else if (ifProperties.size() == 1) + { + triggerKey = ifProperties.constBegin().key(); } - const QString triggerKey = required.at(0).toString(); if (triggerKey.isEmpty()) { return false; } - const QJsonObject ifProperties = ifObj.value("properties").toObject(); const QJsonObject constObj = ifProperties.value(triggerKey).toObject(); if (!constObj.contains("const")) { diff --git a/src/customwidgets/schemaformwidget.h b/src/customwidgets/schemaformwidget.h index c4cd3aa7..6b0cf1fe 100644 --- a/src/customwidgets/schemaformwidget.h +++ b/src/customwidgets/schemaformwidget.h @@ -44,6 +44,10 @@ class SchemaFormWidget : public QWidget */ QJsonObject values() const; +signals: + //! Emitted when a string field's value changes; \a key is the property name. + void fieldChanged(const QString& key, const QString& value); + private slots: //! Called when the trigger combo selection changes; re-evaluates conditional visibility. void onTriggerChanged(int index); @@ -51,6 +55,9 @@ private slots: private: QWidget* createWidgetForProperty(const QJsonObject& propSchema, const QJsonValue& value); + //! If \a widget is a QLineEdit, connects its textChanged to emit fieldChanged(\a key). + void wireFieldChanged(const QString& key, QWidget* widget); + /*! * \brief Parse the top-level \c if/then/else block and populate conditional state. * diff --git a/src/dialogs/adapterdevicesettings.cpp b/src/dialogs/adapterdevicesettings.cpp index cda47875..1640a819 100644 --- a/src/dialogs/adapterdevicesettings.cpp +++ b/src/dialogs/adapterdevicesettings.cpp @@ -3,11 +3,13 @@ #include "customwidgets/addabletabwidget.h" #include "customwidgets/deviceconfigtab.h" #include "models/adapterdata.h" +#include "models/device.h" #include "models/settingsmodel.h" #include #include #include +#include #include AdapterDeviceSettings::AdapterDeviceSettings(SettingsModel* pSettingsModel, QWidget* parent) @@ -16,19 +18,8 @@ AdapterDeviceSettings::AdapterDeviceSettings(SettingsModel* pSettingsModel, QWid auto* layout = new QVBoxLayout(this); setLayout(layout); - // Collect adapter IDs that have a populated schema - QStringList validAdapterIds; - const QStringList allAdapterIds = pSettingsModel->adapterIds(); - for (const auto& id : allAdapterIds) - { - const AdapterData* pAdapter = pSettingsModel->adapterData(id); - if (!pAdapter->schema().isEmpty()) - { - validAdapterIds.append(id); - } - } - - if (validAdapterIds.isEmpty()) + const QStringList adapterIds = validAdapterIds(); + if (adapterIds.isEmpty()) { layout->addWidget(new QLabel("No adapter schema available.", this)); layout->addStretch(); @@ -39,21 +30,49 @@ AdapterDeviceSettings::AdapterDeviceSettings(SettingsModel* pSettingsModel, QWid layout->addWidget(_pDeviceTabs, 1); connect(_pDeviceTabs, &AddableTabWidget::addTabRequested, this, &AdapterDeviceSettings::handleAddTab); + connect(_pDeviceTabs, &AddableTabWidget::tabClosed, this, &AdapterDeviceSettings::handleCloseTab); + + QSet configDeviceIds; + for (const auto& adapterId : adapterIds) + { + const QJsonArray devices = pSettingsModel->adapterData(adapterId)->effectiveConfig().value("devices").toArray(); + for (const auto& device : devices) + { + const int id = device.toObject().value("id").toInt(-1); + if (id >= 0) + { + configDeviceIds.insert(static_cast(id)); + } + } + } + const QList modelDeviceIds = pSettingsModel->deviceList(); + for (const deviceId_t devId : modelDeviceIds) + { + if (!configDeviceIds.contains(devId)) + { + pSettingsModel->removeDevice(devId); + } + } - // Load devices from all adapters with a schema QList pages; QStringList names; - int tabIndex = 1; - for (const auto& adapterId : validAdapterIds) + for (const auto& adapterId : adapterIds) { const AdapterData* pAdapter = pSettingsModel->adapterData(adapterId); const QJsonArray devices = pAdapter->effectiveConfig().value("devices").toArray(); for (const auto& device : devices) { - auto* tab = new DeviceConfigTab(pSettingsModel, adapterId, device.toObject(), _pDeviceTabs); + const QJsonObject deviceObj = device.toObject(); + const int id = deviceObj.value("id").toInt(-1); + if (id >= 0) + { + pSettingsModel->addDevice(static_cast(id)); + } + auto* tab = new DeviceConfigTab(pSettingsModel, adapterId, deviceObj, _pDeviceTabs); + connectTabNameTracking(tab); pages.append(tab); - names.append(constructTabName(device.toObject(), tabIndex++)); + names.append(constructTabName(tab)); } } @@ -71,22 +90,12 @@ AdapterDeviceSettings::AdapterDeviceSettings(SettingsModel* pSettingsModel, QWid */ void AdapterDeviceSettings::handleAddTab() { - QString defaultAdapterId; - const QStringList adapterIds = _pSettingsModel->adapterIds(); - for (const auto& id : adapterIds) - { - const AdapterData* pAdapter = _pSettingsModel->adapterData(id); - if (!pAdapter->schema().isEmpty()) - { - defaultAdapterId = id; - break; - } - } - - if (defaultAdapterId.isEmpty()) + const QStringList adapterIds = validAdapterIds(); + if (adapterIds.isEmpty()) { return; } + const QString defaultAdapterId = adapterIds.first(); QJsonObject defaultValues; const QJsonArray defaultDevices = @@ -96,31 +105,81 @@ void AdapterDeviceSettings::handleAddTab() defaultValues = defaultDevices.first().toObject(); } - deviceId_t newId = _pSettingsModel->addNewDevice(); + deviceId_t maxId = 0; + const QList modelIds = _pSettingsModel->deviceList(); + if (!modelIds.isEmpty()) + { + maxId = modelIds.last(); + } + for (int i = 0; i < _pDeviceTabs->count(); ++i) + { + auto* tab = qobject_cast(_pDeviceTabs->tabContent(i)); + if (tab) + { + const int id = tab->values().value("id").toInt(-1); + if (id >= 0) + { + maxId = qMax(maxId, static_cast(id)); + } + } + } + const deviceId_t newId = (maxId > 0) ? maxId + 1 : Device::cFirstDeviceId; + _pSettingsModel->addDevice(newId); _pSettingsModel->deviceSettings(newId)->setAdapterId(defaultAdapterId); defaultValues["id"] = static_cast(newId); - int tabIndex = _pDeviceTabs->count() + 1; auto* tab = new DeviceConfigTab(_pSettingsModel, defaultAdapterId, defaultValues, _pDeviceTabs); - _pDeviceTabs->addNewTab(constructTabName(defaultValues, tabIndex), tab); + connectTabNameTracking(tab); + _pDeviceTabs->addNewTab(constructTabName(tab), tab); } -QString AdapterDeviceSettings::constructTabName(const QJsonObject& deviceValues, int tabIndex) const +void AdapterDeviceSettings::handleCloseTab(QWidget* widget) { - int id = deviceValues.value("id").toInt(-1); + auto* tab = qobject_cast(widget); + if (tab && tab->deviceId() >= 0) + { + _pSettingsModel->removeDevice(static_cast(tab->deviceId())); + } +} + +QStringList AdapterDeviceSettings::validAdapterIds() const +{ + QStringList result; + const QStringList allAdapterIds = _pSettingsModel->adapterIds(); + for (const auto& id : allAdapterIds) + { + const AdapterData* pAdapter = _pSettingsModel->adapterData(id); + if (!pAdapter->schema().isEmpty()) + { + result.append(id); + } + } + return result; +} + +QString AdapterDeviceSettings::constructTabName(DeviceConfigTab* tab) const +{ + const int id = tab->deviceId(); if (id >= 0) { const deviceId_t devId = static_cast(id); if (_pSettingsModel->hasDevice(devId)) { - Device* pDevice = _pSettingsModel->deviceSettings(devId); - if (!pDevice->name().isEmpty()) + const QString name = _pSettingsModel->deviceSettings(devId)->name(); + if (!name.isEmpty()) { - return pDevice->name(); + return name; } } + return QString("Device #%1").arg(id); } - return QString("Device %1").arg(tabIndex); + return QStringLiteral("Device"); +} + +void AdapterDeviceSettings::connectTabNameTracking(DeviceConfigTab* tab) +{ + connect(tab, &DeviceConfigTab::nameChanged, tab, + [this, tab]() { _pDeviceTabs->setTabName(_pDeviceTabs->indexOf(tab), constructTabName(tab)); }); } void AdapterDeviceSettings::acceptValues() @@ -131,37 +190,19 @@ void AdapterDeviceSettings::acceptValues() } QMap devicesByAdapter; - for (int i = 0; i < _pDeviceTabs->count(); ++i) { auto* tab = qobject_cast(_pDeviceTabs->tabContent(i)); - if (!tab) + if (tab) { - continue; - } - const QJsonObject tabValues = tab->values(); - devicesByAdapter[tab->adapterId()].append(tabValues); - - int deviceId = tabValues.value("id").toInt(-1); - if (deviceId >= 0 && _pSettingsModel->deviceList().contains(static_cast(deviceId))) - { - _pSettingsModel->deviceSettings(static_cast(deviceId))->setName(tab->deviceName()); + devicesByAdapter[tab->adapterId()].append(tab->values()); } } - const QStringList adapterIds = _pSettingsModel->adapterIds(); - for (const auto& adapterId : adapterIds) + const QStringList allAdapterIds = validAdapterIds(); + for (const auto& adapterId : allAdapterIds) { - if (!devicesByAdapter.contains(adapterId)) - { - continue; - } - const AdapterData* pAdapter = _pSettingsModel->adapterData(adapterId); - if (pAdapter->schema().isEmpty()) - { - continue; - } - QJsonObject config = pAdapter->effectiveConfig(); + QJsonObject config = _pSettingsModel->adapterData(adapterId)->effectiveConfig(); config["devices"] = devicesByAdapter.value(adapterId); _pSettingsModel->setAdapterCurrentConfig(adapterId, config); } diff --git a/src/dialogs/adapterdevicesettings.h b/src/dialogs/adapterdevicesettings.h index 765d44ed..05bf57c4 100644 --- a/src/dialogs/adapterdevicesettings.h +++ b/src/dialogs/adapterdevicesettings.h @@ -1,11 +1,12 @@ #ifndef ADAPTERDEVICESETTINGS_H #define ADAPTERDEVICESETTINGS_H -#include +#include #include class SettingsModel; class AddableTabWidget; +class DeviceConfigTab; /*! * \brief Settings page for adapter device configuration. @@ -23,15 +24,18 @@ class AdapterDeviceSettings : public QWidget ~AdapterDeviceSettings() = default; /*! - * \brief Write the current form values back to each adapter's device configuration. + * \brief Write the per-adapter device JSON arrays back to each adapter's stored config. */ void acceptValues(); private slots: void handleAddTab(); + void handleCloseTab(QWidget* widget); private: - QString constructTabName(const QJsonObject& deviceValues, int tabIndex) const; + QStringList validAdapterIds() const; + QString constructTabName(DeviceConfigTab* tab) const; + void connectTabNameTracking(DeviceConfigTab* tab); SettingsModel* _pSettingsModel; AddableTabWidget* _pDeviceTabs{ nullptr }; diff --git a/src/dialogs/adaptersettings.cpp b/src/dialogs/adaptersettings.cpp index da85f5b6..34b8402d 100644 --- a/src/dialogs/adaptersettings.cpp +++ b/src/dialogs/adaptersettings.cpp @@ -53,8 +53,10 @@ void AdapterSettings::buildSection(const QJsonObject& propSchema, const QJsonVal { auto* form = new SchemaFormWidget(_pItemTabs); form->setSchema(_itemSchema, itemsArray.at(i).toObject()); + connectTabNameTracking(form); pages.append(form); - names.append(formatTabName(i + 1)); + const QString itemName = itemsArray.at(i).toObject().value("name").toString(); + names.append(itemName.isEmpty() ? formatTabName(i + 1) : itemName); } if (!pages.isEmpty()) @@ -83,7 +85,30 @@ QString AdapterSettings::formatTabName(int index) const { return QString("Item %1").arg(index); } - return QString("%1 %2").arg(_propertyKey[0].toUpper() + _propertyKey.mid(1)).arg(index); + QString label = (_propertyKey.length() > 1 && _propertyKey.endsWith('s')) ? _propertyKey.chopped(1) : _propertyKey; + if (label.isEmpty()) + { + return QString("Item %1").arg(index); + } + return QString("%1 %2").arg(label[0].toUpper() + label.mid(1)).arg(index); +} + +void AdapterSettings::onSchemaFieldNameChanged(SchemaFormWidget* form, const QString& key, const QString& value) +{ + if (key == "name") + { + const int tabIndex = _pItemTabs->indexOf(form); + if (tabIndex >= 0) + { + _pItemTabs->setTabName(tabIndex, value.isEmpty() ? formatTabName(tabIndex + 1) : value); + } + } +} + +void AdapterSettings::connectTabNameTracking(SchemaFormWidget* form) +{ + connect(form, &SchemaFormWidget::fieldChanged, this, + [this, form](const QString& key, const QString& value) { onSchemaFieldNameChanged(form, key, value); }); } void AdapterSettings::addItemTab() @@ -95,8 +120,37 @@ void AdapterSettings::addItemTab() { defaultValues = defaultItems.first().toObject(); } + + const QJsonObject properties = _itemSchema.value("properties").toObject(); + + int nameIndex = _nextItemTabIndex; + + const QJsonObject idProp = properties.value("id").toObject(); + if (!idProp.isEmpty() && idProp.value("type").toString() == "integer") + { + int maxId = 0; + for (int i = 0; i < _pItemTabs->count(); ++i) + { + auto* existingForm = qobject_cast(_pItemTabs->tabContent(i)); + if (existingForm) + { + maxId = qMax(maxId, existingForm->values().value("id").toInt()); + } + } + defaultValues["id"] = maxId + 1; + nameIndex = maxId + 1; + } + + const QJsonObject nameProp = properties.value("name").toObject(); + if (!nameProp.isEmpty() && nameProp.value("type").toString() == "string") + { + defaultValues["name"] = formatTabName(nameIndex); + } + form->setSchema(_itemSchema, defaultValues); - const QString name = formatTabName(_nextItemTabIndex); + connectTabNameTracking(form); + const QString tabName = defaultValues.value("name").toString(); + const QString name = tabName.isEmpty() ? formatTabName(nameIndex) : tabName; _nextItemTabIndex++; _pItemTabs->addNewTab(name, form); } diff --git a/src/dialogs/adaptersettings.h b/src/dialogs/adaptersettings.h index a50b673d..4e939928 100644 --- a/src/dialogs/adaptersettings.h +++ b/src/dialogs/adaptersettings.h @@ -51,6 +51,10 @@ class AdapterSettings : public QWidget //! \brief Returns a display name for a tab at the given 1-based \a index. QString formatTabName(int index) const; + //! \brief Connects \a form's fieldChanged signal to keep the tab label in sync with the \c name field. + void connectTabNameTracking(SchemaFormWidget* form); + void onSchemaFieldNameChanged(SchemaFormWidget* form, const QString& key, const QString& value); + SettingsModel* _pSettingsModel; QString _adapterId; QString _propertyKey; diff --git a/src/dialogs/devicesettings.cpp b/src/dialogs/devicesettings.cpp index 1b4450b9..8426dc51 100644 --- a/src/dialogs/devicesettings.cpp +++ b/src/dialogs/devicesettings.cpp @@ -12,7 +12,7 @@ DeviceSettings::DeviceSettings(SettingsModel* pSettingsModel, QWidget* parent) QVBoxLayout* layout = new QVBoxLayout(this); layout->addWidget(_pDeviceTabs); - connect(_pDeviceTabs, &AddableTabWidget::tabClosed, this, &DeviceSettings::handleCloseTab, Qt::DirectConnection); + connect(_pDeviceTabs, &AddableTabWidget::tabClosed, this, &DeviceSettings::handleCloseTab); connect(_pDeviceTabs, &AddableTabWidget::addTabRequested, this, &DeviceSettings::handleAddTab); QList pages; @@ -70,12 +70,11 @@ void DeviceSettings::updateTabName(deviceId_t devId) } } -void DeviceSettings::handleCloseTab(int index) +void DeviceSettings::handleCloseTab(QWidget* widget) { - auto tabContent = qobject_cast(_pDeviceTabs->tabContent(index)); - if (tabContent) + auto* form = qobject_cast(widget); + if (form) { - deviceId_t devId = tabContent->deviceId(); - _pSettingsModel->removeDevice(devId); + _pSettingsModel->removeDevice(form->deviceId()); } } diff --git a/src/dialogs/devicesettings.h b/src/dialogs/devicesettings.h index 13ca9ff1..bbf3a635 100644 --- a/src/dialogs/devicesettings.h +++ b/src/dialogs/devicesettings.h @@ -17,7 +17,7 @@ class DeviceSettings : public QWidget private slots: void handleAddTab(); - void handleCloseTab(int index); + void handleCloseTab(QWidget* widget); void updateTabName(deviceId_t devId); private: diff --git a/src/models/settingsmodel.cpp b/src/models/settingsmodel.cpp index f22c0255..74269b16 100644 --- a/src/models/settingsmodel.cpp +++ b/src/models/settingsmodel.cpp @@ -52,12 +52,7 @@ bool SettingsModel::absoluteTimes() deviceId_t SettingsModel::addNewDevice() { - deviceId_t newId = Device::cFirstDeviceId; - - while (_devices.contains(newId)) - { - newId++; - } + deviceId_t newId = _devices.isEmpty() ? Device::cFirstDeviceId : static_cast(_devices.lastKey() + 1); _devices[newId] = Device(newId); diff --git a/tests/customwidgets/tst_schemaformwidget.cpp b/tests/customwidgets/tst_schemaformwidget.cpp index 3c082aac..1a19ef42 100644 --- a/tests/customwidgets/tst_schemaformwidget.cpp +++ b/tests/customwidgets/tst_schemaformwidget.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -96,6 +97,23 @@ QJsonObject makeConditionalSchema() return schema; } +/*! + * \brief Same as makeConditionalSchema() but without a \c required array in the \c if block. + * + * This matches the real adapter schema format where the trigger key is inferred + * from the single property in \c if.properties rather than from \c if.required. + */ +QJsonObject makeConditionalSchemaNoIfRequired() +{ + QJsonObject schema = makeConditionalSchema(); + + QJsonObject ifObj = schema["if"].toObject(); + ifObj.remove("required"); + schema["if"] = ifObj; + + return schema; +} + } // namespace void TestSchemaFormWidget::emptySchemaCreatesEmptyForm() @@ -344,6 +362,10 @@ void TestSchemaFormWidget::conditionalSwitchShowsSerialHidesTcp() } } QVERIFY(typeCombo != nullptr); + if (typeCombo == nullptr) + { + return; + } typeCombo->setCurrentIndex(typeCombo->findData(QStringLiteral("serial"))); const QJsonObject result = w.values(); @@ -429,7 +451,7 @@ void TestSchemaFormWidget::integerEnumMissingValueUsesSchemaDefault() auto* combo = w.findChild(); QVERIFY(combo != nullptr); QCOMPARE(combo->currentText(), QStringLiteral("19200")); - QCOMPARE(w.values()["baudrate"].toInt(), 19200); + QCOMPARE(w.values().value("baudrate").toInt(), 19200); } void TestSchemaFormWidget::stringEnumMissingValueUsesSchemaDefault() @@ -446,7 +468,7 @@ void TestSchemaFormWidget::stringEnumMissingValueUsesSchemaDefault() auto* combo = w.findChild(); QVERIFY(combo != nullptr); QCOMPARE(combo->currentText(), QStringLiteral("None")); - QCOMPARE(w.values()["parity"].toString(), QStringLiteral("N")); + QCOMPARE(w.values().value("parity").toString(), QStringLiteral("N")); } void TestSchemaFormWidget::integerEnumMissingValueNoSchemaDefaultUsesFirstItem() @@ -461,7 +483,43 @@ void TestSchemaFormWidget::integerEnumMissingValueNoSchemaDefaultUsesFirstItem() auto* combo = w.findChild(); QVERIFY(combo != nullptr); QCOMPARE(combo->currentIndex(), 0); - QCOMPARE(w.values()["baudrate"].toInt(), 1200); + QCOMPARE(w.values().value("baudrate").toInt(), 1200); +} + +void TestSchemaFormWidget::conditionalWithoutIfRequiredShowsCorrectFields() +{ + SchemaFormWidget w; + QJsonObject values; + values["type"] = "tcp"; + values["ip"] = "192.168.1.1"; + values["port"] = 502; + w.setSchema(makeConditionalSchemaNoIfRequired(), values); + + const QJsonObject result = w.values(); + QVERIFY(result.contains("ip")); + QVERIFY(result.contains("port")); + QVERIFY(!result.contains("portName")); + QVERIFY(!result.contains("baudrate")); +} + +void TestSchemaFormWidget::fieldChangedEmittedOnStringEdit() +{ + QJsonObject propSchema; + propSchema["type"] = "string"; + + SchemaFormWidget w; + w.setSchema(makeObjectSchema("host", propSchema), QJsonObject{ { "host", "original" } }); + + QSignalSpy spy(&w, &SchemaFormWidget::fieldChanged); + + auto* edit = w.findChild(); + QVERIFY(edit != nullptr); + edit->setText("updated"); + + QCOMPARE(spy.count(), 1); + QList args = spy.takeFirst(); + QCOMPARE(args.at(0).toString(), QStringLiteral("host")); + QCOMPARE(args.at(1).toString(), QStringLiteral("updated")); } QTEST_MAIN(TestSchemaFormWidget) diff --git a/tests/customwidgets/tst_schemaformwidget.h b/tests/customwidgets/tst_schemaformwidget.h index 9e1092dc..2b8b157f 100644 --- a/tests/customwidgets/tst_schemaformwidget.h +++ b/tests/customwidgets/tst_schemaformwidget.h @@ -29,6 +29,8 @@ private slots: void integerEnumMissingValueUsesSchemaDefault(); void stringEnumMissingValueUsesSchemaDefault(); void integerEnumMissingValueNoSchemaDefaultUsesFirstItem(); + void conditionalWithoutIfRequiredShowsCorrectFields(); + void fieldChangedEmittedOnStringEdit(); }; #endif // TST_SCHEMAFORMWIDGET_H diff --git a/tests/dialogs/tst_adapterdevicesettings.cpp b/tests/dialogs/tst_adapterdevicesettings.cpp index 40ab9366..e929e151 100644 --- a/tests/dialogs/tst_adapterdevicesettings.cpp +++ b/tests/dialogs/tst_adapterdevicesettings.cpp @@ -136,7 +136,7 @@ void TestAdapterDeviceSettings::missingNameFallsBackToDeviceN() { return; } - QVERIFY(tabs->tabText(0).startsWith("Device ")); + QVERIFY(tabs->tabText(0).startsWith("Device")); } void TestAdapterDeviceSettings::acceptValuesSavesToAdapterConfig() @@ -306,4 +306,330 @@ void TestAdapterDeviceSettings::deviceIdPreservedWhenAdapterChanged() QCOMPARE(tab->adapterId(), QStringLiteral("adapterB")); } +void TestAdapterDeviceSettings::deviceNamePersistedAfterAcceptAndReopen() +{ + SettingsModel model; + + QJsonObject dev1; + dev1["id"] = 1; + QJsonObject dev2; + dev2["id"] = 2; + setupAdapter(model, "adapterA", QJsonArray{ dev1, dev2 }); + + // Device 2 is in the adapter config but NOT in the SettingsModel device list, + // simulating the case where the project file's devices section omitted it. + QVERIFY(!model.hasDevice(2)); + + // First dialog open: user types a name for device 2. + { + AdapterDeviceSettings w(&model); + + auto* tabs = w.findChild(); + QVERIFY(tabs != nullptr); + + auto* tab2 = qobject_cast(tabs->tabContent(1)); + QVERIFY(tab2 != nullptr); + + auto* nameEdit = tab2->findChild(QString(), Qt::FindDirectChildrenOnly); + QVERIFY(nameEdit != nullptr); + nameEdit->setText("Pump 2"); + + w.acceptValues(); + } + + // After accepting, device 2 must be registered in the model with the correct name. + QVERIFY(model.hasDevice(2)); + QCOMPARE(model.deviceSettings(2)->name(), QStringLiteral("Pump 2")); + + // Second dialog open (reopen): device 2 name must not be reset to empty. + { + AdapterDeviceSettings w2(&model); + + auto* tabs = w2.findChild(); + QVERIFY(tabs != nullptr); + + auto* tab2 = qobject_cast(tabs->tabContent(1)); + QVERIFY(tab2 != nullptr); + + auto* nameEdit = tab2->findChild(QString(), Qt::FindDirectChildrenOnly); + QVERIFY(nameEdit != nullptr); + QCOMPARE(nameEdit->text(), QStringLiteral("Pump 2")); + } +} + +void TestAdapterDeviceSettings::addTabDoesNotReuseIdFromAdapterConfig() +{ + SettingsModel model; + + // Use id=2: SettingsModel pre-populates device 1 (cFirstDeviceId), so the + // first free slot found by addNewDevice() would be 2 — colliding with this tab + // unless the constructor registers it first. + QJsonObject dev; + dev["id"] = 2; + setupAdapter(model, "adapterA", QJsonArray{ dev }); + + // Device 2 is in the adapter config but NOT registered in SettingsModel. + QVERIFY(!model.hasDevice(2)); + + AdapterDeviceSettings w(&model); + + auto* tabs = w.findChild(); + QVERIFY(tabs != nullptr); + QCOMPARE(tabs->count(), 1); + + emit tabs->addTabRequested(); + + QCOMPARE(tabs->count(), 2); + auto* tab = qobject_cast(tabs->tabContent(1)); + QVERIFY(tab != nullptr); + + const int assignedId = tab->values().value("id").toInt(-1); + QVERIFY(assignedId != 2); + QCOMPARE(assignedId, 3); + QVERIFY(model.hasDevice(static_cast(assignedId))); +} + +void TestAdapterDeviceSettings::addTabWithGapAssignsNextAfterMax() +{ + SettingsModel model; + + // Devices 1 and 3 are present — id 2 is a gap. Adding a new device should + // assign id 4 (max + 1), not 2 (gap fill), to avoid confusing tab ordering + // where a new "Device 2" tab appears after an existing "Device 3" tab. + QJsonObject dev1; + dev1["id"] = 1; + QJsonObject dev3; + dev3["id"] = 3; + setupAdapter(model, "adapterA", QJsonArray{ dev1, dev3 }); + + AdapterDeviceSettings w(&model); + + auto* tabs = w.findChild(); + QVERIFY(tabs != nullptr); + QCOMPARE(tabs->count(), 2); + + emit tabs->addTabRequested(); + + QCOMPARE(tabs->count(), 3); + auto* tab = qobject_cast(tabs->tabContent(2)); + QVERIFY(tab != nullptr); + + const int assignedId = tab->values().value("id").toInt(-1); + QCOMPARE(assignedId, 4); + QVERIFY(model.hasDevice(static_cast(assignedId))); +} + +void TestAdapterDeviceSettings::closeTabRemovesDeviceFromModel() +{ + SettingsModel model; + + QJsonObject dev1; + dev1["id"] = 1; + QJsonObject dev2; + dev2["id"] = 2; + setupAdapter(model, "adapterA", QJsonArray{ dev1, dev2 }); + + AdapterDeviceSettings w(&model); + + auto* tabs = w.findChild(); + QVERIFY(tabs != nullptr); + QVERIFY(model.hasDevice(1)); + QVERIFY(model.hasDevice(2)); + + tabs->handleCloseTab(0); + + QVERIFY(!model.hasDevice(1)); + QVERIFY(model.hasDevice(2)); +} + +void TestAdapterDeviceSettings::nameChangeUpdatesModelImmediately() +{ + SettingsModel model; + + QJsonObject dev; + dev["id"] = 1; + setupAdapter(model, "adapterA", QJsonArray{ dev }); + + AdapterDeviceSettings w(&model); + + auto* tabs = w.findChild(); + QVERIFY(tabs != nullptr); + + auto* tab = qobject_cast(tabs->tabContent(0)); + QVERIFY(tab != nullptr); + + auto* nameEdit = tab->findChild(QString(), Qt::FindDirectChildrenOnly); + QVERIFY(nameEdit != nullptr); + nameEdit->setText("Live Name"); + + QCOMPARE(model.deviceSettings(1)->name(), QStringLiteral("Live Name")); +} + +void TestAdapterDeviceSettings::adapterChangeUpdatesModelImmediately() +{ + SettingsModel model; + setupAdapter(model, "adapterA", QJsonArray()); + setupAdapter(model, "adapterB", QJsonArray()); + + AdapterDeviceSettings w(&model); + + auto* tabs = w.findChild(); + QVERIFY(tabs != nullptr); + + emit tabs->addTabRequested(); + auto* tab = qobject_cast(tabs->tabContent(0)); + QVERIFY(tab != nullptr); + + const int devId = tab->values().value("id").toInt(-1); + QVERIFY(devId >= 1); + QCOMPARE(model.deviceSettings(static_cast(devId))->adapterId(), QStringLiteral("adapterA")); + + auto* adapterCombo = tab->findChild(); + QVERIFY(adapterCombo != nullptr); + int adapterBIdx = adapterCombo->findData(QStringLiteral("adapterB")); + QVERIFY(adapterBIdx >= 0); + adapterCombo->setCurrentIndex(adapterBIdx); + + QCOMPARE(model.deviceSettings(static_cast(devId))->adapterId(), QStringLiteral("adapterB")); +} + +void TestAdapterDeviceSettings::multipleAdaptersWithDevices() +{ + SettingsModel model; + + QJsonObject devA; + devA["id"] = 1; + QJsonObject devB; + devB["id"] = 2; + setupAdapter(model, "adapterA", QJsonArray{ devA }); + setupAdapter(model, "adapterB", QJsonArray{ devB }); + + AdapterDeviceSettings w(&model); + + auto* tabs = w.findChild(); + QVERIFY(tabs != nullptr); + QCOMPARE(tabs->count(), 2); + + w.acceptValues(); + + const AdapterData* adapterA = model.adapterData("adapterA"); + const AdapterData* adapterB = model.adapterData("adapterB"); + QCOMPARE(adapterA->currentConfig().value("devices").toArray().size(), 1); + QCOMPARE(adapterB->currentConfig().value("devices").toArray().size(), 1); + QCOMPARE(adapterA->currentConfig().value("devices").toArray().at(0).toObject().value("id").toInt(), 1); + QCOMPARE(adapterB->currentConfig().value("devices").toArray().at(0).toObject().value("id").toInt(), 2); +} + +void TestAdapterDeviceSettings::addTabAfterIdEditDoesNotDuplicate() +{ + SettingsModel model; + + QJsonObject dev1; + dev1["id"] = 1; + setupAdapter(model, "adapterA", QJsonArray{ dev1 }); + + AdapterDeviceSettings w(&model); + + auto* tabs = w.findChild(); + QVERIFY(tabs != nullptr); + QCOMPARE(tabs->count(), 1); + + // Add device → gets ID 2 + emit tabs->addTabRequested(); + QCOMPARE(tabs->count(), 2); + + auto* tab2 = qobject_cast(tabs->tabContent(1)); + QVERIFY(tab2 != nullptr); + QCOMPARE(tab2->values().value("id").toInt(-1), 2); + + // User manually edits device 2's ID spinbox to 3 + auto* spin = tab2->findChild(); + QVERIFY(spin != nullptr); + spin->setValue(3); + QCOMPARE(tab2->values().value("id").toInt(-1), 3); + + // Add another device — must get ID 4, not 3 (which is now shown in an open tab) + emit tabs->addTabRequested(); + QCOMPARE(tabs->count(), 3); + + auto* tab3 = qobject_cast(tabs->tabContent(2)); + QVERIFY(tab3 != nullptr); + const int assignedId = tab3->values().value("id").toInt(-1); + QCOMPARE(assignedId, 4); +} + +void TestAdapterDeviceSettings::acceptValuesClearsDevicesForEmptiedAdapter() +{ + SettingsModel model; + + QJsonObject devA; + devA["id"] = 1; + QJsonObject devB; + devB["id"] = 3; + setupAdapter(model, "adapterA", QJsonArray{ devA }); + setupAdapter(model, "adapterB", QJsonArray{ devB }); + + AdapterDeviceSettings w(&model); + + auto* tabs = w.findChild(); + QVERIFY(tabs != nullptr); + QCOMPARE(tabs->count(), 2); + + // Close adapter B's only device tab (tab index 1 — adapters sorted alphabetically) + tabs->handleCloseTab(1); + QCOMPARE(tabs->count(), 1); + + w.acceptValues(); + + // Adapter B's config must have an empty devices array, not the stale {device 3} + const AdapterData* adapterB = model.adapterData("adapterB"); + QVERIFY(adapterB->hasStoredConfig()); + QCOMPARE(adapterB->currentConfig().value("devices").toArray().size(), 0); + + // Re-open: device 3 must NOT reappear + AdapterDeviceSettings w2(&model); + auto* tabs2 = w2.findChild(); + QVERIFY(tabs2 != nullptr); + QCOMPARE(tabs2->count(), 1); +} + +void TestAdapterDeviceSettings::cancelAndReopenDoesNotLeakDeviceIds() +{ + SettingsModel model; + + QJsonObject dev1; + dev1["id"] = 1; + setupAdapter(model, "adapterA", QJsonArray{ dev1 }); + + // First session: add a device then destroy without accepting (simulate cancel) + { + AdapterDeviceSettings w(&model); + + auto* tabs = w.findChild(); + QVERIFY(tabs != nullptr); + QCOMPARE(tabs->count(), 1); + + emit tabs->addTabRequested(); // addNewDevice() → ID 2; leaks into model on cancel + QCOMPARE(tabs->count(), 2); + // w destroyed without acceptValues() — leaked device 2 remains in model + } + + // Second session: config still has only device 1 + { + AdapterDeviceSettings w2(&model); + + auto* tabs = w2.findChild(); + QVERIFY(tabs != nullptr); + QCOMPARE(tabs->count(), 1); + + emit tabs->addTabRequested(); + QCOMPARE(tabs->count(), 2); + + auto* newTab = qobject_cast(tabs->tabContent(1)); + QVERIFY(newTab != nullptr); + const int assignedId = newTab->values().value("id").toInt(-1); + QCOMPARE(assignedId, 2); // must be 2, not 3 or higher + } +} + QTEST_MAIN(TestAdapterDeviceSettings) diff --git a/tests/dialogs/tst_adapterdevicesettings.h b/tests/dialogs/tst_adapterdevicesettings.h index e8c91900..357b9f35 100644 --- a/tests/dialogs/tst_adapterdevicesettings.h +++ b/tests/dialogs/tst_adapterdevicesettings.h @@ -20,6 +20,16 @@ private slots: void addTabUsesDeviceDefaults(); void addTabIncrementsDeviceId(); void deviceIdPreservedWhenAdapterChanged(); + void deviceNamePersistedAfterAcceptAndReopen(); + void addTabDoesNotReuseIdFromAdapterConfig(); + void addTabWithGapAssignsNextAfterMax(); + void closeTabRemovesDeviceFromModel(); + void nameChangeUpdatesModelImmediately(); + void adapterChangeUpdatesModelImmediately(); + void multipleAdaptersWithDevices(); + void cancelAndReopenDoesNotLeakDeviceIds(); + void addTabAfterIdEditDoesNotDuplicate(); + void acceptValuesClearsDevicesForEmptiedAdapter(); private: //! Populate \a model with an adapter that has a minimal device schema and diff --git a/tests/dialogs/tst_adaptersettings.cpp b/tests/dialogs/tst_adaptersettings.cpp index 264315bd..63dc65cb 100644 --- a/tests/dialogs/tst_adaptersettings.cpp +++ b/tests/dialogs/tst_adaptersettings.cpp @@ -184,6 +184,325 @@ void TestAdapterSettings::addTabUsesPropertyDefaults() QCOMPARE(form->values().value("port").toInt(), 502); } +void TestAdapterSettings::addTabUsesNextIndexForId() +{ + SettingsModel model; + + QJsonObject idProp; + idProp["type"] = "integer"; + idProp["title"] = "ID"; + QJsonObject portProp; + portProp["type"] = "integer"; + portProp["title"] = "Port"; + QJsonObject itemProps; + itemProps["id"] = idProp; + itemProps["port"] = portProp; + + QJsonObject describe = makeDescribeResult("connections", "array", itemProps); + + QJsonObject defaultConn; + defaultConn["id"] = 1; + defaultConn["port"] = 502; + QJsonObject defaults; + defaults["connections"] = QJsonArray{ defaultConn }; + describe["defaults"] = defaults; + + model.updateAdapterFromDescribe("testAdapter", describe); + + QJsonObject conn0; + conn0["id"] = 1; + conn0["port"] = 502; + QJsonObject conn1; + conn1["id"] = 2; + conn1["port"] = 503; + QJsonObject config; + config["connections"] = QJsonArray{ conn0, conn1 }; + model.setAdapterCurrentConfig("testAdapter", config); + + AdapterSettings w(&model, "testAdapter", "connections"); + + auto* tabs = w.findChild(); + QVERIFY(tabs != nullptr); + QCOMPARE(tabs->count(), 2); + + emit tabs->addTabRequested(); + + QCOMPARE(tabs->count(), 3); + auto* form = qobject_cast(tabs->tabContent(2)); + QVERIFY(form != nullptr); + QCOMPARE(form->values().value("id").toInt(), 3); +} + +void TestAdapterSettings::addTabAssignsMaxIdPlusOneForNonContiguousIds() +{ + SettingsModel model; + + QJsonObject idProp; + idProp["type"] = "integer"; + idProp["title"] = "ID"; + QJsonObject portProp; + portProp["type"] = "integer"; + portProp["title"] = "Port"; + QJsonObject itemProps; + itemProps["id"] = idProp; + itemProps["port"] = portProp; + + QJsonObject describe = makeDescribeResult("connections", "array", itemProps); + + QJsonObject defaultConn; + defaultConn["id"] = 1; + defaultConn["port"] = 502; + QJsonObject defaults; + defaults["connections"] = QJsonArray{ defaultConn }; + describe["defaults"] = defaults; + + model.updateAdapterFromDescribe("testAdapter", describe); + + QJsonObject conn0; + conn0["id"] = 1; + conn0["port"] = 502; + QJsonObject conn1; + conn1["id"] = 3; + conn1["port"] = 503; + QJsonObject config; + config["connections"] = QJsonArray{ conn0, conn1 }; + model.setAdapterCurrentConfig("testAdapter", config); + + AdapterSettings w(&model, "testAdapter", "connections"); + + auto* tabs = w.findChild(); + QVERIFY(tabs != nullptr); + QCOMPARE(tabs->count(), 2); + + emit tabs->addTabRequested(); + + QCOMPARE(tabs->count(), 3); + auto* form = qobject_cast(tabs->tabContent(2)); + QVERIFY(form != nullptr); + QCOMPARE(form->values().value("id").toInt(), 4); +} + +void TestAdapterSettings::addTabNameMatchesIdForNonContiguousIds() +{ + SettingsModel model; + + QJsonObject idProp; + idProp["type"] = "integer"; + idProp["title"] = "ID"; + QJsonObject nameProp; + nameProp["type"] = "string"; + nameProp["title"] = "Connection Name"; + QJsonObject itemProps; + itemProps["id"] = idProp; + itemProps["name"] = nameProp; + + QJsonObject describe = makeDescribeResult("connections", "array", itemProps); + + QJsonObject defaultConn; + defaultConn["id"] = 1; + defaultConn["name"] = "Connection 1"; + QJsonObject defaults; + defaults["connections"] = QJsonArray{ defaultConn }; + describe["defaults"] = defaults; + + model.updateAdapterFromDescribe("testAdapter", describe); + + QJsonObject conn0; + conn0["id"] = 1; + conn0["name"] = "Connection 1"; + QJsonObject conn1; + conn1["id"] = 3; + conn1["name"] = "Connection 3"; + QJsonObject config; + config["connections"] = QJsonArray{ conn0, conn1 }; + model.setAdapterCurrentConfig("testAdapter", config); + + AdapterSettings w(&model, "testAdapter", "connections"); + + auto* tabs = w.findChild(); + QVERIFY(tabs != nullptr); + QCOMPARE(tabs->count(), 2); + + emit tabs->addTabRequested(); + + QCOMPARE(tabs->count(), 3); + auto* form = qobject_cast(tabs->tabContent(2)); + QVERIFY(form != nullptr); + + // ID must be maxId+1=4, and the default name and tab title must match. + QCOMPARE(form->values().value("id").toInt(), 4); + QCOMPARE(form->values().value("name").toString(), QStringLiteral("Connection 4")); + QCOMPARE(tabs->tabText(2), QStringLiteral("Connection 4")); +} + +void TestAdapterSettings::addTabNameMatchesIdForConsecutiveAddsWithNonContiguousStart() +{ + SettingsModel model; + + QJsonObject idProp; + idProp["type"] = "integer"; + idProp["title"] = "ID"; + QJsonObject nameProp; + nameProp["type"] = "string"; + nameProp["title"] = "Connection Name"; + QJsonObject itemProps; + itemProps["id"] = idProp; + itemProps["name"] = nameProp; + + QJsonObject describe = makeDescribeResult("connections", "array", itemProps); + + QJsonObject defaultConn; + defaultConn["id"] = 1; + defaultConn["name"] = "Connection 1"; + QJsonObject defaults; + defaults["connections"] = QJsonArray{ defaultConn }; + describe["defaults"] = defaults; + + model.updateAdapterFromDescribe("testAdapter", describe); + + QJsonObject conn0; + conn0["id"] = 1; + conn0["name"] = "Connection 1"; + QJsonObject conn1; + conn1["id"] = 3; + conn1["name"] = "Connection 3"; + QJsonObject config; + config["connections"] = QJsonArray{ conn0, conn1 }; + model.setAdapterCurrentConfig("testAdapter", config); + + AdapterSettings w(&model, "testAdapter", "connections"); + + auto* tabs = w.findChild(); + QVERIFY(tabs != nullptr); + QCOMPARE(tabs->count(), 2); + + // First add: maxId=3, so new id=4 and name="Connection 4" + emit tabs->addTabRequested(); + + QCOMPARE(tabs->count(), 3); + auto* form0 = qobject_cast(tabs->tabContent(2)); + QVERIFY(form0 != nullptr); + QCOMPARE(form0->values().value("id").toInt(), 4); + QCOMPARE(form0->values().value("name").toString(), QStringLiteral("Connection 4")); + QCOMPARE(tabs->tabText(2), QStringLiteral("Connection 4")); + + // Second add: maxId=4, so new id=5 and name="Connection 5" + emit tabs->addTabRequested(); + + QCOMPARE(tabs->count(), 4); + auto* form1 = qobject_cast(tabs->tabContent(3)); + QVERIFY(form1 != nullptr); + QCOMPARE(form1->values().value("id").toInt(), 5); + QCOMPARE(form1->values().value("name").toString(), QStringLiteral("Connection 5")); + QCOMPARE(tabs->tabText(3), QStringLiteral("Connection 5")); +} + +void TestAdapterSettings::addTabInitializesNameToConnectionId() +{ + SettingsModel model; + + QJsonObject idProp; + idProp["type"] = "integer"; + idProp["title"] = "ID"; + QJsonObject nameProp; + nameProp["type"] = "string"; + nameProp["title"] = "Connection Name"; + QJsonObject itemProps; + itemProps["id"] = idProp; + itemProps["name"] = nameProp; + + QJsonObject describe = makeDescribeResult("connections", "array", itemProps); + + QJsonObject defaultConn; + defaultConn["id"] = 1; + defaultConn["name"] = "Connection 1"; + QJsonObject defaults; + defaults["connections"] = QJsonArray{ defaultConn }; + describe["defaults"] = defaults; + + model.updateAdapterFromDescribe("testAdapter", describe); + + QJsonObject conn0; + conn0["id"] = 1; + conn0["name"] = "Connection 1"; + QJsonObject conn1; + conn1["id"] = 2; + conn1["name"] = "Connection 2"; + QJsonObject config; + config["connections"] = QJsonArray{ conn0, conn1 }; + model.setAdapterCurrentConfig("testAdapter", config); + + AdapterSettings w(&model, "testAdapter", "connections"); + + auto* tabs = w.findChild(); + QVERIFY(tabs != nullptr); + QCOMPARE(tabs->count(), 2); + + emit tabs->addTabRequested(); + + QCOMPARE(tabs->count(), 3); + auto* form = qobject_cast(tabs->tabContent(2)); + QVERIFY(form != nullptr); + QCOMPARE(form->values().value("name").toString(), QStringLiteral("Connection 3")); +} + +void TestAdapterSettings::tabNameUsesNameFieldOnLoad() +{ + SettingsModel model; + + QJsonObject nameProp; + nameProp["type"] = "string"; + nameProp["title"] = "Connection Name"; + QJsonObject itemProps; + itemProps["name"] = nameProp; + + model.updateAdapterFromDescribe("testAdapter", makeDescribeResult("connections", "array", itemProps)); + + QJsonObject conn0; + conn0["name"] = "My Conn"; + QJsonObject config; + config["connections"] = QJsonArray{ conn0 }; + model.setAdapterCurrentConfig("testAdapter", config); + + AdapterSettings w(&model, "testAdapter", "connections"); + + auto* tabs = w.findChild(); + QVERIFY(tabs != nullptr); + QCOMPARE(tabs->tabText(0), QStringLiteral("My Conn")); +} + +void TestAdapterSettings::tabNameUpdatesWhenNameFieldChanges() +{ + SettingsModel model; + + QJsonObject nameProp; + nameProp["type"] = "string"; + nameProp["title"] = "Connection Name"; + QJsonObject itemProps; + itemProps["name"] = nameProp; + + model.updateAdapterFromDescribe("testAdapter", makeDescribeResult("connections", "array", itemProps)); + + QJsonObject conn0; + conn0["name"] = "Original"; + QJsonObject config; + config["connections"] = QJsonArray{ conn0 }; + model.setAdapterCurrentConfig("testAdapter", config); + + AdapterSettings w(&model, "testAdapter", "connections"); + + auto* tabs = w.findChild(); + QVERIFY(tabs != nullptr); + auto* form = qobject_cast(tabs->tabContent(0)); + QVERIFY(form != nullptr); + auto* edit = form->findChild(); + QVERIFY(edit != nullptr); + + edit->setText("Renamed"); + + QCOMPARE(tabs->tabText(0), QStringLiteral("Renamed")); +} + void TestAdapterSettings::acceptValuesStoresConfigInAdapterData() { SettingsModel model; diff --git a/tests/dialogs/tst_adaptersettings.h b/tests/dialogs/tst_adaptersettings.h index 3fd96c26..fd0f6b7e 100644 --- a/tests/dialogs/tst_adaptersettings.h +++ b/tests/dialogs/tst_adaptersettings.h @@ -17,6 +17,13 @@ private slots: void arrayPropertyCreatesTabWidget(); void objectPropertyCreatesSingleForm(); void addTabUsesPropertyDefaults(); + void addTabUsesNextIndexForId(); + void addTabAssignsMaxIdPlusOneForNonContiguousIds(); + void addTabNameMatchesIdForNonContiguousIds(); + void addTabNameMatchesIdForConsecutiveAddsWithNonContiguousStart(); + void addTabInitializesNameToConnectionId(); + void tabNameUsesNameFieldOnLoad(); + void tabNameUpdatesWhenNameFieldChanges(); void acceptValuesStoresConfigInAdapterData(); };