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