diff --git a/resources/images/spinner.gif b/resources/images/spinner.gif deleted file mode 100644 index 5e864f2e37..0000000000 Binary files a/resources/images/spinner.gif and /dev/null differ diff --git a/src/calibre/gui2/metadata/single_download.py b/src/calibre/gui2/metadata/single_download.py index 2c9b234fba..c397a8660a 100644 --- a/src/calibre/gui2/metadata/single_download.py +++ b/src/calibre/gui2/metadata/single_download.py @@ -9,12 +9,13 @@ 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.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 @@ def sample_results(self): 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 @@ def run(self): 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 @@ def cancel(self): 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 @@ def __init__(self, log, current_cover, parent=None): 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 @@ def data(self, index, role): 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 @@ def __init__(self, log, current_cover, parent=None): 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 @@ def select(self, num): 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 @@ def __init__(self, log, current_cover, parent=None): 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 @@ def start(self, book, current_cover, title, authors): self.msg.setText('

'+_('Downloading covers for %s, 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 %s')%self.book.title + else: + txt = _('Found %d 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 @@ def __init__(self, log, current_cover=None, parent=None): 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 @@ def reject(self): 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 @@ def next_clicked(self, *args): 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 @@ def start(self, title=None, authors=None, identifiers={}): # }}} if __name__ == '__main__': + DEVELOP_DIALOG = True app = QApplication([]) d = FullFetch(Log()) d.start(title='great gatsby', authors=['Fitzgerald'])