File indexing completed on 2024-04-28 15:29:10

0001 /*
0002     SPDX-FileCopyrightText: 2001-2006 Richard Moore <rich@kde.org>
0003     SPDX-FileCopyrightText: 2004-2005 Sascha Cunz <sascha.cunz@tiscali.de>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "kpassivepopup.h"
0009 
0010 #include "debug_p.h"
0011 #include <config-knotifications.h>
0012 
0013 #if KNOTIFICATIONS_BUILD_DEPRECATED_SINCE(5, 79)
0014 // Qt
0015 #include <QBitmap>
0016 #include <QBoxLayout>
0017 #include <QGuiApplication>
0018 #include <QLabel>
0019 #include <QMouseEvent>
0020 #include <QPainter>
0021 #include <QPainterPath>
0022 #include <QScreen>
0023 #include <QStyle>
0024 #include <QSystemTrayIcon>
0025 #include <QTimer>
0026 #include <QToolTip>
0027 
0028 #if HAVE_X11
0029 #include <QX11Info>
0030 #include <netwm.h>
0031 #endif
0032 
0033 #if HAVE_KWINDOWSYSTEM
0034 #include <KWindowInfo>
0035 #endif
0036 
0037 
0038 static const int DEFAULT_POPUP_TYPE = KPassivePopup::Boxed;
0039 static const int DEFAULT_POPUP_TIME = 6 * 1000;
0040 static const Qt::WindowFlags POPUP_FLAGS = Qt::Tool | Qt::WindowStaysOnTopHint | Qt::FramelessWindowHint;
0041 
0042 class Q_DECL_HIDDEN KPassivePopup::Private
0043 {
0044 public:
0045     Private(KPassivePopup *q, WId winId)
0046         : q(q)
0047         , popupStyle(DEFAULT_POPUP_TYPE)
0048         , window(winId)
0049         , hideDelay(DEFAULT_POPUP_TIME)
0050         , hideTimer(new QTimer(q))
0051         , autoDelete(false)
0052     {
0053 #if HAVE_X11
0054         if (QX11Info::isPlatformX11()) {
0055             q->setWindowFlags(POPUP_FLAGS | Qt::X11BypassWindowManagerHint);
0056         } else
0057 #else
0058         q->setWindowFlags(POPUP_FLAGS);
0059 #endif
0060             q->setFrameStyle(QFrame::Box | QFrame::Plain);
0061         q->setLineWidth(2);
0062 
0063         if (popupStyle == Boxed) {
0064             q->setFrameStyle(QFrame::Box | QFrame::Plain);
0065             q->setLineWidth(2);
0066         } else if (popupStyle == Balloon) {
0067             q->setPalette(QToolTip::palette());
0068         }
0069         connect(hideTimer, &QTimer::timeout, q, &QWidget::hide);
0070         connect(q, qOverload<>(&KPassivePopup::clicked), q, &QWidget::hide);
0071     }
0072 
0073     KPassivePopup *const q;
0074 
0075     int popupStyle;
0076     QPolygon surround;
0077     QPoint anchor;
0078     QPoint fixedPosition;
0079 
0080     WId window;
0081     QWidget *msgView = nullptr;
0082     QBoxLayout *topLayout = nullptr;
0083     int hideDelay;
0084     QTimer *hideTimer = nullptr;
0085 
0086     QLabel *ttlIcon = nullptr;
0087     QLabel *ttl = nullptr;
0088     QLabel *msg = nullptr;
0089 
0090     bool autoDelete = false;
0091 
0092     /**
0093      * Updates the transparency mask. Unused if PopupStyle == Boxed
0094      */
0095     void updateMask()
0096     {
0097         // get screen-geometry for screen our anchor is on
0098         // (geometry can differ from screen to screen!
0099         QRect deskRect = desktopRectForPoint(anchor);
0100 
0101         const int width = q->width();
0102         const int height = q->height();
0103 
0104         int xh = 70;
0105         int xl = 40;
0106         if (width < 80) {
0107             xh = xl = 40;
0108         } else if (width < 110) {
0109             xh = width - 40;
0110         }
0111 
0112         bool bottom = (anchor.y() + height) > ((deskRect.y() + deskRect.height() - 48));
0113         bool right = (anchor.x() + width) > ((deskRect.x() + deskRect.width() - 48));
0114 
0115         /* clang-format off */
0116         QPoint corners[4] = {QPoint(width - 50, 10),
0117                              QPoint(10, 10),
0118                              QPoint(10, height - 50),
0119                              QPoint(width - 50, height - 50)};
0120         /* clang-format on */
0121 
0122         QBitmap mask(width, height);
0123         mask.clear();
0124         QPainter p(&mask);
0125         QBrush brush(Qt::color1, Qt::SolidPattern);
0126         p.setBrush(brush);
0127 
0128         int i = 0;
0129         int z = 0;
0130         for (; i < 4; ++i) {
0131             QPainterPath path;
0132             path.moveTo(corners[i].x(), corners[i].y());
0133             path.arcTo(corners[i].x(), corners[i].y(), 40, 40, i * 90, 90);
0134             QPolygon corner = path.toFillPolygon().toPolygon();
0135 
0136             surround.resize(z + corner.count() - 1);
0137             for (int s = 1; s < corner.count() - 1; ++s, ++z) {
0138                 surround.setPoint(z, corner[s]);
0139             }
0140 
0141             if (bottom && i == 2) {
0142                 if (right) {
0143                     surround.resize(z + 3);
0144                     surround.setPoint(z++, QPoint(width - xh, height - 10));
0145                     surround.setPoint(z++, QPoint(width - 20, height));
0146                     surround.setPoint(z++, QPoint(width - xl, height - 10));
0147                 } else {
0148                     surround.resize(z + 3);
0149                     surround.setPoint(z++, QPoint(xl, height - 10));
0150                     surround.setPoint(z++, QPoint(20, height));
0151                     surround.setPoint(z++, QPoint(xh, height - 10));
0152                 }
0153             } else if (!bottom && i == 0) {
0154                 if (right) {
0155                     surround.resize(z + 3);
0156                     surround.setPoint(z++, QPoint(width - xl, 10));
0157                     surround.setPoint(z++, QPoint(width - 20, 0));
0158                     surround.setPoint(z++, QPoint(width - xh, 10));
0159                 } else {
0160                     surround.resize(z + 3);
0161                     surround.setPoint(z++, QPoint(xh, 10));
0162                     surround.setPoint(z++, QPoint(20, 0));
0163                     surround.setPoint(z++, QPoint(xl, 10));
0164                 }
0165             }
0166         }
0167 
0168         surround.resize(z + 1);
0169         surround.setPoint(z, surround[0]);
0170         p.drawPolygon(surround);
0171         q->setMask(mask);
0172 
0173         q->move(right ? anchor.x() - width + 20 : (anchor.x() < 11 ? 11 : anchor.x() - 20), //
0174                 bottom ? anchor.y() - height : (anchor.y() < 11 ? 11 : anchor.y()));
0175 
0176         q->update();
0177     }
0178 
0179     /**
0180      * Calculates the position to place the popup near the specified rectangle.
0181      */
0182     QPoint calculateNearbyPoint(const QRect &target)
0183     {
0184         QPoint pos = target.topLeft();
0185         int x = pos.x();
0186         int y = pos.y();
0187         int w = q->minimumSizeHint().width();
0188         int h = q->minimumSizeHint().height();
0189 
0190         QRect r = desktopRectForPoint(QPoint(x + w / 2, y + h / 2));
0191 
0192         if (popupStyle == Balloon) {
0193             // find a point to anchor to
0194             if (x + w > r.width()) {
0195                 x = x + target.width();
0196             }
0197 
0198             if (y + h > r.height()) {
0199                 y = y + target.height();
0200             }
0201         } else {
0202             if (x < r.center().x()) {
0203                 x = x + target.width();
0204             } else {
0205                 x = x - w;
0206             }
0207 
0208             // It's apparently trying to go off screen, so display it ALL at the bottom.
0209             if ((y + h) > r.bottom()) {
0210                 y = r.bottom() - h;
0211             }
0212 
0213             if ((x + w) > r.right()) {
0214                 x = r.right() - w;
0215             }
0216         }
0217         if (y < r.top()) {
0218             y = r.top();
0219         }
0220 
0221         if (x < r.left()) {
0222             x = r.left();
0223         }
0224 
0225         return QPoint(x, y);
0226     }
0227 
0228     QRect desktopRectForPoint(const QPoint &point)
0229     {
0230         const QList<QScreen *> screens = QGuiApplication::screens();
0231         for (const QScreen *screen : screens) {
0232             if (screen->geometry().contains(point)) {
0233                 return screen->geometry();
0234             }
0235         }
0236 
0237         // If no screen was found, return the primary screen's geometry
0238         return QGuiApplication::primaryScreen()->geometry();
0239     }
0240 };
0241 
0242 KPassivePopup::KPassivePopup(QWidget *parent, Qt::WindowFlags f)
0243     : QFrame(nullptr, f ? f : POPUP_FLAGS)
0244     , d(new Private(this, parent ? parent->effectiveWinId() : 0L))
0245 {
0246 }
0247 
0248 KPassivePopup::KPassivePopup(WId win)
0249     : QFrame(nullptr)
0250     , d(new Private(this, win))
0251 {
0252 }
0253 
0254 KPassivePopup::~KPassivePopup() = default;
0255 
0256 void KPassivePopup::setPopupStyle(int popupstyle)
0257 {
0258     if (d->popupStyle == popupstyle) {
0259         return;
0260     }
0261 
0262     d->popupStyle = popupstyle;
0263     if (d->popupStyle == Boxed) {
0264         setFrameStyle(QFrame::Box | QFrame::Plain);
0265         setLineWidth(2);
0266     } else if (d->popupStyle == Balloon) {
0267         setPalette(QToolTip::palette());
0268     }
0269 }
0270 
0271 void KPassivePopup::setView(QWidget *child)
0272 {
0273     delete d->msgView;
0274     d->msgView = child;
0275 
0276     delete d->topLayout;
0277     d->topLayout = new QVBoxLayout(this);
0278     if (d->popupStyle == Balloon) {
0279         auto *style = this->style();
0280         const int leftMarginHint = 2 * style->pixelMetric(QStyle::PM_LayoutLeftMargin);
0281         const int topMarginHint = 2 * style->pixelMetric(QStyle::PM_LayoutTopMargin);
0282         const int rightMarginHint = 2 * style->pixelMetric(QStyle::PM_LayoutRightMargin);
0283         const int bottomMarginHint = 2 * style->pixelMetric(QStyle::PM_LayoutBottomMargin);
0284         d->topLayout->setContentsMargins(leftMarginHint, topMarginHint, rightMarginHint, bottomMarginHint);
0285     }
0286     d->topLayout->addWidget(d->msgView);
0287     d->topLayout->activate();
0288 }
0289 
0290 void KPassivePopup::setView(const QString &caption, const QString &text, const QPixmap &icon)
0291 {
0292     // qCDebug(LOG_KNOTIFICATIONS) << "KPassivePopup::setView " << caption << ", " << text;
0293     setView(standardView(caption, text, icon, this));
0294 }
0295 
0296 QWidget *KPassivePopup::standardView(const QString &caption, const QString &text, const QPixmap &icon, QWidget *parent)
0297 {
0298     QWidget *top = new QWidget(parent ? parent : this);
0299     QVBoxLayout *vb = new QVBoxLayout(top);
0300     vb->setContentsMargins(0, 0, 0, 0);
0301 
0302     QHBoxLayout *hb = nullptr;
0303     if (!icon.isNull()) {
0304         hb = new QHBoxLayout;
0305         hb->setContentsMargins(0, 0, 0, 0);
0306         vb->addLayout(hb);
0307         d->ttlIcon = new QLabel(top);
0308         d->ttlIcon->setPixmap(icon);
0309         d->ttlIcon->setAlignment(Qt::AlignLeft);
0310         hb->addWidget(d->ttlIcon);
0311     }
0312 
0313     if (!caption.isEmpty()) {
0314         d->ttl = new QLabel(caption, top);
0315         QFont fnt = d->ttl->font();
0316         fnt.setBold(true);
0317         d->ttl->setFont(fnt);
0318         d->ttl->setAlignment(Qt::AlignHCenter);
0319 
0320         if (hb) {
0321             hb->addWidget(d->ttl);
0322             hb->setStretchFactor(d->ttl, 10); // enforce centering
0323         } else {
0324             vb->addWidget(d->ttl);
0325         }
0326     }
0327 
0328     if (!text.isEmpty()) {
0329         d->msg = new QLabel(text, top);
0330         d->msg->setAlignment(Qt::AlignLeft);
0331         d->msg->setTextInteractionFlags(Qt::LinksAccessibleByMouse);
0332         d->msg->setOpenExternalLinks(true);
0333         vb->addWidget(d->msg);
0334     }
0335 
0336     return top;
0337 }
0338 
0339 void KPassivePopup::setView(const QString &caption, const QString &text)
0340 {
0341     setView(caption, text, QPixmap());
0342 }
0343 
0344 QWidget *KPassivePopup::view() const
0345 {
0346     return d->msgView;
0347 }
0348 
0349 int KPassivePopup::timeout() const
0350 {
0351     return d->hideDelay;
0352 }
0353 
0354 void KPassivePopup::setTimeout(int delay)
0355 {
0356     d->hideDelay = delay < 0 ? DEFAULT_POPUP_TIME : delay;
0357     if (d->hideTimer->isActive()) {
0358         if (delay) {
0359             d->hideTimer->start(delay);
0360         } else {
0361             d->hideTimer->stop();
0362         }
0363     }
0364 }
0365 
0366 bool KPassivePopup::autoDelete() const
0367 {
0368     return d->autoDelete;
0369 }
0370 
0371 void KPassivePopup::setAutoDelete(bool autoDelete)
0372 {
0373     d->autoDelete = autoDelete;
0374 }
0375 
0376 void KPassivePopup::mouseReleaseEvent(QMouseEvent *e)
0377 {
0378     Q_EMIT clicked();
0379     Q_EMIT clicked(e->pos());
0380 }
0381 
0382 //
0383 // Main Implementation
0384 //
0385 
0386 void KPassivePopup::setVisible(bool visible)
0387 {
0388     if (!visible) {
0389         QFrame::setVisible(visible);
0390         return;
0391     }
0392 
0393     if (size() != sizeHint()) {
0394         resize(sizeHint());
0395     }
0396 
0397     if (d->fixedPosition.isNull()) {
0398         positionSelf();
0399     } else {
0400         if (d->popupStyle == Balloon) {
0401             setAnchor(d->fixedPosition);
0402         } else {
0403             move(d->fixedPosition);
0404         }
0405     }
0406     QFrame::setVisible(/*visible=*/true);
0407 
0408     int delay = d->hideDelay;
0409     if (delay < 0) {
0410         delay = DEFAULT_POPUP_TIME;
0411     }
0412 
0413     if (delay > 0) {
0414         d->hideTimer->start(delay);
0415     }
0416 }
0417 
0418 void KPassivePopup::show(const QPoint &p)
0419 {
0420     d->fixedPosition = p;
0421     show();
0422 }
0423 
0424 void KPassivePopup::hideEvent(QHideEvent *)
0425 {
0426     d->hideTimer->stop();
0427     if (d->autoDelete) {
0428         deleteLater();
0429     }
0430 }
0431 
0432 QPoint KPassivePopup::defaultLocation() const
0433 {
0434     const QRect r = QGuiApplication::primaryScreen()->availableGeometry();
0435     return QPoint(r.left(), r.top());
0436 }
0437 
0438 void KPassivePopup::positionSelf()
0439 {
0440     QRect target;
0441 
0442     if (d->window) {
0443 #if HAVE_X11
0444         if (QX11Info::isPlatformX11()) {
0445             NETWinInfo ni(QX11Info::connection(), d->window, QX11Info::appRootWindow(), NET::WMIconGeometry | NET::WMState, NET::Properties2());
0446 
0447             // Try to put the popup by the taskbar entry
0448             if (!(ni.state() & NET::SkipTaskbar)) {
0449                 NETRect r = ni.iconGeometry();
0450                 target.setRect(r.pos.x, r.pos.y, r.size.width, r.size.height);
0451             }
0452         }
0453 #endif
0454         // If that failed, put it by the window itself
0455 
0456         if (target.isNull()) {
0457             // Avoid making calls to the window system if we can
0458             QWidget *widget = QWidget::find(d->window);
0459             if (widget) {
0460                 target = widget->geometry();
0461             }
0462         }
0463 #if HAVE_KWINDOWSYSTEM
0464         if (target.isNull()) {
0465             KWindowInfo info(d->window, NET::WMGeometry);
0466             if (info.valid()) {
0467                 target = info.geometry();
0468             }
0469         }
0470 #endif
0471     }
0472     if (target.isNull()) {
0473         target = QRect(defaultLocation(), QSize(0, 0));
0474     }
0475     moveNear(target);
0476 }
0477 
0478 void KPassivePopup::moveNear(const QRect &target)
0479 {
0480     QPoint pos = d->calculateNearbyPoint(target);
0481     if (d->popupStyle == Balloon) {
0482         setAnchor(pos);
0483     } else {
0484         move(pos.x(), pos.y());
0485     }
0486 }
0487 
0488 QPoint KPassivePopup::anchor() const
0489 {
0490     return d->anchor;
0491 }
0492 
0493 void KPassivePopup::setAnchor(const QPoint &anchor)
0494 {
0495     d->anchor = anchor;
0496     d->updateMask();
0497 }
0498 
0499 void KPassivePopup::paintEvent(QPaintEvent *pe)
0500 {
0501     if (d->popupStyle == Balloon) {
0502         QPainter p;
0503         p.begin(this);
0504         p.drawPolygon(d->surround);
0505     } else {
0506         QFrame::paintEvent(pe);
0507     }
0508 }
0509 
0510 //
0511 // Convenience Methods
0512 //
0513 
0514 KPassivePopup *KPassivePopup::message(const QString &caption, const QString &text, const QPixmap &icon, QWidget *parent, int timeout, const QPoint &p)
0515 {
0516     return message(DEFAULT_POPUP_TYPE, caption, text, icon, parent, timeout, p);
0517 }
0518 
0519 KPassivePopup *KPassivePopup::message(const QString &text, QWidget *parent, const QPoint &p)
0520 {
0521     return message(DEFAULT_POPUP_TYPE, QString(), text, QPixmap(), parent, -1, p);
0522 }
0523 
0524 KPassivePopup *KPassivePopup::message(const QString &caption, const QString &text, QWidget *parent, const QPoint &p)
0525 {
0526     return message(DEFAULT_POPUP_TYPE, caption, text, QPixmap(), parent, -1, p);
0527 }
0528 
0529 KPassivePopup *KPassivePopup::message(const QString &caption, const QString &text, const QPixmap &icon, WId parent, int timeout, const QPoint &p)
0530 {
0531     return message(DEFAULT_POPUP_TYPE, caption, text, icon, parent, timeout, p);
0532 }
0533 
0534 KPassivePopup *KPassivePopup::message(const QString &caption, const QString &text, const QPixmap &icon, QSystemTrayIcon *parent, int timeout)
0535 {
0536     return message(DEFAULT_POPUP_TYPE, caption, text, icon, parent, timeout);
0537 }
0538 
0539 KPassivePopup *KPassivePopup::message(const QString &text, QSystemTrayIcon *parent)
0540 {
0541     return message(DEFAULT_POPUP_TYPE, QString(), text, QPixmap(), parent);
0542 }
0543 
0544 KPassivePopup *KPassivePopup::message(const QString &caption, const QString &text, QSystemTrayIcon *parent)
0545 {
0546     return message(DEFAULT_POPUP_TYPE, caption, text, QPixmap(), parent);
0547 }
0548 
0549 KPassivePopup *
0550 KPassivePopup::message(int popupStyle, const QString &caption, const QString &text, const QPixmap &icon, QWidget *parent, int timeout, const QPoint &p)
0551 {
0552     KPassivePopup *pop = new KPassivePopup(parent);
0553     pop->setPopupStyle(popupStyle);
0554     pop->setAutoDelete(true);
0555     pop->setView(caption, text, icon);
0556     pop->d->hideDelay = timeout < 0 ? DEFAULT_POPUP_TIME : timeout;
0557     if (p.isNull()) {
0558         pop->show();
0559     } else {
0560         pop->show(p);
0561     }
0562 
0563     return pop;
0564 }
0565 
0566 KPassivePopup *KPassivePopup::message(int popupStyle, const QString &text, QWidget *parent, const QPoint &p)
0567 {
0568     return message(popupStyle, QString(), text, QPixmap(), parent, -1, p);
0569 }
0570 
0571 KPassivePopup *KPassivePopup::message(int popupStyle, const QString &caption, const QString &text, QWidget *parent, const QPoint &p)
0572 {
0573     return message(popupStyle, caption, text, QPixmap(), parent, -1, p);
0574 }
0575 
0576 KPassivePopup *
0577 KPassivePopup::message(int popupStyle, const QString &caption, const QString &text, const QPixmap &icon, WId parent, int timeout, const QPoint &p)
0578 {
0579     KPassivePopup *pop = new KPassivePopup(parent);
0580     pop->setPopupStyle(popupStyle);
0581     pop->setAutoDelete(true);
0582     pop->setView(caption, text, icon);
0583     pop->d->hideDelay = timeout < 0 ? DEFAULT_POPUP_TIME : timeout;
0584     if (p.isNull()) {
0585         pop->show();
0586     } else {
0587         pop->show(p);
0588     }
0589 
0590     return pop;
0591 }
0592 
0593 KPassivePopup *KPassivePopup::message(int popupStyle, const QString &caption, const QString &text, const QPixmap &icon, QSystemTrayIcon *parent, int timeout)
0594 {
0595     KPassivePopup *pop = new KPassivePopup();
0596     pop->setPopupStyle(popupStyle);
0597     pop->setAutoDelete(true);
0598     pop->setView(caption, text, icon);
0599     pop->d->hideDelay = timeout < 0 ? DEFAULT_POPUP_TIME : timeout;
0600     QPoint pos = pop->d->calculateNearbyPoint(parent->geometry());
0601     pop->show(pos);
0602     pop->moveNear(parent->geometry());
0603 
0604     return pop;
0605 }
0606 
0607 KPassivePopup *KPassivePopup::message(int popupStyle, const QString &text, QSystemTrayIcon *parent)
0608 {
0609     return message(popupStyle, QString(), text, QPixmap(), parent);
0610 }
0611 
0612 KPassivePopup *KPassivePopup::message(int popupStyle, const QString &caption, const QString &text, QSystemTrayIcon *parent)
0613 {
0614     return message(popupStyle, caption, text, QPixmap(), parent);
0615 }
0616 
0617 #include "moc_kpassivepopup.cpp"
0618 
0619 #endif
0620 
0621 // Local Variables:
0622 // End: