Пример #1
0
 def __init__(self, plugin, window):
     super().__init__()  # top-level QObject, no parent()
     self.server = TimeoutServerProxy('http://%s:%d' % (HOST, PORT),
                                      allow_none=True,
                                      timeout=2.0)
     self.listener = Listener(self)
     self.plugin_ref = Weak.ref(plugin)
     self.window_ref = Weak.ref(window)
     self.cosigner_receive_signal.connect(self.on_receive)
Пример #2
0
    def __init__(self):
        super().__init__(
        )  # Top-level window. Parent needs to hold a reference to us and clean us up appropriately.
        self.setWindowTitle('Oregano - ' + _('Payment Request'))
        self.label = ''
        self.amount = 0
        self.setFocusPolicy(Qt.NoFocus)
        self.setSizePolicy(QSizePolicy.MinimumExpanding,
                           QSizePolicy.MinimumExpanding)

        main_box = QHBoxLayout(self)
        main_box.setContentsMargins(12, 12, 12, 12)
        self.qrw = QRCodeWidget()
        self.qrw.setSizePolicy(QSizePolicy.MinimumExpanding,
                               QSizePolicy.MinimumExpanding)
        main_box.addWidget(self.qrw, 2)

        vbox = QVBoxLayout()
        vbox.setContentsMargins(12, 12, 12, 12)
        main_box.addLayout(vbox, 2)
        main_box.addStretch(1)

        self.address_label = WWLabel()
        self.address_label.setTextInteractionFlags(Qt.TextSelectableByMouse)
        vbox.addWidget(self.address_label)

        self.msg_label = WWLabel()
        self.msg_label.setTextInteractionFlags(Qt.TextSelectableByMouse)
        vbox.addWidget(self.msg_label)

        self.amount_label = WWLabel()
        self.amount_label.setTextInteractionFlags(Qt.TextSelectableByMouse)
        vbox.addWidget(self.amount_label)

        self.op_return_label = WWLabel()
        self.op_return_label.setTextInteractionFlags(Qt.TextSelectableByMouse)
        vbox.addWidget(self.op_return_label)

        vbox.addStretch(2)

        copyBut = QPushButton(_("Copy QR Image"))
        saveBut = QPushButton(_("Save QR Image"))
        vbox.addLayout(Buttons(copyBut, saveBut))

        weakSelf = Weak.ref(
            self
        )  # Qt & Python GC hygeine: don't hold references to self in non-method slots as it appears Qt+Python GC don't like this too much and may leak memory in that case.
        weakQ = Weak.ref(self.qrw)
        weakBut = Weak.ref(copyBut)
        copyBut.clicked.connect(lambda: copy_to_clipboard(weakQ(), weakBut()))
        saveBut.clicked.connect(lambda: save_to_file(weakQ(), weakSelf()))
Пример #3
0
 def init_qt(self, gui):
     if self.initted: return  # already initted
     self.print_error("Initializing...")
     for window in gui.windows:
         self.on_new_window(window)
     Plugin.Instance_ref = Weak.ref(self)
     self.initted = True
Пример #4
0
 def settings_widget(self, window):
     while window and window_parent(window) and not isinstance(window_parent(window), ElectrumWindow):
         # MacOS fixup -- find window.parent() because we can end up with window.parent() not an ElectrumWindow
         window = window_parent(window)
     windowRef = Weak.ref(window)
     return EnterButton(_('Settings'),
                        partial(self.settings_dialog, windowRef))
Пример #5
0
 def __init__(self, state):
     super().__init__()
     self.daemon = True
     self.state_ref = Weak.ref(state)
     self.received = set()
     self.keyhashes = []
     self.timeoutQ = queue.Queue(
     )  # this queue's sole purpose is to provide an interruptible sleep
