1 /* BEGIN_COMMON_COPYRIGHT_HEADER
2  * (c)LGPL2+
3  *
4  * LXQt - a lightweight, Qt based, desktop toolset
5  * https://lxqt.org
6  *
7  * Copyright: 2012 Razor team
8  * Authors:
9  *   Petr Vanek <petr@scribus.info>
10  *
11  * This program or library is free software; you can redistribute it
12  * and/or modify it under the terms of the GNU Lesser General Public
13  * License as published by the Free Software Foundation; either
14  * version 2.1 of the License, or (at your option) any later version.
15  *
16  * This library is distributed in the hope that it will be useful,
17  * but WITHOUT ANY WARRANTY; without even the implied warranty of
18  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
19  * Lesser General Public License for more details.
20 
21  * You should have received a copy of the GNU Lesser General
22  * Public License along with this library; if not, write to the
23  * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
24  * Boston, MA 02110-1301 USA
25  *
26  * END_COMMON_COPYRIGHT_HEADER */
27 
28 #include <QPainter>
29 #include <QUrl>
30 #include <QFile>
31 #include <QDateTime>
32 #include <QtDBus/QDBusArgument>
33 #include <QDebug>
34 #include <XdgIcon>
35 #include <KWindowSystem/KWindowSystem>
36 #include <QMouseEvent>
37 #include <QPushButton>
38 #include <QStyle>
39 #include <QStyleOption>
40 
41 #include "notification.h"
42 #include "notificationwidgets.h"
43 
44 #define ICONSIZE QSize(32, 32)
45 
46 
Notification(const QString & application,const QString & summary,const QString & body,const QString & icon,int timeout,const QStringList & actions,const QVariantMap & hints,QWidget * parent)47 Notification::Notification(const QString &application,
48                            const QString &summary, const QString &body,
49                            const QString &icon, int timeout,
50                            const QStringList& actions, const QVariantMap& hints,
51                            QWidget *parent)
52     : QWidget(parent),
53       m_timer(nullptr),
54       m_linkHovered(false),
55       m_actionWidget(nullptr),
56       m_icon(icon),
57       m_timeout(timeout),
58       m_actions(actions),
59       m_hints(hints)
60 {
61     setupUi(this);
62     setObjectName(QSL("Notification"));
63     setMouseTracking(true);
64 
65     setMaximumWidth(parent->width());
66     setMinimumWidth(parent->width());
67     setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
68 
69     setValues(application, summary, body, icon, timeout, actions, hints);
70 
71     connect(closeButton, &QPushButton::clicked, this, &Notification::closeButton_clicked);
72 
73     for (QLabel *label : {bodyLabel, summaryLabel})
74     {
75         connect(label, &QLabel::linkHovered, this, &Notification::linkHovered);
76 
77         label->installEventFilter(this);
78     }
79 }
80 
setValues(const QString & application,const QString & summary,const QString & body,const QString & icon,int timeout,const QStringList & actions,const QVariantMap & hints)81 void Notification::setValues(const QString &application,
82                              const QString &summary, const QString &body,
83                              const QString &icon, int timeout,
84                              const QStringList& actions, const QVariantMap& hints)
85 {
86     // Basic properties *********************
87 
88     // Notifications spec set real order here:
89     // An implementation which only displays one image or icon must
90     // choose which one to display using the following order:
91     //  - "image-data"
92     //  - "image-path"
93     //  - app_icon parameter
94     //  - for compatibility reason, "icon_data", "image_data" and "image_path"
95 
96     if (!hints[QL1S("image-data")].isNull())
97     {
98         m_pixmap = getPixmapFromHint(hints[QL1S("image-data")]);
99     }
100     else if (!hints[QL1S("image_data")].isNull())
101     {
102         m_pixmap = getPixmapFromHint(hints[QL1S("image_data")]);
103     }
104     else if (!hints[QL1S("image-path")].isNull())
105     {
106         m_pixmap = getPixmapFromHint(hints[QL1S("image-path")]);
107     }
108     else if (!hints[QL1S("image_path")].isNull())
109     {
110         m_pixmap = getPixmapFromString(hints[QL1S("image_path")].toString());
111     }
112     else if (!icon.isEmpty())
113     {
114         m_pixmap = getPixmapFromString(icon);
115     }
116     else if (!hints[QL1S("icon_data")].isNull())
117     {
118        m_pixmap = getPixmapFromHint(hints[QL1S("icon_data")]);
119     }
120     // issue #325: Do not display icon if it's not found...
121     if (m_pixmap.isNull())
122     {
123         iconLabel->hide();
124     }
125     else
126     {
127         if (m_pixmap.size().width() > ICONSIZE.width()
128             || m_pixmap.size().height() > ICONSIZE.height())
129         {
130             m_pixmap = m_pixmap.scaled(ICONSIZE, Qt::KeepAspectRatio, Qt::SmoothTransformation);
131         }
132         iconLabel->setPixmap(m_pixmap);
133         iconLabel->show();
134     }
135     //XXX: workaround to properly set text labels widths (for correct sizeHints after setText)
136     adjustSize();
137 
138     // application
139     appLabel->setVisible(!application.isEmpty());
140     appLabel->setFixedWidth(appLabel->width());
141     appLabel->setText(application);
142 
143     // summary
144     summaryLabel->setVisible(!summary.isEmpty() && application != summary);
145     summaryLabel->setFixedWidth(summaryLabel->width());
146     summaryLabel->setText(summary);
147 
148     // body
149     bodyLabel->setVisible(!body.isEmpty());
150     bodyLabel->setFixedWidth(bodyLabel->width());
151     //https://developer.gnome.org/notification-spec
152     //Body - This is a multi-line body of text. Each line is a paragraph, server implementations are free to word wrap them as they see fit.
153     //XXX: remove all unsupported tags?!? (supported <b>, <i>, <u>, <a>, <img>)
154     QString formatted(body);
155     bodyLabel->setText(formatted.replace(QL1C('\n'), QStringLiteral("<br/>")));
156 
157     // Timeout
158     // Special values:
159     //  < 0: server decides timeout
160     //    0: infifite
161     if (m_timer)
162     {
163         m_timer->stop();
164         m_timer->deleteLater();
165     }
166 
167     // -1 for server decides is handled in notifyd to save QSettings instance
168     if (timeout > 0)
169     {
170         m_timer = new NotificationTimer(this);
171         connect(m_timer, &NotificationTimer::timeout, this, &Notification::timeout);
172         m_timer->start(timeout);
173     }
174 
175     // Categories *********************
176     if (!hints[QL1S("category")].isNull())
177     {
178         // TODO/FIXME: Categories - how to handle it?
179     }
180 
181     // Urgency Levels *********************
182     // Type    Description
183     // 0   Low
184     // 1   Normal
185     // 2   Critical
186     if (!hints[QL1S("urgency")].isNull())
187     {
188         // TODO/FIXME: Urgencies - how to handle it?
189     }
190 
191     // Actions
192     if (actions.count() && m_actionWidget == nullptr)
193     {
194         if (actions.count()/2 < 4)
195             m_actionWidget = new NotificationActionsButtonsWidget(actions, this);
196         else
197             m_actionWidget = new NotificationActionsComboWidget(actions, this);
198 
199         connect(m_actionWidget, &NotificationActionsWidget::actionTriggered,
200                 this, &Notification::actionTriggered);
201 
202         actionsLayout->addWidget(m_actionWidget);
203         m_actionWidget->show();
204     }
205 
206     adjustSize();
207     // ensure layout expansion
208     setMinimumHeight(qMax(rect().height(), childrenRect().height()));
209 }
210 
application() const211 QString Notification::application() const
212 {
213     return appLabel->text();
214 }
215 
summary() const216 QString Notification::summary() const
217 {
218     return summaryLabel->text();
219 }
220 
body() const221 QString Notification::body() const
222 {
223     return bodyLabel->text();
224 }
225 
closeButton_clicked()226 void Notification::closeButton_clicked()
227 {
228     if (m_timer)
229         m_timer->stop();
230     emit userCanceled();
231 }
232 
paintEvent(QPaintEvent *)233 void Notification::paintEvent(QPaintEvent *)
234 {
235     QStyleOption opt;
236     opt.init(this);
237     QPainter p(this);
238     style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
239 }
240 
getPixmapFromHint(const QVariant & argument) const241 QPixmap Notification::getPixmapFromHint(const QVariant &argument) const
242 {
243     int width, height, rowstride, bitsPerSample, channels;
244     bool hasAlpha;
245     QByteArray data;
246 
247     const QDBusArgument arg = argument.value<QDBusArgument>();
248     arg.beginStructure();
249     arg >> width;
250     arg >> height;
251     arg >> rowstride;
252     arg >> hasAlpha;
253     arg >> bitsPerSample;
254     arg >> channels;
255     arg >> data;
256     arg.endStructure();
257 
258     bool rgb = !hasAlpha && channels == 3 && bitsPerSample == 8;
259     QImage::Format imageFormat = rgb ? QImage::Format_RGB888 : QImage::Format_ARGB32;
260 
261     QImage img = QImage((uchar*)data.constData(), width, height, imageFormat);
262 
263     if (!rgb)
264         img = img.rgbSwapped();
265 
266     return QPixmap::fromImage(img);
267 }
268 
getPixmapFromString(const QString & str) const269 QPixmap Notification::getPixmapFromString(const QString &str) const
270 {
271     QUrl url(str);
272     if (url.isValid() && QFile::exists(url.toLocalFile()))
273     {
274 //        qDebug() << "    getPixmapFromString by URL" << url;
275         return QPixmap(url.toLocalFile());
276     }
277     else
278     {
279 //        qDebug() << "    getPixmapFromString by XdgIcon theme" << str << ICONSIZE << XdgIcon::themeName();
280 //        qDebug() << "       " << XdgIcon::fromTheme(str) << "isnull:" << XdgIcon::fromTheme(str).isNull();
281         // They say: do not display an icon if it;s not found - see #325
282         return XdgIcon::fromTheme(str/*, XdgIcon::defaultApplicationIcon()*/).pixmap(ICONSIZE);
283     }
284 }
285 
enterEvent(QEvent *)286 void Notification::enterEvent(QEvent * /*event*/)
287 {
288     if (m_timer)
289         m_timer->pause();
290 }
291 
leaveEvent(QEvent *)292 void Notification::leaveEvent(QEvent * /*event*/)
293 {
294     if (m_timer)
295         m_timer->resume();
296 }
297 
eventFilter(QObject *,QEvent * event)298 bool Notification::eventFilter(QObject * /*obj*/, QEvent * event)
299 {
300     // Catch mouseReleaseEvent on child labels if a link is not currently being hovered.
301     //
302     // This workarounds QTBUG-49025 where clicking on text does not propagate the mouseReleaseEvent
303     // to the parent even though the text is not selectable and no link is being clicked.
304     if (event->type() == QEvent::MouseButtonRelease && !m_linkHovered)
305     {
306         mouseReleaseEvent(static_cast<QMouseEvent*>(event));
307         return true;
308     }
309     return false;
310 }
311 
linkHovered(const QString & link)312 void Notification::linkHovered(const QString& link)
313 {
314     m_linkHovered = !link.isEmpty();
315 }
316 
mouseReleaseEvent(QMouseEvent * event)317 void Notification::mouseReleaseEvent(QMouseEvent * event)
318 {
319 //    qDebug() << "CLICKED" << event;
320     QString appName;
321     QString windowTitle;
322 
323     if (m_actionWidget && m_actionWidget->hasDefaultAction())
324     {
325         emit actionTriggered(m_actionWidget->defaultAction());
326         return;
327     }
328 
329     const auto ids = KWindowSystem::stackingOrder();
330     for (const WId &i : ids)
331     {
332         KWindowInfo info = KWindowInfo(i, NET::WMName | NET::WMVisibleName);
333         appName = info.name();
334         windowTitle = info.visibleName();
335         // qDebug() << "    " << i << "APPNAME" << appName << "TITLE" << windowTitle;
336         if (appName.isEmpty())
337         {
338             QWidget::mouseReleaseEvent(event);
339             return;
340         }
341         if (appName == appLabel->text() || windowTitle == appLabel->text())
342         {
343             KWindowSystem::raiseWindow(i);
344             closeButton_clicked();
345             return;
346         }
347     }
348 }
349 
NotificationTimer(QObject * parent)350 NotificationTimer::NotificationTimer(QObject *parent)
351     : QTimer(parent),
352       m_intervalMsec(-1)
353 {
354 }
355 
start(int msec)356 void NotificationTimer::start(int msec)
357 {
358     m_startTime = QDateTime::currentDateTime();
359     m_intervalMsec = msec;
360     QTimer::start(msec);
361 }
362 
pause()363 void NotificationTimer::pause()
364 {
365     if (!isActive())
366         return;
367 
368     stop();
369     m_intervalMsec = m_startTime.msecsTo(QDateTime::currentDateTime());
370 }
371 
resume()372 void NotificationTimer::resume()
373 {
374     if (isActive())
375         return;
376 
377     start(m_intervalMsec);
378 }
379