File indexing completed on 2024-04-28 15:32:06

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"