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"