%PDF- %PDF-
Direktori : /lib/calibre/calibre/gui2/dialogs/ |
Current File : //lib/calibre/calibre/gui2/dialogs/message_box.py |
#!/usr/bin/env python3 __license__ = 'GPL v3' __copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>' __docformat__ = 'restructuredtext en' import sys from qt.core import ( QAction, QApplication, QCheckBox, QDialog, QDialogButtonBox, QGridLayout, QIcon, QKeySequence, QLabel, QPainter, QPlainTextEdit, QSize, QSizePolicy, Qt, QTextBrowser, QTextDocument, QVBoxLayout, QWidget, pyqtSignal ) from calibre.constants import __version__, isfrozen from calibre.gui2 import gprefs class Icon(QWidget): def __init__(self, parent=None, size=None): QWidget.__init__(self, parent) self.pixmap = None self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) self.size = size or 64 def set_icon(self, qicon): self.pixmap = qicon.pixmap(self.size, self.size) self.update() def sizeHint(self): return QSize(self.size, self.size) def paintEvent(self, ev): if self.pixmap is not None: x = (self.width() - self.size) // 2 y = (self.height() - self.size) // 2 p = QPainter(self) p.drawPixmap(x, y, self.size, self.size, self.pixmap) class MessageBox(QDialog): # {{{ ERROR = 0 WARNING = 1 INFO = 2 QUESTION = 3 resize_needed = pyqtSignal() def setup_ui(self): self.setObjectName("Dialog") self.resize(497, 235) self.gridLayout = l = QGridLayout(self) l.setObjectName("gridLayout") self.icon_widget = Icon(self) l.addWidget(self.icon_widget) self.msg = la = QLabel(self) la.setWordWrap(True), la.setMinimumWidth(400) la.setOpenExternalLinks(True) la.setObjectName("msg") l.addWidget(la, 0, 1, 1, 1) self.det_msg = dm = QTextBrowser(self) dm.setReadOnly(True) dm.setObjectName("det_msg") l.addWidget(dm, 1, 0, 1, 2) self.bb = bb = QDialogButtonBox(self) bb.setStandardButtons(QDialogButtonBox.StandardButton.Ok) bb.setObjectName("bb") bb.accepted.connect(self.accept) bb.rejected.connect(self.reject) l.addWidget(bb, 3, 0, 1, 2) self.toggle_checkbox = tc = QCheckBox(self) tc.setObjectName("toggle_checkbox") l.addWidget(tc, 2, 0, 1, 2) def __init__(self, type_, title, msg, det_msg='', q_icon=None, show_copy_button=True, parent=None, default_yes=True, yes_text=None, no_text=None, yes_icon=None, no_icon=None, add_abort_button=False, only_copy_details=False ): QDialog.__init__(self, parent) self.only_copy_details = only_copy_details self.aborted = False if q_icon is None: icon = { self.ERROR : 'error', self.WARNING: 'warning', self.INFO: 'information', self.QUESTION: 'question', }[type_] icon = 'dialog_%s.png'%icon self.icon = QIcon(I(icon)) else: self.icon = q_icon if isinstance(q_icon, QIcon) else QIcon(I(q_icon)) self.setup_ui() self.setWindowTitle(title) self.setWindowIcon(self.icon) self.icon_widget.set_icon(self.icon) self.msg.setText(msg) if det_msg and Qt.mightBeRichText(det_msg): self.det_msg.setHtml(det_msg) else: self.det_msg.setPlainText(det_msg) self.det_msg.setVisible(False) self.toggle_checkbox.setVisible(False) if show_copy_button: self.ctc_button = self.bb.addButton(_('&Copy to clipboard'), QDialogButtonBox.ButtonRole.ActionRole) self.ctc_button.clicked.connect(self.copy_to_clipboard) self.show_det_msg = _('Show &details') self.hide_det_msg = _('Hide &details') self.det_msg_toggle = self.bb.addButton(self.show_det_msg, QDialogButtonBox.ButtonRole.ActionRole) self.det_msg_toggle.clicked.connect(self.toggle_det_msg) self.det_msg_toggle.setToolTip( _('Show detailed information about this error')) self.copy_action = QAction(self) self.addAction(self.copy_action) self.copy_action.setShortcuts(QKeySequence.StandardKey.Copy) self.copy_action.triggered.connect(self.copy_to_clipboard) self.is_question = type_ == self.QUESTION if self.is_question: self.bb.setStandardButtons(QDialogButtonBox.StandardButton.Yes|QDialogButtonBox.StandardButton.No) self.bb.button(QDialogButtonBox.StandardButton.Yes if default_yes else QDialogButtonBox.StandardButton.No ).setDefault(True) self.default_yes = default_yes if yes_text is not None: self.bb.button(QDialogButtonBox.StandardButton.Yes).setText(yes_text) if no_text is not None: self.bb.button(QDialogButtonBox.StandardButton.No).setText(no_text) if yes_icon is not None: self.bb.button(QDialogButtonBox.StandardButton.Yes).setIcon(yes_icon if isinstance(yes_icon, QIcon) else QIcon(I(yes_icon))) if no_icon is not None: self.bb.button(QDialogButtonBox.StandardButton.No).setIcon(no_icon if isinstance(no_icon, QIcon) else QIcon(I(no_icon))) else: self.bb.button(QDialogButtonBox.StandardButton.Ok).setDefault(True) if add_abort_button: self.bb.addButton(QDialogButtonBox.StandardButton.Abort).clicked.connect(self.on_abort) if not det_msg: self.det_msg_toggle.setVisible(False) self.resize_needed.connect(self.do_resize, type=Qt.ConnectionType.QueuedConnection) self.do_resize() def on_abort(self): self.aborted = True def sizeHint(self): ans = QDialog.sizeHint(self) ans.setWidth(max(min(ans.width(), 500), self.bb.sizeHint().width() + 100)) ans.setHeight(min(ans.height(), 500)) return ans def toggle_det_msg(self, *args): vis = self.det_msg.isVisible() self.det_msg.setVisible(not vis) self.det_msg_toggle.setText(self.show_det_msg if vis else self.hide_det_msg) self.resize_needed.emit() def do_resize(self): self.resize(self.sizeHint()) def copy_to_clipboard(self, *args): text = self.det_msg.toPlainText() if not self.only_copy_details: text = f'calibre, version {__version__}\n{self.windowTitle()}: {self.msg.text()}\n\n{text}' QApplication.clipboard().setText(text) if hasattr(self, 'ctc_button'): self.ctc_button.setText(_('Copied')) def showEvent(self, ev): ret = QDialog.showEvent(self, ev) if self.is_question: try: self.bb.button(QDialogButtonBox.StandardButton.Yes if self.default_yes else QDialogButtonBox.StandardButton.No ).setFocus(Qt.FocusReason.OtherFocusReason) except: pass # Buttons were changed else: self.bb.button(QDialogButtonBox.StandardButton.Ok).setFocus(Qt.FocusReason.OtherFocusReason) return ret def set_details(self, msg): if not msg: msg = '' if Qt.mightBeRichText(msg): self.det_msg.setHtml(msg) else: self.det_msg.setPlainText(msg) self.det_msg_toggle.setText(self.show_det_msg) self.det_msg_toggle.setVisible(bool(msg)) self.det_msg.setVisible(False) self.resize_needed.emit() # }}} class ViewLog(QDialog): # {{{ def __init__(self, title, html, parent=None, unique_name=None): QDialog.__init__(self, parent) self.l = l = QVBoxLayout() self.setLayout(l) self.tb = QTextBrowser(self) self.tb.setHtml('<pre style="font-family: monospace">%s</pre>' % html) l.addWidget(self.tb) self.bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok) self.bb.accepted.connect(self.accept) self.bb.rejected.connect(self.reject) self.copy_button = self.bb.addButton(_('Copy to clipboard'), QDialogButtonBox.ButtonRole.ActionRole) self.copy_button.setIcon(QIcon(I('edit-copy.png'))) self.copy_button.clicked.connect(self.copy_to_clipboard) l.addWidget(self.bb) self.unique_name = unique_name or 'view-log-dialog' self.finished.connect(self.dialog_closing) self.resize(QSize(700, 500)) geom = gprefs.get(self.unique_name, None) if geom is not None: QApplication.instance().safe_restore_geometry(self, geom) self.setModal(False) self.setWindowTitle(title) self.setWindowIcon(QIcon(I('debug.png'))) self.show() def copy_to_clipboard(self): txt = self.tb.toPlainText() QApplication.clipboard().setText(txt) def dialog_closing(self, result): gprefs[self.unique_name] = bytearray(self.saveGeometry()) # }}} _proceed_memory = [] class ProceedNotification(MessageBox): # {{{ ''' WARNING: This class is deprecated. DO not use it as some users have reported crashes when closing the dialog box generated by this class. Instead use: gui.proceed_question(...) The arguments are the same as for this class. ''' def __init__(self, callback, payload, html_log, log_viewer_title, title, msg, det_msg='', show_copy_button=False, parent=None, cancel_callback=None, log_is_file=False): ''' A non modal popup that notifies the user that a background task has been completed. :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 ''' MessageBox.__init__(self, MessageBox.QUESTION, title, msg, det_msg=det_msg, show_copy_button=show_copy_button, parent=parent) self.payload = payload self.html_log = html_log self.log_is_file = log_is_file self.log_viewer_title = log_viewer_title self.vlb = self.bb.addButton(_('&View log'), QDialogButtonBox.ButtonRole.ActionRole) self.vlb.setIcon(QIcon(I('debug.png'))) self.vlb.clicked.connect(self.show_log) self.det_msg_toggle.setVisible(bool(det_msg)) self.setModal(False) self.callback, self.cancel_callback = callback, cancel_callback _proceed_memory.append(self) def show_log(self): log = self.html_log if self.log_is_file: with open(log, 'rb') as f: log = f.read().decode('utf-8') self.log_viewer = ViewLog(self.log_viewer_title, log, parent=self) def do_proceed(self, result): from calibre.gui2.ui import get_gui func = (self.callback if result == QDialog.DialogCode.Accepted else self.cancel_callback) gui = get_gui() gui.proceed_requested.emit(func, self.payload) # Ensure this notification is garbage collected self.vlb.clicked.disconnect() self.callback = self.cancel_callback = self.payload = None self.setParent(None) _proceed_memory.remove(self) def done(self, r): self.do_proceed(r) return MessageBox.done(self, r) # }}} class ErrorNotification(MessageBox): # {{{ def __init__(self, html_log, log_viewer_title, title, msg, det_msg='', show_copy_button=False, parent=None): ''' A non modal popup that notifies the user that a background task has errored. :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 ''' MessageBox.__init__(self, MessageBox.ERROR, title, msg, det_msg=det_msg, show_copy_button=show_copy_button, parent=parent) self.html_log = html_log self.log_viewer_title = log_viewer_title self.finished.connect(self.do_close, type=Qt.ConnectionType.QueuedConnection) self.vlb = self.bb.addButton(_('&View log'), QDialogButtonBox.ButtonRole.ActionRole) self.vlb.setIcon(QIcon(I('debug.png'))) self.vlb.clicked.connect(self.show_log) self.det_msg_toggle.setVisible(bool(det_msg)) self.setModal(False) _proceed_memory.append(self) def show_log(self): self.log_viewer = ViewLog(self.log_viewer_title, self.html_log, parent=self) def do_close(self, result): # Ensure this notification is garbage collected self.setParent(None) self.finished.disconnect() self.vlb.clicked.disconnect() _proceed_memory.remove(self) # }}} 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> ') 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'), QDialogButtonBox.ButtonRole.ActionRole) self.ctc_button.clicked.connect(self.copy_to_clipboard) self.retry_button = self.bb.addButton(_('&Retry'), QDialogButtonBox.ButtonRole.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, QDialogButtonBox.ButtonRole.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, str(self.windowTitle()), str(d.toPlainText()), str(self.det_msg.toPlainText()))) if hasattr(self, 'ctc_button'): self.ctc_button.setText(_('Copied')) def toggle_det_msg(self, *args): vis = str(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(QDialogButtonBox.StandardButton.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() # }}} if __name__ == '__main__': from calibre.gui2 import Application, question_dialog from calibre import prepare_string_for_xml app = Application([]) merged = {'Kovid Goyal': ['Waterloo', 'Doomed'], 'Someone Else': ['Some other book ' * 1000]} lines = [] for author in sorted(merged): lines.append(f'<b><i>{prepare_string_for_xml(author)}</i></b><ol style="margin-top: 0">') for title in sorted(merged[author]): lines.append(f'<li>{prepare_string_for_xml(title)}</li>') lines.append('</ol>') print(question_dialog(None, 'title', 'msg <a href="http://google.com">goog</a> ', det_msg='\n'.join(lines), show_copy_button=True))