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 operator import attrgetter
from Queue import Queue, Empty
from PyQt4.Qt import (QStyledItemDelegate, QTextDocument, QRectF, QIcon, Qt,
QStyle, QApplication, QDialog, QVBoxLayout, QLabel, QDialogButtonBox,
QStackedWidget, QWidget, QTableView, QGridLayout, QFontInfo, QPalette,
QTimer, pyqtSignal, QAbstractTableModel, QVariant, QSize, QListView,
QPixmap, QAbstractListModel, QMovie)
QPixmap, QAbstractListModel, QColor, QRect)
from PyQt4.QtWebKit import QWebView
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.utils.date import utcnow, fromordinal, format_date
from calibre.library.comments import comments_to_html
from calibre import force_unicode
DEVELOP_DIALOG = False
class RichTextDelegate(QStyledItemDelegate): # {{{
@ -269,7 +273,7 @@ class IdentifyWorker(Thread): # {{{
def run(self):
try:
if True:
if DEVELOP_DIALOG:
self.results = self.sample_results()
else:
self.results = identify(self.log, self.abort, title=self.title,
@ -278,7 +282,7 @@ class IdentifyWorker(Thread): # {{{
result.gui_rank = i
except:
import traceback
self.error = traceback.format_exc()
self.error = force_unicode(traceback.format_exc())
# }}}
class IdentifyWidget(QWidget): # {{{
@ -399,9 +403,39 @@ class IdentifyWidget(QWidget): # {{{
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): # {{{
def __init__(self, log, current_cover, parent=None):
def __init__(self, current_cover, parent=None):
QAbstractListModel.__init__(self, parent)
if current_cover is None:
@ -409,13 +443,14 @@ class CoversModel(QAbstractListModel): # {{{
self.blank = QPixmap(I('blank.png')).scaled(150, 200)
self.covers = [self.get_item(_('Current cover'), current_cover, False)]
for plugin in metadata_plugins(['cover']):
self.covers = [self.get_item(_('Current cover'), current_cover)]
self.plugin_map = {}
for i, plugin in enumerate(metadata_plugins(['cover'])):
self.covers.append((plugin.name+'\n'+_('Searching...'),
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())
text = QVariant(src + '\n' + sz)
scaled = pmap.scaled(150, 200, Qt.IgnoreAspectRatio,
@ -437,41 +472,118 @@ class CoversModel(QAbstractListModel): # {{{
if role == Qt.UserRole:
return waiting
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()
def __init__(self, parent=None):
def __init__(self, parent):
QStyledItemDelegate.__init__(self, parent)
self.movie = QMovie(I('spinner.gif'))
self.movie.frameChanged.connect(self.frame_changed)
self.angle = 0
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):
self.angle = (self.angle+30)%360
self.needs_redraw.emit()
def start_movie(self):
self.movie.start()
def start_animation(self):
self.angle = 0
self.timer.start(200)
def stop_movie(self):
self.movie.stop()
def stop_animation(self):
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):
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)
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): # {{{
def __init__(self, log, current_cover, parent=None):
chosen = pyqtSignal()
def __init__(self, current_cover, parent=None):
QListView.__init__(self, parent)
self.m = CoversModel(log, current_cover, self)
self.m = CoversModel(current_cover, self)
self.setModel(self.m)
self.setFlow(self.LeftToRight)
@ -487,6 +599,8 @@ class CoversView(QListView): # {{{
self.delegate.needs_redraw.connect(self.viewport().update,
type=Qt.QueuedConnection)
self.doubleClicked.connect(self.chosen, type=Qt.QueuedConnection)
def select(self, num):
current = self.model().index(num)
sm = self.selectionModel()
@ -494,15 +608,24 @@ class CoversView(QListView): # {{{
def start(self):
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):
QWidget.__init__(self, parent)
self.log = log
self.abort = Event()
self.l = l = QGridLayout()
self.setLayout(l)
@ -511,8 +634,10 @@ class CoverWidget(QWidget): # {{{
self.msg.setWordWrap(True)
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)
self.continue_processing = True
def start(self, book, current_cover, title, authors):
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.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): # {{{
@ -528,6 +713,7 @@ class FullFetch(QDialog): # {{{
def __init__(self, log, current_cover=None, parent=None):
QDialog.__init__(self, parent)
self.log, self.current_cover = log, current_cover
self.book = self.cover_pmap = None
self.setWindowTitle(_('Downloading metadata...'))
self.setWindowIcon(QIcon(I('metadata.png')))
@ -554,17 +740,20 @@ class FullFetch(QDialog): # {{{
self.identify_widget.book_selected.connect(self.book_selected)
self.stack.addWidget(self.identify_widget)
self.cover_widget = CoverWidget(self.log, self.current_cover, parent=self)
self.stack.addWidget(self.cover_widget)
self.covers_widget = CoversWidget(self.log, self.current_cover, parent=self)
self.covers_widget.chosen.connect(self.ok_clicked)
self.stack.addWidget(self.covers_widget)
self.resize(850, 550)
self.finished.connect(self.cleanup)
def book_selected(self, book):
self.next_button.setVisible(False)
self.ok_button.setVisible(True)
self.book = book
self.stack.setCurrentIndex(1)
self.cover_widget.start(book, self.current_cover,
self.covers_widget.start(book, self.current_cover,
self.title, self.authors)
def accept(self):
@ -575,6 +764,9 @@ class FullFetch(QDialog): # {{{
self.identify_widget.cancel()
return QDialog.reject(self)
def cleanup(self):
self.covers_widget.cleanup()
def identify_results_found(self):
self.next_button.setEnabled(True)
@ -582,7 +774,8 @@ class FullFetch(QDialog): # {{{
self.identify_widget.get_result()
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={}):
self.title, self.authors = title, authors
@ -592,6 +785,7 @@ class FullFetch(QDialog): # {{{
# }}}
if __name__ == '__main__':
DEVELOP_DIALOG = True
app = QApplication([])
d = FullFetch(Log())
d.start(title='great gatsby', authors=['Fitzgerald'])