From e022e4fed574a434623f9f468a44f88b898829ca Mon Sep 17 00:00:00 2001 From: James Turner Date: Thu, 5 Oct 2017 12:38:27 +0100 Subject: [PATCH] Command / property bridges for QML / QtQuick --- src/GUI/CMakeLists.txt | 6 + src/GUI/FGQmlInstance.cxx | 106 ++++++++++++++ src/GUI/FGQmlInstance.hxx | 28 ++++ src/GUI/FGQmlPropertyNode.cxx | 143 +++++++++++++++++++ src/GUI/FGQmlPropertyNode.hxx | 54 +++++++ src/GUI/PropertyItemModel.cxx | 257 ++++++++++++++++++++++++++++++++++ src/GUI/PropertyItemModel.hxx | 79 +++++++++++ 7 files changed, 673 insertions(+) create mode 100644 src/GUI/FGQmlInstance.cxx create mode 100644 src/GUI/FGQmlInstance.hxx create mode 100644 src/GUI/FGQmlPropertyNode.cxx create mode 100644 src/GUI/FGQmlPropertyNode.hxx create mode 100644 src/GUI/PropertyItemModel.cxx create mode 100644 src/GUI/PropertyItemModel.hxx diff --git a/src/GUI/CMakeLists.txt b/src/GUI/CMakeLists.txt index 561304d0e..b4ce97216 100644 --- a/src/GUI/CMakeLists.txt +++ b/src/GUI/CMakeLists.txt @@ -163,6 +163,12 @@ if (HAVE_QT) QQuickDrawable.hxx QtQuickFGCanvasItem.cxx QtQuickFGCanvasItem.hxx + PropertyItemModel.cxx + PropertyItemModel.hxx + FGQmlInstance.cxx + FGQmlInstance.hxx + FGQmlPropertyNode.cxx + FGQmlPropertyNode.hxx ) set_property(TARGET fgqmlui PROPERTY AUTOMOC ON) diff --git a/src/GUI/FGQmlInstance.cxx b/src/GUI/FGQmlInstance.cxx new file mode 100644 index 000000000..08696eef9 --- /dev/null +++ b/src/GUI/FGQmlInstance.cxx @@ -0,0 +1,106 @@ +#include "FGQmlInstance.hxx" + +#include +#include + +#include "FGQmlPropertyNode.hxx" + +#include + +#include
+ +namespace { + +void convertJSObjectToPropertyNode(QJSValue js, SGPropertyNode *node); + +void convertSingleJSValue(QJSValue v, SGPropertyNode* node) +{ + if (v.isNumber()) { + node->setDoubleValue(v.toNumber()); + } else if (v.isBool()) { + node->setBoolValue(v.toBool()); + } else if (v.isString()) { + node->setStringValue(v.toString().toStdString()); + } else { + qWarning() << Q_FUNC_INFO << "unhandld JS type"; + } +} + +void convertJSObjectToPropertyNode(QJSValue js, SGPropertyNode* node) +{ + QJSValueIterator it(js); + while (it.hasNext()) { + it.next(); + const auto propName = it.name().toStdString(); + QJSValue v = it.value(); + if (v.isObject()) { + // recursion is fun :) + SGPropertyNode_ptr childNode = node->getChild(propName, true); + convertJSObjectToPropertyNode(v, childNode); + } else if (v.isArray()) { + // mapping arbitrary JS arrays is slightly uncomfortable, but + // this makes the common case work: + // foobar: [1,4, "apples", false] + // becomes foobar[0] = 1; foobar[1] = 4; foobar[2] = "apples"; + // foobar[3] = false; + // not perfect but useful enough to be worth supporting. + QJSValueIterator arrayIt(v); + int propIndex = 0; + while (arrayIt.hasNext()) { + arrayIt.next(); + SGPropertyNode_ptr childNode = node->getChild(propName, propIndex++, true); + if (arrayIt.value().isArray()) { + // there is no sensible mapping of this to SGPropertyNode + qWarning() << Q_FUNC_INFO <<"arrays of array not possible"; + } else if (arrayIt.value().isObject()) { + convertJSObjectToPropertyNode(arrayIt.value(), childNode); + } else { + // simple case, just convert the value in place + convertSingleJSValue(arrayIt.value(), childNode); + } + } + } else { + convertSingleJSValue(v, node->getChild(propName, true)); + } + } +} + +} // of anonymous namespace + +FGQmlInstance::FGQmlInstance(QObject *parent) : QObject(parent) +{ + +} + +bool FGQmlInstance::command(QString name, QJSValue args) +{ + const std::string cmdName = name.toStdString(); + + SGCommandMgr* mgr = SGCommandMgr::instance(); + if (!mgr->getCommand(cmdName)) { + qWarning() << "No such command" << name; + return false; + } + + SGPropertyNode_ptr propArgs(new SGPropertyNode); + + // convert JSValue args into a property tree. + if (args.isObject()) { + convertJSObjectToPropertyNode(args, propArgs); + } + + ////// + + return mgr->execute(cmdName, propArgs, globals->get_props()); +} + +FGQmlPropertyNode *FGQmlInstance::property(QString path, bool create) const +{ + SGPropertyNode_ptr node = fgGetNode(path.toStdString(), create); + if (!node) + return nullptr; + + FGQmlPropertyNode* result = new FGQmlPropertyNode; + result->setNode(node); + return result; +} diff --git a/src/GUI/FGQmlInstance.hxx b/src/GUI/FGQmlInstance.hxx new file mode 100644 index 000000000..25297997b --- /dev/null +++ b/src/GUI/FGQmlInstance.hxx @@ -0,0 +1,28 @@ +#ifndef FGQMLINSTANCE_HXX +#define FGQMLINSTANCE_HXX + +#include + +#include + +// forward decls +class FGQmlPropertyNode; + +class FGQmlInstance : public QObject +{ + Q_OBJECT +public: + explicit FGQmlInstance(QObject *parent = nullptr); + + Q_INVOKABLE bool command(QString name, QJSValue args); + + /** + * retrieve a property by its global path + */ + Q_INVOKABLE FGQmlPropertyNode* property(QString path, bool create = false) const; +signals: + +public slots: +}; + +#endif // FGQMLINSTANCE_HXX diff --git a/src/GUI/FGQmlPropertyNode.cxx b/src/GUI/FGQmlPropertyNode.cxx new file mode 100644 index 000000000..20b254cb2 --- /dev/null +++ b/src/GUI/FGQmlPropertyNode.cxx @@ -0,0 +1,143 @@ +#include "FGQmlPropertyNode.hxx" + +#include +#include +#include + +#include +#include +#include + +#include
+ +///////////////////////////////////////////////////////////////////////// + +FGQmlPropertyNode::FGQmlPropertyNode(QObject *parent) : QObject(parent) +{ + +} + +bool FGQmlPropertyNode::set(QVariant newValue) +{ + if (!_prop || !_prop->getAttribute(SGPropertyNode::WRITE)) + return false; + + // we still need to short circuit setting + if (newValue == value()) { + return true; + } + + switch (_prop->getType()) { + case simgear::props::INT: + case simgear::props::LONG: + _prop->setIntValue(newValue.toInt()); + case simgear::props::BOOL: _prop->setBoolValue(newValue.toBool()); + case simgear::props::DOUBLE: _prop->setDoubleValue(newValue.toDouble()); + case simgear::props::FLOAT: _prop->setFloatValue(newValue.toFloat()); + case simgear::props::STRING: _prop->setStringValue(newValue.toString().toStdString()); + + case simgear::props::VEC3D: { + QVector3D v = newValue.value(); + _prop->setValue(SGVec3d(v.x(), v.y(), v.z())); + } + + case simgear::props::VEC4D: { + QVector4D v = newValue.value(); + _prop->setValue(SGVec4d(v.x(), v.y(), v.z(), v.w())); + } + + default: + qWarning() << Q_FUNC_INFO << "handle untyped property writes"; + break; + } + + return true; +} + +QVariant FGQmlPropertyNode::value() const +{ + if (!_prop) + return {}; + + switch (_prop->getType()) { + case simgear::props::INT: + case simgear::props::LONG: + return _prop->getIntValue(); + case simgear::props::BOOL: return _prop->getBoolValue(); + case simgear::props::DOUBLE: return _prop->getDoubleValue(); + case simgear::props::FLOAT: return _prop->getFloatValue(); + case simgear::props::STRING: return QString::fromStdString(_prop->getStringValue()); + + case simgear::props::VEC3D: { + const SGVec3d v3 = _prop->getValue(); + return QVariant::fromValue(QVector3D(v3.x(), v3.y(), v3.z())); + } + + case simgear::props::VEC4D: { + const SGVec4d v4 = _prop->getValue(); + return QVariant::fromValue(QVector4D(v4.x(), v4.y(), v4.z(), v4.w())); + } + default: + break; + } + + return {}; // null qvariant +} + +QString FGQmlPropertyNode::path() const +{ + if (!_prop) + return QString(); + + return QString::fromStdString(_prop->getPath()); +} + +FGQmlPropertyNode *FGQmlPropertyNode::parentProp() const +{ + if (!_prop || !_prop->getParent()) + return nullptr; + + auto pp = new FGQmlPropertyNode; + pp->setNode(_prop->getParent()); + return pp; +} + +void FGQmlPropertyNode::setNode(SGPropertyNode_ptr node) +{ + if (node == _prop) { + return; + } + + if (_prop) { + _prop->removeChangeListener(this); + } + + _prop = node; + if (_prop) { + _prop->addChangeListener(this, false); + } + + emit parentPropChanged(parentProp()); + emit pathChanged(path()); + emit valueChangedNotify(value()); +} + +SGPropertyNode_ptr FGQmlPropertyNode::node() const +{ + return _prop; +} + +void FGQmlPropertyNode::valueChanged(SGPropertyNode *node) +{ + if (node != _prop) { + return; + } + emit valueChangedNotify(value()); +} + +void FGQmlPropertyNode::setPath(QString path) +{ + SGPropertyNode_ptr node = fgGetNode(path.toStdString(), false /* don't create */); + setNode(node); +} + diff --git a/src/GUI/FGQmlPropertyNode.hxx b/src/GUI/FGQmlPropertyNode.hxx new file mode 100644 index 000000000..5781bf031 --- /dev/null +++ b/src/GUI/FGQmlPropertyNode.hxx @@ -0,0 +1,54 @@ +#ifndef FGQMLPROPERTYNODE_HXX +#define FGQMLPROPERTYNODE_HXX + +#include +#include + +#include + +class FGQmlPropertyNode : public QObject, + public SGPropertyChangeListener +{ + Q_OBJECT +public: + explicit FGQmlPropertyNode(QObject *parent = nullptr); + + Q_PROPERTY(QVariant value READ value NOTIFY valueChangedNotify) + Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged) + Q_PROPERTY(FGQmlPropertyNode* parentProp READ parentProp NOTIFY parentPropChanged) + + Q_INVOKABLE bool set(QVariant newValue); + + // children accessor + QVariant value() const; + + QString path() const; + + FGQmlPropertyNode* parentProp() const; + + // C++ API, not accessible from QML + void setNode(SGPropertyNode_ptr node); + + SGPropertyNode_ptr node() const; +protected: + // SGPropertyChangeListener API + + void valueChanged(SGPropertyNode * node) override; + +signals: + + void valueChangedNotify(QVariant value); + + void pathChanged(QString path); + + void parentPropChanged(FGQmlPropertyNode* parentProp); + +public slots: + + void setPath(QString path); + +private: + SGPropertyNode_ptr _prop; +}; + +#endif // FGQMLPROPERTYNODE_HXX diff --git a/src/GUI/PropertyItemModel.cxx b/src/GUI/PropertyItemModel.cxx new file mode 100644 index 000000000..354427c38 --- /dev/null +++ b/src/GUI/PropertyItemModel.cxx @@ -0,0 +1,257 @@ +#include "PropertyItemModel.hxx" + +#include
+ +#include "FGQmlPropertyNode.hxx" + +class PropertyItemModelRoleMapping : public QObject +{ + Q_OBJECT + + Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged) + Q_PROPERTY(QString roleName READ roleName WRITE setRoleName NOTIFY roleNameChanged) +public: + + QString path() const + { + return QString::fromStdString(_propertyPath); + } + + QString roleName() const + { + return QString::fromLatin1(_roleName); + } + +public slots: + void setPath(QString path) + { + if (_propertyPath == path.toStdString()); + return; + + _propertyPath = path.toStdString(); + emit pathChanged(path); + } + + void setRoleName(QString roleName) + { + if (_roleName == roleName.toLatin1()) + return; + + _roleName = roleName.toLatin1(); + emit roleNameChanged(roleName); + } + +signals: + void pathChanged(QString path); + + void roleNameChanged(QString roleName); + +private: + friend class PropertyItemModel; + std::string _propertyPath; + QByteArray _roleName; +}; + +PropertyItemModel::PropertyItemModel() +{ + +} + +int PropertyItemModel::rowCount(const QModelIndex &parent) const +{ + return _nodes.size(); +} + +QVariant PropertyItemModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return {}; + + if (index.row() >= _nodes.size()) + return {}; + + SGPropertyNode_ptr n = _nodes.at(index.row()); + Q_ASSERT(n); + int mappingIndex = role - Qt::UserRole; + if ((mappingIndex < 0) || (mappingIndex >= _roleMappings.size())) + return {}; + + const std::string& nodePath = _roleMappings.at(mappingIndex)->_propertyPath; + SGPropertyNode_ptr valueNode = n->getNode(nodePath); + + // convert property value to a variant + + return {}; +} + +QHash PropertyItemModel::roleNames() const +{ + QHash result; + int count = 0; + for (auto m : _roleMappings) { + result[Qt::UserRole + count++] = m->_roleName; + } + return result; +} + +QString PropertyItemModel::rootPath() const +{ + return QString::fromStdString(_modelRoot->getPath()); +} + +QString PropertyItemModel::childNameFilter() const +{ + return _childNameFilter; +} + +FGQmlPropertyNode *PropertyItemModel::root() const +{ + auto n = new FGQmlPropertyNode; + n->setNode(_modelRoot); + return n; +} + +void PropertyItemModel::setRootPath(QString rootPath) +{ + if (_modelRoot->getPath() == rootPath.toStdString()) + return; + + innerSetRoot(fgGetNode(rootPath.toStdString())); +} + +void PropertyItemModel::setChildNameFilter(QString childNameFilter) +{ + if (_childNameFilter == childNameFilter) + return; + + _childNameFilter = childNameFilter; + emit childNameFilterChanged(_childNameFilter); + + beginResetModel(); + rebuild(); + endResetModel(); +} + +void PropertyItemModel::setRoot(FGQmlPropertyNode *root) +{ + if (root->node() == _modelRoot) { + return; + } + + innerSetRoot(root->node()); +} + +void PropertyItemModel::valueChanged(SGPropertyNode *node) +{ + // if node's parent is one of our nodes, emit data changed +} + +void PropertyItemModel::childAdded(SGPropertyNode *parent, SGPropertyNode *child) +{ + if (parent == _modelRoot) { + if (_childNameFilter.isEmpty() || (child->getNameString() == _childNameFilter.toStdString())) { + // insert + // need to find appropriate index based on position + } + } + + // if parent is one of our nodes, emit data changed for it +} + +void PropertyItemModel::childRemoved(SGPropertyNode *parent, SGPropertyNode *child) +{ + if (parent == _modelRoot) { + // remove row + } + + // if parent is one of our nodes, emit data changed for it + +} + +QQmlListProperty PropertyItemModel::mappings() +{ + return QQmlListProperty(this, nullptr, + &PropertyItemModel::append_mapping, + &PropertyItemModel::count_mapping, + &PropertyItemModel::at_mapping, + &PropertyItemModel::clear_mapping + ); +} + +void PropertyItemModel::rebuild() +{ + if (!_modelRoot) { + _nodes.clear(); + return; + } + + if (_childNameFilter.isEmpty()) { + // all children + _nodes.clear(); + const int nChildren = _modelRoot->nChildren(); + _nodes.resize(nChildren); + for (int c=0; c < nChildren; ++c) { + _nodes[c] = _modelRoot->getChild(c); + } + } else { + _nodes = _modelRoot->getChildren(_childNameFilter.toStdString()); + } +} + +void PropertyItemModel::innerSetRoot(SGPropertyNode_ptr root) +{ + if (_modelRoot) { + _modelRoot->removeChangeListener(this);; + } + beginResetModel(); + + _modelRoot = root; + if (_modelRoot) { + _modelRoot->addChangeListener(this); + } + + rebuild(); + emit rootChanged(); + endResetModel(); +} + +void PropertyItemModel::append_mapping(QQmlListProperty *list, + PropertyItemModelRoleMapping *mapping) +{ + PropertyItemModel *model = qobject_cast(list->object); + if (model) { + model->_roleMappings.append(mapping); + model->mappingsChanged(); + model->rebuild(); + } +} + +PropertyItemModelRoleMapping* PropertyItemModel::at_mapping(QQmlListProperty *list, int index) +{ + PropertyItemModel *model = qobject_cast(list->object); + if (model) { + return model->_roleMappings.at(index); + } + return 0; +} + +int PropertyItemModel::count_mapping(QQmlListProperty *list) +{ + PropertyItemModel *model = qobject_cast(list->object); + if (model) { + return model->_roleMappings.size(); + } + return 0; +} + +void PropertyItemModel::clear_mapping(QQmlListProperty *list) +{ + PropertyItemModel *model = qobject_cast(list->object); + if (model) { + model->_roleMappings.clear(); + model->mappingsChanged(); + model->rebuild(); + } +} + +#include "PropertyItemModel.moc" diff --git a/src/GUI/PropertyItemModel.hxx b/src/GUI/PropertyItemModel.hxx new file mode 100644 index 000000000..958fdf52d --- /dev/null +++ b/src/GUI/PropertyItemModel.hxx @@ -0,0 +1,79 @@ +#ifndef PROPERTYITEMMODEL_HXX +#define PROPERTYITEMMODEL_HXX + +#include +#include + +#include + +class PropertyItemModelRoleMapping; +class FGQmlPropertyNode; + +class PropertyItemModel : public QAbstractListModel, + public SGPropertyChangeListener +{ + Q_OBJECT + + Q_PROPERTY(QString rootPath READ rootPath WRITE setRootPath NOTIFY rootChanged) + + Q_PROPERTY(FGQmlPropertyNode* root READ root WRITE setRoot NOTIFY rootChanged) + + Q_PROPERTY(QString childNameFilter READ childNameFilter WRITE setChildNameFilter NOTIFY childNameFilterChanged) + Q_PROPERTY(QQmlListProperty mappings READ mappings NOTIFY mappingsChanged) + // list property storing child/role mapping + + QQmlListProperty mappings(); +public: + PropertyItemModel(); + + int rowCount(const QModelIndex &parent) const override; + + QVariant data(const QModelIndex &index, int role) const override; + + QHash roleNames() const override; + + QString rootPath() const; + + QString childNameFilter() const; + + FGQmlPropertyNode* root() const; + +public slots: + void setRootPath(QString rootPath); + + void setChildNameFilter(QString childNameFilter); + + void setRoot(FGQmlPropertyNode* root); + +signals: + void rootChanged(); + + void childNameFilterChanged(QString childNameFilter); + + void mappingsChanged(); +protected: + // implement property-change listener to watch for changes to the + // underlying nodes + void valueChanged(SGPropertyNode *node) override; + void childAdded(SGPropertyNode *parent, SGPropertyNode *child) override; + void childRemoved(SGPropertyNode *parent, SGPropertyNode *child) override; + +private: + void rebuild(); + + void innerSetRoot(SGPropertyNode_ptr root); + + static void append_mapping(QQmlListProperty *list, + PropertyItemModelRoleMapping *mapping); + static PropertyItemModelRoleMapping* at_mapping(QQmlListProperty *list, int index); + static int count_mapping(QQmlListProperty *list); + static void clear_mapping(QQmlListProperty *list); + + SGPropertyNode_ptr _modelRoot; + QVector _roleMappings; + QString _childNameFilter; + + mutable std::vector _nodes; +}; + +#endif // PROPERTYITEMMODEL_HXX