summaryrefslogtreecommitdiff
path: root/src/leap/bitmask/gui
diff options
context:
space:
mode:
Diffstat (limited to 'src/leap/bitmask/gui')
-rw-r--r--src/leap/bitmask/gui/eip_status.py9
-rw-r--r--src/leap/bitmask/gui/mainwindow.py158
-rw-r--r--src/leap/bitmask/gui/twisted_main.py5
-rw-r--r--src/leap/bitmask/gui/ui/wizard.ui2
-rw-r--r--src/leap/bitmask/gui/wizard.py132
5 files changed, 158 insertions, 148 deletions
diff --git a/src/leap/bitmask/gui/eip_status.py b/src/leap/bitmask/gui/eip_status.py
index 8b9f2d44..bd569343 100644
--- a/src/leap/bitmask/gui/eip_status.py
+++ b/src/leap/bitmask/gui/eip_status.py
@@ -88,6 +88,8 @@ class EIPStatusWidget(QtGui.QWidget):
self.is_restart = False
self.is_cold_start = True
+ self.missing_helpers = False
+
# Action for the systray
self._eip_disabled_action = QtGui.QAction(
"{0} is {1}".format(self._service_name, self.tr("disabled")), self)
@@ -298,7 +300,12 @@ class EIPStatusWidget(QtGui.QWidget):
# probably the best thing would be to make a conditional
# transition there, but that's more involved.
self.eip_button.hide()
- msg = self.tr("You must login to use {0}".format(self._service_name))
+ if self.missing_helpers:
+ msg = self.tr(
+ "<font color=red>Disabled: missing helper files</font>")
+ else:
+ msg = self.tr(
+ "You must login to use {0}".format(self._service_name))
self.eip_label.setText(msg)
self._eip_status_menu.setTitle("{0} is {1}".format(
self._service_name, self.tr("disabled")))
diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py
index 3ef994b1..53a7d95a 100644
--- a/src/leap/bitmask/gui/mainwindow.py
+++ b/src/leap/bitmask/gui/mainwindow.py
@@ -18,12 +18,10 @@
Main window for Bitmask.
"""
import logging
-import socket
from datetime import datetime
from PySide import QtCore, QtGui
-from twisted.internet import reactor, threads
from leap.bitmask import __version__ as VERSION
from leap.bitmask import __version_hash__ as VERSION_HASH
@@ -39,11 +37,13 @@ from leap.bitmask.gui.mail_status import MailStatusWidget
from leap.bitmask.gui.preferenceswindow import PreferencesWindow
from leap.bitmask.gui.systray import SysTray
from leap.bitmask.gui.wizard import Wizard
+from leap.bitmask.gui import twisted_main
from leap.bitmask.platform_init import IS_WIN, IS_MAC, IS_LINUX
from leap.bitmask.platform_init.initializers import init_platform
+from leap.bitmask.platform_init.initializers import init_signals
-from leap.bitmask import backend
+from leap.bitmask.backend import leapbackend
from leap.bitmask.services.eip import conductor as eip_conductor
from leap.bitmask.services.mail import conductor as mail_conductor
@@ -89,16 +89,12 @@ class MainWindow(QtGui.QMainWindow):
EIP_START_TIMEOUT = 60000 # in milliseconds
# We give the services some time to a halt before forcing quit.
- SERVICES_STOP_TIMEOUT = 20
+ SERVICES_STOP_TIMEOUT = 20000 # in milliseconds
- def __init__(self, quit_callback, bypass_checks=False, start_hidden=False):
+ def __init__(self, bypass_checks=False, start_hidden=False):
"""
Constructor for the client main window
- :param quit_callback: Function to be called when closing
- the application.
- :type quit_callback: callable
-
:param bypass_checks: Set to true if the app should bypass first round
of checks for CA certificates at bootstrap
:type bypass_checks: bool
@@ -117,14 +113,13 @@ class MainWindow(QtGui.QMainWindow):
reqcbk=lambda req, resp: None) # make rpc call async
# end register leap events ####################################
- self._quit_callback = quit_callback
self._updates_content = ""
# setup UI
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.menuBar().setNativeMenuBar(not IS_LINUX)
- self._backend = backend.Backend(bypass_checks)
+ self._backend = leapbackend.Backend(bypass_checks)
self._backend.start()
self._settings = LeapSettings()
@@ -152,6 +147,9 @@ class MainWindow(QtGui.QMainWindow):
self._settings, self._backend)
self._eip_status = EIPStatusWidget(self, self._eip_conductor)
+ init_signals.eip_missing_helpers.connect(
+ self._disable_eip_missing_helpers)
+
self.ui.eipLayout.addWidget(self._eip_status)
self._eip_conductor.add_eip_widget(self._eip_status)
@@ -181,8 +179,8 @@ class MainWindow(QtGui.QMainWindow):
# Set used to track the services being stopped and need wait.
self._services_being_stopped = {}
- # timeout object used to trigger quit
- self._quit_timeout_callater = None
+ # used to know if we are in the final steps of quitting
+ self._finally_quitting = False
self._backend_connected_signals = []
self._backend_connect()
@@ -407,6 +405,8 @@ class MainWindow(QtGui.QMainWindow):
sig.eip_can_start.connect(self._backend_can_start_eip)
sig.eip_cannot_start.connect(self._backend_cannot_start_eip)
+ sig.eip_dns_error.connect(self._eip_dns_error)
+
# ==================================================================
# Soledad signals
@@ -553,7 +553,7 @@ class MainWindow(QtGui.QMainWindow):
details = self._provider_details
mx_provided = False
if details is not None:
- mx_provided = MX_SERVICE in details.services
+ mx_provided = MX_SERVICE in details['services']
# XXX: handle differently not logged in user?
akm = AdvancedKeyManagement(self, mx_provided, logged_user,
@@ -573,7 +573,7 @@ class MainWindow(QtGui.QMainWindow):
domain = self._login_widget.get_selected_provider()
mx_provided = False
if self._provider_details is not None:
- mx_provided = MX_SERVICE in self._provider_details.services
+ mx_provided = MX_SERVICE in self._provider_details['services']
preferences = PreferencesWindow(self, user, domain, self._backend,
self._soledad_started, mx_provided)
@@ -667,6 +667,16 @@ class MainWindow(QtGui.QMainWindow):
self._eip_status.set_eip_status(self.tr("Disabled"))
@QtCore.Slot()
+ def _disable_eip_missing_helpers(self):
+ """
+ TRIGGERS:
+ init_signals.missing_helpers
+
+ Set the missing_helpers flag, so we can disable EIP.
+ """
+ self._eip_status.missing_helpers = True
+
+ @QtCore.Slot()
def _show_eip_preferences(self):
"""
TRIGGERS:
@@ -901,7 +911,7 @@ class MainWindow(QtGui.QMainWindow):
self.tr('Hello!'),
self.tr('Bitmask has started in the tray.'))
# we wait for the systray to be ready
- reactor.callLater(1, hello)
+ QtDelayedCall(1000, hello)
@QtCore.Slot(int)
def _tray_activated(self, reason=None):
@@ -1271,7 +1281,7 @@ class MainWindow(QtGui.QMainWindow):
sig.soledad_bootstrap_failed.connect(lambda: btn_enabled(True))
sig.soledad_bootstrap_finished.connect(lambda: btn_enabled(True))
- if not MX_SERVICE in self._provider_details.services:
+ if not MX_SERVICE in self._provider_details['services']:
self._set_mx_visible(False)
def _start_eip_bootstrap(self):
@@ -1314,7 +1324,7 @@ class MainWindow(QtGui.QMainWindow):
Set the details for the just downloaded provider.
:param details: the details of the provider.
- :type details: ProviderConfigLight
+ :type details: dict
"""
self._provider_details = details
@@ -1331,7 +1341,7 @@ class MainWindow(QtGui.QMainWindow):
mx_enabled = MX_SERVICE in enabled_services
mx_provided = False
if self._provider_details is not None:
- mx_provided = MX_SERVICE in self._provider_details.services
+ mx_provided = MX_SERVICE in self._provider_details['services']
return mx_enabled and mx_provided
@@ -1348,7 +1358,7 @@ class MainWindow(QtGui.QMainWindow):
eip_enabled = EIP_SERVICE in enabled_services
eip_provided = False
if self._provider_details is not None:
- eip_provided = EIP_SERVICE in self._provider_details.services
+ eip_provided = EIP_SERVICE in self._provider_details['services']
return eip_enabled and eip_provided
@@ -1465,55 +1475,25 @@ class MainWindow(QtGui.QMainWindow):
self._already_started_eip = True
# check for connectivity
- # we might want to leave a little time here...
- self._check_name_resolution(domain)
-
- def _check_name_resolution(self, domain):
- # FIXME this has to be moved to backend !!!
- # Should move to netchecks module.
- # and separate qt from reactor...
- """
- Check if we can resolve the given domain name.
-
- :param domain: the domain to check.
- :type domain: str
- """
- def do_check():
- """
- Try to resolve the domain name.
- """
- socket.gethostbyname(domain.encode('idna'))
-
- def check_err(failure):
- """
- Errback handler for `do_check`.
-
- :param failure: the failure that triggered the errback.
- :type failure: twisted.python.failure.Failure
- """
- logger.error(repr(failure))
- logger.error("Can't resolve hostname.")
-
- msg = self.tr(
- "The server at {0} can't be found, because the DNS lookup "
- "failed. DNS is the network service that translates a "
- "website's name to its Internet address. Either your computer "
- "is having trouble connecting to the network, or you are "
- "missing some helper files that are needed to securely use "
- "DNS while {1} is active. To install these helper files, quit "
- "this application and start it again."
- ).format(domain, self._eip_conductor.eip_name)
-
- show_err = lambda: QtGui.QMessageBox.critical(
- self, self.tr("Connection Error"), msg)
- reactor.callLater(0, show_err)
-
- # python 2.7.4 raises socket.error
- # python 2.7.5 raises socket.gaierror
- failure.trap(socket.gaierror, socket.error)
-
- d = threads.deferToThread(do_check)
- d.addErrback(check_err)
+ self._backend.eip_check_dns(domain)
+
+ @QtCore.Slot()
+ def _eip_dns_error(self):
+ """
+ Trigger this if we don't have a working DNS resolver.
+ """
+ domain = self._login_widget.get_selected_provider()
+ msg = self.tr(
+ "The server at {0} can't be found, because the DNS lookup "
+ "failed. DNS is the network service that translates a "
+ "website's name to its Internet address. Either your computer "
+ "is having trouble connecting to the network, or you are "
+ "missing some helper files that are needed to securely use "
+ "DNS while {1} is active. To install these helper files, quit "
+ "this application and start it again."
+ ).format(domain, self._eip_conductor.eip_name)
+
+ QtGui.QMessageBox.critical(self, self.tr("Connection Error"), msg)
def _try_autostart_eip(self):
"""
@@ -1543,7 +1523,13 @@ class MainWindow(QtGui.QMainWindow):
else:
should_start = self._provides_eip_and_enabled()
- if should_start and not self._already_started_eip:
+ missing_helpers = self._eip_status.missing_helpers
+ already_started = self._already_started_eip
+ can_start = (should_start
+ and not already_started
+ and not missing_helpers)
+
+ if can_start:
if self._eip_status.is_cold_start:
self._backend.tear_fw_down()
# XXX this should be handled by the state machine.
@@ -1563,12 +1549,16 @@ class MainWindow(QtGui.QMainWindow):
else:
if not self._already_started_eip:
if EIP_SERVICE in self._enabled_services:
- self._eip_status.set_eip_status(
- self.tr("Not supported"),
- error=True)
+ if missing_helpers:
+ msg = self.tr(
+ "Disabled: missing helper files")
+ else:
+ msg = self.tr("Not supported"),
+ self._eip_status.set_eip_status(msg, error=True)
else:
+ msg = self.tr("Disabled")
self._eip_status.disable_eip_start()
- self._eip_status.set_eip_status(self.tr("Disabled"))
+ self._eip_status.set_eip_status(msg)
# eip will not start, so we start soledad anyway
self._maybe_run_soledad_setup_checks()
@@ -1772,8 +1762,7 @@ class MainWindow(QtGui.QMainWindow):
# call final quit when all the services are stopped
self.all_services_stopped.connect(self.final_quit)
# or if we reach the timeout
- self._quit_timeout_callater = reactor.callLater(
- self.SERVICES_STOP_TIMEOUT, self.final_quit)
+ QtDelayedCall(self.SERVICES_STOP_TIMEOUT, self.final_quit)
@QtCore.Slot()
def _remove_service(self, service):
@@ -1799,16 +1788,13 @@ class MainWindow(QtGui.QMainWindow):
"""
logger.debug('Final quit...')
- try:
- # disconnect signal if we get here due a timeout.
- self.all_services_stopped.disconnect(self.final_quit)
- except RuntimeError:
- pass # Signal was not connected
+ # We can reach here because all the services are stopped or because a
+ # timeout was triggered. Since we want to run this only once, we exit
+ # if this is called twice.
+ if self._finally_quitting:
+ return
- # Cancel timeout to avoid being called if we reached here through the
- # signal
- if self._quit_timeout_callater.active():
- self._quit_timeout_callater.cancel()
+ self._finally_quitting = True
# Remove lockfiles on a clean shutdown.
logger.debug('Cleaning pidfiles')
@@ -1818,4 +1804,4 @@ class MainWindow(QtGui.QMainWindow):
self._backend.stop()
self.close()
- reactor.callLater(1, self._quit_callback)
+ QtDelayedCall(100, twisted_main.quit)
diff --git a/src/leap/bitmask/gui/twisted_main.py b/src/leap/bitmask/gui/twisted_main.py
index dfd69033..b1ce0ead 100644
--- a/src/leap/bitmask/gui/twisted_main.py
+++ b/src/leap/bitmask/gui/twisted_main.py
@@ -36,11 +36,8 @@ def stop():
logger.debug("Done stopping all the things.")
-def quit(app):
+def quit():
"""
Stop the mainloop.
-
- :param app: the main qt QApplication instance.
- :type app: QtCore.QApplication
"""
reactor.callLater(0, stop)
diff --git a/src/leap/bitmask/gui/ui/wizard.ui b/src/leap/bitmask/gui/ui/wizard.ui
index 6c592522..8c52897d 100644
--- a/src/leap/bitmask/gui/ui/wizard.ui
+++ b/src/leap/bitmask/gui/ui/wizard.ui
@@ -38,7 +38,7 @@
<property name="options">
<set>QWizard::IndependentPages</set>
</property>
- <widget class="QWizardPage" name="introduction_page">
+ <widget class="WizardPage" name="introduction_page">
<property name="title">
<string>Welcome</string>
</property>
diff --git a/src/leap/bitmask/gui/wizard.py b/src/leap/bitmask/gui/wizard.py
index 4d774907..f66c553d 100644
--- a/src/leap/bitmask/gui/wizard.py
+++ b/src/leap/bitmask/gui/wizard.py
@@ -33,6 +33,7 @@ from leap.bitmask.util.keyring_helpers import has_keyring
from ui_wizard import Ui_Wizard
+QtDelayedCall = QtCore.QTimer.singleShot
logger = logging.getLogger(__name__)
@@ -64,6 +65,8 @@ class Wizard(QtGui.QWizard):
self.ui = Ui_Wizard()
self.ui.setupUi(self)
+ self._connected_signals = []
+
self.setPixmap(QtGui.QWizard.LogoPixmap,
QtGui.QPixmap(":/images/mask-icon.png"))
@@ -79,8 +82,8 @@ class Wizard(QtGui.QWizard):
self._use_existing_provider = False
self.ui.grpCheckProvider.setVisible(False)
- self.ui.btnCheck.clicked.connect(self._check_provider)
- self.ui.lnProvider.returnPressed.connect(self._check_provider)
+ self._connect_and_track(self.ui.btnCheck.clicked, self._check_provider)
+ self._connect_and_track(self.ui.lnProvider.returnPressed, self._check_provider)
self._backend = backend
self._backend_connect()
@@ -95,24 +98,25 @@ class Wizard(QtGui.QWizard):
self._provider_select_defer = None
self._provider_setup_defer = None
- self.currentIdChanged.connect(self._current_id_changed)
+ self._connect_and_track(self.currentIdChanged, self._current_id_changed)
- self.ui.lnProvider.textChanged.connect(self._enable_check)
- self.ui.rbNewProvider.toggled.connect(
+ self._connect_and_track(self.ui.lnProvider.textChanged, self._enable_check)
+ self._connect_and_track(self.ui.rbNewProvider.toggled,
lambda x: self._enable_check())
- self.ui.cbProviders.currentIndexChanged[int].connect(
+ self._connect_and_track(self.ui.cbProviders.currentIndexChanged[int],
self._reset_provider_check)
- self.ui.lblUser.returnPressed.connect(
+ self._connect_and_track(self.ui.lblUser.returnPressed,
self._focus_password)
- self.ui.lblPassword.returnPressed.connect(
+ self._connect_and_track(self.ui.lblPassword.returnPressed,
self._focus_second_password)
- self.ui.lblPassword2.returnPressed.connect(
+ self._connect_and_track(self.ui.lblPassword2.returnPressed,
self._register)
- self.ui.btnRegister.clicked.connect(
+ self._connect_and_track(self.ui.btnRegister.clicked,
self._register)
- self.ui.rbExistingProvider.toggled.connect(self._skip_provider_checks)
+ self._connect_and_track(self.ui.rbExistingProvider.toggled,
+ self._skip_provider_checks)
usernameRe = QtCore.QRegExp(USERNAME_REGEX)
self.ui.lblUser.setValidator(
@@ -137,7 +141,19 @@ class Wizard(QtGui.QWizard):
self._provider_checks_ok = False
self._provider_setup_ok = False
- self.finished.connect(self._wizard_finished)
+ self._connect_and_track(self.finished, self._wizard_finished)
+
+ def _connect_and_track(self, signal, method):
+ """
+ Helper to connect signals and keep track of them.
+
+ :param signal: the signal to connect to.
+ :type signal: QtCore.Signal
+ :param method: the method to call when the signal is triggered.
+ :type method: callable, Slot or Signal
+ """
+ self._connected_signals.append((signal, method))
+ signal.connect(method)
@QtCore.Slot()
def _wizard_finished(self):
@@ -153,28 +169,35 @@ class Wizard(QtGui.QWizard):
self._provider_setup_ok = False
self.ui.lnProvider.setText('')
self.ui.grpCheckProvider.setVisible(False)
- self._backend_disconnect()
+ self._disconnect_tracked()
def _load_configured_providers(self):
"""
Loads the configured providers into the wizard providers combo box.
"""
+ self._backend.provider_get_pinned_providers()
+
+ def _load_configured_providers_with_pinned(self, pinned):
+ """
+ Once we have the pinned providers from the backend, we
+ continue setting everything up
+
+ :param pinned: list of pinned providers
+ :type pinned: list of str
+ """
ls = LeapSettings()
providers = ls.get_configured_providers()
- if not providers:
+ if not providers and not pinned:
self.ui.rbExistingProvider.setEnabled(False)
self.ui.label_8.setEnabled(False) # 'https://' label
self.ui.cbProviders.setEnabled(False)
return
- pinned = []
user_added = []
# separate pinned providers from user added ones
for p in providers:
- if ls.is_pinned_provider(p):
- pinned.append(p)
- else:
+ if p not in pinned:
user_added.append(p)
if user_added:
@@ -191,6 +214,9 @@ class Wizard(QtGui.QWizard):
# 'Use existing provider' option.
self.ui.rbExistingProvider.setChecked(True)
+ # We need to set it as complete explicitly
+ self.page(self.INTRO_PAGE).set_completed()
+
def get_domain(self):
return self._domain
@@ -543,7 +569,7 @@ class Wizard(QtGui.QWizard):
Set the details for the just downloaded provider.
:param details: the details of the provider.
- :type details: ProviderConfigLight
+ :type details: dict
"""
self._provider_details = details
@@ -614,9 +640,9 @@ class Wizard(QtGui.QWizard):
the user to enable or disable.
"""
self.ui.grpServices.setTitle(
- self.tr("Services by {0}").format(self._provider_details.name))
+ self.tr("Services by {0}").format(self._provider_details['name']))
- services = get_supported(self._provider_details.services)
+ services = get_supported(self._provider_details['services'])
for service in services:
try:
@@ -659,7 +685,7 @@ class Wizard(QtGui.QWizard):
if not self._provider_setup_ok:
self._reset_provider_setup()
sub_title = self.tr("Gathering configuration options for {0}")
- sub_title = sub_title.format(self._provider_details.name)
+ sub_title = sub_title.format(self._provider_details['name'])
self.page(pageId).setSubTitle(sub_title)
self.ui.lblDownloadCaCert.setPixmap(self.QUESTION_ICON)
self._provider_setup_defer = self._backend.\
@@ -667,22 +693,22 @@ class Wizard(QtGui.QWizard):
if pageId == self.PRESENT_PROVIDER_PAGE:
sub_title = self.tr("Description of services offered by {0}")
- sub_title = sub_title.format(self._provider_details.name)
+ sub_title = sub_title.format(self._provider_details['name'])
self.page(pageId).setSubTitle(sub_title)
details = self._provider_details
- name = "<b>{0}</b>".format(details.name)
- domain = "https://{0}".format(details.domain)
- description = "<i>{0}</i>".format(details.description)
+ name = "<b>{0}</b>".format(details['name'])
+ domain = "https://{0}".format(details['domain'])
+ description = "<i>{0}</i>".format(details['description'])
self.ui.lblProviderName.setText(name)
self.ui.lblProviderURL.setText(domain)
self.ui.lblProviderDesc.setText(description)
- self.ui.lblServicesOffered.setText(details.services_string)
- self.ui.lblProviderPolicy.setText(details.enrollment_policy)
+ self.ui.lblServicesOffered.setText(details['services_string'])
+ self.ui.lblProviderPolicy.setText(details['enrollment_policy'])
if pageId == self.REGISTER_USER_PAGE:
sub_title = self.tr("Register a new user with {0}")
- sub_title = sub_title.format(self._provider_details.name)
+ sub_title = sub_title.format(self._provider_details['name'])
self.page(pageId).setSubTitle(sub_title)
self.ui.chkRemember.setVisible(False)
@@ -727,36 +753,30 @@ class Wizard(QtGui.QWizard):
Connects all the backend signals with the wizard.
"""
sig = self._backend.signaler
- sig.prov_name_resolution.connect(self._name_resolution)
- sig.prov_https_connection.connect(self._https_connection)
- sig.prov_download_provider_info.connect(self._download_provider_info)
- sig.prov_get_details.connect(self._provider_get_details)
+ conntrack = self._connect_and_track
+ conntrack(sig.prov_name_resolution, self._name_resolution)
+ conntrack(sig.prov_https_connection, self._https_connection)
+ conntrack(sig.prov_download_provider_info,
+ self._download_provider_info)
+ conntrack(sig.prov_get_details, self._provider_get_details)
+ conntrack(sig.prov_get_pinned_providers,
+ self._load_configured_providers_with_pinned)
- sig.prov_download_ca_cert.connect(self._download_ca_cert)
- sig.prov_check_ca_fingerprint.connect(self._check_ca_fingerprint)
- sig.prov_check_api_certificate.connect(self._check_api_certificate)
+ conntrack(sig.prov_download_ca_cert, self._download_ca_cert)
+ conntrack(sig.prov_check_ca_fingerprint, self._check_ca_fingerprint)
+ conntrack(sig.prov_check_api_certificate, self._check_api_certificate)
- sig.srp_registration_finished.connect(self._registration_finished)
- sig.srp_registration_failed.connect(self._registration_failed)
- sig.srp_registration_taken.connect(self._registration_taken)
+ conntrack(sig.srp_registration_finished, self._registration_finished)
+ conntrack(sig.srp_registration_failed, self._registration_failed)
+ conntrack(sig.srp_registration_taken, self._registration_taken)
- def _backend_disconnect(self):
+ def _disconnect_tracked(self):
"""
This method is called when the wizard dialog is closed.
- We disconnect all the backend signals in here.
+ We disconnect all the signals in here.
"""
- sig = self._backend.signaler
- try:
- # disconnect backend signals
- sig.prov_name_resolution.disconnect(self._name_resolution)
- sig.prov_https_connection.disconnect(self._https_connection)
- sig.prov_download_provider_info.disconnect(
- self._download_provider_info)
-
- sig.prov_download_ca_cert.disconnect(self._download_ca_cert)
- sig.prov_check_ca_fingerprint.disconnect(
- self._check_ca_fingerprint)
- sig.prov_check_api_certificate.disconnect(
- self._check_api_certificate)
- except RuntimeError:
- pass # Signal was not connected
+ for signal, method in self._connected_signals:
+ try:
+ signal.disconnect(method)
+ except RuntimeError:
+ pass # Signal was not connected