QGIS API Documentation 3.99.0-Master (e9821da5c6b)
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
78
79
80void QgsTaskManagerWidget::modelRowsInserted( const QModelIndex &, int start, int end )
81{
82 for ( int row = start; row <= end; ++row )
83 {
84 QgsTask *task = mModel->indexToTask( mModel->index( row, 1 ) );
85 if ( !task )
86 continue;
87
88 QProgressBar *progressBar = new QProgressBar();
89 progressBar->setAutoFillBackground( true );
90 progressBar->setRange( 0, 0 );
91 connect( task, &QgsTask::progressChanged, progressBar, [progressBar]( double progress ) {
92 //until first progress report, we show a progress bar of interderminant length
93 if ( progress > 0 )
94 {
95 progressBar->setMaximum( 100 );
96 progressBar->setValue( static_cast<int>( progress ) );
97 }
98 else
99 progressBar->setMaximum( 0 );
100 } );
101 mTreeView->setIndexWidget( mModel->index( row, QgsTaskManagerModel::Progress ), progressBar );
102
103 QgsTaskStatusWidget *statusWidget = new QgsTaskStatusWidget( nullptr, task->status(), task->canCancel() );
104 statusWidget->setAutoFillBackground( true );
105 connect( task, &QgsTask::statusChanged, statusWidget, &QgsTaskStatusWidget::setStatus );
106 connect( statusWidget, &QgsTaskStatusWidget::cancelClicked, task, &QgsTask::cancel );
107 mTreeView->setIndexWidget( mModel->index( row, QgsTaskManagerModel::Status ), statusWidget );
108 }
109}
110
111void QgsTaskManagerWidget::clicked( const QModelIndex &index )
112{
113 QgsTask *task = mModel->indexToTask( index );
114 if ( !task )
115 return;
116
117 mManager->triggerTask( task );
118}
119
121//
122// QgsTaskManagerModel
123//
124
125QgsTaskManagerModel::QgsTaskManagerModel( QgsTaskManager *manager, QObject *parent )
126 : QAbstractItemModel( parent )
127 , mManager( manager )
128{
129 Q_ASSERT( mManager );
130
131 //populate row to id map
132 const auto constTasks = mManager->tasks();
133 for ( QgsTask *task : constTasks )
134 {
135 mRowToTaskIdList << mManager->taskId( task );
136 }
137
138 connect( mManager, &QgsTaskManager::taskAdded, this, &QgsTaskManagerModel::taskAdded );
139 connect( mManager, &QgsTaskManager::progressChanged, this, &QgsTaskManagerModel::progressChanged );
140 connect( mManager, &QgsTaskManager::statusChanged, this, &QgsTaskManagerModel::statusChanged );
141}
142
143QModelIndex QgsTaskManagerModel::index( int row, int column, const QModelIndex &parent ) const
144{
145 if ( column < 0 || column >= columnCount() )
146 {
147 //column out of bounds
148 return QModelIndex();
149 }
150
151 if ( !parent.isValid() && row >= 0 && row < mRowToTaskIdList.count() )
152 {
153 //return an index for the task at this position
154 return createIndex( row, column );
155 }
156
157 //only top level supported
158 return QModelIndex();
159}
160
161QModelIndex QgsTaskManagerModel::parent( const QModelIndex &index ) const
162{
163 Q_UNUSED( index )
164
165 //all items are top level
166 return QModelIndex();
167}
168
169int QgsTaskManagerModel::rowCount( const QModelIndex &parent ) const
170{
171 if ( !parent.isValid() )
172 {
173 return mRowToTaskIdList.count();
174 }
175 else
176 {
177 //no children
178 return 0;
179 }
180}
181
182int QgsTaskManagerModel::columnCount( const QModelIndex &parent ) const
183{
184 Q_UNUSED( parent )
185 return 3;
186}
187
188QVariant QgsTaskManagerModel::data( const QModelIndex &index, int role ) const
189{
190 if ( !index.isValid() )
191 return QVariant();
192
193 QgsTask *task = indexToTask( index );
194 if ( task )
195 {
196 switch ( role )
197 {
198 case Qt::DisplayRole:
199 case Qt::EditRole:
200 switch ( index.column() )
201 {
202 case Description:
203 return task->description();
204 case Progress:
205 return task->progress();
206 case Status:
207 // delegate shows status
208 return QVariant();
209 default:
210 return QVariant();
211 }
212
213 case static_cast<int>( CustomRole::Status ):
214 return static_cast<int>( task->status() );
215
216 case Qt::ToolTipRole:
217 switch ( index.column() )
218 {
219 case Description:
220 return createTooltip( task, ToolTipDescription );
221 case Progress:
222 return createTooltip( task, ToolTipProgress );
223 case Status:
224 return createTooltip( task, ToolTipStatus );
225 default:
226 return QVariant();
227 }
228
229
230 default:
231 return QVariant();
232 }
233 }
234
235 return QVariant();
236}
237
238Qt::ItemFlags QgsTaskManagerModel::flags( const QModelIndex &index ) const
239{
240 Qt::ItemFlags flags = QAbstractItemModel::flags( index );
241
242 if ( !index.isValid() )
243 {
244 return flags;
245 }
246
247 QgsTask *task = indexToTask( index );
248 if ( index.column() == Status )
249 {
250 if ( task && task->canCancel() )
251 flags = flags | Qt::ItemIsEditable;
252 }
253 return flags | Qt::ItemIsEnabled | Qt::ItemIsSelectable;
254}
255
256bool QgsTaskManagerModel::setData( const QModelIndex &index, const QVariant &value, int role )
257{
258 Q_UNUSED( role )
259
260 if ( !index.isValid() )
261 return false;
262
263 QgsTask *task = indexToTask( index );
264 if ( !task )
265 return false;
266
267 switch ( index.column() )
268 {
269 case Status:
270 {
271 if ( value.toBool() && task->canCancel() )
272 task->cancel();
273 return true;
274 }
275
276 default:
277 return false;
278 }
279}
280
281void QgsTaskManagerModel::taskAdded( long id )
282{
283 beginInsertRows( QModelIndex(), mRowToTaskIdList.count(), mRowToTaskIdList.count() );
284 mRowToTaskIdList << id;
285 endInsertRows();
286}
287
288void QgsTaskManagerModel::taskDeleted( long id )
289{
290 for ( int row = 0; row < mRowToTaskIdList.count(); ++row )
291 {
292 if ( mRowToTaskIdList.at( row ) == id )
293 {
294 beginRemoveRows( QModelIndex(), row, row );
295 mRowToTaskIdList.removeAt( row );
296 endRemoveRows();
297 return;
298 }
299 }
300}
301
302void QgsTaskManagerModel::progressChanged( long id, double progress )
303{
304 Q_UNUSED( progress )
305
306 const QModelIndex index = idToIndex( id, Progress );
307 if ( !index.isValid() )
308 {
309 return;
310 }
311
312 emit dataChanged( index, index );
313}
314
315void QgsTaskManagerModel::statusChanged( long id, int status )
316{
317 if ( status == QgsTask::Complete || status == QgsTask::Terminated )
318 {
319 taskDeleted( id );
320 }
321 else
322 {
323 const QModelIndex index = idToIndex( id, Status );
324 if ( !index.isValid() )
325 {
326 return;
327 }
328
329 emit dataChanged( index, index );
330 }
331}
332
333QgsTask *QgsTaskManagerModel::indexToTask( const QModelIndex &index ) const
334{
335 if ( !index.isValid() || index.parent().isValid() )
336 return nullptr;
337
338 const long id = index.row() >= 0 && index.row() < mRowToTaskIdList.count() ? mRowToTaskIdList.at( index.row() ) : -1;
339 if ( id >= 0 )
340 return mManager->task( id );
341 else
342 return nullptr;
343}
344
345int QgsTaskManagerModel::idToRow( long id ) const
346{
347 for ( int row = 0; row < mRowToTaskIdList.count(); ++row )
348 {
349 if ( mRowToTaskIdList.at( row ) == id )
350 {
351 return row;
352 }
353 }
354 return -1;
355}
356
357QModelIndex QgsTaskManagerModel::idToIndex( long id, int column ) const
358{
359 const int row = idToRow( id );
360 if ( row < 0 )
361 return QModelIndex();
362
363 return index( row, column );
364}
365
366QString QgsTaskManagerModel::createTooltip( QgsTask *task, ToolTipType type )
367{
368 if ( task->status() != QgsTask::Running )
369 {
370 switch ( type )
371 {
372 case ToolTipDescription:
373 return task->description();
374
375 case ToolTipStatus:
376 case ToolTipProgress:
377 {
378 switch ( task->status() )
379 {
380 case QgsTask::Queued:
381 return tr( "Queued" );
382 case QgsTask::OnHold:
383 return tr( "On hold" );
384 case QgsTask::Running:
385 {
386 if ( type == ToolTipStatus && !task->canCancel() )
387 return tr( "Running (cannot cancel)" );
388 else
389 return tr( "Running" );
390 }
392 return tr( "Complete" );
394 return tr( "Terminated" );
395 }
396 }
397 }
398 }
399
400 QString formattedTime;
401
402 const qint64 elapsed = task->elapsedTime();
403
404 if ( task->progress() > 0 )
405 {
406 // estimate time remaining
407 const qint64 msRemain = static_cast<qint64>( elapsed * 100.0 / task->progress() - elapsed );
408 if ( msRemain > 120 * 1000 )
409 {
410 const long long minutes = msRemain / 1000 / 60;
411 const int seconds = ( msRemain / 1000 ) % 60;
412 formattedTime = tr( "%1:%2 minutes" ).arg( minutes ).arg( seconds, 2, 10, QChar( '0' ) );
413 }
414 else
415 formattedTime = tr( "%1 seconds" ).arg( msRemain / 1000 );
416
417 formattedTime = tr( "Estimated time remaining: %1" ).arg( formattedTime );
418
419 const QTime estimatedEnd = QTime::currentTime().addMSecs( msRemain );
420 formattedTime += tr( " (%1)" ).arg( QLocale::system().toString( estimatedEnd, QLocale::ShortFormat ) );
421 }
422 else
423 {
424 if ( elapsed > 120 * 1000 )
425 {
426 const long long minutes = elapsed / 1000 / 60;
427 const int seconds = ( elapsed / 1000 ) % 60;
428 formattedTime = tr( "%1:%2 minutes" ).arg( minutes ).arg( seconds, 2, 10, QChar( '0' ) );
429 }
430 else
431 formattedTime = tr( "%1 seconds" ).arg( elapsed / 1000 );
432
433 formattedTime = tr( "Time elapsed: %1" ).arg( formattedTime );
434 }
435
436 switch ( type )
437 {
438 case ToolTipDescription:
439 return tr( "%1<br>%2" ).arg( task->description(), formattedTime );
440
441 case ToolTipStatus:
442 case ToolTipProgress:
443 {
444 switch ( task->status() )
445 {
446 case QgsTask::Queued:
447 return tr( "Queued" );
448 case QgsTask::OnHold:
449 return tr( "On hold" );
450 case QgsTask::Running:
451 {
452 QString statusDesc;
453 if ( type == ToolTipStatus && !task->canCancel() )
454 statusDesc = tr( "Running (cannot cancel)" );
455 else
456 statusDesc = tr( "Running" );
457 return tr( "%1<br>%2" ).arg( statusDesc, formattedTime );
458 }
460 return tr( "Complete" );
462 return tr( "Terminated" );
463 }
464 }
465 }
466 // no warnings
467 return QString();
468}
469
470
471//
472// QgsTaskStatusDelegate
473//
474
475QgsTaskStatusWidget::QgsTaskStatusWidget( QWidget *parent, QgsTask::TaskStatus status, bool canCancel )
476 : QWidget( parent )
477 , mCanCancel( canCancel )
478 , mStatus( status )
479{
480 setMouseTracking( true );
481}
482
483QSize QgsTaskStatusWidget::sizeHint() const
484{
485 return QSize( 32, 32 );
486}
487
488void QgsTaskStatusWidget::setStatus( int status )
489{
490 mStatus = static_cast<QgsTask::TaskStatus>( status );
491 update();
492}
493
494void QgsTaskStatusWidget::paintEvent( QPaintEvent *e )
495{
496 QWidget::paintEvent( e );
497
498 QIcon icon;
499 if ( mInside && ( mCanCancel || ( mStatus == QgsTask::Queued || mStatus == QgsTask::OnHold ) ) )
500 {
501 icon = QgsApplication::getThemeIcon( u"/mTaskCancel.svg"_s );
502 }
503 else
504 {
505 switch ( mStatus )
506 {
507 case QgsTask::Queued:
508 icon = QgsApplication::getThemeIcon( u"/mTaskQueued.svg"_s );
509 break;
510 case QgsTask::OnHold:
511 icon = QgsApplication::getThemeIcon( u"/mTaskOnHold.svg"_s );
512 break;
513 case QgsTask::Running:
514 icon = QgsApplication::getThemeIcon( u"/mTaskRunning.svg"_s );
515 break;
517 icon = QgsApplication::getThemeIcon( u"/mTaskComplete.svg"_s );
518 break;
520 icon = QgsApplication::getThemeIcon( u"/mTaskTerminated.svg"_s );
521 break;
522 }
523 }
524
525 QPainter p( this );
526 icon.paint( &p, 1, height() / 2 - 12, 24, 24 );
527 p.end();
528}
529
530void QgsTaskStatusWidget::mousePressEvent( QMouseEvent * )
531{
532 if ( mCanCancel || ( mStatus == QgsTask::Queued || mStatus == QgsTask::OnHold ) )
533 emit cancelClicked();
534}
535
536void QgsTaskStatusWidget::mouseMoveEvent( QMouseEvent * )
537{
538 if ( !mInside )
539 {
540 mInside = true;
541 update();
542 }
543}
544
545void QgsTaskStatusWidget::leaveEvent( QEvent * )
546{
547 mInside = false;
548 update();
549}
550
551
552/*
553bool QgsTaskStatusWidget::editorEvent( QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index )
554{
555 Q_UNUSED( option )
556 if ( event->type() == QEvent::MouseButtonPress )
557 {
558 QMouseEvent *e = static_cast<QMouseEvent*>( event );
559 if ( e->button() == Qt::LeftButton )
560 {
561 if ( !index.model()->flags( index ).testFlag( Qt::ItemIsEditable ) )
562 {
563 //item not editable
564 return false;
565 }
566
567 return model->setData( index, true, Qt::EditRole );
568 }
569 }
570 return false;
571}
572*/
573
574QgsTaskManagerFloatingWidget::QgsTaskManagerFloatingWidget( QgsTaskManager *manager, QWidget *parent )
575 : QgsFloatingWidget( parent )
576{
577 setLayout( new QVBoxLayout() );
578 QgsTaskManagerWidget *w = new QgsTaskManagerWidget( manager );
579
580 const int minWidth = static_cast<int>( fontMetrics().horizontalAdvance( 'X' ) * 60 * Qgis::UI_SCALE_FACTOR );
581 const int minHeight = static_cast<int>( fontMetrics().height() * 15 * Qgis::UI_SCALE_FACTOR );
582 setMinimumSize( minWidth, minHeight );
583 layout()->addWidget( w );
584 setStyleSheet( ".QgsTaskManagerFloatingWidget { border-top-left-radius: 8px;"
585 "border-top-right-radius: 8px; background-color: rgba(0, 0, 0, 70%); }" );
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:6523
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:6924