From 0ba4f9a8adc6f9b2e4b77aa701a214c890f4fb99 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 7 Apr 2011 09:33:47 -0600 Subject: [PATCH 01/11] ... --- recipes/economist.recipe | 3 ++- recipes/economist_free.recipe | 3 ++- recipes/financial_times.recipe | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) 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' From ebe5b28567e467df846ca15bb8c31123b5106026 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 7 Apr 2011 11:01:08 -0600 Subject: [PATCH 02/11] ... --- .../ebooks/metadata/sources/identify.py | 8 +-- src/calibre/gui2/metadata/single_download.py | 64 +++++++++++++++---- src/calibre/utils/logging.py | 24 +++++++ 3 files changed, 78 insertions(+), 18 deletions(-) diff --git a/src/calibre/ebooks/metadata/sources/identify.py b/src/calibre/ebooks/metadata/sources/identify.py index 8c6172f0e2..075c780596 100644 --- a/src/calibre/ebooks/metadata/sources/identify.py +++ b/src/calibre/ebooks/metadata/sources/identify.py @@ -253,10 +253,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/metadata/single_download.py b/src/calibre/gui2/metadata/single_download.py index 049ac611c5..19e92adf4f 100644 --- a/src/calibre/gui2/metadata/single_download.py +++ b/src/calibre/gui2/metadata/single_download.py @@ -11,23 +11,16 @@ 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) 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.gui2 import error_dialog -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,10 +49,11 @@ class RichTextDelegate(QStyledItemDelegate): # {{{ painter.restore() # }}} -class ResultsView(QTableView): +class ResultsView(QTableView): # {{{ def __init__(self, parent=None): QTableView.__init__(self, parent) +# }}} class Comments(QWebView): # {{{ @@ -109,7 +103,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) @@ -131,8 +125,11 @@ class IdentifyWorker(Thread): 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) @@ -199,6 +196,40 @@ class IdentifyWidget(QWidget): # self.worker.start() + QTimer.singleShot(50, self.update) + + def update(self): + if self.worker.is_alive(): + QTimer.singleShot(50, self.update) + return + 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.wroker.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 + + + def cancel(self): + self.abort.set() +# }}} + class FullFetch(QDialog): # {{{ def __init__(self, log, parent=None): @@ -218,6 +249,7 @@ class FullFetch(QDialog): # {{{ self.bb.rejected.connect(self.reject) self.identify_widget = IdentifyWidget(log, self) + self.identify_widget.rejected.connect(self.reject) self.stack.addWidget(self.identify_widget) self.resize(850, 500) @@ -225,6 +257,10 @@ class FullFetch(QDialog): # {{{ # Prevent pressing Enter from closing the dialog pass + def reject(self): + self.identify_widget.cancel() + return QDialog.reject(self) + def start(self, title=None, authors=None, identifiers={}): self.identify_widget.start(title=title, authors=authors, identifiers=identifiers) diff --git a/src/calibre/utils/logging.py b/src/calibre/utils/logging.py index 45e21ded39..dbbca6806b 100644 --- a/src/calibre/utils/logging.py +++ b/src/calibre/utils/logging.py @@ -108,10 +108,13 @@ class UnicodeHTMLStream(HTMLStream): elif not isinstance(arg, unicode): arg = as_unicode(arg) self.data.append(arg+sep) + self.plain_text.append(arg+sep) self.data.append(end) + self.plain_text.append(end) def clear(self): self.data = [] + self.plain_text = [] self.last_col = self.color[INFO] @property @@ -162,4 +165,25 @@ class ThreadSafeLog(Log): with self._lock: Log.prints(self, *args, **kwargs) +class GUILog(ThreadSafeLog): + + ''' + Logs in HTML and plain text as unicode. Ideal for display in a GUI context. + ''' + + def __init__(self): + ThreadSafeLog.__init__(self, level=self.DEBUG) + self.outputs = [UnicodeHTMLStream()] + + def clear(self): + self.outputs[0].clear() + + @property + def html(self): + return self.outputs[0].html + + @property + def plain_text(self): + return u''.join(self.outputs[0].plain_text) + default_log = Log() From 903f628f70045d5bdb41b6ab19a0e7bdee29d8da Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 7 Apr 2011 11:59:47 -0600 Subject: [PATCH 03/11] ... --- .../ebooks/metadata/sources/identify.py | 4 + src/calibre/gui2/metadata/single_download.py | 119 ++++++++++++++++-- 2 files changed, 113 insertions(+), 10 deletions(-) diff --git a/src/calibre/ebooks/metadata/sources/identify.py b/src/calibre/ebooks/metadata/sources/identify.py index 075c780596..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: diff --git a/src/calibre/gui2/metadata/single_download.py b/src/calibre/gui2/metadata/single_download.py index 19e92adf4f..94ee8f8a13 100644 --- a/src/calibre/gui2/metadata/single_download.py +++ b/src/calibre/gui2/metadata/single_download.py @@ -12,14 +12,16 @@ 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, - QTimer, pyqtSignal) + 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 GUILog as Log from calibre.ebooks.metadata.sources.identify import identify -from calibre.gui2 import error_dialog +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 RichTextDelegate(QStyledItemDelegate): # {{{ @@ -49,10 +51,85 @@ class RichTextDelegate(QStyledItemDelegate): # {{{ painter.restore() # }}} +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 role != Qt.DisplayRole: + return NONE + if orientation == Qt.Horizontal: + try: + return QVariant(self.COLUMNS[section]) + except: + return NONE + else: + return QVariant(unicode(section+1)) + + 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): # {{{ @@ -60,8 +137,8 @@ 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) @@ -116,10 +193,30 @@ 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: @@ -194,22 +291,22 @@ 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) - return - self.process_results() + 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.wroker.error) + show=True, det_msg=self.worker.error) self.rejected.emit() return @@ -225,6 +322,8 @@ class IdentifyWidget(QWidget): # {{{ self.rejected.emit() return + self.results_view.show_results(self.worker.results) + def cancel(self): self.abort.set() From f1210c3444f29acb175237b080eb0696f5562b6f Mon Sep 17 00:00:00 2001 From: John Schember Date: Thu, 7 Apr 2011 18:17:28 -0400 Subject: [PATCH 04/11] Disable toolbar icons, forces icon text to be displayed when enabled. --- src/calibre/gui2/layout.py | 4 ++-- src/calibre/gui2/preferences/look_feel.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index 41b415e25c..c73bb0d0fa 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -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/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py index 523a296a37..71b9e38667 100644 --- a/src/calibre/gui2/preferences/look_feel.py +++ b/src/calibre/gui2/preferences/look_feel.py @@ -49,8 +49,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): r('use_roman_numerals_for_series_number', config) r('separate_cover_flow', config, restart_required=True) - choices = [(_('Small'), 'small'), (_('Medium'), 'medium'), - (_('Large'), 'large')] + choices = [(_('Off'), 'off'), (_('Small'), 'small'), + (_('Medium'), 'medium'), (_('Large'), 'large')] r('toolbar_icon_size', gprefs, choices=choices) choices = [(_('Automatic'), 'auto'), (_('Always'), 'always'), From 7fe19d6774c4c00ae86592285537aa5a0777c9e9 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 7 Apr 2011 18:04:22 -0600 Subject: [PATCH 05/11] Fix #754154 (Option to change default Library button behaviour) --- src/calibre/gui2/actions/__init__.py | 1 - src/calibre/gui2/actions/choose_library.py | 7 ++++++- src/calibre/gui2/layout.py | 6 +++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/calibre/gui2/actions/__init__.py b/src/calibre/gui2/actions/__init__.py index c0dd40326d..589e14cff2 100644 --- a/src/calibre/gui2/actions/__init__.py +++ b/src/calibre/gui2/actions/__init__.py @@ -145,7 +145,6 @@ class InterfaceAction(QObject): ans[candidate] = zf.read(candidate) return ans - def genesis(self): ''' Setup this plugin. Only called once during initialization. self.gui is 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 c73bb0d0fa..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__ From 638f70b640c605ae3661c1760f14347399e44fb4 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 7 Apr 2011 18:09:39 -0600 Subject: [PATCH 06/11] ... --- src/calibre/gui2/actions/__init__.py | 2 +- src/calibre/gui2/metadata/single_download.py | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/calibre/gui2/actions/__init__.py b/src/calibre/gui2/actions/__init__.py index 589e14cff2..a68659864a 100644 --- a/src/calibre/gui2/actions/__init__.py +++ b/src/calibre/gui2/actions/__init__.py @@ -148,7 +148,7 @@ class InterfaceAction(QObject): 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/metadata/single_download.py b/src/calibre/gui2/metadata/single_download.py index 94ee8f8a13..eb067e1211 100644 --- a/src/calibre/gui2/metadata/single_download.py +++ b/src/calibre/gui2/metadata/single_download.py @@ -71,15 +71,12 @@ class ResultsModel(QAbstractTableModel): return len(self.COLUMNS) def headerData(self, section, orientation, role): - if role != Qt.DisplayRole: - return NONE - if orientation == Qt.Horizontal: + if orientation == Qt.Horizontal and role == Qt.DisplayRole: try: return QVariant(self.COLUMNS[section]) except: return NONE - else: - return QVariant(unicode(section+1)) + return NONE def data_as_text(self, book, col): if col == 0: @@ -324,6 +321,11 @@ class IdentifyWidget(QWidget): # {{{ self.results_view.show_results(self.worker.results) + self.comments_view.show_data(''' +

