simplified window/icon code, reworked how device updates are signalled
This commit is contained in:
parent
2397c6c0ea
commit
e5a28ac64e
|
@ -45,67 +45,61 @@ def _run(args):
|
|||
|
||||
ui.notify.init()
|
||||
|
||||
from solaar.listener import DUMMY, ReceiverListener
|
||||
window = ui.main_window.create(NAME, DUMMY.name, 6, True)
|
||||
from solaar.listener import DUMMY_RECEIVER, ReceiverListener
|
||||
window = ui.main_window.create(NAME)
|
||||
assert window
|
||||
menu_actions = (ui.action.toggle_notifications,
|
||||
ui.action.about)
|
||||
icon = ui.status_icon.create(window, menu_actions)
|
||||
icon = ui.status_icon.create(window)
|
||||
assert icon
|
||||
|
||||
listener = [None]
|
||||
listeners = {}
|
||||
|
||||
# initializes the receiver listener
|
||||
def check_for_listener(notify=False):
|
||||
# print ("check_for_listener", notify)
|
||||
listener[0] = None
|
||||
|
||||
try:
|
||||
listener[0] = ReceiverListener.open(status_changed)
|
||||
l = ReceiverListener.open(status_changed)
|
||||
except OSError:
|
||||
l = None
|
||||
ui.error_dialog(window, 'Permissions error',
|
||||
'Found a possible Unifying Receiver device,\n'
|
||||
'but did not have permission to open it.')
|
||||
|
||||
if listener[0] is None:
|
||||
listeners.clear()
|
||||
if l:
|
||||
listeners[l.receiver.serial] = l
|
||||
else:
|
||||
if notify:
|
||||
status_changed(DUMMY)
|
||||
status_changed(DUMMY_RECEIVER)
|
||||
else:
|
||||
return True
|
||||
|
||||
from gi.repository import Gtk, GObject
|
||||
from logitech.unifying_receiver import status
|
||||
from gi.repository import Gtk, GLib
|
||||
from logitech.unifying_receiver.status import ALERT
|
||||
|
||||
# callback delivering status notifications from the receiver/devices to the UI
|
||||
def status_changed(receiver, device=None, alert=status.ALERT.NONE, reason=None):
|
||||
if alert & status.ALERT.SHOW_WINDOW:
|
||||
GObject.idle_add(window.present)
|
||||
if window:
|
||||
GObject.idle_add(ui.main_window.update, window, receiver, device)
|
||||
if icon:
|
||||
GObject.idle_add(ui.status_icon.update, icon, receiver, device)
|
||||
def status_changed(device, alert=ALERT.NONE, reason=None):
|
||||
assert device is not None
|
||||
|
||||
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)
|
||||
|
||||
if ui.notify.available:
|
||||
# always notify on receiver updates
|
||||
if device is None or alert & status.ALERT.NOTIFICATION:
|
||||
GObject.idle_add(ui.notify.show, device or receiver, reason)
|
||||
if device is DUMMY_RECEIVER or alert & ALERT.NOTIFICATION:
|
||||
GLib.idle_add(ui.notify.show, device, reason)
|
||||
|
||||
if receiver is DUMMY:
|
||||
GObject.timeout_add(3000, check_for_listener)
|
||||
if device is DUMMY_RECEIVER:
|
||||
GLib.timeout_add(3000, check_for_listener)
|
||||
|
||||
GObject.timeout_add(10, check_for_listener, True)
|
||||
if icon:
|
||||
GObject.timeout_add(1000, ui.status_icon.check_systray, icon, window)
|
||||
GLib.timeout_add(10, check_for_listener, True)
|
||||
Gtk.main()
|
||||
|
||||
if listener[0]:
|
||||
listener[0].stop()
|
||||
|
||||
map(ReceiverListener.stop, listeners.values())
|
||||
ui.notify.uninit()
|
||||
|
||||
if listener[0]:
|
||||
listener[0].join()
|
||||
listener[0] = None
|
||||
map(ReceiverListener.join, listeners.values())
|
||||
|
||||
|
||||
def main():
|
||||
|
|
|
@ -25,7 +25,7 @@ del namedtuple
|
|||
def _ghost(device):
|
||||
return _GHOST_DEVICE(number=device.number, name=device.name, kind=device.kind, status=None, max_devices=None)
|
||||
|
||||
DUMMY = _GHOST_DEVICE(Receiver.number, 'dialog-error', None, 'Receiver not found.', 6)
|
||||
DUMMY_RECEIVER = _GHOST_DEVICE(0xFF, 'Solaar', None, 'Receiver not found.', 0)
|
||||
|
||||
#
|
||||
#
|
||||
|
@ -52,15 +52,15 @@ 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._status_changed(self.receiver, _status.ALERT.NOTIFICATION)
|
||||
self.receiver = None
|
||||
self.status_changed_callback(DUMMY_RECEIVER, _status.ALERT.NOTIFICATION)
|
||||
|
||||
def tick(self, timestamp):
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
|
@ -76,7 +76,7 @@ class ReceiverListener(_listener.EventsListener):
|
|||
self._last_tick = timestamp
|
||||
|
||||
# read these in case they haven't been read already
|
||||
self.receiver.serial, self.receiver.firmware
|
||||
# self.receiver.serial, self.receiver.firmware
|
||||
if self.receiver.status.lock_open:
|
||||
# don't mess with stuff while pairing
|
||||
return
|
||||
|
@ -86,27 +86,36 @@ class ReceiverListener(_listener.EventsListener):
|
|||
dev.status.poll(timestamp)
|
||||
|
||||
def _status_changed(self, device, alert=_status.ALERT.NONE, reason=None):
|
||||
assert device is not None
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("status_changed %s: %s %s (%X) %s", device,
|
||||
None if device is None else 'active' if device.status else 'inactive',
|
||||
None if device is None else device.status,
|
||||
alert, reason or '')
|
||||
if self.status_changed_callback:
|
||||
r = self.receiver or DUMMY
|
||||
if device is None or device.kind is None:
|
||||
# the status of the receiver changed
|
||||
self.status_changed_callback(r, None, alert, reason)
|
||||
else:
|
||||
if device.status is None:
|
||||
# device was unpaired, and since the object is weakref'ed
|
||||
# it won't be valid for much longer
|
||||
device = _ghost(device)
|
||||
_log.debug("%s: status_changed %s: %s, %s (%X) %s", self.receiver, device,
|
||||
'active' if device.status else 'inactive',
|
||||
device.status, alert, reason or '')
|
||||
|
||||
self.status_changed_callback(r, device, alert, reason)
|
||||
if device.kind is None:
|
||||
# print ("self.receiver: ", self.receiver, id(self.receiver))
|
||||
# print ("device: ", device, id(device))
|
||||
assert device == self.receiver
|
||||
# the status of the receiver changed
|
||||
self.status_changed_callback(device, alert, reason)
|
||||
else:
|
||||
if device.status is None:
|
||||
# the device may be paired later, possibly to another receiver?
|
||||
# so maybe we shouldn't forget about it
|
||||
# configuration.forget(device)
|
||||
|
||||
if device.status is None:
|
||||
# the receiver changed status as well
|
||||
self.status_changed_callback(r)
|
||||
# device was unpaired, and since the object is weakref'ed
|
||||
# it won't be valid for much longer
|
||||
device = _ghost(device)
|
||||
|
||||
# elif device.status:
|
||||
# configuration.sync(device)
|
||||
|
||||
self.status_changed_callback(device, alert, reason)
|
||||
|
||||
if device.status is None:
|
||||
# the receiver changed status as well
|
||||
self.status_changed_callback(self.receiver)
|
||||
|
||||
def _notifications_handler(self, n):
|
||||
assert self.receiver
|
||||
|
|
|
@ -44,4 +44,5 @@ def error_dialog(window, title, text):
|
|||
m.destroy()
|
||||
|
||||
|
||||
from . import notify, status_icon, main_window
|
||||
from . import status_icon
|
||||
from . import notify, main_window
|
||||
|
|
|
@ -66,10 +66,12 @@ def _show_about_window(action):
|
|||
))
|
||||
except TypeError:
|
||||
# gtk3 < 3.6 has incorrect gi bindings
|
||||
pass
|
||||
import logging
|
||||
logging.exception("failed to fully create the about dialog")
|
||||
except:
|
||||
# is the Gtk3 version too old?
|
||||
pass
|
||||
import logging
|
||||
logging.exception("failed to fully create the about dialog")
|
||||
|
||||
about.set_website('http://pwr.github.io/Solaar/')
|
||||
about.set_website_label('Solaar')
|
||||
|
@ -78,8 +80,6 @@ def _show_about_window(action):
|
|||
about.destroy()
|
||||
about = make('help-about', 'About ' + _NAME, _show_about_window)
|
||||
|
||||
quit = make('application-exit', 'Quit', Gtk.main_quit)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
|
|
@ -41,7 +41,7 @@ def lux(level):
|
|||
|
||||
_ICON_SETS = {}
|
||||
|
||||
def device_icon_set(name, kind=None):
|
||||
def device_icon_set(name='_', kind=None):
|
||||
icon_set = _ICON_SETS.get(name)
|
||||
if icon_set is None:
|
||||
icon_set = Gtk.IconSet.new()
|
||||
|
@ -56,10 +56,7 @@ def device_icon_set(name, kind=None):
|
|||
elif str(kind) == 'trackball':
|
||||
names += ('input-mouse',)
|
||||
names += ('input-' + str(kind),)
|
||||
|
||||
theme = Gtk.IconTheme.get_default()
|
||||
if theme.has_icon(name):
|
||||
names += (name,)
|
||||
names += (name,)
|
||||
|
||||
source = Gtk.IconSource.new()
|
||||
for n in names:
|
||||
|
|
|
@ -20,17 +20,17 @@ _STATUS_ICON_SIZE = Gtk.IconSize.LARGE_TOOLBAR
|
|||
_TOOLBAR_ICON_SIZE = Gtk.IconSize.MENU
|
||||
_PLACEHOLDER = '~'
|
||||
_FALLBACK_ICON = 'preferences-desktop-peripherals'
|
||||
_MAX_DEVICES = 7
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def _make_receiver_box(name):
|
||||
def _make_receiver_box():
|
||||
frame = Gtk.Frame()
|
||||
frame._device = None
|
||||
frame.set_name(name)
|
||||
|
||||
icon_set = _icons.device_icon_set(name)
|
||||
icon_set = _icons.device_icon_set()
|
||||
icon = Gtk.Image.new_from_icon_set(icon_set, _RECEIVER_ICON_SIZE)
|
||||
icon.set_padding(2, 2)
|
||||
frame._icon = icon
|
||||
|
@ -39,7 +39,7 @@ def _make_receiver_box(name):
|
|||
label.set_alignment(0, 0.5)
|
||||
frame._label = label
|
||||
|
||||
pairing_icon = Gtk.Image.new_from_icon_name('network-wireless', _RECEIVER_ICON_SIZE)
|
||||
pairing_icon = Gtk.Image.new_from_icon_name('network-wireless', _TOOLBAR_ICON_SIZE)
|
||||
pairing_icon.set_tooltip_text('The pairing lock is open.')
|
||||
pairing_icon._tick = 0
|
||||
frame._pairing_icon = pairing_icon
|
||||
|
@ -242,18 +242,48 @@ def _make_device_box(index):
|
|||
return frame
|
||||
|
||||
|
||||
def create(title, name, max_devices, systray=False):
|
||||
def hide(w, trigger):
|
||||
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):
|
||||
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
|
||||
x, y, _ = Gtk.StatusIcon.position_menu(Gtk.Menu(), trigger)
|
||||
w.move(x, y)
|
||||
w.present()
|
||||
return True
|
||||
|
||||
|
||||
def set_icon_name(window, icon_name):
|
||||
icon_file = _icons.icon_file(icon_name)
|
||||
if icon_file:
|
||||
window.set_icon_from_file(icon_file)
|
||||
else:
|
||||
window.set_icon_name(icon_name)
|
||||
|
||||
|
||||
def create(title):
|
||||
window = Gtk.Window()
|
||||
window.set_title(title)
|
||||
window.set_icon_name(_icons.APP_ICON[0])
|
||||
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(name)
|
||||
rbox = _make_receiver_box()
|
||||
vbox.add(rbox)
|
||||
for i in range(1, 1 + max_devices):
|
||||
for i in range(1, _MAX_DEVICES):
|
||||
dbox = _make_device_box(i)
|
||||
vbox.add(dbox)
|
||||
vbox.set_visible(True)
|
||||
|
@ -264,53 +294,13 @@ def create(title, name, max_devices, systray=False):
|
|||
geometry.min_width = 320
|
||||
geometry.min_height = 32
|
||||
window.set_geometry_hints(vbox, geometry, Gdk.WindowHints.MIN_SIZE)
|
||||
|
||||
window.set_resizable(False)
|
||||
|
||||
def _toggle_visible(w, trigger):
|
||||
if w.get_visible():
|
||||
# hiding moves the window to 0,0
|
||||
position = w.get_position()
|
||||
w.hide()
|
||||
w.move(*position)
|
||||
else:
|
||||
if 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
|
||||
x, y, _ = Gtk.StatusIcon.position_menu(Gtk.Menu(), trigger)
|
||||
w.move(x, y)
|
||||
w.present()
|
||||
return True
|
||||
|
||||
def _set_has_systray(w, systray):
|
||||
# print ("set has systray", systray, w._has_systray)
|
||||
if systray != w._has_systray:
|
||||
w._has_systray = systray
|
||||
if systray:
|
||||
if w._delete_event_connection is None or not w.get_skip_taskbar_hint():
|
||||
w.set_skip_taskbar_hint(True)
|
||||
w.set_skip_pager_hint(True)
|
||||
if w._delete_event_connection:
|
||||
w.disconnect(w._delete_event_connection)
|
||||
w._delete_event_connection = w.connect('delete-event', _toggle_visible)
|
||||
else:
|
||||
if w._delete_event_connection is None or w.get_skip_taskbar_hint():
|
||||
w.set_skip_taskbar_hint(False)
|
||||
w.set_skip_pager_hint(False)
|
||||
if w._delete_event_connection:
|
||||
w.disconnect(w._delete_event_connection)
|
||||
w._delete_event_connection = w.connect('delete-event', Gtk.main_quit)
|
||||
w.present()
|
||||
|
||||
from types import MethodType
|
||||
window.toggle_visible = MethodType(_toggle_visible, window)
|
||||
window.set_has_systray = MethodType(_set_has_systray, window)
|
||||
del MethodType
|
||||
|
||||
window.set_skip_taskbar_hint(True)
|
||||
window.set_skip_pager_hint(True)
|
||||
window.set_keep_above(True)
|
||||
window._delete_event_connection = None
|
||||
window._has_systray = None
|
||||
window.set_has_systray(systray)
|
||||
# window.set_decorations(Gdk.DECOR_BORDER | Gdk.DECOR_TITLE)
|
||||
window.connect('delete-event', hide)
|
||||
|
||||
return window
|
||||
|
||||
|
@ -322,6 +312,8 @@ def _update_receiver_box(frame, 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:
|
||||
|
@ -342,6 +334,7 @@ def _update_receiver_box(frame, receiver):
|
|||
frame._toolbar.set_sensitive(True)
|
||||
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)
|
||||
|
@ -420,19 +413,21 @@ def _update_device_box(frame, dev):
|
|||
_config_panel.update(frame)
|
||||
|
||||
|
||||
def update(window, receiver, device=None):
|
||||
assert receiver is not None
|
||||
# print ("update", receiver, receiver.status, len(receiver), device)
|
||||
window.set_icon_name(_icons.APP_ICON[1 if receiver else -1])
|
||||
def update(window, device):
|
||||
assert device is not None
|
||||
# print ("main_window.update", device)
|
||||
|
||||
vbox = window.get_child()
|
||||
frames = list(vbox.get_children())
|
||||
assert len(frames) == 1 + receiver.max_devices, frames
|
||||
|
||||
if device is None:
|
||||
_update_receiver_box(frames[0], receiver)
|
||||
if not 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])
|
||||
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)
|
||||
|
|
|
@ -4,9 +4,11 @@
|
|||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from gi.repository import Gtk, GLib, GdkPixbuf
|
||||
from gi.repository import Gtk, GdkPixbuf
|
||||
|
||||
from . import action as _action, icons as _icons
|
||||
from . import (action as _action,
|
||||
icons as _icons,
|
||||
main_window as _main_window)
|
||||
from logitech.unifying_receiver import status as _status
|
||||
|
||||
#
|
||||
|
@ -15,8 +17,9 @@ from logitech.unifying_receiver import status as _status
|
|||
|
||||
_NO_DEVICES = [None] * 6
|
||||
|
||||
def create(window, menu_actions=None):
|
||||
def create(window):
|
||||
name = window.get_title()
|
||||
|
||||
icon = Gtk.StatusIcon()
|
||||
icon.set_title(name)
|
||||
icon.set_name(name)
|
||||
|
@ -24,16 +27,21 @@ def create(window, menu_actions=None):
|
|||
icon._devices = list(_NO_DEVICES)
|
||||
|
||||
icon.set_tooltip_text(name)
|
||||
icon.connect('activate', window.toggle_visible)
|
||||
icon.connect('activate', _main_window.toggle, window)
|
||||
|
||||
menu = Gtk.Menu()
|
||||
for a in menu_actions or ():
|
||||
if a:
|
||||
menu.append(a.create_menu_item())
|
||||
|
||||
menu.append(_action.quit.create_menu_item())
|
||||
menu.append(Gtk.SeparatorMenuItem.new())
|
||||
|
||||
menu.append(_action.about.create_menu_item())
|
||||
menu.append(_action.make('application-exit', 'Quit', Gtk.main_quit).create_menu_item())
|
||||
menu.show_all()
|
||||
|
||||
for x in _NO_DEVICES:
|
||||
m = Gtk.ImageMenuItem()
|
||||
m.set_sensitive(False)
|
||||
menu.insert(m, 0)
|
||||
|
||||
icon.connect('popup_menu',
|
||||
lambda icon, button, time, menu:
|
||||
menu.popup(None, None, icon.position_menu, icon, button, time),
|
||||
|
@ -41,23 +49,6 @@ def create(window, menu_actions=None):
|
|||
return icon
|
||||
|
||||
|
||||
def check_systray(icon, window):
|
||||
# use size-changed to detect if the systray is available or not
|
||||
def _size_changed(i, size, w):
|
||||
import logging
|
||||
logging.info("size-chagend %s %s", size, w)
|
||||
def _check_systray(i2, w2):
|
||||
logging.info("check_systray %s %s", i2.is_embedded(), i2.get_visible())
|
||||
w2.set_has_systray(i2.is_embedded() and i2.get_visible())
|
||||
# first guess
|
||||
GLib.timeout_add(250, _check_systray, i, w)
|
||||
# just to make sure...
|
||||
# GLib.timeout_add(1000, _check_systray, i, w)
|
||||
|
||||
_size_changed(icon, None, window)
|
||||
icon.connect('size-changed', _size_changed, window)
|
||||
|
||||
|
||||
_PIXMAPS = {}
|
||||
def _icon_with_battery(level, active):
|
||||
battery_icon = _icons.battery(level)
|
||||
|
@ -82,12 +73,18 @@ def _icon_with_battery(level, active):
|
|||
|
||||
return _PIXMAPS[name]
|
||||
|
||||
def update(icon, receiver, device=None):
|
||||
# print ("icon update", receiver, receiver.status, len(receiver), device)
|
||||
if device is not None:
|
||||
def update(icon, device):
|
||||
assert device is not None
|
||||
# print ("icon update", device)
|
||||
|
||||
if device.kind is None:
|
||||
receiver = device
|
||||
if not device:
|
||||
icon._devices[:] = _NO_DEVICES
|
||||
else:
|
||||
icon._devices[device.number] = None if device.status is None else device
|
||||
if not receiver:
|
||||
icon._devices[:] = _NO_DEVICES
|
||||
receiver = device.receiver
|
||||
|
||||
if not icon.is_embedded():
|
||||
return
|
||||
|
||||
|
|
Loading…
Reference in New Issue