Beispiel #1
0
class SyncWidget(QWidget):
    def __init__(self, gui, do_user_config, selected_book_ids,
                 is_sync_selected):
        QWidget.__init__(self, gui)

        api.build_request('/limits')

        self.logger = Logger(
            path.join(gui.current_db.library_path, 'bookfusion_sync.log'))
        self.logger.info(
            'Open sync dialog: selected_book_ids={}; is_sync_selected={}'.
            format(selected_book_ids, is_sync_selected))

        if len(selected_book_ids) == 0:
            is_sync_selected = False

        self.worker_thread = None

        self.do_user_config = do_user_config
        self.db = gui.current_db.new_api

        self.selected_book_ids = selected_book_ids

        self.l = QVBoxLayout()
        self.l.setContentsMargins(0, 0, 0, 0)
        self.setLayout(self.l)

        self.radio_layout = QVBoxLayout()
        self.l.addLayout(self.radio_layout)

        self.sync_all_radio = QRadioButton('Sync all books')
        self.sync_all_radio.setChecked(not is_sync_selected)
        self.radio_layout.addWidget(self.sync_all_radio)

        sync_selected_radio_label = 'Sync selected books'
        if len(selected_book_ids) > 0:
            sync_selected_radio_label = 'Sync {} selected {}'.format(
                len(selected_book_ids),
                'book' if len(selected_book_ids) == 1 else 'books')
        self.sync_selected_radio = QRadioButton(sync_selected_radio_label)
        self.sync_selected_radio.toggled.connect(self.toggle_sync_selected)
        self.sync_selected_radio.setChecked(is_sync_selected)
        self.sync_selected_radio.setEnabled(len(selected_book_ids) > 0)
        self.radio_layout.addWidget(self.sync_selected_radio)

        self.reupload_possible = len(selected_book_ids) > 0 and len(
            selected_book_ids) <= 100
        if self.reupload_possible:
            for book_id in selected_book_ids:
                identifiers = self.db.get_proxy_metadata(book_id).identifiers
                if not identifiers.get('bookfusion'):
                    self.reupload_possible = False

        self.reupload_checkbox = QCheckBox('Re-upload book files', self)
        self.reupload_checkbox.setVisible(is_sync_selected
                                          and self.reupload_possible)
        self.radio_layout.addWidget(self.reupload_checkbox)

        self.btn_layout = QHBoxLayout()
        self.l.addLayout(self.btn_layout)

        self.config_btn = QPushButton('Configure')
        self.config_btn.clicked.connect(self.config)
        self.btn_layout.addWidget(self.config_btn)

        self.btn_layout.addStretch()

        self.start_btn = QPushButton('Start')
        self.start_btn.clicked.connect(self.start)
        self.btn_layout.addWidget(self.start_btn)

        self.cancel_btn = QPushButton('Cancel')
        self.cancel_btn.clicked.connect(self.cancel)
        self.cancel_btn.hide()
        self.btn_layout.addWidget(self.cancel_btn)

        self.info = QHBoxLayout()
        self.info.setContentsMargins(0, 0, 0, 0)
        self.l.addLayout(self.info)
        self.msg = QLabel()
        self.info.addWidget(self.msg)
        self.info.addStretch()
        self.log_btn = QLabel('<a href="#">Log</a>')
        self.log_btn.linkActivated.connect(self.toggle_log)
        self.log_btn.hide()
        self.info.addWidget(self.log_btn)

        self.log = QTableWidget(0, 2)
        self.log.setHorizontalHeaderLabels(['Book', 'Message'])
        self.log.horizontalHeader().setStretchLastSection(True)
        self.log.hide()
        self.l.addWidget(self.log)

        self.apply_config()

    def __del__(self):
        if self.worker_thread:
            self.worker_thread.quit()
            self.worker_thread.terminate()

    def config(self):
        self.do_user_config(parent=self)
        self.apply_config()

    def apply_config(self):
        configured = bool(prefs['api_key'])
        self.start_btn.setEnabled(configured)

    def toggle_sync_selected(self, is_sync_selected):
        if hasattr(self, 'reupload_checkbox'):
            self.reupload_checkbox.setVisible(is_sync_selected
                                              and self.reupload_possible)

    def start(self):
        if self.sync_selected_radio.isChecked(
        ) and self.reupload_checkbox.isChecked():
            reply = QMessageBox.question(
                self, 'BookFusion Sync',
                'Re-uploading book files can potentially result in previous highlights or bookmarks no longer working.\n\nPreviously uploaded files will be overwritten. Are you sure you want to re-upload?',
                QMessageBox.No | QMessageBox.Yes, QMessageBox.Yes)
            if reply != QMessageBox.Yes:
                return

        self.worker = None
        self.valid_book_ids = None
        self.book_log_map = {}
        self.book_progress_map = {}

        if self.sync_selected_radio.isChecked():
            book_ids = list(self.selected_book_ids)
        else:
            book_ids = list(self.db.all_book_ids())

        self.logger.info('Start sync: sync_selected={}; book_ids={}'.format(
            self.sync_selected_radio.isChecked(), book_ids))

        self.in_progress = True
        self.total = len(book_ids)
        self.update_progress(None)
        self.start_btn.hide()
        self.cancel_btn.show()
        self.config_btn.setEnabled(False)
        self.sync_all_radio.setEnabled(False)
        self.sync_selected_radio.setEnabled(False)

        self.worker_thread = QThread(self)

        self.worker = CheckWorker(self.db, self.logger, book_ids)
        self.worker.finished.connect(self.finish_check)
        self.worker.finished.connect(self.worker_thread.quit)
        self.worker.progress.connect(self.update_progress)
        self.worker.limitsAvailable.connect(self.apply_limits)
        self.worker.resultsAvailable.connect(self.apply_results)
        self.worker.aborted.connect(self.abort)
        self.worker.moveToThread(self.worker_thread)

        self.worker_thread.started.connect(self.worker.start)
        self.worker_thread.start()

    def apply_limits(self, limits):
        self.logger.info('Limits: {}'.format(limits))
        self.limits = limits

    def apply_results(self, books_count, valid_ids):
        self.logger.info('Check results: books_count={}; valid_ids={}'.format(
            books_count, valid_ids))
        self.valid_book_ids = valid_ids
        self.books_count = books_count

    def finish_check(self):
        if self.valid_book_ids:
            is_filesize_exceeded = len(self.valid_book_ids) < self.books_count
            is_total_books_exceeded = self.limits[
                'total_books'] and self.books_count > self.limits['total_books']

            if is_filesize_exceeded or is_total_books_exceeded:
                if self.limits['message']:
                    msg_box = QMessageBox(self)
                    msg_box.setWindowTitle('BookFusion Sync')
                    msg_box.addButton(QMessageBox.No)
                    msg_box.addButton(QMessageBox.Yes)
                    msg_box.setText(self.limits['message'])
                    msg_box.setDefaultButton(QMessageBox.Yes)
                    reply = msg_box.exec_()
                    if reply == QMessageBox.Yes:
                        self.start_sync()
                    else:
                        self.in_progress = False
                        self.msg.setText('Canceled.')
                        self.finish_sync()
                else:
                    self.start_sync()
            else:
                self.start_sync()
        else:
            if self.in_progress:
                self.in_progress = False
                self.msg.setText('No supported books selected.')
            self.finish_sync()

    def start_sync(self):
        self.log_btn.show()
        self.log.setRowCount(0)
        self.log.show()

        self.worker_thread = QThread(self)

        book_ids = self.valid_book_ids
        if self.limits['total_books']:
            book_ids = book_ids[:self.limits['total_books']]

        self.total = len(book_ids)

        self.worker = UploadManager(
            self.db, self.logger, book_ids,
            self.sync_selected_radio.isChecked()
            and self.reupload_checkbox.isChecked())
        self.worker.finished.connect(self.finish_sync)
        self.worker.finished.connect(self.worker_thread.quit)
        self.worker.progress.connect(self.update_progress)
        self.worker.uploadProgress.connect(self.update_upload_progress)
        self.worker.started.connect(self.log_start)
        self.worker.skipped.connect(self.log_skip)
        self.worker.failed.connect(self.log_fail)
        self.worker.uploaded.connect(self.log_upload)
        self.worker.updated.connect(self.log_update)
        self.worker.aborted.connect(self.abort)
        self.worker.moveToThread(self.worker_thread)

        self.worker_thread.started.connect(self.worker.start)
        self.worker_thread.start()

    def finish_sync(self):
        if self.in_progress:
            self.msg.setText('Done.')
        self.cancel_btn.hide()
        self.cancel_btn.setEnabled(True)
        self.start_btn.show()
        self.config_btn.setEnabled(True)
        self.sync_all_radio.setEnabled(True)
        self.sync_selected_radio.setEnabled(len(self.selected_book_ids) > 0)

    def abort(self, error):
        self.in_progress = False
        self.msg.setText(error)

    def cancel(self):
        self.in_progress = False
        self.msg.setText('Canceled.')
        self.cancel_btn.setEnabled(False)
        self.worker.cancel()

    def update_progress(self, progress):
        if self.in_progress:
            if isinstance(self.worker, UploadManager):
                msg = 'Synchronizing...'
            else:
                msg = 'Preparing...'
            if progress:
                msg += ' {} of {}'.format(progress + 1, self.total)
            self.msg.setText(msg)

    def update_upload_progress(self, book_id, sent, total):
        if not book_id in self.book_progress_map:
            return

        progress = self.book_progress_map[book_id]

        if sent < total:
            progress.setValue(sent)
            progress.setMaximum(total)
        else:
            progress.setMaximum(0)

    def log_start(self, book_id):
        self.update_log(book_id, None)

    def log_fail(self, book_id, msg):
        self.update_log(book_id, msg)

    def log_skip(self, book_id):
        self.update_log(book_id, 'skipped')

    def log_upload(self, book_id):
        self.update_log(book_id, 'uploaded')

    def log_update(self, book_id):
        self.update_log(book_id, 'updated')

    def toggle_log(self, _):
        self.log.setVisible(not self.log.isVisible())

    def update_log(self, book_id, msg):
        if book_id in self.book_log_map:
            index = self.book_log_map[book_id]
        else:
            index = self.log.rowCount()
            self.book_log_map[book_id] = index

            self.log.insertRow(index)

            title = self.db.get_proxy_metadata(book_id).title
            title_item = QTableWidgetItem(title)
            title_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled
                                | Qt.ItemNeverHasChildren)
            self.log.setItem(index, 0, title_item)

            progress = QProgressBar()
            progress.setMaximum(0)
            self.log.setCellWidget(index, 1, progress)
            self.book_progress_map[book_id] = progress

        if not msg is None:
            del (self.book_progress_map[book_id])
            self.log.setCellWidget(index, 1, None)

            msg_item = QTableWidgetItem(msg)
            msg_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled
                              | Qt.ItemNeverHasChildren)
            self.log.setItem(index, 1, msg_item)

    def maybe_cancel(self):
        if self.worker_thread and self.worker_thread.isRunning():
            reply = QMessageBox.question(
                self, 'BookFusion Sync',
                'Are you sure you want to cancel the currently running process?',
                QMessageBox.No | QMessageBox.Yes, QMessageBox.Yes)
            if reply == QMessageBox.Yes:
                self.cancel()
            else:
                return False
        return True
