1 /* Copyright (C) 2013 Thomas Lübking <thomas.luebking@gmail.com>
3 This file is part of the Trojita Qt IMAP e-mail client,
4 http://trojita.flaska.net/
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License as
8 published by the Free Software Foundation; either version 2 of
9 the License or (at your option) version 3 or any later version
10 accepted by the membership of KDE e.V. (or its successor approved
11 by the membership of KDE e.V.), which shall act as a proxy
12 defined in Section 14 of version 3 of the license.
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 GNU General Public License for more details.
19 You should have received a copy of the GNU General Public License
20 along with this program. If not, see <http://www.gnu.org/licenses/>.
24 #include "Common/InvokeMethod.h"
25 #include "UiUtils/Color.h"
27 #include <QFontMetricsF>
30 #include <QTimerEvent>
36 Spinner::Spinner(QWidget
*parent
) : QWidget(parent
), m_step(0), m_fadeStep(0), m_timer(0),
37 m_startTimer(0), m_textCols(0), m_type(Sun
),
38 m_geometryDirty(false), m_context(Overlay
)
44 void Spinner::setText(const QString
&text
)
46 static const QLatin1Char
newLine('\n');
48 // calculate the maximum glyphs per row
49 // this is later on used in the painting code to determine the font size
50 // size altering pointsizes of fonts does not scale dimensions in a linear way (depending on the
51 // hinter) this is precise enough and by using the maximum glyph width has enough padding from
53 int idx
= text
.indexOf(newLine
);
57 m_textCols
= qMax(m_textCols
, idx
- lidx
);
59 idx
= text
.indexOf(newLine
, lidx
);
61 m_textCols
= qMax(m_textCols
, text
.length() - lidx
);
63 if (m_context
== Throbber
)
67 QString
Spinner::text() const
72 void Spinner::setContext(const Context c
)
75 if (m_context
== Throbber
) {
79 setToolTip(QString());
81 updateAncestors(); // Throbbers don't resize with their parents etc.
84 Spinner::Context
Spinner::context() const
89 void Spinner::setType(const Type t
)
94 Spinner::Type
Spinner::type() const
99 void Spinner::start(uint delay
)
101 if (m_timer
) { // already running...
102 m_fadeStep
= qAbs(m_fadeStep
);
108 m_startTimer
= new QTimer(this);
109 m_startTimer
->setSingleShot(true);
110 connect(m_startTimer
, &QTimer::timeout
, this, static_cast<void (Spinner::*)()>(&Spinner::start
));
112 if (m_startTimer
->remainingTime() > -1) // preserve oldest request original delay
113 delay
= m_startTimer
->remainingTime();
114 m_startTimer
->start(delay
);
119 m_startTimer
->stop();
124 m_timer
= startTimer(100);
127 /** @short Forwarder to solve Qt5's new signal-slot ambiguity wrt QPrivateSlot */
128 void Spinner::start()
136 m_startTimer
->stop();
137 m_fadeStep
= qMax(-11, qMin(-1, -qAbs(m_fadeStep
))); // [-11,-1]
140 bool Spinner::event(QEvent
*e
)
142 if (e
->type() == QEvent::Show
&& m_geometryDirty
) {
144 } else if (e
->type() == QEvent::ParentChange
) {
147 return QWidget::event(e
);
150 bool Spinner::eventFilter(QObject
*o
, QEvent
*e
)
152 if (e
->type() == QEvent::Resize
|| e
->type() == QEvent::Move
) {
153 if (!m_geometryDirty
&& isVisible()) {
154 CALL_LATER_NOARG(this, updateGeometry
);
156 m_geometryDirty
= true;
157 } else if (e
->type() == QEvent::ChildAdded
|| e
->type() == QEvent::ZOrderChange
) {
158 if (o
== parentWidget())
160 } else if (e
->type() == QEvent::ParentChange
) {
166 void Spinner::paintEvent(QPaintEvent
*)
169 return; // w/o animation, we're just a spacer or hidden anyway.
171 QColor
c1(palette().color(backgroundRole())),
172 c2(palette().color(foregroundRole()));
174 const int a
= c1
.alpha();
175 if (m_context
== Overlay
) {
176 c1
.setAlpha(170); // 2/3
177 c2
= UiUtils::tintColor(c2
, c1
);
179 c2
.setAlpha(qAbs(m_fadeStep
)*a
/18);
181 int startAngle(16*90), span(360*16); // full circle starting at 12 o'clock
182 int strokeSize
, segments(0); // segments need to match painting steps "12" -> "2,3,4,6,12,24 ..."
183 qreal
segmentRatio(0.5);
184 QPen
pen(Qt::SolidLine
);
185 pen
.setCapStyle(Qt::RoundCap
);
191 pen
.setCapStyle(Qt::FlatCap
);
192 strokeSize
= qMax(1,width()/8);
193 startAngle
-= 5*16*m_step
;
197 strokeSize
= qMax(1,width()/16);
198 startAngle
-= 10*16*m_step
;
202 pen
.setCapStyle(Qt::FlatCap
);
203 strokeSize
= qMax(1,width()/4);
209 strokeSize
= qMax(1,width()/16);
211 startAngle
-= 40*step
*step
;
213 const int endAngle
= 16*90 - 40*step
*step
;
214 span
= (endAngle
- startAngle
);
216 span
= 360*16+span
; // fix direction.
221 pen
.setWidth(strokeSize
);
223 const int radius
= width() - 2*(strokeSize
/2 + 1);
224 qreal d
= (M_PI
*radius
)/(segments
*strokeSize
);
225 pen
.setDashPattern(QVector
<qreal
>() << d
*segmentRatio
<< d
*(1.0-segmentRatio
));
229 p
.setRenderHint(QPainter::Antialiasing
);
230 p
.setBrush(Qt::NoBrush
);
234 r
.adjust(strokeSize
/2+1, strokeSize
/2+1, -(strokeSize
/2+1), -(strokeSize
/2+1));
235 p
.drawArc(r
, startAngle
, span
);
238 QColor
c3(palette().color(foregroundRole()));
239 c3
.setAlpha(c2
.alpha());
241 startAngle
-= 30*16*m_step
;
244 p
.drawArc(r
, startAngle
, 30*16);
246 for (int i
= 2; i
> 0; --i
) {
248 const int a
= c3
.alpha();
249 c2
.setAlpha(255/(i
+1));
250 c3
= UiUtils::tintColor(c3
, c2
);
254 p
.drawArc(r
, startAngle
, 30*16);
259 if (m_context
== Overlay
&& !m_text
.isEmpty()) {
261 if (fnt
.pointSize() > -1) {
263 fnt
.setPointSizeF((fnt
.pointSizeF() * r
.width()) / (m_textCols
*QFontMetricsF(fnt
).maxWidth()));
266 // cheap "outline" for better readability
267 // this works "good enough" on sublying distorsion (aka. text) but looks crap if the background
268 // is really colored differently from backgroundRole() -> QPainterPath + stroke
271 p
.drawText(r
, Qt::AlignCenter
|Qt::TextDontClip
, m_text
);
273 p
.drawText(r
, Qt::AlignCenter
|Qt::TextDontClip
, m_text
);
275 // actual text painting
276 c2
= QColor(palette().color(foregroundRole()));
277 c2
.setAlpha(qAbs(m_fadeStep
)*c2
.alpha()/12);
279 p
.drawText(r
, Qt::AlignCenter
|Qt::TextDontClip
, m_text
);
284 void Spinner::timerEvent(QTimerEvent
*e
)
286 // timerEvent being used for being more lightweight than QTimer - no particular other reason
287 if (e
->timerId() == m_timer
) {
290 if (m_fadeStep
== -1) { // stop
291 if (m_context
== Overlay
)
301 QWidget::timerEvent(e
);
305 void Spinner::updateAncestors()
307 foreach (QWidget
*w
, m_ancestors
)
308 w
->removeEventFilter(this);
312 if (m_context
== Overlay
) {
314 while ((w
= w
->parentWidget())) {
316 w
->installEventFilter(this);
317 connect(w
, &QObject::destroyed
, this, &Spinner::updateAncestors
);
323 void Spinner::updateGeometry()
326 m_geometryDirty
= true;
329 if (m_ancestors
.isEmpty())
330 return; // valid for Throbbers
331 QRect
visibleRect(m_ancestors
.last()->rect());
333 for (int i
= m_ancestors
.count() - 2; i
> -1; --i
) {
334 visibleRect
&= m_ancestors
.at(i
)->geometry().translated(offset
);
335 offset
+= m_ancestors
.at(i
)->geometry().topLeft();
337 visibleRect
.translate(-offset
);
338 const int size
= 2*qMin(visibleRect
.width(), visibleRect
.height())/3;
339 QRect
r(0, 0, size
, size
);
340 r
.moveCenter(visibleRect
.center());
342 m_geometryDirty
= false;