simplified window/icon code, reworked how device updates are signalled

This commit is contained in:
Daniel Pavel 2013-04-30 19:44:03 +02:00
parent 2397c6c0ea
commit e5a28ac64e
7 changed files with 149 additions and 156 deletions

View File

@ -45,67 +45,61 @@ def _run(args):
ui.notify.init() ui.notify.init()
from solaar.listener import DUMMY, ReceiverListener from solaar.listener import DUMMY_RECEIVER, ReceiverListener
window = ui.main_window.create(NAME, DUMMY.name, 6, True) window = ui.main_window.create(NAME)
assert window assert window
menu_actions = (ui.action.toggle_notifications, icon = ui.status_icon.create(window)
ui.action.about)
icon = ui.status_icon.create(window, menu_actions)
assert icon assert icon
listener = [None] listeners = {}
# initializes the receiver listener # initializes the receiver listener
def check_for_listener(notify=False): def check_for_listener(notify=False):
# print ("check_for_listener", notify) # print ("check_for_listener", notify)
listener[0] = None
try: try:
listener[0] = ReceiverListener.open(status_changed) l = ReceiverListener.open(status_changed)
except OSError: except OSError:
l = None
ui.error_dialog(window, 'Permissions error', ui.error_dialog(window, 'Permissions error',
'Found a possible Unifying Receiver device,\n' 'Found a possible Unifying Receiver device,\n'
'but did not have permission to open it.') '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: if notify:
status_changed(DUMMY) status_changed(DUMMY_RECEIVER)
else: else:
return True return True
from gi.repository import Gtk, GObject from gi.repository import Gtk, GLib
from logitech.unifying_receiver import status from logitech.unifying_receiver.status import ALERT
# callback delivering status notifications from the receiver/devices to the UI # callback delivering status notifications from the receiver/devices to the UI
def status_changed(receiver, device=None, alert=status.ALERT.NONE, reason=None): def status_changed(device, alert=ALERT.NONE, reason=None):
if alert & status.ALERT.SHOW_WINDOW: assert device is not None
GObject.idle_add(window.present)
if window: if alert & ALERT.SHOW_WINDOW:
GObject.idle_add(ui.main_window.update, window, receiver, device) GLib.idle_add(window.present)
if icon: GLib.idle_add(ui.main_window.update, window, device)
GObject.idle_add(ui.status_icon.update, icon, receiver, device) GLib.idle_add(ui.status_icon.update, icon, device)
if ui.notify.available: if ui.notify.available:
# always notify on receiver updates # always notify on receiver updates
if device is None or alert & status.ALERT.NOTIFICATION: if device is DUMMY_RECEIVER or alert & ALERT.NOTIFICATION:
GObject.idle_add(ui.notify.show, device or receiver, reason) GLib.idle_add(ui.notify.show, device, reason)
if receiver is DUMMY: if device is DUMMY_RECEIVER:
GObject.timeout_add(3000, check_for_listener) GLib.timeout_add(3000, check_for_listener)
GObject.timeout_add(10, check_for_listener, True) GLib.timeout_add(10, check_for_listener, True)
if icon:
GObject.timeout_add(1000, ui.status_icon.check_systray, icon, window)
Gtk.main() Gtk.main()
if listener[0]: map(ReceiverListener.stop, listeners.values())
listener[0].stop()
ui.notify.uninit() ui.notify.uninit()
map(ReceiverListener.join, listeners.values())
if listener[0]:
listener[0].join()
listener[0] = None
def main(): def main():

View File

@ -25,7 +25,7 @@ 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 = _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) _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) _log.info("%s: notifications listener has stopped", self.receiver)
if self.receiver: if self.receiver:
self.receiver.enable_notifications(False) self.receiver.enable_notifications(False)
self.receiver.close() self.receiver.close()
self._status_changed(self.receiver, _status.ALERT.NOTIFICATION)
self.receiver = None self.receiver = None
self.status_changed_callback(DUMMY_RECEIVER, _status.ALERT.NOTIFICATION)
def tick(self, timestamp): def tick(self, timestamp):
if _log.isEnabledFor(_DEBUG): if _log.isEnabledFor(_DEBUG):
@ -76,7 +76,7 @@ class ReceiverListener(_listener.EventsListener):
self._last_tick = timestamp self._last_tick = timestamp
# read these in case they haven't been read already # 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: if self.receiver.status.lock_open:
# don't mess with stuff while pairing # don't mess with stuff while pairing
return return
@ -86,27 +86,36 @@ class ReceiverListener(_listener.EventsListener):
dev.status.poll(timestamp) dev.status.poll(timestamp)
def _status_changed(self, device, alert=_status.ALERT.NONE, reason=None): def _status_changed(self, device, alert=_status.ALERT.NONE, reason=None):
assert device is not None
if _log.isEnabledFor(_DEBUG): if _log.isEnabledFor(_DEBUG):
_log.debug("status_changed %s: %s %s (%X) %s", device, _log.debug("%s: status_changed %s: %s, %s (%X) %s", self.receiver, device,
None if device is None else 'active' if device.status else 'inactive', 'active' if device.status else 'inactive',
None if device is None else device.status, device.status, alert, reason or '')
alert, reason or '')
if self.status_changed_callback: if device.kind is None:
r = self.receiver or DUMMY # print ("self.receiver: ", self.receiver, id(self.receiver))
if device is None or device.kind is None: # print ("device: ", device, id(device))
assert device == self.receiver
# the status of the receiver changed # the status of the receiver changed
self.status_changed_callback(r, None, alert, reason) self.status_changed_callback(device, alert, reason)
else: else:
if device.status is None: 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)
# device was unpaired, and since the object is weakref'ed # device was unpaired, and since the object is weakref'ed
# it won't be valid for much longer # it won't be valid for much longer
device = _ghost(device) device = _ghost(device)
self.status_changed_callback(r, device, alert, reason) # elif device.status:
# configuration.sync(device)
self.status_changed_callback(device, alert, reason)
if device.status is None: if device.status is None:
# the receiver changed status as well # the receiver changed status as well
self.status_changed_callback(r) self.status_changed_callback(self.receiver)
def _notifications_handler(self, n): def _notifications_handler(self, n):
assert self.receiver assert self.receiver