Beispiel #2
0
class ProceedQuestion(QDialog):

    ask_question = pyqtSignal(object, object, object)

    def __init__(self, parent):
        QDialog.__init__(self, parent)
        self.setAttribute(Qt.WA_DeleteOnClose, False)
        self.setWindowIcon(QIcon(I('dialog_question.png')))

        self.questions = []

        self._l = l = QGridLayout(self)
        self.setLayout(l)

        self.icon_label = ic = QLabel(self)
        ic.setPixmap(QPixmap(I('dialog_question.png')))
        self.msg_label = msg = QLabel('some random filler text')
        msg.setWordWrap(True)
        ic.setMaximumWidth(110)
        ic.setMaximumHeight(100)
        ic.setScaledContents(True)
        ic.setStyleSheet('QLabel { margin-right: 10px }')
        self.bb = QDialogButtonBox()
        self.bb.accepted.connect(self.accept)
        self.bb.rejected.connect(self.reject)
        self.log_button = self.bb.addButton(_('View log'), self.bb.ActionRole)
        self.log_button.setIcon(QIcon(I('debug.png')))
        self.log_button.clicked.connect(self.show_log)
        self.copy_button = self.bb.addButton(_('&Copy to clipboard'),
                                             self.bb.ActionRole)
        self.copy_button.clicked.connect(self.copy_to_clipboard)
        self.action_button = self.bb.addButton('', self.bb.ActionRole)
        self.action_button.clicked.connect(self.action_clicked)
        self.show_det_msg = _('Show &details')
        self.hide_det_msg = _('Hide &details')
        self.det_msg_toggle = self.bb.addButton(self.show_det_msg,
                                                self.bb.ActionRole)
        self.det_msg_toggle.clicked.connect(self.toggle_det_msg)
        self.det_msg_toggle.setToolTip(
            _('Show detailed information about this error'))
        self.det_msg = QPlainTextEdit(self)
        self.det_msg.setReadOnly(True)
        self.bb.setStandardButtons(self.bb.Yes | self.bb.No | self.bb.Ok)
        self.bb.button(self.bb.Yes).setDefault(True)

        self.checkbox = QCheckBox('', self)

        l.addWidget(ic, 0, 0, 1, 1)
        l.addWidget(msg, 0, 1, 1, 1)
        l.addWidget(self.checkbox, 1, 0, 1, 2)
        l.addWidget(self.det_msg, 2, 0, 1, 2)
        l.addWidget(self.bb, 3, 0, 1, 2)

        self.ask_question.connect(self.do_ask_question,
                                  type=Qt.QueuedConnection)

    def copy_to_clipboard(self, *args):
        QApplication.clipboard().setText(
            'calibre, version %s\n%s: %s\n\n%s' %
            (__version__, unicode(self.windowTitle()),
             unicode(self.msg_label.text()), unicode(
                 self.det_msg.toPlainText())))
        self.copy_button.setText(_('Copied'))

    def action_clicked(self):
        if self.questions:
            q = self.questions[0]
            self.questions[0] = q._replace(callback=q.action_callback)
        self.accept()

    def accept(self):
        if self.geom_pref:
            gprefs[self.geom_pref] = bytearray(self.saveGeometry())
        if self.questions:
            payload, callback, cancel_callback = self.questions[0][:3]
            self.questions = self.questions[1:]
            cb = None
            if self.checkbox.isVisible():
                cb = bool(self.checkbox.isChecked())
            self.ask_question.emit(callback, payload, cb)
        self.hide()

    def reject(self):
        if self.geom_pref:
            gprefs[self.geom_pref] = bytearray(self.saveGeometry())
        if self.questions:
            payload, callback, cancel_callback = self.questions[0][:3]
            self.questions = self.questions[1:]
            cb = None
            if self.checkbox.isVisible():
                cb = bool(self.checkbox.isChecked())
            self.ask_question.emit(cancel_callback, payload, cb)
        self.hide()

    def do_ask_question(self, callback, payload, checkbox_checked):
        if callable(callback):
            args = [payload]
            if checkbox_checked is not None:
                args.append(checkbox_checked)
            callback(*args)
        self.show_question()

    def toggle_det_msg(self, *args):
        vis = unicode(self.det_msg_toggle.text()) == self.hide_det_msg
        self.det_msg_toggle.setText(
            self.show_det_msg if vis else self.hide_det_msg)
        self.det_msg.setVisible(not vis)
        self.do_resize()

    def do_resize(self):
        if self.geom_pref:
            geom = gprefs.get(self.geom_pref, None)
            if geom:
                self.restoreGeometry(geom)
                return
        sz = self.sizeHint() + QSize(100, 0)
        sz.setWidth(min(500, sz.width()))
        sz.setHeight(min(500, sz.height()))
        self.resize(sz)

    def show_question(self):
        if self.isVisible():
            return
        if self.questions:
            question = self.questions[0]
            self.msg_label.setText(question.msg)
            self.setWindowTitle(question.title)
            self.log_button.setVisible(bool(question.html_log))
            self.copy_button.setText(_('&Copy to clipboard'))
            self.copy_button.setVisible(bool(question.show_copy_button))
            self.action_button.setVisible(question.action_callback is not None)
            if question.action_callback is not None:
                self.action_button.setText(question.action_label or '')
                self.action_button.setIcon(QIcon(
                ) if question.action_icon is None else question.action_icon)
            self.det_msg.setPlainText(question.det_msg or '')
            self.det_msg.setVisible(False)
            self.det_msg_toggle.setVisible(bool(question.det_msg))
            self.det_msg_toggle.setText(self.show_det_msg)
            self.checkbox.setVisible(question.checkbox_msg is not None)
            if question.checkbox_msg is not None:
                self.checkbox.setText(question.checkbox_msg)
                self.checkbox.setChecked(question.checkbox_checked)
            self.bb.button(self.bb.Ok).setVisible(question.show_ok)
            self.bb.button(self.bb.Yes).setVisible(not question.show_ok)
            self.bb.button(self.bb.No).setVisible(not question.show_ok)
            self.geom_pref = (
                'proceed question dialog:' +
                question.geom_pref) if question.geom_pref else None
            if question.show_det:
                self.toggle_det_msg()
            else:
                self.do_resize()
            self.show()
            button = self.action_button if question.focus_action and question.action_callback is not None else \
                (self.bb.button(self.bb.Ok) if question.show_ok else self.bb.button(self.bb.Yes))
            button.setDefault(True)
            button.setFocus(Qt.OtherFocusReason)

    def __call__(self,
                 callback,
                 payload,
                 html_log,
                 log_viewer_title,
                 title,
                 msg,
                 det_msg='',
                 show_copy_button=False,
                 cancel_callback=None,
                 log_is_file=False,
                 checkbox_msg=None,
                 checkbox_checked=False,
                 action_callback=None,
                 action_label=None,
                 action_icon=None,
                 focus_action=False,
                 show_det=False,
                 show_ok=False,
                 geom_pref=None):
        '''
        A non modal popup that notifies the user that a background task has
        been completed. This class guarantees that only a single popup is
        visible at any one time. Other requests are queued and displayed after
        the user dismisses the current popup.

        :param callback: A callable that is called with payload if the user
        asks to proceed. Note that this is always called in the GUI thread.
        :param cancel_callback: A callable that is called with the payload if
        the users asks not to proceed.
        :param payload: Arbitrary object, passed to callback
        :param html_log: An HTML or plain text log
        :param log_viewer_title: The title for the log viewer window
        :param title: The title for this popup
        :param msg: The msg to display
        :param det_msg: Detailed message
        :param log_is_file: If True the html_log parameter is interpreted as
                            the path to a file on disk containing the log
                            encoded with utf-8
        :param checkbox_msg: If not None, a checkbox is displayed in the
                             dialog, showing this message. The callback is
                             called with both the payload and the state of the
                             checkbox as arguments.
        :param checkbox_checked: If True the checkbox is checked by default.
        :param action_callback: If not None, an extra button is added, which
                                when clicked will cause action_callback to be called
                                instead of callback. action_callback is called in
                                exactly the same way as callback.
        :param action_label: The text on the action button
        :param action_icon: The icon for the action button, must be a QIcon object or None
        :param focus_action: If True, the action button will be focused instead of the Yes button
        :param show_det: If True, the Detailed message will be shown initially
        :param show_ok: If True, OK will be shown instead of YES/NO
        :param geom_pref: String for preference name to preserve dialog box geometry

        '''
        question = Question(payload, callback, cancel_callback, title, msg,
                            html_log, log_viewer_title, log_is_file, det_msg,
                            show_copy_button, checkbox_msg, checkbox_checked,
                            action_callback, action_label, action_icon,
                            focus_action, show_det, show_ok, geom_pref)
        self.questions.append(question)
        self.show_question()

    def show_log(self):
        if self.questions:
            q = self.questions[0]
            log = q.html_log
            if q.log_is_file:
                with open(log, 'rb') as f:
                    log = f.read().decode('utf-8')
            self.log_viewer = ViewLog(q.log_viewer_title, log, parent=self)
