Esempio n. 1
0
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(self.LeftToRight)
        # We cannot set layout mode to batched, because that breaks
        # restore_vpos()
        # self.setLayoutMode(self.Batched)
        self.setResizeMode(self.Adjust)
        self.setSelectionMode(self.ExtendedSelection)
        self.setVerticalScrollMode(self.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.QueuedConnection)
        self.doubleClicked.connect(self.double_clicked)
        self.setCursor(Qt.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']
        pal = QPalette()
        col = QColor(r, g, b)
        pal.setColor(pal.Base, col)
        tex = gprefs['cover_grid_texture']
        if tex:
            from calibre.gui2.preferences.texture_chooser import texture_path
            path = texture_path(tex)
            if path:
                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)
                    pal.setBrush(pal.Base, QBrush(pm))
        dark = (r + g + b)/3.0 < 128
        pal.setColor(pal.Text, QColor(Qt.white if dark else Qt.black))
        self.setPalette(pal)
        self.delegate.highlight_color = pal.color(pal.Text)

    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.IgnoreAspectRatio, Qt.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)), sm.Select)
        sm.select(sel, sm.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, sm.ClearAndSelect)

    def set_current_row(self, row):
        sm = self.selectionModel()
        sm.setCurrentIndex(self.model().index(row, 0), sm.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.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, sm.NoUpdate)
            if not ci.isValid():
                return
            if not sm.hasSelection():
                sm.select(index, sm.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), sm.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:
            step = max(10, self.spacing())
            for y in range(step, 500, step):
                for x in range(step, 500, step):
                    i = self.indexAt(QPoint(x, y))
                    if i.isValid():
                        for x in range(self.viewport().width() - step, self.viewport().width() - 300, -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.ShiftModifier and k in (Qt.Key_Left, Qt.Key_Right, Qt.Key_Up, Qt.Key_Down):
            ci = self.currentIndex()
            if not ci.isValid():
                return
            c = ci.row()
            delta = {Qt.Key_Left: -1, Qt.Key_Right: 1, Qt.Key_Up: -self.number_of_columns(), Qt.Key_Down: self.number_of_columns()}[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), sm.ClearAndSelect)
            sm.setCurrentIndex(self.model().index(n, 0), sm.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.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), self.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 (QListView.MoveLeft, QListView.MoveRight) and index.isValid():
            ci = self.currentIndex()
            if ci.isValid() and index.row() == ci.row():
                nr = index.row() + (1 if action == QListView.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() == event.KeyPress and event.key() in (Qt.Key_Home, Qt.Key_End) and event.modifiers() & Qt.CTRL:
            return QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows
        return super(GridView, self).selectionCommand(index, event)

    def wheelEvent(self, ev):
        if ev.phase() not in (Qt.ScrollUpdate, 0):
            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)
