mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-06-23 15:30:45 -04:00
Directly draw the spinner instead of using an animated GIF
This commit is contained in:
parent
3de4c6f7ca
commit
d2fe0bf7ee
Binary file not shown.
Before Width: | Height: | Size: 11 KiB |
@ -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'])
|
||||
|
Loading…
x
Reference in New Issue
Block a user