Пример #6
0
 def transaction_dialog(self, d):
     window, state = self._find_window_and_state_for_wallet(d.wallet)
     if window and state:
         d.cosigner_send_button = b = QPushButton(_("Send to cosigner"))
         b.wallet_ref = Weak.ref(window.wallet)
         b.clicked.connect(lambda: Plugin.do_send_static(d))
         d.buttons.insert(0, b)
         self.transaction_dialog_update(d)
Пример #7
0
 def settings_dialog(self, windowRef):
     window = windowRef() # NB: window is the internal plugins dialog and not the wallet window
     if not window or not isinstance(window_parent(window), ElectrumWindow): return
     wallet = window_parent(window).wallet
     d = WindowModalDialog(window.top_level_window(), _("Label Settings"))
     d.ok_button = OkButton(d)
     dlgRef = Weak.ref(d)
     if wallet in self.wallets:
         class MySigs(QObject):
             ok_button_disable_sig = pyqtSignal(bool)
         d.sigs = MySigs(d)
         d.sigs.ok_button_disable_sig.connect(d.ok_button.setDisabled) # disable ok button while the TaskThread runs ..
         hbox = QHBoxLayout()
         hbox.addWidget(QLabel(_("LabelSync options:")))
         upload = ThreadedButton(_("Force upload"),
                                 partial(Weak(self.do_force_upload), wallet, dlgRef),
                                 partial(Weak(self.done_processing), dlgRef),
                                 partial(Weak(self.error_processing), dlgRef))
         download = ThreadedButton(_("Force download"),
                                   partial(Weak(self.do_force_download), wallet, dlgRef),
                                   partial(Weak(self.done_processing), dlgRef),
                                   partial(Weak(self.error_processing), dlgRef))
         d.thread_buts = (upload, download)
         d.finished.connect(partial(Weak(self.on_dlg_finished), dlgRef))
         vbox = QVBoxLayout()
         vbox.addWidget(upload)
         vbox.addWidget(download)
         hbox.addLayout(vbox)
         vbox = QVBoxLayout(d)
         vbox.addLayout(hbox)
     else:
         vbox = QVBoxLayout(d)
         if wallet.network:
             # has network, so the fact that the wallet isn't in the list means it's incompatible
             l = QLabel('<b>' + _("LabelSync not supported for this wallet type") + '</b>')
             l.setAlignment(Qt.AlignCenter)
             vbox.addWidget(l)
             l = QLabel(_("(Only deterministic wallets are supported)"))
             l.setAlignment(Qt.AlignCenter)
             vbox.addWidget(l)
         else:
             # Does not have network, so we won't speak of incompatibility, but instead remind user offline mode means OFFLINE! ;)
             l = QLabel(_("You are using Oregano in offline mode; restart Oregano if you want to get connected"))
             l.setWordWrap(True)
             vbox.addWidget(l)
     vbox.addSpacing(20)
     vbox.addLayout(Buttons(d.ok_button))
     return bool(d.exec_())
Пример #8
0
    def __init__(self, parent):
        super().__init__(parent,
                         self.create_menu, [],
                         3,
                         deferred_updates=True)
        self.refresh_headers()
        self.setColumnHidden(1, True)
        # force attributes to always be defined, even if None, at construction.
        self.wallet = self.parent.wallet
        self.cleaned_up = False

        self.monospaceFont = QFont(MONOSPACE_FONT)
        self.withdrawalBrush = QBrush(QColor("#BC1E1E"))
        self.invoiceIcon = QIcon(":icons/seal")
        self._item_cache = Weak.ValueDictionary()
        self.itemChanged.connect(self.item_changed)

        self.has_unknown_balances = False
