Merge frome trunk

This commit is contained in:
Sengian 2011-01-24 23:04:14 +01:00
commit 12e1c5219b
108 changed files with 126385 additions and 73456 deletions

View File

@ -4,6 +4,122 @@
# for important features/bug fixes.
# Also, each release can have new and improved recipes.
- version: 0.7.42
date: 2011-01-21
new features:
- title: "0.7.42 is a re-release of 0.7.41, because conversion to MOBI was broken in 0.7.41"
- title: "Conversions: Replace the remove header/footer options with a more geenric search replace option, that allows you to not only remove but also replace text"
- title: "Conversion: The preprocess html option has now become a new 'Heuristic Processing' option which allows you to control exactly which heuristics are used"
- title: "Conversion: Various improvements to Heuristic Processing (used to be preprocess HTML)"
- title: "When adding empty books to calibre, optionally set the author to the author of the currently selected book"
tickets: [7702]
- title: "Device drivers for the Archos 101, SmatQ T7 and Acer Lumiread"
- title: "Catalog generation: Make By Authors optional"
- title: "Allow bulk editing of Date and Published columns."
- title: "Add a little button to clear date and published values to the edit metadata dialogs"
- title: "When adding books by ISBN, allow the specification of special tags that will be added to the new book entries"
tickets: [8436]
- title: "Completion on multiple authors"
tickets: [8405]
- title: "Add AZW to default list of internally viewed formats, a I am tired of getting tickets about it"
- title: "Nicer error message when catalog generation fails"
- title: "Add capitalize option to context menus in the edit metadata dialog"
bug fixes:
- title: "RTF Input: Fix regression in 0.7.40 that broke conversion of some old style RTF files"
- title: "Fix Tag editor forgets position"
tickets: [8271]
- title: "When converting books in the calibre GUI, override metadata from the input document, even when empty."
description: >
"So if you have removed all the tags and comments in the calibre GUI for the book in the calibre GUI, but the actual file that is being converted still has tags and comments, they are ignored. This affects only conversions in the calibre GUI, not from the command line via ebook-convert."
tickets: [8390]
- title: "Fix memory leak when switching libraries"
- title: "RTF Output: Fix incorrent spacing between letters."
tickets: [8422]
- title: "Catalog generation: Add composite columns to Merge Comments eligible types"
- title: "Add a confirmation when closing the add a custom news source dialog."
tickets: [8460]
- title: "Another workaround for LibraryThing UA sniffing that was preventing series metadata download, sigh."
tickets: [8477]
- title: "PD Novel driver: Put books on the SD card into the eBooks folder"
- title: "When shortening filepaths to conform to windows path length limitations, remove text from the middle of each component instead of the ends."
tickets: [8451]
- title: "Make completion in most places case insensitive"
tickets: [8441]
- title: "Fix regression that caused the N key to stop working when editing a Yes/no column"
tickets: [8417]
- title: "Email: Fix bug when connecting to SMTP relays that use MD5 auth"
- title: "MOBI Output: Fix bug that could cause a link pointing to the start of a section to go to a point later in the section is the section contained an empty id attribute"
- title: "When auto converting books and the device is unplugged, do not raise an error."
tickets: [8426]
- title: "Ebook-viewer: Display cover when viewing FB2 files"
- title: "MOBI Input: Special case handling of emptu div tags with a defined height used as paragraph separators."
tickets: [8391]
- title: "Fix sorting of author names into sub categories by first letter in the Tag Browser when the first letter has diacritics"
tickets: [8378]
- title: "Fix regression in 0.7.40 that caused commas in author names to become | when converting/saving to disk"
- title: "Fix view specific format on a book with no formats gives an error"
tickets: [8352]
improved recipes:
- Blic
- Las Vegas Review Journal
- La Vanguardia
- New York Times
- El Pais
- Seattle Times
- Ars Technica
- Dilbert
- Nature News
new recipes:
- title: "kath.net"
author: "Bobus"
- title: "iHNed"
author: "Karel Bilek"
- title: "Gulf News"
author: "Darko Miletic"
- title: "South Africa Mail and Guardian"
author: "77ja65"
- version: 0.7.40
date: 2011-01-14

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 B

View File

@ -0,0 +1,17 @@
from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1295310874(BasicNewsRecipe):
title = u'20 Minutos (Boletin)'
__author__ = 'Luis Hernandez'
description = 'Periódico gratuito en español'
cover_url = 'http://estaticos.20minutos.es/mmedia/especiales/corporativo/css/img/logotipos_grupo20minutos.gif'
language = 'es'
oldest_article = 2
max_articles_per_feed = 50
feeds = [(u'VESPERTINO', u'http://20minutos.feedsportal.com/c/32489/f/478284/index.rss')
, (u'DEPORTES', u'http://20minutos.feedsportal.com/c/32489/f/478286/index.rss')
, (u'CULTURA', u'http://www.20minutos.es/rss/ocio/')
, (u'TV', u'http://20minutos.feedsportal.com/c/32489/f/490877/index.rss')
]

View File

@ -0,0 +1,43 @@
import re
from calibre.web.feeds.news import BasicNewsRecipe
class ABCRecipe(BasicNewsRecipe):
title = u'ABC Linuxu'
oldest_article = 5
max_articles_per_feed = 3#5
__author__ = 'Funthomas'
language = 'cs'
feeds = [
#(u'Blogy', u'http://www.abclinuxu.cz/auto/blogDigest.rss'),
(u'Články', u'http://www.abclinuxu.cz/auto/abc.rss'),
(u'Zprávičky','http://www.abclinuxu.cz/auto/zpravicky.rss')
]
remove_javascript = True
no_stylesheets = True
remove_attributes = ['width','height']
remove_tags_before = dict(name='h1')
remove_tags = [
dict(attrs={'class':['meta-vypis','page_tools','cl_perex']}),
dict(attrs={'class':['cl_nadpis-link','komix-nav']})
]
remove_tags_after = [
dict(name='div',attrs={'class':['cl_perex','komix-nav']}),
dict(attrs={'class':['meta-vypis','page_tools']}),
dict(name='',attrs={'':''}),
]
preprocess_regexps = [
(re.compile(r'</div>.*<p class="perex">', re.DOTALL),lambda match: '</div><p class="perex">')
]
def print_version(self, url):
return url + '?varianta=print&noDiz'
extra_css = '''
h1 {font-size:130%; font-weight:bold}
h3 {font-size:111%; font-weight:bold}
'''

View File

@ -0,0 +1,66 @@
__license__ = 'GPL v3'
__copyright__ = '2011, Darko Miletic <darko.miletic at gmail.com>'
'''
daily.tportal.hr
'''
from calibre.web.feeds.news import BasicNewsRecipe
class Pagina12(BasicNewsRecipe):
title = 'Daily tportal.h'
__author__ = 'Darko Miletic'
description = 'News from Croatia'
publisher = 'tportal.hr'
category = 'news, politics, Croatia'
oldest_article = 2
max_articles_per_feed = 200
no_stylesheets = True
encoding = 'utf-8'
use_embedded_content = False
language = 'en_HR'
remove_empty_feeds = True
publication_type = 'newsportal'
extra_css = """
body{font-family: Verdana,sans-serif }
img{margin-bottom: 0.4em; display:block}
h1,h2{color: #2D648A; font-family: Georgia,serif}
.artAbstract{font-size: 1.2em; font-family: Georgia,serif}
"""
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
}
remove_tags = [
dict(name=['meta','link','embed','object','iframe','base'])
,dict(name='div', attrs={'class':'artInfo'})
]
remove_attributes=['lang']
keep_only_tags=dict(attrs={'class':'articleDetails'})
feeds = [(u'News', u'http://daily.tportal.hr/rss/dailynaslovnicarss.xml')]
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
for item in soup.findAll('a'):
limg = item.find('img')
if item.string is not None:
str = item.string
item.replaceWith(str)
else:
if limg:
item.name = 'div'
item.attrs = []
else:
str = self.tag_to_string(item)
item.replaceWith(str)
for item in soup.findAll('img'):
if not item.has_key('alt'):
item['alt'] = 'image'
return soup

View File

