Added support for dbusmenu
[dbusmenu:kdebase-workspace.git] / plasma / generic / dataengines / statusnotifieritem / statusnotifieritemsource.cpp
1 /***************************************************************************
2  *                                                                         *
3  *   Copyright (C) 2009 Marco Martin <notmart@gmail.com>                   *
4  *   Copyright (C) 2009 Matthieu Gallien <matthieu_gallien@yahoo.fr>       *
5  *                                                                         *
6  *   This program is free software; you can redistribute it and/or modify  *
7  *   it under the terms of the GNU General Public License as published by  *
8  *   the Free Software Foundation; either version 2 of the License, or     *
9  *   (at your option) any later version.                                   *
10  *                                                                         *
11  *   This program is distributed in the hope that it will be useful,       *
12  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
13  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
14  *   GNU General Public License for more details.                          *
15  *                                                                         *
16  *   You should have received a copy of the GNU General Public License     *
17  *   along with this program; if not, write to the                         *
18  *   Free Software Foundation, Inc.,                                       *
19  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA .        *
20  ***************************************************************************/
21
22 #include "statusnotifieritemsource.h"
23 #include "systemtraytypes.h"
24 #include "statusnotifieritemservice.h"
25
26 #include <QApplication>
27 #include <QDesktopWidget>
28 #include <QIcon>
29 #include <KIcon>
30 #include <KIconLoader>
31 #include <KStandardDirs>
32 #include <QPainter>
33 #include <QDBusMessage>
34 #include <QDBusPendingCall>
35 #include <QDBusPendingReply>
36 #include <QVariantMap>
37 #include <QImage>
38 #include <QMenu>
39 #include <QPixmap>
40 #include <QSysInfo>
41
42 #include <netinet/in.h>
43
44 #include <dbusmenuimporter.h>
45
46 class PlasmaDBusMenuImporter : public DBusMenuImporter
47 {
48 public:
49     PlasmaDBusMenuImporter(const QString &service, const QString &path, KIconLoader *iconLoader, QObject *parent)
50     : DBusMenuImporter(service, path, parent)
51     , m_iconLoader(iconLoader)
52     {}
53
54 protected:
55     virtual QIcon iconForName(const QString &name)
56     {
57         return KIcon(name, m_iconLoader);
58     }
59
60 private:
61     KIconLoader *m_iconLoader;
62 };
63
64 StatusNotifierItemSource::StatusNotifierItemSource(const QString &notifierItemId, QObject *parent)
65     : Plasma::DataContainer(parent),
66       m_customIconLoader(0),
67       m_menuImporter(0)
68 {
69     setObjectName(notifierItemId);
70     qDBusRegisterMetaType<KDbusImageStruct>();
71     qDBusRegisterMetaType<KDbusImageVector>();
72     qDBusRegisterMetaType<KDbusToolTipStruct>();
73
74     m_typeId = notifierItemId;
75     m_name = notifierItemId;
76
77     int slash = notifierItemId.indexOf('/');
78     if (slash == -1) {
79         kError() << "Invalid notifierItemId:" << notifierItemId;
80         m_valid = false;
81         m_statusNotifierItemInterface = 0;
82         return;
83     }
84     QString service = notifierItemId.left(slash);
85     QString path = notifierItemId.mid(slash);
86
87     m_statusNotifierItemInterface = new org::kde::StatusNotifierItem(service, path,
88                                                                      QDBusConnection::sessionBus(), this);
89
90     m_valid = !service.isEmpty() && m_statusNotifierItemInterface->isValid();
91     if (m_valid) {
92         connect(m_statusNotifierItemInterface, SIGNAL(NewIcon()), this, SLOT(refresh()));
93         connect(m_statusNotifierItemInterface, SIGNAL(NewAttentionIcon()), this, SLOT(refresh()));
94         connect(m_statusNotifierItemInterface, SIGNAL(NewOverlayIcon()), this, SLOT(refresh()));
95         connect(m_statusNotifierItemInterface, SIGNAL(NewToolTip()), this, SLOT(refresh()));
96         connect(m_statusNotifierItemInterface, SIGNAL(NewStatus(QString)), this, SLOT(syncStatus(QString)));
97         refresh();
98     }
99 }
100
101 StatusNotifierItemSource::~StatusNotifierItemSource()
102 {
103 }
104
105 KIconLoader *StatusNotifierItemSource::iconLoader() const
106 {
107     return m_customIconLoader ? m_customIconLoader : KIconLoader::global();
108 }
109
110 Plasma::Service *StatusNotifierItemSource::createService()
111 {
112     return new StatusNotifierItemService(this);
113 }
114
115 void StatusNotifierItemSource::syncStatus(QString status)
116 {
117     setData("Status", status);
118     checkForUpdate();
119 }
120
121 void StatusNotifierItemSource::refresh()
122 {
123     QDBusMessage message = QDBusMessage::createMethodCall(m_statusNotifierItemInterface->service(),
124                                                           m_statusNotifierItemInterface->path(), "org.freedesktop.DBus.Properties", "GetAll");
125
126     message << m_statusNotifierItemInterface->interface();
127     QDBusPendingCall call = m_statusNotifierItemInterface->connection().asyncCall(message);
128     QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(call, this);
129     connect(watcher, SIGNAL(finished(QDBusPendingCallWatcher *)), this, SLOT(refreshCallback(QDBusPendingCallWatcher *)));
130 }
131
132 /**
133   \todo add a smart pointer to guard call and to automatically delete it at the end of the function
134   */
135 void StatusNotifierItemSource::refreshCallback(QDBusPendingCallWatcher *call)
136 {
137     QDBusPendingReply<QVariantMap> reply = *call;
138     QVariantMap properties = reply.argumentAt<0>();
139     if (reply.isError()) {
140         m_valid = false;
141     } else {
142         //IconThemePath (handle this one first, because it has an impact on
143         //others)
144         if (!m_customIconLoader) {
145             QString path = properties["IconThemePath"].toString();
146             if (!path.isEmpty()) {
147                 // FIXME: If last part of path is not "icons", this won't work!
148                 QStringList tokens = path.split('/', QString::SkipEmptyParts);
149                 if (tokens.length() >= 3 && tokens.takeLast() == "icons") {
150                     QString appName = tokens.takeLast();
151                     QString prefix = '/' + tokens.join("/");
152                     // FIXME: Fix KIconLoader and KIconTheme so that we can use
153                     // our own instance of KStandardDirs
154                     KGlobal::dirs()->addResourceDir("data", prefix);
155                     // We use a separate instance of KIconLoader to avoid
156                     // adding all application dirs to KIconLoader::global(), to
157                     // avoid potential icon name clashes between application
158                     // icons
159                     m_customIconLoader = new KIconLoader(appName, 0 /* dirs */, this);
160                 } else {
161                     kWarning() << "Wrong IconThemePath" << path << ": too short or does not end with 'icons'";
162                 }
163             }
164         }
165
166         setData("Category", properties["Category"]);
167         setData("Status", properties["Status"]);
168         setData("Title", properties["Title"]);
169         setData("Id", properties["Id"]);
170         setData("WindowId", properties["WindowId"]);
171
172         //Attention Movie
173         setData("AttentionMovieName", properties["AttentionMovieName"]);
174
175         QIcon overlay;
176         QStringList overlayNames;
177
178         //Icon
179         {
180             KDbusImageVector image;
181             QIcon icon;
182
183             properties["OverlayIconPixmap"].value<QDBusArgument>() >> image;
184             if (image.isEmpty()) {
185                 QString iconName = properties["OverlayIconName"].toString();
186                 setData("OverlayIconName", iconName);
187                 if (!iconName.isEmpty()) {
188                     overlayNames << iconName;
189                     overlay = KIcon(iconName, iconLoader());
190                 }
191             } else {
192                 overlay = imageVectorToPixmap(image);
193             }
194
195             properties["IconPixmap"].value<QDBusArgument>() >> image;
196             if (image.isEmpty()) {
197                 QString iconName = properties["IconName"].toString();
198                 setData("IconName", iconName);
199                 if (!iconName.isEmpty()) {
200                     icon = KIcon(iconName, iconLoader(), overlayNames);
201
202                     if (overlayNames.isEmpty() && !overlay.isNull()) {
203                         overlayIcon(&icon, &overlay);
204                     }
205                 }
206             } else {
207                 icon = imageVectorToPixmap(image);
208                 if (!icon.isNull() && !overlay.isNull()) {
209                     overlayIcon(&icon, &overlay);
210                 }
211             }
212             setData("Icon", icon);
213         }
214
215         //Attention icon
216         {
217             KDbusImageVector image;
218             QIcon attentionIcon;
219
220             properties["AttentionIconPixmap"].value<QDBusArgument>() >> image;
221             if (image.isEmpty()) {
222                 QString iconName = properties["AttentionIconName"].toString();
223                 setData("AttentionIconName", iconName);
224                 if (!iconName.isEmpty()) {
225                     attentionIcon = KIcon(iconName, iconLoader(), overlayNames);
226
227                     if (overlayNames.isEmpty() && !overlay.isNull()) {
228                         overlayIcon(&attentionIcon, &overlay);
229                     }
230                 }
231             } else {
232                 attentionIcon = imageVectorToPixmap(image);
233                 if (!attentionIcon.isNull() && !overlay.isNull()) {
234                     overlayIcon(&attentionIcon, &overlay);
235                 }
236             }
237             setData("AttentionIcon", attentionIcon);
238         }
239
240         //ToolTip
241         {
242             KDbusToolTipStruct toolTip;
243             properties["ToolTip"].value<QDBusArgument>() >> toolTip;
244             if (toolTip.title.isEmpty()) {
245                 setData("ToolTipTitle", QVariant());
246                 setData("ToolTipSubTitle", QVariant());
247                 setData("ToolTipIcon", QVariant());
248             } else {
249                 QIcon toolTipIcon;
250                 if (toolTip.image.size() == 0) {
251                     toolTipIcon = KIcon(toolTip.icon, iconLoader());
252                 } else {
253                     toolTipIcon = imageVectorToPixmap(toolTip.image);
254                 }
255                 setData("ToolTipTitle", toolTip.title);
256                 setData("ToolTipSubTitle", toolTip.subTitle);
257                 setData("ToolTipIcon", toolTipIcon);
258             }
259         }
260
261         //Menu
262         if (!m_menuImporter) {
263             QString menuObjectPath = properties["Menu"].value<QDBusObjectPath>().path();
264             if (!menuObjectPath.isEmpty()) {
265                 if (menuObjectPath == "/NO_DBUSMENU") {
266                     // This is a hack to make it possible to disable DBusMenu in an
267                     // application. The string "/NO_DBUSMENU" must be the same as in
268                     // KStatusNotifierItem::setContextMenu().
269                     kWarning() << "DBusMenu disabled for this application";
270                 } else {
271                     m_menuImporter = new PlasmaDBusMenuImporter(m_statusNotifierItemInterface->service(), menuObjectPath, iconLoader(), this);
272                 }
273             }
274         }
275     }
276
277     checkForUpdate();
278     delete call;
279 }
280
281 QPixmap StatusNotifierItemSource::KDbusImageStructToPixmap(const KDbusImageStruct &image) const
282 {
283     //swap from network byte order if we are little endian
284     if (QSysInfo::ByteOrder == QSysInfo::LittleEndian) {
285         uint *uintBuf = (uint *) image.data.data();
286         for (uint i = 0; i < image.data.size()/sizeof(uint); ++i) {
287             *uintBuf = ntohl(*uintBuf);
288             ++uintBuf;
289         }
290     }
291     QImage iconImage(image.width, image.height, QImage::Format_ARGB32 );
292     memcpy(iconImage.bits(), (uchar*)image.data.data(), iconImage.numBytes());
293
294     return QPixmap::fromImage(iconImage);
295 }
296
297 QIcon StatusNotifierItemSource::imageVectorToPixmap(const KDbusImageVector &vector) const
298 {
299     QIcon icon;
300
301     for (int i = 0; i<vector.size(); ++i) {
302         icon.addPixmap(KDbusImageStructToPixmap(vector[i]));
303     }
304
305     return icon;
306 }
307
308 void StatusNotifierItemSource::overlayIcon(QIcon *icon, QIcon *overlay)
309 {
310     QIcon tmp;
311     QPixmap m_iconPixmap = icon->pixmap(KIconLoader::SizeSmall, KIconLoader::SizeSmall);
312
313     QPainter p(&m_iconPixmap);
314
315     const int size = KIconLoader::SizeSmall/2;
316     p.drawPixmap(QRect(size, size, size, size), overlay->pixmap(size, size), QRect(0,0,size,size));
317     p.end();
318     tmp.addPixmap(m_iconPixmap);
319
320     //if an m_icon exactly that size wasn't found don't add it to the vector
321     m_iconPixmap = icon->pixmap(KIconLoader::SizeSmallMedium, KIconLoader::SizeSmallMedium);
322     if (m_iconPixmap.width() == KIconLoader::SizeSmallMedium) {
323         const int size = KIconLoader::SizeSmall;
324         QPainter p(&m_iconPixmap);
325         p.drawPixmap(QRect(m_iconPixmap.width()-size, m_iconPixmap.height()-size, size, size), overlay->pixmap(size, size), QRect(0,0,size,size));
326         p.end();
327         tmp.addPixmap(m_iconPixmap);
328     }
329
330     m_iconPixmap = icon->pixmap(KIconLoader::SizeMedium, KIconLoader::SizeMedium);
331     if (m_iconPixmap.width() == KIconLoader::SizeMedium) {
332         const int size = KIconLoader::SizeSmall;
333         QPainter p(&m_iconPixmap);
334         p.drawPixmap(QRect(m_iconPixmap.width()-size, m_iconPixmap.height()-size, size, size), overlay->pixmap(size, size), QRect(0,0,size,size));
335         p.end();
336         tmp.addPixmap(m_iconPixmap);
337     }
338
339     m_iconPixmap = icon->pixmap(KIconLoader::SizeLarge, KIconLoader::SizeLarge);
340     if (m_iconPixmap.width() == KIconLoader::SizeLarge) {
341         const int size = KIconLoader::SizeSmallMedium;
342         QPainter p(&m_iconPixmap);
343         p.drawPixmap(QRect(m_iconPixmap.width()-size, m_iconPixmap.height()-size, size, size), overlay->pixmap(size, size), QRect(0,0,size,size));
344         p.end();
345         tmp.addPixmap(m_iconPixmap);
346     }
347
348     // We can't do 'm_icon->addPixmap()' because if 'm_icon' uses KIconEngine,
349     // it will ignore the added pixmaps. This is not a bug in KIconEngine,
350     // QIcon::addPixmap() doc says: "Custom m_icon engines are free to ignore
351     // additionally added pixmaps".
352     *icon = tmp;
353     //hopefully huge and enormous not necessary right now, since it's quite costly
354 }
355
356 void StatusNotifierItemSource::activate(int x, int y)
357 {
358     m_statusNotifierItemInterface->call(QDBus::NoBlock, "Activate", x, y);
359 }
360
361 void StatusNotifierItemSource::secondaryActivate(int x, int y)
362 {
363     m_statusNotifierItemInterface->call(QDBus::NoBlock, "SecondaryActivate", x, y);
364 }
365
366 void StatusNotifierItemSource::scroll(int delta, const QString &direction)
367 {
368     m_statusNotifierItemInterface->call(QDBus::NoBlock, "Scroll", delta, direction);
369 }
370
371 void StatusNotifierItemSource::contextMenu(int x, int y)
372 {
373     if (m_menuImporter) {
374         QMenu *menu = m_menuImporter->menu();
375         // Simulate an "aboutToShow" so that menu->sizeHint() is correct. Otherwise
376         // the menu may show up over the applet if new actions are added on the
377         // fly.
378         QMetaObject::invokeMethod(menu, "aboutToShow");
379
380         // Compute a reasonable position for the menu. Unfortunately we can't
381         // use Plasma::Corona::popupPosition() from here.
382         QPoint pos(x, y);
383         QRect availableRect = QApplication::desktop()->availableGeometry(pos);
384         QRect menuRect = QRect(pos, menu->sizeHint());
385         if (menuRect.left() < availableRect.left()) {
386             menuRect.moveLeft(availableRect.left());
387         } else if (menuRect.right() > availableRect.right()) {
388             menuRect.moveRight(availableRect.right());
389         }
390         if (menuRect.top() < availableRect.top()) {
391             menuRect.moveTop(availableRect.top());
392         } else if (menuRect.bottom() > availableRect.bottom()) {
393             menuRect.moveBottom(availableRect.bottom());
394         }
395
396         menu->popup(menuRect.topLeft());
397     } else {
398         kWarning() << "Could not find DBusMenu interface, falling back to calling ContextMenu()";
399         m_statusNotifierItemInterface->call(QDBus::NoBlock, "ContextMenu", x, y);
400     }
401 }
402
403 #include "statusnotifieritemsource.moc"