mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Grid view renders
This commit is contained in:
parent
fe363c95f5
commit
0b9fe18d91
@ -89,6 +89,7 @@ class Cache(object):
|
|||||||
self.formatter_template_cache = {}
|
self.formatter_template_cache = {}
|
||||||
self.dirtied_cache = {}
|
self.dirtied_cache = {}
|
||||||
self.dirtied_sequence = 0
|
self.dirtied_sequence = 0
|
||||||
|
self.cover_caches = set()
|
||||||
|
|
||||||
# Implement locking for all simple read/write API methods
|
# Implement locking for all simple read/write API methods
|
||||||
# An unlocked version of the method is stored with the name starting
|
# An unlocked version of the method is stored with the name starting
|
||||||
@ -1031,9 +1032,21 @@ class Cache(object):
|
|||||||
path = self._field_for('path', book_id).replace('/', os.sep)
|
path = self._field_for('path', book_id).replace('/', os.sep)
|
||||||
|
|
||||||
self.backend.set_cover(book_id, path, data)
|
self.backend.set_cover(book_id, path, data)
|
||||||
|
for cc in self.cover_caches:
|
||||||
|
cc.invalidate(book_id)
|
||||||
return self._set_field('cover', {
|
return self._set_field('cover', {
|
||||||
book_id:(0 if data is None else 1) for book_id, data in book_id_data_map.iteritems()})
|
book_id:(0 if data is None else 1) for book_id, data in book_id_data_map.iteritems()})
|
||||||
|
|
||||||
|
@write_api
|
||||||
|
def add_cover_cache(self, cover_cache):
|
||||||
|
if not callable(cover_cache.invalidate):
|
||||||
|
raise ValueError('Cover caches must have an invalidate method')
|
||||||
|
self.cover_caches.add(cover_cache)
|
||||||
|
|
||||||
|
@write_api
|
||||||
|
def remove_cover_cache(self, cover_cache):
|
||||||
|
self.cover_caches.discard(cover_cache)
|
||||||
|
|
||||||
@write_api
|
@write_api
|
||||||
def set_metadata(self, book_id, mi, ignore_errors=False, force_changes=False,
|
def set_metadata(self, book_id, mi, ignore_errors=False, force_changes=False,
|
||||||
set_title=True, set_authors=True):
|
set_title=True, set_authors=True):
|
||||||
|
@ -6,7 +6,15 @@ from __future__ import (unicode_literals, division, absolute_import,
|
|||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
|
||||||
from PyQt4.Qt import QListView
|
from time import time
|
||||||
|
from collections import OrderedDict
|
||||||
|
from threading import Lock, Event, Thread
|
||||||
|
from Queue import Queue
|
||||||
|
|
||||||
|
from PyQt4.Qt import (
|
||||||
|
QListView, QSize, QStyledItemDelegate, QModelIndex, Qt, QImage, pyqtSignal, QPalette, QColor)
|
||||||
|
|
||||||
|
from calibre import fit_image
|
||||||
|
|
||||||
class AlternateViews(object):
|
class AlternateViews(object):
|
||||||
|
|
||||||
@ -25,6 +33,7 @@ class AlternateViews(object):
|
|||||||
self.stack_positions[key] = self.stack.count()
|
self.stack_positions[key] = self.stack.count()
|
||||||
self.stack.addWidget(view)
|
self.stack.addWidget(view)
|
||||||
self.stack.setCurrentIndex(0)
|
self.stack.setCurrentIndex(0)
|
||||||
|
view.setModel(self.main_view._model)
|
||||||
|
|
||||||
def show_view(self, key=None):
|
def show_view(self, key=None):
|
||||||
view = self.views[key]
|
view = self.views[key]
|
||||||
@ -32,7 +41,188 @@ class AlternateViews(object):
|
|||||||
return
|
return
|
||||||
self.stack.setCurrentIndex(self.stack_positions[key])
|
self.stack.setCurrentIndex(self.stack_positions[key])
|
||||||
self.current_view = view
|
self.current_view = view
|
||||||
|
if view is not self.main_view:
|
||||||
|
view.shown()
|
||||||
|
|
||||||
|
def set_database(self, db, stage=0):
|
||||||
|
for view in self.views.itervalues():
|
||||||
|
if view is not self.main_view:
|
||||||
|
view.set_database(db, stage=stage)
|
||||||
|
|
||||||
|
class CoverCache(dict):
|
||||||
|
|
||||||
|
def __init__(self, limit=200):
|
||||||
|
self.items = OrderedDict()
|
||||||
|
self.lock = Lock()
|
||||||
|
self.limit = limit
|
||||||
|
|
||||||
|
def invalidate(self, book_id):
|
||||||
|
with self.lock:
|
||||||
|
self.items.pop(book_id, None)
|
||||||
|
|
||||||
|
def __call__(self, key):
|
||||||
|
with self.lock:
|
||||||
|
ans = self.items.pop(key, False)
|
||||||
|
if ans is not False:
|
||||||
|
self.items[key] = ans
|
||||||
|
if len(self.items) > self.limit:
|
||||||
|
del self.items[next(self.items.iterkeys())]
|
||||||
|
|
||||||
|
return ans
|
||||||
|
|
||||||
|
def set(self, key, val):
|
||||||
|
with self.lock:
|
||||||
|
self.items[key] = val
|
||||||
|
if len(self.items) > self.limit:
|
||||||
|
del self.items[next(self.items.iterkeys())]
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
with self.lock:
|
||||||
|
self.items.clear()
|
||||||
|
|
||||||
|
class CoverDelegate(QStyledItemDelegate):
|
||||||
|
|
||||||
|
def __init__(self, parent, width, height):
|
||||||
|
super(CoverDelegate, self).__init__(parent)
|
||||||
|
self.cover_size = QSize(width, height)
|
||||||
|
self.item_size = self.cover_size + QSize(8, 8)
|
||||||
|
self.spacing = max(10, min(50, int(0.1 * width)))
|
||||||
|
self.cover_cache = CoverCache()
|
||||||
|
self.render_queue = Queue()
|
||||||
|
|
||||||
|
def sizeHint(self, option, index):
|
||||||
|
return self.item_size
|
||||||
|
|
||||||
|
def paint(self, painter, option, index):
|
||||||
|
QStyledItemDelegate.paint(self, painter, option, QModelIndex()) # draw the hover and selection highlights
|
||||||
|
db = index.model().db
|
||||||
|
try:
|
||||||
|
book_id = db.id(index.row())
|
||||||
|
except (ValueError, IndexError, KeyError):
|
||||||
|
return
|
||||||
|
db = db.new_api
|
||||||
|
cdata = self.cover_cache(book_id)
|
||||||
|
painter.save()
|
||||||
|
try:
|
||||||
|
rect = option.rect
|
||||||
|
rect.adjust(4, 4, -4, -4)
|
||||||
|
if cdata is None or cdata is False:
|
||||||
|
title = db.field_for('title', book_id, default_value='')
|
||||||
|
painter.drawText(rect, Qt.AlignCenter|Qt.TextWordWrap, title)
|
||||||
|
if cdata is False:
|
||||||
|
self.render_queue.put(book_id)
|
||||||
|
else:
|
||||||
|
dx = max(0, int((rect.width() - cdata.width())/2.0))
|
||||||
|
dy = max(0, rect.height() - cdata.height())
|
||||||
|
rect.adjust(dx, dy, -dx, 0)
|
||||||
|
painter.drawImage(rect, cdata)
|
||||||
|
finally:
|
||||||
|
painter.restore()
|
||||||
|
|
||||||
|
def join_with_timeout(q, timeout=2):
|
||||||
|
q.all_tasks_done.acquire()
|
||||||
|
try:
|
||||||
|
endtime = time() + timeout
|
||||||
|
while q.unfinished_tasks:
|
||||||
|
remaining = endtime - time()
|
||||||
|
if remaining <= 0.0:
|
||||||
|
raise RuntimeError('Waiting for queue to clear timed out')
|
||||||
|
q.all_tasks_done.wait(remaining)
|
||||||
|
finally:
|
||||||
|
q.all_tasks_done.release()
|
||||||
|
|
||||||
class GridView(QListView):
|
class GridView(QListView):
|
||||||
pass
|
|
||||||
|
update_item = pyqtSignal(object)
|
||||||
|
|
||||||
|
def __init__(self, parent):
|
||||||
|
QListView.__init__(self, parent)
|
||||||
|
pal = QPalette(self.palette())
|
||||||
|
r = g = b = 0x50
|
||||||
|
pal.setColor(pal.Base, QColor(r, g, b))
|
||||||
|
pal.setColor(pal.Text, QColor(Qt.white if (r + g + b)/3.0 < 128 else Qt.black))
|
||||||
|
self.setPalette(pal)
|
||||||
|
self.setUniformItemSizes(True)
|
||||||
|
self.setWrapping(True)
|
||||||
|
self.setFlow(self.LeftToRight)
|
||||||
|
self.setLayoutMode(self.Batched)
|
||||||
|
self.setResizeMode(self.Adjust)
|
||||||
|
self.setSelectionMode(self.ExtendedSelection)
|
||||||
|
self.delegate = CoverDelegate(self, 135, 180)
|
||||||
|
self.setItemDelegate(self.delegate)
|
||||||
|
self.setSpacing(self.delegate.spacing)
|
||||||
|
self.ignore_render_requests = Event()
|
||||||
|
self.render_thread = None
|
||||||
|
self.update_item.connect(self.re_render, type=Qt.QueuedConnection)
|
||||||
|
|
||||||
|
def shown(self):
|
||||||
|
if self.render_thread is None:
|
||||||
|
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):
|
||||||
|
cdata = self.model().db.new_api.cover(book_id)
|
||||||
|
if self.ignore_render_requests.is_set():
|
||||||
|
return
|
||||||
|
if cdata is not None:
|
||||||
|
p = QImage()
|
||||||
|
p.loadFromData(cdata)
|
||||||
|
cdata = None
|
||||||
|
if not p.isNull():
|
||||||
|
width, height = p.width(), p.height()
|
||||||
|
scaled, nwidth, nheight = fit_image(width, height, self.delegate.cover_size.width(), self.delegate.cover_size.height())
|
||||||
|
if scaled:
|
||||||
|
if self.ignore_render_requests.is_set():
|
||||||
|
return
|
||||||
|
p = p.scaled(nwidth, nheight, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
|
||||||
|
cdata = p
|
||||||
|
self.delegate.cover_cache.set(book_id, cdata)
|
||||||
|
self.update_item.emit(book_id)
|
||||||
|
|
||||||
|
def re_render(self, book_id):
|
||||||
|
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)
|
||||||
|
|
||||||
|
def set_database(self, newdb, stage=0):
|
||||||
|
if stage == 0:
|
||||||
|
self.ignore_render_requests.set()
|
||||||
|
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()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -632,6 +632,7 @@ class BooksView(QTableView): # {{{
|
|||||||
# Initialization/Delegate Setup {{{
|
# Initialization/Delegate Setup {{{
|
||||||
|
|
||||||
def set_database(self, db):
|
def set_database(self, db):
|
||||||
|
self.alternate_views.set_database(db)
|
||||||
self.save_state()
|
self.save_state()
|
||||||
self._model.set_database(db)
|
self._model.set_database(db)
|
||||||
self.tags_delegate.set_database(db)
|
self.tags_delegate.set_database(db)
|
||||||
@ -639,6 +640,7 @@ class BooksView(QTableView): # {{{
|
|||||||
self.authors_delegate.set_database(db)
|
self.authors_delegate.set_database(db)
|
||||||
self.series_delegate.set_auto_complete_function(db.all_series)
|
self.series_delegate.set_auto_complete_function(db.all_series)
|
||||||
self.publisher_delegate.set_auto_complete_function(db.all_publishers)
|
self.publisher_delegate.set_auto_complete_function(db.all_publishers)
|
||||||
|
self.alternate_views.set_database(db, stage=1)
|
||||||
|
|
||||||
def database_changed(self, db):
|
def database_changed(self, db):
|
||||||
for i in range(self.model().columnCount(None)):
|
for i in range(self.model().columnCount(None)):
|
||||||
|
@ -812,6 +812,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def shutdown(self, write_settings=True):
|
def shutdown(self, write_settings=True):
|
||||||
|
self.grid_view.shutdown()
|
||||||
try:
|
try:
|
||||||
db = self.library_view.model().db
|
db = self.library_view.model().db
|
||||||
cf = db.clean
|
cf = db.clean
|
||||||
|
Loading…
x
Reference in New Issue
Block a user