View File

@ -44,4 +44,5 @@ def error_dialog(window, title, text):
m.destroy() m.destroy()
from . import notify, status_icon, main_window from . import status_icon
from . import notify, main_window

View File

@ -66,10 +66,12 @@ def _show_about_window(action):
)) ))
except TypeError: except TypeError:
# gtk3 < 3.6 has incorrect gi bindings # gtk3 < 3.6 has incorrect gi bindings
pass import logging
logging.exception("failed to fully create the about dialog")
except: except:
# is the Gtk3 version too old? # 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('http://pwr.github.io/Solaar/')
about.set_website_label('Solaar') about.set_website_label('Solaar')
@ -78,8 +80,6 @@ def _show_about_window(action):
about.destroy() about.destroy()
about = make('help-about', 'About ' + _NAME, _show_about_window) about = make('help-about', 'About ' + _NAME, _show_about_window)
quit = make('application-exit', 'Quit', Gtk.main_quit)
# #
# #
# #

View File

@ -41,7 +41,7 @@ def lux(level):
_ICON_SETS = {} _ICON_SETS = {}
def device_icon_set(name, kind=None): def device_icon_set(name='_', kind=None):
icon_set = _ICON_SETS.get(name) icon_set = _ICON_SETS.get(name)
if icon_set is None: if icon_set is None:
icon_set = Gtk.IconSet.new() icon_set = Gtk.IconSet.new()
@ -56,9 +56,6 @@ def device_icon_set(name, kind=None):
elif str(kind) == 'trackball': elif str(kind) == 'trackball':
names += ('input-mouse',) names += ('input-mouse',)
names += ('input-' + str(kind),) names += ('input-' + str(kind),)
theme = Gtk.IconTheme.get_default()
if theme.has_icon(name):
names += (name,) names += (name,)
source = Gtk.IconSource.new() source = Gtk.IconSource.new()

View File

