diff --git a/recipes/dvhn.recipe b/recipes/dvhn.recipe new file mode 100644 index 0000000000..4c093aa9d2 --- /dev/null +++ b/recipes/dvhn.recipe @@ -0,0 +1,32 @@ +from calibre.web.feeds.news import BasicNewsRecipe + +class AdvancedUserRecipe1302341394(BasicNewsRecipe): + title = u'DvhN' + oldest_article = 1 + max_articles_per_feed = 200 + + __author__ = 'Reijndert' + no_stylesheets = True + cover_url = 'http://www.dvhn.nl/template/Dagblad_v2.0/gfx/logo_DvhN.gif' + language = 'nl' + country = 'NL' + version = 1 + publisher = u'Dagblad van het Noorden' + category = u'Nieuws' + description = u'Nieuws uit Noord Nederland' + + + keep_only_tags = [dict(name='div', attrs={'id':'fullPicture'}) + ,dict(name='div', attrs={'id':'articleText'}) + ] + + remove_tags = [ + dict(name=['object','link','iframe','base']) + ,dict(name='span',attrs={'class':'copyright'}) + ] + + feeds = [(u'Drenthe', u'http://www.dvhn.nl/nieuws/drenthe/index.jsp?service=rss'), (u'Groningen', u'http://www.dvhn.nl/nieuws/groningen/index.jsp?service=rss'), (u'Nederland', u'http://www.dvhn.nl/nieuws/nederland/index.jsp?service=rss'), (u'Wereld', u'http://www.dvhn.nl/nieuws/wereld/index.jsp?service=rss'), (u'Economie', u'http://www.dvhn.nl/nieuws/economie/index.jsp?service=rss'), (u'Sport', u'http://www.dvhn.nl/nieuws/sport/index.jsp?service=rss'), (u'Cultuur', u'http://www.dvhn.nl/nieuws/kunst/index.jsp?service=rss'), (u'24 Uur', u'http://www.dvhn.nl/nieuws/24uurdvhn/index.jsp?service=rss&selectiontype=last24hours')] + + extra_css = ''' + body {font-family: verdana, arial, helvetica, geneva, sans-serif;} + ''' diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py index 1cf699efa3..c4c951f980 100644 --- a/resources/default_tweaks.py +++ b/resources/default_tweaks.py @@ -88,13 +88,6 @@ categories_collapsed_rating_template = r'{first.avg_rating:4.2f:ifempty(0)} - {l categories_collapsed_popularity_template = r'{first.count:d} - {last.count:d}' -#: Set boolean custom columns to be tristate -# Set whether boolean custom columns are two- or three-valued. -# Two-values for true booleans -# three-values for yes/no/unknown -# Set to 'yes' for three-values, 'no' for two-values -bool_custom_columns_are_tristate = 'yes' - #: Specify columns to sort the booklist by on startup # Provide a set of columns to be sorted on when calibre starts # The argument is None if saved sort history is to be used diff --git a/resources/images/connect_share_on.png b/resources/images/connect_share_on.png new file mode 100644 index 0000000000..3d431be1a1 Binary files /dev/null and b/resources/images/connect_share_on.png differ diff --git a/src/calibre/devices/edge/driver.py b/src/calibre/devices/edge/driver.py index d14763f313..9491b9bc68 100644 --- a/src/calibre/devices/edge/driver.py +++ b/src/calibre/devices/edge/driver.py @@ -26,9 +26,9 @@ class EDGE(USBMS): PRODUCT_ID = [0x0c02] BCD = [0x0223] - VENDOR_NAME = 'ANDROID' - WINDOWS_MAIN_MEM = '__FILE-STOR_GADG' - WINDOWS_CARD_A_MEM = '__FILE-STOR_GADG' + VENDOR_NAME = ['ANDROID', 'LINUX'] + WINDOWS_MAIN_MEM = ['__FILE-STOR_GADG', 'FILE-CD_GADGET'] + WINDOWS_CARD_A_MEM = ['__FILE-STOR_GADG', 'FILE-CD_GADGET'] MAIN_MEMORY_VOLUME_LABEL = 'Edge Main Memory' STORAGE_CARD_VOLUME_LABEL = 'Edge Storage Card' diff --git a/src/calibre/ebooks/metadata/sources/amazon.py b/src/calibre/ebooks/metadata/sources/amazon.py index b070132de9..4722873f77 100644 --- a/src/calibre/ebooks/metadata/sources/amazon.py +++ b/src/calibre/ebooks/metadata/sources/amazon.py @@ -279,7 +279,7 @@ class Worker(Thread): # Get details {{{ class Amazon(Source): - name = 'Amazon Store' + name = 'Amazon Web' description = _('Downloads metadata from Amazon') capabilities = frozenset(['identify', 'cover']) diff --git a/src/calibre/ebooks/pdf/pdftohtml.py b/src/calibre/ebooks/pdf/pdftohtml.py index 4ac1d0e368..4aa0953738 100644 --- a/src/calibre/ebooks/pdf/pdftohtml.py +++ b/src/calibre/ebooks/pdf/pdftohtml.py @@ -13,7 +13,7 @@ from functools import partial from calibre.ebooks import ConversionError, DRMError from calibre.ptempfile import PersistentTemporaryFile -from calibre import isosx, iswindows, islinux, isfreebsd +from calibre.constants import isosx, iswindows, islinux, isfreebsd from calibre import CurrentDir PDFTOHTML = 'pdftohtml' @@ -43,6 +43,8 @@ def pdftohtml(output_dir, pdf_path, no_images): # This is neccessary as pdftohtml doesn't always (linux) respect absolute paths pdf_path = os.path.abspath(pdf_path) cmd = [PDFTOHTML, '-enc', 'UTF-8', '-noframes', '-p', '-nomerge', '-nodrm', '-q', pdf_path, os.path.basename(index)] + if isfreebsd: + cmd.remove('-nodrm') if no_images: cmd.append('-i') diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 5b6430a5e6..22aaabf592 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -13,7 +13,7 @@ from PyQt4.Qt import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, \ ORG_NAME = 'KovidsBrain' APP_UID = 'libprs500' -from calibre.constants import islinux, iswindows, isfreebsd, isfrozen +from calibre.constants import islinux, iswindows, isfreebsd, isfrozen, isosx from calibre.utils.config import Config, ConfigProxy, dynamic, JSONConfig from calibre.utils.localization import set_qt_translator from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats @@ -23,25 +23,45 @@ from calibre.utils.date import UNDEFINED_DATE # Setup gprefs {{{ gprefs = JSONConfig('gui') -gprefs.defaults['action-layout-menubar'] = () - -gprefs.defaults['action-layout-menubar-device'] = () - -gprefs.defaults['action-layout-toolbar'] = ( +if isosx: + gprefs.defaults['action-layout-menubar'] = ( + 'Add Books', 'Edit Metadata', 'Convert Books', + 'Choose Library', 'Save To Disk', 'Preferences', + 'Help', + ) + gprefs.defaults['action-layout-menubar-device'] = ( + 'Add Books', 'Edit Metadata', 'Convert Books', + 'Location Manager', 'Send To Device', + 'Save To Disk', 'Preferences', 'Help', + ) + gprefs.defaults['action-layout-toolbar'] = ( + 'Add Books', 'Edit Metadata', None, 'Convert Books', 'View', None, + 'Choose Library', 'Donate', None, 'Fetch News', 'Save To Disk', + 'Connect Share', None, 'Remove Books', + ) + gprefs.defaults['action-layout-toolbar-device'] = ( + 'Add Books', 'Edit Metadata', None, 'Convert Books', 'View', + 'Send To Device', None, None, 'Location Manager', None, None, + 'Fetch News', 'Save To Disk', 'Connect Share', None, + 'Remove Books', + ) +else: + gprefs.defaults['action-layout-menubar'] = () + gprefs.defaults['action-layout-menubar-device'] = () + gprefs.defaults['action-layout-toolbar'] = ( 'Add Books', 'Edit Metadata', None, 'Convert Books', 'View', None, 'Choose Library', 'Donate', None, 'Fetch News', 'Save To Disk', 'Connect Share', None, 'Remove Books', None, 'Help', 'Preferences', ) - -gprefs.defaults['action-layout-toolbar-child'] = () - -gprefs.defaults['action-layout-toolbar-device'] = ( + gprefs.defaults['action-layout-toolbar-device'] = ( 'Add Books', 'Edit Metadata', None, 'Convert Books', 'View', 'Send To Device', None, None, 'Location Manager', None, None, 'Fetch News', 'Save To Disk', 'Connect Share', None, 'Remove Books', None, 'Help', 'Preferences', ) +gprefs.defaults['action-layout-toolbar-child'] = () + gprefs.defaults['action-layout-context-menu'] = ( 'Edit Metadata', 'Send To Device', 'Save To Disk', 'Connect Share', 'Copy To Library', None, @@ -61,6 +81,7 @@ gprefs.defaults['toolbar_text'] = 'auto' gprefs.defaults['font'] = None gprefs.defaults['tags_browser_partition_method'] = 'first letter' gprefs.defaults['tags_browser_collapse_at'] = 100 +gprefs.defaults['edit_metadata_single_layout'] = 'default' # }}} diff --git a/src/calibre/gui2/actions/device.py b/src/calibre/gui2/actions/device.py index 64bc4e69d7..debcbb6c1a 100644 --- a/src/calibre/gui2/actions/device.py +++ b/src/calibre/gui2/actions/device.py @@ -165,6 +165,10 @@ class ConnectShareAction(InterfaceAction): def content_server_state_changed(self, running): self.share_conn_menu.server_state_changed(running) + if running: + self.qaction.setIcon(QIcon(I('connect_share_on.png'))) + else: + self.qaction.setIcon(QIcon(I('connect_share.png'))) def toggle_content_server(self): if self.gui.content_server is None: diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index 9a6148eb7b..e98817a02f 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -238,7 +238,7 @@ class Spacer(QWidget): # {{{ self.l.addStretch(10) # }}} -class MenuAction(QAction): +class MenuAction(QAction): # {{{ def __init__(self, clone, parent): QAction.__init__(self, clone.text(), parent) @@ -247,7 +247,7 @@ class MenuAction(QAction): def clone_changed(self): self.setText(self.clone.text()) - +# }}} class MenuBar(QMenuBar): # {{{ @@ -308,22 +308,47 @@ class MenuBar(QMenuBar): # {{{ ac.setMenu(m) return ac - - # }}} -class ToolBar(QToolBar): # {{{ +class BaseToolBar(QToolBar): # {{{ - def __init__(self, donate, location_manager, child_bar, parent): + def __init__(self, parent): QToolBar.__init__(self, parent) - self.gui = parent - self.child_bar = child_bar self.setContextMenuPolicy(Qt.PreventContextMenu) self.setMovable(False) self.setFloatable(False) self.setOrientation(Qt.Horizontal) self.setAllowedAreas(Qt.TopToolBarArea|Qt.BottomToolBarArea) self.setStyleSheet('QToolButton:checked { font-weight: bold }') + self.preferred_width = self.sizeHint().width() + + def resizeEvent(self, ev): + QToolBar.resizeEvent(self, ev) + style = self.get_text_style() + self.setToolButtonStyle(style) + + def get_text_style(self): + style = Qt.ToolButtonTextUnderIcon + s = gprefs['toolbar_icon_size'] + if s != 'off': + p = gprefs['toolbar_text'] + if p == 'never': + style = Qt.ToolButtonIconOnly + elif p == 'auto' and self.preferred_width > self.width()+35: + style = Qt.ToolButtonIconOnly + return style + + def contextMenuEvent(self, *args): + pass + +# }}} + +class ToolBar(BaseToolBar): # {{{ + + def __init__(self, donate, location_manager, child_bar, parent): + BaseToolBar.__init__(self, parent) + self.gui = parent + self.child_bar = child_bar self.donate_button = donate self.apply_settings() @@ -333,7 +358,6 @@ class ToolBar(QToolBar): # {{{ donate.setCursor(Qt.PointingHandCursor) self.added_actions = [] self.build_bar() - self.preferred_width = self.sizeHint().width() self.setAcceptDrops(True) def apply_settings(self): @@ -348,9 +372,6 @@ class ToolBar(QToolBar): # {{{ self.child_bar.setToolButtonStyle(style) self.donate_button.set_normal_icon_size(sz, sz) - def contextMenuEvent(self, *args): - pass - def build_bar(self): self.showing_donate = False showing_device = self.location_manager.has_device @@ -382,21 +403,20 @@ class ToolBar(QToolBar): # {{{ bar.added_actions.append(ac) bar.setup_tool_button(bar, ac, QToolButton.MenuButtonPopup) elif what == 'Donate': + self.d_widget = QWidget() + self.d_widget.setLayout(QVBoxLayout()) + self.d_widget.layout().addWidget(self.donate_button) if isosx: - bar.addAction(self.gui.donate_action) - ch = self.setup_tool_button(bar, self.gui.donate_action) - ch.setText(_('Donate')) - else: - self.d_widget = QWidget() - self.d_widget.setLayout(QVBoxLayout()) - self.d_widget.layout().addWidget(self.donate_button) - bar.addWidget(self.d_widget) - self.showing_donate = True + self.d_widget.setStyleSheet('QWidget, QToolButton {background-color: none; border: none; }') + bar.addWidget(self.d_widget) + self.showing_donate = True elif what in self.gui.iactions: action = self.gui.iactions[what] bar.addAction(action.qaction) self.added_actions.append(action.qaction) self.setup_tool_button(bar, action.qaction, action.popup_type) + self.preferred_width = self.sizeHint().width() + self.child_bar.preferred_width = self.child_bar.sizeHint().width() def setup_tool_button(self, bar, ac, menu_mode=None): ch = bar.widgetForAction(ac) @@ -408,21 +428,6 @@ class ToolBar(QToolBar): # {{{ ch.setPopupMode(menu_mode) return ch - def resizeEvent(self, ev): - QToolBar.resizeEvent(self, ev) - style = Qt.ToolButtonTextUnderIcon - s = gprefs['toolbar_icon_size'] - if s != 'off': - p = gprefs['toolbar_text'] - if p == 'never': - style = Qt.ToolButtonIconOnly - - if p == 'auto' and self.preferred_width > self.width()+35 and \ - not gprefs['action-layout-toolbar-child']: - style = Qt.ToolButtonIconOnly - - self.setToolButtonStyle(style) - def database_changed(self, db): pass @@ -500,7 +505,7 @@ class MainWindowMixin(object): # {{{ self.iactions['Fetch News'].init_scheduler(db) self.search_bar = SearchBar(self) - self.child_bar = QToolBar(self) + self.child_bar = BaseToolBar(self) self.tool_bar = ToolBar(self.donate_button, self.location_manager, self.child_bar, self) self.addToolBar(Qt.TopToolBarArea, self.tool_bar) diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index cba877b249..2ef183fca5 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -658,7 +658,7 @@ editors = {'default': MetadataSingleDialog, 'alt1': MetadataSingleDialogAlt1} def edit_metadata(db, row_list, current_row, parent=None, view_slot=None, set_current_callback=None): - cls = db.prefs.get('edit_metadata_single_layout', '') + cls = gprefs.get('edit_metadata_single_layout', '') if cls not in editors: cls = 'default' d = editors[cls](db, parent) diff --git a/src/calibre/gui2/metadata/single_download.py b/src/calibre/gui2/metadata/single_download.py index 176c164d3d..35c66340c6 100644 --- a/src/calibre/gui2/metadata/single_download.py +++ b/src/calibre/gui2/metadata/single_download.py @@ -9,11 +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) + QTimer, pyqtSignal, QAbstractTableModel, QVariant, QSize, QListView, + QPixmap, QAbstractListModel, QColor, QRect, QTextBrowser) from PyQt4.QtWebKit import QWebView from calibre.customize.ui import metadata_plugins @@ -24,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 + +DEBUG_DIALOG = False class RichTextDelegate(QStyledItemDelegate): # {{{ @@ -52,6 +57,65 @@ class RichTextDelegate(QStyledItemDelegate): # {{{ painter.restore() # }}} +class CoverDelegate(QStyledItemDelegate): # {{{ + + needs_redraw = pyqtSignal() + + def __init__(self, parent): + QStyledItemDelegate.__init__(self, parent) + + 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_animation(self): + self.angle = 0 + self.timer.start(200) + + 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): + 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 ResultsModel(QAbstractTableModel): # {{{ COLUMNS = ( @@ -268,7 +332,7 @@ class IdentifyWorker(Thread): # {{{ def run(self): try: - if True: + if DEBUG_DIALOG: self.results = self.sample_results() else: self.results = identify(self.log, self.abort, title=self.title, @@ -277,7 +341,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): # {{{ @@ -398,11 +462,307 @@ class IdentifyWidget(QWidget): # {{{ self.abort.set() # }}} -class FullFetch(QDialog): # {{{ +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): + images = ['donate.png', 'config.png', 'column.png', 'eject.png', ] + import time + time.sleep(2) + for pl, im in zip(metadata_plugins(['cover']), images): + self.rq.put((pl, 1, 1, 'png', I(im, data=True))) + + def run(self): + try: + if DEBUG_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, current_cover, parent=None): + QAbstractListModel.__init__(self, parent) + + if current_cover is None: + current_cover = QPixmap(I('default_cover.png')) + + self.blank = QPixmap(I('blank.png')).scaled(150, 200) + + 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.plugin_map[plugin] = i+1 + + 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, + Qt.SmoothTransformation) + return (text, QVariant(scaled), pmap, waiting) + + def rowCount(self, parent=None): + return len(self.covers) + + def data(self, index, role): + try: + text, pmap, cover, waiting = self.covers[index.row()] + except: + return NONE + if role == Qt.DecorationRole: + return pmap + if role == Qt.DisplayRole: + return text + 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_pixmap(self, index): + row = index.row() + if row > 0 and row < len(self.covers): + pmap = self.covers[row][2] + if pmap is not None and not pmap.isNull(): + return pmap + +# }}} + +class CoversView(QListView): # {{{ + + chosen = pyqtSignal() + + def __init__(self, current_cover, parent=None): + QListView.__init__(self, parent) + self.m = CoversModel(current_cover, self) + self.setModel(self.m) + + self.setFlow(self.LeftToRight) + self.setWrapping(True) + self.setResizeMode(self.Adjust) + self.setGridSize(QSize(190, 260)) + self.setIconSize(QSize(150, 200)) + self.setSelectionMode(self.SingleSelection) + self.setViewMode(self.IconMode) + + self.delegate = CoverDelegate(self) + self.setItemDelegate(self.delegate) + 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() + sm.select(current, sm.SelectCurrent) + + def start(self): + self.select(0) + 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 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) + + self.msg = QLabel() + self.msg.setWordWrap(True) + l.addWidget(self.msg, 0, 0) + + 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 + self.title, self.authors = title, authors + self.log('\n\nStarting cover download for:', book.title) + 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-1, 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() + + def cover_pixmap(self): + idx = None + for i in self.covers_view.selectionModel().selectedIndexes(): + if i.isValid(): + idx = i + break + if idx is None: + idx = self.covers_view.currentIndex() + return self.covers_view.model().cover_pixmap(idx) + +# }}} + +class LogViewer(QDialog): # {{{ def __init__(self, log, parent=None): QDialog.__init__(self, parent) self.log = log + self.l = l = QVBoxLayout() + self.setLayout(l) + + self.tb = QTextBrowser(self) + l.addWidget(self.tb) + + self.bb = QDialogButtonBox(QDialogButtonBox.Close) + l.addWidget(self.bb) + self.bb.rejected.connect(self.reject) + self.bb.accepted.connect(self.accept) + + self.setWindowTitle(_('Download log')) + self.setWindowIcon(QIcon(I('debug.png'))) + self.resize(QSize(800, 400)) + + self.keep_updating = True + self.last_html = None + self.finished.connect(self.stop) + QTimer.singleShot(1000, self.update_log) + + self.show() + + def stop(self, *args): + self.keep_updating = False + + def update_log(self): + if not self.keep_updating: + return + html = self.log.html + if html != self.last_html: + self.last_html = html + self.tb.setHtml('