Esempio n. 2
0
class Results(QTreeWidget):  # {{{

    show_search_result = pyqtSignal(object)
    current_result_changed = pyqtSignal(object)
    count_changed = pyqtSignal(object)

    def __init__(self, parent=None):
        QTreeWidget.__init__(self, parent)
        self.setHeaderHidden(True)
        self.setFocusPolicy(Qt.FocusPolicy.NoFocus)
        self.delegate = ResultsDelegate(self)
        self.setItemDelegate(self.delegate)
        self.itemClicked.connect(self.item_activated)
        self.blank_icon = QIcon(I('blank.png'))
        self.not_found_icon = QIcon(I('dialog_warning.png'))
        self.currentItemChanged.connect(self.current_item_changed)
        self.section_font = QFont(self.font())
        self.section_font.setItalic(True)
        self.section_map = {}
        self.search_results = []
        self.item_map = {}
        self.gesture_manager = GestureManager(self)
        self.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)

    def viewportEvent(self, ev):
        try:
            ret = self.gesture_manager.handle_event(ev)
        except AttributeError:
            ret = None
        if ret is not None:
            return ret
        return super().viewportEvent(ev)

    def current_item_changed(self, current, previous):
        if current is not None:
            r = current.data(0, SEARCH_RESULT_ROLE)
            if isinstance(r, SearchResult):
                self.current_result_changed.emit(r)
        else:
            self.current_result_changed.emit(None)

    def add_result(self, result):
        section_title = _('Unknown')
        section_id = -1
        toc_nodes = getattr(result, 'toc_nodes', ()) or ()
        if toc_nodes:
            section_title = toc_nodes[-1].get('title') or _('Unknown')
            section_id = toc_nodes[-1].get('id')
            if section_id is None:
                section_id = -1
        section_key = section_id
        section = self.section_map.get(section_key)
        spine_idx = getattr(result, 'spine_idx', -1)
        if section is None:
            section = QTreeWidgetItem([section_title], 1)
            section.setFlags(Qt.ItemFlag.ItemIsEnabled)
            section.setFont(0, self.section_font)
            section.setData(0, SPINE_IDX_ROLE, spine_idx)
            lines = []
            for i, node in enumerate(toc_nodes):
                lines.append('\xa0\xa0' * i + '➤ ' +
                             (node.get('title') or _('Unknown')))
            if lines:
                tt = ngettext('Table of Contents section:',
                              'Table of Contents sections:', len(lines))
                tt += '\n' + '\n'.join(lines)
                section.setToolTip(0, tt)
            self.section_map[section_key] = section
            for s in range(self.topLevelItemCount()):
                ti = self.topLevelItem(s)
                if ti.data(0, SPINE_IDX_ROLE) > spine_idx:
                    self.insertTopLevelItem(s, section)
                    break
            else:
                self.addTopLevelItem(section)
            section.setExpanded(True)
        item = QTreeWidgetItem(section, [' '], 2)
        item.setFlags(Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled
                      | Qt.ItemFlag.ItemNeverHasChildren)
        item.setData(0, SEARCH_RESULT_ROLE, result)
        item.setData(0, RESULT_NUMBER_ROLE, len(self.search_results))
        item.setData(0, SPINE_IDX_ROLE, spine_idx)
        if isinstance(result, SearchResult):
            tt = '<p>…' + escape(result.before, False) + '<b>' + escape(
                result.text, False) + '</b>' + escape(result.after,
                                                      False) + '…'
            item.setData(0, Qt.ItemDataRole.ToolTipRole, tt)
        item.setIcon(0, self.blank_icon)
        self.item_map[len(self.search_results)] = item
        self.search_results.append(result)
        n = self.number_of_results
        self.count_changed.emit(n)

    def item_activated(self):
        i = self.currentItem()
        if i:
            sr = i.data(0, SEARCH_RESULT_ROLE)
            if isinstance(sr, SearchResult):
                if not sr.is_hidden:
                    self.show_search_result.emit(sr)

    def find_next(self, previous):
        if self.number_of_results < 1:
            return
        item = self.currentItem()
        if item is None:
            return
        i = int(item.data(0, RESULT_NUMBER_ROLE))
        i += -1 if previous else 1
        i %= self.number_of_results
        self.setCurrentItem(self.item_map[i])
        self.item_activated()

    def search_result_not_found(self, sr):
        for i in range(self.number_of_results):
            item = self.item_map[i]
            r = item.data(0, SEARCH_RESULT_ROLE)
            if r.is_result(sr):
                r.is_hidden = True
                item.setIcon(0, self.not_found_icon)
                break

    def search_result_discovered(self, sr):
        q = sr['result_num']
        for i in range(self.number_of_results):
            item = self.item_map[i]
            r = item.data(0, SEARCH_RESULT_ROLE)
            if r.result_num == q:
                self.setCurrentItem(item)

    @property
    def current_result_is_hidden(self):
        item = self.currentItem()
        if item is not None:
            sr = item.data(0, SEARCH_RESULT_ROLE)
            if isinstance(sr, SearchResult) and sr.is_hidden:
                return True
        return False

    @property
    def number_of_results(self):
        return len(self.search_results)

    def clear_all_results(self):
        self.section_map = {}
        self.item_map = {}
        self.search_results = []
        self.clear()
        self.count_changed.emit(-1)

    def select_first_result(self):
        if self.number_of_results:
            item = self.item_map[0]
            self.setCurrentItem(item)

    def ensure_current_result_visible(self):
        item = self.currentItem()
        if item is not None:
            self.scrollToItem(item)
