File indexing completed on 2024-04-28 03:59:08

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