%s
'%html) + QTimer.singleShot(1000, self.update_log) + +# }}} + +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_pixmap = None self.setWindowTitle(_('Downloading metadata...')) self.setWindowIcon(QIcon(I('metadata.png'))) @@ -418,22 +778,39 @@ class FullFetch(QDialog): # {{{ self.next_button = self.bb.addButton(_('Next'), self.bb.AcceptRole) self.next_button.setDefault(True) self.next_button.setEnabled(False) + self.next_button.setIcon(QIcon(I('ok.png'))) 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.log_button = self.bb.addButton(_('View log'), self.bb.ActionRole) + self.log_button.clicked.connect(self.view_log) + self.log_button.setIcon(QIcon(I('debug.png'))) + self.ok_button.setVisible(False) 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) + + 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 view_log(self): + self._lv = LogViewer(self.log, self) def book_selected(self, book): - print (book) self.next_button.setVisible(False) self.ok_button.setVisible(True) + self.book = book + self.stack.setCurrentIndex(1) + self.covers_widget.start(book, self.current_cover, + self.title, self.authors) def accept(self): # Prevent the usual dialog accept mechanisms from working @@ -443,6 +820,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) @@ -450,15 +830,25 @@ class FullFetch(QDialog): # {{{ self.identify_widget.get_result() def ok_clicked(self, *args): - pass + self.cover_pixmap = self.covers_widget.cover_pixmap() + if DEBUG_DIALOG: + if self.cover_pixmap is not None: + self.w = QLabel() + self.w.setPixmap(self.cover_pixmap) + self.stack.addWidget(self.w) + self.stack.setCurrentIndex(2) + else: + QDialog.accept(self) def start(self, title=None, authors=None, identifiers={}): + self.title, self.authors = title, authors self.identify_widget.start(title=title, authors=authors, identifiers=identifiers) self.exec_() # }}} if __name__ == '__main__': + DEBUG_DIALOG = True app = QApplication([]) d = FullFetch(Log()) d.start(title='great gatsby', authors=['Fitzgerald']) diff --git a/src/calibre/gui2/preferences/behavior.py b/src/calibre/gui2/preferences/behavior.py index 342b5197c9..d64af343f2 100644 --- a/src/calibre/gui2/preferences/behavior.py +++ b/src/calibre/gui2/preferences/behavior.py @@ -11,7 +11,7 @@ from PyQt4.Qt import Qt, QVariant, QListWidgetItem from calibre.gui2.preferences import ConfigWidgetBase, test_widget, Setting from calibre.gui2.preferences.behavior_ui import Ui_Form -from calibre.gui2 import config, info_dialog, dynamic +from calibre.gui2 import config, info_dialog, dynamic, gprefs from calibre.utils.config import prefs from calibre.customize.ui import available_output_formats, all_input_formats from calibre.utils.search_query_parser import saved_searches @@ -19,6 +19,7 @@ from calibre.ebooks import BOOK_EXTENSIONS from calibre.ebooks.oeb.iterator import is_supported from calibre.constants import iswindows from calibre.utils.icu import sort_key +from calibre.utils.config import test_eight_code class OutputFormatSetting(Setting): @@ -62,6 +63,14 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): signal = getattr(self.opt_internally_viewed_formats, 'item'+signal) signal.connect(self.internally_viewed_formats_changed) + r('bools_are_tristate', db.prefs, restart_required=True) + if test_eight_code: + r = self.register + choices = [(_('Default'), 'default'), (_('Compact Metadata'), 'alt1')] + r('edit_metadata_single_layout', gprefs, choices=choices) + else: + self.opt_edit_metadata_single_layout.setVisible(False) + self.edit_metadata_single_label.setVisible(False) def initialize(self): ConfigWidgetBase.initialize(self) diff --git a/src/calibre/gui2/preferences/behavior.ui b/src/calibre/gui2/preferences/behavior.ui index 0f35d28cd5..544de1457a 100644 --- a/src/calibre/gui2/preferences/behavior.ui +++ b/src/calibre/gui2/preferences/behavior.ui @@ -14,44 +14,92 @@ Form - + + + + Qt::Horizontal + + + + 10 + 00 + + + + + &Overwrite author and title by default when fetching metadata - + Download &social metadata (tags/ratings/etc.) by default - + Show notification when &new version is available - + + + + Yes/No columns have three values (Requires restart) + + + If checked, Yes/No custom columns values can be Yes, No, or Unknown. +If not checked, the values can be Yes or No. + + + + Automatically send downloaded &news to ebook reader - + &Delete news from library when it is automatically sent to reader - - - + + + + + + Preferred &output format: + + + opt_output_format + + + + + + + QComboBox::AdjustToMinimumContentsLengthWithIcon + + + 10 + + + + + + + + Default network &timeout: @@ -61,7 +109,7 @@ - + Set the default timeout for network fetches (i.e. anytime we go out to the internet to get information) @@ -80,7 +128,21 @@ - + + + + + + + + Job &priority: + + + opt_worker_process_priority + + + + QComboBox::AdjustToMinimumContentsLengthWithIcon @@ -105,37 +167,11 @@ - - - - Job &priority: - - - opt_worker_process_priority - - - - - - - Preferred &output format: - - - opt_output_format - - - - - - - QComboBox::AdjustToMinimumContentsLengthWithIcon - - - 10 - - - - + + + + + Restriction to apply when the current library is opened: @@ -145,7 +181,7 @@ - + @@ -166,14 +202,28 @@ - - - - Reset all disabled &confirmation dialogs - - + + + + + + Edit metadata (single) layout: + + + opt_edit_metadata_single_layout + + + + + + + Choose a different layout for the Edit Metadata dialog. The compact metadata layout favors editing custom metadata over changing covers and formats. + + + + - + Preferred &input format order: @@ -235,7 +285,7 @@ - + Use internal &viewer for: @@ -254,6 +304,13 @@ + + + + Reset all disabled &confirmation dialogs + + + diff --git a/src/calibre/gui2/preferences/columns.py b/src/calibre/gui2/preferences/columns.py index 4079e2d3f4..03a50e6f3a 100644 --- a/src/calibre/gui2/preferences/columns.py +++ b/src/calibre/gui2/preferences/columns.py @@ -13,7 +13,6 @@ from calibre.gui2.preferences import ConfigWidgetBase, test_widget from calibre.gui2.preferences.columns_ui import Ui_Form from calibre.gui2.preferences.create_custom_column import CreateCustomColumn from calibre.gui2 import error_dialog, question_dialog, ALL_COLUMNS -from calibre.utils.config import test_eight_code class ConfigWidget(ConfigWidgetBase, Ui_Form): @@ -34,14 +33,6 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): signal = getattr(self.opt_columns, 'item'+signal) signal.connect(self.columns_changed) - if test_eight_code: - r = self.register - choices = [(_('Default'), 'default'), (_('Compact Metadata'), 'alt1')] - r('edit_metadata_single_layout', db.prefs, choices=choices) - r('bools_are_tristate', db.prefs, restart_required=True) - else: - self.items_in_v_eight.setVisible(False) - def initialize(self): ConfigWidgetBase.initialize(self) self.init_columns() @@ -178,10 +169,6 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): must_restart = True return must_restart - def refresh_gui(self, gui): - gui.library_view.reset() - - if __name__ == '__main__': from PyQt4.Qt import QApplication diff --git a/src/calibre/gui2/preferences/columns.ui b/src/calibre/gui2/preferences/columns.ui index 48944b1c04..a9d82530ec 100644 --- a/src/calibre/gui2/preferences/columns.ui +++ b/src/calibre/gui2/preferences/columns.ui @@ -197,67 +197,6 @@ - - - - - - Related Options - - - - - - Edit metadata layout: - - - opt_edit_metadata_single_layout - - - - - - - Choose a different layout for the Edit Metadata dialog. Alternate layouts make it easier to edit custom columns. - - - - - - - Boolean columns are tristate: - - - opt_bools_are_tristate - - - - - - - If checked, boolean columns values can be Yes, No, and Unknown. -If not checked, the values can be Yes and No. - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - diff --git a/src/calibre/gui2/preferences/toolbar.py b/src/calibre/gui2/preferences/toolbar.py index 9512fc7d3d..7f5e0c4441 100644 --- a/src/calibre/gui2/preferences/toolbar.py +++ b/src/calibre/gui2/preferences/toolbar.py @@ -80,6 +80,12 @@ class BaseModel(QAbstractListModel): ans.append(n) return ans + def has_action(self, name): + for a in self._data: + if a.name == name: + return True + return False + class AllModel(BaseModel): @@ -291,16 +297,13 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): def commit(self): # Ensure preferences are showing in either the toolbar or # the menubar. - pref_in_toolbar = lm_in_toolbar = False - cm = self.models['toolbar'] - for x in cm[1]._data: - if x.name == 'Preferences': - pref_in_toolbar = True - if x.name == 'Location Manager': - lm_in_toolbar = True - if not pref_in_toolbar: + pref_in_toolbar = self.models['toolbar'][1].has_action('Preferences') + pref_in_menubar = self.models['menubar'][1].has_action('Preferences') + lm_in_toolbar = self.models['toolbar-device'][1].has_action('Location Manager') + lm_in_menubar = self.models['menubar-device'][1].has_action('Location Manager') + if not pref_in_toolbar and not pref_in_menubar: self.models['menubar'][1].add(['Preferences']) - if not lm_in_toolbar: + if not lm_in_toolbar and not lm_in_menubar: self.models['menubar-device'][1].add(['Location Manager']) # Save data. diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index ccc5b60427..50ecc4f1e5 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -40,7 +40,6 @@ from calibre.ebooks import BOOK_EXTENSIONS, check_ebook_format from calibre.utils.magick.draw import save_cover_data_to from calibre.utils.recycle_bin import delete_file, delete_tree from calibre.utils.formatter_functions import load_user_template_functions -from calibre.utils.config import test_eight_code copyfile = os.link if hasattr(os, 'link') else shutil.copyfile @@ -213,11 +212,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): defs = self.prefs.defaults defs['gui_restriction'] = defs['cs_restriction'] = '' defs['categories_using_hierarchy'] = [] - defs['edit_metadata_single_layout'] = 'default' + # Migrate the bool tristate tweak defs['bools_are_tristate'] = \ tweaks.get('bool_custom_columns_are_tristate', 'yes') == 'yes' - if self.prefs.get('bools_are_tristate') is None or not test_eight_code: + if self.prefs.get('bools_are_tristate') is None: self.prefs.set('bools_are_tristate', defs['bools_are_tristate']) # Migrate saved search and user categories to db preference scheme diff --git a/src/calibre/utils/config.py b/src/calibre/utils/config.py index 078971428d..7f2c75a272 100644 --- a/src/calibre/utils/config.py +++ b/src/calibre/utils/config.py @@ -786,8 +786,7 @@ def write_tweaks(raw): tweaks = read_tweaks() test_eight_code = tweaks.get('test_eight_code', False) # test_eight_code notes -# Change documentation of bool columns are tristate to indicate that it can be -# overridden on a per library basis via Preferences->Custom columns +# Change Amazon plugin name to just Amazon def migrate(): if hasattr(os, 'geteuid') and os.geteuid() == 0: