1#!/usr/local/bin/python3.8
2
3import locale
4import gettext
5import json
6import os
7import sys
8import setproctitle
9
10import gi
11gi.require_version("Gtk", "3.0")
12gi.require_version("XApp", "1.0")
13gi.require_version('MatePanelApplet', '4.0')
14from gi.repository import Gtk, GdkPixbuf, Gdk, GObject, Gio, XApp, GLib, MatePanelApplet
15
16import applet_constants
17
18# Rename the process
19setproctitle.setproctitle('mate-xapp-status-applet')
20
21# i18n
22gettext.install("xapp", applet_constants.LOCALEDIR)
23locale.bindtextdomain("xapp", applet_constants.LOCALEDIR)
24locale.textdomain("xapp")
25
26ICON_SIZE_REDUCTION = 2
27VISIBLE_LABEL_MARGIN = 5 # When an icon has a label, add a margin between icon and label
28SYMBOLIC_ICON_SIZE = 22
29
30statusicon_css_string = """
31.statuswidget-horizontal {
32    border: none;
33    padding-top: 0;
34    padding-left: 2px;
35    padding-bottom: 0;
36    padding-right: 2px;
37}
38.statuswidget-vertical {
39    border: none;
40    padding-top: 2px;
41    padding-left: 0;
42    padding-bottom: 2px;
43    padding-right: 0;
44}
45"""
46
47def translate_applet_orientation_to_xapp(mate_applet_orientation):
48    # wtf...mate panel's orientation is.. the direction to center of monitor?
49    if mate_applet_orientation == MatePanelApplet.AppletOrient.UP:
50        return Gtk.PositionType.BOTTOM
51    elif mate_applet_orientation == MatePanelApplet.AppletOrient.DOWN:
52        return Gtk.PositionType.TOP
53    elif mate_applet_orientation == MatePanelApplet.AppletOrient.LEFT:
54        return Gtk.PositionType.RIGHT
55    elif mate_applet_orientation == MatePanelApplet.AppletOrient.RIGHT:
56        return Gtk.PositionType.LEFT
57
58class StatusWidget(Gtk.ToggleButton):
59    __gsignals__ = {
60        "re-sort": (GObject.SignalFlags.RUN_LAST, None, ())
61    }
62
63    def __init__(self, icon, orientation, size):
64        super(Gtk.ToggleButton, self).__init__()
65        self.theme = Gtk.IconTheme.get_default()
66        self.orientation = orientation
67        self.size = size
68
69        self.proxy = icon
70        self.proxy.props.icon_size = size
71
72        # this is the bus owned name
73        self.name = self.proxy.get_name()
74
75        self.add_events(Gdk.EventMask.SCROLL_MASK)
76
77        # this is (usually) the name of the remote process
78        self.proc_name = self.proxy.props.name
79
80        self.box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
81
82        self.image = Gtk.Image(hexpand=True)
83        self.label = Gtk.Label(no_show_all=True)
84        self.box.pack_start(self.image, True, False, 0)
85        self.box.pack_start(self.label, False, False, 0)
86        self.add(self.box)
87
88        self.set_can_default(False)
89        self.set_can_focus(False)
90        self.set_relief(Gtk.ReliefStyle.NONE)
91        self.set_focus_on_click(False)
92
93        self.show_all()
94
95        flags = GObject.BindingFlags.DEFAULT | GObject.BindingFlags.SYNC_CREATE
96
97        self.proxy.bind_property("label", self.label, "label", flags)
98        self.proxy.bind_property("tooltip-text", self, "tooltip-markup", flags)
99        self.proxy.bind_property("visible", self, "visible", flags)
100
101        self.proxy.connect("notify::primary-menu-is-open", self.menu_state_changed)
102        self.proxy.connect("notify::secondary-menu-is-open", self.menu_state_changed)
103
104        self.highlight_both_menus = False
105
106        if self.proxy.props.metadata not in ("", None):
107            try:
108                meta = json.loads(self.proxy.props.metadata)
109                if meta["highlight-both-menus"]:
110                    self.highlight_both_menus = True
111            except json.JSONDecodeError as e:
112                print("Could not read metadata: %s" % e)
113
114        self.proxy.connect("notify::icon-name", self._on_icon_name_changed)
115        self.proxy.connect("notify::name", self._on_name_changed)
116
117        self.in_widget = False
118        self.plain_surface = None
119        self.saturated_surface = None
120
121        self.menu_opened = False
122
123        self.connect("button-press-event", self.on_button_press)
124        self.connect("button-release-event", self.on_button_release)
125        self.connect("scroll-event", self.on_scroll)
126        self.connect("enter-notify-event", self.on_enter_notify)
127        self.connect("leave-notify-event", self.on_leave_notify)
128
129        self.update_orientation()
130        self.update_icon()
131
132    def _on_icon_name_changed(self, proxy, gparamspec, data=None):
133        self.update_icon()
134
135    def _on_name_changed(self, proxy, gparamspec, data=None):
136        self.emit("re-sort")
137
138    def update_icon(self):
139        string = self.proxy.props.icon_name
140        self.proxy.props.icon_size = self.size
141
142        self.set_icon(string)
143
144    def update_style(self, orientation):
145        ctx = self.get_style_context()
146
147        if orientation == Gtk.Orientation.HORIZONTAL:
148            ctx.remove_class("statuswidget-vertical")
149            ctx.add_class("statuswidget-horizontal")
150        else:
151            ctx.remove_class("statuswidget-horizontal")
152            ctx.add_class("statuswidget-vertical")
153
154    def update_orientation(self):
155        if self.orientation in (MatePanelApplet.AppletOrient.UP, MatePanelApplet.AppletOrient.DOWN):
156            box_orientation = Gtk.Orientation.HORIZONTAL
157        else:
158            box_orientation = Gtk.Orientation.VERTICAL
159
160        self.update_style(box_orientation)
161        self.box.set_orientation(box_orientation)
162
163        if len(self.label.props.label) > 0 and box_orientation == Gtk.Orientation.HORIZONTAL:
164            self.label.set_visible(True)
165            self.label.set_margin_start(VISIBLE_LABEL_MARGIN)
166        else:
167            self.label.set_visible(False)
168            self.label.set_margin_start(0)
169
170    def set_icon(self, string):
171        fallback = True
172
173        if string:
174            if "symbolic" in string:
175                size = SYMBOLIC_ICON_SIZE
176            else:
177                size = self.size - ICON_SIZE_REDUCTION
178
179            self.image.set_pixel_size(size)
180
181            try:
182                if os.path.exists(string):
183                    icon_file = Gio.File.new_for_path(string)
184                    icon = Gio.FileIcon.new(icon_file)
185                    self.image.set_from_gicon(icon, Gtk.IconSize.MENU)
186                else:
187                    if self.theme.has_icon(string):
188                        icon = Gio.ThemedIcon.new(string)
189                        self.image.set_from_gicon(icon, Gtk.IconSize.MENU)
190
191                fallback = False
192            except GLib.Error as e:
193                print("MateXAppStatusApplet: Could not load icon '%s' for '%s': %s" % (string, self.proc_name, e.message))
194            except TypeError as e:
195                print("MateXAppStatusApplet: Could not load icon '%s' for '%s': %s" % (string, self.proc_name, str(e)))
196
197        #fallback
198        if fallback:
199            self.image.set_pixel_size(self.size - ICON_SIZE_REDUCTION)
200            self.image.set_from_icon_name("image-missing", Gtk.IconSize.MENU)
201
202    def menu_state_changed(self, proxy, pspec, data=None):
203        if pspec.name == "primary-menu-is-open":
204            prop = proxy.props.primary_menu_is_open
205        else:
206            prop = proxy.props.secondary_menu_is_open
207
208        if not self.menu_opened or prop == False:
209            self.set_active(False)
210            return
211
212        self.set_active(prop)
213        self.menu_opened = False
214
215    # TODO?
216    def on_enter_notify(self, widget, event):
217        self.in_widget = True
218
219        return Gdk.EVENT_PROPAGATE
220
221    def on_leave_notify(self, widget, event):
222        self.in_widget = False
223
224        return Gdk.EVENT_PROPAGATE
225    # /TODO
226
227    def on_button_press(self, widget, event):
228        self.menu_opened = False
229
230        # If the user does ctrl->right-click, open the applet's about menu
231        # instead of sending to the app.
232        if event.state & Gdk.ModifierType.CONTROL_MASK and event.button == Gdk.BUTTON_SECONDARY:
233           return Gdk.EVENT_PROPAGATE
234
235        orientation = translate_applet_orientation_to_xapp(self.orientation)
236
237        x, y = self.calc_menu_origin(widget, orientation)
238        self.proxy.call_button_press(x, y, event.button, event.time, orientation, None, None)
239
240        if event.button in (Gdk.BUTTON_MIDDLE, Gdk.BUTTON_SECONDARY):
241            # Block the 'remove from panel' menu, and the middle-click drag.
242            # They can still accomplish these things along the edges of the applet
243            return Gdk.EVENT_STOP
244
245        return Gdk.EVENT_STOP
246
247    def on_button_release(self, widget, event):
248        orientation = translate_applet_orientation_to_xapp(self.orientation)
249
250        if event.button == Gdk.BUTTON_PRIMARY:
251            self.menu_opened = True
252        elif event.button == Gdk.BUTTON_SECONDARY and self.highlight_both_menus:
253            self.menu_opened = True
254
255        x, y = self.calc_menu_origin(widget, orientation)
256        self.proxy.call_button_release(x, y, event.button, event.time, orientation, None, None)
257
258        return Gdk.EVENT_PROPAGATE
259
260    def on_scroll(self, widget, event):
261        has, direction = event.get_scroll_direction()
262
263        x_dir = XApp.ScrollDirection.UP
264        delta = 0
265
266        if direction != Gdk.ScrollDirection.SMOOTH:
267            x_dir = XApp.ScrollDirection(int(direction))
268
269            if direction == Gdk.ScrollDirection.UP:
270                delta = -1
271            elif direction == Gdk.ScrollDirection.DOWN:
272                delta = 1
273            elif direction == Gdk.ScrollDirection.LEFT:
274                delta = -1
275            elif direction == Gdk.ScrollDirection.RIGHT:
276                delta = 1
277
278        self.proxy.call_scroll_sync(delta, x_dir, event.time, None)
279
280    def calc_menu_origin(self, widget, orientation):
281        alloc = widget.get_allocation()
282        ignore, x, y = widget.get_window().get_origin()
283        rx = 0
284        ry = 0
285
286        if orientation == Gtk.PositionType.TOP:
287            rx = x + alloc.x
288            ry = y + alloc.y + alloc.height
289        elif orientation == Gtk.PositionType.BOTTOM:
290            rx = x + alloc.x
291            ry = y + alloc.y
292        elif orientation == Gtk.PositionType.LEFT:
293            rx = x + alloc.x + alloc.width
294            ry = y + alloc.y
295        elif orientation == Gtk.PositionType.RIGHT:
296            rx = x + alloc.x
297            ry = y + alloc.y
298        else:
299            rx = x
300            ry = y
301
302        return rx, ry
303
304class MateXAppStatusApplet(object):
305    def __init__(self, applet, iid):
306        self.applet = applet
307        self.applet.set_flags(MatePanelApplet.AppletFlags.EXPAND_MINOR)
308        self.applet.set_can_focus(False)
309        self.applet.set_background_widget(self.applet)
310
311        self.add_about()
312
313        button_css = Gtk.CssProvider()
314
315        if button_css.load_from_data(statusicon_css_string.encode()):
316            Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(), button_css, 600)
317
318        self.applet.connect("realize", self.on_applet_realized)
319        self.applet.connect("destroy", self.on_applet_destroy)
320
321        self.indicators = {}
322        self.monitor = None
323
324    def add_about(self):
325        group = Gtk.ActionGroup(name="xapp-status-applet-group")
326        group.set_translation_domain("xapp")
327
328        about_action = Gtk.Action(name="ShowAbout",
329                                  icon_name="info",
330                                  label=_("About"),
331                                  visible=True)
332
333        about_action.connect("activate", self.show_about)
334        group.add_action(about_action)
335
336        xml = '\
337            <menuitem name="ShowDesktopAboutItem" action="ShowAbout"/> \
338        '
339
340        self.applet.setup_menu(xml, group)
341
342    def show_about(self, action, data=None):
343        dialog = Gtk.AboutDialog.new()
344
345        dialog.set_program_name("XApp Status Applet")
346        dialog.set_version(applet_constants.PKGVERSION)
347        dialog.set_license_type(Gtk.License.GPL_3_0)
348        dialog.set_website("https://github.com/linuxmint/xapps")
349        dialog.set_logo_icon_name("panel-applets")
350        dialog.set_comments(_("Area where XApp status icons appear"))
351
352        dialog.run()
353        dialog.destroy()
354
355    def on_applet_realized(self, widget, data=None):
356        self.indicator_box = Gtk.Box(visible=True)
357
358        self.applet.add(self.indicator_box)
359        self.applet.connect("change-size", self.on_applet_size_changed)
360        self.applet.connect("change-orient", self.on_applet_orientation_changed)
361        self.update_orientation()
362
363        if not self.monitor:
364            self.setup_monitor()
365
366    def on_applet_destroy(self, widget, data=None):
367        self.destroy_monitor()
368        Gtk.main_quit()
369
370    def setup_monitor (self):
371        self.monitor = XApp.StatusIconMonitor()
372        self.monitor.connect("icon-added", self.on_icon_added)
373        self.monitor.connect("icon-removed", self.on_icon_removed)
374
375    def make_key(self, proxy):
376        name = proxy.get_name()
377        path = proxy.get_object_path()
378
379        # print("Key: %s" % (name+path))
380        return name + path
381
382    def destroy_monitor (self):
383        for key in self.indicators.keys():
384            self.indicator_box.remove(self.indicators[key])
385
386        self.monitor = None
387        self.indicators = {}
388
389    def on_icon_added(self, monitor, proxy):
390        key = self.make_key(proxy)
391
392        self.indicators[key] = StatusWidget(proxy, self.applet.get_orient(), self.applet.get_size())
393        self.indicator_box.add(self.indicators[key])
394        self.indicators[key].connect("re-sort", self.sort_icons)
395
396        self.sort_icons()
397
398    def on_icon_removed(self, monitor, proxy):
399        key = self.make_key(proxy)
400
401        self.indicator_box.remove(self.indicators[key])
402        self.indicators[key].disconnect_by_func(self.sort_icons)
403        del(self.indicators[key])
404
405        self.sort_icons()
406
407    def update_orientation(self):
408        self.on_applet_orientation_changed(self, self.applet.get_orient())
409
410    def on_applet_size_changed(self, applet, size, data=None):
411        for key in self.indicators.keys():
412            indicator = self.indicators[key]
413
414            indicator.size = applet.get_size()
415            indicator.update_icon()
416
417        self.applet.queue_resize()
418
419    def on_applet_orientation_changed(self, applet, applet_orient, data=None):
420        orient = self.applet.get_orient()
421
422        for key in self.indicators.keys():
423            indicator = self.indicators[key]
424
425            indicator.orientation = orient
426            indicator.update_orientation()
427
428        if orient in (MatePanelApplet.AppletOrient.LEFT, MatePanelApplet.AppletOrient.RIGHT):
429            self.indicator_box.set_orientation(Gtk.Orientation.VERTICAL)
430
431            self.indicator_box.props.margin_start = 0
432            self.indicator_box.props.margin_end = 0
433            self.indicator_box.props.margin_top = 2
434            self.indicator_box.props.margin_bottom = 2
435        else:
436            self.indicator_box.set_orientation(Gtk.Orientation.HORIZONTAL)
437
438            self.indicator_box.props.margin_start = 2
439            self.indicator_box.props.margin_end = 2
440            self.indicator_box.props.margin_top = 0
441            self.indicator_box.props.margin_bottom = 0
442
443        self.indicator_box.queue_resize()
444
445    def sort_icons(self, status_widget=None):
446        icon_list = list(self.indicators.values())
447
448        # for i in icon_list:
449        #     print("before: ", i.proxy.props.icon_name, i.proxy.props.name.lower())
450
451        icon_list.sort(key=lambda icon: icon.proxy.props.name.replace("org.x.StatusIcon.", "").lower())
452        icon_list.sort(key=lambda icon: icon.proxy.props.icon_name.lower().endswith("symbolic"))
453
454        # for i in icon_list:
455        #     print("after: ", i.proxy.props.icon_name, i.proxy.props.name.lower())
456
457        icon_list.reverse()
458
459        for icon in icon_list:
460            self.indicator_box.reorder_child(icon, 0)
461
462def applet_factory(applet, iid, data):
463    MateXAppStatusApplet(applet, iid)
464    applet.show()
465    return True
466
467def quit_all(widget):
468    Gtk.main_quit()
469    sys.exit(0)
470
471MatePanelApplet.Applet.factory_main("MateXAppStatusAppletFactory", True,
472                                    MatePanelApplet.Applet.__gtype__,
473                                    applet_factory, None)
474