Beispiel #3
0
class ProceedQuestion(QWidget):

    ask_question = pyqtSignal(object, object, object)

    @pyqtProperty(float)
    def show_fraction(self):
        return self._show_fraction

    @show_fraction.setter
    def show_fraction(self, val):
        self._show_fraction = max(0, min(1, float(val)))
        self.update()

    def __init__(self, parent):
        QWidget.__init__(self, parent)
        self.setVisible(False)
        parent.installEventFilter(self)

        self._show_fraction = 0.0
        self.show_animation = a = QPropertyAnimation(self, b"show_fraction", self)
        a.setDuration(1000), a.setEasingCurve(QEasingCurve.OutQuad)
        a.setStartValue(0.0), a.setEndValue(1.0)
        a.finished.connect(self.stop_show_animation)
        self.rendered_pixmap = None

        self.questions = []

        self.icon = ic = Icon(self)
        self.msg_label = msg = QLabel('some random filler text')
        msg.setWordWrap(True)
        self.bb = QDialogButtonBox()
        self.bb.accepted.connect(self.accept)
        self.bb.rejected.connect(self.reject)
        self.log_button = self.bb.addButton(_('View log'), self.bb.ActionRole)
        self.log_button.setIcon(QIcon(I('debug.png')))
        self.log_button.clicked.connect(self.show_log)
        self.copy_button = self.bb.addButton(_('&Copy to clipboard'),
                self.bb.ActionRole)
        self.copy_button.clicked.connect(self.copy_to_clipboard)
        self.action_button = self.bb.addButton('', self.bb.ActionRole)
        self.action_button.clicked.connect(self.action_clicked)
        self.show_det_msg = _('Show &details')
        self.hide_det_msg = _('Hide &details')
        self.det_msg_toggle = self.bb.addButton(self.show_det_msg, self.bb.ActionRole)
        self.det_msg_toggle.clicked.connect(self.toggle_det_msg)
        self.det_msg_toggle.setToolTip(
                _('Show detailed information about this error'))
        self.det_msg = PlainTextEdit(self)
        self.det_msg.setReadOnly(True)
        self.bb.setStandardButtons(self.bb.Yes|self.bb.No|self.bb.Ok)
        self.bb.button(self.bb.Yes).setDefault(True)
        self.title_label = title = QLabel('A dummy title')
        f = title.font()
        f.setBold(True)
        title.setFont(f)

        self.checkbox = QCheckBox('', self)

        self._l = l = QVBoxLayout(self)
        self._h = h = QHBoxLayout()
        self._v = v = QVBoxLayout()
        v.addWidget(title), v.addWidget(msg)
        h.addWidget(ic), h.addSpacing(10), h.addLayout(v), l.addLayout(h)
        l.addSpacing(5)
        l.addWidget(self.checkbox)
        l.addWidget(self.det_msg)
        l.addWidget(self.bb)

        self.ask_question.connect(self.do_ask_question,
                type=Qt.QueuedConnection)
        self.setFocusPolicy(Qt.NoFocus)
        for child in self.findChildren(QWidget):
            child.setFocusPolicy(Qt.NoFocus)
        self.setFocusProxy(self.parent())
        self.resize_timer = t = QTimer(self)
        t.setSingleShot(True), t.setInterval(100), t.timeout.connect(self.parent_resized)

    def eventFilter(self, obj, ev):
        if ev.type() == ev.Resize and self.isVisible():
            self.resize_timer.start()
        return False

    def parent_resized(self):
        if self.isVisible():
            self.do_resize()

    def copy_to_clipboard(self, *args):
        QApplication.clipboard().setText(
                'calibre, version %s\n%s: %s\n\n%s' %
                (__version__, unicode_type(self.windowTitle()),
                    unicode_type(self.msg_label.text()),
                    unicode_type(self.det_msg.toPlainText())))
        self.copy_button.setText(_('Copied'))

    def action_clicked(self):
        if self.questions:
            q = self.questions[0]
            self.questions[0] = q._replace(callback=q.action_callback)
        self.accept()

    def accept(self):
        if self.questions:
            payload, callback, cancel_callback = self.questions[0][:3]
            self.questions = self.questions[1:]
            cb = None
            if self.checkbox.isVisible():
                cb = bool(self.checkbox.isChecked())
            self.ask_question.emit(callback, payload, cb)
        self.hide()

    def reject(self):
        if self.questions:
            payload, callback, cancel_callback = self.questions[0][:3]
            self.questions = self.questions[1:]
            cb = None
            if self.checkbox.isVisible():
                cb = bool(self.checkbox.isChecked())
            self.ask_question.emit(cancel_callback, payload, cb)
        self.hide()

    def do_ask_question(self, callback, payload, checkbox_checked):
        if callable(callback):
            args = [payload]
            if checkbox_checked is not None:
                args.append(checkbox_checked)
            callback(*args)
        self.show_question()

    def toggle_det_msg(self, *args):
        vis = unicode_type(self.det_msg_toggle.text()) == self.hide_det_msg
        self.det_msg_toggle.setText(self.show_det_msg if vis else
                self.hide_det_msg)
        self.det_msg.setVisible(not vis)
        self.do_resize()

    def do_resize(self):
        sz = self.sizeHint()
        sz.setWidth(min(self.parent().width(), sz.width()))
        sb = self.parent().statusBar().height() + 10
        sz.setHeight(min(self.parent().height() - sb, sz.height()))
        self.resize(sz)
        self.position_widget()

    def show_question(self):
        if not self.questions:
            return
        if not self.isVisible():
            question = self.questions[0]
            self.msg_label.setText(question.msg)
            self.icon.set_icon(question.icon)
            self.title_label.setText(question.title)
            self.log_button.setVisible(bool(question.html_log))
            self.copy_button.setText(_('&Copy to clipboard'))
            if question.action_callback is not None:
                self.action_button.setText(question.action_label or '')
                self.action_button.setIcon(
                    QIcon() if question.action_icon is None else question.action_icon)
            # Force the button box to relayout its buttons, as button text
            # might have changed
            self.bb.setOrientation(Qt.Vertical), self.bb.setOrientation(Qt.Horizontal)
            self.det_msg.setPlainText(question.det_msg or '')
            self.det_msg.setVisible(False)
            self.det_msg_toggle.setVisible(bool(question.det_msg))
            self.det_msg_toggle.setText(self.show_det_msg)
            self.checkbox.setVisible(question.checkbox_msg is not None)
            if question.checkbox_msg is not None:
                self.checkbox.setText(question.checkbox_msg)
                self.checkbox.setChecked(question.checkbox_checked)
            self.bb.button(self.bb.Ok).setVisible(question.show_ok)
            self.bb.button(self.bb.Yes).setVisible(not question.show_ok)
            self.bb.button(self.bb.No).setVisible(not question.show_ok)
            self.copy_button.setVisible(bool(question.show_copy_button))
            self.action_button.setVisible(question.action_callback is not None)
            self.toggle_det_msg() if question.show_det else self.do_resize()
            self.show_widget()
            button = self.action_button if question.focus_action and question.action_callback is not None else \
                (self.bb.button(self.bb.Ok) if question.show_ok else self.bb.button(self.bb.Yes))
            button.setDefault(True)
            self.raise_()
            self.start_show_animation()

    def start_show_animation(self):
        if self.rendered_pixmap is not None:
            return

        dpr = getattr(self, 'devicePixelRatioF', self.devicePixelRatio)()
        p = QImage(dpr * self.size(), QImage.Format_ARGB32_Premultiplied)
        p.setDevicePixelRatio(dpr)
        self.render(p)
        self.rendered_pixmap = QPixmap.fromImage(p)
        self.original_visibility = v = []
        for child in self.findChildren(QWidget):
            if child.isVisible():
                child.setVisible(False)
                v.append(child)
        self.show_animation.start()

    def stop_show_animation(self):
        self.rendered_pixmap = None
        [c.setVisible(True) for c in getattr(self, 'original_visibility', ())]
        self.update()
        for child in self.findChildren(QWidget):
            child.update()
            if hasattr(child, 'viewport'):
                child.viewport().update()

    def position_widget(self):
        geom = self.parent().geometry()
        x = geom.width() - self.width() - 5
        sb = self.parent().statusBar()
        if sb is None:
            y = geom.height() - self.height()
        else:
            y = sb.geometry().top() - self.height()
        self.move(x, y)

    def show_widget(self):
        self.show()
        self.position_widget()

    def dummy_question(self, action_label=None):
        self(lambda *args:args, (), 'dummy log', 'Log Viewer', 'A Dummy Popup',
             'This is a dummy popup to easily test things, with a long line of text that should wrap. '
             'This is a dummy popup to easily test things, with a long line of text that should wrap',
             checkbox_msg='A dummy checkbox',
             action_callback=lambda *args: args, action_label=action_label or 'An action')

    def __call__(self, callback, payload, html_log, log_viewer_title, title,
            msg, det_msg='', show_copy_button=False, cancel_callback=None,
            log_is_file=False, checkbox_msg=None, checkbox_checked=False,
            action_callback=None, action_label=None, action_icon=None, focus_action=False,
            show_det=False, show_ok=False, icon=None, log_viewer_unique_name=None, **kw):
        '''
        A non modal popup that notifies the user that a background task has
        been completed. This class guarantees that only a single popup is
        visible at any one time. Other requests are queued and displayed after
        the user dismisses the current popup.

        :param callback: A callable that is called with payload if the user
        asks to proceed. Note that this is always called in the GUI thread.
        :param cancel_callback: A callable that is called with the payload if
        the users asks not to proceed.
        :param payload: Arbitrary object, passed to callback
        :param html_log: An HTML or plain text log
        :param log_viewer_title: The title for the log viewer window
        :param title: The title for this popup
        :param msg: The msg to display
        :param det_msg: Detailed message
        :param log_is_file: If True the html_log parameter is interpreted as
                            the path to a file on disk containing the log
                            encoded with utf-8
        :param checkbox_msg: If not None, a checkbox is displayed in the
                             dialog, showing this message. The callback is
                             called with both the payload and the state of the
                             checkbox as arguments.
        :param checkbox_checked: If True the checkbox is checked by default.
        :param action_callback: If not None, an extra button is added, which
                                when clicked will cause action_callback to be called
                                instead of callback. action_callback is called in
                                exactly the same way as callback.
        :param action_label: The text on the action button
        :param action_icon: The icon for the action button, must be a QIcon object or None
        :param focus_action: If True, the action button will be focused instead of the Yes button
        :param show_det: If True, the Detailed message will be shown initially
        :param show_ok: If True, OK will be shown instead of YES/NO
        :param icon: The icon to be used for this popop (defaults to question mark). Can be either a QIcon or a string to be used with I()
        :log_viewer_unique_name: If set, ViewLog will remember/reuse its size for this name in calibre.gui2.gprefs
        '''
        question = Question(
            payload, callback, cancel_callback, title, msg, html_log,
            log_viewer_title, log_is_file, det_msg, show_copy_button,
            checkbox_msg, checkbox_checked, action_callback, action_label,
            action_icon, focus_action, show_det, show_ok, icon, log_viewer_unique_name)
        self.questions.append(question)
        self.show_question()

    def show_log(self):
        if self.questions:
            q = self.questions[0]
            log = q.html_log
            if q.log_is_file:
                with open(log, 'rb') as f:
                    log = f.read().decode('utf-8')
            self.log_viewer = ViewLog(q.log_viewer_title, log,
                        parent=self, unique_name=q.log_viewer_unique_name)

    def paintEvent(self, ev):
        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing, True)
        painter.setRenderHint(QPainter.SmoothPixmapTransform, True)
        try:
            if self.rendered_pixmap is None:
                self.paint_background(painter)
            else:
                self.animated_paint(painter)
        finally:
            painter.end()

    def animated_paint(self, painter):
        top = (1 - self._show_fraction) * self.height()
        painter.drawPixmap(0, top, self.rendered_pixmap)

    def paint_background(self, painter):
        br = 12  # border_radius
        bw = 1  # border_width
        pal = self.palette()
        c = pal.color(pal.Window)
        c.setAlphaF(0.9)
        p = QPainterPath()
        p.addRoundedRect(QRectF(self.rect()), br, br)
        painter.fillPath(p, c)
        p.addRoundedRect(QRectF(self.rect()).adjusted(bw, bw, -bw, -bw), br, br)
        painter.fillPath(p, pal.color(pal.WindowText))
