Package list gnome-shell-extension-appindicator / 4e802c0
New upstream version 35 Marco Trevisan (TreviƱo) 3 months ago
18 changed file(s) with 2086 addition(s) and 1440 deletion(s). Raw diff Collapse all Expand all
0 extends:
1 - ./lint/eslintrc-gjs.yml
2 - ./lint/eslintrc-shell.yml
0 name: ESLint
1 on: push
2 jobs:
3 build:
4 runs-on: ubuntu-latest
5 steps:
6 - uses: actions/checkout@v2
7 - name: Install modules
8 run: sudo npm install eslint -g
9 - name: Run ESLint
10 run: eslint . --ext .js,.jsx,.ts,.tsx
99 rm -f build/appindicator-support.zip
1010 zip build/appindicator-support.zip $(ZIP)
1111
12 check:
13 eslint $(shell find -name '*.js')
14
1215 clean:
1316 rm -rf build
1313 // along with this program; if not, write to the Free Software
1414 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
1515
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 /* exported AppIndicator, IconActor */
17
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;
2524
2625 const Extension = imports.misc.extensionUtils.getCurrentExtension();
27 const Signals = imports.signals
28
29 const DBusMenu = Extension.imports.dbusMenu;
26 const Signals = imports.signals;
27
3028 const IconCache = Extension.imports.iconCache;
3129 const Util = Extension.imports.util;
3230 const Interfaces = Extension.imports.interfaces;
31 const PromiseUtils = Extension.imports.promiseUtils;
32
33 PromiseUtils._promisify(Gio.File.prototype, 'read_async', 'read_finish');
34 PromiseUtils._promisify(Gio._LocalFilePrototype, 'read_async', 'read_finish');
35 PromiseUtils._promisify(GdkPixbuf.Pixbuf, 'get_file_info_async', 'get_file_info_finish');
36 PromiseUtils._promisify(GdkPixbuf.Pixbuf, 'new_from_stream_at_scale_async', 'new_from_stream_finish');
37 PromiseUtils._promisify(Gio.DBusProxy.prototype, 'init_async', 'init_finish');
3338
3439 const MAX_UPDATE_FREQUENCY = 100; // In ms
3540
41 // eslint-disable-next-line no-unused-vars
3642 const SNICategory = {
3743 APPLICATION: 'ApplicationStatus',
3844 COMMUNICATIONS: 'Communications',
3945 SYSTEM: 'SystemServices',
40 HARDWARE: 'Hardware'
46 HARDWARE: 'Hardware',
4147 };
4248
4349 var SNIStatus = {
4450 PASSIVE: 'Passive',
4551 ACTIVE: 'Active',
46 NEEDS_ATTENTION: 'NeedsAttention'
52 NEEDS_ATTENTION: 'NeedsAttention',
4753 };
4854
4955 const SNIconType = {
5662 * the AppIndicator class serves as a generic container for indicator information and functions common
5763 * for every displaying implementation (IndicatorMessageSource and IndicatorStatusIcon)
5864 */
59 var AppIndicator = class AppIndicators_AppIndicator {
60
61 constructor(bus_name, object) {
62 this.busName = bus_name
63 this._uniqueId = bus_name + object
64 this._accumuledSignals = new Set();
65
66 let interface_info = Gio.DBusInterfaceInfo.new_for_xml(Interfaces.StatusNotifierItem)
67
68 //HACK: we cannot use Gio.DBusProxy.makeProxyWrapper because we need
65 var AppIndicator = class AppIndicatorsAppIndicator {
66
67 constructor(service, busName, object) {
68 this.busName = busName;
69 this._uniqueId = busName + object;
70 this._accumulatedSignals = new Set();
71
72 const interfaceInfo = Gio.DBusInterfaceInfo.new_for_xml(Interfaces.StatusNotifierItem);
73
74 // HACK: we cannot use Gio.DBusProxy.makeProxyWrapper because we need
6975 // to specify G_DBUS_PROXY_FLAGS_GET_INVALIDATED_PROPERTIES
7076 this._cancellable = new Gio.Cancellable();
7177 this._proxy = new Gio.DBusProxy({ g_connection: Gio.DBus.session,
72 g_interface_name: interface_info.name,
73 g_interface_info: interface_info,
74 g_name: bus_name,
75 g_object_path: object,
76 g_flags: Gio.DBusProxyFlags.GET_INVALIDATED_PROPERTIES })
77 this._proxy.init_async(GLib.PRIORITY_DEFAULT, this._cancellable, ((initable, result) => {
78 try {
79 initable.init_finish(result);
80 this._checkIfReady();
81
82 if (!this.isReady && !this.menuPath) {
83 let checks = 0;
84 this._delayCheck = GLib.timeout_add_seconds(
85 GLib.PRIORITY_DEFAULT_IDLE, 1, () => {
86 Util.refreshPropertyOnProxy(this._proxy, 'Menu');
87 return !this.isReady && ++checks < 3;
88 });
89 }
90 } catch(e) {
91 if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
92 Util.Logger.warn(`While intializing proxy for ${bus_name} ${object}: ${e}`);
93 }
94 }))
95
96 Util.connectSmart(this._proxy, 'g-properties-changed', this, '_onPropertiesChanged')
97 Util.connectSmart(this._proxy, 'g-signal', this, this._onProxySignal)
98 Util.connectSmart(this._proxy, 'notify::g-name-owner', this, '_nameOwnerChanged')
78 g_interface_name: interfaceInfo.name,
79 g_interface_info: interfaceInfo,
80 g_name: busName,
81 g_object_path: object,
82 g_flags: Gio.DBusProxyFlags.GET_INVALIDATED_PROPERTIES });
83
84 this._setupProxy();
85 Util.connectSmart(this._proxy, 'g-properties-changed', this, this._onPropertiesChanged);
86 Util.connectSmart(this._proxy, 'g-signal', this, this._onProxySignal);
87 Util.connectSmart(this._proxy, 'notify::g-name-owner', this, this._nameOwnerChanged);
88
89 if (service !== busName && service.match(Util.BUS_ADDRESS_REGEX)) {
90 this._uniqueId = service;
91 this._nameWatcher = new Util.NameWatcher(service);
92 Util.connectSmart(this._nameWatcher, 'changed', this, this._nameOwnerChanged);
93 }
94 }
95
96 async _setupProxy() {
97 try {
98 await this._proxy.init_async(GLib.PRIORITY_DEFAULT, this._cancellable);
99 this._checkIfReady();
100 this._checkMenuReady();
101 } catch (e) {
102 if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
103 Util.Logger.warn(`While initalizing proxy for ${this._uniqueId}: ${e}`);
104 }
99105 }
100106
101107 _checkIfReady() {
102108 let wasReady = this.isReady;
103109 let isReady = false;
104110
105 if (this._proxy.g_name_owner && this.menuPath)
111 if (this.hasNameOwner && this.menuPath)
106112 isReady = true;
107113
108114 this.isReady = isReady;
110116
111117 if (this.isReady && !wasReady) {
112118 if (this._delayCheck) {
113 GLib.Source.remove(this._delayCheck);
119 this._delayCheck.cancel();
114120 delete this._delayCheck;
115121 }
116122
121127 return false;
122128 }
123129
130 async _checkMenuReady() {
131 if (this.menuPath)
132 return true;
133
134 const cancellable = this._cancellable;
135 for (let checks = 0; checks < 3 && !this.isReady; ++checks) {
136 this._delayCheck = new PromiseUtils.TimeoutSecondsPromise(1,
137 GLib.PRIORITY_DEFAULT_IDLE, cancellable);
138 // eslint-disable-next-line no-await-in-loop
139 await this._delayCheck;
140 Util.refreshPropertyOnProxy(this._proxy, 'Menu');
141 }
142
143 return !!this.menuPath;
144 }
145
124146 _nameOwnerChanged() {
125 if (!this._proxy.g_name_owner)
147 if (!this.hasNameOwner)
126148 this._checkIfReady();
149 else
150 this._checkMenuReady();
151
152 this.emit('name-owner-changed');
127153 }
128154
129155 _addExtraProperty(name) {
137163 get: () => {
138164 const v = this._proxy.get_cached_property(name);
139165 return v ? v.deep_unpack() : null;
140 }
166 },
141167 });
142168 }
143169
148174 let interfaceProps = this._proxy.g_interface_info.properties;
149175 this._proxyPropertyList =
150176 (this._proxy.get_cached_property_names() || []).filter(p =>
151 interfaceProps.some(propinfo => propinfo.name == p));
177 interfaceProps.some(propinfo => propinfo.name === p));
152178
153179 if (this._proxyPropertyList.length) {
154180 this._addExtraProperty('XAyatanaLabel');
162188 let prop = null;
163189
164190 if (signal.startsWith('New'))
165 prop = signal.substr(3)
191 prop = signal.substr(3);
166192 else if (signal.startsWith('XAyatanaNew'))
167 prop = 'XAyatana' + signal.substr(11)
193 prop = `XAyatana${signal.substr(11)}`;
168194
169195 if (!prop)
170196 return;
171197
172198 [prop, `${prop}Name`, `${prop}Pixmap`].filter(p =>
173199 this._proxyPropertyList.includes(p)).forEach(p =>
174 Util.refreshPropertyOnProxy(this._proxy, p, {
175 skipEqualtyCheck: p.endsWith('Pixmap'),
176 })
177 );
178 }
179
180 _onProxySignal(_proxy, _sender, signal, _params) {
181 this._accumuledSignals.add(signal);
182
183 if (this._signalsAccumulatorId)
200 Util.refreshPropertyOnProxy(this._proxy, p, {
201 skipEqualityCheck: p.endsWith('Pixmap'),
202 }),
203 );
204 }
205
206 async _onProxySignal(_proxy, _sender, signal, _params) {
207 this._accumulatedSignals.add(signal);
208
209 if (this._signalsAccumulator)
184210 return;
185211
186 this._signalsAccumulatorId = GLib.timeout_add(
187 GLib.PRIORITY_DEFAULT_IDLE, MAX_UPDATE_FREQUENCY, () => {
188 this._accumuledSignals.forEach((s) => this._translateNewSignals(s));
189 this._accumuledSignals.clear();
190 delete this._signalsAccumulatorId;
191 });
192 }
193
194 //public property getters
212 this._signalsAccumulator = new PromiseUtils.TimeoutPromise(
213 GLib.PRIORITY_DEFAULT_IDLE, MAX_UPDATE_FREQUENCY, this._cancellable);
214 try {
215 await this._signalsAccumulator;
216 this._accumulatedSignals.forEach(s => this._translateNewSignals(s));
217 this._accumulatedSignals.clear();
218 } finally {
219 delete this._signalsAccumulator;
220 }
221 }
222
223 // public property getters
195224 get title() {
196225 return this._proxy.Title;
197226 }
227
198228 get id() {
199229 return this._proxy.Id;
200230 }
231
201232 get uniqueId() {
202233 return this._uniqueId;
203234 }
235
204236 get status() {
205237 return this._proxy.Status;
206238 }
239
207240 get label() {
208241 return this._proxy.XAyatanaLabel;
209242 }
243
210244 get menuPath() {
211 if (this._proxy.Menu == '/NO_DBUSMENU')
245 if (this._proxy.Menu === '/NO_DBUSMENU')
212246 return null;
213247
214248 return this._proxy.Menu || '/MenuBar';
218252 return [
219253 this._proxy.AttentionIconName,
220254 this._proxy.AttentionIconPixmap,
221 this._proxy.IconThemePath
222 ]
255 this._proxy.IconThemePath,
256 ];
223257 }
224258
225259 get icon() {
226260 return [
227261 this._proxy.IconName,
228262 this._proxy.IconPixmap,
229 this._proxy.IconThemePath
230 ]
263 this._proxy.IconThemePath,
264 ];
231265 }
232266
233267 get overlayIcon() {
234268 return [
235269 this._proxy.OverlayIconName,
236270 this._proxy.OverlayIconPixmap,
237 this._proxy.IconThemePath
238 ]
239 }
240
241 _onPropertiesChanged(proxy, changed, invalidated) {
271 this._proxy.IconThemePath,
272 ];
273 }
274
275 get hasNameOwner() {
276 return !!this._proxy.g_name_owner ||
277 this._nameWatcher && this._nameWatcher.nameOnBus;
278 }
279
280 get cancellable() {
281 return this._cancellable;
282 }
283
284 _onPropertiesChanged(_proxy, changed, _invalidated) {
242285 let props = Object.keys(changed.unpack());
243286 let signalsToEmit = new Set();
244287
245 props.forEach((property) => {
288 props.forEach(property => {
246289 // some property changes require updates on our part,
247290 // a few need to be passed down to the displaying code
248291
249292 // all these can mean that the icon has to be changed
250 if (property == 'Status' ||
293 if (property === 'Status' ||
251294 property.startsWith('Icon') ||
252 property.startsWith('AttentionIcon')) {
253 signalsToEmit.add('icon')
254 }
295 property.startsWith('AttentionIcon'))
296 signalsToEmit.add('icon');
297
255298
256299 // same for overlays
257300 if (property.startsWith('OverlayIcon'))
258 signalsToEmit.add('overlay-icon')
301 signalsToEmit.add('overlay-icon');
259302
260303 // this may make all of our icons invalid
261 if (property == 'IconThemePath') {
262 signalsToEmit.add('icon')
263 signalsToEmit.add('overlay-icon')
304 if (property === 'IconThemePath') {
305 signalsToEmit.add('icon');
306 signalsToEmit.add('overlay-icon');
264307 }
265308
266309 // the label will be handled elsewhere
267 if (property == 'XAyatanaLabel')
268 signalsToEmit.add('label')
269
270 if (property == 'Menu') {
310 if (property === 'XAyatanaLabel')
311 signalsToEmit.add('label');
312
313 if (property === 'Menu') {
271314 if (!this._checkIfReady() && this.isReady)
272 signalsToEmit.add('menu')
315 signalsToEmit.add('menu');
273316 }
274317
275318 // status updates may cause the indicator to be hidden
276 if (property == 'Status')
277 signalsToEmit.add('status')
319 if (property === 'Status')
320 signalsToEmit.add('status');
278321 });
279322
280323 signalsToEmit.forEach(s => this.emit(s));
285328 }
286329
287330 destroy() {
288 this.emit('destroy')
289
290 this.disconnectAll()
331 this.emit('destroy');
332
333 this.disconnectAll();
291334 this._cancellable.cancel();
292335 Util.cancelRefreshPropertyOnProxy(this._proxy);
336 if (this._nameWatcher)
337 this._nameWatcher.destroy();
293338 delete this._cancellable;
294 delete this._proxy
295
296 if (this._signalsAccumulatorId) {
297 GLib.Source.remove(this._signalsAccumulatorId);
298 delete this._signalsAccumulatorId;
299 }
300
301 if (this._delayCheck) {
302 GLib.Source.remove(this._delayCheck);
303 delete this._delayCheck;
304 }
339 delete this._proxy;
340 delete this._nameWatcher;
305341 }
306342
307343 open() {
309345 // nor can we call any X11 functions. Luckily, the Activate method usually works fine.
310346 // parameters are "an hint to the item where to show eventual windows" [sic]
311347 // ... and don't seem to have any effect.
312 this._proxy.ActivateRemote(0, 0)
348 this._proxy.ActivateRemote(0, 0);
313349 }
314350
315351 secondaryActivate() {
316 this._proxy.SecondaryActivateRemote(0, 0)
352 this._proxy.SecondaryActivateRemote(0, 0);
317353 }
318354
319355 scroll(dx, dy) {
320 if (dx != 0)
321 this._proxy.ScrollRemote(Math.floor(dx), 'horizontal')
322
323 if (dy != 0)
324 this._proxy.ScrollRemote(Math.floor(dy), 'vertical')
356 if (dx !== 0)
357 this._proxy.ScrollRemote(Math.floor(dx), 'horizontal');
358
359 if (dy !== 0)
360 this._proxy.ScrollRemote(Math.floor(dy), 'vertical');
325361 }
326362 };
327363 Signals.addSignalMethods(AppIndicator.prototype);
328364
329365 var IconActor = GObject.registerClass(
330 class AppIndicators_IconActor extends St.Icon {
331
332 _init(indicator, icon_size) {
366 class AppIndicatorsIconActor extends St.Icon {
367
368 _init(indicator, iconSize) {
333369 super._init({
334370 reactive: true,
335371 style_class: 'system-status-icon',
340376 this.add_style_class_name('appindicator-icon');
341377 this.set_style('padding:0');
342378
379 // eslint-disable-next-line no-undef
343380 let themeContext = St.ThemeContext.get_for_stage(global.stage);
344 this.height = icon_size * themeContext.scale_factor;
345
346 this._indicator = indicator
347 this._iconSize = icon_size
348 this._iconCache = new IconCache.IconCache()
381 this.height = iconSize * themeContext.scale_factor;
382
383 this._indicator = indicator;
384 this._iconSize = iconSize;
385 this._iconCache = new IconCache.IconCache();
349386 this._cancellable = new Gio.Cancellable();
350387 this._loadingIcons = new Set();
351388
352 Util.connectSmart(this._indicator, 'icon', this, '_updateIcon')
353 Util.connectSmart(this._indicator, 'overlay-icon', this, '_updateOverlayIcon')
354 Util.connectSmart(this._indicator, 'reset', this, '_invalidateIcon')
355 Util.connectSmart(this, 'scroll-event', this, '_handleScrollEvent')
356
357 Util.connectSmart(themeContext, 'notify::scale-factor', this, (tc) => {
358 this.height = icon_size * tc.scale_factor;
389 Util.connectSmart(this._indicator, 'icon', this, this._updateIcon);
390 Util.connectSmart(this._indicator, 'overlay-icon', this, this._updateOverlayIcon);
391 Util.connectSmart(this._indicator, 'reset', this, this._invalidateIcon);
392
393 Util.connectSmart(themeContext, 'notify::scale-factor', this, tc => {
394 this.height = iconSize * tc.scale_factor;
359395 this._invalidateIcon();
360396 });
361397
362398 Util.connectSmart(this._indicator, 'ready', this, () => {
363399 this._updateIconClass();
364400 this._invalidateIcon();
365 })
366
367 Util.connectSmart(Gtk.IconTheme.get_default(), 'changed', this, '_invalidateIcon')
401 });
402
403 Util.connectSmart(Gtk.IconTheme.get_default(), 'changed', this, this._invalidateIcon);
368404
369405 if (indicator.isReady)
370 this._invalidateIcon()
406 this._invalidateIcon();
371407
372408 this.connect('destroy', () => {
373409 this._iconCache.destroy();
374410 this._cancellable.cancel();
375
376 if (this._callbackIdle) {
377 GLib.source_remove(this._callbackIdle);
378 delete this._callbackIdle;
379 }
380411 });
381412 }
382413
385416 `appindicator-icon-${this._indicator.id.toLowerCase().replace(/_|\s/g, '-')}`);
386417 }
387418
388 // Will look the icon up in the cache, if it's found
389 // it will return it. Otherwise, it will create it and cache it.
390 _cacheOrCreateIconByName(iconSize, iconName, themePath, callback) {
391 let {scale_factor} = St.ThemeContext.get_for_stage(global.stage);
392 let id = `${iconName}@${iconSize * scale_factor}${themePath || ''}`;
393 let gicon = this._iconCache.get(id);
394
395 if (gicon) {
396 callback(gicon);
397 return;
398 }
399
400 if (this._loadingIcons.has(id)) {
401 Util.Logger.debug(`${this._indicator.id}, Icon ${id} Is still loading, ignoring the request`);
402 return;
403 } else if (this._loadingIcons.size > 0) {
419 _cancelLoading() {
420 if (this._loadingIcons.size > 0) {
404421 this._cancellable.cancel();
405422 this._cancellable = new Gio.Cancellable();
406423 this._loadingIcons.clear();
407424 }
425 }
426
427 // Will look the icon up in the cache, if it's found
428 // it will return it. Otherwise, it will create it and cache it.
429 async _cacheOrCreateIconByName(iconSize, iconName, themePath) {
430 // eslint-disable-next-line no-undef
431 let { scale_factor: scaleFactor } = St.ThemeContext.get_for_stage(global.stage);
432 let id = `${iconName}@${iconSize * scaleFactor}${themePath || ''}`;
433 let gicon = this._iconCache.get(id);
434
435 if (gicon)
436 return gicon;
437
438 if (this._loadingIcons.has(id)) {
439 Util.Logger.debug(`${this._indicator.id}, Icon ${id} Is still loading, ignoring the request`);
440 throw new GLib.Error(Gio.IOErrorEnum, Gio.IOErrorEnum.PENDING,
441 'Already in progress');
442 } else {
443 this._cancelLoading();
444 }
408445
409446 this._loadingIcons.add(id);
410 let path = this._getIconInfo(iconName, themePath, iconSize, scale_factor);
411 this._createIconByName(path, (gicon) => {
412 this._loadingIcons.delete(id);
413 if (gicon)
414 gicon = this._iconCache.add(id, gicon);
415 callback(gicon);
416 });
417 }
418
419 _createIconByPath(path, width, height, callback) {
447 let path = this._getIconInfo(iconName, themePath, iconSize, scaleFactor);
448 gicon = await this._createIconByName(path);
449 this._loadingIcons.delete(id);
450 if (gicon)
451 gicon = this._iconCache.add(id, gicon);
452 return gicon;
453 }
454
455 async _createIconByPath(path, width, height) {
420456 let file = Gio.File.new_for_path(path);
421 file.read_async(GLib.PRIORITY_DEFAULT, this._cancellable, (file, res) => {
457 try {
458 const inputStream = await file.read_async(GLib.PRIORITY_DEFAULT, this._cancellable);
459 const pixbuf = GdkPixbuf.Pixbuf.new_from_stream_at_scale_async(inputStream,
460 height, width, true, this._cancellable);
461 this.icon_size = width > 0 ? width : this._iconSize;
462 return pixbuf;
463 } catch (e) {
464 if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
465 Util.Logger.warn(`${this._indicator.id}, Impossible to read image from path '${path}': ${e}`);
466 throw e;
467 }
468 }
469
470 async _createIconByName(path) {
471 if (!path) {
472 if (this._createIconIdle) {
473 throw new GLib.Error(Gio.IOErrorEnum, Gio.IOErrorEnum.PENDING,
474 'Already in progress');
475 }
476
422477 try {
423 let inputStream = file.read_finish(res);
424
425 GdkPixbuf.Pixbuf.new_from_stream_at_scale_async(
426 inputStream, height, width, true, this._cancellable, (_p, res) => {
427 try {
428 callback(GdkPixbuf.Pixbuf.new_from_stream_finish(res));
429 this.icon_size = width > 0 ? width : this._iconSize;
430 } catch (e) {
431 if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
432 Util.Logger.warn(`${this._indicator.id}, Impossible to create image from path '${path}': ${e}`);
433 callback(null);
434 }
435 }
436 });
478 this._createIconIdle = new PromiseUtils.IdlePromise(GLib.PRIORITY_DEFAULT_IDLE,
479 this._cancellable);
480 await this._createIconIdle;
437481 } catch (e) {
438 if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
439 Util.Logger.warn(`${this._indicator.id}, Impossible to read image from path '${path}': ${e}`);
440 callback(null);
441 }
442 }
443 });
444 }
445
446 _createIconByName(path, callback) {
447 if (!path) {
448 if (this._callbackIdle)
449 return;
450
451 this._callbackIdle = GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => {
452 delete this._callbackIdle;
453 callback(null);
454 return false;
455 });
456 return;
457 } else if (this._callbackIdle) {
458 GLib.source_remove(this._callbackIdle);
459 delete this._callbackIdle;
460 }
461
462 GdkPixbuf.Pixbuf.get_file_info_async(path, this._cancellable, (_p, res) => {
463 try {
464 let [format, width, height] = GdkPixbuf.Pixbuf.get_file_info_finish(res);
465
466 if (!format) {
467 Util.Logger.critical(`${this._indicator.id}, Invalid image format: ${path}`);
468 callback(null);
469 return;
470 }
471
472 if (width >= height * 1.5) {
473 /* Hello indicator-multiload! */
474 this._createIconByPath(path, width, -1, callback);
475 } else {
476 callback(new Gio.FileIcon({
477 file: Gio.File.new_for_path(path)
478 }));
479 this.icon_size = this._iconSize;
480 }
481 } catch (e) {
482 if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
483 Util.Logger.warn(`${this._indicator.id}, Impossible to read image info from path '${path}': ${e}`);
484 callback(null);
485 }
486 }
487 });
482 if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
483 logError(e);
484 throw e;
485 } finally {
486 delete this._createIconIdle;
487 }
488 return null;
489 } else if (this._createIconIdle) {
490 this._createIconIdle.cancel();
491 delete this._createIconIdle;
492 }
493
494 try {
495 const [format, width, height] = await GdkPixbuf.Pixbuf.get_file_info_async(
496 path, this._cancellable);
497
498 if (!format) {
499 Util.Logger.critical(`${this._indicator.id}, Invalid image format: ${path}`);
500 return null;
501 }
502
503 if (width >= height * 1.5) {
504 /* Hello indicator-multiload! */
505 return this._createIconByPath(path, width, -1);
506 } else {
507 this.icon_size = this._iconSize;
508 return new Gio.FileIcon({
509 file: Gio.File.new_for_path(path),
510 });
511 }
512 } catch (e) {
513 if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
514 Util.Logger.warn(`${this._indicator.id}, Impossible to read image info from path '${path}': ${e}`);
515 throw e;
516 }
488517 }
489518
490519 _getIconInfo(name, themePath, size, scale) {
491520 let path = null;
492 if (name && name[0] == "/") {
493 //HACK: icon is a path name. This is not specified by the api but at least inidcator-sensors uses it.
521 if (name && name[0] === '/') {
522 // HACK: icon is a path name. This is not specified by the api but at least inidcator-sensors uses it.
494523 path = name;
495524 } else if (name) {
496525 // we manually look up the icon instead of letting st.icon do it for us
497526 // this allows us to sneak in an indicator provided search path and to avoid ugly upscaled icons
498527
499528 // indicator-application looks up a special "panel" variant, we just replicate that here
500 name = name + "-panel";
529 name += '-panel';
501530
502531 // icon info as returned by the lookup
503532 let iconInfo = null;
504533
505534 // we try to avoid messing with the default icon theme, so we'll create a new one if needed
506 let icon_theme = null;
535 let iconTheme = null;
507536 if (themePath) {
508 icon_theme = new Gtk.IconTheme();
509 Gtk.IconTheme.get_default().get_search_path().forEach((path) => {
510 icon_theme.append_search_path(path);
511 });
512 icon_theme.append_search_path(themePath);
513 icon_theme.set_screen(imports.gi.Gdk.Screen.get_default());
537 iconTheme = new Gtk.IconTheme();
538 Gtk.IconTheme.get_default().get_search_path().forEach(p =>
539 iconTheme.append_search_path(p));
540 iconTheme.append_search_path(themePath);
541 iconTheme.set_screen(imports.gi.Gdk.Screen.get_default());
514542 } else {
515 icon_theme = Gtk.IconTheme.get_default();
516 }
517 if (icon_theme) {
543 iconTheme = Gtk.IconTheme.get_default();
544 }
545 if (iconTheme) {
518546 // try to look up the icon in the icon theme
519 iconInfo = icon_theme.lookup_icon_for_scale(name, size, scale,
547 iconInfo = iconTheme.lookup_icon_for_scale(name, size, scale,
520548 Gtk.IconLookupFlags.GENERIC_FALLBACK);
521549 // no icon? that's bad!
522550 if (iconInfo === null) {
531559 return path;
532560 }
533561
534 argbToRgba(src) {
535 let dest = new Uint8Array(src.length);
536
537 for (let i = 0; i < src.length; i += 4) {
538 let srcAlpha = src[i]
539
540 dest[i] = src[i + 1]; /* red */
541 dest[i + 1] = src[i + 2]; /* green */
542 dest[i + 2] = src[i + 3]; /* blue */
543 dest[i + 3] = srcAlpha; /* alpha */
544 }
545
562 async argbToRgba(src, cancellable) {
563 const CHUNK_SIZE = 1024;
564 const ops = [];
565 const dest = new Uint8Array(src.length);
566
567 for (let i = 0; i < src.length;) {
568 const chunkSize = Math.min(CHUNK_SIZE, src.length - i);
569
570 ops.push(new PromiseUtils.CancellablePromise(async resolve => {
571 const start = i;
572 const end = i + chunkSize;
573 await new PromiseUtils.IdlePromise(GLib.PRIORITY_LOW, cancellable);
574
575 for (let j = start; j < end; j += 4) {
576 let srcAlpha = src[j];
577
578 dest[j] = src[j + 1]; /* red */
579 dest[j + 1] = src[j + 2]; /* green */
580 dest[j + 2] = src[j + 3]; /* blue */
581 dest[j + 3] = srcAlpha; /* alpha */
582 }
583 resolve();
584 }, cancellable));
585
586 i += chunkSize;
587 }
588
589 await Promise.all(ops);
546590 return dest;
547591 }
548592
549 _createIconFromPixmap(iconSize, iconPixmapArray, snIconType) {
550 let {scale_factor} = St.ThemeContext.get_for_stage(global.stage);
551 iconSize = iconSize * scale_factor
593 async _createIconFromPixmap(iconSize, iconPixmapArray) {
594 // eslint-disable-next-line no-undef
595 const { scale_factor: scaleFactor } = St.ThemeContext.get_for_stage(global.stage);
596 iconSize *= scaleFactor;
552597 // the pixmap actually is an array of pixmaps with different sizes
553598 // we use the one that is smaller or equal the iconSize
554599
555600 // maybe it's empty? that's bad.
556601 if (!iconPixmapArray || iconPixmapArray.length < 1)
557 return null
558
559 let sortedIconPixmapArray = iconPixmapArray.sort((pixmapA, pixmapB) => {
560 // we sort smallest to biggest
561 let areaA = pixmapA[0] * pixmapA[1]
562 let areaB = pixmapB[0] * pixmapB[1]
563
564 return areaA - areaB
565 })
566
567 let qualifiedIconPixmapArray = sortedIconPixmapArray.filter((pixmap) => {
568 // we prefer any pixmap that is equal or bigger than our requested size
569 return pixmap[0] >= iconSize && pixmap[1] >= iconSize;
570 })
571
572 let iconPixmap = qualifiedIconPixmapArray.length > 0 ? qualifiedIconPixmapArray[0] : sortedIconPixmapArray.pop()
573
574 let [ width, height, bytes ] = iconPixmap
575 let rowstride = width * 4 // hopefully this is correct
576
577 try {
578 return GdkPixbuf.Pixbuf.new_from_bytes(
579 this.argbToRgba(bytes),
580 GdkPixbuf.Colorspace.RGB, true,
581 8, width, height, rowstride);
582 } catch (e) {
583 // the image data was probably bogus. We don't really know why, but it _does_ happen.
584 Util.Logger.warn(`${this._indicator.id}, Impossible to create image from data: ${e}`)
585 return null
586 }
602 throw TypeError('Empty Icon found');
603
604 const sortedIconPixmapArray = iconPixmapArray.sort((pixmapA, pixmapB) => {
605 // we sort smallest to biggest
606 const areaA = pixmapA[0] * pixmapA[1];
607 const areaB = pixmapB[0] * pixmapB[1];
608
609 return areaA - areaB;
610 });
611
612 const qualifiedIconPixmapArray = sortedIconPixmapArray.filter(pixmap =>
613 // we prefer any pixmap that is equal or bigger than our requested size
614 pixmap[0] >= iconSize && pixmap[1] >= iconSize);
615
616 const iconPixmap = qualifiedIconPixmapArray.length > 0
617 ? qualifiedIconPixmapArray[0] : sortedIconPixmapArray.pop();
618
619 const [width, height, bytes] = iconPixmap;
620 const rowStride = width * 4; // hopefully this is correct
621
622 const id = `__PIXMAP_ICON_${width}x${height}`;
623 if (this._loadingIcons.has(id)) {
624 Util.Logger.debug(`${this._indicator.id}, Pixmap ${width}x${height} ` +
625 'Is still loading, ignoring the request');
626 throw new GLib.Error(Gio.IOErrorEnum, Gio.IOErrorEnum.PENDING,
627 'Already in progress');
628 } else {
629 this._cancelLoading();
630 }
631
632 this._loadingIcons.add(id);
633
634 try {
635 return GdkPixbuf.Pixbuf.new_from_bytes(
636 await this.argbToRgba(bytes, this._cancellable),
637 GdkPixbuf.Colorspace.RGB, true,
638 8, width, height, rowStride);
639 } catch (e) {
640 // the image data was probably bogus. We don't really know why, but it _does_ happen.
641 if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
642 Util.Logger.warn(`${this._indicator.id}, Impossible to create image from data: ${e}`);
643 throw e;
644 } finally {
645 this._loadingIcons.delete(id);
646 }
587647 }
588648
589649 // The .inUse flag will be set to true if the used gicon matches the cached
591651 // So when it's not need anymore we make sure to check the .inUse property
592652 // and set it to false so that it can be picked up by the garbage collector.
593653 _setGicon(iconType, gicon) {
594 if (iconType != SNIconType.OVERLAY) {
654 if (iconType !== SNIconType.OVERLAY) {
595655 if (gicon) {
596656 this.gicon = new Gio.EmblemedIcon({ gicon });
597657
598658 if (!(gicon instanceof GdkPixbuf.Pixbuf))
599 gicon.inUse = (this.gicon.get_icon() == gicon);
659 gicon.inUse = this.gicon.get_icon() === gicon;
600660 } else {
601661 this.gicon = null;
602662 Util.Logger.critical(`unable to update icon for ${this._indicator.id}`);
603663 }
664 } else if (gicon) {
665 this._emblem = new Gio.Emblem({ icon: gicon });
666
667 if (!(gicon instanceof GdkPixbuf.Pixbuf))
668 gicon.inUse = true;
604669 } else {
605 if (gicon) {
606 this._emblem = new Gio.Emblem({ icon: gicon });
607
608 if (!(gicon instanceof GdkPixbuf.Pixbuf))
609 gicon.inUse = true;
610 } else {
611 this._emblem = null;
612 Util.Logger.debug(`unable to update icon emblem for ${this._indicator.id}`);
613 }
670 this._emblem = null;
671 Util.Logger.debug(`unable to update icon emblem for ${this._indicator.id}`);
614672 }
615673
616674 if (this.gicon) {
622680 }
623681 }
624682
625 _updateIconByType(iconType, iconSize) {
683 async _updateIconByType(iconType, iconSize) {
626684 let icon;
627685 switch (iconType) {
628 case SNIconType.ATTENTION:
629 icon = this._indicator.attentionIcon;
630 break;
631 case SNIconType.NORMAL:
632 icon = this._indicator.icon;
633 break;
634 case SNIconType.OVERLAY:
635 icon = this._indicator.overlayIcon;
636 break;
637 }
638
639 let [name, pixmap, theme] = icon;
640 if (name && name.length) {
641 this._cacheOrCreateIconByName(iconSize, name, theme, (gicon) => {
642 if (!gicon && pixmap) {
643 gicon = this._createIconFromPixmap(iconSize,
644 pixmap, iconType);
645 }
646 this._setGicon(iconType, gicon);
647 });
648 } else if (pixmap) {
649 let gicon = this._createIconFromPixmap(iconSize,
650 pixmap, iconType);
686 case SNIconType.ATTENTION:
687 icon = this._indicator.attentionIcon;
688 break;
689 case SNIconType.NORMAL:
690 icon = this._indicator.icon;
691 break;
692 case SNIconType.OVERLAY:
693 icon = this._indicator.overlayIcon;
694 break;
695 }
696
697 const [name, pixmap, theme] = icon;
698 let gicon = null;
699 try {
700 if (name && name.length) {
701 gicon = await this._cacheOrCreateIconByName(iconSize, name, theme);
702 if (!gicon && pixmap)
703 gicon = await this._createIconFromPixmap(iconSize, pixmap, iconType);
704 } else if (pixmap) {
705 gicon = await this._createIconFromPixmap(iconSize, pixmap, iconType);
706 }
707
651708 this._setGicon(iconType, gicon);
709 } catch (e) {
710 /* We handle the error messages already */
711 if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED) &&
712 !e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.PENDING))
713 Util.Logger.debug(`${this._indicator.id}, Impossible to load icon: ${e}`);
652714 }
653715 }
654716
658720 let { gicon } = this.gicon;
659721
660722 if (gicon.inUse)
661 gicon.inUse = false
723 gicon.inUse = false;
662724 }
663725
664726 // we might need to use the AttentionIcon*, which have precedence over the normal icons
665 let iconType = this._indicator.status == SNIStatus.NEEDS_ATTENTION ?
666 SNIconType.ATTENTION : SNIconType.NORMAL;
727 let iconType = this._indicator.status === SNIStatus.NEEDS_ATTENTION
728 ? SNIconType.ATTENTION : SNIconType.NORMAL;
667729
668730 this._updateIconByType(iconType, this._iconSize);
669731 }
679741 // KDE hardcodes the overlay icon size to 10px (normal icon size 16px)
680742 // we approximate that ratio for other sizes, too.
681743 // our algorithms will always pick a smaller one instead of stretching it.
682 let iconSize = Math.floor(this._iconSize / 1.6)
744 let iconSize = Math.floor(this._iconSize / 1.6);
683745
684746 this._updateIconByType(SNIconType.OVERLAY, iconSize);
685 }
686
687 _handleScrollEvent(actor, event) {
688 if (actor != this)
689 return Clutter.EVENT_PROPAGATE
690
691 if (event.get_source() != this)
692 return Clutter.EVENT_PROPAGATE
693
694 if (event.type() != Clutter.EventType.SCROLL)
695 return Clutter.EVENT_PROPAGATE
696
697 // Since Clutter 1.10, clutter will always send a smooth scrolling event
698 // with explicit deltas, no matter what input device is used
699 // In fact, for every scroll there will be a smooth and non-smooth scroll
700 // event, and we can choose which one we interpret.
701 if (event.get_scroll_direction() == Clutter.ScrollDirection.SMOOTH) {
702 let [ dx, dy ] = event.get_scroll_delta()
703
704 this._indicator.scroll(dx, dy)
705 }
706
707 return Clutter.EVENT_STOP
708747 }
709748
710749 // called when the icon theme changes
711750 _invalidateIcon() {
712 this._iconCache.clear()
713
714 this._updateIcon()
715 this._updateOverlayIcon()
751 this._iconCache.clear();
752 this._cancelLoading();
753
754 this._updateIcon();
755 this._updateOverlayIcon();
716756 }
717757 });
1212 // You should have received a copy of the GNU General Public License
1313 // along with this program; if not, write to the Free Software
1414 // 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
27 const Util = Extension.imports.util
28
29 //////////////////////////////////////////////////////////////////////////
15 const Gio = imports.gi.Gio;
16 const GLib = imports.gi.GLib;
17 const GdkPixbuf = imports.gi.GdkPixbuf;
18 const PopupMenu = imports.ui.popupMenu;
19 const Signals = imports.signals;
20 const St = imports.gi.St;
21
22 const Extension = imports.misc.extensionUtils.getCurrentExtension();
23
24 const DBusInterfaces = Extension.imports.interfaces;
25 const PromiseUtils = Extension.imports.promiseUtils;
26 const Util = Extension.imports.util;
27
28 // ////////////////////////////////////////////////////////////////////////
3029 // PART ONE: "ViewModel" backend implementation.
3130 // Both code and design are inspired by libdbusmenu
32 //////////////////////////////////////////////////////////////////////////
31 // ////////////////////////////////////////////////////////////////////////
3332
3433 /**
3534 * Saves menu property values and handles type checking and defaults
3635 */
37 var PropertyStore = class AppIndicators_PropertyStore {
38
39 constructor(initial_properties) {
36 var PropertyStore = class AppIndicatorsPropertyStore {
37
38 constructor(initialProperties) {
4039 this._props = new Map();
4140
42 if (initial_properties) {
43 for (let i in initial_properties) {
44 this.set(i, initial_properties[i])
45 }
41 if (initialProperties) {
42 for (let i in initialProperties)
43 this.set(i, initialProperties[i]);
44
4645 }
4746 }
4847
4948 set(name, value) {
5049 if (name in PropertyStore.MandatedTypes && value && !value.is_of_type(PropertyStore.MandatedTypes[name]))
51 Util.Logger.warn("Cannot set property "+name+": type mismatch!")
50 Util.Logger.warn(`Cannot set property ${name}: type mismatch!`);
5251 else if (value)
5352 this._props.set(name, value);
5453 else
6059 if (prop)
6160 return prop;
6261 else if (name in PropertyStore.DefaultValues)
63 return PropertyStore.DefaultValues[name]
62 return PropertyStore.DefaultValues[name];
6463 else
65 return null
64 return null;
6665 }
6766 };
6867
6968 // we list all the properties we know and use here, so we won' have to deal with unexpected type mismatches
7069 PropertyStore.MandatedTypes = {
71 'visible' : GLib.VariantType.new("b"),
72 'enabled' : GLib.VariantType.new("b"),
73 'label' : GLib.VariantType.new("s"),
74 'type' : GLib.VariantType.new("s"),
75 'children-display' : GLib.VariantType.new("s"),
76 'icon-name' : GLib.VariantType.new("s"),
77 'icon-data' : GLib.VariantType.new("ay"),
78 'toggle-type' : GLib.VariantType.new("s"),
79 'toggle-state' : GLib.VariantType.new("i")
80 }
70 'visible': GLib.VariantType.new('b'),
71 'enabled': GLib.VariantType.new('b'),
72 'label': GLib.VariantType.new('s'),
73 'type': GLib.VariantType.new('s'),
74 'children-display': GLib.VariantType.new('s'),
75 'icon-name': GLib.VariantType.new('s'),
76 'icon-data': GLib.VariantType.new('ay'),
77 'toggle-type': GLib.VariantType.new('s'),
78 'toggle-state': GLib.VariantType.new('i'),
79 };
8180
8281 PropertyStore.DefaultValues = {
8382 'visible': GLib.Variant.new_boolean(true),
8483 'enabled': GLib.Variant.new_boolean(true),
85 'label' : GLib.Variant.new_string(''),
86 'type' : GLib.Variant.new_string("standard")
84 'label': GLib.Variant.new_string(''),
85 'type': GLib.Variant.new_string('standard'),
8786 // elements not in here must return null
88 }
87 };
8988
9089 /**
9190 * Represents a single menu item
9291 */
93 var DbusMenuItem = class AppIndicators_DbusMenuItem {
92 var DbusMenuItem = class AppIndicatorsDbusMenuItem {
9493
9594 // will steal the properties object
96 constructor(client, id, properties, children_ids) {
97 this._client = client
98 this._id = id
99 this._propStore = new PropertyStore(properties)
100 this._children_ids = children_ids
101 }
102
103 property_get(prop_name) {
104 let prop = this.property_get_variant(prop_name)
105 return prop ? prop.get_string()[0] : null
106 }
107
108 property_get_variant(prop_name) {
109 return this._propStore.get(prop_name)
110 }
111
112 property_get_bool(prop_name) {
113 let prop = this.property_get_variant(prop_name)
114 return prop ? prop.get_boolean() : false
115 }
116
117 property_get_int(prop_name) {
118 let prop = this.property_get_variant(prop_name)
119 return prop ? prop.get_int32() : 0
120 }
121
122 property_set(prop, value) {
123 this._propStore.set(prop, value)
124
125 this.emit('property-changed', prop, this.property_get_variant(prop))
126 }
127
128 get_children_ids() {
129 return this._children_ids.concat() // clone it!
130 }
131
132 add_child(pos, child_id) {
133 this._children_ids.splice(pos, 0, child_id)
134 this.emit('child-added', this._client.get_item(child_id), pos)
135 }
136
137 remove_child(child_id) {
95 constructor(client, id, properties, childrenIds) {
96 this._client = client;
97 this._id = id;
98 this._propStore = new PropertyStore(properties);
99 this._children_ids = childrenIds;
100 }
101
102 propertyGet(propName) {
103 let prop = this.propertyGetVariant(propName);
104 return prop ? prop.get_string()[0] : null;
105 }
106
107 propertyGetVariant(propName) {
108 return this._propStore.get(propName);
109 }
110
111 propertyGetBool(propName) {
112 let prop = this.propertyGetVariant(propName);
113 return prop ? prop.get_boolean() : false;
114 }
115
116 propertyGetInt(propName) {
117 let prop = this.propertyGetVariant(propName);
118 return prop ? prop.get_int32() : 0;
119 }
120
121 propertySet(prop, value) {
122 this._propStore.set(prop, value);
123
124 this.emit('property-changed', prop, this.propertyGetVariant(prop));
125 }
126
127 getChildrenIds() {
128 return this._children_ids.concat(); // clone it!
129 }
130
131 addChild(pos, childId) {
132 this._children_ids.splice(pos, 0, childId);
133 this.emit('child-added', this._client.getItem(childId), pos);
134 }
135
136 removeChild(childId) {
138137 // find it
139 let pos = -1
138 let pos = -1;
140139 for (let i = 0; i < this._children_ids.length; ++i) {
141 if (this._children_ids[i] == child_id) {
142 pos = i
143 break
140 if (this._children_ids[i] === childId) {
141 pos = i;
142 break;
144143 }
145144 }
146145
147146 if (pos < 0) {
148 Util.Logger.critical("Trying to remove child which doesn't exist")
147 Util.Logger.critical("Trying to remove child which doesn't exist");
149148 } else {
150 this._children_ids.splice(pos, 1)
151 this.emit('child-removed', this._client.get_item(child_id))
152 }
153 }
154
155 move_child(child_id, newpos) {
149 this._children_ids.splice(pos, 1);
150 this.emit('child-removed', this._client.getItem(childId));
151 }
152 }
153
154 moveChild(childId, newPos) {
156155 // find the old position
157 let oldpos = -1
156 let oldPos = -1;
158157 for (let i = 0; i < this._children_ids.length; ++i) {
159 if (this._children_ids[i] == child_id) {
160 oldpos = i
161 break
158 if (this._children_ids[i] === childId) {
159 oldPos = i;
160 break;
162161 }
163162 }
164163
165 if (oldpos < 0) {
166 Util.Logger.critical("tried to move child which wasn't in the list")
167 return
168 }
169
170 if (oldpos != newpos) {
171 this._children_ids.splice(oldpos, 1)
172 this._children_ids.splice(newpos, 0, child_id)
173 this.emit('child-moved', oldpos, newpos, this._client.get_item(child_id))
174 }
175 }
176
177 get_children() {
178 return this._children_ids.map(el => this._client.get_item(el));
179 }
180
181 handle_event(event, data, timestamp) {
164 if (oldPos < 0) {
165 Util.Logger.critical("tried to move child which wasn't in the list");
166 return;
167 }
168
169 if (oldPos !== newPos) {
170 this._children_ids.splice(oldPos, 1);
171 this._children_ids.splice(newPos, 0, childId);
172 this.emit('child-moved', oldPos, newPos, this._client.getItem(childId));
173 }
174 }
175
176 getChildren() {
177 return this._children_ids.map(el => this._client.getItem(el));
178 }
179
180 handleEvent(event, data, timestamp) {
182181 if (!data)
183 data = GLib.Variant.new_int32(0)
184
185 this._client.send_event(this._id, event, data, timestamp)
186 }
187
188 get_id() {
189 return this._id
190 }
191
192 send_about_to_show() {
193 this._client.send_about_to_show(this._id)
194 }
195 }
196 Signals.addSignalMethods(DbusMenuItem.prototype)
182 data = GLib.Variant.new_int32(0);
183
184 this._client.sendEvent(this._id, event, data, timestamp);
185 }
186
187 getId() {
188 return this._id;
189 }
190
191 sendAboutToShow() {
192 this._client.sendAboutToShow(this._id);
193 }
194 };
195 Signals.addSignalMethods(DbusMenuItem.prototype);
197196
198197
199198 const BusClientProxy = Gio.DBusProxy.makeProxyWrapper(DBusInterfaces.DBusMenu);
201200 /**
202201 * The client does the heavy lifting of actually reading layouts and distributing events
203202 */
204 var DBusClient = class AppIndicators_DBusClient {
203 var DBusClient = class AppIndicatorsDBusClient {
205204
206205 constructor(busName, busPath) {
207206 this._cancellable = new Gio.Cancellable();
209208 busName,
210209 busPath,
211210 this._clientReady.bind(this),
212 this._cancellable)
211 this._cancellable);
213212 this._items = new Map([
214213 [
215214 0,
216215 new DbusMenuItem(this, 0, {
217216 'children-display': GLib.Variant.new_string('submenu'),
218217 }, []),
219 ]
218 ],
220219 ]);
221220
222221 // will be set to true if a layout update is requested while one is already in progress
223222 // then the handler that completes the layout update will request another update
224 this._flagLayoutUpdateRequired = false
225 this._flagLayoutUpdateInProgress = false
223 this._flagLayoutUpdateRequired = false;
224 this._flagLayoutUpdateInProgress = false;
226225
227226 // property requests are queued
228227 this._propertiesRequestedFor = new Set(/* ids */);
237236 return !!this._proxy.g_name_owner;
238237 }
239238
240 get_root() {
239 getRoot() {
241240 return this._items.get(0);
242241 }
243242
244243 _requestLayoutUpdate() {
245244 if (this._flagLayoutUpdateInProgress)
246 this._flagLayoutUpdateRequired = true
245 this._flagLayoutUpdateRequired = true;
247246 else
248 this._beginLayoutUpdate()
249 }
250
251 _requestProperties(id) {
247 this._beginLayoutUpdate();
248 }
249
250 async _requestProperties(id) {
251 this._propertiesRequestedFor.add(id);
252
252253 // if we don't have any requests queued, we'll need to add one
253 if (!this._propertiesRequestId) {
254 this._propertiesRequestId = GLib.idle_add(
255 GLib.PRIORITY_DEFAULT_IDLE, () => this._beginRequestProperties())
256 }
257
258 this._propertiesRequestedFor.add(id);
254 if (!this._propertiesRequest || !this._propertiesRequest.pending()) {
255 this._propertiesRequest = new PromiseUtils.IdlePromise(
256 GLib.PRIORITY_DEFAULT_IDLE, this._cancellable);
257 await this._propertiesRequest;
258 this._beginRequestProperties();
259 }
259260 }
260261
261262 _beginRequestProperties() {
262263 this._proxy.GetGroupPropertiesRemote(
263 Array.from(this._propertiesRequestedFor),
264 [],
265 this._cancellable,
266 this._endRequestProperties.bind(this))
264 Array.from(this._propertiesRequestedFor),
265 [],
266 this._cancellable,
267 this._endRequestProperties.bind(this));
267268
268269 this._propertiesRequestedFor.clear();
269 delete this._propertiesRequestId;
270
271 return false
270 return false;
272271 }
273272
274273 _endRequestProperties(result, error) {
275274 if (error) {
276275 if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
277276 Util.Logger.warn(`Could not retrieve properties: ${error}`);
278 return
277 return;
279278 }
280279
281280 // for some funny reason, the result array is hidden in an array
282281 result[0].forEach(([id, properties]) => {
283282 let item = this._items.get(id);
284283 if (!item)
285 return
284 return;
286285
287286 for (let prop in properties)
288 item.property_set(prop, properties[prop])
287 item.propertySet(prop, properties[prop]);
289288 });
290289 }
291290
292291 // Traverses the list of cached menu items and removes everyone that is not in the list
293292 // so we don't keep alive unused items
294293 _gcItems() {
295 let tag = new Date().getTime()
296
297 let toTraverse = [ 0 ]
294 let tag = new Date().getTime();
295
296 let toTraverse = [0];
298297 while (toTraverse.length > 0) {
299 let item = this.get_item(toTraverse.shift())
300 item._dbusClientGcTag = tag
301 Array.prototype.push.apply(toTraverse, item.get_children_ids())
298 let item = this.getItem(toTraverse.shift());
299 item._dbusClientGcTag = tag;
300 Array.prototype.push.apply(toTraverse, item.getChildrenIds());
302301 }
303302
304303 this._items.forEach((i, id) => {
305 if (i._dbusClientGcTag != tag)
304 if (i._dbusClientGcTag !== tag)
306305 this._items.delete(id);
307306 });
308307 }
313312 // we only read the type property, because if the type changes after reading all properties,
314313 // the view would have to replace the item completely which we try to avoid
315314 this._proxy.GetLayoutRemote(0, -1,
316 [ 'type', 'children-display' ],
315 ['type', 'children-display'],
317316 this._cancellable,
318 this._endLayoutUpdate.bind(this))
319
320 this._flagLayoutUpdateRequired = false
321 this._flagLayoutUpdateInProgress = true
317 this._endLayoutUpdate.bind(this));
318
319 this._flagLayoutUpdateRequired = false;
320 this._flagLayoutUpdateInProgress = true;
322321 }
323322
324323 _endLayoutUpdate(result, error) {
325324 if (error) {
326325 if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
327326 Util.Logger.warn(`While reading menu layout on proxy ${this._proxy.g_name_owner}: ${error}`);
328 return
329 }
330
331 let [ revision, root ] = result
332 this._doLayoutUpdate(root)
333 this._gcItems()
327 return;
328 }
329
330 let [revision_, root] = result;
331 this._doLayoutUpdate(root);
332 this._gcItems();
334333
335334 if (this._flagLayoutUpdateRequired)
336 this._beginLayoutUpdate()
335 this._beginLayoutUpdate();
337336 else
338 this._flagLayoutUpdateInProgress = false
337 this._flagLayoutUpdateInProgress = false;
339338 }
340339
341340 _doLayoutUpdate(item) {
342 let [ id, properties, children ] = item
343
344 let childrenUnpacked = children.map(c => c.deep_unpack())
345 let childrenIds = childrenUnpacked.map(c => c[0])
341 let [id, properties, children] = item;
342
343 let childrenUnpacked = children.map(c => c.deep_unpack());
344 let childrenIds = childrenUnpacked.map(c => c[0]);
346345
347346 // make sure all our children exist
348347 childrenUnpacked.forEach(c => this._doLayoutUpdate(c));
351350 const menuItem = this._items.get(id);
352351 if (menuItem) {
353352 // we do, update our properties if necessary
354 for (let prop in properties) {
355 menuItem.property_set(prop, properties[prop])
356 }
353 for (let prop in properties)
354 menuItem.propertySet(prop, properties[prop]);
355
357356
358357 // make sure our children are all at the right place, and exist
359 let oldChildrenIds = menuItem.get_children_ids()
358 let oldChildrenIds = menuItem.getChildrenIds();
360359 for (let i = 0; i < childrenIds.length; ++i) {
361360 // try to recycle an old child
362 let oldChild = -1
361 let oldChild = -1;
363362 for (let j = 0; j < oldChildrenIds.length; ++j) {
364 if (oldChildrenIds[j] == childrenIds[i]) {
365 oldChild = oldChildrenIds.splice(j, 1)[0]
366 break
363 if (oldChildrenIds[j] === childrenIds[i]) {
364 oldChild = oldChildrenIds.splice(j, 1)[0];
365 break;
367366 }
368367 }
369368
370369 if (oldChild < 0) {
371370 // no old child found, so create a new one!
372 menuItem.add_child(i, childrenIds[i])
371 menuItem.addChild(i, childrenIds[i]);
373372 } else {
374373 // old child found, reuse it!
375 menuItem.move_child(childrenIds[i], i)
374 menuItem.moveChild(childrenIds[i], i);
376375 }
377376 }
378377
379378 // remove any old children that weren't reused
380 oldChildrenIds.forEach(child_id => menuItem.remove_child(child_id));
379 oldChildrenIds.forEach(c => menuItem.removeChild(c));
381380 } else {
382381 // we don't, so let's create us
383382 this._items.set(id, new DbusMenuItem(this, id, properties, childrenIds));
384 this._requestProperties(id)
385 }
386
387 return id
383 this._requestProperties(id);
384 }
385
386 return id;
388387 }
389388
390389 _clientReady(result, error) {
394393 return;
395394 }
396395
397 this._requestLayoutUpdate()
396 this._requestLayoutUpdate();
398397
399398 // listen for updated layouts and properties
400 this._proxy.connectSignal("LayoutUpdated", this._onLayoutUpdated.bind(this))
401 this._proxy.connectSignal("ItemsPropertiesUpdated", this._onPropertiesUpdated.bind(this))
402 }
403
404 get_item(id) {
399 this._proxy.connectSignal('LayoutUpdated', this._onLayoutUpdated.bind(this));
400 this._proxy.connectSignal('ItemsPropertiesUpdated', this._onPropertiesUpdated.bind(this));
401 }
402
403 getItem(id) {
405404 let item = this._items.get(id);
406405 if (!item)
407406 Util.Logger.warn(`trying to retrieve item for non-existing id ${id} !?`);
409408 }
410409
411410 // we don't need to cache and burst-send that since it will not happen that frequently
412 send_about_to_show(id) {
411 sendAboutToShow(id) {
413412 /* Some indicators (you, dropbox!) don't use the right signature
414413 * and don't return a boolean, so we need to support both cases */
415414 let connection = this._proxy.get_connection();
416415 connection.call(this._proxy.get_name(), this._proxy.get_object_path(),
417 this._proxy.get_interface_name(), 'AboutToShow',
418 new GLib.Variant("(i)", [id]), null,
419 Gio.DBusCallFlags.NONE, -1, null, (proxy, res) => {
420 try {
421 let ret = proxy.call_finish(res);
422 if ((ret.is_of_type(new GLib.VariantType('(b)')) &&
416 this._proxy.get_interface_name(), 'AboutToShow',
417 new GLib.Variant('(i)', [id]), null,
418 Gio.DBusCallFlags.NONE, -1, null, (proxy, res) => {
419 try {
420 let ret = proxy.call_finish(res);
421 if ((ret.is_of_type(new GLib.VariantType('(b)')) &&
423422 ret.get_child_value(0).get_boolean()) ||
424 ret.is_of_type(new GLib.VariantType('()'))) {
425 this._requestLayoutUpdate();
423 ret.is_of_type(new GLib.VariantType('()')))
424 this._requestLayoutUpdate();
425
426 } catch (e) {
427 Util.Logger.warn(`Impossible to send about-to-show to menu: ${e}`);
426428 }
427 } catch (e) {
428 Util.Logger.warn("Impossible to send about-to-show to menu: " + e);
429 }
430 });
431 }
432
433 send_event(id, event, params, timestamp) {
429 });
430 }
431
432 sendEvent(id, event, params, timestamp) {
434433 if (!this._proxy)
435 return
434 return;
436435
437436 this._proxy.EventRemote(id, event, params, timestamp, this._cancellable,
438 () => { /* we don't care */ })
437 () => { /* we don't care */ });
439438 }
440439
441440 _onLayoutUpdated() {
442 this._requestLayoutUpdate()
441 this._requestLayoutUpdate();
443442 }
444443
445444 _onPropertiesUpdated(proxy, name, [changed, removed]) {
446445 changed.forEach(([id, props]) => {
447446 let item = this._items.get(id);
448447 if (!item)
449 return
448 return;
450449
451450 for (let prop in props)
452 item.property_set(prop, props[prop])
451 item.propertySet(prop, props[prop]);
453452 });
454453 removed.forEach(([id, propNames]) => {
455454 let item = this._items.get(id);
456455 if (!item)
457 return
458
459 propNames.forEach(propName => item.property_set(propName, null));
456 return;
457
458 propNames.forEach(propName => item.propertySet(propName, null));
460459 });
461460 }
462461
463462 destroy() {
464 this.emit('destroy')
465
466 if (this._propertiesRequestId) {
467 GLib.Source.remove(this._propertiesRequestId);
468 delete this._propertiesRequestId;
469 }
463 this.emit('destroy');
470464
471465 this._cancellable.cancel();
472 Signals._disconnectAll.apply(this._proxy)
473
474 this._proxy = null
475 }
476 }
477 Signals.addSignalMethods(DBusClient.prototype)
478
479 //////////////////////////////////////////////////////////////////////////
466 Signals._disconnectAll.apply(this._proxy);
467
468 this._proxy = null;
469 }
470 };
471 Signals.addSignalMethods(DBusClient.prototype);
472
473 // ////////////////////////////////////////////////////////////////////////
480474 // PART TWO: "View" frontend implementation.
481 //////////////////////////////////////////////////////////////////////////
475 // ////////////////////////////////////////////////////////////////////////
482476
483477 // https://bugzilla.gnome.org/show_bug.cgi?id=731514
484478 // GNOME 3.10 and 3.12 can't open a nested submenu.
485479 // Patches have been written, but it's not clear when (if?) they will be applied.
486480 // We also don't know whether they will be backported to 3.10, so we will work around
487481 // it in the meantime. Offending versions can be clearly identified:
488 const NEED_NESTED_SUBMENU_FIX = '_setOpenedSubMenu' in PopupMenu.PopupMenu.prototype
482 const NEED_NESTED_SUBMENU_FIX = '_setOpenedSubMenu' in PopupMenu.PopupMenu.prototype;
489483
490484 /**
491485 * Creates new wrapper menu items and injects methods for managing them at runtime.
494488 * handlers, so any `this` will refer to a menu item create in createItem
495489 */
496490 const MenuItemFactory = {
497 createItem: function(client, dbusItem) {
491 createItem(client, dbusItem) {
498492 // first, decide whether it's a submenu or not
499 if (dbusItem.property_get("children-display") == "submenu")
500 var shellItem = new PopupMenu.PopupSubMenuMenuItem("FIXME")
501 else if (dbusItem.property_get("type") == "separator")
502 var shellItem = new PopupMenu.PopupSeparatorMenuItem('')
493 let shellItem;
494 if (dbusItem.propertyGet('children-display') === 'submenu')
495 shellItem = new PopupMenu.PopupSubMenuMenuItem('FIXME');
496 else if (dbusItem.propertyGet('type') === 'separator')
497 shellItem = new PopupMenu.PopupSeparatorMenuItem('');
503498 else
504 var shellItem = new PopupMenu.PopupMenuItem("FIXME")
505
506 shellItem._dbusItem = dbusItem
507 shellItem._dbusClient = client
499 shellItem = new PopupMenu.PopupMenuItem('FIXME');
500
501 shellItem._dbusItem = dbusItem;
502 shellItem._dbusClient = client;
508503
509504 if (shellItem instanceof PopupMenu.PopupMenuItem) {
510 shellItem._icon = new St.Icon({ style_class: 'popup-menu-icon', x_align: St.Align.END })
505 shellItem._icon = new St.Icon({ style_class: 'popup-menu-icon', x_align: St.Align.END });
511506 shellItem.add_child(shellItem._icon);
512507 shellItem.label.x_expand = true;
513508 }
514509
515510 // initialize our state
516 MenuItemFactory._updateLabel.call(shellItem)
517 MenuItemFactory._updateOrnament.call(shellItem)
518 MenuItemFactory._updateImage.call(shellItem)
519 MenuItemFactory._updateVisible.call(shellItem)
520 MenuItemFactory._updateSensitive.call(shellItem)
511 MenuItemFactory._updateLabel.call(shellItem);
512 MenuItemFactory._updateOrnament.call(shellItem);
513 MenuItemFactory._updateImage.call(shellItem);
514 MenuItemFactory._updateVisible.call(shellItem);
515 MenuItemFactory._updateSensitive.call(shellItem);
521516
522517 // initially create children
523518 if (shellItem instanceof PopupMenu.PopupSubMenuMenuItem) {
524 let children = dbusItem.get_children()
525 for (let i = 0; i < children.length; ++i) {
526 shellItem.menu.addMenuItem(MenuItemFactory.createItem(client, children[i]))
527 }
519 let children = dbusItem.getChildren();
520 for (let i = 0; i < children.length; ++i)
521 shellItem.menu.addMenuItem(MenuItemFactory.createItem(client, children[i]));
522
528523 }
529524
530525 // now, connect various events
531 Util.connectSmart(dbusItem, 'property-changed', shellItem, MenuItemFactory._onPropertyChanged)
532 Util.connectSmart(dbusItem, 'child-added', shellItem, MenuItemFactory._onChildAdded)
533 Util.connectSmart(dbusItem, 'child-removed', shellItem, MenuItemFactory._onChildRemoved)
534 Util.connectSmart(dbusItem, 'child-moved', shellItem, MenuItemFactory._onChildMoved)
535 Util.connectSmart(shellItem, 'activate', shellItem, MenuItemFactory._onActivate)
526 Util.connectSmart(dbusItem, 'property-changed', shellItem, MenuItemFactory._onPropertyChanged);
527 Util.connectSmart(dbusItem, 'child-added', shellItem, MenuItemFactory._onChildAdded);
528 Util.connectSmart(dbusItem, 'child-removed', shellItem, MenuItemFactory._onChildRemoved);
529 Util.connectSmart(dbusItem, 'child-moved', shellItem, MenuItemFactory._onChildMoved);
530 Util.connectSmart(shellItem, 'activate', shellItem, MenuItemFactory._onActivate);
536531
537532 if (shellItem.menu)
538 Util.connectSmart(shellItem.menu, "open-state-changed", shellItem, MenuItemFactory._onOpenStateChanged)
539
540 return shellItem
533 Util.connectSmart(shellItem.menu, 'open-state-changed', shellItem, MenuItemFactory._onOpenStateChanged);
534
535 return shellItem;
541536 },
542537
543538 _onOpenStateChanged(menu, open) {
545540 if (NEED_NESTED_SUBMENU_FIX) {
546541 // close our own submenus
547542 if (menu._openedSubMenu)
548 menu._openedSubMenu.close(false)
543 menu._openedSubMenu.close(false);
549544
550545 // register ourselves and close sibling submenus
551546 if (menu._parent._openedSubMenu && menu._parent._openedSubMenu !== menu)
552 menu._parent._openedSubMenu.close(true)
553
554 menu._parent._openedSubMenu = menu
547 menu._parent._openedSubMenu.close(true);
548
549 menu._parent._openedSubMenu = menu;
555550 }
556551
557 this._dbusItem.handle_event("opened", null, 0)
558 this._dbusItem.send_about_to_show()
552 this._dbusItem.handleEvent('opened', null, 0);
553 this._dbusItem.sendAboutToShow();
559554 } else {
560555 if (NEED_NESTED_SUBMENU_FIX) {
561556 // close our own submenus
562557 if (menu._openedSubMenu)
563 menu._openedSubMenu.close(false)
558 menu._openedSubMenu.close(false);
564559 }
565560
566 this._dbusItem.handle_event("closed", null, 0)
561 this._dbusItem.handleEvent('closed', null, 0);
567562 }
568563 },
569564
570565 _onActivate() {
571 this._dbusItem.handle_event("clicked", GLib.Variant.new("i", 0), 0)
572 },
573
574 _onPropertyChanged(dbusItem, prop, value) {
575 if (prop == "toggle-type" || prop == "toggle-state")
576 MenuItemFactory._updateOrnament.call(this)
577 else if (prop == "label")
578 MenuItemFactory._updateLabel.call(this)
579 else if (prop == "enabled")
580 MenuItemFactory._updateSensitive.call(this)
581 else if (prop == "visible")
582 MenuItemFactory._updateVisible.call(this)
583 else if (prop == "icon-name" || prop == "icon-data")
584 MenuItemFactory._updateImage.call(this)
585 else if (prop == "type" || prop == "children-display")
586 MenuItemFactory._replaceSelf.call(this)
587 //else
588 // Util.Logger.debug("Unhandled property change: "+prop)
566 this._dbusItem.handleEvent('clicked', GLib.Variant.new('i', 0), 0);
567 },
568
569 _onPropertyChanged(dbusItem, prop, _value) {
570 if (prop === 'toggle-type' || prop === 'toggle-state')
571 MenuItemFactory._updateOrnament.call(this);
572 else if (prop === 'label')
573 MenuItemFactory._updateLabel.call(this);
574 else if (prop === 'enabled')
575 MenuItemFactory._updateSensitive.call(this);
576 else if (prop === 'visible')
577 MenuItemFactory._updateVisible.call(this);
578 else if (prop === 'icon-name' || prop === 'icon-data')
579 MenuItemFactory._updateImage.call(this);
580 else if (prop === 'type' || prop === 'children-display')
581 MenuItemFactory._replaceSelf.call(this);
582 else
583 Util.Logger.debug(`Unhandled property change: ${prop}`);
589584 },
590585
591586 _onChildAdded(dbusItem, child, position) {
592587 if (!(this instanceof PopupMenu.PopupSubMenuMenuItem)) {
593 Util.Logger.warn("Tried to add a child to non-submenu item. Better recreate it as whole")
594 MenuItemFactory._replaceSelf.call(this)
588 Util.Logger.warn('Tried to add a child to non-submenu item. Better recreate it as whole');
589 MenuItemFactory._replaceSelf.call(this);
595590 } else {
596 this.menu.addMenuItem(MenuItemFactory.createItem(this._dbusClient, child), position)
591 this.menu.addMenuItem(MenuItemFactory.createItem(this._dbusClient, child), position);
597592 }
598593 },
599594
600595 _onChildRemoved(dbusItem, child) {
601596 if (!(this instanceof PopupMenu.PopupSubMenuMenuItem)) {
602 Util.Logger.warn("Tried to remove a child from non-submenu item. Better recreate it as whole")
603 MenuItemFactory._replaceSelf.call(this)
597 Util.Logger.warn('Tried to remove a child from non-submenu item. Better recreate it as whole');
598 MenuItemFactory._replaceSelf.call(this);
604599 } else {
605600 // find it!
606 this.menu._getMenuItems().forEach((item) => {
607 if (item._dbusItem == child)
608 item.destroy()
609 })
601 this.menu._getMenuItems().forEach(item => {
602 if (item._dbusItem === child)
603 item.destroy();
604 });
610605 }
611606 },
612607
613608 _onChildMoved(dbusItem, child, oldpos, newpos) {
614609 if (!(this instanceof PopupMenu.PopupSubMenuMenuItem)) {
615 Util.Logger.warn("Tried to move a child in non-submenu item. Better recreate it as whole")
616 MenuItemFactory._replaceSelf.call(this)
610 Util.Logger.warn('Tried to move a child in non-submenu item. Better recreate it as whole');
611 MenuItemFactory._replaceSelf.call(this);
617612 } else {
618 MenuUtils.moveItemInMenu(this.menu, child, newpos)
613 MenuUtils.moveItemInMenu(this.menu, child, newpos);
619614 }
620615 },
621616
622617 _updateLabel() {
623 let label = this._dbusItem.property_get("label").replace(/_([^_])/, "$1")
618 let label = this._dbusItem.propertyGet('label').replace(/_([^_])/, '$1');
624619
625620 if (this.label) // especially on GS3.8, the separator item might not even have a hidden label
626 this.label.set_text(label)
621 this.label.set_text(label);
627622 },
628623
629624 _updateOrnament() {
630 if (!this.setOrnament) return // separators and alike might not have gotten the polyfill
631
632 if (this._dbusItem.property_get("toggle-type") == "checkmark" && this._dbusItem.property_get_int("toggle-state"))
633 this.setOrnament(PopupMenu.Ornament.CHECK)
634 else if (this._dbusItem.property_get("toggle-type") == "radio" && this._dbusItem.property_get_int("toggle-state"))
635 this.setOrnament(PopupMenu.Ornament.DOT)
625 if (!this.setOrnament)
626 return; // separators and alike might not have gotten the polyfill
627
628 if (this._dbusItem.propertyGet('toggle-type') === 'checkmark' && this._dbusItem.propertyGetInt('toggle-state'))
629 this.setOrnament(PopupMenu.Ornament.CHECK);
630 else if (this._dbusItem.propertyGet('toggle-type') === 'radio' && this._dbusItem.propertyGetInt('toggle-state'))
631 this.setOrnament(PopupMenu.Ornament.DOT);
636632 else
637 this.setOrnament(PopupMenu.Ornament.NONE)
633 this.setOrnament(PopupMenu.Ornament.NONE);
638634 },
639635
640636 _updateImage() {
641 if (!this._icon) return // might be missing on submenus / separators
642
643 let iconName = this._dbusItem.property_get("icon-name")
644 let iconData = this._dbusItem.property_get_variant("icon-data")
637 if (!this._icon)
638 return; // might be missing on submenus / separators
639
640 let iconName = this._dbusItem.propertyGet('icon-name');
641 let iconData = this._dbusItem.propertyGetVariant('icon-data');
645642 if (iconName)
646 this._icon.icon_name = iconName
643 this._icon.icon_name = iconName;
647644 else if (iconData)
648 this._icon.gicon = GdkPixbuf.Pixbuf.new_from_stream(Gio.MemoryInputStream.new_from_bytes(iconData.get_data_as_bytes()), null)
645 this._icon.gicon = GdkPixbuf.Pixbuf.new_from_stream(Gio.MemoryInputStream.new_from_bytes(iconData.get_data_as_bytes()), null);
649646 },
650647
651648 _updateVisible() {
652 this.visible = this._dbusItem.property_get_bool("visible")
649 this.visible = this._dbusItem.propertyGetBool('visible');
653650 },
654651
655652 _updateSensitive() {
656 this.setSensitive(this._dbusItem.property_get_bool("enabled"))
653 this.setSensitive(this._dbusItem.propertyGetBool('enabled'));
657654 },
658655
659656 _replaceSelf(newSelf) {
660657 // create our new self if needed
661658 if (!newSelf)
662 newSelf = MenuItemFactory.createItem(this._dbusClient, this._dbusItem)
659 newSelf = MenuItemFactory.createItem(this._dbusClient, this._dbusItem);
663660
664661 // first, we need to find our old position
665 let pos = -1
666 let family = this._parent._getMenuItems()
662 let pos = -1;
663 let family = this._parent._getMenuItems();
667664 for (let i = 0; i < family.length; ++i) {
668665 if (family[i] === this)
669 pos = i
666 pos = i;
670667 }
671668
672669 if (pos < 0)
673 throw new Error("DBusMenu: can't replace non existing menu item")
670 throw new Error("DBusMenu: can't replace non existing menu item");
674671
675672
676673 // add our new self while we're still alive
677 this._parent.addMenuItem(newSelf, pos)
674 this._parent.addMenuItem(newSelf, pos);
678675
679676 // now destroy our old self
680 this.destroy()
681 }
682 }
677 this.destroy();
678 },
679 };
683680
684681 /**
685682 * Utility functions not necessarily belonging into the item factory
686683 */
687684 const MenuUtils = {
688685 moveItemInMenu(menu, dbusItem, newpos) {
689 //HACK: we're really getting into the internals of the PopupMenu implementation
686 // HACK: we're really getting into the internals of the PopupMenu implementation
690687
691688 // First, find our wrapper. Children tend to lie. We do not trust the old positioning.
692 let family = menu._getMenuItems()
689 let family = menu._getMenuItems();
693690 for (let i = 0; i < family.length; ++i) {
694 if (family[i]._dbusItem == dbusItem) {
691 if (family[i]._dbusItem === dbusItem) {
695692 // now, remove it
696 menu.box.remove_child(family[i])
693 menu.box.remove_child(family[i]);
697694
698695 // and add it again somewhere else
699 if (newpos < family.length && family[newpos] != family[i])
700 menu.box.insert_child_below(family[i], family[newpos])
696 if (newpos < family.length && family[newpos] !== family[i])
697 menu.box.insert_child_below(family[i], family[newpos]);
701698 else
702 menu.box.add(family[i])
699 menu.box.add(family[i]);
703700
704701 // skip the rest
705 return
702 return;
706703 }
707704 }
708 }
709 }
705 },
706 };
710707
711708
712709 /**
714711 *
715712 * Something like a mini-god-object
716713 */
717 var Client = class AppIndicators_Client {
714 var Client = class AppIndicatorsClient {
718715
719716 constructor(busName, path) {
720 this._busName = busName
721 this._busPath = path
722 this._client = new DBusClient(busName, path)
723 this._rootMenu = null // the shell menu
724 this._rootItem = null // the DbusMenuItem for the root
717 this._busName = busName;
718 this._busPath = path;
719 this._client = new DBusClient(busName, path);
720 this._rootMenu = null; // the shell menu
721 this._rootItem = null; // the DbusMenuItem for the root
725722 }
726723
727724 get isReady() {
731728 // this will attach the client to an already existing menu that will be used as the root menu.
732729 // it will also connect the client to be automatically destroyed when the menu dies.
733730 attachToMenu(menu) {
734 this._rootMenu = menu
735 this._rootItem = this._client.get_root()
731 this._rootMenu = menu;
732 this._rootItem = this._client.getRoot();
736733
737734 // cleanup: remove existing children (just in case)
738 this._rootMenu.removeAll()
735 this._rootMenu.removeAll();
739736
740737 if (NEED_NESTED_SUBMENU_FIX)
741 menu._setOpenedSubMenu = this._setOpenedSubmenu.bind(this)
738 menu._setOpenedSubMenu = this._setOpenedSubmenu.bind(this);
742739
743740 // connect handlers
744 Util.connectSmart(menu, 'open-state-changed', this, '_onMenuOpened')
745 Util.connectSmart(menu, 'destroy', this, 'destroy')
746
747 Util.connectSmart(this._rootItem, 'child-added', this, '_onRootChildAdded')
748 Util.connectSmart(this._rootItem, 'child-removed', this, '_onRootChildRemoved')
749 Util.connectSmart(this._rootItem, 'child-moved', this, '_onRootChildMoved')
741 Util.connectSmart(menu, 'open-state-changed', this, this._onMenuOpened);
742 Util.connectSmart(menu, 'destroy', this, this.destroy);
743
744 Util.connectSmart(this._rootItem, 'child-added', this, this._onRootChildAdded);
745 Util.connectSmart(this._rootItem, 'child-removed', this, this._onRootChildRemoved);
746 Util.connectSmart(this._rootItem, 'child-moved', this, this._onRootChildMoved);
750747
751748 // Dropbox requires us to call AboutToShow(0) first
752 this._rootItem.send_about_to_show()
749 this._rootItem.sendAboutToShow();
753750
754751 // fill the menu for the first time
755 this._rootItem.get_children().forEach(child =>
756 this._rootMenu.addMenuItem(MenuItemFactory.createItem(this, child))
752 this._rootItem.getChildren().forEach(child =>
753 this._rootMenu.addMenuItem(MenuItemFactory.createItem(this, child)),
757754 );
758755 }
759756
760757 _setOpenedSubmenu(submenu) {
761758 if (!submenu)
762 return
763
764 if (submenu._parent != this._rootMenu)
765 return
759 return;
760
761 if (submenu._parent !== this._rootMenu)
762 return;
766763
767764 if (submenu === this._openedSubMenu)
768 return
765 return;
769766
770767 if (this._openedSubMenu && this._openedSubMenu.isOpen)
771 this._openedSubMenu.close(true)
772
773 this._openedSubMenu = submenu
768 this._openedSubMenu.close(true);
769
770 this._openedSubMenu = submenu;
774771 }
775772
776773 _onRootChildAdded(dbusItem, child, position) {
777 this._rootMenu.addMenuItem(MenuItemFactory.createItem(this, child), position)
774 this._rootMenu.addMenuItem(MenuItemFactory.createItem(this, child), position);
778775 }
779776
780777 _onRootChildRemoved(dbusItem, child) {
781778 // children like to play hide and seek
782779 // but we know how to find it for sure!
783 this._rootMenu._getMenuItems().forEach((item) => {
784 if (item._dbusItem == child)
785 item.destroy()
786 })
780 this._rootMenu._getMenuItems().forEach(item => {
781 if (item._dbusItem === child)
782 item.destroy();
783 });
787784 }
788785
789786 _onRootChildMoved(dbusItem, child, oldpos, newpos) {
790 MenuUtils.moveItemInMenu(this._rootMenu, dbusItem, newpos)
787 MenuUtils.moveItemInMenu(this._rootMenu, dbusItem, newpos);
791788 }
792789
793790 _onMenuOpened(menu, state) {
794 if (!this._rootItem) return
791 if (!this._rootItem)
792 return;
795793
796794 if (state) {
797795 if (this._openedSubMenu && this._openedSubMenu.isOpen)
798 this._openedSubMenu.close()
799
800 this._rootItem.handle_event("opened", null, 0)
801 this._rootItem.send_about_to_show()
796 this._openedSubMenu.close();
797
798 this._rootItem.handleEvent('opened', null, 0);
799 this._rootItem.sendAboutToShow();
802800 } else {
803 this._rootItem.handle_event("closed", null, 0)
801 this._rootItem.handleEvent('closed', null, 0);
804802 }
805803 }
806804
807805 destroy() {
808 this.emit('destroy')
806 this.emit('destroy');
809807
810808 if (this._client)
811 this._client.destroy()
812
813 this._client = null
814 this._rootItem = null
815 this._rootMenu = null
816 }
817 }
818 Signals.addSignalMethods(Client.prototype)
809 this._client.destroy();
810
811 this._client = null;
812 this._rootItem = null;
813 this._rootMenu = null;
814 }
815 };
816 Signals.addSignalMethods(Client.prototype);
1212 // You should have received a copy of the GNU General Public License
1313 // along with this program; if not, write to the Free Software
1414 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
15 const Gio = imports.gi.Gio;
16 const GLib = imports.gi.GLib;
17 const Gtk = imports.gi.Gtk;
18 const Gdk = imports.gi.Gdk;
19 const Main = imports.ui.main;
20 const Mainloop = imports.mainloop;
2115
22 const Extension = imports.misc.extensionUtils.getCurrentExtension()
16 /* exported init, enable, disable */
2317
24 const StatusNotifierWatcher = Extension.imports.statusNotifierWatcher
25 const Util = Extension.imports.util
18 const Extension = imports.misc.extensionUtils.getCurrentExtension();
19
20 const StatusNotifierWatcher = Extension.imports.statusNotifierWatcher;
21 const Util = Extension.imports.util;
2622
2723 let statusNotifierWatcher = null;
2824 let isEnabled = false;
2925 let watchDog = null;
30 let startupPreparedId = 0;
31 let waitForThemeId = 0;
32 let startupComplete = false;
33 let displayAvailable = false;
3426
3527 function init() {
36 watchDog = new NameWatchdog();
37 watchDog.onVanished = maybe_enable_after_name_available;
28 watchDog = new Util.NameWatcher(StatusNotifierWatcher.WATCHER_BUS_NAME);
29 watchDog.connect('vanished', () => maybeEnableAfterNameAvailable());
3830
39 //HACK: we want to leave the watchdog alive when disabling the extension,
31 // HACK: we want to leave the watchdog alive when disabling the extension,
4032 // but if we are being reloaded, we destroy it since it could be considered
4133 // a leak and spams our log, too.
42 if (typeof global['--appindicator-extension-on-reload'] == 'function')
43 global['--appindicator-extension-on-reload']()
34 /* eslint-disable no-undef */
35 if (typeof global['--appindicator-extension-on-reload'] === 'function')
36 global['--appindicator-extension-on-reload']();
4437
4538 global['--appindicator-extension-on-reload'] = () => {
46 Util.Logger.debug("Reload detected, destroying old watchdog")
39 Util.Logger.debug('Reload detected, destroying old watchdog');
4740 watchDog.destroy();
48 }
41 };
42 /* eslint-enable no-undef */
4943 }
5044
51 //FIXME: when entering/leaving the lock screen, the extension might be enabled/disabled rapidly.
45 // FIXME: when entering/leaving the lock screen, the extension might be enabled/disabled rapidly.
5246 // This will create very bad side effects in case we were not done unowning the name while trying
5347 // to own it again. Since g_bus_unown_name doesn't fire any callback when it's done, we need to
5448 // monitor the bus manually to find out when the name vanished so we can reclaim it again.
55 function maybe_enable_after_name_available() {
49 function maybeEnableAfterNameAvailable() {
5650 // by the time we get called whe might not be enabled
57 if (isEnabled && (!watchDog.nameAcquired || !watchDog.isPresent) && statusNotifierWatcher === null)
51 if (isEnabled && (!watchDog.nameAcquired || !watchDog.nameOnBus) && statusNotifierWatcher === null)
5852 statusNotifierWatcher = new StatusNotifierWatcher.StatusNotifierWatcher(watchDog);
5953 }
6054
61 function inner_enable() {
62 if (startupComplete && displayAvailable) {
63 isEnabled = true;
64 maybe_enable_after_name_available();
65 }
66 }
67
6855 function enable() {
69 // If the desktop is still starting up, we must wait until it is ready
70 if (Main.layoutManager._startingUp) {
71 startupPreparedId = Main.layoutManager.connect('startup-complete', () => {
72 Main.layoutManager.disconnect(startupPreparedId);
73 startupComplete = true;
74 inner_enable();
75 });
76 } else {
77 startupComplete = true;
78 }
79
80 // Ensure that the default Gdk Screen is available
81 if (Gtk.IconTheme.get_default() == null) {
82 waitForThemeId = Gdk.DisplayManager.get().connect('display-opened', () => {
83 Gdk.DisplayManager.get().disconnect(waitForThemeId);
84 displayAvailable = true;
85 inner_enable();
86 });
87 } else {
88 displayAvailable = true;
89 }
90 inner_enable();
56 isEnabled = true;
57 maybeEnableAfterNameAvailable();
9158 }
9259
9360 function disable() {
9764 statusNotifierWatcher = null;
9865 }
9966 }
100
101 /**
102 * NameWatchdog will monitor the ork.kde.StatusNotifierWatcher bus name for us
103 */
104 var NameWatchdog = class AppIndicators_NameWatchdog {
105
106 constructor() {
107 this.onAppeared = null;
108 this.onVanished = null;
109
110 // will be set in the handlers which are guaranteed to be called at least once
111 this.isPresent = false;
112
113 this._watcher_id = Gio.DBus.session.watch_name("org.kde.StatusNotifierWatcher", 0,
114 this._appeared_handler.bind(this), this._vanished_handler.bind(this));
115 }
116
117 destroy() {
118 Gio.DBus.session.unwatch_name(this._watcher_id);
119 }
120
121 _appeared_handler() {
122 this.isPresent = true;
123 if (this.onAppeared) this.onAppeared();
124 }
125
126 _vanished_handler() {
127 this.isPresent = false;
128 if (this.onVanished) this.onVanished();
129 }
130 }
1313 // along with this program; if not, write to the Free Software
1414 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
1515
16 const GLib = imports.gi.GLib
17 const Gio = imports.gi.Gio
16 /* exported IconCache */
1817
19 const Util = imports.misc.extensionUtils.getCurrentExtension().imports.util;
18 const GLib = imports.gi.GLib;
19 const Gio = imports.gi.Gio;
20
21 const Extension = imports.misc.extensionUtils.getCurrentExtension();
22 const PromiseUtils = Extension.imports.promiseUtils;
23 const Util = Extension.imports.util;
2024
2125 // The icon cache caches icon objects in case they're reused shortly aftwerwards.
2226 // This is necessary for some indicators like skype which rapidly switch between serveral icons.
2832 const LIFETIME_TIMESPAN = 10; // seconds
2933
3034 // how to use: see IconCache.add, IconCache.get
31 var IconCache = class AppIndicators_IconCache {
35 var IconCache = class AppIndicatorsIconCache {
3236 constructor() {
3337 this._cache = new Map();
34 this._lifetime = new Map(); //we don't want to attach lifetime to the object
38 this._lifetime = new Map(); // we don't want to attach lifetime to the object
3539 }
3640
3741 add(id, icon) {
8084
8185 // marks all the icons as removable, if something doesn't claim them before
8286 weakClear() {
83 this._cache.forEach((icon) => icon.inUse = false);
87 this._cache.forEach(icon => (icon.inUse = false));
8488 this._checkGC();
8589 }
8690
102106 return null;
103107 }
104108
105 _checkGC() {
106 let cacheIsEmpty = this._cache.size == 0;
109 async _checkGC() {
110 let cacheIsEmpty = this._cache.size === 0;
107111
108112 if (!cacheIsEmpty && !this._gcTimeout) {
109 Util.Logger.debug("IconCache: garbage collector started");
110 this._gcTimeout = GLib.timeout_add_seconds(
111 GLib.PRIORITY_LOW,
112 GC_INTERVAL,
113 () => this._gc());
113 Util.Logger.debug('IconCache: garbage collector started');
114 this._gcTimeout = new PromiseUtils.TimeoutSecondsPromise(GC_INTERVAL,
115 GLib.PRIORITY_LOW);
116 await this._gcTimeout;
114117 } else if (cacheIsEmpty && this._gcTimeout) {
115 Util.Logger.debug("IconCache: garbage collector stopped");
116 GLib.Source.remove(this._gcTimeout);
118 Util.Logger.debug('IconCache: garbage collector stopped');
119 this._gcTimeout.cancel();
117120 delete this._gcTimeout;
118121 }
119122 }
121124 _gc() {
122125 let time = new Date().getTime();
123126 this._cache.forEach((icon, id) => {
124 if (icon.inUse) {
127 if (icon.inUse)
125128 Util.Logger.debug(`IconCache: ${id} is in use.`);
126 } else if (this._lifetime.get(id) < time) {
129 else if (this._lifetime.get(id) < time)
127130 this._remove(id);
128 } else {
131 else
129132 Util.Logger.debug(`IconCache: ${id} survived this round.`);
130 }
131133 });
132134
133135 return true;
3333
3434 (() => {
3535
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,
5038 });
5139
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();
14244 });
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', () => {
172141 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', () => {
175147 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 item.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 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', () => {
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 });
187285 });
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);
287287
288288 })();
1212 // You should have received a copy of the GNU General Public License
1313 // along with this program; if not, write to the Free Software
1414 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
15
16 /* exported IndicatorStatusIcon */
17
1518 const Clutter = imports.gi.Clutter;
1619 const GObject = imports.gi.GObject;
1720 const St = imports.gi.St;
1922 const Main = imports.ui.main;
2023 const Panel = imports.ui.panel;
2124 const PanelMenu = imports.ui.panelMenu;
22 const PopupMenu = imports.ui.popupMenu;
2325
2426 const Config = imports.misc.config;
2527 const ExtensionUtils = imports.misc.extensionUtils;
2628 const Extension = ExtensionUtils.getCurrentExtension();
2729
28 const AppIndicator = Extension.imports.appIndicator
30 const AppIndicator = Extension.imports.appIndicator;
2931 const DBusMenu = Extension.imports.dbusMenu;
3032 const Util = Extension.imports.util;
3133
3335 * IndicatorStatusIcon implements an icon in the system status area
3436 */
3537 var IndicatorStatusIcon = GObject.registerClass(
36 class AppIndicators_IndicatorStatusIcon extends PanelMenu.Button {
38 class AppIndicatorsIndicatorStatusIcon extends PanelMenu.Button {
3739 _init(indicator) {
38 super._init(0.5, indicator._uniqueId);
40 super._init(0.5, indicator.uniqueId);
3941 this._indicator = indicator;
4042
4143 this._iconBox = new AppIndicator.IconActor(indicator, Panel.PANEL_ICON_SIZE);
4446 this.add_child(this._box);
4547
4648 this._box.add_child(this._iconBox);
47 Util.connectSmart(this, 'button-press-event', this, '_boxClicked')
4849
49 Util.connectSmart(this._indicator, 'ready', this, '_display')
50 Util.connectSmart(this._indicator, 'menu', this, '_updateMenu')
51 Util.connectSmart(this._indicator, 'label', this, '_updateLabel')
52 Util.connectSmart(this._indicator, 'status', this, '_updateStatus')
50 Util.connectSmart(this._indicator, 'ready', this, this._display);
51 Util.connectSmart(this._indicator, 'menu', this, this._updateMenu);
52 Util.connectSmart(this._indicator, 'label', this, this._updateLabel);
53 Util.connectSmart(this._indicator, 'status', this, this._updateStatus);
5354 Util.connectSmart(this._indicator, 'reset', this, () => {
5455 this._updateStatus();
5556 this._updateLabel();
6061 this._menuClient.destroy();
6162 this._menuClient = null;
6263 }
63 })
64 });
6465
6566 if (this._indicator.isReady)
66 this._display()
67 this._display();
6768 }
6869
6970 _updateLabel() {
7172 if (label) {
7273 if (!this._label || !this._labelBin) {
7374 this._labelBin = new St.Bin({
74 y_align: ExtensionUtils.versionCheck(['3.34'], Config.PACKAGE_VERSION) ?
75 St.Align.MIDDLE : Clutter.ActorAlign.CENTER,
75 y_align: ExtensionUtils.versionCheck(['3.34'], Config.PACKAGE_VERSION)
76 ? St.Align.MIDDLE : Clutter.ActorAlign.CENTER,
7677 });
7778 this._label = new St.Label();
7879 this._labelBin.add_actor(this._label);
7980 this._box.add_actor(this._labelBin);
8081 }
8182 this._label.set_text(label);
82 if (!this._box.contains(this._labelBin)) this._box.add_actor(this._labelBin); //FIXME: why is it suddenly necessary?
83 } else {
84 if (this._label) {
85 this._labelBin.destroy_all_children();
86 this._box.remove_actor(this._labelBin);
87 this._labelBin.destroy();
88 delete this._labelBin;
89 delete this._label;
90 }
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;
9191 }
9292 }
9393
9494 _updateStatus() {
95 this.visible = this._indicator.status != AppIndicator.SNIStatus.PASSIVE;
95 this.visible = this._indicator.status !== AppIndicator.SNIStatus.PASSIVE;
9696 }
9797
9898 _updateMenu() {
104104
105105 if (this._indicator.menuPath) {
106106 this._menuClient = new DBusMenu.Client(this._indicator.busName,
107 this._indicator.menuPath);
107 this._indicator.menuPath);
108108 this._menuClient.attachToMenu(this.menu);
109109 }
110110 }
114114 this._updateStatus();
115115 this._updateMenu();
116116
117 Main.panel.addToStatusArea("appindicator-"+this._indicator.uniqueId, this, 1, 'right')
117 Main.panel.addToStatusArea(`appindicator-${this._indicator.uniqueId}`, this, 1, 'right');
118118 }
119119
120 _boxClicked(actor, event) {
120 vfunc_button_press_event(buttonEvent) {
121121 // if middle mouse button clicked send SecondaryActivate dbus event and do not show appindicator menu
122 if (event.get_button() == 2) {
122 if (buttonEvent.button === 2) {
123123 Main.panel.menuManager._closeMenu(true, Main.panel.menuManager.activeMenu);
124124 this._indicator.secondaryActivate();
125 return;
125 return Clutter.EVENT_STOP;
126126 }
127127
128 //HACK: event should be a ClutterButtonEvent but we get only a ClutterEvent (why?)
129 // because we can't access click_count, we'll create our own double click detector.
130 var treshold = Clutter.Settings.get_default().double_click_time;
131 var now = new Date().getTime();
132 if (this._lastClicked && (now - this._lastClicked) < treshold) {
133 this._lastClicked = null; //reset double click detector
128 if (buttonEvent.button === 1 && buttonEvent.click_count === 2) {
134129 this._indicator.open();
135 } else {
136 this._lastClicked = now;
130 return Clutter.EVENT_STOP;
137131 }
132
133 return Clutter.EVENT_PROPAGATE;
134 }
135
136 vfunc_scroll_event(scrollEvent) {
137 // Since Clutter 1.10, clutter will always send a smooth scrolling event
138 // with explicit deltas, no matter what input device is used
139 // In fact, for every scroll there will be a smooth and non-smooth scroll
140 // event, and we can choose which one we interpret.
141 if (scrollEvent.direction === Clutter.ScrollDirection.SMOOTH) {
142 const event = Clutter.get_current_event();
143 let [dx, dy] = event.get_scroll_delta();
144
145 this._indicator.scroll(dx, dy);
146 return Clutter.EVENT_STOP;
147 }
148
149 return Clutter.EVENT_PROPAGATE;
138150 }
139151 });
+0
-16
interfaces-xml/Properties.xml less more
0 <interface name="org.freedesktop.DBus.Properties">
1 <method name="Get">
2 <arg type="s" direction="in" />
3 <arg type="s" direction="in" />
4 <arg type="v" direction="out" />
5 </method>
6 <method name="GetAll">
7 <arg type="s" direction="in" />
8 <arg type="a{sv}" direction="out" />
9 </method>
10 <signal name="PropertiesChanged">
11 <arg type="s" direction="out" />
12 <arg type="a{sv}" direction="out" />
13 <arg type="as" direction="out" />
14 </signal>
15 </interface>
1313 // along with this program; if not, write to the Free Software
1414 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
1515
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 /* exported StatusNotifierItem, StatusNotifierWatcher, DBusMenu */
17
18 var StatusNotifierItem = loadInterfaceXml('StatusNotifierItem.xml');
19 var StatusNotifierWatcher = loadInterfaceXml('StatusNotifierWatcher.xml');
20 var DBusMenu = loadInterfaceXml('DBusMenu.xml');
2021
2122 // loads a xml file into an in-memory string
2223 function loadInterfaceXml(filename) {
23 let extension = imports.misc.extensionUtils.getCurrentExtension()
24
25 let interfaces_dir = extension.dir.get_child("interfaces-xml")
26
27 let file = interfaces_dir.get_child(filename)
28
29 let [ result, contents ] = imports.gi.GLib.file_get_contents(file.get_path())
24 const extension = imports.misc.extensionUtils.getCurrentExtension();
25 const interfacesDir = extension.dir.get_child('interfaces-xml');
26 const file = interfacesDir.get_child(filename);
27 let [result, contents] = imports.gi.GLib.file_get_contents(file.get_path());
3028
3129 if (result) {
32 //HACK: The "" + trick is important as hell because file_get_contents returns
33 // an object (WTF?) but Gio.makeProxyWrapper requires `typeof() == "string"`
30 // HACK: The "" + trick is important as hell because file_get_contents returns
31 // an object (WTF?) but Gio.makeProxyWrapper requires `typeof() === "string"`
3432 // Otherwise, it will try to check `instanceof XML` and fail miserably because there
3533 // is no `XML` on very recent SpiderMonkey releases (or, if SpiderMonkey is old enough,
3634 // will spit out a TypeError soon).
3735 if (contents instanceof Uint8Array)
38 contents = imports.byteArray.toString(contents);
39 return "<node>" + contents + "</node>"
36 contents = imports.byteArray.toString(contents);
37 return `<node>${contents}</node>`;
4038 } else {
41 throw new Error("AppIndicatorSupport: Could not load file: "+filename)
39 throw new Error(`AppIndicatorSupport: Could not load file: ${filename}`);
4240 }
4341 }
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