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()
class CoverDelegate(QStyledItemDelegate): MARGIN = 4 TOP, LEFT, RIGHT, BOTTOM = object(), object(), object(), object() @pyqtProperty(float) def animated_size(self): return self._animated_size @animated_size.setter def animated_size(self, val): self._animated_size = val def __init__(self, parent): super(CoverDelegate, self).__init__(parent) self._animated_size = 1.0 self.animation = QPropertyAnimation(self, b'animated_size', self) self.animation.setEasingCurve(QEasingCurve.Type.OutInCirc) self.animation.setDuration(500) self.set_dimensions() self.cover_cache = CoverCache() self.render_queue = LifoQueue() self.animating = None self.highlight_color = QColor(Qt.GlobalColor.white) self.rating_font = QFont(rating_font()) def set_dimensions(self): width = self.original_width = gprefs['cover_grid_width'] height = self.original_height = gprefs['cover_grid_height'] self.original_show_title = show_title = gprefs['cover_grid_show_title'] self.original_show_emblems = gprefs['show_emblems'] self.orginal_emblem_size = gprefs['emblem_size'] self.orginal_emblem_position = gprefs['emblem_position'] self.emblem_size = gprefs[ 'emblem_size'] if self.original_show_emblems else 0 try: self.gutter_position = getattr( self, self.orginal_emblem_position.upper()) except Exception: self.gutter_position = self.TOP if height < 0.1: height = auto_height(self.parent()) else: height *= self.parent().logicalDpiY() * CM_TO_INCH if width < 0.1: width = 0.75 * height else: width *= self.parent().logicalDpiX() * CM_TO_INCH self.cover_size = QSize(width, height) self.title_height = 0 if show_title: f = self.parent().font() sz = f.pixelSize() if sz < 5: sz = f.pointSize() * self.parent().logicalDpiY() / 72.0 self.title_height = max(25, sz + 10) self.item_size = self.cover_size + QSize( 2 * self.MARGIN, (2 * self.MARGIN) + self.title_height) if self.emblem_size > 0: extra = self.emblem_size + self.MARGIN self.item_size += QSize(extra, 0) if self.gutter_position in ( self.LEFT, self.RIGHT) else QSize(0, extra) self.calculate_spacing() self.animation.setStartValue(1.0) self.animation.setKeyValueAt(0.5, 0.5) self.animation.setEndValue(1.0) def calculate_spacing(self): spc = self.original_spacing = gprefs['cover_grid_spacing'] if spc < 0.01: self.spacing = max(10, min(50, int(0.1 * self.original_width))) else: self.spacing = self.parent().logicalDpiX() * CM_TO_INCH * spc def sizeHint(self, option, index): return self.item_size def render_field(self, db, book_id): is_stars = False try: field = db.pref('field_under_covers_in_grid', 'title') if field == 'size': ans = human_readable( db.field_for(field, book_id, default_value=0)) else: mi = db.get_proxy_metadata(book_id) display_name, ans, val, fm = mi.format_field_extended(field) if fm and fm['datatype'] == 'rating': ans = rating_to_stars( val, fm['display'].get('allow_half_stars', False)) is_stars = True return ('' if ans is None else unicode_type(ans)), is_stars except Exception: if DEBUG: import traceback traceback.print_exc() return '', is_stars def render_emblem(self, book_id, rule, rule_index, cache, mi, db, formatter, template_cache): ans = cache[book_id].get(rule, False) if ans is not False: return ans, mi ans = None if mi is None: mi = db.get_proxy_metadata(book_id) ans = formatter.safe_format(rule, mi, '', mi, column_name='cover_grid%d' % rule_index, template_cache=template_cache) or None cache[book_id][rule] = ans return ans, mi def cached_emblem(self, cache, name, raw_icon=None): ans = cache.get(name, False) if ans is not False: return ans sz = self.emblem_size ans = None if raw_icon is not None: ans = raw_icon.pixmap(sz, sz) elif name == ':ondevice': ans = QIcon(I('ok.png')).pixmap(sz, sz) elif name: pmap = QIcon(os.path.join(config_dir, 'cc_icons', name)).pixmap(sz, sz) if not pmap.isNull(): ans = pmap cache[name] = ans return ans def paint(self, painter, option, index): QStyledItemDelegate.paint( self, painter, option, empty_index) # draw the hover and selection highlights m = index.model() db = m.db try: book_id = db.id(index.row()) except (ValueError, IndexError, KeyError): return if book_id in m.ids_to_highlight_set: painter.save() try: painter.setPen(self.highlight_color) painter.setRenderHint(QPainter.RenderHint.Antialiasing, True) painter.drawRoundedRect(option.rect, 10, 10, Qt.SizeMode.RelativeSize) finally: painter.restore() marked = db.data.get_marked(book_id) db = db.new_api cdata = self.cover_cache[book_id] device_connected = self.parent().gui.device_connected is not None on_device = device_connected and db.field_for('ondevice', book_id) emblem_rules = db.pref('cover_grid_icon_rules', default=()) emblems = [] if self.emblem_size > 0: mi = None for i, (kind, column, rule) in enumerate(emblem_rules): icon_name, mi = self.render_emblem(book_id, rule, i, m.cover_grid_emblem_cache, mi, db, m.formatter, m.cover_grid_template_cache) if icon_name is not None: for one_icon in filter(None, (i.strip() for i in icon_name.split(':'))): pixmap = self.cached_emblem(m.cover_grid_bitmap_cache, one_icon) if pixmap is not None: emblems.append(pixmap) if marked: emblems.insert( 0, self.cached_emblem(m.cover_grid_bitmap_cache, ':marked', m.marked_icon)) if on_device: emblems.insert( 0, self.cached_emblem(m.cover_grid_bitmap_cache, ':ondevice')) painter.save() right_adjust = 0 try: rect = option.rect rect.adjust(self.MARGIN, self.MARGIN, -self.MARGIN, -self.MARGIN) if self.emblem_size > 0: self.paint_emblems(painter, rect, emblems) orect = QRect(rect) trect = QRect(rect) if self.title_height != 0: rect.setBottom(rect.bottom() - self.title_height) trect.setTop(trect.bottom() - self.title_height + 5) if cdata is None or cdata is False: title = db.field_for('title', book_id, default_value='') authors = ' & '.join( db.field_for('authors', book_id, default_value=())) painter.setRenderHint(QPainter.RenderHint.TextAntialiasing, True) painter.drawText( rect, Qt.AlignmentFlag.AlignCenter | Qt.TextFlag.TextWordWrap, '%s\n\n%s' % (title, authors)) if cdata is False: self.render_queue.put(book_id) if self.title_height != 0: self.paint_title(painter, trect, db, book_id) else: if self.animating is not None and self.animating.row( ) == index.row(): cdata = cdata.scaled(cdata.size() * self._animated_size) dpr = cdata.devicePixelRatio() cw, ch = int(cdata.width() / dpr), int(cdata.height() / dpr) dx = max(0, int((rect.width() - cw) / 2.0)) dy = max(0, int((rect.height() - ch) / 2.0)) right_adjust = dx rect.adjust(dx, dy, -dx, -dy) painter.drawPixmap(rect, cdata) if self.title_height != 0: self.paint_title(painter, trect, db, book_id) if self.emblem_size > 0: return # We dont draw embossed emblems as the ondevice/marked emblems are drawn in the gutter if marked: try: p = self.marked_emblem except AttributeError: p = self.marked_emblem = m.marked_icon.pixmap(48, 48) self.paint_embossed_emblem(p, painter, orect, right_adjust) if on_device: try: p = self.on_device_emblem except AttributeError: p = self.on_device_emblem = QIcon(I('ok.png')).pixmap( 48, 48) self.paint_embossed_emblem(p, painter, orect, right_adjust, left=False) finally: painter.restore() def paint_title(self, painter, rect, db, book_id): painter.setRenderHint(QPainter.RenderHint.TextAntialiasing, True) title, is_stars = self.render_field(db, book_id) if is_stars: painter.setFont(self.rating_font) metrics = painter.fontMetrics() painter.setPen(self.highlight_color) painter.drawText( rect, Qt.AlignmentFlag.AlignCenter | Qt.TextFlag.TextSingleLine, metrics.elidedText(title, Qt.TextElideMode.ElideRight, rect.width())) def paint_emblems(self, painter, rect, emblems): gutter = self.emblem_size + self.MARGIN grect = QRect(rect) gpos = self.gutter_position if gpos is self.TOP: grect.setBottom(grect.top() + gutter) rect.setTop(rect.top() + gutter) elif gpos is self.BOTTOM: grect.setTop(grect.bottom() - gutter + self.MARGIN) rect.setBottom(rect.bottom() - gutter) elif gpos is self.LEFT: grect.setRight(grect.left() + gutter) rect.setLeft(rect.left() + gutter) else: grect.setLeft(grect.right() - gutter + self.MARGIN) rect.setRight(rect.right() - gutter) horizontal = gpos in (self.TOP, self.BOTTOM) painter.save() painter.setClipRect(grect) try: for i, emblem in enumerate(emblems): delta = 0 if i == 0 else self.emblem_size + self.MARGIN grect.moveLeft(grect.left() + delta) if horizontal else grect.moveTop( grect.top() + delta) rect = QRect(grect) rect.setWidth(int(emblem.width() / emblem.devicePixelRatio())), rect.setHeight( int(emblem.height() / emblem.devicePixelRatio())) painter.drawPixmap(rect, emblem) finally: painter.restore() def paint_embossed_emblem(self, pixmap, painter, orect, right_adjust, left=True): drect = QRect(orect) pw = int(pixmap.width() / pixmap.devicePixelRatio()) ph = int(pixmap.height() / pixmap.devicePixelRatio()) if left: drect.setLeft(drect.left() + right_adjust) drect.setRight(drect.left() + pw) else: drect.setRight(drect.right() - right_adjust) drect.setLeft(drect.right() - pw + 1) drect.setBottom(drect.bottom() - self.title_height) drect.setTop(drect.bottom() - ph) painter.drawPixmap(drect, pixmap) @pyqtSlot(QHelpEvent, QAbstractItemView, QStyleOptionViewItem, QModelIndex, result=bool) def helpEvent(self, event, view, option, index): if event is not None and view is not None and event.type( ) == QEvent.Type.ToolTip: try: db = index.model().db except AttributeError: return False try: book_id = db.id(index.row()) except (ValueError, IndexError, KeyError): return False db = db.new_api device_connected = self.parent().gui.device_connected on_device = device_connected is not None and db.field_for( 'ondevice', book_id) p = prepare_string_for_xml title = db.field_for('title', book_id) authors = db.field_for('authors', book_id) if title and authors: title = '<b>%s</b>' % ('<br>'.join(wrap(p(title), 120))) authors = '<br>'.join(wrap(p(' & '.join(authors)), 120)) tt = '%s<br><br>%s' % (title, authors) series = db.field_for('series', book_id) if series: use_roman_numbers = config[ 'use_roman_numerals_for_series_number'] val = _( 'Book %(sidx)s of <span class="series_name">%(series)s</span>' ) % dict(sidx=fmt_sidx(db.field_for( 'series_index', book_id), use_roman=use_roman_numbers), series=p(series)) tt += '<br><br>' + val if on_device: val = _('This book is on the device in %s') % on_device tt += '<br><br>' + val QToolTip.showText(event.globalPos(), tt, view) return True return False