Beispiel #4
0
class JobError(QDialog):  # {{{

    WIDTH = 600
    do_pop = pyqtSignal()

    def __init__(self, parent):
        QDialog.__init__(self, parent)
        self.setAttribute(Qt.WA_DeleteOnClose, False)
        self.queue = []
        self.do_pop.connect(self.pop, type=Qt.QueuedConnection)

        self._layout = l = QGridLayout()
        self.setLayout(l)
        self.icon = QIcon(I('dialog_error.png'))
        self.setWindowIcon(self.icon)
        self.icon_label = QLabel()
        self.icon_label.setPixmap(self.icon.pixmap(68, 68))
        self.icon_label.setMaximumSize(QSize(68, 68))
        self.msg_label = QLabel('<p>&nbsp;')
        self.msg_label.setStyleSheet('QLabel { margin-top: 1ex; }')
        self.msg_label.setWordWrap(True)
        self.msg_label.setTextFormat(Qt.RichText)
        self.det_msg = QPlainTextEdit(self)
        self.det_msg.setVisible(False)

        self.bb = QDialogButtonBox(QDialogButtonBox.Close, parent=self)
        self.bb.accepted.connect(self.accept)
        self.bb.rejected.connect(self.reject)
        self.ctc_button = self.bb.addButton(_('&Copy to clipboard'),
                self.bb.ActionRole)
        self.ctc_button.clicked.connect(self.copy_to_clipboard)
        self.retry_button = self.bb.addButton(_('&Retry'), self.bb.ActionRole)
        self.retry_button.clicked.connect(self.retry)
        self.retry_func = None
        self.show_det_msg = _('Show &details')
        self.hide_det_msg = _('Hide &details')
        self.det_msg_toggle = self.bb.addButton(self.show_det_msg, self.bb.ActionRole)
        self.det_msg_toggle.clicked.connect(self.toggle_det_msg)
        self.det_msg_toggle.setToolTip(
                _('Show detailed information about this error'))
        self.suppress = QCheckBox(self)

        l.addWidget(self.icon_label, 0, 0, 1, 1)
        l.addWidget(self.msg_label,  0, 1, 1, 1)
        l.addWidget(self.det_msg,    1, 0, 1, 2)
        l.addWidget(self.suppress,   2, 0, 1, 2, Qt.AlignLeft|Qt.AlignBottom)
        l.addWidget(self.bb,         3, 0, 1, 2, Qt.AlignRight|Qt.AlignBottom)
        l.setColumnStretch(1, 100)

        self.setModal(False)
        self.suppress.setVisible(False)
        self.do_resize()

    def retry(self):
        if self.retry_func is not None:
            self.accept()
            self.retry_func()

    def update_suppress_state(self):
        self.suppress.setText(_(
            'Hide the remaining %d error messages'%len(self.queue)))
        self.suppress.setVisible(len(self.queue) > 3)
        self.do_resize()

    def copy_to_clipboard(self, *args):
        d = QTextDocument()
        d.setHtml(self.msg_label.text())
        QApplication.clipboard().setText(
                u'calibre, version %s (%s, embedded-python: %s)\n%s: %s\n\n%s' %
                (__version__, sys.platform, isfrozen,
                    unicode(self.windowTitle()), unicode(d.toPlainText()),
                    unicode(self.det_msg.toPlainText())))
        if hasattr(self, 'ctc_button'):
            self.ctc_button.setText(_('Copied'))

    def toggle_det_msg(self, *args):
        vis = unicode(self.det_msg_toggle.text()) == self.hide_det_msg
        self.det_msg_toggle.setText(self.show_det_msg if vis else
                self.hide_det_msg)
        self.det_msg.setVisible(not vis)
        self.do_resize()

    def do_resize(self):
        h = self.sizeHint().height()
        self.setMinimumHeight(0)  # Needed as this gets set if det_msg is shown
        # Needed otherwise re-showing the box after showing det_msg causes the box
        # to not reduce in height
        self.setMaximumHeight(h)
        self.resize(QSize(self.WIDTH, h))

    def showEvent(self, ev):
        ret = QDialog.showEvent(self, ev)
        self.bb.button(self.bb.Close).setFocus(Qt.OtherFocusReason)
        return ret

    def show_error(self, title, msg, det_msg=u'', retry_func=None):
        self.queue.append((title, msg, det_msg, retry_func))
        self.update_suppress_state()
        self.pop()

    def pop(self):
        if not self.queue or self.isVisible():
            return
        title, msg, det_msg, retry_func = self.queue.pop(0)
        self.setWindowTitle(title)
        self.msg_label.setText(msg)
        self.det_msg.setPlainText(det_msg)
        self.det_msg.setVisible(False)
        self.det_msg_toggle.setText(self.show_det_msg)
        self.det_msg_toggle.setVisible(True)
        self.suppress.setChecked(False)
        self.update_suppress_state()
        if not det_msg:
            self.det_msg_toggle.setVisible(False)
        self.retry_button.setVisible(retry_func is not None)
        self.retry_func = retry_func
        self.do_resize()
        self.show()

    def done(self, r):
        if self.suppress.isChecked():
            self.queue = []
        QDialog.done(self, r)
        self.do_pop.emit()
