class Comments(HTMLDisplay): # {{{ def __init__(self, parent=None): HTMLDisplay.__init__(self, parent) self.setAcceptDrops(False) self.wait_timer = QTimer(self) self.wait_timer.timeout.connect(self.update_wait) self.wait_timer.setInterval(800) self.dots_count = 0 self.anchor_clicked.connect(self.link_activated) def link_activated(self, url): from calibre.gui2 import open_url if url.scheme() in {'http', 'https'}: open_url(url) def show_wait(self): self.dots_count = 0 self.wait_timer.start() self.update_wait() def update_wait(self): self.dots_count += 1 self.dots_count %= 10 self.dots_count = self.dots_count or 1 self.setHtml( '<h2>'+_('Please wait')+ '<br><span id="dots">{}</span></h2>'.format('.' * self.dots_count)) def show_data(self, html): self.wait_timer.stop() def color_to_string(col): ans = '#000000' if col.isValid(): col = col.toRgb() if col.isValid(): ans = str(col.name()) return ans c = color_to_string(QApplication.palette().color(QPalette.ColorGroup.Normal, QPalette.ColorRole.WindowText)) templ = '''\ <html> <head> <style type="text/css"> body, td {background-color: transparent; color: %s } a { text-decoration: none; } div.description { margin-top: 0; padding-top: 0; text-indent: 0 } table { margin-bottom: 0; padding-bottom: 0; } </style> </head> <body> <div class="description"> %%s </div> </body> <html> '''%(c,) self.setHtml(templ%html)
class View(common.View): def __init__(self, scene): common.View.__init__(self, scene) self.setMouseTracking(True) # fix for not updating position for simulated events self.scene.text_changed.connect(self.viewport().update) # ensure a full redraw self.progress_loaded_timer = QTimer() self.progress_loaded_timer.setInterval(1500) self.progress_loaded_timer.setSingleShot(True) self.progress_loaded_timer.timeout.connect(self.viewport().update) def resizeEvent(self, e): common.View.resizeEvent(self, e) if not self.scene.playtest: self.fit() def fit(self): rect = self.scene.itemsBoundingRect().adjusted(-0.3, -0.3, 0.3, 0.3) self.setSceneRect(rect) self.fitInView(rect, qt.KeepAspectRatio) zoom = self.transform().mapRect(QRectF(0, 0, 1, 1)).width() if zoom > 100: self.resetTransform() self.scale(100, 100) def paintEvent(self, e): common.View.paintEvent(self, e) g = QPainter(self.viewport()) g.setRenderHints(self.renderHints()) area = self.viewport().rect().adjusted(5, 2, -5, -2) if self.progress_loaded_timer.isActive(): g.setPen(QPen(Color.dark_text)) g.drawText(area, qt.AlignTop | qt.AlignLeft, "Progress loaded") try: self._info_font except AttributeError: self._info_font = g.font() multiply_font_size(self._info_font, 3) try: txt = ('{r} ({m})' if self.scene.mistakes else '{r}').format(r=self.scene.remaining, m=self.scene.mistakes) g.setFont(self._info_font) g.setPen(QPen(Color.dark_text)) g.drawText(area, qt.AlignTop | qt.AlignRight, txt) except AttributeError: pass def wheelEvent(self, e): pass
class Splitter(QSplitter): state_changed = pyqtSignal(object) reapply_sizes = pyqtSignal(object) def __init__(self, name, label, icon, initial_show=True, initial_side_size=120, connect_button=True, orientation=Qt.Orientation.Horizontal, side_index=0, parent=None, shortcut=None, hide_handle_on_single_panel=True): QSplitter.__init__(self, parent) self.reapply_sizes.connect(self.setSizes, type=Qt.ConnectionType.QueuedConnection) self.hide_handle_on_single_panel = hide_handle_on_single_panel if hide_handle_on_single_panel: self.state_changed.connect(self.update_handle_width) self.original_handle_width = self.handleWidth() self.resize_timer = QTimer(self) self.resize_timer.setSingleShot(True) self.desired_side_size = initial_side_size self.desired_show = initial_show self.resize_timer.setInterval(5) self.resize_timer.timeout.connect(self.do_resize) self.setOrientation(orientation) self.side_index = side_index self._name = name self.label = label self.initial_side_size = initial_side_size self.initial_show = initial_show self.splitterMoved.connect(self.splitter_moved, type=Qt.ConnectionType.QueuedConnection) self.button = LayoutButton(icon, label, self, shortcut=shortcut) if connect_button: self.button.clicked.connect(self.double_clicked) if shortcut is not None: self.action_toggle = QAction(QIcon(icon), _('Toggle') + ' ' + label, self) self.action_toggle.changed.connect(self.update_shortcut) self.action_toggle.triggered.connect(self.toggle_triggered) if parent is not None: parent.addAction(self.action_toggle) if hasattr(parent, 'keyboard'): parent.keyboard.register_shortcut( 'splitter %s %s' % (name, label), str(self.action_toggle.text()), default_keys=(shortcut, ), action=self.action_toggle) else: self.action_toggle.setShortcut(shortcut) else: self.action_toggle.setShortcut(shortcut) def update_shortcut(self): self.button.update_shortcut(self.action_toggle) def toggle_triggered(self, *args): self.toggle_side_pane() def createHandle(self): return SplitterHandle(self.orientation(), self) def initialize(self): for i in range(self.count()): h = self.handle(i) if h is not None: h.splitter_moved() self.state_changed.emit(not self.is_side_index_hidden) def splitter_moved(self, *args): self.desired_side_size = self.side_index_size self.state_changed.emit(not self.is_side_index_hidden) def update_handle_width(self, not_one_panel): self.setHandleWidth(self.original_handle_width if not_one_panel else 0) @property def is_side_index_hidden(self): sizes = list(self.sizes()) try: return sizes[self.side_index] == 0 except IndexError: return True @property def save_name(self): ori = 'horizontal' if self.orientation() == Qt.Orientation.Horizontal \ else 'vertical' return self._name + '_' + ori def print_sizes(self): if self.count() > 1: print(self.save_name, 'side:', self.side_index_size, 'other:', end=' ') print(list(self.sizes())[self.other_index]) @property def side_index_size(self): if self.count() < 2: return 0 return self.sizes()[self.side_index] @side_index_size.setter def side_index_size(self, val): if self.count() < 2: return side_index_hidden = self.is_side_index_hidden if val == 0 and not side_index_hidden: self.save_state() sizes = list(self.sizes()) for i in range(len(sizes)): sizes[i] = val if i == self.side_index else 10 self.setSizes(sizes) sizes = list(self.sizes()) total = sum(sizes) total_needs_adjustment = self.hide_handle_on_single_panel and side_index_hidden if total_needs_adjustment: total -= self.original_handle_width for i in range(len(sizes)): sizes[i] = val if i == self.side_index else total - val self.setSizes(sizes) self.initialize() if total_needs_adjustment: # the handle visibility and therefore size distribution will change # when the event loop ticks self.reapply_sizes.emit(sizes) def do_resize(self, *args): orig = self.desired_side_size QSplitter.resizeEvent(self, self._resize_ev) if orig > 20 and self.desired_show: c = 0 while abs(self.side_index_size - orig) > 10 and c < 5: self.apply_state(self.get_state(), save_desired=False) c += 1 def resizeEvent(self, ev): if self.resize_timer.isActive(): self.resize_timer.stop() self._resize_ev = ev self.resize_timer.start() def get_state(self): if self.count() < 2: return (False, 200) return (self.desired_show, self.desired_side_size) def apply_state(self, state, save_desired=True): if state[0]: self.side_index_size = state[1] if save_desired: self.desired_side_size = self.side_index_size else: self.side_index_size = 0 self.desired_show = state[0] def default_state(self): return (self.initial_show, self.initial_side_size) # Public API {{{ def update_desired_state(self): self.desired_show = not self.is_side_index_hidden def save_state(self): if self.count() > 1: gprefs[self.save_name + '_state'] = self.get_state() @property def other_index(self): return (self.side_index + 1) % 2 def restore_state(self): if self.count() > 1: state = gprefs.get(self.save_name + '_state', self.default_state()) self.apply_state(state, save_desired=False) self.desired_side_size = state[1] def toggle_side_pane(self, hide=None): if hide is None: action = 'show' if self.is_side_index_hidden else 'hide' else: action = 'hide' if hide else 'show' getattr(self, action + '_side_pane')() def show_side_pane(self): if self.count() < 2 or not self.is_side_index_hidden: return if self.desired_side_size == 0: self.desired_side_size = self.initial_side_size self.apply_state((True, self.desired_side_size)) def hide_side_pane(self): if self.count() < 2 or self.is_side_index_hidden: return self.apply_state((False, self.desired_side_size)) def double_clicked(self, *args): self.toggle_side_pane()
class GridView(QListView): update_item = pyqtSignal(object) files_dropped = pyqtSignal(object) books_dropped = pyqtSignal(object) def __init__(self, parent): QListView.__init__(self, parent) self._ncols = None self.gesture_manager = GestureManager(self) setup_dnd_interface(self) self.setUniformItemSizes(True) self.setWrapping(True) self.setFlow(QListView.Flow.LeftToRight) # We cannot set layout mode to batched, because that breaks # restore_vpos() # self.setLayoutMode(QListView.ResizeMode.Batched) self.setResizeMode(QListView.ResizeMode.Adjust) self.setSelectionMode( QAbstractItemView.SelectionMode.ExtendedSelection) self.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel) self.delegate = CoverDelegate(self) self.delegate.animation.valueChanged.connect( self.animation_value_changed) self.delegate.animation.finished.connect(self.animation_done) self.setItemDelegate(self.delegate) self.setSpacing(self.delegate.spacing) self.set_color() self.ignore_render_requests = Event() dpr = self.device_pixel_ratio self.thumbnail_cache = ThumbnailCache( max_size=gprefs['cover_grid_disk_cache_size'], thumbnail_size=(int(dpr * self.delegate.cover_size.width()), int(dpr * self.delegate.cover_size.height()))) self.render_thread = None self.update_item.connect(self.re_render, type=Qt.ConnectionType.QueuedConnection) self.doubleClicked.connect(self.double_clicked) self.setCursor(Qt.CursorShape.PointingHandCursor) self.gui = parent self.context_menu = None self.update_timer = QTimer(self) self.update_timer.setInterval(200) self.update_timer.timeout.connect(self.update_viewport) self.update_timer.setSingleShot(True) self.resize_timer = t = QTimer(self) t.setInterval(200), t.setSingleShot(True) t.timeout.connect(self.update_memory_cover_cache_size) def viewportEvent(self, ev): try: ret = self.gesture_manager.handle_event(ev) except AttributeError: ret = None if ret is not None: return ret return QListView.viewportEvent(self, ev) @property def device_pixel_ratio(self): try: return self.devicePixelRatioF() except AttributeError: return self.devicePixelRatio() @property def first_visible_row(self): geom = self.viewport().geometry() for y in range(geom.top(), (self.spacing() * 2) + geom.top(), 5): for x in range(geom.left(), (self.spacing() * 2) + geom.left(), 5): ans = self.indexAt(QPoint(x, y)).row() if ans > -1: return ans @property def last_visible_row(self): geom = self.viewport().geometry() for y in range(geom.bottom(), geom.bottom() - 2 * self.spacing(), -5): for x in range(geom.left(), (self.spacing() * 2) + geom.left(), 5): ans = self.indexAt(QPoint(x, y)).row() if ans > -1: item_width = self.delegate.item_size.width( ) + 2 * self.spacing() return ans + (geom.width() // item_width) def update_viewport(self): self.ignore_render_requests.clear() self.update_timer.stop() m = self.model() for r in range(self.first_visible_row or 0, self.last_visible_row or (m.count() - 1)): self.update(m.index(r, 0)) def start_view_animation(self, index): d = self.delegate if d.animating is None and not config['disable_animations']: d.animating = index d.animation.start() def double_clicked(self, index): self.start_view_animation(index) if tweaks['doubleclick_on_library_view'] == 'open_viewer': self.gui.iactions['View'].view_triggered(index) elif tweaks['doubleclick_on_library_view'] in { 'edit_metadata', 'edit_cell' }: self.gui.iactions['Edit Metadata'].edit_metadata(False, False) def animation_value_changed(self, value): if self.delegate.animating is not None: self.update(self.delegate.animating) def animation_done(self): if self.delegate.animating is not None: idx = self.delegate.animating self.delegate.animating = None self.update(idx) def set_color(self): r, g, b = gprefs['cover_grid_color'] tex = gprefs['cover_grid_texture'] pal = self.palette() pal.setColor(QPalette.ColorRole.Base, QColor(r, g, b)) self.setPalette(pal) ss = '' if tex: from calibre.gui2.preferences.texture_chooser import texture_path path = texture_path(tex) if path: path = os.path.abspath(path).replace(os.sep, '/') ss += 'background-image: url({});'.format(path) ss += 'background-attachment: fixed;' pm = QPixmap(path) if not pm.isNull(): val = pm.scaled(1, 1).toImage().pixel(0, 0) r, g, b = qRed(val), qGreen(val), qBlue(val) dark = max(r, g, b) < 115 col = '#eee' if dark else '#111' ss += 'color: {};'.format(col) self.delegate.highlight_color = QColor(col) self.setStyleSheet('QListView {{ {} }}'.format(ss)) def refresh_settings(self): size_changed = ( gprefs['cover_grid_width'] != self.delegate.original_width or gprefs['cover_grid_height'] != self.delegate.original_height) if (size_changed or gprefs['cover_grid_show_title'] != self.delegate.original_show_title or gprefs['show_emblems'] != self.delegate.original_show_emblems or gprefs['emblem_size'] != self.delegate.orginal_emblem_size or gprefs['emblem_position'] != self.delegate.orginal_emblem_position): self.delegate.set_dimensions() self.setSpacing(self.delegate.spacing) if size_changed: self.delegate.cover_cache.clear() if gprefs['cover_grid_spacing'] != self.delegate.original_spacing: self.delegate.calculate_spacing() self.setSpacing(self.delegate.spacing) self.set_color() self.set_thumbnail_cache_image_size() cs = gprefs['cover_grid_disk_cache_size'] if (cs * (1024**2)) != self.thumbnail_cache.max_size: self.thumbnail_cache.set_size(cs) self.update_memory_cover_cache_size() def set_thumbnail_cache_image_size(self): dpr = self.device_pixel_ratio self.thumbnail_cache.set_thumbnail_size( int(dpr * self.delegate.cover_size.width()), int(dpr * self.delegate.cover_size.height())) def resizeEvent(self, ev): self._ncols = None self.resize_timer.start() return QListView.resizeEvent(self, ev) def update_memory_cover_cache_size(self): try: sz = self.delegate.item_size except AttributeError: return rows, cols = self.width() // sz.width(), self.height() // sz.height() num = (rows + 1) * (cols + 1) limit = max(100, num * max(2, gprefs['cover_grid_cache_size_multiple'])) if limit != self.delegate.cover_cache.limit: self.delegate.cover_cache.set_limit(limit) def shown(self): self.update_memory_cover_cache_size() if self.render_thread is None: self.thumbnail_cache.set_database(self.gui.current_db) self.render_thread = Thread(target=self.render_covers) self.render_thread.daemon = True self.render_thread.start() def render_covers(self): q = self.delegate.render_queue while True: book_id = q.get() try: if book_id is None: return if self.ignore_render_requests.is_set(): continue try: self.render_cover(book_id) except: import traceback traceback.print_exc() finally: q.task_done() def render_cover(self, book_id): if self.ignore_render_requests.is_set(): return dpr = self.device_pixel_ratio page_width = int(dpr * self.delegate.cover_size.width()) page_height = int(dpr * self.delegate.cover_size.height()) tcdata, timestamp = self.thumbnail_cache[book_id] use_cache = False if timestamp is None: # Not in cache has_cover, cdata, timestamp = self.model( ).db.new_api.cover_or_cache(book_id, 0) else: has_cover, cdata, timestamp = self.model( ).db.new_api.cover_or_cache(book_id, timestamp) if has_cover and cdata is None: # The cached cover is fresh cdata = tcdata use_cache = True if has_cover: p = QImage() p.loadFromData(cdata, CACHE_FORMAT if cdata is tcdata else 'JPEG') p.setDevicePixelRatio(dpr) if p.isNull() and cdata is tcdata: # Invalid image in cache self.thumbnail_cache.invalidate((book_id, )) self.update_item.emit(book_id) return cdata = None if p.isNull() else p if not use_cache: # cache is stale if cdata is not None: width, height = p.width(), p.height() scaled, nwidth, nheight = fit_image( width, height, page_width, page_height) if scaled: if self.ignore_render_requests.is_set(): return p = p.scaled( nwidth, nheight, Qt.AspectRatioMode.IgnoreAspectRatio, Qt.TransformationMode.SmoothTransformation) p.setDevicePixelRatio(dpr) cdata = p # update cache if cdata is None: self.thumbnail_cache.invalidate((book_id, )) else: try: self.thumbnail_cache.insert(book_id, timestamp, image_to_data(cdata)) except EncodeError as err: self.thumbnail_cache.invalidate((book_id, )) prints(err) except Exception: import traceback traceback.print_exc() elif tcdata is not None: # Cover was removed, but it exists in cache, remove from cache self.thumbnail_cache.invalidate((book_id, )) self.delegate.cover_cache.set(book_id, cdata) self.update_item.emit(book_id) def re_render(self, book_id): self.delegate.cover_cache.clear_staging() m = self.model() try: index = m.db.row(book_id) except (IndexError, ValueError, KeyError): return self.update(m.index(index, 0)) def shutdown(self): self.ignore_render_requests.set() self.delegate.render_queue.put(None) self.thumbnail_cache.shutdown() def set_database(self, newdb, stage=0): if stage == 0: self.ignore_render_requests.set() try: for x in (self.delegate.cover_cache, self.thumbnail_cache): self.model().db.new_api.remove_cover_cache(x) except AttributeError: pass # db is None for x in (self.delegate.cover_cache, self.thumbnail_cache): newdb.new_api.add_cover_cache(x) try: # Use a timeout so that if, for some reason, the render thread # gets stuck, we dont deadlock, future covers wont get # rendered, but this is better than a deadlock join_with_timeout(self.delegate.render_queue) except RuntimeError: print('Cover rendering thread is stuck!') finally: self.ignore_render_requests.clear() else: self.delegate.cover_cache.clear() def select_rows(self, rows): sel = QItemSelection() sm = self.selectionModel() m = self.model() # Create a range based selector for each set of contiguous rows # as supplying selectors for each individual row causes very poor # performance if a large number of rows has to be selected. for k, g in itertools.groupby(enumerate(rows), lambda i_x: i_x[0] - i_x[1]): group = list(map(operator.itemgetter(1), g)) sel.merge( QItemSelection(m.index(min(group), 0), m.index(max(group), 0)), QItemSelectionModel.SelectionFlag.Select) sm.select(sel, QItemSelectionModel.SelectionFlag.ClearAndSelect) def selectAll(self): # We re-implement this to ensure that only indexes from column 0 are # selected. The base class implementation selects all columns. This # causes problems with selection syncing, see # https://bugs.launchpad.net/bugs/1236348 m = self.model() sm = self.selectionModel() sel = QItemSelection(m.index(0, 0), m.index(m.rowCount(QModelIndex()) - 1, 0)) sm.select(sel, QItemSelectionModel.SelectionFlag.ClearAndSelect) def set_current_row(self, row): sm = self.selectionModel() sm.setCurrentIndex(self.model().index(row, 0), QItemSelectionModel.SelectionFlag.NoUpdate) def set_context_menu(self, menu): self.context_menu = menu def contextMenuEvent(self, event): if self.context_menu is None: return from calibre.gui2.main_window import clone_menu m = clone_menu(self.context_menu) if islinux else self.context_menu m.popup(event.globalPos()) event.accept() def get_selected_ids(self): m = self.model() return [m.id(i) for i in self.selectionModel().selectedIndexes()] def restore_vpos(self, vpos): self.verticalScrollBar().setValue(vpos) def restore_hpos(self, hpos): pass def handle_mouse_press_event(self, ev): if QApplication.keyboardModifiers( ) & Qt.KeyboardModifier.ShiftModifier: # Shift-Click in QListView is broken. It selects extra items in # various circumstances, for example, click on some item in the # middle of a row then click on an item in the next row, all items # in the first row will be selected instead of only items after the # middle item. index = self.indexAt(ev.pos()) if not index.isValid(): return ci = self.currentIndex() sm = self.selectionModel() sm.setCurrentIndex(index, QItemSelectionModel.SelectionFlag.NoUpdate) if not ci.isValid(): return if not sm.hasSelection(): sm.select(index, QItemSelectionModel.SelectionFlag.ClearAndSelect) return cr = ci.row() tgt = index.row() top = self.model().index(min(cr, tgt), 0) bottom = self.model().index(max(cr, tgt), 0) sm.select(QItemSelection(top, bottom), QItemSelectionModel.SelectionFlag.Select) else: return QListView.mousePressEvent(self, ev) def indices_for_merge(self, resolved=True): return self.selectionModel().selectedIndexes() def number_of_columns(self): # Number of columns currently visible in the grid if self._ncols is None: dpr = self.device_pixel_ratio width = int(dpr * self.delegate.cover_size.width()) height = int(dpr * self.delegate.cover_size.height()) step = max(10, self.spacing()) for y in range(step, 2 * height, step): for x in range(step, 2 * width, step): i = self.indexAt(QPoint(x, y)) if i.isValid(): for x in range(self.viewport().width() - step, self.viewport().width() - width, -step): j = self.indexAt(QPoint(x, y)) if j.isValid(): self._ncols = j.row() - i.row() + 1 return self._ncols return self._ncols def keyPressEvent(self, ev): if handle_enter_press(self, ev, self.start_view_animation, False): return k = ev.key() if ev.modifiers() & Qt.KeyboardModifier.ShiftModifier and k in ( Qt.Key.Key_Left, Qt.Key.Key_Right, Qt.Key.Key_Up, Qt.Key.Key_Down): ci = self.currentIndex() if not ci.isValid(): return c = ci.row() ncols = self.number_of_columns() or 1 delta = { Qt.Key.Key_Left: -1, Qt.Key.Key_Right: 1, Qt.Key.Key_Up: -ncols, Qt.Key.Key_Down: ncols }[k] n = max(0, min(c + delta, self.model().rowCount(None) - 1)) if n == c: return sm = self.selectionModel() rows = {i.row() for i in sm.selectedIndexes()} if rows: mi, ma = min(rows), max(rows) end = mi if c == ma else ma if c == mi else c else: end = c top = self.model().index(min(n, end), 0) bottom = self.model().index(max(n, end), 0) sm.select(QItemSelection(top, bottom), QItemSelectionModel.SelectionFlag.ClearAndSelect) sm.setCurrentIndex(self.model().index(n, 0), QItemSelectionModel.SelectionFlag.NoUpdate) else: return QListView.keyPressEvent(self, ev) @property def current_book(self): ci = self.currentIndex() if ci.isValid(): try: return self.model().db.data.index_to_id(ci.row()) except (IndexError, ValueError, KeyError, TypeError, AttributeError): pass def current_book_state(self): return self.current_book def restore_current_book_state(self, state): book_id = state self.setFocus(Qt.FocusReason.OtherFocusReason) try: row = self.model().db.data.id_to_index(book_id) except (IndexError, ValueError, KeyError, TypeError, AttributeError): return self.set_current_row(row) self.select_rows((row, )) self.scrollTo(self.model().index(row, 0), QAbstractItemView.ScrollHint.PositionAtCenter) def marked_changed(self, old_marked, current_marked): changed = old_marked | current_marked m = self.model() for book_id in changed: try: self.update(m.index(m.db.data.id_to_index(book_id), 0)) except ValueError: pass def moveCursor(self, action, modifiers): index = QListView.moveCursor(self, action, modifiers) if action in ( QAbstractItemView.CursorAction.MoveLeft, QAbstractItemView.CursorAction.MoveRight) and index.isValid(): ci = self.currentIndex() if ci.isValid() and index.row() == ci.row(): nr = index.row() + (1 if action == QAbstractItemView. CursorAction.MoveRight else -1) if 0 <= nr < self.model().rowCount(QModelIndex()): index = self.model().index(nr, 0) return index def selectionCommand(self, index, event): if event and event.type() == QEvent.Type.KeyPress and event.key() in ( Qt.Key.Key_Home, Qt.Key.Key_End) and event.modifiers() & Qt.Modifier.CTRL: return QItemSelectionModel.SelectionFlag.ClearAndSelect | QItemSelectionModel.SelectionFlag.Rows return super(GridView, self).selectionCommand(index, event) def wheelEvent(self, ev): if ev.phase() not in (Qt.ScrollPhase.ScrollUpdate, 0, Qt.ScrollPhase.ScrollMomentum): return number_of_pixels = ev.pixelDelta() number_of_degrees = ev.angleDelta() / 8.0 b = self.verticalScrollBar() if number_of_pixels.isNull() or islinux: # pixelDelta() is broken on linux with wheel mice dy = number_of_degrees.y() / 15.0 # Scroll by approximately half a row dy = int(math.ceil((dy) * b.singleStep() / 2.0)) else: dy = number_of_pixels.y() if abs(dy) > 0: b.setValue(b.value() - dy) def paintEvent(self, ev): dpr = self.device_pixel_ratio page_width = int(dpr * self.delegate.cover_size.width()) page_height = int(dpr * self.delegate.cover_size.height()) size_changed = self.thumbnail_cache.set_thumbnail_size( page_width, page_height) if size_changed: self.delegate.cover_cache.clear() return super(GridView, self).paintEvent(ev)
class LiveCSS(QWidget): goto_declaration = pyqtSignal(object) def __init__(self, preview, parent=None): QWidget.__init__(self, parent) self.preview = preview preview.live_css_data.connect(self.got_live_css_data) self.preview_is_refreshing = False self.refresh_needed = False preview.refresh_starting.connect(self.preview_refresh_starting) preview.refreshed.connect(self.preview_refreshed) self.apply_theme() self.setAutoFillBackground(True) self.update_timer = QTimer(self) self.update_timer.timeout.connect(self.update_data) self.update_timer.setSingleShot(True) self.update_timer.setInterval(500) self.now_showing = (None, None, None) self.stack = s = QStackedLayout(self) self.setLayout(s) self.clear_label = la = QLabel( '<h3>' + _('No style information found') + '</h3><p>' + _('Move the cursor inside a HTML tag to see what styles' ' apply to that tag.')) la.setWordWrap(True) la.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft) s.addWidget(la) self.box = box = Box(self) box.hyperlink_activated.connect( self.goto_declaration, type=Qt.ConnectionType.QueuedConnection) self.scroll = sc = QScrollArea(self) sc.setWidget(box) sc.setWidgetResizable(True) s.addWidget(sc) def preview_refresh_starting(self): self.preview_is_refreshing = True def preview_refreshed(self): self.preview_is_refreshing = False self.refresh_needed = True self.start_update_timer() def apply_theme(self): f = self.font() f.setFamily(tprefs['editor_font_family'] or default_font_family()) f.setPointSizeF(tprefs['editor_font_size']) self.setFont(f) theme = get_theme(tprefs['editor_theme']) pal = self.palette() pal.setColor(QPalette.ColorRole.Window, theme_color(theme, 'Normal', 'bg')) pal.setColor(QPalette.ColorRole.WindowText, theme_color(theme, 'Normal', 'fg')) pal.setColor(QPalette.ColorRole.AlternateBase, theme_color(theme, 'HighlightRegion', 'bg')) pal.setColor(QPalette.ColorRole.Link, theme_color(theme, 'Link', 'fg')) pal.setColor(QPalette.ColorRole.LinkVisited, theme_color(theme, 'Keyword', 'fg')) self.setPalette(pal) if hasattr(self, 'box'): self.box.relayout() self.update() def clear(self): self.stack.setCurrentIndex(0) def show_data(self, editor_name, sourceline, tags): if self.preview_is_refreshing: return if sourceline is None: self.clear() else: self.preview.request_live_css_data(editor_name, sourceline, tags) def got_live_css_data(self, result): maximum_specificities = {} for node in result['nodes']: for rule in node['css']: self.process_rule(rule, node['ancestor_specificity'], maximum_specificities) for node in result['nodes']: for rule in node['css']: for prop in rule['properties']: if prop.specificity < maximum_specificities[prop.name]: prop.is_overriden = True self.display_received_live_css_data(result) def display_received_live_css_data(self, data): editor_name = data['editor_name'] sourceline = data['sourceline'] tags = data['tags'] if data is None or len(data['computed_css']) < 1: if editor_name == self.current_name and (editor_name, sourceline, tags) == self.now_showing: # Try again in a little while in case there was a transient # error in the web view self.start_update_timer() return self.clear() return self.now_showing = (editor_name, sourceline, tags) data['html_name'] = editor_name self.box.show_data(data) self.refresh_needed = False self.stack.setCurrentIndex(1) def process_rule(self, rule, ancestor_specificity, maximum_specificities): selector = rule['selector'] sheet_index = rule['sheet_index'] rule_address = rule['rule_address'] or () if selector is not None: try: specificity = [0] + list(parse(selector)[0].specificity()) except (AttributeError, TypeError, SelectorError): specificity = [0, 0, 0, 0] else: # style attribute specificity = [1, 0, 0, 0] specificity.extend((sheet_index, tuple(rule_address))) properties = [] for prop in rule['properties']: important = 1 if prop[-1] == 'important' else 0 p = Property(prop, [ancestor_specificity] + [important] + specificity) properties.append(p) if p.specificity > maximum_specificities.get( p.name, lowest_specificity): maximum_specificities[p.name] = p.specificity rule['properties'] = properties href = rule['href'] if hasattr(href, 'startswith') and href.startswith( f'{FAKE_PROTOCOL}://{FAKE_HOST}'): qurl = QUrl(href) name = qurl.path()[1:] if name: rule['href'] = name @property def current_name(self): return self.preview.current_name @property def is_visible(self): return self.isVisible() def showEvent(self, ev): self.update_timer.start() actions['auto-reload-preview'].setEnabled(True) return QWidget.showEvent(self, ev) def sync_to_editor(self): self.update_data() def update_data(self): if not self.is_visible or self.preview_is_refreshing: return editor_name = self.current_name ed = editors.get(editor_name, None) if self.update_timer.isActive() or (ed is None and editor_name is not None): return QTimer.singleShot(100, self.update_data) if ed is not None: sourceline, tags = ed.current_tag(for_position_sync=False) if self.refresh_needed or self.now_showing != (editor_name, sourceline, tags): self.show_data(editor_name, sourceline, tags) def start_update_timer(self): if self.is_visible: self.update_timer.start() def stop_update_timer(self): self.update_timer.stop() def navigate_to_declaration(self, data, editor): if data['type'] == 'inline': sourceline, tags = data['sourceline_address'] editor.goto_sourceline(sourceline, tags, attribute='style') elif data['type'] == 'sheet': editor.goto_css_rule(data['rule_address']) elif data['type'] == 'elem': editor.goto_css_rule(data['rule_address'], sourceline_address=data['sourceline_address'])