xapp-sn-watcher: rewrite in C due to leaky dbus python bindings.
Michael Webster
4 years ago
13 | 13 | libglib2.0-dev (>= 2.37.3), |
14 | 14 | libgnomekbd-dev, |
15 | 15 | libgtk-3-dev (>= 3.3.16), |
16 | libdbusmenu-gtk3-dev, | |
16 | 17 | libx11-dev, |
17 | 18 | libxkbfile-dev, |
18 | 19 | meson, |
50 | 51 | gir1.2-xapp-1.0 (= ${binary:Version}), |
51 | 52 | libgnomekbd-dev, |
52 | 53 | libgtk-3-dev (>= 3.3.16), |
53 | gir1.2-dbusmenu-gtk3-0.4, | |
54 | 54 | libxapp1 (= ${binary:Version}), |
55 | 55 | libxkbfile-dev, |
56 | 56 | ${misc:Depends}, |
1 | 1 | usr/bin/ |
2 | 2 | usr/share/icons |
3 | 3 | usr/share/locale |
4 | usr/libexec/xapps | |
4 | usr/libexec/xapps/*.py | |
5 | 5 | usr/share/mate-panel/applets |
6 | 6 | usr/share/dbus-1/services |
33 | 33 | 'xapp-status-icon-monitor.c' |
34 | 34 | ] |
35 | 35 | |
36 | codegen = find_program('g-codegen.py') | |
37 | ||
38 | 36 | dbus_headers = [] |
39 | 37 | |
40 | 38 | # FIXME: Ugly workaround that simulates the generation of |
60 | 58 | dbus_headers += xapp_statusicon_interface_sources[0] |
61 | 59 | xapp_sources += xapp_statusicon_interface_sources[1] |
62 | 60 | |
63 | fdo_sn_watcher_interface_sources = custom_target( | |
64 | 'fdo-sn-watcher-interface', | |
65 | input: 'sn-watcher.xml', | |
66 | output: ['fdo-sn-watcher-interface.h', 'fdo-sn-watcher-interface.c'], | |
67 | command: [ | |
68 | codegen, | |
69 | 'org.kde.StatusNotifierWatcher', | |
70 | 'fdo-sn-watcher-interface', | |
71 | 'FdoSnWatcher', | |
72 | meson.current_build_dir(), | |
73 | '@INPUT@', '@OUTPUT@' | |
74 | ] | |
75 | ) | |
76 | ||
77 | dbus_headers += fdo_sn_watcher_interface_sources[0] | |
78 | xapp_sources += fdo_sn_watcher_interface_sources[1] | |
79 | ||
80 | fdo_sn_item_interface_sources = custom_target( | |
81 | 'fdo-sn-item-interface', | |
82 | input: 'sn-item.xml', | |
83 | output: ['fdo-sn-item-interface.h', 'fdo-sn-item-interface.c'], | |
84 | command: [ | |
85 | codegen, | |
86 | 'org.kde.StatusNotifierItem', | |
87 | 'fdo-sn-item-interface', | |
88 | 'FdoSnItem', | |
89 | meson.current_build_dir(), | |
90 | '@INPUT@', '@OUTPUT@' | |
91 | ] | |
92 | ) | |
93 | ||
94 | dbus_headers += fdo_sn_item_interface_sources[0] | |
95 | xapp_sources += fdo_sn_item_interface_sources[1] | |
96 | ||
97 | 61 | # You can't actually access the generated header udring the install_header command below, |
98 | 62 | # because the command is evaluated prior to the files being generated. So we need to manually |
99 | 63 | # install the dbus header file (custom install scripts really *do* get evaluated after build, |
100 | 64 | # during the install phase.) |
101 | meson.add_install_script('install_generated_header.py', 'xapp-statusicon-interface.h') | |
65 | codegen = find_program(join_paths(meson.source_root(), 'meson-scripts', 'g-codegen.py')) | |
102 | 66 | |
103 | # dbus_headers += generated_sources[0] | |
104 | # xapp_sources += generated_sources[1] | |
67 | meson.add_install_script(join_paths(meson.source_root(), 'meson-scripts', 'install_generated_header.py'), | |
68 | 'xapp-statusicon-interface.h' | |
69 | ) | |
105 | 70 | |
106 | 71 | xapp_enums = gnome.mkenums('xapp-enums', |
107 | 72 | sources : xapp_headers, |
0 | <?xml version="1.0" encoding="UTF-8"?> | |
1 | ||
2 | <node> | |
3 | <interface name="org.kde.StatusNotifierItem"> | |
4 | <property name="Category" type="s" access="read"/> | |
5 | <property name="Id" type="s" access="read"/> | |
6 | <property name="Title" type="s" access="read"/> | |
7 | <property name="Status" type="s" access="read"/> | |
8 | <property name="WindowId" type="i" access="read"/> | |
9 | <property name="Menu" type="o" access="read" /> | |
10 | ||
11 | <!-- main icon --> | |
12 | <!-- names are preferred over pixmaps --> | |
13 | <property name="IconName" type="s" access="read" /> | |
14 | <property name="IconThemePath" type="s" access="read" /> | |
15 | ||
16 | <!-- struct containing width, height and image data--> | |
17 | <!-- implementation has been dropped as of now --> | |
18 | <property name="IconPixmap" type="a(iiay)" access="read" /> | |
19 | ||
20 | <!-- not used in ayatana code, no test case so far --> | |
21 | <property name="OverlayIconName" type="s" access="read"/> | |
22 | <property name="OverlayIconPixmap" type="a(iiay)" access="read" /> | |
23 | ||
24 | <!-- Requesting attention icon --> | |
25 | <property name="AttentionIconName" type="s" access="read"/> | |
26 | ||
27 | <!--same definition as image--> | |
28 | <property name="AttentionIconPixmap" type="a(iiay)" access="read" /> | |
29 | ||
30 | <!-- tooltip data --> | |
31 | <!-- unimplemented as of now --> | |
32 | <!--(iiay) is an image--> | |
33 | <property name="ToolTip" type="(sa(iiay)ss)" access="read" /> | |
34 | ||
35 | ||
36 | <method name="Activate"> | |
37 | <arg name="x" type="i" direction="in"/> | |
38 | <arg name="y" type="i" direction="in"/> | |
39 | </method> | |
40 | <method name="SecondaryActivate"> | |
41 | <arg name="x" type="i" direction="in"/> | |
42 | <arg name="y" type="i" direction="in"/> | |
43 | </method> | |
44 | <method name="ContextMenu"> | |
45 | <arg name="x" type="i" direction="in"/> | |
46 | <arg name="y" type="i" direction="in"/> | |
47 | </method> | |
48 | <method name="Scroll"> | |
49 | <arg name="delta" type="i" direction="in"/> | |
50 | <arg name="dir" type="s" direction="in"/> | |
51 | </method> | |
52 | ||
53 | ||
54 | <!-- Signals: the client wants to change something in the status--> | |
55 | <signal name="NewTitle"></signal> | |
56 | <signal name="NewIcon"></signal> | |
57 | <signal name="NewIconThemePath"> | |
58 | <arg type="s" name="icon_theme_path" direction="out" /> | |
59 | </signal> | |
60 | <signal name="NewAttentionIcon"></signal> | |
61 | <signal name="NewOverlayIcon"></signal> | |
62 | <signal name="NewMenu"></signal> | |
63 | <signal name="NewToolTip"></signal> | |
64 | <signal name="NewStatus"> | |
65 | <arg name="status" type="s" /> | |
66 | </signal> | |
67 | ||
68 | <!-- ayatana labels --> | |
69 | <!-- These are commented out because GDBusProxy would otherwise require them, | |
70 | but they are not available for KDE indicators | |
71 | --> | |
72 | <signal name="XAyatanaNewLabel"> | |
73 | <arg type="s" name="label" direction="out" /> | |
74 | <arg type="s" name="guide" direction="out" /> | |
75 | </signal> | |
76 | <property name="XAyatanaLabel" type="s" access="read" /> | |
77 | <property name="XAyatanaLabelGuide" type="s" access="read" /> | |
78 | ||
79 | ||
80 | </interface> | |
81 | </node> |
0 | <?xml version="1.0" encoding="UTF-8"?> | |
1 | ||
2 | <node name="/StatusNotifierWatcher"> | |
3 | <interface name="org.kde.StatusNotifierWatcher"> | |
4 | <annotation name="org.gtk.GDBus.C.Name" value="SnWatcher" /> | |
5 | ||
6 | <method name="RegisterStatusNotifierItem"> | |
7 | <arg name="service" type="s" direction="in" /> | |
8 | </method> | |
9 | ||
10 | <method name="RegisterStatusNotifierHost"> | |
11 | <arg name="service" type="s" direction="in" /> | |
12 | </method> | |
13 | ||
14 | <property name="RegisteredStatusNotifierItems" type="as" access="read" /> | |
15 | <property name="IsStatusNotifierHostRegistered" type="b" access="read" /> | |
16 | <property name="ProtocolVersion" type="i" access="read" /> | |
17 | ||
18 | <signal name="StatusNotifierItemRegistered"> | |
19 | <arg type="s" name="service" direction="out" /> | |
20 | </signal> | |
21 | ||
22 | <signal name="StatusNotifierItemUnregistered"> | |
23 | <arg type="s" name="service" direction="out" /> | |
24 | </signal> | |
25 | ||
26 | <signal name="StatusNotifierHostRegistered" /> | |
27 | </interface> | |
28 | </node> |
27 | 27 | #define MAX_NAME_FAILS 3 |
28 | 28 | |
29 | 29 | #define MAX_SANE_ICON_SIZE 96 |
30 | #define FALLBACK_ICON_SIZE 24 | |
30 | 31 | |
31 | 32 | static gint unique_id = 0; |
32 | 33 | |
1492 | 1493 | gint |
1493 | 1494 | xapp_status_icon_get_icon_size (XAppStatusIcon *icon) |
1494 | 1495 | { |
1495 | g_return_val_if_fail (XAPP_IS_STATUS_ICON (icon), 0); | |
1496 | g_return_val_if_fail (XAPP_IS_STATUS_ICON (icon), FALLBACK_ICON_SIZE); | |
1497 | ||
1498 | if (icon->priv->skeleton == NULL) | |
1499 | { | |
1500 | g_debug ("XAppStatusIcon get_icon_size: %d (fallback)", FALLBACK_ICON_SIZE); | |
1501 | ||
1502 | return FALLBACK_ICON_SIZE; | |
1503 | } | |
1496 | 1504 | |
1497 | 1505 | gint size; |
1498 | 1506 |
0 | #!/usr/bin/env python3 | |
1 | ||
2 | ''' | |
3 | FIXME | |
4 | ||
5 | This script is used only to call gdbus-codegen and simulate the | |
6 | generation of the source code and header as different targets. | |
7 | ||
8 | Both are generated implicitly, so meson is not able to know how | |
9 | many files are generated, so it does generate only one opaque | |
10 | target that represents the two files. | |
11 | ||
12 | originally from: | |
13 | https://gitlab.gnome.org/GNOME/gnome-settings-daemon/commit/5924d72931a030b24554116a48140a661a99652b | |
14 | ||
15 | Please see: | |
16 | https://bugzilla.gnome.org/show_bug.cgi?id=791015 | |
17 | https://github.com/mesonbuild/meson/pull/2930 | |
18 | ''' | |
19 | ||
20 | import subprocess | |
21 | import sys | |
22 | import os | |
23 | ||
24 | subprocess.call([ | |
25 | 'gdbus-codegen', | |
26 | '--interface-prefix=' + sys.argv[1], | |
27 | '--generate-c-code=' + os.path.join(sys.argv[4], sys.argv[2]), | |
28 | '--c-namespace=XApp', | |
29 | '--annotate', sys.argv[1], 'org.gtk.GDBus.C.Name', sys.argv[3], | |
30 | sys.argv[5] | |
31 | ]) |
0 | #!/usr/bin/python3 | |
1 | ||
2 | import os | |
3 | import sys | |
4 | import subprocess | |
5 | ||
6 | install_dir = os.path.join(os.environ['MESON_INSTALL_DESTDIR_PREFIX'], 'include', 'xapp', 'libxapp') | |
7 | header_path = os.path.join(os.environ['MESON_BUILD_ROOT'], 'libxapp', sys.argv[1]) | |
8 | ||
9 | print("\nInstalling generated header '%s' to %s\n" % (sys.argv[1], install_dir)) | |
10 | ||
11 | subprocess.call(['cp', header_path, install_dir]) |
34 | 34 | ) |
35 | 35 | |
36 | 36 | top_inc = include_directories('.') |
37 | codegen = find_program(join_paths(meson.source_root(), 'meson-scripts', 'g-codegen.py')) | |
37 | 38 | |
38 | 39 | subdir('icons') |
39 | 40 | subdir('libxapp') |
0 | #!/usr/bin/python3 | |
1 | ||
2 | import locale | |
3 | import gettext | |
4 | import os | |
5 | import functools | |
6 | import sys | |
7 | import setproctitle | |
8 | ||
9 | import cairo | |
10 | ||
11 | import gi | |
12 | gi.require_version("Gtk", "3.0") | |
13 | gi.require_version("XApp", "1.0") | |
14 | gi.require_version("DbusmenuGtk3", "0.4") | |
15 | from gi.repository import Gtk, GdkPixbuf, Gdk, GObject, Gio, XApp, GLib, DbusmenuGtk3 | |
16 | ||
17 | from notifierItem import SnItem | |
18 | ||
19 | FALLBACK_ICON_SIZE = 24 | |
20 | ||
21 | class SnItemWrapper(GObject.Object): | |
22 | def __init__(self, sn_item_proxy): | |
23 | GObject.Object.__init__(self) | |
24 | ||
25 | self.sn_item = SnItem(sn_item_proxy) | |
26 | ||
27 | self.sn_item.connect("ready", lambda p: self.sn_item_ready()) | |
28 | self.sn_item.connect("update-status", lambda p: self.update_status()) | |
29 | self.sn_item.connect("update-icon", lambda p: self.update_icon()) | |
30 | self.sn_item.connect("update-menu", lambda p: self.update_menu()) | |
31 | self.sn_item.connect("update-tooltip", lambda p: self.update_tooltip()) | |
32 | ||
33 | self.status = "Passive" | |
34 | self.icon_theme_path = None | |
35 | self.menu = None | |
36 | self.gtk_menu = None | |
37 | ||
38 | self.old_png_path = None | |
39 | self.png_path = None | |
40 | self.current_icon_id = 0 | |
41 | ||
42 | def sn_item_ready(self): | |
43 | self.xapp_icon = XApp.StatusIcon() | |
44 | self.xapp_icon.set_name(self.sn_item.id()) | |
45 | self.xapp_icon.connect("activate", self.on_xapp_icon_activated) | |
46 | self.xapp_icon.connect("button-press-event", self.on_xapp_button_pressed) | |
47 | self.xapp_icon.connect("button-release-event", self.on_xapp_button_released) | |
48 | self.xapp_icon.connect("scroll-event", self.on_xapp_scroll_event) | |
49 | self.xapp_icon.connect("state-changed", self.xapp_icon_state_changed); | |
50 | ||
51 | def xapp_icon_state_changed(self, state, data=None): | |
52 | if state != XApp.StatusIconState.NO_SUPPORT: | |
53 | self.update_all() | |
54 | ||
55 | def update_all(self): | |
56 | self.update_status() | |
57 | self.update_icon() | |
58 | self.update_menu() | |
59 | self.update_tooltip() | |
60 | ||
61 | def destroy(self): | |
62 | # print("Destroying itemWrapper") | |
63 | try: | |
64 | os.unlink(self.png_path) | |
65 | except: | |
66 | pass | |
67 | ||
68 | try: | |
69 | self.sn_item.destroy() | |
70 | self.sn_item = None | |
71 | except Exception as e: | |
72 | print(str(e)) | |
73 | ||
74 | self.xapp_icon = None | |
75 | ||
76 | def on_xapp_icon_activated(self, icon, button, time, data=None): | |
77 | pass | |
78 | ||
79 | def on_xapp_button_pressed(self, icon, x, y, button, _time, panel_position): | |
80 | # Use this for activate so we can pass the x,y coordinates | |
81 | self.sn_item.activate(button, x, y) | |
82 | ||
83 | def on_xapp_button_released(self, icon, x, y, button, _time, panel_position): | |
84 | if not self.gtk_menu: | |
85 | self.sn_item.show_context_menu(button, x, y) | |
86 | ||
87 | def on_xapp_scroll_event(self, icon, delta, direction, _time): | |
88 | o_str = "horizontal" if direction in (XApp.ScrollDirection.LEFT, XApp.ScrollDirection.RIGHT) else "vertical" | |
89 | ||
90 | self.sn_item.scroll(delta, o_str) | |
91 | ||
92 | def update_menu(self): | |
93 | # print("ItemIsMenu: ", self.sn_item.item_is_menu()) | |
94 | menu_path = self.sn_item.menu() | |
95 | ||
96 | if menu_path == None or menu_path == "": | |
97 | self.menu = None | |
98 | self.gtk_menu = None | |
99 | self.xapp_icon.set_secondary_menu(None) | |
100 | return | |
101 | ||
102 | self.gtk_menu = DbusmenuGtk3.Menu.new(self.sn_item.sn_item_proxy.get_name(), menu_path) | |
103 | ||
104 | self.xapp_icon.set_secondary_menu(self.gtk_menu) | |
105 | ||
106 | def update_tooltip(self): | |
107 | tooltip = self.sn_item.tooltip() | |
108 | ||
109 | self.xapp_icon.set_tooltip_text(tooltip) | |
110 | ||
111 | def update_status(self): | |
112 | # print(self, self.sn_item) | |
113 | self.status = self.sn_item.status() | |
114 | ||
115 | if self.status == "Passive": | |
116 | self.xapp_icon.set_visible(False) | |
117 | return | |
118 | ||
119 | self.xapp_icon.set_visible(True) | |
120 | ||
121 | def update_icon(self): | |
122 | i = self.sn_item | |
123 | self.icon_theme_path = i.icon_theme_path() | |
124 | ||
125 | # print("IconName: '%s', OverlayIconName: '%s', AttentionIconName: '%s', \n" | |
126 | # "IconPixmap: %d, OverlayIconPixmap: %d, AttentionIconPixmap: %d, \n" | |
127 | # "Path: %s, Status: %s" | |
128 | # % (i.icon_name(), i.overlay_icon_name(), i.att_icon_name(), | |
129 | # i.icon_pixmap() != None, i.overlay_icon_pixmap() != None, i.att_icon_pixmap() != None, | |
130 | # i.icon_theme_path(), i.status())) | |
131 | ||
132 | if self.status == "NeedsAttention": | |
133 | if i.att_icon_name() or i.att_icon_pixmap(): | |
134 | self.set_icon(i.att_icon_name(), | |
135 | i.att_icon_pixmap(), | |
136 | i.overlay_icon_name(), | |
137 | i.ovrelay_icon_pixmap()) | |
138 | return | |
139 | ||
140 | self.set_icon(i.icon_name(), | |
141 | i.icon_pixmap(), | |
142 | i.overlay_icon_name(), | |
143 | i.overlay_icon_pixmap()) | |
144 | ||
145 | def set_icon(self, primary_name, primary_pixmap, overlay_name, overlay_pixmap): | |
146 | if overlay_name or overlay_pixmap: | |
147 | pass # Not worrying about this for now | |
148 | self.build_composite_icon(primary_name, | |
149 | primary_pixmap, | |
150 | overlay_name, | |
151 | overlay_pixmap) | |
152 | return | |
153 | ||
154 | if primary_name: | |
155 | # absolute path provided | |
156 | if os.path.isabs(primary_name): | |
157 | self.xapp_icon.set_icon_name(primary_name) | |
158 | return | |
159 | ||
160 | # icon name provided, with custom path | |
161 | if self.icon_theme_path != None: | |
162 | for x in (".svg", ".png"): | |
163 | path = os.path.join(self.icon_theme_path, primary_name + x) | |
164 | if os.path.exists(path): | |
165 | self.xapp_icon.set_icon_name(path) | |
166 | return | |
167 | ||
168 | self.xapp_icon.set_icon_name(primary_name) | |
169 | return | |
170 | ||
171 | if primary_pixmap: | |
172 | path = self.create_png_file(primary_pixmap) | |
173 | ||
174 | if path: | |
175 | self.xapp_icon.set_icon_name(path) | |
176 | GLib.timeout_add_seconds(1, self.remove_old_tmpfile) | |
177 | ||
178 | def remove_old_tmpfile(self): | |
179 | try: | |
180 | os.unlink(self.old_png_path) | |
181 | except: | |
182 | pass | |
183 | ||
184 | return GLib.SOURCE_REMOVE | |
185 | ||
186 | def create_png_file(self, primary_pixmap): | |
187 | best_size = self.xapp_icon.props.icon_size if self.xapp_icon.props.icon_size > 0 else FALLBACK_ICON_SIZE | |
188 | ||
189 | # Sort smallest to largest | |
190 | def cmp_icon_sizes(a, b): | |
191 | area_a = a[0] * a[1] | |
192 | area_b = b[0] * b[1] | |
193 | return area_a < area_b | |
194 | ||
195 | sorted_icons = sorted(primary_pixmap, key=functools.cmp_to_key(cmp_icon_sizes)) | |
196 | pixmap_to_use = primary_pixmap[0] | |
197 | ||
198 | if len(sorted_icons) > 1: | |
199 | best_index = 0 | |
200 | ||
201 | for x in range(0, len(sorted_icons)): | |
202 | pixmap = sorted_icons[x] | |
203 | ||
204 | width = pixmap[0] | |
205 | height = pixmap[1] | |
206 | ||
207 | if width <= best_size and height <= best_size: | |
208 | pixmap_to_use = sorted_icons[x] | |
209 | continue | |
210 | else: | |
211 | break | |
212 | ||
213 | surface = self.surface_from_pixmap_array(pixmap_to_use) | |
214 | ||
215 | if surface: | |
216 | self.old_png_path = self.png_path | |
217 | self.png_path = os.path.join(GLib.get_tmp_dir(), "xapp-tmp-%s-%d.png" % (hash(self), self.get_icon_id())) | |
218 | ||
219 | try: | |
220 | surface.write_to_png(self.png_path) | |
221 | except Exception as e: | |
222 | print("Failed to save png of status icon: %s" % e) | |
223 | ||
224 | return self.png_path | |
225 | ||
226 | return None | |
227 | ||
228 | def surface_from_pixmap_array(self, pixmap_array): | |
229 | surface = None | |
230 | ||
231 | width, height, b = pixmap_array | |
232 | rowstride = width * 4 # (argb) | |
233 | ||
234 | # convert argb to rgba | |
235 | i = 0 | |
236 | ||
237 | while i < 4 * width * height: | |
238 | alpha = b[i ] | |
239 | b[i ] = b[i + 1] | |
240 | b[i + 1] = b[i + 2] | |
241 | b[i + 2] = b[i + 3] | |
242 | b[i + 3] = alpha | |
243 | ||
244 | i += 4 | |
245 | ||
246 | pixbuf = GdkPixbuf.Pixbuf.new_from_bytes(GLib.Bytes.new(b), | |
247 | GdkPixbuf.Colorspace.RGB, | |
248 | True, 8, | |
249 | width, height, | |
250 | rowstride) | |
251 | ||
252 | if pixbuf: | |
253 | scale = 1 | |
254 | ||
255 | sval = GObject.Value(int) | |
256 | screen = Gdk.Screen.get_default() | |
257 | ||
258 | if screen.get_setting("gdk-window-scaling-factor", sval): | |
259 | scale = sval.get_int() | |
260 | ||
261 | surface = Gdk.cairo_surface_create_from_pixbuf(pixbuf, | |
262 | scale, | |
263 | None) | |
264 | ||
265 | return surface | |
266 | ||
267 | def get_icon_id(self): | |
268 | self.current_icon_id = 1 if self.current_icon_id == 0 else 0 | |
269 | return self.current_icon_id | |
270 | ||
271 | # TODO: pixmaps | |
272 | ||
273 | def build_composite_icon(self, primary_name, primary_pixmap, overlay_name, overlay_pixmap): | |
274 | # TODO: bleh | |
275 | pass | |
276 |
0 | sn_watcher_generated = gnome.gdbus_codegen( | |
1 | 'sn-watcher-interface', | |
2 | 'sn-watcher.xml', | |
3 | interface_prefix: 'org.x.' | |
4 | ) | |
0 | 5 | |
1 | libexec_files = [ | |
2 | 'itemWrapper.py', | |
3 | 'nameWatcher.py', | |
4 | 'notifierItem.py', | |
5 | 'util.py', | |
6 | 'xapp-sn-watcher.py' | |
7 | ] | |
6 | sn_item_generated = gnome.gdbus_codegen( | |
7 | 'sn-item-interface', | |
8 | 'sn-item.xml', | |
9 | interface_prefix: 'org.x.' | |
10 | ) | |
8 | 11 | |
9 | 12 | libexec_path = join_paths(get_option('prefix'), get_option('libexecdir'), 'xapps', 'sn-watcher') |
10 | ||
11 | install_data(libexec_files, | |
12 | install_dir: libexec_path | |
13 | ) | |
14 | 13 | |
15 | 14 | ## DBus service file |
16 | 15 | |
26 | 25 | install_data(service_file, |
27 | 26 | install_dir: dbus_services_dir |
28 | 27 | ) |
28 | ||
29 | dbusmenu = dependency('dbusmenu-gtk3-0.4', required: true) | |
30 | cairo = dependency('cairo-gobject', required: true) | |
31 | ||
32 | watcher_sources = [ | |
33 | sn_watcher_generated, | |
34 | sn_item_generated, | |
35 | 'xapp-sn-watcher.c', | |
36 | 'sn-item.c' | |
37 | ] | |
38 | ||
39 | watcher = executable('xapp-sn-watcher', | |
40 | watcher_sources, | |
41 | include_directories: [ top_inc ], | |
42 | dependencies: [libxapp_dep, dbusmenu, cairo], | |
43 | install_dir: libexec_path, | |
44 | install: true | |
45 | )⏎ |
0 | #!/usr/bin/python3 | |
1 | ||
2 | import gi | |
3 | from gi.repository import GObject, Gio, GLib | |
4 | ||
5 | import util | |
6 | ||
7 | class BusNameWatcher(GObject.Object): | |
8 | """ | |
9 | We can't rely on our StatusNotifier instances to tell us when their NameOwner | |
10 | changes. | |
11 | """ | |
12 | __gsignals__ = { | |
13 | "owner-lost": (GObject.SignalFlags.RUN_LAST, None, (str,str)), | |
14 | "owner-appeared": (GObject.SignalFlags.RUN_LAST, None, (str,str)) | |
15 | } | |
16 | def __init__(self): | |
17 | """ | |
18 | Connect to the bus and retrieve a list of interfaces. | |
19 | """ | |
20 | super(BusNameWatcher, self).__init__() | |
21 | ||
22 | Gio.DBusProxy.new_for_bus(Gio.BusType.SESSION, | |
23 | Gio.DBusProxyFlags.NONE, | |
24 | None, | |
25 | "org.freedesktop.DBus", | |
26 | "/org/freedesktop/DBus", | |
27 | "org.freedesktop.DBus", | |
28 | None, | |
29 | self.connected) | |
30 | ||
31 | def connected(self, source, result, data=None): | |
32 | try: | |
33 | self.proxy = Gio.DBusProxy.new_for_bus_finish(result) | |
34 | except GLib.Error as e: | |
35 | # FIXME: what to do here? | |
36 | return | |
37 | ||
38 | self.proxy.connect("g-signal", | |
39 | self.signal_received) | |
40 | ||
41 | def signal_received(self, proxy, sender, signal, parameters, data=None): | |
42 | if signal == "NameOwnerChanged": | |
43 | # name, old_owner, new_owner | |
44 | if parameters[2] == "": | |
45 | self.name_lost(parameters[0], parameters[1]) | |
46 | else: | |
47 | self.name_appeared(parameters[0], parameters[2]) | |
48 | ||
49 | @util._idle | |
50 | def name_lost(self, name, old_name_owner): | |
51 | self.emit("owner-lost", name, old_name_owner) | |
52 | ||
53 | @util._idle | |
54 | def name_appeared(self, name, new_owner): | |
55 | self.emit("owner-appeared", name, new_owner) | |
56 | ||
57 | def destroy(self): | |
58 | try: | |
59 | # print("Destroying nameWatcher") | |
60 | self.proxy.disconnect_by_func(self.signal_received) | |
61 | self.proxy = None | |
62 | except Exception as e: | |
63 | print(str(e)) |
0 | #!/usr/bin/python3 | |
1 | ||
2 | import gi | |
3 | from gi.repository import GObject, Gio, GLib, Gdk | |
4 | ||
5 | # We shouldn't need this class but appindicator doesn't cache their properties so it's better | |
6 | # to hide the ugliness of fetching properties in here. If this situation changes it will be easier | |
7 | # to modify the behavior on its own. | |
8 | ||
9 | APPINDICATOR_PATH_PREFIX = "/org/ayatana/NotificationItem/" | |
10 | ||
11 | class SnItem(GObject.Object): | |
12 | __gsignals__ = { | |
13 | "ready": (GObject.SignalFlags.RUN_LAST, None, ()), | |
14 | "update-icon": (GObject.SignalFlags.RUN_LAST, None, ()), | |
15 | "update-status": (GObject.SignalFlags.RUN_LAST, None, ()), | |
16 | "update-menu": (GObject.SignalFlags.RUN_LAST, None, ()), | |
17 | "update-tooltip": (GObject.SignalFlags.RUN_LAST, None, ()) | |
18 | } | |
19 | def __init__(self, sn_item_proxy): | |
20 | GObject.Object.__init__(self) | |
21 | ||
22 | self.sn_item_proxy = sn_item_proxy | |
23 | self.prop_proxy = None | |
24 | self.ready = False | |
25 | self.update_icon_id = 0 | |
26 | ||
27 | self._status = "Active" | |
28 | ||
29 | Gio.DBusProxy.new_for_bus(Gio.BusType.SESSION, | |
30 | Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES, | |
31 | None, | |
32 | self.sn_item_proxy.get_name(), | |
33 | self.sn_item_proxy.get_object_path(), | |
34 | "org.freedesktop.DBus.Properties", | |
35 | None, | |
36 | self.prop_proxy_acquired) | |
37 | ||
38 | def prop_proxy_acquired(self, source, result, data=None): | |
39 | try: | |
40 | self.prop_proxy = Gio.DBusProxy.new_for_bus_finish(result) | |
41 | except GLib.Error as e: | |
42 | print(e.message) | |
43 | # FIXME: what to do here? | |
44 | return | |
45 | ||
46 | self.sn_item_proxy.connect("g-signal", | |
47 | self.signal_received) | |
48 | ||
49 | self.emit("ready") | |
50 | ||
51 | def signal_received(self, proxy, sender, signal, parameters, data=None): | |
52 | if self.prop_proxy == None: | |
53 | return | |
54 | ||
55 | # print("Signal from %s: %s" % (self.sn_item_proxy.get_name(), signal)) | |
56 | if signal in ("NewIcon", | |
57 | "NewAttentionIcon", | |
58 | "NewOverlayIcon"): | |
59 | self._emit_update_icon_signal() | |
60 | elif signal == "NewStatus": | |
61 | # libappindicator sends NewStatus during its dispose phase - by the time we want to act | |
62 | # on it, we can no longer fetch the status via Get, so we'll cache the status we receive | |
63 | # in the signal, in case this happens we can send it as a default with our own update-status | |
64 | # signal. | |
65 | self._status = parameters[0] | |
66 | self.emit("update-status") | |
67 | elif signal in ("NewMenu"): | |
68 | self.emit("update-menu") | |
69 | elif signal in ("XAyatanaNewLabel", | |
70 | "Tooltip"): | |
71 | self.emit("update-tooltip") | |
72 | ||
73 | def _emit_update_icon_signal(self): | |
74 | if self.update_icon_id > 0: | |
75 | GLib.source_remove(self.update_icon_id) | |
76 | self.update_icon_id = 0 | |
77 | ||
78 | self.update_icon_id = GLib.timeout_add(25, self._emit_update_icon_cb) | |
79 | ||
80 | def _emit_update_icon_cb(self): | |
81 | if self.sn_item_proxy != None: | |
82 | self.emit("update-icon") | |
83 | ||
84 | self.update_icon_id = 0 | |
85 | return GLib.SOURCE_REMOVE | |
86 | ||
87 | def _get_property(self, name): | |
88 | res = self.prop_proxy.call_sync("Get", | |
89 | GLib.Variant("(ss)", | |
90 | (self.sn_item_proxy.get_interface_name(), | |
91 | name)), | |
92 | Gio.DBusCallFlags.NONE, | |
93 | 5 * 1000, | |
94 | None) | |
95 | ||
96 | return res | |
97 | ||
98 | def _get_string_prop(self, name, default=""): | |
99 | try: | |
100 | res = self._get_property(name) | |
101 | if res[0] == "": | |
102 | return default | |
103 | return res[0] | |
104 | except GLib.Error as e: | |
105 | if e.code != Gio.DBusError.INVALID_ARGS: | |
106 | print("Couldn't get %s property: %s... or this is libappindicator's closing Status update" % (name, e.message)) | |
107 | ||
108 | return default | |
109 | ||
110 | def _get_bool_prop(self, name, default=False): | |
111 | try: | |
112 | res = self._get_property(name) | |
113 | ||
114 | return res[0] | |
115 | except GLib.Error as e: | |
116 | if e.code != Gio.DBusError.INVALID_ARGS: | |
117 | print("Couldn't get %s property: %s" % (name, e.message)) | |
118 | return default | |
119 | ||
120 | def _get_pixmap_array_prop(self, name, default=None): | |
121 | try: | |
122 | res = self._get_property(name) | |
123 | if res[0] == "": | |
124 | return default | |
125 | ||
126 | return res[0] | |
127 | except GLib.Error as e: | |
128 | if e.code != Gio.DBusError.INVALID_ARGS: | |
129 | print("Couldn't get %s property: %s" % (name, e.message)) | |
130 | return default | |
131 | ||
132 | def category (self): return self._get_string_prop("Category", "ApplicationStatus") | |
133 | def id (self): return self._get_string_prop("Id") | |
134 | def title (self): return self._get_string_prop("Title") | |
135 | def status (self): return self._get_string_prop("Status", self._status) | |
136 | def menu (self): return self._get_string_prop("Menu") | |
137 | def item_is_menu (self): return self._get_bool_prop ("ItemIsMenu") | |
138 | def icon_theme_path (self): return self._get_string_prop("IconThemePath", None) | |
139 | def icon_name (self): return self._get_string_prop("IconName", None) | |
140 | def icon_pixmap (self): return self._get_pixmap_array_prop("IconPixmap", None) | |
141 | def att_icon_name (self): return self._get_string_prop("AttentionIconName", None) | |
142 | def att_icon_pixmap (self): return self._get_pixmap_array_prop("AttentionIconPixmap", None) | |
143 | def overlay_icon_name (self): return self._get_string_prop("OverlayIconName", None) | |
144 | def overlay_icon_pixmap (self): return self._get_pixmap_array_prop("OverlayIconPixmap", None) | |
145 | def tooltip (self): | |
146 | # For now only appindicator seems to provide anything remotely like a tooltip | |
147 | if self.sn_item_proxy.get_object_path().startswith(APPINDICATOR_PATH_PREFIX): | |
148 | return self._get_string_prop("XAyatanaLabel") | |
149 | else: | |
150 | # For everything else, no tooltip | |
151 | return "" | |
152 | ||
153 | def activate(self, button, x, y): | |
154 | if button == Gdk.BUTTON_PRIMARY: | |
155 | try: | |
156 | # This sucks, nothing is consistent. Most programs don't have a primary | |
157 | # activate (all appindicator ones). One that I checked that does, claims | |
158 | # (according to proxyinfo.get_method_info()) it only accepts SecondaryActivate, | |
159 | # but only listens for "Activate", so we attempt a sync primary call, and async | |
160 | # secondary if needed. Otherwise we're waiting for the first to finish in a | |
161 | # callback before we can try the secondary. Maybe we just call secondary alwayS?? | |
162 | self.sn_item_proxy.call_activate_sync(x, y, None) | |
163 | except GLib.Error: | |
164 | self.sn_item_proxy.call_secondary_activate(x, y, None, None) | |
165 | elif button == Gdk.BUTTON_MIDDLE: | |
166 | self.sn_item_proxy.call_secondary_activate(x, y, None, None) | |
167 | ||
168 | def show_context_menu(self, button, x, y): | |
169 | if button == Gdk.BUTTON_SECONDARY: | |
170 | self.sn_item_proxy.call_context_menu(x, y, None, None) | |
171 | ||
172 | def scroll(self, delta, o_str): | |
173 | self.sn_item_proxy.call_scroll(delta, o_str, None, None) | |
174 | ||
175 | def destroy(self): | |
176 | try: | |
177 | self.sn_item_proxy.disconnect_by_func(self.signal_received) | |
178 | self.prop_proxy = None | |
179 | except Exception as e: | |
180 | print(str(e)) |
0 | 0 | [D-BUS Service] |
1 | 1 | Name=org.x.StatusNotifierWatcher |
2 | Exec=@launch_folder@/xapp-sn-watcher.py --gapplication-service | |
2 | Exec=@launch_folder@/xapp-sn-watcher --gapplication-service |
0 | ||
1 | #include <config.h> | |
2 | ||
3 | #include <stdio.h> | |
4 | #include <stdlib.h> | |
5 | #include <string.h> | |
6 | ||
7 | #include <sys/types.h> | |
8 | #include <unistd.h> | |
9 | ||
10 | #include <glib/gstdio.h> | |
11 | #include <gtk/gtk.h> | |
12 | #include <cairo-gobject.h> | |
13 | #include <libxapp/xapp-status-icon.h> | |
14 | #include <libdbusmenu-gtk/menu.h> | |
15 | ||
16 | #include "sn-item-interface.h" | |
17 | #include "sn-item.h" | |
18 | ||
19 | #define FALLBACK_ICON_SIZE 24 | |
20 | ||
21 | typedef enum | |
22 | { | |
23 | STATUS_PASSIVE, | |
24 | STATUS_ACTIVE, | |
25 | STATUS_NEEDS_ATTENTION | |
26 | } Status; | |
27 | ||
28 | struct _SnItem | |
29 | { | |
30 | GObject parent_instance; | |
31 | ||
32 | GDBusProxy *sn_item_proxy; // SnItemProxy | |
33 | GDBusProxy *prop_proxy; // dbus properties (we can't trust SnItemProxy) | |
34 | ||
35 | GtkWidget *menu; | |
36 | XAppStatusIcon *status_icon; | |
37 | ||
38 | Status status; | |
39 | gchar *last_png_path; | |
40 | gchar *png_path; | |
41 | ||
42 | gint current_icon_id; | |
43 | ||
44 | gboolean is_ai; | |
45 | }; | |
46 | ||
47 | G_DEFINE_TYPE (SnItem, sn_item, G_TYPE_OBJECT) | |
48 | ||
49 | static void update_menu (SnItem *item); | |
50 | static void update_status (SnItem *item); | |
51 | static void update_tooltip (SnItem *item); | |
52 | static void update_icon (SnItem *item); | |
53 | ||
54 | static void | |
55 | sn_item_init (SnItem *self) | |
56 | { | |
57 | } | |
58 | ||
59 | static void | |
60 | sn_item_dispose (GObject *object) | |
61 | { | |
62 | SnItem *item = SN_ITEM (object); | |
63 | g_debug ("SnItem dispose (%p)", object); | |
64 | ||
65 | if (item->png_path != NULL) | |
66 | { | |
67 | g_unlink (item->png_path); | |
68 | g_free (item->png_path); | |
69 | item->png_path = NULL; | |
70 | } | |
71 | ||
72 | if (item->last_png_path != NULL) | |
73 | { | |
74 | g_unlink (item->last_png_path); | |
75 | g_free (item->last_png_path); | |
76 | item->last_png_path = NULL; | |
77 | } | |
78 | ||
79 | g_clear_object (&item->status_icon); | |
80 | g_clear_object (&item->menu); | |
81 | g_clear_object (&item->prop_proxy); | |
82 | g_clear_object (&item->sn_item_proxy); | |
83 | ||
84 | G_OBJECT_CLASS (sn_item_parent_class)->dispose (object); | |
85 | } | |
86 | ||
87 | static void | |
88 | sn_item_finalize (GObject *object) | |
89 | { | |
90 | g_debug ("SnItem finalize (%p)", object); | |
91 | ||
92 | G_OBJECT_CLASS (sn_item_parent_class)->finalize (object); | |
93 | } | |
94 | ||
95 | static void | |
96 | sn_item_class_init (SnItemClass *klass) | |
97 | { | |
98 | GObjectClass *gobject_class = G_OBJECT_CLASS (klass); | |
99 | ||
100 | gobject_class->dispose = sn_item_dispose; | |
101 | gobject_class->finalize = sn_item_finalize; | |
102 | ||
103 | } | |
104 | ||
105 | static guint | |
106 | lookup_ui_scale (void) | |
107 | { | |
108 | GdkScreen *screen; | |
109 | GValue value = G_VALUE_INIT; | |
110 | guint scale = 1; | |
111 | ||
112 | g_value_init (&value, G_TYPE_UINT); | |
113 | ||
114 | screen = gdk_screen_get_default (); | |
115 | ||
116 | if (gdk_screen_get_setting (screen, "gdk-window-scaling-factor", &value)) | |
117 | { | |
118 | scale = g_value_get_uint (&value); | |
119 | } | |
120 | ||
121 | return scale; | |
122 | } | |
123 | ||
124 | static gint | |
125 | get_icon_id (SnItem *item) | |
126 | { | |
127 | item->current_icon_id = (!item->current_icon_id); | |
128 | ||
129 | return item->current_icon_id; | |
130 | } | |
131 | ||
132 | static gint | |
133 | get_icon_size (SnItem *item) | |
134 | { | |
135 | gint size = 0; | |
136 | ||
137 | size = xapp_status_icon_get_icon_size (item->status_icon); | |
138 | ||
139 | if (size > 0) | |
140 | { | |
141 | return size; | |
142 | } | |
143 | ||
144 | return FALLBACK_ICON_SIZE; | |
145 | } | |
146 | ||
147 | static GVariant * | |
148 | get_property (SnItem *item, | |
149 | const gchar *prop_name) | |
150 | { | |
151 | GVariant *res, *var; | |
152 | GError *error = NULL; | |
153 | ||
154 | res = g_dbus_proxy_call_sync (item->prop_proxy, | |
155 | "Get", | |
156 | g_variant_new ("(ss)", | |
157 | g_dbus_proxy_get_interface_name (item->sn_item_proxy), | |
158 | prop_name), | |
159 | G_DBUS_CALL_FLAGS_NONE, | |
160 | 5 * 1000, | |
161 | NULL, | |
162 | &error); | |
163 | ||
164 | if (error != NULL) | |
165 | { | |
166 | g_error_free (error); | |
167 | return NULL; | |
168 | } | |
169 | ||
170 | g_variant_get (res, "(v)", &var); | |
171 | g_variant_unref (res); | |
172 | ||
173 | return var; | |
174 | } | |
175 | ||
176 | static GVariant * | |
177 | get_pixmap_property (SnItem *item, | |
178 | const gchar *name) | |
179 | { | |
180 | GVariant *var = NULL; | |
181 | ||
182 | var = get_property (item, name); | |
183 | ||
184 | if (var == NULL) | |
185 | { | |
186 | return NULL; | |
187 | } | |
188 | ||
189 | return var; | |
190 | } | |
191 | ||
192 | static gchar * | |
193 | get_string_property (SnItem *item, | |
194 | const gchar *name) | |
195 | { | |
196 | GVariant *var = NULL; | |
197 | gchar *result = NULL; | |
198 | ||
199 | var = get_property (item, name); | |
200 | ||
201 | if (var == NULL) | |
202 | { | |
203 | return NULL; | |
204 | } | |
205 | ||
206 | result = g_variant_dup_string (var, NULL); | |
207 | g_variant_unref (var); | |
208 | ||
209 | if (g_strcmp0 (result, "") == 0) | |
210 | { | |
211 | g_clear_pointer (&result, g_free); | |
212 | } | |
213 | ||
214 | return result; | |
215 | } | |
216 | ||
217 | static cairo_surface_t * | |
218 | surface_from_pixmap_data (gint width, | |
219 | gint height, | |
220 | GBytes *bytes) | |
221 | { | |
222 | cairo_surface_t *surface; | |
223 | GdkPixbuf *pixbuf; | |
224 | gint rowstride, i; | |
225 | gsize size; | |
226 | gconstpointer data; | |
227 | guchar *copy; | |
228 | guchar alpha; | |
229 | ||
230 | data = g_bytes_get_data (bytes, &size); | |
231 | copy = g_memdup ((guchar *) data, size); | |
232 | ||
233 | surface = NULL; | |
234 | rowstride = width * 4; | |
235 | i = 0; | |
236 | ||
237 | while (i < 4 * width * height) | |
238 | { | |
239 | alpha = copy[i ]; | |
240 | copy[i ] = copy[i + 1]; | |
241 | copy[i + 1] = copy[i + 2]; | |
242 | copy[i + 2] = copy[i + 3]; | |
243 | copy[i + 3] = alpha; | |
244 | i += 4; | |
245 | } | |
246 | ||
247 | pixbuf = gdk_pixbuf_new_from_data (copy, | |
248 | GDK_COLORSPACE_RGB, | |
249 | TRUE, 8, | |
250 | width, height, | |
251 | rowstride, | |
252 | (GdkPixbufDestroyNotify) g_free, | |
253 | NULL); | |
254 | ||
255 | if (pixbuf) | |
256 | { | |
257 | guint scale = lookup_ui_scale (); | |
258 | ||
259 | surface = gdk_cairo_surface_create_from_pixbuf (pixbuf, scale, NULL); | |
260 | g_object_unref (pixbuf); | |
261 | ||
262 | return surface; | |
263 | } | |
264 | } | |
265 | ||
266 | static gboolean | |
267 | process_pixmaps (SnItem *item, | |
268 | GVariant *pixmaps, | |
269 | gchar **image_path) | |
270 | { | |
271 | GVariantIter iter; | |
272 | cairo_surface_t *surface; | |
273 | gint width, height; | |
274 | gint largest_width, largest_height; | |
275 | GVariant *byte_array_var; | |
276 | GBytes *best_image_bytes = NULL; | |
277 | ||
278 | largest_width = largest_height = 0; | |
279 | ||
280 | g_variant_iter_init (&iter, pixmaps); | |
281 | ||
282 | while (g_variant_iter_loop (&iter, "(ii@ay)", &width, &height, &byte_array_var)) | |
283 | { | |
284 | if (width > 0 & height > 0 && | |
285 | ((width * height) > (largest_width * largest_height))) | |
286 | { | |
287 | gsize data_size = g_variant_get_size (byte_array_var); | |
288 | ||
289 | if (data_size == width * height * 4) | |
290 | { | |
291 | g_clear_pointer (&best_image_bytes, g_bytes_unref); | |
292 | ||
293 | largest_width = width; | |
294 | largest_height = height; | |
295 | best_image_bytes = g_variant_get_data_as_bytes (byte_array_var); | |
296 | } | |
297 | } | |
298 | } | |
299 | ||
300 | if (best_image_bytes == NULL) | |
301 | { | |
302 | g_warning ("No valid pixmaps found."); | |
303 | return FALSE; | |
304 | } | |
305 | ||
306 | surface = surface_from_pixmap_data (largest_width, largest_height, best_image_bytes); | |
307 | ||
308 | if (cairo_surface_status (surface) != CAIRO_STATUS_SUCCESS) | |
309 | { | |
310 | cairo_surface_destroy (surface); | |
311 | return FALSE; | |
312 | } | |
313 | ||
314 | item->last_png_path = item->png_path; | |
315 | ||
316 | gchar *filename = g_strdup_printf ("xapp-tmp-%p-%d.png", item, get_icon_id (item)); | |
317 | gchar *save_filename = g_build_path ("/", g_get_tmp_dir (), filename, NULL); | |
318 | g_free (filename); | |
319 | ||
320 | cairo_status_t status = CAIRO_STATUS_SUCCESS; | |
321 | status = cairo_surface_write_to_png (surface, save_filename); | |
322 | ||
323 | if (status != CAIRO_STATUS_SUCCESS) | |
324 | { | |
325 | g_warning ("Failed to save png of status icon"); | |
326 | g_free (image_path); | |
327 | cairo_surface_destroy (surface); | |
328 | return FALSE; | |
329 | } | |
330 | ||
331 | *image_path = save_filename; | |
332 | cairo_surface_destroy (surface); | |
333 | ||
334 | return TRUE; | |
335 | } | |
336 | ||
337 | static void | |
338 | set_icon_from_pixmap (SnItem *item) | |
339 | { | |
340 | GVariant *pixmaps; | |
341 | gchar *image_path; | |
342 | ||
343 | if (item->status == STATUS_ACTIVE) | |
344 | { | |
345 | pixmaps = get_pixmap_property (item, "IconPixmap"); | |
346 | } | |
347 | else | |
348 | if (item->status == STATUS_NEEDS_ATTENTION) | |
349 | { | |
350 | pixmaps = get_pixmap_property (item, "AttentionIconPixmap"); | |
351 | ||
352 | if (!pixmaps) | |
353 | { | |
354 | pixmaps = get_pixmap_property (item, "IconPixmap"); | |
355 | } | |
356 | } | |
357 | ||
358 | if (!pixmaps) | |
359 | { | |
360 | xapp_status_icon_set_icon_name (item->status_icon, "image-missing"); | |
361 | g_warning ("No pixmaps to use"); | |
362 | return; | |
363 | } | |
364 | ||
365 | if (process_pixmaps (item, pixmaps, &image_path)) | |
366 | { | |
367 | xapp_status_icon_set_icon_name (item->status_icon, image_path); | |
368 | g_free (image_path); | |
369 | } | |
370 | ||
371 | g_variant_unref (pixmaps); | |
372 | } | |
373 | ||
374 | static gchar * | |
375 | get_icon_filename_from_theme (SnItem *item, | |
376 | const gchar *theme_path, | |
377 | const gchar *icon_name) | |
378 | { | |
379 | GtkIconInfo *info; | |
380 | gchar *filename; | |
381 | const gchar *array[2]; | |
382 | ||
383 | array[0] = icon_name; | |
384 | array[1] = NULL; | |
385 | ||
386 | // We have a theme path, but try the system theme first | |
387 | GtkIconTheme *theme = gtk_icon_theme_get_default (); | |
388 | ||
389 | info = gtk_icon_theme_choose_icon_for_scale (theme, | |
390 | array, | |
391 | get_icon_size (item), | |
392 | lookup_ui_scale (), | |
393 | GTK_ICON_LOOKUP_FORCE_SVG | GTK_ICON_LOOKUP_FORCE_SYMBOLIC); | |
394 | ||
395 | if (info == NULL) | |
396 | { | |
397 | // Make a temp theme based off of the provided path | |
398 | GtkIconTheme *theme = gtk_icon_theme_new (); | |
399 | ||
400 | gtk_icon_theme_prepend_search_path (theme, theme_path); | |
401 | ||
402 | info = gtk_icon_theme_choose_icon_for_scale (theme, | |
403 | array, | |
404 | get_icon_size (item), | |
405 | lookup_ui_scale (), | |
406 | GTK_ICON_LOOKUP_FORCE_SVG | GTK_ICON_LOOKUP_FORCE_SYMBOLIC); | |
407 | ||
408 | g_object_unref (theme); | |
409 | } | |
410 | ||
411 | if (info == NULL) | |
412 | { | |
413 | return NULL; | |
414 | } | |
415 | ||
416 | filename = g_strdup (gtk_icon_info_get_filename(info)); | |
417 | g_object_unref (info); | |
418 | ||
419 | return filename; | |
420 | } | |
421 | ||
422 | static void | |
423 | process_icon_name (SnItem *item, | |
424 | const gchar *icon_theme_path, | |
425 | const gchar *icon_name) | |
426 | { | |
427 | if (g_path_is_absolute (icon_name) || !icon_theme_path) | |
428 | { | |
429 | xapp_status_icon_set_icon_name (item->status_icon, icon_name); | |
430 | } | |
431 | else | |
432 | { | |
433 | gchar *filename = get_icon_filename_from_theme (item, icon_theme_path, icon_name); | |
434 | ||
435 | if (filename != NULL) | |
436 | { | |
437 | xapp_status_icon_set_icon_name (item->status_icon, filename); | |
438 | g_free (filename); | |
439 | } | |
440 | else | |
441 | { | |
442 | xapp_status_icon_set_icon_name (item->status_icon, "image-missing"); | |
443 | } | |
444 | } | |
445 | } | |
446 | ||
447 | static void | |
448 | set_icon_name_or_path (SnItem *item, | |
449 | const gchar *icon_theme_path, | |
450 | const gchar *icon_name, | |
451 | const gchar *att_icon_name, | |
452 | const gchar *olay_icon_name) | |
453 | { | |
454 | const gchar *name_to_use = NULL; | |
455 | ||
456 | if (item->status == STATUS_ACTIVE) | |
457 | { | |
458 | if (icon_name) | |
459 | { | |
460 | name_to_use = icon_name; | |
461 | } | |
462 | } | |
463 | else | |
464 | if (item->status == STATUS_NEEDS_ATTENTION) | |
465 | { | |
466 | if (att_icon_name) | |
467 | { | |
468 | name_to_use = att_icon_name; | |
469 | } | |
470 | else | |
471 | if (icon_name) | |
472 | { | |
473 | name_to_use = icon_name; | |
474 | } | |
475 | } | |
476 | ||
477 | if (name_to_use == NULL) | |
478 | { | |
479 | name_to_use = "image-missing"; | |
480 | } | |
481 | ||
482 | process_icon_name (item, icon_theme_path, name_to_use); | |
483 | } | |
484 | ||
485 | static void | |
486 | update_icon (SnItem *item) | |
487 | { | |
488 | gchar *icon_theme_path; | |
489 | gchar *icon_name, *att_icon_name, *olay_icon_name; | |
490 | ||
491 | icon_theme_path = get_string_property (item, "IconThemePath"); | |
492 | icon_name = get_string_property (item, "IconName"); | |
493 | att_icon_name = get_string_property (item, "AttentionIconName"); | |
494 | olay_icon_name = get_string_property (item, "OverlayIconName"); | |
495 | ||
496 | if (icon_name || att_icon_name || olay_icon_name) | |
497 | { | |
498 | // g_printerr ("icon name '%s' '%s' '%s'\n", icon_name, att_icon_name, olay_icon_name); | |
499 | set_icon_name_or_path (item, | |
500 | icon_theme_path, | |
501 | icon_name, | |
502 | att_icon_name, | |
503 | olay_icon_name); | |
504 | } | |
505 | else | |
506 | { | |
507 | set_icon_from_pixmap (item); | |
508 | } | |
509 | ||
510 | g_free (icon_theme_path); | |
511 | g_free (icon_name); | |
512 | g_free (att_icon_name); | |
513 | g_free (olay_icon_name); | |
514 | } | |
515 | ||
516 | static void | |
517 | update_menu (SnItem *item) | |
518 | { | |
519 | gchar *menu_path; | |
520 | ||
521 | menu_path = get_string_property (item, "Menu"); | |
522 | ||
523 | if (menu_path == NULL) | |
524 | { | |
525 | g_clear_object (&item->menu); | |
526 | ||
527 | xapp_status_icon_set_secondary_menu (item->status_icon, NULL); | |
528 | return; | |
529 | } | |
530 | ||
531 | item->menu = GTK_WIDGET (dbusmenu_gtkmenu_new ((gchar *) g_dbus_proxy_get_name (item->sn_item_proxy), menu_path)); | |
532 | g_object_ref_sink (item->menu); | |
533 | ||
534 | xapp_status_icon_set_secondary_menu (item->status_icon, GTK_MENU (item->menu)); | |
535 | ||
536 | g_free (menu_path); | |
537 | } | |
538 | ||
539 | static gchar * | |
540 | capitalize (const gchar *string) | |
541 | { | |
542 | gchar *utf8; | |
543 | gunichar first; | |
544 | gchar *remaining; | |
545 | gchar *ret; | |
546 | ||
547 | utf8 = g_utf8_make_valid (string, -1); | |
548 | ||
549 | first = g_utf8_get_char (utf8); | |
550 | first = g_unichar_toupper (first); | |
551 | ||
552 | remaining = g_utf8_substring (utf8, 1, g_utf8_strlen (utf8, -1)); | |
553 | ||
554 | ret = g_strdup_printf ("%s%s", (gchar *) &first, remaining); | |
555 | ||
556 | g_free (utf8); | |
557 | g_free (remaining); | |
558 | ||
559 | return ret; | |
560 | } | |
561 | ||
562 | static void | |
563 | update_tooltip (SnItem *item) | |
564 | { | |
565 | g_autoptr(GVariant) tt_var; | |
566 | ||
567 | if (item->is_ai) | |
568 | { | |
569 | gchar *text; | |
570 | ||
571 | text = get_string_property (item, "XAyatanaLabel"); | |
572 | ||
573 | if (text) | |
574 | { | |
575 | xapp_status_icon_set_tooltip_text (item->status_icon, text); | |
576 | g_debug ("Tooltip text from XAyatanaLabel: %s", text); | |
577 | ||
578 | g_free (text); | |
579 | return; | |
580 | } | |
581 | } | |
582 | ||
583 | tt_var = get_property (item, "ToolTip"); | |
584 | ||
585 | if (tt_var) | |
586 | { | |
587 | const gchar *type_str; | |
588 | type_str = g_variant_get_type_string (tt_var); | |
589 | ||
590 | if (g_strcmp0 (type_str, "(sa(iiay)ss)") == 0) | |
591 | { | |
592 | const gchar *tooltip_title, *tooltip_body; | |
593 | ||
594 | g_variant_get (tt_var, "(sa(iiay)&s&s)", NULL, NULL, &tooltip_title, &tooltip_body); | |
595 | ||
596 | if (g_strcmp0 (tooltip_title, "") != 0) | |
597 | { | |
598 | ||
599 | if (g_strcmp0 (tooltip_body, "") != 0) | |
600 | { | |
601 | gchar *text; | |
602 | text = g_strdup_printf ("%s\n%s", tooltip_title, tooltip_body); | |
603 | ||
604 | xapp_status_icon_set_tooltip_text (item->status_icon, text); | |
605 | g_debug ("Tooltip text from ToolTip: %s", text); | |
606 | ||
607 | g_free (text); | |
608 | } | |
609 | else | |
610 | { | |
611 | g_debug ("Tooltip text from ToolTip: %s", tooltip_title); | |
612 | xapp_status_icon_set_tooltip_text (item->status_icon, tooltip_title); | |
613 | } | |
614 | ||
615 | return; | |
616 | } | |
617 | } | |
618 | } | |
619 | ||
620 | gchar *title_string; | |
621 | title_string = get_string_property (item, "Title"); | |
622 | ||
623 | if (title_string != NULL) | |
624 | { | |
625 | gchar *capped_string; | |
626 | ||
627 | capped_string = capitalize (title_string); | |
628 | xapp_status_icon_set_tooltip_text (item->status_icon, capped_string); | |
629 | g_debug ("Tooltip text from Title: %s", capped_string); | |
630 | ||
631 | g_free (title_string); | |
632 | g_free (capped_string); | |
633 | return; | |
634 | } | |
635 | ||
636 | xapp_status_icon_set_tooltip_text (item->status_icon, ""); | |
637 | } | |
638 | ||
639 | static void | |
640 | update_status (SnItem *item) | |
641 | { | |
642 | Status old_status; | |
643 | gchar *status; | |
644 | ||
645 | old_status = item->status; | |
646 | ||
647 | status = get_string_property (item, "Status"); | |
648 | ||
649 | if (g_strcmp0 (status, "Passive") == 0) | |
650 | { | |
651 | item->status = STATUS_PASSIVE; | |
652 | xapp_status_icon_set_visible (item->status_icon, FALSE); | |
653 | } | |
654 | else if (g_strcmp0 (status, "NeedsAttention") == 0) | |
655 | { | |
656 | item->status = STATUS_NEEDS_ATTENTION; | |
657 | xapp_status_icon_set_visible (item->status_icon, TRUE); | |
658 | } | |
659 | else | |
660 | { | |
661 | item->status = STATUS_ACTIVE; | |
662 | xapp_status_icon_set_visible (item->status_icon, TRUE); | |
663 | } | |
664 | ||
665 | g_free (status); | |
666 | ||
667 | if (old_status != item->status) | |
668 | { | |
669 | update_icon (item); | |
670 | } | |
671 | } | |
672 | ||
673 | static void | |
674 | sn_signal_received (GDBusProxy *sn_item_proxy, | |
675 | const gchar *sender_name, | |
676 | const gchar *signal_name, | |
677 | GVariant *parameters, | |
678 | gpointer user_data) | |
679 | { | |
680 | SnItem *item = SN_ITEM (user_data); | |
681 | ||
682 | if (item->prop_proxy == NULL) | |
683 | { | |
684 | return; | |
685 | } | |
686 | ||
687 | if (g_strcmp0 (signal_name, "NewIcon") == 0 || | |
688 | g_strcmp0 (signal_name, "NewAttentionIcon") == 0 || | |
689 | g_strcmp0 (signal_name, "NewOverlayIcon") == 0) | |
690 | { | |
691 | update_icon (item); | |
692 | } | |
693 | else | |
694 | if (g_strcmp0 (signal_name, "NewStatus") == 0) | |
695 | { | |
696 | update_status (item); // This will update_icon(item) also. | |
697 | } | |
698 | else | |
699 | if (g_strcmp0 (signal_name, "NewMenu") == 0) | |
700 | { | |
701 | update_menu (item); | |
702 | } | |
703 | else | |
704 | if (g_strcmp0 (signal_name, "XAyatanaNewLabel") || | |
705 | g_strcmp0 (signal_name, "NewToolTip") || | |
706 | g_strcmp0 (signal_name, "NewTitle")) | |
707 | { | |
708 | update_tooltip (item); | |
709 | } | |
710 | } | |
711 | ||
712 | static void | |
713 | xapp_icon_activated (XAppStatusIcon *status_icon, | |
714 | guint button, | |
715 | guint _time, | |
716 | gpointer user_data) | |
717 | { | |
718 | } | |
719 | ||
720 | static void | |
721 | xapp_icon_button_press (XAppStatusIcon *status_icon, | |
722 | gint x, | |
723 | gint y, | |
724 | guint button, | |
725 | guint _time, | |
726 | gint panel_position, | |
727 | gpointer user_data) | |
728 | { | |
729 | SnItem *item = SN_ITEM (user_data); | |
730 | ||
731 | GError *error = NULL; | |
732 | ||
733 | if (button == GDK_BUTTON_PRIMARY) | |
734 | { | |
735 | /* This sucks, nothing is consistent. Most programs don't have a primary | |
736 | activate (all appindicator ones). One that I checked that does, claims | |
737 | (according to proxyinfo.get_method_info()) it only accepts SecondaryActivate, | |
738 | but only listens for "Activate", so we attempt a sync primary call, and async | |
739 | secondary if needed. Otherwise we're waiting for the first to finish in a | |
740 | callback before we can try the secondary. Maybe we just call secondary always?? */ | |
741 | ||
742 | sn_item_interface_call_activate_sync (SN_ITEM_INTERFACE (item->sn_item_proxy), x, y, NULL, &error); | |
743 | ||
744 | if (error != NULL) | |
745 | { | |
746 | g_error_free (error); | |
747 | ||
748 | sn_item_interface_call_secondary_activate (SN_ITEM_INTERFACE (item->sn_item_proxy), x, y, NULL, NULL, NULL); | |
749 | } | |
750 | } | |
751 | else | |
752 | if (button == GDK_BUTTON_MIDDLE) | |
753 | { | |
754 | sn_item_interface_call_secondary_activate (SN_ITEM_INTERFACE (item->sn_item_proxy), x, y, NULL, NULL, NULL); | |
755 | } | |
756 | } | |
757 | ||
758 | static void | |
759 | xapp_icon_button_release (XAppStatusIcon *status_icon, | |
760 | gint x, | |
761 | gint y, | |
762 | guint button, | |
763 | guint _time, | |
764 | gint panel_position, | |
765 | gpointer user_data) | |
766 | { | |
767 | SnItem *item = SN_ITEM (user_data); | |
768 | ||
769 | if (button == GDK_BUTTON_SECONDARY && item->menu == NULL) | |
770 | { | |
771 | sn_item_interface_call_context_menu (SN_ITEM_INTERFACE (item->sn_item_proxy), x, y, NULL, NULL, NULL); | |
772 | } | |
773 | } | |
774 | ||
775 | static void | |
776 | xapp_icon_scroll (XAppStatusIcon *status_icon, | |
777 | gint delta, | |
778 | XAppScrollDirection dir, | |
779 | guint _time, | |
780 | gpointer user_data) | |
781 | { | |
782 | SnItem *item = SN_ITEM (user_data); | |
783 | ||
784 | switch (dir) | |
785 | { | |
786 | case XAPP_SCROLL_LEFT: | |
787 | case XAPP_SCROLL_RIGHT: | |
788 | sn_item_interface_call_scroll (SN_ITEM_INTERFACE (item->sn_item_proxy), delta, "horizontal", NULL, NULL, NULL); | |
789 | break; | |
790 | case XAPP_SCROLL_UP: | |
791 | case XAPP_SCROLL_DOWN: | |
792 | sn_item_interface_call_scroll (SN_ITEM_INTERFACE (item->sn_item_proxy), delta, "vertical", NULL, NULL, NULL); | |
793 | break; | |
794 | } | |
795 | } | |
796 | ||
797 | static void | |
798 | xapp_icon_state_changed (XAppStatusIcon *status_icon, | |
799 | XAppStatusIconState new_state, | |
800 | gpointer user_data) | |
801 | { | |
802 | SnItem *item = SN_ITEM (user_data); | |
803 | ||
804 | if (new_state == XAPP_STATUS_ICON_STATE_NO_SUPPORT) | |
805 | { | |
806 | return; | |
807 | } | |
808 | ||
809 | update_icon (item); | |
810 | update_status (item); | |
811 | update_icon (item); | |
812 | update_tooltip (item); | |
813 | } | |
814 | ||
815 | static void | |
816 | assign_sortable_name (SnItem *item, | |
817 | XAppStatusIcon *status_icon) | |
818 | { | |
819 | gchar *sortable_name; | |
820 | ||
821 | sortable_name = sn_item_interface_dup_id (SN_ITEM_INTERFACE (item->sn_item_proxy)); | |
822 | ||
823 | if (sortable_name == NULL) | |
824 | { | |
825 | sortable_name = get_string_property (item, "Title"); | |
826 | } | |
827 | ||
828 | g_debug ("Sort name for %s is '%s'", g_dbus_proxy_get_name (G_DBUS_PROXY (item->sn_item_proxy)), sortable_name); | |
829 | xapp_status_icon_set_name (status_icon, sortable_name); | |
830 | ||
831 | g_free (sortable_name); | |
832 | } | |
833 | ||
834 | static void | |
835 | property_proxy_acquired (GObject *source, | |
836 | GAsyncResult *res, | |
837 | gpointer user_data) | |
838 | { | |
839 | SnItem *item = SN_ITEM (user_data); | |
840 | GError *error = NULL; | |
841 | ||
842 | item->prop_proxy = g_dbus_proxy_new_finish (res, &error); | |
843 | ||
844 | if (error != NULL) | |
845 | { | |
846 | g_printerr ("Could not get prop proxy: %s\n", error->message); | |
847 | g_error_free (error); | |
848 | return; | |
849 | } | |
850 | ||
851 | g_signal_connect (item->sn_item_proxy, | |
852 | "g-signal", | |
853 | G_CALLBACK (sn_signal_received), | |
854 | item); | |
855 | ||
856 | item->status_icon = xapp_status_icon_new (); | |
857 | ||
858 | g_signal_connect (item->status_icon, "activate", G_CALLBACK (xapp_icon_activated), item); | |
859 | g_signal_connect (item->status_icon, "button-press-event", G_CALLBACK (xapp_icon_button_press), item); | |
860 | g_signal_connect (item->status_icon, "button-release-event", G_CALLBACK (xapp_icon_button_release), item); | |
861 | g_signal_connect (item->status_icon, "scroll-event", G_CALLBACK (xapp_icon_scroll), item); | |
862 | g_signal_connect (item->status_icon, "state-changed", G_CALLBACK (xapp_icon_state_changed), item); | |
863 | ||
864 | assign_sortable_name (item, item->status_icon); | |
865 | ||
866 | update_status (item); | |
867 | update_menu (item); | |
868 | update_tooltip (item); | |
869 | update_icon (item); | |
870 | } | |
871 | ||
872 | static void | |
873 | initialize_item (SnItem *item) | |
874 | { | |
875 | g_dbus_proxy_new (g_dbus_proxy_get_connection (item->sn_item_proxy), | |
876 | G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES, | |
877 | NULL, | |
878 | g_dbus_proxy_get_name (item->sn_item_proxy), | |
879 | g_dbus_proxy_get_object_path (item->sn_item_proxy), | |
880 | "org.freedesktop.DBus.Properties", | |
881 | NULL, | |
882 | property_proxy_acquired, | |
883 | item); | |
884 | } | |
885 | ||
886 | SnItem * | |
887 | sn_item_new (GDBusProxy *sn_item_proxy, | |
888 | gboolean is_ai) | |
889 | { | |
890 | SnItem *item = g_object_new (sn_item_get_type (), NULL); | |
891 | ||
892 | item->sn_item_proxy = sn_item_proxy; | |
893 | item->is_ai = is_ai; | |
894 | ||
895 | initialize_item (item); | |
896 | return item; | |
897 | }⏎ |
0 | #ifndef __SN_ITEM_H__ | |
1 | #define __SN_ITEM_H__ | |
2 | ||
3 | #include <stdio.h> | |
4 | ||
5 | #include <glib-object.h> | |
6 | #include <gtk/gtk.h> | |
7 | ||
8 | G_BEGIN_DECLS | |
9 | ||
10 | #define SN_TYPE_ITEM (sn_item_get_type ()) | |
11 | ||
12 | G_DECLARE_FINAL_TYPE (SnItem, sn_item, SN, ITEM, GObject) | |
13 | ||
14 | SnItem *sn_item_new (GDBusProxy *sn_item_proxy, gboolean is_ai); | |
15 | ||
16 | G_END_DECLS | |
17 | ||
18 | #endif /* __SN_ITEM_H__ */ |
0 | <?xml version="1.0" encoding="UTF-8"?> | |
1 | ||
2 | <node> | |
3 | <interface name="org.kde.StatusNotifierItem"> | |
4 | <annotation name="org.gtk.GDBus.C.Name" value="SnItemInterface" /> | |
5 | <property name="Category" type="s" access="read"/> | |
6 | <property name="Id" type="s" access="read"/> | |
7 | <property name="Title" type="s" access="read"/> | |
8 | <property name="Status" type="s" access="read"/> | |
9 | <property name="WindowId" type="i" access="read"/> | |
10 | <property name="Menu" type="o" access="read" /> | |
11 | ||
12 | <!-- main icon --> | |
13 | <!-- names are preferred over pixmaps --> | |
14 | <property name="IconName" type="s" access="read" /> | |
15 | <property name="IconThemePath" type="s" access="read" /> | |
16 | ||
17 | <!-- struct containing width, height and image data--> | |
18 | <!-- implementation has been dropped as of now --> | |
19 | <property name="IconPixmap" type="a(iiay)" access="read" /> | |
20 | ||
21 | <!-- not used in ayatana code, no test case so far --> | |
22 | <property name="OverlayIconName" type="s" access="read"/> | |
23 | <property name="OverlayIconPixmap" type="a(iiay)" access="read" /> | |
24 | ||
25 | <!-- Requesting attention icon --> | |
26 | <property name="AttentionIconName" type="s" access="read"/> | |
27 | ||
28 | <!--same definition as image--> | |
29 | <property name="AttentionIconPixmap" type="a(iiay)" access="read" /> | |
30 | ||
31 | <!-- tooltip data --> | |
32 | <!-- unimplemented as of now --> | |
33 | <!--(iiay) is an image--> | |
34 | <property name="ToolTip" type="(sa(iiay)ss)" access="read" /> | |
35 | ||
36 | ||
37 | <method name="Activate"> | |
38 | <arg name="x" type="i" direction="in"/> | |
39 | <arg name="y" type="i" direction="in"/> | |
40 | </method> | |
41 | <method name="SecondaryActivate"> | |
42 | <arg name="x" type="i" direction="in"/> | |
43 | <arg name="y" type="i" direction="in"/> | |
44 | </method> | |
45 | <method name="ContextMenu"> | |
46 | <arg name="x" type="i" direction="in"/> | |
47 | <arg name="y" type="i" direction="in"/> | |
48 | </method> | |
49 | <method name="Scroll"> | |
50 | <arg name="delta" type="i" direction="in"/> | |
51 | <arg name="dir" type="s" direction="in"/> | |
52 | </method> | |
53 | ||
54 | ||
55 | <!-- Signals: the client wants to change something in the status--> | |
56 | <signal name="NewTitle"></signal> | |
57 | <signal name="NewIcon"></signal> | |
58 | <signal name="NewIconThemePath"> | |
59 | <arg type="s" name="icon_theme_path" direction="out" /> | |
60 | </signal> | |
61 | <signal name="NewAttentionIcon"></signal> | |
62 | <signal name="NewOverlayIcon"></signal> | |
63 | <signal name="NewMenu"></signal> | |
64 | <signal name="NewToolTip"></signal> | |
65 | <signal name="NewStatus"> | |
66 | <arg name="status" type="s" /> | |
67 | </signal> | |
68 | ||
69 | <!-- ayatana labels --> | |
70 | <!-- These are commented out because GDBusProxy would otherwise require them, | |
71 | but they are not available for KDE indicators | |
72 | --> | |
73 | <signal name="XAyatanaNewLabel"> | |
74 | <arg type="s" name="label" direction="out" /> | |
75 | <arg type="s" name="guide" direction="out" /> | |
76 | </signal> | |
77 | <property name="XAyatanaLabel" type="s" access="read" /> | |
78 | <property name="XAyatanaLabelGuide" type="s" access="read" /> | |
79 | ||
80 | ||
81 | </interface> | |
82 | </node> |
0 | <?xml version="1.0" encoding="UTF-8"?> | |
1 | ||
2 | <node name="/StatusNotifierWatcher"> | |
3 | <interface name="org.kde.StatusNotifierWatcher"> | |
4 | <annotation name="org.gtk.GDBus.C.Name" value="SnWatcherInterface" /> | |
5 | ||
6 | <method name="RegisterStatusNotifierItem"> | |
7 | <arg name="service" type="s" direction="in" /> | |
8 | </method> | |
9 | ||
10 | <method name="RegisterStatusNotifierHost"> | |
11 | <arg name="service" type="s" direction="in" /> | |
12 | </method> | |
13 | ||
14 | <property name="RegisteredStatusNotifierItems" type="as" access="read" /> | |
15 | <property name="IsStatusNotifierHostRegistered" type="b" access="read" /> | |
16 | <property name="ProtocolVersion" type="i" access="read" /> | |
17 | ||
18 | <signal name="StatusNotifierItemRegistered"> | |
19 | <arg type="s" name="service" direction="out" /> | |
20 | </signal> | |
21 | ||
22 | <signal name="StatusNotifierItemUnregistered"> | |
23 | <arg type="s" name="service" direction="out" /> | |
24 | </signal> | |
25 | ||
26 | <signal name="StatusNotifierHostRegistered" /> | |
27 | </interface> | |
28 | </node> |
0 | import threading | |
1 | import os | |
2 | ||
3 | from gi.repository import GLib | |
4 | ||
5 | # Used as a decorator to run things in the background | |
6 | def _async(func): | |
7 | def wrapper(*args, **kwargs): | |
8 | thread = threading.Thread(target=func, args=args, kwargs=kwargs) | |
9 | thread.daemon = True | |
10 | thread.start() | |
11 | return thread | |
12 | return wrapper | |
13 | ||
14 | # Used as a decorator to run things in the main loop, from another thread | |
15 | def _idle(func): | |
16 | def wrapper(*args, **kwargs): | |
17 | GLib.idle_add(func, *args, **kwargs) | |
18 | return wrapper | |
19 |
0 | #include <stdlib.h> | |
1 | #include <gtk/gtk.h> | |
2 | ||
3 | #include <libxapp/xapp-status-icon.h> | |
4 | #include <glib-unix.h> | |
5 | ||
6 | #include "sn-watcher-interface.h" | |
7 | #include "sn-item-interface.h" | |
8 | #include "sn-item.h" | |
9 | ||
10 | #define XAPP_TYPE_SN_WATCHER xapp_sn_watcher_get_type () | |
11 | G_DECLARE_FINAL_TYPE (XAppSnWatcher, xapp_sn_watcher, XAPP, SN_WATCHER, GtkApplication) | |
12 | ||
13 | struct _XAppSnWatcher | |
14 | { | |
15 | GtkApplication parent_instance; | |
16 | ||
17 | SnWatcherInterface *skeleton; | |
18 | GDBusConnection *connection; | |
19 | ||
20 | guint owner_id; | |
21 | guint name_listener_id; | |
22 | ||
23 | GHashTable *items; | |
24 | ||
25 | gboolean shutdown_pending; | |
26 | }; | |
27 | ||
28 | G_DEFINE_TYPE (XAppSnWatcher, xapp_sn_watcher, GTK_TYPE_APPLICATION) | |
29 | ||
30 | #define NOTIFICATION_WATCHER_NAME "org.kde.StatusNotifierWatcher" | |
31 | #define NOTIFICATION_WATCHER_PATH "/StatusNotifierWatcher" | |
32 | #define STATUS_ICON_MONITOR_PREFIX "org.x.StatusIconMonitor" | |
33 | ||
34 | #define FDO_DBUS_NAME "org.freedesktop.DBus" | |
35 | #define FDO_DBUS_PATH "/org/freedesktop/DBus" | |
36 | ||
37 | #define STATUS_ICON_MONITOR_MATCH "org.x.StatusIconMonitor" | |
38 | #define APPINDICATOR_PATH_PREFIX "/org/ayatana/NotificationItem/" | |
39 | ||
40 | static void continue_startup (XAppSnWatcher *watcher); | |
41 | static void update_published_items (XAppSnWatcher *watcher); | |
42 | ||
43 | static void | |
44 | handle_status_applet_name_owner_appeared (XAppSnWatcher *watcher, | |
45 | const gchar *name, | |
46 | const gchar *new_owner) | |
47 | { | |
48 | if (g_str_has_prefix (name, STATUS_ICON_MONITOR_PREFIX)) | |
49 | { | |
50 | if (watcher->shutdown_pending) | |
51 | { | |
52 | g_debug ("A monitor appeared on the bus, cancelling shutdown\n"); | |
53 | ||
54 | watcher->shutdown_pending = FALSE; | |
55 | g_application_hold (G_APPLICATION (watcher)); | |
56 | ||
57 | if (watcher->owner_id == 0) | |
58 | { | |
59 | continue_startup (watcher); | |
60 | return; | |
61 | } | |
62 | else | |
63 | { | |
64 | sn_watcher_interface_set_is_status_notifier_host_registered (watcher->skeleton, | |
65 | TRUE); | |
66 | g_dbus_interface_skeleton_flush (G_DBUS_INTERFACE_SKELETON (watcher->skeleton)); | |
67 | sn_watcher_interface_emit_status_notifier_host_registered (watcher->skeleton); | |
68 | } | |
69 | } | |
70 | } | |
71 | } | |
72 | ||
73 | static void | |
74 | handle_sn_item_name_owner_lost (XAppSnWatcher *watcher, | |
75 | const gchar *name, | |
76 | const gchar *old_owner) | |
77 | { | |
78 | GList *keys, *l; | |
79 | ||
80 | keys = g_hash_table_get_keys (watcher->items); | |
81 | ||
82 | for (l = keys; l != NULL; l = l->next) | |
83 | { | |
84 | const gchar *key = l->data; | |
85 | ||
86 | if (g_str_has_prefix (key, name)) | |
87 | { | |
88 | g_debug ("Client %s has exited, removing status icon", key); | |
89 | g_hash_table_remove (watcher->items, key); | |
90 | ||
91 | update_published_items (watcher); | |
92 | break; | |
93 | } | |
94 | } | |
95 | ||
96 | g_list_free (keys); | |
97 | } | |
98 | ||
99 | static void | |
100 | handle_status_applet_name_owner_lost (XAppSnWatcher *watcher, | |
101 | const gchar *name, | |
102 | const gchar *old_owner) | |
103 | { | |
104 | if (g_str_has_prefix (name, STATUS_ICON_MONITOR_PREFIX)) | |
105 | { | |
106 | g_debug ("Lost a monitor, checking for any more"); | |
107 | ||
108 | if (xapp_status_icon_any_monitors ()) | |
109 | { | |
110 | g_debug ("Still have a monitor, continuing"); | |
111 | ||
112 | return; | |
113 | } | |
114 | else | |
115 | { | |
116 | g_debug ("Lost our last monitor, starting countdown\n"); | |
117 | ||
118 | if (!watcher->shutdown_pending) | |
119 | { | |
120 | watcher->shutdown_pending = TRUE; | |
121 | g_application_release (G_APPLICATION (watcher)); | |
122 | ||
123 | sn_watcher_interface_set_is_status_notifier_host_registered (watcher->skeleton, | |
124 | FALSE); | |
125 | g_dbus_interface_skeleton_flush (G_DBUS_INTERFACE_SKELETON (watcher->skeleton)); | |
126 | } | |
127 | } | |
128 | } | |
129 | else | |
130 | { | |
131 | handle_sn_item_name_owner_lost (watcher, name, old_owner); | |
132 | } | |
133 | } | |
134 | ||
135 | static void | |
136 | name_owner_changed_signal (GDBusConnection *connection, | |
137 | const gchar *sender_name, | |
138 | const gchar *object_path, | |
139 | const gchar *interface_name, | |
140 | const gchar *signal_name, | |
141 | GVariant *parameters, | |
142 | gpointer user_data) | |
143 | { | |
144 | XAppSnWatcher *watcher = XAPP_SN_WATCHER (user_data); | |
145 | ||
146 | const gchar *name, *old_owner, *new_owner; | |
147 | ||
148 | g_variant_get (parameters, "(&s&s&s)", &name, &old_owner, &new_owner); | |
149 | ||
150 | g_debug("XAppSnWatcher: NameOwnerChanged signal received (n: %s, old: %s, new: %s", name, old_owner, new_owner); | |
151 | ||
152 | if (!name) | |
153 | { | |
154 | return; | |
155 | } | |
156 | ||
157 | if (g_strcmp0 (new_owner, "") == 0) | |
158 | { | |
159 | handle_status_applet_name_owner_lost (watcher, name, old_owner); | |
160 | } | |
161 | else | |
162 | { | |
163 | handle_status_applet_name_owner_appeared (watcher, name, new_owner); | |
164 | } | |
165 | } | |
166 | ||
167 | static void | |
168 | add_name_listener (XAppSnWatcher *watcher) | |
169 | { | |
170 | g_debug ("XAppSnWatcher: Adding NameOwnerChanged listener for status monitor existence"); | |
171 | ||
172 | watcher->name_listener_id = g_dbus_connection_signal_subscribe (watcher->connection, | |
173 | FDO_DBUS_NAME, | |
174 | FDO_DBUS_NAME, | |
175 | "NameOwnerChanged", | |
176 | FDO_DBUS_PATH, | |
177 | NULL, | |
178 | G_DBUS_SIGNAL_FLAGS_NONE, | |
179 | name_owner_changed_signal, | |
180 | watcher, | |
181 | NULL); | |
182 | } | |
183 | ||
184 | static void | |
185 | on_name_lost (GDBusConnection *connection, | |
186 | const gchar *name, | |
187 | gpointer user_data) | |
188 | { | |
189 | XAppSnWatcher *watcher = XAPP_SN_WATCHER (user_data); | |
190 | ||
191 | g_debug ("Lost StatusNotifierWatcher name (maybe something replaced us), exiting immediately"); | |
192 | g_application_quit (G_APPLICATION (watcher)); | |
193 | } | |
194 | ||
195 | static void | |
196 | on_name_acquired (GDBusConnection *connection, | |
197 | const gchar *name, | |
198 | gpointer user_data) | |
199 | { | |
200 | XAppSnWatcher *watcher = XAPP_SN_WATCHER (user_data); | |
201 | ||
202 | g_debug ("Name acquired on dbus"); | |
203 | ||
204 | sn_watcher_interface_set_is_status_notifier_host_registered (watcher->skeleton, | |
205 | TRUE); | |
206 | g_dbus_interface_skeleton_flush (G_DBUS_INTERFACE_SKELETON (watcher->skeleton)); | |
207 | sn_watcher_interface_emit_status_notifier_host_registered (watcher->skeleton); | |
208 | } | |
209 | ||
210 | static gboolean | |
211 | handle_register_host (SnWatcherInterface *skeleton, | |
212 | GDBusMethodInvocation *invocation, | |
213 | const gchar* service, | |
214 | XAppSnWatcher *watcher) | |
215 | { | |
216 | // Nothing to do - we wouldn't be here if there wasn't a host (status applet) | |
217 | sn_watcher_interface_complete_register_status_notifier_host (skeleton, | |
218 | invocation); | |
219 | ||
220 | return TRUE; | |
221 | } | |
222 | ||
223 | static void | |
224 | populate_published_list (const gchar *key, | |
225 | gpointer item, | |
226 | GPtrArray *array) | |
227 | { | |
228 | g_ptr_array_add (array, g_strdup (key)); | |
229 | } | |
230 | ||
231 | static void | |
232 | update_published_items (XAppSnWatcher *watcher) | |
233 | { | |
234 | GPtrArray *array; | |
235 | gpointer as; | |
236 | ||
237 | array = g_ptr_array_new (); | |
238 | ||
239 | g_hash_table_foreach (watcher->items, (GHFunc) populate_published_list, array); | |
240 | g_ptr_array_add (array, NULL); | |
241 | ||
242 | as = g_ptr_array_free (array, FALSE); | |
243 | ||
244 | sn_watcher_interface_set_registered_status_notifier_items (watcher->skeleton, | |
245 | (const gchar * const *) as); | |
246 | ||
247 | g_strfreev ((gchar **) as); | |
248 | ||
249 | g_dbus_interface_skeleton_flush (G_DBUS_INTERFACE_SKELETON (watcher->skeleton)); | |
250 | } | |
251 | ||
252 | static gboolean | |
253 | create_key (const gchar *sender, | |
254 | const gchar *service, | |
255 | gchar **key, | |
256 | gchar **bus_name, | |
257 | gchar **path) | |
258 | { | |
259 | gchar *temp_key, *temp_bname, *temp_path; | |
260 | ||
261 | temp_key = temp_bname = temp_path = NULL; | |
262 | *key = *bus_name = *path = NULL; | |
263 | ||
264 | if (g_str_has_prefix (service, "/")) | |
265 | { | |
266 | temp_bname = g_strdup (sender); | |
267 | temp_path = g_strdup (service); | |
268 | } | |
269 | else | |
270 | { | |
271 | temp_bname = g_strdup (service); | |
272 | temp_path = g_strdup ("/StatusNotifierItem"); | |
273 | } | |
274 | ||
275 | if (!g_dbus_is_name (temp_bname)) | |
276 | { | |
277 | g_free (temp_bname); | |
278 | g_free (temp_path); | |
279 | ||
280 | return FALSE; | |
281 | } | |
282 | ||
283 | temp_key = g_strdup_printf ("%s%s", temp_bname, temp_path); | |
284 | ||
285 | g_debug ("Key: '%s', busname '%s', path '%s'", temp_key, temp_bname, temp_path); | |
286 | ||
287 | *key = temp_key; | |
288 | *bus_name = temp_bname; | |
289 | *path = temp_path; | |
290 | ||
291 | return TRUE; | |
292 | } | |
293 | ||
294 | static gboolean | |
295 | handle_register_item (SnWatcherInterface *skeleton, | |
296 | GDBusMethodInvocation *invocation, | |
297 | const gchar* service, | |
298 | XAppSnWatcher *watcher) | |
299 | { | |
300 | SnItem *item; | |
301 | GError *error; | |
302 | const gchar *sender; | |
303 | g_autofree gchar *key, *bus_name, *path; | |
304 | ||
305 | sender = g_dbus_method_invocation_get_sender (invocation); | |
306 | ||
307 | if (!create_key (sender, service, &key, &bus_name, &path)) | |
308 | { | |
309 | error = g_error_new (g_dbus_error_quark (), | |
310 | G_DBUS_ERROR_INVALID_ARGS, | |
311 | "Invalid bus name from: %s, %s", service, sender); | |
312 | g_dbus_method_invocation_return_gerror (invocation, error); | |
313 | ||
314 | return FALSE; | |
315 | } | |
316 | ||
317 | item = g_hash_table_lookup (watcher->items, key); | |
318 | ||
319 | if (item == NULL) | |
320 | { | |
321 | SnItemInterface *proxy; | |
322 | error = NULL; | |
323 | g_debug ("Key: '%s'", key); | |
324 | ||
325 | proxy = sn_item_interface_proxy_new_sync (watcher->connection, | |
326 | G_DBUS_PROXY_FLAGS_NONE, | |
327 | bus_name, | |
328 | path, | |
329 | NULL, | |
330 | &error); | |
331 | ||
332 | if (error != NULL) | |
333 | { | |
334 | g_debug ("Could not create new status notifier proxy item for item at %s: %s", bus_name, error->message); | |
335 | ||
336 | g_dbus_method_invocation_return_gerror (invocation, error); | |
337 | ||
338 | return FALSE; | |
339 | } | |
340 | ||
341 | item = sn_item_new ((GDBusProxy *) proxy, | |
342 | g_str_has_prefix (path, APPINDICATOR_PATH_PREFIX)); | |
343 | ||
344 | g_hash_table_insert (watcher->items, | |
345 | g_strdup (key), | |
346 | item); | |
347 | ||
348 | update_published_items (watcher); | |
349 | ||
350 | sn_watcher_interface_emit_status_notifier_item_registered (skeleton, | |
351 | service); | |
352 | } | |
353 | ||
354 | sn_watcher_interface_complete_register_status_notifier_item (skeleton, | |
355 | invocation); | |
356 | ||
357 | return TRUE; | |
358 | } | |
359 | ||
360 | static gboolean | |
361 | export_watcher_interface (XAppSnWatcher *watcher) | |
362 | { | |
363 | GError *error = NULL; | |
364 | ||
365 | if (watcher->skeleton) { | |
366 | return TRUE; | |
367 | } | |
368 | ||
369 | watcher->skeleton = sn_watcher_interface_skeleton_new (); | |
370 | ||
371 | g_debug ("XAppSnWatcher: exporting StatusNotifierWatcher dbus interface to %s", NOTIFICATION_WATCHER_PATH); | |
372 | ||
373 | g_signal_connect (watcher->skeleton, | |
374 | "handle-register-status-notifier-item", | |
375 | G_CALLBACK (handle_register_item), | |
376 | watcher); | |
377 | ||
378 | g_signal_connect (watcher->skeleton, | |
379 | "handle-register-status-notifier-host", | |
380 | G_CALLBACK (handle_register_host), | |
381 | watcher); | |
382 | g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (watcher->skeleton), | |
383 | watcher->connection, | |
384 | NOTIFICATION_WATCHER_PATH, | |
385 | &error); | |
386 | ||
387 | if (error != NULL) { | |
388 | g_critical ("XAppSnWatcher: could not export StatusNotifierWatcher interface: %s", error->message); | |
389 | g_error_free (error); | |
390 | ||
391 | return FALSE; | |
392 | } | |
393 | ||
394 | ||
395 | return TRUE; | |
396 | } | |
397 | ||
398 | static gboolean | |
399 | on_interrupt (XAppSnWatcher *watcher) | |
400 | { | |
401 | g_debug ("SIGINT - shutting down immediately"); | |
402 | ||
403 | g_application_quit (G_APPLICATION (watcher)); | |
404 | return FALSE; | |
405 | } | |
406 | ||
407 | static void | |
408 | continue_startup (XAppSnWatcher *watcher) | |
409 | { | |
410 | g_debug ("Trying to acquire session bus connection"); | |
411 | ||
412 | g_unix_signal_add (SIGINT, (GSourceFunc) on_interrupt, watcher); | |
413 | g_application_hold (G_APPLICATION (watcher)); | |
414 | ||
415 | export_watcher_interface (watcher); | |
416 | ||
417 | watcher->owner_id = g_bus_own_name_on_connection (watcher->connection, | |
418 | NOTIFICATION_WATCHER_NAME, | |
419 | G_BUS_NAME_OWNER_FLAGS_REPLACE, | |
420 | on_name_acquired, | |
421 | on_name_lost, | |
422 | watcher, | |
423 | NULL); | |
424 | } | |
425 | ||
426 | static void | |
427 | watcher_startup (GApplication *application) | |
428 | { | |
429 | XAppSnWatcher *watcher = (XAppSnWatcher*) application; | |
430 | GError *error; | |
431 | ||
432 | G_APPLICATION_CLASS (xapp_sn_watcher_parent_class)->startup (application); | |
433 | ||
434 | watcher->items = g_hash_table_new_full (g_str_hash, g_str_equal, | |
435 | g_free, g_object_unref); | |
436 | ||
437 | /* This buys us 30 seconds (gapp timeout) - we'll either be re-held immediately | |
438 | * because there's a monitor or exit after the 30 seconds. */ | |
439 | g_application_hold (application); | |
440 | g_application_release (application); | |
441 | ||
442 | error = NULL; | |
443 | ||
444 | watcher->connection = g_bus_get_sync (G_BUS_TYPE_SESSION, | |
445 | NULL, | |
446 | &error); | |
447 | ||
448 | if (error != NULL) | |
449 | { | |
450 | g_critical ("Could not get session bus: %s\n", error->message); | |
451 | g_application_quit (application); | |
452 | } | |
453 | ||
454 | add_name_listener (watcher); | |
455 | ||
456 | if (xapp_status_icon_any_monitors ()) | |
457 | { | |
458 | continue_startup (watcher); | |
459 | } | |
460 | else | |
461 | { | |
462 | g_debug ("No active monitors, exiting in 30s"); | |
463 | watcher->shutdown_pending = TRUE; | |
464 | } | |
465 | } | |
466 | ||
467 | static void | |
468 | watcher_finalize (GObject *object) | |
469 | { | |
470 | G_OBJECT_CLASS (xapp_sn_watcher_parent_class)->finalize (object); | |
471 | } | |
472 | ||
473 | static void | |
474 | watcher_shutdown (GApplication *application) | |
475 | { | |
476 | XAppSnWatcher *watcher = (XAppSnWatcher *) application; | |
477 | ||
478 | if (watcher->name_listener_id > 0) | |
479 | { | |
480 | g_dbus_connection_signal_unsubscribe (watcher->connection, watcher->name_listener_id); | |
481 | watcher->name_listener_id = 0; | |
482 | } | |
483 | ||
484 | g_clear_pointer (&watcher->items, g_hash_table_unref); | |
485 | ||
486 | if (watcher->owner_id > 0) | |
487 | { | |
488 | g_bus_unown_name (watcher->owner_id); | |
489 | } | |
490 | ||
491 | if (watcher->skeleton != NULL) | |
492 | { | |
493 | g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (watcher->skeleton)); | |
494 | g_clear_object (&watcher->skeleton); | |
495 | } | |
496 | ||
497 | g_clear_object (&watcher->connection); | |
498 | ||
499 | G_APPLICATION_CLASS (xapp_sn_watcher_parent_class)->shutdown (application); | |
500 | } | |
501 | ||
502 | static void | |
503 | watcher_activate (GApplication *application) | |
504 | { | |
505 | } | |
506 | ||
507 | static void | |
508 | xapp_sn_watcher_init (XAppSnWatcher *watcher) | |
509 | { | |
510 | } | |
511 | ||
512 | static void | |
513 | xapp_sn_watcher_class_init (XAppSnWatcherClass *class) | |
514 | { | |
515 | GApplicationClass *application_class = G_APPLICATION_CLASS (class); | |
516 | GObjectClass *object_class = G_OBJECT_CLASS (class); | |
517 | ||
518 | application_class->startup = watcher_startup; | |
519 | application_class->shutdown = watcher_shutdown; | |
520 | application_class->activate = watcher_activate; | |
521 | object_class->finalize = watcher_finalize; | |
522 | } | |
523 | ||
524 | XAppSnWatcher * | |
525 | watcher_new (void) | |
526 | { | |
527 | XAppSnWatcher *watcher; | |
528 | ||
529 | g_set_application_name ("xapp-sn-watcher"); | |
530 | ||
531 | watcher = g_object_new (xapp_sn_watcher_get_type (), | |
532 | "application-id", "org.x.StatusNotifierWatcher", | |
533 | "inactivity-timeout", 30000, | |
534 | "register-session", TRUE, | |
535 | NULL); | |
536 | ||
537 | return watcher; | |
538 | } | |
539 | ||
540 | int | |
541 | main (int argc, char **argv) | |
542 | { | |
543 | XAppSnWatcher *watcher; | |
544 | int status; | |
545 | ||
546 | watcher = watcher_new (); | |
547 | ||
548 | status = g_application_run (G_APPLICATION (watcher), argc, argv); | |
549 | ||
550 | g_object_unref (watcher); | |
551 | ||
552 | return status; | |
553 | } |
0 | #!/usr/bin/env python3 | |
1 | ||
2 | import sys | |
3 | import gettext | |
4 | import gi | |
5 | gi.require_version('Gtk', '3.0') | |
6 | gi.require_version('XApp', '1.0') | |
7 | from gi.repository import Gtk, Gdk, Gio, XApp, GLib | |
8 | import setproctitle | |
9 | import signal | |
10 | ||
11 | from itemWrapper import SnItemWrapper | |
12 | from nameWatcher import BusNameWatcher | |
13 | ||
14 | setproctitle.setproctitle("xapp-sn-watcher") | |
15 | ||
16 | NOTIFICATION_WATCHER_NAME = "org.kde.StatusNotifierWatcher" | |
17 | NOTIFICATION_WATCHER_PATH = "/StatusNotifierWatcher" | |
18 | STATUS_ICON_MONITOR_PREFIX = "org.x.StatusIconMonitor" | |
19 | ||
20 | class XAppSNDaemon(Gtk.Application): | |
21 | def __init__(self): | |
22 | super(XAppSNDaemon, self).__init__(register_session=True, | |
23 | application_id="org.x.StatusNotifierWatcher", | |
24 | flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE) | |
25 | ||
26 | self.items = {} | |
27 | self.watcher = None | |
28 | self.bus = None | |
29 | self.bus_watcher = None | |
30 | self.shutdown_timer_id = 0 | |
31 | ||
32 | self.add_main_option("quit", ord("q"), GLib.OptionFlags.NONE, | |
33 | GLib.OptionArg.NONE, "End the watcher process", None) | |
34 | ||
35 | signal.signal(signal.SIGINT, self.interrupt) | |
36 | ||
37 | def do_command_line(self, command_line): | |
38 | options = command_line.get_options_dict() | |
39 | options = options.end().unpack() | |
40 | ||
41 | if "quit" in options: | |
42 | if self.watcher != None: | |
43 | print("Shutting down the XApp StatusNotifierWatcher") | |
44 | self.shutdown() | |
45 | else: | |
46 | print("XApp StatusNotifierWatcher not running") | |
47 | exit(0) | |
48 | ||
49 | return 0 | |
50 | ||
51 | def do_activate(self): | |
52 | self.hold() | |
53 | ||
54 | def interrupt(self, signal, frame): | |
55 | self.shutdown() | |
56 | ||
57 | def do_startup(self): | |
58 | Gtk.Application.do_startup(self) | |
59 | ||
60 | self.bus_watcher = BusNameWatcher() | |
61 | self.bus_watcher.connect("owner-lost", self.name_owner_lost) | |
62 | self.bus_watcher.connect("owner-appeared", self.name_owner_appeared) | |
63 | ||
64 | # Don't bother to continue if there are no monitors | |
65 | if XApp.StatusIcon.any_monitors(): | |
66 | self.continue_startup() | |
67 | else: | |
68 | print("No active monitors, exiting in 30s") | |
69 | self.start_shutdown_timer() | |
70 | ||
71 | def continue_startup(self): | |
72 | self.hold() | |
73 | ||
74 | Gio.bus_own_name(Gio.BusType.SESSION, | |
75 | NOTIFICATION_WATCHER_NAME, | |
76 | Gio.BusNameOwnerFlags.REPLACE, | |
77 | self.on_bus_acquired, | |
78 | self.on_name_acquired, | |
79 | self.on_name_lost) | |
80 | ||
81 | def on_name_lost(self, connection, name, data=None): | |
82 | """ | |
83 | Failed to acquire our name - just exit. | |
84 | """ | |
85 | print("Something is wrong, exiting.") | |
86 | self.shutdown() | |
87 | ||
88 | def on_name_acquired(self, connection, name, data=None): | |
89 | print("Starting xapp-sn-daemon...") | |
90 | ||
91 | def on_bus_acquired(self, connection, name, data=None): | |
92 | self.bus = connection | |
93 | ||
94 | self.watcher = XApp.FdoSnWatcherSkeleton.new() | |
95 | ||
96 | self.watcher.props.is_status_notifier_host_registered = True | |
97 | self.watcher.props.registered_status_notifier_items = [] | |
98 | self.protocol_version = 0 | |
99 | ||
100 | self.watcher.connect("handle-register-status-notifier-item", self.handle_register_item) | |
101 | self.watcher.connect("handle-register-status-notifier-host", self.handle_register_host) | |
102 | ||
103 | self.watcher.export(self.bus, NOTIFICATION_WATCHER_PATH) | |
104 | ||
105 | def handle_register_item(self, watcher, invocation, service): | |
106 | sender = invocation.get_sender() | |
107 | # print("register item: %s, %s" % (service, sender)) | |
108 | ||
109 | ||
110 | try: | |
111 | key, bus_name, path = self.create_key(sender, service) | |
112 | ||
113 | if key == None: | |
114 | invocation.return_error_literal(domain=int(Gio.dbus_error_quark()), | |
115 | code=Gio.DBusError.INVALID_ARGS, | |
116 | message="Invalid bus name from : %s, %s" % (service, sender)) | |
117 | return False | |
118 | ||
119 | try: | |
120 | existing = self.items[key] | |
121 | except KeyError: | |
122 | existing = None | |
123 | ||
124 | if not existing: | |
125 | proxy = XApp.FdoSnItemProxy.new_sync(self.bus, | |
126 | Gio.DBusProxyFlags.NONE, | |
127 | bus_name, | |
128 | path, | |
129 | None) | |
130 | ||
131 | wrapper = SnItemWrapper(proxy) | |
132 | ||
133 | self.items[key] = wrapper | |
134 | self.update_published_items() | |
135 | ||
136 | except GLib.Error as e: | |
137 | print(e.message) | |
138 | invocation.return_gerror(e) | |
139 | return False | |
140 | ||
141 | watcher.complete_register_status_notifier_item(invocation) | |
142 | watcher.emit_status_notifier_item_registered(service) | |
143 | ||
144 | return True | |
145 | ||
146 | def create_key(self, sender, service): | |
147 | if service[0] == "/": | |
148 | bus_name = sender | |
149 | path = service | |
150 | else: | |
151 | bus_name = service | |
152 | path = "/StatusNotifierItem" | |
153 | ||
154 | if not Gio.dbus_is_name(bus_name): | |
155 | return None, None, None | |
156 | ||
157 | key = "%s%s" % (bus_name, path) | |
158 | ||
159 | # print("Key: '%s' - from busname '%s', path '%s'" % (key, bus_name, path)) | |
160 | ||
161 | return key, bus_name, path | |
162 | ||
163 | def update_published_items(self): | |
164 | self.watcher.props.registered_status_notifier_items = list(self.items.keys()) | |
165 | ||
166 | def name_owner_lost(self, watcher, name, old_owner): | |
167 | for key in self.items.keys(): | |
168 | if key.startswith(name): | |
169 | # print("'%s' left the bus, owned by %s" % (name, old_owner)) | |
170 | self.remove_item(key) | |
171 | return | |
172 | ||
173 | if name.startswith(STATUS_ICON_MONITOR_PREFIX): | |
174 | # We lost a consumer, we'll check if there are any more and exit if not | |
175 | print("Lost a monitor, checking for any more") | |
176 | ||
177 | if XApp.StatusIcon.any_monitors(): | |
178 | return | |
179 | else: | |
180 | print("Lost our last monitor, starting countdown") | |
181 | self.start_shutdown_timer() | |
182 | ||
183 | def name_owner_appeared(self, watcher, name, new_owner): | |
184 | if name.startswith(STATUS_ICON_MONITOR_PREFIX): | |
185 | print("A monitor appeared on the bus") | |
186 | self.cancel_shutdown_timer() | |
187 | ||
188 | # finish setting up if we haven't yet | |
189 | if self.watcher == None: | |
190 | self.continue_startup() | |
191 | ||
192 | def cancel_shutdown_timer(self): | |
193 | if self.shutdown_timer_id > 0: | |
194 | GLib.source_remove(self.shutdown_timer_id) | |
195 | self.shutdown_timer_id = 0 | |
196 | ||
197 | def start_shutdown_timer(self): | |
198 | self.cancel_shutdown_timer() | |
199 | ||
200 | self.shutdown_timer_id = GLib.timeout_add_seconds(30, self.delayed_exit_timeout) | |
201 | ||
202 | def delayed_exit_timeout(self): | |
203 | if not XApp.StatusIcon.any_monitors(): | |
204 | print("No monitors after 30s, exiting") | |
205 | self.shutdown() | |
206 | ||
207 | return GLib.SOURCE_REMOVE | |
208 | ||
209 | def remove_item(self, key): | |
210 | try: | |
211 | item = self.items[key] | |
212 | ||
213 | item.destroy() | |
214 | del self.items[key] | |
215 | ||
216 | self.update_published_items() | |
217 | ||
218 | except KeyError: | |
219 | print("destroying non-existent item: %s" % key) | |
220 | ||
221 | def handle_register_host(self, watcher, invocation, service): | |
222 | watcher.complete_register_status_notifier_host(invocation) | |
223 | ||
224 | return True | |
225 | ||
226 | def shutdown(self): | |
227 | print("Shutting down") | |
228 | ||
229 | if self.bus_watcher != None: | |
230 | self.bus_watcher.destroy() | |
231 | self.bus_watcher = None | |
232 | ||
233 | keys = list(self.items.keys()) | |
234 | ||
235 | for key in keys: | |
236 | self.remove_item(key) | |
237 | ||
238 | if self.watcher: | |
239 | self.watcher.unexport() | |
240 | self.watcher = None | |
241 | ||
242 | self.quit() | |
243 | ||
244 | if __name__ == "__main__": | |
245 | d = XAppSNDaemon() | |
246 | ||
247 | try: | |
248 | d.run(sys.argv) | |
249 | except: | |
250 | d.shutdown() |