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

source code of kwidgetsaddons/src/kmessagewidget.cpp