Directly draw the spinner instead of using an animated GIF

This commit is contained in:
Kovid Goyal 2011-04-09 15:41:24 -06:00
parent 3de4c6f7ca
commit d2fe0bf7ee
2 changed files with 225 additions and 31 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@ -9,12 +9,13 @@ __docformat__ = 'restructuredtext en'
from threading import Thread, Event from threading import Thread, Event
from operator import attrgetter from operator import attrgetter
from Queue import Queue, Empty
from PyQt4.Qt import (QStyledItemDelegate, QTextDocument, QRectF, QIcon, Qt, from PyQt4.Qt import (QStyledItemDelegate, QTextDocument, QRectF, QIcon, Qt,
QStyle, QApplication, QDialog, QVBoxLayout, QLabel, QDialogButtonBox, QStyle, QApplication, QDialog, QVBoxLayout, QLabel, QDialogButtonBox,
QStackedWidget, QWidget, QTableView, QGridLayout, QFontInfo, QPalette, QStackedWidget, QWidget, QTableView, QGridLayout, QFontInfo, QPalette,
QTimer, pyqtSignal, QAbstractTableModel, QVariant, QSize, QListView, QTimer, pyqtSignal, QAbstractTableModel, QVariant, QSize, QListView,
QPixmap, QAbstractListModel, QMovie) QPixmap, QAbstractListModel, QColor, QRect)
from PyQt4.QtWebKit import QWebView from PyQt4.QtWebKit import QWebView
from calibre.customize.ui import metadata_plugins from calibre.customize.ui import metadata_plugins
@ -25,6 +26,9 @@ from calibre.ebooks.metadata.book.base import Metadata
from calibre.gui2 import error_dialog, NONE from calibre.gui2 import error_dialog, NONE
from calibre.utils.date import utcnow, fromordinal, format_date from calibre.utils.date import utcnow, fromordinal, format_date
from calibre.library.comments import comments_to_html from calibre.library.comments import comments_to_html
from calibre import force_unicode
DEVELOP_DIALOG = False
class RichTextDelegate(QStyledItemDelegate): # {{{ class RichTextDelegate(QStyledItemDelegate): # {{{
@ -269,7 +273,7 @@ class IdentifyWorker(Thread): # {{{
def run(self): def run(self):
try: try:
if True: if DEVELOP_DIALOG:
self.results = self.sample_results() self.results = self.sample_results()
else: else:
self.results = identify(self.log, self.abort, title=self.title, self.results = identify(self.log, self.abort, title=self.title,
@ -278,7 +282,7 @@ class IdentifyWorker(Thread): # {{{
result.gui_rank = i result.gui_rank = i
except: except:
import traceback import traceback
self.error = traceback.format_exc() self.error = force_unicode(traceback.format_exc())
# }}} # }}}
class IdentifyWidget(QWidget): # {{{ class IdentifyWidget(QWidget): # {{{
@ -399,9 +403,39 @@ class IdentifyWidget(QWidget): # {{{
self.abort.set() self.abort.set()
# }}} # }}}
class CoverWorker(Thread): # {{{
def __init__(self, log, abort, title, authors, identifiers):
Thread.__init__(self)
self.daemon = True
self.log, self.abort = log, abort
self.title, self.authors, self.identifiers = (title, authors,
identifiers)
self.rq = Queue()
self.error = None
def fake_run(self):
import time
time.sleep(2)
def run(self):
try:
if DEVELOP_DIALOG:
self.fake_run()
else:
from calibre.ebooks.metadata.sources.covers import run_download
run_download(self.log, self.rq, self.abort, title=self.title,
authors=self.authors, identifiers=self.identifiers)
except:
import traceback
self.error = force_unicode(traceback.format_exc())
# }}}
class CoversModel(QAbstractListModel): # {{{ class CoversModel(QAbstractListModel): # {{{
def __init__(self, log, current_cover, parent=None): def __init__(self, current_cover, parent=None):
QAbstractListModel.__init__(self, parent) QAbstractListModel.__init__(self, parent)
if current_cover is None: if current_cover is None:
@ -409,13 +443,14 @@ class CoversModel(QAbstractListModel): # {{{
self.blank = QPixmap(I('blank.png')).scaled(150, 200) self.blank = QPixmap(I('blank.png')).scaled(150, 200)
self.covers = [self.get_item(_('Current cover'), current_cover, False)] self.covers = [self.get_item(_('Current cover'), current_cover)]
for plugin in metadata_plugins(['cover']): self.plugin_map = {}
for i, plugin in enumerate(metadata_plugins(['cover'])):
self.covers.append((plugin.name+'\n'+_('Searching...'), self.covers.append((plugin.name+'\n'+_('Searching...'),
QVariant(self.blank), None, True)) QVariant(self.blank), None, True))
self.log = log self.plugin_map[plugin] = i+1
def get_item(self, src, pmap, waiting=True): def get_item(self, src, pmap, waiting=False):
sz = '%dx%d'%(pmap.width(), pmap.height()) sz = '%dx%d'%(pmap.width(), pmap.height())
text = QVariant(src + '\n' + sz) text = QVariant(src + '\n' + sz)
scaled = pmap.scaled(150, 200, Qt.IgnoreAspectRatio, scaled = pmap.scaled(150, 200, Qt.IgnoreAspectRatio,
@ -437,41 +472,118 @@ class CoversModel(QAbstractListModel): # {{{
if role == Qt.UserRole: if role == Qt.UserRole:
return waiting return waiting
return NONE return NONE
def plugin_for_index(self, index):
row = index.row() if hasattr(index, 'row') else index
for k, v in self.plugin_map.iteritems():
if v == row:
return k
def clear_failed(self):
good = []
pmap = {}
for i, x in enumerate(self.covers):
if not x[-1]:
good.append(x)
if i > 0:
plugin = self.plugin_for_index(i)
pmap[plugin] = len(good) - 1
good = [x for x in self.covers if not x[-1]]
self.covers = good
self.plugin_map = pmap
self.reset()
def index_for_plugin(self, plugin):
idx = self.plugin_map.get(plugin, 0)
return self.index(idx)
def update_result(self, plugin, width, height, data):
try:
idx = self.plugin_map[plugin]
except:
return
pmap = QPixmap()
pmap.loadFromData(data)
if pmap.isNull():
return
self.covers[idx] = self.get_item(plugin.name, pmap, waiting=False)
self.dataChanged.emit(self.index(idx), self.index(idx))
def cover_pmap(self, index):
row = index.row()
if row > 0 and row < len(self.covers):
pmap = self.books[row][2]
if pmap is not None and not pmap.isNull():
return pmap
# }}} # }}}
class CoverDelegate(QStyledItemDelegate): class CoverDelegate(QStyledItemDelegate): # {{{
needs_redraw = pyqtSignal() needs_redraw = pyqtSignal()
def __init__(self, parent=None): def __init__(self, parent):
QStyledItemDelegate.__init__(self, parent) QStyledItemDelegate.__init__(self, parent)
self.movie = QMovie(I('spinner.gif')) self.angle = 0
self.movie.frameChanged.connect(self.frame_changed) self.timer = QTimer(self)
self.timer.timeout.connect(self.frame_changed)
self.color = parent.palette().color(QPalette.WindowText)
self.spinner_width = 64
def frame_changed(self, *args): def frame_changed(self, *args):
self.angle = (self.angle+30)%360
self.needs_redraw.emit() self.needs_redraw.emit()
def start_movie(self): def start_animation(self):
self.movie.start() self.angle = 0
self.timer.start(200)
def stop_movie(self): def stop_animation(self):
self.movie.stop() self.timer.stop()
def draw_spinner(self, painter, rect):
width = rect.width()
outer_radius = (width-1)*0.5
inner_radius = (width-1)*0.5*0.38
capsule_height = outer_radius - inner_radius
capsule_width = int(capsule_height * (0.23 if width > 32 else 0.35))
capsule_radius = capsule_width//2
painter.save()
painter.setRenderHint(painter.Antialiasing)
for i in xrange(12):
color = QColor(self.color)
color.setAlphaF(1.0 - (i/12.0))
painter.setPen(Qt.NoPen)
painter.setBrush(color)
painter.save()
painter.translate(rect.center())
painter.rotate(self.angle - i*30.0)
painter.drawRoundedRect(-capsule_width*0.5,
-(inner_radius+capsule_height), capsule_width,
capsule_height, capsule_radius, capsule_radius)
painter.restore()
painter.restore()
def paint(self, painter, option, index): def paint(self, painter, option, index):
waiting = index.data(Qt.UserRole).toBool()
if waiting:
pixmap = self.movie.currentPixmap()
prect = pixmap.rect()
prect.moveCenter(option.rect.center())
painter.drawPixmap(prect, pixmap, pixmap.rect())
QStyledItemDelegate.paint(self, painter, option, index) QStyledItemDelegate.paint(self, painter, option, index)
if self.timer.isActive() and index.data(Qt.UserRole).toBool():
rect = QRect(0, 0, self.spinner_width, self.spinner_width)
rect.moveCenter(option.rect.center())
self.draw_spinner(painter, rect)
# }}}
class CoversView(QListView): # {{{ class CoversView(QListView): # {{{
def __init__(self, log, current_cover, parent=None): chosen = pyqtSignal()
def __init__(self, current_cover, parent=None):
QListView.__init__(self, parent) QListView.__init__(self, parent)
self.m = CoversModel(log, current_cover, self) self.m = CoversModel(current_cover, self)
self.setModel(self.m) self.setModel(self.m)
self.setFlow(self.LeftToRight) self.setFlow(self.LeftToRight)
@ -487,6 +599,8 @@ class CoversView(QListView): # {{{
self.delegate.needs_redraw.connect(self.viewport().update, self.delegate.needs_redraw.connect(self.viewport().update,
type=Qt.QueuedConnection) type=Qt.QueuedConnection)
self.doubleClicked.connect(self.chosen, type=Qt.QueuedConnection)
def select(self, num): def select(self, num):
current = self.model().index(num) current = self.model().index(num)
sm = self.selectionModel() sm = self.selectionModel()
@ -494,15 +608,24 @@ class CoversView(QListView): # {{{
def start(self): def start(self):
self.select(0) self.select(0)
self.delegate.start_movie() self.delegate.start_animation()
def clear_failed(self):
plugin = self.m.plugin_for_index(self.currentIndex())
self.m.clear_failed()
self.select(self.m.index_for_plugin(plugin).row())
# }}} # }}}
class CoverWidget(QWidget): # {{{ class CoversWidget(QWidget): # {{{
chosen = pyqtSignal()
finished = pyqtSignal()
def __init__(self, log, current_cover, parent=None): def __init__(self, log, current_cover, parent=None):
QWidget.__init__(self, parent) QWidget.__init__(self, parent)
self.log = log self.log = log
self.abort = Event()
self.l = l = QGridLayout() self.l = l = QGridLayout()
self.setLayout(l) self.setLayout(l)
@ -511,8 +634,10 @@ class CoverWidget(QWidget): # {{{
self.msg.setWordWrap(True) self.msg.setWordWrap(True)
l.addWidget(self.msg, 0, 0) l.addWidget(self.msg, 0, 0)
self.covers_view = CoversView(log, current_cover, self) self.covers_view = CoversView(current_cover, self)
self.covers_view.chosen.connect(self.chosen)
l.addWidget(self.covers_view, 1, 0) l.addWidget(self.covers_view, 1, 0)
self.continue_processing = True
def start(self, book, current_cover, title, authors): def start(self, book, current_cover, title, authors):
self.book, self.current_cover = book, current_cover self.book, self.current_cover = book, current_cover
@ -521,6 +646,66 @@ class CoverWidget(QWidget): # {{{
self.msg.setText('<p>'+_('Downloading covers for <b>%s</b>, please wait...')%book.title) self.msg.setText('<p>'+_('Downloading covers for <b>%s</b>, please wait...')%book.title)
self.covers_view.start() self.covers_view.start()
self.worker = CoverWorker(self.log, self.abort, self.title,
self.authors, book.identifiers)
self.worker.start()
QTimer.singleShot(50, self.check)
self.covers_view.setFocus(Qt.OtherFocusReason)
def check(self):
if self.worker.is_alive() and not self.abort.is_set():
QTimer.singleShot(50, self.check)
try:
self.process_result(self.worker.rq.get_nowait())
except Empty:
pass
else:
self.process_results()
def process_results(self):
while self.continue_processing:
try:
self.process_result(self.worker.rq.get_nowait())
except Empty:
break
self.covers_view.clear_failed()
if self.worker.error is not None:
error_dialog(self, _('Download failed'),
_('Failed to download any covers, click'
' "Show details" for details.'),
det_msg=self.worker.error, show=True)
num = self.covers_view.model().rowCount()
if num < 2:
txt = _('Could not find any covers for <b>%s</b>')%self.book.title
else:
txt = _('Found <b>%d</b> covers of %s. Pick the one you like'
' best.')%(num, self.title)
self.msg.setText(txt)
self.finished.emit()
def process_result(self, result):
if not self.continue_processing:
return
plugin, width, height, fmt, data = result
self.covers_view.model().update_result(plugin, width, height, data)
def cleanup(self):
self.covers_view.delegate.stop_animation()
self.continue_processing = False
def cancel(self):
self.continue_processing = False
self.abort.set()
@property
def cover_pmap(self):
return self.covers_view.model().cover_pmap(
self.covers_view.currentIndex())
# }}} # }}}
class FullFetch(QDialog): # {{{ class FullFetch(QDialog): # {{{
@ -528,6 +713,7 @@ class FullFetch(QDialog): # {{{
def __init__(self, log, current_cover=None, parent=None): def __init__(self, log, current_cover=None, parent=None):
QDialog.__init__(self, parent) QDialog.__init__(self, parent)
self.log, self.current_cover = log, current_cover self.log, self.current_cover = log, current_cover
self.book = self.cover_pmap = None
self.setWindowTitle(_('Downloading metadata...')) self.setWindowTitle(_('Downloading metadata...'))
self.setWindowIcon(QIcon(I('metadata.png'))) self.setWindowIcon(QIcon(I('metadata.png')))
@ -554,17 +740,20 @@ class FullFetch(QDialog): # {{{
self.identify_widget.book_selected.connect(self.book_selected) self.identify_widget.book_selected.connect(self.book_selected)
self.stack.addWidget(self.identify_widget) self.stack.addWidget(self.identify_widget)
self.cover_widget = CoverWidget(self.log, self.current_cover, parent=self) self.covers_widget = CoversWidget(self.log, self.current_cover, parent=self)
self.stack.addWidget(self.cover_widget) self.covers_widget.chosen.connect(self.ok_clicked)
self.stack.addWidget(self.covers_widget)
self.resize(850, 550) self.resize(850, 550)
self.finished.connect(self.cleanup)
def book_selected(self, book): def book_selected(self, book):
self.next_button.setVisible(False) self.next_button.setVisible(False)
self.ok_button.setVisible(True) self.ok_button.setVisible(True)
self.book = book self.book = book
self.stack.setCurrentIndex(1) self.stack.setCurrentIndex(1)
self.cover_widget.start(book, self.current_cover, self.covers_widget.start(book, self.current_cover,
self.title, self.authors) self.title, self.authors)
def accept(self): def accept(self):
@ -575,6 +764,9 @@ class FullFetch(QDialog): # {{{
self.identify_widget.cancel() self.identify_widget.cancel()
return QDialog.reject(self) return QDialog.reject(self)
def cleanup(self):
self.covers_widget.cleanup()
def identify_results_found(self): def identify_results_found(self):
self.next_button.setEnabled(True) self.next_button.setEnabled(True)
@ -582,7 +774,8 @@ class FullFetch(QDialog): # {{{
self.identify_widget.get_result() self.identify_widget.get_result()
def ok_clicked(self, *args): def ok_clicked(self, *args):
pass self.cover_pmap = self.covers_widget.cover_pmap
QDialog.accept(self)
def start(self, title=None, authors=None, identifiers={}): def start(self, title=None, authors=None, identifiers={}):
self.title, self.authors = title, authors self.title, self.authors = title, authors
@ -592,6 +785,7 @@ class FullFetch(QDialog): # {{{
# }}} # }}}
if __name__ == '__main__': if __name__ == '__main__':
DEVELOP_DIALOG = True
app = QApplication([]) app = QApplication([])
d = FullFetch(Log()) d = FullFetch(Log())
d.start(title='great gatsby', authors=['Fitzgerald']) d.start(title='great gatsby', authors=['Fitzgerald'])