diff --git a/playground/CMakeLists.txt b/playground/CMakeLists.txt index 592c4938..478937c6 100644 --- a/playground/CMakeLists.txt +++ b/playground/CMakeLists.txt @@ -9,6 +9,7 @@ add_subdirectory(shadows) add_subdirectory(shapes) add_subdirectory(charts) add_subdirectory(plots) +add_subdirectory(models) if (BUILD_INPUTCONTEXT) add_subdirectory(inputpanel) diff --git a/playground/models/CMakeLists.txt b/playground/models/CMakeLists.txt new file mode 100644 index 00000000..c9e3347a --- /dev/null +++ b/playground/models/CMakeLists.txt @@ -0,0 +1,7 @@ +############################################################################ +# QSkinny - Copyright (C) The authors +# SPDX-License-Identifier: BSD-3-Clause +############################################################################ + +qsk_add_example(models MainView.h MainView.cpp main.cpp) +target_link_libraries(models) diff --git a/playground/models/MainView.cpp b/playground/models/MainView.cpp new file mode 100644 index 00000000..f53e2386 --- /dev/null +++ b/playground/models/MainView.cpp @@ -0,0 +1,215 @@ +/****************************************************************************** + * QSkinny - Copyright (C) The authors + * SPDX-License-Identifier: BSD-3-Clause + *****************************************************************************/ + +#include "MainView.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace +{ + class TitleLabel : public QskTextLabel + { + public: + TitleLabel( const QString& text, QQuickItem* parent = nullptr ) + : QskTextLabel( text, parent ) + { + setFontRole( QskFontRole::Title ); + } + }; + + class SpinBox : public QskSpinBox + { + public: + SpinBox( QQuickItem* parent = nullptr ) + : QskSpinBox( -100.0, 100.0, 1.0, parent ) + { + initSizePolicy( QskSizePolicy::Fixed, QskSizePolicy::Fixed ); + } + }; + + class Header : public QskLinearBox + { + Q_OBJECT + + public: + Header( QQuickItem* parent = nullptr ) + : QskLinearBox( Qt::Horizontal, parent ) + { + initSizePolicy( QskSizePolicy::Ignored, QskSizePolicy::Fixed ); + + setPaddingHint( QskBox::Panel, 5 ); + + setPanel( true ); + + auto rowButton = new QskPushButton( "Toggle Row" ); + auto submitButton = new QskPushButton( "Submit Changes" ); + + connect( rowButton, &QskPushButton::clicked, + this, &Header::rowClicked ); + + connect( submitButton, &QskPushButton::clicked, + this, &Header::submitClicked ); + + addItem( rowButton ); + addItem( submitButton ); + addStretch( 1 ); + } + + Q_SIGNALS: + void rowClicked(); + void submitClicked(); + }; + + class Model : public QStandardItemModel + { + public: + Model( QObject* parent = nullptr ) + : QStandardItemModel( 2, 2, parent ) + { + initValue( 0, 0, 1.0 ); + initValue( 0, 1, "HELLO" ); + + initValue( 1, 0, 2.0 ); + initValue( 1, 1, "WORLD" ); + } + + private: + void initValue( int row, int col, const QVariant& value ) + { + setData( index( row, col ), value, Qt::EditRole ); + } + }; + + class DisplayBox : public QskLinearBox + { + public: + DisplayBox( QQuickItem* parent = nullptr ) + : QskLinearBox( Qt::Horizontal, parent ) + { + setMargins( 10, 0, 10, 0 ); + initSizePolicy( QskSizePolicy::MinimumExpanding, QskSizePolicy::Fixed ); + + addItem( new SpinBox() ); + addItem( new QskTextField() ); + } + }; + + class ModelBox : public QskLinearBox + { + public: + ModelBox( QAbstractItemModel* model ) + : QskLinearBox( Qt::Horizontal, model->columnCount() ) + , m_model( model ) + { + setMargins( 10, 0, 10, 0 ); + setExtraSpacingAt( Qt::BottomEdge ); + + for ( int row = 0; row < model->rowCount(); row++ ) + { + for ( int col = 0; col < model->columnCount(); col++ ) + { + const auto value = model->data( + model->index( row, col ), Qt::EditRole ); + + if ( value.userType() == QVariant::Double ) + { + auto spinBox = new SpinBox( this ); + connect( spinBox, &QskSpinBox::valueChanged, + this, &ModelBox::updateModel ); + } + else + { + auto textField = new QskTextField( this ); + connect( textField, &QskTextField::textChanged, + this, &ModelBox::updateModel ); + } + } + } + + updateDisplay(); + + connect( m_model, &Model::dataChanged, this, &ModelBox::updateDisplay ); + } + + private: + void updateModel() + { + if ( auto item = qobject_cast< const QQuickItem* >( sender() ) ) + { + const int index = indexOf( item ); + + const auto modelIndex = m_model->index( + index / dimension(), index % dimension() ); + + const auto property = item->metaObject()->userProperty(); + m_model->setData( modelIndex, property.read( item ), Qt::EditRole ); + } + } + + void updateDisplay() const + { + for ( int row = 0; row < m_model->rowCount(); row++ ) + { + for ( int col = 0; col < m_model->columnCount(); col++ ) + { + const auto index = m_model->index( row, col ); + + if ( auto item = itemAtIndex( row * dimension() + col ) ) + { + const auto property = item->metaObject()->userProperty(); + property.write( item, m_model->data( index, Qt::EditRole ) ); + } + } + } + } + + QPointer< QAbstractItemModel > m_model; + }; +} + +MainView::MainView( QQuickItem* parent ) + : QskMainView( parent ) +{ + m_binder = new QskModelObjectBinder( new Model( this ), this ); + + auto header = new Header(); + + auto displayBox = new DisplayBox(); + for ( int i = 0; i < displayBox->elementCount(); i++ ) + m_binder->bindObject( displayBox->itemAtIndex( i ), i ); + + auto box = new QskLinearBox( Qt::Vertical ); + + box->addItem( new TitleLabel( "Editor:" ) ); + box->addItem( displayBox ); + box->addItem( new QskSeparator() ); + box->addItem( new TitleLabel( "Model:" ) ); + box->addItem( new ModelBox( m_binder->model() ) ); + + setHeader( header ); + setBody( box ); + + connect( header, &Header::rowClicked, + this, &MainView::toogleRow ); + + connect( header, &Header::submitClicked, + m_binder, &QskModelObjectBinder::submit ); +} + +void MainView::toogleRow() +{ + m_binder->setCurrentRow( m_binder->currentRow() == 0 ? 1 : 0 ); +} + +#include "MainView.moc" diff --git a/playground/models/MainView.h b/playground/models/MainView.h new file mode 100644 index 00000000..7f80c80a --- /dev/null +++ b/playground/models/MainView.h @@ -0,0 +1,21 @@ +/****************************************************************************** + * QSkinny - Copyright (C) The authors + * SPDX-License-Identifier: BSD-3-Clause + *****************************************************************************/ + +#pragma once + +#include + +class QskModelObjectBinder; + +class MainView : public QskMainView +{ + public: + MainView( QQuickItem* = nullptr ); + + private: + void toogleRow(); + + QskModelObjectBinder* m_binder; +}; diff --git a/playground/models/main.cpp b/playground/models/main.cpp new file mode 100644 index 00000000..27f25d63 --- /dev/null +++ b/playground/models/main.cpp @@ -0,0 +1,37 @@ +/****************************************************************************** + * QSkinny - Copyright (C) The authors + * SPDX-License-Identifier: BSD-3-Clause + *****************************************************************************/ + +#include "MainView.h" + +#include + +#include +#include +#include + +#include + +int main( int argc, char* argv[] ) +{ +#ifdef ITEM_STATISTICS + QskObjectCounter counter( true ); +#endif + + QGuiApplication app( argc, argv ); + + SkinnyShortcut::enable( SkinnyShortcut::AllShortcuts ); + + QskWindow window; + window.resize( 600, 400 ); + + auto focusIndicator = new QskFocusIndicator(); + focusIndicator->setObjectName( "FocusIndicator" ); + + window.addItem( focusIndicator ); + window.addItem( new MainView() ); + window.show(); + + return app.exec(); +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3b587adc..06405bb7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -235,6 +235,7 @@ list(APPEND HEADERS controls/QskListViewSkinlet.h controls/QskMenu.h controls/QskMenuSkinlet.h + controls/QskModelObjectBinder.h controls/QskObjectTree.h controls/QskPageIndicator.h controls/QskPageIndicatorSkinlet.h @@ -347,6 +348,7 @@ list(APPEND SOURCES controls/QskListViewSkinlet.cpp controls/QskMenuSkinlet.cpp controls/QskMenu.cpp + controls/QskModelObjectBinder.cpp controls/QskObjectTree.cpp controls/QskPageIndicator.cpp controls/QskPageIndicatorSkinlet.cpp diff --git a/src/controls/QskModelObjectBinder.cpp b/src/controls/QskModelObjectBinder.cpp new file mode 100644 index 00000000..bc978809 --- /dev/null +++ b/src/controls/QskModelObjectBinder.cpp @@ -0,0 +1,274 @@ +/****************************************************************************** + * QSkinny - Copyright (C) The authors + * SPDX-License-Identifier: BSD-3-Clause + *****************************************************************************/ + +#include "QskModelObjectBinder.h" + +#include +#include +#include + +static inline QMetaProperty qskMetaProperty( + const QMetaObject* metaObject, const QByteArray& name ) +{ + if ( name.isEmpty() ) + return metaObject->userProperty(); + + const auto idx = metaObject->indexOfProperty( name ); + return ( idx >= 0 ) ? metaObject->property( idx ) : QMetaProperty(); +} + +static void qskEnableConnections( QObject* object, + QskModelObjectBinder* binder, bool on ) +{ + if ( on ) + { + QObject::connect( object, &QObject::destroyed, + binder, &QskModelObjectBinder::unbindObject ); + } + else + { + QObject::disconnect( object, &QObject::destroyed, + binder, &QskModelObjectBinder::unbindObject ); + } +} + +namespace +{ + struct Binding + { + Binding( int column, const QMetaProperty& property ) + : property( property ) + , column( column ) + { + } + + QMetaProperty property; + int column; + }; +} + +class QskModelObjectBinder::PrivateData +{ + public: + void updateProperties() + { + if ( model && currentRowIndex.isValid() ) + { + for ( auto it = bindings.constBegin(); it != bindings.constEnd(); ++it ) + updateObjectProperty( it.key(), it.value() ); + } + } + + void updateProperties( const QModelIndex& topLeft, + const QModelIndex& bottomRight, const QVector< int >& roles ) + { + if ( !( model && currentRowIndex.isValid() ) ) + return; + + if ( !( roles.isEmpty() || roles.contains( Qt::EditRole ) ) ) + return; + + const int row = currentRowIndex.row(); + + if ( topLeft.row() <= row && row <= bottomRight.row() ) + { + for ( auto it = bindings.constBegin(); it != bindings.constEnd(); ++it ) + { + const int col = it.value().column; + + if( topLeft.column() <= col && col <= bottomRight.column() ) + updateObjectProperty( it.key(), it.value() ); + } + } + } + + void updateObjectProperty( QObject* object, const Binding& binding ) + { + const auto index = model->index( currentRowIndex.row(), binding.column ); + binding.property.write( object, index.data( Qt::EditRole ) ); + } + + public: + QMap< QObject*, Binding > bindings; + QPointer< QAbstractItemModel > model; + QPersistentModelIndex currentRowIndex; +}; + +QskModelObjectBinder::QskModelObjectBinder( QObject* parent ) + : QObject( parent ) + , m_data( new PrivateData() ) +{ +} + +QskModelObjectBinder::QskModelObjectBinder( QAbstractItemModel* model, QObject* parent ) + : QskModelObjectBinder( parent ) +{ + setModel( model ); +} + +QskModelObjectBinder::~QskModelObjectBinder() +{ +} + +void QskModelObjectBinder::bindObject( + QObject* object, int column, const QByteArray& propertyName ) +{ + if( object == nullptr ) + return; + + // does not work with dynamic properties ... + + const auto metaProperty = qskMetaProperty( object->metaObject(), propertyName ); + Q_ASSERT( metaProperty.isValid() ); + + if ( metaProperty.isValid() ) + { + const Binding binding = { column, metaProperty }; + + m_data->bindings.insert( object, binding ); + qskEnableConnections( object, this, true ); + + if ( m_data->model && m_data->currentRowIndex.isValid() ) + m_data->updateObjectProperty( object, binding ); + } +} + +void QskModelObjectBinder::unbindObject( QObject* object ) +{ + auto& bindings = m_data->bindings; + + auto it = bindings.find( object ); + if ( it != bindings.end() ) + { + qskEnableConnections( object, this, false ); + bindings.erase( it ); + } +} + +void QskModelObjectBinder::clearBindings() +{ + auto& bindings = m_data->bindings; + + for ( auto it = bindings.constBegin(); it != bindings.constEnd(); ++it ) + qskEnableConnections( it.key(), this, false ); + + bindings.clear(); +} + +QObjectList QskModelObjectBinder::boundObjects() const +{ + auto& bindings = m_data->bindings; + + QObjectList objects; + objects.reserve( bindings.count() ); + + for ( auto it = bindings.constBegin(); it != bindings.constEnd(); ++it ) + objects += it.key(); + + return objects; +} + +QMetaProperty QskModelObjectBinder::boundProperty( const QObject* object ) const +{ + auto it = m_data->bindings.constFind( const_cast< QObject* >( object ) ); + if ( it != m_data->bindings.constEnd() ) + return it.value().property; + + return QMetaProperty(); +} + +void QskModelObjectBinder::setModel( QAbstractItemModel* model ) +{ + if ( model == m_data->model ) + return; + + if ( m_data->model ) + { + disconnect( m_data->model, &QAbstractItemModel::dataChanged, this, nullptr ); + + m_data->model = nullptr; + m_data->currentRowIndex = QModelIndex(); + + clearBindings(); + } + + m_data->model = model; + + if( model ) + { + auto updateProperties = [this]( + const QModelIndex& topLeft, const QModelIndex& bottomRight, + const QVector< int >& roles ) + { + m_data->updateProperties( topLeft, bottomRight, roles ); + }; + + connect( m_data->model, &QAbstractItemModel::dataChanged, + this, updateProperties ); + + connect( m_data->model, &QObject::destroyed, + this, &QskModelObjectBinder::clearBindings ); + + setCurrentRow( 0 ); + } +} + +const QAbstractItemModel* QskModelObjectBinder::model() const +{ + return m_data->model; +} + +QAbstractItemModel* QskModelObjectBinder::model() +{ + return m_data->model; +} + +void QskModelObjectBinder::setCurrentRow( int row ) +{ + auto model = m_data->model.data(); + auto& bindings = m_data->bindings; + + Q_ASSERT( model != nullptr ); + + if ( model && row >= 0 && row < model->rowCount() ) + { + m_data->currentRowIndex = model->index( row, 0 ); + + for ( auto it = bindings.constBegin(); it != bindings.constEnd(); ++it ) + m_data->updateObjectProperty( it.key(), it.value() ); + + Q_EMIT currentRowChanged( row ); + } +} + +int QskModelObjectBinder::currentRow() const +{ + return m_data->currentRowIndex.row(); +} + +void QskModelObjectBinder::submit() +{ + if ( auto model = m_data->model ) + { + const auto& bindings = m_data->bindings; + + for ( auto it = bindings.begin(); it != bindings.end(); ++it ) + { + const auto value = it.value().property.read( it.key() ); + + const auto row = m_data->currentRowIndex.row(); + const auto index = model->index( row, it.value().column ); + + model->setData( index, value ); + } + } +} + +void QskModelObjectBinder::revert() +{ + m_data->updateProperties(); +} + +#include "moc_QskModelObjectBinder.cpp" diff --git a/src/controls/QskModelObjectBinder.h b/src/controls/QskModelObjectBinder.h new file mode 100644 index 00000000..4133a17b --- /dev/null +++ b/src/controls/QskModelObjectBinder.h @@ -0,0 +1,61 @@ +/****************************************************************************** + * QSkinny - Copyright (C) The authors + * SPDX-License-Identifier: BSD-3-Clause + *****************************************************************************/ + +#ifndef QSK_MODEL_OBJECT_BINDER_H +#define QSK_MODEL_OBJECT_BINDER_H + +#include "QskGlobal.h" + +#include +#include + +#include + +class QAbstractItemModel; +class QMetaProperty; + +class QSK_EXPORT QskModelObjectBinder : public QObject +{ + Q_OBJECT + + Q_PROPERTY( int currentRow READ currentRow + WRITE setCurrentRow NOTIFY currentRowChanged ) + + public: + QskModelObjectBinder( QObject* parent = nullptr ); + QskModelObjectBinder( QAbstractItemModel*, QObject* parent = nullptr ); + + ~QskModelObjectBinder() override; + + void setModel( QAbstractItemModel* ); + + const QAbstractItemModel* model() const; + QAbstractItemModel* model(); + + void setCurrentRow( int row ); + int currentRow() const; + + void bindObject( QObject*, int column, + const QByteArray& propertyName = QByteArray() ); + + void unbindObject( QObject* ); + + QMetaProperty boundProperty( const QObject* ) const; + QObjectList boundObjects() const; + + Q_SIGNALS: + void currentRowChanged( int ); + + public Q_SLOTS: + void submit(); + void revert(); + void clearBindings(); + + private: + class PrivateData; + std::unique_ptr< PrivateData > m_data; +}; + +#endif