diff --git a/resources/images/news/kurier.png b/resources/images/news/kurier.png new file mode 100644 index 0000000000..f11c1dca11 Binary files /dev/null and b/resources/images/news/kurier.png differ diff --git a/resources/images/news/virtualshackles.png b/resources/images/news/virtualshackles.png new file mode 100644 index 0000000000..986545d91f Binary files /dev/null and b/resources/images/news/virtualshackles.png differ diff --git a/resources/recipes/guardian.recipe b/resources/recipes/guardian.recipe index f74414a569..344e061c26 100644 --- a/resources/recipes/guardian.recipe +++ b/resources/recipes/guardian.recipe @@ -119,5 +119,7 @@ class Guardian(BasicNewsRecipe): raise NotImplementedError + def postprocess_html(self,soup,first): + return soup.findAll('html')[0] diff --git a/resources/recipes/kurier.recipe b/resources/recipes/kurier.recipe new file mode 100644 index 0000000000..7c9e1e2256 --- /dev/null +++ b/resources/recipes/kurier.recipe @@ -0,0 +1,50 @@ +__license__ = 'GPL v3' +__copyright__ = '2010, Darko Miletic ' +''' +kurier.at +''' + +from calibre.web.feeds.news import BasicNewsRecipe + +class Kurier(BasicNewsRecipe): + title = 'Kurier' + __author__ = 'Darko Miletic' + description = 'News from Austria' + publisher = 'KURIER' + category = 'news, politics, Austria' + oldest_article = 2 + max_articles_per_feed = 200 + no_stylesheets = True + encoding = 'cp1252' + use_embedded_content = False + language = 'de_AT' + remove_empty_feeds = True + publication_type = 'newspaper' + extra_css = ' body{font-family: Verdana,Helvetica,sans-serif } img{margin-bottom: 0.4em} .bild_us{font-size: x-small} ' + + conversion_options = { + 'comment' : description + , 'tags' : category + , 'publisher' : publisher + , 'language' : language + } + + remove_tags = [dict(attrs={'class':['contenttabs','drucken','versenden','leserbrief','kommentieren','addthis_button']})] + keep_only_tags = [dict(attrs={'id':'content'})] + remove_tags_after = dict(attrs={'id':'author'}) + remove_attributes = ['width','height'] + + feeds = [ + (u'Nachrichten', u'http://kurier.at/rss/nachrichten_nachrichten_rss.xml' ) + ,(u'Techno' , u'http://kurier.at/rss/techno_techno_rss.xml' ) + ,(u'Wirtschaft' , u'http://kurier.at/rss/wirtschaft_wirtschaft_rss.xml' ) + ,(u'Kultur' , u'http://kurier.at/rss/kultur_kultur_rss.xml' ) + ,(u'Freizeit' , u'http://kurier.at/rss/freizeit_freizeit_rss.xml' ) + ,(u'Wetter' , u'http://kurier.at/rss/oewetter_rss.xml' ) + ,(u'Verkehr' , u'http://kurier.at/rss/verkehr_rss.xml' ) + ] + + def preprocess_html(self, soup): + for item in soup.findAll(style=True): + del item['style'] + return self.adeify_images(soup) diff --git a/resources/recipes/la_republica.recipe b/resources/recipes/la_republica.recipe index 3bc1fa5ece..107232daa6 100644 --- a/resources/recipes/la_republica.recipe +++ b/resources/recipes/la_republica.recipe @@ -22,21 +22,36 @@ class LaRepublica(BasicNewsRecipe): language = 'it' timefmt = '[%a, %d %b, %Y]' - oldest_article = 1 + oldest_article = 5 max_articles_per_feed = 100 use_embedded_content = False recursion = 10 remove_javascript = True + def get_article_url(self, article): + link = article.get('id', article.get('guid', None)) + if link is None: + return article + return link + + keep_only_tags = [dict(name='div', attrs={'class':'articolo'}), + dict(name='div', attrs={'class':'body-text'}), + dict(name='div', attrs={'class':'page-content'}), + dict(name='div', attrs={'id':'contA'}) + ] - keep_only_tags = [dict(name='div', attrs={'class':'articolo'})] remove_tags = [ dict(name=['object','link']), dict(name='span',attrs={'class':'linkindice'}), - dict(name='div',attrs={'class':'bottom-mobile'}), - dict(name='div',attrs={'id':['rssdiv','blocco']}) + dict(name='div', attrs={'class':'bottom-mobile'}), + dict(name='div', attrs={'id':['rssdiv','blocco']}), + dict(name='div', attrs={'class':'utility'}), + dict(name='div', attrs={'class':'generalbox'}) ] + remove_tags_after = [ + dict(name='div',attrs={'id':'ugc_linkUpload'}) + ] feeds = [ (u'Repubblica Rilievo', u'http://www.repubblica.it/rss/homepage/rss2.0.xml'), @@ -48,8 +63,12 @@ class LaRepublica(BasicNewsRecipe): (u'Repubblica Tecnologia', u'http://www.repubblica.it/rss/tecnologia/rss2.0.xml'), (u'Repubblica Scuola e Universita', u'http://www.repubblica.it/rss/scuola_e_universita/rss2.0.xml'), (u'Repubblica Ambiente', u'http://www.repubblica.it/rss/ambiente/rss2.0.xml'), - (u'Repubblica Cultura', u'http://www.repubblica.it/rss/spettacoli_e_cultura/rss2.0.xml'), - (u'Repubblica Persone', u'http://www.repubblica.it/rss/persone/rss2.0.xml'), - (u'Repubblica Sport', u'http://www.repubblica.it/rss/sport/rss2.0.xml'), - (u'Repubblica Calcio', u'http://www.repubblica.it/rss/sport/calcio/rss2.0.xml') - ] + (u'Repubblica Cultura', u'http://www.repubblica.it/rss/spettacoli_e_cultura/rss2.0.xml'), + (u'Repubblica Persone', u'http://www.repubblica.it/rss/persone/rss2.0.xml'), + (u'Repubblica Sport', u'http://www.repubblica.it/rss/sport/rss2.0.xml'), + (u'Repubblica Calcio', u'http://www.repubblica.it/rss/sport/calcio/rss2.0.xml'), + (u'Repubblica Motori', u'http://www.repubblica.it/rss/motori/rss2.0.xml'), + (u'Repubblica Roma', u'http://roma.repubblica.it/rss/rss2.0.xml'), + (u'Repubblica Torino', u'http://torino.repubblica.it/rss/rss2.0.xml') + ] + diff --git a/resources/recipes/npr_music_blogs.recipe b/resources/recipes/npr_music_blogs.recipe new file mode 100644 index 0000000000..940bb20d5b --- /dev/null +++ b/resources/recipes/npr_music_blogs.recipe @@ -0,0 +1,18 @@ +from calibre.web.feeds.news import BasicNewsRecipe + +class nprmusic(BasicNewsRecipe): + title = 'NPR Music Blogs' + __author__ = 'cix3' + timefmt = ' [%b %d, %Y]' + language = 'en' + + oldest_article = 30 + max_articles_per_feed = 100 + no_stylesheets = True + + remove_tags = [dict(name='div', attrs={'id':['logo', 'comments', 'related_objects', 'inset module', 'footer', 'strip_control', 'header', 'navigation']}), dict(name='hr'), dict(name='img')] + + feeds = [ + ('A Blog Supreme', 'http://www.npr.org/blogs/ablogsupreme/index.xml'), + ('All Songs Considered', 'http://www.npr.org/blogs/allsongs/index.xml'), + ('Monitor Mix', 'http://www.npr.org/blogs/monitormix/index.xml')] diff --git a/resources/recipes/usatoday.recipe b/resources/recipes/usatoday.recipe index 5b036d145d..bd47262563 100644 --- a/resources/recipes/usatoday.recipe +++ b/resources/recipes/usatoday.recipe @@ -377,8 +377,9 @@ class USAToday(BasicNewsRecipe): if byline: byline['class'] = 'byline' # Replace comma with middot - byline.contents[0].replaceWith(re.sub(","," ·", byline.renderContents())) - return byline.renderContents() + byline.contents[0].replaceWith(re.sub(u",", u" ·", + byline.renderContents(encoding=None))) + return byline.renderContents(encoding=None) else : paras = soup.findAll(text=True) for para in paras: diff --git a/resources/recipes/virtualshackles.recipe b/resources/recipes/virtualshackles.recipe new file mode 100644 index 0000000000..52660877ea --- /dev/null +++ b/resources/recipes/virtualshackles.recipe @@ -0,0 +1,33 @@ +__license__ = 'GPL v3' +__copyright__ = '2010, Darko Miletic ' +''' +www.virtualshackles.com +''' + +from calibre.web.feeds.recipes import BasicNewsRecipe + +class Virtualshackles(BasicNewsRecipe): + title = 'Virtual Shackles' + __author__ = 'Darko Miletic' + description = "The adventures of Orion and Jack, making games they'd never play for people they don't like." + category = 'virtual shackles, virtualshackles, games, webcomic, comic, video game, orion, jack' + oldest_article = 10 + max_articles_per_feed = 100 + no_stylesheets = True + use_embedded_content = True + encoding = 'cp1252' + publisher = 'Virtual Shackles' + language = 'en' + publication_type = 'comic' + + conversion_options = { + 'comments' : description + ,'tags' : category + ,'language' : language + ,'publisher' : publisher + } + + feeds = [(u'Virtual Shackles', u'http://feeds2.feedburner.com/virtualshackles' )] + + def preprocess_html(self, soup): + return self.adeify_images(soup) diff --git a/src/calibre/__init__.py b/src/calibre/__init__.py index d21fcc8e87..2babb9182b 100644 --- a/src/calibre/__init__.py +++ b/src/calibre/__init__.py @@ -399,38 +399,49 @@ def my_unichr(num): except ValueError: return u'?' -def entity_to_unicode(match, exceptions=[], encoding='cp1252'): +def entity_to_unicode(match, exceptions=[], encoding='cp1252', + result_exceptions={}): ''' - @param match: A match object such that '&'+match.group(1)';' is the entity. - @param exceptions: A list of entities to not convert (Each entry is the name of the entity, for e.g. 'apos' or '#1234' - @param encoding: The encoding to use to decode numeric entities between 128 and 256. + :param match: A match object such that '&'+match.group(1)';' is the entity. + + :param exceptions: A list of entities to not convert (Each entry is the name of the entity, for e.g. 'apos' or '#1234' + + :param encoding: The encoding to use to decode numeric entities between 128 and 256. If None, the Unicode UCS encoding is used. A common encoding is cp1252. + + :param result_exceptions: A mapping of characters to entities. If the result + is in result_exceptions, result_exception[result] is returned instead. + Convenient way to specify exception for things like < or > that can be + specified by various actual entities. ''' + def check(ch): + return result_exceptions.get(ch, ch) + ent = match.group(1) if ent in exceptions: return '&'+ent+';' if ent == 'apos': - return "'" + return check("'") if ent == 'hellips': ent = 'hellip' - if ent.startswith(u'#x'): + if ent.lower().startswith(u'#x'): num = int(ent[2:], 16) if encoding is None or num > 255: - return my_unichr(num) - return chr(num).decode(encoding) + return check(my_unichr(num)) + return check(chr(num).decode(encoding)) if ent.startswith(u'#'): try: num = int(ent[1:]) except ValueError: return '&'+ent+';' if encoding is None or num > 255: - return my_unichr(num) + return check(my_unichr(num)) try: - return chr(num).decode(encoding) + return check(chr(num).decode(encoding)) except UnicodeDecodeError: - return my_unichr(num) + return check(my_unichr(num)) try: - return my_unichr(name2codepoint[ent]) + return check(my_unichr(name2codepoint[ent])) except KeyError: return '&'+ent+';' diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index c1d779bed9..44f4c61ca9 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -444,7 +444,7 @@ from calibre.devices.eslick.driver import ESLICK from calibre.devices.nuut2.driver import NUUT2 from calibre.devices.iriver.driver import IRIVER_STORY from calibre.devices.binatone.driver import README -from calibre.devices.hanvon.driver import N516, EB511 +from calibre.devices.hanvon.driver import N516, EB511, ALEX from calibre.devices.edge.driver import EDGE from calibre.devices.teclast.driver import TECLAST_K3 from calibre.devices.sne.driver import SNE @@ -526,7 +526,8 @@ plugins += [ ELONEX, TECLAST_K3, EDGE, - SNE + SNE, + ALEX ] plugins += [x for x in list(locals().values()) if isinstance(x, type) and \ x.__name__.endswith('MetadataReader')] diff --git a/src/calibre/devices/hanvon/driver.py b/src/calibre/devices/hanvon/driver.py index d4f6e87d06..1e76b62eb6 100644 --- a/src/calibre/devices/hanvon/driver.py +++ b/src/calibre/devices/hanvon/driver.py @@ -34,6 +34,22 @@ class N516(USBMS): EBOOK_DIR_MAIN = 'e_book' SUPPORTS_SUB_DIRS = True +class ALEX(N516): + + name = 'Alex driver' + gui_name = 'SpringDesign Alex' + description = _('Communicate with the SpringDesign Alex eBook reader.') + author = 'Kovid Goyal' + + FORMATS = ['epub', 'pdf'] + VENDOR_NAME = 'ALEX' + WINDOWS_MAIN_MEM = 'READER' + + MAIN_MEMORY_VOLUME_LABEL = 'Alex Internal Memory' + + EBOOK_DIR_MAIN = 'eBooks' + SUPPORTS_SUB_DIRS = True + class EB511(USBMS): name = 'Elonex EB 511 driver' gui_name = 'EB 511' diff --git a/src/calibre/ebooks/comic/input.py b/src/calibre/ebooks/comic/input.py index 925416627f..3a3cb7d83e 100755 --- a/src/calibre/ebooks/comic/input.py +++ b/src/calibre/ebooks/comic/input.py @@ -322,7 +322,7 @@ class ComicInput(InputFormatPlugin): ('margin_bottom', 0, OptionRecommendation.HIGH), ('insert_blank_line', False, OptionRecommendation.HIGH), ('remove_paragraph_spacing', False, OptionRecommendation.HIGH), - ('dont_justify', True, OptionRecommendation.HIGH), + ('change_justification', 'left', OptionRecommendation.HIGH), ('dont_split_on_pagebreaks', True, OptionRecommendation.HIGH), ('chapter', None, OptionRecommendation.HIGH), ('page_breaks_brefore', None, OptionRecommendation.HIGH), diff --git a/src/calibre/ebooks/conversion/cli.py b/src/calibre/ebooks/conversion/cli.py index 0b5c15b3f0..7439718cf6 100644 --- a/src/calibre/ebooks/conversion/cli.py +++ b/src/calibre/ebooks/conversion/cli.py @@ -124,7 +124,7 @@ def add_pipeline_options(parser, plumber): 'linearize_tables', 'extra_css', 'margin_top', 'margin_left', 'margin_right', - 'margin_bottom', 'dont_justify', + 'margin_bottom', 'change_justification', 'insert_blank_line', 'remove_paragraph_spacing','remove_paragraph_spacing_indent_size', 'asciiize', 'remove_header', 'header_regex', 'remove_footer', 'footer_regex', diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py index 304593a2ca..b9ff631cd2 100644 --- a/src/calibre/ebooks/conversion/plumber.py +++ b/src/calibre/ebooks/conversion/plumber.py @@ -299,12 +299,13 @@ OptionRecommendation(name='margin_right', help=_('Set the right margin in pts. Default is %default. ' 'Note: 72 pts equals 1 inch')), -OptionRecommendation(name='dont_justify', - recommended_value=False, level=OptionRecommendation.LOW, - help=_('Do not force text to be justified in output. Whether text ' - 'is actually displayed justified or not depends on whether ' - 'the ebook format and reading device support justification.') - ), +OptionRecommendation(name='change_justification', + recommended_value='original', level=OptionRecommendation.LOW, + choices=['left','justify','original'], + help=_('Specify optional justification override. A value of ' + '"left" or "justify" overrides default justification.' + 'A value of ' + '"original" uses existing alignment.')), OptionRecommendation(name='remove_paragraph_spacing', recommended_value=False, level=OptionRecommendation.LOW, diff --git a/src/calibre/ebooks/lrf/output.py b/src/calibre/ebooks/lrf/output.py index 4d6b248db5..8d3fb72be7 100644 --- a/src/calibre/ebooks/lrf/output.py +++ b/src/calibre/ebooks/lrf/output.py @@ -130,7 +130,7 @@ class LRFOutput(OutputFormatPlugin): ]) recommendations = set([ - ('dont_justify', True, OptionRecommendation.HIGH), + ('change_justification', 'original', OptionRecommendation.HIGH), ]) def convert_images(self, pages, opts, wide): diff --git a/src/calibre/ebooks/mobi/reader.py b/src/calibre/ebooks/mobi/reader.py index 84e6208086..1b266740d7 100644 --- a/src/calibre/ebooks/mobi/reader.py +++ b/src/calibre/ebooks/mobi/reader.py @@ -303,7 +303,12 @@ class MobiReader(object): for pat in ENCODING_PATS: self.processed_html = pat.sub('', self.processed_html) e2u = functools.partial(entity_to_unicode, - exceptions=['lt', 'gt', 'amp', 'apos', 'quot', '#60', '#62']) + result_exceptions={ + '<' : u'<', + '>' : u'>', + '&' : u'&', + '"' : u'"', + "'" : u'''}) self.processed_html = re.sub(r'&(\S+?);', e2u, self.processed_html) self.extract_images(processed_records, output_dir) @@ -619,6 +624,7 @@ class MobiReader(object): opf.cover = None cover = opf.cover + cover_copied = None if cover is not None: cover = cover.replace('/', os.sep) if os.path.exists(cover): @@ -626,13 +632,19 @@ class MobiReader(object): if os.path.exists(ncover): os.remove(ncover) shutil.copyfile(cover, ncover) - opf.cover = ncover.replace(os.sep, '/') + cover_copied = os.path.abspath(ncover) + opf.cover = ncover.replace(os.sep, '/') manifest = [(htmlfile, 'application/xhtml+xml'), (os.path.abspath('styles.css'), 'text/css')] bp = os.path.dirname(htmlfile) + added = set([]) for i in getattr(self, 'image_names', []): - manifest.append((os.path.join(bp, 'images/', i), 'image/jpeg')) + path = os.path.join(bp, 'images', i) + added.add(path) + manifest.append((path, 'image/jpeg')) + if cover_copied is not None: + manifest.append((cover_copied, 'image/jpeg')) opf.create_manifest(manifest) opf.create_spine([os.path.basename(htmlfile)]) diff --git a/src/calibre/ebooks/oeb/iterator.py b/src/calibre/ebooks/oeb/iterator.py index 87ce8683a9..2312ea308b 100644 --- a/src/calibre/ebooks/oeb/iterator.py +++ b/src/calibre/ebooks/oeb/iterator.py @@ -212,11 +212,12 @@ class EbookIterator(object): cover = self.opf.cover if self.ebook_ext in ('lit', 'mobi', 'prc', 'opf') and cover: - cfile = os.path.join(os.path.dirname(self.spine[0]), - 'calibre_iterator_cover.html') - chtml = (TITLEPAGE%cover).encode('utf-8') + cfile = os.path.join(self.base, 'calibre_iterator_cover.html') + chtml = (TITLEPAGE%os.path.relpath(cover, self.base).replace(os.sep, + '/')).encode('utf-8') open(cfile, 'wb').write(chtml) - self.spine[0:0] = [SpineItem(cfile)] + self.spine[0:0] = [SpineItem(cfile, + mime_type='application/xhtml+xml')] self.delete_on_exit.append(cfile) if self.opf.path_to_html_toc is not None and \ diff --git a/src/calibre/ebooks/oeb/stylizer.py b/src/calibre/ebooks/oeb/stylizer.py index 75c64a7dfd..c9f228a091 100644 --- a/src/calibre/ebooks/oeb/stylizer.py +++ b/src/calibre/ebooks/oeb/stylizer.py @@ -318,8 +318,8 @@ class Stylizer(object): if text == 'inherit': style['text-align'] = 'inherit' else: - if text in ('left', 'justify'): - val = 'left' if self.opts.dont_justify else 'justify' + if text in ('left', 'justify') and self.opts.change_justification in ('left', 'justify'): + val = self.opts.change_justification style['text-align'] = val else: style['text-align'] = text diff --git a/src/calibre/ebooks/oeb/transforms/flatcss.py b/src/calibre/ebooks/oeb/transforms/flatcss.py index 1eb6afc1b5..b8debff812 100644 --- a/src/calibre/ebooks/oeb/transforms/flatcss.py +++ b/src/calibre/ebooks/oeb/transforms/flatcss.py @@ -138,8 +138,8 @@ class CSSFlattener(object): float(self.context.margin_left)) bs.append('margin-right : %fpt'%\ float(self.context.margin_right)) - bs.append('text-align: '+ \ - ('left' if self.context.dont_justify else 'justify')) + if self.context.change_justification != 'original': + bs.append('text-align: '+ self.context.change_justification) body.set('style', '; '.join(bs)) stylizer = Stylizer(html, item.href, self.oeb, self.context, profile, user_css=self.context.extra_css, diff --git a/src/calibre/gui2/convert/look_and_feel.py b/src/calibre/gui2/convert/look_and_feel.py index 4d43f64910..8ef1f77351 100644 --- a/src/calibre/gui2/convert/look_and_feel.py +++ b/src/calibre/gui2/convert/look_and_feel.py @@ -19,7 +19,7 @@ class LookAndFeelWidget(Widget, Ui_Form): def __init__(self, parent, get_option, get_help, db=None, book_id=None): Widget.__init__(self, parent, 'look_and_feel', - ['dont_justify', 'extra_css', 'base_font_size', + ['change_justification', 'extra_css', 'base_font_size', 'font_size_mapping', 'line_height', 'linearize_tables', 'disable_font_rescaling', 'insert_blank_line', diff --git a/src/calibre/gui2/convert/look_and_feel.ui b/src/calibre/gui2/convert/look_and_feel.ui index 84d587d7b2..6fbf4e11cd 100644 --- a/src/calibre/gui2/convert/look_and_feel.ui +++ b/src/calibre/gui2/convert/look_and_feel.ui @@ -84,7 +84,7 @@ ... - + :/images/wizard.svg:/images/wizard.svg @@ -181,21 +181,7 @@ - - - - Insert &blank line - - - - - - No text &justification - - - - &Linearize tables @@ -221,6 +207,42 @@ + + + + Insert &blank line + + + + + + + Text justification: + + + + + + + 2 + + + + justify + + + + + left + + + + + original + + + + diff --git a/src/calibre/gui2/dialogs/scheduler.py b/src/calibre/gui2/dialogs/scheduler.py index 2fd26cffa1..296d01ecdd 100644 --- a/src/calibre/gui2/dialogs/scheduler.py +++ b/src/calibre/gui2/dialogs/scheduler.py @@ -29,6 +29,7 @@ class SchedulerDialog(QDialog, Ui_Dialog): self.recipe_model.do_refresh() self.search = SearchBox2(self) + self.search.setMinimumContentsLength(25) self.search.initialize('scheduler_search_history') self.recipe_box.layout().insertWidget(0, self.search) self.connect(self.search, SIGNAL('search(PyQt_PyObject,PyQt_PyObject)'), diff --git a/src/calibre/gui2/library.py b/src/calibre/gui2/library.py index 746d97ca32..3fe1cbc908 100644 --- a/src/calibre/gui2/library.py +++ b/src/calibre/gui2/library.py @@ -7,9 +7,9 @@ from math import cos, sin, pi from contextlib import closing from PyQt4.QtGui import QTableView, QAbstractItemView, QColor, \ - QItemDelegate, QPainterPath, QLinearGradient, QBrush, \ - QPen, QStyle, QPainter, \ - QImage, QApplication, QMenu, \ + QPainterPath, QLinearGradient, QBrush, \ + QPen, QStyle, QPainter, QStyleOptionViewItemV4, \ + QImage, QMenu, \ QStyledItemDelegate, QCompleter from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, pyqtSignal, \ SIGNAL, QObject, QSize, QModelIndex, QDate @@ -28,14 +28,15 @@ from calibre.ebooks.metadata import string_to_authors, fmt_sidx, \ from calibre.utils.config import tweaks from calibre.utils.date import dt_factory, qt_to_dt, isoformat -class LibraryDelegate(QItemDelegate): +class LibraryDelegate(QStyledItemDelegate): COLOR = QColor("blue") SIZE = 16 PEN = QPen(COLOR, 1, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin) def __init__(self, parent): - QItemDelegate.__init__(self, parent) + QStyledItemDelegate.__init__(self, parent) self._parent = parent + self.dummy = QModelIndex() self.star_path = QPainterPath() self.star_path.moveTo(90, 50) for i in range(1, 5): @@ -54,6 +55,9 @@ class LibraryDelegate(QItemDelegate): return QSize(5*(self.SIZE), self.SIZE+4) def paint(self, painter, option, index): + style = self._parent.style() + option = QStyleOptionViewItemV4(option) + self.initStyleOption(option, self.dummy) num = index.model().data(index, Qt.DisplayRole).toInt()[0] def draw_star(): painter.save() @@ -66,11 +70,10 @@ class LibraryDelegate(QItemDelegate): painter.save() if hasattr(QStyle, 'CE_ItemViewItem'): - QApplication.style().drawControl(QStyle.CE_ItemViewItem, option, + style.drawControl(QStyle.CE_ItemViewItem, option, painter, self._parent) elif option.state & QStyle.State_Selected: painter.fillRect(option.rect, option.palette.highlight()) - self.drawFocus(painter, option, option.rect) try: painter.setRenderHint(QPainter.Antialiasing) painter.setClipRect(option.rect) @@ -89,7 +92,7 @@ class LibraryDelegate(QItemDelegate): painter.restore() def createEditor(self, parent, option, index): - sb = QItemDelegate.createEditor(self, parent, option, index) + sb = QStyledItemDelegate.createEditor(self, parent, option, index) sb.setMinimum(0) sb.setMaximum(5) return sb diff --git a/src/calibre/gui2/viewer/main.py b/src/calibre/gui2/viewer/main.py index f27f17e6ca..689613111e 100644 --- a/src/calibre/gui2/viewer/main.py +++ b/src/calibre/gui2/viewer/main.py @@ -194,6 +194,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer): self.tool_bar2.insertSeparator(self.action_find_next) self.setFocusPolicy(Qt.StrongFocus) self.search = SearchBox2(self) + self.search.setMinimumContentsLength(20) self.search.initialize('viewer_search_history') self.search.setToolTip(_('Search for text in book')) self.search.setMinimumWidth(200) diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py index c7031992e8..586966d94f 100644 --- a/src/calibre/gui2/widgets.py +++ b/src/calibre/gui2/widgets.py @@ -551,7 +551,8 @@ class LineEditECM(object): self.setText(unicode(self.text()).swapcase()) def title_case(self): - self.setText(unicode(self.text()).title()) + from calibre.utils.titlecase import titlecase + self.setText(titlecase(unicode(self.text()))) class EnLineEdit(LineEditECM, QLineEdit): diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 9ff1c14576..8f42107944 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -123,10 +123,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.is_case_sensitive = not iswindows and not isosx and \ not os.path.exists(self.dbpath.replace('metadata.db', 'MeTAdAtA.dB')) SchemaUpgrade.__init__(self) - CustomColumns.__init__(self) self.initialize_dynamic() def initialize_dynamic(self): + CustomColumns.__init__(self) template = '''\ (SELECT {query} FROM books_{table}_link AS link INNER JOIN {table} ON(link.{link_col}={table}.id) WHERE link.book=books.id) @@ -1428,6 +1428,7 @@ books_series_link feeds os.remove(self.dbpath) shutil.copyfile(dest, self.dbpath) self.connect() + self.initialize_dynamic() self.refresh() if os.path.exists(dest): os.remove(dest) diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst index b88c4474df..7c2f4ce49e 100644 --- a/src/calibre/manual/faq.rst +++ b/src/calibre/manual/faq.rst @@ -81,7 +81,7 @@ Device Integration What devices does |app| support? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -At the moment |app| has full support for the SONY PRS 300/500/505/600/700/900, Barnes & Noble Nook, Cybook Gen 3/Opus, Amazon Kindle 1/2/DX, Entourage Edge, Longshine ShineBook, Ectaco Jetbook, BeBook/BeBook Mini, Irex Illiad/DR1000, Foxit eSlick, PocketBook 360, Italica, eClicto, Iriver Story, Airis dBook, Hanvon N515, Binatone Readme, Teclast K3, various Android phones and the iPhone. In addition, using the :guilabel:`Save to disk` function you can use it with any ebook reader that exports itself as a USB disk. +At the moment |app| has full support for the SONY PRS 300/500/505/600/700/900, Barnes & Noble Nook, Cybook Gen 3/Opus, Amazon Kindle 1/2/DX, Entourage Edge, Longshine ShineBook, Ectaco Jetbook, BeBook/BeBook Mini, Irex Illiad/DR1000, Foxit eSlick, PocketBook 360, Italica, eClicto, Iriver Story, Airis dBook, Hanvon N515, Binatone Readme, Teclast K3, SpringDesign Alex, various Android phones and the iPhone. In addition, using the :guilabel:`Save to disk` function you can use it with any ebook reader that exports itself as a USB disk. How can I help get my device supported in |app|? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/calibre/utils/titlecase.py b/src/calibre/utils/titlecase.py new file mode 100755 index 0000000000..3ead4848fd --- /dev/null +++ b/src/calibre/utils/titlecase.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Original Perl version by: John Gruber http://daringfireball.net/ 10 May 2008 +Python version by Stuart Colville http://muffinresearch.co.uk +License: http://www.opensource.org/licenses/mit-license.php +""" + +import re + +__all__ = ['titlecase'] +__version__ = '0.5' + +SMALL = 'a|an|and|as|at|but|by|en|for|if|in|of|on|or|the|to|v\.?|via|vs\.?' +PUNCT = r"""!"#$%&'‘()*+,\-./:;?@[\\\]_`{|}~""" + +SMALL_WORDS = re.compile(r'^(%s)$' % SMALL, re.I) +INLINE_PERIOD = re.compile(r'[a-z][.][a-z]', re.I) +UC_ELSEWHERE = re.compile(r'[%s]*?[a-zA-Z]+[A-Z]+?' % PUNCT) +CAPFIRST = re.compile(r"^[%s]*?([A-Za-z])" % PUNCT) +SMALL_FIRST = re.compile(r'^([%s]*)(%s)\b' % (PUNCT, SMALL), re.I) +SMALL_LAST = re.compile(r'\b(%s)[%s]?$' % (SMALL, PUNCT), re.I) +SUBPHRASE = re.compile(r'([:.;?!][ ])(%s)' % SMALL) +APOS_SECOND = re.compile(r"^[dol]{1}['‘]{1}[a-z]+$", re.I) +ALL_CAPS = re.compile(r'^[A-Z\s%s]+$' % PUNCT) +UC_INITIALS = re.compile(r"^(?:[A-Z]{1}\.{1}|[A-Z]{1}\.{1}[A-Z]{1})+$") +MAC_MC = re.compile(r"^([Mm]a?c)(\w+)") + +def titlecase(text): + + """ + Titlecases input text + + This filter changes all words to Title Caps, and attempts to be clever + about *un*capitalizing SMALL words like a/an/the in the input. + + The list of "SMALL words" which are not capped comes from + the New York Times Manual of Style, plus 'vs' and 'v'. + + """ + + all_caps = ALL_CAPS.match(text) + + words = re.split('\s', text) + line = [] + for word in words: + if all_caps: + if UC_INITIALS.match(word): + line.append(word) + continue + else: + word = word.lower() + + if APOS_SECOND.match(word): + word = word.replace(word[0], word[0].upper()) + word = word.replace(word[2], word[2].upper()) + line.append(word) + continue + if INLINE_PERIOD.search(word) or UC_ELSEWHERE.match(word): + line.append(word) + continue + if SMALL_WORDS.match(word): + line.append(word.lower()) + continue + + match = MAC_MC.match(word) + if match: + line.append("%s%s" % (match.group(1).capitalize(), + match.group(2).capitalize())) + continue + + hyphenated = [] + for item in word.split('-'): + hyphenated.append(CAPFIRST.sub(lambda m: m.group(0).upper(), item)) + line.append("-".join(hyphenated)) + + + result = " ".join(line) + + result = SMALL_FIRST.sub(lambda m: '%s%s' % ( + m.group(1), + m.group(2).capitalize() + ), result) + + result = SMALL_LAST.sub(lambda m: m.group(0).capitalize(), result) + + result = SUBPHRASE.sub(lambda m: '%s%s' % ( + m.group(1), + m.group(2).capitalize() + ), result) + + return result + diff --git a/src/calibre/web/feeds/recipes/model.py b/src/calibre/web/feeds/recipes/model.py index d7da358765..1584908237 100644 --- a/src/calibre/web/feeds/recipes/model.py +++ b/src/calibre/web/feeds/recipes/model.py @@ -113,7 +113,7 @@ class NewsItem(NewsTreeItem): return NONE def __cmp__(self, other): - return cmp(self.title, getattr(other, 'title', '')) + return cmp(self.title.lower(), getattr(other, 'title', '').lower()) class RecipeModel(QAbstractItemModel, SearchQueryParser):