1 /*
2     SPDX-FileCopyrightText: 2006 Peter Penz <peter.penz@gmx.at>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "konqstatusbarmessagelabel.h"
8 #include <QStyle>
9 #include <QTextDocument>
10 
11 #include <kcolorscheme.h>
12 #include <QIcon>
13 #include <KLocalizedString>
14 #include "konqdebug.h"
15 
16 #include <QFontMetrics>
17 #include <QPainter>
18 #include <QPixmap>
19 #include <QToolButton>
20 #include <QTimer>
21 
22 enum { GeometryTimeout = 100 };
23 enum { BorderGap = 2 };
24 
25 class KonqStatusBarMessageLabel::Private
26 {
27 public:
Private()28     Private() :
29         m_type(Default),
30         m_state(DefaultState),
31         m_illumination(0),
32         m_minTextHeight(-1),
33         m_timer(nullptr),
34         m_closeButton(nullptr)
35     {}
36 
isRichText() const37     bool isRichText() const
38     {
39         return m_text.startsWith(QLatin1String("<html>")) || m_text.startsWith(QLatin1String("<qt>"));
40     }
41 
42     KonqStatusBarMessageLabel::Type m_type;
43     KonqStatusBarMessageLabel::State m_state;
44     int m_illumination;
45     int m_minTextHeight;
46     QTimer *m_timer;
47     QString m_text;
48     QString m_defaultText;
49     QTextDocument m_textDocument;
50     QList<QString> m_pendingMessages;
51     QPixmap m_pixmap;
52     QToolButton *m_closeButton;
53 };
54 
KonqStatusBarMessageLabel(QWidget * parent)55 KonqStatusBarMessageLabel::KonqStatusBarMessageLabel(QWidget *parent) :
56     QWidget(parent), d(new Private)
57 {
58     setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum /*the sizeHint is the max*/);
59 
60     d->m_timer = new QTimer(this);
61     connect(d->m_timer, &QTimer::timeout,
62             this, &KonqStatusBarMessageLabel::timerDone);
63 
64     d->m_closeButton = new QToolButton(this);
65     d->m_closeButton->setAutoRaise(true);
66     d->m_closeButton->setIcon(QIcon::fromTheme(QStringLiteral("dialog-close")));
67     d->m_closeButton->setToolTip(i18nc("@info", "Close"));
68     d->m_closeButton->setAccessibleName(i18n("Close"));
69     d->m_closeButton->hide();
70     connect(d->m_closeButton, &QToolButton::clicked,
71             this, &KonqStatusBarMessageLabel::closeErrorMessage);
72 }
73 
~KonqStatusBarMessageLabel()74 KonqStatusBarMessageLabel::~KonqStatusBarMessageLabel()
75 {
76     delete d;
77 }
78 
setMessage(const QString & text,Type type)79 void KonqStatusBarMessageLabel::setMessage(const QString &text,
80         Type type)
81 {
82     if ((text == d->m_text) && (type == d->m_type)) {
83         return;
84     }
85 
86     if (d->m_type == Error) {
87         if (type == Error) {
88             d->m_pendingMessages.insert(0, d->m_text);
89         } else if ((d->m_state != DefaultState) || !d->m_pendingMessages.isEmpty()) {
90             // a non-error message should not be shown, as there
91             // are other pending error messages in the queue
92             return;
93         }
94     }
95 
96     d->m_text = text;
97     d->m_type = type;
98 
99     if (d->isRichText()) {
100         d->m_textDocument.setTextWidth(-1);
101         d->m_textDocument.setDefaultFont(font());
102         QString html = QStringLiteral("<html><font color=\"");
103         html += palette().windowText().color().name();
104         html += QLatin1String("\">");
105         html += d->m_text;
106         d->m_textDocument.setHtml(html);
107     }
108 
109     d->m_timer->stop();
110     d->m_illumination = 0;
111     d->m_state = DefaultState;
112 
113     const char *iconName = nullptr;
114     QPixmap pixmap;
115     switch (type) {
116     case OperationCompleted:
117         iconName = "dialog-ok";
118         // "ok" icon should probably be "dialog-success", but we don't have that icon in KDE 4.0
119         d->m_closeButton->hide();
120         break;
121 
122     case Information:
123         iconName = "dialog-information";
124         d->m_closeButton->hide();
125         break;
126 
127     case Error:
128         d->m_timer->start(100);
129         d->m_state = Illuminate;
130 
131         updateCloseButtonPosition();
132         d->m_closeButton->show();
133         updateGeometry();
134         break;
135 
136     case Default:
137     default:
138         d->m_closeButton->hide();
139         updateGeometry();
140         break;
141     }
142 
143     d->m_pixmap = (iconName == nullptr) ? QPixmap() : QIcon::fromTheme(iconName).pixmap(style()->pixelMetric(QStyle::PM_SmallIconSize));
144     QTimer::singleShot(GeometryTimeout, this, SLOT(assureVisibleText()));
145 
146     if (type == Error) {
147         setAccessibleName(i18n("Error: %1", text));
148     } else {
149         setAccessibleName(text);
150     }
151 
152     update();
153 }
154 
type() const155 KonqStatusBarMessageLabel::Type KonqStatusBarMessageLabel::type() const
156 {
157     return d->m_type;
158 }
159 
text() const160 QString KonqStatusBarMessageLabel::text() const
161 {
162     return d->m_text;
163 }
164 
setDefaultText(const QString & text)165 void KonqStatusBarMessageLabel::setDefaultText(const QString &text)
166 {
167     d->m_defaultText = text;
168 }
169 
defaultText() const170 QString KonqStatusBarMessageLabel::defaultText() const
171 {
172     return d->m_defaultText;
173 }
174 
setMinimumTextHeight(int min)175 void KonqStatusBarMessageLabel::setMinimumTextHeight(int min)
176 {
177     if (min != d->m_minTextHeight) {
178         d->m_minTextHeight = min;
179         setMinimumHeight(min);
180         if (d->m_closeButton->height() > min) {
181             d->m_closeButton->setFixedHeight(min);
182         }
183     }
184 }
185 
minimumTextHeight() const186 int KonqStatusBarMessageLabel::minimumTextHeight() const
187 {
188     return d->m_minTextHeight;
189 }
190 
paintEvent(QPaintEvent *)191 void KonqStatusBarMessageLabel::paintEvent(QPaintEvent * /* event */)
192 {
193     QPainter painter(this);
194 
195     if (d->m_illumination > 0) {
196         // at this point, a: we are a second label being drawn over the already
197         // painted status area, so we can be translucent, and b: our palette's
198         // window color (bg only) seems to be wrong (always black)
199         KColorScheme scheme(palette().currentColorGroup(), KColorScheme::Window);
200         QColor backgroundColor = scheme.background(KColorScheme::NegativeBackground).color();
201         backgroundColor.setAlpha(qMin(255, d->m_illumination * 2));
202         painter.setBrush(backgroundColor);
203         painter.setPen(Qt::NoPen);
204         painter.drawRect(QRect(0, 0, width(), height()));
205     }
206 
207     // draw pixmap
208     int x = BorderGap;
209     const int y = (d->m_minTextHeight - d->m_pixmap.height()) / 2;
210 
211     if (!d->m_pixmap.isNull()) {
212         painter.drawPixmap(x, y, d->m_pixmap);
213         x += d->m_pixmap.width() + BorderGap;
214     }
215 
216     // draw text
217 
218     const QRect availTextRect(x, 0, availableTextWidth(), height());
219 
220     if (d->isRichText()) {
221         const QSize sz = d->m_textDocument.size().toSize();
222 
223         // Vertical centering
224         const QRect textRect = QStyle::alignedRect(Qt::LeftToRight, Qt::AlignLeft | Qt::AlignVCenter, sz, availTextRect);
225         //qCDebug(KONQUEROR_LOG) << d->m_text << " sz=" << sz << textRect;
226 
227         // What about wordwrap here?
228 
229         painter.translate(textRect.left(), textRect.top());
230         d->m_textDocument.drawContents(&painter);
231     } else {
232         // plain text
233         painter.setPen(palette().windowText().color());
234         int flags = Qt::AlignVCenter;
235         if (height() > d->m_minTextHeight) {
236             flags = flags | Qt::TextWordWrap;
237         }
238         painter.drawText(availTextRect, flags, d->m_text);
239     }
240     painter.end();
241 }
242 
resizeEvent(QResizeEvent * event)243 void KonqStatusBarMessageLabel::resizeEvent(QResizeEvent *event)
244 {
245     QWidget::resizeEvent(event);
246     updateCloseButtonPosition();
247     QTimer::singleShot(GeometryTimeout, this, SLOT(assureVisibleText()));
248 }
249 
timerDone()250 void KonqStatusBarMessageLabel::timerDone()
251 {
252     switch (d->m_state) {
253     case Illuminate: {
254         // increase the illumination
255         const int illumination_max = 128;
256         if (d->m_illumination < illumination_max) {
257             d->m_illumination += 32;
258             if (d->m_illumination > illumination_max) {
259                 d->m_illumination = illumination_max;
260             }
261             update();
262         } else {
263             d->m_state = Illuminated;
264             d->m_timer->start(5000);
265         }
266         break;
267     }
268 
269     case Illuminated: {
270         // start desaturation
271         d->m_state = Desaturate;
272         d->m_timer->start(100);
273         break;
274     }
275 
276     case Desaturate: {
277         // desaturate
278         if (d->m_illumination > 0) {
279             d->m_illumination -= 5;
280             update();
281         } else {
282             d->m_state = DefaultState;
283             d->m_timer->stop();
284         }
285         break;
286     }
287 
288     default:
289         break;
290     }
291 }
292 
assureVisibleText()293 void KonqStatusBarMessageLabel::assureVisibleText()
294 {
295     if (d->m_text.isEmpty()) {
296         return;
297     }
298 
299     int requiredHeight = d->m_minTextHeight;
300     if (d->m_type != Default) {
301         // Calculate the required height of the widget thats
302         // needed for having a fully visible text. Note that for the default
303         // statusbar type (e. g. hover information) increasing the text height
304         // is not wanted, as this might rearrange the layout of items.
305 
306         QFontMetrics fontMetrics(font());
307         const QRect bounds(fontMetrics.boundingRect(0, 0, availableTextWidth(), height(),
308                            Qt::AlignVCenter | Qt::TextWordWrap, d->m_text));
309         requiredHeight = bounds.height();
310         if (requiredHeight < d->m_minTextHeight) {
311             requiredHeight = d->m_minTextHeight;
312         }
313     }
314 
315     // Increase/decrease the current height of the widget to the
316     // required height. The increasing/decreasing is done in several
317     // steps to have an animation if the height is modified
318     // (see KonqStatusBarMessageLabel::resizeEvent())
319     const int gap = d->m_minTextHeight / 2;
320     int minHeight = minimumHeight();
321     if (minHeight < requiredHeight) {
322         minHeight += gap;
323         if (minHeight > requiredHeight) {
324             minHeight = requiredHeight;
325         }
326         setMinimumHeight(minHeight);
327         updateGeometry();
328     } else if (minHeight > requiredHeight) {
329         minHeight -= gap;
330         if (minHeight < requiredHeight) {
331             minHeight = requiredHeight;
332         }
333         setMinimumHeight(minHeight);
334         updateGeometry();
335     }
336 
337     updateCloseButtonPosition();
338 }
339 
availableTextWidth() const340 int KonqStatusBarMessageLabel::availableTextWidth() const
341 {
342     const int buttonWidth = (d->m_type == Error) ?
343                             d->m_closeButton->width() + BorderGap : 0;
344     return width() - d->m_pixmap.width() - (BorderGap * 4) - buttonWidth;
345 }
346 
updateCloseButtonPosition()347 void KonqStatusBarMessageLabel::updateCloseButtonPosition()
348 {
349     const int x = width() - d->m_closeButton->width() - BorderGap;
350     d->m_closeButton->move(x, 0);
351 }
352 
closeErrorMessage()353 void KonqStatusBarMessageLabel::closeErrorMessage()
354 {
355     if (!showPendingMessage()) {
356         d->m_state = DefaultState;
357         setMessage(d->m_defaultText, Default);
358     }
359 }
360 
showPendingMessage()361 bool KonqStatusBarMessageLabel::showPendingMessage()
362 {
363     if (!d->m_pendingMessages.isEmpty()) {
364         reset();
365         setMessage(d->m_pendingMessages.takeFirst(), Error);
366         return true;
367     }
368     return false;
369 }
370 
reset()371 void KonqStatusBarMessageLabel::reset()
372 {
373     d->m_text.clear();
374     d->m_type = Default;
375 }
376 
sizeHint() const377 QSize KonqStatusBarMessageLabel::sizeHint() const
378 {
379     return minimumSizeHint();
380 }
381 
minimumSizeHint() const382 QSize KonqStatusBarMessageLabel::minimumSizeHint() const
383 {
384     const int fontHeight = fontMetrics().height();
385     QSize sz(100, fontHeight);
386     if (d->m_closeButton->isVisible()) {
387         const QSize toolButtonSize = d->m_closeButton->sizeHint();
388         sz = toolButtonSize.expandedTo(sz);
389     }
390     return sz;
391 }
392