949 lines
32 KiB
Python
949 lines
32 KiB
Python
## Copyright (C) 2012-2013 Daniel Pavel
|
|
##
|
|
## This program is free software; you can redistribute it and/or modify
|
|
## it under the terms of the GNU General Public License as published by
|
|
## the Free Software Foundation; either version 2 of the License, or
|
|
## (at your option) any later version.
|
|
##
|
|
## This program is distributed in the hope that it will be useful,
|
|
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
## GNU General Public License for more details.
|
|
##
|
|
## You should have received a copy of the GNU General Public License along
|
|
## with this program; if not, write to the Free Software Foundation, Inc.,
|
|
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
|
|
import logging
|
|
|
|
import gi
|
|
|
|
from gi.repository.GObject import TYPE_PYOBJECT
|
|
from logitech_receiver import hidpp10_constants as _hidpp10_constants
|
|
from logitech_receiver.common import NamedInt as _NamedInt
|
|
from logitech_receiver.common import NamedInts as _NamedInts
|
|
|
|
from solaar import NAME
|
|
from solaar.i18n import _, ngettext
|
|
|
|
from . import action as _action
|
|
from . import config_panel as _config_panel
|
|
from . import icons as _icons
|
|
from .about import show_window as _show_about_window
|
|
from .common import ui_async as _ui_async
|
|
from .diversion_rules import show_window as _show_diversion_window
|
|
|
|
# from solaar import __version__ as VERSION
|
|
|
|
gi.require_version("Gdk", "3.0")
|
|
from gi.repository import Gdk, GLib, Gtk # NOQA: E402
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
#
|
|
# constants
|
|
#
|
|
|
|
_SMALL_BUTTON_ICON_SIZE = Gtk.IconSize.MENU
|
|
_NORMAL_BUTTON_ICON_SIZE = Gtk.IconSize.BUTTON
|
|
_TREE_ICON_SIZE = Gtk.IconSize.BUTTON
|
|
_INFO_ICON_SIZE = Gtk.IconSize.LARGE_TOOLBAR
|
|
_DEVICE_ICON_SIZE = Gtk.IconSize.DND
|
|
try:
|
|
gi.check_version("3.7.4")
|
|
_CAN_SET_ROW_NONE = None
|
|
except (ValueError, AttributeError):
|
|
_CAN_SET_ROW_NONE = ""
|
|
|
|
# tree model columns
|
|
_COLUMN = _NamedInts(PATH=0, NUMBER=1, ACTIVE=2, NAME=3, ICON=4, STATUS_TEXT=5, STATUS_ICON=6, DEVICE=7)
|
|
_COLUMN_TYPES = (str, int, bool, str, str, str, str, TYPE_PYOBJECT)
|
|
_TREE_SEPATATOR = (None, 0, False, None, None, None, None, None)
|
|
assert len(_TREE_SEPATATOR) == len(_COLUMN_TYPES)
|
|
assert len(_COLUMN_TYPES) == len(_COLUMN)
|
|
|
|
#
|
|
# create UI layout
|
|
#
|
|
|
|
|
|
def _new_button(label, icon_name=None, icon_size=_NORMAL_BUTTON_ICON_SIZE, tooltip=None, toggle=False, clicked=None):
|
|
b = Gtk.ToggleButton() if toggle else Gtk.Button()
|
|
c = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 5)
|
|
if icon_name:
|
|
c.pack_start(Gtk.Image.new_from_icon_name(icon_name, icon_size), True, True, 0)
|
|
if label:
|
|
c.pack_start(Gtk.Label(label), True, True, 0)
|
|
b.add(c)
|
|
if clicked is not None:
|
|
b.connect("clicked", clicked)
|
|
if tooltip:
|
|
b.set_tooltip_text(tooltip)
|
|
if not label and icon_size < _NORMAL_BUTTON_ICON_SIZE:
|
|
b.set_relief(Gtk.ReliefStyle.NONE)
|
|
b.set_focus_on_click(False)
|
|
return b
|
|
|
|
|
|
def _create_receiver_panel():
|
|
p = Gtk.Box.new(Gtk.Orientation.VERTICAL, 4)
|
|
|
|
p._count = Gtk.Label()
|
|
p._count.set_padding(24, 0)
|
|
p._count.set_alignment(0, 0.5)
|
|
p.pack_start(p._count, True, True, 0)
|
|
|
|
p._scanning = Gtk.Label(_("Scanning") + "...")
|
|
p._spinner = Gtk.Spinner()
|
|
|
|
bp = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 8)
|
|
bp.pack_start(Gtk.Label(" "), True, True, 0)
|
|
bp.pack_start(p._scanning, False, False, 0)
|
|
bp.pack_end(p._spinner, False, False, 0)
|
|
p.pack_end(bp, False, False, 0)
|
|
|
|
return p
|
|
|
|
|
|
def _create_device_panel():
|
|
p = Gtk.Box.new(Gtk.Orientation.VERTICAL, 4)
|
|
|
|
def _status_line(label_text):
|
|
b = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 8)
|
|
b.set_size_request(10, 28)
|
|
|
|
b._label = Gtk.Label(label_text)
|
|
b._label.set_alignment(0, 0.5)
|
|
b._label.set_size_request(170, 10)
|
|
b.pack_start(b._label, False, False, 0)
|
|
|
|
b._icon = Gtk.Image()
|
|
b.pack_start(b._icon, False, False, 0)
|
|
|
|
b._text = Gtk.Label()
|
|
b._text.set_alignment(0, 0.5)
|
|
b.pack_start(b._text, True, True, 0)
|
|
|
|
return b
|
|
|
|
p._battery = _status_line(_("Battery"))
|
|
p.pack_start(p._battery, False, False, 0)
|
|
|
|
p._secure = _status_line(_("Wireless Link"))
|
|
p._secure._icon.set_from_icon_name("dialog-warning", _INFO_ICON_SIZE)
|
|
p.pack_start(p._secure, False, False, 0)
|
|
|
|
p._lux = _status_line(_("Lighting"))
|
|
p.pack_start(p._lux, False, False, 0)
|
|
|
|
p.pack_start(Gtk.Separator.new(Gtk.Orientation.HORIZONTAL), False, False, 0) # spacer
|
|
|
|
p._config = _config_panel.create()
|
|
p.pack_end(p._config, True, True, 4)
|
|
|
|
return p
|
|
|
|
|
|
def _create_details_panel():
|
|
p = Gtk.Frame()
|
|
p.set_shadow_type(Gtk.ShadowType.NONE)
|
|
p.set_size_request(240, 0)
|
|
p.set_state_flags(Gtk.StateFlags.ACTIVE, True)
|
|
|
|
p._text = Gtk.Label()
|
|
p._text.set_padding(6, 4)
|
|
p._text.set_alignment(0, 0)
|
|
p._text.set_selectable(True)
|
|
p.add(p._text)
|
|
|
|
return p
|
|
|
|
|
|
def _create_buttons_box():
|
|
bb = Gtk.ButtonBox(Gtk.Orientation.HORIZONTAL)
|
|
bb.set_layout(Gtk.ButtonBoxStyle.END)
|
|
|
|
bb._details = _new_button(
|
|
None,
|
|
"dialog-information",
|
|
_SMALL_BUTTON_ICON_SIZE,
|
|
tooltip=_("Show Technical Details"),
|
|
toggle=True,
|
|
clicked=_update_details,
|
|
)
|
|
bb.add(bb._details)
|
|
bb.set_child_secondary(bb._details, True)
|
|
bb.set_child_non_homogeneous(bb._details, True)
|
|
|
|
def _pair_new_device(trigger):
|
|
assert _find_selected_device_id() is not None
|
|
receiver = _find_selected_device()
|
|
assert receiver is not None
|
|
assert bool(receiver)
|
|
assert receiver.kind is None
|
|
_action.pair(_window, receiver)
|
|
|
|
bb._pair = _new_button(_("Pair new device"), "list-add", clicked=_pair_new_device)
|
|
bb.add(bb._pair)
|
|
|
|
def _unpair_current_device(trigger):
|
|
assert _find_selected_device_id() is not None
|
|
device = _find_selected_device()
|
|
assert device is not None
|
|
assert device.kind is not None
|
|
_action.unpair(_window, device)
|
|
|
|
bb._unpair = _new_button(_("Unpair"), "edit-delete", clicked=_unpair_current_device)
|
|
bb.add(bb._unpair)
|
|
|
|
return bb
|
|
|
|
|
|
def _create_empty_panel():
|
|
p = Gtk.Label()
|
|
p.set_markup("<small>" + _("Select a device") + "</small>")
|
|
p.set_sensitive(False)
|
|
|
|
return p
|
|
|
|
|
|
def _create_info_panel():
|
|
p = Gtk.Box.new(Gtk.Orientation.VERTICAL, 4)
|
|
|
|
p._title = Gtk.Label(" ")
|
|
p._title.set_alignment(0, 0.5)
|
|
p._icon = Gtk.Image()
|
|
|
|
b1 = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 4)
|
|
b1.pack_start(p._title, True, True, 0)
|
|
b1.pack_start(p._icon, False, False, 0)
|
|
p.pack_start(b1, False, False, 0)
|
|
|
|
p.pack_start(Gtk.Separator.new(Gtk.Orientation.HORIZONTAL), False, False, 0) # spacer
|
|
|
|
p._receiver = _create_receiver_panel()
|
|
p.pack_start(p._receiver, True, True, 0)
|
|
|
|
p._device = _create_device_panel()
|
|
p.pack_start(p._device, True, True, 0)
|
|
|
|
p.pack_start(Gtk.Separator.new(Gtk.Orientation.HORIZONTAL), False, False, 0) # spacer
|
|
|
|
p._buttons = _create_buttons_box()
|
|
p.pack_end(p._buttons, False, False, 0)
|
|
|
|
return p
|
|
|
|
|
|
def _create_tree(model):
|
|
tree = Gtk.TreeView()
|
|
tree.set_size_request(330, 0) # enough width for simple setups
|
|
tree.set_headers_visible(False)
|
|
tree.set_show_expanders(False)
|
|
tree.set_level_indentation(20)
|
|
# tree.set_fixed_height_mode(True)
|
|
tree.set_enable_tree_lines(True)
|
|
tree.set_reorderable(False)
|
|
tree.set_enable_search(False)
|
|
tree.set_model(model)
|
|
|
|
def _is_separator(model, item, _ignore=None):
|
|
return model.get_value(item, _COLUMN.PATH) is None
|
|
|
|
tree.set_row_separator_func(_is_separator, None)
|
|
|
|
icon_cell_renderer = Gtk.CellRendererPixbuf()
|
|
icon_cell_renderer.set_property("stock-size", _TREE_ICON_SIZE)
|
|
icon_column = Gtk.TreeViewColumn("Icon", icon_cell_renderer)
|
|
icon_column.add_attribute(icon_cell_renderer, "sensitive", _COLUMN.ACTIVE)
|
|
icon_column.add_attribute(icon_cell_renderer, "icon-name", _COLUMN.ICON)
|
|
tree.append_column(icon_column)
|
|
|
|
name_cell_renderer = Gtk.CellRendererText()
|
|
name_column = Gtk.TreeViewColumn("device name", name_cell_renderer)
|
|
name_column.add_attribute(name_cell_renderer, "sensitive", _COLUMN.ACTIVE)
|
|
name_column.add_attribute(name_cell_renderer, "text", _COLUMN.NAME)
|
|
name_column.set_expand(True)
|
|
tree.append_column(name_column)
|
|
tree.set_expander_column(name_column)
|
|
|
|
status_cell_renderer = Gtk.CellRendererText()
|
|
status_cell_renderer.set_property("scale", 0.85)
|
|
status_cell_renderer.set_property("xalign", 1)
|
|
status_column = Gtk.TreeViewColumn("status text", status_cell_renderer)
|
|
status_column.add_attribute(status_cell_renderer, "sensitive", _COLUMN.ACTIVE)
|
|
status_column.add_attribute(status_cell_renderer, "text", _COLUMN.STATUS_TEXT)
|
|
status_column.set_expand(True)
|
|
tree.append_column(status_column)
|
|
|
|
battery_cell_renderer = Gtk.CellRendererPixbuf()
|
|
battery_cell_renderer.set_property("stock-size", _TREE_ICON_SIZE)
|
|
battery_column = Gtk.TreeViewColumn("status icon", battery_cell_renderer)
|
|
battery_column.add_attribute(battery_cell_renderer, "sensitive", _COLUMN.ACTIVE)
|
|
battery_column.add_attribute(battery_cell_renderer, "icon-name", _COLUMN.STATUS_ICON)
|
|
tree.append_column(battery_column)
|
|
|
|
return tree
|
|
|
|
|
|
def _create_window_layout():
|
|
assert _tree is not None
|
|
assert _details is not None
|
|
assert _info is not None
|
|
assert _empty is not None
|
|
|
|
assert _tree.get_selection().get_mode() == Gtk.SelectionMode.SINGLE
|
|
_tree.get_selection().connect("changed", _device_selected)
|
|
|
|
tree_scroll = Gtk.ScrolledWindow()
|
|
tree_scroll.add(_tree)
|
|
tree_scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
|
|
tree_scroll.set_shadow_type(Gtk.ShadowType.IN)
|
|
|
|
tree_panel = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0)
|
|
tree_panel.set_homogeneous(False)
|
|
tree_panel.pack_start(tree_scroll, True, True, 0)
|
|
tree_panel.pack_start(_details, False, False, 0)
|
|
|
|
panel = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 16)
|
|
panel.pack_start(tree_panel, False, False, 0)
|
|
panel.pack_start(_info, True, True, 0)
|
|
panel.pack_start(_empty, True, True, 0)
|
|
|
|
bottom_buttons_box = Gtk.ButtonBox(Gtk.Orientation.HORIZONTAL)
|
|
bottom_buttons_box.set_layout(Gtk.ButtonBoxStyle.START)
|
|
bottom_buttons_box.set_spacing(20)
|
|
quit_button = _new_button(_("Quit %s") % NAME, "application-exit", _SMALL_BUTTON_ICON_SIZE, clicked=destroy)
|
|
bottom_buttons_box.add(quit_button)
|
|
about_button = _new_button(_("About %s") % NAME, "help-about", _SMALL_BUTTON_ICON_SIZE, clicked=_show_about_window)
|
|
bottom_buttons_box.add(about_button)
|
|
diversion_button = _new_button(
|
|
_("Rule Editor"), "", _SMALL_BUTTON_ICON_SIZE, clicked=lambda *_trigger: _show_diversion_window(_model)
|
|
)
|
|
bottom_buttons_box.add(diversion_button)
|
|
bottom_buttons_box.set_child_secondary(diversion_button, True)
|
|
|
|
# solaar_version = Gtk.Label()
|
|
# solaar_version.set_markup('<small>' + NAME + ' v' + VERSION + '</small>')
|
|
# bottom_buttons_box.add(solaar_version)
|
|
# bottom_buttons_box.set_child_secondary(solaar_version, True)
|
|
|
|
vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 8)
|
|
vbox.set_border_width(8)
|
|
vbox.pack_start(panel, True, True, 0)
|
|
vbox.pack_end(bottom_buttons_box, False, False, 0)
|
|
vbox.show_all()
|
|
|
|
_details.set_visible(False)
|
|
_info.set_visible(False)
|
|
return vbox
|
|
|
|
|
|
def _create(delete_action):
|
|
window = Gtk.Window()
|
|
window.set_title(NAME)
|
|
window.set_role("status-window")
|
|
|
|
# window.set_type_hint(Gdk.WindowTypeHint.UTILITY)
|
|
# window.set_skip_taskbar_hint(True)
|
|
# window.set_skip_pager_hint(True)
|
|
window.connect("delete-event", delete_action)
|
|
|
|
vbox = _create_window_layout()
|
|
window.add(vbox)
|
|
|
|
geometry = Gdk.Geometry()
|
|
geometry.min_width = 600
|
|
geometry.min_height = 320
|
|
window.set_geometry_hints(vbox, geometry, Gdk.WindowHints.MIN_SIZE)
|
|
window.set_position(Gtk.WindowPosition.CENTER)
|
|
|
|
style = window.get_style_context()
|
|
style.add_class("solaar")
|
|
|
|
return window
|
|
|
|
|
|
#
|
|
# window updates
|
|
#
|
|
|
|
|
|
def _find_selected_device():
|
|
selection = _tree.get_selection()
|
|
model, item = selection.get_selected()
|
|
return model.get_value(item, _COLUMN.DEVICE) if item else None
|
|
|
|
|
|
def _find_selected_device_id():
|
|
selection = _tree.get_selection()
|
|
model, item = selection.get_selected()
|
|
if item:
|
|
return _model.get_value(item, _COLUMN.PATH), _model.get_value(item, _COLUMN.NUMBER)
|
|
|
|
|
|
# triggered by changing selection in the tree
|
|
def _device_selected(selection):
|
|
model, item = selection.get_selected()
|
|
device = model.get_value(item, _COLUMN.DEVICE) if item else None
|
|
# if logger.isEnabledFor(logging.DEBUG):
|
|
# logger.debug("window tree selected device %s", device)
|
|
if device:
|
|
_update_info_panel(device, full=True)
|
|
else:
|
|
# When removing a receiver, one of its children may get automatically selected
|
|
# before the tree had time to remove them as well.
|
|
# Rather than chase around for another device to select, just clear the selection.
|
|
_tree.get_selection().unselect_all()
|
|
_update_info_panel(None, full=True)
|
|
|
|
|
|
def _receiver_row(receiver_path, receiver=None):
|
|
assert receiver_path
|
|
r = _model.get_iter_first()
|
|
while r:
|
|
r = _model.iter_next(r)
|
|
|
|
item = _model.get_iter_first()
|
|
while item:
|
|
# first row matching the path must be the receiver one
|
|
if _model.get_value(item, _COLUMN.PATH) == receiver_path:
|
|
return item
|
|
item = _model.iter_next(item)
|
|
|
|
if not item and receiver:
|
|
icon_name = _icons.device_icon_name(receiver.name)
|
|
status_text = None
|
|
status_icon = None
|
|
row_data = (receiver_path, 0, True, receiver.name, icon_name, status_text, status_icon, receiver)
|
|
assert len(row_data) == len(_TREE_SEPATATOR)
|
|
if logger.isEnabledFor(logging.DEBUG):
|
|
logger.debug("new receiver row %s", row_data)
|
|
item = _model.append(None, row_data)
|
|
if _TREE_SEPATATOR:
|
|
_model.append(None, _TREE_SEPATATOR)
|
|
|
|
return item or None
|
|
|
|
|
|
def _device_row(receiver_path, device_number, device=None):
|
|
assert receiver_path
|
|
assert device_number is not None
|
|
|
|
receiver_row = _receiver_row(receiver_path, None if device is None else device.receiver)
|
|
|
|
if device_number == 0xFF or device_number == 0x0: # direct-connected device, receiver row is device row
|
|
if receiver_row:
|
|
return receiver_row
|
|
item = None
|
|
new_child_index = 0
|
|
else:
|
|
item = _model.iter_children(receiver_row)
|
|
new_child_index = 0
|
|
while item:
|
|
if _model.get_value(item, _COLUMN.PATH) != receiver_path:
|
|
logger.warning(
|
|
"path for device row %s different from path for receiver %s",
|
|
_model.get_value(item, _COLUMN.PATH),
|
|
receiver_path,
|
|
)
|
|
item_number = _model.get_value(item, _COLUMN.NUMBER)
|
|
if item_number == device_number:
|
|
return item
|
|
if item_number > device_number:
|
|
item = None
|
|
break
|
|
new_child_index += 1
|
|
item = _model.iter_next(item)
|
|
|
|
if not item and device:
|
|
icon_name = _icons.device_icon_name(device.name, device.kind)
|
|
status_text = None
|
|
status_icon = None
|
|
row_data = (
|
|
receiver_path,
|
|
device_number,
|
|
bool(device.online),
|
|
device.codename,
|
|
icon_name,
|
|
status_text,
|
|
status_icon,
|
|
device,
|
|
)
|
|
assert len(row_data) == len(_TREE_SEPATATOR)
|
|
if logger.isEnabledFor(logging.DEBUG):
|
|
logger.debug("new device row %s at index %d", row_data, new_child_index)
|
|
item = _model.insert(receiver_row, new_child_index, row_data)
|
|
|
|
return item or None
|
|
|
|
|
|
#
|
|
#
|
|
#
|
|
|
|
|
|
def select(receiver_path, device_number=None):
|
|
assert _window
|
|
assert receiver_path is not None
|
|
if device_number is None:
|
|
item = _receiver_row(receiver_path)
|
|
else:
|
|
item = _device_row(receiver_path, device_number)
|
|
if item:
|
|
selection = _tree.get_selection()
|
|
selection.select_iter(item)
|
|
else:
|
|
logger.warning("select(%s, %s) failed to find an item", receiver_path, device_number)
|
|
|
|
|
|
def _hide(w, _ignore=None):
|
|
assert w == _window
|
|
# some window managers move the window to 0,0 after hide()
|
|
# so try to remember the last position
|
|
position = _window.get_position()
|
|
_window.hide()
|
|
_window.move(*position)
|
|
return True
|
|
|
|
|
|
def popup(trigger=None, receiver_path=None, device_id=None):
|
|
if receiver_path:
|
|
select(receiver_path, device_id)
|
|
_window.present()
|
|
return True
|
|
|
|
|
|
def toggle(trigger=None):
|
|
if _window.get_visible():
|
|
_hide(_window)
|
|
else:
|
|
_window.present()
|
|
|
|
|
|
#
|
|
#
|
|
#
|
|
|
|
|
|
def _update_details(button):
|
|
assert button
|
|
visible = button.get_active()
|
|
|
|
if visible:
|
|
# _details._text.set_markup('<small>reading...</small>')
|
|
|
|
def _details_items(device, read_all=False):
|
|
# If read_all is False, only return stuff that is ~100% already
|
|
# cached, and involves no HID++ calls.
|
|
|
|
yield (_("Path"), device.path)
|
|
if device.kind is None:
|
|
# 046d is the Logitech vendor id
|
|
yield (_("USB ID"), "046d:" + device.product_id)
|
|
|
|
if read_all:
|
|
yield (_("Serial"), device.serial)
|
|
else:
|
|
yield (_("Serial"), "...")
|
|
|
|
else:
|
|
# yield ('Codename', device.codename)
|
|
yield (_("Index"), device.number)
|
|
if device.wpid:
|
|
yield (_("Wireless PID"), device.wpid)
|
|
if device.product_id:
|
|
yield (_("Product ID"), "046d:" + device.product_id)
|
|
hid_version = device.protocol
|
|
yield (_("Protocol"), f"HID++ {hid_version:1.1f}" if hid_version else _("Unknown"))
|
|
if read_all and device.polling_rate:
|
|
yield (_("Polling rate"), device.polling_rate)
|
|
|
|
if read_all or not device.online:
|
|
yield (_("Serial"), device.serial)
|
|
else:
|
|
yield (_("Serial"), "...")
|
|
if read_all and device.unitId and device.unitId != device.serial:
|
|
yield (_("Unit ID"), device.unitId)
|
|
|
|
if read_all:
|
|
if device.firmware:
|
|
for fw in list(device.firmware):
|
|
yield (" " + _(str(fw.kind)), (fw.name + " " + fw.version).strip())
|
|
elif device.kind is None or device.online:
|
|
yield (f" {_('Firmware')}", "...")
|
|
|
|
flag_bits = device.notification_flags
|
|
if flag_bits is not None:
|
|
flag_names = (
|
|
(f"({_('none')})",) if flag_bits == 0 else _hidpp10_constants.NOTIFICATION_FLAG.flag_names(flag_bits)
|
|
)
|
|
yield (_("Notifications"), ("\n%15s" % " ").join(flag_names))
|
|
|
|
def _set_details(text):
|
|
_details._text.set_markup(text)
|
|
|
|
def _make_text(items):
|
|
text = "\n".join("%-13s: %s" % (name, value) for name, value in items)
|
|
return "<small><tt>" + text + "</tt></small>"
|
|
|
|
def _displayable_items(items):
|
|
for name, value in items:
|
|
value = GLib.markup_escape_text(str(value).replace("\x00", "")).strip()
|
|
if value:
|
|
yield name, value
|
|
|
|
def _read_slow(device):
|
|
items = _details_items(selected_device, True)
|
|
items = _displayable_items(items)
|
|
text = _make_text(items)
|
|
if device == _details._current_device:
|
|
GLib.idle_add(_set_details, text)
|
|
|
|
selected_device = _find_selected_device()
|
|
assert selected_device
|
|
_details._current_device = selected_device
|
|
|
|
read_all = not (selected_device.kind is None or selected_device.online)
|
|
items = _details_items(selected_device, read_all)
|
|
_set_details(_make_text(items))
|
|
|
|
if read_all:
|
|
_details._current_device = None
|
|
else:
|
|
_ui_async(_read_slow, selected_device)
|
|
|
|
_details.set_visible(visible)
|
|
|
|
|
|
def _update_receiver_panel(receiver, panel, buttons, full=False):
|
|
assert receiver
|
|
|
|
devices_count = len(receiver)
|
|
|
|
paired_text = (
|
|
_(_("No device paired."))
|
|
if devices_count == 0
|
|
else ngettext("%(count)s paired device.", "%(count)s paired devices.", devices_count) % {"count": devices_count}
|
|
)
|
|
|
|
if receiver.max_devices > 0:
|
|
paired_text += (
|
|
"\n\n<small>%s</small>"
|
|
% ngettext(
|
|
"Up to %(max_count)s device can be paired to this receiver.",
|
|
"Up to %(max_count)s devices can be paired to this receiver.",
|
|
receiver.max_devices,
|
|
)
|
|
% {"max_count": receiver.max_devices}
|
|
)
|
|
elif devices_count > 0:
|
|
paired_text += f"\n\n<small>{_('Only one device can be paired to this receiver.')}</small>"
|
|
pairings = receiver.remaining_pairings()
|
|
if pairings is not None and pairings >= 0:
|
|
paired_text += "\n<small>%s</small>" % (
|
|
ngettext("This receiver has %d pairing remaining.", "This receiver has %d pairings remaining.", pairings)
|
|
% pairings
|
|
)
|
|
|
|
panel._count.set_markup(paired_text)
|
|
|
|
is_pairing = receiver.pairing.lock_open
|
|
if is_pairing:
|
|
panel._scanning.set_visible(True)
|
|
if not panel._spinner.get_visible():
|
|
panel._spinner.start()
|
|
panel._spinner.set_visible(True)
|
|
else:
|
|
panel._scanning.set_visible(False)
|
|
if panel._spinner.get_visible():
|
|
panel._spinner.stop()
|
|
panel._spinner.set_visible(False)
|
|
|
|
panel.set_visible(True)
|
|
|
|
# b._insecure.set_visible(False)
|
|
buttons._unpair.set_visible(False)
|
|
|
|
if (
|
|
not is_pairing
|
|
and (receiver.remaining_pairings() is None or receiver.remaining_pairings() != 0)
|
|
and (receiver.re_pairs or devices_count < receiver.max_devices)
|
|
):
|
|
buttons._pair.set_sensitive(True)
|
|
else:
|
|
buttons._pair.set_sensitive(False)
|
|
|
|
buttons._pair.set_visible(True)
|
|
|
|
|
|
def _update_device_panel(device, panel, buttons, full=False):
|
|
assert device
|
|
is_online = bool(device.online)
|
|
panel.set_sensitive(is_online)
|
|
|
|
if device.battery_info is None or device.battery_info.level is None:
|
|
device.read_battery()
|
|
|
|
battery_level = device.battery_info.level if device.battery_info is not None else None
|
|
battery_voltage = device.battery_info.voltage if device.battery_info is not None else None
|
|
if battery_level is None and battery_voltage is None:
|
|
panel._battery.set_visible(False)
|
|
else:
|
|
panel._battery.set_visible(True)
|
|
battery_next_level = device.battery_info.next_level
|
|
charging = device.battery_info.charging() if device.battery_info is not None else None
|
|
icon_name = _icons.battery(battery_level, charging)
|
|
panel._battery._icon.set_from_icon_name(icon_name, _INFO_ICON_SIZE)
|
|
panel._battery._icon.set_sensitive(True)
|
|
panel._battery._text.set_sensitive(is_online)
|
|
|
|
if battery_voltage is not None:
|
|
panel._battery._label.set_text(_("Battery Voltage"))
|
|
text = "%dmV" % battery_voltage
|
|
tooltip_text = _("Voltage reported by battery")
|
|
else:
|
|
panel._battery._label.set_text(_("Battery Level"))
|
|
text = ""
|
|
tooltip_text = _("Approximate level reported by battery")
|
|
if battery_voltage is not None and battery_level is not None:
|
|
text += ", "
|
|
if battery_level is not None:
|
|
text += _(str(battery_level)) if isinstance(battery_level, _NamedInt) else "%d%%" % battery_level
|
|
if battery_next_level is not None and not charging:
|
|
if isinstance(battery_next_level, _NamedInt):
|
|
text += "<small> (" + _("next reported ") + _(str(battery_next_level)) + ")</small>"
|
|
else:
|
|
text += "<small> (" + _("next reported ") + ("%d%%" % battery_next_level) + ")</small>"
|
|
tooltip_text = tooltip_text + _(" and next level to be reported.")
|
|
if is_online:
|
|
if charging:
|
|
text += f" <small>({_('charging')})</small>"
|
|
else:
|
|
text += f" <small>({_('last known')})</small>"
|
|
|
|
panel._battery._text.set_markup(text)
|
|
panel._battery.set_tooltip_text(tooltip_text)
|
|
|
|
if device.link_encrypted is None:
|
|
panel._secure.set_visible(False)
|
|
elif is_online:
|
|
panel._secure.set_visible(True)
|
|
panel._secure._icon.set_visible(True)
|
|
if device.link_encrypted is True:
|
|
panel._secure._text.set_text(_("encrypted"))
|
|
panel._secure._icon.set_from_icon_name("security-high", _INFO_ICON_SIZE)
|
|
panel._secure.set_tooltip_text(_("The wireless link between this device and its receiver is encrypted."))
|
|
else:
|
|
panel._secure._text.set_text(_("not encrypted"))
|
|
panel._secure._icon.set_from_icon_name("security-low", _INFO_ICON_SIZE)
|
|
panel._secure.set_tooltip_text(
|
|
_(
|
|
"The wireless link between this device and its receiver is not encrypted.\n"
|
|
"This is a security issue for pointing devices, and a major security issue for text-input devices."
|
|
)
|
|
)
|
|
else:
|
|
panel._secure.set_visible(True)
|
|
panel._secure._icon.set_visible(False)
|
|
panel._secure._text.set_markup(f"<small>{_('offline')}</small>")
|
|
panel._secure.set_tooltip_text("")
|
|
|
|
if is_online:
|
|
light_level = device.battery_info.light_level if device.battery_info is not None else None
|
|
if light_level is None:
|
|
panel._lux.set_visible(False)
|
|
else:
|
|
panel._lux._icon.set_from_icon_name(_icons.lux(light_level), _INFO_ICON_SIZE)
|
|
panel._lux._text.set_text(_("%(light_level)d lux") % {"light_level": light_level})
|
|
panel._lux.set_visible(True)
|
|
else:
|
|
panel._lux.set_visible(False)
|
|
|
|
buttons._pair.set_visible(False)
|
|
buttons._unpair.set_sensitive(device.receiver.may_unpair if device.receiver else False)
|
|
buttons._unpair.set_visible(True)
|
|
|
|
panel.set_visible(True)
|
|
|
|
if full:
|
|
_config_panel.update(device, is_online)
|
|
|
|
|
|
def _update_info_panel(device, full=False):
|
|
if device is None:
|
|
# no selected device, show the 'empty' panel
|
|
_details.set_visible(False)
|
|
_info.set_visible(False)
|
|
_empty.set_visible(True)
|
|
return
|
|
|
|
# a receiver must be valid
|
|
# a device must be paired
|
|
assert device
|
|
|
|
_info._title.set_markup(f"<b>{device.name}</b>")
|
|
icon_name = _icons.device_icon_name(device.name, device.kind)
|
|
_info._icon.set_from_icon_name(icon_name, _DEVICE_ICON_SIZE)
|
|
|
|
if device.kind is None:
|
|
_info._device.set_visible(False)
|
|
_info._icon.set_sensitive(True)
|
|
_info._title.set_sensitive(True)
|
|
_update_receiver_panel(device, _info._receiver, _info._buttons, full)
|
|
else:
|
|
_info._receiver.set_visible(False)
|
|
is_online = bool(device.online)
|
|
_info._icon.set_sensitive(is_online)
|
|
_info._title.set_sensitive(is_online)
|
|
_update_device_panel(device, _info._device, _info._buttons, full)
|
|
|
|
_empty.set_visible(False)
|
|
_info.set_visible(True)
|
|
|
|
if full:
|
|
_update_details(_info._buttons._details)
|
|
|
|
|
|
#
|
|
# window layout:
|
|
# +--------------------------------+
|
|
# | tree | receiver | empty |
|
|
# | | or device | |
|
|
# |------------| status | |
|
|
# | details | | |
|
|
# |--------------------------------|
|
|
# | (about) |
|
|
# +--------------------------------|
|
|
# either the status or empty panel is visible at any point
|
|
# the details panel can be toggle on/off
|
|
|
|
_model = None
|
|
_tree = None
|
|
_details = None
|
|
_info = None
|
|
_empty = None
|
|
_window = None
|
|
|
|
|
|
def init(show_window, hide_on_close):
|
|
Gtk.Window.set_default_icon_name(NAME.lower())
|
|
|
|
global _model, _tree, _details, _info, _empty, _window
|
|
_model = Gtk.TreeStore(*_COLUMN_TYPES)
|
|
_tree = _create_tree(_model)
|
|
_details = _create_details_panel()
|
|
_info = _create_info_panel()
|
|
_empty = _create_empty_panel()
|
|
_window = _create(_hide if hide_on_close else destroy)
|
|
if show_window:
|
|
_window.present()
|
|
|
|
|
|
def destroy(_ignore1=None, _ignore2=None):
|
|
global _model, _tree, _details, _info, _empty, _window
|
|
w, _window = _window, None
|
|
w.destroy()
|
|
w = None
|
|
_config_panel.destroy()
|
|
|
|
_empty = None
|
|
_info = None
|
|
_details = None
|
|
_tree = None
|
|
_model = None
|
|
|
|
|
|
def update(device, need_popup=False, refresh=False):
|
|
if _window is None:
|
|
return
|
|
|
|
assert device is not None
|
|
|
|
if need_popup:
|
|
popup()
|
|
|
|
selected_device_id = _find_selected_device_id()
|
|
|
|
if device.kind is None: # receiver
|
|
# receiver
|
|
is_alive = bool(device)
|
|
item = _receiver_row(device.path, device if is_alive else None)
|
|
|
|
if is_alive and item:
|
|
was_pairing = bool(_model.get_value(item, _COLUMN.STATUS_ICON))
|
|
is_pairing = (not device.isDevice) and bool(device.pairing.lock_open)
|
|
_model.set_value(item, _COLUMN.STATUS_ICON, "network-wireless" if is_pairing else _CAN_SET_ROW_NONE)
|
|
|
|
if selected_device_id == (device.path, 0):
|
|
full_update = need_popup or was_pairing != is_pairing
|
|
_update_info_panel(device, full=full_update)
|
|
|
|
elif item:
|
|
if _TREE_SEPATATOR:
|
|
separator = _model.iter_next(item)
|
|
if separator:
|
|
_model.remove(separator)
|
|
_model.remove(item)
|
|
|
|
else:
|
|
path = device.receiver.path if device.receiver is not None else device.path
|
|
assert device.number is not None and device.number >= 0, "invalid device number" + str(device.number)
|
|
item = _device_row(path, device.number, device if bool(device) else None)
|
|
|
|
if bool(device) and item:
|
|
update_device(device, item, selected_device_id, need_popup, full=refresh)
|
|
elif item:
|
|
_model.remove(item)
|
|
_config_panel.clean(device)
|
|
|
|
# make sure all rows are visible
|
|
_tree.expand_all()
|
|
|
|
|
|
def update_device(device, item, selected_device_id, need_popup, full=False):
|
|
was_online = _model.get_value(item, _COLUMN.ACTIVE)
|
|
is_online = bool(device.online)
|
|
_model.set_value(item, _COLUMN.ACTIVE, is_online)
|
|
|
|
battery_level = device.battery_info.level if device.battery_info is not None else None
|
|
battery_voltage = device.battery_info.voltage if device.battery_info is not None else None
|
|
if battery_level is None:
|
|
_model.set_value(item, _COLUMN.STATUS_TEXT, _CAN_SET_ROW_NONE)
|
|
_model.set_value(item, _COLUMN.STATUS_ICON, _CAN_SET_ROW_NONE)
|
|
else:
|
|
if battery_voltage is not None and False: # Use levels instead of voltage here
|
|
status_text = "%(battery_voltage)dmV" % {"battery_voltage": battery_voltage}
|
|
elif isinstance(battery_level, _NamedInt):
|
|
status_text = _(str(battery_level))
|
|
else:
|
|
status_text = "%(battery_percent)d%%" % {"battery_percent": battery_level}
|
|
_model.set_value(item, _COLUMN.STATUS_TEXT, status_text)
|
|
|
|
charging = device.battery_info.charging() if device.battery_info is not None else None
|
|
icon_name = _icons.battery(battery_level, charging)
|
|
_model.set_value(item, _COLUMN.STATUS_ICON, icon_name)
|
|
|
|
_model.set_value(item, _COLUMN.NAME, device.codename)
|
|
|
|
if selected_device_id is None or need_popup:
|
|
select(device.receiver.path if device.receiver else device.path, device.number)
|
|
elif selected_device_id == (device.receiver.path if device.receiver else device.path, device.number):
|
|
full_update = full or was_online != is_online
|
|
_update_info_panel(device, full=full_update)
|
|
|
|
|
|
def find_device(serial):
|
|
assert serial, "need serial number or unit ID to find a device"
|
|
result = None
|
|
|
|
def check(_store, _treepath, row):
|
|
nonlocal result
|
|
device = _model.get_value(row, _COLUMN.DEVICE)
|
|
if device and device.kind and (device.unitId == serial or device.serial == serial):
|
|
result = device
|
|
return True
|
|
|
|
_model.foreach(check)
|
|
return result
|