new single-window UI
This commit is contained in:
parent
4af714f1dd
commit
cd44cc6396
|
@ -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()
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 '<b>%s</b>: no receivers' % NAME
|
||||
return
|
||||
|
||||
yield '<b>%s</b>' % 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()
|
|
@ -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\n<small>Up to %d devices can be paired to this receiver.</small>',
|
||||
'%d paired device(s).\n\n<small>Up to %d devices can be paired to this receiver.</small>',
|
||||
)
|
||||
_NANO_RECEIVER_TEXT = (
|
||||
'No paired device.\n\n<small> \n </small>',
|
||||
' \n\n<small>Only one device can be paired to this receiver.</small>',
|
||||
)
|
||||
|
||||
#
|
||||
# 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('<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(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('<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():
|
||||
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('<small>reading...</small>')
|
||||
|
||||
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 = '<small><tt>' + '\n'.join('%-14s: %s' % i for i in items if i) + '</tt></small>'
|
||||
_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('<small>unknown</small>')
|
||||
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 += ' <small>(charging)</small>'
|
||||
panel._battery._text.set_sensitive(True)
|
||||
else:
|
||||
text += ' <small>(last known)</small>'
|
||||
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('<small>offline</small>')
|
||||
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('<b>%s</b>' % 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)
|
Loading…
Reference in New Issue