diff --git a/resources/recipes/new_yorker.recipe b/resources/recipes/new_yorker.recipe index 605b34e79b..1a2091cd52 100644 --- a/resources/recipes/new_yorker.recipe +++ b/resources/recipes/new_yorker.recipe @@ -32,15 +32,15 @@ class NewYorker(BasicNewsRecipe): , 'publisher' : publisher , 'language' : language } - + keep_only_tags = [ dict(name='div', attrs={'class':'headers'}) ,dict(name='div', attrs={'id':['articleheads','items-container','articleRail','articletext','photocredits']}) ] remove_tags = [ dict(name=['meta','iframe','base','link','embed','object']) - ,dict(attrs={'class':['utils','articleRailLinks','icons'] }) - ,dict(attrs={'id':['show-header','show-footer'] }) + ,dict(attrs={'class':['utils','articleRailLinks','icons'] }) + ,dict(attrs={'id':['show-header','show-footer'] }) ] remove_attributes = ['lang'] feeds = [(u'The New Yorker', u'http://feeds.newyorker.com/services/rss/feeds/everything.xml')] @@ -58,4 +58,4 @@ class NewYorker(BasicNewsRecipe): if cover_item: cover_url = 'http://www.newyorker.com' + cover_item['src'].strip() return cover_url - \ No newline at end of file + diff --git a/resources/recipes/revista_muy.recipe b/resources/recipes/revista_muy.recipe index b101fe97ce..c4516f718d 100644 --- a/resources/recipes/revista_muy.recipe +++ b/resources/recipes/revista_muy.recipe @@ -13,6 +13,8 @@ class RevistaMuyInteresante(BasicNewsRecipe): no_stylesheets = True remove_javascript = True + conversion_options = {'linearize_tables': True} + extra_css = ' .txt_articulo{ font-family: sans-serif; font-size: medium; text-align: justify } .contentheading{font-family: serif; font-size: large; font-weight: bold; color: #000000; text-align: center}' @@ -39,11 +41,12 @@ class RevistaMuyInteresante(BasicNewsRecipe): keep_only_tags = [dict(name='div', attrs={'class':['article']}),dict(name='td', attrs={'class':['txt_articulo']})] remove_tags = [ - dict(name=['object','link','script','ul']) + dict(name=['object','link','script','ul','iframe','ins']) ,dict(name='div', attrs={'id':['comment']}) ,dict(name='td', attrs={'class':['buttonheading']}) - ,dict(name='div', attrs={'class':['tags_articles']}) + ,dict(name='div', attrs={'class':['tags_articles','bajo_title']}) ,dict(name='table', attrs={'class':['pagenav']}) + ,dict(name='form', attrs={'class':['voteform']}) ] remove_tags_after = dict(name='div', attrs={'class':'tags_articles'}) @@ -115,3 +118,5 @@ class RevistaMuyInteresante(BasicNewsRecipe): if link_item: cover_url = "http://www.muyinteresante.es"+link_item['src'] return cover_url + + diff --git a/resources/recipes/rusiahoy.recipe b/resources/recipes/rusiahoy.recipe index 326c1695b0..8dfa55eece 100644 --- a/resources/recipes/rusiahoy.recipe +++ b/resources/recipes/rusiahoy.recipe @@ -19,7 +19,7 @@ class RusiaHoy(BasicNewsRecipe): use_embedded_content = False language = 'es' remove_empty_feeds = True - extra_css = """ + extra_css = """ body{font-family: Arial,sans-serif } .article_article_title{font-size: xx-large; font-weight: bold} .article_date{color: black; font-size: small} @@ -44,4 +44,4 @@ class RusiaHoy(BasicNewsRecipe): for item in soup.findAll(style=True): del item['style'] return soup - \ No newline at end of file + diff --git a/src/calibre/customize/__init__.py b/src/calibre/customize/__init__.py index 523fc51d92..b4e9b6c448 100644 --- a/src/calibre/customize/__init__.py +++ b/src/calibre/customize/__init__.py @@ -370,6 +370,15 @@ class InterfaceActionBase(Plugin): # {{{ can_be_disabled = False actual_plugin = None + + def load_actual_plugin(self, gui): + ''' + This method must return the actual interface action plugin object. + ''' + mod, cls = self.actual_plugin.split(':') + return getattr(__import__(mod, fromlist=['1'], level=0), cls)(gui, + self.site_customization) + # }}} class PreferencesPlugin(Plugin): # {{{ diff --git a/src/calibre/ebooks/rtf/input.py b/src/calibre/ebooks/rtf/input.py index 75c839eb83..57903a6711 100644 --- a/src/calibre/ebooks/rtf/input.py +++ b/src/calibre/ebooks/rtf/input.py @@ -292,7 +292,8 @@ class RTFInput(InputFormatPlugin): # Replace newlines inserted by the 'empty_paragraphs' option in rtf2xml with html blank lines if not getattr(self.options, 'remove_paragraph_spacing', False): res = re.sub('\s*', '', res) - res = re.sub('(?<=\n)\n{2}', u'

