Codebase list gnome-shell-extension-appindicator / 7e7a328
Huge icon handling cleanup and extension Now supporting overlays and pixmaps! Also worked on getting updatinng and caching semantics right. Jonas Kümmerlin 9 years ago
3 changed file(s) with 335 addition(s) and 85 deletion(s). Raw diff Collapse all Expand all
2424 const St = imports.gi.St;
2525 const Shell = imports.gi.Shell;
2626 const GdkPixbuf = imports.gi.GdkPixbuf;
27 const Clutter = imports.gi.Clutter;
28 const Cogl = imports.gi.Cogl;
2729
2830 const Gettext = imports.gettext.domain('gnome-shell');
2931 const _ = Gettext.gettext;
5759 _init: function(bus_name, object) {
5860 this.busName = bus_name
5961
60 this._iconSize = 16 //arbitrary value
62 this._iconBin = new IconContainer(16)
63
64 this._iconCache = new IconCache.IconCache()
6165
6266 this._proxy = new Util.XmlLessDBusProxy({
6367 connection: Gio.DBus.session,
6468 name: bus_name,
6569 path: object,
6670 interface: 'org.kde.StatusNotifierItem',
67 propertyWhitelist: [
71 propertyWhitelist: [ //keep sorted alphabetically, please
72 'AttentionIconName',
73 'AttentionIconPixmap',
74 'Category',
75 'IconName',
76 'IconPixmap',
77 'IconThemePath',
78 'Id',
79 'Menu',
80 'OverlayIconName',
81 'OverlayIconPixmap',
82 'Status',
6883 'Title',
69 'Id',
70 'Category',
71 'Status',
7284 'ToolTip',
73 'XAyatanaLabel',
74 'Menu',
75 'IconName',
76 'AttentionIconName',
77 'OverlayIconName',
85 'XAyatanaLabel'
7886 ],
7987 onReady: (function() {
8088 this.isReady = true
98106
99107 if (this._proxy.propertyWhitelist.indexOf(prop + 'Pixmap') > -1)
100108 this._proxy.invalidateProperty(prop + 'Pixmap')
109
110 if (this._proxy.propertyWhitelist.indexOf(prop + 'Name') > -1)
111 this._proxy.invalidateProperty(prop + 'Name')
101112 } else if (signal == 'XAyatanaNewLabel') {
102113 // and the ayatana guys made sure to invent yet another way of composing these signals...
103114 this._proxy.invalidateProperty('XAyatanaLabel')
113124 },
114125 get status() {
115126 return this._proxy.cachedProperties.Status;
116 },
117 get iconName() {
118 if (this.status == SNIStatus.NEEDS_ATTENTION) {
119 return this._proxy.cachedProperties.AttentionIconName;
120 } else {
121 return this._proxy.cachedProperties.IconName;
122 }
123127 },
124128 get label() {
125129 return this._proxy.cachedProperties.XAyatanaLabel;
166170 // a few need to be passed down to the displaying code
167171
168172 // all these can mean that the icon has to be changed
169 if (property == 'Status' || property == 'IconName' || property == 'AttentionIconName')
173 if (property == 'Status' || property.substr(0, 4) == 'Icon' || property.substr(0, 13) == 'AttentionIcon')
170174 this._updateIcon()
175
176 // same for overlays
177 if (property.substr(0, 11) == 'OverlayIcon')
178 this._updateOverlayIcon()
179
180 // this may make all of our icons invalid
181 if (property == 'IconThemePath')
182 this._invalidateIcon()
171183
172184 // the label will be handled elsewhere
173185 if (property == 'XAyatanaLabel')
189201 Gtk.IconTheme.get_default().disconnect(this._iconThemeChangedHandle)
190202
191203 this.disconnectAll()
192 if (this._iconBin) this._iconBin.destroy()
204 this._iconBin.destroy()
193205 this._proxy.destroy()
194 },
195
196 _createIcon: function(icon_size) {
197 // shortcut variable
198 var icon_name = this.iconName;
199 // fallback icon
200 var gicon = Gio.icon_new_for_string("dialog-info");
206 this._iconCache.destroy()
207 },
208
209 _createIconByName: function(icon_size, icon_name) {
201210 // real_icon_size will contain the actual icon size in contrast to the requested icon size
202211 var real_icon_size = icon_size;
212 var gicon;
203213
204214 if (icon_name && icon_name[0] == "/") {
205215 //HACK: icon is a path name. This is not specified by the api but at least inidcator-sensors uses it.
223233 var icon_info = null;
224234
225235 // we try to avoid messing with the default icon theme, so we'll create a new one if needed
226 if (this._proxy.IconThemePath) {
236 if (this._proxy.cachedProperties.IconThemePath) {
227237 var icon_theme = new Gtk.IconTheme();
228238 Gtk.IconTheme.get_default().get_search_path().forEach(function(path) {
229239 icon_theme.append_search_path(path)
230240 });
231 icon_theme.append_search_path(this._proxy.IconThemePath);
241 icon_theme.append_search_path(this._proxy.cachedProperties.IconThemePath);
232242 icon_theme.set_screen(imports.gi.Gdk.Screen.get_default());
233243 } else {
234244 var icon_theme = Gtk.IconTheme.get_default();
253263 }
254264 }
255265
256 return new St.Icon({ gicon: gicon, icon_size: real_icon_size });
266 if (gicon)
267 return new St.Icon({ gicon: gicon, icon_size: real_icon_size });
268 else
269 return null;
270 },
271
272 _createIconFromPixmap: function(iconSize, iconPixmapArray) {
273 // the pixmap actually is an array of pixmaps with different sizes
274 // we use the one that is smaller or equal the iconSize
275
276 // maybe it's empty? that's bad.
277 if (!iconPixmapArray || iconPixmapArray.length < 1)
278 return null
279
280 let sortedIconPixmapArray = iconPixmapArray.sort(function(pixmapA, pixmapB) {
281 // we sort biggest to smallest
282 let areaA = pixmapA[0] * pixmapA[1]
283 let areaB = pixmapB[0] * pixmapB[1]
284
285 return areaB - areaA
286 })
287
288 let qualifiedIconPixmapArray = sortedIconPixmapArray.filter(function(pixmap) {
289 // we disqualify any pixmap that is bigger than our requested size
290 return pixmap[0] <= iconSize && pixmap[1] <= iconSize
291 })
292
293 // if no one got qualified, we use the smallest one available
294 let iconPixmap = qualifiedIconPixmapArray.length > 0 ? qualifiedIconPixmapArray[0] : sortedIconPixmapArray.pop()
295
296 let [ width, height, bytes ] = iconPixmap
297 let rowstride = width * 4 // hopefully this is correct
298
299 try {
300 let image = new Clutter.Image()
301 image.set_bytes(bytes,
302 Cogl.PixelFormat.ABGR_8888,
303 width,
304 height,
305 rowstride)
306
307 return new Clutter.Actor({
308 width: Math.min(width, iconSize),
309 height: Math.min(height, iconSize),
310 content: image
311 })
312 } catch (e) {
313 // the image data was probably bogus. We don't really know why, but it _does_ happen.
314 // we could log it here, but that doesn't really help in tracking it down.
315 return null
316 }
257317 },
258318
259319 // updates the icon in this._iconBin, managing caching
260 _updateIcon: function(force_redraw) {
320 _updateIcon: function() {
261321 // remove old icon
262 if (this._iconBin && this._iconBin.get_child()) {
263 this._iconBin.get_child().inUse = false
264 this._iconBin.set_child(null)
265 }
266
267 let icon_id = this.iconName + "@" + this._iconSize
268 let new_icon = IconCache.IconCache.instance.get(icon_id)
269
270 if (new_icon && force_redraw) {
271 IconCache.IconCache.instance.forceDestroy(icon_id)
272 new_icon = null
273 }
274
275 if (!new_icon) {
276 new_icon = this._createIcon(this._iconSize)
277 IconCache.IconCache.instance.add(icon_id, new_icon)
278 }
279
280 new_icon.inUse = true
281
282 if (this._iconBin)
283 this._iconBin.set_child(new_icon)
284 },
285
286 // returns an icon actor in the right size that contains the icon.
287 // the icon will be update automatically if it changes.
322 if (this._iconBin.baseIcon) {
323 if (this._iconBin.baseIcon.inUse) // cached icon
324 this._iconBin.baseIcon.inUse = false
325 else if (this._iconBin.baseIcon.destroy) // uncached
326 this._iconBin.baseIcon.destroy()
327
328 this._iconBin.baseIcon = null
329 }
330
331 let iconSize = this._iconBin.iconSize
332
333 // place to save the new icon
334 let newIcon = null
335
336 // me might need to use the AttentionIcon*, which have precedence over the normal icons
337 if (this._proxy.cachedProperties.Status == SNIStatus.NEEDS_ATTENTION) {
338 // try the attention name
339 if (!newIcon && this._proxy.cachedProperties.AttentionIconName)
340 newIcon = this._cacheOrCreateIconByName(iconSize, this._proxy.cachedProperties.AttentionIconName)
341
342 // or the attention pixmap
343 if (!newIcon && this._proxy.cachedProperties.AttentionIconPixmap)
344 newIcon = this._createIconFromPixmap(iconSize, this._proxy.cachedProperties.AttentionIconPixmap)
345 }
346
347 if (!newIcon && this._proxy.cachedProperties.IconName)
348 newIcon = this._cacheOrCreateIconByName(iconSize, this._proxy.cachedProperties.IconName)
349
350 if (!newIcon && this._proxy.cachedProperties.IconPixmap)
351 newIcon = this._createIconFromPixmap(iconSize, this._proxy.cachedProperties.IconPixmap)
352
353 this._iconBin.baseIcon = newIcon
354 },
355
356 _updateOverlayIcon: function() {
357 // remove old icon
358 if (this._iconBin.overlayIcon) {
359 if (this._iconBin.overlayIcon.inUse) // cached
360 this._iconBin.overlayIcon.inUse = false
361 else if (this._iconBin.overlayIcon.destroy) // uncached, but with destroy method
362 this._iconBin.overlayIcon.destroy()
363
364 this._iconBin.overlayIcon = null
365 }
366
367 // KDE hardcodes the overlay icon size to 10px (normal icon size 16px)
368 // we approximate that ratio for other sizes, too.
369 // our algorithms will always pick a smaller one instead of stretching it.
370 let iconSize = Math.floor(this._iconBin.iconSize / 1.6)
371
372 let newIcon = null
373
374 // create new
375 if (!newIcon && this._proxy.cachedProperties.OverlayIconName)
376 newIcon = this._cacheOrCreateIconByName(iconSize, this._proxy.cachedProperties.OverlayIconName)
377
378 if (!newIcon && this._proxy.cachedProperties.OverlayIconPixmap)
379 newIcon = this._createIconFromPixmap(iconSize, this._proxy.cachedProperties.OverlayIconPixmap)
380
381 this._iconBin.overlayIcon = newIcon
382 },
383
384 // Will look the icon up in the cache, if it's found
385 // it will return it. Otherwise, it will create it and cache it.
386 // The .inUse flag will be set to true. So when you don't need
387 // the returned icon anymore, make sure to check the .inUse property
388 // and set it to false if needed so that it can be picked up by the garbage
389 // collector.
390 _cacheOrCreateIconByName: function(iconSize, iconName) {
391 let id = iconName + '@' + iconSize
392
393 let icon = this._iconCache.get(id) || this._createIconByName(iconSize, iconName)
394
395 if (icon) {
396 icon.inUse = true
397 this._iconCache.add(id, icon)
398 }
399
400 return icon
401 },
402
403 // Returns an icon actor in the right size that contains the icon.
404 // the icon will be update automatically if it changes. Please make
405 // sure to destroy the returned actor when you don't need it anymore.
406 // When anyone request a _new_ icon actor, the old one will be emptied
407 // and the icon will be moved to the newly requested one.
288408 getIconActor: function(icon_size) {
289 this._iconSize = icon_size
290
291 if (this._iconBin) {
292 if (this._iconBin.get_child())
293 this._iconBin.get_child().inUse = false
294
295 this._iconBin.destroy()
296 }
297
298 this._iconBin = new St.Bin({
299 width: icon_size,
300 height: icon_size,
301 x_fill: false,
302 y_fill: false
303 })
304
305 if (this.isReady)
306 this._updateIcon(true)
307 else
308 Util.connectOnce(this, 'ready', this._updateIcon.bind(this))
309
310 return this._iconBin
409 this._iconBin.iconSize = icon_size
410
411 // defensive coding: if the icon bin still has a parent,
412 // we will liberate it now. The returned actor will adopt it.
413 if (this._iconBin.get_parent())
414 this._iconBin.get_parent().remove_child(this._iconBin)
415
416 // Because the size changed, we should update all the icons.
417 // If we are not constructed completely, it won't matter because
418 // when the properties arrive, the icons will be updated again.
419 this._updateIcon()
420 this._updateOverlayIcon()
421
422 return new St.Bin({ child: this._iconBin })
311423 },
312424
313425 // called when the icon theme changes
314426 _invalidateIcon: function() {
315 this._updateIcon(true);
427 this._iconCache.clear()
428
429 this._updateIcon()
430 this._updateOverlayIcon()
316431 },
317432
318433 open: function() {
329444 }
330445 });
331446 Signals.addSignalMethods(AppIndicator.prototype);
447
448 const IconContainer = new Lang.Class({
449 Name: 'AppIndicatorIconContainer',
450 Extends: Clutter.Actor,
451
452 _init: function(icon_size) {
453 this.parent()
454
455 this._icon_size = icon_size
456
457 this._baseIcon = null
458 this._overlayIcon = null
459 },
460
461 set baseIcon(newIcon) {
462 if (this._baseIcon && this._baseIcon.get_parent() == this)
463 this.remove_child(this._baseIcon)
464
465 this._baseIcon = newIcon
466
467 if (this._baseIcon)
468 this.add_child(this._baseIcon)
469 },
470
471 get baseIcon() {
472 return this._baseIcon
473 },
474
475 set overlayIcon(newIcon) {
476 if (this._overlayIcon && this._overlayIcon.get_parent() == this)
477 this.remove_child(this._overlayIcon)
478
479 this._overlayIcon = newIcon
480
481 if (this._overlayIcon)
482 this.add_child(this._overlayIcon)
483 },
484
485 get overlayIcon() {
486 return this._overlayIcon
487 },
488
489 set iconSize(newIconSize) {
490 this._icon_size = newIconSize
491 this.queue_relayout()
492 },
493
494 get iconSize() {
495 return this._icon_size
496 },
497
498 vfunc_get_preferred_height: function() {
499 return [ this._icon_size, this._icon_size ]
500 },
501
502 vfunc_get_preferred_width: function() {
503 return [ this._icon_size, this._icon_size ]
504 },
505
506 vfunc_allocate: function(box, flags) {
507 let [ availWidth, availHeight ] = box.get_size()
508
509 if (this._baseIcon) {
510 let [ minWidth, natWidth ] = this._baseIcon.get_preferred_width(-1)
511 let [ minHeight, natHeight ] = this._baseIcon.get_preferred_height(-1)
512
513 let childWidth = Math.min(availWidth, natWidth)
514 let childHeight = Math.min(availHeight, natHeight)
515
516 let childBox = new Clutter.ActorBox()
517
518 childBox.x1 = Math.floor((availWidth - childWidth)/2)
519 childBox.y1 = Math.floor((availHeight - childHeight)/2)
520
521 childBox.x2 = childBox.x1 + childWidth
522 childBox.y2 = childBox.y1 + childHeight
523
524 this._baseIcon.allocate(childBox, flags)
525 }
526
527 if (this._overlayIcon) {
528 let [ minWidth, natWidth ] = this._overlayIcon.get_preferred_width(-1)
529 let [ minHeight, natHeight ] = this._overlayIcon.get_preferred_height(-1)
530
531 let childWidth = Math.min(availWidth, natWidth)
532 let childHeight = Math.min(availHeight, natHeight)
533
534 let childBox = new Clutter.ActorBox()
535
536 childBox.x1 = availWidth - childWidth - 1
537 childBox.x2 = availWidth - 1
538 childBox.y1 = availHeight - childHeight - 1
539 childBox.y2 = availHeight - 1
540
541 this._overlayIcon.allocate(childBox, flags)
542 }
543 },
544
545 vfunc_paint: function() {
546 // paint the base and then the overlay
547 if (this._baseIcon)
548 this._baseIcon.paint()
549
550 if (this._overlayIcon)
551 this._overlayIcon.paint()
552 }
553 })
1818 const GLib = imports.gi.GLib;
1919 const Mainloop = imports.mainloop;
2020
21 const Util = imports.misc.extensionUtils.getCurrentExtension().imports.util;
22
2123 // The icon cache caches icon objects in case they're reused shortly aftwerwards.
2224 // This is necessary for some indicators like skype which rapidly switch between serveral icons.
2325 // Without caching, the garbage collection would never be able to handle the amount of new icon data.
2527 // The presence of an inUse property set to true on the icon will extend the lifetime.
2628
2729 // how to use: see IconCache.add, IconCache.get
28 // make sure the cached icon has a property inUse set to true if you don't want icon cache to destroy it for you.
2930 const IconCache = new Lang.Class({
3031 Name: 'IconCache',
3132
3940 },
4041
4142 add: function(id, o) {
42 //log("adding to cache: "+id);
43 //Util.Logger.debug("IconCache: adding "+id);
4344 if (!(o && id)) return null;
4445 if (id in this._cache)
4546 this._remove(id);
4950 },
5051
5152 _remove: function(id) {
52 //log('removing from cache: '+id);
53 //Util.Logger.debug('IconCache: removing '+id);
5354 if ('destroy' in this._cache[id]) this._cache[id].destroy();
5455 delete this._cache[id];
5556 delete this._lifetime[id];
5859 forceDestroy: function(id) {
5960 this._remove(id);
6061 },
62
63 // removes everything from the cache
64 clear: function() {
65 for (let id in this._cache)
66 this._remove(id)
67 },
6168
69 // returns an object from the cache, or null if it can't be found.
6270 get: function(id) {
6371 if (id in this._cache) {
6472 this._lifetime[id] = new Date().getTime() + this.LIFETIME_TIMESPAN; //renew lifetime
7179 var time = new Date().getTime();
7280 for (var id in this._cache) {
7381 if (this._cache[id].inUse) {
74 //log (id + " is in use.");
82 //Util.Logger.debug ("IconCache: " + id + " is in use.");
7583 continue;
7684 } else if (this._lifetime[id] < time) {
7785 this._remove(id);
7886 } else {
79 //log (id + " survived this round.");
87 //Util.Logger.debug("IconCache: " + id + " survived this round.");
8088 }
8189 }
8290 if (!this._stopGc) Mainloop.timeout_add(this.GC_INTERVAL, Lang.bind(this, this._gc));
8795 this._stopGc = true;
8896 }
8997 });
90 IconCache.instance = new IconCache();
2323 self.menu.addAction("Hello", self.onHelloClicked)
2424 self.menu.addAction("Change Status", self.toggleStatus)
2525 self.menu.addAction("Hide for some seconds", self.hideForAWhile)
26 self.menu.addAction("Switch to pixmap icon", self.usePixmap)
27 self.menu.addSeparator()
28 self.menu.addAction("Set overlay pixmap", self.setOverlayPixmap)
29 self.menu.addAction("Set overlay icon name", self.setOverlayName)
30 self.menu.addAction("Remove overlay icon", self.removeOverlay)
2631 self.tray.setContextMenu(self.menu)
2732
2833 self.tray.activateRequested.connect(self.onActivated)
4348 self.tray.setStatus(KStatusNotifierItem.Passive)
4449 Qt.QTimer.singleShot(2000, self.toggleStatus)
4550
51 def usePixmap(self):
52 self.tray.setIconByName(QString(""))
53 self.tray.setIconByPixmap(Qt.QIcon.fromTheme("accessories-calculator"))
54
55 def setOverlayPixmap(self):
56 self.tray.setOverlayIconByName(QString(""))
57 self.tray.setOverlayIconByPixmap(Qt.QIcon.fromTheme("gtk-dialog-info"))
58
59 def setOverlayName(self):
60 self.tray.setOverlayIconByPixmap(Qt.QIcon())
61 self.tray.setOverlayIconByName(QString("gtk-dialog-error"))
62
63 def removeOverlay(self):
64 self.tray.setOverlayIconByName(QString(""))
65 self.tray.setOverlayIconByPixmap(Qt.QIcon())
66
4667 if __name__ == '__main__':
4768 notifer = Notifier()
4869 App.exec_()