diff --git a/src/blackgui/models/listmodelbase.cpp b/src/blackgui/models/listmodelbase.cpp index fa2bc0f3f..59b78faa0 100644 --- a/src/blackgui/models/listmodelbase.cpp +++ b/src/blackgui/models/listmodelbase.cpp @@ -26,36 +26,16 @@ namespace BlackGui /* * Column count */ - template - int CListModelBase::columnCount(const QModelIndex & /** modelIndex **/) const + int CListModelBaseNonTemplate::columnCount(const QModelIndex & /** modelIndex **/) const { int c = this->m_columns.size(); return c; } - /* - * Row count - */ - template - int CListModelBase::rowCount(const QModelIndex & /** parent */) const - { - return this->m_container.size(); - } - - /* - * Column to property index - */ - template - BlackMisc::CPropertyIndex CListModelBase::columnToPropertyIndex(int column) const - { - return this->m_columns.columnToPropertyIndex(column); - } - /* * Header data */ - template QVariant - CListModelBase::headerData(int section, Qt::Orientation orientation, int role) const + QVariant CListModelBaseNonTemplate::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal) { @@ -75,6 +55,54 @@ namespace BlackGui return QVariant(); } + /* + * Column to property index + */ + BlackMisc::CPropertyIndex CListModelBaseNonTemplate::columnToPropertyIndex(int column) const + { + return this->m_columns.columnToPropertyIndex(column); + } + + /* + * Sort column? + */ + bool CListModelBaseNonTemplate::hasValidSortColumn() const + { + return this->m_sortedColumn >= 0 && this->m_sortedColumn < this->m_columns.size(); + } + + /* + * Make editable + */ + Qt::ItemFlags CListModelBaseNonTemplate::flags(const QModelIndex &index) const + { + Qt::ItemFlags f = QAbstractListModel::flags(index); + if (this->m_columns.isEditable(index)) + return f | Qt::ItemIsEditable; + else + return f; + } + + /* + * Row count + */ + template + int CListModelBase::rowCount(const QModelIndex & /** parent */) const + { + return this->m_container.size(); + } + + /* + * Valid index? + */ + template + bool CListModelBase::isValidIndex(const QModelIndex &index) const + { + if (!index.isValid()) return false; + return (index.row() >= 0 && index.row() < this->m_container.size() && + index.column() >= 0 && index.column() < this->columnCount(index)); + } + /* * Data */ @@ -100,12 +128,26 @@ namespace BlackGui int CListModelBase::update(const ContainerType &container, bool sort) { // KWB remove: qDebug() will be removed soon - qDebug() << "update" << this->objectName() << "size" << container.size(); + qDebug() << "update" << this->objectName() << "size" << container.size() << "thread:" << QThread::currentThreadId(); + + // Keep sorting out of begin/end reset model + QTime myTimer; + + ContainerType sortedContainer; + bool performSort = sort && container.size() > 1 && this->hasValidSortColumn(); + if (performSort) + { + myTimer.start(); + sortedContainer = this->sortContainerByColumn(container, this->getSortColumn(), this->m_sortOrder); + qDebug() << this->objectName() << "Sort performed ms:" << myTimer.restart() << "thread:" << QThread::currentThreadId(); + } + this->beginResetModel(); - this->m_container = (sort && container.size() > 1 && this->hasValidSortColumn() ? - this->sortListByColumn(container, this->getSortColumn(), this->m_sortOrder) : - container); + this->m_container = performSort ? sortedContainer : container; this->endResetModel(); + + // TODO: KWB remove + qDebug() << this->objectName() << "Reset performed ms:" << myTimer.restart() << "objects:" << this->m_container.size() << "thread:" << QThread::currentThreadId(); return this->m_container.size(); } @@ -123,6 +165,56 @@ namespace BlackGui emit this->dataChanged(i1, i2); // which range has been changed } + /* + * Async update + */ + template + BlackGui::IUpdateWorker *CListModelBase::updateAsync(const ContainerType &container, bool sort) + { + // TODO: mutex + CModelUpdateWorker *worker = new CModelUpdateWorker(this, container, sort); + if (worker->start()) { return worker; } + + // start failed, we have responsibility to clean up the worker + Q_ASSERT_X(false, "CModelBase", "cannot start worker"); + worker->terminate(); + return nullptr; + } + + /* + * Container size decides async/sync + */ + template + void CListModelBase::updateContainerMaybeAsync(const ContainerType &container, bool sort) + { + if (container.size() > asyncThreshold && sort) + { + // larger container with sorting + updateAsync(container, sort); + } + else + { + update(container, sort); + } + } + + /* + * At + */ + template + const ObjectType &CListModelBase::at(const QModelIndex &index) const + { + if (index.row() < 0 || index.row() >= this->m_container.size()) + { + const static ObjectType def; // default object + return def; + } + else + { + return this->m_container[index.row()]; + } + } + /* * Push back */ @@ -159,14 +251,23 @@ namespace BlackGui /* * Clear */ - template - void CListModelBase::clear() + template void CListModelBase::clear() { beginResetModel(); this->m_container.clear(); endResetModel(); } + /* + * Update on container + */ + template int CListModelBase::performUpdateContainer(const QVariant &variant, bool sort) + { + ContainerType c; + c.convertFromQVariant(variant); + return this->update(c, sort); + } + /* * Sort requested by abstract model */ @@ -180,21 +281,23 @@ namespace BlackGui if (this->m_container.size() < 2) return; // nothing to do // sort the values - this->update(this->m_container, true); + this->updateContainerMaybeAsync(this->m_container, true); } /* * Sort list */ - template ContainerType CListModelBase::sortListByColumn(const ContainerType &list, int column, Qt::SortOrder order) + template ContainerType CListModelBase::sortContainerByColumn(const ContainerType &container, int column, Qt::SortOrder order) const { - if (list.size() < 2) return list; // nothing to do + if (container.size() < 2) return container; // nothing to do + + // this is the only part not really thread safe, but columns do not change so far BlackMisc::CPropertyIndex propertyIndex = this->m_columns.columnToPropertyIndex(column); Q_ASSERT(!propertyIndex.isEmpty()); - if (propertyIndex.isEmpty()) return list; // at release build do nothing + if (propertyIndex.isEmpty()) return container; // at release build do nothing // sort the values - auto p = [ = ](const ObjectType & a, const ObjectType & b) -> bool + const auto p = [ = ](const ObjectType & a, const ObjectType & b) -> bool { QVariant aQv = a.propertyByIndex(propertyIndex); QVariant bQv = b.propertyByIndex(propertyIndex); @@ -205,20 +308,11 @@ namespace BlackGui }; // KWB: qDebug() will be removed soon - qDebug() << "sort" << this->objectName() << "column" << column << propertyIndex.toQString(); - return list.sorted(p); // synchronous sorted - } - - /* - * Make editable - */ - template Qt::ItemFlags CListModelBase::flags(const QModelIndex &index) const - { - Qt::ItemFlags f = QAbstractListModel::flags(index); - if (this->m_columns.isEditable(index)) - return f | Qt::ItemIsEditable; - else - return f; + QTime t; + t.start(); + const ContainerType sorted = container.sorted(p); + qDebug() << "Sort" << this->objectName() << "column" << column << "index:" << propertyIndex.toQString() << "ms:" << t.elapsed() << "thread:" << QThread::currentThreadId(); + return sorted; } // see here for the reason of thess forward instantiations @@ -232,5 +326,6 @@ namespace BlackGui template class CListModelBase; template class CListModelBase; template class CListModelBase; - } + + } // namespace } // namespace diff --git a/src/blackgui/models/listmodelbase.h b/src/blackgui/models/listmodelbase.h index 5912a6a93..c31530d44 100644 --- a/src/blackgui/models/listmodelbase.h +++ b/src/blackgui/models/listmodelbase.h @@ -13,24 +13,26 @@ #define BLACKGUI_LISTMODELBASE_H #include "blackgui/models/columns.h" +#include "blackgui/updateworker.h" #include "blackmisc/propertyindex.h" #include +#include namespace BlackGui { namespace Models { - - /*! - * List model - */ - template class CListModelBase : public QAbstractListModel + //! Non templated base class, allows Q_OBJECT and signals to be used + class CListModelBaseNonTemplate : public QAbstractListModel { + Q_OBJECT public: + //! Number of elements when to use asynchronous updates + static const int asyncThreshold = 50; //! Destructor - virtual ~CListModelBase() {} + virtual ~CListModelBaseNonTemplate() {} //! \copydoc QAbstractListModel::columnCount() virtual int columnCount(const QModelIndex &modelIndex) const override; @@ -47,14 +49,6 @@ namespace BlackGui return this->columnToPropertyIndex(index.column()); } - //! Valid index (in range) - virtual bool isValidIndex(const QModelIndex &index) const - { - if (!index.isValid()) return false; - return (index.row() >= 0 && index.row() < this->m_container.size() && - index.column() >= 0 && index.column() < this->columnCount(index)); - } - //! Set sort column virtual void setSortColumn(int column) { this->m_sortedColumn = column; } @@ -71,14 +65,67 @@ namespace BlackGui virtual int getSortColumn() const { return this->m_sortedColumn; } //! Has valid sort column? - virtual bool hasValidSortColumn() const - { - return this->m_sortedColumn >= 0 && this->m_sortedColumn < this->m_columns.size(); - } + virtual bool hasValidSortColumn() const; //! Get sort order virtual Qt::SortOrder getSortOrder() const { return this->m_sortOrder; } + //! \copydoc QAbstractTableModel::flags + Qt::ItemFlags flags(const QModelIndex &index) const override; + + //! Translation context + virtual const QString &getTranslationContext() const + { + return m_columns.getTranslationContext(); + } + + signals: + //! Asynchronous update finished + void asyncUpdateFinished(); + + protected slots: + //! Helper method with template free signature + int updateContainer(const QVariant &variant, bool sort) + { + return this->performUpdateContainer(variant, sort); + } + + protected: + /*! + * Constructor + * \param translationContext I18N context + * \param parent + */ + CListModelBaseNonTemplate(const QString &translationContext, QObject *parent = nullptr) + : QAbstractListModel(parent), m_columns(translationContext), m_sortedColumn(-1), m_sortOrder(Qt::AscendingOrder) + { + // non unique default name, set translation context as default + this->setObjectName(translationContext); + } + + //! Helper method with template free signature + virtual int performUpdateContainer(const QVariant &variant, bool sort) = 0; + + CColumns m_columns; //!< columns metadata + int m_sortedColumn; //!< current sort column + Qt::SortOrder m_sortOrder; //!< sort order (asc/desc) + }; + + + /*! + * List model + */ + template class CListModelBase : + public CListModelBaseNonTemplate + { + + public: + //! Destructor + virtual ~CListModelBase() {} + + //! Valid index (in range) + virtual bool isValidIndex(const QModelIndex &index) const; + //! Used container data virtual const ContainerType &getContainer() const { return this->m_container; } @@ -88,13 +135,17 @@ namespace BlackGui //! \copydoc QAbstractListModel::rowCount() virtual int rowCount(const QModelIndex &index = QModelIndex()) const override; - //! \copydoc QAbstractTableModel::flags - Qt::ItemFlags flags(const QModelIndex &index) const override; - //! Update by new container //! \remarks a sorting is performed only if a valid sort column is set virtual int update(const ContainerType &container, bool sort = true); + //! Asynchronous update + //! \return worker or nullptr if worker could not be started + virtual BlackGui::IUpdateWorker *updateAsync(const ContainerType &container, bool sort = true); + + //! Update by new container + virtual void updateContainerMaybeAsync(const ContainerType &container, bool sort = true); + //! Update single element virtual void update(const QModelIndex &index, const ObjectType &object); @@ -105,22 +156,21 @@ namespace BlackGui } //! Object at row position - virtual const ObjectType &at(const QModelIndex &index) const - { - if (index.row() < 0 || index.row() >= this->m_container.size()) - { - const static ObjectType def; // default object - return def; - } - else - { - return this->m_container[index.row()]; - } - } + virtual const ObjectType &at(const QModelIndex &index) const; //! \copydoc QAbstractListModel::sort() virtual void sort(int column, Qt::SortOrder order) override; + /*! + * Sort container by given column / order. This is used by sort() but als + * for asynchronous updates in the views + * \param container used list + * \param column column inder + * \param order sort order (ascending / descending) + * \threadsafe + */ + ContainerType sortContainerByColumn(const ContainerType &container, int column, Qt::SortOrder order) const; + //! Similar to ContainerType::push_back virtual void push_back(const ObjectType &object); @@ -133,17 +183,8 @@ namespace BlackGui //! Clear the list virtual void clear(); - //! Translation context - virtual const QString &getTranslationContext() const - { - return m_columns.getTranslationContext(); - } - protected: ContainerType m_container; //!< used container - CColumns m_columns; //!< columns metadata - int m_sortedColumn; //!< current sort column - Qt::SortOrder m_sortOrder; //!< sort order (asc/desc) /*! * Constructor @@ -151,22 +192,61 @@ namespace BlackGui * \param parent */ CListModelBase(const QString &translationContext, QObject *parent = nullptr) - : QAbstractListModel(parent), m_columns(translationContext), m_sortedColumn(-1), m_sortOrder(Qt::AscendingOrder) - { - // non unique default name, set translation context as default - this->setObjectName(translationContext); - } + : CListModelBaseNonTemplate(translationContext, parent) + { } - /*! - * Sort container by given column / order. This is used by sort(). - * \param list used list - * \param column column inder - * \param order sort order (ascending / descending) - * \return - */ - ContainerType sortListByColumn(const ContainerType &list, int column, Qt::SortOrder order); + //! \copydoc CModelBaseNonTemplate::performUpdateContainer + virtual int performUpdateContainer(const QVariant &variant, bool sort) override; + + // ---- worker ----------------------------------------------------------------------------------- + + //! Worker class performing update and sorting in background + class CModelUpdateWorker : public BlackGui::IUpdateWorker + { + + public: + //! Constructor + CModelUpdateWorker(CListModelBase *model, const ContainerType &container, bool sort) : + BlackGui::IUpdateWorker(sort), m_model(model), m_container(container) + { + + Q_ASSERT(model); + this->m_sortColumn = model->getSortColumn(); + this->m_sortOrder = model->getSortOrder(); + connect(this, &CModelUpdateWorker::updateFinished, model, &CListModelBase::asyncUpdateFinished, Qt::QueuedConnection); + this->setObjectName(model->objectName().append(":CModelUpdateWorker")); + } + + //! Destructor + virtual ~CModelUpdateWorker() {} + + protected: + //! \copydoc CUpdateWorkerPrivate::update + virtual void update() override + { + // KWB remove later + qDebug() << this->objectName() << "thread:" << QThread::currentThreadId(); + if (m_model) + { + if (m_sort) + { + // almost thread safe sorting in background + m_container = m_model->sortContainerByColumn(m_container, m_sortColumn, m_sortOrder); + } + // now update model itself thread safe, but time for sort was saved + QMetaObject::invokeMethod(m_model, "updateContainer", Qt::QueuedConnection, + Q_ARG(QVariant, m_container.toQVariant()), Q_ARG(bool, false)); + } + } + + CListModelBase *m_model = nullptr; //!< model to be updated, actually const but invokeMethod does not allow const + ContainerType m_container; //!< container with data + }; + + // ---- worker ----------------------------------------------------------------------------------- }; + } // namespace } // namespace #endif // guard diff --git a/src/blackgui/views/aircraftview.h b/src/blackgui/views/aircraftview.h index 515bc9418..805e10027 100644 --- a/src/blackgui/views/aircraftview.h +++ b/src/blackgui/views/aircraftview.h @@ -20,7 +20,7 @@ namespace BlackGui namespace Views { //! Aircrafts view - class CAircraftView : public CViewBase + class CAircraftView : public CViewBase { public: diff --git a/src/blackgui/views/airportview.h b/src/blackgui/views/airportview.h index 4897b0a64..173be7414 100644 --- a/src/blackgui/views/airportview.h +++ b/src/blackgui/views/airportview.h @@ -20,7 +20,7 @@ namespace BlackGui namespace Views { //! Airports view - class CAirportView : public CViewBase + class CAirportView : public CViewBase { public: diff --git a/src/blackgui/views/atcstationview.cpp b/src/blackgui/views/atcstationview.cpp index 3049a726e..44d7705ac 100644 --- a/src/blackgui/views/atcstationview.cpp +++ b/src/blackgui/views/atcstationview.cpp @@ -46,8 +46,7 @@ namespace BlackGui void CAtcStationView::changedAtcStationConnectionStatus(const Aviation::CAtcStation &station, bool added) { this->m_model->changedAtcStationConnectionStatus(station, added); - this->resizeColumnsToContents(); - this->resizeRowsToContents(); + this->resizeToContents(); } void CAtcStationView::customMenu(QMenu &menu) const diff --git a/src/blackgui/views/atcstationview.h b/src/blackgui/views/atcstationview.h index b1cbcdf2e..a59a3e2de 100644 --- a/src/blackgui/views/atcstationview.h +++ b/src/blackgui/views/atcstationview.h @@ -21,7 +21,7 @@ namespace BlackGui namespace Views { //! ATC stations view - class CAtcStationView : public CViewBase + class CAtcStationView : public CViewBase { Q_OBJECT diff --git a/src/blackgui/views/clientview.h b/src/blackgui/views/clientview.h index 8fc17cbe9..9f620ef5b 100644 --- a/src/blackgui/views/clientview.h +++ b/src/blackgui/views/clientview.h @@ -20,7 +20,7 @@ namespace BlackGui namespace Views { //! Client view - class CClientView : public CViewBase + class CClientView : public CViewBase { public: //! Constructor diff --git a/src/blackgui/views/keyboardkeyview.h b/src/blackgui/views/keyboardkeyview.h index 0ac7b21f6..7d5fafd25 100644 --- a/src/blackgui/views/keyboardkeyview.h +++ b/src/blackgui/views/keyboardkeyview.h @@ -20,7 +20,7 @@ namespace BlackGui namespace Views { //! Keyboard key view - class CKeyboardKeyView : public CViewBase + class CKeyboardKeyView : public CViewBase { public: diff --git a/src/blackgui/views/namevariantpairview.h b/src/blackgui/views/namevariantpairview.h index 01013d055..335792f24 100644 --- a/src/blackgui/views/namevariantpairview.h +++ b/src/blackgui/views/namevariantpairview.h @@ -20,7 +20,7 @@ namespace BlackGui namespace Views { //! User view - class CNameVariantPairView : public CViewBase + class CNameVariantPairView : public CViewBase { public: diff --git a/src/blackgui/views/serverview.h b/src/blackgui/views/serverview.h index 9a65017e5..e1e360725 100644 --- a/src/blackgui/views/serverview.h +++ b/src/blackgui/views/serverview.h @@ -20,7 +20,7 @@ namespace BlackGui namespace Views { //! Network servers - class CServerView : public CViewBase + class CServerView : public CViewBase { public: diff --git a/src/blackgui/views/statusmessageview.h b/src/blackgui/views/statusmessageview.h index 80fea0bea..121181cc4 100644 --- a/src/blackgui/views/statusmessageview.h +++ b/src/blackgui/views/statusmessageview.h @@ -20,7 +20,7 @@ namespace BlackGui namespace Views { //! Status message view - class CStatusMessageView : public CViewBase + class CStatusMessageView : public CViewBase { public: diff --git a/src/blackgui/views/userview.h b/src/blackgui/views/userview.h index d3f7feec4..3e7da9ec2 100644 --- a/src/blackgui/views/userview.h +++ b/src/blackgui/views/userview.h @@ -20,7 +20,7 @@ namespace BlackGui namespace Views { //! User view - class CUserView : public CViewBase + class CUserView : public CViewBase { public: diff --git a/src/blackgui/views/viewbase.cpp b/src/blackgui/views/viewbase.cpp new file mode 100644 index 000000000..1947f1194 --- /dev/null +++ b/src/blackgui/views/viewbase.cpp @@ -0,0 +1,261 @@ +/* Copyright (C) 2013 + * swift project Community / Contributors + * + * This file is part of swift project. It is subject to the license terms in the LICENSE file found in the top-level + * directory of this distribution and at http://www.swift-project.org/license.html. No part of swift project, + * including this file, may be copied, modified, propagated, or distributed except according to the terms + * contained in the LICENSE file. + */ + +#include "viewbase.h" +#include "../models/statusmessagelistmodel.h" +#include "../models/namevariantpairlistmodel.h" +#include "../models/atcstationlistmodel.h" +#include "../models/aircraftlistmodel.h" +#include "../models/airportlistmodel.h" +#include "../models/serverlistmodel.h" +#include "../models/userlistmodel.h" +#include "../models/clientlistmodel.h" +#include "../models/keyboardkeylistmodel.h" + +#include +#include +#include +#include + +using namespace BlackMisc; +using namespace BlackGui::Models; + +namespace BlackGui +{ + namespace Views + { + + void CViewBaseNonTemplate::resizeToContents() + { + this->performResizeToContents(); + } + + CViewBaseNonTemplate::CViewBaseNonTemplate(QWidget *parent) : QTableView(parent) + { + this->setContextMenuPolicy(Qt::CustomContextMenu); + connect(this, &QWidget::customContextMenuRequested, this, &CViewBaseNonTemplate::ps_customMenuRequested); + } + + void CViewBaseNonTemplate::customMenu(QMenu &menu) const + { + menu.addAction(BlackMisc::CIcons::refresh16(), "Update", this, SIGNAL(requestUpdate())); + menu.addAction(BlackMisc::CIcons::delete16(), "Clear", this, SLOT(ps_clear())); + menu.addSeparator(); + menu.addAction(BlackMisc::CIcons::resize16(), "Full resize", this, SLOT(fullResizeToContents())); + + // resize to content might decrease performance, + // so I only allow changing to "content resizing" if size matches + bool enabled = !this->reachedResizeThreshold(); + QAction *actionInteractiveResize = new QAction(&menu); + actionInteractiveResize->setObjectName(this->objectName().append("ActionResizing")); + actionInteractiveResize->setIconText("Resize (auto)"); + actionInteractiveResize->setIcon(CIcons::viewMulticolumn()); + actionInteractiveResize->setCheckable(true); + actionInteractiveResize->setChecked(this->m_resizeMode == ResizingAuto); + actionInteractiveResize->setEnabled(enabled); + menu.addAction(actionInteractiveResize); + connect(actionInteractiveResize, &QAction::toggled, this, &CViewBaseNonTemplate::ps_toggleResizeMode); + } + + int CViewBaseNonTemplate::getHorizontalHeaderFontHeight() const + { + QFontMetrics m(this->getHorizontalHeaderFont()); + int h = m.height(); + return h; + } + + void CViewBaseNonTemplate::standardInit() + { + int fh = qRound(1.5 * this->getHorizontalHeaderFontHeight()); + this->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive); // faster mode + this->horizontalHeader()->setStretchLastSection(true); + this->verticalHeader()->setDefaultSectionSize(fh); + this->verticalHeader()->setMinimumSectionSize(fh); + this->initRowsResizeModeToInteractive(); + } + + void CViewBaseNonTemplate::initRowsResizeModeToInteractive() + { + const int h = this->verticalHeader()->minimumSectionSize(); + this->setRowsResizeModeToInteractive(h); + } + + void CViewBaseNonTemplate::setRowsResizeModeToInteractive(int height) + { + QHeaderView *verticalHeader = this->verticalHeader(); + Q_ASSERT(verticalHeader); + verticalHeader->setSectionResizeMode(QHeaderView::Interactive); + verticalHeader->setDefaultSectionSize(height); + } + + bool CViewBaseNonTemplate::resize() const + { + if (m_resizeMode == ResizingOnce) { return m_resizeCount < 1; } + if (m_resizeMode == ResizingAuto) + { + if (reachedResizeThreshold()) return false; + if (m_resizeAutoNthTime < 2) return true; + return (m_resizeCount % m_resizeAutoNthTime) == 0; + } + return m_resizeMode == ResizingOff; + } + + void CViewBaseNonTemplate::fullResizeToContents() + { + // KWB remove + QTime t; + t.start(); + + m_resizeCount++; + this->resizeColumnsToContents(); + this->resizeRowsToContents(); + + qDebug() << this->objectName() << "resize ms:" << t.elapsed() << QThread::currentThreadId(); + } + + void CViewBaseNonTemplate::ps_customMenuRequested(QPoint pos) + { + QMenu menu; + this->customMenu(menu); + if (menu.isEmpty()) { return; } + + QPoint globalPos = this->mapToGlobal(pos); + menu.exec(globalPos); + } + + void CViewBaseNonTemplate::ps_toggleResizeMode(bool checked) + { + if (checked) + { + this->m_resizeMode = ResizingAuto; + } + else + { + this->m_resizeMode = ResizingOff; + } + } + + template int CViewBase::updateContainer(const ContainerType &container, bool sort, bool resize) + { + Q_ASSERT(this->m_model); + int c = this->m_model->update(container, sort); + if (resize) { this->resizeToContents(); } + return c; + } + + template IUpdateWorker *CViewBase::updateContainerAsync(const ContainerType &container, bool sort, bool resize) + { + // TODO: mutex + CViewUpdateWorker *worker = new CViewUpdateWorker(this, container, sort, resize); + if (worker->start()) { return worker; } + + // start failed, we have responsibility to clean up the worker + Q_ASSERT_X(false, "CViewBase", "cannot start worker"); + worker->terminate(); + return nullptr; + } + + template void CViewBase::updateContainerMaybeAsync(const ContainerType &container, bool sort, bool resize) + { + if (container.size() > asyncThreshold && sort) + { + // larger container with sorting + updateContainerAsync(container, sort, resize); + } + else + { + updateContainer(container, sort, resize); + } + } + + template int CViewBase::rowCount() const + { + Q_ASSERT(this->m_model); + return this->m_model->rowCount(); + } + + template int CViewBase::columnCount() const + { + Q_ASSERT(this->m_model); + return this->m_model->columnCount(QModelIndex()); + } + + template bool CViewBase::isEmpty() const + { + Q_ASSERT(this->m_model); + return this->m_model->rowCount() < 1; + } + + template void CViewBase::setObjectName(const QString &name) + { + // then name here is mainly set for debugging purposes so each model can be identified + Q_ASSERT(m_model); + QTableView::setObjectName(name); + QString modelName = QString(name).append(':').append(this->m_model->getTranslationContext()); + this->m_model->setObjectName(modelName); + } + + template void CViewBase::setSortIndicator() + { + if (this->m_model->hasValidSortColumn()) + { + Q_ASSERT(this->horizontalHeader()); + this->horizontalHeader()->setSortIndicator( + this->m_model->getSortColumn(), + this->m_model->getSortOrder()); + } + } + + template void CViewBase::standardInit(ModelClass *model) + { + Q_ASSERT(model || this->m_model); + if (model) + { + this->m_model = model; + connect(this->m_model, &ModelClass::rowCountChanged, this, &CViewBase::countChanged); + } + this->setModel(this->m_model); // via QTableView + CViewBaseNonTemplate::standardInit(); + this->setSortIndicator(); + } + + template void CViewBase::performResizeToContents() + { + // small set or large set? + if (this->resize()) + { + this->fullResizeToContents(); + } + else + { + this->m_resizeCount++; // skipped resize + } + } + + template int CViewBase::performUpdateContainer(const QVariant &variant, bool sort, bool resize) + { + ContainerType c; + c.convertFromQVariant(variant); + return this->updateContainer(c, sort, resize); + } + + // see here for the reason of thess forward instantiations + // http://www.parashift.com/c++-faq/separate-template-class-defn-from-decl.html + template class CViewBase; + template class CViewBase; + template class CViewBase; + template class CViewBase; + template class CViewBase; + template class CViewBase; + template class CViewBase; + template class CViewBase; + template class CViewBase; + + } // namespace +} // namespace diff --git a/src/blackgui/views/viewbase.h b/src/blackgui/views/viewbase.h index 6ba7dea64..98950119d 100644 --- a/src/blackgui/views/viewbase.h +++ b/src/blackgui/views/viewbase.h @@ -13,66 +13,132 @@ #define BLACKGUI_VIEWBASE_H #include "blackmisc/icons.h" +#include "blackgui/updateworker.h" #include +#include #include #include +#include namespace BlackGui { namespace Views { - //! Non templated base class, allows Q_OBJECT and signals to be used + //! Non templated base class, allows Q_OBJECT and signals / slots to be used class CViewBaseNonTemplate : public QTableView { Q_OBJECT public: + + //! Resize mode + //! \remarks Using own resizing (other than QHeaderView::ResizeMode) + enum ResizeMode + { + ResizingAuto, //!< always resizing, \sa m_resizeAutoNthTime + ResizingOnce, //!< only one time + ResizingOff + }; + + //! When to use asynchronous updates + static const int asyncThreshold = 50; + //! Clear data virtual void clear() = 0; + //! Current rows resize mode + ResizeMode getResizeMode() const { return m_resizeMode; } + + //! Set resize mode + void setResizeMode(ResizeMode mode) { m_resizeMode = mode; } + + //! In ResizeAuto mode, how often to update. "1" updates every time, "2" every 2nd time, .. + void setAutoResizeFrequency(int updateEveryNthTime) { this->m_resizeAutoNthTime = updateEveryNthTime; } + + //! Header (horizontal) font + const QFont &getHorizontalHeaderFont() const { Q_ASSERT(this->horizontalHeader()); return this->horizontalHeader()->font(); } + + //! Horizontal font height + int getHorizontalHeaderFontHeight() const; + signals: //! Ask for new data void requestUpdate(); + //! Asynchronous update finished + void asyncUpdateFinished(); + + public slots: + //! Resize to contents, strategy depends on container size + virtual void resizeToContents(); + + //! Full resizing to content, might be slow + virtual void fullResizeToContents(); + protected: //! Constructor - CViewBaseNonTemplate(QWidget *parent) : QTableView(parent) - { - this->setContextMenuPolicy(Qt::CustomContextMenu); - connect(this, &QWidget::customContextMenuRequested, this, &CViewBaseNonTemplate::ps_customMenuRequested); - } + CViewBaseNonTemplate(QWidget *parent); //! Method creating the menu //! \remarks override this method to contribute to the menu - virtual void customMenu(QMenu &menu) const + virtual void customMenu(QMenu &menu) const; + + //! Perform resizing / non slot method for template + virtual void performResizeToContents() = 0; + + //! Init as interactive, as this allows manually resizing + void initRowsResizeModeToInteractive(); + + //! Set fixed row height (vertical header with height) + //! \sa initRowsResizeModeToFixed + virtual void setRowsResizeModeToInteractive(int height); + + //! Helper method with template free signature + //! \param variant contains the container + //! \param sort + //! \param resize + virtual int performUpdateContainer(const QVariant &variant, bool sort, bool resize) = 0; + + //! Skip resizing + virtual bool skipResize() const = 0; + + //! Resize or skip resize? + virtual bool resize() const; + + //! Init default values + virtual void standardInit(); + + ResizeMode m_resizeMode = ResizingAuto; //!< mode + int m_resizeCount = 0; //!< flag / counter,how many resize activities + int m_skipResizeThreshold = 40; //!< when to skip resize + int m_resizeAutoNthTime = 1; //!< with ResizeAuto, resize every n-th time + + protected slots: + //! Helper method with template free signature serving as callback from threaded worker + int updateContainer(const QVariant &variant, bool sort, bool resize) { - menu.addAction(BlackMisc::CIcons::refresh16(), "Update", this, SIGNAL(requestUpdate())); - menu.addAction(BlackMisc::CIcons::delete16(), "Clear", this, SLOT(ps_clear())); + return this->performUpdateContainer(variant, sort, resize); } private slots: //! Custom menu was requested - void ps_customMenuRequested(QPoint pos) - { - QMenu menu; - this->customMenu(menu); - if (menu.isEmpty()) { return; } + void ps_customMenuRequested(QPoint pos); - QPoint globalPos = this->mapToGlobal(pos); - menu.exec(globalPos); - } + //! Toggle the resize mode + void ps_toggleResizeMode(bool checked); //! Clear the model virtual void ps_clear() { this->clear(); } - }; //! Base class for views - template class CViewBase : public CViewBaseNonTemplate + template class CViewBase : public CViewBaseNonTemplate { public: + //! Destructor + virtual ~CViewBase() {} //! Model ModelClass *derivedModel() { return this->m_model; } @@ -84,24 +150,21 @@ namespace BlackGui virtual void clear() override { Q_ASSERT(this->m_model); this->m_model->clear(); } //! Update whole container - template int updateContainer(const ContainerType &container, bool sort = true, bool resize = true) - { - Q_ASSERT(this->m_model); - int c = this->m_model->update(container, sort); - if (!resize) return c; - this->resizeColumnsToContents(); - this->resizeRowsToContents(); - return c; - } + int updateContainer(const ContainerType &container, bool sort = true, bool resize = true); + + //! Update whole container in background + //! \returns Worker or nullptr if worker cannot be started + IUpdateWorker *updateContainerAsync(const ContainerType &container, bool sort = true, bool resize = true); + + //! Based on size call sync / async update + void updateContainerMaybeAsync(const ContainerType &container, bool sort = true, bool resize = true); //! Insert template void insert(const ObjectType &value, bool resize = true) { Q_ASSERT(this->m_model); this->m_model->insert(value); - if (!resize) return; - this->resizeColumnsToContents(); - this->resizeRowsToContents(); + if (resize) { this->performResizeToContents(); } } //! Value object at @@ -112,35 +175,16 @@ namespace BlackGui } //! Row count - int rowCount() const - { - Q_ASSERT(this->m_model); - return this->m_model->rowCount(); - } + int rowCount() const; //! Column count - int columnCount() const - { - Q_ASSERT(this->m_model); - return this->m_model->columnCount(); - } + int columnCount() const; //! Any data? - bool isEmpty() const - { - Q_ASSERT(this->m_model); - return this->m_model->rowCount() < 1; - } + bool isEmpty() const; //! Set own name and the model's name - void setObjectName(const QString &name) - { - // then name here is mainly set for debugging purposes so each model can be identified - Q_ASSERT(m_model); - QTableView::setObjectName(name); - QString modelName = QString(name).append(':').append(this->m_model->getTranslationContext()); - this->m_model->setObjectName(modelName); - } + virtual void setObjectName(const QString &name); protected: ModelClass *m_model = nullptr; //!< corresponding model @@ -152,37 +196,77 @@ namespace BlackGui if (model) { this->setModel(this->m_model); } } - //! Destructor - virtual ~CViewBase() {} - //! Set the search indicator based on model - void setSortIndicator() + void setSortIndicator(); + + //! \copydoc CViewBaseNonTemplate::standardInit + void standardInit(ModelClass *model = nullptr); + + //! \copydoc CViewBaseNonTemplate::reachedResizeThreshold + virtual bool reachedResizeThreshold() const override { return this->rowCount() > m_skipResizeThreshold; } + + //! \copydoc CViewBaseNonTemplate::performResizing + virtual void performResizeToContents() override; + + //! \copydoc CViewBaseNonTemplate::performUpdateContainer + virtual int performUpdateContainer(const QVariant &variant, bool sort, bool resize) override; + + // ---- worker ----------------------------------------------------------------------------------- + + //! Worker class performing update and sorting in background + class CViewUpdateWorker : public BlackGui::IUpdateWorker { - if (this->m_model->hasValidSortColumn()) + + public: + //! Constructor + CViewUpdateWorker(CViewBase *view, const ContainerType &container, bool sort, bool resize) : + BlackGui::IUpdateWorker(sort), m_view(view), m_container(container), m_resize(resize) { - this->horizontalHeader()->setSortIndicator( - this->m_model->getSortColumn(), - this->m_model->getSortOrder()); + Q_ASSERT(view); + this->m_sortColumn = view->derivedModel()->getSortColumn(); + this->m_sortOrder = view->derivedModel()->getSortOrder(); + this->setObjectName(view->objectName().append(":CViewUpdateWorker")); + connect(this, &CViewUpdateWorker::updateFinished, view, &CViewBase::asyncUpdateFinished, Qt::QueuedConnection); } - } - //! Resize to content - void resizeToContents() - { - this->resizeColumnsToContents(); - this->resizeRowsToContents(); - } + //! Destructor + virtual ~CViewUpdateWorker() {} + + protected: + //! \copydoc CUpdateWorkerPrivate::update + virtual void update() override + { + Q_ASSERT(m_view); + Q_ASSERT(m_view->derivedModel()); + if (m_view) + { + // KWB remove later + qDebug() << this->objectName() << "worker thread:" << QThread::currentThreadId(); + + // resize has to be in main thread + ModelClass *model = m_view->derivedModel(); + if (m_sort) + { + // thread safe sort: + // 1) the container itself is copied when worker is created and hence thread safe + // 2) the sort order itself is not really thread safe, + // but always represents the latest value from CListModelBase/QAbstractListModel::sort() + m_container = model->sortContainerByColumn(m_container, m_sortColumn, m_sortOrder); + } + // now update view itself thread safe, but time for sort was saved + QMetaObject::invokeMethod(m_view, "updateContainer", Qt::QueuedConnection, + Q_ARG(QVariant, m_container.toQVariant()), Q_ARG(bool, false), Q_ARG(bool, m_resize)); + } + } + + CViewBase *m_view = nullptr; //!< view to be updated, actually const but invokeMethod does not allow const + ContainerType m_container; //!< container with data + bool m_resize; //!< with resizing + }; + + // ---- worker ----------------------------------------------------------------------------------- - //! Init - void standardInit(ModelClass *model = nullptr) - { - Q_ASSERT(model || this->m_model); - if (model) { this->m_model = model; } - this->setModel(this->m_model); // via QTableView - this->setSortIndicator(); - this->horizontalHeader()->setStretchLastSection(true); - } }; - } -} + } // namespace +} // namespace #endif // guard