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
30 | 30 | const DBusMenu = Extension.imports.dbusMenu; |
31 | 31 | const IconCache = Extension.imports.iconCache; |
32 | 32 | const Util = Extension.imports.util; |
33 | const Interfaces = Extension.imports.interfaces; | |
33 | 34 | |
34 | 35 | const SNICategory = { |
35 | 36 | APPLICATION: 'ApplicationStatus', |
55 | 56 | this.busName = bus_name |
56 | 57 | this._uniqueId = bus_name + object |
57 | 58 | |
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') | |
87 | 85 | }, |
88 | 86 | |
89 | 87 | // 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) { | |
91 | 89 | if (signal.substr(0, 3) == 'New') { |
92 | 90 | let prop = signal.substr(3) |
93 | 91 | |
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') | |
102 | 100 | } else if (signal == 'XAyatanaNewLabel') { |
103 | 101 | // 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') | |
105 | 103 | } |
106 | 104 | }, |
107 | 105 | |
108 | 106 | //public property getters |
109 | 107 | get title() { |
110 | return this._proxy.cachedProperties.Title; | |
108 | return this._proxy.Title; | |
111 | 109 | }, |
112 | 110 | get id() { |
113 | return this._proxy.cachedProperties.Id; | |
111 | return this._proxy.Id; | |
114 | 112 | }, |
115 | 113 | get uniqueId() { |
116 | 114 | return this._uniqueId; |
117 | 115 | }, |
118 | 116 | get status() { |
119 | return this._proxy.cachedProperties.Status; | |
117 | return this._proxy.Status; | |
120 | 118 | }, |
121 | 119 | 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 | |
123 | 124 | }, |
124 | 125 | get menuPath() { |
125 | return this._proxy.cachedProperties.Menu || "/MenuBar" | |
126 | return this._proxy.Menu || "/MenuBar" | |
126 | 127 | }, |
127 | 128 | |
128 | 129 | get attentionIcon() { |
129 | 130 | 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 | |
133 | 134 | ] |
134 | 135 | }, |
135 | 136 | |
136 | 137 | get icon() { |
137 | 138 | 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 | |
141 | 142 | ] |
142 | 143 | }, |
143 | 144 | |
144 | 145 | get overlayIcon() { |
145 | 146 | 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 | |
149 | 150 | ] |
150 | 151 | }, |
151 | 152 | |
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') | |
182 | 187 | }, |
183 | 188 | |
184 | 189 | destroy: function() { |
185 | 190 | this.emit('destroy') |
186 | 191 | |
187 | 192 | this.disconnectAll() |
188 | this._proxy.destroy() | |
193 | delete this._proxy | |
189 | 194 | }, |
190 | 195 | |
191 | 196 | open: function() { |
193 | 198 | // nor can we call any X11 functions. Luckily, the Activate method usually works fine. |
194 | 199 | // parameters are "an hint to the item where to show eventual windows" [sic] |
195 | 200 | // ... 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) | |
202 | 202 | }, |
203 | 203 | |
204 | 204 | scroll: function(dx, dy) { |
205 | 205 | 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') | |
211 | 207 | |
212 | 208 | 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') | |
218 | 210 | } |
219 | 211 | }); |
220 | 212 | Signals.addSignalMethods(AppIndicator.prototype); |
35 | 35 | <arg name="x" type="i" direction="in"/> |
36 | 36 | <arg name="y" type="i" direction="in"/> |
37 | 37 | </method> |
38 | <method name="Scroll"> | |
39 | <arg name="delta" type="i" direction="in"/> | |
40 | <arg name="dir" type="s" direction="in"/> | |
41 | </method> | |
38 | 42 | |
39 | 43 | <!-- Signals: the client wants to change something in the status--> |
40 | 44 | <signal name="NewTitle"></signal> |
50 | 54 | </signal> |
51 | 55 | |
52 | 56 | <!-- 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"> | |
54 | 61 | <arg type="s" name="label" direction="out" /> |
55 | 62 | <arg type="s" name="guide" direction="out" /> |
56 | 63 | </signal> |
57 | 64 | <property name="XAyatanaLabel" type="s" access="read" /> |
58 | <property name="XAyatanaLabelGuide" type="s" access="read" /> <!-- unimplemented --> | |
65 | <property name="XAyatanaLabelGuide" type="s" access="read" />--> | |
59 | 66 | |
60 | 67 | |
61 | 68 | </interface> |
14 | 14 | // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
15 | 15 | const Gio = imports.gi.Gio |
16 | 16 | const GLib = imports.gi.GLib |
17 | const GObject = imports.gi.GObject | |
17 | 18 | |
18 | 19 | const Lang = imports.lang |
19 | 20 | const Signals = imports.signals |
20 | 21 | |
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 | ||
21 | 75 | /** |
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() { ... }) | |
25 | 83 | */ |
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 | } | |
194 | 90 | |
195 | 91 | /** |
196 | 92 | * Helper class for logging stuff |