diff --git a/python/core/core.sip b/python/core/core.sip index 89ff092bd37..e4381264f3f 100644 --- a/python/core/core.sip +++ b/python/core/core.sip @@ -138,6 +138,7 @@ %Include qgsstatisticalsummary.sip %Include qgsstringstatisticalsummary.sip %Include qgsstringutils.sip +%Include qgstaskmanager.sip %Include qgstextrenderer.sip %Include qgstolerance.sip %Include qgstracer.sip diff --git a/python/core/qgstaskmanager.sip b/python/core/qgstaskmanager.sip new file mode 100644 index 00000000000..d09e123105e --- /dev/null +++ b/python/core/qgstaskmanager.sip @@ -0,0 +1,174 @@ +/** \ingroup core + * \class QgsTask + * \brief Interface class for long running tasks which will be handled by a QgsTaskManager + * \note Added in version 2.16 + */ +class QgsTask : QObject +{ + +%TypeHeaderCode +#include +%End + public: + + //! Status of tasks + enum TaskStatus + { + Running, /*!< Task is currently running */ + Complete, /*!< Task successfully completed */ + Terminated, /*!< Task was terminated or errored */ + // Paused, + // Queued, + }; + + /** Constructor for QgsTask. + * @param description text description of task + */ + QgsTask( const QString& description = QString() ); + + //! Will be called when task has been terminated, either through + //! user interaction or other reason (eg application exit) + //! @note derived classes must ensure they call the base method + virtual void terminate(); + + //! Returns true if the task is active, ie it is not complete and has + //! not been terminated. + bool isActive() const; + + //! Returns the current task status. + TaskStatus status() const; + + //! Returns the task's description. + QString description() const; + + //! Returns the task's progress (between 0.0 and 100.0) + double progress() const; + + signals: + + //! Will be emitted by task when its progress changes + //! @param progress percent of progress, from 0.0 - 100.0 + //! @note derived classes should not emit this signal directly, instead they should call + //! setProgress() + void progressChanged( double progress ); + + //! Will be emitted by task when its status changes + //! @param status new task status + //! @note derived classes should not emit this signal directly, instead they should call + //! completed() or stopped() + void statusChanged( int status ); + + //! Will be emitted by task to indicate its completion. + //! @note derived classes should not emit this signal directly, instead they should call + //! completed() + void taskCompleted(); + + //! Will be emitted by task if it has terminated for any reason + //! other then completion. + //! @note derived classes should not emit this signal directly, instead they should call + //! stopped()//! + void taskStopped(); + + protected: + + //! Sets the task's current progress. Should be called whenever the + //! task wants to update it's progress. Calling will automatically emit the progressChanged + //! signal. + //! @param progress percent of progress, from 0.0 - 100.0 + void setProgress( double progress ); + + //! Sets the task as completed. Should be called when the task is complete. + //! Calling will automatically emit the statusChanged and taskCompleted signals. + void completed(); + + //! Sets the task as stopped. Should be called whenever the task ends for any + //! reason other than successful completion. + //! Calling will automatically emit the statusChanged and taskStopped signals. + void stopped(); +}; + +/** \ingroup core + * \class QgsTaskManager + * \brief Task manager for managing a set of long-running QgsTask tasks. This class can be created directly, + * or accessed via a global instance. + * \note Added in version 2.16 + */ +class QgsTaskManager : QObject +{ +%TypeHeaderCode +#include +%End + public: + + /** Returns the global task manager instance pointer, creating the object on the first call. + */ + static QgsTaskManager * instance(); + + /** Constructor for QgsTaskManager. + * @param parent parent QObject + */ + QgsTaskManager( QObject* parent /TransferThis/ = nullptr ); + + virtual ~QgsTaskManager(); + + /** Adds a task to the manager. Ownership of the task is transferred + * to the manager. + * @param task task to add + * @returns unique task ID + */ + long addTask( QgsTask* task /Transfer/ ); + + /** Deletes the specified task, first terminating it if it is currently + * running. + * @param id task ID + * @returns true if task was found and deleted + */ + bool deleteTask( long id ); + + /** Deletes the specified task, first terminating it if it is currently + * running. + * @param task task to delete + * @returns true if task was contained in manager and deleted + */ + bool deleteTask( QgsTask* task ); + + /** Returns the task with matching ID. + * @param id task ID + * @returns task if found, or nullptr + */ + QgsTask* task( long id ) const; + + /** Returns all tasks tracked by the manager. + */ + QList tasks() const; + + //! Returns the number of tasks tracked by the manager. + int count() const; + + /** Returns the unique task ID corresponding to a task managed by the class. + * @param task task to find + * @returns task ID, or -1 if task not found + */ + long taskId( QgsTask* task ) const; + + signals: + + //! Will be emitted when a task reports a progress change + //! @param taskId ID of task + //! @param progress percent of progress, from 0.0 - 100.0 + void progressChanged( long taskId, double progress ); + + //! Will be emitted when a task reports a status change + //! @param taskId ID of task + //! @param status new task status + void statusChanged( long taskId, int status ); + + //! Emitted when a new task has been added to the manager + //! @param taskId ID of task + void taskAdded( long taskId ); + + //! Emitted when a task is about to be deleted + //! @param taskId ID of task + void taskAboutToBeDeleted( long taskId ); + +}; diff --git a/python/gui/gui.sip b/python/gui/gui.sip index 868e31ca745..5032c14d942 100644 --- a/python/gui/gui.sip +++ b/python/gui/gui.sip @@ -161,6 +161,7 @@ %Include qgstablewidgetbase.sip %Include qgstabwidget.sip %Include qgstablewidgetitem.sip +%Include qgstaskmanagerwidget.sip %Include qgstextannotationitem.sip %Include qgstextformatwidget.sip %Include qgstextpreview.sip diff --git a/python/gui/qgstaskmanagerwidget.sip b/python/gui/qgstaskmanagerwidget.sip new file mode 100644 index 00000000000..03fe5ad6cb0 --- /dev/null +++ b/python/gui/qgstaskmanagerwidget.sip @@ -0,0 +1,95 @@ +/** \ingroup gui + * \class QgsTaskManagerWidget + * A widget which displays tasks from a QgsTaskManager and allows for interaction with the manager + * @see QgsTaskManager + * @note introduced in QGIS 2.16 + */ +class QgsTaskManagerWidget : QTreeView +{ +%TypeHeaderCode +#include +%End + public: + + /** Constructor for QgsTaskManagerWidget + * @param manager task manager associated with widget + * @param parent parent widget + */ + QgsTaskManagerWidget( QgsTaskManager* manager, QWidget* parent /TransferThis/ = nullptr ); + +}; + + +/** \ingroup gui + * \class QgsTaskManagerModel + * A model representing a QgsTaskManager + * @see QgsTaskManager + * @note introduced in QGIS 2.16 + */ +class QgsTaskManagerModel: QAbstractItemModel +{ +%TypeHeaderCode +#include +%End + public: + + /** Constructor for QgsTaskManagerModel + * @param manager task manager for model + * @param parent parent object + */ + explicit QgsTaskManagerModel( QgsTaskManager* manager, QObject* parent /TransferThis/ = nullptr ); + + //reimplemented QAbstractItemModel methods + QModelIndex index( int row, int column, const QModelIndex &parent = QModelIndex() ) const; + QModelIndex parent( const QModelIndex &index ) const; + int rowCount( const QModelIndex &parent = QModelIndex() ) const; + int columnCount( const QModelIndex &parent = QModelIndex() ) const; + QVariant data( const QModelIndex &index, int role = Qt::DisplayRole ) const; + Qt::ItemFlags flags( const QModelIndex & index ) const; + bool setData( const QModelIndex & index, const QVariant & value, int role = Qt::EditRole ); + +}; + + +/** \ingroup gui + * \class QgsProgressBarDelegate + * A delegate for showing a progress bar within a view + * @note introduced in QGIS 2.16 + */ +class QgsProgressBarDelegate : QStyledItemDelegate +{ +%TypeHeaderCode +#include +%End + public: + + /** Constructor for QgsProgressBarDelegate + * @param parent parent object + */ + QgsProgressBarDelegate( QObject* parent /TransferThis/ = nullptr ); + + void paint( QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index ) const; + QSize sizeHint( const QStyleOptionViewItem &option, const QModelIndex &index ) const; +}; + +/** \ingroup gui + * \class QgsProgressBarDelegate + * A delegate for showing task status within a view. Clicks on the delegate will cause the task to be cancelled (via the model). + * @note introduced in QGIS 2.16 + */ +class QgsTaskStatusDelegate : QStyledItemDelegate +{ +%TypeHeaderCode +#include +%End + public: + + /** Constructor for QgsTaskStatusDelegate + * @param parent parent object + */ + QgsTaskStatusDelegate( QObject* parent /TransferThis/ = nullptr ); + + void paint( QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index ) const; + QSize sizeHint( const QStyleOptionViewItem &option, const QModelIndex &index ) const; + bool editorEvent( QEvent * event, QAbstractItemModel * model, const QStyleOptionViewItem & option, const QModelIndex & index ); +}; diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index 34e9ed4b6d6..661d67a584e 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -221,6 +221,8 @@ #include "qgsstatusbarscalewidget.h" #include "qgsstyle.h" #include "qgssvgannotationitem.h" +#include "qgstaskmanager.h" +#include "qgstaskmanagerwidget.h" #include "qgssymbolselectordialog.h" #include "qgstextannotationitem.h" #include "qgstipgui.h" @@ -851,6 +853,13 @@ QgisApp::QgisApp( QSplashScreen *splash, bool restorePlugins, bool skipVersionCh QMainWindow::addDockWidget( Qt::BottomDockWidgetArea, mUserInputDockWidget ); mUserInputDockWidget->setFloating( true ); + // create the task manager dock on starting QGIS - this is like the browser + mTaskManagerDock = new QDockWidget( tr( "Task Manager" ), this ); + mTaskManagerDock->setObjectName( "TaskManager" ); + addDockWidget( Qt::RightDockWidgetArea, mTaskManagerDock ); + mTaskManagerDock->setWidget( new QgsTaskManagerWidget( QgsTaskManager::instance(), mTaskManagerDock ) ); + mTaskManagerDock->hide(); + // create the GPS tool on starting QGIS - this is like the browser mpGpsWidget = new QgsGPSInformationWidget( mMapCanvas ); //create the dock widget @@ -1139,6 +1148,7 @@ QgisApp::QgisApp() , mOverviewDock( nullptr ) , mpGpsDock( nullptr ) , mLogDock( nullptr ) + , mTaskManagerDock( nullptr ) , mNonEditMapTool( nullptr ) , mScaleWidget( nullptr ) , mMagnifierWidget( nullptr ) diff --git a/src/app/qgisapp.h b/src/app/qgisapp.h index 42e62bacc59..3ef375e01c8 100644 --- a/src/app/qgisapp.h +++ b/src/app/qgisapp.h @@ -1560,6 +1560,7 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow QgsDockWidget *mOverviewDock; QgsDockWidget *mpGpsDock; QgsDockWidget *mLogDock; + QDockWidget *mTaskManagerDock; #ifdef Q_OS_MAC //! Window menu action to select this window diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 6b7dbbe2b2d..b18910afc21 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -211,6 +211,7 @@ SET(QGIS_CORE_SRCS qgsstatisticalsummary.cpp qgsstringstatisticalsummary.cpp qgsstringutils.cpp + qgstaskmanager.cpp qgstextlabelfeature.cpp qgstextrenderer.cpp qgstolerance.cpp @@ -495,6 +496,7 @@ SET(QGIS_CORE_MOC_HDRS qgsrelationmanager.h qgsrunprocess.h qgssnappingutils.h + qgstaskmanager.h qgstracer.h qgstrackedvectorlayertools.h qgstransaction.h diff --git a/src/core/qgstaskmanager.cpp b/src/core/qgstaskmanager.cpp new file mode 100644 index 00000000000..2dc941650f2 --- /dev/null +++ b/src/core/qgstaskmanager.cpp @@ -0,0 +1,181 @@ +/*************************************************************************** + qgstaskmanager.cpp + ------------------ + begin : April 2016 + copyright : (C) 2016 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgstaskmanager.h" +#include + + +// +// QgsTask +// + +QgsTask::QgsTask( const QString &name ) + : QObject() + , mDescription( name ) + , mStatus( Running ) + , mProgress( 0.0 ) +{} + +void QgsTask::setProgress( double progress ) +{ + mProgress = progress; + emit progressChanged( progress ); +} + +void QgsTask::completed() +{ + mStatus = Complete; + emit statusChanged( Complete ); + emit taskCompleted(); +} + +void QgsTask::stopped() +{ + mStatus = Terminated; + emit statusChanged( Terminated ); + emit taskStopped(); +} + + +// +// QgsTaskManager +// + +// Static calls to enforce singleton behaviour +QgsTaskManager *QgsTaskManager::sInstance = nullptr; +QgsTaskManager *QgsTaskManager::instance() +{ + if ( !sInstance ) + { + sInstance = new QgsTaskManager(); + } + + return sInstance; +} + +QgsTaskManager::QgsTaskManager( QObject* parent ) + : QObject( parent ) + , mNextTaskId( 0 ) +{} + +QgsTaskManager::~QgsTaskManager() +{ + QMap< long, QgsTask* >::const_iterator it = mTasks.constBegin(); + for ( ; it != mTasks.constEnd(); ++it ) + { + cleanupAndDeleteTask( it.value() ); + } +} + +long QgsTaskManager::addTask( QgsTask* task ) +{ + static QMutex sAddMutex( QMutex::Recursive ); + QMutexLocker locker( &sAddMutex ); + + mTasks.insert( mNextTaskId, task ); + connect( task, SIGNAL( progressChanged( double ) ), this, SLOT( taskProgressChanged( double ) ) ); + connect( task, SIGNAL( statusChanged( int ) ), this, SLOT( taskStatusChanged( int ) ) ); + emit taskAdded( mNextTaskId ); + return mNextTaskId++; +} + +bool QgsTaskManager::deleteTask( long id ) +{ + QgsTask* task = mTasks.value( id ); + return deleteTask( task ); +} + +bool QgsTaskManager::deleteTask( QgsTask *task ) +{ + if ( !task ) + return false; + + bool result = cleanupAndDeleteTask( task ); + + // remove from internal task list + for ( QMap< long, QgsTask* >::iterator it = mTasks.begin(); it != mTasks.end(); ) + { + if ( it.value() == task ) + it = mTasks.erase( it ); + else + ++it; + } + + return result; +} + +QgsTask*QgsTaskManager::task( long id ) const +{ + return mTasks.value( id ); +} + +QList QgsTaskManager::tasks() const +{ + return mTasks.values(); +} + +long QgsTaskManager::taskId( QgsTask *task ) const +{ + if ( !task ) + return -1; + + QMap< long, QgsTask* >::const_iterator it = mTasks.constBegin(); + for ( ; it != mTasks.constEnd(); ++it ) + { + if ( it.value() == task ) + return it.key(); + } + return -1; +} + +void QgsTaskManager::taskProgressChanged( double progress ) +{ + QgsTask* task = qobject_cast< QgsTask* >( sender() ); + + //find ID of task + long id = taskId( task ); + if ( id < 0 ) + return; + + emit progressChanged( id, progress ); +} + +void QgsTaskManager::taskStatusChanged( int status ) +{ + QgsTask* task = qobject_cast< QgsTask* >( sender() ); + + //find ID of task + long id = taskId( task ); + if ( id < 0 ) + return; + + emit statusChanged( id, status ); +} + +bool QgsTaskManager::cleanupAndDeleteTask( QgsTask *task ) +{ + if ( !task ) + return false; + + if ( task->isActive() ) + task->terminate(); + + emit taskAboutToBeDeleted( taskId( task ) ); + + task->deleteLater(); + return true; +} diff --git a/src/core/qgstaskmanager.h b/src/core/qgstaskmanager.h new file mode 100644 index 00000000000..7779ed07b59 --- /dev/null +++ b/src/core/qgstaskmanager.h @@ -0,0 +1,222 @@ +/*************************************************************************** + qgstaskmanager.h + ---------------- + begin : April 2016 + copyright : (C) 2016 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSTASKMANAGER_H +#define QGSTASKMANAGER_H + +#include +#include +#include + +/** \ingroup core + * \class QgsTask + * \brief Interface class for long running background tasks which will be handled by a QgsTaskManager + * \note Added in version 2.16 + */ +class CORE_EXPORT QgsTask : public QObject +{ + Q_OBJECT + + public: + + //! Status of tasks + enum TaskStatus + { + Running, /*!< Task is currently running */ + Complete, /*!< Task successfully completed */ + Terminated, /*!< Task was terminated or errored */ + // Paused, + // Queued, + }; + + /** Constructor for QgsTask. + * @param description text description of task + */ + QgsTask( const QString& description = QString() ); + + //! Will be called when task has been terminated, either through + //! user interaction or other reason (eg application exit) + //! @note derived classes must ensure they call the base method + virtual void terminate() + { + stopped(); + } + + //! Returns true if the task is active, ie it is not complete and has + //! not been terminated. + bool isActive() const { return mStatus == Running; } + + //! Returns the current task status. + TaskStatus status() const { return mStatus; } + + //! Returns the task's description. + QString description() const { return mDescription; } + + //! Returns the task's progress (between 0.0 and 100.0) + double progress() const { return mProgress; } + + signals: + + //! Will be emitted by task when its progress changes + //! @param progress percent of progress, from 0.0 - 100.0 + //! @note derived classes should not emit this signal directly, instead they should call + //! setProgress() + void progressChanged( double progress ); + + //! Will be emitted by task when its status changes + //! @param status new task status + //! @note derived classes should not emit this signal directly, instead they should call + //! completed() or stopped() + void statusChanged( int status ); + + //! Will be emitted by task to indicate its completion. + //! @note derived classes should not emit this signal directly, instead they should call + //! completed() + void taskCompleted(); + + //! Will be emitted by task if it has terminated for any reason + //! other then completion. + //! @note derived classes should not emit this signal directly, instead they should call + //! stopped()//! + void taskStopped(); + + protected: + + //! Sets the task's current progress. Should be called whenever the + //! task wants to update it's progress. Calling will automatically emit the progressChanged + //! signal. + //! @param progress percent of progress, from 0.0 - 100.0 + void setProgress( double progress ); + + //! Sets the task as completed. Should be called when the task is complete. + //! Calling will automatically emit the statusChanged and taskCompleted signals. + void completed(); + + //! Sets the task as stopped. Should be called whenever the task ends for any + //! reason other than successful completion. + //! Calling will automatically emit the statusChanged and taskStopped signals. + void stopped(); + + private: + + QString mDescription; + TaskStatus mStatus; + double mProgress; + +}; + +/** \ingroup core + * \class QgsTaskManager + * \brief Task manager for managing a set of long-running QgsTask tasks. This class can be created directly, + * or accessed via a global instance. + * \note Added in version 2.16 + */ +class CORE_EXPORT QgsTaskManager : public QObject +{ + Q_OBJECT + + public: + + /** Returns the global task manager instance pointer, creating the object on the first call. + */ + static QgsTaskManager * instance(); + + /** Constructor for QgsTaskManager. + * @param parent parent QObject + */ + QgsTaskManager( QObject* parent = nullptr ); + + virtual ~QgsTaskManager(); + + /** Adds a task to the manager. Ownership of the task is transferred + * to the manager. + * @param task task to add + * @returns unique task ID + */ + long addTask( QgsTask* task ); + + /** Deletes the specified task, first terminating it if it is currently + * running. + * @param id task ID + * @returns true if task was found and deleted + */ + bool deleteTask( long id ); + + /** Deletes the specified task, first terminating it if it is currently + * running. + * @param task task to delete + * @returns true if task was contained in manager and deleted + */ + bool deleteTask( QgsTask* task ); + + /** Returns the task with matching ID. + * @param id task ID + * @returns task if found, or nullptr + */ + QgsTask* task( long id ) const; + + /** Returns all tasks tracked by the manager. + */ + QList tasks() const; + + //! Returns the number of tasks tracked by the manager. + int count() const { return mTasks.count(); } + + /** Returns the unique task ID corresponding to a task managed by the class. + * @param task task to find + * @returns task ID, or -1 if task not found + */ + long taskId( QgsTask* task ) const; + + signals: + + //! Will be emitted when a task reports a progress change + //! @param taskId ID of task + //! @param progress percent of progress, from 0.0 - 100.0 + void progressChanged( long taskId, double progress ); + + //! Will be emitted when a task reports a status change + //! @param taskId ID of task + //! @param status new task status + void statusChanged( long taskId, int status ); + + //! Emitted when a new task has been added to the manager + //! @param taskId ID of task + void taskAdded( long taskId ); + + //! Emitted when a task is about to be deleted + //! @param taskId ID of task + void taskAboutToBeDeleted( long taskId ); + + private slots: + + void taskProgressChanged( double progress ); + void taskStatusChanged( int status ); + + private: + + static QgsTaskManager *sInstance; + QMap< long, QgsTask* > mTasks; + + //! Tracks the next unique task ID + long mNextTaskId; + + bool cleanupAndDeleteTask( QgsTask* task ); + +}; + +#endif //QGSTASKMANAGER_H diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 03037ef7f25..b12d115ed6c 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -308,6 +308,7 @@ SET(QGIS_GUI_SRCS qgstablewidgetbase.cpp qgstabwidget.cpp qgstablewidgetitem.cpp + qgstaskmanagerwidget.cpp qgstextannotationitem.cpp qgstextformatwidget.cpp qgstextpreview.cpp @@ -465,6 +466,7 @@ SET(QGIS_GUI_MOC_HDRS qgssubstitutionlistwidget.h qgstablewidgetbase.h qgstabwidget.h + qgstaskmanagerwidget.h qgstextformatwidget.h qgstextpreview.h qgstreewidgetitem.h diff --git a/src/gui/qgstaskmanagerwidget.cpp b/src/gui/qgstaskmanagerwidget.cpp new file mode 100644 index 00000000000..fd0ceab2a4b --- /dev/null +++ b/src/gui/qgstaskmanagerwidget.cpp @@ -0,0 +1,363 @@ +/*************************************************************************** + qgstaskmanagerwidget.cpp + ------------------------ + begin : April 2016 + copyright : (C) 2016 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgstaskmanagerwidget.h" +#include "qgstaskmanager.h" +#include "qgsapplication.h" +#include +#include + +// +// QgsTaskManagerWidget +// + +QgsTaskManagerWidget::QgsTaskManagerWidget( QgsTaskManager *manager, QWidget *parent ) + : QTreeView( parent ) +{ + Q_ASSERT( manager ); + + setModel( new QgsTaskManagerModel( manager, this ) ); + + setItemDelegateForColumn( 1, new QgsProgressBarDelegate( this ) ); + setItemDelegateForColumn( 2, new QgsTaskStatusDelegate( this ) ); + + setHeaderHidden( true ); + setRootIsDecorated( false ); + setSelectionBehavior( QAbstractItemView::SelectRows ); +} + + + + +// +// QgsTaskManagerModel +// + +QgsTaskManagerModel::QgsTaskManagerModel( QgsTaskManager *manager, QObject *parent ) + : QAbstractItemModel( parent ) + , mManager( manager ) +{ + Q_ASSERT( mManager ); + + //populate row to id map + int i = 0; + Q_FOREACH ( QgsTask* task, mManager->tasks() ) + { + mRowToTaskIdMap.insert( i, mManager->taskId( task ) ); + } + + connect( mManager, SIGNAL( taskAdded( long ) ), this, SLOT( taskAdded( long ) ) ); + connect( mManager, SIGNAL( taskAboutToBeDeleted( long ) ), this, SLOT( taskDeleted( long ) ) ); + connect( mManager, SIGNAL( progressChanged( long, double ) ), this, SLOT( progressChanged( long, double ) ) ); + connect( mManager, SIGNAL( statusChanged( long, int ) ), this, SLOT( statusChanged( long, int ) ) ); +} + +QModelIndex QgsTaskManagerModel::index( int row, int column, const QModelIndex &parent ) const +{ + if ( column < 0 || column >= columnCount() ) + { + //column out of bounds + return QModelIndex(); + } + + if ( !parent.isValid() && row >= 0 && row < mManager->count() ) + { + //return an index for the task at this position + return createIndex( row, column ); + } + + //only top level supported + return QModelIndex(); + +} + +QModelIndex QgsTaskManagerModel::parent( const QModelIndex &index ) const +{ + Q_UNUSED( index ); + + //all items are top level + return QModelIndex(); +} + +int QgsTaskManagerModel::rowCount( const QModelIndex &parent ) const +{ + if ( !parent.isValid() ) + { + return mManager->count(); + } + else + { + //no children + return 0; + } +} + +int QgsTaskManagerModel::columnCount( const QModelIndex &parent ) const +{ + Q_UNUSED( parent ); + return 3; +} + +QVariant QgsTaskManagerModel::data( const QModelIndex &index, int role ) const +{ + if ( !index.isValid() ) + return QVariant(); + + QgsTask* task = indexToTask( index ); + if ( !task ) + return QVariant(); + + switch ( role ) + { + case Qt::DisplayRole: + case Qt::EditRole: + switch ( index.column() ) + { + case Description: + return task->description(); + case Progress: + return task->progress(); + case Status: + return static_cast( task->status() ); + default: + return QVariant(); + } + + case StatusRole: + return static_cast( task->status() ); + + default: + return QVariant(); + } +} + +Qt::ItemFlags QgsTaskManagerModel::flags( const QModelIndex &index ) const +{ + Qt::ItemFlags flags = QAbstractItemModel::flags( index ); + + if ( ! index.isValid() ) + { + return flags; + } + + if ( index.column() == Status ) + { + if ( static_cast< QgsTask::TaskStatus >( data( index, StatusRole ).toInt() ) == QgsTask::Running ) + flags = flags | Qt::ItemIsEditable; + } + return flags | Qt::ItemIsEnabled | Qt::ItemIsSelectable; +} + +bool QgsTaskManagerModel::setData( const QModelIndex &index, const QVariant &value, int role ) +{ + Q_UNUSED( role ); + + if ( !index.isValid() ) + return false; + + QgsTask* task = indexToTask( index ); + if ( !task ) + return false; + + switch ( index.column() ) + { + case Status: + { + if ( value.toBool() ) + task->terminate(); + return true; + } + + default: + return false; + } +} + +void QgsTaskManagerModel::taskAdded( long id ) +{ + beginInsertRows( QModelIndex(), mRowToTaskIdMap.count(), mRowToTaskIdMap.count() ); + mRowToTaskIdMap.insert( mRowToTaskIdMap.count(), id ); + endInsertRows(); +} + +void QgsTaskManagerModel::taskDeleted( long id ) +{ + for ( QMap< int, long >::iterator it = mRowToTaskIdMap.begin(); it != mRowToTaskIdMap.end(); ) + { + if ( it.value() == id ) + { + beginRemoveRows( QModelIndex(), it.key(), it.key() ); + it = mRowToTaskIdMap.erase( it ); + endRemoveRows(); + return; + } + else + ++it; + } +} + +void QgsTaskManagerModel::progressChanged( long id, double progress ) +{ + Q_UNUSED( progress ); + + QModelIndex index = idToIndex( id, Progress ); + if ( !index.isValid() ) + { + return; + } + + emit dataChanged( index, index ); +} + +void QgsTaskManagerModel::statusChanged( long id, int status ) +{ + Q_UNUSED( status ); + + QModelIndex index = idToIndex( id, Status ); + if ( !index.isValid() ) + { + return; + } + + emit dataChanged( index, index ); +} + +QgsTask *QgsTaskManagerModel::indexToTask( const QModelIndex &index ) const +{ + if ( !index.isValid() || index.parent().isValid() ) + return nullptr; + + long id = mRowToTaskIdMap.value( index.row() ); + return mManager->task( id ); +} + +int QgsTaskManagerModel::idToRow( long id ) const +{ + for ( QMap< int, long >::const_iterator it = mRowToTaskIdMap.constBegin(); it != mRowToTaskIdMap.constEnd(); ++it ) + { + if ( it.value() == id ) + { + return it.key(); + } + } + return -1; +} + +QModelIndex QgsTaskManagerModel::idToIndex( long id, int column ) const +{ + int row = idToRow( id ); + if ( row < 0 ) + return QModelIndex(); + + return index( row, column ); +} + + + +// +// QgsProgressBarDelegate +// + +QgsProgressBarDelegate::QgsProgressBarDelegate( QObject *parent ) + : QStyledItemDelegate( parent ) +{} + +void QgsProgressBarDelegate::paint( QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index ) const +{ + QStyledItemDelegate::paint( painter, option, index ); + + int progress = index.data().toInt(); + + QStyleOptionProgressBarV2 progressBarOption; + progressBarOption.state = option.state; + progressBarOption.rect = option.rect; + progressBarOption.rect.setTop( option.rect.top() + 1 ); + progressBarOption.rect.setHeight( option.rect.height() - 2 ); + progressBarOption.minimum = 0; + progressBarOption.maximum = 100; + progressBarOption.progress = progress; + progressBarOption.text = QString::number( progress ) + "%"; + progressBarOption.textVisible = true; + progressBarOption.textAlignment = Qt::AlignCenter; + + QgsApplication::style()->drawControl( QStyle::CE_ProgressBar, &progressBarOption, painter ); +} + +QSize QgsProgressBarDelegate::sizeHint( const QStyleOptionViewItem &option, const QModelIndex &index ) const +{ + Q_UNUSED( index ); + return QSize( option.rect.width(), option.fontMetrics.height() + 10 ); +} + + +// +// QgsTaskStatusDelegate +// + +QgsTaskStatusDelegate::QgsTaskStatusDelegate( QObject *parent ) + : QStyledItemDelegate( parent ) +{ + +} + +void QgsTaskStatusDelegate::paint( QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index ) const +{ + QStyledItemDelegate::paint( painter, option, index ); + + QIcon icon; + switch ( static_cast< QgsTask::TaskStatus >( index.data().toInt() ) ) + { + case QgsTask::Running: + icon = QgsApplication::getThemeIcon( "/mActionRefresh.png" ); + break; + case QgsTask::Complete: + icon = QgsApplication::getThemeIcon( "/mActionCheckQgisVersion.png" ); + break; + case QgsTask::Terminated: + icon = QgsApplication::getThemeIcon( "/mActionRemove.svg" ); + break; + } + icon.paint( painter, option.rect.left() + 1, ( option.rect.top() + option.rect.bottom() ) / 2 - 12, 24, 24 ); +} + +QSize QgsTaskStatusDelegate::sizeHint( const QStyleOptionViewItem &option, const QModelIndex &index ) const +{ + Q_UNUSED( option ); + Q_UNUSED( index ); + return QSize( 32, 32 ); +} + +bool QgsTaskStatusDelegate::editorEvent( QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index ) +{ + Q_UNUSED( option ); + if ( event->type() == QEvent::MouseButtonPress ) + { + QMouseEvent *e = static_cast( event ); + if ( e->button() == Qt::LeftButton ) + { + if ( !index.model()->flags( index ).testFlag( Qt::ItemIsEditable ) ) + { + //item not editable + return false; + } + + return model->setData( index, true, Qt::EditRole ); + } + } + + return false; +} diff --git a/src/gui/qgstaskmanagerwidget.h b/src/gui/qgstaskmanagerwidget.h new file mode 100644 index 00000000000..f3a9cbbaf85 --- /dev/null +++ b/src/gui/qgstaskmanagerwidget.h @@ -0,0 +1,146 @@ +/*************************************************************************** + qgstaskmanagerwidget.h + ---------------------- + begin : April 2016 + copyright : (C) 2016 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +#ifndef QGSTASKMANAGERWIDGET_H +#define QGSTASKMANAGERWIDGET_H + +#include +#include + +class QgsTaskManager; +class QgsTask; + +/** \ingroup gui + * \class QgsTaskManagerWidget + * A widget which displays tasks from a QgsTaskManager and allows for interaction with the manager + * @see QgsTaskManager + * @note introduced in QGIS 2.16 + */ +class GUI_EXPORT QgsTaskManagerWidget : public QTreeView +{ + Q_OBJECT + + public: + + /** Constructor for QgsTaskManagerWidget + * @param manager task manager associated with widget + * @param parent parent widget + */ + QgsTaskManagerWidget( QgsTaskManager* manager, QWidget* parent = nullptr ); + +}; + + +/** \ingroup gui + * \class QgsTaskManagerModel + * A model representing a QgsTaskManager + * @see QgsTaskManager + * @note introduced in QGIS 2.16 + */ +class GUI_EXPORT QgsTaskManagerModel: public QAbstractItemModel +{ + Q_OBJECT + + public: + + /** Constructor for QgsTaskManagerModel + * @param manager task manager for model + * @param parent parent object + */ + explicit QgsTaskManagerModel( QgsTaskManager* manager, QObject* parent = nullptr ); + + //reimplemented QAbstractItemModel methods + QModelIndex index( int row, int column, const QModelIndex &parent = QModelIndex() ) const override; + QModelIndex parent( const QModelIndex &index ) const override; + int rowCount( const QModelIndex &parent = QModelIndex() ) const override; + int columnCount( const QModelIndex &parent = QModelIndex() ) const override; + QVariant data( const QModelIndex &index, int role = Qt::DisplayRole ) const override; + Qt::ItemFlags flags( const QModelIndex & index ) const override; + bool setData( const QModelIndex & index, const QVariant & value, int role = Qt::EditRole ) override; + + private slots: + + void taskAdded( long id ); + void taskDeleted( long id ); + void progressChanged( long id, double progress ); + void statusChanged( long id, int status ); + + private: + + enum Columns + { + Description = 0, + Progress = 1, + Status = 2, + }; + + enum Roles + { + StatusRole = Qt::UserRole, + }; + + QgsTaskManager* mManager; + + QMap< int, long > mRowToTaskIdMap; + + QgsTask* indexToTask( const QModelIndex& index ) const; + int idToRow( long id ) const; + QModelIndex idToIndex( long id, int column ) const; +}; + + +/** \ingroup gui + * \class QgsProgressBarDelegate + * A delegate for showing a progress bar within a view + * @note introduced in QGIS 2.16 + */ +class GUI_EXPORT QgsProgressBarDelegate : public QStyledItemDelegate +{ + Q_OBJECT + + public: + + /** Constructor for QgsProgressBarDelegate + * @param parent parent object + */ + QgsProgressBarDelegate( QObject* parent = nullptr ); + + void paint( QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index ) const override; + QSize sizeHint( const QStyleOptionViewItem &option, const QModelIndex &index ) const override; +}; + +/** \ingroup gui + * \class QgsProgressBarDelegate + * A delegate for showing task status within a view. Clicks on the delegate will cause the task to be cancelled (via the model). + * @note introduced in QGIS 2.16 + */ +class GUI_EXPORT QgsTaskStatusDelegate : public QStyledItemDelegate +{ + Q_OBJECT + + public: + + /** Constructor for QgsTaskStatusDelegate + * @param parent parent object + */ + QgsTaskStatusDelegate( QObject* parent = nullptr ); + + void paint( QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index ) const override; + QSize sizeHint( const QStyleOptionViewItem &option, const QModelIndex &index ) const override; + bool editorEvent( QEvent * event, QAbstractItemModel * model, const QStyleOptionViewItem & option, const QModelIndex & index ) override; +}; + +#endif //QGSTASKMANAGERWIDGET_H diff --git a/tests/src/core/CMakeLists.txt b/tests/src/core/CMakeLists.txt index 9f7b80525d6..68d93bdd22f 100644 --- a/tests/src/core/CMakeLists.txt +++ b/tests/src/core/CMakeLists.txt @@ -184,6 +184,7 @@ ADD_QGIS_TEST(stringutilstest testqgsstringutils.cpp) ADD_QGIS_TEST(styletest testqgsstyle.cpp) ADD_QGIS_TEST(svgmarkertest testqgssvgmarker.cpp) ADD_QGIS_TEST(symboltest testqgssymbol.cpp) +ADD_QGIS_TEST(taskmanagertest testqgstaskmanager.cpp) ADD_QGIS_TEST(tracertest testqgstracer.cpp) #for some obscure reason calling this test "fontutils" kills the build on Ubuntu 15.10 ADD_QGIS_TEST(typographicstylingutils testqgsfontutils.cpp) diff --git a/tests/src/core/testqgstaskmanager.cpp b/tests/src/core/testqgstaskmanager.cpp new file mode 100644 index 00000000000..29f211e99f0 --- /dev/null +++ b/tests/src/core/testqgstaskmanager.cpp @@ -0,0 +1,289 @@ +/*************************************************************************** + testqgstaskmanager.cpp + ---------------------- + begin : April 2016 + copyright : (C) 2016 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgstaskmanager.h" +#include +#include +#include + +class TestTask : public QgsTask +{ + Q_OBJECT + + public: + + TestTask( const QString& desc = QString() ) : QgsTask( desc ) {} + + void emitProgressChanged( double progress ) { setProgress( progress ); } + void emitTaskStopped() { stopped(); } + void emitTaskCompleted() { completed(); } + +}; + +class TestTerminationTask : public TestTask +{ + Q_OBJECT + + public: + + ~TestTerminationTask() + { + //make sure task has been terminated by manager prior to deletion + Q_ASSERT( status() == QgsTask::Terminated ); + } +}; + + +class TestQgsTaskManager : public QObject +{ + Q_OBJECT + + private slots: + void initTestCase();// will be called before the first testfunction is executed. + void cleanupTestCase();// will be called after the last testfunction was executed. + void init();// will be called before each testfunction is executed. + void cleanup();// will be called after every testfunction. + void task(); + void createInstance(); + void addTask(); + void deleteTask(); + void taskTerminationBeforeDelete(); + void taskId(); + void progressChanged(); + void statusChanged(); + + private: + +}; + +void TestQgsTaskManager::initTestCase() +{ + +} + +void TestQgsTaskManager::cleanupTestCase() +{ + +} + +void TestQgsTaskManager::init() +{ + +} + +void TestQgsTaskManager::cleanup() +{ + +} + +void TestQgsTaskManager::task() +{ + QScopedPointer< TestTask > task( new TestTask( "desc" ) ); + QCOMPARE( task->status(), QgsTask::Running ); + QCOMPARE( task->description(), QString( "desc" ) ); + QVERIFY( task->isActive() ); + + //test that calling stopped sets correct state + QSignalSpy stoppedSpy( task.data(), SIGNAL( taskStopped() ) ); + QSignalSpy statusSpy( task.data(), SIGNAL( statusChanged( int ) ) ); + task->emitTaskStopped(); + QCOMPARE( task->status(), QgsTask::Terminated ); + QVERIFY( !task->isActive() ); + QCOMPARE( stoppedSpy.count(), 1 ); + QCOMPARE( statusSpy.count(), 1 ); + QCOMPARE( static_cast< QgsTask::TaskStatus >( statusSpy.last().at( 0 ).toInt() ), QgsTask::Terminated ); + + //test that calling completed sets correct state + task.reset( new TestTask() ); + QSignalSpy completeSpy( task.data(), SIGNAL( taskCompleted() ) ); + QSignalSpy statusSpy2( task.data(), SIGNAL( statusChanged( int ) ) ); + task->emitTaskCompleted(); + QCOMPARE( task->status(), QgsTask::Complete ); + QVERIFY( !task->isActive() ); + QCOMPARE( completeSpy.count(), 1 ); + QCOMPARE( statusSpy2.count(), 1 ); + QCOMPARE( static_cast< QgsTask::TaskStatus >( statusSpy2.last().at( 0 ).toInt() ), QgsTask::Complete ); +} + + +void TestQgsTaskManager::createInstance() +{ + QgsTaskManager* manager = QgsTaskManager::instance(); + QVERIFY( manager ); +} + +void TestQgsTaskManager::addTask() +{ + //create an empty manager + QgsTaskManager manager; + + //should be empty + QVERIFY( manager.tasks().isEmpty() ); + QCOMPARE( manager.count(), 0 ); + QVERIFY( !manager.task( 0L ) ); + + QSignalSpy spy( &manager, SIGNAL( taskAdded( long ) ) ); + + //add a task + TestTask* task = new TestTask(); + long id = manager.addTask( task ); + QCOMPARE( id, 0L ); + QCOMPARE( manager.tasks().count(), 1 ); + QCOMPARE( manager.count(), 1 ); + QCOMPARE( spy.count(), 1 ); + QCOMPARE( spy.last().at( 0 ).toLongLong(), 0LL ); + + //retrieve task + QCOMPARE( manager.task( 0L ), task ); + QCOMPARE( manager.tasks().at( 0 ), task ); + + //add a second task + TestTask* task2 = new TestTask(); + id = manager.addTask( task2 ); + QCOMPARE( id, 1L ); + QCOMPARE( manager.tasks().count(), 2 ); + QCOMPARE( manager.count(), 2 ); + QCOMPARE( manager.task( 0L ), task ); + QCOMPARE( manager.tasks().at( 0 ), task ); + QCOMPARE( manager.task( 1L ), task2 ); + QCOMPARE( manager.tasks().at( 1 ), task2 ); + + QCOMPARE( spy.count(), 2 ); + QCOMPARE( spy.last().at( 0 ).toLongLong(), 1LL ); +} + +void TestQgsTaskManager::deleteTask() +{ + //create manager with some tasks + QgsTaskManager manager; + TestTask* task = new TestTask(); + TestTask* task2 = new TestTask(); + TestTask* task3 = new TestTask(); + manager.addTask( task ); + manager.addTask( task2 ); + manager.addTask( task3 ); + + QSignalSpy spy( &manager, SIGNAL( taskAboutToBeDeleted( long ) ) ); + + //try deleting a non-existant task + QVERIFY( !manager.deleteTask( 56 ) ); + QCOMPARE( spy.count(), 0 ); + + //try deleting a task by ID + QVERIFY( manager.deleteTask( 1 ) ); + QCOMPARE( manager.tasks().count(), 2 ); + QVERIFY( !manager.task( 1 ) ); + QCOMPARE( spy.count(), 1 ); + QCOMPARE( spy.last().at( 0 ).toLongLong(), 1LL ); + + //can't delete twice + QVERIFY( !manager.deleteTask( 1 ) ); + QCOMPARE( spy.count(), 1 ); + + //delete task by reference + QVERIFY( manager.deleteTask( task ) ); + QCOMPARE( manager.tasks().count(), 1 ); + QVERIFY( !manager.task( 0 ) ); + QCOMPARE( spy.count(), 2 ); + QCOMPARE( spy.last().at( 0 ).toLongLong(), 0LL ); +} + +void TestQgsTaskManager::taskTerminationBeforeDelete() +{ + //test that task is terminated by manager prior to delete + QgsTaskManager* manager = new QgsTaskManager(); + + //TestTerminationTask will assert that it's been terminated prior to deletion + TestTask* task = new TestTerminationTask(); + manager->addTask( task ); + + // if task is not terminated assert will trip + delete manager; + QApplication::sendPostedEvents( nullptr, QEvent::DeferredDelete ); +} + +void TestQgsTaskManager::taskId() +{ + //test finding task IDs + + //create manager with some tasks + QgsTaskManager manager; + TestTask* task = new TestTask(); + TestTask* task2 = new TestTask(); + manager.addTask( task ); + manager.addTask( task2 ); + + //also a task not in the manager + TestTask* task3 = new TestTask(); + + QCOMPARE( manager.taskId( nullptr ), -1L ); + QCOMPARE( manager.taskId( task ), 0L ); + QCOMPARE( manager.taskId( task2 ), 1L ); + QCOMPARE( manager.taskId( task3 ), -1L ); + + delete task3; +} + +void TestQgsTaskManager::progressChanged() +{ + // check that progressChanged signals emitted by tasks result in progressChanged signal from manager + QgsTaskManager manager; + TestTask* task = new TestTask(); + TestTask* task2 = new TestTask(); + manager.addTask( task ); + manager.addTask( task2 ); + + QSignalSpy spy( &manager, SIGNAL( progressChanged( long, double ) ) ); + + task->emitProgressChanged( 50.0 ); + QCOMPARE( task->progress(), 50.0 ); + QCOMPARE( spy.count(), 1 ); + QCOMPARE( spy.last().at( 0 ).toLongLong(), 0LL ); + QCOMPARE( spy.last().at( 1 ).toDouble(), 50.0 ); + + task2->emitProgressChanged( 75.0 ); + QCOMPARE( task2->progress(), 75.0 ); + QCOMPARE( spy.count(), 2 ); + QCOMPARE( spy.last().at( 0 ).toLongLong(), 1LL ); + QCOMPARE( spy.last().at( 1 ).toDouble(), 75.0 ); +} + +void TestQgsTaskManager::statusChanged() +{ + // check that statusChanged signals emitted by tasks result in statusChanged signal from manager + QgsTaskManager manager; + TestTask* task = new TestTask(); + TestTask* task2 = new TestTask(); + manager.addTask( task ); + manager.addTask( task2 ); + + QSignalSpy spy( &manager, SIGNAL( statusChanged( long, int ) ) ); + + task->emitTaskStopped(); + QCOMPARE( spy.count(), 1 ); + QCOMPARE( spy.last().at( 0 ).toLongLong(), 0LL ); + QCOMPARE( static_cast< QgsTask::TaskStatus >( spy.last().at( 1 ).toInt() ), QgsTask::Terminated ); + + task2->emitTaskCompleted(); + QCOMPARE( spy.count(), 2 ); + QCOMPARE( spy.last().at( 0 ).toLongLong(), 1LL ); + QCOMPARE( static_cast< QgsTask::TaskStatus >( spy.last().at( 1 ).toInt() ), QgsTask::Complete ); +} + + +QTEST_MAIN( TestQgsTaskManager ) +#include "testqgstaskmanager.moc"