Beispiel #5
0
class JobError(QDialog):  # {{{

    WIDTH = 600
    do_pop = pyqtSignal()

    def __init__(self, parent):
        QDialog.__init__(self, parent)
        self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, False)
        self.queue = []
        self.do_pop.connect(self.pop, type=Qt.ConnectionType.QueuedConnection)

        self._layout = l = QGridLayout()
        self.setLayout(l)
        self.icon = QIcon(I('dialog_error.png'))
        self.setWindowIcon(self.icon)
        self.icon_widget = Icon(self)
        self.icon_widget.set_icon(self.icon)
        self.msg_label = QLabel('<p>&nbsp;')
        self.msg_label.setStyleSheet('QLabel { margin-top: 1ex; }')
        self.msg_label.setWordWrap(True)
        self.msg_label.setTextFormat(Qt.TextFormat.RichText)
        self.det_msg = QPlainTextEdit(self)
        self.det_msg.setVisible(False)

        self.bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Close,
                                   parent=self)
        self.bb.accepted.connect(self.accept)
        self.bb.rejected.connect(self.reject)
        self.ctc_button = self.bb.addButton(_('&Copy to clipboard'),
                                            self.bb.ActionRole)
        self.ctc_button.clicked.connect(self.copy_to_clipboard)
        self.retry_button = self.bb.addButton(_('&Retry'), self.bb.ActionRole)
        self.retry_button.clicked.connect(self.retry)
        self.retry_func = None
        self.show_det_msg = _('Show &details')
        self.hide_det_msg = _('Hide &details')
        self.det_msg_toggle = self.bb.addButton(self.show_det_msg,
                                                self.bb.ActionRole)
        self.det_msg_toggle.clicked.connect(self.toggle_det_msg)
        self.det_msg_toggle.setToolTip(
            _('Show detailed information about this error'))
        self.suppress = QCheckBox(self)

        l.addWidget(self.icon_widget, 0, 0, 1, 1)
        l.addWidget(self.msg_label, 0, 1, 1, 1)
        l.addWidget(self.det_msg, 1, 0, 1, 2)
        l.addWidget(self.suppress, 2, 0, 1, 2,
                    Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignBottom)
        l.addWidget(self.bb, 3, 0, 1, 2,
                    Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignBottom)
        l.setColumnStretch(1, 100)

        self.setModal(False)
        self.suppress.setVisible(False)
        self.do_resize()

    def retry(self):
        if self.retry_func is not None:
            self.accept()
            self.retry_func()

    def update_suppress_state(self):
        self.suppress.setText(
            ngettext('Hide the remaining error message',
                     'Hide the {} remaining error messages',
                     len(self.queue)).format(len(self.queue)))
        self.suppress.setVisible(len(self.queue) > 3)
        self.do_resize()

    def copy_to_clipboard(self, *args):
        d = QTextDocument()
        d.setHtml(self.msg_label.text())
        QApplication.clipboard().setText(
            'calibre, version %s (%s, embedded-python: %s)\n%s: %s\n\n%s' %
            (__version__, sys.platform, isfrozen,
             unicode_type(self.windowTitle()), unicode_type(
                 d.toPlainText()), unicode_type(self.det_msg.toPlainText())))
        if hasattr(self, 'ctc_button'):
            self.ctc_button.setText(_('Copied'))

    def toggle_det_msg(self, *args):
        vis = unicode_type(self.det_msg_toggle.text()) == self.hide_det_msg
        self.det_msg_toggle.setText(
            self.show_det_msg if vis else self.hide_det_msg)
        self.det_msg.setVisible(not vis)
        self.do_resize()

    def do_resize(self):
        h = self.sizeHint().height()
        self.setMinimumHeight(0)  # Needed as this gets set if det_msg is shown
        # Needed otherwise re-showing the box after showing det_msg causes the box
        # to not reduce in height
        self.setMaximumHeight(h)
        self.resize(QSize(self.WIDTH, h))

    def showEvent(self, ev):
        ret = QDialog.showEvent(self, ev)
        self.bb.button(self.bb.Close).setFocus(Qt.FocusReason.OtherFocusReason)
        return ret

    def show_error(self, title, msg, det_msg='', retry_func=None):
        self.queue.append((title, msg, det_msg, retry_func))
        self.update_suppress_state()
        self.pop()

    def pop(self):
        if not self.queue or self.isVisible():
            return
        title, msg, det_msg, retry_func = self.queue.pop(0)
        self.setWindowTitle(title)
        self.msg_label.setText(msg)
        self.det_msg.setPlainText(det_msg)
        self.det_msg.setVisible(False)
        self.det_msg_toggle.setText(self.show_det_msg)
        self.det_msg_toggle.setVisible(True)
        self.suppress.setChecked(False)
        self.update_suppress_state()
        if not det_msg:
            self.det_msg_toggle.setVisible(False)
        self.retry_button.setVisible(retry_func is not None)
        self.retry_func = retry_func
        self.do_resize()
        self.show()

    def done(self, r):
        if self.suppress.isChecked():
            self.queue = []
        QDialog.done(self, r)
        self.do_pop.emit()