Пример #9
0
 def initiate_fetch_input_data(self, force):
     weakSelfRef = Weak.ref(self)
     def dl_prog(pct):
         slf = weakSelfRef()
         if slf:
             slf._dl_pct = pct
             slf.throttled_update_sig.emit()
     def dl_done():
         slf = weakSelfRef()
         if slf:
             slf._dl_pct = None
             slf.throttled_update_sig.emit()
             slf.dl_done_sig.emit()
     dl_retries = 0
     def dl_done_mainthread():
         nonlocal dl_retries
         slf = weakSelfRef()
         if slf:
             if slf._closed:
                 return
             dl_retries += 1
             fee = slf.try_calculate_fee()
             if fee is None and dl_retries < 2:
                 if not self.is_fetch_input_data():
                     slf.print_error("input fetch incomplete; network use is disabled in GUI")
                     return
                 # retry at most once -- in case a slow server scrwed us up
                 slf.print_error("input fetch appears incomplete; retrying download once ...")
                 slf.tx.fetch_input_data(self.wallet, done_callback=dl_done, prog_callback=dl_prog, force=True, use_network=self.is_fetch_input_data())  # in this case we reallly do force
             elif fee is not None:
                 slf.print_error("input fetch success")
             else:
                 slf.print_error("input fetch failed")
     try: self.dl_done_sig.disconnect()  # disconnect previous
     except TypeError: pass
     self.dl_done_sig.connect(dl_done_mainthread, Qt.QueuedConnection)
     self.tx.fetch_input_data(self.wallet, done_callback=dl_done, prog_callback=dl_prog, force=force, use_network=self.is_fetch_input_data())
