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 }