Esempio n. 3
0
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(self.LeftToRight)
        # We cannot set layout mode to batched, because that breaks
        # restore_vpos()
        # self.setLayoutMode(self.Batched)
        self.setResizeMode(self.Adjust)
        self.setSelectionMode(self.ExtendedSelection)
        self.setVerticalScrollMode(self.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.QueuedConnection)
        self.doubleClicked.connect(self.double_clicked)
        self.setCursor(Qt.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(pal.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
        ss += 'color: {};'.format('white' if dark else 'black')
        self.delegate.highlight_color = QColor(Qt.white if dark else Qt.black)
        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.IgnoreAspectRatio, Qt.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)), sm.Select)
        sm.select(sel, sm.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, sm.ClearAndSelect)

    def set_current_row(self, row):
        sm = self.selectionModel()
        sm.setCurrentIndex(self.model().index(row, 0), sm.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.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, sm.NoUpdate)
            if not ci.isValid():
                return
            if not sm.hasSelection():
                sm.select(index, sm.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), sm.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.ShiftModifier and k in (Qt.Key_Left, Qt.Key_Right, Qt.Key_Up, Qt.Key_Down):
            ci = self.currentIndex()
            if not ci.isValid():
                return
            c = ci.row()
            ncols = self.number_of_columns() or 1
            delta = {Qt.Key_Left: -1, Qt.Key_Right: 1, Qt.Key_Up: -ncols, Qt.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), sm.ClearAndSelect)
            sm.setCurrentIndex(self.model().index(n, 0), sm.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.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), self.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 (QListView.MoveLeft, QListView.MoveRight) and index.isValid():
            ci = self.currentIndex()
            if ci.isValid() and index.row() == ci.row():
                nr = index.row() + (1 if action == QListView.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() == event.KeyPress and event.key() in (Qt.Key_Home, Qt.Key_End) and event.modifiers() & Qt.CTRL:
            return QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows
        return super(GridView, self).selectionCommand(index, event)

    def wheelEvent(self, ev):
        if ev.phase() not in (Qt.ScrollUpdate, 0):
            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)
Esempio n. 4
0
class GridView(QListView):

    update_item = pyqtSignal(object)
    files_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(self.LeftToRight)
        # We cannot set layout mode to batched, because that breaks
        # restore_vpos()
        # self.setLayoutMode(self.Batched)
        self.setResizeMode(self.Adjust)
        self.setSelectionMode(self.ExtendedSelection)
        self.setVerticalScrollMode(self.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.QueuedConnection)
        self.doubleClicked.connect(self.double_clicked)
        self.setCursor(Qt.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 xrange(geom.top(), (self.spacing() * 2) + geom.top(), 5):
            for x in xrange(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 xrange(geom.bottom(), geom.bottom() - 2 * self.spacing(), -5):
            for x in xrange(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 xrange(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']
        pal = QPalette()
        col = QColor(r, g, b)
        pal.setColor(pal.Base, col)
        tex = gprefs['cover_grid_texture']
        if tex:
            from calibre.gui2.preferences.texture_chooser import texture_path
            path = texture_path(tex)
            if path:
                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)
                    pal.setBrush(pal.Base, QBrush(pm))
        dark = (r + g + b) / 3.0 < 128
        pal.setColor(pal.Text, QColor(Qt.white if dark else Qt.black))
        self.setPalette(pal)
        self.delegate.highlight_color = pal.color(pal.Text)

    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()
        if size_changed:
            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()))
        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 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
        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')
            dpr = self.device_pixel_ratio
            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,
                        int(dpr * self.delegate.cover_size.width()),
                        int(dpr * self.delegate.cover_size.height()))
                    if scaled:
                        if self.ignore_render_requests.is_set():
                            return
                        p = p.scaled(nwidth, nheight, Qt.IgnoreAspectRatio,
                                     Qt.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):
            group = list(map(operator.itemgetter(1), g))
            sel.merge(
                QItemSelection(m.index(min(group), 0), m.index(max(group), 0)),
                sm.Select)
        sm.select(sel, sm.ClearAndSelect)
