From 14663ca2042c2569c7dd671c816f3b77ab148672 Mon Sep 17 00:00:00 2001 From: Daniel Pavel Date: Fri, 30 Nov 2012 15:23:16 +0200 Subject: [PATCH] re-wrote loading of icons for devices --- app/listener.py | 64 ++++++++++++++++++++---------------- app/ui/__init__.py | 67 ++++++++++++++++++++++++++++++-------- app/ui/action.py | 2 +- app/ui/main_window.py | 32 +++++++++++++----- app/ui/notify.py | 15 +++------ app/ui/pair_window.py | 75 +++++++++++++++++++++---------------------- app/ui/status_icon.py | 45 +++++++++++++++++--------- lib/hidapi/udev.py | 3 +- 8 files changed, 184 insertions(+), 119 deletions(-) diff --git a/app/listener.py b/app/listener.py index 6e80a166..d24ba9b1 100644 --- a/app/listener.py +++ b/app/listener.py @@ -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 '' % (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 diff --git a/app/ui/__init__.py b/app/ui/__init__.py index d91b5d02..fb09b31f 100644 --- a/app/ui/__init__.py +++ b/app/ui/__init__.py @@ -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): diff --git a/app/ui/action.py b/app/ui/action.py index 355320ec..7e9c5aa1 100644 --- a/app/ui/action.py +++ b/app/ui/action.py @@ -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 diff --git a/app/ui/main_window.py b/app/ui/main_window.py index 8d12a828..79dc14d8 100644 --- a/app/ui/main_window.py +++ b/app/ui/main_window.py @@ -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('' + dev.name + '') status_icons = ui.find_children(frame, 'status').get_children() diff --git a/app/ui/notify.py b/app/ui/notify.py index f2615b31..b6ca790e 100644 --- a/app/ui/notify.py +++ b/app/ui/notify.py @@ -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) diff --git a/app/ui/pair_window.py b/app/ui/pair_window.py index ac225e2e..81683192 100644 --- a/app/ui/pair_window.py +++ b/app/ui/pair_window.py @@ -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('' + device.name + '') @@ -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('Use the controls above to confirm\n' - # 'this is the device you want to pair.') - # 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 diff --git a/app/ui/status_icon.py b/app/ui/status_icon.py index 39f7a7b0..4e8098a0 100644 --- a/app/ui/status_icon.py +++ b/app/ui/status_icon.py @@ -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)) diff --git a/lib/hidapi/udev.py b/lib/hidapi/udev.py index 600a190b..7a931ae7 100644 --- a/lib/hidapi/udev.py +++ b/lib/hidapi/udev.py @@ -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'