%PDF- %PDF-
| Direktori : /lib/calibre/calibre/gui2/ |
| Current File : //lib/calibre/calibre/gui2/proceed.py |
#!/usr/bin/env python3
__license__ = 'GPL v3'
__copyright__ = '2012, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from collections import namedtuple
from qt.core import (
QWidget, Qt, QLabel, QVBoxLayout, QDialogButtonBox, QApplication, QTimer, QPixmap, QEvent,
QSize, pyqtSignal, QIcon, QPlainTextEdit, QCheckBox, QPainter, QHBoxLayout, QFontMetrics,
QPainterPath, QRectF, pyqtProperty, QPropertyAnimation, QEasingCurve, QSizePolicy, QImage, QPalette)
from calibre.constants import __version__
from calibre.gui2.dialogs.message_box import ViewLog
Question = namedtuple('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')
class Icon(QWidget):
@pyqtProperty(float)
def fraction(self):
return self._fraction
@fraction.setter
def fraction(self, val):
self._fraction = max(0, min(2, float(val)))
self.update()
def showEvent(self, ev):
self.animation.start()
return QWidget.showEvent(self, ev)
def hideEvent(self, ev):
self.animation.stop()
return QWidget.hideEvent(self, ev)
def __init__(self, parent):
QWidget.__init__(self, parent)
self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
self.set_icon('dialog_question.png')
self.default_icon = self.icon
self._fraction = 0.0
self.animation = a = QPropertyAnimation(self, b"fraction", self)
a.setDuration(2000), a.setEasingCurve(QEasingCurve.Type.Linear)
a.setStartValue(0.0), a.setEndValue(2.0), a.setLoopCount(10)
def set_icon(self, icon):
if isinstance(icon, QIcon):
self.icon = icon.pixmap(self.sizeHint())
elif icon is None:
self.icon = self.default_icon
else:
self.icon = QIcon(I(icon)).pixmap(self.sizeHint())
self.update()
def sizeHint(self):
return QSize(64, 64)
def paintEvent(self, ev):
p = QPainter(self)
p.setOpacity(min(1, abs(1 - self._fraction)))
p.drawPixmap(self.rect(), self.icon)
p.end()
class PlainTextEdit(QPlainTextEdit):
def sizeHint(self):
fm = QFontMetrics(self.font())
ans = QPlainTextEdit.sizeHint(self)
ans.setWidth(fm.averageCharWidth() * 50)
return ans
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.Type.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'), QDialogButtonBox.ButtonRole.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'),
QDialogButtonBox.ButtonRole.ActionRole)
self.copy_button.clicked.connect(self.copy_to_clipboard)
self.action_button = self.bb.addButton('', QDialogButtonBox.ButtonRole.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, 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.det_msg = PlainTextEdit(self)
self.det_msg.setReadOnly(True)
self.bb.setStandardButtons(
QDialogButtonBox.StandardButton.Yes | QDialogButtonBox.StandardButton.No | QDialogButtonBox.StandardButton.Ok)
self.bb.button(QDialogButtonBox.StandardButton.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.ConnectionType.QueuedConnection)
self.setFocusPolicy(Qt.FocusPolicy.NoFocus)
for child in self.findChildren(QWidget):
child.setFocusPolicy(Qt.FocusPolicy.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() == QEvent.Type.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__, str(self.windowTitle()),
str(self.msg_label.text()),
str(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 = 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):
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.Orientation.Vertical), self.bb.setOrientation(Qt.Orientation.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(QDialogButtonBox.StandardButton.Ok).setVisible(question.show_ok)
self.bb.button(QDialogButtonBox.StandardButton.Yes).setVisible(not question.show_ok)
self.bb.button(QDialogButtonBox.StandardButton.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(QDialogButtonBox.StandardButton.Ok) if question.show_ok else self.bb.button(QDialogButtonBox.StandardButton.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.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.RenderHint.Antialiasing, True)
painter.setRenderHint(QPainter.RenderHint.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, int(top), self.rendered_pixmap)
def paint_background(self, painter):
br = 12 # border_radius
bw = 1 # border_width
pal = self.palette()
c = pal.color(QPalette.ColorRole.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(QPalette.ColorRole.WindowText))
def main():
from calibre.gui2 import Application
from qt.core import QMainWindow, QStatusBar, QTimer
app = Application([])
w = QMainWindow()
s = QStatusBar(w)
w.setStatusBar(s)
s.showMessage('Testing ProceedQuestion')
w.show()
p = ProceedQuestion(w)
def doit():
p.dummy_question()
p.dummy_question(action_label='A very long button for testing relayout (indeed)')
p(
lambda p:None, None, 'ass2', 'ass2', 'testing2', 'testing2',
det_msg='details shown first, with a long line to test wrapping of text and width layout',
show_det=True, show_ok=True)
QTimer.singleShot(10, doit)
app.exec()
if __name__ == '__main__':
main()