Found %d results
+
To see details, click on any result
''' % + len(self.worker.results)) + def cancel(self): self.abort.set() From 90a5a5ad41478363144466e2d35bcf7ed2ff54a3 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 7 Apr 2011 22:51:05 -0600 Subject: [PATCH 07/11] ... --- src/calibre/gui2/metadata/single_download.py | 96 +++++++++++++++++++- 1 file changed, 93 insertions(+), 3 deletions(-) diff --git a/src/calibre/gui2/metadata/single_download.py b/src/calibre/gui2/metadata/single_download.py index eb067e1211..79a1db131b 100644 --- a/src/calibre/gui2/metadata/single_download.py +++ b/src/calibre/gui2/metadata/single_download.py @@ -8,6 +8,7 @@ __copyright__ = '2011, Kovid Goyal ' __docformat__ = 'restructuredtext en' from threading import Thread, Event +from operator import attrgetter from PyQt4.Qt import (QStyledItemDelegate, QTextDocument, QRectF, QIcon, Qt, QStyle, QApplication, QDialog, QVBoxLayout, QLabel, QDialogButtonBox, @@ -22,7 +23,7 @@ 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 - +from calibre.library.comments import comments_to_html class RichTextDelegate(QStyledItemDelegate): # {{{ @@ -107,10 +108,33 @@ class ResultsModel(QAbstractTableModel): return self.yes_icon if col == 4 and book.comments: return self.yes_icon + elif role == Qt.UserRole: + return book return NONE + def sort(self, col, order=Qt.AscendingOrder): + key = lambda x: x + if col == 0: + key = attrgetter('gui_rank') + elif col == 1: + key = attrgetter('title') + elif col == 2: + key = attrgetter('authors') + elif col == 3: + key = attrgetter('has_cached_cover_url') + elif key == 4: + key = lambda x: bool(x.comments) + + self.results.sort(key=key, reverse=order==Qt.AscendingOrder) + self.reset() + + + class ResultsView(QTableView): # {{{ + show_details_signal = pyqtSignal(object) + book_selected = pyqtSignal(object) + def __init__(self, parent=None): QTableView.__init__(self, parent) self.rt_delegate = RichTextDelegate(self) @@ -118,6 +142,9 @@ class ResultsView(QTableView): # {{{ self.setAlternatingRowColors(True) self.setSelectionBehavior(self.SelectRows) self.setIconSize(QSize(24, 24)) + self.clicked.connect(self.show_details) + self.doubleClicked.connect(self.select_index) + self.setSortingEnabled(True) def show_results(self, results): self._model = ResultsModel(results, self) @@ -126,6 +153,38 @@ class ResultsView(QTableView): # {{{ self.setItemDelegateForColumn(i, self.rt_delegate) self.resizeRowsToContents() self.resizeColumnsToContents() + self.setFocus(Qt.OtherFocusReason) + + def currentChanged(self, current, previous): + ret = QTableView.currentChanged(self, current, previous) + self.show_details(current) + return ret + + def show_details(self, index): + book = self.model().data(index, Qt.UserRole) + parts = [ + '
', + '

%s

'%book.title, + '
%s
'%authors_to_string(book.authors), + ] + if not book.is_null('rating'): + parts.append('
%s
'%('\u2605'*int(book.rating))) + parts.append('
') + if book.tags: + parts.append('
%s
\u00a0
'%', '.join(book.tags)) + if book.comments: + parts.append(comments_to_html(book.comments)) + + self.show_details_signal.emit(''.join(parts)) + + def select_index(self, index): + if not index.isValid(): + index = self.model().index(0, 0) + book = self.model().data(index, Qt.UserRole) + self.book_selected.emit(book) + + def get_result(self): + self.select_index(self.currentIndex()) # }}} @@ -224,6 +283,8 @@ class IdentifyWorker(Thread): # {{{ class IdentifyWidget(QWidget): # {{{ rejected = pyqtSignal() + results_found = pyqtSignal() + book_selected = pyqtSignal(object) def __init__(self, log, parent=None): QWidget.__init__(self, parent) @@ -241,11 +302,15 @@ class IdentifyWidget(QWidget): # {{{ l.addWidget(self.top, 0, 0) self.results_view = ResultsView(self) + self.results_view.book_selected.connect(self.book_selected.emit) + self.get_result = self.results_view.get_result l.addWidget(self.results_view, 1, 0) self.comments_view = Comments(self) l.addWidget(self.comments_view, 1, 1) + self.results_view.show_details_signal.connect(self.comments_view.show_data) + self.query = QLabel('download starting...') f = self.query.font() f.setPointSize(f.pointSize()-2) @@ -326,6 +391,8 @@ class IdentifyWidget(QWidget): # {{{
To see details, click on any result
''' % len(self.worker.results)) + self.results_found.emit() + def cancel(self): self.abort.set() @@ -345,23 +412,46 @@ class FullFetch(QDialog): # {{{ self.setLayout(l) l.addWidget(self.stack) - self.bb = QDialogButtonBox(QDialogButtonBox.Cancel) + self.bb = QDialogButtonBox(QDialogButtonBox.Cancel|QDialogButtonBox.Ok) l.addWidget(self.bb) self.bb.rejected.connect(self.reject) + self.next_button = self.bb.addButton(_('Next'), self.bb.AcceptRole) + self.next_button.setDefault(True) + self.next_button.setEnabled(False) + self.next_button.clicked.connect(self.next_clicked) + self.ok_button = self.bb.button(self.bb.Ok) + self.ok_button.setVisible(False) + self.ok_button.clicked.connect(self.ok_clicked) self.identify_widget = IdentifyWidget(log, self) self.identify_widget.rejected.connect(self.reject) + self.identify_widget.results_found.connect(self.identify_results_found) + self.identify_widget.book_selected.connect(self.book_selected) self.stack.addWidget(self.identify_widget) self.resize(850, 500) + def book_selected(self, book): + print (book) + self.next_button.setVisible(False) + self.ok_button.setVisible(True) + def accept(self): - # Prevent pressing Enter from closing the dialog + # Prevent the usual dialog accept mechanisms from working pass def reject(self): self.identify_widget.cancel() return QDialog.reject(self) + def identify_results_found(self): + self.next_button.setEnabled(True) + + def next_clicked(self, *args): + self.identify_widget.get_result() + + def ok_clicked(self, *args): + pass + def start(self, title=None, authors=None, identifiers={}): self.identify_widget.start(title=title, authors=authors, identifiers=identifiers) From 77df277db3e9dff36f280d67ea931ee4d16c524b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 8 Apr 2011 07:28:28 -0600 Subject: [PATCH 08/11] Conversion pipeline: Workaround for bug in lxml that causes a massive mem leak on windows and OS X when the input document contains non ASCII CSS selectors. Fixes #754555 (Private bug) --- src/calibre/ebooks/oeb/stylizer.py | 4 ++++ src/calibre/gui2/metadata/single_download.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/calibre/ebooks/oeb/stylizer.py b/src/calibre/ebooks/oeb/stylizer.py index 42974be355..634f7f5fce 100644 --- a/src/calibre/ebooks/oeb/stylizer.py +++ b/src/calibre/ebooks/oeb/stylizer.py @@ -97,6 +97,10 @@ class CSSSelector(etree.XPath): def __init__(self, css, namespaces=XPNSMAP): css = self.MIN_SPACE_RE.sub(r'\1', css) + if isinstance(css, unicode): + # Workaround for bug in lxml on windows/OS X that causes a massive + # memory leak with non ASCII selectors + css = css.encode('ascii', 'ignore').decode('ascii') try: path = css_to_xpath(css) except UnicodeEncodeError: # Bug in css_to_xpath diff --git a/src/calibre/gui2/metadata/single_download.py b/src/calibre/gui2/metadata/single_download.py index 79a1db131b..176c164d3d 100644 --- a/src/calibre/gui2/metadata/single_download.py +++ b/src/calibre/gui2/metadata/single_download.py @@ -52,7 +52,7 @@ class RichTextDelegate(QStyledItemDelegate): # {{{ painter.restore() # }}} -class ResultsModel(QAbstractTableModel): +class ResultsModel(QAbstractTableModel): # {{{ COLUMNS = ( '#', _('Title'), _('Published'), _('Has cover'), _('Has summary') @@ -128,7 +128,7 @@ class ResultsModel(QAbstractTableModel): self.results.sort(key=key, reverse=order==Qt.AscendingOrder) self.reset() - +# }}} class ResultsView(QTableView): # {{{ From 9c8a97855b192a35faadfe27f0e6ec89519d70d0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 8 Apr 2011 07:44:51 -0600 Subject: [PATCH 09/11] Fix #754483 (Comments not updating in book details after edit metadata) --- src/calibre/gui2/actions/edit_metadata.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/calibre/gui2/actions/edit_metadata.py b/src/calibre/gui2/actions/edit_metadata.py index c3ceb27e7e..d0e3732231 100644 --- a/src/calibre/gui2/actions/edit_metadata.py +++ b/src/calibre/gui2/actions/edit_metadata.py @@ -183,6 +183,7 @@ class EditMetadataAction(InterfaceAction): current_row += d.row_delta self.gui.library_view.set_current_row(current_row) self.gui.library_view.scroll_to_row(current_row) + return changed def do_edit_metadata(self, row_list, current_row): from calibre.gui2.metadata.single import edit_metadata From 9e3008c484f5b838fd36dc55094a63da011200d9 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 8 Apr 2011 07:51:54 -0600 Subject: [PATCH 10/11] Al Ahram by Hassan Williamson --- recipes/al_ahram.recipe | 62 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 recipes/al_ahram.recipe diff --git a/recipes/al_ahram.recipe b/recipes/al_ahram.recipe new file mode 100644 index 0000000000..8850e82ebf --- /dev/null +++ b/recipes/al_ahram.recipe @@ -0,0 +1,62 @@ +# coding=utf-8 +__license__ = 'GPL v3' +__copyright__ = '2011, Hassan Williamson ' +''' +ahram.org.eg +''' +from calibre.web.feeds.recipes import BasicNewsRecipe + +class AlAhram(BasicNewsRecipe): + title = 'Al-Ahram' + __author__ = 'Hassan Williamson' + description = 'News from Egypt in Arabic.' + oldest_article = 7 + max_articles_per_feed = 100 + no_stylesheets = True + #delay = 1 + use_embedded_content = False + encoding = 'utf8' + publisher = 'Al-Ahram' + category = 'News' + language = 'ar' + publication_type = 'newsportal' + extra_css = ' body{ font-family: Verdana,Helvetica,Arial,sans-serif; direction: rtl; } .txtTitle{ font-weight: bold; } ' + + + keep_only_tags = [ + dict(name='div', attrs={'class':['bbcolright']}) + ] + + remove_tags = [ + dict(name='div', attrs={'class':['bbnav', 'bbsp']}), + dict(name='div', attrs={'id':['AddThisButton']}) + ] + + remove_attributes = [ + 'width','height' + ] + + feeds = [ + (u'الأولى', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=25'), + (u'مصر', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=27'), + (u'المحافظات', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=29'), + (u'الوطن العربي', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=31'), + (u'العالم', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=26'), + (u'تقارير المراسلين', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=2'), + (u'تحقيقات', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=3'), + (u'قضايا واراء', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=4'), + (u'اقتصاد', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=5'), + (u'رياضة', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=6'), + (u'حوادث', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=38'), + (u'دنيا الثقافة', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=7'), + (u'المراة والطفل', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=8'), + (u'يوم جديد', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=9'), + (u'الكتاب', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=10'), + (u'الاعمدة', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=11'), + (u'أراء حرة', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=59'), + (u'ملفات الاهرام', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=12'), + (u'بريد الاهرام', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=15'), + (u'الاخيرة', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=16'), + ] + + From bc0cec05fa3cf728f26542cc647d64f198b167bf Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 8 Apr 2011 08:07:33 -0600 Subject: [PATCH 11/11] ... --- src/calibre/gui2/actions/edit_metadata.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/calibre/gui2/actions/edit_metadata.py b/src/calibre/gui2/actions/edit_metadata.py index d0e3732231..e29e6c344d 100644 --- a/src/calibre/gui2/actions/edit_metadata.py +++ b/src/calibre/gui2/actions/edit_metadata.py @@ -141,15 +141,18 @@ class EditMetadataAction(InterfaceAction): list(range(self.gui.library_view.model().rowCount(QModelIndex()))) current_row = row_list.index(cr) - if test_eight_code: - changed = self.do_edit_metadata(row_list, current_row) - else: - changed = self.do_edit_metadata_old(row_list, current_row) + func = (self.do_edit_metadata if test_eight_code else + self.do_edit_metadata_old) + changed, rows_to_refresh = func(row_list, current_row) + + m = self.gui.library_view.model() + + if rows_to_refresh: + m.refresh_rows(rows_to_refresh) if changed: - self.gui.library_view.model().refresh_ids(list(changed)) + m.refresh_ids(list(changed)) current = self.gui.library_view.currentIndex() - m = self.gui.library_view.model() if self.gui.cover_flow: self.gui.cover_flow.dataChanged() m.current_changed(current, previous) @@ -183,7 +186,7 @@ class EditMetadataAction(InterfaceAction): current_row += d.row_delta self.gui.library_view.set_current_row(current_row) self.gui.library_view.scroll_to_row(current_row) - return changed + return changed, set() def do_edit_metadata(self, row_list, current_row): from calibre.gui2.metadata.single import edit_metadata @@ -191,7 +194,7 @@ class EditMetadataAction(InterfaceAction): changed, rows_to_refresh = edit_metadata(db, row_list, current_row, parent=self.gui, view_slot=self.view_format_callback, set_current_callback=self.set_current_callback) - return changed + return changed, rows_to_refresh def set_current_callback(self, id_): db = self.gui.library_view.model().db