\u00a0

\n', res) + res = re.sub('(?<=\n)\n{2}', + u'

\u00a0

\n'.encode('utf-8'), res) if self.options.preprocess_html: preprocessor = PreProcessor(self.options, log=getattr(self, 'log', None)) res = preprocessor(res) diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py index 28ca06b352..4ffc8da650 100644 --- a/src/calibre/gui2/book_details.py +++ b/src/calibre/gui2/book_details.py @@ -214,9 +214,18 @@ class BookInfo(QWebView): def _show_data(self, rows, comments): + + def color_to_string(col): + ans = '#000000' + if col.isValid(): + col = col.toRgb() + if col.isValid(): + ans = unicode(col.name()) + return ans + f = QFontInfo(QApplication.font(self.parent())).pixelSize() - c = unicode(QApplication.palette().color(QPalette.Normal, - QPalette.WindowText).name()) + c = color_to_string(QApplication.palette().color(QPalette.Normal, + QPalette.WindowText)) templ = u'''\ diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py index 3103d7c459..053dd7a743 100644 --- a/src/calibre/gui2/custom_column_widgets.py +++ b/src/calibre/gui2/custom_column_widgets.py @@ -437,7 +437,7 @@ class BulkBool(BulkBase, Bool): if tweaks['bool_custom_columns_are_tristate'] == 'no' and val is None: val = False if value is not None and value != val: - return None + return 'nochange' value = val return value @@ -445,19 +445,23 @@ class BulkBool(BulkBase, Bool): self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent), QComboBox(parent)] w = self.widgets[1] - items = [_('Yes'), _('No'), _('Undefined')] - icons = [I('ok.png'), I('list_remove.png'), I('blank.png')] + items = [_('Yes'), _('No'), _('Undefined'), _('Do not change')] + icons = [I('ok.png'), I('list_remove.png'), I('blank.png'), I('blank.png')] for icon, text in zip(icons, items): w.addItem(QIcon(icon), text) + def getter(self): + val = self.widgets[1].currentIndex() + return {3: 'nochange', 2: None, 1: False, 0: True}[val] + def setter(self, val): - val = {None: 2, False: 1, True: 0}[val] + val = {'nochange': 3, None: 2, False: 1, True: 0}[val] self.widgets[1].setCurrentIndex(val) def commit(self, book_ids, notify=False): val = self.gui_val val = self.normalize_ui_val(val) - if val != self.initial_val: + if val != self.initial_val and val != 'nochange': if tweaks['bool_custom_columns_are_tristate'] == 'no' and val is None: val = False self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify) diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index bd0743e819..00bba2b491 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -102,9 +102,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ self.device_connected = None acmap = OrderedDict() for action in interface_actions(): - mod, cls = action.actual_plugin.split(':') - ac = getattr(__import__(mod, fromlist=['1'], level=0), cls)(self, - action.site_customization) + ac = action.load_actual_plugin(self) if ac.name in acmap: if ac.priority >= acmap[ac.name].priority: acmap[ac.name] = ac diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index 075fbe664a..6148019906 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -403,7 +403,7 @@ class ResultCache(SearchQueryParser): # {{{ '<=':[2, lambda r, q: r <= q] } - def get_numeric_matches(self, location, query): + def get_numeric_matches(self, location, query, val_func = None): matches = set([]) if len(query) == 0: return matches @@ -419,7 +419,10 @@ class ResultCache(SearchQueryParser): # {{{ if relop is None: (p, relop) = self.numeric_search_relops['='] - loc = self.field_metadata[location]['rec_index'] + if val_func is None: + loc = self.field_metadata[location]['rec_index'] + val_func = lambda item, loc=loc: item[loc] + dt = self.field_metadata[location]['datatype'] if dt == 'int': cast = (lambda x: int (x)) @@ -430,6 +433,9 @@ class ResultCache(SearchQueryParser): # {{{ elif dt == 'float': cast = lambda x : float (x) adjust = lambda x: x + else: # count operation + cast = (lambda x: int (x)) + adjust = lambda x: x if len(query) > 1: mult = query[-1:].lower() @@ -446,10 +452,11 @@ class ResultCache(SearchQueryParser): # {{{ for item in self._data: if item is None: continue - if not item[loc]: + v = val_func(item) + if not v: i = 0 else: - i = adjust(item[loc]) + i = adjust(v) if relop(i, q): matches.add(item[0]) return matches @@ -467,15 +474,23 @@ class ResultCache(SearchQueryParser): # {{{ return matches raise ParseException(query, len(query), 'Recursive query group detected', self) - # take care of dates special case - if location in self.field_metadata and \ - self.field_metadata[location]['datatype'] == 'datetime': - return self.get_dates_matches(location, query.lower()) + if location in self.field_metadata: + fm = self.field_metadata[location] + # take care of dates special case + if fm['datatype'] == 'datetime': + return self.get_dates_matches(location, query.lower()) - # take care of numbers special case - if location in self.field_metadata and \ - self.field_metadata[location]['datatype'] in ('rating', 'int', 'float'): - return self.get_numeric_matches(location, query.lower()) + # take care of numbers special case + if fm['datatype'] in ('rating', 'int', 'float'): + return self.get_numeric_matches(location, query.lower()) + + # take care of the 'count' operator for is_multiples + if fm['is_multiple'] and \ + len(query) > 1 and query.startswith('#') and \ + query[1:1] in '=<>!': + vf = lambda item, loc=fm['rec_index'], ms=fm['is_multiple']:\ + len(item[loc].split(ms)) if item[loc] is not None else 0 + return self.get_numeric_matches(location, query[1:], val_func=vf) # everything else, or 'all' matches matchkind = CONTAINS_MATCH diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index ffb97e481f..47c575386b 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -268,8 +268,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): base, prefer_custom=True) - self.field_metadata.set_field_record_index('cover', - self.FIELD_MAP['cover'], prefer_custom=False) self.FIELD_MAP['ondevice'] = base+1 self.field_metadata.set_field_record_index('ondevice', base+1, prefer_custom=False) self.FIELD_MAP['all_metadata'] = base+2 diff --git a/src/calibre/library/field_metadata.py b/src/calibre/library/field_metadata.py index dbc871026e..d10dc5da71 100644 --- a/src/calibre/library/field_metadata.py +++ b/src/calibre/library/field_metadata.py @@ -3,6 +3,7 @@ Created on 25 May 2010 @author: charles ''' +import copy from calibre.utils.ordered_dict import OrderedDict from calibre.utils.config import tweaks @@ -86,7 +87,7 @@ class FieldMetadata(dict): # Builtin metadata {{{ - _field_metadata = [ + _field_metadata_prototype = [ ('authors', {'table':'authors', 'column':'name', 'link_column':'author', @@ -161,6 +162,15 @@ class FieldMetadata(dict): 'search_terms':['tags', 'tag'], 'is_custom':False, 'is_category':True}), + ('all_metadata',{'table':None, + 'column':None, + 'datatype':None, + 'is_multiple':None, + 'kind':'field', + 'name':None, + 'search_terms':[], + 'is_custom':False, + 'is_category':False}), ('author_sort',{'table':None, 'column':None, 'datatype':'text', @@ -180,7 +190,7 @@ class FieldMetadata(dict): 'is_custom':False, 'is_category':False}), ('cover', {'table':None, 'column':None, - 'datatype':None, + 'datatype':'int', 'is_multiple':None, 'kind':'field', 'name':None, @@ -223,15 +233,6 @@ class FieldMetadata(dict): 'search_terms':[], 'is_custom':False, 'is_category':False}), - ('all_metadata',{'table':None, - 'column':None, - 'datatype':None, - 'is_multiple':None, - 'kind':'field', - 'name':None, - 'search_terms':[], - 'is_custom':False, - 'is_category':False}), ('ondevice', {'table':None, 'column':None, 'datatype':'text', @@ -322,6 +323,7 @@ class FieldMetadata(dict): ] def __init__(self): + self._field_metadata = copy.deepcopy(self._field_metadata_prototype) self._tb_cats = OrderedDict() self._search_term_map = {} self.custom_label_to_key_map = {} diff --git a/src/calibre/manual/customize.rst b/src/calibre/manual/customize.rst index e0f799f572..0d6f75b9cf 100644 --- a/src/calibre/manual/customize.rst +++ b/src/calibre/manual/customize.rst @@ -98,6 +98,44 @@ Every time you use calibre to convert a book, the plugin's :meth:`run` method wi converted book will have its publisher set to "Hello World". For more information about |app|'s plugin system, read on... + +A Hello World GUI plugin +--------------------------- + +Here's a simple Hello World plugin for the |app| GUI. It will cause a box to popup with the message "Hellooo World!" when you press Ctrl+Shift+H + +.. code-block:: python + + from calibre.customize import InterfaceActionBase + + class HelloWorldBase(InterfaceActionBase): + + name = 'Hello World GUI' + author = 'The little green man' + + def load_actual_plugin(self, gui): + from calibre.gui2.actions import InterfaceAction + + class HelloWorld(InterfaceAction): + name = 'Hello World GUI' + action_spec = ('Hello World!', 'add_book.png', None, + _('Ctrl+Shift+H')) + + def genesis(self): + self.qaction.triggered.connect(self.hello_world) + + def hello_world(self, *args): + from calibre.gui2 import info_dialog + info_dialog(self.gui, 'Hello World!', 'Hellooo World!', + show=True) + + return HelloWorld(gui, self.site_customization) + +You can also have it show up in the toolbars/context menu by going to Preferences->Toolbars and adding this plugin to the locations you want it to be in. + +While this plugin is utterly useless, note that all calibre GUI actions like adding/saving/removing/viewing/etc. are implemented as plugins, so there is no limit to what you can achieve. The key thing to remember is that the plugin has access to the full |app| GUI via ``self.gui``. + + The Plugin base class ------------------------ diff --git a/src/calibre/manual/gui.rst b/src/calibre/manual/gui.rst index c81688ba8c..a91124b214 100644 --- a/src/calibre/manual/gui.rst +++ b/src/calibre/manual/gui.rst @@ -274,6 +274,14 @@ Searching for ``no`` or ``unchecked`` will find all books with ``No`` in the col :guilabel:`Advanced Search Dialog` +You can test for the number of items in multiple-value columns, such as tags, formats, authors, and tags-like custom columns. This is done using a syntax very similar to numeric tests (discussed above), except that the relational operator begins with a ``#`` character. For example:: + + tags:#>3 will give you books with more than three tags + tags:#!=3 will give you books that do not have three tags + authors:#=1 will give you books with exactly one author + #cust:#<5 will give you books with less than five items in custom column #cust + formats:#>1 will give you books with more than one format + Saving searches ----------------- diff --git a/src/calibre/manual/plugins.rst b/src/calibre/manual/plugins.rst index eb955aebee..0a62218fb9 100644 --- a/src/calibre/manual/plugins.rst +++ b/src/calibre/manual/plugins.rst @@ -161,11 +161,20 @@ The base class for such devices is :class:`calibre.devices.usbms.driver.USBMS`. User Interface Actions -------------------------- +If you are adding your own plugin in a zip file, you should subclass both InterfaceActionBase and InterfaceAction. The :meth:`load_actual_plugin` method of you InterfaceActionBase subclass must return an instantiated object of your InterfaceBase subclass. + + .. autoclass:: calibre.gui2.actions.InterfaceAction :show-inheritance: :members: :member-order: bysource +.. autoclass:: calibre.customize.InterfaceActionBase + :show-inheritance: + :members: + :member-order: bysource + + Preferences Plugins --------------------------