Esempio n. 5
0
class TOCView(QTreeView):

    searched = pyqtSignal(object)

    def __init__(self, *args):
        QTreeView.__init__(self, *args)
        self.setFocusPolicy(Qt.FocusPolicy.NoFocus)
        self.delegate = Delegate(self)
        self.setItemDelegate(self.delegate)
        self.setMinimumWidth(80)
        self.header().close()
        self.setMouseTracking(True)
        self.set_style_sheet()
        self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.context_menu = None
        self.customContextMenuRequested.connect(self.show_context_menu)
        QApplication.instance().palette_changed.connect(
            self.set_style_sheet, type=Qt.ConnectionType.QueuedConnection)
        self.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
        self.gesture_manager = GestureManager(self)

    def viewportEvent(self, ev):
        try:
            ret = self.gesture_manager.handle_event(ev)
        except AttributeError:
            ret = None
        if ret is not None:
            return ret
        return super().viewportEvent(ev)

    def setModel(self, model):
        QTreeView.setModel(self, model)
        model.current_toc_nodes_changed.connect(
            self.current_toc_nodes_changed,
            type=Qt.ConnectionType.QueuedConnection)

    def current_toc_nodes_changed(self, ancestors, nodes):
        if ancestors:
            self.auto_expand_indices(ancestors)
        if nodes:
            self.scrollTo(nodes[-1].index())

    def auto_expand_indices(self, indices):
        for idx in indices:
            self.setExpanded(idx, True)

    def set_style_sheet(self):
        self.setStyleSheet('''
            QTreeView {
                background-color: palette(window);
                color: palette(window-text);
                border: none;
            }

            QTreeView::item {
                border: 1px solid transparent;
                padding-top:0.5ex;
                padding-bottom:0.5ex;
            }

            QTreeView::item:hover {
                background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #e7effd, stop: 1 #cbdaf1);
                color: black;
                border: 1px solid #bfcde4;
                border-radius: 6px;
            }
        ''')

    def mouseMoveEvent(self, ev):
        if self.indexAt(ev.pos()).isValid():
            self.setCursor(Qt.CursorShape.PointingHandCursor)
        else:
            self.unsetCursor()
        return QTreeView.mouseMoveEvent(self, ev)

    def expand_tree(self, index):
        self.expand(index)
        i = -1
        while True:
            i += 1
            child = index.child(i, 0)
            if not child.isValid():
                break
            self.expand_tree(child)

    def collapse_at_level(self, index):
        item = self.model().itemFromIndex(index)
        for x in self.model().items_at_depth(item.depth):
            self.collapse(self.model().indexFromItem(x))

    def expand_at_level(self, index):
        item = self.model().itemFromIndex(index)
        for x in self.model().items_at_depth(item.depth):
            self.expand(self.model().indexFromItem(x))

    def show_context_menu(self, pos):
        index = self.indexAt(pos)
        m = QMenu(self)
        if index.isValid():
            m.addAction(QIcon.ic('plus.png'),
                        _('Expand all items under %s') % index.data(),
                        partial(self.expand_tree, index))
        m.addSeparator()
        m.addAction(QIcon.ic('plus.png'), _('Expand all items'),
                    self.expandAll)
        m.addAction(QIcon.ic('minus.png'), _('Collapse all items'),
                    self.collapseAll)
        m.addSeparator()
        if index.isValid():
            m.addAction(
                QIcon.ic('plus.png'),
                _('Expand all items at the level of {}').format(index.data()),
                partial(self.expand_at_level, index))
            m.addAction(
                QIcon.ic('minus.png'),
                _('Collapse all items at the level of {}').format(
                    index.data()), partial(self.collapse_at_level, index))
        m.addSeparator()
        m.addAction(QIcon.ic('edit-copy.png'),
                    _('Copy Table of Contents to clipboard'),
                    self.copy_to_clipboard)
        self.context_menu = m
        m.exec(self.mapToGlobal(pos))

    def copy_to_clipboard(self):
        m = self.model()
        QApplication.clipboard().setText(getattr(m, 'as_plain_text', ''))

    def update_current_toc_nodes(self, families):
        self.model().update_current_toc_nodes(families)

    def scroll_to_current_toc_node(self):
        try:
            nodes = self.model().viewed_nodes()
        except AttributeError:
            nodes = ()
        if nodes:
            self.scrollTo(nodes[-1].index())
