new single-window UI

This commit is contained in:
Daniel Pavel 2013-06-19 15:28:13 +02:00
parent 4af714f1dd
commit cd44cc6396
8 changed files with 942 additions and 241 deletions

View File

@ -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()

View File

@ -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))

View File

@ -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()

View File

@ -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)

View File

@ -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):

View File

@ -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)

View File

@ -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()

674
lib/solaar/ui/window.py Normal file
View File

@ -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)