merge from trunk

This commit is contained in:
ldolse 2010-12-14 14:48:16 -05:00
commit ec0095c2c5
44 changed files with 797 additions and 105 deletions

View File

@ -0,0 +1,23 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Hiroshi Miura <miurahr@linux.com>'
'''
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')]

View File

@ -0,0 +1,37 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Hiroshi Miura <miurahr@linux.com>'
'''
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

View File

@ -3,15 +3,16 @@ __copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>'
''' '''
http://www.dilbert.com http://www.dilbert.com
''' '''
import re
from calibre.web.feeds.recipes import BasicNewsRecipe from calibre.web.feeds.recipes import BasicNewsRecipe
import re
class DosisDiarias(BasicNewsRecipe): class DilbertBig(BasicNewsRecipe):
title = 'Dilbert' title = 'Dilbert'
__author__ = 'Darko Miletic' __author__ = 'Darko Miletic and Starson17'
description = 'Dilbert' description = 'Dilbert'
oldest_article = 5 reverse_article_order = True
oldest_article = 15
max_articles_per_feed = 100 max_articles_per_feed = 100
no_stylesheets = True no_stylesheets = True
use_embedded_content = True use_embedded_content = True
@ -29,20 +30,23 @@ class DosisDiarias(BasicNewsRecipe):
feeds = [(u'Dilbert', u'http://feeds.dilbert.com/DilbertDailyStrip' )] 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): def get_article_url(self, article):
return article.get('feedburner_origlink', None) 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): def preprocess_html(self, soup):
for tag in soup.findAll(name='a'): for tag in soup.findAll(name='a'):
if tag['href'].find('http://feedads') >= 0: if tag['href'].find('http://feedads') >= 0:
tag.extract() tag.extract()
return soup 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;}
'''

View File

@ -0,0 +1,31 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Hiroshi Miura <miurahr@linux.com>'
'''
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"}]

View File

@ -0,0 +1,38 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Hiroshi Miura <miurahr@linux.com>'
'''
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

View File

@ -0,0 +1,20 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Hiroshi Miura <miurahr@linux.com>'
'''
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)

View File

@ -10,8 +10,8 @@ import mechanize
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
class NikkeiNet_sub_life(BasicNewsRecipe): class NikkeiNet_sub_shakai(BasicNewsRecipe):
title = u'\u65e5\u7d4c\u65b0\u805e\u96fb\u5b50\u7248(\u751f\u6d3b)' title = u'\u65e5\u7d4c\u65b0\u805e\u96fb\u5b50\u7248(Social)'
__author__ = 'Hiroshi Miura' __author__ = 'Hiroshi Miura'
description = 'News and current market affairs from Japan' description = 'News and current market affairs from Japan'
cover_url = 'http://parts.nikkei.com/parts/ds/images/common/logo_r1.svg' cover_url = 'http://parts.nikkei.com/parts/ds/images/common/logo_r1.svg'

View File

@ -0,0 +1,58 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Hiroshi Miura <miurahr@linux.com>'
'''
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

View File

@ -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)

View File

@ -0,0 +1,36 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Hiroshi Miura <miurahr@linux.com>'
'''
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

View File

@ -60,8 +60,8 @@ class ZeitDe(BasicNewsRecipe):
for tag in soup.findAll(name=['ul','li']): for tag in soup.findAll(name=['ul','li']):
tag.name = 'div' tag.name = 'div'
soup.html['xml:lang'] = self.lang soup.html['xml:lang'] = self.language.replace('_', '-')
soup.html['lang'] = self.lang soup.html['lang'] = self.language.replace('_', '-')
mtag = '<meta http-equiv="Content-Type" content="text/html; charset=' + self.encoding + '">' mtag = '<meta http-equiv="Content-Type" content="text/html; charset=' + self.encoding + '">'
soup.head.insert(0,mtag) soup.head.insert(0,mtag)
return soup return soup

View File

@ -36,6 +36,11 @@ class UserFeedback(DeviceError):
self.details = details self.details = details
self.msg = msg self.msg = msg
class OpenFeedback(DeviceError):
def __init__(self, msg):
self.feedback_msg = msg
DeviceError.__init__(self, msg)
class DeviceBusy(ProtocolError): class DeviceBusy(ProtocolError):
""" Raised when device is busy """ """ Raised when device is busy """
def __init__(self, uerr=""): def __init__(self, uerr=""):

View File

@ -216,6 +216,9 @@ class DevicePlugin(Plugin):
an implementation of an implementation of
this function that should serve as a good example for USB Mass storage this function that should serve as a good example for USB Mass storage
devices. devices.
This method can raise an OpenFeedback exception to display a message to
the user.
''' '''
raise NotImplementedError() raise NotImplementedError()

View File

