You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
362 lines
10 KiB
362 lines
10 KiB
/* BEGIN_COMMON_COPYRIGHT_HEADER
|
|
* (c)LGPL2+
|
|
*
|
|
* LXDE-Qt - a lightweight, Qt based, desktop toolset
|
|
* http://razor-qt.org
|
|
*
|
|
* Copyright: 2012 Razor team
|
|
* Authors:
|
|
* Petr Vanek <petr@scribus.info>
|
|
*
|
|
* This program or library is free software; you can redistribute it
|
|
* and/or modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2.1 of the License, or (at your option) any later version.
|
|
*
|
|
* This library is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Lesser General Public License for more details.
|
|
|
|
* You should have received a copy of the GNU Lesser General
|
|
* Public License along with this library; if not, write to the
|
|
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
* Boston, MA 02110-1301 USA
|
|
*
|
|
* END_COMMON_COPYRIGHT_HEADER */
|
|
|
|
#include <QPainter>
|
|
#include <QUrl>
|
|
#include <QFile>
|
|
#include <QDateTime>
|
|
#include <QtDBus/QDBusArgument>
|
|
#include <QDebug>
|
|
#include <XdgIcon>
|
|
#include <KWindowSystem/KWindowSystem>
|
|
#include <QMouseEvent>
|
|
#include <QPushButton>
|
|
|
|
#include "notification.h"
|
|
#include "notificationwidgets.h"
|
|
|
|
#define ICONSIZE QSize(32, 32)
|
|
|
|
|
|
Notification::Notification(const QString &application,
|
|
const QString &summary, const QString &body,
|
|
const QString &icon, int timeout,
|
|
const QStringList& actions, const QVariantMap& hints,
|
|
QWidget *parent)
|
|
: QWidget(parent),
|
|
m_timer(0),
|
|
m_linkHovered(false),
|
|
m_actionWidget(0)
|
|
{
|
|
setupUi(this);
|
|
setObjectName("Notification");
|
|
setMouseTracking(true);
|
|
|
|
setMaximumWidth(parent->width());
|
|
setMinimumWidth(parent->width());
|
|
|
|
setValues(application, summary, body, icon, timeout, actions, hints);
|
|
|
|
connect(closeButton, &QPushButton::clicked, this, &Notification::closeButton_clicked);
|
|
|
|
for (QLabel *label : {bodyLabel, summaryLabel})
|
|
{
|
|
connect(label, &QLabel::linkHovered, this, &Notification::linkHovered);
|
|
|
|
label->installEventFilter(this);
|
|
}
|
|
}
|
|
|
|
void Notification::setValues(const QString &application,
|
|
const QString &summary, const QString &body,
|
|
const QString &icon, int timeout,
|
|
const QStringList& actions, const QVariantMap& hints)
|
|
{
|
|
// Basic properties *********************
|
|
|
|
// Notifications spec set real order here:
|
|
// An implementation which only displays one image or icon must
|
|
// choose which one to display using the following order:
|
|
// - "image-data"
|
|
// - "image-path"
|
|
// - app_icon parameter
|
|
// - for compatibility reason, "icon_data"
|
|
if (!hints["image_data"].isNull())
|
|
{
|
|
m_pixmap = getPixmapFromHint(hints["image_data"]);
|
|
}
|
|
else if (!hints["image_path"].isNull())
|
|
{
|
|
m_pixmap = getPixmapFromString(hints["image_path"].toString());
|
|
}
|
|
else if (!icon.isEmpty())
|
|
{
|
|
m_pixmap = getPixmapFromString(icon);
|
|
}
|
|
else if (!hints["icon_data"].isNull())
|
|
{
|
|
m_pixmap = getPixmapFromHint(hints["icon_data"]);
|
|
}
|
|
// issue #325: Do not display icon if it's not found...
|
|
if (m_pixmap.isNull())
|
|
{
|
|
iconLabel->hide();
|
|
}
|
|
else
|
|
{
|
|
if (m_pixmap.size().width() > ICONSIZE.width()
|
|
|| m_pixmap.size().height() > ICONSIZE.height())
|
|
{
|
|
m_pixmap = m_pixmap.scaled(ICONSIZE, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
|
}
|
|
iconLabel->setPixmap(m_pixmap);
|
|
iconLabel->show();
|
|
}
|
|
//XXX: workaround to properly set text labels widths (for correct sizeHints after setText)
|
|
adjustSize();
|
|
|
|
// application
|
|
appLabel->setVisible(!application.isEmpty());
|
|
appLabel->setFixedWidth(appLabel->width());
|
|
appLabel->setText(application);
|
|
|
|
// summary
|
|
summaryLabel->setVisible(!summary.isEmpty() && application != summary);
|
|
summaryLabel->setFixedWidth(summaryLabel->width());
|
|
summaryLabel->setText(summary);
|
|
|
|
// body
|
|
bodyLabel->setVisible(!body.isEmpty());
|
|
bodyLabel->setFixedWidth(bodyLabel->width());
|
|
//https://developer.gnome.org/notification-spec
|
|
//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.
|
|
//XXX: remove all unsupported tags?!? (supported <b>, <i>, <u>, <a>, <img>)
|
|
QString formatted(body);
|
|
bodyLabel->setText(formatted.replace('\n', QStringLiteral("<br/>")));
|
|
|
|
// Timeout
|
|
// Special values:
|
|
// < 0: server decides timeout
|
|
// 0: infifite
|
|
if (m_timer)
|
|
{
|
|
m_timer->stop();
|
|
m_timer->deleteLater();
|
|
}
|
|
|
|
// -1 for server decides is handled in notifyd to save QSettings instance
|
|
if (timeout > 0)
|
|
{
|
|
m_timer = new NotificationTimer(this);
|
|
connect(m_timer, &NotificationTimer::timeout, this, &Notification::timeout);
|
|
m_timer->start(timeout);
|
|
}
|
|
|
|
// Categories *********************
|
|
if (!hints["category"].isNull())
|
|
{
|
|
// TODO/FIXME: Categories - how to handle it?
|
|
}
|
|
|
|
// Urgency Levels *********************
|
|
// Type Description
|
|
// 0 Low
|
|
// 1 Normal
|
|
// 2 Critical
|
|
if (!hints["urgency"].isNull())
|
|
{
|
|
// TODO/FIXME: Urgencies - how to handle it?
|
|
}
|
|
|
|
// Actions
|
|
if (actions.count() && m_actionWidget == 0)
|
|
{
|
|
if (actions.count()/2 < 4)
|
|
m_actionWidget = new NotificationActionsButtonsWidget(actions, this);
|
|
else
|
|
m_actionWidget = new NotificationActionsComboWidget(actions, this);
|
|
|
|
connect(m_actionWidget, &NotificationActionsWidget::actionTriggered,
|
|
this, &Notification::actionTriggered);
|
|
|
|
actionsLayout->addWidget(m_actionWidget);
|
|
m_actionWidget->show();
|
|
}
|
|
|
|
adjustSize();
|
|
// ensure layout expansion
|
|
setMinimumHeight(qMax(rect().height(), childrenRect().height()));
|
|
}
|
|
|
|
QString Notification::application() const
|
|
{
|
|
return appLabel->text();
|
|
}
|
|
|
|
QString Notification::summary() const
|
|
{
|
|
return summaryLabel->text();
|
|
}
|
|
|
|
QString Notification::body() const
|
|
{
|
|
return bodyLabel->text();
|
|
}
|
|
|
|
void Notification::closeButton_clicked()
|
|
{
|
|
if (m_timer)
|
|
m_timer->stop();
|
|
emit userCanceled();
|
|
}
|
|
|
|
void Notification::paintEvent(QPaintEvent *)
|
|
{
|
|
QStyleOption opt;
|
|
opt.init(this);
|
|
QPainter p(this);
|
|
style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
|
|
}
|
|
|
|
QPixmap Notification::getPixmapFromHint(const QVariant &argument) const
|
|
{
|
|
int width, height, rowstride, bitsPerSample, channels;
|
|
bool hasAlpha;
|
|
QByteArray data;
|
|
|
|
const QDBusArgument arg = argument.value<QDBusArgument>();
|
|
arg.beginStructure();
|
|
arg >> width;
|
|
arg >> height;
|
|
arg >> rowstride;
|
|
arg >> hasAlpha;
|
|
arg >> bitsPerSample;
|
|
arg >> channels;
|
|
arg >> data;
|
|
arg.endStructure();
|
|
|
|
bool rgb = !hasAlpha && channels == 3 && bitsPerSample == 8;
|
|
QImage::Format imageFormat = rgb ? QImage::Format_RGB888 : QImage::Format_ARGB32;
|
|
|
|
QImage img = QImage((uchar*)data.constData(), width, height, imageFormat);
|
|
|
|
if (!rgb)
|
|
img = img.rgbSwapped();
|
|
|
|
return QPixmap::fromImage(img);
|
|
}
|
|
|
|
QPixmap Notification::getPixmapFromString(const QString &str) const
|
|
{
|
|
QUrl url(str);
|
|
if (url.isValid() && QFile::exists(url.toLocalFile()))
|
|
{
|
|
// qDebug() << " getPixmapFromString by URL" << url;
|
|
return QPixmap(url.toLocalFile());
|
|
}
|
|
else
|
|
{
|
|
// qDebug() << " getPixmapFromString by XdgIcon theme" << str << ICONSIZE << XdgIcon::themeName();
|
|
// qDebug() << " " << XdgIcon::fromTheme(str) << "isnull:" << XdgIcon::fromTheme(str).isNull();
|
|
// They say: do not display an icon if it;s not found - see #325
|
|
return XdgIcon::fromTheme(str/*, XdgIcon::defaultApplicationIcon()*/).pixmap(ICONSIZE);
|
|
}
|
|
}
|
|
|
|
void Notification::enterEvent(QEvent * event)
|
|
{
|
|
if (m_timer)
|
|
m_timer->pause();
|
|
}
|
|
|
|
void Notification::leaveEvent(QEvent * event)
|
|
{
|
|
if (m_timer)
|
|
m_timer->resume();
|
|
}
|
|
|
|
bool Notification::eventFilter(QObject *obj, QEvent *event)
|
|
{
|
|
// Catch mouseReleaseEvent on child labels if a link is not currently being hovered.
|
|
//
|
|
// This workarounds QTBUG-49025 where clicking on text does not propagate the mouseReleaseEvent
|
|
// to the parent even though the text is not selectable and no link is being clicked.
|
|
if (event->type() == QEvent::MouseButtonRelease && !m_linkHovered)
|
|
{
|
|
mouseReleaseEvent(static_cast<QMouseEvent*>(event));
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void Notification::linkHovered(QString link)
|
|
{
|
|
m_linkHovered = !link.isEmpty();
|
|
}
|
|
|
|
void Notification::mouseReleaseEvent(QMouseEvent * event)
|
|
{
|
|
// qDebug() << "CLICKED" << event;
|
|
QString appName;
|
|
QString windowTitle;
|
|
|
|
if (m_actionWidget && m_actionWidget->hasDefaultAction())
|
|
{
|
|
emit actionTriggered(m_actionWidget->defaultAction());
|
|
return;
|
|
}
|
|
|
|
foreach (WId i, KWindowSystem::stackingOrder())
|
|
{
|
|
KWindowInfo info = KWindowInfo(i, NET::WMName | NET::WMVisibleName);
|
|
appName = info.name();
|
|
windowTitle = info.visibleName();
|
|
// qDebug() << " " << i << "APPNAME" << appName << "TITLE" << windowTitle;
|
|
if (appName.isEmpty())
|
|
{
|
|
QWidget::mouseReleaseEvent(event);
|
|
return;
|
|
}
|
|
if (appName == appLabel->text() || windowTitle == appLabel->text())
|
|
{
|
|
KWindowSystem::raiseWindow(i);
|
|
closeButton_clicked();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
NotificationTimer::NotificationTimer(QObject *parent)
|
|
: QTimer(parent),
|
|
m_intervalMsec(-1)
|
|
{
|
|
}
|
|
|
|
void NotificationTimer::start(int msec)
|
|
{
|
|
m_startTime = QDateTime::currentDateTime();
|
|
m_intervalMsec = msec;
|
|
QTimer::start(msec);
|
|
}
|
|
|
|
void NotificationTimer::pause()
|
|
{
|
|
if (!isActive())
|
|
return;
|
|
|
|
stop();
|
|
m_intervalMsec = m_startTime.msecsTo(QDateTime::currentDateTime());
|
|
}
|
|
|
|
void NotificationTimer::resume()
|
|
{
|
|
if (isActive())
|
|
return;
|
|
|
|
start(m_intervalMsec);
|
|
}
|