File indexing completed on 2024-05-12 04:38:18

0001 /*
0002     SPDX-FileCopyrightText: 2004 Till Adam <adam@kde.org>
0003     SPDX-FileCopyrightText: 2004 Don Sanders
0004     SPDX-FileCopyrightText: 2004 David Faure <faure@kde.org>
0005 
0006     Includes StatusbarProgressWidget which is based on KIOLittleProgressDlg
0007     SPDX-FileCopyrightText: Matt Koss <koss@miesto.sk>
0008 
0009     SPDX-License-Identifier: GPL-2.0-or-later
0010 */
0011 
0012 #include "statusbarprogresswidget.h"
0013 #include "progressdialog.h"
0014 #include "progressmanager.h"
0015 #ifdef Q_OS_OSX
0016 #include "../macdockprogressview.h"
0017 #endif
0018 
0019 #include <KLocalizedString>
0020 
0021 #include <QEvent>
0022 #include <QFrame>
0023 #include <QHBoxLayout>
0024 #include <QLabel>
0025 #include <QMouseEvent>
0026 #include <QProgressBar>
0027 #include <QPushButton>
0028 #include <QStackedWidget>
0029 #include <QTimer>
0030 #include <QToolButton>
0031 #include <QApplication>
0032 #include <QStyle>
0033 
0034 using namespace KDevelop;
0035 
0036 //-----------------------------------------------------------------------------
0037 StatusbarProgressWidget::StatusbarProgressWidget( ProgressDialog* progressDialog, QWidget* parent, bool button )
0038     : QFrame( parent ), mCurrentItem( nullptr ), mProgressDialog( progressDialog ),
0039       mDelayTimer( nullptr ), mCleanTimer( nullptr )
0040 {
0041     m_bShowButton = button;
0042     int w = fontMetrics().horizontalAdvance(QStringLiteral(" 999.9 kB/s 00:00:01 ")) + 8;
0043     box = new QHBoxLayout( this );
0044     box->setContentsMargins(0, 0, 0, 0);
0045     box->setSpacing(0);
0046     stack = new QStackedWidget( this );
0047 
0048     m_pButton = new QToolButton( this );
0049     m_pButton->setSizePolicy( QSizePolicy( QSizePolicy::Fixed,
0050                                            QSizePolicy::Fixed ) );
0051     QIcon smallIcon = QIcon::fromTheme( QStringLiteral("go-up") );
0052     if ( smallIcon.isNull() ) {
0053         // this can happen everywhere but in particular with a standard build on OS X.
0054         // QToolButtons won't be visible without an icon, so fall back to showing a Qt::UpArrow.
0055         m_pButton->setArrowType( Qt::UpArrow );
0056     } else {
0057         m_pButton->setIcon( smallIcon );
0058     }
0059     m_pButton->setAutoRaise(true);
0060     QSize iconSize = m_pButton->iconSize();
0061 
0062     m_pProgressBar = new QProgressBar( this );
0063     m_pProgressBar->setSizePolicy( QSizePolicy( QSizePolicy::Fixed,
0064                                                 QSizePolicy::Fixed ) );
0065     m_pProgressBar->installEventFilter( this );
0066     m_pProgressBar->setMinimumWidth( w );
0067     m_pProgressBar->setAttribute( Qt::WA_LayoutUsesWidgetRect, true );
0068 
0069     // Determine maximumHeight from the progressbar's height and scale the icon.
0070     // This operation is style specific and cannot infer the style in use
0071     // from Q_OS_??? because users can have started us using the -style option
0072     // (or even be using an unexpected QPA).
0073     // In most cases, maximumHeight should be set to fontMetrics().height() + 2
0074     // (Breeze, Oxygen, Fusion, Windows, QtCurve etc.); this corresponds to the actual
0075     // progressbar height plus a 1 pixel margin above and below.
0076     int maximumHeight = m_pProgressBar->fontMetrics().height() + 2;
0077     const bool isMacWidgetStyle = QApplication::style()->objectName() == QLatin1String( "macintosh" );
0078 
0079     if ( isMacWidgetStyle && !smallIcon.isNull() ) {
0080         // QProgressBar height is fixed with the macintosh native widget style
0081         // and alignment with m_pButton is tricky. Sizing the icon to maximumHeight
0082         // gives a button that is slightly too high and not perfectly
0083         // aligned. Annoyingly that doesn't improve by calling setMaximumHeight()
0084         // which even causes the button to change shape. So we use a "flat" button,
0085         // an invisible outline which is more in line with platform practices anyway.
0086         maximumHeight = m_pProgressBar->sizeHint().height();
0087         iconSize.scale( maximumHeight, maximumHeight, Qt::KeepAspectRatio );
0088     } else {
0089         // The icon is scaled to maximumHeight but with 1 pixel margins on each side
0090         // because it will be in a visible button.
0091         iconSize.scale( maximumHeight - 2, maximumHeight - 2, Qt::KeepAspectRatio );
0092         // additional adjustments:
0093         m_pButton->setAttribute( Qt::WA_LayoutUsesWidgetRect, true );
0094     }
0095     stack->setMaximumHeight( maximumHeight );
0096     m_pButton->setIconSize( iconSize );
0097     box->addWidget( m_pButton  );
0098 
0099     m_pButton->setToolTip( i18nc("@info:tooltip", "Open detailed progress dialog") );
0100 
0101     box->addWidget( stack );
0102 
0103     stack->insertWidget( 1, m_pProgressBar );
0104 
0105     if (m_bShowButton) {
0106         // create an empty, inactive QToolButton that's as high as m_pButton but only 1 pixel wide
0107         // this will act as a placeholder when the widget is invisible.
0108         m_pPlaceHolder.button = new QToolButton(this);
0109         m_pPlaceHolder.button->setSizePolicy( QSizePolicy( QSizePolicy::Fixed,
0110                                            QSizePolicy::Fixed ) );
0111         m_pPlaceHolder.button->setMinimumHeight(m_pButton->minimumSizeHint().height());
0112         m_pPlaceHolder.button->setMaximumWidth(1);
0113         m_pPlaceHolder.button->setAutoRaise(true);
0114         m_pPlaceHolder.button->setAttribute( Qt::WA_LayoutUsesWidgetRect, true );
0115         m_pPlaceHolder.button->setEnabled(false);
0116         m_pPlaceHolder.button->installEventFilter( this );
0117         // the placeholder button should not go into the stack to avoid misalignment
0118         box->addWidget( m_pPlaceHolder.button );
0119         m_pPlaceHolder.button->hide();
0120     } else {
0121         // when the widget doesn't show m_pButton we can use a QLabel as the placeholder.
0122         m_pPlaceHolder.label = new QLabel( QString(), this );
0123         m_pPlaceHolder.label->setSizePolicy( QSizePolicy( QSizePolicy::Fixed,
0124                                               QSizePolicy::Fixed ) );
0125         m_pPlaceHolder.label->setAlignment( Qt::AlignHCenter );
0126         m_pPlaceHolder.label->installEventFilter( this );
0127         m_pPlaceHolder.label->setMinimumWidth( w );
0128         m_pPlaceHolder.label->setMaximumHeight( maximumHeight );
0129         stack->insertWidget( 2, m_pPlaceHolder.label );
0130     }
0131 
0132     setMinimumWidth( minimumSizeHint().width() );
0133 
0134     mode = None;
0135     setMode();
0136 
0137     connect( m_pButton, &QPushButton::clicked,
0138              progressDialog, &ProgressDialog::slotToggleVisibility );
0139 
0140     connect ( ProgressManager::instance(), &ProgressManager::progressItemAdded,
0141               this, &StatusbarProgressWidget::slotProgressItemAdded );
0142     connect ( ProgressManager::instance(), &ProgressManager::progressItemCompleted,
0143               this, &StatusbarProgressWidget::slotProgressItemCompleted );
0144     connect ( ProgressManager::instance(), &ProgressManager::progressItemUsesBusyIndicator,
0145               this, &StatusbarProgressWidget::updateBusyMode );
0146 
0147     connect ( progressDialog, &ProgressDialog::visibilityChanged,
0148               this, &StatusbarProgressWidget::slotProgressDialogVisible );
0149 
0150     mDelayTimer = new QTimer( this );
0151     mDelayTimer->setSingleShot( true );
0152     mDelayTimer->setInterval(1000);
0153     connect ( mDelayTimer, &QTimer::timeout,
0154               this, &StatusbarProgressWidget::slotShowItemDelayed );
0155 
0156     mCleanTimer = new QTimer( this );
0157     mCleanTimer->setSingleShot( true );
0158     mCleanTimer->setInterval(5000);
0159     connect ( mCleanTimer, &QTimer::timeout,
0160               this, &StatusbarProgressWidget::slotClean );
0161 }
0162 
0163 // There are three cases: no progressitem, one progressitem (connect to it directly),
0164 // or many progressitems (display busy indicator). Let's call them 0,1,N.
0165 // In slot..Added we can only end up in 1 or N.
0166 // In slot..Removed we can end up in 0, 1, or we can stay in N if we were already.
0167 
0168 void StatusbarProgressWidget::updateBusyMode()
0169 {
0170     connectSingleItem(); // if going to 1 item
0171     if (!mDelayTimer->isActive())
0172         mDelayTimer->start();
0173 }
0174 
0175 void StatusbarProgressWidget::slotProgressItemAdded( ProgressItem *item )
0176 {
0177     if ( item->parent() )
0178         return; // we are only interested in top level items
0179 
0180     updateBusyMode();
0181 }
0182 
0183 void StatusbarProgressWidget::slotProgressItemCompleted( ProgressItem *item )
0184 {
0185     if ( item->parent() ) {
0186         item->deleteLater();
0187         item = nullptr;
0188         return; // we are only interested in top level items
0189     }
0190 
0191     item->deleteLater();
0192     item = nullptr;
0193 
0194     connectSingleItem(); // if going back to 1 item
0195     if ( ProgressManager::instance()->isEmpty() ) { // No item
0196         // If the progress manager doesn't have other progress items, set the progress to 100%
0197         // to indicate completion. Otherwise, if @p item uses a busy indicator, or had been running
0198         // for less than a second and was preceded by a different item that uses a busy indicator,
0199         // we could be showing a busy indicator for 5 seconds without any task in progress.
0200         activateSingleItemMode( 100 );
0201         // Done. In 5s the progress-widget will close, then we can clean up the statusbar
0202         mCleanTimer->start();
0203     } else if ( mCurrentItem ) { // Exactly one item
0204         activateSingleItemMode();
0205     }
0206 }
0207 
0208 void StatusbarProgressWidget::connectSingleItem()
0209 {
0210     auto* const singleItem = ProgressManager::instance()->singleItem();
0211     if ( singleItem == mCurrentItem ) {
0212         return; // No need to waste time reconnecting the same signal/slot pair.
0213     }
0214 
0215     if ( mCurrentItem ) {
0216         disconnect ( mCurrentItem, &ProgressItem::progressItemProgress,
0217                      this, &StatusbarProgressWidget::slotProgressItemProgress );
0218         mCurrentItem = nullptr;
0219     }
0220     mCurrentItem = singleItem;
0221     if ( mCurrentItem ) {
0222         connect ( mCurrentItem, &ProgressItem::progressItemProgress,
0223                   this, &StatusbarProgressWidget::slotProgressItemProgress );
0224     }
0225 }
0226 
0227 void StatusbarProgressWidget::activateSingleItemMode()
0228 {
0229     activateSingleItemMode( mCurrentItem->progress() );
0230 }
0231 
0232 void StatusbarProgressWidget::activateSingleItemMode( unsigned int progress )
0233 {
0234     m_pProgressBar->setMaximum( 100 );
0235     m_pProgressBar->setValue( progress );
0236     m_pProgressBar->setTextVisible( true );
0237 #ifdef Q_OS_OSX
0238     MacDockProgressView::setRange( 0, 100 );
0239     MacDockProgressView::setProgress( progress );
0240 #endif
0241 }
0242 
0243 void StatusbarProgressWidget::slotShowItemDelayed()
0244 {
0245     bool noItems = ProgressManager::instance()->isEmpty();
0246     if ( mCurrentItem ) {
0247         activateSingleItemMode();
0248     } else if ( !noItems ) { // N items
0249         m_pProgressBar->setMaximum( 0 );
0250         m_pProgressBar->setTextVisible( false );
0251 #ifdef Q_OS_OSX
0252         MacDockProgressView::setRange( 0, 0 );
0253         MacDockProgressView::setProgress( 0 );
0254 #endif
0255     }
0256 
0257     if ( !noItems && mode == None ) {
0258         mode = Progress;
0259         setMode();
0260     }
0261 }
0262 
0263 void StatusbarProgressWidget::slotProgressItemProgress( ProgressItem *item, unsigned int value )
0264 {
0265     Q_ASSERT( item == mCurrentItem); // the only one we should be connected to
0266     Q_UNUSED( item );
0267     m_pProgressBar->setValue( value );
0268 #ifdef Q_OS_OSX
0269     MacDockProgressView::setProgress( value );
0270 #endif
0271 }
0272 
0273 void StatusbarProgressWidget::setMode() {
0274     switch ( mode ) {
0275     case None:
0276         m_pButton->hide();
0277         if ( m_bShowButton ) {
0278             // show the empty button in order to make the status bar look better
0279             m_pPlaceHolder.button->show();
0280         } else {
0281             // show the empty label in order to make the status bar look better
0282             stack->setCurrentWidget( m_pPlaceHolder.label );
0283         }
0284         m_pProgressBar->hide();
0285         stack->show();
0286 #ifdef Q_OS_OSX
0287         MacDockProgressView::setProgressVisible( false );
0288 #endif
0289         break;
0290 
0291     case Progress:
0292         stack->show();
0293         m_pProgressBar->show();
0294         stack->setCurrentWidget( m_pProgressBar );
0295         if ( m_bShowButton ) {
0296             m_pButton->show();
0297             m_pPlaceHolder.button->hide();
0298         }
0299 #ifdef Q_OS_OSX
0300         MacDockProgressView::setProgressVisible( true );
0301 #endif
0302         break;
0303     }
0304 }
0305 
0306 void StatusbarProgressWidget::slotClean()
0307 {
0308     // check if a new item showed up since we started the timer. If not, clear
0309     if ( ProgressManager::instance()->isEmpty() ) {
0310         m_pProgressBar->setValue( 0 );
0311         //m_pPlaceHolder.label->clear();
0312         mode = None;
0313         setMode();
0314     }
0315 }
0316 
0317 bool StatusbarProgressWidget::eventFilter(QObject* object, QEvent* ev)
0318 {
0319     if ( ev->type() == QEvent::MouseButtonPress ) {
0320         auto* e = static_cast<QMouseEvent*>(ev);
0321 
0322         if ( e->button() == Qt::LeftButton && mode != None ) {    // toggle view on left mouse button
0323             // Consensus seems to be that we should show/hide the fancy dialog when the user
0324             // clicks anywhere in the small one.
0325             mProgressDialog->slotToggleVisibility();
0326             return true;
0327         }
0328     }
0329     return QFrame::eventFilter(object, ev);
0330 }
0331 
0332 void StatusbarProgressWidget::slotProgressDialogVisible( bool b )
0333 {
0334     // Update the hide/show button when the detailed one is shown/hidden
0335     if ( b ) {
0336         m_pButton->setIcon( QIcon::fromTheme( QStringLiteral("go-down") ) );
0337         m_pButton->setToolTip( i18nc("@info:tooltip", "Hide detailed progress window") );
0338         setMode();
0339     } else {
0340         m_pButton->setIcon( QIcon::fromTheme( QStringLiteral("go-up") ) );
0341         m_pButton->setToolTip( i18nc("@info:tooltip", "Show detailed progress window") );
0342     }
0343 }
0344 
0345 #include "moc_statusbarprogresswidget.cpp"