diff --git a/resources/recipes/ajiajin.recipe b/resources/recipes/ajiajin.recipe
new file mode 100644
index 0000000000..344d3d21fb
--- /dev/null
+++ b/resources/recipes/ajiajin.recipe
@@ -0,0 +1,23 @@
+__license__ = 'GPL v3'
+__copyright__ = '2010, Hiroshi Miura '
+'''
+ajiajin.com/blog
+'''
+
+from calibre.web.feeds.news import BasicNewsRecipe
+
+class AjiajinBlog(BasicNewsRecipe):
+ title = u'Ajiajin blog'
+ __author__ = 'Hiroshi Miura'
+ oldest_article = 5
+ publication_type = 'blog'
+ max_articles_per_feed = 100
+ description = 'The next generation internet trends in Japan and Asia'
+ publisher = ''
+ category = 'internet, asia, japan'
+ language = 'en'
+ encoding = 'utf-8'
+
+ feeds = [(u'blog', u'http://feeds.feedburner.com/Asiajin')]
+
+
diff --git a/resources/recipes/chouchoublog.recipe b/resources/recipes/chouchoublog.recipe
new file mode 100644
index 0000000000..8c953deef0
--- /dev/null
+++ b/resources/recipes/chouchoublog.recipe
@@ -0,0 +1,37 @@
+__license__ = 'GPL v3'
+__copyright__ = '2010, Hiroshi Miura '
+'''
+http://ameblo.jp/
+'''
+
+import re
+from calibre.web.feeds.news import BasicNewsRecipe
+
+class SakuraBlog(BasicNewsRecipe):
+ title = u'chou chou blog'
+ __author__ = 'Hiroshi Miura'
+ oldest_article = 4
+ publication_type = 'blog'
+ max_articles_per_feed = 20
+ description = 'Japanese popular dog blog'
+ publisher = ''
+ category = 'dog, pet, japan'
+ language = 'ja'
+ encoding = 'utf-8'
+ use_embedded_content = True
+
+ feeds = [(u'blog', u'http://feedblog.ameba.jp/rss/ameblo/chouchou1218/rss20.xml')]
+
+ def parse_feeds(self):
+ feeds = BasicNewsRecipe.parse_feeds(self)
+ for curfeed in feeds:
+ delList = []
+ for a,curarticle in enumerate(curfeed.articles):
+ if re.search(r'rssad.jp', curarticle.url):
+ delList.append(curarticle)
+ if len(delList)>0:
+ for d in delList:
+ index = curfeed.articles.index(d)
+ curfeed.articles[index:index+1] = []
+ return feeds
+
diff --git a/resources/recipes/dilbert.recipe b/resources/recipes/dilbert.recipe
index 82966b1d15..2c3268da2f 100644
--- a/resources/recipes/dilbert.recipe
+++ b/resources/recipes/dilbert.recipe
@@ -3,15 +3,16 @@ __copyright__ = '2009, Darko Miletic '
'''
http://www.dilbert.com
'''
-import re
from calibre.web.feeds.recipes import BasicNewsRecipe
+import re
-class DosisDiarias(BasicNewsRecipe):
+class DilbertBig(BasicNewsRecipe):
title = 'Dilbert'
- __author__ = 'Darko Miletic'
+ __author__ = 'Darko Miletic and Starson17'
description = 'Dilbert'
- oldest_article = 5
+ reverse_article_order = True
+ oldest_article = 15
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = True
@@ -29,20 +30,23 @@ class DosisDiarias(BasicNewsRecipe):
feeds = [(u'Dilbert', u'http://feeds.dilbert.com/DilbertDailyStrip' )]
- preprocess_regexps = [
- (re.compile('strip\..*\.gif', re.DOTALL|re.IGNORECASE),
- lambda match: 'strip.zoom.gif')
- ]
-
-
def get_article_url(self, article):
return article.get('feedburner_origlink', None)
+ preprocess_regexps = [
+ (re.compile('strip\..*\.gif', re.DOTALL|re.IGNORECASE), lambda match: 'strip.zoom.gif')
+ ]
+
def preprocess_html(self, soup):
for tag in soup.findAll(name='a'):
if tag['href'].find('http://feedads') >= 0:
tag.extract()
return soup
-
-
+ extra_css = '''
+ h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;}
+ h2{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;}
+ img {max-width:100%; min-width:100%;}
+ p{font-family:Arial,Helvetica,sans-serif;font-size:small;}
+ body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
+ '''
diff --git a/resources/recipes/kahokushinpo.recipe b/resources/recipes/kahokushinpo.recipe
new file mode 100644
index 0000000000..06879a1375
--- /dev/null
+++ b/resources/recipes/kahokushinpo.recipe
@@ -0,0 +1,31 @@
+__license__ = 'GPL v3'
+__copyright__ = '2010, Hiroshi Miura '
+'''
+www.kahoku.co.jp
+'''
+
+from calibre.web.feeds.news import BasicNewsRecipe
+
+
+class KahokuShinpoNews(BasicNewsRecipe):
+ title = u'\u6cb3\u5317\u65b0\u5831'
+ __author__ = 'Hiroshi Miura'
+ oldest_article = 2
+ max_articles_per_feed = 20
+ description = 'Tohoku regional news paper in Japan'
+ publisher = 'Kahoku Shinpo Sha'
+ category = 'news, japan'
+ language = 'ja'
+ encoding = 'Shift_JIS'
+ no_stylesheets = True
+
+ feeds = [(u'news', u'http://www.kahoku.co.jp/rss/index_thk.xml')]
+
+ keep_only_tags = [ dict(id="page_title"),
+ dict(id="news_detail"),
+ dict(id="bt_title"),
+ {'class':"photoLeft"},
+ dict(id="bt_body")
+ ]
+ remove_tags = [ {'class':"button"}]
+
diff --git a/resources/recipes/nationalgeographic.recipe b/resources/recipes/nationalgeographic.recipe
new file mode 100644
index 0000000000..f00c9206bd
--- /dev/null
+++ b/resources/recipes/nationalgeographic.recipe
@@ -0,0 +1,38 @@
+__license__ = 'GPL v3'
+__copyright__ = '2010, Hiroshi Miura '
+'''
+nationalgeographic.com
+'''
+
+from calibre.web.feeds.news import BasicNewsRecipe
+import re
+
+class NationalGeographicNews(BasicNewsRecipe):
+ title = u'National Geographic News'
+ oldest_article = 7
+ max_articles_per_feed = 100
+ remove_javascript = True
+ no_stylesheets = True
+ use_embedded_content = False
+
+ feeds = [(u'news', u'http://feeds.nationalgeographic.com/ng/News/News_Main')]
+
+ remove_tags_before = dict(id='page_head')
+ remove_tags_after = [dict(id='social_buttons'),{'class':'aside'}]
+ remove_tags = [
+ {'class':'hidden'}
+
+ ]
+
+ def parse_feeds(self):
+ feeds = BasicNewsRecipe.parse_feeds(self)
+ for curfeed in feeds:
+ delList = []
+ for a,curarticle in enumerate(curfeed.articles):
+ if re.search(r'ads\.pheedo\.com', curarticle.url):
+ delList.append(curarticle)
+ if len(delList)>0:
+ for d in delList:
+ index = curfeed.articles.index(d)
+ curfeed.articles[index:index+1] = []
+ return feeds
diff --git a/resources/recipes/nationalgeographicjp.recipe b/resources/recipes/nationalgeographicjp.recipe
new file mode 100644
index 0000000000..5798acb102
--- /dev/null
+++ b/resources/recipes/nationalgeographicjp.recipe
@@ -0,0 +1,20 @@
+__license__ = 'GPL v3'
+__copyright__ = '2010, Hiroshi Miura '
+'''
+nationalgeographic.co.jp
+'''
+
+from calibre.web.feeds.news import BasicNewsRecipe
+import re
+
+class NationalGeoJp(BasicNewsRecipe):
+ title = u'\u30ca\u30b7\u30e7\u30ca\u30eb\u30fb\u30b8\u30aa\u30b0\u30e9\u30d5\u30a3\u30c3\u30af\u30cb\u30e5\u30fc\u30b9'
+ oldest_article = 7
+ max_articles_per_feed = 100
+ no_stylesheets = True
+
+ feeds = [(u'news', u'http://www.nationalgeographic.co.jp/news/rss.php')]
+
+ def print_version(self, url):
+ return re.sub(r'news_article.php','news_printer_friendly.php', url)
+
diff --git a/resources/recipes/nikkei_sub_shakai.recipe b/resources/recipes/nikkei_sub_shakai.recipe
index ed86493265..9a53e910e6 100644
--- a/resources/recipes/nikkei_sub_shakai.recipe
+++ b/resources/recipes/nikkei_sub_shakai.recipe
@@ -10,8 +10,8 @@ import mechanize
from calibre.ptempfile import PersistentTemporaryFile
-class NikkeiNet_sub_life(BasicNewsRecipe):
- title = u'\u65e5\u7d4c\u65b0\u805e\u96fb\u5b50\u7248(\u751f\u6d3b)'
+class NikkeiNet_sub_shakai(BasicNewsRecipe):
+ title = u'\u65e5\u7d4c\u65b0\u805e\u96fb\u5b50\u7248(Social)'
__author__ = 'Hiroshi Miura'
description = 'News and current market affairs from Japan'
cover_url = 'http://parts.nikkei.com/parts/ds/images/common/logo_r1.svg'
diff --git a/resources/recipes/paperli_topic.recipe b/resources/recipes/paperli_topic.recipe
new file mode 100644
index 0000000000..1ccf5f7945
--- /dev/null
+++ b/resources/recipes/paperli_topic.recipe
@@ -0,0 +1,58 @@
+__license__ = 'GPL v3'
+__copyright__ = '2010, Hiroshi Miura '
+'''
+paperli
+'''
+
+from calibre.web.feeds.news import BasicNewsRecipe
+from calibre import strftime
+
+class paperli_topics(BasicNewsRecipe):
+ # Customize this recipe and change paperli_tag and title below to
+ # download news on your favorite tag
+ paperli_tag = 'climate'
+ title = u'The #climate Daily - paperli'
+#-------------------------------------------------------------
+ __author__ = 'Hiroshi Miura'
+ oldest_article = 7
+ max_articles_per_feed = 100
+ description = 'paper.li page about '+ paperli_tag
+ publisher = 'paper.li'
+ category = 'paper.li'
+ language = 'en'
+ encoding = 'utf-8'
+ remove_javascript = True
+ masthead_title = u'The '+ paperli_tag +' Daily'
+ timefmt = '[%y/%m/%d]'
+ base_url = 'http://paper.li'
+ index = base_url+'/tag/'+paperli_tag
+
+
+ def parse_index(self):
+ # get topics
+ topics = []
+ soup = self.index_to_soup(self.index)
+ topics_lists = soup.find('div',attrs={'class':'paper-nav-bottom'})
+ for item in topics_lists.findAll('li', attrs={'class':""}):
+ itema = item.find('a',href=True)
+ topics.append({'title': itema.string, 'url': itema['href']})
+
+ #get feeds
+ feeds = []
+ for topic in topics:
+ newsarticles = []
+ soup = self.index_to_soup(''.join([self.base_url, topic['url'] ]))
+ topstories = soup.findAll('div',attrs={'class':'yui-u'})
+ for itt in topstories:
+ itema = itt.find('a',href=True,attrs={'class':'ts'})
+ if itema is not None:
+ itemd = itt.find('div',text=True, attrs={'class':'text'})
+ newsarticles.append({
+ 'title' :itema.string
+ ,'date' :strftime(self.timefmt)
+ ,'url' :itema['href']
+ ,'description':itemd.string
+ })
+ feeds.append((topic['title'], newsarticles))
+ return feeds
+
diff --git a/resources/recipes/science_based_medicine.recipe b/resources/recipes/science_based_medicine.recipe
new file mode 100644
index 0000000000..7aa28cb170
--- /dev/null
+++ b/resources/recipes/science_based_medicine.recipe
@@ -0,0 +1,42 @@
+#!/usr/bin/env python
+
+import re
+from calibre.web.feeds.news import BasicNewsRecipe
+from calibre.ebooks.BeautifulSoup import Tag
+
+class SBM(BasicNewsRecipe):
+ title = 'Science Based Medicine'
+ __author__ = 'BuzzKill'
+ description = 'Exploring issues and controversies in the relationship between science and medicine'
+ oldest_article = 5
+ max_articles_per_feed = 15
+ no_stylesheets = True
+ use_embedded_content = False
+ encoding = 'utf-8'
+ publisher = 'SBM'
+ category = 'science, sbm, ebm, blog, pseudoscience'
+ language = 'en'
+
+ lang = 'en-US'
+
+ conversion_options = {
+ 'comment' : description
+ , 'tags' : category
+ , 'publisher' : publisher
+ , 'language' : lang
+ , 'pretty_print' : True
+ }
+
+ keep_only_tags = [
+ dict(name='a', attrs={'title':re.compile(r'Posts by.*', re.DOTALL|re.IGNORECASE)}),
+ dict(name='div', attrs={'class':'entry'})
+ ]
+
+ feeds = [(u'Science Based Medicine', u'http://www.sciencebasedmedicine.org/?feed=rss2')]
+
+ def preprocess_html(self, soup):
+ mtag = Tag(soup,'meta',[('http-equiv','Content-Type'),('context','text/html; charset=utf-8')])
+ soup.head.insert(0,mtag)
+ soup.html['lang'] = self.lang
+ return self.adeify_images(soup)
+
diff --git a/resources/recipes/uninohimitu.recipe b/resources/recipes/uninohimitu.recipe
new file mode 100644
index 0000000000..aac412744c
--- /dev/null
+++ b/resources/recipes/uninohimitu.recipe
@@ -0,0 +1,36 @@
+__license__ = 'GPL v3'
+__copyright__ = '2010, Hiroshi Miura '
+'''
+http://ameblo.jp/sauta19/
+'''
+
+import re
+from calibre.web.feeds.news import BasicNewsRecipe
+
+class UniNoHimituKichiBlog(BasicNewsRecipe):
+ title = u'Uni secret base'
+ __author__ = 'Hiroshi Miura'
+ oldest_article = 2
+ publication_type = 'blog'
+ max_articles_per_feed = 20
+ description = 'Japanese famous Cat blog'
+ publisher = ''
+ category = 'cat, pet, japan'
+ language = 'ja'
+ encoding = 'utf-8'
+
+ feeds = [(u'blog', u'http://feedblog.ameba.jp/rss/ameblo/sauta19/rss20.xml')]
+
+ def parse_feeds(self):
+ feeds = BasicNewsRecipe.parse_feeds(self)
+ for curfeed in feeds:
+ delList = []
+ for a,curarticle in enumerate(curfeed.articles):
+ if re.search(r'rssad.jp', curarticle.url):
+ delList.append(curarticle)
+ if len(delList)>0:
+ for d in delList:
+ index = curfeed.articles.index(d)
+ curfeed.articles[index:index+1] = []
+ return feeds
+
diff --git a/resources/recipes/zeitde.recipe b/resources/recipes/zeitde.recipe
index 64345ea675..389bdec670 100644
--- a/resources/recipes/zeitde.recipe
+++ b/resources/recipes/zeitde.recipe
@@ -60,8 +60,8 @@ class ZeitDe(BasicNewsRecipe):
for tag in soup.findAll(name=['ul','li']):
tag.name = 'div'
- soup.html['xml:lang'] = self.lang
- soup.html['lang'] = self.lang
+ soup.html['xml:lang'] = self.language.replace('_', '-')
+ soup.html['lang'] = self.language.replace('_', '-')
mtag = ''
soup.head.insert(0,mtag)
return soup
diff --git a/src/calibre/devices/errors.py b/src/calibre/devices/errors.py
index 7464d6635e..3d88eb741f 100644
--- a/src/calibre/devices/errors.py
+++ b/src/calibre/devices/errors.py
@@ -36,6 +36,11 @@ class UserFeedback(DeviceError):
self.details = details
self.msg = msg
+class OpenFeedback(DeviceError):
+ def __init__(self, msg):
+ self.feedback_msg = msg
+ DeviceError.__init__(self, msg)
+
class DeviceBusy(ProtocolError):
""" Raised when device is busy """
def __init__(self, uerr=""):
diff --git a/src/calibre/devices/interface.py b/src/calibre/devices/interface.py
index 48d751fc29..2a92f46e8d 100644
--- a/src/calibre/devices/interface.py
+++ b/src/calibre/devices/interface.py
@@ -216,6 +216,9 @@ class DevicePlugin(Plugin):
an implementation of
this function that should serve as a good example for USB Mass storage
devices.
+
+ This method can raise an OpenFeedback exception to display a message to
+ the user.
'''
raise NotImplementedError()
diff --git a/src/calibre/ebooks/conversion/cli.py b/src/calibre/ebooks/conversion/cli.py
index 62a941142b..3178fe1b43 100644
--- a/src/calibre/ebooks/conversion/cli.py
+++ b/src/calibre/ebooks/conversion/cli.py
@@ -120,7 +120,7 @@ def add_pipeline_options(parser, plumber):
[
'base_font_size', 'disable_font_rescaling',
'font_size_mapping',
- 'line_height',
+ 'line_height', 'minimum_line_height',
'linearize_tables',
'extra_css', 'smarten_punctuation',
'margin_top', 'margin_left', 'margin_right',
diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py
index 9a863d7e66..f5beba375d 100644
--- a/src/calibre/ebooks/conversion/plumber.py
+++ b/src/calibre/ebooks/conversion/plumber.py
@@ -160,13 +160,30 @@ OptionRecommendation(name='disable_font_rescaling',
)
),
+OptionRecommendation(name='minimum_line_height',
+ recommended_value=120.0, level=OptionRecommendation.LOW,
+ help=_(
+ 'The minimum line height, as a percentage of the element\'s '
+ 'calculated font size. calibre will ensure that every element '
+ 'has a line height of at least this setting, irrespective of '
+ 'what the input document specifies. Set to zero to disable. '
+ 'Default is 120%. Use this setting in preference to '
+ 'the direct line height specification, unless you know what '
+ 'you are doing. For example, you can achieve "double spaced" '
+ 'text by setting this to 240.'
+ )
+ ),
+
OptionRecommendation(name='line_height',
recommended_value=0, level=OptionRecommendation.LOW,
- help=_('The line height in pts. Controls spacing between consecutive '
- 'lines of text. By default no line height manipulation is '
- 'performed.'
- )
+ help=_(
+ 'The line height in pts. Controls spacing between consecutive '
+ 'lines of text. Only applies to elements that do not define '
+ 'their own line height. In most cases, the minimum line height '
+ 'option is more useful. '
+ 'By default no line height manipulation is performed.'
+ )
),
OptionRecommendation(name='linearize_tables',
diff --git a/src/calibre/ebooks/fb2/fb2ml.py b/src/calibre/ebooks/fb2/fb2ml.py
index 51bfaa7293..5efc360f1f 100644
--- a/src/calibre/ebooks/fb2/fb2ml.py
+++ b/src/calibre/ebooks/fb2/fb2ml.py
@@ -73,6 +73,10 @@ class FB2MLizer(object):
text = re.sub(r'(?miu)\s*
', '', text)
text = re.sub(r'(?miu)\s+
', '', text)
text = re.sub(r'(?miu)', '
\n\n', text)
+
+ if self.opts.insert_blank_line:
+ text = re.sub(r'(?miu)
', '', text)
+
return text
def fb2_header(self):
@@ -293,6 +297,18 @@ class FB2MLizer(object):
s_out, s_tags = self.handle_simple_tag('emphasis', tag_stack+tags)
fb2_out += s_out
tags += s_tags
+ elif tag in ('del', 'strike'):
+ s_out, s_tags = self.handle_simple_tag('strikethrough', tag_stack+tags)
+ fb2_out += s_out
+ tags += s_tags
+ elif tag == 'sub':
+ s_out, s_tags = self.handle_simple_tag('sub', tag_stack+tags)
+ fb2_out += s_out
+ tags += s_tags
+ elif tag == 'sup':
+ s_out, s_tags = self.handle_simple_tag('sup', tag_stack+tags)
+ fb2_out += s_out
+ tags += s_tags
# Processes style information.
if style['font-style'] == 'italic':
@@ -303,6 +319,10 @@ class FB2MLizer(object):
s_out, s_tags = self.handle_simple_tag('strong', tag_stack+tags)
fb2_out += s_out
tags += s_tags
+ elif style['text-decoration'] == 'line-through':
+ s_out, s_tags = self.handle_simple_tag('strikethrough', tag_stack+tags)
+ fb2_out += s_out
+ tags += s_tags
# Process element text.
if hasattr(elem_tree, 'text') and elem_tree.text:
diff --git a/src/calibre/ebooks/html/input.py b/src/calibre/ebooks/html/input.py
index 059aeca324..6f875ae803 100644
--- a/src/calibre/ebooks/html/input.py
+++ b/src/calibre/ebooks/html/input.py
@@ -314,6 +314,8 @@ class HTMLInput(InputFormatPlugin):
rewrite_links, urlnormalize, urldefrag, BINARY_MIME, OEB_STYLES, \
xpath
from calibre import guess_type
+ from calibre.ebooks.oeb.transforms.metadata import \
+ meta_info_to_oeb_metadata
import cssutils
self.OEB_STYLES = OEB_STYLES
oeb = create_oebbook(log, None, opts, self,
@@ -321,15 +323,7 @@ class HTMLInput(InputFormatPlugin):
self.oeb = oeb
metadata = oeb.metadata
- if mi.title:
- metadata.add('title', mi.title)
- if mi.authors:
- for a in mi.authors:
- metadata.add('creator', a, attrib={'role':'aut'})
- if mi.publisher:
- metadata.add('publisher', mi.publisher)
- if mi.isbn:
- metadata.add('identifier', mi.isbn, attrib={'scheme':'ISBN'})
+ meta_info_to_oeb_metadata(mi, metadata, log)
if not metadata.language:
oeb.logger.warn(u'Language not specified')
metadata.add('language', get_lang().replace('_', '-'))
diff --git a/src/calibre/ebooks/metadata/html.py b/src/calibre/ebooks/metadata/html.py
index f4eaa7cc61..fd42b2882f 100644
--- a/src/calibre/ebooks/metadata/html.py
+++ b/src/calibre/ebooks/metadata/html.py
@@ -170,7 +170,27 @@ def get_metadata_(src, encoding=None):
if match:
series = match.group(1)
if series:
+ pat = re.compile(r'\[([.0-9]+)\]')
+ match = pat.search(series)
+ series_index = None
+ if match is not None:
+ try:
+ series_index = float(match.group(1))
+ except:
+ pass
+ series = series.replace(match.group(), '').strip()
+
mi.series = ent_pat.sub(entity_to_unicode, series)
+ if series_index is None:
+ pat = get_meta_regexp_("Seriesnumber")
+ match = pat.search(src)
+ if match:
+ try:
+ series_index = float(match.group(1))
+ except:
+ pass
+ if series_index is not None:
+ mi.series_index = series_index
# RATING
rating = None
diff --git a/src/calibre/ebooks/mobi/mobiml.py b/src/calibre/ebooks/mobi/mobiml.py
index 8d20179250..001cf2c1e9 100644
--- a/src/calibre/ebooks/mobi/mobiml.py
+++ b/src/calibre/ebooks/mobi/mobiml.py
@@ -184,7 +184,7 @@ class MobiMLizer(object):
para.attrib['value'] = str(istates[-2].list_num)
elif tag in NESTABLE_TAGS and istate.rendered:
para = wrapper = bstate.nested[-1]
- elif left > 0 and indent >= 0:
+ elif not self.opts.mobi_ignore_margins and left > 0 and indent >= 0:
ems = self.profile.mobi_ems_per_blockquote
para = wrapper = etree.SubElement(parent, XHTML('blockquote'))
para = wrapper
diff --git a/src/calibre/ebooks/mobi/output.py b/src/calibre/ebooks/mobi/output.py
index 4159c6dd40..a6f6c52b7f 100644
--- a/src/calibre/ebooks/mobi/output.py
+++ b/src/calibre/ebooks/mobi/output.py
@@ -39,6 +39,12 @@ class MOBIOutput(OutputFormatPlugin):
OptionRecommendation(name='personal_doc', recommended_value='[PDOC]',
help=_('Tag marking book to be filed with Personal Docs')
),
+ OptionRecommendation(name='mobi_ignore_margins',
+ recommended_value=False,
+ help=_('Ignore margins in the input document. If False, then '
+ 'the MOBI output plugin will try to convert margins specified'
+ ' in the input document, otherwise it will ignore them.')
+ ),
])
def check_for_periodical(self):
diff --git a/src/calibre/ebooks/oeb/stylizer.py b/src/calibre/ebooks/oeb/stylizer.py
index 6c0c384eb3..616cd3b800 100644
--- a/src/calibre/ebooks/oeb/stylizer.py
+++ b/src/calibre/ebooks/oeb/stylizer.py
@@ -633,12 +633,12 @@ class Style(object):
parent = self._getparent()
if 'line-height' in self._style:
lineh = self._style['line-height']
+ if lineh == 'normal':
+ lineh = '1.2'
try:
- float(lineh)
+ result = float(lineh) * self.fontSize
except ValueError:
result = self._unit_convert(lineh, base=self.fontSize)
- else:
- result = float(lineh) * self.fontSize
elif parent is not None:
# TODO: proper inheritance
result = parent.lineHeight
diff --git a/src/calibre/ebooks/oeb/transforms/flatcss.py b/src/calibre/ebooks/oeb/transforms/flatcss.py
index 7b83421097..653aa4533b 100644
--- a/src/calibre/ebooks/oeb/transforms/flatcss.py
+++ b/src/calibre/ebooks/oeb/transforms/flatcss.py
@@ -245,6 +245,8 @@ class CSSFlattener(object):
del node.attrib['bgcolor']
if cssdict.get('font-weight', '').lower() == 'medium':
cssdict['font-weight'] = 'normal' # ADE chokes on font-weight medium
+
+ fsize = font_size
if not self.context.disable_font_rescaling:
_sbase = self.sbase if self.sbase is not None else \
self.context.source.fbase
@@ -258,6 +260,14 @@ class CSSFlattener(object):
fsize = self.fmap[font_size]
cssdict['font-size'] = "%0.5fem" % (fsize / psize)
psize = fsize
+
+ try:
+ minlh = self.context.minimum_line_height / 100.
+ if style['line-height'] < minlh * fsize:
+ cssdict['line-height'] = str(minlh)
+ except:
+ self.oeb.logger.exception('Failed to set minimum line-height')
+
if cssdict:
if self.lineh and self.fbase and tag != 'body':
self.clean_edges(cssdict, style, psize)
@@ -290,6 +300,7 @@ class CSSFlattener(object):
lineh = self.lineh / psize
cssdict['line-height'] = "%0.5fem" % lineh
+
if (self.context.remove_paragraph_spacing or
self.context.insert_blank_line) and tag in ('p', 'div'):
if item_id != 'calibre_jacket' or self.context.output_profile.name == 'Kindle':
diff --git a/src/calibre/ebooks/txt/input.py b/src/calibre/ebooks/txt/input.py
index b444bf1cf4..44b98304ea 100644
--- a/src/calibre/ebooks/txt/input.py
+++ b/src/calibre/ebooks/txt/input.py
@@ -77,10 +77,14 @@ class TXTInput(InputFormatPlugin):
base = os.getcwdu()
if hasattr(stream, 'name'):
base = os.path.dirname(stream.name)
- htmlfile = open(os.path.join(base, 'temp_calibre_txt_input_to_html.html'),
- 'wb')
- htmlfile.write(html.encode('utf-8'))
- htmlfile.close()
+ fname = os.path.join(base, 'index.html')
+ c = 0
+ while os.path.exists(fname):
+ c += 1
+ fname = 'index%d.html'%c
+ htmlfile = open(fname, 'wb')
+ with htmlfile:
+ htmlfile.write(html.encode('utf-8'))
cwd = os.getcwdu()
odi = options.debug_pipeline
options.debug_pipeline = None
diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py
index 57ca2a1880..f96c64080d 100644
--- a/src/calibre/gui2/__init__.py
+++ b/src/calibre/gui2/__init__.py
@@ -9,7 +9,7 @@ from PyQt4.Qt import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, \
QByteArray, QTranslator, QCoreApplication, QThread, \
QEvent, QTimer, pyqtSignal, QDate, QDesktopServices, \
QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \
- QIcon, QApplication, QDialog, QPushButton, QUrl
+ QIcon, QApplication, QDialog, QPushButton, QUrl, QFont
ORG_NAME = 'KovidsBrain'
APP_UID = 'libprs500'
@@ -52,6 +52,7 @@ gprefs.defaults['show_splash_screen'] = True
gprefs.defaults['toolbar_icon_size'] = 'medium'
gprefs.defaults['toolbar_text'] = 'auto'
gprefs.defaults['show_child_bar'] = False
+gprefs.defaults['font'] = None
# }}}
@@ -613,6 +614,10 @@ class Application(QApplication):
qt_app = self
self._file_open_paths = []
self._file_open_lock = RLock()
+ self.original_font = QFont(QApplication.font())
+ fi = gprefs['font']
+ if fi is not None:
+ QApplication.setFont(QFont(*fi))
def _send_file_open_events(self):
with self._file_open_lock:
diff --git a/src/calibre/gui2/actions/edit_metadata.py b/src/calibre/gui2/actions/edit_metadata.py
index 60a943ccb9..11949632e9 100644
--- a/src/calibre/gui2/actions/edit_metadata.py
+++ b/src/calibre/gui2/actions/edit_metadata.py
@@ -154,15 +154,17 @@ class EditMetadataAction(InterfaceAction):
d.view_format.connect(lambda
fmt:self.gui.iactions['View'].view_format(row_list[current_row],
fmt))
- if d.exec_() != d.Accepted:
- d.view_format.disconnect()
+ ret = d.exec_()
+ d.break_cycles()
+ if ret != d.Accepted:
break
- d.view_format.disconnect()
+
changed.add(d.id)
if d.row_delta == 0:
break
current_row += d.row_delta
+
if changed:
self.gui.library_view.model().refresh_ids(list(changed))
current = self.gui.library_view.currentIndex()
diff --git a/src/calibre/gui2/convert/__init__.py b/src/calibre/gui2/convert/__init__.py
index 0c3a8b5a4e..c1efe5b9af 100644
--- a/src/calibre/gui2/convert/__init__.py
+++ b/src/calibre/gui2/convert/__init__.py
@@ -10,7 +10,7 @@ import textwrap
from functools import partial
from PyQt4.Qt import QWidget, QSpinBox, QDoubleSpinBox, QLineEdit, QTextEdit, \
- QCheckBox, QComboBox, Qt, QIcon, pyqtSignal
+ QCheckBox, QComboBox, Qt, QIcon, pyqtSignal, QLabel
from calibre.customize.conversion import OptionRecommendation
from calibre.ebooks.conversion.config import load_defaults, \
@@ -81,6 +81,21 @@ class Widget(QWidget):
self.apply_recommendations(defaults)
self.setup_help(get_help)
+ def process_child(child):
+ for g in child.children():
+ if isinstance(g, QLabel):
+ buddy = g.buddy()
+ if buddy is not None and hasattr(buddy, '_help'):
+ g._help = buddy._help
+ htext = unicode(buddy.toolTip()).strip()
+ g.setToolTip(htext)
+ g.setWhatsThis(htext)
+ g.__class__.enterEvent = lambda obj, event: self.set_help(getattr(obj, '_help', obj.toolTip()))
+ else:
+ process_child(g)
+ process_child(self)
+
+
def restore_defaults(self, get_option):
defaults = GuiRecommendations()
defaults.merge_recommendations(get_option, OptionRecommendation.LOW,
diff --git a/src/calibre/gui2/convert/look_and_feel.py b/src/calibre/gui2/convert/look_and_feel.py
index ec3f0b944d..98b9cb8155 100644
--- a/src/calibre/gui2/convert/look_and_feel.py
+++ b/src/calibre/gui2/convert/look_and_feel.py
@@ -21,7 +21,7 @@ class LookAndFeelWidget(Widget, Ui_Form):
def __init__(self, parent, get_option, get_help, db=None, book_id=None):
Widget.__init__(self, parent,
['change_justification', 'extra_css', 'base_font_size',
- 'font_size_mapping', 'line_height',
+ 'font_size_mapping', 'line_height', 'minimum_line_height',
'linearize_tables', 'smarten_punctuation',
'disable_font_rescaling', 'insert_blank_line',
'remove_paragraph_spacing', 'remove_paragraph_spacing_indent_size','input_encoding',
diff --git a/src/calibre/gui2/convert/look_and_feel.ui b/src/calibre/gui2/convert/look_and_feel.ui
index c683300854..367233e2c0 100644
--- a/src/calibre/gui2/convert/look_and_feel.ui
+++ b/src/calibre/gui2/convert/look_and_feel.ui
@@ -97,7 +97,7 @@
- -
+
-
Line &height:
@@ -107,7 +107,7 @@
- -
+
-
pt
@@ -117,7 +117,7 @@
- -
+
-
Input character &encoding:
@@ -127,17 +127,17 @@
- -
+
-
- -
+
-
Remove &spacing between paragraphs
- -
+
-
-
@@ -164,21 +164,21 @@
- -
+
-
Text justification:
- -
+
-
&Linearize tables
- -
+
-
Extra &CSS
@@ -190,37 +190,60 @@
- -
+
-
- -
+
-
&Transliterate unicode characters to ASCII
- -
+
-
Insert &blank line
- -
+
-
Keep &ligatures
- -
+
-
Smarten &punctuation
+ -
+
+
+ Minimum &line height:
+
+
+ opt_minimum_line_height
+
+
+
+ -
+
+
+ %
+
+
+ 1
+
+
+ 900.000000000000000
+
+
+
diff --git a/src/calibre/gui2/convert/mobi_output.py b/src/calibre/gui2/convert/mobi_output.py
index 23c0b30253..14aca24db5 100644
--- a/src/calibre/gui2/convert/mobi_output.py
+++ b/src/calibre/gui2/convert/mobi_output.py
@@ -25,6 +25,7 @@ class PluginWidget(Widget, Ui_Form):
def __init__(self, parent, get_option, get_help, db=None, book_id=None):
Widget.__init__(self, parent,
['prefer_author_sort', 'rescale_images', 'toc_title',
+ 'mobi_ignore_margins',
'dont_compress', 'no_inline_toc', 'masthead_font','personal_doc']
)
self.db, self.book_id = db, book_id
diff --git a/src/calibre/gui2/convert/mobi_output.ui b/src/calibre/gui2/convert/mobi_output.ui
index 176ce681c0..e9eab45e1a 100644
--- a/src/calibre/gui2/convert/mobi_output.ui
+++ b/src/calibre/gui2/convert/mobi_output.ui
@@ -55,7 +55,7 @@
- -
+
-
Kindle options
@@ -101,7 +101,7 @@
- -
+
-
Qt::Vertical
@@ -114,6 +114,13 @@
+ -
+
+
+ Ignore &margins
+
+
+
diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py
index 008649f534..07bfeccc4f 100644
--- a/src/calibre/gui2/device.py
+++ b/src/calibre/gui2/device.py
@@ -12,7 +12,7 @@ from PyQt4.Qt import QMenu, QAction, QActionGroup, QIcon, SIGNAL, \
from calibre.customize.ui import available_input_formats, available_output_formats, \
device_plugins
from calibre.devices.interface import DevicePlugin
-from calibre.devices.errors import UserFeedback
+from calibre.devices.errors import UserFeedback, OpenFeedback
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
from calibre.utils.ipc.job import BaseJob
from calibre.devices.scanner import DeviceScanner
@@ -122,7 +122,8 @@ def device_name_for_plugboards(device_class):
class DeviceManager(Thread): # {{{
- def __init__(self, connected_slot, job_manager, open_feedback_slot, sleep_time=2):
+ def __init__(self, connected_slot, job_manager, open_feedback_slot,
+ open_feedback_msg, sleep_time=2):
'''
:sleep_time: Time to sleep between device probes in secs
'''
@@ -143,6 +144,7 @@ class DeviceManager(Thread): # {{{
self.ejected_devices = set([])
self.mount_connection_requests = Queue.Queue(0)
self.open_feedback_slot = open_feedback_slot
+ self.open_feedback_msg = open_feedback_msg
def report_progress(self, *args):
pass
@@ -163,6 +165,9 @@ class DeviceManager(Thread): # {{{
dev.reset(detected_device=detected_device,
report_progress=self.report_progress)
dev.open()
+ except OpenFeedback, e:
+ self.open_feedback_msg(dev.get_gui_name(), e.feedback_msg)
+ continue
except:
tb = traceback.format_exc()
if DEBUG or tb not in self.reported_errors:
@@ -594,11 +599,16 @@ class DeviceMixin(object): # {{{
_('Error communicating with device'), ' ')
self.device_error_dialog.setModal(Qt.NonModal)
self.device_manager = DeviceManager(Dispatcher(self.device_detected),
- self.job_manager, Dispatcher(self.status_bar.show_message))
+ self.job_manager, Dispatcher(self.status_bar.show_message),
+ Dispatcher(self.show_open_feedback))
self.device_manager.start()
if tweaks['auto_connect_to_folder']:
self.connect_to_folder_named(tweaks['auto_connect_to_folder'])
+ def show_open_feedback(self, devname, msg):
+ self.__of_dev_mem__ = d = info_dialog(self, devname, msg)
+ d.show()
+
def auto_convert_question(self, msg, autos):
autos = u'\n'.join(map(unicode, map(force_unicode, autos)))
return self.ask_a_yes_no_question(
diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py
index a640c50fb8..e0f1f83c73 100644
--- a/src/calibre/gui2/dialogs/metadata_bulk.py
+++ b/src/calibre/gui2/dialogs/metadata_bulk.py
@@ -102,7 +102,7 @@ class MyBlockingBusy(QDialog):
remove_all, remove, add, au, aus, do_aus, rating, pub, do_series, \
do_autonumber, do_remove_format, remove_format, do_swap_ta, \
do_remove_conv, do_auto_author, series, do_series_restart, \
- series_start_value, do_title_case, clear_series = self.args
+ series_start_value, do_title_case, cover_action, clear_series = self.args
# first loop: do author and title. These will commit at the end of each
@@ -129,6 +129,23 @@ class MyBlockingBusy(QDialog):
self.db.set_title(id, titlecase(title), notify=False)
if au:
self.db.set_authors(id, string_to_authors(au), notify=False)
+ if cover_action == 'remove':
+ self.db.remove_cover(id)
+ elif cover_action == 'generate':
+ from calibre.ebooks import calibre_cover
+ from calibre.ebooks.metadata import fmt_sidx
+ from calibre.gui2 import config
+ mi = self.db.get_metadata(id, index_is_id=True)
+ series_string = None
+ if mi.series:
+ series_string = _('Book %s of %s')%(
+ fmt_sidx(mi.series_index,
+ use_roman=config['use_roman_numerals_for_series_number']),
+ mi.series)
+
+ cdata = calibre_cover(mi.title, mi.format_field('authors')[-1],
+ series_string=series_string)
+ self.db.set_cover(id, cdata)
elif self.current_phase == 2:
# All of these just affect the DB, so we can tolerate a total rollback
if do_auto_author:
@@ -678,11 +695,16 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
do_remove_conv = self.remove_conversion_settings.isChecked()
do_auto_author = self.auto_author_sort.isChecked()
do_title_case = self.change_title_to_title_case.isChecked()
+ cover_action = None
+ if self.cover_remove.isChecked():
+ cover_action = 'remove'
+ elif self.cover_generate.isChecked():
+ cover_action = 'generate'
args = (remove_all, remove, add, au, aus, do_aus, rating, pub, do_series,
do_autonumber, do_remove_format, remove_format, do_swap_ta,
do_remove_conv, do_auto_author, series, do_series_restart,
- series_start_value, do_title_case, clear_series)
+ series_start_value, do_title_case, cover_action, clear_series)
bb = MyBlockingBusy(_('Applying changes to %d books.\nPhase {0} {1}%%.')
%len(self.ids), args, self.db, self.ids,
diff --git a/src/calibre/gui2/dialogs/metadata_bulk.ui b/src/calibre/gui2/dialogs/metadata_bulk.ui
index 344bde0fa0..cd644f88ba 100644
--- a/src/calibre/gui2/dialogs/metadata_bulk.ui
+++ b/src/calibre/gui2/dialogs/metadata_bulk.ui
@@ -381,7 +381,7 @@ Future conversion of these books will use the default settings.
- -
+
-
Qt::Vertical
@@ -394,6 +394,39 @@ Future conversion of these books will use the default settings.
+ -
+
+
+ Change &cover
+
+
+
-
+
+
+ &No change
+
+
+ true
+
+
+
+ -
+
+
+ &Remove cover
+
+
+
+ -
+
+
+ &Generate default cover
+
+
+
+
+
+
diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py
index 3205b1d23c..4a9bb784c8 100644
--- a/src/calibre/gui2/dialogs/metadata_single.py
+++ b/src/calibre/gui2/dialogs/metadata_single.py
@@ -240,37 +240,39 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.cover_fetcher = CoverFetcher(None, None, isbn,
self.timeout, title, author)
self.cover_fetcher.start()
- self._hangcheck = QTimer(self)
- self._hangcheck.timeout.connect(self.hangcheck,
- type=Qt.QueuedConnection)
self.cf_start_time = time.time()
self.pi.start(_('Downloading cover...'))
- self._hangcheck.start(100)
+ QTimer.singleShot(100, self.hangcheck)
def hangcheck(self):
- if self.cover_fetcher.is_alive() and \
- time.time()-self.cf_start_time < self.COVER_FETCH_TIMEOUT:
+ cf = self.cover_fetcher
+ if cf is None:
+ # Called after dialog closed
+ return
+
+ if cf.is_alive() and \
+ time.time()-self.cf_start_time < self.COVER_FETCH_TIMEOUT:
+ QTimer.singleShot(100, self.hangcheck)
return
- self._hangcheck.stop()
try:
- if self.cover_fetcher.is_alive():
+ if cf.is_alive():
error_dialog(self, _('Cannot fetch cover'),
_('Could not fetch cover.
')+
_('The download timed out.')).exec_()
return
- if self.cover_fetcher.needs_isbn:
+ if cf.needs_isbn:
error_dialog(self, _('Cannot fetch cover'),
_('Could not find cover for this book. Try '
'specifying the ISBN first.')).exec_()
return
- if self.cover_fetcher.exception is not None:
- err = self.cover_fetcher.exception
+ if cf.exception is not None:
+ err = cf.exception
error_dialog(self, _('Cannot fetch cover'),
_('Could not fetch cover.
')+unicode(err)).exec_()
return
- if self.cover_fetcher.errors and self.cover_fetcher.cover_data is None:
- details = u'\n\n'.join([e[-1] + ': ' + e[1] for e in self.cover_fetcher.errors])
+ if cf.errors and cf.cover_data is None:
+ details = u'\n\n'.join([e[-1] + ': ' + e[1] for e in cf.errors])
error_dialog(self, _('Cannot fetch cover'),
_('Could not fetch cover.
') +
_('For the error message from each cover source, '
@@ -278,7 +280,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
return
pix = QPixmap()
- pix.loadFromData(self.cover_fetcher.cover_data)
+ pix.loadFromData(cf.cover_data)
if pix.isNull():
error_dialog(self, _('Bad cover'),
_('The cover is not a valid picture')).exec_()
@@ -287,11 +289,12 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.update_cover_tooltip()
self.cover_changed = True
self.cpixmap = pix
- self.cover_data = self.cover_fetcher.cover_data
+ self.cover_data = cf.cover_data
finally:
self.fetch_cover_button.setEnabled(True)
self.unsetCursor()
- self.pi.stop()
+ if self.pi is not None:
+ self.pi.stop()
# }}}
@@ -438,8 +441,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
def __init__(self, window, row, db, prev=None,
next_=None):
ResizableDialog.__init__(self, window)
+ self.cover_fetcher = None
self.bc_box.layout().setAlignment(self.cover, Qt.AlignCenter|Qt.AlignHCenter)
- self.cancel_all = False
base = unicode(self.author_sort.toolTip())
self.ok_aus_tooltip = '' + textwrap.fill(base+'
'+
_(' The green color indicates that the current '
@@ -570,7 +573,6 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
QObject.connect(self.series, SIGNAL('editTextChanged(QString)'), self.enable_series_index)
self.series.lineEdit().editingFinished.connect(self.increment_series_index)
- self.show()
pm = QPixmap()
if cover:
pm.loadFromData(cover)
@@ -590,6 +592,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.original_author = unicode(self.authors.text()).strip()
self.original_title = unicode(self.title.text()).strip()
+ self.show()
+
def create_custom_column_editors(self):
w = self.central_widget.widget(1)
layout = w.layout()
@@ -828,10 +832,6 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.accept()
def accept(self):
- cf = getattr(self, 'cover_fetcher', None)
- if cf is not None and hasattr(cf, 'terminate'):
- cf.terminate()
- cf.wait()
try:
if self.formats_changed:
self.sync_formats()
@@ -888,14 +888,12 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
show=True)
raise
self.save_state()
+ self.cover_fetcher = None
QDialog.accept(self)
def reject(self, *args):
- cf = getattr(self, 'cover_fetcher', None)
- if cf is not None and hasattr(cf, 'terminate'):
- cf.terminate()
- cf.wait()
self.save_state()
+ self.cover_fetcher = None
QDialog.reject(self, *args)
def read_state(self):
@@ -910,3 +908,48 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
dynamic.set('metasingle_window_geometry', bytes(self.saveGeometry()))
dynamic.set('metasingle_splitter_state',
bytes(self.splitter.saveState()))
+
+ def break_cycles(self):
+ # Break any reference cycles that could prevent python
+ # from garbage collecting this dialog
+ def disconnect(signal):
+ try:
+ signal.disconnect()
+ except:
+ pass # Fails if view format was never connected
+ disconnect(self.view_format)
+ for b in ('next_button', 'prev_button'):
+ x = getattr(self, b, None)
+ if x is not None:
+ disconnect(x.clicked)
+
+if __name__ == '__main__':
+ from calibre.library import db
+ from PyQt4.Qt import QApplication
+ from calibre.utils.mem import memory
+ import gc
+
+
+ app = QApplication([])
+ db = db()
+
+ # Initialize all Qt Objects once
+ d = MetadataSingleDialog(None, 4, db)
+ d.break_cycles()
+ d.reject()
+ del d
+
+ for i in range(5):
+ gc.collect()
+ before = memory()
+
+ d = MetadataSingleDialog(None, 4, db)
+ d.reject()
+ d.break_cycles()
+ del d
+
+ for i in range(5):
+ gc.collect()
+ print 'Used memory:', memory(before)/1024.**2, 'MB'
+
+
diff --git a/src/calibre/gui2/dialogs/tag_categories.py b/src/calibre/gui2/dialogs/tag_categories.py
index 60092e4bd2..7573f04012 100644
--- a/src/calibre/gui2/dialogs/tag_categories.py
+++ b/src/calibre/gui2/dialogs/tag_categories.py
@@ -145,7 +145,7 @@ class TagCategories(QDialog, Ui_TagCategories):
index = self.all_items[node.data(Qt.UserRole).toPyObject()].index
if index not in self.applied_items:
self.applied_items.append(index)
- self.applied_items.sort(key=lambda x:sort_key(self.all_items[x]))
+ self.applied_items.sort(key=lambda x:sort_key(self.all_items[x].name))
self.display_filtered_categories(None)
def unapply_tags(self, node=None):
diff --git a/src/calibre/gui2/dialogs/tag_list_editor.py b/src/calibre/gui2/dialogs/tag_list_editor.py
index a7d6fe03e7..ced0e9a505 100644
--- a/src/calibre/gui2/dialogs/tag_list_editor.py
+++ b/src/calibre/gui2/dialogs/tag_list_editor.py
@@ -105,9 +105,13 @@ class TagListEditor(QDialog, Ui_TagListEditor):
if not question_dialog(self, _('Are your sure?'),
'
'+_('Are you certain you want to delete the following items?')+'
'+ct):
return
-
+ row = self.available_tags.row(deletes[0])
for item in deletes:
(id,ign) = item.data(Qt.UserRole).toInt()
self.to_delete.append(id)
self.available_tags.takeItem(self.available_tags.row(item))
+ if row >= self.available_tags.count():
+ row = self.available_tags.count() - 1
+ if row >= 0:
+ self.available_tags.scrollToItem(self.available_tags.item(row))
diff --git a/src/calibre/gui2/init.py b/src/calibre/gui2/init.py
index 27a6a2352a..fc70f0579d 100644
--- a/src/calibre/gui2/init.py
+++ b/src/calibre/gui2/init.py
@@ -123,6 +123,8 @@ class Stack(QStackedWidget): # {{{
_('Tag Browser'), I('tags.png'),
parent=parent, side_index=0, initial_side_size=200,
shortcut=_('Shift+Alt+T'))
+ parent.tb_splitter.state_changed.connect(
+ self.tb_widget.set_pane_is_visible, Qt.QueuedConnection)
parent.tb_splitter.addWidget(self.tb_widget)
parent.tb_splitter.addWidget(parent.cb_splitter)
parent.tb_splitter.setCollapsible(parent.tb_splitter.other_index, False)
diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py
index 10c2fcfe95..b2ba87d1e0 100644
--- a/src/calibre/gui2/preferences/look_feel.py
+++ b/src/calibre/gui2/preferences/look_feel.py
@@ -5,10 +5,11 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal '
__docformat__ = 'restructuredtext en'
+from PyQt4.Qt import QApplication, QFont, QFontInfo, QFontDialog
from calibre.gui2.preferences import ConfigWidgetBase, test_widget
from calibre.gui2.preferences.look_feel_ui import Ui_Form
-from calibre.gui2 import config, gprefs
+from calibre.gui2 import config, gprefs, qt_app
from calibre.utils.localization import available_translations, \
get_language, get_lang
from calibre.utils.config import prefs
@@ -56,12 +57,64 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
(_('Never'), 'never')]
r('toolbar_text', gprefs, choices=choices)
+ self.current_font = None
+ self.change_font_button.clicked.connect(self.change_font)
+
+
+ def initialize(self):
+ ConfigWidgetBase.initialize(self)
+ self.current_font = gprefs['font']
+ self.update_font_display()
+
+ def restore_defaults(self):
+ ConfigWidgetBase.restore_defaults(self)
+ ofont = self.current_font
+ self.current_font = None
+ if ofont is not None:
+ self.changed_signal.emit()
+ self.update_font_display()
+
+ def build_font_obj(self):
+ font_info = self.current_font
+ if font_info is not None:
+ font = QFont(*font_info)
+ else:
+ font = qt_app.original_font
+ return font
+
+ def update_font_display(self):
+ font = self.build_font_obj()
+ fi = QFontInfo(font)
+ name = unicode(fi.family())
+
+ self.font_display.setFont(font)
+ self.font_display.setText(_('Current font:') + ' ' + name +
+ ' [%dpt]'%fi.pointSize())
+
+ def change_font(self, *args):
+ fd = QFontDialog(self.build_font_obj(), self)
+ if fd.exec_() == fd.Accepted:
+ font = fd.selectedFont()
+ fi = QFontInfo(font)
+ self.current_font = (unicode(fi.family()), fi.pointSize(),
+ fi.weight(), fi.italic())
+ self.update_font_display()
+ self.changed_signal.emit()
+
+ def commit(self, *args):
+ rr = ConfigWidgetBase.commit(self, *args)
+ if self.current_font != gprefs['font']:
+ gprefs['font'] = self.current_font
+ QApplication.setFont(self.font_display.font())
+ rr = True
+ return rr
+
+
def refresh_gui(self, gui):
gui.search.search_as_you_type(config['search_as_you_type'])
-
+ self.update_font_display()
if __name__ == '__main__':
- from PyQt4.Qt import QApplication
app = QApplication([])
test_widget('Interface', 'Look & Feel')
diff --git a/src/calibre/gui2/preferences/look_feel.ui b/src/calibre/gui2/preferences/look_feel.ui
index 1de55d51ef..91f45a155f 100644
--- a/src/calibre/gui2/preferences/look_feel.ui
+++ b/src/calibre/gui2/preferences/look_feel.ui
@@ -183,7 +183,7 @@
- -
+
-
Qt::Vertical
@@ -196,6 +196,20 @@
+ -
+
+
+ true
+
+
+
+ -
+
+
+ Change &font (needs restart)
+
+
+
diff --git a/src/calibre/gui2/preferences/main.py b/src/calibre/gui2/preferences/main.py
index fc01a33cf6..f7d49427c8 100644
--- a/src/calibre/gui2/preferences/main.py
+++ b/src/calibre/gui2/preferences/main.py
@@ -251,10 +251,28 @@ class Preferences(QMainWindow):
self.close()
self.run_wizard_requested.emit()
+ def set_tooltips_for_labels(self):
+
+ def process_child(child):
+ for g in child.children():
+ if isinstance(g, QLabel):
+ buddy = g.buddy()
+ if buddy is not None and hasattr(buddy, 'toolTip'):
+ htext = unicode(buddy.toolTip()).strip()
+ etext = unicode(g.toolTip()).strip()
+ if htext and not etext:
+ g.setToolTip(htext)
+ g.setWhatsThis(htext)
+ else:
+ process_child(g)
+
+ process_child(self.showing_widget)
+
def show_plugin(self, plugin):
self.showing_widget = plugin.create_widget(self.scroll_area)
self.showing_widget.genesis(self.gui)
self.showing_widget.initialize()
+ self.set_tooltips_for_labels()
self.scroll_area.setWidget(self.showing_widget)
self.stack.setCurrentIndex(1)
self.showing_widget.show()
diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py
index 2ede698c85..f75061da12 100644
--- a/src/calibre/gui2/tag_view.py
+++ b/src/calibre/gui2/tag_view.py
@@ -87,6 +87,13 @@ class TagsView(QTreeView): # {{{
self.setDragDropMode(self.DropOnly)
self.setDropIndicatorShown(True)
self.setAutoExpandDelay(500)
+ self.pane_is_visible = False
+
+ def set_pane_is_visible(self, to_what):
+ pv = self.pane_is_visible
+ self.pane_is_visible = to_what
+ if to_what and not pv:
+ self.recount()
def set_database(self, db, tag_match, sort_by):
self.hidden_categories = config['tag_browser_hidden_categories']
@@ -94,6 +101,7 @@ class TagsView(QTreeView): # {{{
hidden_categories=self.hidden_categories,
search_restriction=None,
drag_drop_finished=self.drag_drop_finished)
+ self.pane_is_visible = True # because TagsModel.init did a recount
self.sort_by = sort_by
self.tag_match = tag_match
self.db = db
@@ -300,7 +308,7 @@ class TagsView(QTreeView): # {{{
return self.isExpanded(idx)
def recount(self, *args):
- if self.disable_recounting:
+ if self.disable_recounting or not self.pane_is_visible:
return
self.refresh_signal_processed = True
ci = self.currentIndex()
@@ -969,6 +977,7 @@ class TagBrowserWidget(QWidget): # {{{
self._layout.setContentsMargins(0,0,0,0)
parent.tags_view = TagsView(parent)
+ self.tags_view = parent.tags_view
self._layout.addWidget(parent.tags_view)
parent.sort_by = QComboBox(parent)
@@ -998,6 +1007,9 @@ class TagBrowserWidget(QWidget): # {{{
_('Add your own categories to the Tag Browser'))
parent.edit_categories.setStatusTip(parent.edit_categories.toolTip())
+ def set_pane_is_visible(self, to_what):
+ self.tags_view.set_pane_is_visible(to_what)
+
# }}}
diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py
index cb25f75d4a..7279b7f8df 100644
--- a/src/calibre/gui2/ui.py
+++ b/src/calibre/gui2/ui.py
@@ -234,7 +234,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
######################### Search Restriction ##########################
SearchRestrictionMixin.__init__(self)
- self.apply_named_search_restriction(db.prefs['gui_restriction'])
+ if db.prefs['gui_restriction']:
+ self.apply_named_search_restriction(db.prefs['gui_restriction'])
########################### Cover Flow ################################
diff --git a/src/calibre/library/__init__.py b/src/calibre/library/__init__.py
index 8ff23c0a0a..177c5063ac 100644
--- a/src/calibre/library/__init__.py
+++ b/src/calibre/library/__init__.py
@@ -19,12 +19,15 @@ def generate_test_db(library_path,
max_tags=10
):
import random, string, os, sys, time
+ from calibre.constants import preferred_encoding
if not os.path.exists(library_path):
os.makedirs(library_path)
+ letters = string.letters.decode(preferred_encoding)
+
def randstr(length):
- return ''.join(random.choice(string.letters) for i in
+ return ''.join(random.choice(letters) for i in
xrange(length))
all_tags = [randstr(tag_length) for j in xrange(num_of_tags)]
diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py
index 1229b60577..33e4295f05 100644
--- a/src/calibre/library/database2.py
+++ b/src/calibre/library/database2.py
@@ -10,11 +10,10 @@ import os, sys, shutil, cStringIO, glob, time, functools, traceback, re
from itertools import repeat
from math import floor
from Queue import Queue
-from operator import itemgetter
from PyQt4.QtGui import QImage
-
+from calibre import prints
from calibre.ebooks.metadata import title_sort, author_to_author_sort
from calibre.ebooks.metadata.opf2 import metadata_to_opf
from calibre.library.database import LibraryDatabase
@@ -1039,43 +1038,175 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
tn=field['table'], col=field['link_column']), (id_,))
return set(x[0] for x in ans)
+########## data structures for get_categories
+
CATEGORY_SORTS = ('name', 'popularity', 'rating')
- def get_categories(self, sort='name', ids=None, icon_map=None):
- self.books_list_filter.change([] if not ids else ids)
+ class TCat_Tag(object):
- categories = {}
+ def __init__(self, name, sort):
+ self.n = name
+ self.s = sort
+ self.c = 0
+ self.rt = 0
+ self.rc = 0
+ self.id = None
+
+ def set_all(self, c, rt, rc, id):
+ self.c = c
+ self.rt = rt
+ self.rc = rc
+ self.id = id
+
+ def __str__(self):
+ return unicode(self)
+
+ def __unicode__(self):
+ return 'n=%s s=%s c=%d rt=%d rc=%d id=%s'%\
+ (self.n, self.s, self.c, self.rt, self.rc, self.id)
+
+
+ def get_categories(self, sort='name', ids=None, icon_map=None):
+ #start = last = time.clock()
if icon_map is not None and type(icon_map) != TagsIcons:
raise TypeError('icon_map passed to get_categories must be of type TagIcons')
+ if sort not in self.CATEGORY_SORTS:
+ raise ValueError('sort ' + sort + ' not a valid value')
+
+ self.books_list_filter.change([] if not ids else ids)
+ id_filter = None if not ids else frozenset(ids)
tb_cats = self.field_metadata
- #### First, build the standard and custom-column categories ####
+ tcategories = {}
+ tids = {}
+ md = []
+
+ # First, build the maps. We need a category->items map and an
+ # item -> (item_id, sort_val) map to use in the books loop
for category in tb_cats.keys():
cat = tb_cats[category]
- if not cat['is_category'] or cat['kind'] in ['user', 'search']:
+ if not cat['is_category'] or cat['kind'] in ['user', 'search'] \
+ or category in ['news', 'formats']:
continue
- tn = cat['table']
- categories[category] = [] #reserve the position in the ordered list
- if tn is None: # Nothing to do for the moment
+ # Get the ids for the item values
+ if not cat['is_custom']:
+ funcs = {
+ 'authors' : self.get_authors_with_ids,
+ 'series' : self.get_series_with_ids,
+ 'publisher': self.get_publishers_with_ids,
+ 'tags' : self.get_tags_with_ids,
+ 'rating' : self.get_ratings_with_ids,
+ }
+ func = funcs.get(category, None)
+ if func:
+ list = func()
+ else:
+ raise ValueError(category + ' has no get with ids function')
+ else:
+ list = self.get_custom_items_with_ids(label=cat['label'])
+ tids[category] = {}
+ if category == 'authors':
+ for l in list:
+ (id, val, sort_val) = (l[0], l[1], l[2])
+ tids[category][val] = (id, sort_val)
+ else:
+ for l in list:
+ (id, val) = (l[0], l[1])
+ tids[category][val] = (id, val)
+ # add an empty category to the category map
+ tcategories[category] = {}
+ # create a list of category/field_index for the books scan to use.
+ # This saves iterating through field_metadata for each book
+ md.append((category, cat['rec_index'], cat['is_multiple']))
+
+ #print 'end phase "collection":', time.clock() - last, 'seconds'
+ #last = time.clock()
+
+ # Now scan every book looking for category items.
+ # Code below is duplicated because it shaves off 10% of the loop time
+ id_dex = self.FIELD_MAP['id']
+ rating_dex = self.FIELD_MAP['rating']
+ tag_class = LibraryDatabase2.TCat_Tag
+ for book in self.data.iterall():
+ if id_filter and book[id_dex] not in id_filter:
continue
- cn = cat['column']
- if ids is None:
- query = '''SELECT id, {0}, count, avg_rating, sort
- FROM tag_browser_{1}'''.format(cn, tn)
- else:
- query = '''SELECT id, {0}, count, avg_rating, sort
- FROM tag_browser_filtered_{1}'''.format(cn, tn)
- if sort == 'popularity':
- query += ' ORDER BY count DESC, sort ASC'
- elif sort == 'name':
- query += ' ORDER BY sort COLLATE icucollate'
- else:
- query += ' ORDER BY avg_rating DESC, sort ASC'
- data = self.conn.get(query)
+ rating = book[rating_dex]
+ # We kept track of all possible category field_map positions above
+ for (cat, dex, mult) in md:
+ if book[dex] is None:
+ continue
+ if not mult:
+ val = book[dex]
+ try:
+ (item_id, sort_val) = tids[cat][val] # let exceptions fly
+ item = tcategories[cat].get(val, None)
+ if not item:
+ item = tag_class(val, sort_val)
+ tcategories[cat][val] = item
+ item.c += 1
+ item.id = item_id
+ if rating > 0:
+ item.rt += rating
+ item.rc += 1
+ except:
+ prints('get_categories: item', val, 'is not in', cat, 'list!')
+ else:
+ vals = book[dex].split(mult)
+ for val in vals:
+ try:
+ (item_id, sort_val) = tids[cat][val] # let exceptions fly
+ item = tcategories[cat].get(val, None)
+ if not item:
+ item = tag_class(val, sort_val)
+ tcategories[cat][val] = item
+ item.c += 1
+ item.id = item_id
+ if rating > 0:
+ item.rt += rating
+ item.rc += 1
+ except:
+ prints('get_categories: item', val, 'is not in', cat, 'list!')
+
+ #print 'end phase "books":', time.clock() - last, 'seconds'
+ #last = time.clock()
+
+ # Now do news
+ tcategories['news'] = {}
+ cat = tb_cats['news']
+ tn = cat['table']
+ cn = cat['column']
+ if ids is None:
+ query = '''SELECT id, {0}, count, avg_rating, sort
+ FROM tag_browser_{1}'''.format(cn, tn)
+ else:
+ query = '''SELECT id, {0}, count, avg_rating, sort
+ FROM tag_browser_filtered_{1}'''.format(cn, tn)
+ # results will be sorted later
+ data = self.conn.get(query)
+ for r in data:
+ item = LibraryDatabase2.TCat_Tag(r[1], r[1])
+ item.set_all(c=r[2], rt=r[2]*r[3], rc=r[2], id=r[0])
+ tcategories['news'][r[1]] = item
+
+ #print 'end phase "news":', time.clock() - last, 'seconds'
+ #last = time.clock()
+
+ # Build the real category list by iterating over the temporary copy
+ # and building the Tag instances.
+ categories = {}
+ tag_class = Tag
+ for category in tb_cats.keys():
+ if category not in tcategories:
+ continue
+ cat = tb_cats[category]
+
+ # prepare the place where we will put the array of Tags
+ categories[category] = []
# icon_map is not None if get_categories is to store an icon and
# possibly a tooltip in the tag structure.
- icon, tooltip = None, ''
+ icon = None
+ tooltip = ''
label = tb_cats.key_to_label(category)
if icon_map:
if not tb_cats.is_custom_field(category):
@@ -1087,23 +1218,46 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
tooltip = self.custom_column_label_map[label]['name']
datatype = cat['datatype']
- avgr = itemgetter(3)
- item_not_zero_func = lambda x: x[2] > 0
+ avgr = lambda x: 0.0 if x.rc == 0 else x.rt/x.rc
+ # Duplicate the build of items below to avoid using a lambda func
+ # in the main Tag loop. Saves a few %
if datatype == 'rating':
- # eliminate the zero ratings line as well as count == 0
- item_not_zero_func = (lambda x: x[1] > 0 and x[2] > 0)
formatter = (lambda x:u'\u2605'*int(x/2))
- avgr = itemgetter(1)
+ avgr = lambda x : x.n
+ # eliminate the zero ratings line as well as count == 0
+ items = [v for v in tcategories[category].values() if v.c > 0 and v.n != 0]
elif category == 'authors':
# Clean up the authors strings to human-readable form
formatter = (lambda x: x.replace('|', ','))
+ items = [v for v in tcategories[category].values() if v.c > 0]
else:
formatter = (lambda x:unicode(x))
+ items = [v for v in tcategories[category].values() if v.c > 0]
- categories[category] = [Tag(formatter(r[1]), count=r[2], id=r[0],
- avg=avgr(r), sort=r[4], icon=icon,
+ # sort the list
+ if sort == 'name':
+ def get_sort_key(x):
+ sk = x.s
+ if isinstance(sk, unicode):
+ sk = sort_key(sk)
+ return sk
+ kf = get_sort_key
+ reverse=False
+ elif sort == 'popularity':
+ kf = lambda x: x.c
+ reverse=True
+ else:
+ kf = avgr
+ reverse=True
+ items.sort(key=kf, reverse=reverse)
+
+ categories[category] = [tag_class(formatter(r.n), count=r.c, id=r.id,
+ avg=avgr(r), sort=r.s, icon=icon,
tooltip=tooltip, category=category)
- for r in data if item_not_zero_func(r)]
+ for r in items]
+
+ #print 'end phase "tags list":', time.clock() - last, 'seconds'
+ #last = time.clock()
# Needed for legacy databases that have multiple ratings that
# map to n stars
@@ -1189,8 +1343,13 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
icon_map['search'] = icon_map['search']
categories['search'] = items
+ #print 'last phase ran in:', time.clock() - last, 'seconds'
+ #print 'get_categories ran in:', time.clock() - start, 'seconds'
+
return categories
+ ############# End get_categories
+
def tags_older_than(self, tag, delta):
tag = tag.lower().strip()
now = nowf()
@@ -1486,6 +1645,12 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
# Note: we generally do not need to refresh_ids because library_view will
# refresh everything.
+ def get_ratings_with_ids(self):
+ result = self.conn.get('SELECT id,rating FROM ratings')
+ if not result:
+ return []
+ return result
+
def dirty_books_referencing(self, field, id, commit=True):
# Get the list of books to dirty -- all books that reference the item
table = self.field_metadata[field]['table']
diff --git a/src/calibre/library/save_to_disk.py b/src/calibre/library/save_to_disk.py
index af57d563ac..7090a2afa8 100644
--- a/src/calibre/library/save_to_disk.py
+++ b/src/calibre/library/save_to_disk.py
@@ -119,10 +119,8 @@ class SafeFormat(TemplateFormatter):
try:
b = self.book.get_user_metadata(key, False)
except:
- if DEBUG:
- traceback.print_exc()
+ traceback.print_exc()
b = None
-
if b is not None and b['datatype'] == 'composite':
if key in self.composite_values:
return self.composite_values[key]
@@ -135,8 +133,7 @@ class SafeFormat(TemplateFormatter):
return val.replace('/', '_').replace('\\', '_')
return ''
except:
- if DEBUG:
- traceback.print_exc()
+ traceback.print_exc()
return key
def get_components(template, mi, id, timefmt='%b %Y', length=250,
@@ -155,6 +152,8 @@ def get_components(template, mi, id, timefmt='%b %Y', length=250,
format_args['tags'] = mi.format_tags()
if format_args['tags'].startswith('/'):
format_args['tags'] = format_args['tags'][1:]
+ else:
+ format_args['tags'] = ''
if mi.series:
format_args['series'] = tsfmt(mi.series)
if mi.series_index is not None:
@@ -254,6 +253,7 @@ def do_save_book_to_disk(id_, mi, cover, plugboards,
if not os.path.exists(dirpath):
raise
+ ocover = mi.cover
if opts.save_cover and cover and os.access(cover, os.R_OK):
with open(base_path+'.jpg', 'wb') as f:
with open(cover, 'rb') as s:
@@ -267,6 +267,8 @@ def do_save_book_to_disk(id_, mi, cover, plugboards,
with open(base_path+'.opf', 'wb') as f:
f.write(opf)
+ mi.cover = ocover
+
written = False
for fmt in formats:
global plugboard_save_to_disk_value, plugboard_any_format_value
diff --git a/src/calibre/utils/mem.py b/src/calibre/utils/mem.py
new file mode 100644
index 0000000000..f48aec34c6
--- /dev/null
+++ b/src/calibre/utils/mem.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python
+# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
+
+__license__ = 'GPL v3'
+__copyright__ = '2010, Kovid Goyal '
+__docformat__ = 'restructuredtext en'
+
+## {{{ http://code.activestate.com/recipes/286222/ (r1)
+import os
+
+_proc_status = '/proc/%d/status' % os.getpid()
+
+_scale = {'kB': 1024.0, 'mB': 1024.0*1024.0,
+ 'KB': 1024.0, 'MB': 1024.0*1024.0}
+
+def _VmB(VmKey):
+ '''Private.
+ '''
+ global _proc_status, _scale
+ # get pseudo file /proc//status
+ try:
+ t = open(_proc_status)
+ v = t.read()
+ t.close()
+ except:
+ return 0.0 # non-Linux?
+ # get VmKey line e.g. 'VmRSS: 9999 kB\n ...'
+ i = v.index(VmKey)
+ v = v[i:].split(None, 3) # whitespace
+ if len(v) < 3:
+ return 0.0 # invalid format?
+ # convert Vm value to bytes
+ return float(v[1]) * _scale[v[2]]
+
+
+def memory(since=0.0):
+ '''Return memory usage in bytes.
+ '''
+ return _VmB('VmSize:') - since
+
+
+def resident(since=0.0):
+ '''Return resident memory usage in bytes.
+ '''
+ return _VmB('VmRSS:') - since
+
+
+def stacksize(since=0.0):
+ '''Return stack size in bytes.
+ '''
+ return _VmB('VmStk:') - since
+## end of http://code.activestate.com/recipes/286222/ }}}
+
+
+