Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
485 changes: 256 additions & 229 deletions adapters/describe.json

Large diffs are not rendered by default.

Binary file modified adapters/dummymodbusadapter
Binary file not shown.
Binary file modified adapters/dummymodbusadapter.exe
Binary file not shown.
2 changes: 2 additions & 0 deletions adapters/json-rpc-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ Each property in `schema` includes the following additional fields (standard JSO
| `title` | Standard JSON Schema annotation. UI-friendly label for the field, suitable for use in form inputs and dialog labels |
| `x-enumLabels` | Custom extension. Present on enum properties only. A string array, parallel to `enum`, giving a UI-friendly display name for each allowed value |

The connection schema uses JSON Schema Draft 7 `if`/`then`/`else` to express type-dependent fields. When `type` equals `"tcp"`, the fields in `then.properties` apply (TCP-specific). Otherwise, when `type` is not `"tcp"`, the fields in `else.properties` apply (e.g., serial-specific or other non-TCP types). A UI can use this to enable or disable the relevant fields based on the selected connection type.

---

### `adapter.configure`
Expand Down
Binary file modified adapters/modbusadapter
Binary file not shown.
Binary file modified adapters/modbusadapter.exe
Binary file not shown.
9 changes: 8 additions & 1 deletion src/customwidgets/deviceconfigtab.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#include <QComboBox>
#include <QHBoxLayout>
#include <QJsonArray>
#include <QLabel>
#include <QLineEdit>
#include <QVBoxLayout>
Expand Down Expand Up @@ -77,7 +78,13 @@ DeviceConfigTab::DeviceConfigTab(SettingsModel* pSettingsModel,
void DeviceConfigTab::onAdapterChanged(int index)
{
QString newAdapterId = _pAdapterCombo->itemData(index).toString();
rebuildSchemaForm(newAdapterId, QJsonObject());
QJsonObject defaultValues;
const QJsonArray defaultDevices = _pSettingsModel->adapterData(newAdapterId)->defaults().value("devices").toArray();
if (!defaultDevices.isEmpty())
{
defaultValues = defaultDevices.first().toObject();
}
rebuildSchemaForm(newAdapterId, defaultValues);
}

void DeviceConfigTab::rebuildSchemaForm(const QString& adapterId, const QJsonObject& deviceValues)
Expand Down
150 changes: 150 additions & 0 deletions src/customwidgets/schemaformwidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <QSpinBox>
#include <climits>
#include <limits>
#include <utility>

SchemaFormWidget::SchemaFormWidget(QWidget* parent) : QWidget(parent), _pFormLayout(new QFormLayout(this))
{
Expand All @@ -18,6 +19,11 @@ SchemaFormWidget::SchemaFormWidget(QWidget* parent) : QWidget(parent), _pFormLay
void SchemaFormWidget::setSchema(const QJsonObject& schema, const QJsonObject& values)
{
_fields.clear();
_conditionalTriggerKey.clear();
_conditionalTriggerConst.clear();
_thenKeys.clear();
_elseKeys.clear();
_currentTriggerValue.clear();

// removeRow() deletes both the label and field widget
while (_pFormLayout->rowCount() > 0)
Expand Down Expand Up @@ -72,6 +78,51 @@ void SchemaFormWidget::setSchema(const QJsonObject& schema, const QJsonObject& v
_fields.append({ key, widget });
_pFormLayout->addRow(label + ":", widget);
}

if (!parseConditional(schema))
{
return;
}

const QJsonObject thenProps = schema.value("then").toObject().value("properties").toObject();
for (const QString& key : std::as_const(_thenKeys))
{
QJsonObject propSchema = thenProps.value(key).toObject();
QString label = propSchema.value("title").toString(key);
QWidget* widget = createWidgetForProperty(propSchema, values.value(key));
_fields.append({ key, widget });
_pFormLayout->addRow(label + ":", widget);
}

const QJsonObject elseProps = schema.value("else").toObject().value("properties").toObject();
for (const QString& key : std::as_const(_elseKeys))
{
QJsonObject propSchema = elseProps.value(key).toObject();
QString label = propSchema.value("title").toString(key);
QWidget* widget = createWidgetForProperty(propSchema, values.value(key));
_fields.append({ key, widget });
_pFormLayout->addRow(label + ":", widget);
}

QComboBox* triggerCombo = nullptr;
for (const auto& [key, widget] : std::as_const(_fields))
{
if (key == _conditionalTriggerKey)
{
triggerCombo = qobject_cast<QComboBox*>(widget);
break;
}
}

if (triggerCombo != nullptr)
{
applyConditional(triggerCombo->currentData().toString());
connect(triggerCombo, &QComboBox::currentIndexChanged, this, &SchemaFormWidget::onTriggerChanged);
}
else
{
applyConditional(values.value(_conditionalTriggerKey).toVariant().toString());
}
}

QWidget* SchemaFormWidget::createWidgetForProperty(const QJsonObject& propSchema, const QJsonValue& value)
Expand Down Expand Up @@ -155,11 +206,110 @@ QWidget* SchemaFormWidget::createWidgetForProperty(const QJsonObject& propSchema
}
}

bool SchemaFormWidget::parseConditional(const QJsonObject& schema)
{
const QJsonObject ifObj = schema.value("if").toObject();
const QJsonObject thenObj = schema.value("then").toObject();
const QJsonObject elseObj = schema.value("else").toObject();

if (ifObj.isEmpty() || thenObj.isEmpty() || elseObj.isEmpty())
{
return false;
}

const QJsonArray required = ifObj.value("required").toArray();
if (required.size() != 1)
{
return false;
}

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"))
{
return false;
}

_conditionalTriggerKey = triggerKey;
_conditionalTriggerConst = constObj.value("const").toVariant().toString();

const QJsonObject thenProps = thenObj.value("properties").toObject();
for (auto it = thenProps.constBegin(); it != thenProps.constEnd(); ++it)
{
_thenKeys.append(it.key());
}

const QJsonObject elseProps = elseObj.value("properties").toObject();
for (auto it = elseProps.constBegin(); it != elseProps.constEnd(); ++it)
{
_elseKeys.append(it.key());
}

return true;
}

void SchemaFormWidget::applyConditional(const QString& triggerValue)
{
_currentTriggerValue = triggerValue;
const bool conditionMet = (triggerValue == _conditionalTriggerConst);

for (const auto& [key, widget] : std::as_const(_fields))
{
const bool isThen = _thenKeys.contains(key);
const bool isElse = _elseKeys.contains(key);

if (!isThen && !isElse)
{
continue;
}

const bool visible = isThen ? conditionMet : !conditionMet;
widget->setVisible(visible);

QWidget* label = _pFormLayout->labelForField(widget);
if (label != nullptr)
{
label->setVisible(visible);
}
}
}

void SchemaFormWidget::onTriggerChanged(int /*index*/)
{
auto* combo = qobject_cast<QComboBox*>(sender());
if (combo == nullptr)
{
return;
}

applyConditional(combo->currentData().toString());
}

QJsonObject SchemaFormWidget::values() const
{
const bool conditionMet = !_conditionalTriggerKey.isEmpty() && (_currentTriggerValue == _conditionalTriggerConst);

QJsonObject result;
for (const auto& [key, widget] : _fields)
{
const bool isThen = _thenKeys.contains(key);
const bool isElse = _elseKeys.contains(key);

if (isThen && !conditionMet)
{
continue;
}
if (isElse && conditionMet)
{
continue;
}

if (auto* spin = qobject_cast<QSpinBox*>(widget))
{
result[key] = spin->value();
Expand Down
44 changes: 43 additions & 1 deletion src/customwidgets/schemaformwidget.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#include <QJsonObject>
#include <QJsonValue>
#include <QStringList>
#include <QWidget>

class QFormLayout;
Expand All @@ -14,6 +15,11 @@ class QFormLayout;
* number, boolean, and enum-constrained string/integer properties.
* The label for each row is taken from the property's \c "title" field, falling
* back to the property key name if no title is provided.
*
* Supports the JSON Schema Draft 7 \c if/then/else pattern with a single-property
* \c const condition. When detected, the active branch's fields are shown and the
* inactive branch's fields are hidden, and visibility updates live as the trigger
* field changes.
*/
class SchemaFormWidget : public QWidget
{
Expand All @@ -32,15 +38,51 @@ class SchemaFormWidget : public QWidget

/*!
* \brief Return current form input as a JSON object.
* \return A QJsonObject with one entry per schema property.
*
* Fields belonging to the inactive conditional branch are omitted.
* \return A QJsonObject with one entry per visible schema property.
*/
QJsonObject values() const;

private slots:
//! Called when the trigger combo selection changes; re-evaluates conditional visibility.
void onTriggerChanged(int index);

private:
QWidget* createWidgetForProperty(const QJsonObject& propSchema, const QJsonValue& value);

/*!
* \brief Parse the top-level \c if/then/else block and populate conditional state.
*
* Only the narrow single-property \c const pattern is supported.
* \param schema Full schema object passed to setSchema().
* \return \c true if a supported pattern was found and state was populated.
*/
bool parseConditional(const QJsonObject& schema);

/*!
* \brief Show or hide rows based on whether the trigger value matches the const.
* \param triggerValue Current string value of the trigger field.
*/
void applyConditional(const QString& triggerValue);

QFormLayout* _pFormLayout;
QList<QPair<QString, QWidget*>> _fields;

//! Key in \c "properties" whose value drives the if/then/else switch.
QString _conditionalTriggerKey;

//! The \c "const" value that activates the \c "then" branch.
QString _conditionalTriggerConst;

//! Keys belonging to the \c "then" branch (shown when condition is true).
QStringList _thenKeys;

//! Keys belonging to the \c "else" branch (shown when condition is false).
QStringList _elseKeys;

//! Current value of the trigger field; used to filter \c values() output.
QString _currentTriggerValue;
};

#endif // SCHEMAFORMWIDGET_H
Loading