diff --git a/lib/solaar/gtk.py b/lib/solaar/gtk.py index 84d15709..3a62491d 100644 --- a/lib/solaar/gtk.py +++ b/lib/solaar/gtk.py @@ -44,78 +44,67 @@ def _run(args): ui.notify.init() - window = ui.main_window.create(NAME) - assert window - icon = ui.status_icon.create(window) + icon = ui.status_icon.create(ui.main_window.toggle_all) assert icon listeners = {} - from logitech.unifying_receiver import base as _base - from solaar.listener import ReceiverListener def handle_receivers_events(action, device): assert action is not None assert device is not None + # whatever the action, stop any previous receivers at this path + l = listeners.pop(device.path, None) + if l is not None: + assert isinstance(l, ReceiverListener) + l.stop() + if action == 'add': # a new receiver device was detected - if not listeners: - # handle only one receiver for now, the rest are ignored - try: - l = ReceiverListener.open(device.path, status_changed) - if l is not None: - listeners[device.path] = l - except OSError: - # permission error, blacklist this path for now - listeners.pop(device.path, None) - import logging - logging.exception("failed to open %s", device.path) - # ui.error_dialog(window, 'Permissions error', - # 'Found a possible Unifying Receiver device,\n' - # 'but did not have permission to open it.') + try: + l = ReceiverListener.open(device.path, status_changed) + if l is not None: + listeners[device.path] = l + except OSError: + # permission error, blacklist this path for now + listeners.pop(device.path, None) + import logging + logging.exception("failed to open %s", device.path) + # ui.error_dialog(window, 'Permissions error', + # 'Found a possible Unifying Receiver device,\n' + # 'but did not have permission to open it.') - elif action == 'remove': - # we'll be receiving remove events for any hidraw devices, - # not just Logitech receivers, so it's okay if the device is not - # already in our listeners map - l = listeners.pop(device.path, None) - if l is not None: - assert isinstance(l, ReceiverListener) - l.stop() + # elif action == 'remove': + # # we'll be receiving remove events for any hidraw devices, + # # not just Logitech receivers, so it's okay if the device is not + # # already in our listeners map + # l = listeners.pop(device.path, None) + # if l is not None: + # l.stop() - # print ("****", action, device, listeners) + print ("****", action, device, listeners) # callback delivering status notifications from the receiver/devices to the UI from gi.repository import GLib from logitech.unifying_receiver.status import ALERT def status_changed(device, alert=ALERT.NONE, reason=None): assert device is not None - # print ("status changed", device, reason) + print ("status changed", device, reason) - if alert & ALERT.SHOW_WINDOW: - GLib.idle_add(window.present) - GLib.idle_add(ui.main_window.update, window, device) GLib.idle_add(ui.status_icon.update, icon, device) + GLib.idle_add(ui.main_window.update, device, alert & ALERT.SHOW_WINDOW) - if ui.notify.available: - if alert & ALERT.NOTIFICATION: - GLib.idle_add(ui.notify.show, device, reason) - elif device.kind is None and not device: - # notify when a receiver was removed - GLib.idle_add(ui.notify.show, device, reason) - - # if device.kind is None and not device: - # # a receiver was removed - # listeners.clear() + if alert & ALERT.NOTIFICATION: + GLib.idle_add(ui.notify.show, device, reason) # ugly... def _startup_check_receiver(): - from solaar.listener import DUMMY_RECEIVER if not listeners: - status_changed(DUMMY_RECEIVER, ALERT.NOTIFICATION) + ui.notify.alert('No receiver found.') GLib.timeout_add(1000, _startup_check_receiver) + from logitech.unifying_receiver import base as _base GLib.timeout_add(10, _base.notify_on_receivers, handle_receivers_events) from gi.repository import Gtk Gtk.main() diff --git a/lib/solaar/listener.py b/lib/solaar/listener.py index 50319be6..60deefe0 100644 --- a/lib/solaar/listener.py +++ b/lib/solaar/listener.py @@ -25,15 +25,13 @@ del namedtuple def _ghost(device): return _GHOST_DEVICE(number=device.number, name=device.name, kind=device.kind, status=None, max_devices=None) -DUMMY_RECEIVER = _GHOST_DEVICE(0xFF, 'Solaar', None, 'Receiver not found.', 0) - # # # # how often to poll devices that haven't updated their statuses on their own # (through notifications) -_POLL_TICK = 120 # seconds +_POLL_TICK = 100 # seconds class ReceiverListener(_listener.EventsListener): @@ -52,15 +50,16 @@ class ReceiverListener(_listener.EventsListener): _log.info("%s: notifications listener has started (%s)", self.receiver, self.receiver.handle) self.receiver.enable_notifications() self.receiver.notify_devices() - # self._status_changed(self.receiver, _status.ALERT.NOTIFICATION) + self._status_changed(self.receiver) #, _status.ALERT.NOTIFICATION) def has_stopped(self): - _log.info("%s: notifications listener has stopped", self.receiver) - if self.receiver: - self.receiver.enable_notifications(False) - self.receiver.close() - self.receiver = None - self.status_changed_callback(DUMMY_RECEIVER, _status.ALERT.NOTIFICATION) + r, self.receiver = self.receiver, None + _log.info("%s: notifications listener has stopped", r) + if r: + r.enable_notifications(False) + r.close() + r.status = 'The device was unplugged.' + self.status_changed_callback(r) #, _status.ALERT.NOTIFICATION) def tick(self, timestamp): if _log.isEnabledFor(_DEBUG): diff --git a/lib/solaar/ui/main_window.py b/lib/solaar/ui/main_window.py index 9e8762b4..084c2356 100644 --- a/lib/solaar/ui/main_window.py +++ b/lib/solaar/ui/main_window.py @@ -27,11 +27,11 @@ _MAX_DEVICES = 7 # # -def _make_receiver_box(): +def _make_receiver_box(receiver): frame = Gtk.Frame() - frame._device = None + frame._device = receiver - icon_set = _icons.device_icon_set() + icon_set = _icons.device_icon_set(receiver.name) icon = Gtk.Image.new_from_icon_set(icon_set, _RECEIVER_ICON_SIZE) icon.set_padding(2, 2) frame._icon = icon @@ -243,18 +243,15 @@ def _make_device_box(index): return frame -def hide(w, trigger): +def _hide(w): position = w.get_position() w.hide() w.move(*position) return True -def toggle(trigger, w): - if w.get_visible(): - return hide(w, trigger) - - if isinstance(trigger, Gtk.StatusIcon): +def _show(w, trigger=None): + if trigger and isinstance(trigger, Gtk.StatusIcon): x, y = w.get_position() if x == 0 and y == 0: # if the window hasn't been shown yet, position it next to the status icon @@ -264,25 +261,26 @@ def toggle(trigger, w): return True -def set_icon_name(window, icon_name): - icon_file = _icons.icon_file(icon_name) +# all created windows will be placed here, keyed by the receiver path +_windows = {} + +def _create(receiver): + window = Gtk.Window() + + window.set_title(NAME + ': ' + receiver.name) + icon_file = _icons.icon_file(_icons.APP_ICON[1]) if icon_file: window.set_icon_from_file(icon_file) else: - window.set_icon_name(icon_name) + window.set_icon_name(_icons.APP_ICON[1]) - -def create(title): - window = Gtk.Window() - window.set_title(title) - set_icon_name(window, _icons.APP_ICON[0]) window.set_role('status-window') window.set_type_hint(Gdk.WindowTypeHint.UTILITY) vbox = Gtk.VBox(homogeneous=False, spacing=12) vbox.set_border_width(4) - rbox = _make_receiver_box() + rbox = _make_receiver_box(receiver) vbox.add(rbox) for i in range(1, _MAX_DEVICES): dbox = _make_device_box(i) @@ -301,46 +299,57 @@ def create(title): window.set_skip_pager_hint(True) window.set_keep_above(True) # window.set_decorations(Gdk.DECOR_BORDER | Gdk.DECOR_TITLE) - window.connect('delete-event', hide) + window.connect('delete-event', _hide) + _windows[receiver.path] = window return window + +def _destroy(receiver): + w = _windows.pop(receiver.path, None) + if w: + w.destroy() + + +def toggle_all(trigger): + if not _windows: + return + + visible = [w.get_visible() for w in _windows.values()] + if all(visible): + map(_hide, _windows.values()) + else: + for w in _windows.values(): + if w.get_visible(): + _hide(w) + else: + _show(w, trigger) + # # # def _update_receiver_box(frame, receiver): + assert frame + assert receiver + frame._label.set_text(str(receiver.status)) - if receiver: - frame._device = receiver - icon_set = _icons.device_icon_set(receiver.name) - frame._icon.set_from_icon_set(icon_set, _RECEIVER_ICON_SIZE) - frame._icon.set_sensitive(True) - if receiver.status.lock_open: - if frame._pairing_icon._tick == 0: - def _pairing_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 - frame._pairing_icon.set_visible(True) - GLib.timeout_add(1000, _pairing_tick, frame._pairing_icon, receiver.status) - else: - frame._pairing_icon.set_visible(False) - frame._pairing_icon.set_sensitive(True) - frame._pairing_icon._tick = 0 - frame._toolbar.set_sensitive(True) + if receiver.status.lock_open: + if frame._pairing_icon._tick == 0: + def _pairing_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 + frame._pairing_icon.set_visible(True) + GLib.timeout_add(1000, _pairing_tick, frame._pairing_icon, receiver.status) else: - frame._device = None - frame._icon.set_from_icon_name('dialog-error', _RECEIVER_ICON_SIZE) - frame._icon.set_sensitive(False) frame._pairing_icon.set_visible(False) - frame._toolbar.set_sensitive(False) - frame._toolbar.get_children()[0].set_active(False) - frame._info_label.set_text('') + frame._pairing_icon.set_sensitive(True) + frame._pairing_icon._tick = 0 def _update_device_box(frame, dev): @@ -414,21 +423,25 @@ def _update_device_box(frame, dev): _config_panel.update(frame) -def update(window, device): +def update(device, popup=False): assert device is not None - # print ("main_window.update", device) + print ("main_window.update", device) - vbox = window.get_child() - frames = list(vbox.get_children()) + receiver = device if device.kind is None else device.receiver + w = _windows.get(receiver.path) + if receiver and not w: + w = _create(receiver) - if device.kind is None: - # update on the receiver - _update_receiver_box(frames[0], device) - if device: - set_icon_name(window, _icons.APP_ICON[1]) + if w: + if receiver: + if popup: + w.present() + vbox = w.get_child() + frames = list(vbox.get_children()) + + if device is receiver: + _update_receiver_box(frames[0], receiver) + else: + _update_device_box(frames[device.number], None if device.status is None else device) else: - for frame in frames[1:]: - _update_device_box(frame, None) - set_icon_name(window, _icons.APP_ICON[-1]) - else: - _update_device_box(frames[device.number], None if device.status is None else device) + _destroy(receiver) diff --git a/lib/solaar/ui/status_icon.py b/lib/solaar/ui/status_icon.py index 41587226..0dee2bd4 100644 --- a/lib/solaar/ui/status_icon.py +++ b/lib/solaar/ui/status_icon.py @@ -6,10 +6,8 @@ from __future__ import absolute_import, division, print_function, unicode_litera from gi.repository import Gtk, GdkPixbuf -from . import (action as _action, - icons as _icons, - main_window as _main_window) from solaar import NAME +from . import action as _action, icons as _icons from logitech.unifying_receiver import status as _status # @@ -18,7 +16,9 @@ from logitech.unifying_receiver import status as _status _NO_DEVICES = [None] * 6 -def create(window): + +def create(activate_callback): + assert activate_callback icon = Gtk.StatusIcon() icon.set_title(NAME) @@ -26,8 +26,8 @@ def create(window): icon.set_from_icon_name(_icons.APP_ICON[0]) icon._devices = list(_NO_DEVICES) - icon.connect('activate', _main_window.toggle, window) icon.set_tooltip_text(NAME) + icon.connect('activate', activate_callback) menu = Gtk.Menu()