re-wrote loading of icons for devices

This commit is contained in:
Daniel Pavel 2012-11-30 15:23:16 +02:00
parent 64d2b35ace
commit 14663ca204
8 changed files with 184 additions and 119 deletions

View File

@ -6,16 +6,22 @@ from logging import getLogger, DEBUG as _DEBUG
_log = getLogger('listener')
del getLogger
import logitech.unifying_receiver as _lur
from logitech.unifying_receiver import (
Receiver, PairedDevice,
listener as _listener,
hidpp10 as _hidpp10,
hidpp20 as _hidpp20,
status as _status)
#
#
#
class _DUMMY_RECEIVER(object):
__slots__ = ['name', 'max_devices', 'status']
name = _lur.Receiver.name
max_devices = _lur.Receiver.max_devices
# __slots__ = ['name', 'max_devices', 'status']
__slots__ = []
name = Receiver.name
max_devices = Receiver.max_devices
status = 'Receiver not found.'
__bool__ = __nonzero__ = lambda self: False
__str__ = lambda self: 'DUMMY'
@ -39,7 +45,7 @@ _DEVICE_STATUS_POLL = 60 # seconds
# dev.status = _lur.status.DeviceStatus(dev, listener._status_changed)
# return dev
class ReceiverListener(_lur.listener.EventsListener):
class ReceiverListener(_listener.EventsListener):
"""Keeps the status of a Unifying Receiver.
"""
def __init__(self, receiver, status_changed_callback=None):
@ -48,12 +54,12 @@ class ReceiverListener(_lur.listener.EventsListener):
self.status_changed_callback = status_changed_callback
receiver.status = _lur.status.ReceiverStatus(receiver, self._status_changed)
_lur.Receiver.create_device = self.create_device
receiver.status = _status.ReceiverStatus(receiver, self._status_changed)
Receiver.create_device = self.create_device
def create_device(self, receiver, number):
dev = _lur.PairedDevice(receiver, number)
dev.status = _lur.status.DeviceStatus(dev, self._status_changed)
dev = PairedDevice(receiver, number)
dev.status = _status.DeviceStatus(dev, self._status_changed)
return dev
def has_started(self):
@ -66,10 +72,10 @@ class ReceiverListener(_lur.listener.EventsListener):
# fake = _fake_device(self)
# self.receiver._devices[fake.number] = fake
# self._status_changed(fake, _lur.status.ALERT.LOW)
# self._status_changed(fake, _status.ALERT.LOW)
self.receiver.notify_devices()
self._status_changed(self.receiver, _lur.status.ALERT.LOW)
self._status_changed(self.receiver, _status.ALERT.LOW)
def has_stopped(self):
if self.receiver:
@ -77,7 +83,7 @@ class ReceiverListener(_lur.listener.EventsListener):
self.receiver.close()
self.receiver = None
self._status_changed(None, alert=_lur.status.ALERT.LOW)
self._status_changed(None, alert=_status.ALERT.LOW)
def tick(self, timestamp):
if _log.isEnabledFor(_DEBUG):
@ -95,17 +101,17 @@ class ReceiverListener(_lur.listener.EventsListener):
# read these in case they haven't been read already
dev.wpid, dev.serial, dev.protocol, dev.firmware
if dev.status.get(_lur.status.BATTERY_LEVEL) is None:
battery = _lur.hidpp20.get_battery(dev) or _lur.hidpp10.get_battery(dev)
if _status.BATTERY_LEVEL not in dev.status:
battery = _hidpp20.get_battery(dev) or _hidpp10.get_battery(dev)
if battery:
dev.status[_lur.status.BATTERY_LEVEL], dev.status[_lur.status.BATTERY_STATUS] = battery
dev.status[_status.BATTERY_LEVEL], dev.status[_status.BATTERY_STATUS] = battery
self._status_changed(dev)
elif len(dev.status) > 0 and timestamp - dev.status.updated > _DEVICE_TIMEOUT:
dev.status.clear()
self._status_changed(dev, _lur.status.ALERT.LOW)
self._status_changed(dev, _status.ALERT.LOW)
def _status_changed(self, device, alert=_lur.status.ALERT.NONE, reason=None):
def _status_changed(self, device, alert=_status.ALERT.NONE, reason=None):
if _log.isEnabledFor(_DEBUG):
_log.debug("status_changed %s: %s (%X) %s", device, None if device is None else device.status, alert, reason or '')
if self.status_changed_callback:
@ -117,32 +123,34 @@ class ReceiverListener(_lur.listener.EventsListener):
self.status_changed_callback(self.receiver, None)
def _events_handler(self, event):
assert self.receiver
if event.devnumber == 0xFF:
# a receiver envent
if self.receiver.status is not None:
self.receiver.status.process_event(event)
else:
# a paired device envent
assert event.devnumber > 0 and event.devnumber <= self.receiver.max_devices
known_device = event.devnumber in self.receiver
dev = self.receiver[event.devnumber]
if dev:
if dev.status is not None and dev.status.process_event(event):
if self.receiver.status.lock_open and not known_device:
assert event.sub_id == 0x41
self.receiver.status.new_device = dev
if dev.status is not None:
dev.status.process_event(event)
else:
_log.warn("received event %s for invalid device %d", event, event.devnumber)
if self.receiver.status.lock_open:
assert event.sub_id == 0x41
self.receiver.status.new_device = dev
else:
_log.warn("received event %s for invalid device %d", event, event.devnumber)
def __str__(self):
return '<ReceiverListener(%s,%d)>' % (self.receiver.path, self.receiver.status)
@classmethod
def open(self, status_changed_callback=None):
receiver = _lur.Receiver.open()
receiver = Receiver.open()
if receiver:
receiver.handle = _lur.listener.ThreadedHandle(receiver.handle, receiver.path)
receiver.kind = 'applications-system'
receiver.handle = _listener.ThreadedHandle(receiver.handle, receiver.path)
receiver.kind = None
rl = ReceiverListener(receiver, status_changed_callback)
rl.start()
return rl

