class RevealBar(QWidget): # {{{ def __init__(self, parent): QWidget.__init__(self, parent) self.setVisible(False) self._animated_size = 1.0 self.animation = QPropertyAnimation(self, b'animated_size', self) self.animation.setEasingCurve(QEasingCurve.Type.Linear) self.animation.setDuration(1000), self.animation.setStartValue(0.0), self.animation.setEndValue(1.0) self.animation.valueChanged.connect(self.animation_value_changed) self.animation.finished.connect(self.animation_done) @pyqtProperty(float) def animated_size(self): return self._animated_size @animated_size.setter def animated_size(self, val): self._animated_size = val def animation_value_changed(self, *args): self.update() def animation_done(self): self.setVisible(False) self.update() def start(self, bar): self.setGeometry(bar.geometry()) self.setVisible(True) self.animation.start() def paintEvent(self, ev): if self._animated_size < 1.0: rect = self.rect() painter = QPainter(self) pal = self.palette() col = pal.color(QPalette.ColorRole.Button) rect.setLeft(rect.left() + int(rect.width() * self._animated_size)) painter.setClipRect(rect) painter.fillRect(self.rect(), col)
class CoverView(QWidget): # {{{ cover_changed = pyqtSignal(object, object) cover_removed = pyqtSignal(object) open_cover_with = pyqtSignal(object, object) search_internet = pyqtSignal(object) def __init__(self, vertical, parent=None): QWidget.__init__(self, parent) self._current_pixmap_size = QSize(120, 120) self.vertical = vertical self.animation = QPropertyAnimation(self, b'current_pixmap_size', self) self.animation.setEasingCurve(QEasingCurve(QEasingCurve.Type.OutExpo)) self.animation.setDuration(1000) self.animation.setStartValue(QSize(0, 0)) self.animation.valueChanged.connect(self.value_changed) self.setSizePolicy( QSizePolicy.Policy.Expanding if vertical else QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) self.default_pixmap = QPixmap(I('default_cover.png')) self.pixmap = self.default_pixmap self.pwidth = self.pheight = None self.data = {} self.do_layout() def value_changed(self, val): self.update() def setCurrentPixmapSize(self, val): self._current_pixmap_size = val def do_layout(self): if self.rect().width() == 0 or self.rect().height() == 0: return pixmap = self.pixmap pwidth, pheight = pixmap.width(), pixmap.height() try: self.pwidth, self.pheight = fit_image(pwidth, pheight, self.rect().width(), self.rect().height())[1:] except: self.pwidth, self.pheight = self.rect().width()-1, \ self.rect().height()-1 self.current_pixmap_size = QSize(self.pwidth, self.pheight) self.animation.setEndValue(self.current_pixmap_size) def show_data(self, data): self.animation.stop() same_item = getattr(data, 'id', True) == self.data.get('id', False) self.data = {'id':data.get('id', None)} if data.cover_data[1]: self.pixmap = QPixmap.fromImage(data.cover_data[1]) if self.pixmap.isNull() or self.pixmap.width() < 5 or \ self.pixmap.height() < 5: self.pixmap = self.default_pixmap else: self.pixmap = self.default_pixmap self.do_layout() self.update() if (not same_item and not config['disable_animations'] and self.isVisible()): self.animation.start() def paintEvent(self, event): canvas_size = self.rect() width = self.current_pixmap_size.width() extrax = canvas_size.width() - width if extrax < 0: extrax = 0 x = int(extrax//2) height = self.current_pixmap_size.height() extray = canvas_size.height() - height if extray < 0: extray = 0 y = int(extray//2) target = QRect(x, y, width, height) p = QPainter(self) p.setRenderHints(QPainter.RenderHint.Antialiasing | QPainter.RenderHint.SmoothPixmapTransform) try: dpr = self.devicePixelRatioF() except AttributeError: dpr = self.devicePixelRatio() spmap = self.pixmap.scaled(target.size() * dpr, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation) spmap.setDevicePixelRatio(dpr) p.drawPixmap(target, spmap) if gprefs['bd_overlay_cover_size']: sztgt = target.adjusted(0, 0, 0, -4) f = p.font() f.setBold(True) p.setFont(f) sz = '\u00a0%d x %d\u00a0'%(self.pixmap.width(), self.pixmap.height()) flags = Qt.AlignmentFlag.AlignBottom|Qt.AlignmentFlag.AlignRight|Qt.TextFlag.TextSingleLine szrect = p.boundingRect(sztgt, flags, sz) p.fillRect(szrect.adjusted(0, 0, 0, 4), QColor(0, 0, 0, 200)) p.setPen(QPen(QColor(255,255,255))) p.drawText(sztgt, flags, sz) p.end() current_pixmap_size = pyqtProperty('QSize', fget=lambda self: self._current_pixmap_size, fset=setCurrentPixmapSize ) def contextMenuEvent(self, ev): cm = QMenu(self) paste = cm.addAction(_('Paste cover')) copy = cm.addAction(_('Copy cover')) save = cm.addAction(_('Save cover to disk')) remove = cm.addAction(_('Remove cover')) gc = cm.addAction(_('Generate cover from metadata')) cm.addSeparator() if not QApplication.instance().clipboard().mimeData().hasImage(): paste.setEnabled(False) copy.triggered.connect(self.copy_to_clipboard) paste.triggered.connect(self.paste_from_clipboard) remove.triggered.connect(self.remove_cover) gc.triggered.connect(self.generate_cover) save.triggered.connect(self.save_cover) create_open_cover_with_menu(self, cm) cm.si = m = create_search_internet_menu(self.search_internet.emit) cm.addMenu(m) cm.exec_(ev.globalPos()) def open_with(self, entry): id_ = self.data.get('id', None) if id_ is not None: self.open_cover_with.emit(id_, entry) def choose_open_with(self): from calibre.gui2.open_with import choose_program entry = choose_program('cover_image', self) if entry is not None: self.open_with(entry) def copy_to_clipboard(self): QApplication.instance().clipboard().setPixmap(self.pixmap) def paste_from_clipboard(self, pmap=None): if not isinstance(pmap, QPixmap): cb = QApplication.instance().clipboard() pmap = cb.pixmap() if pmap.isNull() and cb.supportsSelection(): pmap = cb.pixmap(QClipboard.Mode.Selection) if not pmap.isNull(): self.update_cover(pmap) def save_cover(self): from calibre.gui2.ui import get_gui book_id = self.data.get('id') db = get_gui().current_db.new_api path = choose_save_file( self, 'save-cover-from-book-details', _('Choose cover save location'), filters=[(_('JPEG images'), ['jpg', 'jpeg'])], all_files=False, initial_filename='{}.jpeg'.format(sanitize_file_name(db.field_for('title', book_id, default_value='cover'))) ) if path: db.copy_cover_to(book_id, path) def update_cover(self, pmap=None, cdata=None): if pmap is None: pmap = QPixmap() pmap.loadFromData(cdata) if pmap.isNull(): return if pmap.hasAlphaChannel(): pmap = QPixmap.fromImage(blend_image(image_from_x(pmap))) self.pixmap = pmap self.do_layout() self.update() self.update_tooltip(getattr(self.parent(), 'current_path', '')) if not config['disable_animations']: self.animation.start() id_ = self.data.get('id', None) if id_ is not None: self.cover_changed.emit(id_, cdata or pixmap_to_data(pmap)) def generate_cover(self, *args): book_id = self.data.get('id') if book_id is None: return from calibre.gui2.ui import get_gui mi = get_gui().current_db.new_api.get_metadata(book_id) if not mi.has_cover or confirm( _('Are you sure you want to replace the cover? The existing cover will be permanently lost.'), 'book_details_generate_cover'): from calibre.ebooks.covers import generate_cover cdata = generate_cover(mi) self.update_cover(cdata=cdata) def remove_cover(self): if not confirm_delete( _('Are you sure you want to delete the cover permanently?'), 'book-details-confirm-cover-remove', parent=self): return id_ = self.data.get('id', None) self.pixmap = self.default_pixmap self.do_layout() self.update() if id_ is not None: self.cover_removed.emit(id_) def update_tooltip(self, current_path): try: sz = self.pixmap.size() except: sz = QSize(0, 0) self.setToolTip( '<p>'+_('Double click to open the Book details window') + '<br><br>' + _('Path') + ': ' + current_path + '<br><br>' + _('Cover size: %(width)d x %(height)d pixels')%dict( width=sz.width(), height=sz.height()) )
class Pointer(QWidget): def __init__(self, gui): QWidget.__init__(self, gui) self.setObjectName('jobs_pointer') self.setVisible(False) self.resize(100, 80) self.animation = QPropertyAnimation(self, b"geometry", self) self.animation.setDuration(750) self.animation.setLoopCount(2) self.animation.setEasingCurve(QEasingCurve.Type.Linear) self.animation.finished.connect(self.hide) taily, heady = 0, 55 self.arrow_path = QPainterPath(QPointF(40, taily)) self.arrow_path.lineTo(40, heady) self.arrow_path.lineTo(20, heady) self.arrow_path.lineTo(50, self.height()) self.arrow_path.lineTo(80, heady) self.arrow_path.lineTo(60, heady) self.arrow_path.lineTo(60, taily) self.arrow_path.closeSubpath() c = self.palette().color(QPalette.ColorGroup.Active, QPalette.ColorRole.WindowText) self.color = QColor(c) self.color.setAlpha(100) self.brush = QBrush(self.color, Qt.BrushStyle.SolidPattern) # from qt.core import QTimer # QTimer.singleShot(1000, self.start) @property def gui(self): return self.parent() def point_at(self, frac): return (self.path.pointAtPercent(frac).toPoint() - QPoint(self.rect().center().x(), self.height())) def rect_at(self, frac): return QRect(self.point_at(frac), self.size()) def abspos(self, widget): pos = widget.pos() parent = widget.parent() while parent is not self.gui: pos += parent.pos() parent = parent.parent() return pos def start(self): if config['disable_animations']: return self.setVisible(True) self.raise_() end = self.abspos(self.gui.jobs_button) end = QPointF(end.x() + self.gui.jobs_button.width() / 3.0, end.y() + 20) start = QPointF(end.x(), end.y() - 0.5 * self.height()) self.path = QPainterPath(QPointF(start)) self.path.lineTo(end) self.path.closeSubpath() self.animation.setStartValue(self.rect_at(0.0)) self.animation.setEndValue(self.rect_at(1.0)) self.animation.setDirection(QAbstractAnimation.Direction.Backward) num_keys = 100 for i in range(1, num_keys): i /= num_keys self.animation.setKeyValueAt(i, self.rect_at(i)) self.animation.start() def paintEvent(self, ev): p = QPainter(self) p.setRenderHints(QPainter.RenderHint.Antialiasing) p.setBrush(self.brush) p.setPen(Qt.PenStyle.NoPen) p.drawPath(self.arrow_path) p.end()