diff --git a/lib/solaar/gtk.py b/lib/solaar/gtk.py
index 2ae9b694..66539694 100644
--- a/lib/solaar/gtk.py
+++ b/lib/solaar/gtk.py
@@ -42,13 +42,13 @@ def _parse_arguments():
def _run(args):
import solaar.ui as ui
- ui.init()
-
import solaar.listener as listener
listener.start_scanner(ui.status_changed, ui.error_dialog)
# main UI event loop
+ ui.init()
ui.run_loop()
+ ui.destroy()
listener.stop_all()
diff --git a/lib/solaar/ui/__init__.py b/lib/solaar/ui/__init__.py
index 85c2d406..7def7b3a 100644
--- a/lib/solaar/ui/__init__.py
+++ b/lib/solaar/ui/__init__.py
@@ -20,64 +20,62 @@ def _error_dialog(reason, object):
'\n'
'If you\'ve just installed Solaar, try removing the receiver\n'
'and plugging it back in.' % object)
+ elif reason == 'unpair':
+ title = 'Unpairing failed'
+ text = ('Failed to unpair device\n%s .' % object)
else:
raise Exception("ui.error_dialog: don't know how to handle (%s, %s)", reason, object)
+ assert text
+ assert text
+
m = Gtk.MessageDialog(None, Gtk.DialogFlags.MODAL, Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE, text)
m.set_title(title)
m.run()
m.destroy()
+
def error_dialog(reason, object):
+ assert reason is not None
GLib.idle_add(_error_dialog, reason, object)
#
#
#
-_tray_icon = None
+
+from . import notify, tray, window
def init():
notify.init()
-
- global _tray_icon
- _tray_icon = status_icon.create(main_window.toggle_all, main_window.popup)
- assert _tray_icon
+ tray.init()
+ window.init()
def run_loop():
- global _tray_icon
Gtk.main()
- t, _tray_icon = _tray_icon, None
- status_icon.destroy(t)
+
+def destroy():
+ tray.destroy()
+ window.destroy()
notify.uninit()
+
from logitech.unifying_receiver.status import ALERT
def _status_changed(device, alert, reason):
assert device is not None
if _log.isEnabledFor(_DEBUG):
_log.debug("status changed: %s, %s, %s", device, alert, reason)
- status_icon.update(_tray_icon, device)
+ tray.update(device)
if alert & ALERT.ATTENTION:
- status_icon.attention(_tray_icon, reason)
+ tray.attention(reason)
need_popup = alert & (ALERT.SHOW_WINDOW | ALERT.ATTENTION)
- main_window.update(device, need_popup, _tray_icon)
+ window.update(device, need_popup)
if alert & ALERT.NOTIFICATION:
notify.show(device, reason)
+
def status_changed(device, alert=ALERT.NONE, reason=None):
GLib.idle_add(_status_changed, device, alert, reason)
-
-#
-#
-#
-
-from . import status_icon
-from . import notify, main_window
-
-from . import icons
-# for some reason, set_icon_name does not always work on windows
-Gtk.Window.set_default_icon_name(main_window.NAME.lower())
-Gtk.Window.set_default_icon_from_file(icons.icon_file(main_window.NAME.lower(), 32))
diff --git a/lib/solaar/ui/about.py b/lib/solaar/ui/about.py
index 64b4beb6..fd783cae 100644
--- a/lib/solaar/ui/about.py
+++ b/lib/solaar/ui/about.py
@@ -25,6 +25,7 @@ def _create():
about.set_authors(('Daniel Pavel http://github.com/pwr',))
try:
+ about.add_credit_section('GUI design', ('Julien Gascard',))
about.add_credit_section('Testing', (
'Douglas Wagner',
'Julien Gascard',
@@ -56,7 +57,7 @@ def _create():
return about
-def show_window(_):
+def show_window(trigger=None):
global _dialog
if _dialog is None:
_dialog = _create()
diff --git a/lib/solaar/ui/action.py b/lib/solaar/ui/action.py
index b99683fd..4ce6e89f 100644
--- a/lib/solaar/ui/action.py
+++ b/lib/solaar/ui/action.py
@@ -43,10 +43,11 @@ about = make('help-about', 'About ' + NAME, _show_about_window)
#
from . import pair_window
-def _pair_device(action, frame):
- window = frame.get_toplevel()
+def pair(window, receiver):
+ assert receiver is not None
+ assert receiver.kind is None
- pair_dialog = pair_window.create(action, frame._device)
+ pair_dialog = pair_window.create(receiver)
pair_dialog.set_transient_for(window)
pair_dialog.set_destroy_with_parent(True)
pair_dialog.set_modal(True)
@@ -54,14 +55,12 @@ def _pair_device(action, frame):
pair_dialog.set_position(Gtk.WindowPosition.CENTER)
pair_dialog.present()
-def pair(frame):
- return make('list-add', 'Pair new device', _pair_device, frame)
-
from ..ui import error_dialog
-def _unpair_device(action, frame):
- window = frame.get_toplevel()
- device = frame._device
+def unpair(window, device):
+ assert device is not None
+ assert device.kind is not None
+
qdialog = Gtk.MessageDialog(window, 0,
Gtk.MessageType.QUESTION, Gtk.ButtonsType.NONE,
"Unpair device\n%s ?" % device.name)
@@ -71,10 +70,11 @@ def _unpair_device(action, frame):
choice = qdialog.run()
qdialog.destroy()
if choice == Gtk.ResponseType.ACCEPT:
- try:
- del device.receiver[device.number]
- except:
- error_dialog(window, 'Unpairing failed', 'Failed to unpair device\n%s .' % device.name)
+ receiver = device.receiver
+ assert receiver
+ device_number = device.number
-def unpair(frame):
- return make('edit-delete', 'Unpair', _unpair_device, frame)
+ try:
+ del receiver[device_number]
+ except:
+ error_dialog('unpair', device)
diff --git a/lib/solaar/ui/config_panel.py b/lib/solaar/ui/config_panel.py
index d3950e68..61dd6176 100644
--- a/lib/solaar/ui/config_panel.py
+++ b/lib/solaar/ui/config_panel.py
@@ -81,54 +81,53 @@ def _combo_notify(cbbox, setting, spinner):
# return True
-def _add_settings(box, device):
- for s in device.settings:
- sbox = Gtk.HBox(homogeneous=False, spacing=8)
- sbox.pack_start(Gtk.Label(s.label), False, False, 0)
+def _create_sbox(s):
+ sbox = Gtk.HBox(homogeneous=False, spacing=8)
+ sbox.pack_start(Gtk.Label(s.label), False, False, 0)
- spinner = Gtk.Spinner()
- spinner.set_tooltip_text('Working...')
+ spinner = Gtk.Spinner()
+ spinner.set_tooltip_text('Working...')
- failed = Gtk.Image.new_from_icon_name('dialog-warning', Gtk.IconSize.SMALL_TOOLBAR)
- failed.set_tooltip_text('Failed to read value from the device.')
+ failed = Gtk.Image.new_from_icon_name('dialog-warning', Gtk.IconSize.SMALL_TOOLBAR)
+ failed.set_tooltip_text('Failed to read value from the device.')
- if s.kind == _settings.KIND.toggle:
- control = Gtk.Switch()
- control.connect('notify::active', _switch_notify, s, spinner)
- elif s.kind == _settings.KIND.choice:
- control = Gtk.ComboBoxText()
- for entry in s.choices:
- control.append(str(entry), str(entry))
- control.connect('changed', _combo_notify, s, spinner)
- # elif s.kind == _settings.KIND.range:
- # first, second = s.choices[:2]
- # last = s.choices[-1:][0]
- # control = Gtk.HScale.new_with_range(first, last, second - first)
- # control.set_draw_value(False)
- # control.set_has_origin(False)
- # for entry in s.choices:
- # control.add_mark(int(entry), Gtk.PositionType.TOP, str(entry))
- # control.connect('change-value', _snap_to_markers, s)
- # control.connect('value-changed', _scale_notify, s, spinner)
- else:
- raise NotImplemented
+ if s.kind == _settings.KIND.toggle:
+ control = Gtk.Switch()
+ control.connect('notify::active', _switch_notify, s, spinner)
+ elif s.kind == _settings.KIND.choice:
+ control = Gtk.ComboBoxText()
+ for entry in s.choices:
+ control.append(str(entry), str(entry))
+ control.connect('changed', _combo_notify, s, spinner)
+ # elif s.kind == _settings.KIND.range:
+ # first, second = s.choices[:2]
+ # last = s.choices[-1:][0]
+ # control = Gtk.HScale.new_with_range(first, last, second - first)
+ # control.set_draw_value(False)
+ # control.set_has_origin(False)
+ # for entry in s.choices:
+ # control.add_mark(int(entry), Gtk.PositionType.TOP, str(entry))
+ # control.connect('change-value', _snap_to_markers, s)
+ # control.connect('value-changed', _scale_notify, s, spinner)
+ else:
+ raise NotImplemented
- control.set_sensitive(False) # the first read will enable it
- sbox.pack_end(control, False, False, 0)
- sbox.pack_end(spinner, False, False, 0)
- sbox.pack_end(failed, False, False, 0)
+ control.set_sensitive(False) # the first read will enable it
+ sbox.pack_end(control, False, False, 0)
+ sbox.pack_end(spinner, False, False, 0)
+ sbox.pack_end(failed, False, False, 0)
- if s.description:
- sbox.set_tooltip_text(s.description)
+ if s.description:
+ sbox.set_tooltip_text(s.description)
- sbox.show_all()
- spinner.start() # the first read will stop it
- failed.set_visible(False)
- box.pack_start(sbox, False, False, 0)
- yield sbox
+ sbox.show_all()
+ spinner.start() # the first read will stop it
+ failed.set_visible(False)
+
+ return sbox
-def _update_setting_item(sbox, value):
+def _update_setting_item(sbox, value, is_active=True):
_, failed, spinner, control = sbox.get_children()
spinner.set_visible(False)
spinner.stop()
@@ -136,7 +135,7 @@ def _update_setting_item(sbox, value):
# print ("update", control, "with new value", value)
if value is None:
control.set_sensitive(False)
- failed.set_visible(True)
+ failed.set_visible(is_active)
return
failed.set_visible(False)
@@ -156,45 +155,43 @@ def _update_setting_item(sbox, value):
def create():
b = Gtk.VBox(homogeneous=False, spacing=4)
- b.set_property('margin', 8)
+ # b.set_property('margin', 8)
+ b._last_device = None
+ b._items = {}
return b
-def update(frame):
- box = frame._config_box
- assert box
- device = frame._device
+def update(box, device, is_active):
+ assert box is not None
+ assert device is not None
- if device is None:
- # remove all settings widgets
- # if another device gets paired here, it will add its own widgets
- _remove_children(box)
- return
+ # if the device changed since last update, clear the box first
+ if not box._last_device:
+ box._last_device = None
+ if device.serial != box._last_device:
+ box.set_visible(False)
- if not box.get_visible():
- # no point in doing this right now, is there?
- return
-
- if not device.settings:
- # nothing to do here
- return
-
- force_read = False
- items = box.get_children()
- if len(device.settings) != len(items):
- _remove_children(box)
- if device.status:
- items = list(_add_settings(box, device))
- assert len(device.settings) == len(items)
- # force_read = True
-
- device_active = bool(device.status)
# if the device just became active, re-read the settings
- # force_read |= device_active and not box.get_sensitive()
- box.set_sensitive(device_active)
- if device_active:
- for sbox, s in zip(items, device.settings):
- _apply_queue.put(('read', s, force_read, sbox))
+ box.foreach(lambda x, s: x.set_visible(x.get_name() == s), device.serial)
+
+ if device.serial != box._last_device:
+ box._last_device = device.serial
+ box.set_visible(True)
+
+ for s in device.settings:
+ k = device.serial + '_' + s.name
+ if k not in box._items:
+ sbox = _create_sbox(s)
+ sbox.set_name(device.serial)
+ box._items[k] = sbox
+ box.pack_start(sbox, False, False, 0)
+ else:
+ sbox = box._items[k]
+
+ if is_active:
+ _apply_queue.put(('read', s, False, sbox))
+ else:
+ _update_setting_item(sbox, None, False)
def _remove_children(container):
diff --git a/lib/solaar/ui/pair_window.py b/lib/solaar/ui/pair_window.py
index e3464fba..267a7506 100644
--- a/lib/solaar/ui/pair_window.py
+++ b/lib/solaar/ui/pair_window.py
@@ -175,10 +175,13 @@ def _pairing_succeeded(assistant, receiver, device):
assistant.commit()
-def create(action, receiver):
+def create(receiver):
+ assert receiver is not None
+ assert receiver.kind is None
+
assistant = Gtk.Assistant()
- assistant.set_title(action.get_label())
- assistant.set_icon_name(action.get_icon_name())
+ assistant.set_title(receiver.name + ': pair new device')
+ assistant.set_icon_name('list-add')
assistant.set_size_request(400, 240)
assistant.set_resizable(False)
diff --git a/lib/solaar/ui/status_icon.py b/lib/solaar/ui/tray.py
similarity index 55%
rename from lib/solaar/ui/status_icon.py
rename to lib/solaar/ui/tray.py
index c4d340a4..f0d048b9 100644
--- a/lib/solaar/ui/status_icon.py
+++ b/lib/solaar/ui/tray.py
@@ -14,8 +14,15 @@ from gi.repository import Gtk, GLib
from gi.repository.Gdk import ScrollDirection
from solaar import NAME
-from . import action as _action, icons as _icons
-from logitech.unifying_receiver import status as _status
+from logitech.unifying_receiver.status import (
+ BATTERY_LEVEL as _BATTERY_LEVEL,
+ BATTERY_CHARGING as _BATTERY_CHARGING,
+ )
+from . import icons as _icons
+from .window import (
+ popup as _window_popup,
+ toggle as _window_toggle
+ )
_TRAY_ICON_SIZE = 32 # pixels
_MENU_ICON_SIZE = Gtk.IconSize.LARGE_TOOLBAR
@@ -29,13 +36,8 @@ _picked_device = None
-def _create_common(icon, menu_activate_callback):
- icon._devices_info = []
-
- icon.set_title(NAME)
-
- icon._menu_activate_callback = menu_activate_callback
- icon._menu = menu = Gtk.Menu()
+def _create_menu():
+ menu = Gtk.Menu()
# per-device menu entries will be generated as-needed
@@ -44,12 +46,18 @@ def _create_common(icon, menu_activate_callback):
menu.append(no_receiver)
menu.append(Gtk.SeparatorMenuItem.new())
- menu.append(_action.about.create_menu_item())
- menu.append(_action.make('application-exit', 'Quit', Gtk.main_quit).create_menu_item())
+ from .action import about, make
+ menu.append(about.create_menu_item())
+ menu.append(make('application-exit', 'Quit', Gtk.main_quit).create_menu_item())
+ del about, make
+
menu.show_all()
+ return menu
+
try:
+ # raise ImportError
from gi.repository import AppIndicator3
_log.info("using AppIndicator3")
@@ -61,7 +69,7 @@ try:
# ignore all other directions
return
- if len(ind._devices_info) < 4:
+ if len(_devices_info) < 4:
# don't bother with scrolling when there's only one receiver
# with only one device (3 = [receiver, device, separator])
return
@@ -81,14 +89,14 @@ try:
candidate = None
if _picked_device is None:
- for info in ind._devices_info:
+ for info in _devices_info:
# pick first peripheral found
if info[1] is not None:
candidate = info
break
else:
found = False
- for info in ind._devices_info:
+ for info in _devices_info:
if not info[1]:
# only conside peripherals
continue
@@ -122,13 +130,10 @@ try:
_picked_device = candidate or _picked_device
if _log.isEnabledFor(_DEBUG):
_log.debug("scroll: picked %s", _picked_device)
- _update_tray_icon(ind)
+ _update_tray_icon()
- def create(activate_callback, menu_activate_callback):
- assert activate_callback
- assert menu_activate_callback
-
+ def _create(menu):
theme_paths = Gtk.IconTheme.get_default().get_search_path()
ind = AppIndicator3.Indicator.new_with_path(
@@ -136,39 +141,38 @@ try:
_icons.TRAY_INIT,
AppIndicator3.IndicatorCategory.HARDWARE,
':'.join(theme_paths))
+ ind.set_title(NAME)
ind.set_status(AppIndicator3.IndicatorStatus.ACTIVE)
ind.set_attention_icon_full(_icons.TRAY_ATTENTION, '')
# ind.set_label(NAME, NAME)
- _create_common(ind, menu_activate_callback)
- ind.set_menu(ind._menu)
-
+ ind.set_menu(menu)
ind.connect('scroll-event', _scroll)
return ind
- def destroy(ind):
- ind.set_status(AppIndicator3.IndicatorStatus.PASSIVE)
+ def _destroy(indicator):
+ indicator.set_status(AppIndicator3.IndicatorStatus.PASSIVE)
- def _update_tray_icon(ind):
+ def _update_tray_icon():
if _picked_device:
_, _, name, _, device_status = _picked_device
- battery_level = device_status.get(_status.BATTERY_LEVEL)
- battery_charging = device_status.get(_status.BATTERY_CHARGING)
+ battery_level = device_status.get(_BATTERY_LEVEL)
+ battery_charging = device_status.get(_BATTERY_CHARGING)
tray_icon_name = _icons.battery(battery_level, battery_charging)
description = '%s: %s' % (name, device_status)
else:
# there may be a receiver, but no peripherals
- tray_icon_name = _icons.TRAY_OKAY if ind._devices_info else _icons.TRAY_INIT
+ tray_icon_name = _icons.TRAY_OKAY if _devices_info else _icons.TRAY_INIT
- tooltip_lines = _generate_tooltip_lines(ind._devices_info)
+ tooltip_lines = _generate_tooltip_lines()
description = '\n'.join(tooltip_lines).rstrip('\n')
# icon_file = _icons.icon_file(icon_name, _TRAY_ICON_SIZE)
- ind.set_icon_full(tray_icon_name, description)
+ _icon.set_icon_full(tray_icon_name, description)
def _update_menu_icon(image_widget, icon_name):
@@ -178,52 +182,48 @@ try:
# image_widget.set_pixel_size(_TRAY_ICON_SIZE)
- def attention(ind, reason=None):
- if ind.get_status != AppIndicator3.IndicatorStatus.ATTENTION:
- ind.set_attention_icon_full(_icons.TRAY_ATTENTION, reason or '')
- ind.set_status(AppIndicator3.IndicatorStatus.ATTENTION)
- GLib.timeout_add(10 * 1000, ind.set_status, AppIndicator3.IndicatorStatus.ACTIVE)
+ def attention(reason=None):
+ if _icon.get_status != AppIndicator3.IndicatorStatus.ATTENTION:
+ _icon.set_attention_icon_full(_icons.TRAY_ATTENTION, reason or '')
+ _icon.set_status(AppIndicator3.IndicatorStatus.ATTENTION)
+ GLib.timeout_add(10 * 1000, _icon.set_status, AppIndicator3.IndicatorStatus.ACTIVE)
except ImportError:
_log.info("using StatusIcon")
- def create(activate_callback, menu_activate_callback):
- assert activate_callback
- assert menu_activate_callback
-
+ def _create(menu):
icon = Gtk.StatusIcon.new_from_icon_name(_icons.TRAY_INIT)
icon.set_name(NAME)
+ icon.set_title(NAME)
icon.set_tooltip_text(NAME)
- icon.connect('activate', activate_callback)
+ icon.connect('activate', _window_toggle)
- _create_common(icon, menu_activate_callback)
icon.connect('popup_menu',
- lambda icon, button, time, menu:
- icon._menu.popup(None, None, icon.position_menu, icon, button, time),
- icon._menu)
+ lambda icon, button, time:
+ menu.popup(None, None, icon.position_menu, icon, button, time))
return icon
- def destroy(icon):
+ def _destroy(icon):
icon.set_visible(False)
- def _update_tray_icon(icon):
- tooltip_lines = _generate_tooltip_lines(icon._devices_info)
+ def _update_tray_icon():
+ tooltip_lines = _generate_tooltip_lines()
tooltip = '\n'.join(tooltip_lines).rstrip('\n')
- icon.set_tooltip_markup(tooltip)
+ _icon.set_tooltip_markup(tooltip)
if _picked_device:
_, _, name, _, device_status = _picked_device
- battery_level = device_status.get(_status.BATTERY_LEVEL)
- battery_charging = device_status.get(_status.BATTERY_CHARGING)
+ battery_level = device_status.get(_BATTERY_LEVEL)
+ battery_charging = device_status.get(_BATTERY_CHARGING)
tray_icon_name = _icons.battery(battery_level, battery_charging)
else:
# there may be a receiver, but no peripherals
- tray_icon_name = _icons.TRAY_OKAY if icon._devices_info else _icons.TRAY_ATTENTION
- icon.set_from_icon_name(tray_icon_name)
+ tray_icon_name = _icons.TRAY_OKAY if _devices_info else _icons.TRAY_ATTENTION
+ _icon.set_from_icon_name(tray_icon_name)
def _update_menu_icon(image_widget, icon_name):
@@ -232,35 +232,35 @@ except ImportError:
_icon_before_attention = None
- def _blink(icon, count):
+ def _blink(count):
global _icon_before_attention
if count % 2:
- icon.set_from_icon_name(_icons.TRAY_ATTENTION)
+ _icon.set_from_icon_name(_icons.TRAY_ATTENTION)
else:
- icon.set_from_icon_name(_icon_before_attention)
+ _icon.set_from_icon_name(_icon_before_attention)
if count > 0:
- GLib.timeout_add(1000, _blink, icon, count - 1)
+ GLib.timeout_add(1000, _blink, count - 1)
- def attention(icon, reason=None):
+ def attention(reason=None):
global _icon_before_attention
if _icon_before_attention is None:
- _icon_before_attention = icon.get_icon_name()
- GLib.idle_add(_blink, icon, 9)
+ _icon_before_attention = _icon.get_icon_name()
+ GLib.idle_add(_blink, 9)
#
#
#
-def _generate_tooltip_lines(devices_info):
- if not devices_info:
+def _generate_tooltip_lines():
+ if not _devices_info:
yield '%s: no receivers' % NAME
return
yield '%s' % NAME
yield ''
- for _, serial, name, _, status in devices_info:
+ for _, serial, name, _, status in _devices_info:
if serial is None: # receiver
continue
@@ -280,17 +280,17 @@ def _generate_tooltip_lines(devices_info):
yield ''
-def _pick_device_with_lowest_battery(devices_info):
- if not devices_info:
+def _pick_device_with_lowest_battery():
+ if not _devices_info:
return None
picked = None
picked_level = 1000
- for info in devices_info:
+ for info in _devices_info:
if info[1] is None: # is receiver/separator
continue
- level = info[-1].get(_status.BATTERY_LEVEL)
+ level = info[-1].get(_BATTERY_LEVEL)
if not picked or (level is not None and picked_level > level):
picked = info
picked_level = level or 0
@@ -305,10 +305,15 @@ def _pick_device_with_lowest_battery(devices_info):
#
#
-def _add_device(icon, device):
+def _add_device(device):
+ assert device
+ assert device.receiver
+ receiver_path = device.receiver.path
+ assert receiver_path
+
index = None
- for idx, (rserial, _, _, _, _) in enumerate(icon._devices_info):
- if rserial == device.receiver.serial:
+ for idx, (path, _, _, _, _) in enumerate(_devices_info):
+ if path == receiver_path:
# the first entry matching the receiver serial should be for the receiver itself
index = idx + 1
break
@@ -316,89 +321,86 @@ def _add_device(icon, device):
# proper ordering (according to device.number) for a receiver's devices
while True:
- rserial, _, _, number, _ = icon._devices_info[index]
- if rserial == '-':
+ path, _, _, number, _ = _devices_info[index]
+ if path == '-':
break
- assert rserial == device.receiver.serial
+ assert path == receiver_path
assert number != device.number
if number > device.number:
break
index = index + 1
- device_info = (device.receiver.serial, device.serial, device.name, device.number, device.status)
- icon._devices_info.insert(index, device_info)
-
- # print ("status_icon: added", index, ":", device_info)
+ new_device_info = (receiver_path, device.serial, device.name, device.number, device.status)
+ _devices_info.insert(index, new_device_info)
# label_prefix = b'\xE2\x94\x84 '.decode('utf-8')
label_prefix = ' '
- menu_item = Gtk.ImageMenuItem.new_with_label(label_prefix + device.name)
- menu_item.set_image(Gtk.Image())
- menu_item.show_all()
- menu_item.connect('activate', icon._menu_activate_callback, device.receiver.path, icon)
-
- icon._menu.insert(menu_item, index)
+ new_menu_item = Gtk.ImageMenuItem.new_with_label(label_prefix + device.name)
+ new_menu_item.set_image(Gtk.Image())
+ new_menu_item.show_all()
+ new_menu_item.connect('activate', _window_popup, receiver_path, device.serial)
+ _menu.insert(new_menu_item, index)
return index
-def _remove_device(icon, index):
+def _remove_device(index):
assert index is not None
- menu_items = icon._menu.get_children()
- icon._menu.remove(menu_items[index])
+ menu_items = _menu.get_children()
+ _menu.remove(menu_items[index])
- removed_device = icon._devices_info.pop(index)
+ removed_device = _devices_info.pop(index)
global _picked_device
if _picked_device and _picked_device[1] == removed_device[1]:
# the current pick was unpaired
_picked_device = None
-def _add_receiver(icon, receiver):
- device_info = (receiver.serial, None, receiver.name, None, None)
- icon._devices_info.insert(0, device_info)
+def _add_receiver(receiver):
+ device_info = (receiver.path, None, receiver.name, None, None)
+ _devices_info.insert(0, device_info)
- menu_item = Gtk.ImageMenuItem.new_with_label(receiver.name)
- icon._menu.insert(menu_item, 0)
+ new_menu_item = Gtk.ImageMenuItem.new_with_label(receiver.name)
+ _menu.insert(new_menu_item, 0)
icon_set = _icons.device_icon_set(receiver.name)
- menu_item.set_image(Gtk.Image().new_from_icon_set(icon_set, _MENU_ICON_SIZE))
- menu_item.show_all()
- menu_item.connect('activate', icon._menu_activate_callback, receiver.path, icon)
+ new_menu_item.set_image(Gtk.Image().new_from_icon_set(icon_set, _MENU_ICON_SIZE))
+ new_menu_item.show_all()
+ new_menu_item.connect('activate', _window_popup, receiver.path)
- icon._devices_info.insert(1, ('-', None, None, None, None))
+ _devices_info.insert(1, ('-', None, None, None, None))
separator = Gtk.SeparatorMenuItem.new()
separator.set_visible(True)
- icon._menu.insert(separator, 1)
+ _menu.insert(separator, 1)
return 0
-def _remove_receiver(icon, receiver):
+def _remove_receiver(receiver):
index = 0
found = False
# remove all entries in devices_info that match this receiver
- while index < len(icon._devices_info):
- rserial, _, _, _, _ = icon._devices_info[index]
- if rserial == receiver.serial:
+ while index < len(_devices_info):
+ path, _, _, _, _ = _devices_info[index]
+ if path == receiver.path:
found = True
- _remove_device(icon, index)
- elif found and rserial == '-':
+ _remove_device(index)
+ elif found and path == '-':
# the separator after this receiver
- _remove_device(icon, index)
+ _remove_device(index)
break
else:
index += 1
-def _update_menu_item(icon, index, device_status):
- menu_items = icon._menu.get_children()
+def _update_menu_item(index, device_status):
+ menu_items = _menu.get_children()
menu_item = menu_items[index]
- level = device_status.get(_status.BATTERY_LEVEL)
- charging = device_status.get(_status.BATTERY_CHARGING)
+ level = device_status.get(_BATTERY_LEVEL)
+ charging = device_status.get(_BATTERY_CHARGING)
icon_name = _icons.battery(level, charging)
image_widget = menu_item.get_image()
@@ -409,46 +411,72 @@ def _update_menu_item(icon, index, device_status):
#
#
-def update(icon, device=None):
+_devices_info = []
+_menu = None
+_icon = None
+
+def init():
+ global _menu, _icon
+ _menu = _create_menu()
+ _icon = _create(_menu)
+
+
+def destroy():
+ global _icon, _menu, _devices_info
+ i, _icon = _icon, None
+ _destroy(i)
+ i = None
+
+ _icon = None
+ _menu = None
+ _devices_info = None
+
+
+def update(device=None):
+ if _icon is None:
+ return
+
if device is not None:
if device.kind is None:
# receiver
receiver = device
+ receiver_path = receiver.path
if receiver:
index = None
- for idx, (rserial, _, _, _, _) in enumerate(icon._devices_info):
- if rserial == receiver.serial:
+ for idx, (path, _, _, _, _) in enumerate(_devices_info):
+ if path == receiver_path:
index = idx
break
if index is None:
- _add_receiver(icon, receiver)
+ _add_receiver(receiver)
else:
- _remove_receiver(icon, receiver)
+ _remove_receiver(receiver)
else:
+ receiver_path = device.receiver.path
# peripheral
index = None
- for idx, (rserial, serial, name, _, _) in enumerate(icon._devices_info):
- if rserial == device.receiver.serial and serial == device.serial:
+ for idx, (path, serial, name, _, _) in enumerate(_devices_info):
+ if path == receiver_path and serial == device.serial:
index = idx
if device.status is None:
# was just unpaired
assert index is not None
- _remove_device(icon, index)
+ _remove_device(index)
else:
if index is None:
- index = _add_device(icon, device)
- _update_menu_item(icon, index, device.status)
+ index = _add_device(device)
+ _update_menu_item(index, device.status)
- menu_items = icon._menu.get_children()
- no_receivers_index = len(icon._devices_info)
- menu_items[no_receivers_index].set_visible(not icon._devices_info)
- menu_items[no_receivers_index + 1].set_visible(not icon._devices_info)
+ menu_items = _menu.get_children()
+ no_receivers_index = len(_devices_info)
+ menu_items[no_receivers_index].set_visible(not _devices_info)
+ menu_items[no_receivers_index + 1].set_visible(not _devices_info)
global _picked_device
if not _picked_device:
- _picked_device = _pick_device_with_lowest_battery(icon._devices_info)
+ _picked_device = _pick_device_with_lowest_battery()
- _update_tray_icon(icon)
+ _update_tray_icon()
diff --git a/lib/solaar/ui/window.py b/lib/solaar/ui/window.py
new file mode 100644
index 00000000..18cfa51e
--- /dev/null
+++ b/lib/solaar/ui/window.py
@@ -0,0 +1,674 @@
+#
+#
+#
+
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+from logging import getLogger
+_log = getLogger(__name__)
+del getLogger
+
+from gi.repository import Gtk, Gdk
+from gi.repository.GObject import TYPE_PYOBJECT
+
+from solaar import NAME
+# from solaar import __version__ as VERSION
+from logitech.unifying_receiver import hidpp10 as _hidpp10
+from logitech.unifying_receiver.common import NamedInts as _NamedInts
+from logitech.unifying_receiver.status import (
+ BATTERY_LEVEL as _BATTERY_LEVEL,
+ BATTERY_CHARGING as _BATTERY_CHARGING,
+ LIGHT_LEVEL as _LIGHT_LEVEL,
+ ENCRYPTED as _ENCRYPTED,
+ )
+from . import config_panel as _config_panel
+from . import action as _action, icons as _icons
+from .about import show_window as _show_about_window
+
+
+#
+# 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
+
+# tree model columns
+_COLUMN = _NamedInts(ID=0, ACTIVE=1, NAME=2, ICON=3, STATUS_ICON=4, DEVICE=5)
+_COLUMN_TYPES = (str, bool, str, str, str, TYPE_PYOBJECT)
+_TREE_SEPATATOR = (None, False, None, None, None, None)
+
+_TOOLTIP_LINK_SECURE = 'The wireless link between this device and its receiver is not encrypted.'
+_TOOLTIP_LINK_INSECURE = ('The wireless link between this device and its receiver is not encrypted.\n'
+ '\n'
+ 'For pointing devices (mice, trackballs, trackpads), this is a minor security issue.\n'
+ '\n'
+ 'It is, however, a major security issue for text-input devices (keyboards, numpads),\n'
+ 'because typed text can be sniffed inconspicuously by 3rd parties within range.')
+
+_UNIFYING_RECEIVER_TEXT = (
+ 'No paired devices.\n\nUp to %d devices can be paired to this receiver.',
+ '%d paired device(s).\n\nUp to %d devices can be paired to this receiver.',
+ )
+_NANO_RECEIVER_TEXT = (
+ 'No paired device.\n\n \n ',
+ ' \n\nOnly one device can be paired to this receiver.',
+ )
+
+#
+# create UI layout
+#
+
+Gtk.Window.set_default_icon_name(NAME.lower())
+Gtk.Window.set_default_icon_from_file(_icons.icon_file(NAME.lower(), 32))
+
+def _new_button(label, icon_name=None, icon_size=_NORMAL_BUTTON_ICON_SIZE, tooltip=None, toggle=False, clicked=None):
+ if toggle:
+ b = Gtk.ToggleButton()
+ else:
+ b = Gtk.Button(label) if label else Gtk.Button()
+
+ if icon_name:
+ image = Gtk.Image.new_from_icon_name(icon_name, icon_size)
+ b.set_image(image)
+
+ 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)
+
+ if clicked is not None:
+ b.connect('clicked', clicked)
+
+ return b
+
+
+def _create_receiver_panel():
+ p = Gtk.Box.new(Gtk.Orientation.VERTICAL, 4)
+
+ p._count = Gtk.Label()
+ p._count.set_padding(32, 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._config = _config_panel.create()
+ p.pack_end(p._config, False, False, 8)
+
+ return p
+
+
+def _create_details_panel():
+ p = Gtk.Frame()
+ # p.set_border_width(2)
+ p.set_shadow_type(Gtk.ShadowType.ETCHED_OUT)
+
+ p._text = Gtk.Label()
+ p._text.set_padding(4, 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 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):
+ receiver = _find_selected_device()
+ assert receiver is not None
+ 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):
+ 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('Select a device')
+ 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(240, 120)
+ tree.set_headers_visible(False)
+ tree.set_show_expanders(False)
+ tree.set_level_indentation(16)
+ tree.set_enable_tree_lines(True)
+ # tree.set_rules_hint(True)
+ tree.set_model(model)
+
+ def _is_separator(model, item, _=None):
+ return model.get_value(item, _COLUMN.ID) 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)
+ icon_column.set_fixed_width(1)
+ tree.append_column(icon_column)
+
+ name_cell_renderer = Gtk.CellRendererText()
+ name_column = Gtk.TreeViewColumn('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)
+
+ battery_cell_renderer = Gtk.CellRendererPixbuf()
+ battery_cell_renderer.set_property('stock-size', _TREE_ICON_SIZE)
+ battery_column = Gtk.TreeViewColumn('Status', 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)
+ battery_column.set_fixed_width(1)
+ 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_panel = Gtk.Box.new(Gtk.Orientation.VERTICAL, 4)
+ tree_panel.pack_start(_tree, True, True, 0)
+ tree_panel.pack_start(_details, True, True, 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)
+
+ about_button = _new_button('About', 'help-about', clicked=_show_about_window)
+ # about_button.set_relief(Gtk.ReliefStyle.NONE)
+
+ bottom_buttons_box = Gtk.ButtonBox(Gtk.Orientation.HORIZONTAL)
+ bottom_buttons_box.set_layout(Gtk.ButtonBoxStyle.START)
+ bottom_buttons_box.add(about_button)
+
+ # solaar_version = Gtk.Label()
+ # solaar_version.set_markup('' + NAME + ' v' + VERSION + '')
+ # 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():
+ 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.set_keep_above(True)
+ window.connect('delete-event', _hide)
+
+ vbox = _create_window_layout()
+ window.add(vbox)
+
+ geometry = Gdk.Geometry()
+ geometry.min_width = 600
+ geometry.min_height = 320
+ geometry.max_width = 800
+ geometry.max_height = 600
+ window.set_geometry_hints(vbox, geometry, Gdk.WindowHints.MIN_SIZE | Gdk.WindowHints.MAX_SIZE)
+ window.set_position(Gtk.WindowPosition.CENTER)
+
+ return window
+
+#
+# window updates
+#
+
+def _find_selected_device():
+ selection = _tree.get_selection()
+ model, item = selection.get_selected()
+ device = model.get_value(item, _COLUMN.DEVICE) if item else None
+ return device or None
+
+
+# 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
+ _update_info_panel(device, full=True)
+
+
+def _receiver_row(receiver_path, receiver=None):
+ item = _model.get_iter_first()
+ while item:
+ if _model.get_value(item, _COLUMN.ID) == receiver_path:
+ return item
+ item = _model.iter_next(item)
+
+ if not item and receiver is not None:
+ _model.insert(None, 0, _TREE_SEPATATOR)
+ row_data = (receiver_path, True, receiver.name, _icons.device_icon_name(receiver.name), '', receiver)
+ item = _model.insert(None, 0, row_data)
+
+ return item or None
+
+
+def _device_row(receiver_path, device_serial, device=None):
+ receiver_row = _receiver_row(receiver_path, None if device is None else device.receiver)
+ item = _model.iter_children(receiver_row)
+ while item:
+ if _model.get_value(item, _COLUMN.ID) == device_serial:
+ return item
+ item = _model.iter_next(item)
+
+ if not item and device is not None:
+ # print ("new device row", device)
+ row_data = (device_serial, bool(device.status), device.codename, _icons.device_icon_name(device.name, device.kind), '', device)
+ item = _model.append(receiver_row, row_data)
+
+ return item or None
+
+#
+#
+#
+
+def select(receiver_path, device_id=None):
+ assert _window
+ assert receiver_path is not None
+ if device_id is None:
+ item = _receiver_row(receiver_path)
+ else:
+ item = _device_row(receiver_path, device_id)
+ if item:
+ selection = _tree.get_selection()
+ selection.select_iter(item)
+
+
+def _hide(w, _=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()
+ device = _find_selected_device()
+
+ if visible:
+ _details._text.set_markup('reading...')
+
+ def _details_items(device):
+ if device.kind is None:
+ yield ('Path', device.path)
+ else:
+ hid = device.protocol
+ yield ('Protocol', 'HID++ %1.1f' % hid if hid else 'unknown')
+ if device.polling_rate:
+ yield ('Polling rate', '%d ms' % device.polling_rate)
+ yield ('Wireless PID', device.wpid)
+
+ yield ('Serial', device.serial)
+
+ for fw in device.firmware:
+ yield (fw.kind, (fw.name + ' ' + fw.version).strip())
+
+ if device.kind is None or device.status:
+ # don't show notifications for offline devices
+ notification_flags = _hidpp10.get_notification_flags(device)
+ if notification_flags:
+ notification_flags = _hidpp10.NOTIFICATION_FLAG.flag_names(notification_flags)
+ else:
+ notification_flags = ('(none)',)
+
+ yield ('Notifications', ('\n%16s' % ' ').join(notification_flags))
+
+ items = _details_items(device)
+ markup_text = '' + '\n'.join('%-14s: %s' % i for i in items if i) + ''
+ _details._text.set_markup(markup_text)
+
+ _details.set_visible(visible)
+
+
+def _update_receiver_panel(receiver, panel, buttons, full=False):
+ devices_count = len(receiver)
+ if receiver.max_devices > 1:
+ if devices_count == 0:
+ panel._count.set_markup(_UNIFYING_RECEIVER_TEXT[0] % receiver.max_devices)
+ else:
+ panel._count.set_markup(_UNIFYING_RECEIVER_TEXT[1] % (devices_count, receiver.max_devices))
+ else:
+ if devices_count == 0:
+ panel._count.set_text(_NANO_RECEIVER_TEXT[0])
+ else:
+ panel._count.set_markup(_NANO_RECEIVER_TEXT[1])
+
+ is_pairing = receiver and receiver.status.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)
+ buttons._pair.set_sensitive(devices_count < receiver.max_devices and not is_pairing)
+ buttons._pair.set_visible(True)
+
+
+def _update_device_panel(device, panel, buttons, full=False):
+ is_active = bool(device.status)
+ panel.set_sensitive(is_active)
+
+ battery_level = device.status.get(_BATTERY_LEVEL)
+ if battery_level is None:
+ icon_name = _icons.battery()
+ panel._battery._icon.set_sensitive(False)
+ panel._battery._icon.set_from_icon_name(icon_name, _INFO_ICON_SIZE)
+ panel._battery._text.set_sensitive(True)
+ panel._battery._text.set_markup('unknown')
+ else:
+ charging = device.status.get(_BATTERY_CHARGING)
+ 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)
+
+ text = '%d%%' % battery_level
+ if bool(device.status):
+ if charging:
+ text += ' (charging)'
+ panel._battery._text.set_sensitive(True)
+ else:
+ text += ' (last known)'
+ panel._battery._text.set_sensitive(False)
+ panel._battery._text.set_markup(text)
+
+ if is_active:
+ not_secure = device.status.get(_ENCRYPTED) == False
+ if not_secure:
+ panel._secure._text.set_text('not encrypted')
+ panel._secure._icon.set_from_icon_name('security-low', _INFO_ICON_SIZE)
+ panel._secure.set_tooltip_text(_TOOLTIP_LINK_INSECURE)
+ else:
+ panel._secure._text.set_text('encrypted')
+ panel._secure._icon.set_from_icon_name('security-high', _INFO_ICON_SIZE)
+ panel._secure.set_tooltip_text(_TOOLTIP_LINK_SECURE)
+ panel._secure._icon.set_visible(True)
+ else:
+ panel._secure._text.set_markup('offline')
+ panel._secure._icon.set_visible(False)
+ panel._secure.set_tooltip_text('')
+
+ if is_active:
+ light_level = device.status.get(_LIGHT_LEVEL)
+ 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('%d lux' % light_level)
+ panel._lux.set_visible(True)
+ else:
+ panel._lux.set_visible(False)
+
+ buttons._pair.set_visible(False)
+ buttons._unpair.set_visible(True)
+
+ panel.set_visible(True)
+
+ if full:
+ _config_panel.update(panel._config, device, is_active)
+
+
+def _update_info_panel(device, full=False):
+ if device is None:
+ _details.set_visible(False)
+ _info.set_visible(False)
+ _empty.set_visible(True)
+ return
+
+ is_active = bool(device.status)
+
+ _info._title.set_markup('%s' % device.name)
+ _info._title.set_sensitive(is_active)
+ icon_name = _icons.device_icon_name(device.name, device.kind)
+ _info._icon.set_from_icon_name(icon_name, _DEVICE_ICON_SIZE)
+ _info._icon.set_sensitive(is_active)
+
+ if device.kind is None:
+ _info._device.set_visible(False)
+ _update_receiver_panel(device, _info._receiver, _info._buttons, full)
+ else:
+ _info._receiver.set_visible(False)
+ _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():
+ 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()
+
+
+def destroy():
+ global _model, _tree, _details, _info, _empty, _window
+ w, _window = _window, None
+ w.destroy()
+ w = None
+
+ _empty = None
+ _info = None
+ _details = None
+ _tree = None
+ _model = None
+
+
+def update(device, need_popup=False):
+ if _window is None:
+ return
+
+ assert device is not None
+
+ if need_popup:
+ popup()
+
+ if device.kind is None:
+ is_alive = bool(device)
+ item = _receiver_row(device.path, device if is_alive else None)
+ assert item
+ if is_alive:
+ _model.set_value(item, _COLUMN.ACTIVE, True)
+ elif item:
+ separator = _model.iter_next(item)
+ _model.remove(separator)
+ _model.remove(item)
+
+ is_pairing = is_alive and device.status.lock_open
+ _model.set_value(item, _COLUMN.STATUS_ICON, 'network-wireless' if is_pairing else '')
+
+ else:
+ is_alive = device.status is not None
+ item = _device_row(device.receiver.path, device.serial, device if is_alive else None)
+ assert item
+ _model.set_value(item, _COLUMN.ACTIVE, bool(device.status))
+ battery_level = device.status.get(_BATTERY_LEVEL)
+ if battery_level is None:
+ charging = device.status.get(_BATTERY_CHARGING)
+ battery_icon_name = _icons.battery(battery_level, charging)
+ _model.set_value(item, _COLUMN.STATUS_ICON, '' if battery_level is None else battery_icon_name)
+
+ # make sure all rows are visible
+ _tree.expand_all()
+ selected_device = _find_selected_device()
+ if device == selected_device:
+ _update_info_panel(device, need_popup)
+ elif selected_device is None and device.kind is not None:
+ select(device.receiver.path, device.serial)