QGIS API Documentation 4.1.0-Master (376402f9aeb)
Loading...
Searching...
No Matches
qgstaskmanagerwidget.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgstaskmanagerwidget.cpp
3 ------------------------
4 begin : April 2016
5 copyright : (C) 2016 by Nyall Dawson
6 email : nyall dot dawson at gmail dot com
7 ***************************************************************************/
8
9/***************************************************************************
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 ***************************************************************************/
17
19
20#include "qgsapplication.h"
21#include "qgstaskmanager.h"
22
23#include <QAction>
24#include <QHeaderView>
25#include <QLayout>
26#include <QMouseEvent>
27#include <QPainter>
28#include <QProgressBar>
29#include <QString>
30#include <QToolBar>
31#include <QTreeView>
32
33#include "moc_qgstaskmanagerwidget.cpp"
34
35using namespace Qt::StringLiterals;
36
37//
38// QgsTaskManagerWidget
39//
40
42 : QWidget( parent )
43 , mManager( manager )
44{
45 Q_ASSERT( manager );
46
47 QVBoxLayout *vLayout = new QVBoxLayout();
48 vLayout->setContentsMargins( 0, 0, 0, 0 );
49 mTreeView = new QTreeView();
50 mModel = new QgsTaskManagerModel( manager, this );
51 mTreeView->setModel( mModel );
52 connect( mModel, &QgsTaskManagerModel::rowsInserted, this, &QgsTaskManagerWidget::modelRowsInserted );
53 mTreeView->setHeaderHidden( true );
54 mTreeView->setRootIsDecorated( false );
55 mTreeView->setSelectionBehavior( QAbstractItemView::SelectRows );
56
57 const int progressColWidth = static_cast<int>( fontMetrics().horizontalAdvance( 'X' ) * 10 * Qgis::UI_SCALE_FACTOR );
58 mTreeView->setColumnWidth( QgsTaskManagerModel::Progress, progressColWidth );
59
60 const int statusColWidth = static_cast<int>( fontMetrics().horizontalAdvance( 'X' ) * 2 * Qgis::UI_SCALE_FACTOR );
61 mTreeView->setColumnWidth( QgsTaskManagerModel::Status, statusColWidth );
62 mTreeView->setHorizontalScrollBarPolicy( Qt::ScrollBarAlwaysOff );
63 mTreeView->setVerticalScrollBarPolicy( Qt::ScrollBarAlwaysOn );
64 mTreeView->header()->setStretchLastSection( false );
65 mTreeView->header()->setSectionResizeMode( QgsTaskManagerModel::Description, QHeaderView::Stretch );
66
67 connect( mTreeView, &QTreeView::clicked, this, &QgsTaskManagerWidget::clicked );
68
69 vLayout->addWidget( mTreeView );
70
71 setLayout( vLayout );
72}
73
76
77
78void QgsTaskManagerWidget::modelRowsInserted( const QModelIndex &, int start, int end )
79{
80 for ( int row = start; row <= end; ++row )
81 {
82 QgsTask *task = mModel->indexToTask( mModel->index( row, 1 ) );
83 if ( !task )
84 continue;
85
86 QProgressBar *progressBar = new QProgressBar();
87 progressBar->setAutoFillBackground( true );
88 progressBar->setRange( 0, 0 );
89 connect( task, &QgsTask::progressChanged, progressBar, [progressBar]( double progress ) {
90 //until first progress report, we show a progress bar of interderminant length
91 if ( progress > 0 )
92 {
93 progressBar->setMaximum( 100 );
94 progressBar->setValue( static_cast<int>( progress ) );
95 }
96 else
97 progressBar->setMaximum( 0 );
98 } );
99 mTreeView->setIndexWidget( mModel->index( row, QgsTaskManagerModel::Progress ), progressBar );
100
101 QgsTaskStatusWidget *statusWidget = new QgsTaskStatusWidget( nullptr, task->status(), task->canCancel() );
102 statusWidget->setAutoFillBackground( true );
103 connect( task, &QgsTask::statusChanged, statusWidget, &QgsTaskStatusWidget::setStatus );
104 connect( statusWidget, &QgsTaskStatusWidget::cancelClicked, task, &QgsTask::cancel );
105 mTreeView->setIndexWidget( mModel->index( row, QgsTaskManagerModel::Status ), statusWidget );
106 }
107}
108
109void QgsTaskManagerWidget::clicked( const QModelIndex &index )
110{
111 QgsTask *task = mModel->indexToTask( index );
112 if ( !task )
113 return;
114
115 mManager->triggerTask( task );
116}
117
119//
120// QgsTaskManagerModel
121//
122
123QgsTaskManagerModel::QgsTaskManagerModel( QgsTaskManager *manager, QObject *parent )
124 : QAbstractItemModel( parent )
125 , mManager( manager )
126{
127 Q_ASSERT( mManager );
128
129 //populate row to id map
130 const auto constTasks = mManager->tasks();
131 for ( QgsTask *task : constTasks )
132 {
133 mRowToTaskIdList << mManager->taskId( task );
134 }
135
136 connect( mManager, &QgsTaskManager::taskAdded, this, &QgsTaskManagerModel::taskAdded );
137 connect( mManager, &QgsTaskManager::progressChanged, this, &QgsTaskManagerModel::progressChanged );
138 connect( mManager, &QgsTaskManager::statusChanged, this, &QgsTaskManagerModel::statusChanged );
139}
140
141QModelIndex QgsTaskManagerModel::index( int row, int column, const QModelIndex &parent ) const
142{
143 if ( column < 0 || column >= columnCount() )
144 {
145 //column out of bounds
146 return QModelIndex();
147 }
148
149 if ( !parent.isValid() && row >= 0 && row < mRowToTaskIdList.count() )
150 {
151 //return an index for the task at this position
152 return createIndex( row, column );
153 }
154
155 //only top level supported
156 return QModelIndex();
157}
158
159QModelIndex QgsTaskManagerModel::parent( const QModelIndex &index ) const
160{
161 Q_UNUSED( index )
162
163 //all items are top level
164 return QModelIndex();
165}
166
167int QgsTaskManagerModel::rowCount( const QModelIndex &parent ) const
168{
169 if ( !parent.isValid() )
170 {
171 return mRowToTaskIdList.count();
172 }
173 else
174 {
175 //no children
176 return 0;
177 }
178}
179
180int QgsTaskManagerModel::columnCount( const QModelIndex &parent ) const
181{
182 Q_UNUSED( parent )
183 return 3;
184}
185
186QVariant QgsTaskManagerModel::data( const QModelIndex &index, int role ) const
187{
188 if ( !index.isValid() )
189 return QVariant();
190
191 QgsTask *task = indexToTask( index );
192 if ( task )
193 {
194 switch ( role )
195 {
196 case Qt::DisplayRole:
197 case Qt::EditRole:
198 switch ( index.column() )
199 {
200 case Description:
201 return task->description();
202 case Progress:
203 return task->progress();
204 case Status:
205 // delegate shows status
206 return QVariant();
207 default:
208 return QVariant();
209 }
210
211 case static_cast<int>( CustomRole::Status ):
212 return static_cast<int>( task->status() );
213
214 case Qt::ToolTipRole:
215 switch ( index.column() )
216 {
217 case Description:
218 return createTooltip( task, ToolTipDescription );
219 case Progress:
220 return createTooltip( task, ToolTipProgress );
221 case Status:
222 return createTooltip( task, ToolTipStatus );
223 default:
224 return QVariant();
225 }
226
227
228 default:
229 return QVariant();
230 }
231 }
232
233 return QVariant();
234}
235
236Qt::ItemFlags QgsTaskManagerModel::flags( const QModelIndex &index ) const
237{
238 Qt::ItemFlags flags = QAbstractItemModel::flags( index );
239
240 if ( !index.isValid() )
241 {
242 return flags;
243 }
244
245 QgsTask *task = indexToTask( index );
246 if ( index.column() == Status )
247 {
248 if ( task && task->canCancel() )
249 flags = flags | Qt::ItemIsEditable;
250 }
251 return flags | Qt::ItemIsEnabled | Qt::ItemIsSelectable;
252}
253
254bool QgsTaskManagerModel::setData( const QModelIndex &index, const QVariant &value, int role )
255{
256 Q_UNUSED( role )
257
258 if ( !index.isValid() )
259 return false;
260
261 QgsTask *task = indexToTask( index );
262 if ( !task )
263 return false;
264
265 switch ( index.column() )
266 {
267 case Status:
268 {
269 if ( value.toBool() && task->canCancel() )
270 task->cancel();
271 return true;
272 }
273
274 default:
275 return false;
276 }
277}
278
279void QgsTaskManagerModel::taskAdded( long id )
280{
281 beginInsertRows( QModelIndex(), mRowToTaskIdList.count(), mRowToTaskIdList.count() );
282 mRowToTaskIdList << id;
283 endInsertRows();
284}
285
286void QgsTaskManagerModel::taskDeleted( long id )
287{
288 for ( int row = 0; row < mRowToTaskIdList.count(); ++row )
289 {
290 if ( mRowToTaskIdList.at( row ) == id )
291 {
292 beginRemoveRows( QModelIndex(), row, row );
293 mRowToTaskIdList.removeAt( row );
294 endRemoveRows();
295 return;
296 }
297 }
298}
299
300void QgsTaskManagerModel::progressChanged( long id, double progress )
301{
302 Q_UNUSED( progress )
303
304 const QModelIndex index = idToIndex( id, Progress );
305 if ( !index.isValid() )
306 {
307 return;
308 }
309
310 emit dataChanged( index, index );
311}
312
313void QgsTaskManagerModel::statusChanged( long id, int status )
314{
315 if ( status == QgsTask::Complete || status == QgsTask::Terminated )
316 {
317 taskDeleted( id );
318 }
319 else
320 {
321 const QModelIndex index = idToIndex( id, Status );
322 if ( !index.isValid() )
323 {
324 return;
325 }
326
327 emit dataChanged( index, index );
328 }
329}
330
331QgsTask *QgsTaskManagerModel::indexToTask( const QModelIndex &index ) const
332{
333 if ( !index.isValid() || index.parent().isValid() )
334 return nullptr;
335
336 const long id = index.row() >= 0 && index.row() < mRowToTaskIdList.count() ? mRowToTaskIdList.at( index.row() ) : -1;
337 if ( id >= 0 )
338 return mManager->task( id );
339 else
340 return nullptr;
341}
342
343int QgsTaskManagerModel::idToRow( long id ) const
344{
345 for ( int row = 0; row < mRowToTaskIdList.count(); ++row )
346 {
347 if ( mRowToTaskIdList.at( row ) == id )
348 {
349 return row;
350 }
351 }
352 return -1;
353}
354
355QModelIndex QgsTaskManagerModel::idToIndex( long id, int column ) const
356{
357 const int row = idToRow( id );
358 if ( row < 0 )
359 return QModelIndex();
360
361 return index( row, column );
362}
363
364QString QgsTaskManagerModel::createTooltip( QgsTask *task, ToolTipType type )
365{
366 if ( task->status() != QgsTask::Running )
367 {
368 switch ( type )
369 {
370 case ToolTipDescription:
371 return task->description();
372
373 case ToolTipStatus:
374 case ToolTipProgress:
375 {
376 switch ( task->status() )
377 {
378 case QgsTask::Queued:
379 return tr( "Queued" );
380 case QgsTask::OnHold:
381 return tr( "On hold" );
382 case QgsTask::Running:
383 {
384 if ( type == ToolTipStatus && !task->canCancel() )
385 return tr( "Running (cannot cancel)" );
386 else
387 return tr( "Running" );
388 }
390 return tr( "Complete" );
392 return tr( "Terminated" );
393 }
394 }
395 }
396 }
397
398 QString formattedTime;
399
400 const qint64 elapsed = task->elapsedTime();
401
402 if ( task->progress() > 0 )
403 {
404 // estimate time remaining
405 const qint64 msRemain = static_cast<qint64>( elapsed * 100.0 / task->progress() - elapsed );
406 if ( msRemain > 120 * 1000 )
407 {
408 const long long minutes = msRemain / 1000 / 60;
409 const int seconds = ( msRemain / 1000 ) % 60;
410 formattedTime = tr( "%1:%2 minutes" ).arg( minutes ).arg( seconds, 2, 10, QChar( '0' ) );
411 }
412 else
413 formattedTime = tr( "%1 seconds" ).arg( msRemain / 1000 );
414
415 formattedTime = tr( "Estimated time remaining: %1" ).arg( formattedTime );
416
417 const QTime estimatedEnd = QTime::currentTime().addMSecs( msRemain );
418 formattedTime += tr( " (%1)" ).arg( QLocale::system().toString( estimatedEnd, QLocale::ShortFormat ) );
419 }
420 else
421 {
422 if ( elapsed > 120 * 1000 )
423 {
424 const long long minutes = elapsed / 1000 / 60;
425 const int seconds = ( elapsed / 1000 ) % 60;
426 formattedTime = tr( "%1:%2 minutes" ).arg( minutes ).arg( seconds, 2, 10, QChar( '0' ) );
427 }
428 else
429 formattedTime = tr( "%1 seconds" ).arg( elapsed / 1000 );
430
431 formattedTime = tr( "Time elapsed: %1" ).arg( formattedTime );
432 }
433
434 switch ( type )
435 {
436 case ToolTipDescription:
437 return tr( "%1<br>%2" ).arg( task->description(), formattedTime );
438
439 case ToolTipStatus:
440 case ToolTipProgress:
441 {
442 switch ( task->status() )
443 {
444 case QgsTask::Queued:
445 return tr( "Queued" );
446 case QgsTask::OnHold:
447 return tr( "On hold" );
448 case QgsTask::Running:
449 {
450 QString statusDesc;
451 if ( type == ToolTipStatus && !task->canCancel() )
452 statusDesc = tr( "Running (cannot cancel)" );
453 else
454 statusDesc = tr( "Running" );
455 return tr( "%1<br>%2" ).arg( statusDesc, formattedTime );
456 }
458 return tr( "Complete" );
460 return tr( "Terminated" );
461 }
462 }
463 }
464 // no warnings
465 return QString();
466}
467
468
469//
470// QgsTaskStatusDelegate
471//
472
473QgsTaskStatusWidget::QgsTaskStatusWidget( QWidget *parent, QgsTask::TaskStatus status, bool canCancel )
474 : QWidget( parent )
475 , mCanCancel( canCancel )
476 , mStatus( status )
477{
478 setMouseTracking( true );
479}
480
481QSize QgsTaskStatusWidget::sizeHint() const
482{
483 return QSize( 32, 32 );
484}
485
486void QgsTaskStatusWidget::setStatus( int status )
487{
488 mStatus = static_cast<QgsTask::TaskStatus>( status );
489 update();
490}
491
492void QgsTaskStatusWidget::paintEvent( QPaintEvent *e )
493{
494 QWidget::paintEvent( e );
495
496 QIcon icon;
497 if ( mInside && ( mCanCancel || ( mStatus == QgsTask::Queued || mStatus == QgsTask::OnHold ) ) )
498 {
499 icon = QgsApplication::getThemeIcon( u"/mTaskCancel.svg"_s );
500 }
501 else
502 {
503 switch ( mStatus )
504 {
505 case QgsTask::Queued:
506 icon = QgsApplication::getThemeIcon( u"/mTaskQueued.svg"_s );
507 break;
508 case QgsTask::OnHold:
509 icon = QgsApplication::getThemeIcon( u"/mTaskOnHold.svg"_s );
510 break;
511 case QgsTask::Running:
512 icon = QgsApplication::getThemeIcon( u"/mTaskRunning.svg"_s );
513 break;
515 icon = QgsApplication::getThemeIcon( u"/mTaskComplete.svg"_s );
516 break;
518 icon = QgsApplication::getThemeIcon( u"/mTaskTerminated.svg"_s );
519 break;
520 }
521 }
522
523 QPainter p( this );
524 icon.paint( &p, 1, height() / 2 - 12, 24, 24 );
525 p.end();
526}
527
528void QgsTaskStatusWidget::mousePressEvent( QMouseEvent * )
529{
530 if ( mCanCancel || ( mStatus == QgsTask::Queued || mStatus == QgsTask::OnHold ) )
531 emit cancelClicked();
532}
533
534void QgsTaskStatusWidget::mouseMoveEvent( QMouseEvent * )
535{
536 if ( !mInside )
537 {
538 mInside = true;
539 update();
540 }
541}
542
543void QgsTaskStatusWidget::leaveEvent( QEvent * )
544{
545 mInside = false;
546 update();
547}
548
549
550/*
551bool QgsTaskStatusWidget::editorEvent( QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index )
552{
553 Q_UNUSED( option )
554 if ( event->type() == QEvent::MouseButtonPress )
555 {
556 QMouseEvent *e = static_cast<QMouseEvent*>( event );
557 if ( e->button() == Qt::LeftButton )
558 {
559 if ( !index.model()->flags( index ).testFlag( Qt::ItemIsEditable ) )
560 {
561 //item not editable
562 return false;
563 }
564
565 return model->setData( index, true, Qt::EditRole );
566 }
567 }
568 return false;
569}
570*/
571
572QgsTaskManagerFloatingWidget::QgsTaskManagerFloatingWidget( QgsTaskManager *manager, QWidget *parent )
573 : QgsFloatingWidget( parent )
574{
575 setLayout( new QVBoxLayout() );
576 QgsTaskManagerWidget *w = new QgsTaskManagerWidget( manager );
577
578 const int minWidth = static_cast<int>( fontMetrics().horizontalAdvance( 'X' ) * 60 * Qgis::UI_SCALE_FACTOR );
579 const int minHeight = static_cast<int>( fontMetrics().height() * 15 * Qgis::UI_SCALE_FACTOR );
580 setMinimumSize( minWidth, minHeight );
581 layout()->addWidget( w );
582 setStyleSheet(
583 ".QgsTaskManagerFloatingWidget { border-top-left-radius: 8px;"
584 "border-top-right-radius: 8px; background-color: rgba(0, 0, 0, 70%); }"
585 );
586}
587
588
589QgsTaskManagerStatusBarWidget::QgsTaskManagerStatusBarWidget( QgsTaskManager *manager, QWidget *parent )
590 : QToolButton( parent )
591 , mManager( manager )
592{
593 setAutoRaise( true );
594 setSizePolicy( QSizePolicy::Fixed, QSizePolicy::MinimumExpanding );
595 setLayout( new QVBoxLayout() );
596
597 mProgressBar = new QProgressBar();
598 mProgressBar->setMinimum( 0 );
599 mProgressBar->setMaximum( 0 );
600 layout()->setContentsMargins( 5, 5, 5, 5 );
601 layout()->addWidget( mProgressBar );
602
603 mFloatingWidget = new QgsTaskManagerFloatingWidget( manager, parent ? parent->window() : nullptr );
604 mFloatingWidget->setAnchorWidget( this );
605 mFloatingWidget->setAnchorPoint( QgsFloatingWidget::BottomMiddle );
606 mFloatingWidget->setAnchorWidgetPoint( QgsFloatingWidget::TopMiddle );
607 mFloatingWidget->hide();
608 connect( this, &QgsTaskManagerStatusBarWidget::clicked, this, &QgsTaskManagerStatusBarWidget::toggleDisplay );
609 hide();
610
611 connect( manager, &QgsTaskManager::taskAdded, this, &QgsTaskManagerStatusBarWidget::showButton );
612 connect( manager, &QgsTaskManager::allTasksFinished, this, &QgsTaskManagerStatusBarWidget::allFinished );
613 connect( manager, &QgsTaskManager::finalTaskProgressChanged, this, &QgsTaskManagerStatusBarWidget::overallProgressChanged );
614 connect( manager, &QgsTaskManager::countActiveTasksChanged, this, &QgsTaskManagerStatusBarWidget::countActiveTasksChanged );
615
616 if ( manager->countActiveTasks() )
617 showButton();
618}
619
620QSize QgsTaskManagerStatusBarWidget::sizeHint() const
621{
622 const int width = static_cast<int>( fontMetrics().horizontalAdvance( 'X' ) * 20 * Qgis::UI_SCALE_FACTOR );
623 const int height = QToolButton::sizeHint().height();
624 return QSize( width, height );
625}
626
627void QgsTaskManagerStatusBarWidget::changeEvent( QEvent *event )
628{
629 QToolButton::changeEvent( event );
630
631 if ( event->type() == QEvent::FontChange )
632 {
633 mProgressBar->setFont( font() );
634 }
635}
636
637void QgsTaskManagerStatusBarWidget::toggleDisplay()
638{
639 if ( mFloatingWidget->isVisible() )
640 mFloatingWidget->hide();
641 else
642 {
643 mFloatingWidget->show();
644 mFloatingWidget->raise();
645 }
646}
647
648void QgsTaskManagerStatusBarWidget::overallProgressChanged( double progress )
649{
650 mProgressBar->setValue( static_cast<int>( progress ) );
651 if ( qgsDoubleNear( progress, 0.0 ) )
652 mProgressBar->setMaximum( 0 );
653 else if ( mProgressBar->maximum() == 0 )
654 mProgressBar->setMaximum( 100 );
655 setToolTip( QgsTaskManagerModel::createTooltip( mManager->activeTasks().at( 0 ), QgsTaskManagerModel::ToolTipDescription ) );
656}
657
658void QgsTaskManagerStatusBarWidget::countActiveTasksChanged( int count )
659{
660 if ( count > 1 )
661 {
662 mProgressBar->setMaximum( 0 );
663 setToolTip( tr( "%n active task(s) running", nullptr, count ) );
664 }
665}
666
667void QgsTaskManagerStatusBarWidget::allFinished()
668{
669 mFloatingWidget->hide();
670 hide();
671
672 mProgressBar->setMaximum( 0 );
673 mProgressBar->setValue( 0 );
674}
675
676void QgsTaskManagerStatusBarWidget::showButton()
677{
678 if ( !isVisible() )
679 {
680 mProgressBar->setMaximum( 0 );
681 mProgressBar->setValue( 0 );
682 show();
683 }
684}
static const double UI_SCALE_FACTOR
UI scaling factor.
Definition qgis.h:6747
static QIcon getThemeIcon(const QString &name, const QColor &fillColor=QColor(), const QColor &strokeColor=QColor())
Helper to get a theme icon.
A QWidget subclass for creating widgets which float outside of the normal Qt layout system.
@ BottomMiddle
Bottom center of widget.
@ TopMiddle
Top center of widget.
QgsTaskManagerWidget(QgsTaskManager *manager, QWidget *parent=nullptr)
Constructor for QgsTaskManagerWidget.
Task manager for managing a set of long-running QgsTask tasks.
void finalTaskProgressChanged(double progress)
Will be emitted when only a single task remains to complete and that task has reported a progress cha...
void statusChanged(long taskId, int status)
Will be emitted when a task reports a status change.
void taskAdded(long taskId)
Emitted when a new task has been added to the manager.
void allTasksFinished()
Emitted when all tasks are complete.
void progressChanged(long taskId, double progress)
Will be emitted when a task reports a progress change.
int countActiveTasks(bool includeHidden=true) const
Returns the number of active (queued or running) tasks.
void countActiveTasksChanged(int count)
Emitted when the number of active tasks changes.
Abstract base class for long running background tasks.
TaskStatus status() const
Returns the current task status.
double progress() const
Returns the task's progress (between 0.0 and 100.0).
void progressChanged(double progress)
Will be emitted by task when its progress changes.
virtual void cancel()
Notifies the task that it should terminate.
void statusChanged(int status)
Will be emitted by task when its status changes.
qint64 elapsedTime() const
Returns the elapsed time since the task commenced, in milliseconds.
TaskStatus
Status of tasks.
@ Terminated
Task was terminated or errored.
@ Queued
Task is queued and has not begun.
@ OnHold
Task is queued but on hold and will not be started.
@ Running
Task is currently running.
@ Complete
Task successfully completed.
QString description() const
Returns the task's description.
bool canCancel() const
Returns true if the task can be canceled.
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference).
Definition qgis.h:7134