Codebase list xapp / e450f95
xapp-sn-watcher: rewrite in C due to leaky dbus python bindings. Michael Webster 4 years ago
22 changed file(s) with 1673 addition(s) and 958 deletion(s). Raw diff Collapse all Expand all
1313 libglib2.0-dev (>= 2.37.3),
1414 libgnomekbd-dev,
1515 libgtk-3-dev (>= 3.3.16),
16 libdbusmenu-gtk3-dev,
1617 libx11-dev,
1718 libxkbfile-dev,
1819 meson,
5051 gir1.2-xapp-1.0 (= ${binary:Version}),
5152 libgnomekbd-dev,
5253 libgtk-3-dev (>= 3.3.16),
53 gir1.2-dbusmenu-gtk3-0.4,
5454 libxapp1 (= ${binary:Version}),
5555 libxkbfile-dev,
5656 ${misc:Depends},
00 usr/lib/*/libxapp.so.1*
1 usr/libexec/xapps/sn-watcher/*
11 usr/bin/
22 usr/share/icons
33 usr/share/locale
4 usr/libexec/xapps
4 usr/libexec/xapps/*.py
55 usr/share/mate-panel/applets
66 usr/share/dbus-1/services
3333 'xapp-status-icon-monitor.c'
3434 ]
3535
36 codegen = find_program('g-codegen.py')
37
3836 dbus_headers = []
3937
4038 # FIXME: Ugly workaround that simulates the generation of
6058 dbus_headers += xapp_statusicon_interface_sources[0]
6159 xapp_sources += xapp_statusicon_interface_sources[1]
6260
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
9761 # You can't actually access the generated header udring the install_header command below,
9862 # because the command is evaluated prior to the files being generated. So we need to manually
9963 # install the dbus header file (custom install scripts really *do* get evaluated after build,
10064 # 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'))
10266
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 )
10570
10671 xapp_enums = gnome.mkenums('xapp-enums',
10772 sources : xapp_headers,
+0
-82
libxapp/sn-item.xml less more
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
-29
libxapp/sn-watcher.xml less more
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>
2727 #define MAX_NAME_FAILS 3
2828
2929 #define MAX_SANE_ICON_SIZE 96
30 #define FALLBACK_ICON_SIZE 24
3031
3132 static gint unique_id = 0;
3233
14921493 gint
14931494 xapp_status_icon_get_icon_size (XAppStatusIcon *icon)
14941495 {
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 }
14961504
14971505 gint size;
14981506
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])
3434 )
3535
3636 top_inc = include_directories('.')
37 codegen = find_program(join_paths(meson.source_root(), 'meson-scripts', 'g-codegen.py'))
3738
3839 subdir('icons')
3940 subdir('libxapp')
+0
-277
xapp-sn-watcher/itemWrapper.py less more
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 )
05
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 )
811
912 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 )
1413
1514 ## DBus service file
1615
2625 install_data(service_file,
2726 install_dir: dbus_services_dir
2827 )
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
-64
xapp-sn-watcher/nameWatcher.py less more
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
-181
xapp-sn-watcher/notifierItem.py less more
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))
00 [D-BUS Service]
11 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
-20
xapp-sn-watcher/util.py less more
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
-251
xapp-sn-watcher/xapp-sn-watcher.py less more
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()