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