Пример #10
0
class Plugin(BasePlugin):

    Instance_ref = Weak.ref(_Dead(
    ))  # Make sure Instance_ref is always defined, defaults to dead object

    def __init__(self, parent, config, name):
        BasePlugin.__init__(self, parent, config, name)
        self.windows = []
        self.initted = False

    @hook
    def init_qt(self, gui):
        if self.initted: return  # already initted
        self.print_error("Initializing...")
        for window in gui.windows:
            self.on_new_window(window)
        Plugin.Instance_ref = Weak.ref(self)
        self.initted = True

    @hook
    def on_new_window(self, window):
        try:
            wallet = window.wallet
        except AttributeError:
            # this can happen if wallet is not started up properly
            self.print_error(
                "WARNING: Window {} lacks a wallet -- startup race condition likely. FIXME!"
                .format(window.diagnostic_name()))
            return
        if isinstance(wallet, Multisig_Wallet):
            window.cosigner_pool_state = state = State(self, window)
            self.windows.append(window)
            self.update(window)
            # un-gray-out buttons for tx dialogs left around related to this window
            for b in Plugin.get_all_cosigner_buttons():
                if b.wallet_ref() == wallet:
                    b.setEnabled(True)

    @hook
    def on_close_window(self, window):
        if window in self.windows:
            state = getattr(window, 'cosigner_pool_state', None)
            if state:
                if state.listener:
                    self.print_error("shutting down listener for",
                                     window.diagnostic_name())
                    state.listener.stop_join()
                state.deleteLater()
                delattr(window, 'cosigner_pool_state')
            self.print_error("unregistered for window",
                             window.diagnostic_name())
            self.windows.remove(window)
            # gray out buttons for tx dialogs left around related to this window
            for b in Plugin.get_all_cosigner_buttons():
                if b.wallet_ref() == window.wallet:
                    b.setEnabled(False)

    @staticmethod
    def get_all_cosigner_buttons():
        ret = []
        app = QApplication.instance()
        for w in app.topLevelWidgets():
            if isinstance(w, TxDialog):
                but = getattr(w, 'cosigner_send_button', None)
                if but: ret.append(but)
        return ret

    def is_available(self):
        return True

    def on_close(self):
        for w in self.windows.copy():
            self.on_close_window(w)
        self.windows = []
        self.initted = False
        super().on_close()

    def update(self, window):
        wallet = window.wallet
        state = window.cosigner_pool_state
        if not state:
            self.print_error("No cosigner pool state object for window",
                             window.diagnostic_name())
            return
        listener = state.listener
        state.keys = []
        state.cosigner_list = []
        for key, keystore in wallet.keystores.items():
            xpub = keystore.get_master_public_key()
            K = bitcoin.deserialize_xpub(xpub)[-1]
            _hash = bh2u(bitcoin.Hash(K))
            if not keystore.is_watching_only():
                state.keys.append((key, _hash))
            else:
                state.cosigner_list.append((xpub, K, _hash))
        listener.set_keyhashes([t[1] for t in state.keys])
        if not listener.is_running():
            self.print_error("Starting listener for", window.diagnostic_name())
            listener.start()

    @hook
    def transaction_dialog(self, d):
        window, state = self._find_window_and_state_for_wallet(d.wallet)
        if window and state:
            d.cosigner_send_button = b = QPushButton(_("Send to cosigner"))
            b.wallet_ref = Weak.ref(window.wallet)
            b.clicked.connect(lambda: Plugin.do_send_static(d))
            d.buttons.insert(0, b)
            self.transaction_dialog_update(d)

    @hook
    def transaction_dialog_update(self, d):
        window, state = self._find_window_and_state_for_wallet(d.wallet)
        but = getattr(d, 'cosigner_send_button', None)
        if not but or not window or not state or d.tx.is_complete(
        ) or d.wallet.can_sign(d.tx):
            but and but.hide()
            return
        for xpub, K, _hash in state.cosigner_list:
            if self.cosigner_can_sign(d.tx, xpub):
                but and but.show()
                break
        else:
            but and but.hide()

    def _find_window_and_state_for_wallet(self, wallet):
        for window in self.windows:
            if window.wallet == wallet:
                return window, window.cosigner_pool_state
        return None, None

    def cosigner_can_sign(self, tx, cosigner_xpub):
        from oregano.keystore import is_xpubkey, parse_xpubkey
        xpub_set = set([])
        for txin in tx.inputs():
            for x_pubkey in txin['x_pubkeys']:
                if is_xpubkey(x_pubkey):
                    xpub, s = parse_xpubkey(x_pubkey)
                    xpub_set.add(xpub)
        return cosigner_xpub in xpub_set

    @staticmethod
    def do_send_static(d):
        ''' Decouples button slot from running instance in case user stops/restarts the plugin while TxDialogs are up. '''
        plugin = Plugin.Instance_ref()
        if plugin:
            plugin.do_send(d)
        else:
            print_error("[cosigner_pool] No plugin.")

    def do_send(self, d):
        tx = d.tx
        window, state = self._find_window_and_state_for_wallet(d.wallet)
        if not tx or not window or not state:
            self.print_error("Missing tx or window or state")
            return
        for xpub, K, _hash in state.cosigner_list:
            if not self.cosigner_can_sign(tx, xpub):
                continue
            message = bitcoin.encrypt_message(bfh(tx.raw),
                                              bh2u(K)).decode('ascii')
            try:
                state.server.put(_hash, message)
            except Exception as e:
                traceback.print_exc(file=sys.stdout)
                window.show_error(
                    _("Failed to send transaction to cosigning pool."))
                return
            d.show_message(
                _("Your transaction was sent to the cosigning pool.") + '\n' +
                _("Open your cosigner wallet to retrieve it."))

    def on_receive(self, window, keyhash, message):
        self.print_error("signal arrived for", keyhash, "@",
                         window.diagnostic_name())
        state = getattr(window, 'cosigner_pool_state', None)
        if not state:
            self.print_error("Error: state object not found")
            return
        keys = state.keys
        for key, _hash in keys:
            if _hash == keyhash:
                break
        else:
            self.print_error("keyhash not found")
            return

        wallet = window.wallet
        if isinstance(wallet.keystore, keystore.Hardware_KeyStore):
            window.show_warning(
                _('An encrypted transaction was retrieved from cosigning pool.'
                  ) + '\n' +
                _('However, hardware wallets do not support message decryption, '
                  'which makes them not compatible with the current design of cosigner pool.'
                  ))
            return
        password = None
        if wallet.has_password():
            password = window.password_dialog(
                _('An encrypted transaction was retrieved from cosigning pool.'
                  ) + '\n' + _('Please enter your password to decrypt it.'))
            if not password:
                return
        else:
            details = (_(
                "If you choose 'Yes', it will be decrypted and a transaction window will be shown, giving you the opportunity to sign the transaction."
            ) + "\n\n" + _(
                "If you choose 'No', you will be asked again later (the next time this wallet window is opened)."
            ))
            ret = window.msg_box(
                icon=QMessageBox.Question,
                parent=None,
                title=_("Cosigner Pool"),
                buttons=QMessageBox.Yes | QMessageBox.No,
                text=
                _("An encrypted transaction was retrieved from cosigning pool."
                  ) + '\n' + _("Do you want to open it now?"),
                detail_text=details)
            if ret != QMessageBox.Yes:
                return

        err, badpass = "******", False
        try:
            xprv = wallet.keystore.get_master_private_key(password)
        except InvalidPassword as e:
            err, badpass = str(e), True
            xprv = None
        if not xprv:
            window.show_error(err)
            if badpass:
                self.on_receive(window, keyhash, message)  # try again
            return
        try:
            k = bh2u(bitcoin.deserialize_xprv(xprv)[-1])
            EC = bitcoin.EC_KEY(bfh(k))
            message = bh2u(EC.decrypt_message(message))
        except Exception as e:
            traceback.print_exc(file=sys.stdout)
            window.show_error(repr(e))
            return

        state.listener.clear(keyhash)
        tx = transaction.Transaction(message)
        show_transaction(tx, window, prompt_if_unsaved=True)
