Merge "persistent color scheme selection"
[trojita.git] / src / Gui / Spinner.cpp
blob73a1f76e699010d2f14a12b397aef65ac5cd94c7
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/>.
23 #include "Spinner.h"
24 #include "Common/InvokeMethod.h"
25 #include "UiUtils/Color.h"
27 #include <QFontMetricsF>
28 #include <QPainter>
29 #include <QTimer>
30 #include <QTimerEvent>
31 #include <qmath.h>
32 #include <QtDebug>
34 using namespace Gui;
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)
40 updateAncestors();
41 hide();
44 void Spinner::setText(const QString &text)
46 static const QLatin1Char newLine('\n');
47 m_text = text;
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
52 // the circle
53 int idx = text.indexOf(newLine);
54 int lidx = 0;
55 m_textCols = 0;
56 while (idx > -1) {
57 m_textCols = qMax(m_textCols, idx - lidx);
58 lidx = idx + 1;
59 idx = text.indexOf(newLine, lidx);
61 m_textCols = qMax(m_textCols, text.length() - lidx);
63 if (m_context == Throbber)
64 setToolTip(text);
67 QString Spinner::text() const
69 return m_text;
72 void Spinner::setContext(const Context c)
74 m_context = c;
75 if (m_context == Throbber) {
76 setToolTip(m_text);
77 show();
78 } else {
79 setToolTip(QString());
81 updateAncestors(); // Throbbers don't resize with their parents etc.
84 Spinner::Context Spinner::context() const
86 return m_context;
89 void Spinner::setType(const Type t)
91 m_type = t;
94 Spinner::Type Spinner::type() const
96 return m_type;
99 void Spinner::start(uint delay)
101 if (m_timer) { // already running...
102 m_fadeStep = qAbs(m_fadeStep);
103 return;
106 if (delay) {
107 if (!m_startTimer) {
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);
115 return;
118 if (m_startTimer)
119 m_startTimer->stop();
120 m_step = 0;
121 m_fadeStep = 0;
122 show();
123 raise();
124 m_timer = startTimer(100);
127 /** @short Forwarder to solve Qt5's new signal-slot ambiguity wrt QPrivateSlot */
128 void Spinner::start()
130 start(0);
133 void Spinner::stop()
135 if (m_startTimer)
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) {
143 updateGeometry();
144 } else if (e->type() == QEvent::ParentChange) {
145 updateAncestors();
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())
159 raise();
160 } else if (e->type() == QEvent::ParentChange) {
161 updateAncestors();
163 return false;
166 void Spinner::paintEvent(QPaintEvent *)
168 if (!m_timer)
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);
186 pen.setColor(c2);
188 switch (m_type) {
189 case Aperture:
190 default:
191 pen.setCapStyle(Qt::FlatCap);
192 strokeSize = qMax(1,width()/8);
193 startAngle -= 5*16*m_step;
194 segments = 6;
195 break;
196 case Scythe:
197 strokeSize = qMax(1,width()/16);
198 startAngle -= 10*16*m_step;
199 segments = 3;
200 break;
201 case Sun: {
202 pen.setCapStyle(Qt::FlatCap);
203 strokeSize = qMax(1,width()/4);
204 segments = 12;
205 segmentRatio = 0.8;
206 break;
208 case Elastic: {
209 strokeSize = qMax(1,width()/16);
210 int step = m_step;
211 startAngle -= 40*step*step;
212 step = (step+9)%12;
213 const int endAngle = 16*90 - 40*step*step;
214 span = (endAngle - startAngle);
215 if (span < 0)
216 span = 360*16+span; // fix direction.
217 break;
221 pen.setWidth(strokeSize);
222 if (segments) {
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));
228 QPainter p(this);
229 p.setRenderHint(QPainter::Antialiasing);
230 p.setBrush(Qt::NoBrush);
231 p.setPen(pen);
233 QRect r(rect());
234 r.adjust(strokeSize/2+1, strokeSize/2+1, -(strokeSize/2+1), -(strokeSize/2+1));
235 p.drawArc(r, startAngle, span);
237 if (m_type == Sun) {
238 QColor c3(palette().color(foregroundRole()));
239 c3.setAlpha(c2.alpha());
241 startAngle -= 30*16*m_step;
242 pen.setColor(c3);
243 p.setPen(pen);
244 p.drawArc(r, startAngle, 30*16);
246 for (int i = 2; i > 0; --i) {
247 startAngle += 30*16;
248 const int a = c3.alpha();
249 c2.setAlpha(255/(i+1));
250 c3 = UiUtils::tintColor(c3, c2);
251 c3.setAlpha(a);
252 pen.setColor(c3);
253 p.setPen(pen);
254 p.drawArc(r, startAngle, 30*16);
259 if (m_context == Overlay && !m_text.isEmpty()) {
260 QFont fnt;
261 if (fnt.pointSize() > -1) {
262 fnt.setBold(true);
263 fnt.setPointSizeF((fnt.pointSizeF() * r.width()) / (m_textCols*QFontMetricsF(fnt).maxWidth()));
264 p.setFont(fnt);
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
269 p.setPen(c1);
270 r.translate(-1,-1);
271 p.drawText(r, Qt::AlignCenter|Qt::TextDontClip, m_text);
272 r.translate(2,2);
273 p.drawText(r, Qt::AlignCenter|Qt::TextDontClip, m_text);
274 r.translate(-1,-1);
275 // actual text painting
276 c2 = QColor(palette().color(foregroundRole()));
277 c2.setAlpha(qAbs(m_fadeStep)*c2.alpha()/12);
278 p.setPen(c2);
279 p.drawText(r, Qt::AlignCenter|Qt::TextDontClip, m_text);
281 p.end();
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) {
288 if (++m_step > 11)
289 m_step = 0;
290 if (m_fadeStep == -1) { // stop
291 if (m_context == Overlay)
292 hide();
293 killTimer(m_timer);
294 m_timer = 0;
295 m_step = 0;
297 if (m_fadeStep < 12)
298 ++m_fadeStep;
299 repaint();
300 } else {
301 QWidget::timerEvent(e);
305 void Spinner::updateAncestors()
307 foreach (QWidget *w, m_ancestors)
308 w->removeEventFilter(this);
310 m_ancestors.clear();
312 if (m_context == Overlay) {
313 QWidget *w = this;
314 while ((w = w->parentWidget())) {
315 m_ancestors << w;
316 w->installEventFilter(this);
317 connect(w, &QObject::destroyed, this, &Spinner::updateAncestors);
319 updateGeometry();
323 void Spinner::updateGeometry()
325 if (!isVisible()) {
326 m_geometryDirty = true;
327 return;
329 if (m_ancestors.isEmpty())
330 return; // valid for Throbbers
331 QRect visibleRect(m_ancestors.last()->rect());
332 QPoint offset;
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());
341 setGeometry(r);
342 m_geometryDirty = false;