kde-workspace/knotify/notifybypopup.cpp
Ivailo Monev de7cddaba2 generic: adjust to Katie changes
Signed-off-by: Ivailo Monev <xakepa10@laimg.moc>
2019-05-08 14:51:07 +00:00

590 lines
17 KiB
C++

/*
Copyright (C) 2005-2009 by Olivier Goffart <ogoffart at kde.org>
Copyright (C) 2008 by Dmitry Suzdalev <dimsuz@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2, or (at your option)
any later version.
This program 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "notifybypopup.h"
#include "imageconverter.h"
#include <kdebug.h>
#include <kpassivepopup.h>
#include <kiconloader.h>
#include <kdialog.h>
#include <khbox.h>
#include <kvbox.h>
#include <kcharsets.h>
#include <knotifyconfig.h>
#include <QBuffer>
#include <QImage>
#include <QLabel>
#include <QTextDocument>
#include <QApplication>
#include <QDesktopWidget>
#include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <QDBusServiceWatcher>
#include <QXmlStreamReader>
#include <kconfiggroup.h>
static const char dbusServiceName[] = "org.freedesktop.Notifications";
static const char dbusInterfaceName[] = "org.freedesktop.Notifications";
static const char dbusPath[] = "/org/freedesktop/Notifications";
NotifyByPopup::NotifyByPopup(QObject *parent)
: KNotifyPlugin(parent) , m_animationTimer(0), m_dbusServiceExists(false),
m_dbusServiceCapCacheDirty(true)
{
QRect screen = QApplication::desktop()->availableGeometry();
m_nextPosition = screen.top();
// check if service already exists on plugin instantiation
QDBusConnectionInterface* interface = QDBusConnection::sessionBus().interface();
m_dbusServiceExists = interface && interface->isServiceRegistered(dbusServiceName);
if( m_dbusServiceExists )
slotServiceOwnerChanged(dbusServiceName, QString(), "_"); //connect signals
// to catch register/unregister events from service in runtime
QDBusServiceWatcher *watcher = new QDBusServiceWatcher(this);
watcher->setConnection(QDBusConnection::sessionBus());
watcher->setWatchMode(QDBusServiceWatcher::WatchForOwnerChange);
watcher->addWatchedService(dbusServiceName);
connect(watcher, SIGNAL(serviceOwnerChanged(const QString&, const QString&, const QString&)),
SLOT(slotServiceOwnerChanged(const QString&, const QString&, const QString&)));
if(!m_dbusServiceExists)
{
bool startfdo = false;
if (qgetenv("KDE_FULL_SESSION").isEmpty())
{
QDBusMessage message = QDBusMessage::createMethodCall("org.freedesktop.DBus",
"/org/freedesktop/DBus",
"org.freedesktop.DBus",
"ListActivatableNames");
QDBusReply<QStringList> reply = QDBusConnection::sessionBus().call(message);
if (reply.isValid() && reply.value().contains(dbusServiceName)) {
startfdo = true;
// We need to set m_dbusServiceExists to true because dbus might be too slow
// starting the service and the first call to NotifyByPopup::notify
// might not have had the service up, by setting this to true we
// guarantee it will still go through dbus and dbus will do the correct
// thing and wait for the service to go up
m_dbusServiceExists = true;
}
}
if (startfdo)
QDBusConnection::sessionBus().interface()->startService(dbusServiceName);
}
}
NotifyByPopup::~NotifyByPopup()
{
foreach(KPassivePopup *p,m_popups)
p->deleteLater();
}
void NotifyByPopup::notify( int id, KNotifyConfig * config )
{
kDebug() << id << " active notifications:" << m_popups.keys() << m_idMap.keys();
if(m_popups.contains(id) || m_idMap.contains(id))
{
kDebug() << "the popup is already shown";
finish(id);
return;
}
// if Notifications DBus service exists on bus,
// it'll be used instead
if(m_dbusServiceExists)
{
if(!sendNotificationDBus(id, 0, config))
finish(id); //an error ocurred.
return;
}
// Default to 6 seconds if no timeout has been defined
int timeout = config->timeout == -1 ? 6000 : config->timeout;
KPassivePopup *pop = new KPassivePopup( config->winId );
m_popups[id]=pop;
fillPopup(pop,id,config);
QRect screen = QApplication::desktop()->availableGeometry();
pop->setAutoDelete( true );
connect(pop, SIGNAL(destroyed()) , this, SLOT(slotPopupDestroyed()) );
pop->setTimeout(timeout);
pop->adjustSize();
pop->show(QPoint(screen.left() + screen.width()/2 - pop->width()/2 , m_nextPosition));
m_nextPosition+=pop->height();
}
void NotifyByPopup::slotPopupDestroyed( )
{
const QObject *s=sender();
if(!s)
return;
QMap<int,KPassivePopup*>::iterator it;
for(it=m_popups.begin() ; it!=m_popups.end(); ++it )
{
QObject *o=it.value();
if(o && o == s)
{
finish(it.key());
m_popups.remove(it.key());
break;
}
}
//relocate popup
if(!m_animationTimer)
m_animationTimer = startTimer(10);
}
void NotifyByPopup::timerEvent(QTimerEvent * event)
{
if(event->timerId() != m_animationTimer)
return KNotifyPlugin::timerEvent(event);
bool cont=false;
QRect screen = QApplication::desktop()->availableGeometry();
m_nextPosition = screen.top();
foreach(KPassivePopup *pop,m_popups)
{
int posy=pop->pos().y();
if(posy > m_nextPosition)
{
posy=qMax(posy-5,m_nextPosition);
m_nextPosition = posy + pop->height();
cont = cont || posy != m_nextPosition;
pop->move(pop->pos().x(),posy);
}
else
m_nextPosition += pop->height();
}
if(!cont)
{
killTimer(m_animationTimer);
m_animationTimer = 0;
}
}
void NotifyByPopup::slotLinkClicked( const QString &adr )
{
unsigned int id=adr.section('/' , 0 , 0).toUInt();
unsigned int action=adr.section('/' , 1 , 1).toUInt();
// kDebug() << id << " " << action;
if(id==0 || action==0)
return;
emit actionInvoked(id,action);
}
void NotifyByPopup::close( int id )
{
delete m_popups.take(id);
if( m_dbusServiceExists)
{
closeNotificationDBus(id);
}
}
void NotifyByPopup::update(int id, KNotifyConfig * config)
{
if (m_popups.contains(id))
{
KPassivePopup *p=m_popups[id];
fillPopup(p, id, config);
return;
}
// if Notifications DBus service exists on bus,
// it'll be used instead
if( m_dbusServiceExists)
{
sendNotificationDBus(id, id, config);
return;
}
}
void NotifyByPopup::fillPopup(KPassivePopup *pop,int id,KNotifyConfig * config)
{
QString appCaption, iconName;
getAppCaptionAndIconName(config, &appCaption, &iconName);
KIconLoader iconLoader(iconName);
QPixmap appIcon = iconLoader.loadIcon( iconName, KIconLoader::Small );
KVBox *vb = pop->standardView( config->title.isEmpty() ? appCaption : config->title , config->image.isNull() ? config->text : QString() , appIcon );
KVBox *vb2 = vb;
if(!config->image.isNull())
{
QPixmap pix = QPixmap::fromImage(config->image.toImage());
KHBox *hb = new KHBox(vb);
hb->setSpacing(KDialog::spacingHint());
QLabel *pil=new QLabel(hb);
pil->setPixmap( pix );
pil->setScaledContents(true);
if(pix.height() > 80 && pix.height() > pix.width() )
{
pil->setMaximumHeight(80);
pil->setMaximumWidth(80*pix.width()/pix.height());
}
else if(pix.width() > 80 && pix.height() <= pix.width())
{
pil->setMaximumWidth(80);
pil->setMaximumHeight(80*pix.height()/pix.width());
}
vb=new KVBox(hb);
QLabel *msg = new QLabel( config->text, vb );
msg->setAlignment( Qt::AlignLeft );
}
if ( !config->actions.isEmpty() )
{
QString linkCode=QString::fromLatin1("<p align=\"right\">");
int i=0;
foreach ( const QString & it , config->actions )
{
i++;
linkCode+=QString::fromLatin1("&nbsp;<a href=\"%1/%2\">%3</a> ").arg( id ).arg( i ).arg( Qt::escape(it) );
}
linkCode+=QString::fromLatin1("</p>");
QLabel *link = new QLabel(linkCode , vb );
link->setTextInteractionFlags(Qt::LinksAccessibleByMouse);
link->setOpenExternalLinks(false);
//link->setAlignment( AlignRight );
QObject::connect(link, SIGNAL(linkActivated(const QString &)), this, SLOT(slotLinkClicked(const QString& ) ) );
QObject::connect(link, SIGNAL(linkActivated(const QString &)), pop, SLOT(hide()));
}
pop->setView( vb2 );
}
void NotifyByPopup::slotServiceOwnerChanged( const QString & serviceName,
const QString & oldOwner, const QString & newOwner )
{
kDebug() << serviceName << oldOwner << newOwner;
// tell KNotify that all existing notifications which it sent
// to DBus had been closed
foreach (int id, m_idMap.keys())
finished(id);
m_idMap.clear();
m_dbusServiceCapCacheDirty = true;
m_dbusServiceCapabilities.clear();
if(newOwner.isEmpty())
{
m_dbusServiceExists = false;
}
else if(oldOwner.isEmpty())
{
m_dbusServiceExists = true;
// connect to action invocation signals
bool connected = QDBusConnection::sessionBus().connect(QString(), // from any service
dbusPath,
dbusInterfaceName,
"ActionInvoked",
this,
SLOT(slotDBusNotificationActionInvoked(uint,const QString&)));
if (!connected) {
kWarning() << "warning: failed to connect to ActionInvoked dbus signal";
}
connected = QDBusConnection::sessionBus().connect(QString(), // from any service
dbusPath,
dbusInterfaceName,
"NotificationClosed",
this,
SLOT(slotDBusNotificationClosed(uint,uint)));
if (!connected) {
kWarning() << "warning: failed to connect to NotificationClosed dbus signal";
}
}
}
void NotifyByPopup::slotDBusNotificationActionInvoked(uint dbus_id, const QString& actKey)
{
// find out knotify id
int id = m_idMap.key(dbus_id, 0);
if (id == 0) {
kDebug() << "failed to find knotify id for dbus_id" << dbus_id;
return;
}
kDebug() << "action" << actKey << "invoked for notification " << id;
// emulate link clicking
slotLinkClicked( QString("%1/%2").arg(id).arg(actKey) );
// now close notification - similar to popup behaviour
// (popups are hidden after link activation - see 'connects' of linkActivated signal above)
closeNotificationDBus(id);
}
void NotifyByPopup::slotDBusNotificationClosed(uint dbus_id, uint reason)
{
Q_UNUSED(reason)
// find out knotify id
int id = m_idMap.key(dbus_id, 0);
kDebug() << dbus_id << " -> " << id;
if (id == 0) {
kDebug() << "failed to find knotify id for dbus_id" << dbus_id;
return;
}
// tell KNotify that this notification has been closed
m_idMap.remove(id);
finished(id);
}
void NotifyByPopup::getAppCaptionAndIconName(KNotifyConfig *config, QString *appCaption, QString *iconName)
{
KConfigGroup globalgroup(&(*config->eventsfile), "Global");
*appCaption = globalgroup.readEntry("Name", globalgroup.readEntry("Comment", config->appname));
KConfigGroup eventGroup(&(*config->eventsfile), QString("Event/%1").arg(config->eventid));
if (eventGroup.hasKey("IconName")) {
*iconName = eventGroup.readEntry("IconName", config->appname);
} else {
*iconName = globalgroup.readEntry("IconName", config->appname);
}
}
bool NotifyByPopup::sendNotificationDBus(int id, int replacesId, KNotifyConfig* config_nocheck)
{
// figure out dbus id to replace if needed
uint dbus_replaces_id = 0;
if (replacesId != 0 ) {
dbus_replaces_id = m_idMap.value(replacesId, 0);
if (!dbus_replaces_id)
return false; //the popup has been closed, there is nothing to replace.
}
QDBusMessage m = QDBusMessage::createMethodCall( dbusServiceName, dbusPath, dbusInterfaceName, "Notify" );
QList<QVariant> args;
QString appCaption, iconName;
getAppCaptionAndIconName(config_nocheck, &appCaption, &iconName);
KNotifyConfig *config = ensurePopupCompatibility( config_nocheck );
args.append( appCaption ); // app_name
args.append( dbus_replaces_id ); // replaces_id
args.append( iconName ); // app_icon
args.append( config->title.isEmpty() ? appCaption : config->title ); // summary
args.append( config->text ); // body
// galago spec defines action list to be list like
// (act_id1, action1, act_id2, action2, ...)
//
// assign id's to actions like it's done in fillPopup() method
// (i.e. starting from 1)
QStringList actionList;
int actId = 0;
foreach (const QString& actName, config->actions) {
actId++;
actionList.append(QString::number(actId));
actionList.append(actName);
}
args.append( actionList ); // actions
QVariantMap map;
// Add the application name to the hints.
// According to fdo spec, the app_name is supposed to be the applicaton's "pretty name"
// but in some places it's handy to know the application name itself
if (!config->appname.isEmpty()) {
map["x-kde-appname"] = config->appname;
}
// let's see if we've got an image, and store the image in the hints map
if (!config->image.isNull()) {
QImage image = config->image.toImage();
map["image_data"] = ImageConverter::variantForImage(image);
}
args.append( map ); // hints
args.append( config->timeout ); // expire timout
m.setArguments( args );
QDBusMessage replyMsg = QDBusConnection::sessionBus().call(m);
delete config;
if(replyMsg.type() == QDBusMessage::ReplyMessage) {
if (!replyMsg.arguments().isEmpty()) {
uint dbus_id = replyMsg.arguments().at(0).toUInt();
if (dbus_id == 0)
{
kDebug() << "error: dbus_id is null";
return false;
}
if (dbus_replaces_id && dbus_id == dbus_replaces_id)
return true;
#if 1
int oldId = m_idMap.key(dbus_id, 0);
if (oldId != 0) {
kWarning() << "Received twice the same id "<< dbus_id << "( previous notification: " << oldId << ")";
m_idMap.remove(oldId);
finish(oldId);
}
#endif
m_idMap.insert(id, dbus_id);
kDebug() << "mapping knotify id to dbus id:"<< id << "=>" << dbus_id;
return true;
} else {
kDebug() << "error: received reply with no arguments";
}
} else if (replyMsg.type() == QDBusMessage::ErrorMessage) {
kDebug() << "error: failed to send dbus message";
} else {
kDebug() << "unexpected reply type";
}
return false;
}
void NotifyByPopup::closeNotificationDBus(int id)
{
uint dbus_id = m_idMap.take(id);
if (dbus_id == 0) {
kDebug() << "not found dbus id to close" << id;
return;
}
QDBusMessage m = QDBusMessage::createMethodCall( dbusServiceName, dbusPath,
dbusInterfaceName, "CloseNotification" );
QList<QVariant> args;
args.append( dbus_id );
m.setArguments( args );
bool queued = QDBusConnection::sessionBus().send(m);
if(!queued)
{
kDebug() << "warning: failed to queue dbus message";
}
}
QStringList NotifyByPopup::popupServerCapabilities()
{
if (!m_dbusServiceExists) {
// Return capabilities of the KPassivePopup implementation
return QStringList() << "actions" << "body" << "body-hyperlinks"
<< "body-markup" << "icon-static";
}
if(m_dbusServiceCapCacheDirty) {
QDBusMessage m = QDBusMessage::createMethodCall( dbusServiceName, dbusPath,
dbusInterfaceName, "GetCapabilities" );
QDBusMessage replyMsg = QDBusConnection::sessionBus().call(m);
if (replyMsg.type() != QDBusMessage::ReplyMessage) {
kWarning(300) << "Error while calling popup server GetCapabilities()";
return QStringList();
}
if (replyMsg.arguments().isEmpty()) {
kWarning(300) << "popup server GetCapabilities() returned an empty reply";
return QStringList();
}
m_dbusServiceCapabilities = replyMsg.arguments().at(0).toStringList();
m_dbusServiceCapCacheDirty = false;
}
return m_dbusServiceCapabilities;
}
KNotifyConfig *NotifyByPopup::ensurePopupCompatibility( const KNotifyConfig *config )
{
KNotifyConfig *c = config->copy();
QStringList cap = popupServerCapabilities();
if( !cap.contains( "actions" ) )
{
c->actions.clear();
}
if( !cap.contains( "body-markup" ) )
{
if( c->title.startsWith( "<html>" ) )
c->title = stripHtml( config->title );
if( c->text.startsWith( "<html>" ) )
c->text = stripHtml( config->text );
}
return c;
}
QString NotifyByPopup::stripHtml( const QString &text )
{
QXmlStreamReader r( "<elem>" + text + "</elem>" );
HtmlEntityResolver resolver;
r.setEntityResolver( &resolver );
QString result;
while( !r.atEnd() ) {
r.readNext();
if( r.tokenType() == QXmlStreamReader::Characters )
{
result.append( r.text() );
}
else if( r.tokenType() == QXmlStreamReader::StartElement
&& r.name() == "br" )
{
result.append( "\n" );
}
}
if(r.hasError())
{
// XML error in the given text, just return the original string
kWarning(300) << "Notification to send to backend which does "
"not support HTML, contains invalid XML:"
<< r.errorString() << "line" << r.lineNumber()
<< "col" << r.columnNumber();
return text;
}
return result;
}
QString NotifyByPopup::HtmlEntityResolver::resolveUndeclaredEntity(
const QString &name )
{
QString result =
QXmlStreamEntityResolver::resolveUndeclaredEntity(name);
if( !result.isEmpty() )
return result;
QChar ent = KCharsets::fromEntity( '&' + name );
if( ent.isNull() ) {
kWarning(300) << "Notification to send to backend which does "
"not support HTML, contains invalid entity: "
<< name;
ent = ' ';
}
return QString(ent);
}
#include "moc_notifybypopup.cpp"