eslint: Add linter rules based on upstream gnome-shell and gjs and use auto-fix
Cleanup the code a bit with auto-fix feature of eslint
Marco Trevisan (TreviƱo)
3 years ago
13 | 13 | // along with this program; if not, write to the Free Software |
14 | 14 | // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
15 | 15 | |
16 | const Clutter = imports.gi.Clutter | |
17 | const Cogl = imports.gi.Cogl | |
18 | const GdkPixbuf = imports.gi.GdkPixbuf | |
19 | const Gio = imports.gi.Gio | |
20 | const GLib = imports.gi.GLib | |
21 | const GObject = imports.gi.GObject | |
22 | const Gtk = imports.gi.Gtk | |
23 | const St = imports.gi.St | |
24 | const Shell = imports.gi.Shell | |
16 | const Clutter = imports.gi.Clutter; | |
17 | const Cogl = imports.gi.Cogl; | |
18 | const GdkPixbuf = imports.gi.GdkPixbuf; | |
19 | const Gio = imports.gi.Gio; | |
20 | const GLib = imports.gi.GLib; | |
21 | const GObject = imports.gi.GObject; | |
22 | const Gtk = imports.gi.Gtk; | |
23 | const St = imports.gi.St; | |
24 | const Shell = imports.gi.Shell; | |
25 | 25 | |
26 | 26 | const Extension = imports.misc.extensionUtils.getCurrentExtension(); |
27 | const Signals = imports.signals | |
27 | const Signals = imports.signals; | |
28 | 28 | |
29 | 29 | const DBusMenu = Extension.imports.dbusMenu; |
30 | 30 | const IconCache = Extension.imports.iconCache; |
44 | 44 | APPLICATION: 'ApplicationStatus', |
45 | 45 | COMMUNICATIONS: 'Communications', |
46 | 46 | SYSTEM: 'SystemServices', |
47 | HARDWARE: 'Hardware' | |
47 | HARDWARE: 'Hardware', | |
48 | 48 | }; |
49 | 49 | |
50 | 50 | var SNIStatus = { |
51 | 51 | PASSIVE: 'Passive', |
52 | 52 | ACTIVE: 'Active', |
53 | NEEDS_ATTENTION: 'NeedsAttention' | |
53 | NEEDS_ATTENTION: 'NeedsAttention', | |
54 | 54 | }; |
55 | 55 | |
56 | 56 | const SNIconType = { |
66 | 66 | var AppIndicator = class AppIndicators_AppIndicator { |
67 | 67 | |
68 | 68 | constructor(service, bus_name, object) { |
69 | this.busName = bus_name | |
70 | this._uniqueId = bus_name + object | |
69 | this.busName = bus_name; | |
70 | this._uniqueId = bus_name + object; | |
71 | 71 | this._accumulatedSignals = new Set(); |
72 | 72 | |
73 | let interface_info = Gio.DBusInterfaceInfo.new_for_xml(Interfaces.StatusNotifierItem) | |
74 | ||
75 | //HACK: we cannot use Gio.DBusProxy.makeProxyWrapper because we need | |
73 | let interface_info = Gio.DBusInterfaceInfo.new_for_xml(Interfaces.StatusNotifierItem); | |
74 | ||
75 | // HACK: we cannot use Gio.DBusProxy.makeProxyWrapper because we need | |
76 | 76 | // to specify G_DBUS_PROXY_FLAGS_GET_INVALIDATED_PROPERTIES |
77 | 77 | this._cancellable = new Gio.Cancellable(); |
78 | 78 | this._proxy = new Gio.DBusProxy({ g_connection: Gio.DBus.session, |
79 | g_interface_name: interface_info.name, | |
80 | g_interface_info: interface_info, | |
81 | g_name: bus_name, | |
82 | g_object_path: object, | |
83 | g_flags: Gio.DBusProxyFlags.GET_INVALIDATED_PROPERTIES }) | |
79 | g_interface_name: interface_info.name, | |
80 | g_interface_info: interface_info, | |
81 | g_name: bus_name, | |
82 | g_object_path: object, | |
83 | g_flags: Gio.DBusProxyFlags.GET_INVALIDATED_PROPERTIES }); | |
84 | 84 | |
85 | 85 | this._setupProxy(); |
86 | Util.connectSmart(this._proxy, 'g-properties-changed', this, '_onPropertiesChanged') | |
87 | Util.connectSmart(this._proxy, 'g-signal', this, this._onProxySignal) | |
88 | Util.connectSmart(this._proxy, 'notify::g-name-owner', this, '_nameOwnerChanged') | |
86 | Util.connectSmart(this._proxy, 'g-properties-changed', this, '_onPropertiesChanged'); | |
87 | Util.connectSmart(this._proxy, 'g-signal', this, this._onProxySignal); | |
88 | Util.connectSmart(this._proxy, 'notify::g-name-owner', this, '_nameOwnerChanged'); | |
89 | 89 | |
90 | 90 | if (service !== bus_name && service.match(Util.BUS_ADDRESS_REGEX)) { |
91 | 91 | this._uniqueId = service; |
163 | 163 | get: () => { |
164 | 164 | const v = this._proxy.get_cached_property(name); |
165 | 165 | return v ? v.deep_unpack() : null; |
166 | } | |
166 | }, | |
167 | 167 | }); |
168 | 168 | } |
169 | 169 | |
188 | 188 | let prop = null; |
189 | 189 | |
190 | 190 | if (signal.startsWith('New')) |
191 | prop = signal.substr(3) | |
191 | prop = signal.substr(3); | |
192 | 192 | else if (signal.startsWith('XAyatanaNew')) |
193 | prop = 'XAyatana' + signal.substr(11) | |
193 | prop = `XAyatana${signal.substr(11)}`; | |
194 | 194 | |
195 | 195 | if (!prop) |
196 | 196 | return; |
197 | 197 | |
198 | 198 | [prop, `${prop}Name`, `${prop}Pixmap`].filter(p => |
199 | 199 | this._proxyPropertyList.includes(p)).forEach(p => |
200 | Util.refreshPropertyOnProxy(this._proxy, p, { | |
201 | skipEqualityCheck: p.endsWith('Pixmap'), | |
202 | }) | |
203 | ); | |
200 | Util.refreshPropertyOnProxy(this._proxy, p, { | |
201 | skipEqualityCheck: p.endsWith('Pixmap'), | |
202 | }), | |
203 | ); | |
204 | 204 | } |
205 | 205 | |
206 | 206 | async _onProxySignal(_proxy, _sender, signal, _params) { |
213 | 213 | GLib.PRIORITY_DEFAULT_IDLE, MAX_UPDATE_FREQUENCY, this._cancellable); |
214 | 214 | try { |
215 | 215 | await this._signalsAccumulator; |
216 | this._accumulatedSignals.forEach((s) => this._translateNewSignals(s)); | |
216 | this._accumulatedSignals.forEach(s => this._translateNewSignals(s)); | |
217 | 217 | this._accumulatedSignals.clear(); |
218 | 218 | } finally { |
219 | 219 | delete this._signalsAccumulator; |
220 | 220 | } |
221 | 221 | } |
222 | 222 | |
223 | //public property getters | |
223 | // public property getters | |
224 | 224 | get title() { |
225 | 225 | return this._proxy.Title; |
226 | 226 | } |
227 | ||
227 | 228 | get id() { |
228 | 229 | return this._proxy.Id; |
229 | 230 | } |
231 | ||
230 | 232 | get uniqueId() { |
231 | 233 | return this._uniqueId; |
232 | 234 | } |
235 | ||
233 | 236 | get status() { |
234 | 237 | return this._proxy.Status; |
235 | 238 | } |
239 | ||
236 | 240 | get label() { |
237 | 241 | return this._proxy.XAyatanaLabel; |
238 | 242 | } |
243 | ||
239 | 244 | get menuPath() { |
240 | 245 | if (this._proxy.Menu == '/NO_DBUSMENU') |
241 | 246 | return null; |
247 | 252 | return [ |
248 | 253 | this._proxy.AttentionIconName, |
249 | 254 | this._proxy.AttentionIconPixmap, |
250 | this._proxy.IconThemePath | |
251 | ] | |
255 | this._proxy.IconThemePath, | |
256 | ]; | |
252 | 257 | } |
253 | 258 | |
254 | 259 | get icon() { |
255 | 260 | return [ |
256 | 261 | this._proxy.IconName, |
257 | 262 | this._proxy.IconPixmap, |
258 | this._proxy.IconThemePath | |
259 | ] | |
263 | this._proxy.IconThemePath, | |
264 | ]; | |
260 | 265 | } |
261 | 266 | |
262 | 267 | get overlayIcon() { |
263 | 268 | return [ |
264 | 269 | this._proxy.OverlayIconName, |
265 | 270 | this._proxy.OverlayIconPixmap, |
266 | this._proxy.IconThemePath | |
267 | ] | |
271 | this._proxy.IconThemePath, | |
272 | ]; | |
268 | 273 | } |
269 | 274 | |
270 | 275 | get hasNameOwner() { |
280 | 285 | let props = Object.keys(changed.unpack()); |
281 | 286 | let signalsToEmit = new Set(); |
282 | 287 | |
283 | props.forEach((property) => { | |
288 | props.forEach(property => { | |
284 | 289 | // some property changes require updates on our part, |
285 | 290 | // a few need to be passed down to the displaying code |
286 | 291 | |
287 | 292 | // all these can mean that the icon has to be changed |
288 | 293 | if (property == 'Status' || |
289 | 294 | property.startsWith('Icon') || |
290 | property.startsWith('AttentionIcon')) { | |
291 | signalsToEmit.add('icon') | |
292 | } | |
295 | property.startsWith('AttentionIcon')) | |
296 | signalsToEmit.add('icon'); | |
297 | ||
293 | 298 | |
294 | 299 | // same for overlays |
295 | 300 | if (property.startsWith('OverlayIcon')) |
296 | signalsToEmit.add('overlay-icon') | |
301 | signalsToEmit.add('overlay-icon'); | |
297 | 302 | |
298 | 303 | // this may make all of our icons invalid |
299 | 304 | if (property == 'IconThemePath') { |
300 | signalsToEmit.add('icon') | |
301 | signalsToEmit.add('overlay-icon') | |
305 | signalsToEmit.add('icon'); | |
306 | signalsToEmit.add('overlay-icon'); | |
302 | 307 | } |
303 | 308 | |
304 | 309 | // the label will be handled elsewhere |
305 | 310 | if (property == 'XAyatanaLabel') |
306 | signalsToEmit.add('label') | |
311 | signalsToEmit.add('label'); | |
307 | 312 | |
308 | 313 | if (property == 'Menu') { |
309 | 314 | if (!this._checkIfReady() && this.isReady) |
310 | signalsToEmit.add('menu') | |
315 | signalsToEmit.add('menu'); | |
311 | 316 | } |
312 | 317 | |
313 | 318 | // status updates may cause the indicator to be hidden |
314 | 319 | if (property == 'Status') |
315 | signalsToEmit.add('status') | |
320 | signalsToEmit.add('status'); | |
316 | 321 | }); |
317 | 322 | |
318 | 323 | signalsToEmit.forEach(s => this.emit(s)); |
323 | 328 | } |
324 | 329 | |
325 | 330 | destroy() { |
326 | this.emit('destroy') | |
327 | ||
328 | this.disconnectAll() | |
331 | this.emit('destroy'); | |
332 | ||
333 | this.disconnectAll(); | |
329 | 334 | this._cancellable.cancel(); |
330 | 335 | this._nameWatcher && this._nameWatcher.destroy(); |
331 | 336 | Util.cancelRefreshPropertyOnProxy(this._proxy); |
338 | 343 | // nor can we call any X11 functions. Luckily, the Activate method usually works fine. |
339 | 344 | // parameters are "an hint to the item where to show eventual windows" [sic] |
340 | 345 | // ... and don't seem to have any effect. |
341 | this._proxy.ActivateRemote(0, 0) | |
346 | this._proxy.ActivateRemote(0, 0); | |
342 | 347 | } |
343 | 348 | |
344 | 349 | secondaryActivate() { |
345 | this._proxy.SecondaryActivateRemote(0, 0) | |
350 | this._proxy.SecondaryActivateRemote(0, 0); | |
346 | 351 | } |
347 | 352 | |
348 | 353 | scroll(dx, dy) { |
349 | 354 | if (dx != 0) |
350 | this._proxy.ScrollRemote(Math.floor(dx), 'horizontal') | |
355 | this._proxy.ScrollRemote(Math.floor(dx), 'horizontal'); | |
351 | 356 | |
352 | 357 | if (dy != 0) |
353 | this._proxy.ScrollRemote(Math.floor(dy), 'vertical') | |
358 | this._proxy.ScrollRemote(Math.floor(dy), 'vertical'); | |
354 | 359 | } |
355 | 360 | }; |
356 | 361 | Signals.addSignalMethods(AppIndicator.prototype); |
372 | 377 | let themeContext = St.ThemeContext.get_for_stage(global.stage); |
373 | 378 | this.height = icon_size * themeContext.scale_factor; |
374 | 379 | |
375 | this._indicator = indicator | |
376 | this._iconSize = icon_size | |
377 | this._iconCache = new IconCache.IconCache() | |
380 | this._indicator = indicator; | |
381 | this._iconSize = icon_size; | |
382 | this._iconCache = new IconCache.IconCache(); | |
378 | 383 | this._cancellable = new Gio.Cancellable(); |
379 | 384 | this._loadingIcons = new Set(); |
380 | 385 | |
381 | Util.connectSmart(this._indicator, 'icon', this, '_updateIcon') | |
382 | Util.connectSmart(this._indicator, 'overlay-icon', this, '_updateOverlayIcon') | |
383 | Util.connectSmart(this._indicator, 'reset', this, '_invalidateIcon') | |
384 | ||
385 | Util.connectSmart(themeContext, 'notify::scale-factor', this, (tc) => { | |
386 | Util.connectSmart(this._indicator, 'icon', this, '_updateIcon'); | |
387 | Util.connectSmart(this._indicator, 'overlay-icon', this, '_updateOverlayIcon'); | |
388 | Util.connectSmart(this._indicator, 'reset', this, '_invalidateIcon'); | |
389 | ||
390 | Util.connectSmart(themeContext, 'notify::scale-factor', this, tc => { | |
386 | 391 | this.height = icon_size * tc.scale_factor; |
387 | 392 | this._invalidateIcon(); |
388 | 393 | }); |
390 | 395 | Util.connectSmart(this._indicator, 'ready', this, () => { |
391 | 396 | this._updateIconClass(); |
392 | 397 | this._invalidateIcon(); |
393 | }) | |
394 | ||
395 | Util.connectSmart(Gtk.IconTheme.get_default(), 'changed', this, '_invalidateIcon') | |
398 | }); | |
399 | ||
400 | Util.connectSmart(Gtk.IconTheme.get_default(), 'changed', this, '_invalidateIcon'); | |
396 | 401 | |
397 | 402 | if (indicator.isReady) |
398 | this._invalidateIcon() | |
403 | this._invalidateIcon(); | |
399 | 404 | |
400 | 405 | this.connect('destroy', () => { |
401 | 406 | this._iconCache.destroy(); |
419 | 424 | // Will look the icon up in the cache, if it's found |
420 | 425 | // it will return it. Otherwise, it will create it and cache it. |
421 | 426 | async _cacheOrCreateIconByName(iconSize, iconName, themePath) { |
422 | let {scale_factor} = St.ThemeContext.get_for_stage(global.stage); | |
427 | let { scale_factor } = St.ThemeContext.get_for_stage(global.stage); | |
423 | 428 | let id = `${iconName}@${iconSize * scale_factor}${themePath || ''}`; |
424 | 429 | let gicon = this._iconCache.get(id); |
425 | 430 | |
497 | 502 | } else { |
498 | 503 | this.icon_size = this._iconSize; |
499 | 504 | return new Gio.FileIcon({ |
500 | file: Gio.File.new_for_path(path) | |
505 | file: Gio.File.new_for_path(path), | |
501 | 506 | }); |
502 | 507 | } |
503 | 508 | } catch (e) { |
509 | 514 | |
510 | 515 | _getIconInfo(name, themePath, size, scale) { |
511 | 516 | let path = null; |
512 | if (name && name[0] == "/") { | |
513 | //HACK: icon is a path name. This is not specified by the api but at least inidcator-sensors uses it. | |
517 | if (name && name[0] == '/') { | |
518 | // HACK: icon is a path name. This is not specified by the api but at least inidcator-sensors uses it. | |
514 | 519 | path = name; |
515 | 520 | } else if (name) { |
516 | 521 | // we manually look up the icon instead of letting st.icon do it for us |
517 | 522 | // this allows us to sneak in an indicator provided search path and to avoid ugly upscaled icons |
518 | 523 | |
519 | 524 | // indicator-application looks up a special "panel" variant, we just replicate that here |
520 | name = name + "-panel"; | |
525 | name += '-panel'; | |
521 | 526 | |
522 | 527 | // icon info as returned by the lookup |
523 | 528 | let iconInfo = null; |
526 | 531 | let icon_theme = null; |
527 | 532 | if (themePath) { |
528 | 533 | icon_theme = new Gtk.IconTheme(); |
529 | Gtk.IconTheme.get_default().get_search_path().forEach((path) => { | |
534 | Gtk.IconTheme.get_default().get_search_path().forEach(path => { | |
530 | 535 | icon_theme.append_search_path(path); |
531 | 536 | }); |
532 | 537 | icon_theme.append_search_path(themePath); |
565 | 570 | await new PromiseUtils.IdlePromise(GLib.PRIORITY_LOW, cancellable); |
566 | 571 | |
567 | 572 | for (let j = start; j < end; j += 4) { |
568 | let srcAlpha = src[j] | |
573 | let srcAlpha = src[j]; | |
569 | 574 | |
570 | 575 | dest[j] = src[j + 1]; /* red */ |
571 | 576 | dest[j + 1] = src[j + 2]; /* green */ |
583 | 588 | } |
584 | 589 | |
585 | 590 | async _createIconFromPixmap(iconSize, iconPixmapArray) { |
586 | let {scale_factor} = St.ThemeContext.get_for_stage(global.stage); | |
587 | iconSize = iconSize * scale_factor | |
591 | let { scale_factor } = St.ThemeContext.get_for_stage(global.stage); | |
592 | iconSize *= scale_factor; | |
588 | 593 | // the pixmap actually is an array of pixmaps with different sizes |
589 | 594 | // we use the one that is smaller or equal the iconSize |
590 | 595 | |
594 | 599 | |
595 | 600 | const sortedIconPixmapArray = iconPixmapArray.sort((pixmapA, pixmapB) => { |
596 | 601 | // we sort smallest to biggest |
597 | const areaA = pixmapA[0] * pixmapA[1] | |
598 | const areaB = pixmapB[0] * pixmapB[1] | |
599 | ||
600 | return areaA - areaB | |
601 | }) | |
602 | const areaA = pixmapA[0] * pixmapA[1]; | |
603 | const areaB = pixmapB[0] * pixmapB[1]; | |
604 | ||
605 | return areaA - areaB; | |
606 | }); | |
602 | 607 | |
603 | 608 | const qualifiedIconPixmapArray = sortedIconPixmapArray.filter(pixmap => |
604 | 609 | // we prefer any pixmap that is equal or bigger than our requested size |
605 | pixmap[0] >= iconSize && pixmap[1] >= iconSize) | |
606 | ||
607 | const iconPixmap = qualifiedIconPixmapArray.length > 0 ? | |
608 | qualifiedIconPixmapArray[0] : sortedIconPixmapArray.pop() | |
609 | ||
610 | const [ width, height, bytes ] = iconPixmap | |
611 | const rowStride = width * 4 // hopefully this is correct | |
610 | pixmap[0] >= iconSize && pixmap[1] >= iconSize); | |
611 | ||
612 | const iconPixmap = qualifiedIconPixmapArray.length > 0 | |
613 | ? qualifiedIconPixmapArray[0] : sortedIconPixmapArray.pop(); | |
614 | ||
615 | const [width, height, bytes] = iconPixmap; | |
616 | const rowStride = width * 4; // hopefully this is correct | |
612 | 617 | |
613 | 618 | const id = `__PIXMAP_ICON_${width}x${height}`; |
614 | 619 | if (this._loadingIcons.has(id)) { |
647 | 652 | this.gicon = new Gio.EmblemedIcon({ gicon }); |
648 | 653 | |
649 | 654 | if (!(gicon instanceof GdkPixbuf.Pixbuf)) |
650 | gicon.inUse = (this.gicon.get_icon() == gicon); | |
655 | gicon.inUse = this.gicon.get_icon() == gicon; | |
651 | 656 | } else { |
652 | 657 | this.gicon = null; |
653 | 658 | Util.Logger.critical(`unable to update icon for ${this._indicator.id}`); |
654 | 659 | } |
660 | } else if (gicon) { | |
661 | this._emblem = new Gio.Emblem({ icon: gicon }); | |
662 | ||
663 | if (!(gicon instanceof GdkPixbuf.Pixbuf)) | |
664 | gicon.inUse = true; | |
655 | 665 | } else { |
656 | if (gicon) { | |
657 | this._emblem = new Gio.Emblem({ icon: gicon }); | |
658 | ||
659 | if (!(gicon instanceof GdkPixbuf.Pixbuf)) | |
660 | gicon.inUse = true; | |
661 | } else { | |
662 | this._emblem = null; | |
663 | Util.Logger.debug(`unable to update icon emblem for ${this._indicator.id}`); | |
664 | } | |
666 | this._emblem = null; | |
667 | Util.Logger.debug(`unable to update icon emblem for ${this._indicator.id}`); | |
665 | 668 | } |
666 | 669 | |
667 | 670 | if (this.gicon) { |
676 | 679 | async _updateIconByType(iconType, iconSize) { |
677 | 680 | let icon; |
678 | 681 | switch (iconType) { |
679 | case SNIconType.ATTENTION: | |
680 | icon = this._indicator.attentionIcon; | |
681 | break; | |
682 | case SNIconType.NORMAL: | |
683 | icon = this._indicator.icon; | |
684 | break; | |
685 | case SNIconType.OVERLAY: | |
686 | icon = this._indicator.overlayIcon; | |
687 | break; | |
682 | case SNIconType.ATTENTION: | |
683 | icon = this._indicator.attentionIcon; | |
684 | break; | |
685 | case SNIconType.NORMAL: | |
686 | icon = this._indicator.icon; | |
687 | break; | |
688 | case SNIconType.OVERLAY: | |
689 | icon = this._indicator.overlayIcon; | |
690 | break; | |
688 | 691 | } |
689 | 692 | |
690 | 693 | const [name, pixmap, theme] = icon; |
713 | 716 | let { gicon } = this.gicon; |
714 | 717 | |
715 | 718 | if (gicon.inUse) |
716 | gicon.inUse = false | |
719 | gicon.inUse = false; | |
717 | 720 | } |
718 | 721 | |
719 | 722 | // we might need to use the AttentionIcon*, which have precedence over the normal icons |
720 | let iconType = this._indicator.status == SNIStatus.NEEDS_ATTENTION ? | |
721 | SNIconType.ATTENTION : SNIconType.NORMAL; | |
723 | let iconType = this._indicator.status == SNIStatus.NEEDS_ATTENTION | |
724 | ? SNIconType.ATTENTION : SNIconType.NORMAL; | |
722 | 725 | |
723 | 726 | this._updateIconByType(iconType, this._iconSize); |
724 | 727 | } |
734 | 737 | // KDE hardcodes the overlay icon size to 10px (normal icon size 16px) |
735 | 738 | // we approximate that ratio for other sizes, too. |
736 | 739 | // our algorithms will always pick a smaller one instead of stretching it. |
737 | let iconSize = Math.floor(this._iconSize / 1.6) | |
740 | let iconSize = Math.floor(this._iconSize / 1.6); | |
738 | 741 | |
739 | 742 | this._updateIconByType(SNIconType.OVERLAY, iconSize); |
740 | 743 | } |
741 | 744 | |
742 | 745 | // called when the icon theme changes |
743 | 746 | _invalidateIcon() { |
744 | this._iconCache.clear() | |
747 | this._iconCache.clear(); | |
745 | 748 | this._cancelLoading(); |
746 | 749 | |
747 | this._updateIcon() | |
748 | this._updateOverlayIcon() | |
750 | this._updateIcon(); | |
751 | this._updateOverlayIcon(); | |
749 | 752 | } |
750 | 753 | }); |
12 | 12 | // You should have received a copy of the GNU General Public License |
13 | 13 | // along with this program; if not, write to the Free Software |
14 | 14 | // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
15 | const Atk = imports.gi.Atk | |
16 | const Clutter = imports.gi.Clutter | |
17 | const Gio = imports.gi.Gio | |
18 | const GLib = imports.gi.GLib | |
19 | const GdkPixbuf = imports.gi.GdkPixbuf | |
20 | const PopupMenu = imports.ui.popupMenu | |
21 | const Signals = imports.signals | |
22 | const St = imports.gi.St | |
23 | ||
24 | const Extension = imports.misc.extensionUtils.getCurrentExtension() | |
25 | ||
26 | const DBusInterfaces = Extension.imports.interfaces | |
15 | const Atk = imports.gi.Atk; | |
16 | const Clutter = imports.gi.Clutter; | |
17 | const Gio = imports.gi.Gio; | |
18 | const GLib = imports.gi.GLib; | |
19 | const GdkPixbuf = imports.gi.GdkPixbuf; | |
20 | const PopupMenu = imports.ui.popupMenu; | |
21 | const Signals = imports.signals; | |
22 | const St = imports.gi.St; | |
23 | ||
24 | const Extension = imports.misc.extensionUtils.getCurrentExtension(); | |
25 | ||
26 | const DBusInterfaces = Extension.imports.interfaces; | |
27 | 27 | const PromiseUtils = Extension.imports.promiseUtils; |
28 | const Util = Extension.imports.util | |
29 | ||
30 | ////////////////////////////////////////////////////////////////////////// | |
28 | const Util = Extension.imports.util; | |
29 | ||
30 | // //////////////////////////////////////////////////////////////////////// | |
31 | 31 | // PART ONE: "ViewModel" backend implementation. |
32 | 32 | // Both code and design are inspired by libdbusmenu |
33 | ////////////////////////////////////////////////////////////////////////// | |
33 | // //////////////////////////////////////////////////////////////////////// | |
34 | 34 | |
35 | 35 | /** |
36 | 36 | * Saves menu property values and handles type checking and defaults |
41 | 41 | this._props = new Map(); |
42 | 42 | |
43 | 43 | if (initial_properties) { |
44 | for (let i in initial_properties) { | |
45 | this.set(i, initial_properties[i]) | |
46 | } | |
44 | for (let i in initial_properties) | |
45 | this.set(i, initial_properties[i]); | |
46 | ||
47 | 47 | } |
48 | 48 | } |
49 | 49 | |
50 | 50 | set(name, value) { |
51 | 51 | if (name in PropertyStore.MandatedTypes && value && !value.is_of_type(PropertyStore.MandatedTypes[name])) |
52 | Util.Logger.warn("Cannot set property "+name+": type mismatch!") | |
52 | Util.Logger.warn(`Cannot set property ${name}: type mismatch!`); | |
53 | 53 | else if (value) |
54 | 54 | this._props.set(name, value); |
55 | 55 | else |
61 | 61 | if (prop) |
62 | 62 | return prop; |
63 | 63 | else if (name in PropertyStore.DefaultValues) |
64 | return PropertyStore.DefaultValues[name] | |
64 | return PropertyStore.DefaultValues[name]; | |
65 | 65 | else |
66 | return null | |
66 | return null; | |
67 | 67 | } |
68 | 68 | }; |
69 | 69 | |
70 | 70 | // we list all the properties we know and use here, so we won' have to deal with unexpected type mismatches |
71 | 71 | PropertyStore.MandatedTypes = { |
72 | 'visible' : GLib.VariantType.new("b"), | |
73 | 'enabled' : GLib.VariantType.new("b"), | |
74 | 'label' : GLib.VariantType.new("s"), | |
75 | 'type' : GLib.VariantType.new("s"), | |
76 | 'children-display' : GLib.VariantType.new("s"), | |
77 | 'icon-name' : GLib.VariantType.new("s"), | |
78 | 'icon-data' : GLib.VariantType.new("ay"), | |
79 | 'toggle-type' : GLib.VariantType.new("s"), | |
80 | 'toggle-state' : GLib.VariantType.new("i") | |
81 | } | |
72 | 'visible': GLib.VariantType.new('b'), | |
73 | 'enabled': GLib.VariantType.new('b'), | |
74 | 'label': GLib.VariantType.new('s'), | |
75 | 'type': GLib.VariantType.new('s'), | |
76 | 'children-display': GLib.VariantType.new('s'), | |
77 | 'icon-name': GLib.VariantType.new('s'), | |
78 | 'icon-data': GLib.VariantType.new('ay'), | |
79 | 'toggle-type': GLib.VariantType.new('s'), | |
80 | 'toggle-state': GLib.VariantType.new('i'), | |
81 | }; | |
82 | 82 | |
83 | 83 | PropertyStore.DefaultValues = { |
84 | 84 | 'visible': GLib.Variant.new_boolean(true), |
85 | 85 | 'enabled': GLib.Variant.new_boolean(true), |
86 | 'label' : GLib.Variant.new_string(''), | |
87 | 'type' : GLib.Variant.new_string("standard") | |
86 | 'label': GLib.Variant.new_string(''), | |
87 | 'type': GLib.Variant.new_string('standard'), | |
88 | 88 | // elements not in here must return null |
89 | } | |
89 | }; | |
90 | 90 | |
91 | 91 | /** |
92 | 92 | * Represents a single menu item |
95 | 95 | |
96 | 96 | // will steal the properties object |
97 | 97 | constructor(client, id, properties, children_ids) { |
98 | this._client = client | |
99 | this._id = id | |
100 | this._propStore = new PropertyStore(properties) | |
101 | this._children_ids = children_ids | |
98 | this._client = client; | |
99 | this._id = id; | |
100 | this._propStore = new PropertyStore(properties); | |
101 | this._children_ids = children_ids; | |
102 | 102 | } |
103 | 103 | |
104 | 104 | property_get(prop_name) { |
105 | let prop = this.property_get_variant(prop_name) | |
106 | return prop ? prop.get_string()[0] : null | |
105 | let prop = this.property_get_variant(prop_name); | |
106 | return prop ? prop.get_string()[0] : null; | |
107 | 107 | } |
108 | 108 | |
109 | 109 | property_get_variant(prop_name) { |
110 | return this._propStore.get(prop_name) | |
110 | return this._propStore.get(prop_name); | |
111 | 111 | } |
112 | 112 | |
113 | 113 | property_get_bool(prop_name) { |
114 | let prop = this.property_get_variant(prop_name) | |
115 | return prop ? prop.get_boolean() : false | |
114 | let prop = this.property_get_variant(prop_name); | |
115 | return prop ? prop.get_boolean() : false; | |
116 | 116 | } |
117 | 117 | |
118 | 118 | property_get_int(prop_name) { |
119 | let prop = this.property_get_variant(prop_name) | |
120 | return prop ? prop.get_int32() : 0 | |
119 | let prop = this.property_get_variant(prop_name); | |
120 | return prop ? prop.get_int32() : 0; | |
121 | 121 | } |
122 | 122 | |
123 | 123 | property_set(prop, value) { |
124 | this._propStore.set(prop, value) | |
125 | ||
126 | this.emit('property-changed', prop, this.property_get_variant(prop)) | |
124 | this._propStore.set(prop, value); | |
125 | ||
126 | this.emit('property-changed', prop, this.property_get_variant(prop)); | |
127 | 127 | } |
128 | 128 | |
129 | 129 | get_children_ids() { |
130 | return this._children_ids.concat() // clone it! | |
130 | return this._children_ids.concat(); // clone it! | |
131 | 131 | } |
132 | 132 | |
133 | 133 | add_child(pos, child_id) { |
134 | this._children_ids.splice(pos, 0, child_id) | |
135 | this.emit('child-added', this._client.get_item(child_id), pos) | |
134 | this._children_ids.splice(pos, 0, child_id); | |
135 | this.emit('child-added', this._client.get_item(child_id), pos); | |
136 | 136 | } |
137 | 137 | |
138 | 138 | remove_child(child_id) { |
139 | 139 | // find it |
140 | let pos = -1 | |
140 | let pos = -1; | |
141 | 141 | for (let i = 0; i < this._children_ids.length; ++i) { |
142 | 142 | if (this._children_ids[i] == child_id) { |
143 | pos = i | |
144 | break | |
143 | pos = i; | |
144 | break; | |
145 | 145 | } |
146 | 146 | } |
147 | 147 | |
148 | 148 | if (pos < 0) { |
149 | Util.Logger.critical("Trying to remove child which doesn't exist") | |
149 | Util.Logger.critical("Trying to remove child which doesn't exist"); | |
150 | 150 | } else { |
151 | this._children_ids.splice(pos, 1) | |
152 | this.emit('child-removed', this._client.get_item(child_id)) | |
151 | this._children_ids.splice(pos, 1); | |
152 | this.emit('child-removed', this._client.get_item(child_id)); | |
153 | 153 | } |
154 | 154 | } |
155 | 155 | |
156 | 156 | move_child(child_id, newpos) { |
157 | 157 | // find the old position |
158 | let oldpos = -1 | |
158 | let oldpos = -1; | |
159 | 159 | for (let i = 0; i < this._children_ids.length; ++i) { |
160 | 160 | if (this._children_ids[i] == child_id) { |
161 | oldpos = i | |
162 | break | |
161 | oldpos = i; | |
162 | break; | |
163 | 163 | } |
164 | 164 | } |
165 | 165 | |
166 | 166 | if (oldpos < 0) { |
167 | Util.Logger.critical("tried to move child which wasn't in the list") | |
168 | return | |
167 | Util.Logger.critical("tried to move child which wasn't in the list"); | |
168 | return; | |
169 | 169 | } |
170 | 170 | |
171 | 171 | if (oldpos != newpos) { |
172 | this._children_ids.splice(oldpos, 1) | |
173 | this._children_ids.splice(newpos, 0, child_id) | |
174 | this.emit('child-moved', oldpos, newpos, this._client.get_item(child_id)) | |
172 | this._children_ids.splice(oldpos, 1); | |
173 | this._children_ids.splice(newpos, 0, child_id); | |
174 | this.emit('child-moved', oldpos, newpos, this._client.get_item(child_id)); | |
175 | 175 | } |
176 | 176 | } |
177 | 177 | |
181 | 181 | |
182 | 182 | handle_event(event, data, timestamp) { |
183 | 183 | if (!data) |
184 | data = GLib.Variant.new_int32(0) | |
185 | ||
186 | this._client.send_event(this._id, event, data, timestamp) | |
184 | data = GLib.Variant.new_int32(0); | |
185 | ||
186 | this._client.send_event(this._id, event, data, timestamp); | |
187 | 187 | } |
188 | 188 | |
189 | 189 | get_id() { |
190 | return this._id | |
190 | return this._id; | |
191 | 191 | } |
192 | 192 | |
193 | 193 | send_about_to_show() { |
194 | this._client.send_about_to_show(this._id) | |
195 | } | |
196 | } | |
197 | Signals.addSignalMethods(DbusMenuItem.prototype) | |
194 | this._client.send_about_to_show(this._id); | |
195 | } | |
196 | }; | |
197 | Signals.addSignalMethods(DbusMenuItem.prototype); | |
198 | 198 | |
199 | 199 | |
200 | 200 | const BusClientProxy = Gio.DBusProxy.makeProxyWrapper(DBusInterfaces.DBusMenu); |
210 | 210 | busName, |
211 | 211 | busPath, |
212 | 212 | this._clientReady.bind(this), |
213 | this._cancellable) | |
213 | this._cancellable); | |
214 | 214 | this._items = new Map([ |
215 | 215 | [ |
216 | 216 | 0, |
217 | 217 | new DbusMenuItem(this, 0, { |
218 | 218 | 'children-display': GLib.Variant.new_string('submenu'), |
219 | 219 | }, []), |
220 | ] | |
220 | ], | |
221 | 221 | ]); |
222 | 222 | |
223 | 223 | // will be set to true if a layout update is requested while one is already in progress |
224 | 224 | // then the handler that completes the layout update will request another update |
225 | this._flagLayoutUpdateRequired = false | |
226 | this._flagLayoutUpdateInProgress = false | |
225 | this._flagLayoutUpdateRequired = false; | |
226 | this._flagLayoutUpdateInProgress = false; | |
227 | 227 | |
228 | 228 | // property requests are queued |
229 | 229 | this._propertiesRequestedFor = new Set(/* ids */); |
244 | 244 | |
245 | 245 | _requestLayoutUpdate() { |
246 | 246 | if (this._flagLayoutUpdateInProgress) |
247 | this._flagLayoutUpdateRequired = true | |
247 | this._flagLayoutUpdateRequired = true; | |
248 | 248 | else |
249 | this._beginLayoutUpdate() | |
249 | this._beginLayoutUpdate(); | |
250 | 250 | } |
251 | 251 | |
252 | 252 | async _requestProperties(id) { |
263 | 263 | |
264 | 264 | _beginRequestProperties() { |
265 | 265 | this._proxy.GetGroupPropertiesRemote( |
266 | Array.from(this._propertiesRequestedFor), | |
267 | [], | |
268 | this._cancellable, | |
269 | this._endRequestProperties.bind(this)) | |
266 | Array.from(this._propertiesRequestedFor), | |
267 | [], | |
268 | this._cancellable, | |
269 | this._endRequestProperties.bind(this)); | |
270 | 270 | |
271 | 271 | this._propertiesRequestedFor.clear(); |
272 | return false | |
272 | return false; | |
273 | 273 | } |
274 | 274 | |
275 | 275 | _endRequestProperties(result, error) { |
276 | 276 | if (error) { |
277 | 277 | if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) |
278 | 278 | Util.Logger.warn(`Could not retrieve properties: ${error}`); |
279 | return | |
279 | return; | |
280 | 280 | } |
281 | 281 | |
282 | 282 | // for some funny reason, the result array is hidden in an array |
283 | 283 | result[0].forEach(([id, properties]) => { |
284 | 284 | let item = this._items.get(id); |
285 | 285 | if (!item) |
286 | return | |
286 | return; | |
287 | 287 | |
288 | 288 | for (let prop in properties) |
289 | item.property_set(prop, properties[prop]) | |
289 | item.property_set(prop, properties[prop]); | |
290 | 290 | }); |
291 | 291 | } |
292 | 292 | |
293 | 293 | // Traverses the list of cached menu items and removes everyone that is not in the list |
294 | 294 | // so we don't keep alive unused items |
295 | 295 | _gcItems() { |
296 | let tag = new Date().getTime() | |
297 | ||
298 | let toTraverse = [ 0 ] | |
296 | let tag = new Date().getTime(); | |
297 | ||
298 | let toTraverse = [0]; | |
299 | 299 | while (toTraverse.length > 0) { |
300 | let item = this.get_item(toTraverse.shift()) | |
301 | item._dbusClientGcTag = tag | |
302 | Array.prototype.push.apply(toTraverse, item.get_children_ids()) | |
300 | let item = this.get_item(toTraverse.shift()); | |
301 | item._dbusClientGcTag = tag; | |
302 | Array.prototype.push.apply(toTraverse, item.get_children_ids()); | |
303 | 303 | } |
304 | 304 | |
305 | 305 | this._items.forEach((i, id) => { |
314 | 314 | // we only read the type property, because if the type changes after reading all properties, |
315 | 315 | // the view would have to replace the item completely which we try to avoid |
316 | 316 | this._proxy.GetLayoutRemote(0, -1, |
317 | [ 'type', 'children-display' ], | |
317 | ['type', 'children-display'], | |
318 | 318 | this._cancellable, |
319 | this._endLayoutUpdate.bind(this)) | |
320 | ||
321 | this._flagLayoutUpdateRequired = false | |
322 | this._flagLayoutUpdateInProgress = true | |
319 | this._endLayoutUpdate.bind(this)); | |
320 | ||
321 | this._flagLayoutUpdateRequired = false; | |
322 | this._flagLayoutUpdateInProgress = true; | |
323 | 323 | } |
324 | 324 | |
325 | 325 | _endLayoutUpdate(result, error) { |
326 | 326 | if (error) { |
327 | 327 | if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) |
328 | 328 | Util.Logger.warn(`While reading menu layout on proxy ${this._proxy.g_name_owner}: ${error}`); |
329 | return | |
330 | } | |
331 | ||
332 | let [ revision, root ] = result | |
333 | this._doLayoutUpdate(root) | |
334 | this._gcItems() | |
329 | return; | |
330 | } | |
331 | ||
332 | let [revision, root] = result; | |
333 | this._doLayoutUpdate(root); | |
334 | this._gcItems(); | |
335 | 335 | |
336 | 336 | if (this._flagLayoutUpdateRequired) |
337 | this._beginLayoutUpdate() | |
337 | this._beginLayoutUpdate(); | |
338 | 338 | else |
339 | this._flagLayoutUpdateInProgress = false | |
339 | this._flagLayoutUpdateInProgress = false; | |
340 | 340 | } |
341 | 341 | |
342 | 342 | _doLayoutUpdate(item) { |
343 | let [ id, properties, children ] = item | |
344 | ||
345 | let childrenUnpacked = children.map(c => c.deep_unpack()) | |
346 | let childrenIds = childrenUnpacked.map(c => c[0]) | |
343 | let [id, properties, children] = item; | |
344 | ||
345 | let childrenUnpacked = children.map(c => c.deep_unpack()); | |
346 | let childrenIds = childrenUnpacked.map(c => c[0]); | |
347 | 347 | |
348 | 348 | // make sure all our children exist |
349 | 349 | childrenUnpacked.forEach(c => this._doLayoutUpdate(c)); |
352 | 352 | const menuItem = this._items.get(id); |
353 | 353 | if (menuItem) { |
354 | 354 | // we do, update our properties if necessary |
355 | for (let prop in properties) { | |
356 | menuItem.property_set(prop, properties[prop]) | |
357 | } | |
355 | for (let prop in properties) | |
356 | menuItem.property_set(prop, properties[prop]); | |
357 | ||
358 | 358 | |
359 | 359 | // make sure our children are all at the right place, and exist |
360 | let oldChildrenIds = menuItem.get_children_ids() | |
360 | let oldChildrenIds = menuItem.get_children_ids(); | |
361 | 361 | for (let i = 0; i < childrenIds.length; ++i) { |
362 | 362 | // try to recycle an old child |
363 | let oldChild = -1 | |
363 | let oldChild = -1; | |
364 | 364 | for (let j = 0; j < oldChildrenIds.length; ++j) { |
365 | 365 | if (oldChildrenIds[j] == childrenIds[i]) { |
366 | oldChild = oldChildrenIds.splice(j, 1)[0] | |
367 | break | |
366 | oldChild = oldChildrenIds.splice(j, 1)[0]; | |
367 | break; | |
368 | 368 | } |
369 | 369 | } |
370 | 370 | |
371 | 371 | if (oldChild < 0) { |
372 | 372 | // no old child found, so create a new one! |
373 | menuItem.add_child(i, childrenIds[i]) | |
373 | menuItem.add_child(i, childrenIds[i]); | |
374 | 374 | } else { |
375 | 375 | // old child found, reuse it! |
376 | menuItem.move_child(childrenIds[i], i) | |
376 | menuItem.move_child(childrenIds[i], i); | |
377 | 377 | } |
378 | 378 | } |
379 | 379 | |
382 | 382 | } else { |
383 | 383 | // we don't, so let's create us |
384 | 384 | this._items.set(id, new DbusMenuItem(this, id, properties, childrenIds)); |
385 | this._requestProperties(id) | |
386 | } | |
387 | ||
388 | return id | |
385 | this._requestProperties(id); | |
386 | } | |
387 | ||
388 | return id; | |
389 | 389 | } |
390 | 390 | |
391 | 391 | _clientReady(result, error) { |
395 | 395 | return; |
396 | 396 | } |
397 | 397 | |
398 | this._requestLayoutUpdate() | |
398 | this._requestLayoutUpdate(); | |
399 | 399 | |
400 | 400 | // listen for updated layouts and properties |
401 | this._proxy.connectSignal("LayoutUpdated", this._onLayoutUpdated.bind(this)) | |
402 | this._proxy.connectSignal("ItemsPropertiesUpdated", this._onPropertiesUpdated.bind(this)) | |
401 | this._proxy.connectSignal('LayoutUpdated', this._onLayoutUpdated.bind(this)); | |
402 | this._proxy.connectSignal('ItemsPropertiesUpdated', this._onPropertiesUpdated.bind(this)); | |
403 | 403 | } |
404 | 404 | |
405 | 405 | get_item(id) { |
415 | 415 | * and don't return a boolean, so we need to support both cases */ |
416 | 416 | let connection = this._proxy.get_connection(); |
417 | 417 | connection.call(this._proxy.get_name(), this._proxy.get_object_path(), |
418 | this._proxy.get_interface_name(), 'AboutToShow', | |
419 | new GLib.Variant("(i)", [id]), null, | |
420 | Gio.DBusCallFlags.NONE, -1, null, (proxy, res) => { | |
421 | try { | |
422 | let ret = proxy.call_finish(res); | |
423 | if ((ret.is_of_type(new GLib.VariantType('(b)')) && | |
418 | this._proxy.get_interface_name(), 'AboutToShow', | |
419 | new GLib.Variant('(i)', [id]), null, | |
420 | Gio.DBusCallFlags.NONE, -1, null, (proxy, res) => { | |
421 | try { | |
422 | let ret = proxy.call_finish(res); | |
423 | if ((ret.is_of_type(new GLib.VariantType('(b)')) && | |
424 | 424 | ret.get_child_value(0).get_boolean()) || |
425 | ret.is_of_type(new GLib.VariantType('()'))) { | |
426 | this._requestLayoutUpdate(); | |
425 | ret.is_of_type(new GLib.VariantType('()'))) | |
426 | this._requestLayoutUpdate(); | |
427 | ||
428 | } catch (e) { | |
429 | Util.Logger.warn(`Impossible to send about-to-show to menu: ${e}`); | |
427 | 430 | } |
428 | } catch (e) { | |
429 | Util.Logger.warn("Impossible to send about-to-show to menu: " + e); | |
430 | } | |
431 | }); | |
431 | }); | |
432 | 432 | } |
433 | 433 | |
434 | 434 | send_event(id, event, params, timestamp) { |
435 | 435 | if (!this._proxy) |
436 | return | |
436 | return; | |
437 | 437 | |
438 | 438 | this._proxy.EventRemote(id, event, params, timestamp, this._cancellable, |
439 | () => { /* we don't care */ }) | |
439 | () => { /* we don't care */ }); | |
440 | 440 | } |
441 | 441 | |
442 | 442 | _onLayoutUpdated() { |
443 | this._requestLayoutUpdate() | |
443 | this._requestLayoutUpdate(); | |
444 | 444 | } |
445 | 445 | |
446 | 446 | _onPropertiesUpdated(proxy, name, [changed, removed]) { |
447 | 447 | changed.forEach(([id, props]) => { |
448 | 448 | let item = this._items.get(id); |
449 | 449 | if (!item) |
450 | return | |
450 | return; | |
451 | 451 | |
452 | 452 | for (let prop in props) |
453 | item.property_set(prop, props[prop]) | |
453 | item.property_set(prop, props[prop]); | |
454 | 454 | }); |
455 | 455 | removed.forEach(([id, propNames]) => { |
456 | 456 | let item = this._items.get(id); |
457 | 457 | if (!item) |
458 | return | |
458 | return; | |
459 | 459 | |
460 | 460 | propNames.forEach(propName => item.property_set(propName, null)); |
461 | 461 | }); |
462 | 462 | } |
463 | 463 | |
464 | 464 | destroy() { |
465 | this.emit('destroy') | |
465 | this.emit('destroy'); | |
466 | 466 | |
467 | 467 | this._cancellable.cancel(); |
468 | Signals._disconnectAll.apply(this._proxy) | |
469 | ||
470 | this._proxy = null | |
471 | } | |
472 | } | |
473 | Signals.addSignalMethods(DBusClient.prototype) | |
474 | ||
475 | ////////////////////////////////////////////////////////////////////////// | |
468 | Signals._disconnectAll.apply(this._proxy); | |
469 | ||
470 | this._proxy = null; | |
471 | } | |
472 | }; | |
473 | Signals.addSignalMethods(DBusClient.prototype); | |
474 | ||
475 | // //////////////////////////////////////////////////////////////////////// | |
476 | 476 | // PART TWO: "View" frontend implementation. |
477 | ////////////////////////////////////////////////////////////////////////// | |
477 | // //////////////////////////////////////////////////////////////////////// | |
478 | 478 | |
479 | 479 | // https://bugzilla.gnome.org/show_bug.cgi?id=731514 |
480 | 480 | // GNOME 3.10 and 3.12 can't open a nested submenu. |
481 | 481 | // Patches have been written, but it's not clear when (if?) they will be applied. |
482 | 482 | // We also don't know whether they will be backported to 3.10, so we will work around |
483 | 483 | // it in the meantime. Offending versions can be clearly identified: |
484 | const NEED_NESTED_SUBMENU_FIX = '_setOpenedSubMenu' in PopupMenu.PopupMenu.prototype | |
484 | const NEED_NESTED_SUBMENU_FIX = '_setOpenedSubMenu' in PopupMenu.PopupMenu.prototype; | |
485 | 485 | |
486 | 486 | /** |
487 | 487 | * Creates new wrapper menu items and injects methods for managing them at runtime. |
490 | 490 | * handlers, so any `this` will refer to a menu item create in createItem |
491 | 491 | */ |
492 | 492 | const MenuItemFactory = { |
493 | createItem: function(client, dbusItem) { | |
493 | createItem(client, dbusItem) { | |
494 | 494 | // first, decide whether it's a submenu or not |
495 | if (dbusItem.property_get("children-display") == "submenu") | |
496 | var shellItem = new PopupMenu.PopupSubMenuMenuItem("FIXME") | |
497 | else if (dbusItem.property_get("type") == "separator") | |
498 | var shellItem = new PopupMenu.PopupSeparatorMenuItem('') | |
495 | if (dbusItem.property_get('children-display') == 'submenu') | |
496 | var shellItem = new PopupMenu.PopupSubMenuMenuItem('FIXME'); | |
497 | else if (dbusItem.property_get('type') == 'separator') | |
498 | var shellItem = new PopupMenu.PopupSeparatorMenuItem(''); | |
499 | 499 | else |
500 | var shellItem = new PopupMenu.PopupMenuItem("FIXME") | |
501 | ||
502 | shellItem._dbusItem = dbusItem | |
503 | shellItem._dbusClient = client | |
500 | var shellItem = new PopupMenu.PopupMenuItem('FIXME'); | |
501 | ||
502 | shellItem._dbusItem = dbusItem; | |
503 | shellItem._dbusClient = client; | |
504 | 504 | |
505 | 505 | if (shellItem instanceof PopupMenu.PopupMenuItem) { |
506 | shellItem._icon = new St.Icon({ style_class: 'popup-menu-icon', x_align: St.Align.END }) | |
506 | shellItem._icon = new St.Icon({ style_class: 'popup-menu-icon', x_align: St.Align.END }); | |
507 | 507 | shellItem.add_child(shellItem._icon); |
508 | 508 | shellItem.label.x_expand = true; |
509 | 509 | } |
510 | 510 | |
511 | 511 | // initialize our state |
512 | MenuItemFactory._updateLabel.call(shellItem) | |
513 | MenuItemFactory._updateOrnament.call(shellItem) | |
514 | MenuItemFactory._updateImage.call(shellItem) | |
515 | MenuItemFactory._updateVisible.call(shellItem) | |
516 | MenuItemFactory._updateSensitive.call(shellItem) | |
512 | MenuItemFactory._updateLabel.call(shellItem); | |
513 | MenuItemFactory._updateOrnament.call(shellItem); | |
514 | MenuItemFactory._updateImage.call(shellItem); | |
515 | MenuItemFactory._updateVisible.call(shellItem); | |
516 | MenuItemFactory._updateSensitive.call(shellItem); | |
517 | 517 | |
518 | 518 | // initially create children |
519 | 519 | if (shellItem instanceof PopupMenu.PopupSubMenuMenuItem) { |
520 | let children = dbusItem.get_children() | |
521 | for (let i = 0; i < children.length; ++i) { | |
522 | shellItem.menu.addMenuItem(MenuItemFactory.createItem(client, children[i])) | |
523 | } | |
520 | let children = dbusItem.get_children(); | |
521 | for (let i = 0; i < children.length; ++i) | |
522 | shellItem.menu.addMenuItem(MenuItemFactory.createItem(client, children[i])); | |
523 | ||
524 | 524 | } |
525 | 525 | |
526 | 526 | // now, connect various events |
527 | Util.connectSmart(dbusItem, 'property-changed', shellItem, MenuItemFactory._onPropertyChanged) | |
528 | Util.connectSmart(dbusItem, 'child-added', shellItem, MenuItemFactory._onChildAdded) | |
529 | Util.connectSmart(dbusItem, 'child-removed', shellItem, MenuItemFactory._onChildRemoved) | |
530 | Util.connectSmart(dbusItem, 'child-moved', shellItem, MenuItemFactory._onChildMoved) | |
531 | Util.connectSmart(shellItem, 'activate', shellItem, MenuItemFactory._onActivate) | |
527 | Util.connectSmart(dbusItem, 'property-changed', shellItem, MenuItemFactory._onPropertyChanged); | |
528 | Util.connectSmart(dbusItem, 'child-added', shellItem, MenuItemFactory._onChildAdded); | |
529 | Util.connectSmart(dbusItem, 'child-removed', shellItem, MenuItemFactory._onChildRemoved); | |
530 | Util.connectSmart(dbusItem, 'child-moved', shellItem, MenuItemFactory._onChildMoved); | |
531 | Util.connectSmart(shellItem, 'activate', shellItem, MenuItemFactory._onActivate); | |
532 | 532 | |
533 | 533 | if (shellItem.menu) |
534 | Util.connectSmart(shellItem.menu, "open-state-changed", shellItem, MenuItemFactory._onOpenStateChanged) | |
535 | ||
536 | return shellItem | |
534 | Util.connectSmart(shellItem.menu, 'open-state-changed', shellItem, MenuItemFactory._onOpenStateChanged); | |
535 | ||
536 | return shellItem; | |
537 | 537 | }, |
538 | 538 | |
539 | 539 | _onOpenStateChanged(menu, open) { |
541 | 541 | if (NEED_NESTED_SUBMENU_FIX) { |
542 | 542 | // close our own submenus |
543 | 543 | if (menu._openedSubMenu) |
544 | menu._openedSubMenu.close(false) | |
544 | menu._openedSubMenu.close(false); | |
545 | 545 | |
546 | 546 | // register ourselves and close sibling submenus |
547 | 547 | if (menu._parent._openedSubMenu && menu._parent._openedSubMenu !== menu) |
548 | menu._parent._openedSubMenu.close(true) | |
549 | ||
550 | menu._parent._openedSubMenu = menu | |
548 | menu._parent._openedSubMenu.close(true); | |
549 | ||
550 | menu._parent._openedSubMenu = menu; | |
551 | 551 | } |
552 | 552 | |
553 | this._dbusItem.handle_event("opened", null, 0) | |
554 | this._dbusItem.send_about_to_show() | |
553 | this._dbusItem.handle_event('opened', null, 0); | |
554 | this._dbusItem.send_about_to_show(); | |
555 | 555 | } else { |
556 | 556 | if (NEED_NESTED_SUBMENU_FIX) { |
557 | 557 | // close our own submenus |
558 | 558 | if (menu._openedSubMenu) |
559 | menu._openedSubMenu.close(false) | |
559 | menu._openedSubMenu.close(false); | |
560 | 560 | } |
561 | 561 | |
562 | this._dbusItem.handle_event("closed", null, 0) | |
562 | this._dbusItem.handle_event('closed', null, 0); | |
563 | 563 | } |
564 | 564 | }, |
565 | 565 | |
566 | 566 | _onActivate() { |
567 | this._dbusItem.handle_event("clicked", GLib.Variant.new("i", 0), 0) | |
567 | this._dbusItem.handle_event('clicked', GLib.Variant.new('i', 0), 0); | |
568 | 568 | }, |
569 | 569 | |
570 | 570 | _onPropertyChanged(dbusItem, prop, value) { |
571 | if (prop == "toggle-type" || prop == "toggle-state") | |
572 | MenuItemFactory._updateOrnament.call(this) | |
573 | else if (prop == "label") | |
574 | MenuItemFactory._updateLabel.call(this) | |
575 | else if (prop == "enabled") | |
576 | MenuItemFactory._updateSensitive.call(this) | |
577 | else if (prop == "visible") | |
578 | MenuItemFactory._updateVisible.call(this) | |
579 | else if (prop == "icon-name" || prop == "icon-data") | |
580 | MenuItemFactory._updateImage.call(this) | |
581 | else if (prop == "type" || prop == "children-display") | |
582 | MenuItemFactory._replaceSelf.call(this) | |
583 | //else | |
571 | if (prop == 'toggle-type' || prop == 'toggle-state') | |
572 | MenuItemFactory._updateOrnament.call(this); | |
573 | else if (prop == 'label') | |
574 | MenuItemFactory._updateLabel.call(this); | |
575 | else if (prop == 'enabled') | |
576 | MenuItemFactory._updateSensitive.call(this); | |
577 | else if (prop == 'visible') | |
578 | MenuItemFactory._updateVisible.call(this); | |
579 | else if (prop == 'icon-name' || prop == 'icon-data') | |
580 | MenuItemFactory._updateImage.call(this); | |
581 | else if (prop == 'type' || prop == 'children-display') | |
582 | MenuItemFactory._replaceSelf.call(this); | |
583 | // else | |
584 | 584 | // Util.Logger.debug("Unhandled property change: "+prop) |
585 | 585 | }, |
586 | 586 | |
587 | 587 | _onChildAdded(dbusItem, child, position) { |
588 | 588 | if (!(this instanceof PopupMenu.PopupSubMenuMenuItem)) { |
589 | Util.Logger.warn("Tried to add a child to non-submenu item. Better recreate it as whole") | |
590 | MenuItemFactory._replaceSelf.call(this) | |
589 | Util.Logger.warn('Tried to add a child to non-submenu item. Better recreate it as whole'); | |
590 | MenuItemFactory._replaceSelf.call(this); | |
591 | 591 | } else { |
592 | this.menu.addMenuItem(MenuItemFactory.createItem(this._dbusClient, child), position) | |
592 | this.menu.addMenuItem(MenuItemFactory.createItem(this._dbusClient, child), position); | |
593 | 593 | } |
594 | 594 | }, |
595 | 595 | |
596 | 596 | _onChildRemoved(dbusItem, child) { |
597 | 597 | if (!(this instanceof PopupMenu.PopupSubMenuMenuItem)) { |
598 | Util.Logger.warn("Tried to remove a child from non-submenu item. Better recreate it as whole") | |
599 | MenuItemFactory._replaceSelf.call(this) | |
598 | Util.Logger.warn('Tried to remove a child from non-submenu item. Better recreate it as whole'); | |
599 | MenuItemFactory._replaceSelf.call(this); | |
600 | 600 | } else { |
601 | 601 | // find it! |
602 | this.menu._getMenuItems().forEach((item) => { | |
602 | this.menu._getMenuItems().forEach(item => { | |
603 | 603 | if (item._dbusItem == child) |
604 | item.destroy() | |
605 | }) | |
604 | item.destroy(); | |
605 | }); | |
606 | 606 | } |
607 | 607 | }, |
608 | 608 | |
609 | 609 | _onChildMoved(dbusItem, child, oldpos, newpos) { |
610 | 610 | if (!(this instanceof PopupMenu.PopupSubMenuMenuItem)) { |
611 | Util.Logger.warn("Tried to move a child in non-submenu item. Better recreate it as whole") | |
612 | MenuItemFactory._replaceSelf.call(this) | |
611 | Util.Logger.warn('Tried to move a child in non-submenu item. Better recreate it as whole'); | |
612 | MenuItemFactory._replaceSelf.call(this); | |
613 | 613 | } else { |
614 | MenuUtils.moveItemInMenu(this.menu, child, newpos) | |
614 | MenuUtils.moveItemInMenu(this.menu, child, newpos); | |
615 | 615 | } |
616 | 616 | }, |
617 | 617 | |
618 | 618 | _updateLabel() { |
619 | let label = this._dbusItem.property_get("label").replace(/_([^_])/, "$1") | |
619 | let label = this._dbusItem.property_get('label').replace(/_([^_])/, '$1'); | |
620 | 620 | |
621 | 621 | if (this.label) // especially on GS3.8, the separator item might not even have a hidden label |
622 | this.label.set_text(label) | |
622 | this.label.set_text(label); | |
623 | 623 | }, |
624 | 624 | |
625 | 625 | _updateOrnament() { |
626 | if (!this.setOrnament) return // separators and alike might not have gotten the polyfill | |
627 | ||
628 | if (this._dbusItem.property_get("toggle-type") == "checkmark" && this._dbusItem.property_get_int("toggle-state")) | |
629 | this.setOrnament(PopupMenu.Ornament.CHECK) | |
630 | else if (this._dbusItem.property_get("toggle-type") == "radio" && this._dbusItem.property_get_int("toggle-state")) | |
631 | this.setOrnament(PopupMenu.Ornament.DOT) | |
626 | if (!this.setOrnament) | |
627 | return; // separators and alike might not have gotten the polyfill | |
628 | ||
629 | if (this._dbusItem.property_get('toggle-type') == 'checkmark' && this._dbusItem.property_get_int('toggle-state')) | |
630 | this.setOrnament(PopupMenu.Ornament.CHECK); | |
631 | else if (this._dbusItem.property_get('toggle-type') == 'radio' && this._dbusItem.property_get_int('toggle-state')) | |
632 | this.setOrnament(PopupMenu.Ornament.DOT); | |
632 | 633 | else |
633 | this.setOrnament(PopupMenu.Ornament.NONE) | |
634 | this.setOrnament(PopupMenu.Ornament.NONE); | |
634 | 635 | }, |
635 | 636 | |
636 | 637 | _updateImage() { |
637 | if (!this._icon) return // might be missing on submenus / separators | |
638 | ||
639 | let iconName = this._dbusItem.property_get("icon-name") | |
640 | let iconData = this._dbusItem.property_get_variant("icon-data") | |
638 | if (!this._icon) | |
639 | return; // might be missing on submenus / separators | |
640 | ||
641 | let iconName = this._dbusItem.property_get('icon-name'); | |
642 | let iconData = this._dbusItem.property_get_variant('icon-data'); | |
641 | 643 | if (iconName) |
642 | this._icon.icon_name = iconName | |
644 | this._icon.icon_name = iconName; | |
643 | 645 | else if (iconData) |
644 | this._icon.gicon = GdkPixbuf.Pixbuf.new_from_stream(Gio.MemoryInputStream.new_from_bytes(iconData.get_data_as_bytes()), null) | |
646 | this._icon.gicon = GdkPixbuf.Pixbuf.new_from_stream(Gio.MemoryInputStream.new_from_bytes(iconData.get_data_as_bytes()), null); | |
645 | 647 | }, |
646 | 648 | |
647 | 649 | _updateVisible() { |
648 | this.visible = this._dbusItem.property_get_bool("visible") | |
650 | this.visible = this._dbusItem.property_get_bool('visible'); | |
649 | 651 | }, |
650 | 652 | |
651 | 653 | _updateSensitive() { |
652 | this.setSensitive(this._dbusItem.property_get_bool("enabled")) | |
654 | this.setSensitive(this._dbusItem.property_get_bool('enabled')); | |
653 | 655 | }, |
654 | 656 | |
655 | 657 | _replaceSelf(newSelf) { |
656 | 658 | // create our new self if needed |
657 | 659 | if (!newSelf) |
658 | newSelf = MenuItemFactory.createItem(this._dbusClient, this._dbusItem) | |
660 | newSelf = MenuItemFactory.createItem(this._dbusClient, this._dbusItem); | |
659 | 661 | |
660 | 662 | // first, we need to find our old position |
661 | let pos = -1 | |
662 | let family = this._parent._getMenuItems() | |
663 | let pos = -1; | |
664 | let family = this._parent._getMenuItems(); | |
663 | 665 | for (let i = 0; i < family.length; ++i) { |
664 | 666 | if (family[i] === this) |
665 | pos = i | |
667 | pos = i; | |
666 | 668 | } |
667 | 669 | |
668 | 670 | if (pos < 0) |
669 | throw new Error("DBusMenu: can't replace non existing menu item") | |
671 | throw new Error("DBusMenu: can't replace non existing menu item"); | |
670 | 672 | |
671 | 673 | |
672 | 674 | // add our new self while we're still alive |
673 | this._parent.addMenuItem(newSelf, pos) | |
675 | this._parent.addMenuItem(newSelf, pos); | |
674 | 676 | |
675 | 677 | // now destroy our old self |
676 | this.destroy() | |
677 | } | |
678 | } | |
678 | this.destroy(); | |
679 | }, | |
680 | }; | |
679 | 681 | |
680 | 682 | /** |
681 | 683 | * Utility functions not necessarily belonging into the item factory |
682 | 684 | */ |
683 | 685 | const MenuUtils = { |
684 | 686 | moveItemInMenu(menu, dbusItem, newpos) { |
685 | //HACK: we're really getting into the internals of the PopupMenu implementation | |
687 | // HACK: we're really getting into the internals of the PopupMenu implementation | |
686 | 688 | |
687 | 689 | // First, find our wrapper. Children tend to lie. We do not trust the old positioning. |
688 | let family = menu._getMenuItems() | |
690 | let family = menu._getMenuItems(); | |
689 | 691 | for (let i = 0; i < family.length; ++i) { |
690 | 692 | if (family[i]._dbusItem == dbusItem) { |
691 | 693 | // now, remove it |
692 | menu.box.remove_child(family[i]) | |
694 | menu.box.remove_child(family[i]); | |
693 | 695 | |
694 | 696 | // and add it again somewhere else |
695 | 697 | if (newpos < family.length && family[newpos] != family[i]) |
696 | menu.box.insert_child_below(family[i], family[newpos]) | |
698 | menu.box.insert_child_below(family[i], family[newpos]); | |
697 | 699 | else |
698 | menu.box.add(family[i]) | |
700 | menu.box.add(family[i]); | |
699 | 701 | |
700 | 702 | // skip the rest |
701 | return | |
703 | return; | |
702 | 704 | } |
703 | 705 | } |
704 | } | |
705 | } | |
706 | }, | |
707 | }; | |
706 | 708 | |
707 | 709 | |
708 | 710 | /** |
713 | 715 | var Client = class AppIndicators_Client { |
714 | 716 | |
715 | 717 | constructor(busName, path) { |
716 | this._busName = busName | |
717 | this._busPath = path | |
718 | this._client = new DBusClient(busName, path) | |
719 | this._rootMenu = null // the shell menu | |
720 | this._rootItem = null // the DbusMenuItem for the root | |
718 | this._busName = busName; | |
719 | this._busPath = path; | |
720 | this._client = new DBusClient(busName, path); | |
721 | this._rootMenu = null; // the shell menu | |
722 | this._rootItem = null; // the DbusMenuItem for the root | |
721 | 723 | } |
722 | 724 | |
723 | 725 | get isReady() { |
727 | 729 | // this will attach the client to an already existing menu that will be used as the root menu. |
728 | 730 | // it will also connect the client to be automatically destroyed when the menu dies. |
729 | 731 | attachToMenu(menu) { |
730 | this._rootMenu = menu | |
731 | this._rootItem = this._client.get_root() | |
732 | this._rootMenu = menu; | |
733 | this._rootItem = this._client.get_root(); | |
732 | 734 | |
733 | 735 | // cleanup: remove existing children (just in case) |
734 | this._rootMenu.removeAll() | |
736 | this._rootMenu.removeAll(); | |
735 | 737 | |
736 | 738 | if (NEED_NESTED_SUBMENU_FIX) |
737 | menu._setOpenedSubMenu = this._setOpenedSubmenu.bind(this) | |
739 | menu._setOpenedSubMenu = this._setOpenedSubmenu.bind(this); | |
738 | 740 | |
739 | 741 | // connect handlers |
740 | Util.connectSmart(menu, 'open-state-changed', this, '_onMenuOpened') | |
741 | Util.connectSmart(menu, 'destroy', this, 'destroy') | |
742 | ||
743 | Util.connectSmart(this._rootItem, 'child-added', this, '_onRootChildAdded') | |
744 | Util.connectSmart(this._rootItem, 'child-removed', this, '_onRootChildRemoved') | |
745 | Util.connectSmart(this._rootItem, 'child-moved', this, '_onRootChildMoved') | |
742 | Util.connectSmart(menu, 'open-state-changed', this, '_onMenuOpened'); | |
743 | Util.connectSmart(menu, 'destroy', this, 'destroy'); | |
744 | ||
745 | Util.connectSmart(this._rootItem, 'child-added', this, '_onRootChildAdded'); | |
746 | Util.connectSmart(this._rootItem, 'child-removed', this, '_onRootChildRemoved'); | |
747 | Util.connectSmart(this._rootItem, 'child-moved', this, '_onRootChildMoved'); | |
746 | 748 | |
747 | 749 | // Dropbox requires us to call AboutToShow(0) first |
748 | this._rootItem.send_about_to_show() | |
750 | this._rootItem.send_about_to_show(); | |
749 | 751 | |
750 | 752 | // fill the menu for the first time |
751 | 753 | this._rootItem.get_children().forEach(child => |
752 | this._rootMenu.addMenuItem(MenuItemFactory.createItem(this, child)) | |
754 | this._rootMenu.addMenuItem(MenuItemFactory.createItem(this, child)), | |
753 | 755 | ); |
754 | 756 | } |
755 | 757 | |
756 | 758 | _setOpenedSubmenu(submenu) { |
757 | 759 | if (!submenu) |
758 | return | |
760 | return; | |
759 | 761 | |
760 | 762 | if (submenu._parent != this._rootMenu) |
761 | return | |
763 | return; | |
762 | 764 | |
763 | 765 | if (submenu === this._openedSubMenu) |
764 | return | |
766 | return; | |
765 | 767 | |
766 | 768 | if (this._openedSubMenu && this._openedSubMenu.isOpen) |
767 | this._openedSubMenu.close(true) | |
768 | ||
769 | this._openedSubMenu = submenu | |
769 | this._openedSubMenu.close(true); | |
770 | ||
771 | this._openedSubMenu = submenu; | |
770 | 772 | } |
771 | 773 | |
772 | 774 | _onRootChildAdded(dbusItem, child, position) { |
773 | this._rootMenu.addMenuItem(MenuItemFactory.createItem(this, child), position) | |
775 | this._rootMenu.addMenuItem(MenuItemFactory.createItem(this, child), position); | |
774 | 776 | } |
775 | 777 | |
776 | 778 | _onRootChildRemoved(dbusItem, child) { |
777 | 779 | // children like to play hide and seek |
778 | 780 | // but we know how to find it for sure! |
779 | this._rootMenu._getMenuItems().forEach((item) => { | |
781 | this._rootMenu._getMenuItems().forEach(item => { | |
780 | 782 | if (item._dbusItem == child) |
781 | item.destroy() | |
782 | }) | |
783 | item.destroy(); | |
784 | }); | |
783 | 785 | } |
784 | 786 | |
785 | 787 | _onRootChildMoved(dbusItem, child, oldpos, newpos) { |
786 | MenuUtils.moveItemInMenu(this._rootMenu, dbusItem, newpos) | |
788 | MenuUtils.moveItemInMenu(this._rootMenu, dbusItem, newpos); | |
787 | 789 | } |
788 | 790 | |
789 | 791 | _onMenuOpened(menu, state) { |
790 | if (!this._rootItem) return | |
792 | if (!this._rootItem) | |
793 | return; | |
791 | 794 | |
792 | 795 | if (state) { |
793 | 796 | if (this._openedSubMenu && this._openedSubMenu.isOpen) |
794 | this._openedSubMenu.close() | |
795 | ||
796 | this._rootItem.handle_event("opened", null, 0) | |
797 | this._rootItem.send_about_to_show() | |
797 | this._openedSubMenu.close(); | |
798 | ||
799 | this._rootItem.handle_event('opened', null, 0); | |
800 | this._rootItem.send_about_to_show(); | |
798 | 801 | } else { |
799 | this._rootItem.handle_event("closed", null, 0) | |
802 | this._rootItem.handle_event('closed', null, 0); | |
800 | 803 | } |
801 | 804 | } |
802 | 805 | |
803 | 806 | destroy() { |
804 | this.emit('destroy') | |
807 | this.emit('destroy'); | |
805 | 808 | |
806 | 809 | if (this._client) |
807 | this._client.destroy() | |
808 | ||
809 | this._client = null | |
810 | this._rootItem = null | |
811 | this._rootMenu = null | |
812 | } | |
813 | } | |
814 | Signals.addSignalMethods(Client.prototype) | |
810 | this._client.destroy(); | |
811 | ||
812 | this._client = null; | |
813 | this._rootItem = null; | |
814 | this._rootMenu = null; | |
815 | } | |
816 | }; | |
817 | Signals.addSignalMethods(Client.prototype); |
13 | 13 | // along with this program; if not, write to the Free Software |
14 | 14 | // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
15 | 15 | const Gio = imports.gi.Gio; |
16 | const Extension = imports.misc.extensionUtils.getCurrentExtension() | |
16 | const Extension = imports.misc.extensionUtils.getCurrentExtension(); | |
17 | 17 | |
18 | const StatusNotifierWatcher = Extension.imports.statusNotifierWatcher | |
19 | const Util = Extension.imports.util | |
18 | const StatusNotifierWatcher = Extension.imports.statusNotifierWatcher; | |
19 | const Util = Extension.imports.util; | |
20 | 20 | |
21 | 21 | let statusNotifierWatcher = null; |
22 | 22 | let isEnabled = false; |
26 | 26 | watchDog = new Util.NameWatcher(StatusNotifierWatcher.WATCHER_BUS_NAME); |
27 | 27 | watchDog.connect('vanished', () => maybe_enable_after_name_available()); |
28 | 28 | |
29 | //HACK: we want to leave the watchdog alive when disabling the extension, | |
29 | // HACK: we want to leave the watchdog alive when disabling the extension, | |
30 | 30 | // but if we are being reloaded, we destroy it since it could be considered |
31 | 31 | // a leak and spams our log, too. |
32 | if (typeof global['--appindicator-extension-on-reload'] == 'function') | |
33 | global['--appindicator-extension-on-reload']() | |
32 | if (typeof global['--appindicator-extension-on-reload'] === 'function') | |
33 | global['--appindicator-extension-on-reload'](); | |
34 | 34 | |
35 | 35 | global['--appindicator-extension-on-reload'] = () => { |
36 | Util.Logger.debug("Reload detected, destroying old watchdog") | |
36 | Util.Logger.debug('Reload detected, destroying old watchdog'); | |
37 | 37 | watchDog.destroy(); |
38 | } | |
38 | }; | |
39 | 39 | } |
40 | 40 | |
41 | //FIXME: when entering/leaving the lock screen, the extension might be enabled/disabled rapidly. | |
41 | // FIXME: when entering/leaving the lock screen, the extension might be enabled/disabled rapidly. | |
42 | 42 | // This will create very bad side effects in case we were not done unowning the name while trying |
43 | 43 | // to own it again. Since g_bus_unown_name doesn't fire any callback when it's done, we need to |
44 | 44 | // monitor the bus manually to find out when the name vanished so we can reclaim it again. |
13 | 13 | // along with this program; if not, write to the Free Software |
14 | 14 | // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
15 | 15 | |
16 | const GLib = imports.gi.GLib | |
17 | const Gio = imports.gi.Gio | |
16 | const GLib = imports.gi.GLib; | |
17 | const Gio = imports.gi.Gio; | |
18 | 18 | |
19 | 19 | const Extension = imports.misc.extensionUtils.getCurrentExtension(); |
20 | 20 | const PromiseUtils = Extension.imports.promiseUtils; |
33 | 33 | var IconCache = class AppIndicators_IconCache { |
34 | 34 | constructor() { |
35 | 35 | this._cache = new Map(); |
36 | this._lifetime = new Map(); //we don't want to attach lifetime to the object | |
36 | this._lifetime = new Map(); // we don't want to attach lifetime to the object | |
37 | 37 | } |
38 | 38 | |
39 | 39 | add(id, icon) { |
82 | 82 | |
83 | 83 | // marks all the icons as removable, if something doesn't claim them before |
84 | 84 | weakClear() { |
85 | this._cache.forEach((icon) => icon.inUse = false); | |
85 | this._cache.forEach(icon => icon.inUse = false); | |
86 | 86 | this._checkGC(); |
87 | 87 | } |
88 | 88 | |
108 | 108 | let cacheIsEmpty = this._cache.size == 0; |
109 | 109 | |
110 | 110 | if (!cacheIsEmpty && !this._gcTimeout) { |
111 | Util.Logger.debug("IconCache: garbage collector started"); | |
111 | Util.Logger.debug('IconCache: garbage collector started'); | |
112 | 112 | this._gcTimeout = new PromiseUtils.TimeoutSecondsPromise(GC_INTERVAL, |
113 | 113 | GLib.PRIORITY_LOW); |
114 | 114 | await this._gcTimeout; |
115 | 115 | } else if (cacheIsEmpty && this._gcTimeout) { |
116 | Util.Logger.debug("IconCache: garbage collector stopped"); | |
116 | Util.Logger.debug('IconCache: garbage collector stopped'); | |
117 | 117 | this._gcTimeout.cancel(); |
118 | 118 | delete this._gcTimeout; |
119 | 119 | } |
122 | 122 | _gc() { |
123 | 123 | let time = new Date().getTime(); |
124 | 124 | this._cache.forEach((icon, id) => { |
125 | if (icon.inUse) { | |
125 | if (icon.inUse) | |
126 | 126 | Util.Logger.debug(`IconCache: ${id} is in use.`); |
127 | } else if (this._lifetime.get(id) < time) { | |
127 | else if (this._lifetime.get(id) < time) | |
128 | 128 | this._remove(id); |
129 | } else { | |
129 | else | |
130 | 130 | Util.Logger.debug(`IconCache: ${id} survived this round.`); |
131 | } | |
131 | ||
132 | 132 | }); |
133 | 133 | |
134 | 134 | return true; |
33 | 33 | |
34 | 34 | (() => { |
35 | 35 | |
36 | var app = new Gtk.Application({ | |
37 | application_id: null | |
38 | }); | |
39 | ||
40 | var window = null; | |
41 | ||
42 | app.connect("activate", () => { | |
43 | window.present(); | |
44 | }); | |
45 | ||
46 | app.connect("startup", () => { | |
47 | window = new Gtk.ApplicationWindow({ | |
48 | title: "test", | |
49 | application: app | |
36 | var app = new Gtk.Application({ | |
37 | application_id: null, | |
50 | 38 | }); |
51 | 39 | |
52 | let getRandomIcon = () => | |
53 | iconsPool[Math.floor(Math.random() * (iconsPool.length - 1))]; | |
54 | ||
55 | let setRandomIconPath = () => { | |
56 | let iconName = getRandomIcon(); | |
57 | let iconInfo = Gtk.IconTheme.get_default().lookup_icon(iconName, | |
58 | 16, Gtk.IconLookupFlags.GENERIC_FALLBACK); | |
59 | let iconFile = Gio.File.new_for_path(iconInfo.get_filename()); | |
60 | let [, extension] = iconFile.get_basename().split('.'); | |
61 | let newName = `${iconName}-${Math.floor(Math.random() * 100)}.${extension}`; | |
62 | let newFile = Gio.File.new_for_path( | |
63 | `${GLib.dir_make_tmp('indicator-test-XXXXXX')}/${newName}`); | |
64 | iconFile.copy(newFile, Gio.FileCopyFlagsOVERWRITE, null, null); | |
65 | ||
66 | indicator.set_icon_theme_path(newFile.get_parent().get_path()); | |
67 | indicator.set_icon(newFile.get_basename()); | |
68 | } | |
69 | ||
70 | var menu = new Gtk.Menu(); | |
71 | ||
72 | var item = Gtk.MenuItem.new_with_label("A standard item"); | |
73 | menu.append(item); | |
74 | ||
75 | item = Gtk.MenuItem.new_with_label("Foo"); | |
76 | menu.append(item); | |
77 | ||
78 | item = Gtk.ImageMenuItem.new_with_label("Calculator"); | |
79 | item.image = Gtk.Image.new_from_icon_name("gnome-calculator", Gtk.IconSize.MENU); | |
80 | menu.append(item); | |
81 | ||
82 | item = Gtk.CheckMenuItem.new_with_label("Check me!"); | |
83 | menu.append(item); | |
84 | ||
85 | item = Gtk.MenuItem.new_with_label("Blub"); | |
86 | let sub = new Gtk.Menu(); | |
87 | item.set_submenu(sub); | |
88 | menu.append(item); | |
89 | ||
90 | item = Gtk.MenuItem.new_with_label("Blubdablub"); | |
91 | sub.append(item); | |
92 | ||
93 | item = new Gtk.SeparatorMenuItem(); | |
94 | menu.append(item); | |
95 | ||
96 | item = Gtk.MenuItem.new_with_label("Foo"); | |
97 | menu.append(item); | |
98 | ||
99 | let submenu = new Gtk.Menu(); | |
100 | item.set_submenu(submenu); | |
101 | ||
102 | item = Gtk.MenuItem.new_with_label("Hello"); | |
103 | submenu.append(item); | |
104 | ||
105 | item = Gtk.MenuItem.new_with_label("Nested"); | |
106 | submenu.append(item); | |
107 | ||
108 | let submenu1 = new Gtk.Menu(); | |
109 | item.set_submenu(submenu1); | |
110 | ||
111 | item = Gtk.MenuItem.new_with_label("Another nested"); | |
112 | submenu.append(item); | |
113 | ||
114 | let submenu2 = new Gtk.Menu(); | |
115 | item.set_submenu(submenu2); | |
116 | ||
117 | item = Gtk.MenuItem.new_with_label("Some other item"); | |
118 | submenu1.append(item); | |
119 | ||
120 | item = Gtk.MenuItem.new_with_label("abcdefg"); | |
121 | submenu2.append(item); | |
122 | ||
123 | item = new Gtk.SeparatorMenuItem(); | |
124 | menu.append(item); | |
125 | ||
126 | var group = []; | |
127 | ||
128 | for (let i = 0; i < 5; ++i) { | |
129 | item = Gtk.RadioMenuItem.new_with_label(group, "Example Radio "+i); | |
130 | group = Gtk.RadioMenuItem.prototype.get_group.apply(item)//.get_group(); | |
131 | if (i == 1) | |
132 | item.set_active(true); | |
133 | menu.append(item); | |
134 | } | |
135 | ||
136 | item = new Gtk.SeparatorMenuItem(); | |
137 | menu.append(item); | |
138 | ||
139 | item = Gtk.MenuItem.new_with_label("Set Label"); | |
140 | item.connect('activate', () => { | |
141 | indicator.set_label(''+new Date().getTime(), 'Blub'); | |
40 | var window = null; | |
41 | ||
42 | app.connect('activate', () => { | |
43 | window.present(); | |
142 | 44 | }); |
143 | menu.append(item); | |
144 | ||
145 | item = Gtk.MenuItem.new_with_label("Unset Label"); | |
146 | item.connect('activate', () => { | |
147 | indicator.set_label('', ''); | |
148 | }) | |
149 | menu.append(item); | |
150 | ||
151 | item = Gtk.MenuItem.new_with_label("Autodestroy Label"); | |
152 | item.connect('activate', () => { | |
153 | let i = 30; | |
154 | GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 1, () => { | |
155 | indicator.set_label(i > 0 ? `Label timeout ${i--}` : '', ''); | |
156 | return (i >= 0); | |
157 | }); | |
158 | }) | |
159 | menu.append(item); | |
160 | ||
161 | item = Gtk.MenuItem.new_with_label('Set Random icon'); | |
162 | item.connect('activate', () => indicator.set_icon(getRandomIcon())); | |
163 | menu.append(item); | |
164 | ||
165 | item = Gtk.MenuItem.new_with_label('Set Random custom theme icon'); | |
166 | item.connect('activate', setRandomIconPath); | |
167 | menu.append(item); | |
168 | ||
169 | item = Gtk.CheckMenuItem.new_with_label('Toggle Label and Icon'); | |
170 | item.connect('activate', (item) => { | |
171 | if (item.get_active()) { | |
45 | ||
46 | app.connect('startup', () => { | |
47 | window = new Gtk.ApplicationWindow({ | |
48 | title: 'test', | |
49 | application: app, | |
50 | }); | |
51 | ||
52 | let getRandomIcon = () => | |
53 | iconsPool[Math.floor(Math.random() * (iconsPool.length - 1))]; | |
54 | ||
55 | let setRandomIconPath = () => { | |
56 | let iconName = getRandomIcon(); | |
57 | let iconInfo = Gtk.IconTheme.get_default().lookup_icon(iconName, | |
58 | 16, Gtk.IconLookupFlags.GENERIC_FALLBACK); | |
59 | let iconFile = Gio.File.new_for_path(iconInfo.get_filename()); | |
60 | let [, extension] = iconFile.get_basename().split('.'); | |
61 | let newName = `${iconName}-${Math.floor(Math.random() * 100)}.${extension}`; | |
62 | let newFile = Gio.File.new_for_path( | |
63 | `${GLib.dir_make_tmp('indicator-test-XXXXXX')}/${newName}`); | |
64 | iconFile.copy(newFile, Gio.FileCopyFlagsOVERWRITE, null, null); | |
65 | ||
66 | indicator.set_icon_theme_path(newFile.get_parent().get_path()); | |
67 | indicator.set_icon(newFile.get_basename()); | |
68 | }; | |
69 | ||
70 | var menu = new Gtk.Menu(); | |
71 | ||
72 | var item = Gtk.MenuItem.new_with_label('A standard item'); | |
73 | menu.append(item); | |
74 | ||
75 | item = Gtk.MenuItem.new_with_label('Foo'); | |
76 | menu.append(item); | |
77 | ||
78 | item = Gtk.ImageMenuItem.new_with_label('Calculator'); | |
79 | item.image = Gtk.Image.new_from_icon_name('gnome-calculator', Gtk.IconSize.MENU); | |
80 | menu.append(item); | |
81 | ||
82 | item = Gtk.CheckMenuItem.new_with_label('Check me!'); | |
83 | menu.append(item); | |
84 | ||
85 | item = Gtk.MenuItem.new_with_label('Blub'); | |
86 | let sub = new Gtk.Menu(); | |
87 | item.set_submenu(sub); | |
88 | menu.append(item); | |
89 | ||
90 | item = Gtk.MenuItem.new_with_label('Blubdablub'); | |
91 | sub.append(item); | |
92 | ||
93 | item = new Gtk.SeparatorMenuItem(); | |
94 | menu.append(item); | |
95 | ||
96 | item = Gtk.MenuItem.new_with_label('Foo'); | |
97 | menu.append(item); | |
98 | ||
99 | let submenu = new Gtk.Menu(); | |
100 | item.set_submenu(submenu); | |
101 | ||
102 | item = Gtk.MenuItem.new_with_label('Hello'); | |
103 | submenu.append(item); | |
104 | ||
105 | item = Gtk.MenuItem.new_with_label('Nested'); | |
106 | submenu.append(item); | |
107 | ||
108 | let submenu1 = new Gtk.Menu(); | |
109 | item.set_submenu(submenu1); | |
110 | ||
111 | item = Gtk.MenuItem.new_with_label('Another nested'); | |
112 | submenu.append(item); | |
113 | ||
114 | let submenu2 = new Gtk.Menu(); | |
115 | item.set_submenu(submenu2); | |
116 | ||
117 | item = Gtk.MenuItem.new_with_label('Some other item'); | |
118 | submenu1.append(item); | |
119 | ||
120 | item = Gtk.MenuItem.new_with_label('abcdefg'); | |
121 | submenu2.append(item); | |
122 | ||
123 | item = new Gtk.SeparatorMenuItem(); | |
124 | menu.append(item); | |
125 | ||
126 | var group = []; | |
127 | ||
128 | for (let i = 0; i < 5; ++i) { | |
129 | item = Gtk.RadioMenuItem.new_with_label(group, `Example Radio ${i}`); | |
130 | group = Gtk.RadioMenuItem.prototype.get_group.apply(item);// .get_group(); | |
131 | if (i === 1) | |
132 | item.set_active(true); | |
133 | menu.append(item); | |
134 | } | |
135 | ||
136 | item = new Gtk.SeparatorMenuItem(); | |
137 | menu.append(item); | |
138 | ||
139 | item = Gtk.MenuItem.new_with_label('Set Label'); | |
140 | item.connect('activate', () => { | |
172 | 141 | indicator.set_label(`${new Date().getTime()}`, 'Blub'); |
173 | item.connect('activate', () => indicator.set_icon(getRandomIcon())); | |
174 | } else { | |
142 | }); | |
143 | menu.append(item); | |
144 | ||
145 | item = Gtk.MenuItem.new_with_label('Unset Label'); | |
146 | item.connect('activate', () => { | |
175 | 147 | indicator.set_label('', ''); |
176 | indicator.set_icon(DEFAULT_ICON); | |
177 | } | |
178 | }) | |
179 | menu.append(item); | |
180 | let toggleBrandingItem = item; | |
181 | ||
182 | item = Gtk.CheckMenuItem.new_with_label('Toggle Attention'); | |
183 | let toggleAttentionId = item.connect('activate', () => { | |
184 | indicator.set_status(indicator.get_status() != AppIndicator.IndicatorStatus.ATTENTION ? | |
185 | AppIndicator.IndicatorStatus.ATTENTION : | |
186 | AppIndicator.IndicatorStatus.ACTIVE); | |
148 | }); | |
149 | menu.append(item); | |
150 | ||
151 | item = Gtk.MenuItem.new_with_label('Autodestroy Label'); | |
152 | item.connect('activate', () => { | |
153 | let i = 30; | |
154 | GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 1, () => { | |
155 | indicator.set_label(i > 0 ? `Label timeout ${i--}` : '', ''); | |
156 | return i >= 0; | |
157 | }); | |
158 | }); | |
159 | menu.append(item); | |
160 | ||
161 | item = Gtk.MenuItem.new_with_label('Set Random icon'); | |
162 | item.connect('activate', () => indicator.set_icon(getRandomIcon())); | |
163 | menu.append(item); | |
164 | ||
165 | item = Gtk.MenuItem.new_with_label('Set Random custom theme icon'); | |
166 | item.connect('activate', setRandomIconPath); | |
167 | menu.append(item); | |
168 | ||
169 | item = Gtk.CheckMenuItem.new_with_label('Toggle Label and Icon'); | |
170 | item.connect('activate', it => { | |
171 | if (it.get_active()) { | |
172 | indicator.set_label(`${new Date().getTime()}`, 'Blub'); | |
173 | it.connect('activate', () => indicator.set_icon(getRandomIcon())); | |
174 | } else { | |
175 | indicator.set_label('', ''); | |
176 | indicator.set_icon(DEFAULT_ICON); | |
177 | } | |
178 | }); | |
179 | menu.append(item); | |
180 | let toggleBrandingItem = item; | |
181 | ||
182 | item = Gtk.CheckMenuItem.new_with_label('Toggle Attention'); | |
183 | let toggleAttentionId = item.connect('activate', () => { | |
184 | indicator.set_status(indicator.get_status() !== AppIndicator.IndicatorStatus.ATTENTION | |
185 | ? AppIndicator.IndicatorStatus.ATTENTION | |
186 | : AppIndicator.IndicatorStatus.ACTIVE); | |
187 | }); | |
188 | menu.append(item); | |
189 | let toggleAttentionItem = item; | |
190 | ||
191 | item = new Gtk.SeparatorMenuItem(); | |
192 | menu.append(item); | |
193 | ||
194 | /* Double separaptors test */ | |
195 | ||
196 | item = new Gtk.SeparatorMenuItem(); | |
197 | menu.append(item); | |
198 | ||
199 | /* Simulate similar behavior of #226 and #236 */ | |
200 | item = Gtk.CheckMenuItem.new_with_label('Crazy icons updates'); | |
201 | item.connect('activate', it => { | |
202 | if (it.get_active()) { | |
203 | it._timeoutID = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 16, () => { | |
204 | setRandomIconPath(); | |
205 | indicator.set_label(`${new Date().getSeconds()}`, ''); | |
206 | return GLib.SOURCE_CONTINUE; | |
207 | }); | |
208 | } else { | |
209 | GLib.source_remove(item._timeoutID); | |
210 | delete item._timeoutID; | |
211 | } | |
212 | }); | |
213 | menu.append(item); | |
214 | ||
215 | item = Gtk.MenuItem.new_with_label('Hide for some time'); | |
216 | item.connect('activate', () => { | |
217 | indicator.set_status(AppIndicator.IndicatorStatus.PASSIVE); | |
218 | GLib.timeout_add(0, 5000, () => { | |
219 | indicator.set_status(AppIndicator.IndicatorStatus.ACTIVE); | |
220 | return false; | |
221 | }); | |
222 | }); | |
223 | menu.append(item); | |
224 | ||
225 | item = Gtk.MenuItem.new_with_label('Close in 5 seconds'); | |
226 | item.connect('activate', () => { | |
227 | GLib.timeout_add(0, 5000, () => { | |
228 | app.quit(); | |
229 | return false; | |
230 | }); | |
231 | }); | |
232 | menu.append(item); | |
233 | ||
234 | menu.show_all(); | |
235 | ||
236 | var indicator = AppIndicator.Indicator.new('Hello', 'indicator-test', AppIndicator.IndicatorCategory.APPLICATION_STATUS); | |
237 | ||
238 | indicator.set_status(AppIndicator.IndicatorStatus.ACTIVE); | |
239 | indicator.set_icon(DEFAULT_ICON); | |
240 | indicator.set_attention_icon(ATTENTION_ICON); | |
241 | indicator.set_menu(menu); | |
242 | indicator.set_secondary_activate_target(toggleBrandingItem); | |
243 | ||
244 | indicator.connect('connection-changed', (_indicator, connected) => { | |
245 | print(`Signal "connection-changed" emitted. Connected: ${connected}`); | |
246 | }); | |
247 | indicator.connect('new-attention-icon', () => { | |
248 | print('Signal "new-attention-icon" emitted.'); | |
249 | }); | |
250 | indicator.connect('new-icon', () => { | |
251 | let icon = '<none>'; | |
252 | if (indicator.get_status() === AppIndicator.IndicatorStatus.ATTENTION) | |
253 | icon = indicator.get_attention_icon(); | |
254 | else if (indicator.get_status() === AppIndicator.IndicatorStatus.ACTIVE) | |
255 | icon = indicator.get_icon(); | |
256 | ||
257 | print(`Signal "new-icon" emitted. Icon: ${icon}`); | |
258 | }); | |
259 | indicator.connect('new-icon-theme-path', (_indicator, path) => { | |
260 | print(`Signal "new-icon-theme-path" emitted. Path: ${path}`); | |
261 | }); | |
262 | indicator.connect('new-label', (_indicator, label, guide) => { | |
263 | print(`Signal "new-label" emitted. Label: ${label}, Guide: ${guide}`); | |
264 | }); | |
265 | indicator.connect('new-status', (_indicator, status) => { | |
266 | print(`Signal "new-status" emitted. Status: ${status}`); | |
267 | ||
268 | toggleAttentionItem.block_signal_handler(toggleAttentionId); | |
269 | toggleAttentionItem.set_active(status === 'NeedsAttention'); | |
270 | toggleAttentionItem.unblock_signal_handler(toggleAttentionId); | |
271 | }); | |
272 | indicator.connect('scroll-event', (_indicator, steps, direction) => { | |
273 | print(`Signal "scroll-event" emitted. Steps: ${steps}, Direction: ${direction}`); | |
274 | let currentIndex = iconsPool.indexOf(indicator.get_icon()); | |
275 | let iconIndex; | |
276 | ||
277 | if (direction === ScrollType.UP) | |
278 | iconIndex = (currentIndex + 1) % iconsPool.length; | |
279 | else | |
280 | iconIndex = (currentIndex <= 0 ? iconsPool.length : currentIndex) - 1; | |
281 | ||
282 | ||
283 | indicator.set_icon(iconsPool[iconIndex]); | |
284 | }); | |
187 | 285 | }); |
188 | menu.append(item); | |
189 | let toggleAttentionItem = item; | |
190 | ||
191 | item = new Gtk.SeparatorMenuItem(); | |
192 | menu.append(item); | |
193 | ||
194 | /* Double separaptors test */ | |
195 | ||
196 | item = new Gtk.SeparatorMenuItem(); | |
197 | menu.append(item); | |
198 | ||
199 | /* Simulate similar behavior of #226 and #236 */ | |
200 | item = Gtk.CheckMenuItem.new_with_label('Crazy icons updates'); | |
201 | item.connect('activate', (item) => { | |
202 | if (item.get_active()) { | |
203 | item._timeoutID = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 16, () => { | |
204 | setRandomIconPath(); | |
205 | indicator.set_label(`${new Date().getSeconds()}`, ''); | |
206 | return GLib.SOURCE_CONTINUE; | |
207 | }); | |
208 | } else { | |
209 | GLib.source_remove(item._timeoutID); | |
210 | delete item._timeoutID; | |
211 | } | |
212 | }); | |
213 | menu.append(item); | |
214 | ||
215 | item = Gtk.MenuItem.new_with_label("Hide for some time"); | |
216 | item.connect('activate', () => { | |
217 | indicator.set_status(AppIndicator.IndicatorStatus.PASSIVE); | |
218 | GLib.timeout_add(0, 5000, () => { | |
219 | indicator.set_status(AppIndicator.IndicatorStatus.ACTIVE); | |
220 | return false; | |
221 | }); | |
222 | }); | |
223 | menu.append(item); | |
224 | ||
225 | item = Gtk.MenuItem.new_with_label("Close in 5 seconds"); | |
226 | item.connect('activate', () => { | |
227 | GLib.timeout_add(0, 5000, () => { | |
228 | app.quit(); | |
229 | return false; | |
230 | }); | |
231 | }); | |
232 | menu.append(item); | |
233 | ||
234 | menu.show_all(); | |
235 | ||
236 | var indicator = AppIndicator.Indicator.new("Hello", "indicator-test", AppIndicator.IndicatorCategory.APPLICATION_STATUS); | |
237 | ||
238 | indicator.set_status(AppIndicator.IndicatorStatus.ACTIVE); | |
239 | indicator.set_icon(DEFAULT_ICON); | |
240 | indicator.set_attention_icon(ATTENTION_ICON); | |
241 | indicator.set_menu(menu); | |
242 | indicator.set_secondary_activate_target(toggleBrandingItem); | |
243 | ||
244 | indicator.connect("connection-changed", (indicator, connected) => { | |
245 | print(`Signal "connection-changed" emitted. Connected: ${connected}`); | |
246 | }); | |
247 | indicator.connect("new-attention-icon", (indicator) => { | |
248 | print(`Signal "new-attention-icon" emitted.`); | |
249 | }); | |
250 | indicator.connect("new-icon", (indicator) => { | |
251 | let icon = "<none>"; | |
252 | if (indicator.get_status() == AppIndicator.IndicatorStatus.ATTENTION) | |
253 | icon = indicator.get_attention_icon(); | |
254 | else if (indicator.get_status() == AppIndicator.IndicatorStatus.ACTIVE) | |
255 | icon = indicator.get_icon(); | |
256 | ||
257 | print(`Signal "new-icon" emitted. Icon: ${icon}`); | |
258 | }); | |
259 | indicator.connect("new-icon-theme-path", (indicator, path) => { | |
260 | print(`Signal "new-icon-theme-path" emitted. Path: ${path}`); | |
261 | }); | |
262 | indicator.connect("new-label", (indicator, label, guide) => { | |
263 | print(`Signal "new-label" emitted. Label: ${label}, Guide: ${guide}`); | |
264 | }); | |
265 | indicator.connect("new-status", (indicator, status) => { | |
266 | print(`Signal "new-status" emitted. Status: ${status}`); | |
267 | ||
268 | toggleAttentionItem.block_signal_handler(toggleAttentionId); | |
269 | toggleAttentionItem.set_active(status == 'NeedsAttention'); | |
270 | toggleAttentionItem.unblock_signal_handler(toggleAttentionId); | |
271 | }); | |
272 | indicator.connect("scroll-event", (indicator, steps, direction) => { | |
273 | print(`Signal "scroll-event" emitted. Steps: ${steps}, Direction: ${direction}`); | |
274 | let currentIndex = iconsPool.indexOf(indicator.get_icon()); | |
275 | let iconIndex; | |
276 | ||
277 | if (direction == ScrollType.UP) { | |
278 | iconIndex = (currentIndex + 1) % iconsPool.length; | |
279 | } else { | |
280 | iconIndex = (currentIndex <= 0 ? iconsPool.length : currentIndex) - 1; | |
281 | } | |
282 | ||
283 | indicator.set_icon(iconsPool[iconIndex]); | |
284 | }); | |
285 | }); | |
286 | app.run(ARGV); | |
286 | app.run(ARGV); | |
287 | 287 | |
288 | 288 | })(); |
12 | 12 | // You should have received a copy of the GNU General Public License |
13 | 13 | // along with this program; if not, write to the Free Software |
14 | 14 | // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
15 | ||
16 | /* exported IndicatorStatusIcon */ | |
17 | ||
15 | 18 | const Clutter = imports.gi.Clutter; |
16 | 19 | const GObject = imports.gi.GObject; |
17 | 20 | const St = imports.gi.St; |
19 | 22 | const Main = imports.ui.main; |
20 | 23 | const Panel = imports.ui.panel; |
21 | 24 | const PanelMenu = imports.ui.panelMenu; |
22 | const PopupMenu = imports.ui.popupMenu; | |
23 | 25 | |
24 | 26 | const Config = imports.misc.config; |
25 | 27 | const ExtensionUtils = imports.misc.extensionUtils; |
26 | 28 | const Extension = ExtensionUtils.getCurrentExtension(); |
27 | 29 | |
28 | const AppIndicator = Extension.imports.appIndicator | |
30 | const AppIndicator = Extension.imports.appIndicator; | |
29 | 31 | const DBusMenu = Extension.imports.dbusMenu; |
30 | 32 | const Util = Extension.imports.util; |
31 | 33 | |
33 | 35 | * IndicatorStatusIcon implements an icon in the system status area |
34 | 36 | */ |
35 | 37 | var IndicatorStatusIcon = GObject.registerClass( |
36 | class AppIndicators_IndicatorStatusIcon extends PanelMenu.Button { | |
38 | class AppIndicatorsIndicatorStatusIcon extends PanelMenu.Button { | |
37 | 39 | _init(indicator) { |
38 | 40 | super._init(0.5, indicator.uniqueId); |
39 | 41 | this._indicator = indicator; |
45 | 47 | |
46 | 48 | this._box.add_child(this._iconBox); |
47 | 49 | |
48 | Util.connectSmart(this._indicator, 'ready', this, '_display') | |
49 | Util.connectSmart(this._indicator, 'menu', this, '_updateMenu') | |
50 | Util.connectSmart(this._indicator, 'label', this, '_updateLabel') | |
51 | Util.connectSmart(this._indicator, 'status', this, '_updateStatus') | |
50 | Util.connectSmart(this._indicator, 'ready', this, '_display'); | |
51 | Util.connectSmart(this._indicator, 'menu', this, '_updateMenu'); | |
52 | Util.connectSmart(this._indicator, 'label', this, '_updateLabel'); | |
53 | Util.connectSmart(this._indicator, 'status', this, '_updateStatus'); | |
52 | 54 | Util.connectSmart(this._indicator, 'reset', this, () => { |
53 | 55 | this._updateStatus(); |
54 | 56 | this._updateLabel(); |
59 | 61 | this._menuClient.destroy(); |
60 | 62 | this._menuClient = null; |
61 | 63 | } |
62 | }) | |
64 | }); | |
63 | 65 | |
64 | 66 | if (this._indicator.isReady) |
65 | this._display() | |
67 | this._display(); | |
66 | 68 | } |
67 | 69 | |
68 | 70 | _updateLabel() { |
70 | 72 | if (label) { |
71 | 73 | if (!this._label || !this._labelBin) { |
72 | 74 | this._labelBin = new St.Bin({ |
73 | y_align: ExtensionUtils.versionCheck(['3.34'], Config.PACKAGE_VERSION) ? | |
74 | St.Align.MIDDLE : Clutter.ActorAlign.CENTER, | |
75 | y_align: ExtensionUtils.versionCheck(['3.34'], Config.PACKAGE_VERSION) | |
76 | ? St.Align.MIDDLE : Clutter.ActorAlign.CENTER, | |
75 | 77 | }); |
76 | 78 | this._label = new St.Label(); |
77 | 79 | this._labelBin.add_actor(this._label); |
78 | 80 | this._box.add_actor(this._labelBin); |
79 | 81 | } |
80 | 82 | this._label.set_text(label); |
81 | if (!this._box.contains(this._labelBin)) this._box.add_actor(this._labelBin); //FIXME: why is it suddenly necessary? | |
82 | } else { | |
83 | if (this._label) { | |
84 | this._labelBin.destroy_all_children(); | |
85 | this._box.remove_actor(this._labelBin); | |
86 | this._labelBin.destroy(); | |
87 | delete this._labelBin; | |
88 | delete this._label; | |
89 | } | |
83 | if (!this._box.contains(this._labelBin)) | |
84 | this._box.add_actor(this._labelBin); // FIXME: why is it suddenly necessary? | |
85 | } else if (this._label) { | |
86 | this._labelBin.destroy_all_children(); | |
87 | this._box.remove_actor(this._labelBin); | |
88 | this._labelBin.destroy(); | |
89 | delete this._labelBin; | |
90 | delete this._label; | |
90 | 91 | } |
91 | 92 | } |
92 | 93 | |
93 | 94 | _updateStatus() { |
94 | this.visible = this._indicator.status != AppIndicator.SNIStatus.PASSIVE; | |
95 | this.visible = this._indicator.status !== AppIndicator.SNIStatus.PASSIVE; | |
95 | 96 | } |
96 | 97 | |
97 | 98 | _updateMenu() { |
103 | 104 | |
104 | 105 | if (this._indicator.menuPath) { |
105 | 106 | this._menuClient = new DBusMenu.Client(this._indicator.busName, |
106 | this._indicator.menuPath); | |
107 | this._indicator.menuPath); | |
107 | 108 | this._menuClient.attachToMenu(this.menu); |
108 | 109 | } |
109 | 110 | } |
113 | 114 | this._updateStatus(); |
114 | 115 | this._updateMenu(); |
115 | 116 | |
116 | Main.panel.addToStatusArea("appindicator-"+this._indicator.uniqueId, this, 1, 'right') | |
117 | Main.panel.addToStatusArea(`appindicator-${this._indicator.uniqueId}`, this, 1, 'right'); | |
117 | 118 | } |
118 | 119 | |
119 | 120 | vfunc_button_press_event(buttonEvent) { |
139 | 140 | // event, and we can choose which one we interpret. |
140 | 141 | if (scrollEvent.direction === Clutter.ScrollDirection.SMOOTH) { |
141 | 142 | const event = Clutter.get_current_event(); |
142 | let [dx, dy] = event.get_scroll_delta() | |
143 | let [dx, dy] = event.get_scroll_delta(); | |
143 | 144 | |
144 | this._indicator.scroll(dx, dy) | |
145 | this._indicator.scroll(dx, dy); | |
145 | 146 | return Clutter.EVENT_STOP; |
146 | 147 | } |
147 | 148 |
13 | 13 | // along with this program; if not, write to the Free Software |
14 | 14 | // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
15 | 15 | |
16 | var StatusNotifierItem = loadInterfaceXml("StatusNotifierItem.xml") | |
17 | const Properties = loadInterfaceXml("Properties.xml") | |
18 | var StatusNotifierWatcher = loadInterfaceXml("StatusNotifierWatcher.xml") | |
19 | var DBusMenu = loadInterfaceXml("DBusMenu.xml") | |
16 | var StatusNotifierItem = loadInterfaceXml('StatusNotifierItem.xml'); | |
17 | const Properties = loadInterfaceXml('Properties.xml'); | |
18 | var StatusNotifierWatcher = loadInterfaceXml('StatusNotifierWatcher.xml'); | |
19 | var DBusMenu = loadInterfaceXml('DBusMenu.xml'); | |
20 | 20 | |
21 | 21 | // loads a xml file into an in-memory string |
22 | 22 | function loadInterfaceXml(filename) { |
23 | let extension = imports.misc.extensionUtils.getCurrentExtension() | |
23 | let extension = imports.misc.extensionUtils.getCurrentExtension(); | |
24 | 24 | |
25 | let interfaces_dir = extension.dir.get_child("interfaces-xml") | |
25 | let interfaces_dir = extension.dir.get_child('interfaces-xml'); | |
26 | 26 | |
27 | let file = interfaces_dir.get_child(filename) | |
27 | let file = interfaces_dir.get_child(filename); | |
28 | 28 | |
29 | let [ result, contents ] = imports.gi.GLib.file_get_contents(file.get_path()) | |
29 | let [result, contents] = imports.gi.GLib.file_get_contents(file.get_path()); | |
30 | 30 | |
31 | 31 | if (result) { |
32 | //HACK: The "" + trick is important as hell because file_get_contents returns | |
32 | // HACK: The "" + trick is important as hell because file_get_contents returns | |
33 | 33 | // an object (WTF?) but Gio.makeProxyWrapper requires `typeof() == "string"` |
34 | 34 | // Otherwise, it will try to check `instanceof XML` and fail miserably because there |
35 | 35 | // is no `XML` on very recent SpiderMonkey releases (or, if SpiderMonkey is old enough, |
36 | 36 | // will spit out a TypeError soon). |
37 | 37 | if (contents instanceof Uint8Array) |
38 | contents = imports.byteArray.toString(contents); | |
39 | return "<node>" + contents + "</node>" | |
38 | contents = imports.byteArray.toString(contents); | |
39 | return `<node>${contents}</node>`; | |
40 | 40 | } else { |
41 | throw new Error("AppIndicatorSupport: Could not load file: "+filename) | |
41 | throw new Error(`AppIndicatorSupport: Could not load file: ${filename}`); | |
42 | 42 | } |
43 | 43 | } |
0 | --- | |
1 | env: | |
2 | es6: true | |
3 | extends: 'eslint:recommended' | |
4 | rules: | |
5 | array-bracket-newline: | |
6 | - error | |
7 | - consistent | |
8 | array-bracket-spacing: | |
9 | - error | |
10 | - never | |
11 | array-callback-return: error | |
12 | arrow-parens: | |
13 | - error | |
14 | - as-needed | |
15 | arrow-spacing: error | |
16 | block-scoped-var: error | |
17 | block-spacing: error | |
18 | brace-style: error | |
19 | # Waiting for this to have matured a bit in eslint | |
20 | # camelcase: | |
21 | # - error | |
22 | # - properties: never | |
23 | # allow: [^vfunc_, ^on_, _instance_init] | |
24 | comma-dangle: | |
25 | - error | |
26 | - always-multiline | |
27 | comma-spacing: | |
28 | - error | |
29 | - before: false | |
30 | after: true | |
31 | comma-style: | |
32 | - error | |
33 | - last | |
34 | computed-property-spacing: error | |
35 | curly: | |
36 | - error | |
37 | - multi-or-nest | |
38 | - consistent | |
39 | dot-location: | |
40 | - error | |
41 | - property | |
42 | eol-last: error | |
43 | eqeqeq: error | |
44 | func-call-spacing: error | |
45 | func-name-matching: error | |
46 | func-style: | |
47 | - error | |
48 | - declaration | |
49 | - allowArrowFunctions: true | |
50 | indent: | |
51 | - error | |
52 | - 4 | |
53 | - ignoredNodes: | |
54 | # Allow not indenting the body of GObject.registerClass, since in the | |
55 | # future it's intended to be a decorator | |
56 | - 'CallExpression[callee.object.name=GObject][callee.property.name=registerClass] > ClassExpression:first-child' | |
57 | # Allow dedenting chained member expressions | |
58 | MemberExpression: 'off' | |
59 | key-spacing: | |
60 | - error | |
61 | - beforeColon: false | |
62 | afterColon: true | |
63 | keyword-spacing: | |
64 | - error | |
65 | - before: true | |
66 | after: true | |
67 | linebreak-style: | |
68 | - error | |
69 | - unix | |
70 | lines-between-class-members: error | |
71 | max-nested-callbacks: error | |
72 | max-statements-per-line: error | |
73 | new-parens: error | |
74 | no-array-constructor: error | |
75 | no-await-in-loop: error | |
76 | no-caller: error | |
77 | no-constant-condition: | |
78 | - error | |
79 | - checkLoops: false | |
80 | no-div-regex: error | |
81 | no-empty: | |
82 | - error | |
83 | - allowEmptyCatch: true | |
84 | no-extra-bind: error | |
85 | no-extra-parens: | |
86 | - error | |
87 | - all | |
88 | - conditionalAssign: false | |
89 | nestedBinaryExpressions: false | |
90 | returnAssign: false | |
91 | no-implicit-coercion: | |
92 | - error | |
93 | - allow: | |
94 | - '!!' | |
95 | no-invalid-this: error | |
96 | no-iterator: error | |
97 | no-label-var: error | |
98 | no-lonely-if: error | |
99 | no-loop-func: error | |
100 | no-nested-ternary: error | |
101 | no-new-object: error | |
102 | no-new-wrappers: error | |
103 | no-octal-escape: error | |
104 | no-proto: error | |
105 | no-prototype-builtins: 'off' | |
106 | no-restricted-properties: | |
107 | - error | |
108 | - object: Lang | |
109 | property: bind | |
110 | message: Use arrow notation or Function.prototype.bind() | |
111 | - object: Lang | |
112 | property: Class | |
113 | message: Use ES6 classes | |
114 | - object: imports | |
115 | property: mainloop | |
116 | message: Use GLib main loops and timeouts | |
117 | no-restricted-syntax: | |
118 | - error | |
119 | - selector: >- | |
120 | MethodDefinition[key.name="_init"] > | |
121 | FunctionExpression[params.length=1] > | |
122 | BlockStatement[body.length=1] | |
123 | CallExpression[arguments.length=1][callee.object.type="Super"][callee.property.name="_init"] > | |
124 | Identifier:first-child | |
125 | message: _init() that only calls super._init() is unnecessary | |
126 | - selector: >- | |
127 | MethodDefinition[key.name="_init"] > | |
128 | FunctionExpression[params.length=0] > | |
129 | BlockStatement[body.length=1] | |
130 | CallExpression[arguments.length=0][callee.object.type="Super"][callee.property.name="_init"] | |
131 | message: _init() that only calls super._init() is unnecessary | |
132 | no-return-assign: error | |
133 | no-return-await: error | |
134 | no-self-compare: error | |
135 | no-shadow: error | |
136 | no-shadow-restricted-names: error | |
137 | no-spaced-func: error | |
138 | no-tabs: error | |
139 | no-template-curly-in-string: error | |
140 | no-throw-literal: error | |
141 | no-trailing-spaces: error | |
142 | no-undef-init: error | |
143 | no-unneeded-ternary: error | |
144 | no-unused-expressions: error | |
145 | no-unused-vars: | |
146 | - error | |
147 | # Vars use a suffix _ instead of a prefix because of file-scope private vars | |
148 | - varsIgnorePattern: (^unused|_$) | |
149 | argsIgnorePattern: ^(unused|_) | |
150 | no-useless-call: error | |
151 | no-useless-computed-key: error | |
152 | no-useless-concat: error | |
153 | no-useless-constructor: error | |
154 | no-useless-rename: error | |
155 | no-useless-return: error | |
156 | no-whitespace-before-property: error | |
157 | no-with: error | |
158 | nonblock-statement-body-position: | |
159 | - error | |
160 | - below | |
161 | object-curly-newline: | |
162 | - error | |
163 | - consistent: true | |
164 | object-curly-spacing: error | |
165 | object-shorthand: error | |
166 | operator-assignment: error | |
167 | operator-linebreak: error | |
168 | # These may be a bit controversial, we can try them out and enable them later | |
169 | # prefer-const: error | |
170 | # prefer-destructuring: error | |
171 | prefer-numeric-literals: error | |
172 | prefer-promise-reject-errors: error | |
173 | prefer-rest-params: error | |
174 | prefer-spread: error | |
175 | prefer-template: error | |
176 | quotes: | |
177 | - error | |
178 | - single | |
179 | - avoidEscape: true | |
180 | require-await: error | |
181 | rest-spread-spacing: error | |
182 | semi: | |
183 | - error | |
184 | - always | |
185 | semi-spacing: | |
186 | - error | |
187 | - before: false | |
188 | after: true | |
189 | semi-style: error | |
190 | space-before-blocks: error | |
191 | space-before-function-paren: | |
192 | - error | |
193 | - named: never | |
194 | # for `function ()` and `async () =>`, preserve space around keywords | |
195 | anonymous: always | |
196 | asyncArrow: always | |
197 | space-in-parens: error | |
198 | space-infix-ops: | |
199 | - error | |
200 | - int32Hint: false | |
201 | space-unary-ops: error | |
202 | spaced-comment: error | |
203 | switch-colon-spacing: error | |
204 | symbol-description: error | |
205 | template-curly-spacing: error | |
206 | template-tag-spacing: error | |
207 | unicode-bom: error | |
208 | valid-jsdoc: | |
209 | - error | |
210 | - requireReturn: false | |
211 | wrap-iife: | |
212 | - error | |
213 | - inside | |
214 | yield-star-spacing: error | |
215 | yoda: error | |
216 | globals: | |
217 | ARGV: readonly | |
218 | Debugger: readonly | |
219 | GIRepositoryGType: readonly | |
220 | globalThis: readonly | |
221 | imports: readonly | |
222 | Intl: readonly | |
223 | log: readonly | |
224 | logError: readonly | |
225 | print: readonly | |
226 | printerr: readonly | |
227 | parserOptions: | |
228 | ecmaVersion: 2020 |
0 | rules: | |
1 | eqeqeq: off | |
2 | indent: | |
3 | - error | |
4 | - 4 | |
5 | - ignoredNodes: | |
6 | - 'CallExpression[callee.object.name=GObject][callee.property.name=registerClass] > ClassExpression:first-child' | |
7 | CallExpression: | |
8 | arguments: first | |
9 | ArrayExpression: first | |
10 | ObjectExpression: first | |
11 | MemberExpression: off | |
12 | prefer-template: off | |
13 | quotes: off |
0 | rules: | |
1 | camelcase: | |
2 | - error | |
3 | - properties: never | |
4 | allow: [^vfunc_, ^on_] | |
5 | consistent-return: error | |
6 | key-spacing: | |
7 | - error | |
8 | - mode: minimum | |
9 | beforeColon: false | |
10 | afterColon: true | |
11 | object-curly-spacing: | |
12 | - error | |
13 | - always | |
14 | prefer-arrow-callback: error | |
15 | ||
16 | overrides: | |
17 | - files: js/** | |
18 | excludedFiles: | |
19 | - js/portalHelper/* | |
20 | globals: | |
21 | global: readonly | |
22 | _: readonly | |
23 | C_: readonly | |
24 | N_: readonly | |
25 | ngettext: readonly | |
26 | - files: subprojects/extensions-app/js/** | |
27 | globals: | |
28 | _: readonly | |
29 | C_: readonly | |
30 | N_: readonly |
297 | 297 | var _promisify = Gio._promisify; |
298 | 298 | if (imports.system.version < 16501) { |
299 | 299 | /* This is backported from upstream gjs, so that all the features are available */ |
300 | _promisify = function(proto, asyncFunc, finishFunc) { | |
300 | _promisify = function (proto, asyncFunc, finishFunc) { | |
301 | 301 | if (proto[`_original_${asyncFunc}`] !== undefined) |
302 | 302 | return; |
303 | 303 | proto[`_original_${asyncFunc}`] = proto[asyncFunc]; |
306 | 306 | return this[`_original_${asyncFunc}`](...args); |
307 | 307 | return new Promise((resolve, reject) => { |
308 | 308 | const callStack = new Error().stack.split('\n').filter(line => !line.match(/promisify/)).join('\n'); |
309 | this[`_original_${asyncFunc}`](...args, function (source, res) { | |
309 | this[`_original_${asyncFunc}`](...args, (source, res) => { | |
310 | 310 | try { |
311 | 311 | const result = source !== null && source[finishFunc] !== undefined |
312 | 312 | ? source[finishFunc](res) |
324 | 324 | }); |
325 | 325 | }); |
326 | 326 | }; |
327 | } | |
327 | }; | |
328 | 328 | } |
329 | 329 | |
330 | 330 | if (!Promise.allSettled) { |
13 | 13 | // along with this program; if not, write to the Free Software |
14 | 14 | // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
15 | 15 | |
16 | const Gio = imports.gi.Gio | |
17 | const GLib = imports.gi.GLib | |
18 | ||
19 | const Extension = imports.misc.extensionUtils.getCurrentExtension() | |
20 | ||
21 | const AppIndicator = Extension.imports.appIndicator | |
22 | const IndicatorStatusIcon = Extension.imports.indicatorStatusIcon | |
23 | const Interfaces = Extension.imports.interfaces | |
16 | const Gio = imports.gi.Gio; | |
17 | const GLib = imports.gi.GLib; | |
18 | ||
19 | const Extension = imports.misc.extensionUtils.getCurrentExtension(); | |
20 | ||
21 | const AppIndicator = Extension.imports.appIndicator; | |
22 | const IndicatorStatusIcon = Extension.imports.indicatorStatusIcon; | |
23 | const Interfaces = Extension.imports.interfaces; | |
24 | 24 | const PromiseUtils = Extension.imports.promiseUtils; |
25 | const Util = Extension.imports.util | |
25 | const Util = Extension.imports.util; | |
26 | 26 | |
27 | 27 | |
28 | 28 | // TODO: replace with org.freedesktop and /org/freedesktop when approved |
29 | 29 | const KDE_PREFIX = 'org.kde'; |
30 | 30 | |
31 | var WATCHER_BUS_NAME = KDE_PREFIX + '.StatusNotifierWatcher'; | |
31 | var WATCHER_BUS_NAME = `${KDE_PREFIX}.StatusNotifierWatcher`; | |
32 | 32 | const WATCHER_OBJECT = '/StatusNotifierWatcher'; |
33 | 33 | |
34 | 34 | const DEFAULT_ITEM_OBJECT_PATH = '/StatusNotifierItem'; |
42 | 42 | this._watchDog = watchDog; |
43 | 43 | this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(Interfaces.StatusNotifierWatcher, this); |
44 | 44 | this._dbusImpl.export(Gio.DBus.session, WATCHER_OBJECT); |
45 | this._cancellable = new Gio.Cancellable; | |
45 | this._cancellable = new Gio.Cancellable(); | |
46 | 46 | this._everAcquiredName = false; |
47 | 47 | this._ownName = Gio.DBus.session.own_name(WATCHER_BUS_NAME, |
48 | Gio.BusNameOwnerFlags.NONE, | |
49 | this._acquiredName.bind(this), | |
50 | this._lostName.bind(this)); | |
48 | Gio.BusNameOwnerFlags.NONE, | |
49 | this._acquiredName.bind(this), | |
50 | this._lostName.bind(this)); | |
51 | 51 | this._items = new Map(); |
52 | 52 | |
53 | 53 | this._seekStatusNotifierItems(); |
59 | 59 | |
60 | 60 | _lostName() { |
61 | 61 | if (this._everAcquiredName) |
62 | Util.Logger.debug('Lost name' + WATCHER_BUS_NAME); | |
62 | Util.Logger.debug(`Lost name${WATCHER_BUS_NAME}`); | |
63 | 63 | else |
64 | Util.Logger.warn('Failed to acquire ' + WATCHER_BUS_NAME); | |
64 | Util.Logger.warn(`Failed to acquire ${WATCHER_BUS_NAME}`); | |
65 | 65 | this._watchDog.nameAcquired = false; |
66 | 66 | } |
67 | 67 | |
91 | 91 | GLib.PRIORITY_DEFAULT, this._cancellable); |
92 | 92 | if (!indicator.hasNameOwner) |
93 | 93 | this._itemVanished(id); |
94 | }; | |
94 | } | |
95 | 95 | }); |
96 | 96 | |
97 | 97 | // if the desktop is not ready delay the icon creation and signal emissions |
115 | 115 | let item = this._items.get(id); |
116 | 116 | |
117 | 117 | if (item) { |
118 | //delete the old one and add the new indicator | |
118 | // delete the old one and add the new indicator | |
119 | 119 | Util.Logger.debug(`Attempting to re-register ${id}; resetting instead`); |
120 | 120 | item.reset(); |
121 | 121 | return; |
122 | 122 | } |
123 | 123 | |
124 | this._registerItem(service, bus_name, obj_path) | |
124 | this._registerItem(service, bus_name, obj_path); | |
125 | 125 | } |
126 | 126 | |
127 | 127 | async _seekStatusNotifierItems() { |
170 | 170 | } |
171 | 171 | |
172 | 172 | if (!bus_name || !obj_path) { |
173 | let error = "Impossible to register an indicator for parameters '"+ | |
174 | service.toString()+"'"; | |
173 | let error = `Impossible to register an indicator for parameters '${ | |
174 | service.toString()}'`; | |
175 | 175 | Util.Logger.warn(error); |
176 | 176 | |
177 | 177 | invocation.return_dbus_error('org.gnome.gjs.JSError.ValueError', |
178 | error); | |
178 | error); | |
179 | 179 | return; |
180 | 180 | } |
181 | 181 | |
186 | 186 | |
187 | 187 | _itemVanished(id) { |
188 | 188 | // FIXME: this is useless if the path name disappears while the bus stays alive (not unheard of) |
189 | if (this._items.has(id)) { | |
189 | if (this._items.has(id)) | |
190 | 190 | this._remove(id); |
191 | } | |
191 | ||
192 | 192 | } |
193 | 193 | |
194 | 194 | _remove(id) { |
17 | 17 | introspectBusObject, dbusNodeImplementsInterfaces, waitForStartupCompletion, |
18 | 18 | BUS_ADDRESS_REGEX */ |
19 | 19 | |
20 | const Gio = imports.gi.Gio | |
21 | const GLib = imports.gi.GLib | |
20 | const Gio = imports.gi.Gio; | |
21 | const GLib = imports.gi.GLib; | |
22 | 22 | const Gtk = imports.gi.Gtk; |
23 | 23 | const Gdk = imports.gi.Gdk; |
24 | 24 | const Main = imports.ui.main; |
25 | const GObject = imports.gi.GObject | |
25 | const GObject = imports.gi.GObject; | |
26 | 26 | const Extension = imports.misc.extensionUtils.getCurrentExtension(); |
27 | 27 | const Params = imports.misc.params; |
28 | 28 | const PromiseUtils = Extension.imports.promiseUtils; |
29 | 29 | |
30 | const Signals = imports.signals | |
31 | ||
32 | var BUS_ADDRESS_REGEX = /([a-zA-Z0-9._-]+\.[a-zA-Z0-9.-]+)|(:[0-9]+\.[0-9]+)$/ | |
30 | const Signals = imports.signals; | |
31 | ||
32 | var BUS_ADDRESS_REGEX = /([a-zA-Z0-9._-]+\.[a-zA-Z0-9.-]+)|(:[0-9]+\.[0-9]+)$/; | |
33 | 33 | |
34 | 34 | PromiseUtils._promisify(Gio.DBusConnection.prototype, 'call', 'call_finish'); |
35 | 35 | |
43 | 43 | |
44 | 44 | let cancellable = cancelRefreshPropertyOnProxy(proxy, { |
45 | 45 | propertyName, |
46 | addNew: true | |
46 | addNew: true, | |
47 | 47 | }); |
48 | 48 | |
49 | 49 | try { |
50 | 50 | const [valueVariant] = (await proxy.g_connection.call(proxy.g_name, |
51 | 51 | proxy.g_object_path, 'org.freedesktop.DBus.Properties', 'Get', |
52 | GLib.Variant.new('(ss)', [ proxy.g_interface_name, propertyName ]), | |
52 | GLib.Variant.new('(ss)', [proxy.g_interface_name, propertyName]), | |
53 | 53 | GLib.VariantType.new('(v)'), Gio.DBusCallFlags.NONE, -1, |
54 | 54 | cancellable)).deep_unpack(); |
55 | 55 | |
59 | 59 | proxy.get_cached_property(propertyName).equal(valueVariant)) |
60 | 60 | return; |
61 | 61 | |
62 | proxy.set_cached_property(propertyName, valueVariant) | |
62 | proxy.set_cached_property(propertyName, valueVariant); | |
63 | 63 | |
64 | 64 | // synthesize a batched property changed event |
65 | 65 | if (!proxy._proxyChangedProperties) |
84 | 84 | } |
85 | 85 | } |
86 | 86 | |
87 | var cancelRefreshPropertyOnProxy = function(proxy, params) { | |
87 | var cancelRefreshPropertyOnProxy = function (proxy, params) { | |
88 | 88 | if (!proxy._proxyCancellables) |
89 | 89 | return; |
90 | 90 | |
112 | 112 | delete proxy._proxyChangedProperties; |
113 | 113 | delete proxy._proxyCancellables; |
114 | 114 | } |
115 | } | |
115 | }; | |
116 | 116 | |
117 | 117 | async function getUniqueBusName(bus, name, cancellable) { |
118 | 118 | if (name[0] == ':') |
154 | 154 | |
155 | 155 | async function introspectBusObject(bus, name, cancellable, path = undefined) { |
156 | 156 | if (!path) |
157 | path = "/"; | |
157 | path = '/'; | |
158 | 158 | |
159 | 159 | const [introspection] = (await bus.call(name, path, 'org.freedesktop.DBus.Introspectable', |
160 | 160 | 'Introspect', null, new GLib.VariantType('(s)'), Gio.DBusCallFlags.NONE, |
182 | 182 | return nodes; |
183 | 183 | } |
184 | 184 | |
185 | var dbusNodeImplementsInterfaces = function(node_info, interfaces) { | |
185 | var dbusNodeImplementsInterfaces = function (node_info, interfaces) { | |
186 | 186 | if (!(node_info instanceof Gio.DBusNodeInfo) || !Array.isArray(interfaces)) |
187 | 187 | return false; |
188 | 188 | |
192 | 192 | } |
193 | 193 | |
194 | 194 | return false; |
195 | } | |
195 | }; | |
196 | 196 | |
197 | 197 | var NameWatcher = class AppIndicatorsNameWatcher { |
198 | 198 | constructor(name) { |
199 | 199 | this._watcherId = Gio.DBus.session.watch_name(name, |
200 | 200 | Gio.BusNameWatcherFlags.NONE, () => { |
201 | this._nameOnBus = true | |
201 | this._nameOnBus = true; | |
202 | 202 | Logger.debug(`Name ${name} appeared`); |
203 | 203 | this.emit('changed'); |
204 | 204 | this.emit('appeared'); |
223 | 223 | }; |
224 | 224 | Signals.addSignalMethods(NameWatcher.prototype); |
225 | 225 | |
226 | const connectSmart3A = function(src, signal, handler) { | |
227 | let id = src.connect(signal, handler) | |
226 | const connectSmart3A = function (src, signal, handler) { | |
227 | let id = src.connect(signal, handler); | |
228 | 228 | |
229 | 229 | if (src.connect && (!(src instanceof GObject.Object) || GObject.signal_lookup('destroy', src))) { |
230 | 230 | let destroy_id = src.connect('destroy', () => { |
231 | src.disconnect(id) | |
232 | src.disconnect(destroy_id) | |
233 | }) | |
234 | } | |
235 | } | |
236 | ||
237 | const connectSmart4A = function(src, signal, target, method) { | |
231 | src.disconnect(id); | |
232 | src.disconnect(destroy_id); | |
233 | }); | |
234 | } | |
235 | }; | |
236 | ||
237 | const connectSmart4A = function (src, signal, target, method) { | |
238 | 238 | if (typeof method === 'string') |
239 | method = target[method].bind(target) | |
239 | method = target[method].bind(target); | |
240 | 240 | if (typeof method === 'function') |
241 | method = method.bind(target) | |
242 | ||
243 | let signal_id = src.connect(signal, method) | |
241 | method = method.bind(target); | |
242 | ||
243 | let signal_id = src.connect(signal, method); | |
244 | 244 | |
245 | 245 | // GObject classes might or might not have a destroy signal |
246 | 246 | // JS Classes will not complain when connecting to non-existent signals |
247 | let src_destroy_id = src.connect && (!(src instanceof GObject.Object) || GObject.signal_lookup('destroy', src)) ? src.connect('destroy', on_destroy) : 0 | |
248 | let tgt_destroy_id = target.connect && (!(target instanceof GObject.Object) || GObject.signal_lookup('destroy', target)) ? target.connect('destroy', on_destroy) : 0 | |
247 | let src_destroy_id = src.connect && (!(src instanceof GObject.Object) || GObject.signal_lookup('destroy', src)) ? src.connect('destroy', on_destroy) : 0; | |
248 | let tgt_destroy_id = target.connect && (!(target instanceof GObject.Object) || GObject.signal_lookup('destroy', target)) ? target.connect('destroy', on_destroy) : 0; | |
249 | 249 | |
250 | 250 | function on_destroy() { |
251 | src.disconnect(signal_id) | |
252 | if (src_destroy_id) src.disconnect(src_destroy_id) | |
253 | if (tgt_destroy_id) target.disconnect(tgt_destroy_id) | |
254 | } | |
255 | } | |
251 | src.disconnect(signal_id); | |
252 | if (src_destroy_id) | |
253 | src.disconnect(src_destroy_id); | |
254 | if (tgt_destroy_id) | |
255 | target.disconnect(tgt_destroy_id); | |
256 | } | |
257 | }; | |
256 | 258 | |
257 | 259 | /** |
258 | 260 | * Connect signals to slots, and remove the connection when either source or |
263 | 265 | * or |
264 | 266 | * Util.connectSmart(srcOb, 'signal', () => { ... }) |
265 | 267 | */ |
266 | var connectSmart = function() { | |
268 | var connectSmart = function () { | |
267 | 269 | if (arguments.length == 4) |
268 | return connectSmart4A.apply(null, arguments) | |
270 | return connectSmart4A.apply(null, arguments); | |
269 | 271 | else |
270 | return connectSmart3A.apply(null, arguments) | |
271 | } | |
272 | return connectSmart3A.apply(null, arguments); | |
273 | }; | |
272 | 274 | |
273 | 275 | // eslint-disable-next-line valid-jsdoc |
274 | 276 | /** |