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)
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'), 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()
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)
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> ') 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()
class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(WINDOW_TITLE) from pathlib import Path file_name = str(Path(__file__).resolve().parent / 'favicon.ico') icon = QIcon(file_name) self.setWindowIcon(icon) self.tray = QSystemTrayIcon(icon) self.tray.setToolTip(self.windowTitle()) self.tray.activated.connect(self._on_tray_activated) self.tray.show() self.logged_dict = dict() self.pb_refresh = QPushButton('REFRESH') self.pb_refresh.clicked.connect(self.refresh) self.cb_show_log = QCheckBox() self.cb_show_log.setChecked(True) self.log = QPlainTextEdit() self.log.setReadOnly(True) self.log.setWordWrapMode(QTextOption.NoWrap) log_font = self.log.font() log_font.setFamily('Courier New') self.log.setFont(log_font) self.cb_show_log.clicked.connect(self.log.setVisible) self.log.setVisible(self.cb_show_log.isChecked()) header_labels = ['DATE', 'TOTAL LOGGED TIME'] self.table_logged = QTableWidget() self.table_logged.setEditTriggers(QTableWidget.NoEditTriggers) self.table_logged.setSelectionBehavior(QTableWidget.SelectRows) self.table_logged.setSelectionMode(QTableWidget.SingleSelection) self.table_logged.setColumnCount(len(header_labels)) self.table_logged.setHorizontalHeaderLabels(header_labels) self.table_logged.horizontalHeader().setStretchLastSection(True) self.table_logged.itemClicked.connect( self._on_table_logged_item_clicked) header_labels = ['TIME', 'LOGGED', 'JIRA'] self.table_logged_info = QTableWidget() self.table_logged_info.setEditTriggers(QTableWidget.NoEditTriggers) self.table_logged_info.setSelectionBehavior(QTableWidget.SelectRows) self.table_logged_info.setSelectionMode(QTableWidget.SingleSelection) self.table_logged_info.setColumnCount(len(header_labels)) self.table_logged_info.setHorizontalHeaderLabels(header_labels) self.table_logged_info.horizontalHeader().setSectionResizeMode( 1, QHeaderView.ResizeToContents) self.table_logged_info.horizontalHeader().setStretchLastSection(True) self.table_logged_info.itemDoubleClicked.connect( self._on_table_logged_info_item_double_clicked) main_layout = QVBoxLayout() central_widget = QWidget() central_widget.setLayout(main_layout) self.setCentralWidget(central_widget) self.pb_refresh.setSizePolicy( QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)) h_layout = QHBoxLayout() h_layout.addWidget(self.pb_refresh) h_layout.addWidget(self.cb_show_log) layout_table_widget = QVBoxLayout() layout_table_widget.setContentsMargins(0, 0, 0, 0) layout_table_widget.addWidget(self.table_logged) layout_table_widget.addWidget(self.table_logged_info) table_widget = QWidget() table_widget.setLayout(layout_table_widget) splitter = QSplitter(Qt.Horizontal) splitter.addWidget(table_widget) splitter.addWidget(self.log) main_layout.addLayout(h_layout) main_layout.addWidget(splitter) def _fill_tables(self, xml_data: bytes): import io buffer_io = io.StringIO() from contextlib import redirect_stdout try: with redirect_stdout(buffer_io): print(len(xml_data), repr(xml_data[:50])) # Структура документа -- xml self.logged_dict = parse_logged_dict(xml_data) print(self.logged_dict) if not self.logged_dict: return import json print( json.dumps(self.logged_dict, indent=4, ensure_ascii=False)) print() logged_list = get_logged_list_by_now_utc_date(self.logged_dict) logged_total_seconds = get_logged_total_seconds(logged_list) logged_total_seconds_str = seconds_to_str(logged_total_seconds) print('entry_logged_list:', logged_list) print('today seconds:', logged_total_seconds) print('today time:', logged_total_seconds_str) print() # Для красоты выводим результат в табличном виде lines = [] # Удаление строк таблицы while self.table_logged.rowCount(): self.table_logged.removeRow(0) for i, (date_str, logged_list) in enumerate( get_sorted_logged(self.logged_dict)): total_seconds = get_logged_total_seconds(logged_list) total_seconds_str = seconds_to_str(total_seconds) row = date_str, total_seconds_str, total_seconds lines.append(row) self.table_logged.setRowCount( self.table_logged.rowCount() + 1) self.table_logged.setItem(i, 0, QTableWidgetItem(date_str)) item = QTableWidgetItem(total_seconds_str) item.setToolTip('Total seconds: {}'.format(total_seconds)) self.table_logged.setItem(i, 1, item) self.table_logged.setCurrentCell(0, 0) self.table_logged.setFocus() self._on_table_logged_item_clicked( self.table_logged.currentItem()) # Список строк станет списком столбцов, у каждого столбца подсчитается максимальная длина max_len_columns = [ max(map(len, map(str, col))) for col in zip(*lines) ] # Создание строки форматирования: [30, 14, 5] -> "{:<30} | {:<14} | {:<5}" my_table_format = ' | '.join('{:<%s}' % max_len for max_len in max_len_columns) for line in lines: print(my_table_format.format(*line)) finally: text = buffer_io.getvalue() self.log.setPlainText(text) print(text) def refresh(self): progress_dialog = QProgressDialog(self) thread = RunFuncThread(func=get_rss_jira_log) thread.run_finished.connect(self._fill_tables) thread.run_finished.connect(progress_dialog.close) thread.start() progress_dialog.setWindowTitle('Please wait...') progress_dialog.setLabelText(progress_dialog.windowTitle()) progress_dialog.setRange(0, 0) progress_dialog.exec() from datetime import datetime self.setWindowTitle(WINDOW_TITLE + ". Last refresh date: " + datetime.now().strftime('%d/%m/%Y %H:%M:%S')) def _on_table_logged_item_clicked(self, item: QTableWidgetItem): # Удаление строк таблицы while self.table_logged_info.rowCount(): self.table_logged_info.removeRow(0) row = item.row() date_str = self.table_logged.item(row, 0).text() logged_list = self.logged_dict[date_str] logged_list = reversed(logged_list) for i, logged in enumerate(logged_list): self.table_logged_info.setRowCount( self.table_logged_info.rowCount() + 1) self.table_logged_info.setItem(i, 0, QTableWidgetItem(logged['time'])) self.table_logged_info.setItem( i, 1, QTableWidgetItem(logged['logged_human_time'])) item = QTableWidgetItem(logged['jira_id']) item.setToolTip(logged['jira_title']) self.table_logged_info.setItem(i, 2, item) def _on_table_logged_info_item_double_clicked(self, item: QTableWidgetItem): row = item.row() jira_id = self.table_logged_info.item(row, 2).text() url = 'https://jira.compassplus.ru/browse/' + jira_id import webbrowser webbrowser.open(url) def _on_tray_activated(self, reason): self.setVisible(not self.isVisible()) if self.isVisible(): self.showNormal() self.activateWindow() def changeEvent(self, event: QEvent): if event.type() == QEvent.WindowStateChange: # Если окно свернули if self.isMinimized(): # Прячем окно с панели задач QTimer.singleShot(0, self.hide)