File indexing completed on 2025-02-16 13:11:45
0001 /* 0002 This file is part of the KDE libraries 0003 0004 SPDX-FileCopyrightText: 2011 Aurélien Gâteau <agateau@kde.org> 0005 SPDX-FileCopyrightText: 2014 Dominik Haumann <dhaumann@kde.org> 0006 0007 SPDX-License-Identifier: LGPL-2.1-or-later 0008 */ 0009 #include "kmessagewidget.h" 0010 0011 #include <QAction> 0012 #include <QApplication> 0013 #include <QEvent> 0014 #include <QGridLayout> 0015 #include <QGuiApplication> 0016 #include <QHBoxLayout> 0017 #include <QLabel> 0018 #include <QPainter> 0019 #include <QShowEvent> 0020 #include <QStyle> 0021 #include <QStyleOption> 0022 #include <QTimeLine> 0023 #include <QToolButton> 0024 //--------------------------------------------------------------------- 0025 // KMessageWidgetPrivate 0026 //--------------------------------------------------------------------- 0027 0028 constexpr int borderSize = 2; 0029 0030 class KMessageWidgetPrivate 0031 { 0032 public: 0033 void init(KMessageWidget *); 0034 0035 KMessageWidget *q; 0036 QLabel *iconLabel = nullptr; 0037 QLabel *textLabel = nullptr; 0038 QToolButton *closeButton = nullptr; 0039 QTimeLine *timeLine = nullptr; 0040 QIcon icon; 0041 bool ignoreShowAndResizeEventDoingAnimatedShow = false; 0042 KMessageWidget::MessageType messageType; 0043 bool wordWrap; 0044 QList<QToolButton *> buttons; 0045 0046 void createLayout(); 0047 void setPalette(); 0048 void updateLayout(); 0049 void slotTimeLineChanged(qreal); 0050 void slotTimeLineFinished(); 0051 int bestContentHeight() const; 0052 }; 0053 0054 void KMessageWidgetPrivate::init(KMessageWidget *q_ptr) 0055 { 0056 q = q_ptr; 0057 // Note: when changing the value 500, also update KMessageWidgetTest 0058 timeLine = new QTimeLine(500, q); 0059 QObject::connect(timeLine, &QTimeLine::valueChanged, q, [this](qreal value) { 0060 slotTimeLineChanged(value); 0061 }); 0062 QObject::connect(timeLine, &QTimeLine::finished, q, [this]() { 0063 slotTimeLineFinished(); 0064 }); 0065 0066 wordWrap = false; 0067 0068 iconLabel = new QLabel(q); 0069 iconLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); 0070 iconLabel->hide(); 0071 0072 textLabel = new QLabel(q); 0073 textLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); 0074 textLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); 0075 QObject::connect(textLabel, &QLabel::linkActivated, q, &KMessageWidget::linkActivated); 0076 QObject::connect(textLabel, &QLabel::linkHovered, q, &KMessageWidget::linkHovered); 0077 0078 QAction *closeAction = new QAction(q); 0079 closeAction->setText(KMessageWidget::tr("&Close", "@action:button")); 0080 closeAction->setToolTip(KMessageWidget::tr("Close message", "@info:tooltip")); 0081 QStyleOptionFrame opt; 0082 opt.initFrom(q); 0083 closeAction->setIcon(q->style()->standardIcon(QStyle::SP_DialogCloseButton, &opt, q)); 0084 0085 QObject::connect(closeAction, &QAction::triggered, q, &KMessageWidget::animatedHide); 0086 0087 closeButton = new QToolButton(q); 0088 closeButton->setAutoRaise(true); 0089 closeButton->setDefaultAction(closeAction); 0090 0091 q->setMessageType(KMessageWidget::Information); 0092 0093 q->connect(qApp, &QApplication::paletteChanged, q, [this] { 0094 KMessageWidgetPrivate::setPalette(); 0095 }); 0096 } 0097 0098 void KMessageWidgetPrivate::createLayout() 0099 { 0100 delete q->layout(); 0101 0102 qDeleteAll(buttons); 0103 buttons.clear(); 0104 0105 const auto actions = q->actions(); 0106 buttons.reserve(actions.size()); 0107 for (QAction *action : actions) { 0108 QToolButton *button = new QToolButton(q); 0109 button->setDefaultAction(action); 0110 button->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); 0111 auto previous = buttons.isEmpty() ? static_cast<QWidget *>(textLabel) : buttons.back(); 0112 QWidget::setTabOrder(previous, button); 0113 buttons.append(button); 0114 } 0115 0116 // AutoRaise reduces visual clutter, but we don't want to turn it on if 0117 // there are other buttons, otherwise the close button will look different 0118 // from the others. 0119 closeButton->setAutoRaise(buttons.isEmpty()); 0120 if (wordWrap) { 0121 QGridLayout *layout = new QGridLayout(q); 0122 // Set alignment to make sure icon does not move down if text wraps 0123 layout->addWidget(iconLabel, 0, 0, 1, 1, Qt::AlignHCenter | Qt::AlignTop); 0124 layout->addWidget(textLabel, 0, 1); 0125 0126 if (buttons.isEmpty()) { 0127 // Use top-vertical alignment like the icon does. 0128 layout->addWidget(closeButton, 0, 2, 1, 1, Qt::AlignHCenter | Qt::AlignTop); 0129 } else { 0130 // Use an additional layout in row 1 for the buttons. 0131 QHBoxLayout *buttonLayout = new QHBoxLayout; 0132 buttonLayout->addStretch(); 0133 for (QToolButton *button : std::as_const(buttons)) { 0134 // For some reason, calling show() is necessary if wordwrap is true, 0135 // otherwise the buttons do not show up. It is not needed if 0136 // wordwrap is false. 0137 button->show(); 0138 buttonLayout->addWidget(button); 0139 } 0140 buttonLayout->addWidget(closeButton); 0141 layout->addItem(buttonLayout, 1, 0, 1, 2); 0142 } 0143 } else { 0144 QHBoxLayout *layout = new QHBoxLayout(q); 0145 layout->addWidget(iconLabel, 0, Qt::AlignTop); 0146 layout->addWidget(textLabel); 0147 0148 for (QToolButton *button : std::as_const(buttons)) { 0149 layout->addWidget(button, 0, Qt::AlignTop); 0150 } 0151 layout->addWidget(closeButton, 0, Qt::AlignTop); 0152 }; 0153 // Add bordersize to the margin so it starts from the inner border and doesn't look too cramped 0154 q->layout()->setContentsMargins(q->layout()->contentsMargins() + borderSize); 0155 if (q->isVisible()) { 0156 q->setFixedHeight(q->sizeHint().height()); 0157 } 0158 q->updateGeometry(); 0159 } 0160 0161 void KMessageWidgetPrivate::setPalette() 0162 { 0163 QColor bgBaseColor; 0164 0165 // We have to hardcode colors here because KWidgetsAddons is a tier 1 framework 0166 // and therefore can't depend on any other KDE Frameworks 0167 // The following RGB color values come from the "default" scheme in kcolorscheme.cpp 0168 switch (messageType) { 0169 case KMessageWidget::Positive: 0170 bgBaseColor.setRgb(39, 174, 96); // Window: ForegroundPositive 0171 break; 0172 case KMessageWidget::Information: 0173 bgBaseColor.setRgb(61, 174, 233); // Window: ForegroundActive 0174 break; 0175 case KMessageWidget::Warning: 0176 bgBaseColor.setRgb(246, 116, 0); // Window: ForegroundNeutral 0177 break; 0178 case KMessageWidget::Error: 0179 bgBaseColor.setRgb(218, 68, 83); // Window: ForegroundNegative 0180 break; 0181 } 0182 QPalette palette = q->palette(); 0183 palette.setColor(QPalette::Window, bgBaseColor); 0184 const QColor parentTextColor = (q->parentWidget() ? q->parentWidget()->palette() : qApp->palette()).color(QPalette::WindowText); 0185 palette.setColor(QPalette::WindowText, parentTextColor); 0186 q->setPalette(palette); 0187 // Explicitly set the palettes of the labels because some apps use stylesheets which break the 0188 // palette propagation 0189 iconLabel->setPalette(palette); 0190 textLabel->setPalette(palette); 0191 q->style()->polish(q); 0192 // update the Icon in case it is recolorable 0193 q->setIcon(icon); 0194 q->update(); 0195 } 0196 0197 void KMessageWidgetPrivate::updateLayout() 0198 { 0199 createLayout(); 0200 } 0201 0202 void KMessageWidgetPrivate::slotTimeLineChanged(qreal value) 0203 { 0204 q->setFixedHeight(qMin(value * 2, qreal(1.0)) * bestContentHeight()); 0205 q->update(); 0206 } 0207 0208 void KMessageWidgetPrivate::slotTimeLineFinished() 0209 { 0210 if (timeLine->direction() == QTimeLine::Forward) { 0211 q->resize(q->width(), bestContentHeight()); 0212 0213 // notify about finished animation 0214 Q_EMIT q->showAnimationFinished(); 0215 } else { 0216 // hide and notify about finished animation 0217 q->hide(); 0218 Q_EMIT q->hideAnimationFinished(); 0219 } 0220 } 0221 0222 int KMessageWidgetPrivate::bestContentHeight() const 0223 { 0224 int height = q->heightForWidth(q->width()); 0225 if (height == -1) { 0226 height = q->sizeHint().height(); 0227 } 0228 return height; 0229 } 0230 0231 //--------------------------------------------------------------------- 0232 // KMessageWidget 0233 //--------------------------------------------------------------------- 0234 KMessageWidget::KMessageWidget(QWidget *parent) 0235 : QFrame(parent) 0236 , d(new KMessageWidgetPrivate) 0237 { 0238 d->init(this); 0239 } 0240 0241 KMessageWidget::KMessageWidget(const QString &text, QWidget *parent) 0242 : QFrame(parent) 0243 , d(new KMessageWidgetPrivate) 0244 { 0245 d->init(this); 0246 setText(text); 0247 } 0248 0249 KMessageWidget::~KMessageWidget() = default; 0250 0251 QString KMessageWidget::text() const 0252 { 0253 return d->textLabel->text(); 0254 } 0255 0256 void KMessageWidget::setText(const QString &text) 0257 { 0258 d->textLabel->setText(text); 0259 updateGeometry(); 0260 } 0261 0262 KMessageWidget::MessageType KMessageWidget::messageType() const 0263 { 0264 return d->messageType; 0265 } 0266 0267 void KMessageWidget::setMessageType(KMessageWidget::MessageType type) 0268 { 0269 d->messageType = type; 0270 d->setPalette(); 0271 } 0272 0273 QSize KMessageWidget::sizeHint() const 0274 { 0275 ensurePolished(); 0276 return QFrame::sizeHint(); 0277 } 0278 0279 QSize KMessageWidget::minimumSizeHint() const 0280 { 0281 ensurePolished(); 0282 return QFrame::minimumSizeHint(); 0283 } 0284 0285 bool KMessageWidget::event(QEvent *event) 0286 { 0287 if (event->type() == QEvent::Polish && !layout()) { 0288 d->createLayout(); 0289 } else if ((event->type() == QEvent::Show && !d->ignoreShowAndResizeEventDoingAnimatedShow) || (event->type() == QEvent::LayoutRequest)) { 0290 setFixedHeight(d->bestContentHeight()); 0291 } else if (event->type() == QEvent::ParentChange) { 0292 d->setPalette(); 0293 } 0294 return QFrame::event(event); 0295 } 0296 0297 void KMessageWidget::resizeEvent(QResizeEvent *event) 0298 { 0299 QFrame::resizeEvent(event); 0300 if (d->timeLine->state() == QTimeLine::NotRunning && d->ignoreShowAndResizeEventDoingAnimatedShow) { 0301 setFixedHeight(d->bestContentHeight()); 0302 } 0303 } 0304 0305 int KMessageWidget::heightForWidth(int width) const 0306 { 0307 ensurePolished(); 0308 return QFrame::heightForWidth(width); 0309 } 0310 0311 void KMessageWidget::paintEvent(QPaintEvent *event) 0312 { 0313 Q_UNUSED(event) 0314 QPainter painter(this); 0315 if (d->timeLine->state() == QTimeLine::Running) { 0316 painter.setOpacity(d->timeLine->currentValue() * d->timeLine->currentValue()); 0317 } 0318 constexpr float radius = 4 * 0.6; 0319 const QRect innerRect = rect().marginsRemoved(QMargins() + borderSize / 2); 0320 const QColor color = palette().color(QPalette::Window); 0321 constexpr float alpha = 0.2; 0322 const QColor parentWindowColor = (parentWidget() ? parentWidget()->palette() : qApp->palette()).color(QPalette::Window); 0323 const int newRed = (color.red() * alpha) + (parentWindowColor.red() * (1 - alpha)); 0324 const int newGreen = (color.green() * alpha) + (parentWindowColor.green() * (1 - alpha)); 0325 const int newBlue = (color.blue() * alpha) + (parentWindowColor.blue() * (1 - alpha)); 0326 painter.setPen(QPen(color, borderSize)); 0327 painter.setBrush(QColor(newRed, newGreen, newBlue)); 0328 painter.setRenderHint(QPainter::Antialiasing); 0329 painter.drawRoundedRect(innerRect, radius, radius); 0330 } 0331 0332 bool KMessageWidget::wordWrap() const 0333 { 0334 return d->wordWrap; 0335 } 0336 0337 void KMessageWidget::setWordWrap(bool wordWrap) 0338 { 0339 d->wordWrap = wordWrap; 0340 d->textLabel->setWordWrap(wordWrap); 0341 QSizePolicy policy = sizePolicy(); 0342 policy.setHeightForWidth(wordWrap); 0343 setSizePolicy(policy); 0344 d->updateLayout(); 0345 // Without this, when user does wordWrap -> !wordWrap -> wordWrap, a minimum 0346 // height is set, causing the widget to be too high. 0347 // Mostly visible in test programs. 0348 if (wordWrap) { 0349 setMinimumHeight(0); 0350 } 0351 } 0352 0353 bool KMessageWidget::isCloseButtonVisible() const 0354 { 0355 return d->closeButton->isVisible(); 0356 } 0357 0358 void KMessageWidget::setCloseButtonVisible(bool show) 0359 { 0360 d->closeButton->setVisible(show); 0361 updateGeometry(); 0362 } 0363 0364 void KMessageWidget::addAction(QAction *action) 0365 { 0366 QFrame::addAction(action); 0367 d->updateLayout(); 0368 } 0369 0370 void KMessageWidget::removeAction(QAction *action) 0371 { 0372 QFrame::removeAction(action); 0373 d->updateLayout(); 0374 } 0375 0376 void KMessageWidget::clearActions() 0377 { 0378 const auto ourActions = actions(); 0379 for (auto *action : ourActions) { 0380 removeAction(action); 0381 } 0382 d->updateLayout(); 0383 } 0384 0385 void KMessageWidget::animatedShow() 0386 { 0387 // Test before styleHint, as there might have been a style change while animation was running 0388 if (isHideAnimationRunning()) { 0389 d->timeLine->stop(); 0390 Q_EMIT hideAnimationFinished(); 0391 } 0392 0393 if (!style()->styleHint(QStyle::SH_Widget_Animate, nullptr, this) || (parentWidget() && !parentWidget()->isVisible())) { 0394 show(); 0395 Q_EMIT showAnimationFinished(); 0396 return; 0397 } 0398 0399 if (isVisible() && (d->timeLine->state() == QTimeLine::NotRunning) && (height() == d->bestContentHeight())) { 0400 Q_EMIT showAnimationFinished(); 0401 return; 0402 } 0403 0404 d->ignoreShowAndResizeEventDoingAnimatedShow = true; 0405 show(); 0406 d->ignoreShowAndResizeEventDoingAnimatedShow = false; 0407 setFixedHeight(0); 0408 0409 d->timeLine->setDirection(QTimeLine::Forward); 0410 if (d->timeLine->state() == QTimeLine::NotRunning) { 0411 d->timeLine->start(); 0412 } 0413 } 0414 0415 void KMessageWidget::animatedHide() 0416 { 0417 // test this before isVisible, as animatedShow might have been called directly before, 0418 // so the first timeline event is not yet done and the widget is still hidden 0419 // And before styleHint, as there might have been a style change while animation was running 0420 if (isShowAnimationRunning()) { 0421 d->timeLine->stop(); 0422 Q_EMIT showAnimationFinished(); 0423 } 0424 0425 if (!style()->styleHint(QStyle::SH_Widget_Animate, nullptr, this)) { 0426 hide(); 0427 Q_EMIT hideAnimationFinished(); 0428 return; 0429 } 0430 0431 if (!isVisible()) { 0432 // explicitly hide it, so it stays hidden in case it is only not visible due to the parents 0433 hide(); 0434 Q_EMIT hideAnimationFinished(); 0435 return; 0436 } 0437 0438 d->timeLine->setDirection(QTimeLine::Backward); 0439 if (d->timeLine->state() == QTimeLine::NotRunning) { 0440 d->timeLine->start(); 0441 } 0442 } 0443 0444 bool KMessageWidget::isHideAnimationRunning() const 0445 { 0446 return (d->timeLine->direction() == QTimeLine::Backward) && (d->timeLine->state() == QTimeLine::Running); 0447 } 0448 0449 bool KMessageWidget::isShowAnimationRunning() const 0450 { 0451 return (d->timeLine->direction() == QTimeLine::Forward) && (d->timeLine->state() == QTimeLine::Running); 0452 } 0453 0454 QIcon KMessageWidget::icon() const 0455 { 0456 return d->icon; 0457 } 0458 0459 void KMessageWidget::setIcon(const QIcon &icon) 0460 { 0461 d->icon = icon; 0462 if (d->icon.isNull()) { 0463 d->iconLabel->hide(); 0464 } else { 0465 const int size = style()->pixelMetric(QStyle::PM_ToolBarIconSize); 0466 d->iconLabel->setPixmap(d->icon.pixmap(size)); 0467 d->iconLabel->show(); 0468 } 0469 } 0470 0471 #include "moc_kmessagewidget.cpp"