@ -120,7 +120,7 @@ def add_pipeline_options(parser, plumber):
[ [
'base_font_size', 'disable_font_rescaling', 'base_font_size', 'disable_font_rescaling',
'font_size_mapping', 'font_size_mapping',
'line_height', 'line_height', 'minimum_line_height',
'linearize_tables', 'linearize_tables',
'extra_css', 'smarten_punctuation', 'extra_css', 'smarten_punctuation',
'margin_top', 'margin_left', 'margin_right', 'margin_top', 'margin_left', 'margin_right',

View File

@ -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', OptionRecommendation(name='line_height',
recommended_value=0, level=OptionRecommendation.LOW, recommended_value=0, level=OptionRecommendation.LOW,
help=_('The line height in pts. Controls spacing between consecutive ' help=_(
'lines of text. By default no line height manipulation is ' 'The line height in pts. Controls spacing between consecutive '
'performed.' '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', OptionRecommendation(name='linearize_tables',

View File

@ -73,6 +73,10 @@ class FB2MLizer(object):
text = re.sub(r'(?miu)<p>\s*</p>', '', text) text = re.sub(r'(?miu)<p>\s*</p>', '', text)
text = re.sub(r'(?miu)\s+</p>', '</p>', text) text = re.sub(r'(?miu)\s+</p>', '</p>', text)
text = re.sub(r'(?miu)</p><p>', '</p>\n\n<p>', text) text = re.sub(r'(?miu)</p><p>', '</p>\n\n<p>', text)
if self.opts.insert_blank_line:
text = re.sub(r'(?miu)</p>', '</p><empty-line />', text)
return text return text
def fb2_header(self): def fb2_header(self):
@ -293,6 +297,18 @@ class FB2MLizer(object):
s_out, s_tags = self.handle_simple_tag('emphasis', tag_stack+tags) s_out, s_tags = self.handle_simple_tag('emphasis', tag_stack+tags)
fb2_out += s_out fb2_out += s_out
tags += s_tags 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. # Processes style information.
if style['font-style'] == 'italic': if style['font-style'] == 'italic':
@ -303,6 +319,10 @@ class FB2MLizer(object):
s_out, s_tags = self.handle_simple_tag('strong', tag_stack+tags) s_out, s_tags = self.handle_simple_tag('strong', tag_stack+tags)
fb2_out += s_out fb2_out += s_out
tags += s_tags 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. # Process element text.
if hasattr(elem_tree, 'text') and elem_tree.text: if hasattr(elem_tree, 'text') and elem_tree.text:

View File

@ -314,6 +314,8 @@ class HTMLInput(InputFormatPlugin):
rewrite_links, urlnormalize, urldefrag, BINARY_MIME, OEB_STYLES, \ rewrite_links, urlnormalize, urldefrag, BINARY_MIME, OEB_STYLES, \
xpath xpath
from calibre import guess_type from calibre import guess_type
from calibre.ebooks.oeb.transforms.metadata import \
meta_info_to_oeb_metadata
import cssutils import cssutils
self.OEB_STYLES = OEB_STYLES self.OEB_STYLES = OEB_STYLES
oeb = create_oebbook(log, None, opts, self, oeb = create_oebbook(log, None, opts, self,
@ -321,15 +323,7 @@ class HTMLInput(InputFormatPlugin):
self.oeb = oeb self.oeb = oeb
metadata = oeb.metadata metadata = oeb.metadata
if mi.title: meta_info_to_oeb_metadata(mi, metadata, log)
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'})
if not metadata.language: if not metadata.language:
oeb.logger.warn(u'Language not specified') oeb.logger.warn(u'Language not specified')
metadata.add('language', get_lang().replace('_', '-')) metadata.add('language', get_lang().replace('_', '-'))

View File

@ -170,7 +170,27 @@ def get_metadata_(src, encoding=None):
if match: if match:
series = match.group(1) series = match.group(1)
if series: 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) 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
rating = None rating = None

View File

@ -184,7 +184,7 @@ class MobiMLizer(object):
para.attrib['value'] = str(istates[-2].list_num) para.attrib['value'] = str(istates[-2].list_num)
elif tag in NESTABLE_TAGS and istate.rendered: elif tag in NESTABLE_TAGS and istate.rendered:
para = wrapper = bstate.nested[-1] 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 ems = self.profile.mobi_ems_per_blockquote
para = wrapper = etree.SubElement(parent, XHTML('blockquote')) para = wrapper = etree.SubElement(parent, XHTML('blockquote'))
para = wrapper para = wrapper

View File

@ -39,6 +39,12 @@ class MOBIOutput(OutputFormatPlugin):
OptionRecommendation(name='personal_doc', recommended_value='[PDOC]', OptionRecommendation(name='personal_doc', recommended_value='[PDOC]',
help=_('Tag marking book to be filed with Personal Docs') 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): def check_for_periodical(self):

View File

@ -633,12 +633,12 @@ class Style(object):
parent = self._getparent() parent = self._getparent()
if 'line-height' in self._style: if 'line-height' in self._style:
lineh = self._style['line-height'] lineh = self._style['line-height']
if lineh == 'normal':
lineh = '1.2'
try: try:
float(lineh) result = float(lineh) * self.fontSize
except ValueError: except ValueError:
result = self._unit_convert(lineh, base=self.fontSize) result = self._unit_convert(lineh, base=self.fontSize)
else:
result = float(lineh) * self.fontSize
elif parent is not None: elif parent is not None:
# TODO: proper inheritance # TODO: proper inheritance
result = parent.lineHeight result = parent.lineHeight

View File

@ -245,6 +245,8 @@ class CSSFlattener(object):
del node.attrib['bgcolor'] del node.attrib['bgcolor']
if cssdict.get('font-weight', '').lower() == 'medium': if cssdict.get('font-weight', '').lower() == 'medium':
cssdict['font-weight'] = 'normal' # ADE chokes on font-weight medium cssdict['font-weight'] = 'normal' # ADE chokes on font-weight medium
fsize = font_size
if not self.context.disable_font_rescaling: if not self.context.disable_font_rescaling:
_sbase = self.sbase if self.sbase is not None else \ _sbase = self.sbase if self.sbase is not None else \
self.context.source.fbase self.context.source.fbase
@ -258,6 +260,14 @@ class CSSFlattener(object):
fsize = self.fmap[font_size] fsize = self.fmap[font_size]
cssdict['font-size'] = "%0.5fem" % (fsize / psize) cssdict['font-size'] = "%0.5fem" % (fsize / psize)
psize = fsize 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 cssdict:
if self.lineh and self.fbase and tag != 'body': if self.lineh and self.fbase and tag != 'body':
self.clean_edges(cssdict, style, psize) self.clean_edges(cssdict, style, psize)
@ -290,6 +300,7 @@ class CSSFlattener(object):
lineh = self.lineh / psize lineh = self.lineh / psize
cssdict['line-height'] = "%0.5fem" % lineh cssdict['line-height'] = "%0.5fem" % lineh
if (self.context.remove_paragraph_spacing or if (self.context.remove_paragraph_spacing or
self.context.insert_blank_line) and tag in ('p', 'div'): self.context.insert_blank_line) and tag in ('p', 'div'):
if item_id != 'calibre_jacket' or self.context.output_profile.name == 'Kindle': if item_id != 'calibre_jacket' or self.context.output_profile.name == 'Kindle':

View File

@ -77,10 +77,14 @@ class TXTInput(InputFormatPlugin):
base = os.getcwdu() base = os.getcwdu()
if hasattr(stream, 'name'): if hasattr(stream, 'name'):
base = os.path.dirname(stream.name) base = os.path.dirname(stream.name)
htmlfile = open(os.path.join(base, 'temp_calibre_txt_input_to_html.html'), fname = os.path.join(base, 'index.html')
'wb') c = 0
htmlfile.write(html.encode('utf-8')) while os.path.exists(fname):
htmlfile.close() c += 1
fname = 'index%d.html'%c
htmlfile = open(fname, 'wb')
with htmlfile:
htmlfile.write(html.encode('utf-8'))
cwd = os.getcwdu() cwd = os.getcwdu()
odi = options.debug_pipeline odi = options.debug_pipeline
options.debug_pipeline = None options.debug_pipeline = None

View File

@ -9,7 +9,7 @@ from PyQt4.Qt import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, \
QByteArray, QTranslator, QCoreApplication, QThread, \ QByteArray, QTranslator, QCoreApplication, QThread, \
QEvent, QTimer, pyqtSignal, QDate, QDesktopServices, \ QEvent, QTimer, pyqtSignal, QDate, QDesktopServices, \
QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \ QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \
QIcon, QApplication, QDialog, QPushButton, QUrl QIcon, QApplication, QDialog, QPushButton, QUrl, QFont
ORG_NAME = 'KovidsBrain' ORG_NAME = 'KovidsBrain'
APP_UID = 'libprs500' APP_UID = 'libprs500'
@ -52,6 +52,7 @@ gprefs.defaults['show_splash_screen'] = True
gprefs.defaults['toolbar_icon_size'] = 'medium' gprefs.defaults['toolbar_icon_size'] = 'medium'
gprefs.defaults['toolbar_text'] = 'auto' gprefs.defaults['toolbar_text'] = 'auto'
gprefs.defaults['show_child_bar'] = False gprefs.defaults['show_child_bar'] = False
gprefs.defaults['font'] = None
# }}} # }}}
@ -613,6 +614,10 @@ class Application(QApplication):
qt_app = self qt_app = self
self._file_open_paths = [] self._file_open_paths = []
self._file_open_lock = RLock() 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): def _send_file_open_events(self):
with self._file_open_lock: with self._file_open_lock:

View File

@ -154,15 +154,17 @@ class EditMetadataAction(InterfaceAction):
d.view_format.connect(lambda d.view_format.connect(lambda
fmt:self.gui.iactions['View'].view_format(row_list[current_row], fmt:self.gui.iactions['View'].view_format(row_list[current_row],
fmt)) fmt))
if d.exec_() != d.Accepted: ret = d.exec_()
d.view_format.disconnect() d.break_cycles()
if ret != d.Accepted:
break break
d.view_format.disconnect()
changed.add(d.id) changed.add(d.id)
if d.row_delta == 0: if d.row_delta == 0:
break break
current_row += d.row_delta current_row += d.row_delta
if changed: if changed:
self.gui.library_view.model().refresh_ids(list(changed)) self.gui.library_view.model().refresh_ids(list(changed))
current = self.gui.library_view.currentIndex() current = self.gui.library_view.currentIndex()

View File

@ -10,7 +10,7 @@ import textwrap
from functools import partial from functools import partial
from PyQt4.Qt import QWidget, QSpinBox, QDoubleSpinBox, QLineEdit, QTextEdit, \ 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.customize.conversion import OptionRecommendation
from calibre.ebooks.conversion.config import load_defaults, \ from calibre.ebooks.conversion.config import load_defaults, \
@ -81,6 +81,21 @@ class Widget(QWidget):
self.apply_recommendations(defaults) self.apply_recommendations(defaults)
self.setup_help(get_help) 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): def restore_defaults(self, get_option):
defaults = GuiRecommendations() defaults = GuiRecommendations()
defaults.merge_recommendations(get_option, OptionRecommendation.LOW, defaults.merge_recommendations(get_option, OptionRecommendation.LOW,

View File

@ -21,7 +21,7 @@ class LookAndFeelWidget(Widget, Ui_Form):
def __init__(self, parent, get_option, get_help, db=None, book_id=None): def __init__(self, parent, get_option, get_help, db=None, book_id=None):
Widget.__init__(self, parent, Widget.__init__(self, parent,
['change_justification', 'extra_css', 'base_font_size', ['change_justification', 'extra_css', 'base_font_size',
'font_size_mapping', 'line_height', 'font_size_mapping', 'line_height', 'minimum_line_height',
'linearize_tables', 'smarten_punctuation', 'linearize_tables', 'smarten_punctuation',
'disable_font_rescaling', 'insert_blank_line', 'disable_font_rescaling', 'insert_blank_line',
'remove_paragraph_spacing', 'remove_paragraph_spacing_indent_size','input_encoding', 'remove_paragraph_spacing', 'remove_paragraph_spacing_indent_size','input_encoding',

View File

@ -97,7 +97,7 @@
</item> </item>
</layout> </layout>
</item> </item>
<item row="3" column="0"> <item row="4" column="0">
<widget class="QLabel" name="label"> <widget class="QLabel" name="label">
<property name="text"> <property name="text">
<string>Line &amp;height:</string> <string>Line &amp;height:</string>
@ -107,7 +107,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="3" column="1" colspan="2"> <item row="4" column="1" colspan="2">
<widget class="QDoubleSpinBox" name="opt_line_height"> <widget class="QDoubleSpinBox" name="opt_line_height">
<property name="suffix"> <property name="suffix">
<string> pt</string> <string> pt</string>
@ -117,7 +117,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="0"> <item row="5" column="0">
<widget class="QLabel" name="label_3"> <widget class="QLabel" name="label_3">
<property name="text"> <property name="text">
<string>Input character &amp;encoding:</string> <string>Input character &amp;encoding:</string>
@ -127,17 +127,17 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="1" colspan="3"> <item row="5" column="1" colspan="3">
<widget class="QLineEdit" name="opt_input_encoding"/> <widget class="QLineEdit" name="opt_input_encoding"/>
</item> </item>
<item row="5" column="0" colspan="2"> <item row="6" column="0" colspan="2">
<widget class="QCheckBox" name="opt_remove_paragraph_spacing"> <widget class="QCheckBox" name="opt_remove_paragraph_spacing">
<property name="text"> <property name="text">
<string>Remove &amp;spacing between paragraphs</string> <string>Remove &amp;spacing between paragraphs</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="2" colspan="2"> <item row="6" column="2" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_2"> <layout class="QHBoxLayout" name="horizontalLayout_2">
<item> <item>
<widget class="QLabel" name="label_4"> <widget class="QLabel" name="label_4">
@ -164,21 +164,21 @@
</item> </item>
</layout> </layout>
</item> </item>
<item row="6" column="0"> <item row="7" column="0">
<widget class="QLabel" name="label_5"> <widget class="QLabel" name="label_5">
<property name="text"> <property name="text">
<string>Text justification:</string> <string>Text justification:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="7" column="0"> <item row="8" column="0">
<widget class="QCheckBox" name="opt_linearize_tables"> <widget class="QCheckBox" name="opt_linearize_tables">
<property name="text"> <property name="text">
<string>&amp;Linearize tables</string> <string>&amp;Linearize tables</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="10" column="0" colspan="4"> <item row="11" column="0" colspan="4">
<widget class="QGroupBox" name="groupBox"> <widget class="QGroupBox" name="groupBox">
<property name="title"> <property name="title">
<string>Extra &amp;CSS</string> <string>Extra &amp;CSS</string>
@ -190,37 +190,60 @@
</layout> </layout>
</widget> </widget>
</item> </item>
<item row="6" column="2" colspan="2"> <item row="7" column="2" colspan="2">
<widget class="QComboBox" name="opt_change_justification"/> <widget class="QComboBox" name="opt_change_justification"/>
</item> </item>
<item row="7" column="1" colspan="3"> <item row="8" column="1" colspan="3">
<widget class="QCheckBox" name="opt_asciiize"> <widget class="QCheckBox" name="opt_asciiize">
<property name="text"> <property name="text">
<string>&amp;Transliterate unicode characters to ASCII</string> <string>&amp;Transliterate unicode characters to ASCII</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="8" column="0"> <item row="9" column="0">
<widget class="QCheckBox" name="opt_insert_blank_line"> <widget class="QCheckBox" name="opt_insert_blank_line">
<property name="text"> <property name="text">
<string>Insert &amp;blank line</string> <string>Insert &amp;blank line</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="8" column="1" colspan="2"> <item row="9" column="1" colspan="2">
<widget class="QCheckBox" name="opt_keep_ligatures"> <widget class="QCheckBox" name="opt_keep_ligatures">
<property name="text"> <property name="text">
<string>Keep &amp;ligatures</string> <string>Keep &amp;ligatures</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="9" column="0"> <item row="10" column="0">
<widget class="QCheckBox" name="opt_smarten_punctuation"> <widget class="QCheckBox" name="opt_smarten_punctuation">
<property name="text"> <property name="text">
<string>Smarten &amp;punctuation</string> <string>Smarten &amp;punctuation</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="3" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Minimum &amp;line height:</string>
</property>
<property name="buddy">
<cstring>opt_minimum_line_height</cstring>
</property>
</widget>
</item>
<item row="3" column="1" colspan="2">
<widget class="QDoubleSpinBox" name="opt_minimum_line_height">
<property name="suffix">
<string> %</string>
</property>
<property name="decimals">
<number>1</number>
</property>
<property name="maximum">
<double>900.000000000000000</double>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<resources> <resources>

View File

@ -25,6 +25,7 @@ class PluginWidget(Widget, Ui_Form):
def __init__(self, parent, get_option, get_help, db=None, book_id=None): def __init__(self, parent, get_option, get_help, db=None, book_id=None):
Widget.__init__(self, parent, Widget.__init__(self, parent,
['prefer_author_sort', 'rescale_images', 'toc_title', ['prefer_author_sort', 'rescale_images', 'toc_title',
'mobi_ignore_margins',
'dont_compress', 'no_inline_toc', 'masthead_font','personal_doc'] 'dont_compress', 'no_inline_toc', 'masthead_font','personal_doc']
) )
self.db, self.book_id = db, book_id self.db, self.book_id = db, book_id

View File

@ -55,7 +55,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="0" colspan="2"> <item row="6" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox"> <widget class="QGroupBox" name="groupBox">
<property name="title"> <property name="title">
<string>Kindle options</string> <string>Kindle options</string>
@ -101,7 +101,7 @@
</layout> </layout>
</widget> </widget>
</item> </item>
<item row="6" column="0"> <item row="7" column="0">
<spacer name="verticalSpacer_2"> <spacer name="verticalSpacer_2">
<property name="orientation"> <property name="orientation">
<enum>Qt::Vertical</enum> <enum>Qt::Vertical</enum>
@ -114,6 +114,13 @@
</property> </property>
</spacer> </spacer>
</item> </item>
<item row="5" column="0">
<widget class="QCheckBox" name="opt_mobi_ignore_margins">
<property name="text">
<string>Ignore &amp;margins</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<resources/> <resources/>

View File

@ -12,7 +12,7 @@ from PyQt4.Qt import QMenu, QAction, QActionGroup, QIcon, SIGNAL, \
from calibre.customize.ui import available_input_formats, available_output_formats, \ from calibre.customize.ui import available_input_formats, available_output_formats, \
device_plugins device_plugins
from calibre.devices.interface import DevicePlugin 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.gui2.dialogs.choose_format import ChooseFormatDialog
from calibre.utils.ipc.job import BaseJob from calibre.utils.ipc.job import BaseJob
from calibre.devices.scanner import DeviceScanner from calibre.devices.scanner import DeviceScanner
@ -122,7 +122,8 @@ def device_name_for_plugboards(device_class):
class DeviceManager(Thread): # {{{ 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 :sleep_time: Time to sleep between device probes in secs
''' '''
@ -143,6 +144,7 @@ class DeviceManager(Thread): # {{{
self.ejected_devices = set([]) self.ejected_devices = set([])
self.mount_connection_requests = Queue.Queue(0) self.mount_connection_requests = Queue.Queue(0)
self.open_feedback_slot = open_feedback_slot self.open_feedback_slot = open_feedback_slot
self.open_feedback_msg = open_feedback_msg
def report_progress(self, *args): def report_progress(self, *args):
pass pass
@ -163,6 +165,9 @@ class DeviceManager(Thread): # {{{
dev.reset(detected_device=detected_device, dev.reset(detected_device=detected_device,
report_progress=self.report_progress) report_progress=self.report_progress)
dev.open() dev.open()
except OpenFeedback, e:
self.open_feedback_msg(dev.get_gui_name(), e.feedback_msg)
continue
except: except:
tb = traceback.format_exc() tb = traceback.format_exc()
if DEBUG or tb not in self.reported_errors: if DEBUG or tb not in self.reported_errors:
@ -594,11 +599,16 @@ class DeviceMixin(object): # {{{
_('Error communicating with device'), ' ') _('Error communicating with device'), ' ')
self.device_error_dialog.setModal(Qt.NonModal) self.device_error_dialog.setModal(Qt.NonModal)
self.device_manager = DeviceManager(Dispatcher(self.device_detected), 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() self.device_manager.start()
if tweaks['auto_connect_to_folder']: if tweaks['auto_connect_to_folder']:
self.connect_to_folder_named(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): def auto_convert_question(self, msg, autos):
autos = u'\n'.join(map(unicode, map(force_unicode, autos))) autos = u'\n'.join(map(unicode, map(force_unicode, autos)))
return self.ask_a_yes_no_question( return self.ask_a_yes_no_question(

View File

@ -102,7 +102,7 @@ class MyBlockingBusy(QDialog):
remove_all, remove, add, au, aus, do_aus, rating, pub, do_series, \ remove_all, remove, add, au, aus, do_aus, rating, pub, do_series, \
do_autonumber, do_remove_format, remove_format, do_swap_ta, \ do_autonumber, do_remove_format, remove_format, do_swap_ta, \
do_remove_conv, do_auto_author, series, do_series_restart, \ 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 # 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) self.db.set_title(id, titlecase(title), notify=False)
if au: if au:
self.db.set_authors(id, string_to_authors(au), notify=False) 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: elif self.current_phase == 2:
# All of these just affect the DB, so we can tolerate a total rollback # All of these just affect the DB, so we can tolerate a total rollback
if do_auto_author: if do_auto_author:
@ -678,11 +695,16 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
do_remove_conv = self.remove_conversion_settings.isChecked() do_remove_conv = self.remove_conversion_settings.isChecked()
do_auto_author = self.auto_author_sort.isChecked() do_auto_author = self.auto_author_sort.isChecked()
do_title_case = self.change_title_to_title_case.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, args = (remove_all, remove, add, au, aus, do_aus, rating, pub, do_series,
do_autonumber, do_remove_format, remove_format, do_swap_ta, do_autonumber, do_remove_format, remove_format, do_swap_ta,
do_remove_conv, do_auto_author, series, do_series_restart, 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}%%.') bb = MyBlockingBusy(_('Applying changes to %d books.\nPhase {0} {1}%%.')
%len(self.ids), args, self.db, self.ids, %len(self.ids), args, self.db, self.ids,

View File

@ -381,7 +381,7 @@ Future conversion of these books will use the default settings.</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="15" column="0" colspan="3"> <item row="14" column="0" colspan="3">
<spacer name="verticalSpacer_2"> <spacer name="verticalSpacer_2">
<property name="orientation"> <property name="orientation">
<enum>Qt::Vertical</enum> <enum>Qt::Vertical</enum>
@ -394,6 +394,39 @@ Future conversion of these books will use the default settings.</string>
</property> </property>
</spacer> </spacer>
</item> </item>
<item row="13" column="0" colspan="3">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Change &amp;cover</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QRadioButton" name="cover_no_change">
<property name="text">
<string>&amp;No change</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="cover_remove">
<property name="text">
<string>&amp;Remove cover</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="cover_generate">
<property name="text">
<string>&amp;Generate default cover</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<widget class="QWidget" name="tab"> <widget class="QWidget" name="tab">

View File

@ -240,37 +240,39 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.cover_fetcher = CoverFetcher(None, None, isbn, self.cover_fetcher = CoverFetcher(None, None, isbn,
self.timeout, title, author) self.timeout, title, author)
self.cover_fetcher.start() self.cover_fetcher.start()
self._hangcheck = QTimer(self)
self._hangcheck.timeout.connect(self.hangcheck,
type=Qt.QueuedConnection)
self.cf_start_time = time.time() self.cf_start_time = time.time()
self.pi.start(_('Downloading cover...')) self.pi.start(_('Downloading cover...'))
self._hangcheck.start(100) QTimer.singleShot(100, self.hangcheck)
def hangcheck(self): def hangcheck(self):
if self.cover_fetcher.is_alive() and \ cf = self.cover_fetcher
time.time()-self.cf_start_time < self.COVER_FETCH_TIMEOUT: 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 return
self._hangcheck.stop()
try: try:
if self.cover_fetcher.is_alive(): if cf.is_alive():
error_dialog(self, _('Cannot fetch cover'), error_dialog(self, _('Cannot fetch cover'),
_('<b>Could not fetch cover.</b><br/>')+ _('<b>Could not fetch cover.</b><br/>')+
_('The download timed out.')).exec_() _('The download timed out.')).exec_()
return return
if self.cover_fetcher.needs_isbn: if cf.needs_isbn:
error_dialog(self, _('Cannot fetch cover'), error_dialog(self, _('Cannot fetch cover'),
_('Could not find cover for this book. Try ' _('Could not find cover for this book. Try '
'specifying the ISBN first.')).exec_() 'specifying the ISBN first.')).exec_()
return return
if self.cover_fetcher.exception is not None: if cf.exception is not None:
err = self.cover_fetcher.exception err = cf.exception
error_dialog(self, _('Cannot fetch cover'), error_dialog(self, _('Cannot fetch cover'),
_('<b>Could not fetch cover.</b><br/>')+unicode(err)).exec_() _('<b>Could not fetch cover.</b><br/>')+unicode(err)).exec_()
return return
if self.cover_fetcher.errors and self.cover_fetcher.cover_data is None: if cf.errors and cf.cover_data is None:
details = u'\n\n'.join([e[-1] + ': ' + e[1] for e in self.cover_fetcher.errors]) details = u'\n\n'.join([e[-1] + ': ' + e[1] for e in cf.errors])
error_dialog(self, _('Cannot fetch cover'), error_dialog(self, _('Cannot fetch cover'),
_('<b>Could not fetch cover.</b><br/>') + _('<b>Could not fetch cover.</b><br/>') +
_('For the error message from each cover source, ' _('For the error message from each cover source, '
@ -278,7 +280,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
return return
pix = QPixmap() pix = QPixmap()
pix.loadFromData(self.cover_fetcher.cover_data) pix.loadFromData(cf.cover_data)
if pix.isNull(): if pix.isNull():
error_dialog(self, _('Bad cover'), error_dialog(self, _('Bad cover'),
_('The cover is not a valid picture')).exec_() _('The cover is not a valid picture')).exec_()
@ -287,11 +289,12 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.update_cover_tooltip() self.update_cover_tooltip()
self.cover_changed = True self.cover_changed = True
self.cpixmap = pix self.cpixmap = pix
self.cover_data = self.cover_fetcher.cover_data self.cover_data = cf.cover_data
finally: finally:
self.fetch_cover_button.setEnabled(True) self.fetch_cover_button.setEnabled(True)
self.unsetCursor() 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, def __init__(self, window, row, db, prev=None,
next_=None): next_=None):
ResizableDialog.__init__(self, window) ResizableDialog.__init__(self, window)
self.cover_fetcher = None
self.bc_box.layout().setAlignment(self.cover, Qt.AlignCenter|Qt.AlignHCenter) self.bc_box.layout().setAlignment(self.cover, Qt.AlignCenter|Qt.AlignHCenter)
self.cancel_all = False
base = unicode(self.author_sort.toolTip()) base = unicode(self.author_sort.toolTip())
self.ok_aus_tooltip = '<p>' + textwrap.fill(base+'<br><br>'+ self.ok_aus_tooltip = '<p>' + textwrap.fill(base+'<br><br>'+
_(' The green color indicates that the current ' _(' 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) QObject.connect(self.series, SIGNAL('editTextChanged(QString)'), self.enable_series_index)
self.series.lineEdit().editingFinished.connect(self.increment_series_index) self.series.lineEdit().editingFinished.connect(self.increment_series_index)
self.show()
pm = QPixmap() pm = QPixmap()
if cover: if cover:
pm.loadFromData(cover) pm.loadFromData(cover)
@ -590,6 +592,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.original_author = unicode(self.authors.text()).strip() self.original_author = unicode(self.authors.text()).strip()
self.original_title = unicode(self.title.text()).strip() self.original_title = unicode(self.title.text()).strip()
self.show()
def create_custom_column_editors(self): def create_custom_column_editors(self):
w = self.central_widget.widget(1) w = self.central_widget.widget(1)
layout = w.layout() layout = w.layout()
@ -828,10 +832,6 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.accept() self.accept()
def accept(self): def accept(self):
cf = getattr(self, 'cover_fetcher', None)
if cf is not None and hasattr(cf, 'terminate'):
cf.terminate()
cf.wait()
try: try:
if self.formats_changed: if self.formats_changed:
self.sync_formats() self.sync_formats()
@ -888,14 +888,12 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
show=True) show=True)
raise raise
self.save_state() self.save_state()
self.cover_fetcher = None
QDialog.accept(self) QDialog.accept(self)
def reject(self, *args): 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.save_state()
self.cover_fetcher = None
QDialog.reject(self, *args) QDialog.reject(self, *args)
def read_state(self): def read_state(self):
@ -910,3 +908,48 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
dynamic.set('metasingle_window_geometry', bytes(self.saveGeometry())) dynamic.set('metasingle_window_geometry', bytes(self.saveGeometry()))
dynamic.set('metasingle_splitter_state', dynamic.set('metasingle_splitter_state',
bytes(self.splitter.saveState())) 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'

View File

@ -145,7 +145,7 @@ class TagCategories(QDialog, Ui_TagCategories):
index = self.all_items[node.data(Qt.UserRole).toPyObject()].index index = self.all_items[node.data(Qt.UserRole).toPyObject()].index
if index not in self.applied_items: if index not in self.applied_items:
self.applied_items.append(index) 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) self.display_filtered_categories(None)
def unapply_tags(self, node=None): def unapply_tags(self, node=None):

View File

@ -105,9 +105,13 @@ class TagListEditor(QDialog, Ui_TagListEditor):
if not question_dialog(self, _('Are your sure?'), if not question_dialog(self, _('Are your sure?'),
'<p>'+_('Are you certain you want to delete the following items?')+'<br>'+ct): '<p>'+_('Are you certain you want to delete the following items?')+'<br>'+ct):
return return
row = self.available_tags.row(deletes[0])
for item in deletes: for item in deletes:
(id,ign) = item.data(Qt.UserRole).toInt() (id,ign) = item.data(Qt.UserRole).toInt()
self.to_delete.append(id) self.to_delete.append(id)
self.available_tags.takeItem(self.available_tags.row(item)) 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))

View File

@ -123,6 +123,8 @@ class Stack(QStackedWidget): # {{{
_('Tag Browser'), I('tags.png'), _('Tag Browser'), I('tags.png'),
parent=parent, side_index=0, initial_side_size=200, parent=parent, side_index=0, initial_side_size=200,
shortcut=_('Shift+Alt+T')) 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(self.tb_widget)
parent.tb_splitter.addWidget(parent.cb_splitter) parent.tb_splitter.addWidget(parent.cb_splitter)
parent.tb_splitter.setCollapsible(parent.tb_splitter.other_index, False) parent.tb_splitter.setCollapsible(parent.tb_splitter.other_index, False)

View File

@ -5,10 +5,11 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
from PyQt4.Qt import QApplication, QFont, QFontInfo, QFontDialog
from calibre.gui2.preferences import ConfigWidgetBase, test_widget from calibre.gui2.preferences import ConfigWidgetBase, test_widget
from calibre.gui2.preferences.look_feel_ui import Ui_Form 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, \ from calibre.utils.localization import available_translations, \
get_language, get_lang get_language, get_lang
from calibre.utils.config import prefs from calibre.utils.config import prefs
@ -56,12 +57,64 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
(_('Never'), 'never')] (_('Never'), 'never')]
r('toolbar_text', gprefs, choices=choices) 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): def refresh_gui(self, gui):
gui.search.search_as_you_type(config['search_as_you_type']) gui.search.search_as_you_type(config['search_as_you_type'])
self.update_font_display()
if __name__ == '__main__': if __name__ == '__main__':
from PyQt4.Qt import QApplication
app = QApplication([]) app = QApplication([])
test_widget('Interface', 'Look & Feel') test_widget('Interface', 'Look & Feel')

View File

@ -183,7 +183,7 @@
</layout> </layout>
</widget> </widget>
</item> </item>
<item row="8" column="0" colspan="2"> <item row="9" column="0" colspan="2">
<spacer name="verticalSpacer"> <spacer name="verticalSpacer">
<property name="orientation"> <property name="orientation">
<enum>Qt::Vertical</enum> <enum>Qt::Vertical</enum>
@ -196,6 +196,20 @@
</property> </property>
</spacer> </spacer>
</item> </item>
<item row="8" column="0">
<widget class="QLineEdit" name="font_display">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="QPushButton" name="change_font_button">
<property name="text">
<string>Change &amp;font (needs restart)</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<resources/> <resources/>

View File

@ -251,10 +251,28 @@ class Preferences(QMainWindow):
self.close() self.close()
self.run_wizard_requested.emit() 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): def show_plugin(self, plugin):
self.showing_widget = plugin.create_widget(self.scroll_area) self.showing_widget = plugin.create_widget(self.scroll_area)
self.showing_widget.genesis(self.gui) self.showing_widget.genesis(self.gui)
self.showing_widget.initialize() self.showing_widget.initialize()
self.set_tooltips_for_labels()
self.scroll_area.setWidget(self.showing_widget) self.scroll_area.setWidget(self.showing_widget)
self.stack.setCurrentIndex(1) self.stack.setCurrentIndex(1)
self.showing_widget.show() self.showing_widget.show()

View File

@ -87,6 +87,13 @@ class TagsView(QTreeView): # {{{
self.setDragDropMode(self.DropOnly) self.setDragDropMode(self.DropOnly)
self.setDropIndicatorShown(True) self.setDropIndicatorShown(True)
self.setAutoExpandDelay(500) 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): def set_database(self, db, tag_match, sort_by):
self.hidden_categories = config['tag_browser_hidden_categories'] self.hidden_categories = config['tag_browser_hidden_categories']
@ -300,7 +307,7 @@ class TagsView(QTreeView): # {{{
return self.isExpanded(idx) return self.isExpanded(idx)
def recount(self, *args): def recount(self, *args):
if self.disable_recounting: if self.disable_recounting or not self.pane_is_visible:
return return
self.refresh_signal_processed = True self.refresh_signal_processed = True
ci = self.currentIndex() ci = self.currentIndex()
@ -969,6 +976,7 @@ class TagBrowserWidget(QWidget): # {{{
self._layout.setContentsMargins(0,0,0,0) self._layout.setContentsMargins(0,0,0,0)
parent.tags_view = TagsView(parent) parent.tags_view = TagsView(parent)
self.tags_view = parent.tags_view
self._layout.addWidget(parent.tags_view) self._layout.addWidget(parent.tags_view)
parent.sort_by = QComboBox(parent) parent.sort_by = QComboBox(parent)
@ -998,6 +1006,9 @@ class TagBrowserWidget(QWidget): # {{{
_('Add your own categories to the Tag Browser')) _('Add your own categories to the Tag Browser'))
parent.edit_categories.setStatusTip(parent.edit_categories.toolTip()) 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)
# }}} # }}}

View File

@ -19,12 +19,15 @@ def generate_test_db(library_path,
max_tags=10 max_tags=10
): ):
import random, string, os, sys, time import random, string, os, sys, time
from calibre.constants import preferred_encoding
if not os.path.exists(library_path): if not os.path.exists(library_path):
os.makedirs(library_path) os.makedirs(library_path)
letters = string.letters.decode(preferred_encoding)
def randstr(length): def randstr(length):
return ''.join(random.choice(string.letters) for i in return ''.join(random.choice(letters) for i in
xrange(length)) xrange(length))
all_tags = [randstr(tag_length) for j in xrange(num_of_tags)] all_tags = [randstr(tag_length) for j in xrange(num_of_tags)]

View File

@ -119,10 +119,8 @@ class SafeFormat(TemplateFormatter):
try: try:
b = self.book.get_user_metadata(key, False) b = self.book.get_user_metadata(key, False)
except: except:
if DEBUG: traceback.print_exc()
traceback.print_exc()
b = None b = None
if b is not None and b['datatype'] == 'composite': if b is not None and b['datatype'] == 'composite':
if key in self.composite_values: if key in self.composite_values:
return self.composite_values[key] return self.composite_values[key]
@ -135,8 +133,7 @@ class SafeFormat(TemplateFormatter):
return val.replace('/', '_').replace('\\', '_') return val.replace('/', '_').replace('\\', '_')
return '' return ''
except: except:
if DEBUG: traceback.print_exc()
traceback.print_exc()
return key return key
def get_components(template, mi, id, timefmt='%b %Y', length=250, 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() format_args['tags'] = mi.format_tags()
if format_args['tags'].startswith('/'): if format_args['tags'].startswith('/'):
format_args['tags'] = format_args['tags'][1:] format_args['tags'] = format_args['tags'][1:]
else:
format_args['tags'] = ''
if mi.series: if mi.series:
format_args['series'] = tsfmt(mi.series) format_args['series'] = tsfmt(mi.series)
if mi.series_index is not None: 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): if not os.path.exists(dirpath):
raise raise
ocover = mi.cover
if opts.save_cover and cover and os.access(cover, os.R_OK): if opts.save_cover and cover and os.access(cover, os.R_OK):
with open(base_path+'.jpg', 'wb') as f: with open(base_path+'.jpg', 'wb') as f:
with open(cover, 'rb') as s: 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: with open(base_path+'.opf', 'wb') as f:
f.write(opf) f.write(opf)
mi.cover = ocover
written = False written = False
for fmt in formats: for fmt in formats:
global plugboard_save_to_disk_value, plugboard_any_format_value global plugboard_save_to_disk_value, plugboard_any_format_value

55
src/calibre/utils/mem.py Normal file
View File

@ -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 <kovid@kovidgoyal.net>'
__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/<pid>/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/ }}}