diff --git a/recipes/economist.recipe b/recipes/economist.recipe
index 9447fe2193..894f5880b3 100644
--- a/recipes/economist.recipe
+++ b/recipes/economist.recipe
@@ -18,7 +18,8 @@ class Economist(BasicNewsRecipe):
__author__ = "Kovid Goyal"
INDEX = 'http://www.economist.com/printedition'
- description = 'Global news and current affairs from a European perspective.'
+ description = ('Global news and current affairs from a European'
+ ' perspective. Best downloaded on Friday mornings (GMT)')
oldest_article = 7.0
cover_url = 'http://www.economist.com/images/covers/currentcoverus_large.jpg'
diff --git a/recipes/economist_free.recipe b/recipes/economist_free.recipe
index d1766211d7..4f060dc487 100644
--- a/recipes/economist_free.recipe
+++ b/recipes/economist_free.recipe
@@ -11,7 +11,8 @@ class Economist(BasicNewsRecipe):
language = 'en'
__author__ = "Kovid Goyal"
- description = ('Global news and current affairs from a European perspective.'
+ description = ('Global news and current affairs from a European'
+ ' perspective. Best downloaded on Friday mornings (GMT).'
' Much slower than the print edition based version.')
oldest_article = 7.0
diff --git a/recipes/financial_times.recipe b/recipes/financial_times.recipe
index 25efc56e45..0e3c91d3e3 100644
--- a/recipes/financial_times.recipe
+++ b/recipes/financial_times.recipe
@@ -11,7 +11,8 @@ from calibre.web.feeds.news import BasicNewsRecipe
class FinancialTimes(BasicNewsRecipe):
title = u'Financial Times'
__author__ = 'Darko Miletic and Sujata Raman'
- description = 'Financial world news'
+ description = ('Financial world news. Available after 5AM '
+ 'GMT, daily.')
oldest_article = 2
language = 'en'
diff --git a/src/calibre/ebooks/metadata/sources/identify.py b/src/calibre/ebooks/metadata/sources/identify.py
index 8c6172f0e2..85549904e7 100644
--- a/src/calibre/ebooks/metadata/sources/identify.py
+++ b/src/calibre/ebooks/metadata/sources/identify.py
@@ -217,6 +217,10 @@ class ISBNMerge(object):
for r in results:
ans.identifiers.update(r.identifiers)
+ # Cover URL
+ ans.has_cached_cover_url = bool([r for r in results if
+ getattr(r, 'has_cached_cover_url', False)])
+
# Merge any other fields with no special handling (random merge)
touched_fields = set()
for r in results:
@@ -253,10 +257,10 @@ def identify(log, abort, # {{{
plugins = [p for p in metadata_plugins(['identify']) if p.is_configured()]
kwargs = {
- 'title': title,
- 'authors': authors,
- 'identifiers': identifiers,
- 'timeout': timeout,
+ 'title': title,
+ 'authors': authors,
+ 'identifiers': identifiers,
+ 'timeout': timeout,
}
log('Running identify query with parameters:')
diff --git a/src/calibre/gui2/actions/__init__.py b/src/calibre/gui2/actions/__init__.py
index c0dd40326d..a68659864a 100644
--- a/src/calibre/gui2/actions/__init__.py
+++ b/src/calibre/gui2/actions/__init__.py
@@ -145,11 +145,10 @@ class InterfaceAction(QObject):
ans[candidate] = zf.read(candidate)
return ans
-
def genesis(self):
'''
Setup this plugin. Only called once during initialization. self.gui is
- available. The action secified by :attr:`action_spec` is available as
+ available. The action specified by :attr:`action_spec` is available as
``self.qaction``.
'''
pass
diff --git a/src/calibre/gui2/actions/choose_library.py b/src/calibre/gui2/actions/choose_library.py
index 6f4ca624cb..fd7959a30e 100644
--- a/src/calibre/gui2/actions/choose_library.py
+++ b/src/calibre/gui2/actions/choose_library.py
@@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en'
import os, shutil
from functools import partial
-from PyQt4.Qt import QMenu, Qt, QInputDialog
+from PyQt4.Qt import QMenu, Qt, QInputDialog, QToolButton
from calibre import isbytestring
from calibre.constants import filesystem_encoding
@@ -88,6 +88,9 @@ class ChooseLibraryAction(InterfaceAction):
type=Qt.QueuedConnection)
self.stats = LibraryUsageStats()
+ self.popup_type = (QToolButton.InstantPopup if len(self.stats.stats) > 1 else
+ QToolButton.MenuButtonPopup)
+
self.create_action(spec=(_('Switch/create library...'), 'lt.png', None,
None), attr='action_choose')
self.action_choose.triggered.connect(self.choose_library,
@@ -123,6 +126,7 @@ class ChooseLibraryAction(InterfaceAction):
type=Qt.QueuedConnection)
self.choose_menu.addAction(ac)
+
self.rename_separator = self.choose_menu.addSeparator()
self.maintenance_menu = QMenu(_('Library Maintenance'))
@@ -172,6 +176,7 @@ class ChooseLibraryAction(InterfaceAction):
return
db = self.gui.library_view.model().db
locations = list(self.stats.locations(db))
+
for ac in self.switch_actions:
ac.setVisible(False)
self.quick_menu.clear()
diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py
index 41b415e25c..29c2cd4a7a 100644
--- a/src/calibre/gui2/layout.py
+++ b/src/calibre/gui2/layout.py
@@ -7,9 +7,9 @@ __docformat__ = 'restructuredtext en'
from functools import partial
-from PyQt4.Qt import QIcon, Qt, QWidget, QToolBar, QSize, \
- pyqtSignal, QToolButton, QMenu, \
- QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup
+from PyQt4.Qt import (QIcon, Qt, QWidget, QToolBar, QSize,
+ pyqtSignal, QToolButton, QMenu,
+ QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup)
from calibre.constants import __appname__
@@ -264,11 +264,11 @@ class ToolBar(QToolBar): # {{{
def apply_settings(self):
sz = gprefs['toolbar_icon_size']
- sz = {'small':24, 'medium':48, 'large':64}[sz]
+ sz = {'off':0, 'small':24, 'medium':48, 'large':64}[sz]
self.setIconSize(QSize(sz, sz))
self.child_bar.setIconSize(QSize(sz, sz))
style = Qt.ToolButtonTextUnderIcon
- if gprefs['toolbar_text'] == 'never':
+ if sz > 0 and gprefs['toolbar_text'] == 'never':
style = Qt.ToolButtonIconOnly
self.setToolButtonStyle(style)
self.child_bar.setToolButtonStyle(style)
diff --git a/src/calibre/gui2/metadata/single_download.py b/src/calibre/gui2/metadata/single_download.py
index 049ac611c5..eb067e1211 100644
--- a/src/calibre/gui2/metadata/single_download.py
+++ b/src/calibre/gui2/metadata/single_download.py
@@ -11,23 +11,18 @@ from threading import Thread, Event
from PyQt4.Qt import (QStyledItemDelegate, QTextDocument, QRectF, QIcon, Qt,
QStyle, QApplication, QDialog, QVBoxLayout, QLabel, QDialogButtonBox,
- QStackedWidget, QWidget, QTableView, QGridLayout, QFontInfo, QPalette)
+ QStackedWidget, QWidget, QTableView, QGridLayout, QFontInfo, QPalette,
+ QTimer, pyqtSignal, QAbstractTableModel, QVariant, QSize)
from PyQt4.QtWebKit import QWebView
from calibre.customize.ui import metadata_plugins
from calibre.ebooks.metadata import authors_to_string
-from calibre.utils.logging import ThreadSafeLog, UnicodeHTMLStream
+from calibre.utils.logging import GUILog as Log
from calibre.ebooks.metadata.sources.identify import identify
+from calibre.ebooks.metadata.book.base import Metadata
+from calibre.gui2 import error_dialog, NONE
+from calibre.utils.date import utcnow, fromordinal, format_date
-class Log(ThreadSafeLog): # {{{
-
- def __init__(self):
- ThreadSafeLog.__init__(self, level=self.DEBUG)
- self.outputs = [UnicodeHTMLStream()]
-
- def clear(self):
- self.outputs[0].clear()
-# }}}
class RichTextDelegate(QStyledItemDelegate): # {{{
@@ -56,18 +51,91 @@ class RichTextDelegate(QStyledItemDelegate): # {{{
painter.restore()
# }}}
-class ResultsView(QTableView):
+class ResultsModel(QAbstractTableModel):
+
+ COLUMNS = (
+ '#', _('Title'), _('Published'), _('Has cover'), _('Has summary')
+ )
+ HTML_COLS = (1, 2)
+ ICON_COLS = (3, 4)
+
+ def __init__(self, results, parent=None):
+ QAbstractTableModel.__init__(self, parent)
+ self.results = results
+ self.yes_icon = QVariant(QIcon(I('ok.png')))
+
+ def rowCount(self, parent=None):
+ return len(self.results)
+
+ def columnCount(self, parent=None):
+ return len(self.COLUMNS)
+
+ def headerData(self, section, orientation, role):
+ if orientation == Qt.Horizontal and role == Qt.DisplayRole:
+ try:
+ return QVariant(self.COLUMNS[section])
+ except:
+ return NONE
+ return NONE
+
+ def data_as_text(self, book, col):
+ if col == 0:
+ return unicode(book.gui_rank+1)
+ if col == 1:
+ t = book.title if book.title else _('Unknown')
+ a = authors_to_string(book.authors) if book.authors else ''
+ return '%s
%s' % (t, a)
+ if col == 2:
+ d = format_date(book.pubdate, 'yyyy') if book.pubdate else _('Unknown')
+ p = book.publisher if book.publisher else ''
+ return '%s
%s' % (d, p)
+
+
+ def data(self, index, role):
+ row, col = index.row(), index.column()
+ try:
+ book = self.results[row]
+ except:
+ return NONE
+ if role == Qt.DisplayRole and col not in self.ICON_COLS:
+ res = self.data_as_text(book, col)
+ if res:
+ return QVariant(res)
+ return NONE
+ elif role == Qt.DecorationRole and col in self.ICON_COLS:
+ if col == 3 and getattr(book, 'has_cached_cover_url', False):
+ return self.yes_icon
+ if col == 4 and book.comments:
+ return self.yes_icon
+ return NONE
+
+class ResultsView(QTableView): # {{{
def __init__(self, parent=None):
QTableView.__init__(self, parent)
+ self.rt_delegate = RichTextDelegate(self)
+ self.setSelectionMode(self.SingleSelection)
+ self.setAlternatingRowColors(True)
+ self.setSelectionBehavior(self.SelectRows)
+ self.setIconSize(QSize(24, 24))
+
+ def show_results(self, results):
+ self._model = ResultsModel(results, self)
+ self.setModel(self._model)
+ for i in self._model.HTML_COLS:
+ self.setItemDelegateForColumn(i, self.rt_delegate)
+ self.resizeRowsToContents()
+ self.resizeColumnsToContents()
+
+# }}}
class Comments(QWebView): # {{{
def __init__(self, parent=None):
QWebView.__init__(self, parent)
self.setAcceptDrops(False)
- self.setMaximumWidth(270)
- self.setMinimumWidth(270)
+ self.setMaximumWidth(300)
+ self.setMinimumWidth(300)
palette = self.palette()
palette.setBrush(QPalette.Base, Qt.transparent)
@@ -109,7 +177,7 @@ class Comments(QWebView): # {{{
self.setHtml(templ%html)
# }}}
-class IdentifyWorker(Thread):
+class IdentifyWorker(Thread): # {{{
def __init__(self, log, abort, title, authors, identifiers):
Thread.__init__(self)
@@ -122,17 +190,40 @@ class IdentifyWorker(Thread):
self.results = []
self.error = None
+ def sample_results(self):
+ m1 = Metadata('The Great Gatsby', ['Francis Scott Fitzgerald'])
+ m2 = Metadata('The Great Gatsby', ['F. Scott Fitzgerald'])
+ m1.has_cached_cover_url = True
+ m2.has_cached_cover_url = False
+ m1.comments = 'Some comments '*10
+ m1.tags = ['tag%d'%i for i in range(20)]
+ m1.rating = 4.4
+ m1.language = 'en'
+ m2.language = 'fr'
+ m1.pubdate = utcnow()
+ m2.pubdate = fromordinal(1000000)
+ m1.publisher = 'Publisher 1'
+ m2.publisher = 'Publisher 2'
+
+ return [m1, m2]
+
def run(self):
try:
- self.results = identify(self.log, self.abort, title=self.title,
- authors=self.authors, identifiers=self.identifiers)
+ if True:
+ self.results = self.sample_results()
+ else:
+ self.results = identify(self.log, self.abort, title=self.title,
+ authors=self.authors, identifiers=self.identifiers)
for i, result in enumerate(self.results):
result.gui_rank = i
except:
import traceback
self.error = traceback.format_exc()
+# }}}
-class IdentifyWidget(QWidget):
+class IdentifyWidget(QWidget): # {{{
+
+ rejected = pyqtSignal()
def __init__(self, log, parent=None):
QWidget.__init__(self, parent)
@@ -197,7 +288,48 @@ class IdentifyWidget(QWidget):
self.worker = IdentifyWorker(self.log, self.abort, title,
authors, identifiers)
- # self.worker.start()
+ self.worker.start()
+
+ QTimer.singleShot(50, self.update)
+
+ def update(self):
+ if self.worker.is_alive():
+ QTimer.singleShot(50, self.update)
+ else:
+ self.process_results()
+
+ def process_results(self):
+ if self.worker.error is not None:
+ error_dialog(self, _('Download failed'),
+ _('Failed to download metadata. Click '
+ 'Show Details to see details'),
+ show=True, det_msg=self.worker.error)
+ self.rejected.emit()
+ return
+
+ if not self.worker.results:
+ log = ''.join(self.log.plain_text)
+ error_dialog(self, _('No matches found'), '
' + + _('Failed to find any books that ' + 'match your search. Try making the search less ' + 'specific. For example, use only the author\'s ' + 'last name and a single distinctive word from ' + 'the title.
To see the full log, click Show Details.'), + show=True, det_msg=log) + self.rejected.emit() + return + + self.results_view.show_results(self.worker.results) + + self.comments_view.show_data(''' +