def on_event_clicked(self, _widget: Gtk.Widget, event: Gdk.Event) -> bool: if event.type not in (Gdk.EventType._2BUTTON_PRESS, Gdk.EventType.BUTTON_PRESS): return False path = self.get_path_at_pos(int(cast(Gdk.EventButton, event).x), int(cast(Gdk.EventButton, event).y)) if path is None: return False assert path[0] is not None row = self.get(path[0], "device", "connected") if not row: return False if self.Blueman is None: return False if self.menu is None: self.menu = ManagerDeviceMenu(self.Blueman) if event.type == Gdk.EventType._2BUTTON_PRESS and cast( Gdk.EventButton, event).button == 1: if self.menu.show_generic_connect_calc(row["device"]['UUIDs']): self.menu.generic_connect(None, device=row["device"], connect=not row["connected"]) if event.type == Gdk.EventType.BUTTON_PRESS and cast( Gdk.EventButton, event).button == 3: self.menu.popup_at_pointer(event) return False
def on_event_clicked(self, _widget: Gtk.Widget, event: Gdk.Event) -> bool: if event.type not in (Gdk.EventType._2BUTTON_PRESS, Gdk.EventType.BUTTON_PRESS): return False posdata = self.get_path_at_pos(int(cast(Gdk.EventButton, event).x), int(cast(Gdk.EventButton, event).y)) if posdata is None: return False else: path = posdata[0] assert path is not None childpath = self.filter.convert_path_to_child_path(path) assert childpath is not None row = self.get(childpath, "device", "connected") if not row: return False if self.Blueman is None: return False if self.menu is None: self.menu = ManagerDeviceMenu(self.Blueman) if event.type == Gdk.EventType._2BUTTON_PRESS and cast(Gdk.EventButton, event).button == 1: if self.menu.show_generic_connect_calc(row["device"]['UUIDs']): if row["connected"]: self.menu.disconnect_service(row["device"]) else: self.menu.connect_service(row["device"]) if event.type == Gdk.EventType.BUTTON_PRESS and cast(Gdk.EventButton, event).button == 3: self.menu.popup_at_pointer(event) return False
def on_event_clicked(self, widget, event): if event.type not in (Gdk.EventType._2BUTTON_PRESS, Gdk.EventType.BUTTON_PRESS): return path = self.get_path_at_pos(int(event.x), int(event.y)) if path is None: return row = self.get(path[0], "device", "connected") if not row: return if self.Blueman is None: return if self.menu is None: self.menu = ManagerDeviceMenu(self.Blueman) if event.type == Gdk.EventType._2BUTTON_PRESS and event.button == 1: if self.menu.show_generic_connect_calc(row["device"]['UUIDs']): self.menu.generic_connect(item=None, device=row["device"], connect=not row["connected"]) if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3: self.menu.popup_at_pointer(event)
def on_event_clicked(self, widget, event): if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3: path = self.get_path_at_pos(int(event.x), int(event.y)) if path is not None: row = self.get(path[0], "device") if row: if self.Blueman is not None: if self.menu is None: self.menu = ManagerDeviceMenu(self.Blueman) self.menu.popup(None, None, None, None, event.button, event.time)
def _on_popup_menu(self, _widget: Gtk.Widget) -> bool: if self.Blueman is None: return False if self.menu is None: self.menu = ManagerDeviceMenu(self.Blueman) window = self.get_window() assert window is not None selected = self.selected() assert selected is not None rect = self.get_cell_area(self.liststore.get_path(selected), self.get_column(1)) self.menu.popup_at_rect(window, rect, Gdk.Gravity.CENTER, Gdk.Gravity.NORTH) return True
def on_event_clicked(self, widget, event): if event.type == gtk.gdk.BUTTON_PRESS and event.button == 3: path = self.get_path_at_pos(int(event.x), int(event.y)) if path != None: row = self.get(path[0][0], "device") if row: device = row["device"] if self.Blueman != None: if self.menu == None: self.menu = ManagerDeviceMenu(self.Blueman) self.menu.popup(None, None, None, event.button, event.time)
def on_device_selected(self, _lst: ManagerDeviceList, device: Device, tree_iter: Gtk.TreeIter) -> None: if tree_iter and device: self.item_device.props.sensitive = True if self.device_menu is None: self.device_menu = ManagerDeviceMenu(self.blueman) self.item_device.set_submenu(self.device_menu) else: def idle() -> bool: assert self.device_menu is not None # https://github.com/python/mypy/issues/2608 self.device_menu.generate() return False GLib.idle_add(idle, priority=GLib.PRIORITY_LOW) else: self.item_device.props.sensitive = False
def on_request_menu_items(self, manager_menu: ManagerDeviceMenu, device: Device) -> List[DeviceMenuItem]: item = create_menuitem(_("_Info"), "dialog-information") item.props.tooltip_text = _("Show device information") _window = manager_menu.get_toplevel() assert isinstance(_window, Gtk.Window) window = _window # https://github.com/python/mypy/issues/2608 item.connect('activate', lambda x: show_info(device, window)) return [DeviceMenuItem(item, DeviceMenuItem.Group.ACTIONS, 400)]
def on_request_menu_items( self, manager_menu: ManagerDeviceMenu, device: Device) -> List[Tuple[Gtk.MenuItem, int]]: item = create_menuitem(_("Send _note"), "dialog-information") item.props.tooltip_text = _("Send a text note") item.connect('activate', lambda x: send_note(device, manager_menu.get_toplevel())) return [(item, 500)]
def on_request_menu_items( self, manager_menu: ManagerDeviceMenu, device: Device) -> List[Tuple[Gtk.MenuItem, int]]: item = create_menuitem(_("Send _note"), "dialog-information") item.props.tooltip_text = _("Send a text note") _window = manager_menu.get_toplevel() assert isinstance(_window, Gtk.Window) window = _window # https://github.com/python/mypy/issues/2608 item.connect('activate', lambda x: send_note(device, window)) return [(item, 500)]
def on_device_selected(self, lst, device, tree_iter): if tree_iter and device: self.item_device.props.sensitive = True if self.device_menu is None: self.device_menu = ManagerDeviceMenu(self.blueman) self.item_device.set_submenu(self.device_menu) else: GLib.idle_add(self.device_menu.generate, priority=GLib.PRIORITY_LOW) else: self.item_device.props.sensitive = False
def on_device_selected(self, List, device, iter): if iter and device: self.item_device.props.sensitive = True if self.device_menu == None: self.device_menu = ManagerDeviceMenu(self.blueman) self.item_device.set_submenu(self.device_menu) else: GObject.idle_add(self.device_menu.Generate, priority=GObject.PRIORITY_LOW) else: self.item_device.props.sensitive = False
def on_event_clicked(self, widget, event): if event.type==gtk.gdk.BUTTON_PRESS and event.button==3: path = self.get_path_at_pos(int(event.x), int(event.y)) if path != None: row = self.get(path[0][0], "device") if row: device = row["device"] if self.Blueman != None: if self.menu == None: self.menu = ManagerDeviceMenu(self.Blueman) self.menu.popup(None, None, None, event.button, event.time)
class ManagerDeviceList(DeviceList): def __init__(self, adapter=None, inst=None): cr = Gtk.CellRendererText() cr.props.ellipsize = Pango.EllipsizeMode.END tabledata = [ # device picture { "id": "device_surface", "type": str, "renderer": Gtk.CellRendererPixbuf(), "render_attrs": {}, "celldata_func": (self._set_device_cell_data, None) }, # device caption { "id": "caption", "type": str, "renderer": cr, "render_attrs": { "markup": 1 }, "view_props": { "expand": True } }, { "id": "rssi_pb", "type": GdkPixbuf.Pixbuf, "renderer": Gtk.CellRendererPixbuf(), "render_attrs": { "pixbuf": 2 }, "view_props": { "spacing": 0 } }, { "id": "lq_pb", "type": GdkPixbuf.Pixbuf, "renderer": Gtk.CellRendererPixbuf(), "render_attrs": { "pixbuf": 3 }, "view_props": { "spacing": 0 } }, { "id": "tpl_pb", "type": GdkPixbuf.Pixbuf, "renderer": Gtk.CellRendererPixbuf(), "render_attrs": { "pixbuf": 4 }, "view_props": { "spacing": 0 } }, { "id": "alias", "type": str }, # used for quick access instead of device.GetProperties { "id": "connected", "type": bool }, # used for quick access instead of device.GetProperties { "id": "paired", "type": bool }, # used for quick access instead of device.GetProperties { "id": "trusted", "type": bool }, # used for quick access instead of device.GetProperties { "id": "objpush", "type": bool }, # used to set Send File button { "id": "rssi", "type": float }, { "id": "lq", "type": float }, { "id": "tpl", "type": float }, { "id": "icon_info", "type": Gtk.IconInfo }, { "id": "cell_fader", "type": GObject.TYPE_PYOBJECT }, { "id": "row_fader", "type": GObject.TYPE_PYOBJECT }, { "id": "levels_visible", "type": bool }, { "id": "initial_anim", "type": bool }, ] super().__init__(adapter, tabledata) self.set_name("ManagerDeviceList") self.set_headers_visible(False) self.props.has_tooltip = True self.Blueman = inst self.Config = Config("org.blueman.general") self.Config.connect('changed', self._on_settings_changed) # Set the correct sorting self._on_settings_changed(self.Config, "sort-by") self._on_settings_changed(self.Config, "sort-type") self.connect("query-tooltip", self.tooltip_query) self.tooltip_row = None self.tooltip_col = None self.connect("button_press_event", self.on_event_clicked) self.connect("button_release_event", self.on_event_clicked) self.menu = None self.connect("drag_data_received", self.drag_recv) self.connect("drag-motion", self.drag_motion) Gtk.Widget.drag_dest_set(self, Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY | Gdk.DragAction.DEFAULT) Gtk.Widget.drag_dest_add_uri_targets(self) self.set_search_equal_func(self.search_func, None) def _on_settings_changed(self, settings, key): if key in ('sort-by', 'sort-order'): sort_by = settings['sort-by'] sort_order = settings['sort-order'] if sort_order == 'ascending': sort_type = Gtk.SortType.ASCENDING else: sort_type = Gtk.SortType.DESCENDING column_id = self.ids.setdefault(sort_by, None) if column_id: self.liststore.set_sort_column_id(column_id, sort_type) def on_icon_theme_changed(self, widget): for row in self.liststore: device = self.get(row.iter, "device")["device"] self.row_setup_event(row.iter, device) def do_device_found(self, device): tree_iter = self.find_device(device) if tree_iter: anim = TreeRowColorFade(self, self.props.model.get_path(tree_iter), Gdk.RGBA(0, 0, 1, 1)) anim.animate(start=0.8, end=1.0) def search_func(self, model, column, key, tree_iter): row = self.get(tree_iter, "caption") if key.lower() in row["caption"].lower(): return False logging.info("%s %s %s %s" % (model, column, key, tree_iter)) return True def drag_recv(self, widget, context, x, y, selection, target_type, time): uris = list(selection.get_uris()) context.finish(True, False, time) path = self.get_path_at_pos(x, y) if path: tree_iter = self.get_iter(path[0]) device = self.get(tree_iter, "device")["device"] command = "blueman-sendto --device=%s" % device['Address'] launch(command, uris, False, "blueman", _("File Sender")) context.finish(True, False, time) else: context.finish(False, False, time) return True def drag_motion(self, widget, drag_context, x, y, timestamp): result = self.get_path_at_pos(x, y) if result is not None: path = result[0] if not self.selection.path_is_selected(path): tree_iter = self.get_iter(path) has_obj_push = self._has_objpush( self.get(tree_iter, "device")["device"]) if has_obj_push: Gdk.drag_status(drag_context, Gdk.DragAction.COPY, timestamp) self.set_cursor(path) return True else: Gdk.drag_status(drag_context, Gdk.DragAction.DEFAULT, timestamp) return False else: Gdk.drag_status(drag_context, Gdk.DragAction.DEFAULT, timestamp) return False def on_event_clicked(self, widget, event): if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3: path = self.get_path_at_pos(int(event.x), int(event.y)) if path is not None: row = self.get(path[0], "device") if row: if self.Blueman is not None: if self.menu is None: self.menu = ManagerDeviceMenu(self.Blueman) self.menu.popup(None, None, None, None, event.button, event.time) def get_icon_info(self, icon_name, size=48, fallback=True): if icon_name is None and not fallback: return None elif icon_name is None and fallback: icon_name = "image-missing" icon_info = self.icon_theme.lookup_icon_for_scale( icon_name, size, self.get_scale_factor(), Gtk.IconLookupFlags.FORCE_SIZE) return icon_info def make_device_icon(self, icon_info, is_paired=False, is_trusted=False): window = self.get_window() scale = self.get_scale_factor() target = icon_info.load_surface(window) ctx = cairo.Context(target) if is_paired: icon_info = self.get_icon_info("dialog-password", 16, False) paired_surface = icon_info.load_surface(window) ctx.set_source_surface(paired_surface, 1 / scale, 1 / scale) ctx.paint_with_alpha(0.8) if is_trusted: icon_info = self.get_icon_info("blueman-trust", 16, False) trusted_surface = icon_info.load_surface(window) height = target.get_height() mini_height = trusted_surface.get_height() y = height / scale - mini_height / scale - 1 / scale ctx.set_source_surface(trusted_surface, 1 / scale, y) ctx.paint_with_alpha(0.8) return target def device_remove_event(self, device): tree_iter = self.find_device(device) row_fader = self.get(tree_iter, "row_fader")["row_fader"] row_fader.connect("animation-finished", self.__on_fader_finished, device, tree_iter) row_fader.thaw() self.emit("device-selected", None, None) row_fader.animate(start=row_fader.get_state(), end=0.0, duration=400) def __on_fader_finished(self, fader, device, tree_iter): fader.disconnect_by_func(self.__on_fader_finished) fader.freeze() super().device_remove_event(device) def device_add_event(self, device): self.add_device(device) def make_caption(self, name, klass, address): return "<span size='x-large'>%(0)s</span>\n<span size='small'>%(1)s</span>\n<i>%(2)s</i>" \ % {"0": html.escape(name), "1": klass.capitalize(), "2": address} def get_device_class(self, device): klass = get_minor_class(device['Class']) if klass != "uncategorized": return get_minor_class(device['Class'], True) else: return get_major_class(device['Class']) def row_setup_event(self, tree_iter, device): if not self.get(tree_iter, "initial_anim")["initial_anim"]: cell_fader = CellFade(self, self.props.model.get_path(tree_iter), [2, 3, 4]) row_fader = TreeRowFade(self, self.props.model.get_path(tree_iter)) has_objpush = self._has_objpush(device) self.set(tree_iter, row_fader=row_fader, cell_fader=cell_fader, levels_visible=False, objpush=has_objpush) cell_fader.freeze() def on_finished(fader): fader.disconnect_by_func(on_finished) fader.freeze() row_fader.connect("animation-finished", on_finished) row_fader.set_state(0.0) row_fader.animate(start=0.0, end=1.0, duration=500) self.set(tree_iter, initial_anim=True) klass = get_minor_class(device['Class']) # Bluetooth >= 4 devices use Appearance property appearance = device["Appearance"] if klass != "uncategorized" and klass != "unknown": # get translated version description = get_minor_class(device['Class'], True).capitalize() elif klass == "unknown" and appearance: description = gatt_appearance_to_name(appearance) else: description = get_major_class(device['Class']).capitalize() icon_info = self.get_icon_info(device["Icon"], 48, False) caption = self.make_caption(device['Alias'], description, device['Address']) self.set(tree_iter, caption=caption, icon_info=icon_info, alias=device['Alias']) try: self.row_update_event(tree_iter, "Trusted", device['Trusted']) except Exception as e: logging.exception(e) try: self.row_update_event(tree_iter, "Paired", device['Paired']) except Exception as e: logging.exception(e) try: self.row_update_event(tree_iter, "Connected", device["Connected"]) except Exception as e: logging.exception(e) def row_update_event(self, tree_iter, key, value): logging.info("%s %s" % (key, value)) if key == "Trusted": if value: self.set(tree_iter, trusted=True) else: self.set(tree_iter, trusted=False) elif key == "Paired": if value: self.set(tree_iter, paired=True) else: self.set(tree_iter, paired=False) elif key == "Alias": device = self.get(tree_iter, "device")["device"] c = self.make_caption(value, self.get_device_class(device), device['Address']) self.set(tree_iter, caption=c, alias=value) elif key == "UUIDs": device = self.get(tree_iter, "device")["device"] has_objpush = self._has_objpush(device) self.set(tree_iter, objpush=has_objpush) elif key == "Connected": self.set(tree_iter, connected=value) def level_setup_event(self, row_ref, device, cinfo): if not row_ref.valid(): return tree_iter = self.get_iter(row_ref.get_path()) row = self.get(tree_iter, "levels_visible", "cell_fader", "rssi", "lq", "tpl") if cinfo is not None: # cinfo init may fail for bluetooth devices version 4 and up # FIXME Workaround is horrible and we should show something better if cinfo.failed: rssi_perc = tpl_perc = lq_perc = 100 else: try: rssi = float(cinfo.get_rssi()) except ConnInfoReadError: rssi = 0 try: lq = float(cinfo.get_lq()) except ConnInfoReadError: lq = 0 try: tpl = float(cinfo.get_tpl()) except ConnInfoReadError: tpl = 0 rssi_perc = 50 + (rssi / 127 / 2 * 100) tpl_perc = 50 + (tpl / 127 / 2 * 100) lq_perc = lq / 255 * 100 if lq_perc < 10: lq_perc = 10 if rssi_perc < 10: rssi_perc = 10 if tpl_perc < 10: tpl_perc = 10 if not row["levels_visible"]: logging.info("animating up") self.set(tree_iter, levels_visible=True) fader = row["cell_fader"] fader.thaw() fader.set_state(0.0) fader.animate(start=0.0, end=1.0, duration=400) def on_finished(fader): fader.freeze() fader.disconnect_by_func(on_finished) fader.connect("animation-finished", on_finished) to_store = {} if round(row["rssi"], -1) != round(rssi_perc, -1): icon_name = "blueman-rssi-%d.png" % round(rssi_perc, -1) icon = GdkPixbuf.Pixbuf.new_from_file( os.path.join(PIXMAP_PATH, icon_name)) to_store.update({"rssi": rssi_perc, "rssi_pb": icon}) if round(row["lq"], -1) != round(lq_perc, -1): icon_name = "blueman-lq-%d.png" % round(lq_perc, -1) icon = GdkPixbuf.Pixbuf.new_from_file( os.path.join(PIXMAP_PATH, icon_name)) to_store.update({"lq": lq_perc, "lq_pb": icon}) if round(row["tpl"], -1) != round(tpl_perc, -1): icon_name = "blueman-tpl-%d.png" % round(tpl_perc, -1) icon = GdkPixbuf.Pixbuf.new_from_file( os.path.join(PIXMAP_PATH, icon_name)) to_store.update({"tpl": tpl_perc, "tpl_pb": icon}) if to_store: self.set(tree_iter, **to_store) else: if row["levels_visible"]: logging.info("animating down") self.set(tree_iter, levels_visible=False, rssi=-1, lq=-1, tpl=-1) fader = row["cell_fader"] fader.thaw() fader.set_state(1.0) fader.animate(start=fader.get_state(), end=0.0, duration=400) def on_finished(fader): fader.disconnect_by_func(on_finished) fader.freeze() if row_ref.valid(): self.set(tree_iter, rssi_pb=None, lq_pb=None, tpl_pb=None) fader.connect("animation-finished", on_finished) def tooltip_query(self, tw, x, y, kb, tooltip): path = self.get_path_at_pos(x, y) if path is not None: if path[0] != self.tooltip_row or path[1] != self.tooltip_col: self.tooltip_row = path[0] self.tooltip_col = path[1] return False if path[1] == self.columns["device_surface"]: tree_iter = self.get_iter(path[0]) row = self.get(tree_iter, "trusted", "paired") trusted = row["trusted"] paired = row["paired"] if trusted and paired: tooltip.set_markup(_("<b>Trusted and Paired</b>")) elif paired: tooltip.set_markup(_("<b>Paired</b>")) elif trusted: tooltip.set_markup(_("<b>Trusted</b>")) else: return False self.tooltip_row = path[0] self.tooltip_col = path[1] return True if path[1] == self.columns["tpl_pb"] \ or path[1] == self.columns["lq_pb"] \ or path[1] == self.columns["rssi_pb"]: tree_iter = self.get_iter(path[0]) dt = self.get(tree_iter, "connected")["connected"] if dt: rssi = self.get(tree_iter, "rssi")["rssi"] lq = self.get(tree_iter, "lq")["lq"] tpl = self.get(tree_iter, "tpl")["tpl"] if rssi < 30: rssi_state = _("Poor") elif rssi < 40: rssi_state = _("Sub-optimal") elif rssi < 60: rssi_state = _("Optimal") elif rssi < 70: rssi_state = _("Much") else: rssi_state = _("Too much") if tpl < 30: tpl_state = _("Low") elif tpl < 40: tpl_state = _("Sub-optimal") elif tpl < 60: tpl_state = _("Optimal") elif tpl < 70: tpl_state = _("High") else: tpl_state = _("Very High") tooltip_template = None if path[1] == self.columns["tpl_pb"]: tooltip_template = \ "<b>Connected</b>\nReceived Signal Strength: %(rssi)u%% <i>(%(rssi_state)s)</i>\n" \ "Link Quality: %(lq)u%%\n<b>Transmit Power Level: %(tpl)u%%</b> <i>(%(tpl_state)s)</i>" elif path[1] == self.columns["lq_pb"]: tooltip_template = \ "<b>Connected</b>\nReceived Signal Strength: %(rssi)u%% <i>(%(rssi_state)s)</i>\n" \ "<b>Link Quality: %(lq)u%%</b>\nTransmit Power Level: %(tpl)u%% <i>(%(tpl_state)s)</i>" elif path[1] == self.columns["rssi_pb"]: tooltip_template = \ "<b>Connected</b>\n<b>Received Signal Strength: %(rssi)u%%</b> <i>(%(rssi_state)s)</i>\n" \ "Link Quality: %(lq)u%%\nTransmit Power Level: %(tpl)u%% <i>(%(tpl_state)s)</i>" state_dict = { "rssi_state": rssi_state, "rssi": rssi, "lq": lq, "tpl": tpl, "tpl_state": tpl_state } tooltip.set_markup(tooltip_template % state_dict) self.tooltip_row = path[0] self.tooltip_col = path[1] return True return False def _has_objpush(self, device): if device is None: return False for uuid in device["UUIDs"]: if ServiceUUID(uuid).short_uuid == OBEX_OBJPUSH_SVCLASS_ID: return True return False def _set_device_cell_data(self, col, cell, model, tree_iter, data): row = self.get(tree_iter, "icon_info", "trusted", "paired") surface = self.make_device_icon(row["icon_info"], row["paired"], row["trusted"]) cell.set_property("surface", surface)
class ManagerDeviceList(DeviceList): def __init__(self, adapter: Optional[str] = None, inst: Optional["Blueman"] = None) -> None: cr = Gtk.CellRendererText() cr.props.ellipsize = Pango.EllipsizeMode.END tabledata: List[ListDataDict] = [ # device picture {"id": "device_surface", "type": str, "renderer": Gtk.CellRendererPixbuf(), "render_attrs": {}, "celldata_func": (self._set_cell_data, None)}, # device caption {"id": "caption", "type": str, "renderer": cr, "render_attrs": {"markup": 1}, "view_props": {"expand": True}}, {"id": "battery_pb", "type": GdkPixbuf.Pixbuf, "renderer": Gtk.CellRendererPixbuf(), "render_attrs": {}, "view_props": {"spacing": 0}, "celldata_func": (self._set_cell_data, "battery")}, {"id": "rssi_pb", "type": GdkPixbuf.Pixbuf, "renderer": Gtk.CellRendererPixbuf(), "render_attrs": {}, "view_props": {"spacing": 0}, "celldata_func": (self._set_cell_data, "rssi")}, {"id": "lq_pb", "type": GdkPixbuf.Pixbuf, "renderer": Gtk.CellRendererPixbuf(), "render_attrs": {}, "view_props": {"spacing": 0}, "celldata_func": (self._set_cell_data, "lq")}, {"id": "tpl_pb", "type": GdkPixbuf.Pixbuf, "renderer": Gtk.CellRendererPixbuf(), "render_attrs": {}, "view_props": {"spacing": 0}, "celldata_func": (self._set_cell_data, "tpl")}, {"id": "alias", "type": str}, # used for quick access instead of device.GetProperties {"id": "connected", "type": bool}, # used for quick access instead of device.GetProperties {"id": "paired", "type": bool}, # used for quick access instead of device.GetProperties {"id": "trusted", "type": bool}, # used for quick access instead of device.GetProperties {"id": "objpush", "type": bool}, # used to set Send File button {"id": "battery", "type": float}, {"id": "rssi", "type": float}, {"id": "lq", "type": float}, {"id": "tpl", "type": float}, {"id": "icon_info", "type": Gtk.IconInfo}, {"id": "cell_fader", "type": CellFade}, {"id": "row_fader", "type": TreeRowFade}, {"id": "initial_anim", "type": bool}, {"id": "blocked", "type": bool} ] super().__init__(adapter, tabledata) self.set_name("ManagerDeviceList") self.set_headers_visible(False) self.props.has_tooltip = True self.Blueman = inst self._monitored_devices: Set[str] = set() self.manager.connect_signal("battery-created", self.on_battery_created) self.manager.connect_signal("battery-removed", self.on_battery_removed) self._batteries: Dict[str, Battery] = {} self.Config = Config("org.blueman.general") self.Config.connect('changed', self._on_settings_changed) # Set the correct sorting self._on_settings_changed(self.Config, "sort-by") self._on_settings_changed(self.Config, "sort-type") self.connect("query-tooltip", self.tooltip_query) self.tooltip_row: Optional[Gtk.TreePath] = None self.tooltip_col: Optional[Gtk.TreeViewColumn] = None self.connect("popup-menu", self._on_popup_menu) self.connect("button_press_event", self.on_event_clicked) self.connect("button_release_event", self.on_event_clicked) self.menu: Optional[ManagerDeviceMenu] = None self.connect("drag_data_received", self.drag_recv) self.connect("drag-motion", self.drag_motion) Gtk.Widget.drag_dest_set(self, Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY | Gdk.DragAction.DEFAULT) Gtk.Widget.drag_dest_add_uri_targets(self) self.set_search_equal_func(self.search_func) self.filter.set_visible_func(self.filter_func) def _on_settings_changed(self, settings: Config, key: str) -> None: if key in ('sort-by', 'sort-order'): sort_by = settings['sort-by'] sort_order = settings['sort-order'] if sort_order == 'ascending': sort_type = Gtk.SortType.ASCENDING else: sort_type = Gtk.SortType.DESCENDING column_id = self.ids.get(sort_by) if column_id: self.liststore.set_sort_column_id(column_id, sort_type) def on_icon_theme_changed(self, _icon_them: Gtk.IconTheme) -> None: for row in self.liststore: device = self.get(row.iter, "device")["device"] self.row_setup_event(row.iter, device) def on_battery_created(self, _manager: Manager, obj_path: str) -> None: if obj_path not in self._batteries: battery_proxy = Battery(obj_path=obj_path) self._batteries[obj_path] = battery_proxy logging.debug(f"{obj_path} {battery_proxy['Percentage']}") def on_battery_removed(self, _manager: Manager, obj_path: str) -> None: if obj_path in self._batteries: battery = self._batteries.pop(obj_path) battery.destroy() def search_func(self, model: Gtk.TreeModel, column: int, key: str, tree_iter: Gtk.TreeIter) -> bool: row = self.get(tree_iter, "caption") if key.lower() in row["caption"].lower(): return False logging.info(f"{model} {column} {key} {tree_iter}") return True def filter_func(self, _model: Gtk.TreeModel, tree_iter: Gtk.TreeIter, _data: Any) -> bool: no_name = self.get(tree_iter, "no_name")["no_name"] if no_name and self.Config["hide-unnamed"]: logging.debug("Hiding unnamed device") return False else: return True def drag_recv(self, _widget: Gtk.Widget, context: Gdk.DragContext, x: int, y: int, selection: Gtk.SelectionData, _info: int, time: int) -> None: uris = list(selection.get_uris()) context.finish(True, False, time) path = self.get_path_at_pos(x, y) if path: tree_iter = self.get_iter(path[0]) assert tree_iter is not None device = self.get(tree_iter, "device")["device"] command = f"blueman-sendto --device={device['Address']}" launch(command, paths=uris, name=_("File Sender")) context.finish(True, False, time) else: context.finish(False, False, time) def drag_motion(self, _widget: Gtk.Widget, drag_context: Gdk.DragContext, x: int, y: int, timestamp: int) -> bool: result = self.get_path_at_pos(x, y) if result is not None: path = result[0] assert path is not None path = self.filter.convert_path_to_child_path(path) if path is None: return False if not self.selection.path_is_selected(path): tree_iter = self.get_iter(path) assert tree_iter is not None has_obj_push = self._has_objpush(self.get(tree_iter, "device")["device"]) if has_obj_push: Gdk.drag_status(drag_context, Gdk.DragAction.COPY, timestamp) self.set_cursor(path) return True else: Gdk.drag_status(drag_context, Gdk.DragAction.DEFAULT, timestamp) return False return False else: Gdk.drag_status(drag_context, Gdk.DragAction.DEFAULT, timestamp) return False def _on_popup_menu(self, _widget: Gtk.Widget) -> bool: if self.Blueman is None: return False if self.menu is None: self.menu = ManagerDeviceMenu(self.Blueman) window = self.get_window() assert window is not None selected = self.selected() assert selected is not None rect = self.get_cell_area(self.liststore.get_path(selected), self.get_column(1)) self.menu.popup_at_rect(window, rect, Gdk.Gravity.CENTER, Gdk.Gravity.NORTH) return True def on_event_clicked(self, _widget: Gtk.Widget, event: Gdk.Event) -> bool: if event.type not in (Gdk.EventType._2BUTTON_PRESS, Gdk.EventType.BUTTON_PRESS): return False posdata = self.get_path_at_pos(int(cast(Gdk.EventButton, event).x), int(cast(Gdk.EventButton, event).y)) if posdata is None: return False else: path = posdata[0] assert path is not None childpath = self.filter.convert_path_to_child_path(path) assert childpath is not None row = self.get(childpath, "device", "connected") if not row: return False if self.Blueman is None: return False if self.menu is None: self.menu = ManagerDeviceMenu(self.Blueman) if event.type == Gdk.EventType._2BUTTON_PRESS and cast(Gdk.EventButton, event).button == 1: if self.menu.show_generic_connect_calc(row["device"]['UUIDs']): if row["connected"]: self.menu.disconnect_service(row["device"]) else: self.menu.connect_service(row["device"]) if event.type == Gdk.EventType.BUTTON_PRESS and cast(Gdk.EventButton, event).button == 3: self.menu.popup_at_pointer(event) return False def get_icon_info(self, icon_name: str, size: int = 48, fallback: bool = True) -> Optional[Gtk.IconInfo]: if icon_name is None and not fallback: return None elif icon_name is None and fallback: icon_name = "image-missing" icon_info = self.icon_theme.lookup_icon_for_scale(icon_name, size, self.get_scale_factor(), Gtk.IconLookupFlags.FORCE_SIZE) return icon_info def make_device_icon(self, icon_info: Gtk.IconInfo, is_paired: bool = False, is_trusted: bool = False, is_blocked: bool = False) -> cairo.Surface: window = self.get_window() scale = self.get_scale_factor() target = icon_info.load_surface(window) ctx = cairo.Context(target) if is_paired: _icon_info = self.get_icon_info("blueman-paired-emblem", 16, False) assert _icon_info is not None paired_surface = _icon_info.load_surface(window) ctx.set_source_surface(paired_surface, 1 / scale, 1 / scale) ctx.paint_with_alpha(0.8) if is_trusted: _icon_info = self.get_icon_info("blueman-trusted-emblem", 16, False) assert _icon_info is not None trusted_surface = _icon_info.load_surface(window) assert isinstance(target, cairo.ImageSurface) assert isinstance(trusted_surface, cairo.ImageSurface) height = target.get_height() mini_height = trusted_surface.get_height() y = height / scale - mini_height / scale - 1 / scale ctx.set_source_surface(trusted_surface, 1 / scale, y) ctx.paint_with_alpha(0.8) if is_blocked: _icon_info = self.get_icon_info("blueman-blocked-emblem", 16, False) assert _icon_info is not None blocked_surface = _icon_info.load_surface(window) assert isinstance(target, cairo.ImageSurface) assert isinstance(blocked_surface, cairo.ImageSurface) width = target.get_width() mini_width = blocked_surface.get_width() ctx.set_source_surface(blocked_surface, (width - mini_width - 1) / scale, 1 / scale) ctx.paint_with_alpha(0.8) return target def device_remove_event(self, device: Device) -> None: tree_iter = self.find_device(device) assert tree_iter is not None iter_set, _child_tree_iter = self.filter.convert_child_iter_to_iter(tree_iter) if iter_set: row_fader = self.get(tree_iter, "row_fader")["row_fader"] self._prepare_fader(row_fader, lambda: self.__fader_finished(device)) row_fader.animate(start=row_fader.get_state(), end=0.0, duration=400) def __fader_finished(self, device: Device) -> None: super().device_remove_event(device) self.emit("device-selected", None, None) def device_add_event(self, device: Device) -> None: self.add_device(device) @staticmethod def make_caption(name: str, klass: str, address: str) -> str: return "<span size='x-large'>%(0)s</span>\n<span size='small'>%(1)s</span>\n<i>%(2)s</i>" \ % {"0": html.escape(name), "1": klass, "2": address} @staticmethod def get_device_class(device: Device) -> str: klass = get_minor_class(device['Class']) if klass != _("Uncategorized"): return klass else: return get_major_class(device['Class']) def row_setup_event(self, tree_iter: Gtk.TreeIter, device: Device) -> None: if not self.get(tree_iter, "initial_anim")["initial_anim"]: assert self.liststore is not None child_path = self.liststore.get_path(tree_iter) result = self.filter.convert_child_path_to_path(child_path) if child_path is not None: cell_fader = CellFade(self, child_path, [2, 3, 4, 5]) row_fader = TreeRowFade(self, child_path) self.set(tree_iter, row_fader=row_fader, cell_fader=cell_fader) cell_fader.freeze() if result is not None: self._prepare_fader(row_fader).animate(start=0.0, end=1.0, duration=500) self.set(tree_iter, initial_anim=True) else: self.set(tree_iter, initial_anim=False) has_objpush = self._has_objpush(device) klass = get_minor_class(device['Class']) # Bluetooth >= 4 devices use Appearance property appearance = device["Appearance"] if klass != _("Uncategorized") and klass != _("Unknown"): description = klass elif klass == _("Unknown") and appearance: description = gatt_appearance_to_name(appearance) else: description = get_major_class(device['Class']) icon_info = self.get_icon_info(device["Icon"], 48, False) caption = self.make_caption(device['Alias'], description, device['Address']) self.set(tree_iter, caption=caption, icon_info=icon_info, alias=device['Alias'], objpush=has_objpush) try: self.row_update_event(tree_iter, "Trusted", device['Trusted']) except Exception as e: logging.exception(e) try: self.row_update_event(tree_iter, "Paired", device['Paired']) except Exception as e: logging.exception(e) try: self.row_update_event(tree_iter, "Connected", device["Connected"]) except Exception as e: logging.exception(e) try: self.row_update_event(tree_iter, "Blocked", device["Blocked"]) except Exception as e: logging.exception(e) if device["Connected"]: self._monitor_power_levels(tree_iter, device) def _monitor_power_levels(self, tree_iter: Gtk.TreeIter, device: Device) -> None: if device["Address"] in self._monitored_devices: return assert self.Adapter is not None cinfo = conn_info(device["Address"], os.path.basename(self.Adapter.get_object_path())) try: cinfo.init() except ConnInfoReadError: logging.warning("Failed to get power levels, probably a LE device.") model = self.liststore assert isinstance(model, Gtk.TreeModel) r = Gtk.TreeRowReference.new(model, model.get_path(tree_iter)) self._update_power_levels(tree_iter, device, cinfo) GLib.timeout_add(1000, self._check_power_levels, r, cinfo, device["Address"]) self._monitored_devices.add(device["Address"]) def _check_power_levels(self, row_ref: Gtk.TreeRowReference, cinfo: conn_info, address: str) -> bool: if not row_ref.valid(): logging.warning("stopping monitor (row does not exist)") cinfo.deinit() self._monitored_devices.remove(address) return False tree_iter = self.get_iter(row_ref.get_path()) assert tree_iter is not None device = self.get(tree_iter, "device")["device"] if device["Connected"]: self._update_power_levels(tree_iter, device, cinfo) return True else: cinfo.deinit() self._disable_power_levels(tree_iter) self._monitored_devices.remove(address) return False def row_update_event(self, tree_iter: Gtk.TreeIter, key: str, value: Any) -> None: logging.info(f"{key} {value}") if key == "Trusted": if value: self.set(tree_iter, trusted=True) else: self.set(tree_iter, trusted=False) elif key == "Paired": if value: self.set(tree_iter, paired=True) else: self.set(tree_iter, paired=False) elif key == "Alias": device = self.get(tree_iter, "device")["device"] c = self.make_caption(value, self.get_device_class(device), device['Address']) self.set(tree_iter, caption=c, alias=value) elif key == "UUIDs": device = self.get(tree_iter, "device")["device"] has_objpush = self._has_objpush(device) self.set(tree_iter, objpush=has_objpush) elif key == "Connected": self.set(tree_iter, connected=value) if value: self._monitor_power_levels(tree_iter, self.get(tree_iter, "device")["device"]) else: self._disable_power_levels(tree_iter) elif key == "Name": self.set(tree_iter, no_name=False) self.filter.refilter() elif key == "Blocked": self.set(tree_iter, blocked=value) def _update_power_levels(self, tree_iter: Gtk.TreeIter, device: Device, cinfo: conn_info) -> None: row = self.get(tree_iter, "cell_fader", "battery", "rssi", "lq", "tpl") bars = {} obj_path = device.get_object_path() if obj_path in self._batteries: bars["battery"] = self._batteries[obj_path]["Percentage"] # cinfo init may fail for bluetooth devices version 4 and up # FIXME Workaround is horrible and we should show something better if cinfo.failed: bars.update({"rssi": 100.0, "tpl": 100.0, "lq": 100.0}) else: try: bars["rssi"] = max(50 + float(cinfo.get_rssi()) / 127 * 50, 10) except ConnInfoReadError: bars["rssi"] = 50 try: bars["lq"] = max(float(cinfo.get_lq()) / 255 * 100, 10) except ConnInfoReadError: bars["lq"] = 10 try: bars["tpl"] = max(50 + float(cinfo.get_tpl()) / 127 * 50, 10) except ConnInfoReadError: bars["tpl"] = 50 if row["battery"] == row["rssi"] == row["tpl"] == row["lq"] == 0: self._prepare_fader(row["cell_fader"]).animate(start=0.0, end=1.0, duration=400) w = 14 * self.get_scale_factor() h = 48 * self.get_scale_factor() for (name, perc) in bars.items(): if round(row[name], -1) != round(perc, -1): icon_name = f"blueman-{name}-{int(round(perc, -1))}.png" icon = GdkPixbuf.Pixbuf.new_from_file_at_scale(os.path.join(PIXMAP_PATH, icon_name), w, h, True) self.set(tree_iter, **{name: perc, f"{name}_pb": icon}) def _disable_power_levels(self, tree_iter: Gtk.TreeIter) -> None: row = self.get(tree_iter, "cell_fader", "battery", "rssi", "lq", "tpl") if row["battery"] == row["rssi"] == row["tpl"] == row["lq"] == 0: return self.set(tree_iter, battery=0, rssi=0, lq=0, tpl=0) self._prepare_fader(row["cell_fader"], lambda: self.set(tree_iter, battery_pb=None, rssi_pb=None, lq_pb=None, tpl_pb=None)).animate(start=1.0, end=0.0, duration=400) def _prepare_fader(self, fader: AnimBase, callback: Optional[Callable[[], None]] = None) -> AnimBase: def on_finished(finished_fader: AnimBase) -> None: finished_fader.disconnect(handler) finished_fader.freeze() if callback: callback() fader.thaw() handler = fader.connect("animation-finished", on_finished) return fader def tooltip_query(self, _tw: Gtk.Widget, x: int, y: int, _kb: bool, tooltip: Gtk.Tooltip) -> bool: path = self.get_path_at_pos(x, y) if path is None: return False if path[0] != self.tooltip_row or path[1] != self.tooltip_col: self.tooltip_row = path[0] self.tooltip_col = path[1] return False if path[1] == self.columns["device_surface"]: tree_iter = self.get_iter(path[0]) assert tree_iter is not None row = self.get(tree_iter, "trusted", "paired", "blocked") trusted = row["trusted"] paired = row["paired"] blocked = row["blocked"] str_list = [] if trusted: str_list.append(_("Trusted")) if paired: str_list.append(_("Paired")) if blocked: str_list.append(_("Blocked")) text = ", ".join(str_list) if text: tooltip.set_markup(f"<b>{text}</b>") else: return False self.tooltip_row = path[0] self.tooltip_col = path[1] return True elif path[1] == self.columns["battery_pb"] \ or path[1] == self.columns["tpl_pb"] \ or path[1] == self.columns["lq_pb"] \ or path[1] == self.columns["rssi_pb"]: tree_iter = self.get_iter(path[0]) assert tree_iter is not None dt = self.get(tree_iter, "connected")["connected"] if not dt: return False lines = [_("<b>Connected</b>")] battery = self.get(tree_iter, "battery")["battery"] rssi = self.get(tree_iter, "rssi")["rssi"] lq = self.get(tree_iter, "lq")["lq"] tpl = self.get(tree_iter, "tpl")["tpl"] if battery != 0: if path[1] == self.columns["battery_pb"]: lines.append(f"<b>Battery: {int(battery)}%</b>") else: lines.append(f"Battery: {int(battery)}%") if rssi != 0: if rssi < 30: rssi_state = _("Poor") elif rssi < 40: rssi_state = _("Sub-optimal") elif rssi < 60: rssi_state = _("Optimal") elif rssi < 70: rssi_state = _("Much") else: rssi_state = _("Too much") if path[1] == self.columns["rssi_pb"]: lines.append(_("<b>Received Signal Strength: %(rssi)u%%</b> <i>(%(rssi_state)s)</i>") % {"rssi": rssi, "rssi_state": rssi_state}) else: lines.append(_("Received Signal Strength: %(rssi)u%% <i>(%(rssi_state)s)</i>") % {"rssi": rssi, "rssi_state": rssi_state}) if lq != 0: if path[1] == self.columns["lq_pb"]: lines.append(_("<b>Link Quality: %(lq)u%%</b>") % {"lq": lq}) else: lines.append(_("Link Quality: %(lq)u%%") % {"lq": lq}) if tpl != 0: if tpl < 30: tpl_state = _("Low") elif tpl < 40: tpl_state = _("Sub-optimal") elif tpl < 60: tpl_state = _("Optimal") elif tpl < 70: tpl_state = _("High") else: tpl_state = _("Very High") if path[1] == self.columns["tpl_pb"]: lines.append(_("<b>Transmit Power Level: %(tpl)u%%</b> <i>(%(tpl_state)s)</i>") % {"tpl": tpl, "tpl_state": tpl_state}) else: lines.append(_("Transmit Power Level: %(tpl)u%% <i>(%(tpl_state)s)</i>") % {"tpl": tpl, "tpl_state": tpl_state}) tooltip.set_markup("\n".join(lines)) self.tooltip_row = path[0] self.tooltip_col = path[1] return True return False def _has_objpush(self, device: Device) -> bool: if device is None: return False for uuid in device["UUIDs"]: if ServiceUUID(uuid).short_uuid == OBEX_OBJPUSH_SVCLASS_ID: return True return False def _set_cell_data(self, _col: Gtk.TreeViewColumn, cell: Gtk.CellRenderer, model: Gtk.TreeModelFilter, tree_iter: Gtk.TreeIter, data: Optional[str]) -> None: tree_iter = model.convert_iter_to_child_iter(tree_iter) if data is None: row = self.get(tree_iter, "icon_info", "trusted", "paired", "blocked") surface = self.make_device_icon(row["icon_info"], row["paired"], row["trusted"], row["blocked"]) cell.set_property("surface", surface) else: window = self.get_window() scale = self.get_scale_factor() pb = self.get(tree_iter, data + "_pb")[data + "_pb"] if pb: surface = Gdk.cairo_surface_create_from_pixbuf(pb, scale, window) cell.set_property("surface", surface) else: cell.set_property("surface", None)
class ManagerMenu: def __init__(self, blueman: "Blueman"): self.blueman = blueman self.Config = Config("org.blueman.general") self.adapter_items: Dict[str, Tuple[Gtk.RadioMenuItem, Adapter]] = {} self._adapters_group: Sequence[Gtk.RadioMenuItem] = [] self._insert_adapter_item_pos = 2 self.Search = None self.item_adapter = self.blueman.builder.get_widget("item_adapter", Gtk.MenuItem) self.item_device = self.blueman.builder.get_widget("item_device", Gtk.MenuItem) self.item_view = self.blueman.builder.get_widget("item_view", Gtk.MenuItem) self.item_help = self.blueman.builder.get_widget("item_help", Gtk.MenuItem) help_menu = Gtk.Menu() self.item_help.set_submenu(help_menu) help_menu.show() report_item = create_menuitem(_("_Report a Problem"), "dialog-warning") report_item.show() help_menu.append(report_item) report_item.connect("activate", lambda x: launch(f"xdg-open {WEBSITE}/issues")) sep = Gtk.SeparatorMenuItem() sep.show() help_menu.append(sep) help_item = create_menuitem(_("_Help"), "help-about") help_item.show() help_menu.append(help_item) assert self.blueman.window is not None widget = self.blueman.window.get_toplevel() assert isinstance(widget, Gtk.Window) window = widget help_item.connect("activate", lambda x: show_about_dialog('Blueman ' + _('Device Manager'), parent=window)) view_menu = Gtk.Menu() self.item_view.set_submenu(view_menu) view_menu.show() item_toolbar = Gtk.CheckMenuItem.new_with_mnemonic(_("Show _Toolbar")) item_toolbar.show() view_menu.append(item_toolbar) self.blueman.Config.bind_to_widget("show-toolbar", item_toolbar, "active") item_statusbar = Gtk.CheckMenuItem.new_with_mnemonic(_("Show _Statusbar")) item_statusbar.show() view_menu.append(item_statusbar) self.blueman.Config.bind_to_widget("show-statusbar", item_statusbar, "active") item_unnamed = Gtk.CheckMenuItem.new_with_mnemonic(_("Hide _unnamed devices")) item_unnamed.show() view_menu.append(item_unnamed) self.blueman.Config.bind_to_widget("hide-unnamed", item_unnamed, "active") item_services: Gtk.MenuItem = Gtk.SeparatorMenuItem() view_menu.append(item_services) item_services.show() sorting_group: Sequence[Gtk.RadioMenuItem] = [] item_sort = Gtk.MenuItem.new_with_mnemonic(_("S_ort By")) view_menu.append(item_sort) item_sort.show() sorting_menu = Gtk.Menu() item_sort.set_submenu(sorting_menu) self._sort_alias_item = Gtk.RadioMenuItem.new_with_mnemonic(sorting_group, _("_Name")) self._sort_alias_item.show() sorting_group = self._sort_alias_item.get_group() sorting_menu.append(self._sort_alias_item) self._sort_timestamp_item = Gtk.RadioMenuItem.new_with_mnemonic(sorting_group, _("_Added")) self._sort_timestamp_item.show() sorting_menu.append(self._sort_timestamp_item) sort_config = self.Config['sort-by'] if sort_config == "alias": self._sort_alias_item.props.active = True else: self._sort_timestamp_item.props.active = True sort_sep = Gtk.SeparatorMenuItem() sort_sep.show() sorting_menu.append(sort_sep) self._sort_type_item = Gtk.CheckMenuItem.new_with_mnemonic(_("_Descending")) self._sort_type_item.show() sorting_menu.append(self._sort_type_item) if self.Config['sort-order'] == "ascending": self._sort_type_item.props.active = False else: self._sort_type_item.props.active = True sep = Gtk.SeparatorMenuItem() sep.show() view_menu.append(sep) item_plugins = create_menuitem(_("_Plugins"), 'blueman-plugin') item_plugins.show() view_menu.append(item_plugins) item_plugins.connect('activate', self._on_plugin_dialog_activate) item_services = create_menuitem(_("_Local Services") + "…", "preferences-desktop") item_services.connect('activate', lambda *args: launch("blueman-services", name=_("Service Preferences"))) view_menu.append(item_services) item_services.show() adapter_menu = Gtk.Menu() self.item_adapter.set_submenu(adapter_menu) self.item_adapter.props.sensitive = False search_item = create_menuitem(_("_Search"), "edit-find") search_item.connect("activate", lambda x: self.blueman.inquiry()) search_item.show() adapter_menu.prepend(search_item) self.Search = search_item sep = Gtk.SeparatorMenuItem() sep.show() adapter_menu.append(sep) sep = Gtk.SeparatorMenuItem() sep.show() adapter_menu.append(sep) adapter_settings = create_menuitem(_("_Preferences"), "preferences-system") adapter_settings.connect("activate", lambda x: self.blueman.adapter_properties()) adapter_settings.show() adapter_menu.append(adapter_settings) sep = Gtk.SeparatorMenuItem() sep.show() adapter_menu.append(sep) exit_item = create_menuitem(_("_Exit"), "application-exit") exit_item.connect("activate", lambda x: self.blueman.quit()) exit_item.show() adapter_menu.append(exit_item) self.item_adapter.show() self.item_view.show() self.item_help.show() self.item_device.show() self.item_device.props.sensitive = False self._manager = Manager() self._manager.connect_signal("adapter-added", self.on_adapter_added) self._manager.connect_signal("adapter-removed", self.on_adapter_removed) blueman.List.connect("device-selected", self.on_device_selected) for adapter in self._manager.get_adapters(): self.on_adapter_added(None, adapter.get_object_path()) self.device_menu: Optional[ManagerDeviceMenu] = None self.Config.connect("changed", self._on_settings_changed) self._sort_alias_item.connect("activate", self._on_sorting_changed, "alias") self._sort_timestamp_item.connect("activate", self._on_sorting_changed, "timestamp") self._sort_type_item.connect("activate", self._on_sorting_changed, "sort-type") def _on_sorting_changed(self, btn: Gtk.CheckMenuItem, sort_opt: str) -> None: if sort_opt == 'alias' and btn.props.active: self.Config['sort-by'] = "alias" elif sort_opt == "timestamp" and btn.props.active: self.Config['sort-by'] = "timestamp" elif sort_opt == 'sort-type': # FIXME bind widget to gsetting if btn.props.active: self.Config["sort-order"] = "descending" else: self.Config["sort-order"] = "ascending" def _on_settings_changed(self, settings: Config, key: str) -> None: value = settings[key] if key == 'sort-by': if value == "alias": if not self._sort_alias_item.props.active: self._sort_alias_item.props.active = True elif value == "timestamp": if not self._sort_timestamp_item.props.active: self._sort_timestamp_item.props.active = True elif key == "sort-type": if value == "ascending": if not self._sort_type_item.props.active: self._sort_type_item.props.active = True else: if not self._sort_type_item.props.active: self._sort_type_item.props.active = False elif key == "hide-unnamed": self.blueman.List.display_known_devices() def on_device_selected(self, _lst: ManagerDeviceList, device: Device, tree_iter: Gtk.TreeIter) -> None: if tree_iter and device: self.item_device.props.sensitive = True if self.device_menu is None: self.device_menu = ManagerDeviceMenu(self.blueman) self.item_device.set_submenu(self.device_menu) else: def idle() -> bool: assert self.device_menu is not None # https://github.com/python/mypy/issues/2608 self.device_menu.generate() return False GLib.idle_add(idle, priority=GLib.PRIORITY_LOW) else: self.item_device.props.sensitive = False def on_adapter_property_changed(self, _adapter: Adapter, name: str, value: Any, path: str) -> None: if name == "Name" or name == "Alias": item = self.adapter_items[path][0] item.set_label(value) elif name == "Discovering": if self.Search: if value: self.Search.props.sensitive = False else: self.Search.props.sensitive = True def on_adapter_selected(self, menuitem: Gtk.CheckMenuItem, adapter_path: str) -> None: if menuitem.props.active: assert self.blueman.List.Adapter is not None if adapter_path != self.blueman.List.Adapter.get_object_path(): logging.info(f"selected {adapter_path}") self.blueman.Config["last-adapter"] = adapter_path_to_name(adapter_path) self.blueman.List.set_adapter(adapter_path) def on_adapter_added(self, _manager: Optional[Manager], adapter_path: str) -> None: adapter = Adapter(obj_path=adapter_path) menu = self.item_adapter.get_submenu() assert isinstance(menu, Gtk.Menu) object_path = adapter.get_object_path() item = Gtk.RadioMenuItem.new_with_label(self._adapters_group, adapter.get_name()) item.show() self._adapters_group = item.get_group() self._itemhandler = item.connect("activate", self.on_adapter_selected, object_path) self._adapterhandler = adapter.connect_signal("property-changed", self.on_adapter_property_changed) menu.insert(item, self._insert_adapter_item_pos) self._insert_adapter_item_pos += 1 self.adapter_items[object_path] = (item, adapter) assert self.blueman.List.Adapter is not None if adapter_path == self.blueman.List.Adapter.get_object_path(): item.props.active = True if len(self.adapter_items) > 0: self.item_adapter.props.sensitive = True def on_adapter_removed(self, _manager: Manager, adapter_path: str) -> None: item, adapter = self.adapter_items.pop(adapter_path) menu = self.item_adapter.get_submenu() assert isinstance(menu, Gtk.Menu) item.disconnect(self._itemhandler) adapter.disconnect(self._adapterhandler) menu.remove(item) self._insert_adapter_item_pos -= 1 if len(self.adapter_items) == 0: self.item_adapter.props.sensitive = False def _on_plugin_dialog_activate(self, _item: Gtk.MenuItem) -> None: def cb() -> None: pass self.blueman.Applet.OpenPluginDialog(result_handler=cb)
class ManagerDeviceList(DeviceList): def __init__(self, adapter=None, inst=None): cr = Gtk.CellRendererText() cr.props.ellipsize = Pango.EllipsizeMode.END tabledata = [ # device picture {"id": "device_surface", "type": str, "renderer": Gtk.CellRendererPixbuf(), "render_attrs": {}, "celldata_func": (self._set_device_cell_data, None)}, # device caption {"id": "caption", "type": str, "renderer": cr, "render_attrs": {"markup": 1}, "view_props": {"expand": True}}, {"id": "rssi_pb", "type": GdkPixbuf.Pixbuf, "renderer": Gtk.CellRendererPixbuf(), "render_attrs": {"pixbuf": 2}, "view_props": {"spacing": 0}}, {"id": "lq_pb", "type": GdkPixbuf.Pixbuf, "renderer": Gtk.CellRendererPixbuf(), "render_attrs": {"pixbuf": 3}, "view_props": {"spacing": 0}}, {"id": "tpl_pb", "type": GdkPixbuf.Pixbuf, "renderer": Gtk.CellRendererPixbuf(), "render_attrs": {"pixbuf": 4}, "view_props": {"spacing": 0}}, {"id": "alias", "type": str}, # used for quick access instead of device.GetProperties {"id": "connected", "type": bool}, # used for quick access instead of device.GetProperties {"id": "paired", "type": bool}, # used for quick access instead of device.GetProperties {"id": "trusted", "type": bool}, # used for quick access instead of device.GetProperties {"id": "objpush", "type": bool}, # used to set Send File button {"id": "rssi", "type": float}, {"id": "lq", "type": float}, {"id": "tpl", "type": float}, {"id": "icon_info", "type": Gtk.IconInfo}, {"id": "cell_fader", "type": GObject.TYPE_PYOBJECT}, {"id": "row_fader", "type": GObject.TYPE_PYOBJECT}, {"id": "levels_visible", "type": bool}, {"id": "initial_anim", "type": bool}, ] super(ManagerDeviceList, self).__init__(adapter, tabledata) self.set_name("ManagerDeviceList") self.set_headers_visible(False) self.props.has_tooltip = True self.Blueman = inst self.Config = Config("org.blueman.general") self.Config.connect('changed', self._on_settings_changed) # Set the correct sorting self._on_settings_changed(self.Config, "sort-by") self._on_settings_changed(self.Config, "sort-type") self.connect("query-tooltip", self.tooltip_query) self.tooltip_row = None self.tooltip_col = None self.connect("button_press_event", self.on_event_clicked) self.connect("button_release_event", self.on_event_clicked) self.menu = None self.connect("drag_data_received", self.drag_recv) self.connect("drag-motion", self.drag_motion) Gtk.Widget.drag_dest_set(self, Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY | Gdk.DragAction.DEFAULT) Gtk.Widget.drag_dest_add_uri_targets(self) self.set_search_equal_func(self.search_func, None) def _on_settings_changed(self, settings, key): if key in ('sort-by', 'sort-order'): sort_by = settings['sort-by'] sort_order = settings['sort-order'] if sort_order == 'ascending': sort_type = Gtk.SortType.ASCENDING else: sort_type = Gtk.SortType.DESCENDING column_id = self.ids.setdefault(sort_by, None) if column_id: self.liststore.set_sort_column_id(column_id, sort_type) def on_icon_theme_changed(self, widget): for row in self.liststore: device = self.get(row.iter, "device")["device"] self.row_setup_event(row.iter, device) def do_device_found(self, device): tree_iter = self.find_device(device) if tree_iter: anim = TreeRowColorFade(self, self.props.model.get_path(tree_iter), Gdk.RGBA(0, 0, 1, 1)) anim.animate(start=0.8, end=1.0) def search_func(self, model, column, key, tree_iter): row = self.get(tree_iter, "caption") if key.lower() in row["caption"].lower(): return False logging.info("%s %s %s %s" % (model, column, key, tree_iter)) return True def drag_recv(self, widget, context, x, y, selection, target_type, time): uris = list(selection.get_uris()) context.finish(True, False, time) path = self.get_path_at_pos(x, y) if path: tree_iter = self.get_iter(path[0]) device = self.get(tree_iter, "device")["device"] command = "blueman-sendto --device=%s" % device['Address'] launch(command, uris, False, "blueman", _("File Sender")) context.finish(True, False, time) else: context.finish(False, False, time) return True def drag_motion(self, widget, drag_context, x, y, timestamp): result = self.get_path_at_pos(x, y) if result is not None: path = result[0] if not self.selection.path_is_selected(path): tree_iter = self.get_iter(path) has_obj_push = self._has_objpush(self.get(tree_iter, "device")["device"]) if has_obj_push: Gdk.drag_status(drag_context, Gdk.DragAction.COPY, timestamp) self.set_cursor(path) return True else: Gdk.drag_status(drag_context, Gdk.DragAction.DEFAULT, timestamp) return False else: Gdk.drag_status(drag_context, Gdk.DragAction.DEFAULT, timestamp) return False def on_event_clicked(self, widget, event): if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3: path = self.get_path_at_pos(int(event.x), int(event.y)) if path is not None: row = self.get(path[0], "device") if row: if self.Blueman is not None: if self.menu is None: self.menu = ManagerDeviceMenu(self.Blueman) self.menu.popup(None, None, None, None, event.button, event.time) def get_icon_info(self, icon_name, size=48, fallback=True): if icon_name is None and not fallback: return None elif icon_name is None and fallback: icon_name = "image-missing" icon_info = self.icon_theme.lookup_icon_for_scale(icon_name, size, self.get_scale_factor(), Gtk.IconLookupFlags.FORCE_SIZE) return icon_info def make_device_icon(self, icon_info, is_paired=False, is_trusted=False): window = self.get_window() scale = self.get_scale_factor() target = icon_info.load_surface(window) ctx = cairo.Context(target) if is_paired: icon_info = self.get_icon_info("dialog-password", 16, False) paired_surface = icon_info.load_surface(window) ctx.set_source_surface(paired_surface, 1 / scale, 1 / scale) ctx.paint_with_alpha(0.8) if is_trusted: icon_info = self.get_icon_info("blueman-trust", 16, False) trusted_surface = icon_info.load_surface(window) height = target.get_height() mini_height = trusted_surface.get_height() y = height / scale - mini_height / scale - 1 / scale ctx.set_source_surface(trusted_surface, 1 / scale, y) ctx.paint_with_alpha(0.8) return target def device_remove_event(self, device, tree_iter): row_fader = self.get(tree_iter, "row_fader")["row_fader"] def on_finished(fader): fader.disconnect(signal) fader.freeze() super(ManagerDeviceList, self).device_remove_event(device, tree_iter) signal = row_fader.connect("animation-finished", on_finished) row_fader.thaw() self.emit("device-selected", None, None) row_fader.animate(start=row_fader.get_state(), end=0.0, duration=400) def device_add_event(self, device): self.add_device(device) def make_caption(self, name, klass, address): return "<span size='x-large'>%(0)s</span>\n<span size='small'>%(1)s</span>\n<i>%(2)s</i>" \ % {"0": html.escape(name), "1": klass.capitalize(), "2": address} def get_device_class(self, device): klass = get_minor_class(device['Class']) if klass != "uncategorized": return get_minor_class(device['Class'], True) else: return get_major_class(device['Class']) def row_setup_event(self, tree_iter, device): if not self.get(tree_iter, "initial_anim")["initial_anim"]: cell_fader = CellFade(self, self.props.model.get_path(tree_iter), [2, 3, 4]) row_fader = TreeRowFade(self, self.props.model.get_path(tree_iter)) has_objpush = self._has_objpush(device) self.set(tree_iter, row_fader=row_fader, cell_fader=cell_fader, levels_visible=False, objpush=has_objpush) cell_fader.freeze() def on_finished(fader): fader.disconnect(signal) fader.freeze() signal = row_fader.connect("animation-finished", on_finished) row_fader.set_state(0.0) row_fader.animate(start=0.0, end=1.0, duration=500) self.set(tree_iter, initial_anim=True) klass = get_minor_class(device['Class']) # Bluetooth >= 4 devices use Appearance property appearance = device["Appearance"] if klass != "uncategorized" and klass != "unknown": # get translated version description = get_minor_class(device['Class'], True).capitalize() elif klass == "unknown" and appearance: description = gatt_appearance_to_name(appearance) else: description = get_major_class(device['Class']).capitalize() icon_info = self.get_icon_info(device["Icon"], 48, False) caption = self.make_caption(device['Alias'], description, device['Address']) self.set(tree_iter, caption=caption, icon_info=icon_info, alias=device['Alias']) try: self.row_update_event(tree_iter, "Trusted", device['Trusted']) except Exception as e: logging.exception(e) try: self.row_update_event(tree_iter, "Paired", device['Paired']) except Exception as e: logging.exception(e) try: self.row_update_event(tree_iter, "Connected", device["Connected"]) except Exception as e: logging.exception(e) def row_update_event(self, tree_iter, key, value): logging.info("%s %s" % (key, value)) if key == "Trusted": if value: self.set(tree_iter, trusted=True) else: self.set(tree_iter, trusted=False) elif key == "Paired": if value: self.set(tree_iter, paired=True) else: self.set(tree_iter, paired=False) elif key == "Alias": device = self.get(tree_iter, "device")["device"] c = self.make_caption(value, self.get_device_class(device), device['Address']) self.set(tree_iter, caption=c, alias=value) elif key == "UUIDs": device = self.get(tree_iter, "device")["device"] has_objpush = self._has_objpush(device) self.set(tree_iter, objpush=has_objpush) elif key == "Connected": self.set(tree_iter, connected=value) def level_setup_event(self, row_ref, device, cinfo): if not row_ref.valid(): return tree_iter = self.get_iter(row_ref.get_path()) row = self.get(tree_iter, "levels_visible", "cell_fader", "rssi", "lq", "tpl") if cinfo is not None: # cinfo init may fail for bluetooth devices version 4 and up # FIXME Workaround is horrible and we should show something better if cinfo.failed: rssi_perc = tpl_perc = lq_perc = 100 else: try: rssi = float(cinfo.get_rssi()) except ConnInfoReadError: rssi = 0 try: lq = float(cinfo.get_lq()) except ConnInfoReadError: lq = 0 try: tpl = float(cinfo.get_tpl()) except ConnInfoReadError: tpl = 0 rssi_perc = 50 + (rssi / 127 / 2 * 100) tpl_perc = 50 + (tpl / 127 / 2 * 100) lq_perc = lq / 255 * 100 if lq_perc < 10: lq_perc = 10 if rssi_perc < 10: rssi_perc = 10 if tpl_perc < 10: tpl_perc = 10 if not row["levels_visible"]: logging.info("animating up") self.set(tree_iter, levels_visible=True) fader = row["cell_fader"] fader.thaw() fader.set_state(0.0) fader.animate(start=0.0, end=1.0, duration=400) def on_finished(fader): fader.freeze() fader.disconnect(signal) signal = fader.connect("animation-finished", on_finished) to_store = {} if round(row["rssi"], -1) != round(rssi_perc, -1): icon_name = "blueman-rssi-%d.png" % round(rssi_perc, -1) icon = GdkPixbuf.Pixbuf.new_from_file(os.path.join(PIXMAP_PATH, icon_name)) to_store.update({"rssi": rssi_perc, "rssi_pb": icon}) if round(row["lq"], -1) != round(lq_perc, -1): icon_name = "blueman-lq-%d.png" % round(lq_perc, -1) icon = GdkPixbuf.Pixbuf.new_from_file(os.path.join(PIXMAP_PATH, icon_name)) to_store.update({"lq": lq_perc, "lq_pb": icon}) if round(row["tpl"], -1) != round(tpl_perc, -1): icon_name = "blueman-tpl-%d.png" % round(tpl_perc, -1) icon = GdkPixbuf.Pixbuf.new_from_file(os.path.join(PIXMAP_PATH, icon_name)) to_store.update({"tpl": tpl_perc, "tpl_pb": icon}) if to_store: self.set(tree_iter, **to_store) else: if row["levels_visible"]: logging.info("animating down") self.set(tree_iter, levels_visible=False, rssi=-1, lq=-1, tpl=-1) fader = row["cell_fader"] fader.thaw() fader.set_state(1.0) fader.animate(start=fader.get_state(), end=0.0, duration=400) def on_finished(fader): fader.disconnect(signal) fader.freeze() if row_ref.valid(): self.set(tree_iter, rssi_pb=None, lq_pb=None, tpl_pb=None) signal = fader.connect("animation-finished", on_finished) def tooltip_query(self, tw, x, y, kb, tooltip): path = self.get_path_at_pos(x, y) if path is not None: if path[0] != self.tooltip_row or path[1] != self.tooltip_col: self.tooltip_row = path[0] self.tooltip_col = path[1] return False if path[1] == self.columns["device_surface"]: tree_iter = self.get_iter(path[0]) row = self.get(tree_iter, "trusted", "paired") trusted = row["trusted"] paired = row["paired"] if trusted and paired: tooltip.set_markup(_("<b>Trusted and Paired</b>")) elif paired: tooltip.set_markup(_("<b>Paired</b>")) elif trusted: tooltip.set_markup(_("<b>Trusted</b>")) else: return False self.tooltip_row = path[0] self.tooltip_col = path[1] return True if path[1] == self.columns["tpl_pb"] \ or path[1] == self.columns["lq_pb"] \ or path[1] == self.columns["rssi_pb"]: tree_iter = self.get_iter(path[0]) dt = self.get(tree_iter, "connected")["connected"] if dt: rssi = self.get(tree_iter, "rssi")["rssi"] lq = self.get(tree_iter, "lq")["lq"] tpl = self.get(tree_iter, "tpl")["tpl"] if rssi < 30: rssi_state = _("Poor") elif rssi < 40: rssi_state = _("Sub-optimal") elif rssi < 60: rssi_state = _("Optimal") elif rssi < 70: rssi_state = _("Much") else: rssi_state = _("Too much") if tpl < 30: tpl_state = _("Low") elif tpl < 40: tpl_state = _("Sub-optimal") elif tpl < 60: tpl_state = _("Optimal") elif tpl < 70: tpl_state = _("High") else: tpl_state = _("Very High") tooltip_template = None if path[1] == self.columns["tpl_pb"]: tooltip_template = \ "<b>Connected</b>\nReceived Signal Strength: %(rssi)u%% <i>(%(rssi_state)s)</i>\n" \ "Link Quality: %(lq)u%%\n<b>Transmit Power Level: %(tpl)u%%</b> <i>(%(tpl_state)s)</i>" elif path[1] == self.columns["lq_pb"]: tooltip_template = \ "<b>Connected</b>\nReceived Signal Strength: %(rssi)u%% <i>(%(rssi_state)s)</i>\n" \ "<b>Link Quality: %(lq)u%%</b>\nTransmit Power Level: %(tpl)u%% <i>(%(tpl_state)s)</i>" elif path[1] == self.columns["rssi_pb"]: tooltip_template = \ "<b>Connected</b>\n<b>Received Signal Strength: %(rssi)u%%</b> <i>(%(rssi_state)s)</i>\n" \ "Link Quality: %(lq)u%%\nTransmit Power Level: %(tpl)u%% <i>(%(tpl_state)s)</i>" state_dict = {"rssi_state": rssi_state, "rssi": rssi, "lq": lq, "tpl": tpl, "tpl_state": tpl_state} tooltip.set_markup(tooltip_template % state_dict) self.tooltip_row = path[0] self.tooltip_col = path[1] return True return False def _has_objpush(self, device): if device is None: return False for uuid in device["UUIDs"]: if ServiceUUID(uuid).short_uuid == OBEX_OBJPUSH_SVCLASS_ID: return True return False def _set_device_cell_data(self, col, cell, model, tree_iter, data): row = self.get(tree_iter, "icon_info", "trusted", "paired") surface = self.make_device_icon(row["icon_info"], row["paired"], row["trusted"]) cell.set_property("surface", surface)
class ManagerDeviceList(DeviceList): def __init__(self, adapter: Optional[str] = None, inst: Optional["Blueman"] = None) -> None: cr = Gtk.CellRendererText() cr.props.ellipsize = Pango.EllipsizeMode.END tabledata: List[ListDataDict] = [ # device picture { "id": "device_surface", "type": str, "renderer": Gtk.CellRendererPixbuf(), "render_attrs": {}, "celldata_func": (self._set_cell_data, None) }, # device caption { "id": "caption", "type": str, "renderer": cr, "render_attrs": { "markup": 1 }, "view_props": { "expand": True } }, { "id": "rssi_pb", "type": GdkPixbuf.Pixbuf, "renderer": Gtk.CellRendererPixbuf(), "render_attrs": {}, "view_props": { "spacing": 0 }, "celldata_func": (self._set_cell_data, "rssi") }, { "id": "lq_pb", "type": GdkPixbuf.Pixbuf, "renderer": Gtk.CellRendererPixbuf(), "render_attrs": {}, "view_props": { "spacing": 0 }, "celldata_func": (self._set_cell_data, "lq") }, { "id": "tpl_pb", "type": GdkPixbuf.Pixbuf, "renderer": Gtk.CellRendererPixbuf(), "render_attrs": {}, "view_props": { "spacing": 0 }, "celldata_func": (self._set_cell_data, "tpl") }, { "id": "alias", "type": str }, # used for quick access instead of device.GetProperties { "id": "connected", "type": bool }, # used for quick access instead of device.GetProperties { "id": "paired", "type": bool }, # used for quick access instead of device.GetProperties { "id": "trusted", "type": bool }, # used for quick access instead of device.GetProperties { "id": "objpush", "type": bool }, # used to set Send File button { "id": "rssi", "type": float }, { "id": "lq", "type": float }, { "id": "tpl", "type": float }, { "id": "icon_info", "type": Gtk.IconInfo }, { "id": "cell_fader", "type": CellFade }, { "id": "row_fader", "type": TreeRowFade }, { "id": "levels_visible", "type": bool }, { "id": "initial_anim", "type": bool }, ] super().__init__(adapter, tabledata) self.set_name("ManagerDeviceList") self.set_headers_visible(False) self.props.has_tooltip = True self.Blueman = inst self.Config = Config("org.blueman.general") self.Config.connect('changed', self._on_settings_changed) # Set the correct sorting self._on_settings_changed(self.Config, "sort-by") self._on_settings_changed(self.Config, "sort-type") self.connect("query-tooltip", self.tooltip_query) self.tooltip_row: Optional[Gtk.TreePath] = None self.tooltip_col: Optional[Gtk.TreeViewColumn] = None self.connect("popup-menu", self._on_popup_menu) self.connect("button_press_event", self.on_event_clicked) self.connect("button_release_event", self.on_event_clicked) self.menu: Optional[ManagerDeviceMenu] = None self.connect("drag_data_received", self.drag_recv) self.connect("drag-motion", self.drag_motion) Gtk.Widget.drag_dest_set(self, Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY | Gdk.DragAction.DEFAULT) Gtk.Widget.drag_dest_add_uri_targets(self) self.set_search_equal_func(self.search_func) self._faderhandlers: Dict[str, int] = {} def _on_settings_changed(self, settings: Config, key: str) -> None: if key in ('sort-by', 'sort-order'): sort_by = settings['sort-by'] sort_order = settings['sort-order'] if sort_order == 'ascending': sort_type = Gtk.SortType.ASCENDING else: sort_type = Gtk.SortType.DESCENDING column_id = self.ids.get(sort_by) if column_id: self.liststore.set_sort_column_id(column_id, sort_type) def on_icon_theme_changed(self, _icon_them: Gtk.IconTheme) -> None: for row in self.liststore: device = self.get(row.iter, "device")["device"] self.row_setup_event(row.iter, device) def search_func(self, model: Gtk.TreeModel, column: int, key: str, tree_iter: Gtk.TreeIter) -> bool: row = self.get(tree_iter, "caption") if key.lower() in row["caption"].lower(): return False logging.info(f"{model} {column} {key} {tree_iter}") return True def drag_recv(self, _widget: Gtk.Widget, context: Gdk.DragContext, x: int, y: int, selection: Gtk.SelectionData, _info: int, time: int) -> None: uris = list(selection.get_uris()) context.finish(True, False, time) path = self.get_path_at_pos(x, y) if path: tree_iter = self.get_iter(path[0]) assert tree_iter is not None device = self.get(tree_iter, "device")["device"] command = f"blueman-sendto --device={device['Address']}" launch(command, paths=uris, name=_("File Sender")) context.finish(True, False, time) else: context.finish(False, False, time) def drag_motion(self, _widget: Gtk.Widget, drag_context: Gdk.DragContext, x: int, y: int, timestamp: int) -> bool: result = self.get_path_at_pos(x, y) if result is not None: path = result[0] assert path is not None if not self.selection.path_is_selected(path): tree_iter = self.get_iter(path) assert tree_iter is not None has_obj_push = self._has_objpush( self.get(tree_iter, "device")["device"]) if has_obj_push: Gdk.drag_status(drag_context, Gdk.DragAction.COPY, timestamp) self.set_cursor(path) return True else: Gdk.drag_status(drag_context, Gdk.DragAction.DEFAULT, timestamp) return False return False else: Gdk.drag_status(drag_context, Gdk.DragAction.DEFAULT, timestamp) return False def _on_popup_menu(self, _widget: Gtk.Widget) -> bool: if self.Blueman is None: return False if self.menu is None: self.menu = ManagerDeviceMenu(self.Blueman) window = self.get_window() assert window is not None selected = self.selected() assert selected is not None rect = self.get_cell_area(self.liststore.get_path(selected), self.get_column(1)) self.menu.popup_at_rect(window, rect, Gdk.Gravity.CENTER, Gdk.Gravity.NORTH) return True def on_event_clicked(self, _widget: Gtk.Widget, event: Gdk.Event) -> bool: if event.type not in (Gdk.EventType._2BUTTON_PRESS, Gdk.EventType.BUTTON_PRESS): return False path = self.get_path_at_pos(int(cast(Gdk.EventButton, event).x), int(cast(Gdk.EventButton, event).y)) if path is None: return False assert path[0] is not None row = self.get(path[0], "device", "connected") if not row: return False if self.Blueman is None: return False if self.menu is None: self.menu = ManagerDeviceMenu(self.Blueman) if event.type == Gdk.EventType._2BUTTON_PRESS and cast( Gdk.EventButton, event).button == 1: if self.menu.show_generic_connect_calc(row["device"]['UUIDs']): self.menu.generic_connect(None, device=row["device"], connect=not row["connected"]) if event.type == Gdk.EventType.BUTTON_PRESS and cast( Gdk.EventButton, event).button == 3: self.menu.popup_at_pointer(event) return False def get_icon_info(self, icon_name: str, size: int = 48, fallback: bool = True) -> Optional[Gtk.IconInfo]: if icon_name is None and not fallback: return None elif icon_name is None and fallback: icon_name = "image-missing" icon_info = self.icon_theme.lookup_icon_for_scale( icon_name, size, self.get_scale_factor(), Gtk.IconLookupFlags.FORCE_SIZE) return icon_info def make_device_icon(self, icon_info: Gtk.IconInfo, is_paired: bool = False, is_trusted: bool = False) -> cairo.Surface: window = self.get_window() scale = self.get_scale_factor() target = icon_info.load_surface(window) ctx = cairo.Context(target) if is_paired: _icon_info = self.get_icon_info("dialog-password", 16, False) assert _icon_info is not None paired_surface = _icon_info.load_surface(window) ctx.set_source_surface(paired_surface, 1 / scale, 1 / scale) ctx.paint_with_alpha(0.8) if is_trusted: _icon_info = self.get_icon_info("blueman-trust", 16, False) assert _icon_info is not None trusted_surface = _icon_info.load_surface(window) assert isinstance(target, cairo.ImageSurface) assert isinstance(trusted_surface, cairo.ImageSurface) height = target.get_height() mini_height = trusted_surface.get_height() y = height / scale - mini_height / scale - 1 / scale ctx.set_source_surface(trusted_surface, 1 / scale, y) ctx.paint_with_alpha(0.8) return target def device_remove_event(self, device: Device) -> None: tree_iter = self.find_device(device) assert tree_iter is not None row_fader = self.get(tree_iter, "row_fader")["row_fader"] super().device_remove_event(device) self._faderhandlers.update({ device.get_object_path(): row_fader.connect("animation-finished", self.__on_fader_finished, device) }) row_fader.thaw() self.emit("device-selected", None, None) row_fader.animate(start=row_fader.get_state(), end=0.0, duration=400) def __on_fader_finished(self, fader: TreeRowFade, device: Device) -> None: fader.disconnect(self._faderhandlers.pop(device.get_object_path())) fader.freeze() def device_add_event(self, device: Device) -> None: self.add_device(device) @staticmethod def make_caption(name: str, klass: str, address: str) -> str: return "<span size='x-large'>%(0)s</span>\n<span size='small'>%(1)s</span>\n<i>%(2)s</i>" \ % {"0": html.escape(name), "1": klass, "2": address} @staticmethod def get_device_class(device: Device) -> str: klass = get_minor_class(device['Class']) if klass != _("Uncategorized"): return klass else: return get_major_class(device['Class']) def row_setup_event(self, tree_iter: Gtk.TreeIter, device: Device) -> None: if not self.get(tree_iter, "initial_anim")["initial_anim"]: model = self.props.model assert model is not None cell_fader = CellFade(self, model.get_path(tree_iter), [2, 3, 4]) row_fader = TreeRowFade(self, model.get_path(tree_iter)) has_objpush = self._has_objpush(device) self.set(tree_iter, row_fader=row_fader, cell_fader=cell_fader, levels_visible=False, objpush=has_objpush) cell_fader.freeze() def on_finished(fader: TreeRowFade) -> None: fader.disconnect(faderhandler) fader.freeze() faderhandler = row_fader.connect("animation-finished", on_finished) row_fader.set_state(0.0) row_fader.animate(start=0.0, end=1.0, duration=500) self.set(tree_iter, initial_anim=True) klass = get_minor_class(device['Class']) # Bluetooth >= 4 devices use Appearance property appearance = device["Appearance"] if klass != _("Uncategorized") and klass != _("Unknown"): description = klass elif klass == _("Unknown") and appearance: description = gatt_appearance_to_name(appearance) else: description = get_major_class(device['Class']) icon_info = self.get_icon_info(device["Icon"], 48, False) caption = self.make_caption(device['Alias'], description, device['Address']) self.set(tree_iter, caption=caption, icon_info=icon_info, alias=device['Alias']) try: self.row_update_event(tree_iter, "Trusted", device['Trusted']) except Exception as e: logging.exception(e) try: self.row_update_event(tree_iter, "Paired", device['Paired']) except Exception as e: logging.exception(e) try: self.row_update_event(tree_iter, "Connected", device["Connected"]) except Exception as e: logging.exception(e) def row_update_event(self, tree_iter: Gtk.TreeIter, key: str, value: Any) -> None: logging.info(f"{key} {value}") if key == "Trusted": if value: self.set(tree_iter, trusted=True) else: self.set(tree_iter, trusted=False) elif key == "Paired": if value: self.set(tree_iter, paired=True) else: self.set(tree_iter, paired=False) elif key == "Alias": device = self.get(tree_iter, "device")["device"] c = self.make_caption(value, self.get_device_class(device), device['Address']) self.set(tree_iter, caption=c, alias=value) elif key == "UUIDs": device = self.get(tree_iter, "device")["device"] has_objpush = self._has_objpush(device) self.set(tree_iter, objpush=has_objpush) elif key == "Connected": self.set(tree_iter, connected=value) def level_setup_event(self, row_ref: Gtk.TreeRowReference, device: Device, cinfo: Optional[conn_info]) -> None: if not row_ref.valid(): return tree_iter = self.get_iter(row_ref.get_path()) assert tree_iter is not None row = self.get(tree_iter, "levels_visible", "cell_fader", "rssi", "lq", "tpl") if cinfo is not None: # cinfo init may fail for bluetooth devices version 4 and up # FIXME Workaround is horrible and we should show something better if cinfo.failed: rssi_perc = tpl_perc = lq_perc = 100.0 else: try: rssi = float(cinfo.get_rssi()) except ConnInfoReadError: rssi = 0 try: lq = float(cinfo.get_lq()) except ConnInfoReadError: lq = 0 try: tpl = float(cinfo.get_tpl()) except ConnInfoReadError: tpl = 0 rssi_perc = 50 + (rssi / 127 / 2 * 100) tpl_perc = 50 + (tpl / 127 / 2 * 100) lq_perc = lq / 255 * 100 if lq_perc < 10: lq_perc = 10 if rssi_perc < 10: rssi_perc = 10 if tpl_perc < 10: tpl_perc = 10 if not row["levels_visible"]: logging.info("animating up") self.set(tree_iter, levels_visible=True) fader = row["cell_fader"] fader.thaw() fader.set_state(0.0) fader.animate(start=0.0, end=1.0, duration=400) def on_finished(fader: CellFade) -> None: fader.freeze() fader.disconnect(faderhandler) faderhandler = fader.connect("animation-finished", on_finished) w = 14 * self.get_scale_factor() h = 48 * self.get_scale_factor() to_store = {} if round(row["rssi"], -1) != round(rssi_perc, -1): icon_name = "blueman-rssi-%d.png" % round(rssi_perc, -1) icon = GdkPixbuf.Pixbuf.new_from_file_at_scale( os.path.join(PIXMAP_PATH, icon_name), w, h, True) to_store.update({"rssi": rssi_perc, "rssi_pb": icon}) if round(row["lq"], -1) != round(lq_perc, -1): icon_name = "blueman-lq-%d.png" % round(lq_perc, -1) icon = GdkPixbuf.Pixbuf.new_from_file_at_scale( os.path.join(PIXMAP_PATH, icon_name), w, h, True) to_store.update({"lq": lq_perc, "lq_pb": icon}) if round(row["tpl"], -1) != round(tpl_perc, -1): icon_name = "blueman-tpl-%d.png" % round(tpl_perc, -1) icon = GdkPixbuf.Pixbuf.new_from_file_at_scale( os.path.join(PIXMAP_PATH, icon_name), w, h, True) to_store.update({"tpl": tpl_perc, "tpl_pb": icon}) if to_store: self.set(tree_iter, **to_store) else: if row["levels_visible"]: logging.info("animating down") self.set(tree_iter, levels_visible=False, rssi=-1, lq=-1, tpl=-1) fader = row["cell_fader"] fader.thaw() fader.set_state(1.0) fader.animate(start=fader.get_state(), end=0.0, duration=400) def on_finished(fader: CellFade) -> None: fader.disconnect(faderhandler) fader.freeze() if row_ref.valid(): assert tree_iter is not None # https://github.com/python/mypy/issues/2608 self.set(tree_iter, rssi_pb=None, lq_pb=None, tpl_pb=None) faderhandler = fader.connect("animation-finished", on_finished) def tooltip_query(self, _tw: Gtk.Widget, x: int, y: int, _kb: bool, tooltip: Gtk.Tooltip) -> bool: path = self.get_path_at_pos(x, y) if path is not None: if path[0] != self.tooltip_row or path[1] != self.tooltip_col: self.tooltip_row = path[0] self.tooltip_col = path[1] return False if path[1] == self.columns["device_surface"]: tree_iter = self.get_iter(path[0]) assert tree_iter is not None row = self.get(tree_iter, "trusted", "paired") trusted = row["trusted"] paired = row["paired"] if trusted and paired: tooltip.set_markup(_("<b>Trusted and Paired</b>")) elif paired: tooltip.set_markup(_("<b>Paired</b>")) elif trusted: tooltip.set_markup(_("<b>Trusted</b>")) else: return False self.tooltip_row = path[0] self.tooltip_col = path[1] return True if path[1] == self.columns["tpl_pb"] \ or path[1] == self.columns["lq_pb"] \ or path[1] == self.columns["rssi_pb"]: tree_iter = self.get_iter(path[0]) assert tree_iter is not None dt = self.get(tree_iter, "connected")["connected"] if dt: rssi = self.get(tree_iter, "rssi")["rssi"] lq = self.get(tree_iter, "lq")["lq"] tpl = self.get(tree_iter, "tpl")["tpl"] if rssi < 30: rssi_state = _("Poor") elif rssi < 40: rssi_state = _("Sub-optimal") elif rssi < 60: rssi_state = _("Optimal") elif rssi < 70: rssi_state = _("Much") else: rssi_state = _("Too much") if tpl < 30: tpl_state = _("Low") elif tpl < 40: tpl_state = _("Sub-optimal") elif tpl < 60: tpl_state = _("Optimal") elif tpl < 70: tpl_state = _("High") else: tpl_state = _("Very High") tooltip_template: str = "" if path[1] == self.columns["tpl_pb"]: tooltip_template = \ "<b>Connected</b>\nReceived Signal Strength: %(rssi)u%% <i>(%(rssi_state)s)</i>\n" \ "Link Quality: %(lq)u%%\n<b>Transmit Power Level: %(tpl)u%%</b> <i>(%(tpl_state)s)</i>" elif path[1] == self.columns["lq_pb"]: tooltip_template = \ "<b>Connected</b>\nReceived Signal Strength: %(rssi)u%% <i>(%(rssi_state)s)</i>\n" \ "<b>Link Quality: %(lq)u%%</b>\nTransmit Power Level: %(tpl)u%% <i>(%(tpl_state)s)</i>" elif path[1] == self.columns["rssi_pb"]: tooltip_template = \ "<b>Connected</b>\n<b>Received Signal Strength: %(rssi)u%%</b> <i>(%(rssi_state)s)</i>\n" \ "Link Quality: %(lq)u%%\nTransmit Power Level: %(tpl)u%% <i>(%(tpl_state)s)</i>" state_dict = { "rssi_state": rssi_state, "rssi": rssi, "lq": lq, "tpl": tpl, "tpl_state": tpl_state } tooltip.set_markup(tooltip_template % state_dict) self.tooltip_row = path[0] self.tooltip_col = path[1] return True return False def _has_objpush(self, device: Device) -> bool: if device is None: return False for uuid in device["UUIDs"]: if ServiceUUID(uuid).short_uuid == OBEX_OBJPUSH_SVCLASS_ID: return True return False def _set_cell_data(self, _col: Gtk.TreeViewColumn, cell: Gtk.CellRenderer, _model: Gtk.TreeModel, tree_iter: Gtk.TreeIter, data: Optional[str]) -> None: if data is None: row = self.get(tree_iter, "icon_info", "trusted", "paired") surface = self.make_device_icon(row["icon_info"], row["paired"], row["trusted"]) cell.set_property("surface", surface) elif data in ("rssi", "lq", "tpl"): window = self.get_window() scale = self.get_scale_factor() pb = self.get(tree_iter, data + "_pb")[data + "_pb"] if pb: surface = Gdk.cairo_surface_create_from_pixbuf( pb, scale, window) cell.set_property("surface", surface) else: cell.set_property("surface", None) def add_device(self, device: Device) -> None: if "Name" not in device and self.Config["hide-unnamed"]: logging.info(f"Hiding unnamed device: {device['Address']}") return super().add_device(device)
class ManagerDeviceList(DeviceList): def __init__(self, adapter=None, inst=None): cr = gtk.CellRendererText() cr.props.ellipsize = pango.ELLIPSIZE_END data = [ #device picture ["device_pb", 'GdkPixbuf', gtk.CellRendererPixbuf(), {"pixbuf":0}, None], #device caption ["caption", str, cr, {"markup":1}, None, {"expand": True}], ["rssi_pb", 'GdkPixbuf', gtk.CellRendererPixbuf(), {"pixbuf":2}, None, {"spacing": 0, "sizing": gtk.TREE_VIEW_COLUMN_AUTOSIZE}], ["lq_pb", 'GdkPixbuf', gtk.CellRendererPixbuf(), {"pixbuf":3}, None, {"spacing": 0, "sizing": gtk.TREE_VIEW_COLUMN_AUTOSIZE}], ["tpl_pb", 'GdkPixbuf', gtk.CellRendererPixbuf(), {"pixbuf":4}, None, {"spacing": 0, "sizing": gtk.TREE_VIEW_COLUMN_AUTOSIZE}], #trusted/bonded icons #["tb_icons", 'PyObject', CellRendererPixbufTable(), {"pixbuffs":5}, None], ["connected", bool], #used for quick access instead of device.GetProperties ["bonded", bool], #used for quick access instead of device.GetProperties ["trusted", bool], #used for quick access instead of device.GetProperties ["fake", bool], #used for quick access instead of device.GetProperties, #fake determines whether device is "discovered" or a real bluez device ["rssi", float], ["lq", float], ["tpl", float], ["orig_icon", 'GdkPixbuf'], ["cell_fader", gobject.TYPE_PYOBJECT], ["row_fader", gobject.TYPE_PYOBJECT], ["levels_visible", bool], ["initial_anim", bool], ] DeviceList.__init__(self, adapter, data) self.set_headers_visible(False) self.props.has_tooltip = True self.Blueman = inst self.connect("query-tooltip", self.tooltip_query) self.tooltip_row = -1 self.tooltip_col = None self.connect("button_press_event", self.on_event_clicked) self.connect("button_release_event", self.on_event_clicked) self.menu = None self.connect("drag_data_received", self.drag_recv) self.connect("drag-motion", self.drag_motion) self.drag_dest_set(gtk.DEST_DEFAULT_ALL, [], gtk.gdk.ACTION_COPY|gtk.gdk.ACTION_DEFAULT) self.drag_dest_add_uri_targets() self.set_search_equal_func(self.search_func) def do_device_found(self, device): iter = self.find_device(device) if iter: anim = TreeRowColorFade(self, self.props.model.get_path(iter), gtk.gdk.color_parse("blue")) anim.animate(start=0.8, end=1.0) def search_func(self, model, column, key, iter): row = self.get(iter, "caption") if key.lower() in row["caption"].lower(): return False print model, column, key, iter return True def drag_recv(self, widget, context, x, y, selection, target_type, time): uris = selection.get_uris() uris = list(uris) context.finish(True, False, time) path = self.get_path_at_pos(x, y) if path: iter = self.get_iter(path[0][0]) device = self.get(iter, "device")["device"] uris.insert(0, "blueman-sendto") uris.insert(1, "--device=%s" % device.Address) spawn(uris) context.finish(True, False, time) else: context.finish(False, False, time) return True def drag_motion(self, widget, drag_context, x, y, timestamp): path = self.get_path_at_pos(x, y) if path != None: if path[0][0] != self.selected(): iter = self.get_iter(path[0][0]) device = self.get(iter, "device")["device"] if not device.Fake: found = False for uuid in device.UUIDs: uuid16 = uuid128_to_uuid16(uuid) if uuid16 == OBEX_OBJPUSH_SVCLASS_ID: found = True break if found: drag_context.drag_status(gtk.gdk.ACTION_COPY, timestamp) self.set_cursor(path[0]) return True else: drag_context.drag_status(gtk.gdk.ACTION_DEFAULT, timestamp) return False else: drag_context.drag_status(gtk.gdk.ACTION_COPY, timestamp) self.set_cursor(path[0]) return True else: drag_context.drag_status(gtk.gdk.ACTION_DEFAULT, timestamp) return False def on_event_clicked(self, widget, event): if event.type==gtk.gdk.BUTTON_PRESS and event.button==3: path = self.get_path_at_pos(int(event.x), int(event.y)) if path != None: row = self.get(path[0][0], "device") if row: device = row["device"] if self.Blueman != None: if self.menu == None: self.menu = ManagerDeviceMenu(self.Blueman) self.menu.popup(None, None, None, event.button, event.time) def get_device_icon(self, klass): return get_icon("blueman-"+klass.replace(" ", "-").lower(), 48, "blueman") def make_device_icon(self, target, is_bonded=False, is_trusted=False, is_discovered=False, opacity=255): if opacity != 255: target = opacify_pixbuf(target, opacity) sources = [] if is_bonded: sources.append((get_icon("gtk-dialog-authentication", 16), 0, 0, 200)) if is_trusted: sources.append((get_icon("blueman-trust", 16), 0, 32, 200)) if is_discovered: sources.append((get_icon("gtk-find", 24), 24, 0, 255)) return composite_icon(target, sources) def device_remove_event(self, device, iter): if device.Temp: DeviceList.device_remove_event(self, device, iter) else: row_fader = self.get(iter, "row_fader")["row_fader"] def on_finished(fader): fader.disconnect(signal) fader.freeze() DeviceList.device_remove_event(self, device, iter) signal = row_fader.connect("animation-finished", on_finished) row_fader.thaw() self.emit("device-selected", None, None) row_fader.animate(start=row_fader.get_state(), end=0.0, duration=400) def device_add_event(self, device): if device.Fake: self.PrependDevice(device) gobject.idle_add(self.props.vadjustment.set_value , 0) return if self.Blueman.Config.props.latest_last: self.AppendDevice(device) else: self.PrependDevice(device) def make_caption(self, name, klass, address): return "<span size='x-large'>%(0)s</span>\n<span size='small'>%(1)s</span>\n<i>%(2)s</i>" % {"0":cgi.escape(name), "1":klass.capitalize(), "2":address} def row_setup_event(self, iter, device): if not self.get(iter, "initial_anim")["initial_anim"]: cell_fader = CellFade(self, self.props.model.get_path(iter), [2,3,4]) row_fader = TreeRowFade(self, self.props.model.get_path(iter)) self.set(iter, row_fader=row_fader, cell_fader=cell_fader, levels_visible=False) cell_fader.freeze() def on_finished(fader): fader.disconnect(signal) fader.freeze() signal = row_fader.connect("animation-finished", on_finished) row_fader.set_state(0.0) row_fader.animate(start=0.0, end=1.0, duration=500) self.set(iter, initial_anim=True) klass = get_minor_class(device.Class) icon = self.get_device_icon(klass) #get translated version klass = get_minor_class(device.Class, True) name = device.Alias address = device.Address caption = self.make_caption(name, klass, address) #caption = "<span size='x-large'>%(0)s</span>\n<span size='small'>%(1)s</span>\n<i>%(2)s</i>" % {"0":name, "1":klass.capitalize(), "2":address} self.set(iter, caption=caption, orig_icon=icon) self.row_update_event(iter, "Fake", device.Fake) try: self.row_update_event(iter, "Trusted", device.Trusted) except: pass try: self.row_update_event(iter, "Paired", device.Paired) except: pass def row_update_event(self, iter, key, value): dprint("row update event", key, value) #this property is only emitted when device is fake if key == "RSSI": row = self.get(iter, "orig_icon") #minimum opacity 90 #maximum opacity 255 #rssi at minimum opacity -100 #rssi at maximum opacity -45 # y = kx + b #solve linear system #{ 90 = k * -100 + b #{ 255 = k * -45 + b # k = 3 # b = 390 #and we have a formula for opacity based on rssi :) opacity = int(3 * value + 390) if opacity > 255: opacity = 255 if opacity < 90: opacity = 90 print "opacity", opacity icon = self.make_device_icon(row["orig_icon"], is_discovered=True, opacity=opacity) self.set(iter, device_pb=icon) elif key == "Trusted": row = self.get(iter, "bonded", "orig_icon") if value: icon = self.make_device_icon(row["orig_icon"], row["bonded"], True, False) self.set(iter, device_pb=icon, trusted=True) else: icon = self.make_device_icon(row["orig_icon"], row["bonded"], False, False) self.set(iter, device_pb=icon, trusted=False) elif key == "Paired": row = self.get(iter, "trusted", "orig_icon") if value: icon = self.make_device_icon(row["orig_icon"], True, row["trusted"], False) self.set(iter, device_pb=icon, bonded=True) else: icon = self.make_device_icon(row["orig_icon"], False, row["trusted"], False) self.set(iter, device_pb=icon, bonded=False) elif key == "Fake": row = self.get(iter, "bonded", "trusted", "orig_icon") if value: icon = self.make_device_icon(row["orig_icon"], False, False, True) self.set(iter, device_pb=icon, fake=True) else: icon = self.make_device_icon(row["orig_icon"], row["bonded"], row["trusted"], False) self.set(iter, device_pb=icon, fake=False) elif key == "Alias" or key == "Class": device = self.get(iter, "device")["device"] c = self.make_caption(value, get_minor_class(device.Class, True), device.Address) self.set(iter, caption=c) def level_setup_event(self, row_ref, device, cinfo): def rnd(value): return int(round(value,-1)) if not row_ref.valid(): return iter = self.get_iter(row_ref.get_path()) if True: if cinfo != None: try: rssi = float(cinfo.get_rssi()) except: rssi = 0 try: lq = float(cinfo.get_lq()) except: lq = 0 try: tpl = float(cinfo.get_tpl()) except: tpl = 0 rssi_perc = 50 + (rssi / 127 / 2 * 100) tpl_perc = 50 + (tpl / 127 / 2 * 100) lq_perc = lq / 255 * 100 if lq_perc < 10: lq_perc = 10 if rssi_perc < 10: rssi_perc = 10 if tpl_perc < 10: tpl_perc = 10 row = self.get(iter, "levels_visible", "cell_fader", "rssi", "lq", "tpl") if not row["levels_visible"]: dprint("animating up") self.set(iter, levels_visible=True) fader = row["cell_fader"] fader.thaw() fader.set_state(0.0) fader.animate(start=0.0, end=1.0, duration=400) def on_finished(fader): fader.freeze() fader.disconnect(signal) signal = fader.connect("animation-finished", on_finished ) if rnd(row["rssi"]) != rnd(rssi_perc): self.set(iter, rssi_pb=get_icon("blueman-rssi-%s" % rnd(rssi_perc), 48)) if rnd(row["lq"]) != rnd(lq_perc): self.set(iter, lq_pb=get_icon("blueman-lq-%s" % rnd(lq_perc), 48)) if rnd(row["tpl"]) != rnd(tpl_perc): self.set(iter, tpl_pb=get_icon("blueman-tpl-%s" % rnd(tpl_perc), 48)) self.set(iter, rssi=rssi_perc, lq=lq_perc, tpl=tpl_perc, connected=True) else: row = self.get(iter, "levels_visible", "cell_fader") if row["levels_visible"]: dprint("animating down") self.set(iter, levels_visible=False, rssi=-1, lq=-1, tpl=-1) fader = row["cell_fader"] fader.thaw() fader.set_state(1.0) fader.animate(start=fader.get_state(), end=0.0, duration=400) def on_finished(fader): fader.disconnect(signal) fader.freeze() if row_ref.valid(): self.set(iter, rssi_pb=None, lq_pb=None, tpl_pb=None, connected=False) signal = fader.connect("animation-finished", on_finished) else: dprint("invisible") def tooltip_query(self, tw, x, y, kb, tooltip): #print args #args[4].set_text("test"+str(args[1])) path = self.get_path_at_pos(x, y) if path: if path[0][0] != self.tooltip_row or path[1] != self.tooltip_col: self.tooltip_row = path[0][0] self.tooltip_col = path[1] return False if path[1] == self.columns["device_pb"]: iter = self.get_iter(path[0][0]) row = self.get(iter, "trusted", "bonded") trusted = row["trusted"] bonded = row["bonded"] if trusted and bonded: tooltip.set_markup(_("<b>Trusted and Bonded</b>")) elif bonded: tooltip.set_markup(_("<b>Bonded</b>")) elif trusted: tooltip.set_markup(_("<b>Trusted</b>")) else: return False self.tooltip_row = path[0][0] self.tooltip_col = path[1] return True if path[1] == self.columns["tpl_pb"] or path[1] == self.columns["lq_pb"] or path[1] == self.columns["rssi_pb"]: iter = self.get_iter(path[0][0]) dt = self.get(iter, "connected")["connected"] #print dt if dt: rssi = self.get(iter, "rssi")["rssi"] lq = self.get(iter, "lq")["lq"] tpl = self.get(iter, "tpl")["tpl"] if rssi < 30: rssi_state = _("Poor") if rssi < 40 and rssi > 30: rssi_state = _("Sub-optimal") elif rssi > 40 and rssi < 60: rssi_state = _("Optimal") elif rssi > 60: rssi_state = _("Much") elif rssi > 70: rssi_state = _("Too much") if tpl < 30: tpl_state = _("Low") if tpl < 40 and tpl > 30: tpl_state = _("Sub-optimal") elif tpl > 40 and rssi < 60: tpl_state = _("Optimal") elif tpl > 60: tpl_state = _("High") elif tpl > 70: tpl_state = _("Very High") if path[1] == self.columns["tpl_pb"]: tooltip.set_markup(_("<b>Connected</b>\nReceived Signal Strength: %(rssi)u%% <i>(%(rssi_state)s)</i>\nLink Quality: %(lq)u%%\n<b>Transmit Power Level: %(tpl)u%%</b> <i>(%(tpl_state)s)</i>") % {"rssi_state": rssi_state, "rssi":rssi, "lq":lq, "tpl":tpl, "tpl_state": tpl_state}) elif path[1] == self.columns["lq_pb"]: tooltip.set_markup(_("<b>Connected</b>\nReceived Signal Strength: %(rssi)u%% <i>(%(rssi_state)s)</i>\n<b>Link Quality: %(lq)u%%</b>\nTransmit Power Level: %(tpl)u%% <i>(%(tpl_state)s)</i>") % {"rssi_state": rssi_state, "rssi":rssi, "lq":lq, "tpl":tpl, "tpl_state": tpl_state}) elif path[1] == self.columns["rssi_pb"]: tooltip.set_markup(_("<b>Connected</b>\n<b>Received Signal Strength: %(rssi)u%%</b> <i>(%(rssi_state)s)</i>\nLink Quality: %(lq)u%%\nTransmit Power Level: %(tpl)u%% <i>(%(tpl_state)s)</i>") % {"rssi_state": rssi_state, "rssi":rssi, "lq":lq, "tpl":tpl, "tpl_state": tpl_state}) self.tooltip_row = path[0][0] self.tooltip_col = path[1] return True return False
class ManagerDeviceList(DeviceList): def __init__(self, adapter=None, inst=None): cr = Gtk.CellRendererText() cr.props.ellipsize = Pango.EllipsizeMode.END data = [ # device picture ["device_pb", GdkPixbuf.Pixbuf, Gtk.CellRendererPixbuf(), {"pixbuf": 0}, None], # device caption ["caption", str, cr, {"markup": 1}, None, {"expand": True}], ["rssi_pb", GdkPixbuf.Pixbuf, Gtk.CellRendererPixbuf(), {"pixbuf": 2}, None, {"spacing": 0}], ["lq_pb", GdkPixbuf.Pixbuf, Gtk.CellRendererPixbuf(), {"pixbuf": 3}, None, {"spacing": 0}], ["tpl_pb", GdkPixbuf.Pixbuf, Gtk.CellRendererPixbuf(), {"pixbuf": 4}, None, {"spacing": 0}], # trusted/bonded icons # ["tb_icons", 'PyObject', CellRendererPixbufTable(), {"pixbuffs":5}, None], ["alias", str], # used for quick access instead of device.GetProperties ["connected", bool], # used for quick access instead of device.GetProperties ["bonded", bool], # used for quick access instead of device.GetProperties ["trusted", bool], # used for quick access instead of device.GetProperties ["fake", bool], # used for quick access instead of device.GetProperties, # fake determines whether device is "discovered" or a real bluez device ["objpush", bool], # used to set Send File button ["rssi", float], ["lq", float], ["tpl", float], ["orig_icon", GdkPixbuf.Pixbuf], ["cell_fader", GObject.TYPE_PYOBJECT], ["row_fader", GObject.TYPE_PYOBJECT], ["levels_visible", bool], ["initial_anim", bool], ] super(ManagerDeviceList, self).__init__(adapter, data) self.set_name("ManagerDeviceList") self.set_headers_visible(False) self.props.has_tooltip = True self.Blueman = inst self.connect("query-tooltip", self.tooltip_query) self.tooltip_row = None self.tooltip_col = None self.connect("button_press_event", self.on_event_clicked) self.connect("button_release_event", self.on_event_clicked) self.menu = None self.connect("drag_data_received", self.drag_recv) self.connect("drag-motion", self.drag_motion) Gtk.Widget.drag_dest_set(self, Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY | Gdk.DragAction.DEFAULT) Gtk.Widget.drag_dest_add_uri_targets(self) self.set_search_equal_func(self.search_func, None) def do_device_found(self, device): tree_iter = self.find_device(device) if tree_iter: anim = TreeRowColorFade(self, self.props.model.get_path(tree_iter), Gdk.RGBA(0, 0, 1, 1)) anim.animate(start=0.8, end=1.0) def search_func(self, model, column, key, tree_iter): row = self.get(tree_iter, "caption") if key.lower() in row["caption"].lower(): return False print(model, column, key, tree_iter) return True def drag_recv(self, widget, context, x, y, selection, target_type, time): uris = list(selection.get_uris()) context.finish(True, False, time) path = self.get_path_at_pos(x, y) if path: tree_iter = self.get_iter(path[0]) device = self.get(tree_iter, "device")["device"] command = "blueman-sendto --device=%s" % device['Address'] launch(command, uris, False, "blueman", _("File Sender")) context.finish(True, False, time) else: context.finish(False, False, time) return True def drag_motion(self, widget, drag_context, x, y, timestamp): path = self.get_path_at_pos(x, y) if path is not None: if path[0] != self.selected(): tree_iter = self.get_iter(path[0]) device = self.get(tree_iter, "device")["device"] found = False for uuid in device['UUIDs']: uuid16 = uuid128_to_uuid16(uuid) if uuid16 == OBEX_OBJPUSH_SVCLASS_ID: found = True break if found: drag_context.drag_status(Gdk.DragAction.COPY, timestamp) self.set_cursor(path[0]) return True else: drag_context.drag_status(Gdk.DragAction.DEFAULT, timestamp) return False else: drag_context.drag_status(Gdk.DragAction.DEFAULT, timestamp) return False def on_event_clicked(self, widget, event): if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3: path = self.get_path_at_pos(int(event.x), int(event.y)) if path is not None: row = self.get(path[0], "device") if row: device = row["device"] if self.Blueman is not None: if self.menu is None: self.menu = ManagerDeviceMenu(self.Blueman) self.menu.popup(None, None, None, None, event.button, event.time) def get_device_icon(self, klass): return get_icon("blueman-" + klass.replace(" ", "-").lower(), 48, "blueman") def make_device_icon(self, target, is_bonded=False, is_trusted=False, is_discovered=False, opacity=255): if opacity != 255: target = opacify_pixbuf(target, opacity) sources = [] if is_bonded: sources.append((get_icon("dialog-password", 16), 0, 0, 200)) if is_trusted: sources.append((get_icon("blueman-trust", 16), 0, 32, 200)) if is_discovered: sources.append((get_icon("edit-find", 24), 24, 0, 255)) return composite_icon(target, sources) def device_remove_event(self, device, tree_iter): row_fader = self.get(tree_iter, "row_fader")["row_fader"] def on_finished(fader): fader.disconnect(signal) fader.freeze() DeviceList.device_remove_event(self, device, tree_iter) signal = row_fader.connect("animation-finished", on_finished) row_fader.thaw() self.emit("device-selected", None, None) row_fader.animate(start=row_fader.get_state(), end=0.0, duration=400) def device_add_event(self, device): if self.Blueman.Config["latest-last"]: self.add_device(device, append=True) else: self.add_device(device, append=False) def make_caption(self, name, klass, address): return "<span size='x-large'>%(0)s</span>\n<span size='small'>%(1)s</span>\n<i>%(2)s</i>" % {"0": cgi.escape(name), "1": klass.capitalize(), "2": address} def get_device_class(self, device): klass = get_minor_class(device['Class']) if klass != "uncategorized": return get_minor_class(device['Class'], True) else: return get_major_class(device['Class']) def row_setup_event(self, tree_iter, device): if not self.get(tree_iter, "initial_anim")["initial_anim"]: cell_fader = CellFade(self, self.props.model.get_path(tree_iter), [2, 3, 4]) row_fader = TreeRowFade(self, self.props.model.get_path(tree_iter)) has_objpush = self._has_objpush(device) self.set(tree_iter, row_fader=row_fader, cell_fader=cell_fader, levels_visible=False, objpush=has_objpush) cell_fader.freeze() def on_finished(fader): fader.disconnect(signal) fader.freeze() signal = row_fader.connect("animation-finished", on_finished) row_fader.set_state(0.0) row_fader.animate(start=0.0, end=1.0, duration=500) self.set(tree_iter, initial_anim=True) klass = get_minor_class(device['Class']) if klass != "uncategorized": icon = self.get_device_icon(klass) # get translated version klass = get_minor_class(device['Class'], True) else: icon = get_icon(device['Icon'], 48, "blueman") klass = get_major_class(device['Class']) name = device['Alias'] address = device['Address'] caption = self.make_caption(name, klass, address) # caption = "<span size='x-large'>%(0)s</span>\n<span size='small'>%(1)s</span>\n<i>%(2)s</i>" % {"0":name, "1":klass.capitalize(), "2":address} self.set(tree_iter, caption=caption, orig_icon=icon, alias=name) try: self.row_update_event(tree_iter, "Trusted", device['Trusted']) except: pass try: self.row_update_event(tree_iter, "Paired", device['Paired']) except: pass def row_update_event(self, tree_iter, key, value): dprint("row update event", key, value) # this property is only emitted when device is fake if key == "RSSI": row = self.get(tree_iter, "orig_icon") #minimum opacity 90 #maximum opacity 255 #rssi at minimum opacity -100 #rssi at maximum opacity -45 # y = kx + b #solve linear system #{ 90 = k * -100 + b #{ 255 = k * -45 + b # k = 3 # b = 390 #and we have a formula for opacity based on rssi :) opacity = int(3 * value + 390) if opacity > 255: opacity = 255 if opacity < 90: opacity = 90 print("opacity", opacity) icon = self.make_device_icon(row["orig_icon"], is_discovered=True, opacity=opacity) self.set(tree_iter, device_pb=icon) elif key == "Trusted": row = self.get(tree_iter, "bonded", "orig_icon") if value: icon = self.make_device_icon(row["orig_icon"], row["bonded"], True, False) self.set(tree_iter, device_pb=icon, trusted=True) else: icon = self.make_device_icon(row["orig_icon"], row["bonded"], False, False) self.set(tree_iter, device_pb=icon, trusted=False) elif key == "Paired": row = self.get(tree_iter, "trusted", "orig_icon") if value: icon = self.make_device_icon(row["orig_icon"], True, row["trusted"], False) self.set(tree_iter, device_pb=icon, bonded=True) else: icon = self.make_device_icon(row["orig_icon"], False, row["trusted"], False) self.set(tree_iter, device_pb=icon, bonded=False) elif key == "Alias": device = self.get(tree_iter, "device")["device"] c = self.make_caption(value, self.get_device_class(device), device['Address']) self.set(tree_iter, caption=c, alias=value) elif key == "UUIDs": device = self.get(tree_iter, "device")["device"] has_objpush = self._has_objpush(device) self.set(tree_iter, objpush=has_objpush) def level_setup_event(self, row_ref, device, cinfo): def rnd(value): return int(round(value, -1)) if not row_ref.valid(): return tree_iter = self.get_iter(row_ref.get_path()) if True: if cinfo is not None: try: rssi = float(cinfo.get_rssi()) except: rssi = 0 try: lq = float(cinfo.get_lq()) except: lq = 0 try: tpl = float(cinfo.get_tpl()) except: tpl = 0 rssi_perc = 50 + (rssi / 127 / 2 * 100) tpl_perc = 50 + (tpl / 127 / 2 * 100) lq_perc = lq / 255 * 100 if lq_perc < 10: lq_perc = 10 if rssi_perc < 10: rssi_perc = 10 if tpl_perc < 10: tpl_perc = 10 row = self.get(tree_iter, "levels_visible", "cell_fader", "rssi", "lq", "tpl") if not row["levels_visible"]: dprint("animating up") self.set(tree_iter, levels_visible=True) fader = row["cell_fader"] fader.thaw() fader.set_state(0.0) fader.animate(start=0.0, end=1.0, duration=400) def on_finished(fader): fader.freeze() fader.disconnect(signal) signal = fader.connect("animation-finished", on_finished) if rnd(row["rssi"]) != rnd(rssi_perc): icon = GdkPixbuf.Pixbuf.new_from_file(PIXMAP_PATH + "/blueman-rssi-" + str(rnd(rssi_perc)) + ".png") self.set(tree_iter, rssi_pb=icon) if rnd(row["lq"]) != rnd(lq_perc): icon = GdkPixbuf.Pixbuf.new_from_file(PIXMAP_PATH + "/blueman-lq-" + str(rnd(lq_perc)) + ".png") self.set(tree_iter, lq_pb=icon) if rnd(row["tpl"]) != rnd(tpl_perc): icon = GdkPixbuf.Pixbuf.new_from_file(PIXMAP_PATH + "/blueman-tpl-" + str(rnd(tpl_perc)) + ".png") self.set(tree_iter, tpl_pb=icon) self.set(tree_iter, rssi=rssi_perc, lq=lq_perc, tpl=tpl_perc, connected=True) else: row = self.get(tree_iter, "levels_visible", "cell_fader") if row["levels_visible"]: dprint("animating down") self.set(tree_iter, levels_visible=False, rssi=-1, lq=-1, tpl=-1) fader = row["cell_fader"] fader.thaw() fader.set_state(1.0) fader.animate(start=fader.get_state(), end=0.0, duration=400) def on_finished(fader): fader.disconnect(signal) fader.freeze() if row_ref.valid(): self.set(tree_iter, rssi_pb=None, lq_pb=None, tpl_pb=None, connected=False) signal = fader.connect("animation-finished", on_finished) else: dprint("invisible") def tooltip_query(self, tw, x, y, kb, tooltip): # print args #args[4].set_text("test"+str(args[1])) path = self.get_path_at_pos(x, y) if path is not None: if path[0] != self.tooltip_row or path[1] != self.tooltip_col: self.tooltip_row = path[0] self.tooltip_col = path[1] return False if path[1] == self.columns["device_pb"]: tree_iter = self.get_iter(path[0]) row = self.get(tree_iter, "trusted", "bonded") trusted = row["trusted"] bonded = row["bonded"] if trusted and bonded: tooltip.set_markup(_("<b>Trusted and Bonded</b>")) elif bonded: tooltip.set_markup(_("<b>Bonded</b>")) elif trusted: tooltip.set_markup(_("<b>Trusted</b>")) else: return False self.tooltip_row = path[0] self.tooltip_col = path[1] return True if path[1] == self.columns["tpl_pb"] or path[1] == self.columns["lq_pb"] or path[1] == self.columns["rssi_pb"]: tree_iter = self.get_iter(path[0]) dt = self.get(tree_iter, "connected")["connected"] #print dt if dt: rssi = self.get(tree_iter, "rssi")["rssi"] lq = self.get(tree_iter, "lq")["lq"] tpl = self.get(tree_iter, "tpl")["tpl"] if rssi < 30: rssi_state = _("Poor") if 40 > rssi > 30: rssi_state = _("Sub-optimal") elif 40 < rssi < 60: rssi_state = _("Optimal") elif rssi > 60: rssi_state = _("Much") elif rssi > 70: rssi_state = _("Too much") if tpl < 30: tpl_state = _("Low") if 40 > tpl > 30: tpl_state = _("Sub-optimal") elif tpl > 40 and rssi < 60: tpl_state = _("Optimal") elif tpl > 60: tpl_state = _("High") elif tpl > 70: tpl_state = _("Very High") if path[1] == self.columns["tpl_pb"]: tooltip.set_markup(_("<b>Connected</b>\nReceived Signal Strength: %(rssi)u%% <i>(%(rssi_state)s)</i>\nLink Quality: %(lq)u%%\n<b>Transmit Power Level: %(tpl)u%%</b> <i>(%(tpl_state)s)</i>") % {"rssi_state": rssi_state, "rssi": rssi, "lq": lq, "tpl": tpl, "tpl_state": tpl_state}) elif path[1] == self.columns["lq_pb"]: tooltip.set_markup(_("<b>Connected</b>\nReceived Signal Strength: %(rssi)u%% <i>(%(rssi_state)s)</i>\n<b>Link Quality: %(lq)u%%</b>\nTransmit Power Level: %(tpl)u%% <i>(%(tpl_state)s)</i>") % {"rssi_state": rssi_state, "rssi": rssi, "lq": lq, "tpl": tpl, "tpl_state": tpl_state}) elif path[1] == self.columns["rssi_pb"]: tooltip.set_markup(_("<b>Connected</b>\n<b>Received Signal Strength: %(rssi)u%%</b> <i>(%(rssi_state)s)</i>\nLink Quality: %(lq)u%%\nTransmit Power Level: %(tpl)u%% <i>(%(tpl_state)s)</i>") % {"rssi_state": rssi_state, "rssi": rssi, "lq": lq, "tpl": tpl, "tpl_state": tpl_state}) self.tooltip_row = path[0] self.tooltip_col = path[1] return True return False def _has_objpush(self, device): if device is None: return False for uuid in device["UUIDs"]: uuid16 = uuid128_to_uuid16(uuid) if uuid16 == OBEX_OBJPUSH_SVCLASS_ID: return True return False
class ManagerDeviceList(DeviceList): def __init__(self, adapter=None, inst=None): cr = Gtk.CellRendererText() cr.props.ellipsize = Pango.EllipsizeMode.END data = [ # device picture [ "device_pb", GdkPixbuf.Pixbuf, Gtk.CellRendererPixbuf(), { "pixbuf": 0 }, None ], # device caption ["caption", str, cr, { "markup": 1 }, None, { "expand": True }], [ "rssi_pb", GdkPixbuf.Pixbuf, Gtk.CellRendererPixbuf(), { "pixbuf": 2 }, None, { "spacing": 0 } ], [ "lq_pb", GdkPixbuf.Pixbuf, Gtk.CellRendererPixbuf(), { "pixbuf": 3 }, None, { "spacing": 0 } ], [ "tpl_pb", GdkPixbuf.Pixbuf, Gtk.CellRendererPixbuf(), { "pixbuf": 4 }, None, { "spacing": 0 } ], # trusted/bonded icons # ["tb_icons", 'PyObject', CellRendererPixbufTable(), {"pixbuffs":5}, None], ["connected", bool], # used for quick access instead of device.GetProperties ["bonded", bool], # used for quick access instead of device.GetProperties ["trusted", bool], # used for quick access instead of device.GetProperties ["fake", bool], # used for quick access instead of device.GetProperties, # fake determines whether device is "discovered" or a real bluez device ["rssi", float], ["lq", float], ["tpl", float], ["orig_icon", GdkPixbuf.Pixbuf], ["cell_fader", GObject.TYPE_PYOBJECT], ["row_fader", GObject.TYPE_PYOBJECT], ["levels_visible", bool], ["initial_anim", bool], ] DeviceList.__init__(self, adapter, data) self.set_headers_visible(False) self.props.has_tooltip = True self.Blueman = inst self.connect("query-tooltip", self.tooltip_query) self.tooltip_row = None self.tooltip_col = None self.connect("button_press_event", self.on_event_clicked) self.connect("button_release_event", self.on_event_clicked) self.menu = None self.connect("drag_data_received", self.drag_recv) self.connect("drag-motion", self.drag_motion) Gtk.Widget.drag_dest_set(self, Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY | Gdk.DragAction.DEFAULT) Gtk.Widget.drag_dest_add_uri_targets(self) self.set_search_equal_func(self.search_func, None) def do_device_found(self, device): iter = self.find_device(device) if iter: anim = TreeRowColorFade(self, self.props.model.get_path(iter), Gdk.RGBA(0, 0, 1, 1)) anim.animate(start=0.8, end=1.0) def search_func(self, model, column, key, iter): row = self.get(iter, "caption") if key.lower() in row["caption"].lower(): return False print(model, column, key, iter) return True def drag_recv(self, widget, context, x, y, selection, target_type, time): uris = list(selection.get_uris()) context.finish(True, False, time) path = self.get_path_at_pos(x, y) if path: iter = self.get_iter(path[0]) device = self.get(iter, "device")["device"] command = "blueman-sendto --device=%s" % device.Address launch(command, uris, False, "blueman", _("File Sender")) context.finish(True, False, time) else: context.finish(False, False, time) return True def drag_motion(self, widget, drag_context, x, y, timestamp): path = self.get_path_at_pos(x, y) if path != None: if path[0] != self.selected(): iter = self.get_iter(path[0]) device = self.get(iter, "device")["device"] if not device.Fake: found = False for uuid in device.UUIDs: uuid16 = uuid128_to_uuid16(uuid) if uuid16 == OBEX_OBJPUSH_SVCLASS_ID: found = True break if found: drag_context.drag_status(Gdk.DragAction.COPY, timestamp) self.set_cursor(path[0]) return True else: drag_context.drag_status(Gdk.DragAction.DEFAULT, timestamp) return False else: drag_context.drag_status(Gdk.DragAction.COPY, timestamp) self.set_cursor(path[0]) return True else: drag_context.drag_status(Gdk.DragAction.DEFAULT, timestamp) return False def on_event_clicked(self, widget, event): if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3: path = self.get_path_at_pos(int(event.x), int(event.y)) if path != None: row = self.get(path[0], "device") if row: device = row["device"] if self.Blueman != None: if self.menu == None: self.menu = ManagerDeviceMenu(self.Blueman) self.menu.popup(None, None, None, None, event.button, event.time) def get_device_icon(self, klass): return get_icon("blueman-" + klass.replace(" ", "-").lower(), 48, "blueman") def make_device_icon(self, target, is_bonded=False, is_trusted=False, is_discovered=False, opacity=255): if opacity != 255: target = opacify_pixbuf(target, opacity) sources = [] if is_bonded: sources.append((get_icon("dialog-password", 16), 0, 0, 200)) if is_trusted: sources.append((get_icon("blueman-trust", 16), 0, 32, 200)) if is_discovered: sources.append((get_icon("edit-find", 24), 24, 0, 255)) return composite_icon(target, sources) def device_remove_event(self, device, iter): if device.Temp: DeviceList.device_remove_event(self, device, iter) else: row_fader = self.get(iter, "row_fader")["row_fader"] def on_finished(fader): fader.disconnect(signal) fader.freeze() DeviceList.device_remove_event(self, device, iter) signal = row_fader.connect("animation-finished", on_finished) row_fader.thaw() self.emit("device-selected", None, None) row_fader.animate(start=row_fader.get_state(), end=0.0, duration=400) def device_add_event(self, device): if device.Fake: self.PrependDevice(device) GObject.idle_add(self.props.vadjustment.set_value, 0) return if self.Blueman.Config["latest-last"]: self.AppendDevice(device) else: self.PrependDevice(device) def make_caption(self, name, klass, address): return "<span size='x-large'>%(0)s</span>\n<span size='small'>%(1)s</span>\n<i>%(2)s</i>" % { "0": cgi.escape(name), "1": klass.capitalize(), "2": address } def get_device_class(self, device): klass = get_minor_class(device.Class) if klass != "uncategorized": return get_minor_class(device.Class, True) else: return get_major_class(device.Class) def row_setup_event(self, iter, device): if not self.get(iter, "initial_anim")["initial_anim"]: cell_fader = CellFade(self, self.props.model.get_path(iter), [2, 3, 4]) row_fader = TreeRowFade(self, self.props.model.get_path(iter)) self.set(iter, row_fader=row_fader, cell_fader=cell_fader, levels_visible=False) cell_fader.freeze() def on_finished(fader): fader.disconnect(signal) fader.freeze() signal = row_fader.connect("animation-finished", on_finished) row_fader.set_state(0.0) row_fader.animate(start=0.0, end=1.0, duration=500) self.set(iter, initial_anim=True) klass = get_minor_class(device.Class) if klass != "uncategorized": icon = self.get_device_icon(klass) # get translated version klass = get_minor_class(device.Class, True) else: icon = get_icon(device.Icon, 48, "blueman") klass = get_major_class(device.Class) name = device.Alias address = device.Address caption = self.make_caption(name, klass, address) # caption = "<span size='x-large'>%(0)s</span>\n<span size='small'>%(1)s</span>\n<i>%(2)s</i>" % {"0":name, "1":klass.capitalize(), "2":address} self.set(iter, caption=caption, orig_icon=icon) self.row_update_event(iter, "Fake", device.Fake) try: self.row_update_event(iter, "Trusted", device.Trusted) except: pass try: self.row_update_event(iter, "Paired", device.Paired) except: pass def row_update_event(self, iter, key, value): dprint("row update event", key, value) # this property is only emitted when device is fake if key == "RSSI": row = self.get(iter, "orig_icon") #minimum opacity 90 #maximum opacity 255 #rssi at minimum opacity -100 #rssi at maximum opacity -45 # y = kx + b #solve linear system #{ 90 = k * -100 + b #{ 255 = k * -45 + b # k = 3 # b = 390 #and we have a formula for opacity based on rssi :) opacity = int(3 * value + 390) if opacity > 255: opacity = 255 if opacity < 90: opacity = 90 print("opacity", opacity) icon = self.make_device_icon(row["orig_icon"], is_discovered=True, opacity=opacity) self.set(iter, device_pb=icon) elif key == "Trusted": row = self.get(iter, "bonded", "orig_icon") if value: icon = self.make_device_icon(row["orig_icon"], row["bonded"], True, False) self.set(iter, device_pb=icon, trusted=True) else: icon = self.make_device_icon(row["orig_icon"], row["bonded"], False, False) self.set(iter, device_pb=icon, trusted=False) elif key == "Paired": row = self.get(iter, "trusted", "orig_icon") if value: icon = self.make_device_icon(row["orig_icon"], True, row["trusted"], False) self.set(iter, device_pb=icon, bonded=True) else: icon = self.make_device_icon(row["orig_icon"], False, row["trusted"], False) self.set(iter, device_pb=icon, bonded=False) elif key == "Fake": row = self.get(iter, "bonded", "trusted", "orig_icon") if value: icon = self.make_device_icon(row["orig_icon"], False, False, True) self.set(iter, device_pb=icon, fake=True) else: icon = self.make_device_icon(row["orig_icon"], row["bonded"], row["trusted"], False) self.set(iter, device_pb=icon, fake=False) elif key == "Alias": device = self.get(iter, "device")["device"] c = self.make_caption(value, self.get_device_class(device), device.Address) self.set(iter, caption=c) def level_setup_event(self, row_ref, device, cinfo): def rnd(value): return int(round(value, -1)) if not row_ref.valid(): return iter = self.get_iter(row_ref.get_path()) if True: if cinfo != None: try: rssi = float(cinfo.get_rssi()) except: rssi = 0 try: lq = float(cinfo.get_lq()) except: lq = 0 try: tpl = float(cinfo.get_tpl()) except: tpl = 0 rssi_perc = 50 + (rssi / 127 / 2 * 100) tpl_perc = 50 + (tpl / 127 / 2 * 100) lq_perc = lq / 255 * 100 if lq_perc < 10: lq_perc = 10 if rssi_perc < 10: rssi_perc = 10 if tpl_perc < 10: tpl_perc = 10 row = self.get(iter, "levels_visible", "cell_fader", "rssi", "lq", "tpl") if not row["levels_visible"]: dprint("animating up") self.set(iter, levels_visible=True) fader = row["cell_fader"] fader.thaw() fader.set_state(0.0) fader.animate(start=0.0, end=1.0, duration=400) def on_finished(fader): fader.freeze() fader.disconnect(signal) signal = fader.connect("animation-finished", on_finished) if rnd(row["rssi"]) != rnd(rssi_perc): icon = GdkPixbuf.Pixbuf.new_from_file(PIXMAP_PATH + "/blueman-rssi-" + str(rnd(rssi_perc)) + ".png") self.set(iter, rssi_pb=icon) if rnd(row["lq"]) != rnd(lq_perc): icon = GdkPixbuf.Pixbuf.new_from_file(PIXMAP_PATH + "/blueman-lq-" + str(rnd(lq_perc)) + ".png") self.set(iter, lq_pb=icon) if rnd(row["tpl"]) != rnd(tpl_perc): icon = GdkPixbuf.Pixbuf.new_from_file(PIXMAP_PATH + "/blueman-tpl-" + str(rnd(tpl_perc)) + ".png") self.set(iter, tpl_pb=icon) self.set(iter, rssi=rssi_perc, lq=lq_perc, tpl=tpl_perc, connected=True) else: row = self.get(iter, "levels_visible", "cell_fader") if row["levels_visible"]: dprint("animating down") self.set(iter, levels_visible=False, rssi=-1, lq=-1, tpl=-1) fader = row["cell_fader"] fader.thaw() fader.set_state(1.0) fader.animate(start=fader.get_state(), end=0.0, duration=400) def on_finished(fader): fader.disconnect(signal) fader.freeze() if row_ref.valid(): self.set(iter, rssi_pb=None, lq_pb=None, tpl_pb=None, connected=False) signal = fader.connect("animation-finished", on_finished) else: dprint("invisible") def tooltip_query(self, tw, x, y, kb, tooltip): # print args #args[4].set_text("test"+str(args[1])) path = self.get_path_at_pos(x, y) if path is not None: if path[0] != self.tooltip_row or path[1] != self.tooltip_col: self.tooltip_row = path[0] self.tooltip_col = path[1] return False if path[1] == self.columns["device_pb"]: iter = self.get_iter(path[0]) row = self.get(iter, "trusted", "bonded") trusted = row["trusted"] bonded = row["bonded"] if trusted and bonded: tooltip.set_markup(_("<b>Trusted and Bonded</b>")) elif bonded: tooltip.set_markup(_("<b>Bonded</b>")) elif trusted: tooltip.set_markup(_("<b>Trusted</b>")) else: return False self.tooltip_row = path[0] self.tooltip_col = path[1] return True if path[1] == self.columns["tpl_pb"] or path[1] == self.columns[ "lq_pb"] or path[1] == self.columns["rssi_pb"]: iter = self.get_iter(path[0]) dt = self.get(iter, "connected")["connected"] #print dt if dt: rssi = self.get(iter, "rssi")["rssi"] lq = self.get(iter, "lq")["lq"] tpl = self.get(iter, "tpl")["tpl"] if rssi < 30: rssi_state = _("Poor") if rssi < 40 and rssi > 30: rssi_state = _("Sub-optimal") elif rssi > 40 and rssi < 60: rssi_state = _("Optimal") elif rssi > 60: rssi_state = _("Much") elif rssi > 70: rssi_state = _("Too much") if tpl < 30: tpl_state = _("Low") if tpl < 40 and tpl > 30: tpl_state = _("Sub-optimal") elif tpl > 40 and rssi < 60: tpl_state = _("Optimal") elif tpl > 60: tpl_state = _("High") elif tpl > 70: tpl_state = _("Very High") if path[1] == self.columns["tpl_pb"]: tooltip.set_markup( _("<b>Connected</b>\nReceived Signal Strength: %(rssi)u%% <i>(%(rssi_state)s)</i>\nLink Quality: %(lq)u%%\n<b>Transmit Power Level: %(tpl)u%%</b> <i>(%(tpl_state)s)</i>" ) % { "rssi_state": rssi_state, "rssi": rssi, "lq": lq, "tpl": tpl, "tpl_state": tpl_state }) elif path[1] == self.columns["lq_pb"]: tooltip.set_markup( _("<b>Connected</b>\nReceived Signal Strength: %(rssi)u%% <i>(%(rssi_state)s)</i>\n<b>Link Quality: %(lq)u%%</b>\nTransmit Power Level: %(tpl)u%% <i>(%(tpl_state)s)</i>" ) % { "rssi_state": rssi_state, "rssi": rssi, "lq": lq, "tpl": tpl, "tpl_state": tpl_state }) elif path[1] == self.columns["rssi_pb"]: tooltip.set_markup( _("<b>Connected</b>\n<b>Received Signal Strength: %(rssi)u%%</b> <i>(%(rssi_state)s)</i>\nLink Quality: %(lq)u%%\nTransmit Power Level: %(tpl)u%% <i>(%(tpl_state)s)</i>" ) % { "rssi_state": rssi_state, "rssi": rssi, "lq": lq, "tpl": tpl, "tpl_state": tpl_state }) self.tooltip_row = path[0] self.tooltip_col = path[1] return True return False
def on_request_menu_items(self, manager_menu: ManagerDeviceMenu, device: Device) -> List[DeviceMenuItem]: items: List[DeviceMenuItem] = [] appl = AppletService() services = get_services(device) connectable_services = [ service for service in services if service.connectable ] for service in connectable_services: item: Gtk.MenuItem = create_menuitem(service.name, service.icon) if service.description: item.props.tooltip_text = service.description item.connect( "activate", lambda _item: manager_menu.connect_service( service.device, service.uuid)) items.append( DeviceMenuItem(item, DeviceMenuItem.Group.CONNECT, service.priority)) item.props.sensitive = service.available item.show() connected_services = [ service for service in services if service.connected_instances ] for service in connected_services: for instance in service.connected_instances: surface = self._make_x_icon(service.icon, 16) item = create_menuitem(instance.name, surface=surface) item.connect( "activate", lambda _item: manager_menu.disconnect_service( service.device, service.uuid, instance.port)) items.append( DeviceMenuItem(item, DeviceMenuItem.Group.DISCONNECT, service.priority + 100)) item.show() if services: config = AutoConnectConfig() autoconnect_services = set(config["services"]) for service in services: if service.connected_instances or ( device.get_object_path(), service.uuid) in autoconnect_services: item = Gtk.CheckMenuItem(label=service.name) config.bind_to_menuitem(item, device, service.uuid) item.show() items.append( DeviceMenuItem(item, DeviceMenuItem.Group.AUTOCONNECT, service.priority)) for action, priority in set((action, service.priority) for service in services for action in service.common_actions if any(plugin in appl.QueryPlugins() for plugin in action.plugins)): item = create_menuitem(action.title, action.icon) items.append( DeviceMenuItem(item, DeviceMenuItem.Group.ACTIONS, priority + 200)) item.show() item.connect("activate", self._get_activation_handler(action.callback)) return items