@ -20,17 +20,17 @@ _STATUS_ICON_SIZE = Gtk.IconSize.LARGE_TOOLBAR
_TOOLBAR_ICON_SIZE = Gtk.IconSize.MENU _TOOLBAR_ICON_SIZE = Gtk.IconSize.MENU
_PLACEHOLDER = '~' _PLACEHOLDER = '~'
_FALLBACK_ICON = 'preferences-desktop-peripherals' _FALLBACK_ICON = 'preferences-desktop-peripherals'
_MAX_DEVICES = 7
# #
# #
# #
def _make_receiver_box(name): def _make_receiver_box():
frame = Gtk.Frame() frame = Gtk.Frame()
frame._device = None 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 = 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
@ -39,7 +39,7 @@ def _make_receiver_box(name):
label.set_alignment(0, 0.5) label.set_alignment(0, 0.5)
frame._label = label 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.set_tooltip_text('The pairing lock is open.')
pairing_icon._tick = 0 pairing_icon._tick = 0
frame._pairing_icon = pairing_icon frame._pairing_icon = pairing_icon
@ -242,18 +242,48 @@ def _make_device_box(index):
return frame 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 = Gtk.Window()
window.set_title(title) 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_role('status-window')
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(name) rbox = _make_receiver_box()
vbox.add(rbox) vbox.add(rbox)
for i in range(1, 1 + max_devices): for i in range(1, _MAX_DEVICES):
dbox = _make_device_box(i) dbox = _make_device_box(i)
vbox.add(dbox) vbox.add(dbox)
vbox.set_visible(True) vbox.set_visible(True)
@ -264,53 +294,13 @@ def create(title, name, max_devices, systray=False):
geometry.min_width = 320 geometry.min_width = 320
geometry.min_height = 32 geometry.min_height = 32
window.set_geometry_hints(vbox, geometry, Gdk.WindowHints.MIN_SIZE) window.set_geometry_hints(vbox, geometry, Gdk.WindowHints.MIN_SIZE)
window.set_resizable(False) window.set_resizable(False)
window.set_skip_taskbar_hint(True)
def _toggle_visible(w, trigger): window.set_skip_pager_hint(True)
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_keep_above(True) window.set_keep_above(True)
window._delete_event_connection = None # window.set_decorations(Gdk.DECOR_BORDER | Gdk.DECOR_TITLE)
window._has_systray = None window.connect('delete-event', hide)
window.set_has_systray(systray)
return window return window
@ -322,6 +312,8 @@ def _update_receiver_box(frame, receiver):
frame._label.set_text(str(receiver.status)) frame._label.set_text(str(receiver.status))
if receiver: if receiver:
frame._device = 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) frame._icon.set_sensitive(True)
if receiver.status.lock_open: if receiver.status.lock_open:
if frame._pairing_icon._tick == 0: if frame._pairing_icon._tick == 0:
@ -342,6 +334,7 @@ def _update_receiver_box(frame, receiver):
frame._toolbar.set_sensitive(True) frame._toolbar.set_sensitive(True)
else: else:
frame._device = None frame._device = None
frame._icon.set_from_icon_name('dialog-error', _RECEIVER_ICON_SIZE)
frame._icon.set_sensitive(False) frame._icon.set_sensitive(False)
frame._pairing_icon.set_visible(False) frame._pairing_icon.set_visible(False)
frame._toolbar.set_sensitive(False) frame._toolbar.set_sensitive(False)
@ -420,19 +413,21 @@ def _update_device_box(frame, dev):
_config_panel.update(frame) _config_panel.update(frame)
def update(window, receiver, device=None): def update(window, device):
assert receiver is not None assert device is not None
# print ("update", receiver, receiver.status, len(receiver), device) # print ("main_window.update", device)
window.set_icon_name(_icons.APP_ICON[1 if receiver else -1])
vbox = window.get_child() vbox = window.get_child()
frames = list(vbox.get_children()) frames = list(vbox.get_children())
assert len(frames) == 1 + receiver.max_devices, frames
if device is None: if device.kind is None:
_update_receiver_box(frames[0], receiver) # update on the receiver
if not receiver: _update_receiver_box(frames[0], device)
if device:
set_icon_name(window, _icons.APP_ICON[1])
else:
for frame in frames[1:]: for frame in frames[1:]:
_update_device_box(frame, None) _update_device_box(frame, None)
set_icon_name(window, _icons.APP_ICON[-1])
else: else:
_update_device_box(frames[device.number], None if device.status is None else device) _update_device_box(frames[device.number], None if device.status is None else device)

View File

@ -4,9 +4,11 @@
from __future__ import absolute_import, division, print_function, unicode_literals 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 from logitech.unifying_receiver import status as _status
# #
@ -15,8 +17,9 @@ from logitech.unifying_receiver import status as _status
_NO_DEVICES = [None] * 6 _NO_DEVICES = [None] * 6
def create(window, menu_actions=None): def create(window):
name = window.get_title() name = window.get_title()
icon = Gtk.StatusIcon() icon = Gtk.StatusIcon()
icon.set_title(name) icon.set_title(name)
icon.set_name(name) icon.set_name(name)
@ -24,16 +27,21 @@ def create(window, menu_actions=None):
icon._devices = list(_NO_DEVICES) icon._devices = list(_NO_DEVICES)
icon.set_tooltip_text(name) icon.set_tooltip_text(name)
icon.connect('activate', window.toggle_visible) icon.connect('activate', _main_window.toggle, window)
menu = Gtk.Menu() 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() menu.show_all()
for x in _NO_DEVICES:
m = Gtk.ImageMenuItem()
m.set_sensitive(False)
menu.insert(m, 0)
icon.connect('popup_menu', icon.connect('popup_menu',
lambda icon, button, time, menu: lambda icon, button, time, menu:
menu.popup(None, None, icon.position_menu, icon, button, time), menu.popup(None, None, icon.position_menu, icon, button, time),
@ -41,23 +49,6 @@ def create(window, menu_actions=None):
return icon 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 = {} _PIXMAPS = {}
def _icon_with_battery(level, active): def _icon_with_battery(level, active):
battery_icon = _icons.battery(level) battery_icon = _icons.battery(level)
@ -82,12 +73,18 @@ def _icon_with_battery(level, active):
return _PIXMAPS[name] return _PIXMAPS[name]
def update(icon, receiver, device=None): def update(icon, device):
# print ("icon update", receiver, receiver.status, len(receiver), device) assert device is not None
if device is not None: # print ("icon update", device)
icon._devices[device.number] = None if device.status is None else device
if not receiver: if device.kind is None:
receiver = device
if not device:
icon._devices[:] = _NO_DEVICES icon._devices[:] = _NO_DEVICES
else:
icon._devices[device.number] = None if device.status is None else device
receiver = device.receiver
if not icon.is_embedded(): if not icon.is_embedded():
return return