Beispiel #6
0
class ProceedQuestion(QWidget):

    ask_question = pyqtSignal(object, object, object)

    @pyqtProperty(float)
    def show_fraction(self):
        return self._show_fraction

    @show_fraction.setter
    def show_fraction(self, val):
        self._show_fraction = max(0, min(1, float(val)))
        self.update()

    def __init__(self, parent):
        QWidget.__init__(self, parent)
        self.setVisible(False)
        parent.installEventFilter(self)

        self._show_fraction = 0.0
        self.show_animation = a = QPropertyAnimation(self, b"show_fraction",
                                                     self)
        a.setDuration(1000), a.setEasingCurve(QEasingCurve.OutQuad)
        a.setStartValue(0.0), a.setEndValue(1.0)
        a.finished.connect(self.stop_show_animation)
        self.rendered_pixmap = None

        self.questions = []

        self.icon = ic = Icon(self)
        self.msg_label = msg = QLabel('some random filler text')
        msg.setWordWrap(True)
        self.bb = QDialogButtonBox()
        self.bb.accepted.connect(self.accept)
        self.bb.rejected.connect(self.reject)
        self.log_button = self.bb.addButton(_('View log'), self.bb.ActionRole)
        self.log_button.setIcon(QIcon(I('debug.png')))
        self.log_button.clicked.connect(self.show_log)
        self.copy_button = self.bb.addButton(_('&Copy to clipboard'),
                                             self.bb.ActionRole)
        self.copy_button.clicked.connect(self.copy_to_clipboard)
        self.action_button = self.bb.addButton('', self.bb.ActionRole)
        self.action_button.clicked.connect(self.action_clicked)
        self.show_det_msg = _('Show &details')
        self.hide_det_msg = _('Hide &details')
        self.det_msg_toggle = self.bb.addButton(self.show_det_msg,
                                                self.bb.ActionRole)
        self.det_msg_toggle.clicked.connect(self.toggle_det_msg)
        self.det_msg_toggle.setToolTip(
            _('Show detailed information about this error'))
        self.det_msg = PlainTextEdit(self)
        self.det_msg.setReadOnly(True)
        self.bb.setStandardButtons(self.bb.Yes | self.bb.No | self.bb.Ok)
        self.bb.button(self.bb.Yes).setDefault(True)
        self.title_label = title = QLabel('A dummy title')
        f = title.font()
        f.setBold(True)
        title.setFont(f)

        self.checkbox = QCheckBox('', self)

        self._l = l = QVBoxLayout(self)
        self._h = h = QHBoxLayout()
        self._v = v = QVBoxLayout()
        v.addWidget(title), v.addWidget(msg)
        h.addWidget(ic), h.addSpacing(10), h.addLayout(v), l.addLayout(h)
        l.addSpacing(5)
        l.addWidget(self.checkbox)
        l.addWidget(self.det_msg)
        l.addWidget(self.bb)

        self.ask_question.connect(self.do_ask_question,
                                  type=Qt.QueuedConnection)
        self.setFocusPolicy(Qt.NoFocus)
        for child in self.findChildren(QWidget):
            child.setFocusPolicy(Qt.NoFocus)
        self.setFocusProxy(self.parent())
        self.resize_timer = t = QTimer(self)
        t.setSingleShot(True), t.setInterval(100), t.timeout.connect(
            self.parent_resized)

    def eventFilter(self, obj, ev):
        if ev.type() == ev.Resize and self.isVisible():
            self.resize_timer.start()
        return False

    def parent_resized(self):
        if self.isVisible():
            self.do_resize()

    def copy_to_clipboard(self, *args):
        QApplication.clipboard().setText(
            'calibre, version %s\n%s: %s\n\n%s' %
            (__version__, unicode_type(
                self.windowTitle()), unicode_type(self.msg_label.text()),
             unicode_type(self.det_msg.toPlainText())))
        self.copy_button.setText(_('Copied'))

    def action_clicked(self):
        if self.questions:
            q = self.questions[0]
            self.questions[0] = q._replace(callback=q.action_callback)
        self.accept()

    def accept(self):
        if self.questions:
            payload, callback, cancel_callback = self.questions[0][:3]
            self.questions = self.questions[1:]
            cb = None
            if self.checkbox.isVisible():
                cb = bool(self.checkbox.isChecked())
            self.ask_question.emit(callback, payload, cb)
        self.hide()

    def reject(self):
        if self.questions:
            payload, callback, cancel_callback = self.questions[0][:3]
            self.questions = self.questions[1:]
            cb = None
            if self.checkbox.isVisible():
                cb = bool(self.checkbox.isChecked())
            self.ask_question.emit(cancel_callback, payload, cb)
        self.hide()

    def do_ask_question(self, callback, payload, checkbox_checked):
        if callable(callback):
            args = [payload]
            if checkbox_checked is not None:
                args.append(checkbox_checked)
            callback(*args)
        self.show_question()

    def toggle_det_msg(self, *args):
        vis = unicode_type(self.det_msg_toggle.text()) == self.hide_det_msg
        self.det_msg_toggle.setText(
            self.show_det_msg if vis else self.hide_det_msg)
        self.det_msg.setVisible(not vis)
        self.do_resize()

    def do_resize(self):
        sz = self.sizeHint()
        sz.setWidth(min(self.parent().width(), sz.width()))
        sb = self.parent().statusBar().height() + 10
        sz.setHeight(min(self.parent().height() - sb, sz.height()))
        self.resize(sz)
        self.position_widget()

    def show_question(self):
        if not self.questions:
            return
        if not self.isVisible():
            question = self.questions[0]
            self.msg_label.setText(question.msg)
            self.icon.set_icon(question.icon)
            self.title_label.setText(question.title)
            self.log_button.setVisible(bool(question.html_log))
            self.copy_button.setText(_('&Copy to clipboard'))
            if question.action_callback is not None:
                self.action_button.setText(question.action_label or '')
                self.action_button.setIcon(QIcon(
                ) if question.action_icon is None else question.action_icon)
            # Force the button box to relayout its buttons, as button text
            # might have changed
            self.bb.setOrientation(Qt.Vertical), self.bb.setOrientation(
                Qt.Horizontal)
            self.det_msg.setPlainText(question.det_msg or '')
            self.det_msg.setVisible(False)
            self.det_msg_toggle.setVisible(bool(question.det_msg))
            self.det_msg_toggle.setText(self.show_det_msg)
            self.checkbox.setVisible(question.checkbox_msg is not None)
            if question.checkbox_msg is not None:
                self.checkbox.setText(question.checkbox_msg)
                self.checkbox.setChecked(question.checkbox_checked)
            self.bb.button(self.bb.Ok).setVisible(question.show_ok)
            self.bb.button(self.bb.Yes).setVisible(not question.show_ok)
            self.bb.button(self.bb.No).setVisible(not question.show_ok)
            self.copy_button.setVisible(bool(question.show_copy_button))
            self.action_button.setVisible(question.action_callback is not None)
            self.toggle_det_msg() if question.show_det else self.do_resize()
            self.show_widget()
            button = self.action_button if question.focus_action and question.action_callback is not None else \
                (self.bb.button(self.bb.Ok) if question.show_ok else self.bb.button(self.bb.Yes))
            button.setDefault(True)
            self.raise_()
            self.start_show_animation()

    def start_show_animation(self):
        if self.rendered_pixmap is not None:
            return

        dpr = getattr(self, 'devicePixelRatioF', self.devicePixelRatio)()
        p = QImage(dpr * self.size(), QImage.Format_ARGB32_Premultiplied)
        p.setDevicePixelRatio(dpr)
        # For some reason, Qt scrolls the book view when rendering this widget,
        # for the very first time, so manually preserve its position
        pr = getattr(self.parent(), 'library_view', None)
        if not hasattr(pr, 'preserve_state'):
            self.render(p)
        else:
            with pr.preserve_state():
                self.render(p)
        self.rendered_pixmap = QPixmap.fromImage(p)
        self.original_visibility = v = []
        for child in self.findChildren(QWidget):
            if child.isVisible():
                child.setVisible(False)
                v.append(child)
        self.show_animation.start()

    def stop_show_animation(self):
        self.rendered_pixmap = None
        [c.setVisible(True) for c in getattr(self, 'original_visibility', ())]
        self.update()
        for child in self.findChildren(QWidget):
            child.update()
            if hasattr(child, 'viewport'):
                child.viewport().update()

    def position_widget(self):
        geom = self.parent().geometry()
        x = geom.width() - self.width() - 5
        sb = self.parent().statusBar()
        if sb is None:
            y = geom.height() - self.height()
        else:
            y = sb.geometry().top() - self.height()
        self.move(x, y)

    def show_widget(self):
        self.show()
        self.position_widget()

    def dummy_question(self, action_label=None):
        self(
            lambda *args: args, (),
            'dummy log',
            'Log Viewer',
            'A Dummy Popup',
            'This is a dummy popup to easily test things, with a long line of text that should wrap. '
            'This is a dummy popup to easily test things, with a long line of text that should wrap',
            checkbox_msg='A dummy checkbox',
            action_callback=lambda *args: args,
            action_label=action_label or 'An action')

    def __call__(self,
                 callback,
                 payload,
                 html_log,
                 log_viewer_title,
                 title,
                 msg,
                 det_msg='',
                 show_copy_button=False,
                 cancel_callback=None,
                 log_is_file=False,
                 checkbox_msg=None,
                 checkbox_checked=False,
                 action_callback=None,
                 action_label=None,
                 action_icon=None,
                 focus_action=False,
                 show_det=False,
                 show_ok=False,
                 icon=None,
                 log_viewer_unique_name=None,
                 **kw):
        '''
        A non modal popup that notifies the user that a background task has
        been completed. This class guarantees that only a single popup is
        visible at any one time. Other requests are queued and displayed after
        the user dismisses the current popup.

        :param callback: A callable that is called with payload if the user
        asks to proceed. Note that this is always called in the GUI thread.
        :param cancel_callback: A callable that is called with the payload if
        the users asks not to proceed.
        :param payload: Arbitrary object, passed to callback
        :param html_log: An HTML or plain text log
        :param log_viewer_title: The title for the log viewer window
        :param title: The title for this popup
        :param msg: The msg to display
        :param det_msg: Detailed message
        :param log_is_file: If True the html_log parameter is interpreted as
                            the path to a file on disk containing the log
                            encoded with utf-8
        :param checkbox_msg: If not None, a checkbox is displayed in the
                             dialog, showing this message. The callback is
                             called with both the payload and the state of the
                             checkbox as arguments.
        :param checkbox_checked: If True the checkbox is checked by default.
        :param action_callback: If not None, an extra button is added, which
                                when clicked will cause action_callback to be called
                                instead of callback. action_callback is called in
                                exactly the same way as callback.
        :param action_label: The text on the action button
        :param action_icon: The icon for the action button, must be a QIcon object or None
        :param focus_action: If True, the action button will be focused instead of the Yes button
        :param show_det: If True, the Detailed message will be shown initially
        :param show_ok: If True, OK will be shown instead of YES/NO
        :param icon: The icon to be used for this popop (defaults to question mark). Can be either a QIcon or a string to be used with I()
        :log_viewer_unique_name: If set, ViewLog will remember/reuse its size for this name in calibre.gui2.gprefs
        '''
        question = Question(payload, callback, cancel_callback, title, msg,
                            html_log, log_viewer_title, log_is_file, det_msg,
                            show_copy_button, checkbox_msg, checkbox_checked,
                            action_callback, action_label, action_icon,
                            focus_action, show_det, show_ok, icon,
                            log_viewer_unique_name)
        self.questions.append(question)
        self.show_question()

    def show_log(self):
        if self.questions:
            q = self.questions[0]
            log = q.html_log
            if q.log_is_file:
                with open(log, 'rb') as f:
                    log = f.read().decode('utf-8')
            self.log_viewer = ViewLog(q.log_viewer_title,
                                      log,
                                      parent=self,
                                      unique_name=q.log_viewer_unique_name)

    def paintEvent(self, ev):
        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing, True)
        painter.setRenderHint(QPainter.SmoothPixmapTransform, True)
        try:
            if self.rendered_pixmap is None:
                self.paint_background(painter)
            else:
                self.animated_paint(painter)
        finally:
            painter.end()

    def animated_paint(self, painter):
        top = (1 - self._show_fraction) * self.height()
        painter.drawPixmap(0, top, self.rendered_pixmap)

    def paint_background(self, painter):
        br = 12  # border_radius
        bw = 1  # border_width
        pal = self.palette()
        c = pal.color(pal.Window)
        c.setAlphaF(0.9)
        p = QPainterPath()
        p.addRoundedRect(QRectF(self.rect()), br, br)
        painter.fillPath(p, c)
        p.addRoundedRect(
            QRectF(self.rect()).adjusted(bw, bw, -bw, -bw), br, br)
        painter.fillPath(p, pal.color(pal.WindowText))
Beispiel #7
0
class Report(QDialog):  # {{{

    def __init__(self, parent):
        QDialog.__init__(self, parent)
        self.gui = parent
        self.setAttribute(Qt.WA_DeleteOnClose, False)
        self.setWindowIcon(QIcon(I('polish.png')))
        self.reports = []

        self.l = l = QGridLayout()
        self.setLayout(l)
        self.view = v = QTextEdit(self)
        v.setReadOnly(True)
        l.addWidget(self.view, 0, 0, 1, 2)

        self.backup_msg = la = QLabel('')
        l.addWidget(la, 1, 0, 1, 2)
        la.setVisible(False)
        la.setWordWrap(True)

        self.ign = QCheckBox(_('Ignore remaining report'), self)
        l.addWidget(self.ign, 2, 0)

        bb = self.bb = QDialogButtonBox(QDialogButtonBox.Close)
        bb.accepted.connect(self.accept)
        bb.rejected.connect(self.reject)
        b = self.log_button = bb.addButton(_('View full &log'), bb.ActionRole)
        b.clicked.connect(self.view_log)
        bb.button(bb.Close).setDefault(True)
        l.addWidget(bb, 2, 1)

        self.finished.connect(self.show_next, type=Qt.QueuedConnection)

        self.resize(QSize(800, 600))

    def setup_ign(self):
        self.ign.setText(ngettext(
            'Ignore remaining report', 'Ignore remaining {} reports', len(self.reports)).format(len(self.reports)))
        self.ign.setVisible(bool(self.reports))
        self.ign.setChecked(False)

    def __call__(self, *args):
        self.reports.append(args)
        self.setup_ign()
        if not self.isVisible():
            self.show_next()

    def show_report(self, book_title, book_id, fmts, job, report):
        from calibre.ebooks.markdown import markdown
        self.current_log = job.details
        self.setWindowTitle(_('Polishing of %s')%book_title)
        self.view.setText(markdown('# %s\n\n'%book_title + report,
                                   output_format='html4'))
        self.bb.button(self.bb.Close).setFocus(Qt.OtherFocusReason)
        self.backup_msg.setVisible(bool(fmts))
        if fmts:
            m = ngettext('The original file has been saved as %s.',
                     'The original files have been saved as %s.', len(fmts))%(
                _(' and ').join('ORIGINAL_'+f for f in fmts)
                     )
            self.backup_msg.setText(m + ' ' + _(
                'If you polish again, the polishing will run on the originals.')%(
                ))

    def view_log(self):
        self.view.setPlainText(self.current_log)
        self.view.verticalScrollBar().setValue(0)

    def show_next(self, *args):
        if not self.reports:
            return
        if not self.isVisible():
            self.show()
        self.show_report(*self.reports.pop(0))
        self.setup_ign()

    def accept(self):
        if self.ign.isChecked():
            self.reports = []
        if self.reports:
            self.show_next()
            return
        super(Report, self).accept()

    def reject(self):
        if self.ign.isChecked():
            self.reports = []
        if self.reports:
            self.show_next()
            return
        super(Report, self).reject()
