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