24 | 24 |
const St = imports.gi.St;
|
25 | 25 |
const Shell = imports.gi.Shell;
|
26 | 26 |
const GdkPixbuf = imports.gi.GdkPixbuf;
|
|
27 |
const Clutter = imports.gi.Clutter;
|
|
28 |
const Cogl = imports.gi.Cogl;
|
27 | 29 |
|
28 | 30 |
const Gettext = imports.gettext.domain('gnome-shell');
|
29 | 31 |
const _ = Gettext.gettext;
|
|
57 | 59 |
_init: function(bus_name, object) {
|
58 | 60 |
this.busName = bus_name
|
59 | 61 |
|
60 | |
this._iconSize = 16 //arbitrary value
|
|
62 |
this._iconBin = new IconContainer(16)
|
|
63 |
|
|
64 |
this._iconCache = new IconCache.IconCache()
|
61 | 65 |
|
62 | 66 |
this._proxy = new Util.XmlLessDBusProxy({
|
63 | 67 |
connection: Gio.DBus.session,
|
64 | 68 |
name: bus_name,
|
65 | 69 |
path: object,
|
66 | 70 |
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',
|
68 | 83 |
'Title',
|
69 | |
'Id',
|
70 | |
'Category',
|
71 | |
'Status',
|
72 | 84 |
'ToolTip',
|
73 | |
'XAyatanaLabel',
|
74 | |
'Menu',
|
75 | |
'IconName',
|
76 | |
'AttentionIconName',
|
77 | |
'OverlayIconName',
|
|
85 |
'XAyatanaLabel'
|
78 | 86 |
],
|
79 | 87 |
onReady: (function() {
|
80 | 88 |
this.isReady = true
|
|
98 | 106 |
|
99 | 107 |
if (this._proxy.propertyWhitelist.indexOf(prop + 'Pixmap') > -1)
|
100 | 108 |
this._proxy.invalidateProperty(prop + 'Pixmap')
|
|
109 |
|
|
110 |
if (this._proxy.propertyWhitelist.indexOf(prop + 'Name') > -1)
|
|
111 |
this._proxy.invalidateProperty(prop + 'Name')
|
101 | 112 |
} else if (signal == 'XAyatanaNewLabel') {
|
102 | 113 |
// and the ayatana guys made sure to invent yet another way of composing these signals...
|
103 | 114 |
this._proxy.invalidateProperty('XAyatanaLabel')
|
|
113 | 124 |
},
|
114 | 125 |
get status() {
|
115 | 126 |
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 | |
}
|
123 | 127 |
},
|
124 | 128 |
get label() {
|
125 | 129 |
return this._proxy.cachedProperties.XAyatanaLabel;
|
|
166 | 170 |
// a few need to be passed down to the displaying code
|
167 | 171 |
|
168 | 172 |
// 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')
|
170 | 174 |
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()
|
171 | 183 |
|
172 | 184 |
// the label will be handled elsewhere
|
173 | 185 |
if (property == 'XAyatanaLabel')
|
|
189 | 201 |
Gtk.IconTheme.get_default().disconnect(this._iconThemeChangedHandle)
|
190 | 202 |
|
191 | 203 |
this.disconnectAll()
|
192 | |
if (this._iconBin) this._iconBin.destroy()
|
|
204 |
this._iconBin.destroy()
|
193 | 205 |
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) {
|
201 | 210 |
// real_icon_size will contain the actual icon size in contrast to the requested icon size
|
202 | 211 |
var real_icon_size = icon_size;
|
|
212 |
var gicon;
|
203 | 213 |
|
204 | 214 |
if (icon_name && icon_name[0] == "/") {
|
205 | 215 |
//HACK: icon is a path name. This is not specified by the api but at least inidcator-sensors uses it.
|
|
223 | 233 |
var icon_info = null;
|
224 | 234 |
|
225 | 235 |
// 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) {
|
227 | 237 |
var icon_theme = new Gtk.IconTheme();
|
228 | 238 |
Gtk.IconTheme.get_default().get_search_path().forEach(function(path) {
|
229 | 239 |
icon_theme.append_search_path(path)
|
230 | 240 |
});
|
231 | |
icon_theme.append_search_path(this._proxy.IconThemePath);
|
|
241 |
icon_theme.append_search_path(this._proxy.cachedProperties.IconThemePath);
|
232 | 242 |
icon_theme.set_screen(imports.gi.Gdk.Screen.get_default());
|
233 | 243 |
} else {
|
234 | 244 |
var icon_theme = Gtk.IconTheme.get_default();
|
|
253 | 263 |
}
|
254 | 264 |
}
|
255 | 265 |
|
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 |
}
|
257 | 317 |
},
|
258 | 318 |
|
259 | 319 |
// updates the icon in this._iconBin, managing caching
|
260 | |
_updateIcon: function(force_redraw) {
|
|
320 |
_updateIcon: function() {
|
261 | 321 |
// 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.
|
288 | 408 |
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 })
|
311 | 423 |
},
|
312 | 424 |
|
313 | 425 |
// called when the icon theme changes
|
314 | 426 |
_invalidateIcon: function() {
|
315 | |
this._updateIcon(true);
|
|
427 |
this._iconCache.clear()
|
|
428 |
|
|
429 |
this._updateIcon()
|
|
430 |
this._updateOverlayIcon()
|
316 | 431 |
},
|
317 | 432 |
|
318 | 433 |
open: function() {
|
|
329 | 444 |
}
|
330 | 445 |
});
|
331 | 446 |
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 |
})
|