tweaked the ui a bit
This commit is contained in:
parent
e52bfe53a5
commit
f295d1d90e
|
@ -7,8 +7,7 @@ __version__ = '0.4'
|
|||
#
|
||||
|
||||
import logging
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import GObject
|
||||
from gi.repository import (Gtk, GObject)
|
||||
|
||||
from logitech.devices import constants as C
|
||||
|
||||
|
@ -19,32 +18,20 @@ from watcher import Watcher
|
|||
APP_TITLE = 'Solaar'
|
||||
|
||||
|
||||
def _status_updated(watcher, icon, window):
|
||||
while True:
|
||||
watcher.status_changed.wait()
|
||||
text = watcher.status_text
|
||||
watcher.status_changed.clear()
|
||||
|
||||
icon_name = APP_TITLE + '-fail' if watcher.rstatus.code < C.STATUS.CONNECTED else APP_TITLE
|
||||
|
||||
if icon:
|
||||
GObject.idle_add(ui.icon.update, icon, watcher.rstatus, text, icon_name)
|
||||
|
||||
if window:
|
||||
GObject.idle_add(ui.window.update, window, watcher.rstatus, dict(watcher.devices), icon_name)
|
||||
def _notify(status_code, title, text=''):
|
||||
if text:
|
||||
ui.notify.show(status_code, title, text)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import argparse
|
||||
arg_parser = argparse.ArgumentParser(prog=APP_TITLE)
|
||||
arg_parser.add_argument('-v', '--verbose', action='count', default=0,
|
||||
help='increase the logger verbosity')
|
||||
arg_parser.add_argument('-N', '--disable-notifications', action='store_false', dest='notifications',
|
||||
help='disable desktop notifications')
|
||||
arg_parser.add_argument('-H', '--start-hidden', action='store_true', dest='start_hidden',
|
||||
help='hide the application window on start')
|
||||
arg_parser.add_argument('-t', '--close-to-tray', action='store_true',
|
||||
help='closing the application window hides it instead of terminating the application')
|
||||
help='increase the logger verbosity (may be repeated)')
|
||||
arg_parser.add_argument('-s', '--systray', action='store_true',
|
||||
help='embed the application into the systray')
|
||||
arg_parser.add_argument('-N', '--no-notifications', action='store_false', dest='notifications',
|
||||
help='disable desktop notifications (if systray is enabled)')
|
||||
arg_parser.add_argument('-V', '--version', action='version', version='%(prog)s ' + __version__)
|
||||
args = arg_parser.parse_args()
|
||||
|
||||
|
@ -54,23 +41,34 @@ if __name__ == '__main__':
|
|||
|
||||
GObject.threads_init()
|
||||
|
||||
ui.notify.init(APP_TITLE, args.notifications)
|
||||
args.notifications = args.notifications and args.systray
|
||||
if args.notifications:
|
||||
ui.notify.init(APP_TITLE)
|
||||
|
||||
watcher = Watcher(ui.notify.show)
|
||||
tray_icon = None
|
||||
window = None
|
||||
|
||||
def _status_changed(text, rstatus, devices):
|
||||
icon_name = APP_TITLE + '-fail' if rstatus.code < C.STATUS.CONNECTED else APP_TITLE
|
||||
|
||||
if tray_icon:
|
||||
GObject.idle_add(ui.icon.update, tray_icon, rstatus, text, icon_name)
|
||||
|
||||
if window:
|
||||
GObject.idle_add(ui.window.update, window, rstatus, devices, icon_name)
|
||||
|
||||
watcher = Watcher(_status_changed, _notify if args.notifications else None)
|
||||
watcher.start()
|
||||
|
||||
window = ui.window.create(APP_TITLE, watcher.rstatus, not args.start_hidden, args.close_to_tray)
|
||||
window = ui.window.create(APP_TITLE, watcher.rstatus, args.systray)
|
||||
window.set_icon_name(APP_TITLE + '-fail')
|
||||
|
||||
tray_icon = ui.icon.create(APP_TITLE, (ui.window.toggle, window))
|
||||
tray_icon.set_from_icon_name(APP_TITLE + '-fail')
|
||||
|
||||
import threading
|
||||
ui_update_thread = threading.Thread(target=_status_updated, name='ui_update', args=(watcher, tray_icon, window))
|
||||
ui_update_thread.daemon = True
|
||||
ui_update_thread.start()
|
||||
if args.systray:
|
||||
tray_icon = ui.icon.create(APP_TITLE, (ui.window.toggle, window))
|
||||
tray_icon.set_from_icon_name(APP_TITLE + '-fail')
|
||||
|
||||
Gtk.main()
|
||||
|
||||
watcher.stop()
|
||||
ui.notify.set_active(False)
|
||||
if args.notifications:
|
||||
ui.notify.set_active(False)
|
||||
|
|
|
@ -11,7 +11,6 @@ try:
|
|||
available = True # assumed to be working since the import succeeded
|
||||
_active = False # not yet active
|
||||
_app_title = None
|
||||
_notifications = {}
|
||||
|
||||
|
||||
def init(app_title, active=True):
|
||||
|
@ -34,14 +33,6 @@ try:
|
|||
available = False
|
||||
else:
|
||||
if _active:
|
||||
for n in list(_notifications.values()):
|
||||
try:
|
||||
n.close()
|
||||
except Exception:
|
||||
logging.exception("closing open notification %s", n)
|
||||
# DBUS
|
||||
pass
|
||||
_notifications.clear()
|
||||
try:
|
||||
_notify.uninit()
|
||||
except:
|
||||
|
@ -57,31 +48,15 @@ try:
|
|||
|
||||
def show(status_code, title, text='', icon=None):
|
||||
"""Show a notification with title and text."""
|
||||
if not available or not _active:
|
||||
return
|
||||
|
||||
if title in _notifications:
|
||||
notification = _notifications[title]
|
||||
else:
|
||||
_notifications[title] = notification = _notify.Notification(title)
|
||||
|
||||
if text == notification.message:
|
||||
# there's no need to show the same notification twice in a row
|
||||
return
|
||||
|
||||
icon = icon or title
|
||||
notification.update(title, text, title)
|
||||
try:
|
||||
if text:
|
||||
if available and _active:
|
||||
notification = _notify.Notification(title, text, icon or title)
|
||||
try:
|
||||
notification.show()
|
||||
else:
|
||||
notification.close()
|
||||
except Exception:
|
||||
logging.exception("showing notification %s", notification)
|
||||
except Exception:
|
||||
logging.exception("showing notification %s", notification)
|
||||
|
||||
|
||||
except ImportError:
|
||||
logging.exception("ouch")
|
||||
logging.warn("python-notify2 not found, desktop notifications are disabled")
|
||||
available = False
|
||||
active = False
|
||||
|
|
|
@ -10,8 +10,6 @@ def create(parent_window, title):
|
|||
|
||||
Gtk.Window.set_default_icon_name('add')
|
||||
window.set_resizable(False)
|
||||
|
||||
# window.set_wmclass(title, 'status-window')
|
||||
# window.set_role('pair')
|
||||
# window.set_role('pair-device')
|
||||
|
||||
return window
|
||||
|
|
236
app/ui/window.py
236
app/ui/window.py
|
@ -2,79 +2,106 @@
|
|||
#
|
||||
#
|
||||
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gdk
|
||||
from gi.repository import (Gtk, Gdk)
|
||||
|
||||
from logitech.devices import constants as C
|
||||
|
||||
|
||||
_DEVICE_ICON_SIZE = Gtk.IconSize.DIALOG
|
||||
_DEVICE_ICON_SIZE = Gtk.IconSize.DND
|
||||
_STATUS_ICON_SIZE = Gtk.IconSize.DIALOG
|
||||
_PLACEHOLDER = '~'
|
||||
_MAX_DEVICES = 6
|
||||
|
||||
|
||||
def _find_children(container, *child_names):
|
||||
def _iterate_children(widget, names, result, count):
|
||||
wname = widget.get_name()
|
||||
if wname in names:
|
||||
index = names.index(wname)
|
||||
names[index] = None
|
||||
result[index] = widget
|
||||
count -= 1
|
||||
|
||||
if count > 0 and isinstance(widget, Gtk.Container):
|
||||
for w in widget:
|
||||
count = _iterate_children(w, names, result, count)
|
||||
if count == 0:
|
||||
break
|
||||
|
||||
return count
|
||||
|
||||
names = list(child_names)
|
||||
count = len(names)
|
||||
result = [None] * count
|
||||
_iterate_children(container, names, result, count)
|
||||
return result if count > 1 else result[0]
|
||||
|
||||
|
||||
def _update_receiver_box(box, receiver):
|
||||
icon, vbox = box.get_children()
|
||||
label, buttons_box = vbox.get_children()
|
||||
label, buttons_box = _find_children(box, 'receiver-status', 'receiver-buttons')
|
||||
label.set_text(receiver.text or '')
|
||||
buttons_box.set_visible(receiver.code >= C.STATUS.CONNECTED)
|
||||
|
||||
|
||||
def _update_device_box(frame, devstatus):
|
||||
frame.set_visible(devstatus is not None)
|
||||
if devstatus is None:
|
||||
frame.set_visible(False)
|
||||
frame.set_name(_PLACEHOLDER)
|
||||
return
|
||||
|
||||
box = frame.get_child()
|
||||
icon, expander = box.get_children()
|
||||
frame.set_visible(True)
|
||||
if frame.get_name() != devstatus.name:
|
||||
frame.set_name(devstatus.name)
|
||||
icon = _find_children(frame, 'device-icon')
|
||||
icon.set_from_icon_name(devstatus.name, _DEVICE_ICON_SIZE)
|
||||
icon.set_tooltip_text(devstatus.name)
|
||||
|
||||
if devstatus:
|
||||
if icon.get_name() != devstatus.name:
|
||||
icon.set_name(devstatus.name)
|
||||
icon.set_from_icon_name(devstatus.name, _DEVICE_ICON_SIZE)
|
||||
expander = _find_children(frame, 'device-expander')
|
||||
if devstatus.code < C.STATUS.CONNECTED:
|
||||
expander.set_sensitive(False)
|
||||
expander.set_expanded(False)
|
||||
expander.set_label('<big><b>%s</b></big>\n%s' % (devstatus.name, devstatus.text))
|
||||
return
|
||||
|
||||
if devstatus.code < C.STATUS.CONNECTED:
|
||||
expander.set_sensitive(False)
|
||||
expander.set_expanded(False)
|
||||
expander.set_label('<big><b>%s</b></big>\n%s' % (devstatus.name, devstatus.text))
|
||||
else:
|
||||
expander.set_sensitive(True)
|
||||
ebox = expander.get_child()
|
||||
expander.set_sensitive(True)
|
||||
status_icons = expander.get_child().get_children()
|
||||
|
||||
texts = []
|
||||
texts = []
|
||||
|
||||
light_icon = ebox.get_children()[-2]
|
||||
light_level = getattr(devstatus, C.PROPS.LIGHT_LEVEL, None)
|
||||
light_icon.set_visible(light_level is not None)
|
||||
if light_level is not None:
|
||||
texts.append('Light: %d lux' % light_level)
|
||||
icon_name = 'light_%02d' % (20 * ((light_level + 50) // 100))
|
||||
light_icon.set_from_icon_name(icon_name, _STATUS_ICON_SIZE)
|
||||
light_icon.set_tooltip_text(texts[-1])
|
||||
|
||||
battery_icon = ebox.get_children()[-1]
|
||||
battery_level = getattr(devstatus, C.PROPS.BATTERY_LEVEL, None)
|
||||
battery_icon.set_sensitive(battery_level is not None)
|
||||
if battery_level is None:
|
||||
battery_icon.set_from_icon_name('battery_unknown', _STATUS_ICON_SIZE)
|
||||
battery_icon.set_tooltip_text('Battery: unknown')
|
||||
else:
|
||||
texts.append('Battery: %d%%' % battery_level)
|
||||
icon_name = 'battery_%02d' % (20 * ((battery_level + 10) // 20))
|
||||
battery_icon.set_from_icon_name(icon_name, _STATUS_ICON_SIZE)
|
||||
battery_icon.set_tooltip_text(texts[-1])
|
||||
|
||||
battery_status = getattr(devstatus, C.PROPS.BATTERY_STATUS, None)
|
||||
if battery_status is not None:
|
||||
texts.append(battery_status)
|
||||
battery_icon.set_tooltip_text(battery_icon.get_tooltip_text() + '\n' + battery_status)
|
||||
|
||||
if texts:
|
||||
expander.set_label('<big><b>%s</b></big>\n%s' % (devstatus.name, ', '.join(texts)))
|
||||
else:
|
||||
expander.set_label('<big><b>%s</b></big>\n%s' % (devstatus.name, devstatus.text))
|
||||
light_icon = status_icons[-2]
|
||||
light_level = getattr(devstatus, C.PROPS.LIGHT_LEVEL, None)
|
||||
if light_level is None:
|
||||
light_icon.set_visible(False)
|
||||
else:
|
||||
icon.set_name(_PLACEHOLDER)
|
||||
expander.set_label(_PLACEHOLDER)
|
||||
light_icon.set_visible(True)
|
||||
icon_name = 'light_%02d' % (20 * ((light_level + 50) // 100))
|
||||
light_icon.set_from_icon_name(icon_name, _STATUS_ICON_SIZE)
|
||||
tooltip = 'Light: %d lux' % light_level
|
||||
light_icon.set_tooltip_text(tooltip)
|
||||
texts.append(tooltip)
|
||||
|
||||
battery_icon = status_icons[-1]
|
||||
battery_level = getattr(devstatus, C.PROPS.BATTERY_LEVEL, None)
|
||||
if battery_level is None:
|
||||
battery_icon.set_sensitive(False)
|
||||
battery_icon.set_from_icon_name('battery_unknown', _STATUS_ICON_SIZE)
|
||||
battery_icon.set_tooltip_text('Battery: unknown')
|
||||
else:
|
||||
battery_icon.set_sensitive(True)
|
||||
icon_name = 'battery_%02d' % (20 * ((battery_level + 10) // 20))
|
||||
battery_icon.set_from_icon_name(icon_name, _STATUS_ICON_SIZE)
|
||||
tooltip = 'Battery: %d%%' % battery_level
|
||||
battery_icon.set_tooltip_text(tooltip)
|
||||
texts.append(tooltip)
|
||||
|
||||
battery_status = getattr(devstatus, C.PROPS.BATTERY_STATUS, None)
|
||||
if battery_status is not None:
|
||||
texts.append(battery_status)
|
||||
battery_icon.set_tooltip_text(battery_icon.get_tooltip_text() + '\n' + battery_status)
|
||||
|
||||
if texts:
|
||||
expander.set_label('<big><b>%s</b></big>\n%s' % (devstatus.name, ', '.join(texts)))
|
||||
else:
|
||||
expander.set_label('<big><b>%s</b></big>\n%s' % (devstatus.name, devstatus.text))
|
||||
|
||||
|
||||
def update(window, receiver, devices, icon_name=None):
|
||||
|
@ -84,31 +111,30 @@ def update(window, receiver, devices, icon_name=None):
|
|||
|
||||
controls = list(window.get_child().get_children())
|
||||
_update_receiver_box(controls[0], receiver)
|
||||
for index in range(1, 1 + _MAX_DEVICES):
|
||||
for index in range(1, len(controls)):
|
||||
_update_device_box(controls[index], devices.get(index))
|
||||
|
||||
|
||||
def _receiver_box(rstatus):
|
||||
box = Gtk.HBox(homogeneous=False, spacing=8)
|
||||
box.set_border_width(8)
|
||||
box.set_border_width(4)
|
||||
|
||||
icon = Gtk.Image.new_from_icon_name(rstatus.name, _DEVICE_ICON_SIZE)
|
||||
icon.set_alignment(0.5, 0)
|
||||
icon.set_name(rstatus.name)
|
||||
icon.set_tooltip_text(rstatus.name)
|
||||
box.pack_start(icon, False, False, 0)
|
||||
|
||||
vbox = Gtk.VBox(homogeneous=False, spacing=4)
|
||||
box.pack_start(vbox, True, True, 0)
|
||||
label = Gtk.Label('Initializing...')
|
||||
label.set_alignment(0, 0.5)
|
||||
label.set_name('receiver-status')
|
||||
box.pack_start(label, True, True, 0)
|
||||
|
||||
label = Gtk.Label()
|
||||
label.set_can_focus(False)
|
||||
label.set_alignment(0, 0)
|
||||
vbox.pack_start(label, False, False, 0)
|
||||
|
||||
buttons_box = Gtk.HButtonBox()
|
||||
buttons_box.set_spacing(8)
|
||||
buttons_box.set_layout(Gtk.ButtonBoxStyle.START)
|
||||
vbox.pack_start(buttons_box, True, True, 0)
|
||||
toolbar = Gtk.Toolbar()
|
||||
toolbar.set_style(Gtk.ToolbarStyle.ICONS)
|
||||
toolbar.set_name('receiver-buttons')
|
||||
toolbar.set_show_arrow(False)
|
||||
toolbar.set_icon_size(Gtk.IconSize.BUTTON)
|
||||
box.pack_end(toolbar, False, False, 0)
|
||||
|
||||
def _action(button, function, params):
|
||||
button.set_sensitive(False)
|
||||
|
@ -116,51 +142,46 @@ def _receiver_box(rstatus):
|
|||
button.set_sensitive(True)
|
||||
|
||||
def _add_button(name, icon, action):
|
||||
button = Gtk.Button(name.split(' ')[0])
|
||||
button.set_image(Gtk.Image.new_from_icon_name(icon, Gtk.IconSize.BUTTON))
|
||||
button.set_relief(Gtk.ReliefStyle.HALF)
|
||||
button = Gtk.ToolButton()
|
||||
button.set_icon_name(icon)
|
||||
button.set_tooltip_text(name)
|
||||
button.set_focus_on_click(False)
|
||||
if action:
|
||||
function = action[0]
|
||||
params = action[1:]
|
||||
button.connect('clicked', _action, function, params)
|
||||
else:
|
||||
button.set_sensitive(False)
|
||||
buttons_box.pack_start(button, False, False, 0)
|
||||
toolbar.insert(button, -1)
|
||||
|
||||
_add_button('Scan for devices', 'reload', rstatus.refresh)
|
||||
_add_button('Pair new device', 'add', rstatus.pair)
|
||||
|
||||
box.show_all()
|
||||
toolbar.set_visible(False)
|
||||
return box
|
||||
|
||||
|
||||
def _device_box():
|
||||
box = Gtk.HBox(homogeneous=False, spacing=8)
|
||||
box.set_border_width(4)
|
||||
|
||||
icon = Gtk.Image()
|
||||
icon.set_alignment(0.5, 0)
|
||||
icon.set_name(_PLACEHOLDER)
|
||||
|
||||
box = Gtk.HBox(homogeneous=False, spacing=8)
|
||||
icon.set_name('device-icon')
|
||||
box.pack_start(icon, False, False, 0)
|
||||
box.set_border_width(8)
|
||||
|
||||
expander = Gtk.Expander()
|
||||
expander.set_can_focus(False)
|
||||
expander.set_label(_PLACEHOLDER)
|
||||
expander.set_use_markup(True)
|
||||
expander.set_spacing(4)
|
||||
expander.set_name('device-expander')
|
||||
box.pack_start(expander, True, True, 1)
|
||||
|
||||
ebox = Gtk.HBox(False, 8)
|
||||
|
||||
battery_icon = Gtk.Image.new_from_icon_name('battery_unknown', _STATUS_ICON_SIZE)
|
||||
ebox.pack_end(battery_icon, False, True, 0)
|
||||
|
||||
light_icon = Gtk.Image.new_from_icon_name('light_unknown', _STATUS_ICON_SIZE)
|
||||
ebox.pack_end(light_icon, False, True, 0)
|
||||
|
||||
expander.add(ebox)
|
||||
box.pack_start(expander, True, True, 1)
|
||||
|
||||
frame = Gtk.Frame()
|
||||
frame.add(box)
|
||||
|
@ -169,57 +190,46 @@ def _device_box():
|
|||
return frame
|
||||
|
||||
|
||||
def create(title, rstatus, show=True, close_to_tray=False):
|
||||
def create(title, rstatus, systray=False):
|
||||
window = Gtk.Window()
|
||||
|
||||
window.set_title(title)
|
||||
|
||||
window.set_keep_above(True)
|
||||
window.set_deletable(False)
|
||||
window.set_resizable(False)
|
||||
# window.set_size_request(200, 50)
|
||||
window.set_default_size(200, 50)
|
||||
|
||||
window.set_position(Gtk.WindowPosition.MOUSE)
|
||||
window.set_type_hint(Gdk.WindowTypeHint.UTILITY)
|
||||
|
||||
# window.set_skip_taskbar_hint(True)
|
||||
# window.set_skip_pager_hint(True)
|
||||
# window.set_wmclass(title, 'status-window')
|
||||
# window.set_role('status-window')
|
||||
window.set_role('status-window')
|
||||
|
||||
vbox = Gtk.VBox(homogeneous=False, spacing=4)
|
||||
vbox.set_border_width(4)
|
||||
|
||||
vbox.add(_receiver_box(rstatus))
|
||||
for i in range(1, 1 + _MAX_DEVICES):
|
||||
for i in range(1, 1 + rstatus.max_devices):
|
||||
vbox.add(_device_box())
|
||||
vbox.set_visible(True)
|
||||
window.add(vbox)
|
||||
|
||||
if close_to_tray:
|
||||
def _state_event(window, event):
|
||||
if event.new_window_state & Gdk.WindowState.ICONIFIED:
|
||||
position = window.get_position()
|
||||
window.hide()
|
||||
window.deiconify()
|
||||
window.move(*position)
|
||||
return True
|
||||
geometry = Gdk.Geometry()
|
||||
geometry.min_width = 300
|
||||
geometry.min_height = 40
|
||||
window.set_geometry_hints(vbox, geometry, Gdk.WindowHints.MIN_SIZE)
|
||||
|
||||
window.connect('window-state-event', _state_event)
|
||||
window.connect('delete-event', lambda w, e: toggle(None, window) or True)
|
||||
window.set_resizable(False)
|
||||
window.set_default_size(geometry.min_width, geometry.min_height)
|
||||
|
||||
if systray:
|
||||
window.set_keep_above(True)
|
||||
window.set_deletable(False)
|
||||
window.set_decorated(False)
|
||||
window.set_position(Gtk.WindowPosition.MOUSE)
|
||||
window.set_type_hint(Gdk.WindowTypeHint.MENU)
|
||||
window.set_skip_taskbar_hint(True)
|
||||
window.set_skip_pager_hint(True)
|
||||
else:
|
||||
window.set_position(Gtk.WindowPosition.CENTER)
|
||||
window.connect('delete-event', Gtk.main_quit)
|
||||
|
||||
if show:
|
||||
window.present()
|
||||
|
||||
return window
|
||||
|
||||
|
||||
def toggle(_, window):
|
||||
if window.get_visible():
|
||||
position = window.get_position()
|
||||
window.hide()
|
||||
window.move(*position)
|
||||
else:
|
||||
window.present()
|
||||
|
|
121
app/watcher.py
121
app/watcher.py
|
@ -2,9 +2,9 @@
|
|||
#
|
||||
#
|
||||
|
||||
import logging
|
||||
import threading
|
||||
from threading import Thread
|
||||
import time
|
||||
from logging import getLogger as _Logger
|
||||
|
||||
from logitech.unifying_receiver import api
|
||||
from logitech.unifying_receiver.listener import EventsListener
|
||||
|
@ -14,11 +14,11 @@ from logitech.devices import constants as C
|
|||
import actions
|
||||
|
||||
|
||||
_l = logging.getLogger('watcher')
|
||||
_l = _Logger('watcher')
|
||||
|
||||
|
||||
_STATUS_TIMEOUT = 31 # seconds
|
||||
_THREAD_SLEEP = 2 # seconds
|
||||
_STATUS_TIMEOUT = 61 # seconds
|
||||
_THREAD_SLEEP = 3 # seconds
|
||||
_SLEEP_QUANT = 0.33 # seconds
|
||||
|
||||
_UNIFYING_RECEIVER = 'Unifying Receiver'
|
||||
_NO_RECEIVER = 'Receiver not found.'
|
||||
|
@ -38,20 +38,21 @@ class _DevStatus(api.AttachedDeviceInfo):
|
|||
return 'DevStatus(%d,%s,%d)' % (self.number, self.name, self.code)
|
||||
|
||||
|
||||
class Watcher(threading.Thread):
|
||||
class Watcher(Thread):
|
||||
"""Keeps a map of all attached devices and their statuses."""
|
||||
def __init__(self, notify_callback=None):
|
||||
def __init__(self, status_changed_callback, notify_callback=None):
|
||||
super(Watcher, self).__init__(group='Solaar', name='Watcher')
|
||||
self.daemon = True
|
||||
self.active = False
|
||||
|
||||
self.notify = notify_callback
|
||||
self.status_text = None
|
||||
self.status_changed = threading.Event()
|
||||
self.status_changed_callback = status_changed_callback
|
||||
|
||||
self.listener = None
|
||||
|
||||
self.rstatus = _DevStatus(0, 0xFF, None, _UNIFYING_RECEIVER, None, None)
|
||||
self.rstatus.max_devices = api.C.MAX_ATTACHED_DEVICES
|
||||
self.rstatus.refresh = (actions.full_scan, self)
|
||||
self.rstatus.pair = None # (actions.pair, self)
|
||||
|
||||
|
@ -62,7 +63,6 @@ class Watcher(threading.Thread):
|
|||
|
||||
while self.active:
|
||||
if self.listener is None:
|
||||
self._device_status_changed(self.rstatus, (C.STATUS.UNKNOWN, _SCANNING))
|
||||
self._update_status_text()
|
||||
|
||||
receiver = api.open()
|
||||
|
@ -77,13 +77,17 @@ class Watcher(threading.Thread):
|
|||
|
||||
self.listener = EventsListener(receiver, self._events_callback)
|
||||
self.listener.start()
|
||||
|
||||
# need to wait for the thread to come alive
|
||||
time.sleep(_SLEEP_QUANT / 2)
|
||||
elif not self.listener:
|
||||
self.listener = None
|
||||
self.devices.clear()
|
||||
|
||||
if self.listener:
|
||||
if self.devices:
|
||||
update_icon = self._check_old_statuses()
|
||||
update_icon = self._device_status_changed(self.rstatus, (C.STATUS.CONNECTED, _OKAY))
|
||||
update_icon |= self._check_old_statuses()
|
||||
else:
|
||||
update_icon = self._device_status_changed(self.rstatus, (C.STATUS.CONNECTED, _NO_DEVICES))
|
||||
else:
|
||||
|
@ -92,8 +96,11 @@ class Watcher(threading.Thread):
|
|||
if update_icon:
|
||||
self._update_status_text()
|
||||
|
||||
if self.active:
|
||||
time.sleep(_THREAD_SLEEP)
|
||||
for i in range(0, int(_THREAD_SLEEP / _SLEEP_QUANT)):
|
||||
if self.active:
|
||||
time.sleep(_SLEEP_QUANT)
|
||||
else:
|
||||
break
|
||||
|
||||
if self.listener:
|
||||
self.listener.stop()
|
||||
|
@ -137,6 +144,46 @@ class Watcher(threading.Thread):
|
|||
self._device_status_changed(self.rstatus, (C.STATUS.CONNECTED, _OKAY))
|
||||
return devstatus
|
||||
|
||||
def _device_status_changed(self, devstatus, status):
|
||||
if status is None:
|
||||
return False
|
||||
|
||||
old_status_code = devstatus.code
|
||||
devstatus.timestamp = time.time()
|
||||
|
||||
if type(status) == int:
|
||||
status_code = status
|
||||
if status_code in C.STATUS_NAME:
|
||||
status_text = C.STATUS_NAME[status_code]
|
||||
else:
|
||||
status_code = status[0]
|
||||
if isinstance(status[1], str):
|
||||
status_text = status[1]
|
||||
elif isinstance(status[1], dict):
|
||||
status_text = ''
|
||||
for key, value in status[1].items():
|
||||
if key == 'text':
|
||||
status_text = value
|
||||
else:
|
||||
setattr(devstatus, key, value)
|
||||
else:
|
||||
_l.warn("don't know how to handle status %s", status)
|
||||
return False
|
||||
|
||||
if ((status_code == old_status_code and status_text == devstatus.text) or
|
||||
(status_code == C.STATUS.CONNECTED and old_status_code > C.STATUS.CONNECTED)):
|
||||
# this is just successful ping for a device with an already known status
|
||||
return False
|
||||
|
||||
devstatus.code = status_code
|
||||
devstatus.text = status_text
|
||||
_l.debug("%s update %s => %s: %s", devstatus, old_status_code, status_code, status_text)
|
||||
|
||||
if self.notify:
|
||||
self.notify(devstatus.code, devstatus.name, devstatus.text)
|
||||
|
||||
return True
|
||||
|
||||
def _events_callback(self, code, devnumber, data):
|
||||
# _l.debug("event %s", (code, devnumber, data))
|
||||
|
||||
|
@ -161,47 +208,6 @@ class Watcher(threading.Thread):
|
|||
if updated:
|
||||
self._update_status_text()
|
||||
|
||||
def _device_status_changed(self, devstatus, status=None):
|
||||
if status is None:
|
||||
return False
|
||||
|
||||
old_status_code = devstatus.code
|
||||
devstatus.timestamp = time.time()
|
||||
|
||||
if type(status) == int:
|
||||
status_code = status
|
||||
if status_code in C.STATUS_NAME:
|
||||
status_text = C.STATUS_NAME[status_code]
|
||||
else:
|
||||
status_code = status[0]
|
||||
if isinstance(status[1], str):
|
||||
status_text = status[1]
|
||||
elif isinstance(status[1], dict):
|
||||
status_text = ''
|
||||
for key, value in status[1].items():
|
||||
if key == 'text':
|
||||
status_text = value
|
||||
else:
|
||||
setattr(devstatus, key, value)
|
||||
else:
|
||||
status_code = C.STATUS.UNKNOWN
|
||||
status_text = ''
|
||||
|
||||
if ((status_code == old_status_code and status_text == devstatus.text) or
|
||||
(status_code == C.STATUS.CONNECTED and old_status_code > C.STATUS.CONNECTED)):
|
||||
# this is just successful ping for a device with an already known status
|
||||
return False
|
||||
|
||||
devstatus.code = status_code
|
||||
devstatus.text = status_text
|
||||
_l.debug("%s update %s => %s: %s", devstatus, old_status_code, status_code, status_text)
|
||||
|
||||
if self.notify:
|
||||
if status_code < C.STATUS.CONNECTED or old_status_code < C.STATUS.CONNECTED or status_code < old_status_code:
|
||||
self.notify(devstatus.code, devstatus.name, devstatus.text)
|
||||
|
||||
return True
|
||||
|
||||
def _update_status_text(self):
|
||||
last_status_text = self.status_text
|
||||
|
||||
|
@ -210,12 +216,11 @@ class Watcher(threading.Thread):
|
|||
if self.rstatus.code < C.STATUS.CONNECTED:
|
||||
lines += (self.rstatus.text, '')
|
||||
|
||||
devstatuses = [self.devices[d] for d in range(1, 1 + api.C.MAX_ATTACHED_DEVICES) if d in self.devices]
|
||||
devstatuses = [self.devices[d] for d in range(1, 1 + self.rstatus.max_devices) if d in self.devices]
|
||||
for devstatus in devstatuses:
|
||||
if devstatus.text:
|
||||
if ' ' in devstatus.text:
|
||||
lines.append('<b>' + devstatus.name + '</b>')
|
||||
lines.append(' ' + devstatus.text)
|
||||
lines += ('<b>' + devstatus.name + '</b>', ' ' + devstatus.text)
|
||||
else:
|
||||
lines.append('<b>' + devstatus.name + '</b> ' + devstatus.text)
|
||||
else:
|
||||
|
@ -227,4 +232,4 @@ class Watcher(threading.Thread):
|
|||
self.status_text = self.rstatus.text
|
||||
|
||||
if self.status_text != last_status_text:
|
||||
self.status_changed.set()
|
||||
self.status_changed_callback(self.status_text, self.rstatus, self.devices)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
# Logitech Unifying Receiver API.
|
||||
#
|
||||
|
||||
import logging
|
||||
from logging import getLogger as _Logger
|
||||
from struct import pack as _pack
|
||||
from struct import unpack as _unpack
|
||||
from binascii import hexlify as _hexlify
|
||||
|
@ -16,7 +16,7 @@ from . import base as _base
|
|||
|
||||
|
||||
_LOG_LEVEL = 5
|
||||
_l = logging.getLogger('lur.api')
|
||||
_l = _Logger('lur.api')
|
||||
|
||||
#
|
||||
#
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
# Unlikely to be used directly unless you're expanding the API.
|
||||
#
|
||||
|
||||
import logging
|
||||
from logging import getLogger as _Logger
|
||||
from struct import pack as _pack
|
||||
from binascii import hexlify as _hexlify
|
||||
|
||||
|
@ -14,7 +14,7 @@ import hidapi as _hid
|
|||
|
||||
|
||||
_LOG_LEVEL = 4
|
||||
_l = logging.getLogger('lur.base')
|
||||
_l = _Logger('lur.base')
|
||||
|
||||
#
|
||||
# These values are defined by the Logitech documentation.
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
#
|
||||
#
|
||||
|
||||
import logging
|
||||
import threading
|
||||
from logging import getLogger as _Logger
|
||||
from threading import (Thread, Event, Lock)
|
||||
from time import sleep as _sleep
|
||||
|
||||
from . import base as _base
|
||||
|
@ -11,13 +11,13 @@ from . import exceptions as E
|
|||
|
||||
# for both Python 2 and 3
|
||||
try:
|
||||
import Queue as queue
|
||||
from Queue import Queue
|
||||
except ImportError:
|
||||
import queue
|
||||
from queue import Queue
|
||||
|
||||
|
||||
_LOG_LEVEL = 5
|
||||
_l = logging.getLogger('lur.listener')
|
||||
_LOG_LEVEL = 4
|
||||
_l = _Logger('lur.listener')
|
||||
|
||||
|
||||
_READ_EVENT_TIMEOUT = int(_base.DEFAULT_TIMEOUT / 4) # ms
|
||||
|
@ -30,11 +30,14 @@ def _callback_caller(listener, callback):
|
|||
event = listener.events.get()
|
||||
if _l.isEnabledFor(_LOG_LEVEL):
|
||||
_l.log(_LOG_LEVEL, "%s delivering event %s", listener, event)
|
||||
callback.__call__(*event)
|
||||
try:
|
||||
callback.__call__(*event)
|
||||
except:
|
||||
_l.exception("callback for %s", event)
|
||||
# _l.log(_LOG_LEVEL, "%s stopped callback caller", listener)
|
||||
|
||||
|
||||
class EventsListener(threading.Thread):
|
||||
class EventsListener(Thread):
|
||||
"""Listener thread for events from the Unifying Receiver.
|
||||
|
||||
Incoming events (reply_code, devnumber, data) will be passed to the callback
|
||||
|
@ -53,14 +56,14 @@ class EventsListener(threading.Thread):
|
|||
self.receiver = receiver
|
||||
|
||||
self.task = None
|
||||
self.task_processing = threading.Lock()
|
||||
self.task_processing = Lock()
|
||||
|
||||
self.task_reply = None
|
||||
self.task_done = threading.Event()
|
||||
self.task_done = Event()
|
||||
|
||||
self.events = queue.Queue(32)
|
||||
self.events = Queue(16)
|
||||
|
||||
self.event_caller = threading.Thread(group='Unifying Receiver', name='Callback-%x' % receiver, target=_callback_caller, args=(self, events_callback))
|
||||
self.event_caller = Thread(group='Unifying Receiver', name='Callback-%x' % receiver, target=_callback_caller, args=(self, events_callback))
|
||||
self.event_caller.daemon = True
|
||||
|
||||
self.__str_cached = 'Events(%x)' % self.receiver
|
||||
|
@ -102,6 +105,7 @@ class EventsListener(threading.Thread):
|
|||
"""Tells the listener to stop as soon as possible."""
|
||||
_l.log(_LOG_LEVEL, "%s stopping", self)
|
||||
self._active = False
|
||||
self.join()
|
||||
|
||||
def request(self, api_function, *args, **kwargs):
|
||||
"""Make an UR API request through this listener's receiver.
|
||||
|
|
6
solaar
6
solaar
|
@ -1,12 +1,12 @@
|
|||
#!/bin/sh
|
||||
|
||||
cd -P `dirname "$0"`
|
||||
cd -P `dirname "$0"` >/dev/null 2>&1
|
||||
|
||||
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$PWD/lib/native/`uname -m`
|
||||
export PYTHONPATH=$PWD/app:$PWD/lib
|
||||
export XDG_DATA_DIRS=$PWD/resources:$XDG_DATA_DIRS
|
||||
|
||||
cd -
|
||||
cd - >/dev/null 2>&1
|
||||
|
||||
exec python -OO -m solaar "$@"
|
||||
# exec python -OO -m profile -o $TMPDIR/profile.log solaar.py "$@"
|
||||
#exec python -OO -m profile -o $TMPDIR/profile.log app/solaar.py "$@"
|
||||
|
|
Loading…
Reference in New Issue