View File

@ -1,10 +1,15 @@
# pass
#
#
#
from . import (notify, status_icon, main_window, pair_window, action)
from gi.repository import (GObject, Gtk)
from gi.repository import GObject, Gtk
GObject.threads_init()
_LARGE_SIZE = 64
Gtk.IconSize.LARGE = Gtk.icon_size_register('large', _LARGE_SIZE, _LARGE_SIZE)
# Gtk.IconSize.XLARGE = Gtk.icon_size_register('x-large', _LARGE_SIZE * 2, _LARGE_SIZE * 2)
from . import notify, status_icon, main_window, pair_window, action
from solaar import NAME
_APP_ICONS = (NAME + '-init', NAME + '-fail', NAME)
@ -14,22 +19,56 @@ def appicon(receiver_status):
else _APP_ICONS[0])
def get_icon(name, *fallback):
theme = Gtk.IconTheme.get_default()
return (str(name) if name and theme.has_icon(str(name))
else get_icon(*fallback) if fallback
else None)
def get_battery_icon(level):
if level < 0:
return 'battery_unknown'
return 'battery_%03d' % (10 * ((level + 5) // 10))
def icon_file(name):
_ICON_SETS = {}
def device_icon_set(name, kind=None):
icon_set = _ICON_SETS.get(name)
if icon_set is None:
icon_set = Gtk.IconSet.new()
_ICON_SETS[name] = icon_set
names = ['preferences-desktop-peripherals']
if kind:
if str(kind) == 'numpad':
names += ('input-dialpad',)
elif str(kind) == 'touchpad':
names += ('input-tablet',)
elif str(kind) == 'trackball':
names += ('input-mouse',)
names += ('input-' + str(kind),)
theme = Gtk.IconTheme.get_default()
if theme.has_icon(name):
names += (name,)
source = Gtk.IconSource.new()
for n in names:
source.set_icon_name(n)
icon_set.add_source(source)
icon_set.names = names
return icon_set
def device_icon_file(name, kind=None):
icon_set = device_icon_set(name, kind)
assert icon_set
theme = Gtk.IconTheme.get_default()
return (theme.lookup_icon(str(name), 0, 0).get_filename() if name and theme.has_icon(str(name))
else None)
for n in reversed(icon_set.names):
if theme.has_icon(n):
return theme.lookup_icon(n, _LARGE_SIZE, 0).get_filename()
def icon_file(name, size=_LARGE_SIZE):
theme = Gtk.IconTheme.get_default()
if theme.has_icon(name):
return theme.lookup_icon(name, size, 0).get_filename()
def error(window, title, text):

View File

@ -3,7 +3,7 @@
#
# from sys import version as PYTTHON_VERSION
from gi.repository import (Gtk, Gdk)
from gi.repository import Gtk, Gdk
import ui
from solaar import NAME as _NAME

View File

@ -2,7 +2,7 @@
#
#
from gi.repository import (Gtk, Gdk, GObject)
from gi.repository import Gtk, Gdk, GObject
import ui
from logitech.unifying_receiver import status as _status
@ -12,6 +12,7 @@ _RECEIVER_ICON_SIZE = Gtk.IconSize.BUTTON
_DEVICE_ICON_SIZE = Gtk.IconSize.DIALOG
_STATUS_ICON_SIZE = Gtk.IconSize.LARGE_TOOLBAR
_PLACEHOLDER = '~'
_FALLBACK_ICON = 'preferences-desktop-peripherals'
#
#
@ -22,8 +23,8 @@ def _make_receiver_box(name):
frame._device = None
frame.set_name(name)
icon_name = ui.get_icon(name, 'preferences-desktop-peripherals')
icon = Gtk.Image.new_from_icon_name(icon_name, _RECEIVER_ICON_SIZE)
icon_set = ui.device_icon_set(name)
icon = Gtk.Image.new_from_icon_set(icon_set, _RECEIVER_ICON_SIZE)
icon.set_name('icon')
icon.set_padding(2, 2)
@ -34,6 +35,7 @@ def _make_receiver_box(name):
pairing_icon = Gtk.Image.new_from_icon_name('network-wireless', Gtk.IconSize.MENU)
pairing_icon.set_name('pairing-icon')
pairing_icon.set_tooltip_text('The pairing lock is open.')
pairing_icon._tick = 0
toolbar = Gtk.Toolbar()
toolbar.set_name('toolbar')
@ -80,8 +82,7 @@ def _make_device_box(index):
frame._device = None
frame.set_name(_PLACEHOLDER)
icon_name = 'preferences-desktop-peripherals'
icon = Gtk.Image.new_from_icon_name(icon_name, _DEVICE_ICON_SIZE)
icon = Gtk.Image.new_from_icon_name(_FALLBACK_ICON, _DEVICE_ICON_SIZE)
icon.set_name('icon')
icon.set_alignment(0.5, 0)
@ -279,7 +280,22 @@ def _update_receiver_box(frame, receiver):
if receiver:
frame._device = receiver
icon.set_sensitive(True)
pairing_icon.set_visible(receiver.status.lock_open)
if receiver.status.lock_open:
if pairing_icon._tick == 0:
def _tick(i, s):
if s and s.lock_open:
i.set_sensitive(bool(i._tick % 2))
i._tick += 1
return True
i.set_visible(False)
i.set_sensitive(True)
i._tick = 0
pairing_icon.set_visible(True)
GObject.timeout_add(1000, _tick, pairing_icon, receiver.status)
else:
pairing_icon.set_visible(False)
pairing_icon.set_sensitive(True)
pairing_icon._tick = 0
toolbar.set_visible(True)
else:
frame._device = None
@ -299,8 +315,8 @@ def _update_device_box(frame, dev):
if first_run:
frame._device = dev
frame.set_name(dev.name)
icon_name = ui.get_icon(dev.name, dev.kind)
icon.set_from_icon_name(icon_name, _DEVICE_ICON_SIZE)
icon_set = ui.device_icon_set(dev.name, dev.kind)
icon.set_from_icon_set(icon_set, _DEVICE_ICON_SIZE)
label.set_markup('<b>' + dev.name + '</b>')
status_icons = ui.find_children(frame, 'status').get_children()

View File

@ -10,18 +10,8 @@ try:
import ui
# necessary because the notifications daemon does not know about our XDG_DATA_DIRS
_icons = {}
def _icon(title):
if title not in _icons:
_icons[title] = ui.icon_file(title)
return _icons.get(title)
# assumed to be working since the import succeeded
available = True
_notifications = {}
@ -58,7 +48,10 @@ try:
message = reason or ('unpaired' if dev.status is None else
(str(dev.status) or ('connected' if dev.status else 'inactive')))
n.update(summary, message, _icon(summary) or str(dev.kind))
# we need to use the filename here because the notifications daemon
# is an external application that does not know about our icon sets
n.update(summary, message, ui.device_icon_file(dev.name, dev.kind))
urgency = Notify.Urgency.LOW if dev.status else Notify.Urgency.NORMAL
n.set_urgency(urgency)

View File

@ -2,13 +2,17 @@
#
#
import logging
from gi.repository import (Gtk, GObject)
from gi.repository import Gtk, GObject
from logging import getLogger, DEBUG as _DEBUG
_log = getLogger('pair-window')
del getLogger
import ui
from logitech.unifying_receiver import status as _status
_PAIRING_TIMEOUT = 15
_PAIRING_TIMEOUT = 30
def _create_page(assistant, kind, header=None, icon_name=None, text=None):
@ -43,16 +47,20 @@ def _create_page(assistant, kind, header=None, icon_name=None, text=None):
# def _fake_device(receiver):
# from logitech.unifying_receiver import PairedDevice
# dev = PairedDevice(receiver, 6)
# dev._wpid = '1234'
# dev._kind = 'touchpad'
# dev._codename = 'T650'
# dev._name = 'Wireless Rechargeable Touchpad T650'
# dev._serial = '0123456789'
# dev._protocol = 2.0
# dev.status = _status.DeviceStatus(dev, lambda *foo: None)
# dev.status['encrypted'] = False
# return dev
def _check_lock_state(assistant, receiver):
if not assistant.is_drawable():
if _log.isEnabledFor(_DEBUG):
_log.debug("assistant %s destroyed, bailing out", assistant)
return False
if receiver.status.get(_status.ERROR):
@ -72,7 +80,8 @@ def _check_lock_state(assistant, receiver):
def _prepare(assistant, page, receiver):
index = assistant.get_current_page()
# logging.debug("prepare %s %d %s", assistant, index, page)
if _log.isEnabledFor(_DEBUG):
_log.debug("prepare %s %d %s", assistant, index, page)
if index == 0:
if receiver.set_lock(False, timeout=_PAIRING_TIMEOUT):
@ -89,27 +98,20 @@ def _prepare(assistant, page, receiver):
def _finish(assistant, receiver):
logging.debug("finish %s", assistant)
if _log.isEnabledFor(_DEBUG):
_log.debug("finish %s", assistant)
assistant.destroy()
receiver.status.new_device = None
if receiver.status.lock_open:
receiver.set_lock()
def _cancel(assistant, receiver):
logging.debug("cancel %s", assistant)
assistant.destroy()
device, receiver.status.new_device = receiver.status.new_device, None
if device:
try:
del receiver[device.number]
except:
logging.error("failed to unpair %s", device)
if receiver.status.lock_open:
receiver.set_lock()
else:
receiver.status[_status.ERROR] = None
def _pairing_failed(assistant, receiver, error):
if _log.isEnabledFor(_DEBUG):
_log.debug("%s fail: %s", receiver, error)
assistant.commit()
header = 'Pairing failed: %s.' % error
@ -124,15 +126,22 @@ def _pairing_failed(assistant, receiver, error):
def _pairing_succeeded(assistant, receiver):
device = receiver.status.new_device
device, receiver.status.new_device = receiver.status.new_device, None
assert device
page = _create_page(assistant, Gtk.AssistantPageType.CONFIRM)
if _log.isEnabledFor(_DEBUG):
_log.debug("%s success: %s", receiver, device)
page = _create_page(assistant, Gtk.AssistantPageType.SUMMARY)
header = Gtk.Label('Found a new device:')
header.set_alignment(0.5, 0)
page.pack_start(header, False, False, 0)
device_icon = Gtk.Image()
device_icon.set_from_icon_name(ui.get_icon(device.name, device.kind), Gtk.IconSize.DIALOG)
device_icon.set_pixel_size(128)
icon_set = ui.device_icon_set(device.name, device.kind)
device_icon.set_from_icon_set(icon_set, Gtk.IconSize.LARGE)
device_icon.set_alignment(0.5, 1)
page.pack_start(device_icon, False, False, 0)
page.pack_start(device_icon, True, True, 0)
device_label = Gtk.Label()
device_label.set_markup('<b>' + device.name + '</b>')
@ -147,24 +156,12 @@ def _pairing_succeeded(assistant, receiver):
halign.add(hbox)
page.pack_start(halign, False, False, 0)
# hbox = Gtk.HBox(False, 8)
# hbox.pack_start(Gtk.Entry(), False, False, 0)
# hbox.pack_start(Gtk.ToggleButton(' Test '), False, False, 0)
# halign = Gtk.Alignment.new(0.5, 1, 0, 0)
# halign.add(hbox)
# page.pack_start(halign, True, True, 0)
# entry_info = Gtk.Label()
# entry_info.set_markup('<small>Use the controls above to confirm\n'
# 'this is the device you want to pair.</small>')
# entry_info.set_sensitive(False)
# entry_info.set_alignment(0.5, 0)
# page.pack_start(entry_info, True, True, 0)
page.pack_start(Gtk.Label(), True, True, 0)
page.show_all()
assistant.next_page()
assistant.set_page_complete(page, True)
assistant.commit()
def create(action, receiver):
@ -172,7 +169,7 @@ def create(action, receiver):
assistant.set_title(action.get_label())
assistant.set_icon_name(action.get_icon_name())
assistant.set_size_request(420, 260)
assistant.set_size_request(420, 240)
assistant.set_resizable(False)
assistant.set_role('pair-device')
@ -184,7 +181,7 @@ def create(action, receiver):
page_intro.pack_end(spinner, True, True, 24)
assistant.connect('prepare', _prepare, receiver)
assistant.connect('cancel', _cancel, receiver)
assistant.connect('cancel', _finish, receiver)
assistant.connect('close', _finish, receiver)
return assistant

View File

@ -34,9 +34,33 @@ def create(window, menu_actions=None):
return icon
_PIXMAPS = {}
def _icon_with_battery(s):
battery_icon = ui.get_battery_icon(s[_status.BATTERY_LEVEL])
name = '%s-%s' % (battery_icon, bool(s))
if name not in _PIXMAPS:
mask = ui.icon_file(ui.appicon(True) + '-mask', 128)
assert mask
mask = GdkPixbuf.Pixbuf.new_from_file(mask)
assert mask.get_width() == 128 and mask.get_height() == 128
battery = ui.icon_file(battery_icon, 128)
assert battery
battery = GdkPixbuf.Pixbuf.new_from_file(battery)
assert battery.get_width() == 128 and battery.get_height() == 128
if not s:
battery.saturate_and_pixelate(battery, 0, True)
# TODO can the masking be done at runtime?
battery.composite(mask, 0, 7, 80, 121, -32, 7, 1, 1, GdkPixbuf.InterpType.NEAREST, 255)
_PIXMAPS[name] = mask
return _PIXMAPS[name]
def update(icon, receiver, device=None):
# print "icon update", receiver, receiver._devices, device
battery_level = None
battery_status = None
lines = [ui.NAME + ': ' + str(receiver.status), '']
if receiver and receiver._devices:
@ -60,23 +84,12 @@ def update(icon, receiver, device=None):
lines.append('\t' + p)
lines.append('')
if battery_level is None:
battery_level = dev.status.get(_status.BATTERY_LEVEL)
if battery_status is None and dev.status.get(_status.BATTERY_LEVEL):
battery_status = dev.status
icon.set_tooltip_markup('\n'.join(lines).rstrip('\n'))
if battery_level is None:
if battery_status is None:
icon.set_from_icon_name(ui.appicon(receiver.status))
else:
appicon = ui.icon_file(ui.appicon(True) + '-mask')
assert appicon
pbuf = GdkPixbuf.Pixbuf.new_from_file(appicon)
assert pbuf.get_width() == 128 and pbuf.get_height() == 128
baticon = ui.icon_file(ui.get_battery_icon(battery_level))
assert baticon
pbuf2 = GdkPixbuf.Pixbuf.new_from_file(baticon)
assert pbuf2.get_width() == 128 and pbuf2.get_height() == 128
pbuf2.composite(pbuf, 0, 7, 80, 121, -32, 7, 1, 1, GdkPixbuf.InterpType.NEAREST, 255)
icon.set_from_pixbuf(pbuf)
icon.set_from_pixbuf(_icon_with_battery(battery_status))

View File

@ -10,8 +10,7 @@ necessary.
import os as _os
import errno as _errno
from select import select as _select
from pyudev import (Context as _Context,
Device as _Device)
from pyudev import Context as _Context, Device as _Device
native_implementation = 'udev'