@ -0,0 +1,36 @@
from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1295088390(BasicNewsRecipe):
title = u'Everett Herald'
language = 'en'
__author__ = '77ja65'
oldest_article = 4
max_articles_per_feed = 50
no_stylesheets = True
masthead_url = 'http://heraldnet.com/images/hnet/jQueryComponents/jQueryNavigation/heraldnet_logo.png'
extra_css = '.headline {font-size: x-large;} \n .fact { padding-top: 10pt }'
feeds = [(u'Local News',
u'http://heraldnet.com/section/RSS02&mime=xml'),
(u'Sports', u'http://heraldnet.com/section/RSS04&mime=xml'),
(u'Entertainment',
u'http://heraldnet.com/section/RSS07&mime=xml'),
(u'Life', u'http://heraldnet.com/section/RSS03&mime=xml'),
(u'Breaking News',
u'http://heraldnet.com/section/RSS34&mime=xml'),
(u'Seahawks', u'http://heraldnet.com/section/RSS22&mime=xml'),
(u'HeraldNet', u'http://heraldnet.com/section/RSS01&mime=xml'),
(u'Inside Everett',
u'http://heraldnet.com/section/RSS26&mime=xml')
]
def print_version(self, url):
return url + "&template=PrinterFriendly"
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;}
'''

View File

@ -52,6 +52,7 @@ class heiseDe(BasicNewsRecipe):
dict(id='navi_login'),
dict(id='navigation'),
dict(id='breadcrumb'),
dict(id='adblockerwarnung'),
dict(id=''),
dict(id='sitemap'),
dict(id='bannerzone'),
@ -67,3 +68,4 @@ class heiseDe(BasicNewsRecipe):

View File

@ -21,7 +21,7 @@ class hnaDe(BasicNewsRecipe):
max_articles_per_feed = 40
no_stylesheets = True
remove_javascript = True
encoding = 'iso-8859-1'
encoding = 'utf-8'
remove_tags = [dict(id='topnav'),
dict(id='nav_main'),
@ -60,3 +60,4 @@ class hnaDe(BasicNewsRecipe):
feeds = [ ('hna_soehre', 'http://feeds2.feedburner.com/hna/soehre'),
('hna_kassel', 'http://feeds2.feedburner.com/hna/kassel') ]

View File

@ -0,0 +1,54 @@
from calibre.web.feeds.recipes import BasicNewsRecipe
class iHeuteRecipe(BasicNewsRecipe):
__author__ = 'FunThomas'
title = u'iDnes.cz'
publisher = u'MAFRA a.s.'
description = 'iDNES.cz Zprávy, Technet, Komiksy a další'
oldest_article = 3
max_articles_per_feed = 2
feeds = [
(u'Zprávy', u'http://servis.idnes.cz/rss.asp?c=zpravodaj'),
(u'Sport', u'http://servis.idnes.cz/rss.asp?c=sport'),
(u'Technet', u'http://servis.idnes.cz/rss.asp?c=technet'),
(u'Mobil', u'http://servis.idnes.cz/rss.asp?c=mobil'),
(u'Ekonomika', u'http://servis.idnes.cz/rss.asp?c=ekonomikah'),
#(u'Kultura', u'http://servis.idnes.cz/rss.asp?c=kultura'),
(u'Cestování', u'http://servis.idnes.cz/rss.asp?c=iglobe'),
#(u'Kavárna', u'http://servis.idnes.cz/rss.asp?r=kavarna'),
(u'Komixy', u'http://servis.idnes.cz/rss.asp?c=komiksy')
]
encoding = 'cp1250'
language = 'cs'
cover_url = 'http://g.idnes.cz/u/loga-n4/idnes.gif'
remove_javascript = True
no_stylesheets = True
remove_attributes = ['width','height']
remove_tags = [dict(name='div', attrs={'id':['zooming']}),
dict(name='div', attrs={'class':['related','mapa-wrapper']}),
dict(name='table', attrs={'id':['opener-img','portal']}),
dict(name='table', attrs={'class':['video-16ku9']})]
remove_tags_after = [dict(name='div',attrs={'id':['related','related2']})]
keep_only_tags = [dict(name='div', attrs={'class':['art-full adwords-text','dil-day']})
,dict(name='table',attrs={'class':['kemel-box']})]
def print_version(self, url):
print_url = url
split_url = url.split("?")
if (split_url[0].rfind('dilbert.asp') != -1): #dilbert komix
print_url = print_url.replace('.htm','.gif&tisk=1')
print_url = print_url.replace('.asp','.aspx')
elif (split_url[0].rfind('kemel.asp') == -1): #not Kemel komix
print_url = 'http://zpravy.idnes.cz/tiskni.asp?' + split_url[1]
#kemel kemel print page doesn't work
return print_url
extra_css = '''
h1 {font-size:125%; font-weight:bold}
h3 {font-size:110%; font-weight:bold}
'''

View File

@ -0,0 +1,29 @@
from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1294946868(BasicNewsRecipe):
title = u'La Tribuna de Talavera'
__author__ = 'Luis Hernández'
description = 'Diario de Talavera de la Reina'
cover_url = 'http://www.latribunadetalavera.es/entorno/mancheta.gif'
oldest_article = 5
max_articles_per_feed = 50
remove_javascript = True
no_stylesheets = True
use_embedded_content = False
encoding = 'utf-8'
language = 'es'
timefmt = '[%a, %d %b, %Y]'
keep_only_tags = [dict(name='div', attrs={'id':['articulo']})
,dict(name='div', attrs={'class':['foto']})
,dict(name='p', attrs={'id':['texto']})
]
remove_tags_before = dict(name='div' , attrs={'class':['comparte']})
remove_tags_after = dict(name='div' , attrs={'id':['relacionadas']})
feeds = [(u'Portada', u'http://www.latribunadetalavera.es/rss.html')]

View File

@ -1,5 +1,5 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
__copyright__ = '2010-2011, Darko Miletic <darko.miletic at gmail.com>'
'''
nrc.nl
'''
@ -15,13 +15,18 @@ class Pagina12(BasicNewsRecipe):
oldest_article = 2
max_articles_per_feed = 200
no_stylesheets = True
encoding = 'cp1252'
encoding = 'utf8'
use_embedded_content = False
language = 'nl'
country = 'NL'
remove_empty_feeds = True
masthead_url = 'http://www.nrc.nl/nrc.nl/images/logo_nrc.png'
extra_css = ' body{font-family: Verdana,Arial,Helvetica,sans-serif } img{margin-bottom: 0.4em} h1,h2,h3{text-align:left} '
extra_css = """
body{font-family: Georgia,serif }
img{margin-bottom: 0.4em; display: block}
.bijschrift,.sectie{font-size: x-small}
.sectie{color: gray}
"""
conversion_options = {
'comment' : description
@ -30,21 +35,42 @@ class Pagina12(BasicNewsRecipe):
, 'language' : language
}
keep_only_tags = [dict(name='div',attrs={'class':'article clearfix'})]
keep_only_tags = [dict(attrs={'class':'uitstekendekeus'})]
remove_tags = [
dict(name=['meta','base','link','object','embed'])
,dict(attrs={'class':['reclamespace','tags-and-sharing']})
]
remove_attributes=['lang']
feeds = [
(u'Voorpagina' , u'http://feeds.feedburner.com/NRCHandelsbladVoorpagina' )
,(u'Binnenland' , u'http://feeds.feedburner.com/NRCHandelsbladBinnenland' )
,(u'Buitenland' , u'http://feeds.feedburner.com/NRCHandelsbladBuitenland' )
,(u'Economie' , u'http://feeds.feedburner.com/NRCHandelsbladEconomie' )
,(u'Kunst & Film' , u'http://feeds.feedburner.com/nrc/NRCHandelsbladKunstEnFilm')
,(u'Sport' , u'http://feeds.feedburner.com/NRCHandelsbladSport' )
,(u'Wetenschap ' , u'http://www.nrc.nl/rss/wetenschap' )
(u'Voor nieuws', u'http://www.nrc.nl/nieuws/categorie/nieuws/rss.php' )
,(u'Binnenland' , u'http://www.nrc.nl/nieuws/categorie/binnenland/rss.php' )
,(u'Buitenland' , u'http://www.nrc.nl/nieuws/categorie/buitenland/rss.php' )
,(u'Economie' , u'http://www.nrc.nl/nieuws/categorie/economie/rss.php' )
,(u'Cultuur' , u'http://www.nrc.nl/nieuws/categorie/cultuur/rss.php' )
,(u'Sport' , u'http://www.nrc.nl/nieuws/categorie/sport/rss.php' )
,(u'Wetenschap ', u'http://www.nrc.nl/nieuws/categorie/wetenschap-nieuws/rss.php')
]
def print_version(self, url):
return url + '?service=Print'
def preprocess_html(self, soup):
return self.adeify_images(soup)
for item in soup.findAll(style=True):
del item['style']
for item in soup.findAll('a'):
limg = item.find('img')
if item.string is not None:
str = item.string
item.replaceWith(str)
else:
if limg:
item.name = 'div'
atritems =['href','target','rel']
for atit in atritems:
if item.has_key(atit):
del item[atit]
else:
str = self.tag_to_string(item)
item.replaceWith(str)
for item in soup.findAll('img'):
if not item.has_key('alt'):
item['alt'] = 'image'
return soup

View File

@ -0,0 +1,120 @@
import re
import urllib2
from calibre.web.feeds.news import BasicNewsRecipe
from calibre.ebooks.BeautifulSoup import BeautifulSoup, SoupStrainer
class Ebert(BasicNewsRecipe):
title = 'Roger Ebert'
__author__ = 'Shane Erstad'
description = 'Roger Ebert Movie Reviews'
publisher = 'Chicago Sun Times'
category = 'movies'
oldest_article = 8
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
encoding = 'utf-8'
masthead_url = 'http://rogerebert.suntimes.com/graphics/global/roger.jpg'
language = 'en'
remove_empty_feeds = False
PREFIX = 'http://rogerebert.suntimes.com'
patternReviews = r'<span class="*?movietitle"*?>(.*?)</span>.*?<div class="*?headline"*?>(.*?)</div>(.*?)</div>'
patternCommentary = r'<div class="*?headline"*?>.*?(<a href="/apps/pbcs.dll/article\?AID=.*?COMMENTARY.*?" id="ltred">.*?</a>).*?<div class="blurb clear">(.*?)</div>'
patternPeople = r'<div class="*?headline"*?>.*?(<a href="/apps/pbcs.dll/article\?AID=.*?PEOPLE.*?" id="ltred">.*?</a>).*?<div class="blurb clear">(.*?)</div>'
patternGlossary = r'<div class="*?headline"*?>.*?(<a href="/apps/pbcs.dll/article\?AID=.*?GLOSSARY.*?" id="ltred">.*?</a>).*?<div class="blurb clear">(.*?)</div>'
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
, 'linearize_tables' : True
}
feeds = [
(u'Reviews' , u'http://rogerebert.suntimes.com/apps/pbcs.dll/section?category=reviews' )
,(u'Commentary' , u'http://rogerebert.suntimes.com/apps/pbcs.dll/section?category=COMMENTARY')
,(u'Great Movies' , u'http://rogerebert.suntimes.com/apps/pbcs.dll/section?category=REVIEWS08')
,(u'People' , u'http://rogerebert.suntimes.com/apps/pbcs.dll/section?category=PEOPLE')
,(u'Glossary' , u'http://rogerebert.suntimes.com/apps/pbcs.dll/section?category=GLOSSARY')
]
preprocess_regexps = [
(re.compile(r'<font.*?>.*?This is a printer friendly.*?</font>.*?<hr>', re.DOTALL|re.IGNORECASE),
lambda m: '')
]
def print_version(self, url):
return url + '&template=printart'
def parse_index(self):
totalfeeds = []
lfeeds = self.get_feeds()
for feedobj in lfeeds:
feedtitle, feedurl = feedobj
self.log('\tFeedurl: ', feedurl)
self.report_progress(0, _('Fetching feed')+' %s...'%(feedtitle if feedtitle else feedurl))
articles = []
page = urllib2.urlopen(feedurl).read()
if feedtitle == 'Reviews' or feedtitle == 'Great Movies':
pattern = self.patternReviews
elif feedtitle == 'Commentary':
pattern = self.patternCommentary
elif feedtitle == 'People':
pattern = self.patternPeople
elif feedtitle == 'Glossary':
pattern = self.patternGlossary
regex = re.compile(pattern, re.IGNORECASE|re.DOTALL)
for match in regex.finditer(page):
if feedtitle == 'Reviews' or feedtitle == 'Great Movies':
movietitle = match.group(1)
thislink = match.group(2)
description = match.group(3)
elif feedtitle == 'Commentary' or feedtitle == 'People' or feedtitle == 'Glossary':
thislink = match.group(1)
description = match.group(2)
self.log(thislink)
for link in BeautifulSoup(thislink, parseOnlyThese=SoupStrainer('a')):
thisurl = self.PREFIX + link['href']
thislinktext = self.tag_to_string(link)
if feedtitle == 'Reviews' or feedtitle == 'Great Movies':
thistitle = movietitle
elif feedtitle == 'Commentary' or feedtitle == 'People' or feedtitle == 'Glossary':
thistitle = thislinktext
if thistitle == '':
thistitle = 'Ebert Journal Post'
"""
pattern2 = r'AID=\/(.*?)\/'
reg2 = re.compile(pattern2, re.IGNORECASE|re.DOTALL)
match2 = reg2.search(thisurl)
date = match2.group(1)
c = time.strptime(match2.group(1),"%Y%m%d")
date=time.strftime("%a, %b %d, %Y", c)
self.log(date)
"""
articles.append({
'title' :thistitle
,'date' :''
,'url' :thisurl
,'description':description
})
totalfeeds.append((feedtitle, articles))
return totalfeeds

View File

@ -0,0 +1,39 @@
import re
from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1289939440(BasicNewsRecipe):
__author__ = 'FunThomas'
title = u'Root.cz'
description = u'Zprávičky a články z Root.cz'
publisher = u'Internet Info, s.r.o'
oldest_article = 2 #max stari clanku ve dnech
max_articles_per_feed = 50 #max pocet clanku na feed
feeds = [
(u'Články', u'http://www.root.cz/rss/clanky/'),
(u'Zprávičky', u'http://www.root.cz/rss/zpravicky/')
]
publication_type = u'magazine'
language = u'cs'
no_stylesheets = True
remove_javascript = True
cover_url = u'http://i.iinfo.cz/urs/logo-root-bila-oranzova-cerna-111089527143118.gif'
remove_attributes = ['width','height','href'] #,'href'
keep_only_tags = [
dict(name='h1'),
dict(name='a',attrs={'class':'author'}),
dict(name='p', attrs={'class':'intro'}),
dict(name='div',attrs={'class':'urs'})
]
preprocess_regexps = [
(re.compile(u'<p class="perex[^"]*">[^<]*<img[^>]*>', re.DOTALL),lambda match: '<p class="intro">'),
(re.compile(u'<h3><a name="tucnak">Tričko tučňák.*</body>', re.DOTALL),lambda match: '<!--deleted-->')
]
extra_css = '''
h1 {font-size:130%; font-weight:bold}
h3 {font-size:111%; font-weight:bold}
'''

View File

@ -27,12 +27,34 @@ class cdnet(BasicNewsRecipe):
dict(id='header'),
dict(id='search'),
dict(id='nav'),
dict(id='blog-author-info'),
dict(id='post-tags'),
dict(id='bio-naraine'),
dict(id='bio-kennedy'),
dict(id='author-short-disclosure-kennedy'),
dict(id=''),
dict(name='div', attrs={'class':'banner'}),
dict(name='div', attrs={'class':'int'}),
dict(name='div', attrs={'class':'talkback clear space-2'}),
dict(name='div', attrs={'class':'content-1 clear'}),
dict(name='div', attrs={'class':'space-2'}),
dict(name='div', attrs={'class':'space-3'}),
dict(name='div', attrs={'class':'thumb-2 left'}),
dict(name='div', attrs={'class':'hotspot'}),
dict(name='div', attrs={'class':'hed hed-1 space-1'}),
dict(name='div', attrs={'class':'view-1 clear content-3 space-2'}),
dict(name='div', attrs={'class':'hed hed-1 space-1'}),
dict(name='div', attrs={'class':'hed hed-1'}),
dict(name='div', attrs={'class':'post-header'}),
dict(name='div', attrs={'class':'lvl-nav clear'}),
dict(name='div', attrs={'class':'t-share-overlay overlay-pop contain-overlay-4'}),
dict(name='p', attrs={'class':'tags'}),
dict(name='span', attrs={'class':'follow'}),
dict(name='span', attrs={'class':'int'}),
dict(name='h4', attrs={'class':'h s-4'}),
dict(name='a', attrs={'href':'http://www.twitter.com/ryanaraine'}),
dict(name='div', attrs={'class':'special1'})]
remove_tags_after = [dict(name='div', attrs={'class':'bloggerDesc clear'})]
remove_tags_after = [dict(name='div', attrs={'class':'clear'})]
feeds = [ ('zdnet', 'http://feeds.feedburner.com/zdnet/security') ]
@ -43,3 +65,4 @@ class cdnet(BasicNewsRecipe):
return soup

View File

@ -43,8 +43,9 @@ class Stage3(Command):
description = 'Stage 3 of the publish process'
sub_commands = ['upload_user_manual', 'upload_demo', 'sdist',
'upload_to_google_code', 'tag_release', 'upload_to_server',
'upload_to_sourceforge', 'upload_to_mobileread',
'upload_to_google_code', 'upload_to_sourceforge',
'tag_release', 'upload_to_server',
'upload_to_mobileread',
]
class Stage4(Command):

View File

@ -2,7 +2,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
__appname__ = 'calibre'
__version__ = '0.7.40'
__version__ = '0.7.42'
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
import re

View File

@ -54,7 +54,7 @@ class ANDROID(USBMS):
0x1004 : { 0x61cc : [0x100] },
# Archos
0x0e79 : { 0x1419: [0x0216], 0x1420 : [0x0216]},
0x0e79 : { 0x1419: [0x0216], 0x1420 : [0x0216], 0x1422 : [0x0216]},
}
EBOOK_DIR_MAIN = ['eBooks/import', 'wordplayer/calibretransfer', 'Books']
@ -70,7 +70,7 @@ class ANDROID(USBMS):
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897',
'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID',
'SCH-I500_CARD', 'SPH-D700_CARD', 'MB810', 'GT-P1000', 'DESIRE',
'SGH-T849', '_MB300', 'A70S', 'S_ANDROID', 'A101IT']
'SGH-T849', '_MB300', 'A70S', 'S_ANDROID', 'A101IT', 'A70H']
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
'A70S', 'A101IT']

View File

@ -25,13 +25,15 @@ class HeuristicProcessor(object):
self.chapters_with_title = 0
self.blanks_deleted = False
self.linereg = re.compile('(?<=<p).*?(?=</p>)', re.IGNORECASE|re.DOTALL)
self.blankreg = re.compile(r'\s*(?P<openline><p(?!\sid=\"softbreak\")[^>]*>)\s*(?P<closeline></p>)', re.IGNORECASE)
self.blankreg = re.compile(r'\s*(?P<openline><p(?!\sclass=\"softbreak\")[^>]*>)\s*(?P<closeline></p>)', re.IGNORECASE)
self.softbreak = re.compile(r'\s*(?P<openline><p(?=\sclass=\"softbreak\")[^>]*>)\s*(?P<closeline></p>)', re.IGNORECASE)
self.multi_blank = re.compile(r'(\s*<p[^>]*>\s*</p>){2,}', re.IGNORECASE)
def is_pdftohtml(self, src):
return '<!-- created by calibre\'s pdftohtml -->' in src[:1000]
def chapter_head(self, match):
from calibre.utils.html2text import html2text
chap = match.group('chap')
title = match.group('title')
if not title:
@ -40,10 +42,12 @@ class HeuristicProcessor(object):
" chapters. - " + unicode(chap))
return '<h2>'+chap+'</h2>\n'
else:
txt_chap = html2text(chap)
txt_title = html2text(title)
self.html_preprocess_sections = self.html_preprocess_sections + 1
self.log.debug("marked " + unicode(self.html_preprocess_sections) +
" chapters & titles. - " + unicode(chap) + ", " + unicode(title))
return '<h2>'+chap+'</h2>\n<h3>'+title+'</h3>\n'
return '<h2 title="'+txt_chap+', '+txt_title+'">'+chap+'</h2>\n<h3 class="sigilNotInTOC">'+title+'</h3>\n'
def chapter_break(self, match):
chap = match.group('section')
@ -137,17 +141,17 @@ class HeuristicProcessor(object):
]
ITALICIZE_STYLE_PATS = [
r'(?msu)(?<=\s)_(?P<words>\S[^_]{0,40}?\S)?_(?=\s)',
r'(?msu)(?<=\s)/(?P<words>\S[^/]{0,40}?\S)?/(?=\s)',
r'(?msu)(?<=\s)~~(?P<words>\S[^~]{0,40}?\S)?~~(?=\s)',
r'(?msu)(?<=\s)\*(?P<words>\S[^\*]{0,40}?\S)?\*(?=\s)',
r'(?msu)(?<=\s)~(?P<words>\S[^~]{0,40}?\S)?~(?=\s)',
r'(?msu)(?<=\s)_/(?P<words>\S[^/_]{0,40}?\S)?/_(?=\s)',
r'(?msu)(?<=\s)_\*(?P<words>\S[^\*_]{0,40}?\S)?\*_(?=\s)',
r'(?msu)(?<=\s)\*/(?P<words>\S[^/\*]{0,40}?\S)?/\*(?=\s)',
r'(?msu)(?<=\s)_\*/(?P<words>\S[^\*_]{0,40}?\S)?/\*_(?=\s)',
r'(?msu)(?<=\s)/:(?P<words>\S[^:/]{0,40}?\S)?:/(?=\s)',
r'(?msu)(?<=\s)\|:(?P<words>\S[^:\|]{0,40}?\S)?:\|(?=\s)',
r'(?msu)(?<=\s)_(?P<words>\S[^_]{0,40}?\S)?_(?=[\s\.,\!\?])',
r'(?msu)(?<=\s)/(?P<words>\S[^/]{0,40}?\S)?/(?=[\s\.,\!\?])',
r'(?msu)(?<=\s)~~(?P<words>\S[^~]{0,40}?\S)?~~(?=[\s\.,\!\?])',
r'(?msu)(?<=\s)\*(?P<words>\S[^\*]{0,40}?\S)?\*(?=[\s\.,\!\?])',
r'(?msu)(?<=\s)~(?P<words>\S[^~]{0,40}?\S)?~(?=[\s\.,\!\?])',
r'(?msu)(?<=\s)_/(?P<words>\S[^/_]{0,40}?\S)?/_(?=[\s\.,\!\?])',
r'(?msu)(?<=\s)_\*(?P<words>\S[^\*_]{0,40}?\S)?\*_(?=[\s\.,\!\?])',
r'(?msu)(?<=\s)\*/(?P<words>\S[^/\*]{0,40}?\S)?/\*(?=[\s\.,\!\?])',
r'(?msu)(?<=\s)_\*/(?P<words>\S[^\*_]{0,40}?\S)?/\*_(?=[\s\.,\!\?])',
r'(?msu)(?<=\s)/:(?P<words>\S[^:/]{0,40}?\S)?:/(?=[\s\.,\!\?])',
r'(?msu)(?<=\s)\|:(?P<words>\S[^:\|]{0,40}?\S)?:\|(?=[\s\.,\!\?])',
]
for word in ITALICIZE_WORDS:
@ -203,8 +207,8 @@ class HeuristicProcessor(object):
blank_lines = ""
opt_title_open = "("
opt_title_close = ")?"
n_lookahead_open = "\s+(?!"
n_lookahead_close = ")"
n_lookahead_open = "(?!\s*"
n_lookahead_close = ")\s*"
default_title = r"(<[ibu][^>]*>)?\s{0,3}(?!Chapter)([\w\:\'\"-]+\s{0,3}){1,5}?(</[ibu][^>]*>)?(?=<)"
simple_title = r"(<[ibu][^>]*>)?\s{0,3}(?!(Chapter|\s+<)).{0,65}?(</[ibu][^>]*>)?(?=<)"
@ -215,7 +219,7 @@ class HeuristicProcessor(object):
[r"[^'\"]?(Introduction|Synopsis|Acknowledgements|Epilogue|CHAPTER|Kapitel|Volume\b|Prologue|Book\b|Part\b|Dedication|Preface)\s*([\d\w-]+\:?\'?\s*){0,5}", True, True, True, False, "Searching for common section headings", 'common'],
[r"[^'\"]?(CHAPTER|Kapitel)\s*([\dA-Z\-\'\"\?!#,]+\s*){0,7}\s*", True, True, True, False, "Searching for most common chapter headings", 'chapter'], # Highest frequency headings which include titles
[r"<b[^>]*>\s*(<span[^>]*>)?\s*(?!([*#•=]+\s*)+)(\s*(?=[\d.\w#\-*\s]+<)([\d.\w#-*]+\s*){1,5}\s*)(?!\.)(</span>)?\s*</b>", True, True, True, False, "Searching for emphasized lines", 'emphasized'], # Emphasized lines
[r"[^'\"]?(\d+(\.|:))\s*([\dA-Z\-\'\"#,]+\s*){0,7}\s*", True, True, True, False, "Searching for numeric chapter headings", 'numeric'], # Numeric Chapters
[r"[^'\"]?(\d+(\.|:))\s*([\w\-\'\"#,]+\s*){0,7}\s*", True, True, True, False, "Searching for numeric chapter headings", 'numeric'], # Numeric Chapters
[r"([A-Z]\s+){3,}\s*([\d\w-]+\s*){0,3}\s*", True, True, True, False, "Searching for letter spaced headings", 'letter_spaced'], # Spaced Lettering
[r"[^'\"]?(\d+\.?\s+([\d\w-]+\:?\'?-?\s?){0,5})\s*", True, True, True, False, "Searching for numeric chapters with titles", 'numeric_title'], # Numeric Titles
[r"[^'\"]?(\d+)\s*([\dA-Z\-\'\"\?!#,]+\s*){0,7}\s*", True, True, True, False, "Searching for simple numeric headings", 'plain_number'], # Numeric Chapters, no dot or colon
@ -275,7 +279,7 @@ class HeuristicProcessor(object):
self.log.debug(unicode(type_name)+" had "+unicode(hits)+" hits - "+unicode(self.chapters_no_title)+" chapters with no title, "+unicode(self.chapters_with_title)+" chapters with titles, "+unicode(float(self.chapters_with_title) / float(hits))+" percent. ")
if type_name == 'common':
analysis_result.append([chapter_type, n_lookahead_req, strict_title, ignorecase, title_req, log_message, type_name])
elif self.min_chapters <= hits < max_chapters:
elif self.min_chapters <= hits < max_chapters or self.min_chapters < 3 > hits:
analysis_result.append([chapter_type, n_lookahead_req, strict_title, ignorecase, title_req, log_message, type_name])
break
else:
@ -367,6 +371,8 @@ class HeuristicProcessor(object):
html = re.sub(ur'\s*<o:p>\s*</o:p>', ' ', html)
# Delete microsoft 'smart' tags
html = re.sub('(?i)</?st1:\w+>', '', html)
# Delete self closing paragraph tags
html = re.sub('<p\s?/>', '', html)
# Get rid of empty span, bold, font, em, & italics tags
html = re.sub(r"\s*<span[^>]*>\s*(<span[^>]*>\s*</span>){0,2}\s*</span>\s*", " ", html)
html = re.sub(r"\s*<(font|[ibu]|em)[^>]*>\s*(<(font|[ibu]|em)[^>]*>\s*</(font|[ibu]|em)>\s*){0,2}\s*</(font|[ibu]|em)>", " ", html)
@ -467,7 +473,7 @@ class HeuristicProcessor(object):
if blanks_between_paragraphs and getattr(self.extra_opts, 'delete_blank_paragraphs', False):
self.log.debug("deleting blank lines")
self.blanks_deleted = True
html = self.multi_blank.sub('\n<p id="softbreak" style="margin-top:1.5em; margin-bottom:1.5em"> </p>', html)
html = self.multi_blank.sub('\n<p class="softbreak" style="margin-top:1.5em; margin-bottom:1.5em"> </p>', html)
html = self.blankreg.sub('', html)
# Determine line ending type
@ -522,11 +528,11 @@ class HeuristicProcessor(object):
# Center separator lines
html = re.sub(u'<(?P<outer>p|div)[^>]*>\s*(<(?P<inner1>font|span|[ibu])[^>]*>)?\s*(<(?P<inner2>font|span|[ibu])[^>]*>)?\s*(<(?P<inner3>font|span|[ibu])[^>]*>)?\s*(?P<break>([*#•=✦]+\s*)+)\s*(</(?P=inner3)>)?\s*(</(?P=inner2)>)?\s*(</(?P=inner1)>)?\s*</(?P=outer)>', '<p style="text-align:center; margin-top:1.25em; margin-bottom:1.25em">' + '\g<break>' + '</p>', html)
if not self.blanks_deleted:
html = self.multi_blank.sub('\n<p id="softbreak" style="margin-top:1.5em; margin-bottom:1.5em"> </p>', html)
html = re.sub('<p\s+id="softbreak"[^>]*>\s*</p>', '<div id="softbreak" style="margin-left: 45%; margin-right: 45%; margin-top:1.5em; margin-bottom:1.5em"><hr style="height: 3px; background:#505050" /></div>', html)
html = self.multi_blank.sub('\n<p class="softbreak" style="margin-top:1.5em; margin-bottom:1.5em"> </p>', html)
html = re.sub('<p\s+class="softbreak"[^>]*>\s*</p>', '<div id="softbreak" style="margin-left: 45%; margin-right: 45%; margin-top:1.5em; margin-bottom:1.5em"><hr style="height: 3px; background:#505050" /></div>', html)
if self.deleted_nbsps:
# put back non-breaking spaces in empty paragraphs to preserve original formatting
html = self.blankreg.sub('\n'+r'\g<openline>'+u'\u00a0'+r'\g<closeline>', html)
html = self.softbreak.sub('\n'+r'\g<openline>'+u'\u00a0'+r'\g<closeline>', html)
return html

View File

@ -373,7 +373,7 @@ def search(title=None, author=None, publisher=None, isbn=None, isbndb_key=None,
r.pubdate = pubdate
def fix_case(x):
if x and x.isupper():
if x:
x = titlecase(x)
return x

View File

@ -1541,7 +1541,10 @@ class MobiWriter(object):
exth.write(data)
nrecs += 1
if term == 'rights' :
try:
rights = unicode(oeb.metadata.rights[0]).encode('utf-8')
except:
rights = 'Unknown'
exth.write(pack('>II', EXTH_CODES['rights'], len(rights) + 8))
exth.write(rights)

View File

@ -221,7 +221,10 @@ def rewrite_links(root, link_repl_func, resolve_base_href=False):
el.text):
stylesheet = parseString(el.text)
replaceUrls(stylesheet, link_repl_func)
el.text = '\n'+stylesheet.cssText + '\n'
repl = stylesheet.cssText
if isbytestring(repl):
repl = repl.decode('utf-8')
el.text = '\n'+ repl + '\n'
if 'style' in el.attrib:
text = el.attrib['style']
@ -234,8 +237,11 @@ def rewrite_links(root, link_repl_func, resolve_base_href=False):
set_property(item)
elif v.CSS_PRIMITIVE_VALUE == v.cssValueType:
set_property(v)
el.attrib['style'] = stext.cssText.replace('\n', ' ').replace('\r',
repl = stext.cssText.replace('\n', ' ').replace('\r',
' ')
if isbytestring(repl):
repl = repl.decode('utf-8')
el.attrib['style'] = repl

View File

@ -84,13 +84,9 @@ def meta_info_to_oeb_metadata(mi, m, log, override_input_metadata=False):
if not mi.is_null('rights'):
m.clear('rights')
m.add('rights', mi.rights)
elif override_input_metadata:
m.clear('rights')
if not mi.is_null('publication_type'):
m.clear('publication_type')
m.add('publication_type', mi.publication_type)
elif override_input_metadata:
m.clear('publication_type')
if not m.timestamp:
m.add('timestamp', isoformat(now()))

View File

@ -385,13 +385,27 @@ class ChooseLibraryAction(InterfaceAction):
prefs['library_path'] = loc
#from calibre.utils.mem import memory
#import weakref, gc
#ref = weakref.ref(self.gui.library_view.model().db)
#before = memory()/1024**2
#import weakref
#from PyQt4.Qt import QTimer
#self.dbref = weakref.ref(self.gui.library_view.model().db)
#self.before_mem = memory()/1024**2
self.gui.library_moved(loc)
#print gc.get_referrers(ref)[0]
#for i in xrange(3): gc.collect()
#print 'leaked:', memory()/1024**2 - before
#QTimer.singleShot(1000, self.debug_leak)
def debug_leak(self):
import gc
from calibre.utils.mem import memory
ref = self.dbref
for i in xrange(3): gc.collect()
if ref() is not None:
print 11111, ref()
for r in gc.get_referrers(ref())[:10]:
print r
print
print 'before:', self.before_mem
print 'after:', memory()/1024**2
self.dbref = self.before_mem = None
def qs_requested(self, idx, *args):
self.switch_requested(self.qs_locations[idx])

View File

@ -12,7 +12,8 @@ from lxml.html import soupparser
from PyQt4.Qt import QApplication, QFontInfo, QSize, QWidget, QPlainTextEdit, \
QToolBar, QVBoxLayout, QAction, QIcon, Qt, QTabWidget, QUrl, \
QSyntaxHighlighter, QColor, QChar, QColorDialog, QMenu, QInputDialog
QSyntaxHighlighter, QColor, QChar, QColorDialog, QMenu, QInputDialog, \
QHBoxLayout
from PyQt4.QtWebKit import QWebView, QWebPage
from calibre.ebooks.chardet import xml_to_unicode
@ -488,7 +489,7 @@ class Highlighter(QSyntaxHighlighter):
class Editor(QWidget): # {{{
def __init__(self, parent=None):
def __init__(self, parent=None, one_line_toolbar=False):
QWidget.__init__(self, parent)
self.toolbar1 = QToolBar(self)
self.toolbar2 = QToolBar(self)
@ -508,9 +509,14 @@ class Editor(QWidget): # {{{
self.wyswyg.layout = l = QVBoxLayout(self.wyswyg)
self.setLayout(self._layout)
l.setContentsMargins(0, 0, 0, 0)
l.addWidget(self.toolbar1)
l.addWidget(self.toolbar2)
l.addWidget(self.toolbar3)
if one_line_toolbar:
tb = QHBoxLayout()
l.addLayout(tb)
else:
tb = l
tb.addWidget(self.toolbar1)
tb.addWidget(self.toolbar2)
tb.addWidget(self.toolbar3)
l.addWidget(self.editor)
self._layout.addWidget(self.tabs)
self.tabs.addTab(self.wyswyg, _('Normal view'))

View File

@ -6,7 +6,7 @@ __docformat__ = 'restructuredtext en'
import re
from PyQt4.QtCore import SIGNAL, Qt
from PyQt4.QtCore import SIGNAL, Qt, pyqtSignal
from PyQt4.QtGui import QDialog, QWidget, QDialogButtonBox, \
QBrush, QTextCursor, QTextEdit
@ -19,8 +19,8 @@ from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
class RegexBuilder(QDialog, Ui_RegexBuilder):
def __init__(self, db, book_id, regex, *args):
QDialog.__init__(self, *args)
def __init__(self, db, book_id, regex, doc=None, parent=None):
QDialog.__init__(self, parent)
self.setupUi(self)
self.regex.setText(regex)
@ -28,13 +28,21 @@ class RegexBuilder(QDialog, Ui_RegexBuilder):
if not db or not book_id:
self.button_box.addButton(QDialogButtonBox.Open)
elif not self.select_format(db, book_id):
elif not doc and not self.select_format(db, book_id):
self.cancelled = True
return
if doc:
self.preview.setPlainText(doc)
self.cancelled = False
self.connect(self.button_box, SIGNAL('clicked(QAbstractButton*)'), self.button_clicked)
self.connect(self.regex, SIGNAL('textChanged(QString)'), self.regex_valid)
self.connect(self.test, SIGNAL('clicked()'), self.do_test)
self.connect(self.previous, SIGNAL('clicked()'), self.goto_previous)
self.connect(self.next, SIGNAL('clicked()'), self.goto_next)
self.match_locs = []
def regex_valid(self):
regex = unicode(self.regex.text())
@ -42,17 +50,23 @@ class RegexBuilder(QDialog, Ui_RegexBuilder):
try:
re.compile(regex)
self.regex.setStyleSheet('QLineEdit { color: black; background-color: rgba(0,255,0,20%); }')
return True
except:
self.regex.setStyleSheet('QLineEdit { color: black; background-color: rgb(255,0,0,20%); }')
return False
else:
self.regex.setStyleSheet('QLineEdit { color: black; background-color: white; }')
self.preview.setExtraSelections([])
self.match_locs = []
self.next.setEnabled(False)
self.previous.setEnabled(False)
self.occurrences.setText('0')
return False
return True
def do_test(self):
selections = []
self.match_locs = []
if self.regex_valid():
text = unicode(self.preview.toPlainText())
regex = unicode(self.regex.text())
@ -66,9 +80,43 @@ class RegexBuilder(QDialog, Ui_RegexBuilder):
es.cursor.setPosition(match.start(), QTextCursor.MoveAnchor)
es.cursor.setPosition(match.end(), QTextCursor.KeepAnchor)
selections.append(es)
self.match_locs.append((match.start(), match.end()))
except:
pass
self.preview.setExtraSelections(selections)
if self.match_locs:
self.next.setEnabled(True)
self.previous.setEnabled(True)
self.occurrences.setText(str(len(self.match_locs)))
def goto_previous(self):
pos = self.preview.textCursor().position()
if self.match_locs:
match_loc = len(self.match_locs) - 1
for i in xrange(len(self.match_locs) - 1, -1, -1):
loc = self.match_locs[i][1]
if pos > loc:
match_loc = i
break
self.goto_loc(self.match_locs[match_loc][1], operation=QTextCursor.Left, n=self.match_locs[match_loc][1] - self.match_locs[match_loc][0])
def goto_next(self):
pos = self.preview.textCursor().position()
if self.match_locs:
match_loc = 0
for i in xrange(len(self.match_locs)):
loc = self.match_locs[i][0]
if pos < loc:
match_loc = i
break
self.goto_loc(self.match_locs[match_loc][0], n=self.match_locs[match_loc][1] - self.match_locs[match_loc][0])
def goto_loc(self, loc, operation=QTextCursor.Right, mode=QTextCursor.KeepAnchor, n=0):
cursor = QTextCursor(self.preview.document())
cursor.setPosition(loc)
if n:
cursor.movePosition(operation, mode, n)
self.preview.setTextCursor(cursor)
def select_format(self, db, book_id):
format = None
@ -109,24 +157,41 @@ class RegexBuilder(QDialog, Ui_RegexBuilder):
if button == self.button_box.button(QDialogButtonBox.Ok):
self.accept()
def doc(self):
return unicode(self.preview.toPlainText())
class RegexEdit(QWidget, Ui_Edit):
doc_update = pyqtSignal(unicode)
def __init__(self, parent=None):
QWidget.__init__(self, parent)
self.setupUi(self)
self.book_id = None
self.db = None
self.doc_cache = None
self.connect(self.button, SIGNAL('clicked()'), self.builder)
def builder(self):
bld = RegexBuilder(self.db, self.book_id, self.edit.text(), self)
bld = RegexBuilder(self.db, self.book_id, self.edit.text(), self.doc_cache, self)
if bld.cancelled:
return
if not self.doc_cache:
self.doc_cache = bld.doc()
self.doc_update.emit(self.doc_cache)
if bld.exec_() == bld.Accepted:
self.edit.setText(bld.regex.text())
def doc(self):
return self.doc_cache
def setObjectName(self, *args):
QWidget.setObjectName(self, *args)
if hasattr(self, 'edit'):
self.edit.initialize('regex_edit_'+unicode(self.objectName()))
def set_msg(self, msg):
self.msg.setText(msg)
@ -136,8 +201,11 @@ class RegexEdit(QWidget, Ui_Edit):
def set_db(self, db):
self.db = db
def set_doc(self, doc):
self.doc_cache = doc
def break_cycles(self):
self.db = None
self.db = self.doc_cache = None
@property
def text(self):

View File

@ -6,15 +6,102 @@
<rect>
<x>0</x>
<y>0</y>
<width>662</width>
<height>505</height>
<width>580</width>
<height>503</height>
</rect>
</property>
<property name="windowTitle">
<string>Regex Builder</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="0" colspan="5">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Regex:</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="regex"/>
</item>
<item>
<widget class="QPushButton" name="test">
<property name="text">
<string>Test</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Occurrences:</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="occurrences">
<property name="text">
<string>0</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>298</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Goto:</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="previous">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>&amp;Previous</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="next">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>&amp;Next</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Preview</string>
@ -36,7 +123,22 @@
</layout>
</widget>
</item>
<item row="2" column="4">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>328</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="button_box">
<property name="orientation">
<enum>Qt::Horizontal</enum>
@ -46,25 +148,6 @@
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Regex:</string>
</property>
</widget>
</item>
<item row="0" column="1" colspan="4">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="regex"/>
</item>
<item>
<widget class="QPushButton" name="test">
<property name="text">
<string>Test</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>

View File

@ -35,13 +35,26 @@ class SearchAndReplaceWidget(Widget, Ui_Form):
self.opt_sr3_search.set_book_id(book_id)
self.opt_sr3_search.set_db(db)
self.opt_sr1_search.doc_update.connect(self.update_doc)
self.opt_sr2_search.doc_update.connect(self.update_doc)
self.opt_sr3_search.doc_update.connect(self.update_doc)
def break_cycles(self):
Widget.break_cycles(self)
self.opt_sr1_search.doc_update.disconnect()
self.opt_sr2_search.doc_update.disconnect()
self.opt_sr3_search.doc_update.disconnect()
self.opt_sr1_search.break_cycles()
self.opt_sr2_search.break_cycles()
self.opt_sr3_search.break_cycles()
def update_doc(self, doc):
self.opt_sr1_search.set_doc(doc)
self.opt_sr2_search.set_doc(doc)
self.opt_sr3_search.set_doc(doc)
def pre_commit_check(self):
for x in ('sr1_search', 'sr2_search', 'sr3_search'):
x = getattr(self, 'opt_'+x)

View File

@ -13,7 +13,7 @@ from calibre.customize.ui import available_input_formats, available_output_forma
device_plugins
from calibre.devices.interface import DevicePlugin
from calibre.devices.errors import UserFeedback, OpenFeedback
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
from calibre.gui2.dialogs.choose_format_device import ChooseFormatDeviceDialog
from calibre.utils.ipc.job import BaseJob
from calibre.devices.scanner import DeviceScanner
from calibre.gui2 import config, error_dialog, Dispatcher, dynamic, \
@ -826,8 +826,24 @@ class DeviceMixin(object): # {{{
fmt = None
if specific:
d = ChooseFormatDialog(self, _('Choose format to send to device'),
self.device_manager.device.settings().format_map)
formats = []
aval_out_formats = available_output_formats()
format_count = {}
for row in rows:
fmts = self.library_view.model().db.formats(row.row())
if fmts:
for f in fmts.split(','):
f = f.lower()
if format_count.has_key(f):
format_count[f] += 1
else:
format_count[f] = 1
for f in self.device_manager.device.settings().format_map:
if f in format_count.keys():
formats.append((f, _('%i of %i Books' % (format_count[f], len(rows))), True if f in aval_out_formats else False))
elif f in aval_out_formats:
formats.append((f, _('0 of %i Books' % len(rows)), True))
d = ChooseFormatDeviceDialog(self, _('Choose format to send to device'), formats)
if d.exec_() != QDialog.Accepted:
return
if d.format():

View File

@ -0,0 +1,53 @@
__license__ = 'GPL v3'
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
from PyQt4.Qt import QDialog, QTreeWidgetItem, QIcon, SIGNAL
from calibre.gui2 import file_icon_provider
from calibre.gui2.dialogs.choose_format_device_ui import Ui_ChooseFormatDeviceDialog
class ChooseFormatDeviceDialog(QDialog, Ui_ChooseFormatDeviceDialog):
def __init__(self, window, msg, formats):
'''
formats is a list of tuples: [(format, exists, convertible)].
format: Lower case format identifier. E.G. mobi
exists: String representing the number of books that
exist in the format.
convertible: True if the format is a convertible format.
formats should be ordered in the device's preferred format ordering.
'''
QDialog.__init__(self, window)
Ui_ChooseFormatDeviceDialog.__init__(self)
self.setupUi(self)
self.connect(self.formats, SIGNAL('activated(QModelIndex)'),
self.activated_slot)
self.msg.setText(msg)
for i, (format, exists, convertible) in enumerate(formats):
t_item = QTreeWidgetItem()
t_item.setIcon(0, file_icon_provider().icon_from_ext(format.lower()))
t_item.setText(0, format.upper())
t_item.setText(1, exists)
if convertible:
t_item.setIcon(2, QIcon(I('ok.png')))
self.formats.addTopLevelItem(t_item)
if i == 0:
self.formats.setCurrentItem(t_item)
t_item.setSelected(True)
self.formats.resizeColumnToContents(2)
self.formats.resizeColumnToContents(1)
self.formats.resizeColumnToContents(0)
self.formats.header().resizeSection(0, self.formats.header().sectionSize(0) * 2)
self._format = None
def activated_slot(self, *args):
self.accept()
def format(self):
return self._format
def accept(self):
self._format = unicode(self.formats.currentItem().text(0))
return QDialog.accept(self)

View File

@ -0,0 +1,111 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ChooseFormatDeviceDialog</class>
<widget class="QDialog" name="ChooseFormatDeviceDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>507</width>
<height>377</height>
</rect>
</property>
<property name="windowTitle">
<string>Choose Format</string>
</property>
<property name="windowIcon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/mimetypes/unknown.png</normaloff>:/images/mimetypes/unknown.png</iconset>
</property>
<layout class="QVBoxLayout">
<item>
<widget class="QLabel" name="msg">
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QTreeWidget" name="formats">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="iconSize">
<size>
<width>64</width>
<height>64</height>
</size>
</property>
<property name="allColumnsShowFocus">
<bool>true</bool>
</property>
<column>
<property name="text">
<string>Format</string>
</property>
</column>
<column>
<property name="text">
<string>Existing</string>
</property>
<property name="textAlignment">
<set>AlignLeft|AlignVCenter</set>
</property>
</column>
<column>
<property name="text">
<string>Convertible</string>
</property>
</column>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../../../../resources/images.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>ChooseFormatDeviceDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>ChooseFormatDeviceDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -6,8 +6,8 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import re, os
from PyQt4.Qt import Qt, QDialog, QGridLayout, QVBoxLayout, QFont, QLabel, \
pyqtSignal, QDialogButtonBox
from PyQt4 import QtGui
pyqtSignal, QDialogButtonBox, QInputDialog, QLineEdit, \
QMessageBox, QDate
from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog
from calibre.gui2.dialogs.tag_editor import TagEditor
@ -15,9 +15,9 @@ from calibre.ebooks.metadata import string_to_authors, authors_to_string
from calibre.ebooks.metadata.book.base import composite_formatter
from calibre.ebooks.metadata.meta import get_metadata
from calibre.gui2.custom_column_widgets import populate_metadata_page
from calibre.gui2 import error_dialog, ResizableDialog, UNDEFINED_QDATE
from calibre.gui2 import error_dialog, ResizableDialog, UNDEFINED_QDATE, gprefs
from calibre.gui2.progress_indicator import ProgressIndicator
from calibre.utils.config import dynamic
from calibre.utils.config import dynamic, JSONConfig
from calibre.utils.titlecase import titlecase
from calibre.utils.icu import sort_key, capitalize
from calibre.utils.config import prefs, tweaks
@ -302,6 +302,7 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
self.pubdate.setSpecialValueText(_('Undefined'))
self.clear_pubdate_button.clicked.connect(self.clear_pubdate)
self.pubdate.dateChanged.connect(self.do_apply_pubdate)
self.adddate.setDate(QDate.currentDate())
self.adddate.setMinimumDate(UNDEFINED_QDATE)
self.adddate.setSpecialValueText(_('Undefined'))
self.clear_adddate_button.clicked.connect(self.clear_adddate)
@ -320,8 +321,15 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
'This operation cannot be canceled or undone'))
self.do_again = False
self.central_widget.setCurrentIndex(tab)
geom = gprefs.get('bulk_metadata_window_geometry', None)
if geom is not None:
self.restoreGeometry(bytes(geom))
self.exec_()
def save_state(self, *args):
gprefs['bulk_metadata_window_geometry'] = \
bytearray(self.saveGeometry())
def do_apply_pubdate(self, *args):
self.apply_pubdate.setChecked(True)
@ -365,16 +373,16 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
offset = 10
self.s_r_number_of_books = min(10, len(self.ids))
for i in range(1,self.s_r_number_of_books+1):
w = QtGui.QLabel(self.tabWidgetPage3)
w = QLabel(self.tabWidgetPage3)
w.setText(_('Book %d:')%i)
self.testgrid.addWidget(w, i+offset, 0, 1, 1)
w = QtGui.QLineEdit(self.tabWidgetPage3)
w = QLineEdit(self.tabWidgetPage3)
w.setReadOnly(True)
name = 'book_%d_text'%i
setattr(self, name, w)
self.book_1_text.setObjectName(name)
self.testgrid.addWidget(w, i+offset, 1, 1, 1)
w = QtGui.QLineEdit(self.tabWidgetPage3)
w = QLineEdit(self.tabWidgetPage3)
w.setReadOnly(True)
name = 'book_%d_result'%i
setattr(self, name, w)
@ -451,6 +459,15 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
self.results_count.valueChanged[int].connect(self.s_r_display_bounds_changed)
self.starting_from.valueChanged[int].connect(self.s_r_display_bounds_changed)
self.save_button.clicked.connect(self.s_r_save_query)
self.remove_button.clicked.connect(self.s_r_remove_query)
self.queries = JSONConfig("search_replace_queries")
self.query_field.addItem("")
self.query_field.addItems(sorted([q for q in self.queries], key=sort_key))
self.query_field.currentIndexChanged[str].connect(self.s_r_query_change)
self.query_field.setCurrentIndex(0)
def s_r_get_field(self, mi, field):
if field:
if field == '{template}':
@ -780,7 +797,12 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
self.series_start_number.setEnabled(False)
self.series_start_number.setValue(1)
def reject(self):
self.save_state()
ResizableDialog.reject(self)
def accept(self):
self.save_state()
if len(self.ids) < 1:
return QDialog.accept(self)
@ -862,3 +884,117 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
def series_changed(self, *args):
self.write_series = True
def s_r_remove_query(self, *args):
if self.query_field.currentIndex() == 0:
return
ret = QMessageBox.question(self, _("Delete saved search/replace"),
_("The selected saved search/replace will be deleted. "
"Are you sure?"),
QMessageBox.Ok, QMessageBox.Cancel)
if ret == QMessageBox.Cancel:
return
item_id = self.query_field.currentIndex()
item_name = unicode(self.query_field.currentText())
self.query_field.blockSignals(True)
self.query_field.removeItem(item_id)
self.query_field.blockSignals(False)
self.query_field.setCurrentIndex(0)
if item_name in self.queries.keys():
del(self.queries[item_name])
self.queries.commit()
def s_r_save_query(self, *args):
name, ok = QInputDialog.getText(self, _('Save search/replace'),
_('Search/replace name:'))
if not ok:
return
new = True
name = unicode(name)
if name in self.queries.keys():
ret = QMessageBox.question(self, _("Save search/replace"),
_("That saved search/replace already exists and will be overwritten. "
"Are you sure?"),
QMessageBox.Ok, QMessageBox.Cancel)
if ret == QMessageBox.Cancel:
return
new = False
query = {}
query['name'] = name
query['search_field'] = unicode(self.search_field.currentText())
query['search_mode'] = unicode(self.search_mode.currentText())
query['s_r_template'] = unicode(self.s_r_template.text())
query['search_for'] = unicode(self.search_for.text())
query['case_sensitive'] = self.case_sensitive.isChecked()
query['replace_with'] = unicode(self.replace_with.text())
query['replace_func'] = unicode(self.replace_func.currentText())
query['destination_field'] = unicode(self.destination_field.currentText())
query['replace_mode'] = unicode(self.replace_mode.currentText())
query['comma_separated'] = self.comma_separated.isChecked()
query['results_count'] = self.results_count.value()
query['starting_from'] = self.starting_from.value()
query['multiple_separator'] = unicode(self.multiple_separator.text())
self.queries[name] = query
self.queries.commit()
if new:
self.query_field.blockSignals(True)
self.query_field.clear()
self.query_field.addItem('')
self.query_field.addItems(sorted([q for q in self.queries], key=sort_key))
self.query_field.blockSignals(False)
self.query_field.setCurrentIndex(self.query_field.findText(name))
def s_r_query_change(self, item_name):
if not item_name:
self.s_r_reset_query_fields()
return
item = self.queries.get(unicode(item_name), None)
if item is None:
self.s_r_reset_query_fields()
return
def set_index(attr, txt):
try:
attr.setCurrentIndex(attr.findText(txt))
except:
attr.setCurrentIndex(0)
set_index(self.search_mode, item['search_mode'])
set_index(self.search_field, item['search_field'])
self.s_r_template.setText(item['s_r_template'])
self.s_r_template_changed() #simulate gain/loss of focus
self.search_for.setText(item['search_for'])
self.case_sensitive.setChecked(item['case_sensitive'])
self.replace_with.setText(item['replace_with'])
set_index(self.replace_func, item['replace_func'])
set_index(self.destination_field, item['destination_field'])
set_index(self.replace_mode, item['replace_mode'])
self.comma_separated.setChecked(item['comma_separated'])
self.results_count.setValue(int(item['results_count']))
self.starting_from.setValue(int(item['starting_from']))
self.multiple_separator.setText(item['multiple_separator'])
def s_r_reset_query_fields(self):
# Don't reset the search mode. The user will probably want to use it
# as it was
self.search_field.setCurrentIndex(0)
self.s_r_template.setText("")
self.search_for.setText("")
self.case_sensitive.setChecked(False)
self.replace_with.setText("")
self.replace_func.setCurrentIndex(0)
self.destination_field.setCurrentIndex(0)
self.replace_mode.setCurrentIndex(0)
self.comma_separated.setChecked(True)
self.results_count.setValue(999)
self.starting_from.setValue(1)
self.multiple_separator.setText(" ::: ")

View File

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>850</width>
<height>650</height>
<width>962</width>
<height>727</height>
</rect>
</property>
<property name="windowTitle">
@ -44,8 +44,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>842</width>
<height>589</height>
<width>954</width>
<height>666</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
@ -574,7 +574,7 @@ Future conversion of these books will use the default settings.</string>
<property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum>
</property>
<item row="1" column="0" colspan="3">
<item row="0" column="0" colspan="4">
<widget class="QLabel" name="s_r_heading">
<property name="wordWrap">
<bool>true</bool>
@ -584,14 +584,91 @@ Future conversion of these books will use the default settings.</string>
</property>
</widget>
</item>
<item row="2" column="0">
<item row="1" column="0">
<widget class="QLabel" name="filler">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="2" column="0" colspan="3">
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="xlabel_22">
<property name="text">
<string>Load searc&amp;h/replace:</string>
</property>
<property name="buddy">
<cstring>search_field</cstring>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="query_field">
<property name="toolTip">
<string>Select saved search/replace to load.</string>
</property>
</widget>
</item>
<item row="3" column="2">
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="save_button">
<property name="toolTip">
<string>Save current search/replace</string>
</property>
<property name="text">
<string>Sa&amp;ve</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="remove_button">
<property name="toolTip">
<string>Delete saved search/replace</string>
</property>
<property name="text">
<string>Delete</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="4" column="0">
<widget class="QLabel" name="xlabel_21">
<property name="text">
<string>Search &amp;field:</string>
@ -601,14 +678,14 @@ Future conversion of these books will use the default settings.</string>
</property>
</widget>
</item>
<item row="3" column="1">
<item row="4" column="1">
<widget class="QComboBox" name="search_field">
<property name="toolTip">
<string>The name of the field that you want to search</string>
</property>
</widget>
</item>
<item row="3" column="2">
<item row="4" column="2">
<layout class="QHBoxLayout" name="HLayout_3">
<item>
<widget class="QLabel" name="xlabel_24">
@ -642,7 +719,7 @@ Future conversion of these books will use the default settings.</string>
</item>
</layout>
</item>
<item row="4" column="0">
<item row="5" column="0">
<widget class="QLabel" name="template_label">
<property name="text">
<string>Te&amp;mplate:</string>
@ -652,7 +729,7 @@ Future conversion of these books will use the default settings.</string>
</property>
</widget>
</item>
<item row="4" column="1">
<item row="5" column="1">
<widget class="HistoryLineEdit" name="s_r_template">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
@ -665,7 +742,7 @@ Future conversion of these books will use the default settings.</string>
</property>
</widget>
</item>
<item row="5" column="0">
<item row="6" column="0">
<widget class="QLabel" name="xlabel_2">
<property name="text">
<string>&amp;Search for:</string>
@ -675,7 +752,7 @@ Future conversion of these books will use the default settings.</string>
</property>
</widget>
</item>
<item row="5" column="1">
<item row="6" column="1">
<widget class="HistoryLineEdit" name="search_for">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
@ -688,7 +765,7 @@ Future conversion of these books will use the default settings.</string>
</property>
</widget>
</item>
<item row="5" column="2">
<item row="6" column="2">
<widget class="QCheckBox" name="case_sensitive">
<property name="toolTip">
<string>Check this box if the search string must match exactly upper and lower case. Uncheck it if case is to be ignored</string>
@ -701,7 +778,7 @@ Future conversion of these books will use the default settings.</string>
</property>
</widget>
</item>
<item row="6" column="0">
<item row="7" column="0">
<widget class="QLabel" name="xlabel_4">
<property name="text">
<string>&amp;Replace with:</string>
@ -711,14 +788,14 @@ Future conversion of these books will use the default settings.</string>
</property>
</widget>
</item>
<item row="6" column="1">
<item row="7" column="1">
<widget class="HistoryLineEdit" name="replace_with">
<property name="toolTip">
<string>The replacement text. The matched search text will be replaced with this string</string>
</property>
</widget>
</item>
<item row="6" column="2">
<item row="7" column="2">
<layout class="QHBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label_41">
@ -753,7 +830,7 @@ field is processed. In regular expression mode, only the matched text is process
</item>
</layout>
</item>
<item row="7" column="0">
<item row="8" column="0">
<widget class="QLabel" name="destination_field_label">
<property name="text">
<string>&amp;Destination field:</string>
@ -763,7 +840,7 @@ field is processed. In regular expression mode, only the matched text is process
</property>
</widget>
</item>
<item row="7" column="1">
<item row="8" column="1">
<widget class="QComboBox" name="destination_field">
<property name="toolTip">
<string>The field that the text will be put into after all replacements.
@ -771,7 +848,7 @@ If blank, the source field is used if the field is modifiable</string>
</property>
</widget>
</item>
<item row="7" column="2">
<item row="8" column="2">
<layout class="QHBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="replace_mode_label">
@ -820,7 +897,7 @@ not multiple and the destination field is multiple</string>
</item>
</layout>
</item>
<item row="8" column="1" colspan="2">
<item row="9" column="1" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_21">
<item>
<spacer name="HSpacer_347">
@ -906,7 +983,7 @@ not multiple and the destination field is multiple</string>
</item>
</layout>
</item>
<item row="9" column="0" colspan="4">
<item row="10" column="0" colspan="4">
<widget class="QScrollArea" name="scrollArea11">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
@ -1030,6 +1107,9 @@ not multiple and the destination field is multiple</string>
<tabstop>series_numbering_restarts</tabstop>
<tabstop>series_start_number</tabstop>
<tabstop>button_box</tabstop>
<tabstop>query_field</tabstop>
<tabstop>save_button</tabstop>
<tabstop>remove_button</tabstop>
<tabstop>search_field</tabstop>
<tabstop>search_mode</tabstop>
<tabstop>s_r_template</tabstop>
@ -1045,6 +1125,23 @@ not multiple and the destination field is multiple</string>
<tabstop>multiple_separator</tabstop>
<tabstop>test_text</tabstop>
<tabstop>test_result</tabstop>
<tabstop>scrollArea</tabstop>
<tabstop>central_widget</tabstop>
<tabstop>swap_title_and_author</tabstop>
<tabstop>clear_series</tabstop>
<tabstop>adddate</tabstop>
<tabstop>clear_adddate_button</tabstop>
<tabstop>apply_adddate</tabstop>
<tabstop>pubdate</tabstop>
<tabstop>clear_pubdate_button</tabstop>
<tabstop>apply_pubdate</tabstop>
<tabstop>remove_format</tabstop>
<tabstop>change_title_to_title_case</tabstop>
<tabstop>remove_conversion_settings</tabstop>
<tabstop>cover_generate</tabstop>
<tabstop>cover_remove</tabstop>
<tabstop>cover_from_fmt</tabstop>
<tabstop>scrollArea11</tabstop>
</tabstops>
<resources>
<include location="../../../../resources/images.qrc"/>

View File

@ -16,7 +16,7 @@ class TagEditor(QDialog, Ui_TagEditor):
self.setupUi(self)
self.db = db
self.index = db.row(id_)
self.index = db.row(id_) if id_ is not None else None
if self.index is not None:
tags = self.db.tags(self.index)
else:

View File

@ -43,7 +43,17 @@ p, li { white-space: pre-wrap; }
</property>
<layout class="QVBoxLayout">
<item>
<widget class="QLineEdit" name="re"/>
<widget class="QComboBox" name="re">
<property name="editable">
<bool>true</bool>
</property>
<property name="maxCount">
<number>10</number>
</property>
<property name="insertPolicy">
<enum>QComboBox::InsertAtTop</enum>
</property>
</widget>
</item>
</layout>
</widget>
@ -94,8 +104,8 @@ p, li { white-space: pre-wrap; }
<rect>
<x>0</x>
<y>0</y>
<width>301</width>
<height>234</height>
<width>277</width>
<height>276</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout_2">

View File

@ -150,13 +150,13 @@ class GuiRunner(QObject):
if DEBUG:
prints('Starting up...')
def start_gui(self):
def start_gui(self, db):
from calibre.gui2.ui import Main
main = Main(self.opts, gui_debug=self.gui_debug)
if self.splash_screen is not None:
self.splash_screen.showMessage(_('Initializing user interface...'))
self.splash_screen.finish(main)
main.initialize(self.library_path, self.db, self.listener, self.actions)
main.initialize(self.library_path, db, self.listener, self.actions)
if DEBUG:
prints('Started up in', time.time() - self.startup_time)
add_filesystem_book = partial(main.iactions['Add Books'].add_filesystem_book, allow_device=False)
@ -200,8 +200,7 @@ class GuiRunner(QObject):
det_msg=traceback.format_exc(), show=True)
self.initialization_failed()
self.db = db
self.start_gui()
self.start_gui(db)
def initialize_db(self):
db = None

View File

@ -77,9 +77,9 @@ class TitleEdit(EnLineEdit):
def commit(self, db, id_):
title = self.current_val
if self.COMMIT:
getattr(db, 'set_', self.TITLE_ATTR)(id_, title, notify=False)
getattr(db, 'set_'+ self.TITLE_ATTR)(id_, title, notify=False)
else:
getattr(db, 'set_', self.TITLE_ATTR)(id_, title, notify=False,
getattr(db, 'set_'+ self.TITLE_ATTR)(id_, title, notify=False,
commit=False)
return True

View File

@ -11,7 +11,7 @@ from functools import partial
from PyQt4.Qt import Qt, QVBoxLayout, QHBoxLayout, QWidget, QPushButton, \
QGridLayout, pyqtSignal, QDialogButtonBox, QScrollArea, QFont, \
QTabWidget, QIcon, QToolButton, QSplitter, QGroupBox, QSpacerItem, \
QSizePolicy
QSizePolicy, QPalette, QFrame, QSize
from calibre.ebooks.metadata import authors_to_string, string_to_authors
from calibre.gui2 import ResizableDialog, error_dialog, gprefs
@ -22,9 +22,11 @@ from calibre.gui2.metadata.basic_widgets import TitleEdit, AuthorsEdit, \
from calibre.gui2.custom_column_widgets import populate_metadata_page
from calibre.utils.config import tweaks
class MetadataSingleDialog(ResizableDialog):
class MetadataSingleDialogBase(ResizableDialog):
view_format = pyqtSignal(object)
cc_two_column = tweaks['metadata_single_use_2_cols_for_custom_fields']
one_line_comments_toolbar = False
def __init__(self, db, parent=None):
self.db = db
@ -61,13 +63,11 @@ class MetadataSingleDialog(ResizableDialog):
self.l.addWidget(self.button_box)
self.setWindowIcon(QIcon(I('edit_input.png')))
self.setWindowTitle(_('Edit Meta Information'))
self.setWindowTitle(_('Edit Metadata'))
self.create_basic_metadata_widgets()
if len(self.db.custom_column_label_map) == 0:
self.central_widget.tabBar().setVisible(False)
else:
if len(self.db.custom_column_label_map):
self.create_custom_metadata_widgets()
@ -101,7 +101,7 @@ class MetadataSingleDialog(ResizableDialog):
'Using this button to create author sort will change author sort from'
' red to green.'))
self.author_sort = AuthorSortEdit(self, self.authors,
self.deduce_author_sort_button, db)
self.deduce_author_sort_button, self.db)
self.basic_metadata_widgets.extend([self.authors, self.author_sort])
self.swap_title_author_button = QToolButton(self)
@ -127,7 +127,7 @@ class MetadataSingleDialog(ResizableDialog):
self.cover = Cover(self)
self.basic_metadata_widgets.append(self.cover)
self.comments = CommentsEdit(self)
self.comments = CommentsEdit(self, self.one_line_comments_toolbar)
self.basic_metadata_widgets.append(self.comments)
self.rating = RatingEdit(self)
@ -166,19 +166,213 @@ class MetadataSingleDialog(ResizableDialog):
w.setLayout(layout)
self.custom_metadata_widgets, self.__cc_spacers = \
populate_metadata_page(layout, self.db, None, parent=w, bulk=False,
two_column=tweaks['metadata_single_use_2_cols_for_custom_fields'])
two_column=self.cc_two_column)
self.__custom_col_layouts = [layout]
ans = self.custom_metadata_widgets
for i in range(len(ans)-1):
if len(ans[i+1].widgets) == 2:
w.setTabOrder(ans[i].widgets[-1], ans[i+1].widgets[1])
else:
w.setTabOrder(ans[i].widgets[-1], ans[i+1].widgets[0])
for c in range(2, len(ans[i].widgets), 2):
w.setTabOrder(ans[i].widgets[c-1], ans[i].widgets[c+1])
# }}}
def do_layout(self): # {{{
def set_custom_metadata_tab_order(self, before=None, after=None): # {{{
sto = QWidget.setTabOrder
if getattr(self, 'custom_metadata_widgets', []):
ans = self.custom_metadata_widgets
for i in range(len(ans)-1):
if before is not None and i == 0:
pass# Do something
if len(ans[i+1].widgets) == 2:
sto(ans[i].widgets[-1], ans[i+1].widgets[1])
else:
sto(ans[i].widgets[-1], ans[i+1].widgets[0])
for c in range(2, len(ans[i].widgets), 2):
sto(ans[i].widgets[c-1], ans[i].widgets[c+1])
if after is not None:
pass # Do something
# }}}
def do_layout(self):
raise NotImplementedError()
def __call__(self, id_):
self.book_id = id_
for widget in self.basic_metadata_widgets:
widget.initialize(self.db, id_)
for widget in self.custom_metadata_widgets:
widget.initialize(id_)
# Commented out as it doesn't play nice with Next, Prev buttons
#self.fetch_metadata_button.setFocus(Qt.OtherFocusReason)
# Miscellaneous interaction methods {{{
def update_window_title(self, *args):
title = self.title.current_val
if len(title) > 50:
title = title[:50] + u'\u2026'
self.setWindowTitle(_('Edit Metadata') + ' - ' +
title)
def swap_title_author(self, *args):
title = self.title.current_val
self.title.current_val = authors_to_string(self.authors.current_val)
self.authors.current_val = string_to_authors(title)
self.title_sort.auto_generate()
self.author_sort.auto_generate()
def remove_unused_series(self, *args):
self.db.remove_unused_series()
idx = self.series.current_val
self.series.clear()
self.series.initialize(self.db, self.book_id)
if idx:
for i in range(self.series.count()):
if unicode(self.series.itemText(i)) == idx:
self.series.setCurrentIndex(i)
break
def tags_editor(self, *args):
self.tags.edit(self.db, self.book_id)
def metadata_from_format(self, *args):
mi, ext = self.formats_manager.get_selected_format_metadata(self.db,
self.book_id)
if mi is not None:
self.update_from_mi(mi)
def cover_from_format(self, *args):
mi, ext = self.formats_manager.get_selected_format_metadata(self.db,
self.book_id)
if mi is None:
return
cdata = None
if mi.cover and os.access(mi.cover, os.R_OK):
cdata = open(mi.cover).read()
elif mi.cover_data[1] is not None:
cdata = mi.cover_data[1]
if cdata is None:
error_dialog(self, _('Could not read cover'),
_('Could not read cover from %s format')%ext).exec_()
return
orig = self.cover.current_val
self.cover.current_val = cdata
if self.cover.current_val is None:
self.cover.current_val = orig
return error_dialog(self, _('Could not read cover'),
_('The cover in the %s format is invalid')%ext,
show=True)
return
def update_from_mi(self, mi):
if not mi.is_null('title'):
self.title.current_val = mi.title
if not mi.is_null('authors'):
self.authors.current_val = mi.authors
if not mi.is_null('author_sort'):
self.author_sort.current_val = mi.author_sort
if not mi.is_null('rating'):
try:
self.rating.current_val = mi.rating
except:
pass
if not mi.is_null('publisher'):
self.publisher.current_val = mi.publisher
if not mi.is_null('tags'):
self.tags.current_val = mi.tags
if not mi.is_null('isbn'):
self.isbn.current_val = mi.isbn
if not mi.is_null('pubdate'):
self.pubdate.current_val = mi.pubdate
if not mi.is_null('series') and mi.series.strip():
self.series.current_val = mi.series
if mi.series_index is not None:
self.series_index.current_val = float(mi.series_index)
if mi.comments and mi.comments.strip():
self.comments.current_val = mi.comments
def fetch_metadata(self, *args):
pass # TODO: fetch metadata
# }}}
def apply_changes(self):
self.changed.add(self.book_id)
for widget in self.basic_metadata_widgets:
try:
if not widget.commit(self.db, self.book_id):
return False
except IOError, err:
if err.errno == 13: # Permission denied
import traceback
fname = err.filename if err.filename else 'file'
error_dialog(self, _('Permission denied'),
_('Could not open %s. Is it being used by another'
' program?')%fname, det_msg=traceback.format_exc(),
show=True)
return False
raise
for widget in getattr(self, 'custom_metadata_widgets', []):
widget.commit(self.book_id)
self.db.commit()
return True
def accept(self):
self.save_state()
if not self.apply_changes():
return
ResizableDialog.accept(self)
def reject(self):
self.save_state()
ResizableDialog.reject(self)
def save_state(self):
gprefs['metasingle_window_geometry3'] = bytearray(self.saveGeometry())
# Dialog use methods {{{
def start(self, row_list, current_row, view_slot=None):
self.row_list = row_list
self.current_row = current_row
if view_slot is not None:
self.view_format.connect(view_slot)
self.do_one()
ret = self.exec_()
self.break_cycles()
return ret
def do_one(self, delta=0):
self.current_row += delta
prev = next_ = None
if self.current_row > 0:
prev = self.db.title(self.row_list[self.current_row-1])
if self.current_row < len(self.row_list) - 1:
next_ = self.db.title(self.row_list[self.current_row+1])
if next_ is not None:
tip = _('Save changes and edit the metadata of %s')%next_
self.next_button.setToolTip(tip)
self.next_button.setVisible(next_ is not None)
if prev is not None:
tip = _('Save changes and edit the metadata of %s')%prev
self.prev_button.setToolTip(tip)
self.prev_button.setVisible(prev is not None)
self(self.db.id(self.row_list[self.current_row]))
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)
# }}}
class MetadataSingleDialog(MetadataSingleDialogBase): # {{{
def do_layout(self):
if len(self.db.custom_column_label_map) == 0:
self.central_widget.tabBar().setVisible(False)
self.central_widget.clear()
self.tabs = []
self.labels = []
@ -283,181 +477,143 @@ class MetadataSingleDialog(ResizableDialog):
l.addWidget(self.comments)
self.splitter.addWidget(gb)
self.set_custom_metadata_tab_order()
# }}}
def __call__(self, id_):
self.book_id = id_
for widget in self.basic_metadata_widgets:
widget.initialize(self.db, id_)
for widget in self.custom_metadata_widgets:
widget.initialize(id_)
# Commented out as it doesn't play nice with Next, Prev buttons
#self.fetch_metadata_button.setFocus(Qt.OtherFocusReason)
class MetadataSingleDialogAlt(MetadataSingleDialogBase): # {{{
cc_two_column = False
one_line_comments_toolbar = True
def update_window_title(self, *args):
title = self.title.current_val
if len(title) > 50:
title = title[:50] + u'\u2026'
self.setWindowTitle(_('Edit Meta Information') + ' - ' +
title)
def do_layout(self):
self.central_widget.clear()
self.tabs = []
self.labels = []
sto = QWidget.setTabOrder
def swap_title_author(self, *args):
title = self.title.current_val
self.title.current_val = authors_to_string(self.authors.current_val)
self.authors.current_val = string_to_authors(title)
self.title_sort.auto_generate()
self.author_sort.auto_generate()
self.tabs.append(QWidget(self))
self.central_widget.addTab(self.tabs[0], _("&Metadata"))
self.tabs[0].l = QGridLayout()
self.tabs[0].setLayout(self.tabs[0].l)
def remove_unused_series(self, *args):
self.db.remove_unused_series()
idx = self.series.current_val
self.series.clear()
self.series.initialize(self.db, self.book_id)
if idx:
for i in range(self.series.count()):
if unicode(self.series.itemText(i)) == idx:
self.series.setCurrentIndex(i)
break
self.tabs.append(QWidget(self))
self.central_widget.addTab(self.tabs[1], _("&Cover and formats"))
self.tabs[1].l = QGridLayout()
self.tabs[1].setLayout(self.tabs[1].l)
def tags_editor(self, *args):
self.tags.edit(self.db, self.book_id)
# Tab 0
tab0 = self.tabs[0]
def metadata_from_format(self, *args):
mi, ext = self.formats_manager.get_selected_format_metadata(self.db,
self.book_id)
if mi is not None:
self.update_from_mi(mi)
tl = QGridLayout()
gb = QGroupBox(_('&Basic metadata'), self.tabs[0])
self.tabs[0].l.addWidget(gb, 0, 0, 1, 1)
gb.setLayout(tl)
def cover_from_format(self, *args):
mi, ext = self.formats_manager.get_selected_format_metadata(self.db,
self.book_id)
if mi is None:
return
cdata = None
if mi.cover and os.access(mi.cover, os.R_OK):
cdata = open(mi.cover).read()
elif mi.cover_data[1] is not None:
cdata = mi.cover_data[1]
if cdata is None:
error_dialog(self, _('Could not read cover'),
_('Could not read cover from %s format')%ext).exec_()
return
orig = self.cover.current_val
self.cover.current_val = cdata
if self.cover.current_val is None:
self.cover.current_val = orig
return error_dialog(self, _('Could not read cover'),
_('The cover in the %s format is invalid')%ext,
show=True)
return
sto(self.button_box, self.title)
def update_from_mi(self, mi):
if not mi.is_null('title'):
self.title.current_val = mi.title
if not mi.is_null('authors'):
self.authors.current_val = mi.authors
if not mi.is_null('author_sort'):
self.author_sort.current_val = mi.author_sort
if not mi.is_null('rating'):
try:
self.rating.current_val = mi.rating
except:
pass
if not mi.is_null('publisher'):
self.publisher.current_val = mi.publisher
if not mi.is_null('tags'):
self.tags.current_val = mi.tags
if not mi.is_null('isbn'):
self.isbn.current_val = mi.isbn
if not mi.is_null('pubdate'):
self.pubdate.current_val = mi.pubdate
if not mi.is_null('series') and mi.series.strip():
self.series.current_val = mi.series
if mi.series_index is not None:
self.series_index.current_val = float(mi.series_index)
if mi.comments and mi.comments.strip():
self.comments.current_val = mi.comments
def create_row(row, widget, tab_to, button=None, icon=None, span=1):
ql = BuddyLabel(widget)
tl.addWidget(ql, row, 1, 1, 1)
tl.addWidget(widget, row, 2, 1, 1)
if button is not None:
tl.addWidget(button, row, 3, span, 1)
if icon is not None:
button.setIcon(QIcon(I(icon)))
if tab_to is not None:
if button is not None:
sto(widget, button)
sto(button, tab_to)
else:
sto(widget, tab_to)
def fetch_metadata(self, *args):
pass # TODO: fetch metadata
tl.addWidget(self.swap_title_author_button, 0, 0, 2, 1)
def apply_changes(self):
self.changed.add(self.book_id)
for widget in self.basic_metadata_widgets:
try:
if not widget.commit(self.db, self.book_id):
return False
except IOError, err:
if err.errno == 13: # Permission denied
import traceback
fname = err.filename if err.filename else 'file'
error_dialog(self, _('Permission denied'),
_('Could not open %s. Is it being used by another'
' program?')%fname, det_msg=traceback.format_exc(),
show=True)
return False
raise
for widget in getattr(self, 'custom_metadata_widgets', []):
widget.commit(self.book_id)
create_row(0, self.title, self.title_sort,
button=self.deduce_title_sort_button, span=2,
icon='auto_author_sort.png')
create_row(1, self.title_sort, self.authors)
create_row(2, self.authors, self.author_sort,
button=self.deduce_author_sort_button,
span=2, icon='auto_author_sort.png')
create_row(3, self.author_sort, self.series)
create_row(4, self.series, self.series_index,
button=self.remove_unused_series_button, icon='trash.png')
create_row(5, self.series_index, self.tags)
create_row(6, self.tags, self.rating, button=self.tags_editor_button)
create_row(7, self.rating, self.pubdate)
create_row(8, self.pubdate, self.publisher,
button=self.pubdate.clear_button, icon='trash.png')
create_row(9, self.publisher, self.timestamp)
create_row(10, self.timestamp, self.isbn,
button=self.timestamp.clear_button, icon='trash.png')
create_row(11, self.isbn, self.comments)
tl.addItem(QSpacerItem(1, 1, QSizePolicy.Fixed, QSizePolicy.Expanding),
12, 1, 1 ,1)
self.db.commit()
return True
w = getattr(self, 'custom_metadata_widgets_parent', None)
if w is not None:
gb = QGroupBox(_('C&ustom metadata'), tab0)
gbl = QVBoxLayout()
gb.setLayout(gbl)
sr = QScrollArea(tab0)
sr.setWidgetResizable(True)
sr.setBackgroundRole(QPalette.Base)
sr.setFrameStyle(QFrame.NoFrame)
sr.setWidget(w)
gbl.addWidget(sr)
self.tabs[0].l.addWidget(gb, 0, 1, 1, 1)
sto(self.isbn, gb)
def accept(self):
self.save_state()
if not self.apply_changes():
return
ResizableDialog.accept(self)
w = QGroupBox(_('&Comments'), tab0)
sp = QSizePolicy()
sp.setVerticalStretch(10)
sp.setHorizontalPolicy(QSizePolicy.Expanding)
sp.setVerticalPolicy(QSizePolicy.Expanding)
w.setSizePolicy(sp)
l = QHBoxLayout()
w.setLayout(l)
l.addWidget(self.comments)
tab0.l.addWidget(w, 1, 0, 1, 2)
def reject(self):
self.save_state()
ResizableDialog.reject(self)
# Tab 1
tab1 = self.tabs[1]
def save_state(self):
gprefs['metasingle_window_geometry3'] = bytearray(self.saveGeometry())
wsp = QWidget(tab1)
wgl = QVBoxLayout()
wsp.setLayout(wgl)
def start(self, row_list, current_row, view_slot=None):
self.row_list = row_list
self.current_row = current_row
if view_slot is not None:
self.view_format.connect(view_slot)
self.do_one()
ret = self.exec_()
self.break_cycles()
return ret
# right-hand side of splitter
gb = QGroupBox(_('Change cover'), tab1)
l = QGridLayout()
gb.setLayout(l)
sto(self.swap_title_author_button, self.cover.buttons[0])
for i, b in enumerate(self.cover.buttons[:3]):
l.addWidget(b, 0, i, 1, 1)
sto(b, self.cover.buttons[i+1])
hl = QHBoxLayout()
for b in self.cover.buttons[3:]:
hl.addWidget(b)
sto(self.cover.buttons[-2], self.cover.buttons[-1])
l.addLayout(hl, 1, 0, 1, 3)
wgl.addWidget(gb)
wgl.addItem(QSpacerItem(10, 10, QSizePolicy.Expanding,
QSizePolicy.Expanding))
wgl.addWidget(self.fetch_metadata_button)
wgl.addItem(QSpacerItem(10, 10, QSizePolicy.Expanding,
QSizePolicy.Expanding))
wgl.addWidget(self.formats_manager)
def do_one(self, delta=0):
self.current_row += delta
prev = next_ = None
if self.current_row > 0:
prev = self.db.title(self.row_list[self.current_row-1])
if self.current_row < len(self.row_list) - 1:
next_ = self.db.title(self.row_list[self.current_row+1])
self.splitter = QSplitter(Qt.Horizontal, tab1)
tab1.l.addWidget(self.splitter)
self.splitter.addWidget(self.cover)
self.splitter.addWidget(wsp)
if next_ is not None:
tip = _('Save changes and edit the metadata of %s')%next_
self.next_button.setToolTip(tip)
self.next_button.setVisible(next_ is not None)
if prev is not None:
tip = _('Save changes and edit the metadata of %s')%prev
self.prev_button.setToolTip(tip)
self.prev_button.setVisible(prev is not None)
self(self.db.id(self.row_list[self.current_row]))
self.formats_manager.formats.setMaximumWidth(10000)
self.formats_manager.formats.setIconSize(QSize(64, 64))
# }}}
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)
def edit_metadata(db, row_list, current_row, parent=None, view_slot=None):
d = MetadataSingleDialog(db, parent)
@ -467,8 +623,8 @@ def edit_metadata(db, row_list, current_row, parent=None, view_slot=None):
if __name__ == '__main__':
from PyQt4.Qt import QApplication
app = QApplication([])
from calibre.library import db
db = db()
from calibre.library import db as db_
db = db_()
row_list = list(range(len(db.data)))
edit_metadata(db, row_list, 0)

View File

@ -114,6 +114,9 @@ class TagsView(QTreeView): # {{{
def set_database(self, db, tag_match, sort_by):
self.hidden_categories = config['tag_browser_hidden_categories']
old = getattr(self, '_model', None)
if old is not None:
old.break_cycles()
self._model = TagsModel(db, parent=self,
hidden_categories=self.hidden_categories,
search_restriction=None,
@ -183,7 +186,7 @@ class TagsView(QTreeView): # {{{
self.clear()
def context_menu_handler(self, action=None, category=None,
key=None, index=None):
key=None, index=None, negate=None):
if not action:
return
try:
@ -196,6 +199,9 @@ class TagsView(QTreeView): # {{{
if action == 'manage_categories':
self.user_category_edit.emit(category)
return
if action == 'search_category':
self.tags_marked.emit(category + ':' + str(not negate))
return
if action == 'manage_searches':
self.saved_search_edit.emit(category)
return
@ -265,6 +271,15 @@ class TagsView(QTreeView): # {{{
m.addAction(col,
partial(self.context_menu_handler, action='show', category=col))
# search by category
self.context_menu.addAction(
_('Search for books in category %s')%category,
partial(self.context_menu_handler, action='search_category',
category=key, negate=False))
self.context_menu.addAction(
_('Search for books not in category %s')%category,
partial(self.context_menu_handler, action='search_category',
category=key, negate=True))
# Offer specific editors for tags/series/publishers/saved searches
self.context_menu.addSeparator()
if key in ['tags', 'publisher', 'series'] or \
@ -371,6 +386,9 @@ class TagsView(QTreeView): # {{{
# model. Reason: it is much easier than reconstructing the browser tree.
def set_new_model(self, filter_categories_by=None):
try:
old = getattr(self, '_model', None)
if old is not None:
old.break_cycles()
self._model = TagsModel(self.db, parent=self,
hidden_categories=self.hidden_categories,
search_restriction=self.search_restriction,
@ -509,8 +527,8 @@ class TagsModel(QAbstractItemModel): # {{{
QAbstractItemModel.__init__(self, parent)
# must do this here because 'QPixmap: Must construct a QApplication
# before a QPaintDevice'. The ':' in front avoids polluting either the
# user-defined categories (':' at end) or columns namespaces (no ':').
# before a QPaintDevice'. The ':' at the end avoids polluting either of
# the other namespaces (alpha, '#', or '@')
iconmap = {}
for key in category_icon_map:
iconmap[key] = QIcon(I(category_icon_map[key]))
@ -544,6 +562,9 @@ class TagsModel(QAbstractItemModel): # {{{
tooltip=tt, category_key=r)
self.refresh(data=data)
def break_cycles(self):
self.db = self.root_item = None
def mimeTypes(self):
return ["application/calibre+from_library"]
@ -681,7 +702,7 @@ class TagsModel(QAbstractItemModel): # {{{
tb_cats = self.db.field_metadata
for user_cat in sorted(self.db.prefs.get('user_categories', {}).keys(),
key=sort_key):
cat_name = user_cat+':' # add the ':' to avoid name collision
cat_name = '@' + user_cat # add the '@' to avoid name collision
tb_cats.add_user_category(label=cat_name, name=user_cat)
if len(saved_searches().names()):
tb_cats.add_search_category(label='search', name=_('Searches'))
@ -988,7 +1009,7 @@ class TagsModel(QAbstractItemModel): # {{{
if self.hidden_categories and self.categories[i] in self.hidden_categories:
continue
row_index += 1
if key.endswith(':'):
if key.startswith('@'):
# User category, so skip it. The tag will be marked in its real category
continue
category_item = self.root_item.children[row_index]
@ -1007,7 +1028,7 @@ class TagsModel(QAbstractItemModel): # {{{
ans.append('%s%s:"=%s"'%(prefix, category, tag.name))
return ans
def find_node(self, key, txt, start_path):
def find_item_node(self, key, txt, start_path):
'''
Search for an item (a node) in the tags browser list that matches both
the key (exact case-insensitive match) and txt (contains case-
@ -1061,6 +1082,22 @@ class TagsModel(QAbstractItemModel): # {{{
break
return self.path_found
def find_category_node(self, key):
'''
Search for an category node (a top-level node) in the tags browser list
that matches the key (exact case-insensitive match). Returns the path to
the node. Paths are as in find_item_node.
'''
if not key:
return None
for i in xrange(self.rowCount(QModelIndex())):
idx = self.index(i, 0, QModelIndex())
ckey = idx.internalPointer().category_key
if strcmp(ckey, key) == 0:
return self.path_for_index(idx)
return None
def show_item_at_path(self, path, box=False):
'''
Scroll the browser and open categories to show the item referenced by
@ -1109,8 +1146,7 @@ class TagBrowserMixin(object): # {{{
def __init__(self, db):
self.library_view.model().count_changed_signal.connect(self.tags_view.recount)
self.tags_view.set_database(self.library_view.model().db,
self.tag_match, self.sort_by)
self.tags_view.set_database(db, self.tag_match, self.sort_by)
self.tags_view.tags_marked.connect(self.search.set_search_string)
self.tags_view.tag_list_edit.connect(self.do_tags_list_edit)
self.tags_view.user_category_edit.connect(self.do_user_categories_edit)
@ -1347,15 +1383,15 @@ class TagBrowserWidget(QWidget): # {{{
self.search_button.setFocus(True)
self.item_search.lineEdit().blockSignals(False)
colon = txt.find(':')
key = None
colon = txt.rfind(':') if len(txt) > 2 else 0
if colon > 0:
key = self.parent.library_view.model().db.\
field_metadata.search_term_to_field_key(txt[:colon])
txt = txt[colon+1:]
self.current_find_position = model.find_node(key, txt,
self.current_find_position)
self.current_find_position = \
model.find_item_node(key, txt, self.current_find_position)
if self.current_find_position:
model.show_item_at_path(self.current_find_position, box=True)
elif self.item_search.text():

View File

@ -16,7 +16,7 @@ from PyQt4.Qt import Qt, SIGNAL, QTimer, \
QPixmap, QMenu, QIcon, pyqtSignal, \
QDialog, \
QSystemTrayIcon, QApplication, QKeySequence, \
QMessageBox, QHelpEvent
QMessageBox, QHelpEvent, QAction
from calibre import prints
from calibre.constants import __appname__, isosx
@ -198,6 +198,10 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
self.system_tray_icon.activated.connect(
self.system_tray_icon_activated)
self.esc_action = QAction(self)
self.addAction(self.esc_action)
self.esc_action.setShortcut(QKeySequence(Qt.Key_Escape))
self.esc_action.triggered.connect(self.esc)
####################### Start spare job server ########################
QTimer.singleShot(1000, self.add_spare_server)
@ -294,6 +298,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
'the file: %s<p>The '
'log will be displayed automatically.')%self.gui_debug, show=True)
def esc(self, *args):
self.search.clear()
def start_content_server(self):
from calibre.library.server.main import start_threaded_server
@ -305,7 +311,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
self.content_server.state_callback(True)
self.test_server_timer = QTimer.singleShot(10000, self.test_server)
def resizeEvent(self, ev):
MainWindow.resizeEvent(self, ev)
self.search.setMaximumWidth(self.width()-150)

View File

@ -16,7 +16,6 @@ from PyQt4.Qt import QIcon, QFont, QLabel, QListWidget, QAction, \
QTimer, QRect
from calibre.gui2 import NONE, error_dialog, pixmap_to_data, gprefs
from calibre.constants import isosx
from calibre.gui2.filename_pattern_ui import Ui_Form
from calibre import fit_image
from calibre.ebooks import BOOK_EXTENSIONS
@ -67,17 +66,31 @@ class FilenamePattern(QWidget, Ui_Form):
self.setupUi(self)
self.connect(self.test_button, SIGNAL('clicked()'), self.do_test)
self.connect(self.re, SIGNAL('returnPressed()'), self.do_test)
self.initialize()
self.re.textChanged.connect(lambda x: self.changed_signal.emit())
self.connect(self.re.lineEdit(), SIGNAL('returnPressed()'), self.do_test)
self.re.lineEdit().textChanged.connect(lambda x: self.changed_signal.emit())
def initialize(self, defaults=False):
# Get all itmes in the combobox. If we are resting
# to defaults we don't want to lose what the user
# has added.
val_hist = [unicode(self.re.lineEdit().text())] + [unicode(self.re.itemText(i)) for i in xrange(self.re.count())]
self.re.clear()
if defaults:
val = prefs.defaults['filename_pattern']
else:
val = prefs['filename_pattern']
self.re.setText(val)
self.re.lineEdit().setText(val)
val_hist += gprefs.get('filename_pattern_history', ['(?P<title>.+)', '(?P<author>[^_-]+) -?\s*(?P<series>[^_0-9-]*)(?P<series_index>[0-9]*)\s*-\s*(?P<title>[^_].+) ?'])
if val in val_hist:
del val_hist[val_hist.index(val)]
val_hist.insert(0, val)
for v in val_hist:
# Ensure we don't have duplicate items.
if v and self.re.findText(v) == -1:
self.re.addItem(v)
self.re.setCurrentIndex(0)
def do_test(self):
try:
@ -110,12 +123,21 @@ class FilenamePattern(QWidget, Ui_Form):
def pattern(self):
pat = unicode(self.re.text())
pat = unicode(self.re.lineEdit().text())
return re.compile(pat)
def commit(self):
pat = self.pattern().pattern
prefs['filename_pattern'] = pat
history = []
history_pats = [unicode(self.re.lineEdit().text())] + [unicode(self.re.itemText(i)) for i in xrange(self.re.count())]
for p in history_pats[:14]:
# Ensure we don't have duplicate items.
if p and p not in history:
history.append(p)
gprefs['filename_pattern_history'] = history
return pat
@ -304,8 +326,9 @@ class FontFamilyModel(QAbstractListModel):
return NONE
if role == Qt.DisplayRole:
return QVariant(family)
if not isosx and role == Qt.FontRole:
# Causes a Qt crash with some fonts on OS X
if False and role == Qt.FontRole:
# Causes a Qt crash with some fonts
# so disabled.
return QVariant(QFont(family))
return NONE

View File

@ -42,6 +42,9 @@ class MetadataBackup(Thread): # {{{
def stop(self):
self.keep_running = False
# Break cycles so that this object doesn't hold references to db
self.do_write = self.get_metadata_for_dump = self.clear_dirtied = \
self.set_dirtied = self.db = None
def run(self):
while self.keep_running:
@ -132,7 +135,7 @@ def _match(query, value, matchkind):
pass
return False
class CacheRow(list):
class CacheRow(list): # {{{
def __init__(self, db, composites, val):
self.db = db
@ -163,14 +166,16 @@ class CacheRow(list):
def __getslice__(self, i, j):
return self.__getitem__(slice(i, j))
# }}}
class ResultCache(SearchQueryParser): # {{{
'''
Stores sorted and filtered metadata in memory.
'''
def __init__(self, FIELD_MAP, field_metadata):
def __init__(self, FIELD_MAP, field_metadata, db_prefs=None):
self.FIELD_MAP = FIELD_MAP
self.db_prefs = db_prefs
self.composites = {}
for key in field_metadata:
if field_metadata[key]['datatype'] == 'composite':
@ -185,6 +190,11 @@ class ResultCache(SearchQueryParser): # {{{
self.build_date_relop_dict()
self.build_numeric_relop_dict()
def break_cycles(self):
self._data = self.field_metadata = self.FIELD_MAP = \
self.numeric_search_relops = self.date_search_relops = \
self.all_search_locations = self.db_prefs = None
def __getitem__(self, row):
return self._data[self._map_filtered[row]]
@ -397,6 +407,22 @@ class ResultCache(SearchQueryParser): # {{{
matches.add(item[0])
return matches
def get_user_category_matches(self, location, query, candidates):
res = set([])
if self.db_prefs is None:
return res
user_cats = self.db_prefs.get('user_categories', [])
if location not in user_cats:
return res
c = set(candidates)
for (item, category, ign) in user_cats[location]:
s = self.get_matches(category, '=' + item, candidates=c)
c -= s
res |= s
if query == 'false':
return candidates - res
return res
def get_matches(self, location, query, allow_recursion=True, candidates=None):
matches = set([])
if candidates is None:
@ -435,6 +461,10 @@ class ResultCache(SearchQueryParser): # {{{
return self.get_numeric_matches(location, query[1:],
candidates, val_func=vf)
# check for user categories
if len(location) >= 2 and location.startswith('@'):
return self.get_user_category_matches(location[1:], query.lower(),
candidates)
# everything else, or 'all' matches
matchkind = CONTAINS_MATCH
if (len(query) > 1):
@ -460,6 +490,8 @@ class ResultCache(SearchQueryParser): # {{{
for x in range(len(self.FIELD_MAP)):
col_datatype.append('')
for x in self.field_metadata:
if x.startswith('@'):
continue
if len(self.field_metadata[x]['search_terms']):
db_col[x] = self.field_metadata[x]['rec_index']
if self.field_metadata[x]['datatype'] not in \

View File

@ -319,7 +319,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.field_metadata.remove_dynamic_categories()
tb_cats = self.field_metadata
for user_cat in sorted(self.prefs.get('user_categories', {}).keys(), key=sort_key):
cat_name = user_cat+':' # add the ':' to avoid name collision
cat_name = '@' + user_cat # add the '@' to avoid name collision
tb_cats.add_user_category(label=cat_name, name=user_cat)
if len(saved_searches().names()):
tb_cats.add_search_category(label='search', name=_('Searches'))
@ -332,7 +332,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
traceback.print_exc()
self.book_on_device_func = None
self.data = ResultCache(self.FIELD_MAP, self.field_metadata)
self.data = ResultCache(self.FIELD_MAP, self.field_metadata, db_prefs=self.prefs)
self.search = self.data.search
self.search_getting_ids = self.data.search_getting_ids
self.refresh = functools.partial(self.data.refresh, self)
@ -362,7 +362,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.last_update_check = self.last_modified()
def break_cycles(self):
self.data = self.field_metadata = self.prefs = self.listeners = None
self.data.break_cycles()
self.data = self.field_metadata = self.prefs = self.listeners = \
self.refresh_ondevice = None
def initialize_database(self):
metadata_sqlite = open(P('metadata_sqlite.sql'), 'rb').read()
@ -1241,7 +1243,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if category in icon_map:
icon = icon_map[label]
else:
icon = icon_map[':custom']
icon = icon_map['custom:']
icon_map[category] = icon
datatype = cat['datatype']
@ -1337,11 +1339,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if label in taglist and name in taglist[label]:
items.append(taglist[label][name])
# else: do nothing, to not include nodes w zero counts
if len(items):
cat_name = user_cat+':' # add the ':' to avoid name collision
cat_name = '@' + user_cat # add the '@' to avoid name collision
# Not a problem if we accumulate entries in the icon map
if icon_map is not None:
icon_map[cat_name] = icon_map[':user']
icon_map[cat_name] = icon_map['user:']
if sort == 'popularity':
categories[cat_name] = \
sorted(items, key=lambda x: x.count, reverse=True)

View File

@ -16,7 +16,7 @@ class TagsIcons(dict):
'''
category_icons = ['authors', 'series', 'formats', 'publisher', 'rating',
'news', 'tags', ':custom', ':user', 'search',]
'news', 'tags', 'custom:', 'user:', 'search',]
def __init__(self, icon_dict):
for a in self.category_icons:
if a not in icon_dict:
@ -31,8 +31,8 @@ category_icon_map = {
'rating' : 'rating.png',
'news' : 'news.png',
'tags' : 'tags.png',
':custom' : 'column.png',
':user' : 'drawer.png',
'custom:' : 'column.png',
'user:' : 'drawer.png',
'search' : 'search.png'
}
@ -475,6 +475,8 @@ class FieldMetadata(dict):
val = self._tb_cats[key]
if val['is_category'] and val['kind'] in ('user', 'search'):
del self._tb_cats[key]
if key in self._search_term_map:
del self._search_term_map[key]
def cc_series_index_column_for(self, key):
return self._tb_cats[key]['rec_index'] + 1
@ -485,8 +487,9 @@ class FieldMetadata(dict):
self._tb_cats[label] = {'table':None, 'column':None,
'datatype':None, 'is_multiple':None,
'kind':'user', 'name':name,
'search_terms':[], 'is_custom':False,
'search_terms':[label],'is_custom':False,
'is_category':True}
self._add_search_terms_to_map(label, [label])
def add_search_category(self, label, name):
if label in self._tb_cats:

View File

@ -603,7 +603,7 @@ TXT input supports a number of options to differentiate how paragraphs are detec
formatting will be applied.
:guilabel:`Formatting Style: Heuristic`
Analyses the document for common chapter headings, scene breaks, and italicized words and applies the
Analyzes the document for common chapter headings, scene breaks, and italicized words and applies the
appropriate html markup during conversion.
:guilabel:`Formatting Style: Markdown`

View File

@ -478,6 +478,8 @@ Calibre has several keyboard shortcuts to save you time and mouse movement. Thes
- Focus the search bar
* - :kbd:`Shift+Ctrl+F`
- Open the advanced search dialog
* - :kbd:`Esc`
- Clear the current search
* - :kbd:`N or F3`
- Find the next book that matches the current search (only works if the highlight checkbox next to the search bar is checked)
* - :kbd:`Shift+N or Shift+F3`
@ -486,6 +488,8 @@ Calibre has several keyboard shortcuts to save you time and mouse movement. Thes
- Download metadata and shortcuts
* - :kbd:`Ctrl+R`
- Restart calibre
* - :kbd:`Shift+Ctrl+E`
- Add empty books to calibre
* - :kbd:`Ctrl+Q`
- Quit calibre

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More