KWidgetsAddons

kmessagewidget.cpp
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
36 QLabel *iconLabel = nullptr;
37 QLabel *textLabel = nullptr;
38 QToolButton *closeButton = nullptr;
39 QTimeLine *timeLine = nullptr;
40 QIcon icon;
41 bool ignoreShowAndResizeEventDoingAnimatedShow = false;
44 bool wordWrap;
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(timeLine, &QTimeLine::valueChanged, q, [this](qreal value) {
62 slotTimeLineChanged(value);
63 });
64 QObject::connect(timeLine, &QTimeLine::finished, q, [this]() {
65 slotTimeLineFinished();
66 });
67
68 wordWrap = false;
69
70 iconLabel = new QLabel(q);
72 iconLabel->hide();
73
74 textLabel = new QLabel(q);
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("&Close", "@action:button"));
85 closeAction->setToolTip(KMessageWidget::tr("Close message", "@info:tooltip"));
87 opt.initFrom(q);
88 closeAction->setIcon(q->style()->standardIcon(QStyle::SP_DialogCloseButton, &opt, q));
89
91
92 closeButton = new QToolButton(q);
93 closeButton->setAutoRaise(true);
94 closeButton->setDefaultAction(closeAction);
95
97}
98
99void KMessageWidgetPrivate::createLayout()
100{
101 delete q->layout();
102
103 qDeleteAll(buttons);
104 buttons.clear();
105
106 const auto actions = q->actions();
107 buttons.reserve(actions.size());
108 for (QAction *action : actions) {
109 QToolButton *button = new QToolButton(q);
110 button->setDefaultAction(action);
112 auto previous = buttons.isEmpty() ? static_cast<QWidget *>(textLabel) : buttons.back();
113 QWidget::setTabOrder(previous, button);
114 buttons.append(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, 0, 0, 1, 1, Qt::AlignCenter);
125 layout->addWidget(textLabel, 0, 1);
126
127 if (buttons.isEmpty()) {
128 // Use top-vertical alignment like the icon does.
129 layout->addWidget(closeButton, 0, 2, 1, 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(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(buttonLayout, 1, 0, 1, 2);
143 }
144 } else {
145 QHBoxLayout *layout = new QHBoxLayout(q);
146 layout->addWidget(iconLabel, 0, Qt::AlignVCenter);
147 layout->addWidget(textLabel);
148
149 for (QToolButton *button : std::as_const(buttons)) {
150 layout->addWidget(button, 0, Qt::AlignTop);
151 }
152 layout->addWidget(closeButton, 0, 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) {
171 bgBaseColor.setRgb(39, 174, 96); // Window: ForegroundPositive
172 break;
174 bgBaseColor.setRgb(61, 174, 233); // Window: ForegroundActive
175 break;
177 bgBaseColor.setRgb(246, 116, 0); // Window: ForegroundNeutral
178 break;
180 bgBaseColor.setRgb(218, 68, 83); // Window: ForegroundNegative
181 break;
182 }
183 QPalette palette = q->palette();
184 palette.setColor(QPalette::Window, bgBaseColor);
185 const QColor parentTextColor = (q->parentWidget() ? q->parentWidget()->palette() : qApp->palette()).color(QPalette::WindowText);
186 palette.setColor(QPalette::WindowText, 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(value * 2, qreal(1.0)) * bestContentHeight());
208 q->update();
209}
210
211void KMessageWidgetPrivate::slotTimeLineFinished()
212{
213 if (timeLine->direction() == QTimeLine::Forward) {
214 q->resize(q->width(), 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(q->width());
228 if (height == -1) {
229 height = q->sizeHint().height();
230 }
231 return height;
232}
233
234//---------------------------------------------------------------------
235// KMessageWidget
236//---------------------------------------------------------------------
238 : QFrame(parent)
239 , d(new KMessageWidgetPrivate)
240{
241 d->init(this);
242}
243
245 : QFrame(parent)
246 , d(new KMessageWidgetPrivate)
247{
248 d->init(this);
249 setText(text);
250}
251
253
254QString KMessageWidget::text() const
255{
256 return d->textLabel->text();
257}
258
260{
261 d->textLabel->setText(text);
263}
264
265Qt::TextFormat KMessageWidget::textFormat() const
266{
267 return d->textLabel->textFormat();
268}
269
271{
272 d->textLabel->setTextFormat(textFormat);
273}
274
275KMessageWidget::MessageType KMessageWidget::messageType() const
276{
277 return d->messageType;
278}
279
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) {
288 setAccessibleName(KMessageWidget::tr("Success", "accessible name of positively-colored (e.g. green) message box"));
289 break;
291 setAccessibleName(KMessageWidget::tr("Note", "accessible name of info-colored (e.g. blue) message box"));
292 break;
294 setAccessibleName(KMessageWidget::tr("Warning", "accessible name of warning-colored (e.g. orange) message box"));
295 break;
297 setAccessibleName(KMessageWidget::tr("Error", "accessible name of error-colored (e.g. red) message box"));
298 }
299}
300
302{
304 return QFrame::sizeHint();
305}
306
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(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
352{
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(QMargins() + borderSize / 2);
366 const QColor color = palette().color(QPalette::Window);
367 constexpr float alpha = 0.2;
368 const QColor parentWindowColor = (parentWidget() ? parentWidget()->palette() : qApp->palette()).color(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(QPainter::Antialiasing);
374 painter.setBrush(QColor(newRed, newGreen, newBlue));
375 if (d->position == Position::Inline) {
376 painter.setPen(QPen(color, borderSize));
377 painter.drawRoundedRect(innerRect, radius, radius);
378 return;
379 }
380
381 painter.setPen(QPen(Qt::NoPen));
382 painter.drawRect(rect());
383
384 if (d->position == Position::Header) {
385 painter.setPen(QPen(color, 1));
386 painter.drawLine(0, rect().height(), rect().width(), rect().height());
387 } else {
388 painter.setPen(QPen(color, 1));
389 painter.drawLine(0, 0, rect().width(), 0);
390 }
391}
392
393bool KMessageWidget::wordWrap() const
394{
395 return d->wordWrap;
396}
397
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) {
411 }
412}
413
414KMessageWidget::Position KMessageWidget::position() const
415{
416 return d->position;
417}
418
420{
421 d->position = position;
423}
424
426{
427 return d->closeButton->isVisible();
428}
429
431{
432 d->closeButton->setVisible(show);
434}
435
437{
438 QFrame::addAction(action);
439 d->updateLayout();
440}
441
443{
444 QFrame::removeAction(action);
445 d->updateLayout();
446}
447
449{
450 const auto ourActions = actions();
451 for (auto *action : ourActions) {
452 removeAction(action);
453 }
454 d->updateLayout();
455}
456
458{
459 // Test before styleHint, as there might have been a style change while animation was running
461 d->timeLine->stop();
463 }
464
465 if (!style()->styleHint(QStyle::SH_Widget_Animate, nullptr, this) || (parentWidget() && !parentWidget()->isVisible())) {
466 show();
468 return;
469 }
470
471 if (isVisible() && (d->timeLine->state() == QTimeLine::NotRunning) && (height() == d->bestContentHeight())) {
473 return;
474 }
475
476 d->ignoreShowAndResizeEventDoingAnimatedShow = true;
477 show();
478 d->ignoreShowAndResizeEventDoingAnimatedShow = false;
480
481 d->timeLine->setDirection(QTimeLine::Forward);
482 if (d->timeLine->state() == QTimeLine::NotRunning) {
483 d->timeLine->start();
484 }
485}
486
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
493 d->timeLine->stop();
495 }
496
497 if (!style()->styleHint(QStyle::SH_Widget_Animate, nullptr, this)) {
498 hide();
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();
507 return;
508 }
509
510 d->timeLine->setDirection(QTimeLine::Backward);
511 if (d->timeLine->state() == QTimeLine::NotRunning) {
512 d->timeLine->start();
513 }
514}
515
517{
518 return (d->timeLine->direction() == QTimeLine::Backward) && (d->timeLine->state() == QTimeLine::Running);
519}
520
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
532{
533 d->icon = icon;
534 if (d->icon.isNull()) {
535 d->iconLabel->hide();
536 } else {
538 d->iconLabel->setPixmap(d->icon.pixmap(size));
539 d->iconLabel->show();
540 }
541}
542
543#include "moc_kmessagewidget.cpp"
A widget to provide feedback or propose opportunistic interactions.
void removeAction(QAction *action)
Remove action from the message widget.
bool isShowAnimationRunning() const
Check whether the show animation started by calling animatedShow() is still running.
void setCloseButtonVisible(bool visible)
Set the visibility of the close button.
void animatedHide()
Hide the widget using an animation.
bool isHideAnimationRunning() const
Check whether the hide animation started by calling animatedHide() is still running.
void linkActivated(const QString &contents)
This signal is emitted when the user clicks a link in the text label.
void showAnimationFinished()
This signal is emitted when the show animation is finished, started by calling animatedShow().
void linkHovered(const QString &contents)
This signal is emitted when the user hovers over a link in the text label.
void animatedShow()
Show the widget using an animation.
void addAction(QAction *action)
Add action to the message widget.
void hideAnimationFinished()
This signal is emitted when the hide animation is finished, started by calling animatedHide().
~KMessageWidget() override
Destructor.
int heightForWidth(int width) const override
Returns the required height for width.
void setMessageType(KMessageWidget::MessageType type)
Set the message type to type.
void setTextFormat(Qt::TextFormat textFormat)
Set the text format of the message widget's label.
MessageType
Available message types.
@ Error
Error message type.
@ Information
Information message type.
@ Positive
Positive message type.
@ Warning
Warning message type.
void setIcon(const QIcon &icon)
Define an icon to be shown on the left of the text.
KMessageWidget(QWidget *parent=nullptr)
Constructs a KMessageWidget with the specified parent.
bool isCloseButtonVisible() const
Check whether the close button is visible.
QSize sizeHint() const override
Returns the preferred size of the message widget.
void setWordWrap(bool wordWrap)
Set word wrap to wordWrap.
void clearActions()
Clears all actions from the message widget.
void setPosition(Position position)
Set the position of this message.
QSize minimumSizeHint() const override
Returns the minimum size of the message widget.
Position
Position of the KMessageWidget.
@ Inline
The message widget is display inside the content.
@ Header
The message widget is displayed as header.
void setText(const QString &text)
Set the text of the message widget to text.
AKONADI_CALENDAR_EXPORT KCalendarCore::Event::Ptr event(const Akonadi::Item &item)
const QList< QKeySequence > & back()
void setIcon(const QIcon &icon)
void setText(const QString &text)
void setToolTip(const QString &tip)
void triggered(bool checked)
void addStretch(int stretch)
void addWidget(QWidget *widget, int stretch, Qt::Alignment alignment)
int blue() const const
int green() const const
int red() const const
void setRgb(QRgb rgb)
virtual bool event(QEvent *e) override
virtual QSize sizeHint() const const override
virtual void addItem(QLayoutItem *item) override
void addWidget(QWidget *widget, int fromRow, int fromColumn, int rowSpan, int columnSpan, Qt::Alignment alignment)
void linkActivated(const QString &link)
void linkHovered(const QString &link)
void setTextInteractionFlags(Qt::TextInteractionFlags flags)
QMargins contentsMargins() const const
void setContentsMargins(const QMargins &margins)
void append(QList< T > &&value)
void clear()
bool isEmpty() const const
void reserve(qsizetype size)
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QString tr(const char *sourceText, const char *disambiguation, int n)
void setColor(ColorGroup group, ColorRole role, const QColor &color)
QRect marginsRemoved(const QMargins &margins) const const
int height() const const
void setHeightForWidth(bool dependent)
PM_ToolBarIconSize
SP_DialogCloseButton
SH_Widget_Animate
virtual int pixelMetric(PixelMetric metric, const QStyleOption *option, const QWidget *widget) const const=0
virtual QIcon standardIcon(StandardPixmap standardIcon, const QStyleOption *option, const QWidget *widget) const const=0
void initFrom(const QWidget *widget)
AlignCenter
TextFormat
TextBrowserInteraction
ToolButtonTextBesideIcon
void finished()
void valueChanged(qreal value)
void setAutoRaise(bool enable)
void setDefaultAction(QAction *action)
void setToolButtonStyle(Qt::ToolButtonStyle style)
void setAccessibleName(const QString &name)
QList< QAction * > actions() const const
QAction * addAction(const QIcon &icon, const QString &text)
void ensurePolished() const const
virtual int heightForWidth(int w) const const
void hide()
QLayout * layout() const const
void setMinimumHeight(int minh)
QWidget * parentWidget() const const
void removeAction(QAction *action)
virtual void resizeEvent(QResizeEvent *event)
void setFixedHeight(int h)
void setFocusProxy(QWidget *w)
void setTabOrder(QWidget *first, QWidget *second)
void show()
void resize(const QSize &)
void setSizePolicy(QSizePolicy)
QStyle * style() const const
void update()
void updateGeometry()
bool isVisible() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 18 2024 12:09:52 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.