File indexing completed on 2024-06-16 05:01:31

0001 /* Copyright (C) 2013 Thomas Lübking <thomas.luebking@gmail.com>
0002 
0003    This file is part of the Trojita Qt IMAP e-mail client,
0004    http://trojita.flaska.net/
0005 
0006    This program is free software; you can redistribute it and/or
0007    modify it under the terms of the GNU General Public License as
0008    published by the Free Software Foundation; either version 2 of
0009    the License or (at your option) version 3 or any later version
0010    accepted by the membership of KDE e.V. (or its successor approved
0011    by the membership of KDE e.V.), which shall act as a proxy
0012    defined in Section 14 of version 3 of the license.
0013 
0014    This program is distributed in the hope that it will be useful,
0015    but WITHOUT ANY WARRANTY; without even the implied warranty of
0016    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0017    GNU General Public License for more details.
0018 
0019    You should have received a copy of the GNU General Public License
0020    along with this program.  If not, see <http://www.gnu.org/licenses/>.
0021 */
0022 
0023 #include "Spinner.h"
0024 #include "Common/InvokeMethod.h"
0025 #include "UiUtils/Color.h"
0026 
0027 #include <QFontMetricsF>
0028 #include <QPainter>
0029 #include <QTimer>
0030 #include <QTimerEvent>
0031 #include <qmath.h>
0032 #include <QtDebug>
0033 
0034 using namespace Gui;
0035 
0036 Spinner::Spinner(QWidget *parent) : QWidget(parent), m_step(0), m_fadeStep(0), m_timer(0),
0037                                     m_startTimer(0), m_textCols(0), m_type(Sun),
0038                                     m_geometryDirty(false), m_context(Overlay)
0039 {
0040     updateAncestors();
0041     hide();
0042 }
0043 
0044 void Spinner::setText(const QString &text)
0045 {
0046     static const QLatin1Char newLine('\n');
0047     m_text = text;
0048     // calculate the maximum glyphs per row
0049     // this is later on used in the painting code to determine the font size
0050     // size altering pointsizes of fonts does not scale dimensions in a linear way (depending on the
0051     // hinter) this is precise enough and by using the maximum glyph width has enough padding from
0052     // the circle
0053     int idx = text.indexOf(newLine);
0054     int lidx = 0;
0055     m_textCols = 0;
0056     while (idx > -1) {
0057         m_textCols = qMax(m_textCols, idx - lidx);
0058         lidx = idx + 1;
0059         idx = text.indexOf(newLine, lidx);
0060     }
0061     m_textCols = qMax(m_textCols, text.length() - lidx);
0062 
0063     if (m_context == Throbber)
0064         setToolTip(text);
0065 }
0066 
0067 QString Spinner::text() const
0068 {
0069     return m_text;
0070 }
0071 
0072 void Spinner::setContext(const Context c)
0073 {
0074     m_context = c;
0075     if (m_context == Throbber) {
0076         setToolTip(m_text);
0077         show();
0078     } else {
0079         setToolTip(QString());
0080     }
0081     updateAncestors(); // Throbbers don't resize with their parents etc.
0082 }
0083 
0084 Spinner::Context Spinner::context() const
0085 {
0086     return m_context;
0087 }
0088 
0089 void Spinner::setType(const Type t)
0090 {
0091     m_type = t;
0092 }
0093 
0094 Spinner::Type Spinner::type() const
0095 {
0096     return m_type;
0097 }
0098 
0099 void Spinner::start(uint delay)
0100 {
0101     if (m_timer) { // already running...
0102         m_fadeStep = qAbs(m_fadeStep);
0103         return;
0104     }
0105 
0106     if (delay) {
0107         if (!m_startTimer) {
0108             m_startTimer = new QTimer(this);
0109             m_startTimer->setSingleShot(true);
0110             connect(m_startTimer, &QTimer::timeout, this, static_cast<void (Spinner::*)()>(&Spinner::start));
0111         }
0112         if (m_startTimer->remainingTime() > -1) // preserve oldest request original delay
0113             delay = m_startTimer->remainingTime();
0114         m_startTimer->start(delay);
0115         return;
0116     }
0117 
0118     if (m_startTimer)
0119         m_startTimer->stop();
0120     m_step = 0;
0121     m_fadeStep = 0;
0122     show();
0123     raise();
0124     m_timer = startTimer(100);
0125 }
0126 
0127 /** @short Forwarder to solve Qt5's new signal-slot ambiguity wrt QPrivateSlot */
0128 void Spinner::start()
0129 {
0130     start(0);
0131 }
0132 
0133 void Spinner::stop()
0134 {
0135     if (m_startTimer)
0136         m_startTimer->stop();
0137     m_fadeStep = qMax(-11, qMin(-1, -qAbs(m_fadeStep))); // [-11,-1]
0138 }
0139 
0140 bool Spinner::event(QEvent *e)
0141 {
0142     if (e->type() == QEvent::Show && m_geometryDirty) {
0143         updateGeometry();
0144     } else if (e->type() == QEvent::ParentChange) {
0145         updateAncestors();
0146     }
0147     return QWidget::event(e);
0148 }
0149 
0150 bool Spinner::eventFilter(QObject *o, QEvent *e)
0151 {
0152     if (e->type() == QEvent::Resize || e->type() == QEvent::Move) {
0153         if (!m_geometryDirty && isVisible()) {
0154             CALL_LATER_NOARG(this, updateGeometry);
0155         }
0156         m_geometryDirty = true;
0157     } else if (e->type() == QEvent::ChildAdded || e->type() == QEvent::ZOrderChange) {
0158         if (o == parentWidget())
0159             raise();
0160     } else if (e->type() == QEvent::ParentChange) {
0161         updateAncestors();
0162     }
0163     return false;
0164 }
0165 
0166 void Spinner::paintEvent(QPaintEvent *)
0167 {
0168     if (!m_timer)
0169         return; // w/o animation, we're just a spacer or hidden anyway.
0170 
0171     QColor c1(palette().color(backgroundRole())),
0172            c2(palette().color(foregroundRole()));
0173 
0174     const int a = c1.alpha();
0175     if (m_context == Overlay) {
0176         c1.setAlpha(170); // 2/3
0177         c2 = UiUtils::tintColor(c2, c1);
0178     }
0179     c2.setAlpha(qAbs(m_fadeStep)*a/18);
0180 
0181     int startAngle(16*90), span(360*16); // full circle starting at 12 o'clock
0182     int strokeSize, segments(0); // segments need to match painting steps "12" -> "2,3,4,6,12,24 ..."
0183     qreal segmentRatio(0.5);
0184     QPen pen(Qt::SolidLine);
0185     pen.setCapStyle(Qt::RoundCap);
0186     pen.setColor(c2);
0187 
0188     switch (m_type) {
0189         case Aperture:
0190         default:
0191             pen.setCapStyle(Qt::FlatCap);
0192             strokeSize = qMax(1,width()/8);
0193             startAngle -= 5*16*m_step;
0194             segments = 6;
0195             break;
0196         case Scythe:
0197             strokeSize = qMax(1,width()/16);
0198             startAngle -= 10*16*m_step;
0199             segments = 3;
0200             break;
0201         case Sun: {
0202             pen.setCapStyle(Qt::FlatCap);
0203             strokeSize = qMax(1,width()/4);
0204             segments = 12;
0205             segmentRatio = 0.8;
0206             break;
0207         }
0208         case Elastic: {
0209             strokeSize = qMax(1,width()/16);
0210             int step = m_step;
0211             startAngle -= 40*step*step;
0212             step = (step+9)%12;
0213             const int endAngle = 16*90 - 40*step*step;
0214             span = (endAngle - startAngle);
0215             if (span < 0)
0216                 span = 360*16+span; // fix direction.
0217             break;
0218         }
0219     }
0220 
0221     pen.setWidth(strokeSize);
0222     if (segments) {
0223         const int radius = width() - 2*(strokeSize/2 + 1);
0224         qreal d = (M_PI*radius)/(segments*strokeSize);
0225         pen.setDashPattern(QVector<qreal>() << d*segmentRatio << d*(1.0-segmentRatio));
0226     }
0227 
0228     QPainter p(this);
0229     p.setRenderHint(QPainter::Antialiasing);
0230     p.setBrush(Qt::NoBrush);
0231     p.setPen(pen);
0232 
0233     QRect r(rect());
0234     r.adjust(strokeSize/2+1, strokeSize/2+1, -(strokeSize/2+1), -(strokeSize/2+1));
0235     p.drawArc(r, startAngle, span);
0236 
0237     if (m_type == Sun) {
0238         QColor c3(palette().color(foregroundRole()));
0239         c3.setAlpha(c2.alpha());
0240 
0241         startAngle -= 30*16*m_step;
0242         pen.setColor(c3);
0243         p.setPen(pen);
0244         p.drawArc(r, startAngle, 30*16);
0245 
0246         for (int i = 2; i > 0; --i) {
0247             startAngle += 30*16;
0248             const int a = c3.alpha();
0249             c2.setAlpha(255/(i+1));
0250             c3 = UiUtils::tintColor(c3, c2);
0251             c3.setAlpha(a);
0252             pen.setColor(c3);
0253             p.setPen(pen);
0254             p.drawArc(r, startAngle, 30*16);
0255         }
0256 
0257     }
0258 
0259     if (m_context == Overlay && !m_text.isEmpty()) {
0260         QFont fnt;
0261         if (fnt.pointSize() > -1) {
0262             fnt.setBold(true);
0263             fnt.setPointSizeF((fnt.pointSizeF() * r.width()) / (m_textCols*QFontMetricsF(fnt).maxWidth()));
0264             p.setFont(fnt);
0265         }
0266         // cheap "outline" for better readability
0267         // this works "good enough" on sublying distorsion (aka. text) but looks crap if the background
0268         // is really colored differently from backgroundRole() -> QPainterPath + stroke
0269         p.setPen(c1);
0270         r.translate(-1,-1);
0271         p.drawText(r, Qt::AlignCenter|Qt::TextDontClip, m_text);
0272         r.translate(2,2);
0273         p.drawText(r, Qt::AlignCenter|Qt::TextDontClip, m_text);
0274         r.translate(-1,-1);
0275         // actual text painting
0276         c2 = QColor(palette().color(foregroundRole()));
0277         c2.setAlpha(qAbs(m_fadeStep)*c2.alpha()/12);
0278         p.setPen(c2);
0279         p.drawText(r, Qt::AlignCenter|Qt::TextDontClip, m_text);
0280     }
0281     p.end();
0282 }
0283 
0284 void Spinner::timerEvent(QTimerEvent *e)
0285 {
0286     // timerEvent being used for being more lightweight than QTimer - no particular other reason
0287     if (e->timerId() == m_timer) {
0288         if (++m_step > 11)
0289             m_step = 0;
0290         if (m_fadeStep == -1) { // stop
0291             if (m_context == Overlay)
0292                 hide();
0293             killTimer(m_timer);
0294             m_timer = 0;
0295             m_step = 0;
0296         }
0297         if (m_fadeStep < 12)
0298             ++m_fadeStep;
0299         repaint();
0300     } else {
0301         QWidget::timerEvent(e);
0302     }
0303 }
0304 
0305 void Spinner::updateAncestors()
0306 {
0307     foreach (QWidget *w, m_ancestors)
0308         w->removeEventFilter(this);
0309 
0310     m_ancestors.clear();
0311 
0312     if (m_context == Overlay) {
0313         QWidget *w = this;
0314         while ((w = w->parentWidget())) {
0315             m_ancestors << w;
0316             w->installEventFilter(this);
0317             connect(w, &QObject::destroyed, this, &Spinner::updateAncestors);
0318         }
0319         updateGeometry();
0320     }
0321 }
0322 
0323 void Spinner::updateGeometry()
0324 {
0325     if (!isVisible()) {
0326         m_geometryDirty = true;
0327         return;
0328     }
0329     if (m_ancestors.isEmpty())
0330         return; // valid for Throbbers
0331     QRect visibleRect(m_ancestors.last()->rect());
0332     QPoint offset;
0333     for (int i = m_ancestors.count() - 2; i > -1; --i) {
0334         visibleRect &= m_ancestors.at(i)->geometry().translated(offset);
0335         offset += m_ancestors.at(i)->geometry().topLeft();
0336     }
0337     visibleRect.translate(-offset);
0338     const int size = 2*qMin(visibleRect.width(), visibleRect.height())/3;
0339     QRect r(0, 0, size, size);
0340     r.moveCenter(visibleRect.center());
0341     setGeometry(r);
0342     m_geometryDirty = false;
0343 }