Beispiel #8
0
class Report(QDialog):  # {{{

    def __init__(self, parent):
        QDialog.__init__(self, parent)
        self.gui = parent
        self.setAttribute(Qt.WA_DeleteOnClose, False)
        self.setWindowIcon(QIcon(I('polish.png')))
        self.reports = []

        self.l = l = QGridLayout()
        self.setLayout(l)
        self.view = v = QTextEdit(self)
        v.setReadOnly(True)
        l.addWidget(self.view, 0, 0, 1, 2)

        self.backup_msg = la = QLabel('')
        l.addWidget(la, 1, 0, 1, 2)
        la.setVisible(False)
        la.setWordWrap(True)

        self.ign = QCheckBox(_('Ignore remaining report'), self)
        l.addWidget(self.ign, 2, 0)

        bb = self.bb = QDialogButtonBox(QDialogButtonBox.Close)
        bb.accepted.connect(self.accept)
        bb.rejected.connect(self.reject)
        b = self.log_button = bb.addButton(_('View full &log'), bb.ActionRole)
        b.clicked.connect(self.view_log)
        bb.button(bb.Close).setDefault(True)
        l.addWidget(bb, 2, 1)

        self.finished.connect(self.show_next, type=Qt.QueuedConnection)

        self.resize(QSize(800, 600))

    def setup_ign(self):
        self.ign.setText(ngettext(
            'Ignore remaining report', 'Ignore remaining {} reports', len(self.reports)).format(len(self.reports)))
        self.ign.setVisible(bool(self.reports))
        self.ign.setChecked(False)

    def __call__(self, *args):
        self.reports.append(args)
        self.setup_ign()
        if not self.isVisible():
            self.show_next()

    def show_report(self, book_title, book_id, fmts, job, report):
        from calibre.ebooks.markdown import markdown
        self.current_log = job.details
        self.setWindowTitle(_('Polishing of %s')%book_title)
        self.view.setText(markdown('# %s\n\n'%book_title + report,
                                   output_format='html4'))
        self.bb.button(self.bb.Close).setFocus(Qt.OtherFocusReason)
        self.backup_msg.setVisible(bool(fmts))
        if fmts:
            m = ngettext('The original file has been saved as %s.',
                     'The original files have been saved as %s.', len(fmts))%(
                _(' and ').join('ORIGINAL_'+f for f in fmts)
                     )
            self.backup_msg.setText(m + ' ' + _(
                'If you polish again, the polishing will run on the originals.')%(
                ))

    def view_log(self):
        self.view.setPlainText(self.current_log)
        self.view.verticalScrollBar().setValue(0)

    def show_next(self, *args):
        if not self.reports:
            return
        if not self.isVisible():
            self.show()
        self.show_report(*self.reports.pop(0))
        self.setup_ign()

    def accept(self):
        if self.ign.isChecked():
            self.reports = []
        if self.reports:
            self.show_next()
            return
        super(Report, self).accept()

    def reject(self):
        if self.ign.isChecked():
            self.reports = []
        if self.reports:
            self.show_next()
            return
        super(Report, self).reject()
Beispiel #9
0
class ProceedQuestion(QDialog):

    ask_question = pyqtSignal(object, object, object)

    def __init__(self, parent):
        QDialog.__init__(self, parent)
        self.setAttribute(Qt.WA_DeleteOnClose, False)
        self.setWindowIcon(QIcon(I('dialog_question.png')))

        self.questions = []

        self._l = l = QGridLayout(self)
        self.setLayout(l)

        self.icon_label = ic = QLabel(self)
        ic.setPixmap(QPixmap(I('dialog_question.png')))
        self.msg_label = msg = QLabel('some random filler text')
        msg.setWordWrap(True)
        ic.setMaximumWidth(110)
        ic.setMaximumHeight(100)
        ic.setScaledContents(True)
        ic.setStyleSheet('QLabel { margin-right: 10px }')
        self.bb = QDialogButtonBox()
        self.bb.accepted.connect(self.accept)
        self.bb.rejected.connect(self.reject)
        self.log_button = self.bb.addButton(_('View log'), self.bb.ActionRole)
        self.log_button.setIcon(QIcon(I('debug.png')))
        self.log_button.clicked.connect(self.show_log)
        self.copy_button = self.bb.addButton(_('&Copy to clipboard'),
                self.bb.ActionRole)
        self.copy_button.clicked.connect(self.copy_to_clipboard)
        self.action_button = self.bb.addButton('', self.bb.ActionRole)
        self.action_button.clicked.connect(self.action_clicked)
        self.show_det_msg = _('Show &details')
        self.hide_det_msg = _('Hide &details')
        self.det_msg_toggle = self.bb.addButton(self.show_det_msg, self.bb.ActionRole)
        self.det_msg_toggle.clicked.connect(self.toggle_det_msg)
        self.det_msg_toggle.setToolTip(
                _('Show detailed information about this error'))
        self.det_msg = QPlainTextEdit(self)
        self.det_msg.setReadOnly(True)
        self.bb.setStandardButtons(self.bb.Yes|self.bb.No|self.bb.Ok)
        self.bb.button(self.bb.Yes).setDefault(True)

        self.checkbox = QCheckBox('', self)

        l.addWidget(ic, 0, 0, 1, 1)
        l.addWidget(msg, 0, 1, 1, 1)
        l.addWidget(self.checkbox, 1, 0, 1, 2)
        l.addWidget(self.det_msg, 2, 0, 1, 2)
        l.addWidget(self.bb, 3, 0, 1, 2)

        self.ask_question.connect(self.do_ask_question,
                type=Qt.QueuedConnection)

    def copy_to_clipboard(self, *args):
        QApplication.clipboard().setText(
                'calibre, version %s\n%s: %s\n\n%s' %
                (__version__, unicode(self.windowTitle()),
                    unicode(self.msg_label.text()),
                    unicode(self.det_msg.toPlainText())))
        self.copy_button.setText(_('Copied'))

    def action_clicked(self):
        if self.questions:
            q = self.questions[0]
            self.questions[0] = q._replace(callback=q.action_callback)
        self.accept()

    def accept(self):
        if self.geom_pref:
            gprefs[self.geom_pref] = bytearray(self.saveGeometry())
        if self.questions:
            payload, callback, cancel_callback = self.questions[0][:3]
            self.questions = self.questions[1:]
            cb = None
            if self.checkbox.isVisible():
                cb = bool(self.checkbox.isChecked())
            self.ask_question.emit(callback, payload, cb)
        self.hide()

    def reject(self):
        if self.geom_pref:
            gprefs[self.geom_pref] = bytearray(self.saveGeometry())
        if self.questions:
            payload, callback, cancel_callback = self.questions[0][:3]
            self.questions = self.questions[1:]
            cb = None
            if self.checkbox.isVisible():
                cb = bool(self.checkbox.isChecked())
            self.ask_question.emit(cancel_callback, payload, cb)
        self.hide()

    def do_ask_question(self, callback, payload, checkbox_checked):
        if callable(callback):
            args = [payload]
            if checkbox_checked is not None:
                args.append(checkbox_checked)
            callback(*args)
        self.show_question()

    def toggle_det_msg(self, *args):
        vis = unicode(self.det_msg_toggle.text()) == self.hide_det_msg
        self.det_msg_toggle.setText(self.show_det_msg if vis else
                self.hide_det_msg)
        self.det_msg.setVisible(not vis)
        self.do_resize()

    def do_resize(self):
        if self.geom_pref:
            geom = gprefs.get(self.geom_pref, None)
            if geom:
                self.restoreGeometry(geom)
                return
        sz = self.sizeHint() + QSize(100, 0)
        sz.setWidth(min(500, sz.width()))
        sz.setHeight(min(500, sz.height()))
        self.resize(sz)

    def show_question(self):
        if self.isVisible():
            return
        if self.questions:
            question = self.questions[0]
            self.msg_label.setText(question.msg)
            self.setWindowTitle(question.title)
            self.log_button.setVisible(bool(question.html_log))
            self.copy_button.setText(_('&Copy to clipboard'))
            self.copy_button.setVisible(bool(question.show_copy_button))
            self.action_button.setVisible(question.action_callback is not None)
            if question.action_callback is not None:
                self.action_button.setText(question.action_label or '')
                self.action_button.setIcon(
                    QIcon() if question.action_icon is None else question.action_icon)
            self.det_msg.setPlainText(question.det_msg or '')
            self.det_msg.setVisible(False)
            self.det_msg_toggle.setVisible(bool(question.det_msg))
            self.det_msg_toggle.setText(self.show_det_msg)
            self.checkbox.setVisible(question.checkbox_msg is not None)
            if question.checkbox_msg is not None:
                self.checkbox.setText(question.checkbox_msg)
                self.checkbox.setChecked(question.checkbox_checked)
            self.bb.button(self.bb.Ok).setVisible(question.show_ok)
            self.bb.button(self.bb.Yes).setVisible(not question.show_ok)
            self.bb.button(self.bb.No).setVisible(not question.show_ok)
            self.geom_pref = ('proceed question dialog:' + question.geom_pref) if question.geom_pref else None
            if question.show_det:
                self.toggle_det_msg()
            else:
                self.do_resize()
            self.show()
            button = self.action_button if question.focus_action and question.action_callback is not None else \
                (self.bb.button(self.bb.Ok) if question.show_ok else self.bb.button(self.bb.Yes))
            button.setDefault(True)
            button.setFocus(Qt.OtherFocusReason)

    def __call__(self, callback, payload, html_log, log_viewer_title, title,
            msg, det_msg='', show_copy_button=False, cancel_callback=None,
            log_is_file=False, checkbox_msg=None, checkbox_checked=False,
            action_callback=None, action_label=None, action_icon=None, focus_action=False,
            show_det=False, show_ok=False, geom_pref=None):
        '''
        A non modal popup that notifies the user that a background task has
        been completed. This class guarantees that only a single popup is
        visible at any one time. Other requests are queued and displayed after
        the user dismisses the current popup.

        :param callback: A callable that is called with payload if the user
        asks to proceed. Note that this is always called in the GUI thread.
        :param cancel_callback: A callable that is called with the payload if
        the users asks not to proceed.
        :param payload: Arbitrary object, passed to callback
        :param html_log: An HTML or plain text log
        :param log_viewer_title: The title for the log viewer window
        :param title: The title for this popup
        :param msg: The msg to display
        :param det_msg: Detailed message
        :param log_is_file: If True the html_log parameter is interpreted as
                            the path to a file on disk containing the log
                            encoded with utf-8
        :param checkbox_msg: If not None, a checkbox is displayed in the
                             dialog, showing this message. The callback is
                             called with both the payload and the state of the
                             checkbox as arguments.
        :param checkbox_checked: If True the checkbox is checked by default.
        :param action_callback: If not None, an extra button is added, which
                                when clicked will cause action_callback to be called
                                instead of callback. action_callback is called in
                                exactly the same way as callback.
        :param action_label: The text on the action button
        :param action_icon: The icon for the action button, must be a QIcon object or None
        :param focus_action: If True, the action button will be focused instead of the Yes button
        :param show_det: If True, the Detailed message will be shown initially
        :param show_ok: If True, OK will be shown instead of YES/NO
        :param geom_pref: String for preference name to preserve dialog box geometry

        '''
        question = Question(
            payload, callback, cancel_callback, title, msg, html_log,
            log_viewer_title, log_is_file, det_msg, show_copy_button,
            checkbox_msg, checkbox_checked, action_callback, action_label,
            action_icon, focus_action, show_det, show_ok, geom_pref)
        self.questions.append(question)
        self.show_question()

    def show_log(self):
        if self.questions:
            q = self.questions[0]
            log = q.html_log
            if q.log_is_file:
                with open(log, 'rb') as f:
                    log = f.read().decode('utf-8')
            self.log_viewer = ViewLog(q.log_viewer_title, log,
                        parent=self)