Esempio n. 6
0
class BookmarksList(QListWidget):

    changed = pyqtSignal()
    bookmark_activated = pyqtSignal(object)

    def __init__(self, parent=None):
        QListWidget.__init__(self, parent)
        self.setAlternatingRowColors(True)
        self.setStyleSheet('QListView::item { padding: 0.5ex }')
        self.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
        self.ac_edit = ac = QAction(QIcon(I('edit_input.png')), _('Rename this bookmark'), self)
        self.addAction(ac)
        self.ac_delete = ac = QAction(QIcon(I('trash.png')), _('Remove this bookmark'), self)
        self.addAction(ac)
        self.gesture_manager = GestureManager(self)
        self.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)

    def viewportEvent(self, ev):
        try:
            ret = self.gesture_manager.handle_event(ev)
        except AttributeError:
            ret = None
        if ret is not None:
            return ret
        return super().viewportEvent(ev)

    @property
    def current_non_removed_item(self):
        ans = self.currentItem()
        if ans is not None:
            bm = ans.data(Qt.ItemDataRole.UserRole)
            if not bm.get('removed'):
                return ans

    def keyPressEvent(self, ev):
        if ev.key() in (Qt.Key.Key_Enter, Qt.Key.Key_Return):
            i = self.current_non_removed_item
            if i is not None:
                self.bookmark_activated.emit(i)
                ev.accept()
                return
        if ev.key() in (Qt.Key.Key_Delete, Qt.Key.Key_Backspace):
            i = self.current_non_removed_item
            if i is not None:
                self.ac_delete.trigger()
                ev.accept()
                return
        return QListWidget.keyPressEvent(self, ev)

    def activate_related_bookmark(self, delta=1):
        if not self.count():
            return
        items = [self.item(r) for r in range(self.count())]
        row = self.currentRow()
        current_item = items[row]
        items = [i for i in items if not i.isHidden()]
        count = len(items)
        if not count:
            return
        row = items.index(current_item)
        nrow = (row + delta + count) % count
        self.setCurrentItem(items[nrow])
        self.bookmark_activated.emit(self.currentItem())

    def next_bookmark(self):
        self.activate_related_bookmark()

    def previous_bookmark(self):
        self.activate_related_bookmark(-1)
