File indexing completed on 2024-03-24 17:23:08
0001 // SPDX-License-Identifier: GPL-3.0-or-later 0002 /* 0003 Copyright 2017 - 2022 Martin Koller, kollix@aon.at 0004 0005 This file is part of liquidshell. 0006 0007 liquidshell is free software: you can redistribute it and/or modify 0008 it under the terms of the GNU General Public License as published by 0009 the Free Software Foundation, either version 3 of the License, or 0010 (at your option) any later version. 0011 0012 liquidshell is distributed in the hope that it will be useful, 0013 but WITHOUT ANY WARRANTY; without even the implied warranty of 0014 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 0015 GNU General Public License for more details. 0016 0017 You should have received a copy of the GNU General Public License 0018 along with liquidshell. If not, see <http://www.gnu.org/licenses/>. 0019 */ 0020 0021 #include <NotificationList.hxx> 0022 #include <NotificationServer.hxx> 0023 #include <DesktopWidget.hxx> 0024 0025 #include <QVBoxLayout> 0026 #include <QHBoxLayout> 0027 #include <QScrollArea> 0028 #include <QToolButton> 0029 #include <QPushButton> 0030 #include <QProgressBar> 0031 #include <QPointer> 0032 #include <QTimer> 0033 #include <QTime> 0034 #include <QVariant> 0035 #include <QVariantMap> 0036 0037 #include <KRun> 0038 #include <KLocalizedString> 0039 #include <KWindowSystem> 0040 #include <KConfig> 0041 #include <KConfigGroup> 0042 0043 static const Qt::WindowFlags POPUP_FLAGS = Qt::WindowStaysOnTopHint | Qt::FramelessWindowHint; 0044 0045 //-------------------------------------------------------------------------------- 0046 0047 NotifyItem::NotifyItem(QWidget *parent, NotificationServer *server, uint theId, const QString &app, 0048 const QString &theSummary, const QString &theBody, const QIcon &icon, 0049 const QStringList &theActions) 0050 : QFrame(parent, POPUP_FLAGS), id(theId), appName(app), summary(theSummary), body(theBody), actions(theActions) 0051 { 0052 setAttribute(Qt::WA_ShowWithoutActivating); // avoid focus stealing 0053 0054 setFrameShape(QFrame::StyledPanel); 0055 QMargins margins = contentsMargins(); 0056 margins.setRight(0); // allow the close button to reach to the right screen border - easier to click 0057 setContentsMargins(margins); 0058 0059 QVBoxLayout *vbox = new QVBoxLayout; 0060 timeLabel = new QLabel; 0061 iconLabel = new QLabel; 0062 timeoutBar = new QProgressBar; 0063 timeoutBar->setTextVisible(false); 0064 timeoutBar->setFixedSize(50, 10); 0065 timeoutBar->hide(); 0066 vbox->addWidget(timeLabel, 0, Qt::AlignTop | Qt::AlignHCenter); 0067 vbox->addWidget(timeoutBar, 0, Qt::AlignTop | Qt::AlignLeft); 0068 vbox->addWidget(iconLabel, 1, Qt::AlignVCenter | Qt::AlignHCenter); 0069 0070 QVBoxLayout *centerBox = new QVBoxLayout; 0071 0072 QHBoxLayout *hbox = new QHBoxLayout(this); 0073 margins = hbox->contentsMargins(); 0074 margins.setRight(0); // allow the close button to reach to the right screen border - easier to click 0075 hbox->setContentsMargins(margins); 0076 0077 textLabel = new QLabel; 0078 QToolButton *closeButton = new QToolButton; 0079 // easier to click with larger size 0080 closeButton->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); 0081 closeButton->setMinimumWidth(40); 0082 closeButton->setAutoRaise(true); 0083 closeButton->setIcon(QIcon::fromTheme("window-close")); 0084 connect(closeButton, &QToolButton::clicked, this, 0085 [this, server]() 0086 { 0087 emit server->NotificationClosed(id, NotificationServer::CloseReason::Dismissed); 0088 deleteLater(); 0089 }); 0090 0091 textLabel->setWordWrap(true); 0092 textLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); 0093 textLabel->setOpenExternalLinks(true); 0094 textLabel->setMinimumWidth(300); 0095 textLabel->setMaximumWidth(600); 0096 textLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); 0097 0098 centerBox->addWidget(textLabel); 0099 0100 if ( actions.count() ) 0101 { 0102 QHBoxLayout *actionBox = new QHBoxLayout; 0103 centerBox->addLayout(actionBox); 0104 0105 for (int i = 0; i < actions.count(); i++) 0106 { 0107 if ( ((i % 2) != 0) && !actions[i].isEmpty() ) // id 0108 { 0109 QPushButton *button = new QPushButton; 0110 button->setText(actions[i]); 0111 actionBox->addWidget(button); 0112 QString key = actions[i - 1]; 0113 0114 connect(button, &QPushButton::clicked, this, 0115 [this, server, key]() 0116 { 0117 emit server->ActionInvoked(id, key); 0118 }); 0119 } 0120 } 0121 } 0122 0123 hbox->addLayout(vbox); 0124 hbox->addLayout(centerBox); 0125 hbox->addWidget(closeButton); 0126 0127 iconLabel->setFixedSize(32, 32); 0128 iconLabel->setPixmap(icon.pixmap(iconLabel->size())); 0129 0130 QString title = (appName == summary) ? appName : (appName + ": " + summary); 0131 textLabel->setText(QString("<html><center><b>%1</b></center><br>%2</html>") 0132 .arg(title) 0133 .arg(body)); 0134 0135 timeLabel->setText(QTime::currentTime().toString(Qt::SystemLocaleShortDate)); 0136 } 0137 0138 //-------------------------------------------------------------------------------- 0139 0140 void NotifyItem::destroySysResources() 0141 { 0142 destroy(); 0143 } 0144 0145 //-------------------------------------------------------------------------------- 0146 0147 void NotifyItem::setTimeout(int milliSecs) 0148 { 0149 if ( milliSecs == 0 ) 0150 return; 0151 0152 timeoutBar->setMaximum(milliSecs); 0153 timeoutBar->setValue(milliSecs); 0154 timeoutBar->show(); 0155 0156 const int STEP = milliSecs / timeoutBar->width(); 0157 0158 QTimer *timer = new QTimer(this); 0159 timer->setInterval(STEP); 0160 connect(timer, &QTimer::timeout, this, [this, STEP]() 0161 { 0162 if ( timeoutBar->value() >= STEP ) 0163 timeoutBar->setValue(timeoutBar->value() - STEP); 0164 }); 0165 0166 timer->start(); 0167 } 0168 0169 0170 //-------------------------------------------------------------------------------- 0171 //-------------------------------------------------------------------------------- 0172 //-------------------------------------------------------------------------------- 0173 0174 NotificationList::NotificationList(NotificationServer *parent) 0175 : QWidget(parent), server(parent) 0176 { 0177 setWindowFlags(windowFlags() | Qt::Tool); 0178 setWindowTitle(i18n("Notifications")); 0179 0180 scrollArea = new QScrollArea; 0181 scrollArea->setWidgetResizable(true); 0182 0183 QWidget *listWidget = new QWidget; 0184 listVbox = new QVBoxLayout(listWidget); 0185 listVbox->setContentsMargins(QMargins()); 0186 listVbox->addStretch(); 0187 0188 scrollArea->setWidget(listWidget); 0189 0190 QVBoxLayout *vbox = new QVBoxLayout(this); 0191 vbox->setContentsMargins(QMargins()); 0192 vbox->addWidget(scrollArea); 0193 0194 QPushButton *clearButton = new QPushButton; 0195 clearButton->setIcon(QIcon::fromTheme("edit-clear-list")); 0196 connect(clearButton, &QPushButton::clicked, this, [this]() 0197 { 0198 for (NotifyItem *item : items) 0199 { 0200 emit server->NotificationClosed(item->id, NotificationServer::CloseReason::Dismissed); 0201 item->deleteLater(); 0202 } 0203 }); 0204 vbox->addWidget(clearButton); 0205 0206 resize(500, 300); 0207 0208 KConfig config; 0209 KConfigGroup group = config.group("Notifications"); 0210 avoidPopup = group.readEntry<bool>("avoidPopup", avoidPopup); 0211 } 0212 0213 //-------------------------------------------------------------------------------- 0214 0215 NotificationList::~NotificationList() 0216 { 0217 for (NotifyItem *item : items) 0218 item->disconnect(); // make sure destroyed() is no longer triggered 0219 } 0220 0221 //-------------------------------------------------------------------------------- 0222 0223 void NotificationList::addItem(uint id, const QString &appName, const QString &summary, const QString &body, 0224 const QIcon &icon, const QStringList &actions, const QVariantMap &hints, int timeout) 0225 { 0226 QPointer<NotifyItem> item = new NotifyItem(nullptr, server, id, appName, summary, body, icon, actions); 0227 item->resize(500, item->sizeHint().height()); 0228 KWindowSystem::setState(item->winId(), NET::SkipTaskbar | NET::SkipPager); 0229 0230 items.append(item.data()); 0231 0232 placeItems(); 0233 0234 #if QT_VERSION >= QT_VERSION_CHECK(5,14,0) 0235 int wordCount = body.splitRef(' ', Qt::SkipEmptyParts).count(); 0236 #else 0237 int wordCount = body.splitRef(' ', QString::SkipEmptyParts).count(); 0238 #endif 0239 0240 bool neverExpires = timeout == 0; // according to spec 0241 0242 if ( timeout <= 0 ) 0243 { 0244 timeout = 4000 + 250 * wordCount; 0245 0246 if ( actions.count() ) 0247 timeout += 15000; // give user more time to think ... 0248 } 0249 0250 // 0=low, 1=normal, 2=critical 0251 if ( hints.contains("urgency") && (hints["urgency"].toInt() == 2) ) 0252 { 0253 neverExpires = true; 0254 timeout = 20000; // just show it longer since its urgent 0255 } 0256 0257 bool transient = hints.contains("transient") ? hints["transient"].toBool() : false; 0258 0259 // if there are actions, show it longer 0260 if ( actions.count() || neverExpires ) 0261 transient = false; 0262 0263 // exceptions ... 0264 if ( transient && hints.contains("x-kde-eventId") ) 0265 { 0266 // I'd like to keep this much longer 0267 if ( hints["x-kde-eventId"].toString() == "new-email" ) 0268 transient = false; 0269 } 0270 0271 // I often get the same notification multiple times (e.g. KDE-connect or EWS akonadi resource) 0272 // but I don't want to fill up the screen with useless duplicate information. Therefore 0273 // whenever the same notification is received while another instance of it is still visible, 0274 // only put it in the list-view (it has a different id, therefore still store it), but don't show it as popup 0275 for (NotifyItem *it : items) 0276 { 0277 if ( (it != item) && // not the new item already in items 0278 !it->parentWidget() && // temporary item not in the list-view yet 0279 (appName == it->appName) && (summary == it->summary) && 0280 (body == it->body) && (actions == it->actions) ) 0281 { 0282 timeout = 0; 0283 } 0284 } 0285 0286 if ( avoidPopup ) 0287 timeout = 0; 0288 0289 emit itemsCountChanged(); 0290 connect(item.data(), &NotifyItem::destroyed, this, &NotificationList::itemDestroyed); 0291 0292 item->setTimeout(timeout); 0293 0294 QTimer::singleShot(timeout, 0295 [=]() mutable 0296 { 0297 if ( item ) 0298 { 0299 // I must create a new item since otherwise when closing the NotificationList 0300 // and moving this toplevel item into the list, weird behavior happens 0301 // e.g. no longer showing anything in the list, appearing at wrong position when shown... 0302 item->hide(); 0303 item->deleteLater(); 0304 item = new NotifyItem(this, server, id, appName, summary, body, icon, actions); 0305 items.append(item.data()); 0306 emit itemsCountChanged(); 0307 connect(item.data(), &NotifyItem::destroyed, this, &NotificationList::itemDestroyed); 0308 0309 listVbox->insertWidget(listVbox->count() - 1, item); // insert before stretch 0310 item->show(); 0311 0312 placeItems(); // reorder the remaining ones 0313 0314 scrollArea->setWidgetResizable(true); // to update scrollbars, else next line does not work 0315 scrollArea->ensureWidgetVisible(item); 0316 0317 if ( !neverExpires ) 0318 { 0319 if ( !appTimeouts.contains(appName) ) 0320 appTimeouts.insert(appName, 10); // default: 10 minutes lifetime 0321 0322 int expireTimeout; 0323 0324 if ( transient ) 0325 expireTimeout = 2 * 60 * 1000; // still keep it a short time 0326 else 0327 expireTimeout = appTimeouts[appName] * 60 * 1000; 0328 0329 QTimer::singleShot(expireTimeout, item.data(), 0330 [item, this]() 0331 { 0332 emit server->NotificationClosed(item->id, NotificationServer::CloseReason::Expired); 0333 item->NotifyItem::deleteLater(); 0334 }); 0335 } 0336 } 0337 } 0338 ); 0339 } 0340 0341 //-------------------------------------------------------------------------------- 0342 0343 void NotificationList::itemDestroyed(QObject *obj) 0344 { 0345 items.removeOne(static_cast<NotifyItem *>(obj)); 0346 placeItems(); 0347 0348 if ( items.isEmpty() ) 0349 { 0350 hide(); 0351 emit listNowEmpty(); 0352 } 0353 else 0354 emit itemsCountChanged(); 0355 } 0356 0357 //-------------------------------------------------------------------------------- 0358 0359 void NotificationList::closeItem(uint id) 0360 { 0361 for (int i = 0; i < items.count(); i++) 0362 { 0363 if ( items[i]->id == id ) 0364 { 0365 items.takeAt(i)->deleteLater(); 0366 break; 0367 } 0368 } 0369 } 0370 0371 //-------------------------------------------------------------------------------- 0372 0373 void NotificationList::placeItems() 0374 { 0375 QRect screen = DesktopWidget::availableGeometry(); 0376 QPoint point = parentWidget()->mapToGlobal(parentWidget()->pos()); 0377 int x = point.x(); 0378 int y = screen.bottom(); 0379 0380 for (NotifyItem *item : items) 0381 { 0382 if ( !item->parentWidget() ) // temporary item not in the list yet 0383 { 0384 y -= item->sizeHint().height(); 0385 y -= 5; // a small space 0386 0387 point.setX(std::min(x, screen.x() + screen.width() - item->sizeHint().width())); 0388 point.setY(y); 0389 item->move(point); 0390 item->show(); 0391 } 0392 } 0393 } 0394 0395 //-------------------------------------------------------------------------------- 0396 0397 void NotificationList::setAvoidPopup(bool on) 0398 { 0399 avoidPopup = on; 0400 0401 KConfig config; 0402 KConfigGroup group = config.group("Notifications"); 0403 group.writeEntry("avoidPopup", avoidPopup); 0404 } 0405 0406 //--------------------------------------------------------------------------------