KWidgetsAddons

kmessagewidget.cpp
1 /*
2  This file is part of the KDE libraries
3 
4  SPDX-FileCopyrightText: 2011 Aurélien Gâteau <[email protected]>
5  SPDX-FileCopyrightText: 2014 Dominik Haumann <[email protected]>
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 <QHBoxLayout>
16 #include <QLabel>
17 #include <QPainter>
18 #include <QShowEvent>
19 #include <QStyleOption>
20 #include <QTimeLine>
21 #include <QToolButton>
22 #include <QStyle>
23 #include <QGuiApplication>
24 //---------------------------------------------------------------------
25 // KMessageWidgetPrivate
26 //---------------------------------------------------------------------
27 
28 constexpr int borderSize = 2;
29 
30 class KMessageWidgetPrivate
31 {
32 public:
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 ignoreShowEventDoingAnimatedShow = false;
42 
43  KMessageWidget::MessageType messageType;
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 
55 void 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(timeLine, SIGNAL(valueChanged(qreal)), q, SLOT(slotTimeLineChanged(qreal)));
61  QObject::connect(timeLine, SIGNAL(finished()), q, SLOT(slotTimeLineFinished()));
62 
63  wordWrap = false;
64 
65  iconLabel = new QLabel(q);
66  iconLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
67  iconLabel->hide();
68 
69  textLabel = new QLabel(q);
70  textLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
71  textLabel->setTextInteractionFlags(Qt::TextBrowserInteraction);
74 
75  QAction *closeAction = new QAction(q);
76  closeAction->setText(KMessageWidget::tr("&Close", "@action:button"));
77  closeAction->setToolTip(KMessageWidget::tr("Close message", "@info:tooltip"));
79  opt.initFrom(q);
80  closeAction->setIcon(q->style()->standardIcon(QStyle::SP_DialogCloseButton, &opt, q));
81 
83 
84  closeButton = new QToolButton(q);
85  closeButton->setAutoRaise(true);
86  closeButton->setDefaultAction(closeAction);
87 
88  q->setMessageType(KMessageWidget::Information);
89 
90  q->connect(qApp, &QApplication::paletteChanged, q, [this] {KMessageWidgetPrivate::setPalette();});
91 }
92 
93 void KMessageWidgetPrivate::createLayout()
94 {
95  delete q->layout();
96 
97  qDeleteAll(buttons);
98  buttons.clear();
99 
100  const auto actions = q->actions();
101  buttons.reserve(actions.size());
102  for (QAction *action : actions) {
103  QToolButton *button = new QToolButton(q);
104  button->setDefaultAction(action);
106  buttons.append(button);
107  }
108 
109  // AutoRaise reduces visual clutter, but we don't want to turn it on if
110  // there are other buttons, otherwise the close button will look different
111  // from the others.
112  closeButton->setAutoRaise(buttons.isEmpty());
113  if (wordWrap) {
114  QGridLayout *layout = new QGridLayout(q);
115  // Set alignment to make sure icon does not move down if text wraps
116  layout->addWidget(iconLabel, 0, 0, 1, 1, Qt::AlignHCenter | Qt::AlignTop);
117  layout->addWidget(textLabel, 0, 1);
118 
119  if (buttons.isEmpty()) {
120  // Use top-vertical alignment like the icon does.
121  layout->addWidget(closeButton, 0, 2, 1, 1, Qt::AlignHCenter | Qt::AlignTop);
122  } else {
123  // Use an additional layout in row 1 for the buttons.
124  QHBoxLayout *buttonLayout = new QHBoxLayout;
125  buttonLayout->addStretch();
126  for (QToolButton *button : qAsConst(buttons)) {
127  // For some reason, calling show() is necessary if wordwrap is true,
128  // otherwise the buttons do not show up. It is not needed if
129  // wordwrap is false.
130  button->show();
131  buttonLayout->addWidget(button);
132  }
133  buttonLayout->addWidget(closeButton);
134  layout->addItem(buttonLayout, 1, 0, 1, 2);
135  }
136  } else {
137  QHBoxLayout *layout = new QHBoxLayout(q);
138  layout->addWidget(iconLabel);
139  layout->addWidget(textLabel);
140 
141  for (QToolButton *button : qAsConst(buttons)) {
142  layout->addWidget(button);
143  }
144  layout->addWidget(closeButton);
145  };
146  // Add bordersize to the margin so it starts from the inner border and doesn't look too cramped
147  q->layout()->setContentsMargins(q->layout()->contentsMargins() + borderSize);
148  if (q->isVisible()) {
149  q->setFixedHeight(q->sizeHint().height());
150  }
151  q->updateGeometry();
152 }
153 
154 void KMessageWidgetPrivate::setPalette()
155 {
156  QColor bgBaseColor;
157 
158  // We have to hardcode colors here because KWidgetsAddons is a tier 1 framework
159  // and therefore can't depend on any other KDE Frameworks
160  // The following RGB color values come from the "default" scheme in kcolorscheme.cpp
161  switch (messageType) {
162  case KMessageWidget::Positive:
163  bgBaseColor.setRgb(39, 174, 96); // Window: ForegroundPositive
164  break;
165  case KMessageWidget::Information:
166  bgBaseColor.setRgb(61, 174, 233); // Window: ForegroundActive
167  break;
168  case KMessageWidget::Warning:
169  bgBaseColor.setRgb(246, 116, 0); // Window: ForegroundNeutral
170  break;
171  case KMessageWidget::Error:
172  bgBaseColor.setRgb(218, 68, 83); // Window: ForegroundNegative
173  break;
174  }
175  QPalette palette = q->palette();
176  palette.setColor(QPalette::Window, bgBaseColor);
177  const QColor parentTextColor = (q->parentWidget() ? q->parentWidget()->palette() : qApp->palette()).color(QPalette::WindowText);
178  palette.setColor(QPalette::WindowText, parentTextColor);
179  q->setPalette(palette);
180  // Explicitly set the palettes of the labels because some apps use stylesheets which break the
181  // palette propagation
182  iconLabel->setPalette(palette);
183  textLabel->setPalette(palette);
184  q->style()->polish(q);
185  // update the Icon in case it is recolorable
186  q->setIcon(icon);
187  q->update();
188 }
189 
190 void KMessageWidgetPrivate::updateLayout()
191 {
192  createLayout();
193 }
194 
195 void KMessageWidgetPrivate::slotTimeLineChanged(qreal value)
196 {
197  q->setFixedHeight(qMin(value * 2, qreal(1.0)) * bestContentHeight());
198  q->update();
199 }
200 
201 void KMessageWidgetPrivate::slotTimeLineFinished()
202 {
203  if (timeLine->direction() == QTimeLine::Forward) {
204  q->resize(q->width(), bestContentHeight());
205 
206  // notify about finished animation
207  emit q->showAnimationFinished();
208  } else {
209  // hide and notify about finished animation
210  q->hide();
211  emit q->hideAnimationFinished();
212  }
213 }
214 
215 int KMessageWidgetPrivate::bestContentHeight() const
216 {
217  int height = q->heightForWidth(q->width());
218  if (height == -1) {
219  height = q->sizeHint().height();
220  }
221  return height;
222 }
223 
224 //---------------------------------------------------------------------
225 // KMessageWidget
226 //---------------------------------------------------------------------
228  : QFrame(parent)
229  , d(new KMessageWidgetPrivate)
230 {
231  d->init(this);
232 }
233 
235  : QFrame(parent)
236  , d(new KMessageWidgetPrivate)
237 {
238  d->init(this);
239  setText(text);
240 }
241 
243 {
244  delete d;
245 }
246 
248 {
249  return d->textLabel->text();
250 }
251 
253 {
254  d->textLabel->setText(text);
255  updateGeometry();
256 }
257 
259 {
260  return d->messageType;
261 }
262 
264 {
265  d->messageType = type;
266  d->setPalette();
267 }
268 
270 {
271  ensurePolished();
272  return QFrame::sizeHint();
273 }
274 
276 {
277  ensurePolished();
278  return QFrame::minimumSizeHint();
279 }
280 
281 bool KMessageWidget::event(QEvent *event)
282 {
283  if (event->type() == QEvent::Polish && !layout()) {
284  d->createLayout();
285  } else if (event->type() == QEvent::Show && !d->ignoreShowEventDoingAnimatedShow) {
286  setFixedHeight(d->bestContentHeight());
287  } else if (event->type() == QEvent::ParentChange) {
288  d->setPalette();
289  }
290  return QFrame::event(event);
291 }
292 
293 void KMessageWidget::resizeEvent(QResizeEvent *event)
294 {
295  QFrame::resizeEvent(event);
296  if (d->timeLine->state() == QTimeLine::NotRunning) {
297  setFixedHeight(d->bestContentHeight());
298  }
299 }
300 
302 {
303  ensurePolished();
304  return QFrame::heightForWidth(width);
305 }
306 
307 void KMessageWidget::paintEvent(QPaintEvent *event)
308 {
309  Q_UNUSED(event)
310  QPainter painter(this);
311  if (d->timeLine->state() == QTimeLine::Running) {
312  painter.setOpacity(d->timeLine->currentValue() * d->timeLine->currentValue());
313  }
314  constexpr float radius = 4 * 0.6;
315  const QRect innerRect = rect().marginsRemoved(QMargins() + borderSize / 2);
316  const QColor color = palette().color(QPalette::Window);
317  constexpr float alpha = 0.2;
318  const QColor parentWindowColor = (parentWidget() ? parentWidget()->palette() : qApp->palette()).color(QPalette::Window);
319  const int newRed = (color.red() * alpha) + (parentWindowColor.red() * (1 - alpha));
320  const int newGreen = (color.green() * alpha) + (parentWindowColor.green() * (1 - alpha));
321  const int newBlue = (color.blue() * alpha) + (parentWindowColor.blue() * (1 - alpha));
322  painter.setPen(QPen(color, borderSize));
323  painter.setBrush(QColor(newRed, newGreen, newBlue));
325  painter.drawRoundedRect(innerRect, radius, radius);
326 }
327 
328 bool KMessageWidget::wordWrap() const
329 {
330  return d->wordWrap;
331 }
332 
334 {
335  d->wordWrap = wordWrap;
336  d->textLabel->setWordWrap(wordWrap);
337  QSizePolicy policy = sizePolicy();
338  policy.setHeightForWidth(wordWrap);
339  setSizePolicy(policy);
340  d->updateLayout();
341  // Without this, when user does wordWrap -> !wordWrap -> wordWrap, a minimum
342  // height is set, causing the widget to be too high.
343  // Mostly visible in test programs.
344  if (wordWrap) {
345  setMinimumHeight(0);
346  }
347 }
348 
350 {
351  return d->closeButton->isVisible();
352 }
353 
355 {
356  d->closeButton->setVisible(show);
357  updateGeometry();
358 }
359 
361 {
362  QFrame::addAction(action);
363  d->updateLayout();
364 }
365 
367 {
368  QFrame::removeAction(action);
369  d->updateLayout();
370 }
371 
373 {
374  // Test before styleHint, as there might have been a style change while animation was running
375  if (isHideAnimationRunning()) {
376  d->timeLine->stop();
377  emit hideAnimationFinished();
378  }
379 
380  if (!style()->styleHint(QStyle::SH_Widget_Animate, nullptr, this)
381  || (parentWidget() && !parentWidget()->isVisible())) {
382  show();
383  emit showAnimationFinished();
384  return;
385  }
386 
387  if (isVisible() && (d->timeLine->state() == QTimeLine::NotRunning) && (height() == d->bestContentHeight())) {
388  emit showAnimationFinished();
389  return;
390  }
391 
392  d->ignoreShowEventDoingAnimatedShow = true;
393  show();
394  d->ignoreShowEventDoingAnimatedShow = false;
395  setFixedHeight(0);
396 
397  d->timeLine->setDirection(QTimeLine::Forward);
398  if (d->timeLine->state() == QTimeLine::NotRunning) {
399  d->timeLine->start();
400  }
401 }
402 
404 {
405  // test this before isVisible, as animatedShow might have been called directly before,
406  // so the first timeline event is not yet done and the widget is still hidden
407  // And before styleHint, as there might have been a style change while animation was running
408  if (isShowAnimationRunning()) {
409  d->timeLine->stop();
410  emit showAnimationFinished();
411  }
412 
413  if (!style()->styleHint(QStyle::SH_Widget_Animate, nullptr, this)) {
414  hide();
415  emit hideAnimationFinished();
416  return;
417  }
418 
419  if (!isVisible()) {
420  // explicitly hide it, so it stays hidden in case it is only not visible due to the parents
421  hide();
422  emit hideAnimationFinished();
423  return;
424  }
425 
426  d->timeLine->setDirection(QTimeLine::Backward);
427  if (d->timeLine->state() == QTimeLine::NotRunning) {
428  d->timeLine->start();
429  }
430 }
431 
433 {
434  return (d->timeLine->direction() == QTimeLine::Backward)
435  && (d->timeLine->state() == QTimeLine::Running);
436 }
437 
439 {
440  return (d->timeLine->direction() == QTimeLine::Forward)
441  && (d->timeLine->state() == QTimeLine::Running);
442 }
443 
445 {
446  return d->icon;
447 }
448 
450 {
451  d->icon = icon;
452  if (d->icon.isNull()) {
453  d->iconLabel->hide();
454  } else {
456  d->iconLabel->setPixmap(d->icon.pixmap(size));
457  d->iconLabel->show();
458  }
459 }
460 
461 #include "moc_kmessagewidget.cpp"
462 
QLayout * layout() const const
void setText(const QString &text)
void setWordWrap(bool wordWrap)
Set word wrap to wordWrap.
bool isHideAnimationRunning() const
Check whether the hide animation started by calling animatedHide() is still running.
void setCloseButtonVisible(bool visible)
Set the visibility of the close button.
virtual int heightForWidth(int w) const const
void setOpacity(qreal opacity)
void triggered(bool checked)
QEvent::Type type() const const
const QPalette & palette() const const
void setRenderHint(QPainter::RenderHint hint, bool on)
void updateGeometry()
void setColor(QPalette::ColorGroup group, QPalette::ColorRole role, const QColor &color)
void addWidget(QWidget *widget, int row, int column, Qt::Alignment alignment)
bool isShowAnimationRunning() const
Check whether the show animation started by calling animatedShow() is still running.
void addAction(QAction *action)
void setDefaultAction(QAction *action)
QStyle * style() const const
virtual int pixelMetric(QStyle::PixelMetric metric, const QStyleOption *option, const QWidget *widget) const const =0
QString text() const
Get the text of this message widget.
void hideAnimationFinished()
This signal is emitted when the hide animation is finished, started by calling animatedHide().
void setIcon(const QIcon &icon)
const QColor & color(QPalette::ColorGroup group, QPalette::ColorRole role) const const
MessageType
Available message types.
void addAction(QAction *action)
Add action to the message widget.
bool isVisible() const const
SH_Widget_Animate
void ensurePolished() const const
MessageType messageType() const
Get the type of this message.
virtual QSize minimumSizeHint() const const
QSize sizeHint() const override
Returns the preferred size of the message widget.
void setIcon(const QIcon &icon)
Define an icon to be shown on the left of the text.
void setRgb(int r, int g, int b, int a)
virtual QSize sizeHint() const const override
QRect marginsRemoved(const QMargins &margins) const const
QString tr(const char *sourceText, const char *disambiguation, int n)
void showAnimationFinished()
This signal is emitted when the show animation is finished, started by calling animatedShow().
AlignHCenter
void setToolTip(const QString &tip)
void linkHovered(const QString &link)
QIcon icon() const
The icon shown on the left of the text.
void animatedHide()
Hide the widget using an animation.
void initFrom(const QWidget *widget)
int width() const const
~KMessageWidget() override
Destructor.
QSize size() const const
void addWidget(QWidget *widget, int stretch, Qt::Alignment alignment)
int heightForWidth(int width) const override
Returns the required height for width.
int red() const const
void setPen(const QColor &color)
void drawRoundedRect(const QRectF &rect, qreal xRadius, qreal yRadius, Qt::SizeMode mode)
KMessageWidget(QWidget *parent=nullptr)
Constructs a KMessageWidget with the specified parent.
bool isCloseButtonVisible() const
Check whether the close button is visible.
void setBrush(const QBrush &brush)
void linkActivated(const QString &link)
ToolButtonTextBesideIcon
void removeAction(QAction *action)
Remove action from the message widget.
void hide()
TextBrowserInteraction
void animatedShow()
Show the widget using an animation.
QSizePolicy sizePolicy() const const
bool wordWrap() const
Check whether word wrap is enabled.
QRect rect() const const
int green() const const
QSize minimumSizeHint() const override
Returns the minimum size of the message widget.
QCA_EXPORT void init()
void linkActivated(const QString &contents)
This signal is emitted when the user clicks a link in the text label.
int blue() const const
PM_ToolBarIconSize
void addStretch(int stretch)
void setFixedHeight(int h)
QWidget * parentWidget() const const
void addItem(QLayoutItem *item, int row, int column, int rowSpan, int columnSpan, Qt::Alignment alignment)
void setMessageType(KMessageWidget::MessageType type)
Set the message type to type.
void removeAction(QAction *action)
void setMinimumHeight(int minh)
SP_DialogCloseButton
void show()
virtual void resizeEvent(QResizeEvent *event)
A widget to provide feedback or propose opportunistic interactions.
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
QObject * parent() const const
void setText(const QString &text)
Set the text of the message widget to text.
void setToolButtonStyle(Qt::ToolButtonStyle style)
virtual bool event(QEvent *e) override
int height() const const
void paletteChanged(const QPalette &palette)
void setHeightForWidth(bool dependent)
void linkHovered(const QString &contents)
This signal is emitted when the user hovers over a link in the text label.
This file is part of the KDE documentation.
Documentation copyright © 1996-2020 The KDE developers.
Generated on Wed Aug 5 2020 22:42:23 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.