Esempio n. 7
0
class Highlights(QTreeWidget):

    jump_to_highlight = pyqtSignal(object)
    current_highlight_changed = pyqtSignal(object)
    delete_requested = pyqtSignal()
    edit_requested = pyqtSignal()
    edit_notes_requested = pyqtSignal()

    def __init__(self, parent=None):
        QTreeWidget.__init__(self, parent)
        self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.customContextMenuRequested.connect(self.show_context_menu)
        self.default_decoration = QIcon(I('blank.png'))
        self.setHeaderHidden(True)
        self.num_of_items = 0
        self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
        set_no_activate_on_click(self)
        self.itemActivated.connect(self.item_activated)
        self.currentItemChanged.connect(self.current_item_changed)
        self.uuid_map = {}
        self.section_font = QFont(self.font())
        self.section_font.setItalic(True)
        self.gesture_manager = GestureManager(self)
        self.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)

    def viewportEvent(self, ev):
        try:
            ret = self.gesture_manager.handle_event(ev)
        except AttributeError:
            ret = None
        if ret is not None:
            return ret
        return super().viewportEvent(ev)

    def show_context_menu(self, point):
        index = self.indexAt(point)
        h = index.data(Qt.ItemDataRole.UserRole)
        self.context_menu = m = QMenu(self)
        if h is not None:
            m.addAction(QIcon(I('edit_input.png')), _('Modify this highlight'), self.edit_requested.emit)
            m.addAction(QIcon(I('modified.png')), _('Edit notes for this highlight'), self.edit_notes_requested.emit)
            m.addAction(QIcon(I('trash.png')), ngettext(
                'Delete this highlight', 'Delete selected highlights', len(self.selectedItems())
            ), self.delete_requested.emit)
        m.addSeparator()
        m.addAction(QIcon.ic('plus.png'), _('Expand all'), self.expandAll)
        m.addAction(QIcon.ic('minus.png'), _('Collapse all'), self.collapseAll)
        self.context_menu.popup(self.mapToGlobal(point))
        return True

    def current_item_changed(self, current, previous):
        self.current_highlight_changed.emit(current.data(0, Qt.ItemDataRole.UserRole) if current is not None else None)

    def load(self, highlights, preserve_state=False):
        s = self.style()
        expanded_chapters = set()
        if preserve_state:
            root = self.invisibleRootItem()
            for i in range(root.childCount()):
                chapter = root.child(i)
                if chapter.isExpanded():
                    expanded_chapters.add(chapter.data(0, Qt.ItemDataRole.DisplayRole))
        icon_size = s.pixelMetric(QStyle.PixelMetric.PM_SmallIconSize, None, self)
        dpr = self.devicePixelRatioF()
        is_dark = is_dark_theme()
        self.clear()
        self.uuid_map = {}
        highlights = (h for h in highlights if not h.get('removed') and h.get('highlighted_text'))
        section_map = defaultdict(list)
        section_tt_map = {}
        for h in self.sorted_highlights(highlights):
            tfam = h.get('toc_family_titles') or ()
            if tfam:
                tsec = tfam[0]
                lsec = tfam[-1]
            else:
                tsec = h.get('top_level_section_title')
                lsec = h.get('lowest_level_section_title')
            sec = lsec or tsec or _('Unknown')
            if len(tfam) > 1:
                lines = []
                for i, node in enumerate(tfam):
                    lines.append('\xa0\xa0' * i + '➤ ' + node)
                tt = ngettext('Table of Contents section:', 'Table of Contents sections:', len(lines))
                tt += '\n' + '\n'.join(lines)
                section_tt_map[sec] = tt
            section_map[sec].append(h)
        for secnum, (sec, items) in enumerate(section_map.items()):
            section = QTreeWidgetItem([sec], 1)
            section.setFlags(Qt.ItemFlag.ItemIsEnabled)
            section.setFont(0, self.section_font)
            tt = section_tt_map.get(sec)
            if tt:
                section.setToolTip(0, tt)
            self.addTopLevelItem(section)
            section.setExpanded(not preserve_state or sec in expanded_chapters)
            for itemnum, h in enumerate(items):
                txt = h.get('highlighted_text')
                txt = txt.replace('\n', ' ')
                if h.get('notes'):
                    txt = '•' + txt
                if len(txt) > 100:
                    txt = txt[:100] + '…'
                item = QTreeWidgetItem(section, [txt], 2)
                item.setFlags(Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemNeverHasChildren)
                item.setData(0, Qt.ItemDataRole.UserRole, h)
                try:
                    dec = decoration_for_style(self.palette(), h.get('style') or {}, icon_size, dpr, is_dark)
                except Exception:
                    import traceback
                    traceback.print_exc()
                    dec = None
                if dec is None:
                    dec = self.default_decoration
                item.setData(0, Qt.ItemDataRole.DecorationRole, dec)
                self.uuid_map[h['uuid']] = secnum, itemnum
                self.num_of_items += 1

    def sorted_highlights(self, highlights):
        def_idx = 999999999999999
        defval = def_idx, cfi_sort_key('/99999999')

        def cfi_key(h):
            cfi = h.get('start_cfi')
            si = h.get('spine_index', def_idx)
            return (si, cfi_sort_key(cfi)) if cfi else defval

        return sorted(highlights, key=cfi_key)

    def refresh(self, highlights):
        h = self.current_highlight
        self.load(highlights, preserve_state=True)
        if h is not None:
            idx = self.uuid_map.get(h['uuid'])
            if idx is not None:
                sec_idx, item_idx = idx
                self.set_current_row(sec_idx, item_idx)

    def iteritems(self):
        root = self.invisibleRootItem()
        for i in range(root.childCount()):
            sec = root.child(i)
            for k in range(sec.childCount()):
                yield sec.child(k)

    def count(self):
        return self.num_of_items

    def find_query(self, query):
        pat = query.regex
        items = tuple(self.iteritems())
        count = len(items)
        cr = -1
        ch = self.current_highlight
        if ch:
            q = ch['uuid']
            for i, item in enumerate(items):
                h = item.data(0, Qt.ItemDataRole.UserRole)
                if h['uuid'] == q:
                    cr = i
        if query.backwards:
            if cr < 0:
                cr = count
            indices = chain(range(cr - 1, -1, -1), range(count - 1, cr, -1))
        else:
            if cr < 0:
                cr = -1
            indices = chain(range(cr + 1, count), range(0, cr + 1))
        for i in indices:
            h = items[i].data(0, Qt.ItemDataRole.UserRole)
            if pat.search(h['highlighted_text']) is not None or pat.search(h.get('notes') or '') is not None:
                self.set_current_row(*self.uuid_map[h['uuid']])
                return True
        return False

    def find_annot_id(self, annot_id):
        q = self.uuid_map.get(annot_id)
        if q is not None:
            self.set_current_row(*q)
            return True
        return False

    def set_current_row(self, sec_idx, item_idx):
        sec = self.topLevelItem(sec_idx)
        if sec is not None:
            item = sec.child(item_idx)
            if item is not None:
                self.setCurrentItem(item, 0, QItemSelectionModel.SelectionFlag.ClearAndSelect)
                return True
        return False

    def item_activated(self, item):
        h = item.data(0, Qt.ItemDataRole.UserRole)
        if h is not None:
            self.jump_to_highlight.emit(h)

    @property
    def current_highlight(self):
        i = self.currentItem()
        if i is not None:
            return i.data(0, Qt.ItemDataRole.UserRole)

    @property
    def all_highlights(self):
        for item in self.iteritems():
            yield item.data(0, Qt.ItemDataRole.UserRole)

    @property
    def selected_highlights(self):
        for item in self.selectedItems():
            yield item.data(0, Qt.ItemDataRole.UserRole)

    def keyPressEvent(self, ev):
        if ev.matches(QKeySequence.StandardKey.Delete):
            self.delete_requested.emit()
            ev.accept()
            return
        if ev.key() == Qt.Key.Key_F2:
            self.edit_requested.emit()
            ev.accept()
            return
        return super().keyPressEvent(ev)