Пример #11
0
    def __init__(self, tx, parent, desc, prompt_if_unsaved):
        '''Transactions in the wallet will show their description.
        Pass desc to give a description for txs not yet in the wallet.
        '''
        # We want to be a top-level window
        QDialog.__init__(self, parent=None)
        # Take a copy; it might get updated in the main window by
        # e.g. the FX plugin.  If this happens during or after a long
        # sign operation the signatures are lost.
        self.tx = copy.deepcopy(tx)
        self.tx.deserialize()
        self.main_window = parent
        self.wallet = parent.wallet
        self.prompt_if_unsaved = prompt_if_unsaved
        self.saved = False
        self.desc = desc
        self.cashaddr_signal_slots = []
        self._dl_pct = None
        self._closed = False
        self.tx_hash = self.tx.txid_fast() if self.tx.raw and self.tx.is_complete() else None
        self.tx_height = self.wallet.get_tx_height(self.tx_hash)[0] or None
        self.block_hash = None
        Weak.finalization_print_error(self)  # track object lifecycle

        self.setMinimumWidth(750)
        self.setWindowTitle(_("Transaction"))

        vbox = QVBoxLayout()
        self.setLayout(vbox)

        self.tx_hash_e  = ButtonsLineEdit()
        l = QLabel(_("&Transaction ID:"))
        l.setBuddy(self.tx_hash_e)
        vbox.addWidget(l)
        self.tx_hash_e.addCopyButton()
        weakSelfRef = Weak.ref(self)
        qr_show = lambda: weakSelfRef() and weakSelfRef().main_window.show_qrcode(str(weakSelfRef().tx_hash_e.text()), _("Transaction ID"), parent=weakSelfRef())
        icon = ":icons/qrcode_white.svg" if ColorScheme.dark_scheme else ":icons/qrcode.svg"
        self.tx_hash_e.addButton(icon, qr_show, _("Show as QR code"))
        self.tx_hash_e.setReadOnly(True)
        vbox.addWidget(self.tx_hash_e)
        self.tx_desc = QLabel()
        vbox.addWidget(self.tx_desc)
        self.status_label = QLabel()
        vbox.addWidget(self.status_label)
        self.date_label = QLabel()
        vbox.addWidget(self.date_label)
        self.amount_label = QLabel()
        vbox.addWidget(self.amount_label)
        self.size_label = QLabel()
        vbox.addWidget(self.size_label)
        self.fee_label = QLabel()
        vbox.addWidget(self.fee_label)

        for l in (self.tx_desc, self.status_label, self.date_label, self.amount_label, self.size_label, self.fee_label):
            # make these labels selectable by mouse in case user wants to copy-paste things in tx dialog
            l.setTextInteractionFlags(l.textInteractionFlags() | Qt.TextSelectableByMouse)

        def open_be_url(link):
            if link:
                try:
                    kind, thing = link.split(':')
                    url = web.BE_URL(self.main_window.config, kind, thing)
                except:
                    url = None
                if url:
                    webopen( url )
                else:
                    self.show_error(_('Unable to open in block explorer. Please be sure your block explorer is configured correctly in preferences.'))

        self.status_label.linkActivated.connect(open_be_url)

        self.add_io(vbox)

        self.freeze_button = b = QPushButton(self._make_freeze_button_text())
        b.setToolTip(_("Lock/unlock the coin(s) being spent in this transaction.\n\n"
                       "Use this facility if you wish to broadcast this transaction later,\n"
                       "in order to prevent its inputs from being accidentally spent."))
        b.clicked.connect(self.do_freeze_unfreeze)

        self.sign_button = b = QPushButton(_("&Sign"))
        b.clicked.connect(self.sign)
        b.setToolTip(_("Sign the transaction"))

        self.broadcast_button = b = QPushButton(_("&Broadcast"))
        b.clicked.connect(self.do_broadcast)
        b.setToolTip(_("Submit the transaction to the blockchain"))
        self.last_broadcast_time = 0

        self.save_button = b = QPushButton(_("S&ave"))
        b.setToolTip(_("Save the transaction to a file"))
        b.clicked.connect(self.save)

        self.cancel_button = b = CloseButton(self)

        self.qr_button = b = QPushButton()
        b.setToolTip(_("Show transaction QR code"))
        b.setIcon(QIcon(icon))
        b.clicked.connect(self.show_qr)
        b.setShortcut(QKeySequence(Qt.ALT + Qt.Key_Q))

        self.copy_button = b = CopyButton(lambda: str(weakSelfRef() and weakSelfRef().tx),
                                          callback=lambda: weakSelfRef() and weakSelfRef().show_message(
                                              _("Transaction raw hex copied to clipboard.")))
        b.setToolTip(_("Copy transaction raw hex to the clipboard"))


        # Action buttons
        self.buttons = [self.freeze_button, self.sign_button, self.broadcast_button, self.cancel_button]
        # Transaction sharing buttons
        self.sharing_buttons = [self.copy_button, self.qr_button, self.save_button]

        run_hook('transaction_dialog', self)

        hbox = QHBoxLayout()
        hbox.addLayout(Buttons(*self.sharing_buttons))
        hbox.addStretch(1)
        hbox.addLayout(Buttons(*self.buttons))
        vbox.addLayout(hbox)

        if self.tx_height:
            # this avoids downloading the block_height info if we already have it.
            self.tx.ephemeral['block_height'] = self.tx_height

        self.throttled_update_sig.connect(self.throttled_update, Qt.QueuedConnection)
        self.initiate_fetch_input_data(True)

        self.update()

        # connect slots so we update in realtime as blocks come in, etc
        parent.history_updated_signal.connect(self.update_tx_if_in_wallet)
        parent.labels_updated_signal.connect(self.update_tx_if_in_wallet)
        parent.network_signal.connect(self.got_verified_tx)