Beispiel #10
0
class RestoreImageDialog(QDialog):
    def __init__(self, parent, modal=True, flags=Qt.WindowFlags()):
        QDialog.__init__(self, parent, flags)
        self.model = None
        self.setModal(modal)
        self.setWindowTitle("Restore model into image")
        lo = QVBoxLayout(self)
        lo.setContentsMargins(10, 10, 10, 10)
        lo.setSpacing(5)
        # file selector
        self.wfile_in = FileSelector(self,
                                     label="Input FITS file:",
                                     dialog_label="Input FITS file",
                                     default_suffix="fits",
                                     file_types="FITS files (*.fits *.FITS)",
                                     file_mode=QFileDialog.ExistingFile)
        lo.addWidget(self.wfile_in)
        self.wfile_out = FileSelector(self,
                                      label="Output FITS file:",
                                      dialog_label="Output FITS file",
                                      default_suffix="fits",
                                      file_types="FITS files (*.fits *.FITS)",
                                      file_mode=QFileDialog.AnyFile)
        lo.addWidget(self.wfile_out)
        # beam size
        lo1 = QHBoxLayout()
        lo.addLayout(lo1)
        lo1.setContentsMargins(0, 0, 0, 0)
        lo1.addWidget(QLabel("Restoring beam FWHM, major axis:", self))
        self.wbmaj = QLineEdit(self)
        lo1.addWidget(self.wbmaj)
        lo1.addWidget(QLabel("\"     minor axis:", self))
        self.wbmin = QLineEdit(self)
        lo1.addWidget(self.wbmin)
        lo1.addWidget(QLabel("\"     P.A.:", self))
        self.wbpa = QLineEdit(self)
        lo1.addWidget(self.wbpa)
        lo1.addWidget(QLabel("\u00B0", self))
        for w in self.wbmaj, self.wbmin, self.wbpa:
            w.setValidator(QDoubleValidator(self))
        lo1 = QHBoxLayout()
        lo.addLayout(lo1)
        lo1.setContentsMargins(0, 0, 0, 0)
        self.wfile_psf = FileSelector(
            self,
            label="Set restoring beam by fitting PSF image:",
            dialog_label="PSF FITS file",
            default_suffix="fits",
            file_types="FITS files (*.fits *.FITS)",
            file_mode=QFileDialog.ExistingFile)
        lo1.addSpacing(32)
        lo1.addWidget(self.wfile_psf)
        # selection only
        self.wselonly = QCheckBox("restore selected model sources only", self)
        lo.addWidget(self.wselonly)
        # OK/cancel buttons
        lo.addSpacing(10)
        lo2 = QHBoxLayout()
        lo.addLayout(lo2)
        lo2.setContentsMargins(0, 0, 0, 0)
        lo2.setContentsMargins(5, 5, 5, 5)
        self.wokbtn = QPushButton("OK", self)
        self.wokbtn.setMinimumWidth(128)
        self.wokbtn.clicked.connect(self.accept)
        self.wokbtn.setEnabled(False)
        cancelbtn = QPushButton("Cancel", self)
        cancelbtn.setMinimumWidth(128)
        cancelbtn.clicked.connect(self.reject)
        lo2.addWidget(self.wokbtn)
        lo2.addStretch(1)
        lo2.addWidget(cancelbtn)
        self.setMinimumWidth(384)
        # signals
        self.wfile_in.filenameSelected.connect(self._fileSelected)
        self.wfile_in.filenameSelected.connect(self._inputFileSelected)
        self.wfile_out.filenameSelected.connect(self._fileSelected)
        self.wfile_psf.filenameSelected.connect(self._psfFileSelected)
        # internal state
        self.qerrmsg = QErrorMessage(self)

    def setModel(self, model):
        nsel = len([src for src in model.sources if src.selected])
        self.wselonly.setVisible(nsel > 0 and nsel < len(model.sources))
        self.model = model
        self._fileSelected(None)

    def _fileSelected(self, filename):
        self.wokbtn.setEnabled(
            bool(self.wfile_in.filename() and self.wfile_out.filename()))

    def _inputFileSelected(self, filename):
        if filename:
            try:
                header = pyfits.open(filename)[0].header
            except Exception as err:
                self.qerrmsg.showMessage("Error reading FITS file %s: %s" %
                                         (filename, str(err)))
                self.wfile_in.setFilename("")
                return
            # try to get beam extents
            gx, gy, grot = [
                header.get(x, None) for x in ('BMAJ', 'BMIN', 'BPA')
            ]
            if all([x is not None for x in (gx, gy, grot)]):
                # if beam size is already set, ask before overwriting
                print([
                    str(x.text()) for x in (self.wbmaj, self.wbmin, self.wbpa)
                ])
                if any([bool(str(x.text())) for x in (self.wbmaj, self.wbmin, self.wbpa)]) and \
                        QMessageBox.question(self, "Set restoring beam",
                                             "Also reset restoring beam size from this FITS file?",
                                             QMessageBox.Yes | QMessageBox.No) != QMessageBox.Yes:
                    return
                self.wbmaj.setText("%.2f" % (gx * 3600))
                self.wbmin.setText("%.2f" % (gy * 3600))
                self.wbpa.setText("%.2f" % grot)

    def _psfFileSelected(self, filename):
        busy = BusyIndicator()
        filename = str(filename)
        self.parent().showMessage("Fitting gaussian to PSF file %s" % filename)
        try:
            bmaj, bmin, pa = [x / DEG for x in Imaging.fitPsf(filename)]
        except Exception as err:
            busy.reset_cursor()
            self.qerrmsg.showMessage("Error fitting PSF file %s: %s" %
                                     (filename, str(err)))
            return
        bmaj *= 3600 * Imaging.FWHM
        bmin *= 3600 * Imaging.FWHM
        self.wbmaj.setText(str(bmaj))
        self.wbmin.setText(str(bmin))
        self.wbpa.setText(str(pa))
        busy.reset_cursor()

    def accept(self):
        """Tries to restore the image, and closes the dialog if successful."""
        # get list of sources to restore
        sources = self.model.sources
        sel_sources = [src for src in sources if src.selected]
        if len(sel_sources) > 0 and len(sel_sources) < len(
                sources) and self.wselonly.isChecked():
            sources = sel_sources
        if not sources:
            self.qerrmsg.showMessage("No sources to restore.")
            return
        busy = BusyIndicator()
        # get filenames
        infile = self.wfile_in.filename()
        outfile = self.wfile_out.filename()
        self.parent().showMessage(
            "Restoring %d model sources to image %s, writing to %s" %
            (len(sources), infile, outfile))
        # read fits file
        try:
            input_hdu = pyfits.open(infile)[0]
        except Exception as err:
            busy.reset_cursor()
            self.qerrmsg.showMessage("Error reading FITS file %s: %s" %
                                     (infile, str(err)))
            return
        # get beam sizes
        try:
            bmaj = float(str(self.wbmaj.text()))
            bmin = float(str(self.wbmin.text()))
            pa = float(str(self.wbpa.text()) or "0")
        except Exception as err:
            busy.reset_cursor()
            self.qerrmsg.showMessage("Invalid beam size specified")
            return
        bmaj = bmaj / (Imaging.FWHM * 3600) * DEG
        bmin = bmin / (Imaging.FWHM * 3600) * DEG
        pa = pa * DEG
        # restore
        try:
            Imaging.restoreSources(input_hdu, sources, bmaj, bmin, pa)
        except Exception as err:
            busy.reset_cursor()
            self.qerrmsg.showMessage("Error restoring model into image: %s" %
                                     str(err))
            return
        # save fits file
        try:
            input_hdu.writeto(outfile, overwrite=True)
        except Exception as err:
            busy.reset_cursor()
            self.qerrmsg.showMessage("Error writing FITS file %s: %s" %
                                     (outfile, str(err)))
            return
        self.parent().loadImage(outfile)
        busy.reset_cursor()
        return QDialog.accept(self)