Solaar/lib/solaar/ui/pair_window.py

305 lines
11 KiB
Python

# -*- python-mode -*-
## Copyright (C) 2012-2013 Daniel Pavel
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License along
## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
import logging
from gi.repository import GLib, Gtk
from logitech_receiver import hidpp10 as _hidpp10
from logitech_receiver.status import KEYS as _K
from solaar.i18n import _, ngettext
from . import icons as _icons
logger = logging.getLogger(__name__)
#
#
#
_PAIRING_TIMEOUT = 30 # seconds
_STATUS_CHECK = 500 # milliseconds
address = kind = authentication = name = passcode = None
def _create_page(assistant, kind, header=None, icon_name=None, text=None):
p = Gtk.VBox(False, 8)
assistant.append_page(p)
assistant.set_page_type(p, kind)
if header:
item = Gtk.HBox(False, 16)
p.pack_start(item, False, True, 0)
label = Gtk.Label(header)
label.set_alignment(0, 0)
label.set_line_wrap(True)
item.pack_start(label, True, True, 0)
if icon_name:
icon = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.DIALOG)
icon.set_alignment(1, 0)
item.pack_start(icon, False, False, 0)
if text:
label = Gtk.Label(text)
label.set_alignment(0, 0)
label.set_line_wrap(True)
p.pack_start(label, False, False, 0)
p.show_all()
return p
def _check_lock_state(assistant, receiver, count=2):
global address, kind, authentication, name, passcode
if not assistant.is_drawable():
if logger.isEnabledFor(logging.DEBUG):
logger.debug("assistant %s destroyed, bailing out", assistant)
return False
if receiver.status.get(_K.ERROR):
# receiver.status.new_device = _fake_device(receiver)
_pairing_failed(assistant, receiver, receiver.status.pop(_K.ERROR))
return False
if receiver.status.new_device:
receiver.remaining_pairings(False) # Update remaining pairings
device, receiver.status.new_device = receiver.status.new_device, None
_pairing_succeeded(assistant, receiver, device)
return False
elif receiver.status.device_address and receiver.status.device_name and not address:
address = receiver.status.device_address
name = receiver.status.device_name
kind = receiver.status.device_kind
authentication = receiver.status.device_authentication
name = receiver.status.device_name
if receiver.pair_device(
address=address, authentication=authentication, entropy=20 if kind == _hidpp10.DEVICE_KIND.keyboard else 10
):
return True
else:
_pairing_failed(assistant, receiver, "failed to open pairing lock")
return False
elif address and receiver.status.device_passkey and not passcode:
passcode = receiver.status.device_passkey
_show_passcode(assistant, receiver, passcode)
return True
if not receiver.status.lock_open and not receiver.status.discovering:
if count > 0:
# the actual device notification may arrive later so have a little patience
GLib.timeout_add(_STATUS_CHECK, _check_lock_state, assistant, receiver, count - 1)
else:
_pairing_failed(assistant, receiver, "failed to open pairing lock")
return False
return True
def _show_passcode(assistant, receiver, passkey):
if logger.isEnabledFor(logging.DEBUG):
logger.debug("%s show passkey: %s", receiver, passkey)
name = receiver.status.device_name
authentication = receiver.status.device_authentication
intro_text = _("%(receiver_name)s: pair new device") % {"receiver_name": receiver.name}
page_text = _("Enter passcode on %(name)s.") % {"name": name}
page_text += "\n"
if authentication & 0x01:
page_text += _("Type %(passcode)s and then press the enter key.") % {"passcode": receiver.status.device_passkey}
else:
passcode = ", ".join(
[_("right") if bit == "1" else _("left") for bit in f"{int(receiver.status.device_passkey):010b}"]
)
page_text += _("Press %(code)s\nand then press left and right buttons simultaneously.") % {"code": passcode}
page = _create_page(assistant, Gtk.AssistantPageType.PROGRESS, intro_text, "preferences-desktop-peripherals", page_text)
assistant.set_page_complete(page, True)
assistant.next_page()
def _prepare(assistant, page, receiver):
index = assistant.get_current_page()
if logger.isEnabledFor(logging.DEBUG):
logger.debug("prepare %s %d %s", assistant, index, page)
if index == 0:
if receiver.receiver_kind == "bolt":
if receiver.discover(timeout=_PAIRING_TIMEOUT):
assert receiver.status.new_device is None
assert receiver.status.get(_K.ERROR) is None
spinner = page.get_children()[-1]
spinner.start()
GLib.timeout_add(_STATUS_CHECK, _check_lock_state, assistant, receiver)
assistant.set_page_complete(page, True)
else:
GLib.idle_add(_pairing_failed, assistant, receiver, "discovery did not start")
elif receiver.set_lock(False, timeout=_PAIRING_TIMEOUT):
assert receiver.status.new_device is None
assert receiver.status.get(_K.ERROR) is None
spinner = page.get_children()[-1]
spinner.start()
GLib.timeout_add(_STATUS_CHECK, _check_lock_state, assistant, receiver)
assistant.set_page_complete(page, True)
else:
GLib.idle_add(_pairing_failed, assistant, receiver, "the pairing lock did not open")
else:
assistant.remove_page(0)
def _finish(assistant, receiver):
if logger.isEnabledFor(logging.DEBUG):
logger.debug("finish %s", assistant)
assistant.destroy()
receiver.status.new_device = None
if receiver.status.lock_open:
if receiver.receiver_kind == "bolt":
receiver.pair_device("cancel")
else:
receiver.set_lock()
if receiver.status.discovering:
receiver.discover(True)
if not receiver.status.lock_open and not receiver.status.discovering:
receiver.status[_K.ERROR] = None
def _pairing_failed(assistant, receiver, error):
if logger.isEnabledFor(logging.DEBUG):
logger.debug("%s fail: %s", receiver, error)
assistant.commit()
header = _("Pairing failed") + ": " + _(str(error)) + "."
if "timeout" in str(error):
text = _("Make sure your device is within range, and has a decent battery charge.")
elif str(error) == "device not supported":
text = _("A new device was detected, but it is not compatible with this receiver.")
elif "many" in str(error):
text = _("More paired devices than receiver can support.")
else:
text = _("No further details are available about the error.")
_create_page(assistant, Gtk.AssistantPageType.SUMMARY, header, "dialog-error", text)
assistant.next_page()
assistant.commit()
def _pairing_succeeded(assistant, receiver, device):
assert device
if logger.isEnabledFor(logging.DEBUG):
logger.debug("%s success: %s", receiver, device)
page = _create_page(assistant, Gtk.AssistantPageType.SUMMARY)
header = Gtk.Label(_("Found a new device:"))
header.set_alignment(0.5, 0)
page.pack_start(header, False, False, 0)
device_icon = Gtk.Image()
icon_set = _icons.device_icon_set(device.name, device.kind)
device_icon.set_from_icon_set(icon_set, Gtk.IconSize.LARGE)
device_icon.set_alignment(0.5, 1)
page.pack_start(device_icon, True, True, 0)
device_label = Gtk.Label()
device_label.set_markup("<b>%s</b>" % device.name)
device_label.set_alignment(0.5, 0)
page.pack_start(device_label, True, True, 0)
hbox = Gtk.HBox(False, 8)
hbox.pack_start(Gtk.Label(" "), False, False, 0)
hbox.set_property("expand", False)
hbox.set_property("halign", Gtk.Align.CENTER)
page.pack_start(hbox, False, False, 0)
def _check_encrypted(dev):
if assistant.is_drawable():
if device.status.get(_K.LINK_ENCRYPTED) is False:
hbox.pack_start(Gtk.Image.new_from_icon_name("security-low", Gtk.IconSize.MENU), False, False, 0)
hbox.pack_start(Gtk.Label(_("The wireless link is not encrypted") + "!"), False, False, 0)
hbox.show_all()
else:
return True
GLib.timeout_add(_STATUS_CHECK, _check_encrypted, device)
page.show_all()
assistant.next_page()
assistant.commit()
def create(receiver):
assert receiver is not None
assert receiver.kind is None
global address, kind, authentication, name, passcode
address = name = kind = authentication = passcode = None
assistant = Gtk.Assistant()
assistant.set_title(_("%(receiver_name)s: pair new device") % {"receiver_name": receiver.name})
assistant.set_icon_name("list-add")
assistant.set_size_request(400, 240)
assistant.set_resizable(False)
assistant.set_role("pair-device")
if receiver.receiver_kind == "unifying":
page_text = _("Unifying receivers are only compatible with Unifying devices.")
elif receiver.receiver_kind == "bolt":
page_text = _("Bolt receivers are only compatible with Bolt devices.")
else:
page_text = _("Other receivers are only compatible with a few devices.")
page_text += "\n"
page_text += _("The device must not be paired with a nearby powered-on receiver.")
page_text += "\n\n"
if receiver.receiver_kind == "bolt":
page_text += _("Press a pairing button or key until the pairing light flashes quickly.")
page_text += "\n"
page_text += _("You may have to first turn the device off and on again.")
else:
page_text += _("Turn on the device you want to pair.")
page_text += "\n"
page_text += _("If the device is already turned on, turn it off and on again.")
if receiver.remaining_pairings() and receiver.remaining_pairings() >= 0:
page_text += (
ngettext(
"\n\nThis receiver has %d pairing remaining.",
"\n\nThis receiver has %d pairings remaining.",
receiver.remaining_pairings(),
)
% receiver.remaining_pairings()
)
page_text += _("\nCancelling at this point will not use up a pairing.")
intro_text = _("%(receiver_name)s: pair new device") % {"receiver_name": receiver.name}
page_intro = _create_page(
assistant, Gtk.AssistantPageType.PROGRESS, intro_text, "preferences-desktop-peripherals", page_text
)
spinner = Gtk.Spinner()
spinner.set_visible(True)
page_intro.pack_end(spinner, True, True, 24)
assistant.connect("prepare", _prepare, receiver)
assistant.connect("cancel", _finish, receiver)
assistant.connect("close", _finish, receiver)
return assistant