Пример #12
0
class Plugin(LabelsPlugin):

    def __init__(self, *args):
        LabelsPlugin.__init__(self, *args)
        self.obj = LabelsSignalObject()
        self.wallet_windows = {}
        self.initted = False

    def requires_settings(self):
        return True

    def settings_widget(self, window):
        while window and window_parent(window) and not isinstance(window_parent(window), ElectrumWindow):
            # MacOS fixup -- find window.parent() because we can end up with window.parent() not an ElectrumWindow
            window = window_parent(window)
        windowRef = Weak.ref(window)
        return EnterButton(_('Settings'),
                           partial(self.settings_dialog, windowRef))

    def settings_dialog(self, windowRef):
        window = windowRef() # NB: window is the internal plugins dialog and not the wallet window
        if not window or not isinstance(window_parent(window), ElectrumWindow): return
        wallet = window_parent(window).wallet
        d = WindowModalDialog(window.top_level_window(), _("Label Settings"))
        d.ok_button = OkButton(d)
        dlgRef = Weak.ref(d)
        if wallet in self.wallets:
            class MySigs(QObject):
                ok_button_disable_sig = pyqtSignal(bool)
            d.sigs = MySigs(d)
            d.sigs.ok_button_disable_sig.connect(d.ok_button.setDisabled) # disable ok button while the TaskThread runs ..
            hbox = QHBoxLayout()
            hbox.addWidget(QLabel(_("LabelSync options:")))
            upload = ThreadedButton(_("Force upload"),
                                    partial(Weak(self.do_force_upload), wallet, dlgRef),
                                    partial(Weak(self.done_processing), dlgRef),
                                    partial(Weak(self.error_processing), dlgRef))
            download = ThreadedButton(_("Force download"),
                                      partial(Weak(self.do_force_download), wallet, dlgRef),
                                      partial(Weak(self.done_processing), dlgRef),
                                      partial(Weak(self.error_processing), dlgRef))
            d.thread_buts = (upload, download)
            d.finished.connect(partial(Weak(self.on_dlg_finished), dlgRef))
            vbox = QVBoxLayout()
            vbox.addWidget(upload)
            vbox.addWidget(download)
            hbox.addLayout(vbox)
            vbox = QVBoxLayout(d)
            vbox.addLayout(hbox)
        else:
            vbox = QVBoxLayout(d)
            if wallet.network:
                # has network, so the fact that the wallet isn't in the list means it's incompatible
                l = QLabel('<b>' + _("LabelSync not supported for this wallet type") + '</b>')
                l.setAlignment(Qt.AlignCenter)
                vbox.addWidget(l)
                l = QLabel(_("(Only deterministic wallets are supported)"))
                l.setAlignment(Qt.AlignCenter)
                vbox.addWidget(l)
            else:
                # Does not have network, so we won't speak of incompatibility, but instead remind user offline mode means OFFLINE! ;)
                l = QLabel(_("You are using Oregano in offline mode; restart Oregano if you want to get connected"))
                l.setWordWrap(True)
                vbox.addWidget(l)
        vbox.addSpacing(20)
        vbox.addLayout(Buttons(d.ok_button))
        return bool(d.exec_())

    def on_dlg_finished(self, dlgRef, result_code):
        ''' Wait for any threaded buttons that may be still extant so we don't get a crash '''
        #self.print_error("Dialog finished with code", result_code)
        dlg = dlgRef()
        if dlg:
            upload, download = dlg.thread_buts
            if upload.thread and upload.thread.isRunning():
                upload.thread.stop(); upload.thread.wait()
            if download.thread and download.thread.isRunning():
                download.thread.stop(); download.thread.wait()

    def do_force_upload(self, wallet, dlgRef):
        # this runs in a NON-GUI thread
        dlg = dlgRef()
        if dlg: dlg.sigs.ok_button_disable_sig.emit(True) # block window closing prematurely which can cause a temporary hang until thread completes
        self.push_thread(wallet)

    def do_force_download(self, wallet, dlgRef):
        # this runs in a NON-GUI thread
        dlg = dlgRef()
        if dlg: dlg.sigs.ok_button_disable_sig.emit(True) # block window closing prematurely which can cause a temporary hang until thread completes
        self.pull_thread(wallet, True)

    def done_processing(self, dlgRef, result):
        # this runs in the GUI thread
        dlg = dlgRef()
        if dlg:
            dlg.ok_button.setEnabled(True)
            self._ok_synched(dlg)

    def _ok_synched(self, window):
        if window.isVisible():
            window.show_message(_("Your labels have been synchronised."))

    def error_processing(self, dlgRef, exc_info):
        dlg = dlgRef()
        if dlg:
            dlg.ok_button.setEnabled(True)
            self._notok_synch(dlg, exc_info)

    _warn_dlg_flg = Weak.KeyDictionary()
    def _notok_synch(self, window, exc_info):
        # Runs in main thread
        cls = __class__
        if window.isVisible() and not cls._warn_dlg_flg.get(window, False):
            # Guard against duplicate error dialogs (without this we may get error window spam when importing labels)
            cls._warn_dlg_flg[window] = True
            window.show_warning(_("LabelSync error:") + "\n\n" + str(exc_info[1]), rich_text=False)
            cls._warn_dlg_flg.pop(window, None)

    def on_request_exception(self, wallet, exc_info):
        # not main thread
        self.obj.request_exception_signal.emit(wallet, exc_info)

    def request_exception_slot(self, wallet, exc_info):
        # main thread
        window = self.wallet_windows.get(wallet, None)
        if window: self._notok_synch(window, exc_info)

    def start_wallet(self, wallet, window=None):
        ret = super().start_wallet(wallet)
        if ret and window:
            self.wallet_windows[wallet] = window
        return ret

    def stop_wallet(self, wallet):
        ret = super().stop_wallet(wallet)
        window = self.wallet_windows.pop(wallet, None)
        return ret

    def on_pulled(self, wallet):
        # not main thread
        super().on_pulled(wallet) # super just logs to print_error
        self.obj.labels_changed_signal.emit(wallet)

    def on_labels_changed(self, wallet):
        # main thread
        window = self.wallet_windows.get(wallet, None)
        if window:
            #self.print_error("On labels changed", wallet.basename())
            window.update_labels()

    def on_wallet_not_synched(self, wallet):
        # not main thread
        self.obj.wallet_not_synched_signal.emit(wallet)

    def wallet_not_synched_slot(self, wallet):
        # main thread
        window = self.wallet_windows.get(wallet, None)
        if window:
            if window.question(_("LabelSync detected that this wallet is not synched with the label server.")
                               + "\n\n" + _("Synchronize now?")):
                WaitingDialog(window, _("Synchronizing..."),
                              partial(self.pull_thread, wallet, True),
                              lambda *args: self._ok_synched(window),
                              lambda exc: self._notok_synch(window, exc))

    def on_close(self):
        if not self.initted:
            return
        try: self.obj.labels_changed_signal.disconnect(self.on_labels_changed)
        except TypeError: pass # not connected
        try: self.obj.wallet_not_synched_signal.disconnect(self.wallet_not_synched_slot)
        except TypeError: pass # not connected
        try: self.obj.request_exception_signal.disconnect(self.request_exception_slot)
        except TypeError: pass # not connected
        super().on_close()
        assert 0==len(self.wallet_windows), "LabelSync still had extant wallet_windows!"
        self.initted = False

    @hook
    def on_new_window(self, window):
        return self.start_wallet(window.wallet, window)

    @hook
    def on_close_window(self, window):
        return self.stop_wallet(window.wallet)

    @hook
    def init_qt(self, gui):
        if self.initted:
            return
        self.on_init()
        # connect signals. this needs to happen first as below on_new_window depends on these being active
        self.obj.labels_changed_signal.connect(self.on_labels_changed)
        self.obj.wallet_not_synched_signal.connect(self.wallet_not_synched_slot)
        self.obj.request_exception_signal.connect(self.request_exception_slot)

        ct, ct2 = 0, 0
        for window in gui.windows:
            if self.on_new_window(window):
                ct2 += 1
            ct += 1

        self.initted = True
        self.print_error("Initialized (had {} extant windows, added {}).".format(ct,ct2))