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