1/*
2 This file is part of the KDE libraries
3
4 SPDX-FileCopyrightText: 2011 Aurélien Gâteau <agateau@kde.org>
5 SPDX-FileCopyrightText: 2014 Dominik Haumann <dhaumann@kde.org>
6
7 SPDX-License-Identifier: LGPL-2.1-or-later
8*/
9#include "kmessagewidget.h"
10
11#include <QAction>
12#include <QApplication>
13#include <QEvent>
14#include <QGridLayout>
15#include <QGuiApplication>
16#include <QHBoxLayout>
17#include <QLabel>
18#include <QPainter>
19#include <QShowEvent>
20#include <QStyle>
21#include <QStyleOption>
22#include <QTimeLine>
23#include <QToolButton>
24//---------------------------------------------------------------------
25// KMessageWidgetPrivate
26//---------------------------------------------------------------------
27
28constexpr int borderSize = 2;
29
30class KMessageWidgetPrivate
31{
32public:
33 void init(KMessageWidget *);
34
35 KMessageWidget *q;
36 QLabel *iconLabel = nullptr;
37 QLabel *textLabel = nullptr;
38 QToolButton *closeButton = nullptr;
39 QTimeLine *timeLine = nullptr;
40 QIcon icon;
41 bool ignoreShowAndResizeEventDoingAnimatedShow = false;
42 KMessageWidget::MessageType messageType;
43 KMessageWidget::Position position = KMessageWidget::Inline;
44 bool wordWrap;
45 QList<QToolButton *> buttons;
46
47 void createLayout();
48 void setPalette();
49 void updateLayout();
50 void slotTimeLineChanged(qreal);
51 void slotTimeLineFinished();
52 int bestContentHeight() const;
53};
54
55void KMessageWidgetPrivate::init(KMessageWidget *q_ptr)
56{
57 q = q_ptr;
58 // Note: when changing the value 500, also update KMessageWidgetTest
59 timeLine = new QTimeLine(500, q);
60 QObject::connect(sender: timeLine, signal: &QTimeLine::valueChanged, context: q, slot: [this](qreal value) {
61 slotTimeLineChanged(value);
62 });
63 QObject::connect(sender: timeLine, signal: &QTimeLine::finished, context: q, slot: [this]() {
64 slotTimeLineFinished();
65 });
66
67 wordWrap = false;
68
69 iconLabel = new QLabel(q);
70 iconLabel->setSizePolicy(hor: QSizePolicy::Fixed, ver: QSizePolicy::Fixed);
71 iconLabel->hide();
72
73 textLabel = new QLabel(q);
74 textLabel->setSizePolicy(hor: QSizePolicy::Expanding, ver: QSizePolicy::Fixed);
75 textLabel->setTextInteractionFlags(Qt::TextBrowserInteraction);
76 QObject::connect(sender: textLabel, signal: &QLabel::linkActivated, context: q, slot: &KMessageWidget::linkActivated);
77 QObject::connect(sender: textLabel, signal: &QLabel::linkHovered, context: q, slot: &KMessageWidget::linkHovered);
78
79 QAction *closeAction = new QAction(q);
80 closeAction->setText(KMessageWidget::tr(s: "&Close", c: "@action:button"));
81 closeAction->setToolTip(KMessageWidget::tr(s: "Close message", c: "@info:tooltip"));
82 QStyleOptionFrame opt;
83 opt.initFrom(w: q);
84 closeAction->setIcon(q->style()->standardIcon(standardIcon: QStyle::SP_DialogCloseButton, option: &opt, widget: q));
85
86 QObject::connect(sender: closeAction, signal: &QAction::triggered, context: q, slot: &KMessageWidget::animatedHide);
87
88 closeButton = new QToolButton(q);
89 closeButton->setAutoRaise(true);
90 closeButton->setDefaultAction(closeAction);
91
92 q->setMessageType(KMessageWidget::Information);
93}
94
95void KMessageWidgetPrivate::createLayout()
96{
97 delete q->layout();
98
99 qDeleteAll(c: buttons);
100 buttons.clear();
101
102 const auto actions = q->actions();
103 buttons.reserve(asize: actions.size());
104 for (QAction *action : actions) {
105 QToolButton *button = new QToolButton(q);
106 button->setDefaultAction(action);
107 button->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
108 auto previous = buttons.isEmpty() ? static_cast<QWidget *>(textLabel) : buttons.back();
109 QWidget::setTabOrder(previous, button);
110 buttons.append(t: button);
111 }
112
113 // AutoRaise reduces visual clutter, but we don't want to turn it on if
114 // there are other buttons, otherwise the close button will look different
115 // from the others.
116 closeButton->setAutoRaise(buttons.isEmpty());
117 if (wordWrap) {
118 QGridLayout *layout = new QGridLayout(q);
119 // Set alignment to make sure icon does not move down if text wraps
120 layout->addWidget(iconLabel, row: 0, column: 0, rowSpan: 1, columnSpan: 1, Qt::AlignCenter);
121 layout->addWidget(textLabel, row: 0, column: 1);
122
123 if (buttons.isEmpty()) {
124 // Use top-vertical alignment like the icon does.
125 layout->addWidget(closeButton, row: 0, column: 2, rowSpan: 1, columnSpan: 1, Qt::AlignHCenter | Qt::AlignTop);
126 } else {
127 // Use an additional layout in row 1 for the buttons.
128 QHBoxLayout *buttonLayout = new QHBoxLayout;
129 buttonLayout->addStretch();
130 for (QToolButton *button : std::as_const(t&: buttons)) {
131 // For some reason, calling show() is necessary if wordwrap is true,
132 // otherwise the buttons do not show up. It is not needed if
133 // wordwrap is false.
134 button->show();
135 buttonLayout->addWidget(button);
136 }
137 buttonLayout->addWidget(closeButton);
138 layout->addItem(item: buttonLayout, row: 1, column: 0, rowSpan: 1, columnSpan: 2);
139 }
140 } else {
141 QHBoxLayout *layout = new QHBoxLayout(q);
142 layout->addWidget(iconLabel, stretch: 0, alignment: Qt::AlignVCenter);
143 layout->addWidget(textLabel);
144
145 for (QToolButton *button : std::as_const(t&: buttons)) {
146 layout->addWidget(button, stretch: 0, alignment: Qt::AlignTop);
147 }
148 layout->addWidget(closeButton, stretch: 0, alignment: Qt::AlignTop);
149 };
150 // Add bordersize to the margin so it starts from the inner border and doesn't look too cramped
151 q->layout()->setContentsMargins(q->layout()->contentsMargins() + borderSize);
152 if (q->isVisible()) {
153 q->setFixedHeight(q->sizeHint().height());
154 }
155 q->updateGeometry();
156}
157
158void KMessageWidgetPrivate::setPalette()
159{
160 QColor bgBaseColor;
161
162 // We have to hardcode colors here because KWidgetsAddons is a tier 1 framework
163 // and therefore can't depend on any other KDE Frameworks
164 // The following RGB color values come from the "default" scheme in kcolorscheme.cpp
165 switch (messageType) {
166 case KMessageWidget::Positive:
167 bgBaseColor.setRgb(r: 39, g: 174, b: 96); // Window: ForegroundPositive
168 break;
169 case KMessageWidget::Information:
170 bgBaseColor.setRgb(r: 61, g: 174, b: 233); // Window: ForegroundActive
171 break;
172 case KMessageWidget::Warning:
173 bgBaseColor.setRgb(r: 246, g: 116, b: 0); // Window: ForegroundNeutral
174 break;
175 case KMessageWidget::Error:
176 bgBaseColor.setRgb(r: 218, g: 68, b: 83); // Window: ForegroundNegative
177 break;
178 }
179 QPalette palette = q->palette();
180 palette.setColor(acr: QPalette::Window, acolor: bgBaseColor);
181 const QColor parentTextColor = (q->parentWidget() ? q->parentWidget()->palette() : qApp->palette()).color(cr: QPalette::WindowText);
182 palette.setColor(acr: QPalette::WindowText, acolor: parentTextColor);
183 // Explicitly set the palettes of the labels because some apps use stylesheets which break the
184 // palette propagation
185 iconLabel->setPalette(palette);
186 textLabel->setPalette(palette);
187 q->style()->polish(widget: q);
188 // update the Icon in case it is recolorable
189 q->setIcon(icon);
190 q->update();
191}
192
193void KMessageWidgetPrivate::updateLayout()
194{
195 createLayout();
196}
197
198void KMessageWidgetPrivate::slotTimeLineChanged(qreal value)
199{
200 q->setFixedHeight(qMin(a: value * 2, b: qreal(1.0)) * bestContentHeight());
201 q->update();
202}
203
204void KMessageWidgetPrivate::slotTimeLineFinished()
205{
206 if (timeLine->direction() == QTimeLine::Forward) {
207 q->resize(w: q->width(), h: bestContentHeight());
208
209 // notify about finished animation
210 Q_EMIT q->showAnimationFinished();
211 } else {
212 // hide and notify about finished animation
213 q->hide();
214 Q_EMIT q->hideAnimationFinished();
215 }
216}
217
218int KMessageWidgetPrivate::bestContentHeight() const
219{
220 int height = q->heightForWidth(width: q->width());
221 if (height == -1) {
222 height = q->sizeHint().height();
223 }
224 return height;
225}
226
227//---------------------------------------------------------------------
228// KMessageWidget
229//---------------------------------------------------------------------
230KMessageWidget::KMessageWidget(QWidget *parent)
231 : QFrame(parent)
232 , d(new KMessageWidgetPrivate)
233{
234 d->init(q_ptr: this);
235}
236
237KMessageWidget::KMessageWidget(const QString &text, QWidget *parent)
238 : QFrame(parent)
239 , d(new KMessageWidgetPrivate)
240{
241 d->init(q_ptr: this);
242 setText(text);
243}
244
245KMessageWidget::~KMessageWidget() = default;
246
247QString KMessageWidget::text() const
248{
249 return d->textLabel->text();
250}
251
252void KMessageWidget::setText(const QString &text)
253{
254 d->textLabel->setText(text);
255 updateGeometry();
256}
257
258Qt::TextFormat KMessageWidget::textFormat() const
259{
260 return d->textLabel->textFormat();
261}
262
263void KMessageWidget::setTextFormat(Qt::TextFormat textFormat)
264{
265 d->textLabel->setTextFormat(textFormat);
266}
267
268KMessageWidget::MessageType KMessageWidget::messageType() const
269{
270 return d->messageType;
271}
272
273void KMessageWidget::setMessageType(KMessageWidget::MessageType type)
274{
275 d->messageType = type;
276 d->setPalette();
277}
278
279QSize KMessageWidget::sizeHint() const
280{
281 ensurePolished();
282 return QFrame::sizeHint();
283}
284
285QSize KMessageWidget::minimumSizeHint() const
286{
287 ensurePolished();
288 return QFrame::minimumSizeHint();
289}
290
291bool KMessageWidget::event(QEvent *event)
292{
293 if (event->type() == QEvent::Polish && !layout()) {
294 d->createLayout();
295 } else if ((event->type() == QEvent::Show && !d->ignoreShowAndResizeEventDoingAnimatedShow)
296 || (event->type() == QEvent::LayoutRequest && d->timeLine->state() == QTimeLine::NotRunning)) {
297 setFixedHeight(d->bestContentHeight());
298
299 // if we are displaying this when application first starts, there's
300 // a possibility that the layout is not properly updated with the
301 // rest of the application because the setFixedHeight call above has
302 // the same height that was set beforehand, when we lacked a parent
303 // and thus, the layout() geometry is bogus. so we pass a bogus
304 // value to it, just to trigger a recalculation, and revert to the
305 // best content height.
306 if (geometry().height() < layout()->geometry().height()) {
307 setFixedHeight(d->bestContentHeight() + 2); // this triggers a recalculation.
308 setFixedHeight(d->bestContentHeight()); // this actually sets the correct values.
309 }
310
311 } else if (event->type() == QEvent::ParentChange) {
312 d->setPalette();
313 } else if (event->type() == QEvent::PaletteChange) {
314 d->setPalette();
315 }
316 return QFrame::event(e: event);
317}
318
319void KMessageWidget::resizeEvent(QResizeEvent *event)
320{
321 QFrame::resizeEvent(event);
322 if (d->timeLine->state() == QTimeLine::NotRunning && d->ignoreShowAndResizeEventDoingAnimatedShow) {
323 setFixedHeight(d->bestContentHeight());
324 }
325}
326
327int KMessageWidget::heightForWidth(int width) const
328{
329 ensurePolished();
330 return QFrame::heightForWidth(width);
331}
332
333void KMessageWidget::paintEvent(QPaintEvent *event)
334{
335 Q_UNUSED(event)
336 QPainter painter(this);
337 if (d->timeLine->state() == QTimeLine::Running) {
338 painter.setOpacity(d->timeLine->currentValue() * d->timeLine->currentValue());
339 }
340 constexpr float radius = 4 * 0.6;
341 const QRect innerRect = rect().marginsRemoved(margins: QMargins() + borderSize / 2);
342 const QColor color = palette().color(cr: QPalette::Window);
343 constexpr float alpha = 0.2;
344 const QColor parentWindowColor = (parentWidget() ? parentWidget()->palette() : qApp->palette()).color(cr: QPalette::Window);
345 const int newRed = (color.red() * alpha) + (parentWindowColor.red() * (1 - alpha));
346 const int newGreen = (color.green() * alpha) + (parentWindowColor.green() * (1 - alpha));
347 const int newBlue = (color.blue() * alpha) + (parentWindowColor.blue() * (1 - alpha));
348
349 painter.setRenderHint(hint: QPainter::Antialiasing);
350 painter.setBrush(QColor(newRed, newGreen, newBlue));
351 if (d->position == Position::Inline) {
352 painter.setPen(QPen(color, borderSize));
353 painter.drawRoundedRect(rect: innerRect, xRadius: radius, yRadius: radius);
354 return;
355 }
356
357 painter.setPen(QPen(Qt::NoPen));
358 painter.drawRect(r: rect());
359
360 if (d->position == Position::Header) {
361 painter.setPen(QPen(color, 1));
362 painter.drawLine(x1: 0, y1: rect().height(), x2: rect().width(), y2: rect().height());
363 } else {
364 painter.setPen(QPen(color, 1));
365 painter.drawLine(x1: 0, y1: 0, x2: rect().width(), y2: 0);
366 }
367}
368
369bool KMessageWidget::wordWrap() const
370{
371 return d->wordWrap;
372}
373
374void KMessageWidget::setWordWrap(bool wordWrap)
375{
376 d->wordWrap = wordWrap;
377 d->textLabel->setWordWrap(wordWrap);
378 QSizePolicy policy = sizePolicy();
379 policy.setHeightForWidth(wordWrap);
380 setSizePolicy(policy);
381 d->updateLayout();
382 // Without this, when user does wordWrap -> !wordWrap -> wordWrap, a minimum
383 // height is set, causing the widget to be too high.
384 // Mostly visible in test programs.
385 if (wordWrap) {
386 setMinimumHeight(0);
387 }
388}
389
390KMessageWidget::Position KMessageWidget::position() const
391{
392 return d->position;
393}
394
395void KMessageWidget::setPosition(KMessageWidget::Position position)
396{
397 d->position = position;
398 updateGeometry();
399}
400
401bool KMessageWidget::isCloseButtonVisible() const
402{
403 return d->closeButton->isVisible();
404}
405
406void KMessageWidget::setCloseButtonVisible(bool show)
407{
408 d->closeButton->setVisible(show);
409 updateGeometry();
410}
411
412void KMessageWidget::addAction(QAction *action)
413{
414 QFrame::addAction(action);
415 d->updateLayout();
416}
417
418void KMessageWidget::removeAction(QAction *action)
419{
420 QFrame::removeAction(action);
421 d->updateLayout();
422}
423
424void KMessageWidget::clearActions()
425{
426 const auto ourActions = actions();
427 for (auto *action : ourActions) {
428 removeAction(action);
429 }
430 d->updateLayout();
431}
432
433void KMessageWidget::animatedShow()
434{
435 // Test before styleHint, as there might have been a style change while animation was running
436 if (isHideAnimationRunning()) {
437 d->timeLine->stop();
438 Q_EMIT hideAnimationFinished();
439 }
440
441 if (!style()->styleHint(stylehint: QStyle::SH_Widget_Animate, opt: nullptr, widget: this) || (parentWidget() && !parentWidget()->isVisible())) {
442 show();
443 Q_EMIT showAnimationFinished();
444 return;
445 }
446
447 if (isVisible() && (d->timeLine->state() == QTimeLine::NotRunning) && (height() == d->bestContentHeight())) {
448 Q_EMIT showAnimationFinished();
449 return;
450 }
451
452 d->ignoreShowAndResizeEventDoingAnimatedShow = true;
453 show();
454 d->ignoreShowAndResizeEventDoingAnimatedShow = false;
455 setFixedHeight(0);
456
457 d->timeLine->setDirection(QTimeLine::Forward);
458 if (d->timeLine->state() == QTimeLine::NotRunning) {
459 d->timeLine->start();
460 }
461}
462
463void KMessageWidget::animatedHide()
464{
465 // test this before isVisible, as animatedShow might have been called directly before,
466 // so the first timeline event is not yet done and the widget is still hidden
467 // And before styleHint, as there might have been a style change while animation was running
468 if (isShowAnimationRunning()) {
469 d->timeLine->stop();
470 Q_EMIT showAnimationFinished();
471 }
472
473 if (!style()->styleHint(stylehint: QStyle::SH_Widget_Animate, opt: nullptr, widget: this)) {
474 hide();
475 Q_EMIT hideAnimationFinished();
476 return;
477 }
478
479 if (!isVisible()) {
480 // explicitly hide it, so it stays hidden in case it is only not visible due to the parents
481 hide();
482 Q_EMIT hideAnimationFinished();
483 return;
484 }
485
486 d->timeLine->setDirection(QTimeLine::Backward);
487 if (d->timeLine->state() == QTimeLine::NotRunning) {
488 d->timeLine->start();
489 }
490}
491
492bool KMessageWidget::isHideAnimationRunning() const
493{
494 return (d->timeLine->direction() == QTimeLine::Backward) && (d->timeLine->state() == QTimeLine::Running);
495}
496
497bool KMessageWidget::isShowAnimationRunning() const
498{
499 return (d->timeLine->direction() == QTimeLine::Forward) && (d->timeLine->state() == QTimeLine::Running);
500}
501
502QIcon KMessageWidget::icon() const
503{
504 return d->icon;
505}
506
507void KMessageWidget::setIcon(const QIcon &icon)
508{
509 d->icon = icon;
510 if (d->icon.isNull()) {
511 d->iconLabel->hide();
512 } else {
513 const int size = style()->pixelMetric(metric: QStyle::PM_ToolBarIconSize);
514 d->iconLabel->setPixmap(d->icon.pixmap(extent: size));
515 d->iconLabel->show();
516 }
517}
518
519#include "moc_kmessagewidget.cpp"
520

source code of kwidgetsaddons/src/kmessagewidget.cpp