Esempio n. 8
0
class GridView(QListView):

    update_item = pyqtSignal(object)
    files_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(self.LeftToRight)
        # We cannot set layout mode to batched, because that breaks
        # restore_vpos()
        # self.setLayoutMode(self.Batched)
        self.setResizeMode(self.Adjust)
        self.setSelectionMode(self.ExtendedSelection)
        self.setVerticalScrollMode(self.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.QueuedConnection)
        self.doubleClicked.connect(self.double_clicked)
        self.setCursor(Qt.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 xrange(geom.top(), (self.spacing()*2) + geom.top(), 5):
            for x in xrange(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 xrange(geom.bottom(), geom.bottom() - 2 * self.spacing(), -5):
            for x in xrange(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 xrange(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']
        pal = QPalette()
        col = QColor(r, g, b)
        pal.setColor(pal.Base, col)
        tex = gprefs['cover_grid_texture']
        if tex:
            from calibre.gui2.preferences.texture_chooser import texture_path
            path = texture_path(tex)
            if path:
                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)
                    pal.setBrush(pal.Base, QBrush(pm))
        dark = (r + g + b)/3.0 < 128
        pal.setColor(pal.Text, QColor(Qt.white if dark else Qt.black))
        self.setPalette(pal)
        self.delegate.highlight_color = pal.color(pal.Text)

    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()
        if size_changed:
            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()))
        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 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
        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')
            dpr = self.device_pixel_ratio
            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, int(dpr * self.delegate.cover_size.width()), int(dpr * self.delegate.cover_size.height()))
                    if scaled:
                        if self.ignore_render_requests.is_set():
                            return
                        p = p.scaled(nwidth, nheight, Qt.IgnoreAspectRatio, Qt.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):
            group = list(map(operator.itemgetter(1), g))
            sel.merge(QItemSelection(m.index(min(group), 0), m.index(max(group), 0)), sm.Select)
        sm.select(sel, sm.ClearAndSelect)