Codebase list gnome-shell-extension-appindicator / 83fce78
Remove home-grown D-Bus proxy With a little bit of work, the standard one will do. This should reduce the maintainance burden. Jonas Kümmerlin 8 years ago
3 changed file(s) with 168 addition(s) and 273 deletion(s). Raw diff Collapse all Expand all
3030 const DBusMenu = Extension.imports.dbusMenu;
3131 const IconCache = Extension.imports.iconCache;
3232 const Util = Extension.imports.util;
33 const Interfaces = Extension.imports.interfaces;
3334
3435 const SNICategory = {
3536 APPLICATION: 'ApplicationStatus',
5556 this.busName = bus_name
5657 this._uniqueId = bus_name + object
5758
58 this._proxy = new Util.XmlLessDBusProxy({
59 connection: Gio.DBus.session,
60 name: bus_name,
61 path: object,
62 interface: 'org.kde.StatusNotifierItem',
63 propertyWhitelist: [ //keep sorted alphabetically, please
64 'AttentionIconName',
65 'AttentionIconPixmap',
66 'Category',
67 'IconName',
68 'IconPixmap',
69 'IconThemePath',
70 'Id',
71 'Menu',
72 'OverlayIconName',
73 'OverlayIconPixmap',
74 'Status',
75 'Title',
76 'ToolTip',
77 'XAyatanaLabel'
78 ],
79 onReady: (function() {
80 this.isReady = true
81 this.emit('ready')
82 }).bind(this)
83 })
84
85 this._proxy.connect('-property-changed', this._onPropertyChanged.bind(this))
86 this._proxy.connect('-signal', this._translateNewSignals.bind(this))
59 let interface_info = Gio.DBusInterfaceInfo.new_for_xml(Interfaces.StatusNotifierItem)
60
61 //HACK: we cannot use Gio.DBusProxy.makeProxyWrapper because we need
62 // to specify G_DBUS_PROXY_FLAGS_GET_INVALIDATED_PROPERTIES
63 this._proxy = new Gio.DBusProxy({ g_connection: Gio.DBus.session,
64 g_interface_name: interface_info.name,
65 g_interface_info: interface_info,
66 g_name: bus_name,
67 g_object_path: object,
68 g_flags: Gio.DBusProxyFlags.GET_INVALIDATED_PROPERTIES })
69 this._proxy.init_async(GLib.PRIORITY_DEFAULT, null, (function(initable, result) {
70 try {
71 initable.init_finish(result);
72
73 this.isReady = true
74 this.emit('ready')
75 } catch(e) {
76 Util.Logger.warn("While intializing proxy for "+bus_name+object+": "+e)
77 }
78 }).bind(this))
79
80 this._proxyPropertyList = interface_info.properties.map(function(propinfo) { return propinfo.name })
81
82
83 Util.connectSmart(this._proxy, 'g-properties-changed', this, '_onPropertiesChanged')
84 Util.connectSmart(this._proxy, 'g-signal', this, '_translateNewSignals')
8785 },
8886
8987 // The Author of the spec didn't like the PropertiesChanged signal, so he invented his own
90 _translateNewSignals: function(proxy, signal, params) {
88 _translateNewSignals: function(proxy, sender, signal, params) {
9189 if (signal.substr(0, 3) == 'New') {
9290 let prop = signal.substr(3)
9391
94 if (this._proxy.propertyWhitelist.indexOf(prop) > -1)
95 this._proxy.invalidateProperty(prop)
96
97 if (this._proxy.propertyWhitelist.indexOf(prop + 'Pixmap') > -1)
98 this._proxy.invalidateProperty(prop + 'Pixmap')
99
100 if (this._proxy.propertyWhitelist.indexOf(prop + 'Name') > -1)
101 this._proxy.invalidateProperty(prop + 'Name')
92 if (this._proxyPropertyList.indexOf(prop) > -1)
93 Util.refreshPropertyOnProxy(this._proxy, prop)
94
95 if (this._proxyPropertyList.indexOf(prop + 'Pixmap') > -1)
96 Util.refreshPropertyOnProxy(this._proxy, prop + 'Pixmap')
97
98 if (this._proxyPropertyList.indexOf(prop + 'Name') > -1)
99 Util.refreshPropertyOnProxy(this._proxy, prop + 'Name')
102100 } else if (signal == 'XAyatanaNewLabel') {
103101 // and the ayatana guys made sure to invent yet another way of composing these signals...
104 this._proxy.invalidateProperty('XAyatanaLabel')
102 Util.refreshPropertyOnProxy(this._proxy, 'XAyatanaLabel')
105103 }
106104 },
107105
108106 //public property getters
109107 get title() {
110 return this._proxy.cachedProperties.Title;
108 return this._proxy.Title;
111109 },
112110 get id() {
113 return this._proxy.cachedProperties.Id;
111 return this._proxy.Id;
114112 },
115113 get uniqueId() {
116114 return this._uniqueId;
117115 },
118116 get status() {
119 return this._proxy.cachedProperties.Status;
117 return this._proxy.Status;
120118 },
121119 get label() {
122 return this._proxy.cachedProperties.XAyatanaLabel;
120 let v = this._proxy.get_cached_property('XAyatanaLabel');
121
122 if (v) return v.deep_unpack()
123 else return null
123124 },
124125 get menuPath() {
125 return this._proxy.cachedProperties.Menu || "/MenuBar"
126 return this._proxy.Menu || "/MenuBar"
126127 },
127128
128129 get attentionIcon() {
129130 return [
130 this._proxy.cachedProperties.AttentionIconName,
131 this._proxy.cachedProperties.AttentionIconPixmap,
132 this._proxy.cachedProperties.IconThemePath
131 this._proxy.AttentionIconName,
132 this._proxy.AttentionIconPixmap,
133 this._proxy.IconThemePath
133134 ]
134135 },
135136
136137 get icon() {
137138 return [
138 this._proxy.cachedProperties.IconName,
139 this._proxy.cachedProperties.IconPixmap,
140 this._proxy.cachedProperties.IconThemePath
139 this._proxy.IconName,
140 this._proxy.IconPixmap,
141 this._proxy.IconThemePath
141142 ]
142143 },
143144
144145 get overlayIcon() {
145146 return [
146 this._proxy.cachedProperties.OverlayIconName,
147 this._proxy.cachedProperties.OverlayIconPixmap,
148 this._proxy.cachedProperties.IconThemePath
147 this._proxy.OverlayIconName,
148 this._proxy.OverlayIconPixmap,
149 this._proxy.IconThemePath
149150 ]
150151 },
151152
152 _onPropertyChanged: function(proxy, property, newValue) {
153 // some property changes require updates on our part,
154 // a few need to be passed down to the displaying code
155
156 // all these can mean that the icon has to be changed
157 if (property == 'Status' || property.substr(0, 4) == 'Icon' || property.substr(0, 13) == 'AttentionIcon')
158 this.emit('icon')
159
160 // same for overlays
161 if (property.substr(0, 11) == 'OverlayIcon')
162 this.emit('overlay-icon')
163
164 // this may make all of our icons invalid
165 if (property == 'IconThemePath') {
166 this.emit('icon')
167 this.emit('overlay-icon')
168 }
169
170 // the label will be handled elsewhere
171 if (property == 'XAyatanaLabel')
172 this.emit('label')
173
174 // status updates are important for the StatusNotifierDispatcher
175 if (property == 'Status')
176 this.emit('status')
177 },
178
179 // triggers a reload of all properties
180 reset: function(triggerReady) {
181 this._proxy.invalidateAllProperties(this.emit.bind(this, 'reset'))
153 _onPropertiesChanged: function(proxy, changed, invalidated) {
154 let props = Object.keys(changed.deep_unpack())
155
156 props.forEach(function(property) {
157 // some property changes require updates on our part,
158 // a few need to be passed down to the displaying code
159
160 // all these can mean that the icon has to be changed
161 if (property == 'Status' || property.substr(0, 4) == 'Icon' || property.substr(0, 13) == 'AttentionIcon')
162 this.emit('icon')
163
164 // same for overlays
165 if (property.substr(0, 11) == 'OverlayIcon')
166 this.emit('overlay-icon')
167
168 // this may make all of our icons invalid
169 if (property == 'IconThemePath') {
170 this.emit('icon')
171 this.emit('overlay-icon')
172 }
173
174 // the label will be handled elsewhere
175 if (property == 'XAyatanaLabel')
176 this.emit('label')
177
178 // status updates may cause the indicator to be hidden
179 if (property == 'Status')
180 this.emit('status')
181 }, this);
182 },
183
184 reset: function() {
185 //TODO: reload all properties, or do some other useful things
186 this.emit('reset')
182187 },
183188
184189 destroy: function() {
185190 this.emit('destroy')
186191
187192 this.disconnectAll()
188 this._proxy.destroy()
193 delete this._proxy
189194 },
190195
191196 open: function() {
193198 // nor can we call any X11 functions. Luckily, the Activate method usually works fine.
194199 // parameters are "an hint to the item where to show eventual windows" [sic]
195200 // ... and don't seem to have any effect.
196 this._proxy.call({
197 name: 'Activate',
198 paramTypes: 'ii',
199 paramValues: [0, 0]
200 // we don't care about the result
201 })
201 this._proxy.ActivateRemote(0, 0)
202202 },
203203
204204 scroll: function(dx, dy) {
205205 if (dx != 0)
206 this._proxy.call({
207 name: 'Scroll',
208 paramTypes: 'is',
209 paramValues: [ Math.floor(dx), 'horizontal' ]
210 })
206 this._proxy.ScrollRemote(Math.floor(dx), 'horizontal')
211207
212208 if (dy != 0)
213 this._proxy.call({
214 name: 'Scroll',
215 paramTypes: 'is',
216 paramValues: [ Math.floor(dy), 'vertical' ]
217 })
209 this._proxy.ScrollRemote(Math.floor(dy), 'vertical')
218210 }
219211 });
220212 Signals.addSignalMethods(AppIndicator.prototype);
3535 <arg name="x" type="i" direction="in"/>
3636 <arg name="y" type="i" direction="in"/>
3737 </method>
38 <method name="Scroll">
39 <arg name="delta" type="i" direction="in"/>
40 <arg name="dir" type="s" direction="in"/>
41 </method>
3842
3943 <!-- Signals: the client wants to change something in the status-->
4044 <signal name="NewTitle"></signal>
5054 </signal>
5155
5256 <!-- ayatana labels -->
53 <signal name="XAyatanaNewLabel">
57 <!-- These are commented out because GDBusProxy would otherwise require them,
58 but they are not available for KDE indicators
59 -->
60 <!--<signal name="XAyatanaNewLabel">
5461 <arg type="s" name="label" direction="out" />
5562 <arg type="s" name="guide" direction="out" />
5663 </signal>
5764 <property name="XAyatanaLabel" type="s" access="read" />
58 <property name="XAyatanaLabelGuide" type="s" access="read" /> <!-- unimplemented -->
65 <property name="XAyatanaLabelGuide" type="s" access="read" />-->
5966
6067
6168 </interface>
1414 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
1515 const Gio = imports.gi.Gio
1616 const GLib = imports.gi.GLib
17 const GObject = imports.gi.GObject
1718
1819 const Lang = imports.lang
1920 const Signals = imports.signals
2021
22 const refreshPropertyOnProxy = function(proxy, property_name) {
23 proxy.g_connection.call(proxy.g_name,
24 proxy.g_object_path,
25 'org.freedesktop.DBus.Properties',
26 'Get',
27 GLib.Variant.new('(ss)', [ proxy.g_interface_name, property_name ]),
28 GLib.VariantType.new('(v)'),
29 Gio.DBusCallFlags.NONE,
30 -1,
31 null,
32 function(conn, result) {
33 try {
34 let value_variant = conn.call_finish(result).deep_unpack()[0]
35
36 proxy.set_cached_property(property_name, value_variant)
37
38 // synthesize a property changed event
39 let changed_obj = {}
40 changed_obj[property_name] = value_variant
41 proxy.emit('g-properties-changed', GLib.Variant.new('a{sv}', changed_obj), [])
42 } catch (e) {
43 // the property may not even exist, silently ignore it
44 //Logger.debug("While refreshing property "+property_name+": "+e)
45 }
46 })
47 }
48
49 const connectSmart3A = function(src, signal, handler) {
50 let id = src.connect(signal, handler)
51
52 if (src.connect && (!(src instanceof GObject.Object) || GObject.signal_lookup('destroy', src))) {
53 let destroy_id = src.connect('destroy', function() {
54 src.disconnect(id)
55 src.disconnect(destroy_id)
56 })
57 }
58 }
59
60 const connectSmart4A = function(src, signal, target, method) {
61 let signal_id = src.connect(signal, target[method].bind(target))
62
63 // GObject classes might or might not have a destroy signal
64 // JS Classes will not complain when connecting to non-existent signals
65 let src_destroy_id = src.connect && (!(src instanceof GObject.Object) || GObject.signal_lookup('destroy', src)) ? src.connect('destroy', on_destroy) : 0
66 let tgt_destroy_id = target.connect && (!(target instanceof GObject.Object) || GObject.signal_lookup('destroy', target)) ? target.connect('destroy', on_destroy) : 0
67
68 function on_destroy() {
69 src.disconnect(signal_id)
70 if (src_destroy_id) src.disconnect(src_destroy_id)
71 if (tgt_destroy_id) target.disconnect(tgt_destroy_id)
72 }
73 }
74
2175 /**
22 * This proxy works completely without an interface xml, making it both flexible
23 * and mistake-prone. It will cache properties and emit events, and provides
24 * shortcuts for calling methods.
76 * Connect signals to slots, and remove the connection when either source or
77 * target are destroyed
78 *
79 * Usage:
80 * Util.connectSmart(srcOb, 'signal', tgtObj, 'handler')
81 * or
82 * Util.connectSmart(srcOb, 'signal', function() { ... })
2583 */
26 const XmlLessDBusProxy = new Lang.Class({
27 Name: 'XmlLessDBusProxy',
28
29 _init: function(params) {
30 if (!params.connection || !params.name || !params.path || !params.interface)
31 throw new Error("XmlLessDBusProxy: please provide connection, name, path and interface")
32
33 this.connection = params.connection
34 this.name = params.name
35 this.path = params.path
36 this.interface = params.interface
37 this.propertyWhitelist = params.propertyWhitelist || []
38 this.cachedProperties = {}
39
40 this.invalidateAllProperties(params.onReady)
41 this._signalId = this.connection.signal_subscribe(this.name,
42 this.interface,
43 null,
44 this.path,
45 null,
46 Gio.DBusSignalFlags.NONE,
47 this._onSignal.bind(this))
48 this._propChangedId = this.connection.signal_subscribe(this.name,
49 'org.freedesktop.DBus.Properties',
50 'PropertiesChanged',
51 this.path,
52 null,
53 Gio.DBusSignalFlags.NONE,
54 this._onPropertyChanged.bind(this))
55 },
56
57 setProperty: function(propertyName, valueVariant) {
58 //TODO: implement
59 },
60
61 /**
62 * Initiates recaching the given property.
63 *
64 * This is useful if the interface notifies the consumer of changed properties
65 * in unorthodox ways or if you changed the whitelist
66 */
67 invalidateProperty: function(propertyName, callback) {
68 this.connection.call(this.name,
69 this.path,
70 'org.freedesktop.DBus.Properties',
71 'Get',
72 GLib.Variant.new('(ss)', [ this.interface, propertyName ]),
73 GLib.VariantType.new('(v)'),
74 Gio.DBusCallFlags.NONE,
75 -1,
76 null,
77 this._getPropertyCallback.bind(this, propertyName, callback))
78 },
79
80 _getPropertyCallback: function(propertyName, callback, conn, result) {
81 try {
82 let newValue = conn.call_finish(result).deep_unpack()[0].deep_unpack()
83
84 if (this.propertyWhitelist.indexOf(propertyName) > -1) {
85 this.cachedProperties[propertyName] = newValue
86 this.emit("-property-changed", propertyName, newValue)
87 this.emit("-property-changed::"+propertyName, newValue)
88 }
89 } catch (e) {
90 // this can mean two things:
91 // - the interface is gone (or doesn't conform or whatever)
92 // - the property doesn't exist
93 // we do not care and we don't even log it.
94 //Logger.debug("XmlLessDBusProxy: while getting property: "+e)
95 }
96
97 if (callback) callback()
98 },
99
100 invalidateAllProperties: function(callback) {
101 let waitFor = 0
102
103 this.propertyWhitelist.forEach(function(prop) {
104 waitFor += 1
105 this.invalidateProperty(prop, maybeFinished)
106 }, this)
107
108 function maybeFinished() {
109 waitFor -= 1
110 if (waitFor == 0 && callback)
111 callback()
112 }
113 },
114
115 _onPropertyChanged: function(conn, sender, path, iface, signal, params) {
116 let [ , changed, invalidated ] = params.deep_unpack()
117
118 for (let i in changed) {
119 if (this.propertyWhitelist.indexOf(i) > -1) {
120 this.cachedProperties[i] = changed[i].deep_unpack()
121 this.emit("-property-changed", i, this.cachedProperties[i])
122 this.emit("-property-changed::"+i, this.cachedProperties[i])
123 }
124 }
125
126 for (let i = 0; i < invalidated.length; ++i) {
127 if (this.propertyWhitelist.indexOf(invalidated[i]) > -1)
128 this.invalidateProperty(invalidated[i])
129 }
130 },
131
132 _onSignal: function(conn, sender, path, iface, signal, params) {
133 this.emit("-signal", signal, params)
134 this.emit(signal, params.deep_unpack())
135 },
136
137 call: function(params) {
138 if (!params)
139 throw new Error("XmlLessDBusProxy::call: need params argument")
140
141 if (!params.name)
142 throw new Error("XmlLessDBusProxy::call: missing name")
143
144 if (params.params instanceof GLib.Variant) {
145 // good!
146 } else if (params.paramTypes && params.paramValues) {
147 params.params = GLib.Variant.new('(' + params.paramTypes + ')', params.paramValues)
148 } else {
149 throw new Error("XmlLessDBusProxy::call: provide either paramType (string) and paramValues (array) or params (GLib.Variant)")
150 }
151
152 if (!params.returnTypes)
153 params.returnTypes = ''
154
155 if (!params.onSuccess)
156 params.onSuccess = function() {}
157
158 if (!params.onError)
159 params.onError = function(error) {
160 Logger.warn("XmlLessDBusProxy::call: DBus error: "+error)
161 }
162
163 this.connection.call(this.name,
164 this.path,
165 this.interface,
166 params.name,
167 params.params,
168 GLib.VariantType.new('(' + params.returnTypes + ')'),
169 Gio.DBusCallFlags.NONE,
170 -1,
171 null,
172 function(conn, result) {
173 try {
174 let returnVariant = conn.call_finish(result)
175 params.onSuccess(returnVariant.deep_unpack())
176 } catch (e) {
177 params.onError(e)
178 }
179 })
180
181 },
182
183 destroy: function() {
184 this.emit('-destroy')
185
186 this.disconnectAll()
187
188 this.connection.signal_unsubscribe(this._signalId)
189 this.connection.signal_unsubscribe(this._propChangedId)
190 }
191 })
192 Signals.addSignalMethods(XmlLessDBusProxy.prototype)
193
84 const connectSmart = function() {
85 if (arguments.length == 4)
86 return connectSmart4A.apply(null, arguments)
87 else
88 return connectSmart3A.apply(null, arguments)
89 }
19490
19591 /**
19692 * Helper class for logging stuff