mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Merge from trunk
This commit is contained in:
commit
9cc182c669
101
Changelog.yaml
101
Changelog.yaml
@ -4,6 +4,107 @@
|
||||
# for important features/bug fixes.
|
||||
# Also, each release can have new and improved recipes.
|
||||
|
||||
- version: 0.7.32
|
||||
date: 2010-12-03
|
||||
|
||||
new features:
|
||||
- title: "All new linux binary build. With updated libraries and replacing cx_Freeze with my own C python launcher code."
|
||||
|
||||
- title: "Edit metadata dialog: Add Next and Previous buttons and show cover size in tooltip"
|
||||
tickets: [7706, 7711]
|
||||
|
||||
- title: "A new custom column type: Enumeration. This column can take one of a user defined set of values."
|
||||
|
||||
- title: "PML Output: Add option to reduce image sizes/bit depth to allow PML Output to be used with DropBook"
|
||||
|
||||
- title: "TXT Output: Add option to generate Markdown output. Turn <br> tags into spaces."
|
||||
|
||||
- title: "Add a count function to the template language. Make author_sort searchable."
|
||||
|
||||
- title: "Various consistency and usability enhancements to the search box."
|
||||
tickets: [7726]
|
||||
description: >
|
||||
"Always select first book in result set of a search. Similar books searches added to search history. Search history order is no longer randomized. When focussing the search box with a keyboard shortcut, select all text. If you press enter in the search box, the search is executed and the book list os automatically focussed."
|
||||
|
||||
- title: "Driver for samsung fascinate and PocketBook 902"
|
||||
|
||||
- title: "FB2 Output: Add option to create FB2 sections based on internal file structure of input file (useful for EPUB files that have been split on chapter boundaries). Also add options to mark h1/h2/h3 tags as section titles in the FB2 file."
|
||||
tickets: [7738]
|
||||
|
||||
- title: "Metadata jacket: Add publisher information to jacket."
|
||||
|
||||
- title: "Catalog generation: Allow use of custom columns as well as tags to indicate read books. Note that your previously saved read books setting will be lost."
|
||||
|
||||
- title: "Bulk metadata edit dialog: Add an Apply button to allow you to perform multiple operations in sequence"
|
||||
|
||||
- title: "Allow drag and drop of books onto user categories. If you drag a book from a particular column (say authors) and drop it onto a user category, the column value will be added to the user category. So for authors, the authros will be added to the user category."
|
||||
|
||||
- title: "Check Library can now check and repair the has_cover cache"
|
||||
|
||||
- title: "Allow GUI plugins to be distributed in ZIP files. See http://www.mobileread.com/forums/showthread.php?t=108774"
|
||||
|
||||
- title: "Allow searching by the number of tags/authors/formats/etc. See User Manual for details."
|
||||
|
||||
- title: "Tiny speed up when loading large libraries and make various metadata editing tasks a little faster by reducing the number of times the Tag Browser is refreshed"
|
||||
|
||||
bug fixes:
|
||||
- title: "E-book viewer: Fix broken backwards searching"
|
||||
|
||||
- title: "Fix custom ratings column values being displayed incorrectly in book details area"
|
||||
tickets: [7740]
|
||||
|
||||
- title: "Fix book details dialog not using internal viewer to view ebooks"
|
||||
tickets: [7424]
|
||||
|
||||
- title: "MOBI Output: When the input document does not explicitly specify a size for images, set the size to be the natural size of the image. This works around Amazon's *truly wonderful* MOBI renderer's tendency to expand images that do not have a width and height specified."
|
||||
|
||||
- title: "Conversion pipeline: Fix bug that caused height/width specified in %/em of screen size to be incorrectly calculated by a factor of 72./DPI"
|
||||
|
||||
- title: "Conversion pipeline: Respect max-width and max-height when calculating the effective size of an element"
|
||||
|
||||
- title: "Conversion pipeline: Do not override CSS for images with the value of the img width/height attributes, unless no CSS is specified for the image"
|
||||
|
||||
- title: "E-book viewer: Resize automatically to fit on smaller screens"
|
||||
|
||||
- title: "Use the same MIME database on all platforms that calibre runs on, works around python 2.7's crazy insistence on reading MIME data from the registry"
|
||||
|
||||
- title: "Kobo driver: Allow html, txt and rtf documents to be deleted"
|
||||
|
||||
- title: "Always overwrite title/author metadata when downloading metadata for books added by ISBN"
|
||||
|
||||
- title: "Nook Color profile: Reduce screen height to 900px"
|
||||
|
||||
- title: "Fix regression that broke RTF conversion on some linux systems"
|
||||
|
||||
- title: "Fix bug that could break searching after copying and deleting a book from the current library"
|
||||
tickets: [7459]
|
||||
|
||||
improved recipes:
|
||||
- NZZ
|
||||
- Frankfurter Rundschau
|
||||
- JiJi Press
|
||||
- Revista Muy Intersante
|
||||
|
||||
new recipes:
|
||||
- title: "Global Times"
|
||||
author: "malfi"
|
||||
|
||||
- title: "The Philosopher's Magazine"
|
||||
author: "Darko Miletic"
|
||||
|
||||
- title: "Poughkeepsie Journal"
|
||||
author: "weebl"
|
||||
|
||||
- title: "Business Spectator and ABC Australia"
|
||||
author: "Dean Cording"
|
||||
|
||||
- title: "La Rijoa and NacionRed"
|
||||
author: "Arturo Martinez Nieves"
|
||||
|
||||
- title: "Animal Politico"
|
||||
author: "leamsi"
|
||||
|
||||
|
||||
- version: 0.7.31
|
||||
date: 2010-11-27
|
||||
|
||||
|
BIN
resources/images/news/tpm_uk.png
Normal file
BIN
resources/images/news/tpm_uk.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 873 B |
46
resources/recipes/globaltimes.recipe
Normal file
46
resources/recipes/globaltimes.recipe
Normal file
@ -0,0 +1,46 @@
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
import re
|
||||
|
||||
class globaltimes(BasicNewsRecipe):
|
||||
title = u'Global Times'
|
||||
__author__ = 'malfi'
|
||||
language = 'zh'
|
||||
oldest_article = 7
|
||||
max_articles_per_feed = 100
|
||||
no_stylesheets = True
|
||||
cover_url = 'http://enhimg2.huanqiu.com/images/logo.png'
|
||||
language = 'en'
|
||||
keep_only_tags = []
|
||||
keep_only_tags.append(dict(name = 'div', attrs = {'id': 'content'}))
|
||||
remove_tags = []
|
||||
remove_tags.append(dict(name = 'div', attrs = {'class': 'location'}))
|
||||
remove_tags.append(dict(name = 'div', attrs = {'class': 'contentpage'}))
|
||||
remove_tags.append(dict(name = 'li', attrs = {'id': 'pl'}))
|
||||
|
||||
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;}
|
||||
p{font-family:Arial,Helvetica,sans-serif;font-size:small;}
|
||||
body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
|
||||
'''
|
||||
def parse_index(self):
|
||||
catnames = {}
|
||||
catnames["http://china.globaltimes.cn/chinanews/"] = "China Politics"
|
||||
catnames["http://china.globaltimes.cn/diplomacy/"] = "China Diplomacy"
|
||||
catnames["http://military.globaltimes.cn/china/"] = "China Military"
|
||||
catnames["http://business.globaltimes.cn/china-economy/"] = "China Economy"
|
||||
catnames["http://world.globaltimes.cn/asia-pacific/"] = "Asia Pacific"
|
||||
feeds = []
|
||||
|
||||
for cat in catnames.keys():
|
||||
articles = []
|
||||
soup = self.index_to_soup(cat)
|
||||
for a in soup.findAll('a',attrs={'href' : re.compile(cat+"201[0-9]-[0-1][0-9]/[0-9][0-9][0-9][0-9][0-9][0-9].html")}):
|
||||
url = a['href'].strip()
|
||||
myarticle=({'title':self.tag_to_string(a), 'url':url, 'description':'', 'date':''})
|
||||
self.log("found %s" % url)
|
||||
articles.append(myarticle)
|
||||
self.log("Adding URL %s\n" %url)
|
||||
if articles:
|
||||
feeds.append((catnames[cat], articles))
|
||||
return feeds
|
@ -8,8 +8,8 @@ www.nin.co.rs
|
||||
import re
|
||||
from calibre import strftime
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
from contextlib import nested, closing
|
||||
from calibre.ebooks.BeautifulSoup import BeautifulSoup, NavigableString, CData, Tag
|
||||
from contextlib import closing
|
||||
from calibre.ebooks.BeautifulSoup import BeautifulSoup
|
||||
from calibre import entity_to_unicode
|
||||
|
||||
class Nin(BasicNewsRecipe):
|
||||
@ -29,14 +29,14 @@ class Nin(BasicNewsRecipe):
|
||||
use_embedded_content = False
|
||||
language = 'sr'
|
||||
publication_type = 'magazine'
|
||||
extra_css = """
|
||||
extra_css = """
|
||||
@font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)}
|
||||
body{font-family: Verdana, Lucida, sans1, sans-serif}
|
||||
.article_description{font-family: Verdana, Lucida, sans1, sans-serif}
|
||||
.artTitle{font-size: x-large; font-weight: bold; color: #900}
|
||||
.izjava{font-size: x-large; font-weight: bold}
|
||||
.columnhead{font-size: small; font-weight: bold;}
|
||||
img{margin-top:0.5em; margin-bottom: 0.7em; display: block}
|
||||
body{font-family: Verdana, Lucida, sans1, sans-serif}
|
||||
.article_description{font-family: Verdana, Lucida, sans1, sans-serif}
|
||||
.artTitle{font-size: x-large; font-weight: bold; color: #900}
|
||||
.izjava{font-size: x-large; font-weight: bold}
|
||||
.columnhead{font-size: small; font-weight: bold;}
|
||||
img{margin-top:0.5em; margin-bottom: 0.7em; display: block}
|
||||
b{margin-top: 1em}
|
||||
"""
|
||||
|
||||
@ -148,4 +148,4 @@ class Nin(BasicNewsRecipe):
|
||||
img.extract()
|
||||
tbl.replaceWith(img)
|
||||
return soup
|
||||
|
||||
|
||||
|
@ -282,9 +282,9 @@ class NYTimes(BasicNewsRecipe):
|
||||
br = BasicNewsRecipe.get_browser()
|
||||
if self.username is not None and self.password is not None:
|
||||
br.open('http://www.nytimes.com/auth/login')
|
||||
br.select_form(name='login')
|
||||
br['USERID'] = self.username
|
||||
br['PASSWORD'] = self.password
|
||||
br.form = br.forms().next()
|
||||
br['userid'] = self.username
|
||||
br['password'] = self.password
|
||||
raw = br.submit().read()
|
||||
if 'Please try again' in raw:
|
||||
raise Exception('Your username and password are incorrect')
|
||||
|
@ -282,9 +282,9 @@ class NYTimes(BasicNewsRecipe):
|
||||
br = BasicNewsRecipe.get_browser()
|
||||
if self.username is not None and self.password is not None:
|
||||
br.open('http://www.nytimes.com/auth/login')
|
||||
br.select_form(name='login')
|
||||
br['USERID'] = self.username
|
||||
br['PASSWORD'] = self.password
|
||||
br.form = br.forms().next()
|
||||
br['userid'] = self.username
|
||||
br['password'] = self.password
|
||||
raw = br.submit().read()
|
||||
if 'Please try again' in raw:
|
||||
raise Exception('Your username and password are incorrect')
|
||||
|
@ -1,6 +1,6 @@
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>'
|
||||
__copyright__ = '2009-2010, Darko Miletic <darko.miletic at gmail.com>'
|
||||
|
||||
'''
|
||||
www.nzz.ch
|
||||
@ -20,6 +20,19 @@ class Nzz(BasicNewsRecipe):
|
||||
encoding = 'utf-8'
|
||||
use_embedded_content = False
|
||||
language = 'de'
|
||||
extra_css = """
|
||||
body{font-family: Georgia,"Times New Roman",Times,serif }
|
||||
.artikel h3,.artikel h4,.bildLegende,.question,.autor{font-family: Arial,Verdana,Helvetica,sans-serif}
|
||||
.bildLegende{font-size: small}
|
||||
.autor{font-size: 0.9375em; color: #666666}
|
||||
.quote{font-size: large !important;
|
||||
font-style: italic;
|
||||
font-weight: normal !important;
|
||||
border-bottom: 1px dotted #BFBFBF;
|
||||
border-top: 1px dotted #BFBFBF;
|
||||
line-height: 1.25em}
|
||||
.quelle{color: #666666; font-style: italic; white-space: nowrap}
|
||||
"""
|
||||
|
||||
conversion_options = {
|
||||
'comments' : description
|
||||
@ -28,12 +41,14 @@ class Nzz(BasicNewsRecipe):
|
||||
,'publisher' : publisher
|
||||
}
|
||||
|
||||
keep_only_tags = [dict(name='div', attrs={'class':'article'})]
|
||||
|
||||
keep_only_tags = [dict(name='div', attrs={'class':'zone'})]
|
||||
remove_tags_before = dict(name='p', attrs={'class':'dachzeile'})
|
||||
remove_tags_after=dict(name='p', attrs={'class':'fussnote'})
|
||||
remove_attributes=['width','height','lang']
|
||||
remove_tags = [
|
||||
dict(name=['object','link','base'])
|
||||
,dict(name='div',attrs={'class':['more','teaser','advXertXoriXals','legal']})
|
||||
,dict(name='div',attrs={'id':['popup-src','readercomments','google-ad','advXertXoriXals']})
|
||||
dict(name=['object','link','base','meta','iframe'])
|
||||
,dict(attrs={'id':'content_rectangle_1'})
|
||||
,dict(attrs={'class':['weiterfuehrendeLinks','fussnote','video']})
|
||||
]
|
||||
|
||||
feeds = [
|
||||
@ -50,7 +65,7 @@ class Nzz(BasicNewsRecipe):
|
||||
,(u'Reisen' , u'http://www.nzz.ch/magazin/reisen?rss=true')
|
||||
]
|
||||
|
||||
def print_version(self, url):
|
||||
return url + '?printview=true'
|
||||
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
for item in soup.findAll(style=True):
|
||||
del item['style']
|
||||
return self.adeify_images(soup)
|
||||
|
19
resources/recipes/poughkeepsie_journal.recipe
Normal file
19
resources/recipes/poughkeepsie_journal.recipe
Normal file
@ -0,0 +1,19 @@
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class AdvancedUserRecipe1291143841(BasicNewsRecipe):
|
||||
title = u'Poughkeepsipe Journal'
|
||||
language = 'en'
|
||||
__author__ = 'weebl'
|
||||
oldest_article = 7
|
||||
max_articles_per_feed = 100
|
||||
no_stylesheets = True
|
||||
timefmt = ' [%a, %d %b, %Y]'
|
||||
feeds = [(u'Local News', u'http://poughkeepsiejournal.com/apps/pbcs.dll/oversikt?Category=RSS01&mime=xml'),
|
||||
(u'Local Business', u'http://poughkeepsiejournal.com/apps/pbcs.dll/oversikt?Category=RSS02&mime=xml'),
|
||||
(u'Local Sports', u'http://poughkeepsiejournal.com/apps/pbcs.dll/oversikt?Category=RSS03&mime=xml'),
|
||||
(u'Life', u'http://poughkeepsiejournal.com/apps/pbcs.dll/oversikt?Category=RSS04&mime=xml')]
|
||||
remove_tags = [dict(name='img', attrs={'src':'/graphics/mastlogo.gif'})]
|
||||
|
||||
def print_version(self, url):
|
||||
return url.replace('http://www.poughkeepsiejournal.com', 'http://www.poughkeepsiejournal.com/print')
|
||||
|
72
resources/recipes/tpm_uk.recipe
Normal file
72
resources/recipes/tpm_uk.recipe
Normal file
@ -0,0 +1,72 @@
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
|
||||
'''
|
||||
www.philosophypress.co.uk
|
||||
'''
|
||||
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class TPM_uk(BasicNewsRecipe):
|
||||
title = "The Philosophers' Magazine"
|
||||
__author__ = 'Darko Miletic'
|
||||
description = 'Title says it all'
|
||||
publisher = "The Philosophers' Magazine"
|
||||
category = 'philosophy, news'
|
||||
oldest_article = 25
|
||||
max_articles_per_feed = 200
|
||||
no_stylesheets = True
|
||||
encoding = 'utf8'
|
||||
use_embedded_content = False
|
||||
language = 'en_GB'
|
||||
remove_empty_feeds = True
|
||||
publication_type = 'magazine'
|
||||
masthead_url = 'http://www.philosophypress.co.uk/wp-content/themes/masterplan/tma/images/bg/sitelogo.png'
|
||||
extra_css = """
|
||||
body{font-family: Helvetica,Arial,"Lucida Grande",Verdana,sans-serif }
|
||||
img{margin-bottom: 0.4em; display:block}
|
||||
"""
|
||||
|
||||
conversion_options = {
|
||||
'comment' : description
|
||||
, 'tags' : category
|
||||
, 'publisher' : publisher
|
||||
, 'language' : language
|
||||
}
|
||||
|
||||
remove_tags = [
|
||||
dict(name=['meta','link','base','iframe','embed','object','img'])
|
||||
,dict(attrs={'id':['respond','sharethis_0']})
|
||||
,dict(attrs={'class':'wp-caption-text'})
|
||||
]
|
||||
keep_only_tags=[
|
||||
dict(attrs={'class':['post_cat','post_name','post_meta','post_text']})
|
||||
,dict(attrs={'id':'comments'})
|
||||
]
|
||||
remove_attributes=['lang','width','height']
|
||||
|
||||
|
||||
feeds = [
|
||||
(u'Columns' , u'http://www.philosophypress.co.uk/?feed=rss2&cat=15' )
|
||||
,(u'Essays' , u'http://www.philosophypress.co.uk/?feed=rss2&cat=19' )
|
||||
,(u"21'st Century" , u'http://www.philosophypress.co.uk/?feed=rss2&cat=101')
|
||||
,(u'Interviews' , u'http://www.philosophypress.co.uk/?feed=rss2&cat=9' )
|
||||
,(u'News' , u'http://www.philosophypress.co.uk/?feed=rss2&cat=28' )
|
||||
,(u'Profiles' , u'http://www.philosophypress.co.uk/?feed=rss2&cat=59' )
|
||||
,(u'Reviews' , u'http://www.philosophypress.co.uk/?feed=rss2&cat=12' )
|
||||
]
|
||||
|
||||
def get_cover_url(self):
|
||||
soup = self.index_to_soup('http://www.philosophypress.co.uk/')
|
||||
for image in soup.findAll('img',title=True):
|
||||
if image['title'].startswith('Click to Subscribe'):
|
||||
return image['src']
|
||||
return None
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
for item in soup.findAll(style=True):
|
||||
del item['style']
|
||||
for alink in soup.findAll('a', rel=True):
|
||||
if alink.string is not None:
|
||||
tstr = alink.string
|
||||
alink.replaceWith(tstr)
|
||||
return soup
|
@ -2,7 +2,7 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
__appname__ = 'calibre'
|
||||
__version__ = '0.7.31'
|
||||
__version__ = '0.7.32'
|
||||
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
||||
|
||||
import re
|
||||
|
@ -37,6 +37,8 @@ class Plugin(_Plugin):
|
||||
self.fsizes.append((name, num, float(size)))
|
||||
self.fnames = dict((name, sz) for name, _, sz in self.fsizes if name)
|
||||
self.fnums = dict((num, sz) for _, num, sz in self.fsizes if num)
|
||||
self.width_pts = self.width * 72./self.dpi
|
||||
self.height_pts = self.height * 72./self.dpi
|
||||
|
||||
# Input profiles {{{
|
||||
class InputProfile(Plugin):
|
||||
|
@ -531,6 +531,8 @@ class Metadata(object):
|
||||
res = format_date(res, cmeta['display'].get('date_format','dd MMM yyyy'))
|
||||
elif datatype == 'bool':
|
||||
res = _('Yes') if res else _('No')
|
||||
elif datatype == 'rating':
|
||||
res = res/2
|
||||
return (name, unicode(res), orig_res, cmeta)
|
||||
|
||||
# Translate aliases into the standard field name
|
||||
|
@ -10,9 +10,10 @@ import copy
|
||||
import re
|
||||
from lxml import etree
|
||||
from calibre.ebooks.oeb.base import namespace, barename
|
||||
from calibre.ebooks.oeb.base import XHTML, XHTML_NS, OEB_DOCS
|
||||
from calibre.ebooks.oeb.base import XHTML, XHTML_NS, OEB_DOCS, urlnormalize
|
||||
from calibre.ebooks.oeb.stylizer import Stylizer
|
||||
from calibre.ebooks.oeb.transforms.flatcss import KeyMapper
|
||||
from calibre.utils.magick.draw import identify_data
|
||||
|
||||
MBP_NS = 'http://mobipocket.com/ns/mbp'
|
||||
def MBP(name): return '{%s}%s' % (MBP_NS, name)
|
||||
@ -121,6 +122,7 @@ class MobiMLizer(object):
|
||||
body = item.data.find(XHTML('body'))
|
||||
nroot = etree.Element(XHTML('html'), nsmap=MOBI_NSMAP)
|
||||
nbody = etree.SubElement(nroot, XHTML('body'))
|
||||
self.current_spine_item = item
|
||||
self.mobimlize_elem(body, stylizer, BlockState(nbody),
|
||||
[FormatState()])
|
||||
item.data = nroot
|
||||
@ -357,8 +359,9 @@ class MobiMLizer(object):
|
||||
if tag == 'img' and 'src' in elem.attrib:
|
||||
istate.attrib['src'] = elem.attrib['src']
|
||||
istate.attrib['align'] = 'baseline'
|
||||
cssdict = style.cssdict()
|
||||
for prop in ('width', 'height'):
|
||||
if style[prop] != 'auto':
|
||||
if cssdict[prop] != 'auto':
|
||||
value = style[prop]
|
||||
if value == getattr(self.profile, prop):
|
||||
result = '100%'
|
||||
@ -371,8 +374,40 @@ class MobiMLizer(object):
|
||||
(72./self.profile.dpi)))
|
||||
except:
|
||||
continue
|
||||
result = "%d"%pixs
|
||||
result = str(pixs)
|
||||
istate.attrib[prop] = result
|
||||
if 'width' not in istate.attrib or 'height' not in istate.attrib:
|
||||
href = self.current_spine_item.abshref(elem.attrib['src'])
|
||||
try:
|
||||
item = self.oeb.manifest.hrefs[urlnormalize(href)]
|
||||
except:
|
||||
self.oeb.logger.warn('Failed to find image:',
|
||||
href)
|
||||
else:
|
||||
try:
|
||||
width, height = identify_data(item.data)[:2]
|
||||
except:
|
||||
self.oeb.logger.warn('Invalid image:', href)
|
||||
else:
|
||||
if 'width' not in istate.attrib and 'height' not in \
|
||||
istate.attrib:
|
||||
istate.attrib['width'] = str(width)
|
||||
istate.attrib['height'] = str(height)
|
||||
else:
|
||||
ar = float(width)/float(height)
|
||||
if 'width' not in istate.attrib:
|
||||
try:
|
||||
width = int(istate.attrib['height'])*ar
|
||||
except:
|
||||
pass
|
||||
istate.attrib['width'] = str(int(width))
|
||||
else:
|
||||
try:
|
||||
height = int(istate.attrib['width'])/ar
|
||||
except:
|
||||
pass
|
||||
istate.attrib['height'] = str(int(height))
|
||||
item.unload_data_from_memory()
|
||||
elif tag == 'hr' and asfloat(style['width']) > 0:
|
||||
prop = style['width'] / self.profile.width
|
||||
istate.attrib['width'] = "%d%%" % int(round(prop * 100))
|
||||
|
@ -96,7 +96,10 @@ class EbookIterator(object):
|
||||
|
||||
def search(self, text, index, backwards=False):
|
||||
text = text.lower()
|
||||
for i, path in enumerate(self.spine):
|
||||
pmap = [(i, path) for i, path in enumerate(self.spine)]
|
||||
if backwards:
|
||||
pmap.reverse()
|
||||
for i, path in pmap:
|
||||
if (backwards and i < index) or (not backwards and i > index):
|
||||
if text in open(path, 'rb').read().decode(path.encoding).lower():
|
||||
return i
|
||||
|
@ -253,7 +253,10 @@ class Stylizer(object):
|
||||
upd = {}
|
||||
for prop in ('width', 'height'):
|
||||
val = elem.get(prop, '').strip()
|
||||
del elem.attrib[prop]
|
||||
try:
|
||||
del elem.attrib[prop]
|
||||
except:
|
||||
pass
|
||||
if val:
|
||||
if num_pat.match(val) is not None:
|
||||
val += 'px'
|
||||
@ -572,7 +575,7 @@ class Style(object):
|
||||
if parent is not None:
|
||||
base = parent.width
|
||||
else:
|
||||
base = self._profile.width
|
||||
base = self._profile.width_pts
|
||||
if 'width' in self._element.attrib:
|
||||
width = self._element.attrib['width']
|
||||
elif 'width' in self._style:
|
||||
@ -584,6 +587,13 @@ class Style(object):
|
||||
if isinstance(result, (unicode, str, bytes)):
|
||||
result = self._profile.width
|
||||
self._width = result
|
||||
if 'max-width' in self._style:
|
||||
result = self._unit_convert(self._style['max-width'], base=base)
|
||||
if isinstance(result, (unicode, str, bytes)):
|
||||
result = self._width
|
||||
if result < self._width:
|
||||
self._width = result
|
||||
|
||||
return self._width
|
||||
|
||||
@property
|
||||
@ -595,7 +605,7 @@ class Style(object):
|
||||
if parent is not None:
|
||||
base = parent.height
|
||||
else:
|
||||
base = self._profile.height
|
||||
base = self._profile.height_pts
|
||||
if 'height' in self._element.attrib:
|
||||
height = self._element.attrib['height']
|
||||
elif 'height' in self._style:
|
||||
@ -607,6 +617,13 @@ class Style(object):
|
||||
if isinstance(result, (unicode, str, bytes)):
|
||||
result = self._profile.height
|
||||
self._height = result
|
||||
if 'max-height' in self._style:
|
||||
result = self._unit_convert(self._style['max-height'], base=base)
|
||||
if isinstance(result, (unicode, str, bytes)):
|
||||
result = self._height
|
||||
if result < self._height:
|
||||
self._height = result
|
||||
|
||||
return self._height
|
||||
|
||||
@property
|
||||
|
@ -35,6 +35,12 @@ class PMLOutput(OutputFormatPlugin):
|
||||
OptionRecommendation(name='inline_toc',
|
||||
recommended_value=False, level=OptionRecommendation.LOW,
|
||||
help=_('Add Table of Contents to beginning of the book.')),
|
||||
OptionRecommendation(name='full_image_depth',
|
||||
recommended_value=False, level=OptionRecommendation.LOW,
|
||||
help=_('Do not reduce the size or bit depth of images. Images ' \
|
||||
'have their size and depth reduced by default to accommodate ' \
|
||||
'applications that can not convert images on their ' \
|
||||
'own such as Dropbook.')),
|
||||
])
|
||||
|
||||
def convert(self, oeb_book, output_path, input_plugin, opts, log):
|
||||
@ -44,16 +50,20 @@ class PMLOutput(OutputFormatPlugin):
|
||||
with open(os.path.join(tdir, 'index.pml'), 'wb') as out:
|
||||
out.write(pml.encode(opts.output_encoding, 'replace'))
|
||||
|
||||
self.write_images(oeb_book.manifest, pmlmlizer.image_hrefs, tdir)
|
||||
self.write_images(oeb_book.manifest, pmlmlizer.image_hrefs, tdir, opts)
|
||||
|
||||
log.debug('Compressing output...')
|
||||
pmlz = ZipFile(output_path, 'w')
|
||||
pmlz.add_dir(tdir)
|
||||
|
||||
def write_images(self, manifest, image_hrefs, out_dir):
|
||||
def write_images(self, manifest, image_hrefs, out_dir, opts):
|
||||
for item in manifest:
|
||||
if item.media_type in OEB_RASTER_IMAGES and item.href in image_hrefs.keys():
|
||||
im = Image.open(cStringIO.StringIO(item.data))
|
||||
if opts.full_image_depth:
|
||||
im = Image.open(cStringIO.StringIO(item.data))
|
||||
else:
|
||||
im = Image.open(cStringIO.StringIO(item.data)).convert('P')
|
||||
im.thumbnail((300,300), Image.ANTIALIAS)
|
||||
|
||||
data = cStringIO.StringIO()
|
||||
im.save(data, 'PNG')
|
||||
|
63
src/calibre/ebooks/txt/markdownml.py
Normal file
63
src/calibre/ebooks/txt/markdownml.py
Normal file
@ -0,0 +1,63 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__license__ = 'GPL 3'
|
||||
__copyright__ = '2009, John Schember <john@nachtimwald.com>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
'''
|
||||
Transform OEB content into Markdown formatted plain text
|
||||
'''
|
||||
|
||||
import re
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from calibre.utils.html2text import html2text
|
||||
|
||||
class MarkdownMLizer(object):
|
||||
|
||||
def __init__(self, log):
|
||||
self.log = log
|
||||
|
||||
def extract_content(self, oeb_book, opts):
|
||||
self.log.info('Converting XHTML to Markdown formatted TXT...')
|
||||
self.oeb_book = oeb_book
|
||||
self.opts = opts
|
||||
|
||||
return self.mlize_spine()
|
||||
|
||||
def mlize_spine(self):
|
||||
output = [u'']
|
||||
|
||||
for item in self.oeb_book.spine:
|
||||
self.log.debug('Converting %s to Markdown formatted TXT...' % item.href)
|
||||
|
||||
html = unicode(etree.tostring(item.data, encoding=unicode))
|
||||
|
||||
if not self.opts.keep_links:
|
||||
html = re.sub(r'<\s*a[^>]*>', '', html)
|
||||
html = re.sub(r'<\s*/\s*a\s*>', '', html)
|
||||
if not self.opts.keep_image_references:
|
||||
html = re.sub(r'<\s*img[^>]*>', '', html)
|
||||
html = re.sub(r'<\s*img\s*>', '', html)
|
||||
|
||||
text = html2text(html)
|
||||
|
||||
# Ensure the section ends with at least two new line characters.
|
||||
# This is to prevent the last paragraph from a section being
|
||||
# combined into the fist paragraph of the next.
|
||||
end_chars = text[-4:]
|
||||
# Convert all newlines to \n
|
||||
end_chars = end_chars.replace('\r\n', '\n')
|
||||
end_chars = end_chars.replace('\r', '\n')
|
||||
end_chars = end_chars[-2:]
|
||||
if not end_chars[1] == '\n':
|
||||
text += '\n\n'
|
||||
if end_chars[1] == '\n' and not end_chars[0] == '\n':
|
||||
text += '\n'
|
||||
|
||||
output += text
|
||||
|
||||
output = u''.join(output)
|
||||
|
||||
return output
|
@ -8,6 +8,7 @@ import os
|
||||
|
||||
from calibre.customize.conversion import OutputFormatPlugin, \
|
||||
OptionRecommendation
|
||||
from calibre.ebooks.txt.markdownml import MarkdownMLizer
|
||||
from calibre.ebooks.txt.txtml import TXTMLizer
|
||||
from calibre.ebooks.txt.newlines import TxtNewlines, specified_newlines
|
||||
|
||||
@ -44,10 +45,27 @@ class TXTOutput(OutputFormatPlugin):
|
||||
recommended_value=False, level=OptionRecommendation.LOW,
|
||||
help=_('Force splitting on the max-line-length value when no space '
|
||||
'is present. Also allows max-line-length to be below the minimum')),
|
||||
OptionRecommendation(name='markdown_format',
|
||||
recommended_value=False, level=OptionRecommendation.LOW,
|
||||
help=_('Produce Markdown formatted text.')),
|
||||
OptionRecommendation(name='keep_links',
|
||||
recommended_value=False, level=OptionRecommendation.LOW,
|
||||
help=_('Do not remove links within the document. This is only ' \
|
||||
'useful when paired with the markdown-format option because' \
|
||||
'links are always removed with plain text output.')),
|
||||
OptionRecommendation(name='keep_image_references',
|
||||
recommended_value=False, level=OptionRecommendation.LOW,
|
||||
help=_('Do not remove image references within the document. This is only ' \
|
||||
'useful when paired with the markdown-format option because' \
|
||||
'image references are always removed with plain text output.')),
|
||||
])
|
||||
|
||||
def convert(self, oeb_book, output_path, input_plugin, opts, log):
|
||||
writer = TXTMLizer(log)
|
||||
if opts.markdown_format:
|
||||
writer = MarkdownMLizer(log)
|
||||
else:
|
||||
writer = TXTMLizer(log)
|
||||
|
||||
txt = writer.extract_content(oeb_book, opts)
|
||||
|
||||
log.debug('\tReplacing newlines with selected type...')
|
||||
|
@ -35,6 +35,7 @@ BLOCK_STYLES = [
|
||||
|
||||
SPACE_TAGS = [
|
||||
'td',
|
||||
'br',
|
||||
]
|
||||
|
||||
class TXTMLizer(object):
|
||||
|
@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en'
|
||||
import os
|
||||
from functools import partial
|
||||
|
||||
from PyQt4.Qt import Qt, QMenu
|
||||
from PyQt4.Qt import Qt, QMenu, QModelIndex
|
||||
|
||||
from calibre.gui2 import error_dialog, config
|
||||
from calibre.gui2.dialogs.metadata_single import MetadataSingleDialog
|
||||
@ -126,20 +126,35 @@ class EditMetadataAction(InterfaceAction):
|
||||
if bulk or (bulk is None and len(rows) > 1):
|
||||
return self.edit_bulk_metadata(checked)
|
||||
|
||||
def accepted(id):
|
||||
self.gui.library_view.model().refresh_ids([id])
|
||||
row_list = [r.row() for r in rows]
|
||||
current_row = 0
|
||||
changed = set([])
|
||||
db = self.gui.library_view.model().db
|
||||
|
||||
for row in rows:
|
||||
self.gui.iactions['View'].metadata_view_id = self.gui.library_view.model().db.id(row.row())
|
||||
d = MetadataSingleDialog(self.gui, row.row(),
|
||||
self.gui.library_view.model().db,
|
||||
accepted_callback=accepted,
|
||||
cancel_all=rows.index(row) < len(rows)-1)
|
||||
d.view_format.connect(self.gui.iactions['View'].metadata_view_format)
|
||||
d.exec_()
|
||||
if d.cancel_all:
|
||||
if len(row_list) == 1:
|
||||
cr = row_list[0]
|
||||
row_list = \
|
||||
list(range(self.gui.library_view.model().rowCount(QModelIndex())))
|
||||
current_row = row_list.index(cr)
|
||||
|
||||
while True:
|
||||
prev = next_ = None
|
||||
if current_row > 0:
|
||||
prev = db.title(row_list[current_row-1])
|
||||
if current_row < len(row_list) - 1:
|
||||
next_ = db.title(row_list[current_row+1])
|
||||
|
||||
d = MetadataSingleDialog(self.gui, row_list[current_row], db,
|
||||
prev=prev, next_=next_)
|
||||
if d.exec_() != d.Accepted:
|
||||
break
|
||||
if rows:
|
||||
changed.add(d.id)
|
||||
if d.row_delta == 0:
|
||||
break
|
||||
current_row += d.row_delta
|
||||
|
||||
if changed:
|
||||
self.gui.library_view.model().refresh_ids(list(changed))
|
||||
current = self.gui.library_view.currentIndex()
|
||||
m = self.gui.library_view.model()
|
||||
if self.gui.cover_flow:
|
||||
|
@ -29,5 +29,6 @@ class ShowBookDetailsAction(InterfaceAction):
|
||||
return
|
||||
index = self.gui.library_view.currentIndex()
|
||||
if index.isValid():
|
||||
BookInfo(self.gui, self.gui.library_view, index).show()
|
||||
BookInfo(self.gui, self.gui.library_view, index,
|
||||
self.gui.iactions['View'].view_format_by_id).show()
|
||||
|
||||
|
22
src/calibre/gui2/convert/pml_output.py
Normal file
22
src/calibre/gui2/convert/pml_output.py
Normal file
@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__license__ = 'GPL 3'
|
||||
__copyright__ = '2009, John Schember <john@nachtimwald.com>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
from calibre.gui2.convert.pmlz_output_ui import Ui_Form
|
||||
from calibre.gui2.convert import Widget
|
||||
|
||||
format_model = None
|
||||
|
||||
class PluginWidget(Widget, Ui_Form):
|
||||
|
||||
TITLE = _('PMLZ Output')
|
||||
HELP = _('Options specific to')+' PMLZ '+_('output')
|
||||
COMMIT_NAME = 'pmlz_output'
|
||||
ICON = I('mimetypes/unknown.png')
|
||||
|
||||
def __init__(self, parent, get_option, get_help, db=None, book_id=None):
|
||||
Widget.__init__(self, parent, ['inline_toc', 'full_image_depth'])
|
||||
self.db, self.book_id = db, book_id
|
||||
self.initialize_options(get_option, get_help, db, book_id)
|
48
src/calibre/gui2/convert/pmlz_output.ui
Normal file
48
src/calibre/gui2/convert/pmlz_output.ui
Normal file
@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>400</width>
|
||||
<height>300</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="2" column="0">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>246</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QCheckBox" name="opt_inline_toc">
|
||||
<property name="text">
|
||||
<string>&Inline TOC</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QCheckBox" name="opt_full_image_depth">
|
||||
<property name="text">
|
||||
<string>Do not reduce image size and depth</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
@ -21,7 +21,7 @@ class PluginWidget(Widget, Ui_Form):
|
||||
def __init__(self, parent, get_option, get_help, db=None, book_id=None):
|
||||
Widget.__init__(self, parent,
|
||||
['newline', 'max_line_length', 'force_max_line_length',
|
||||
'inline_toc'])
|
||||
'inline_toc', 'markdown_format', 'keep_links', 'keep_image_references'])
|
||||
self.db, self.book_id = db, book_id
|
||||
self.initialize_options(get_option, get_help, db, book_id)
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>400</width>
|
||||
<width>477</width>
|
||||
<height>300</height>
|
||||
</rect>
|
||||
</property>
|
||||
@ -27,7 +27,7 @@
|
||||
<item row="0" column="1">
|
||||
<widget class="QComboBox" name="opt_newline"/>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<item row="7" column="0">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
@ -67,6 +67,27 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QCheckBox" name="opt_markdown_format">
|
||||
<property name="text">
|
||||
<string>Apply Markdown formatting to text</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QCheckBox" name="opt_keep_links">
|
||||
<property name="text">
|
||||
<string>Do not remove links (<a> tags) before processing</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
<widget class="QCheckBox" name="opt_keep_image_references">
|
||||
<property name="text">
|
||||
<string>Do not remove image references before processing</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
|
@ -15,7 +15,7 @@ from PyQt4.Qt import QComboBox, QLabel, QSpinBox, QDoubleSpinBox, QDateEdit, \
|
||||
|
||||
from calibre.utils.date import qt_to_dt, now
|
||||
from calibre.gui2.widgets import TagsLineEdit, EnComboBox
|
||||
from calibre.gui2 import UNDEFINED_QDATE
|
||||
from calibre.gui2 import UNDEFINED_QDATE, error_dialog
|
||||
from calibre.utils.config import tweaks
|
||||
|
||||
class Base(object):
|
||||
@ -310,6 +310,49 @@ class Series(Base):
|
||||
self.db.set_custom(book_id, val, extra=s_index,
|
||||
num=self.col_id, notify=notify, commit=False)
|
||||
|
||||
class Enumeration(Base):
|
||||
|
||||
def setup_ui(self, parent):
|
||||
self.parent = parent
|
||||
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent),
|
||||
QComboBox(parent)]
|
||||
w = self.widgets[1]
|
||||
vals = self.col_metadata['display']['enum_values']
|
||||
w.addItem('')
|
||||
for v in vals:
|
||||
w.addItem(v)
|
||||
|
||||
def initialize(self, book_id):
|
||||
val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
|
||||
val = self.normalize_db_val(val)
|
||||
self.initial_val = val
|
||||
idx = self.widgets[1].findText(val)
|
||||
if idx < 0:
|
||||
error_dialog(self.parent, '',
|
||||
_('The enumeration "{0}" contains an invalid value '
|
||||
'that will be set to the default').format(
|
||||
self.col_metadata['name']),
|
||||
show=True, show_copy_button=False)
|
||||
|
||||
idx = 0
|
||||
self.widgets[1].setCurrentIndex(idx)
|
||||
|
||||
def setter(self, val):
|
||||
self.widgets[1].setCurrentIndex(self.widgets[1].findText(val))
|
||||
|
||||
def getter(self):
|
||||
return unicode(self.widgets[1].currentText())
|
||||
|
||||
def normalize_db_val(self, val):
|
||||
if val is None:
|
||||
val = ''
|
||||
return val
|
||||
|
||||
def normalize_ui_val(self, val):
|
||||
if not val:
|
||||
val = None
|
||||
return val
|
||||
|
||||
widgets = {
|
||||
'bool' : Bool,
|
||||
'rating' : Rating,
|
||||
@ -319,6 +362,7 @@ widgets = {
|
||||
'text' : Text,
|
||||
'comments': Comments,
|
||||
'series': Series,
|
||||
'enumeration': Enumeration
|
||||
}
|
||||
|
||||
def field_sort(y, z, x=None):
|
||||
@ -551,6 +595,61 @@ class BulkSeries(BulkBase):
|
||||
self.db.set_custom_bulk(book_ids, val, extras=extras,
|
||||
num=self.col_id, notify=notify)
|
||||
|
||||
class BulkEnumeration(BulkBase, Enumeration):
|
||||
|
||||
def get_initial_value(self, book_ids):
|
||||
value = None
|
||||
ret_value = None
|
||||
dialog_shown = False
|
||||
for book_id in book_ids:
|
||||
val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
|
||||
if val and val not in self.col_metadata['display']['enum_values']:
|
||||
if not dialog_shown:
|
||||
error_dialog(self.parent, '',
|
||||
_('The enumeration "{0}" contains invalid values '
|
||||
'that will not appear in the list').format(
|
||||
self.col_metadata['name']),
|
||||
show=True, show_copy_button=False)
|
||||
dialog_shown = True
|
||||
ret_value = ' nochange '
|
||||
elif value is not None and value != val:
|
||||
ret_value = ' nochange '
|
||||
value = val
|
||||
if ret_value is None:
|
||||
return value
|
||||
return ret_value
|
||||
|
||||
def setup_ui(self, parent):
|
||||
self.parent = parent
|
||||
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent),
|
||||
QComboBox(parent)]
|
||||
w = self.widgets[1]
|
||||
vals = self.col_metadata['display']['enum_values']
|
||||
w.addItem('Do Not Change')
|
||||
w.addItem('')
|
||||
for v in vals:
|
||||
w.addItem(v)
|
||||
|
||||
def getter(self):
|
||||
if self.widgets[1].currentIndex() == 0:
|
||||
return ' nochange '
|
||||
return unicode(self.widgets[1].currentText())
|
||||
|
||||
def setter(self, val):
|
||||
if val == ' nochange ':
|
||||
self.widgets[1].setCurrentIndex(0)
|
||||
else:
|
||||
if val is None:
|
||||
self.widgets[1].setCurrentIndex(1)
|
||||
else:
|
||||
self.widgets[1].setCurrentIndex(self.widgets[1].findText(val))
|
||||
|
||||
def commit(self, book_ids, notify=False):
|
||||
val = self.gui_val
|
||||
val = self.normalize_ui_val(val)
|
||||
if val != self.initial_val and val != ' nochange ':
|
||||
self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify)
|
||||
|
||||
class RemoveTags(QWidget):
|
||||
|
||||
def __init__(self, parent, values):
|
||||
@ -656,4 +755,5 @@ bulk_widgets = {
|
||||
'datetime': BulkDateTime,
|
||||
'text' : BulkText,
|
||||
'series': BulkSeries,
|
||||
'enumeration': BulkEnumeration,
|
||||
}
|
||||
|
@ -15,12 +15,13 @@ from calibre.library.comments import comments_to_html
|
||||
|
||||
class BookInfo(QDialog, Ui_BookInfo):
|
||||
|
||||
def __init__(self, parent, view, row):
|
||||
def __init__(self, parent, view, row, view_func):
|
||||
QDialog.__init__(self, parent)
|
||||
Ui_BookInfo.__init__(self)
|
||||
self.setupUi(self)
|
||||
self.cover_pixmap = None
|
||||
self.comments.sizeHint = self.comments_size_hint
|
||||
self.view_func = view_func
|
||||
|
||||
desktop = QCoreApplication.instance().desktop()
|
||||
screen_height = desktop.availableGeometry().height() - 100
|
||||
@ -58,10 +59,7 @@ class BookInfo(QDialog, Ui_BookInfo):
|
||||
if os.sep in path:
|
||||
open_local_file(path)
|
||||
else:
|
||||
path = self.view.model().db.format_abspath(self.current_row, path)
|
||||
if path is not None:
|
||||
open_local_file(path)
|
||||
|
||||
self.view_func(self.view.model().id(self.current_row), path)
|
||||
|
||||
def next(self):
|
||||
row = self.view.currentIndex().row()
|
||||
|
@ -255,7 +255,7 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
|
||||
fm = self.db.field_metadata
|
||||
for f in fm:
|
||||
if (f in ['author_sort'] or
|
||||
(fm[f]['datatype'] in ['text', 'series']
|
||||
(fm[f]['datatype'] in ['text', 'series', 'enumeration']
|
||||
and fm[f].get('search_terms', None)
|
||||
and f not in ['formats', 'ondevice', 'sort'])):
|
||||
self.all_fields.append(f)
|
||||
|
@ -7,9 +7,11 @@ add/remove formats
|
||||
'''
|
||||
|
||||
import os, re, time, traceback, textwrap
|
||||
from functools import partial
|
||||
|
||||
from PyQt4.Qt import SIGNAL, QObject, Qt, QTimer, QThread, QDate, \
|
||||
QPixmap, QListWidgetItem, QDialog, pyqtSignal, QMessageBox
|
||||
QPixmap, QListWidgetItem, QDialog, pyqtSignal, QMessageBox, QIcon, \
|
||||
QPushButton
|
||||
|
||||
from calibre.gui2 import error_dialog, file_icon_provider, dynamic, \
|
||||
choose_files, choose_images, ResizableDialog, \
|
||||
@ -31,7 +33,7 @@ from calibre.gui2.preferences.social import SocialMetadata
|
||||
from calibre.gui2.custom_column_widgets import populate_metadata_page
|
||||
from calibre import strftime
|
||||
|
||||
class CoverFetcher(QThread):
|
||||
class CoverFetcher(QThread): # {{{
|
||||
|
||||
def __init__(self, username, password, isbn, timeout, title, author):
|
||||
self.username = username.strip() if username else username
|
||||
@ -74,9 +76,9 @@ class CoverFetcher(QThread):
|
||||
self.traceback = traceback.format_exc()
|
||||
print self.traceback
|
||||
|
||||
# }}}
|
||||
|
||||
|
||||
class Format(QListWidgetItem):
|
||||
class Format(QListWidgetItem): # {{{
|
||||
|
||||
def __init__(self, parent, ext, size, path=None, timestamp=None):
|
||||
self.path = path
|
||||
@ -92,15 +94,70 @@ class Format(QListWidgetItem):
|
||||
self.setToolTip(text)
|
||||
self.setStatusTip(text)
|
||||
|
||||
# }}}
|
||||
|
||||
|
||||
class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
|
||||
COVER_FETCH_TIMEOUT = 240 # seconds
|
||||
view_format = pyqtSignal(object)
|
||||
|
||||
# Cover processing {{{
|
||||
|
||||
def set_cover(self):
|
||||
mi, ext = self.get_selected_format_metadata()
|
||||
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
|
||||
pix = QPixmap()
|
||||
pix.loadFromData(cdata)
|
||||
if pix.isNull():
|
||||
error_dialog(self, _('Could not read cover'),
|
||||
_('The cover in the %s format is invalid')%ext).exec_()
|
||||
return
|
||||
self.cover.setPixmap(pix)
|
||||
self.update_cover_tooltip()
|
||||
self.cover_changed = True
|
||||
self.cpixmap = pix
|
||||
self.cover_data = cdata
|
||||
|
||||
def trim_cover(self, *args):
|
||||
from calibre.utils.magick import Image
|
||||
cdata = self.cover_data
|
||||
if not cdata:
|
||||
return
|
||||
im = Image()
|
||||
im.load(cdata)
|
||||
im.trim(10)
|
||||
cdata = im.export('png')
|
||||
pix = QPixmap()
|
||||
pix.loadFromData(cdata)
|
||||
self.cover.setPixmap(pix)
|
||||
self.update_cover_tooltip()
|
||||
self.cover_changed = True
|
||||
self.cpixmap = pix
|
||||
self.cover_data = cdata
|
||||
|
||||
|
||||
|
||||
def update_cover_tooltip(self):
|
||||
p = self.cover.pixmap()
|
||||
self.cover.setToolTip(_('Cover size: %dx%d pixels') %
|
||||
(p.width(), p.height()))
|
||||
|
||||
|
||||
def do_reset_cover(self, *args):
|
||||
pix = QPixmap(I('default_cover.png'))
|
||||
self.cover.setPixmap(pix)
|
||||
self.update_cover_tooltip()
|
||||
self.cover_changed = True
|
||||
self.cover_data = None
|
||||
|
||||
@ -136,6 +193,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
else:
|
||||
self.cover_path.setText(_file)
|
||||
self.cover.setPixmap(pix)
|
||||
self.update_cover_tooltip()
|
||||
self.cover_changed = True
|
||||
self.cpixmap = pix
|
||||
self.cover_data = cover
|
||||
@ -161,9 +219,80 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
pix = QPixmap()
|
||||
pix.loadFromData(self.cover_data)
|
||||
self.cover.setPixmap(pix)
|
||||
self.update_cover_tooltip()
|
||||
self.cover_changed = True
|
||||
self.cpixmap = pix
|
||||
|
||||
def cover_dropped(self, cover_data):
|
||||
self.cover_changed = True
|
||||
self.cover_data = cover_data
|
||||
self.update_cover_tooltip()
|
||||
|
||||
def fetch_cover(self):
|
||||
isbn = re.sub(r'[^0-9a-zA-Z]', '', unicode(self.isbn.text())).strip()
|
||||
self.fetch_cover_button.setEnabled(False)
|
||||
self.setCursor(Qt.WaitCursor)
|
||||
title, author = map(unicode, (self.title.text(), self.authors.text()))
|
||||
self.cover_fetcher = CoverFetcher(None, None, isbn,
|
||||
self.timeout, title, author)
|
||||
self.cover_fetcher.start()
|
||||
self._hangcheck = QTimer(self)
|
||||
self.connect(self._hangcheck, SIGNAL('timeout()'), self.hangcheck)
|
||||
self.cf_start_time = time.time()
|
||||
self.pi.start(_('Downloading cover...'))
|
||||
self._hangcheck.start(100)
|
||||
|
||||
def hangcheck(self):
|
||||
if not self.cover_fetcher.isFinished() and \
|
||||
time.time()-self.cf_start_time < self.COVER_FETCH_TIMEOUT:
|
||||
return
|
||||
|
||||
self._hangcheck.stop()
|
||||
try:
|
||||
if self.cover_fetcher.isRunning():
|
||||
self.cover_fetcher.terminate()
|
||||
error_dialog(self, _('Cannot fetch cover'),
|
||||
_('<b>Could not fetch cover.</b><br/>')+
|
||||
_('The download timed out.')).exec_()
|
||||
return
|
||||
if self.cover_fetcher.needs_isbn:
|
||||
error_dialog(self, _('Cannot fetch cover'),
|
||||
_('Could not find cover for this book. Try '
|
||||
'specifying the ISBN first.')).exec_()
|
||||
return
|
||||
if self.cover_fetcher.exception is not None:
|
||||
err = self.cover_fetcher.exception
|
||||
error_dialog(self, _('Cannot fetch cover'),
|
||||
_('<b>Could not fetch cover.</b><br/>')+unicode(err)).exec_()
|
||||
return
|
||||
if self.cover_fetcher.errors and self.cover_fetcher.cover_data is None:
|
||||
details = u'\n\n'.join([e[-1] + ': ' + e[1] for e in self.cover_fetcher.errors])
|
||||
error_dialog(self, _('Cannot fetch cover'),
|
||||
_('<b>Could not fetch cover.</b><br/>') +
|
||||
_('For the error message from each cover source, '
|
||||
'click Show details below.'), det_msg=details, show=True)
|
||||
return
|
||||
|
||||
pix = QPixmap()
|
||||
pix.loadFromData(self.cover_fetcher.cover_data)
|
||||
if pix.isNull():
|
||||
error_dialog(self, _('Bad cover'),
|
||||
_('The cover is not a valid picture')).exec_()
|
||||
else:
|
||||
self.cover.setPixmap(pix)
|
||||
self.update_cover_tooltip()
|
||||
self.cover_changed = True
|
||||
self.cpixmap = pix
|
||||
self.cover_data = self.cover_fetcher.cover_data
|
||||
finally:
|
||||
self.fetch_cover_button.setEnabled(True)
|
||||
self.unsetCursor()
|
||||
self.pi.stop()
|
||||
|
||||
|
||||
# }}}
|
||||
|
||||
# Formats processing {{{
|
||||
def add_format(self, x):
|
||||
files = choose_files(self, 'add formats dialog',
|
||||
_("Choose formats for ") + unicode((self.title.text())),
|
||||
@ -276,48 +405,6 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
self.comments.setPlainText(mi.comments)
|
||||
|
||||
|
||||
def set_cover(self):
|
||||
mi, ext = self.get_selected_format_metadata()
|
||||
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
|
||||
pix = QPixmap()
|
||||
pix.loadFromData(cdata)
|
||||
if pix.isNull():
|
||||
error_dialog(self, _('Could not read cover'),
|
||||
_('The cover in the %s format is invalid')%ext).exec_()
|
||||
return
|
||||
self.cover.setPixmap(pix)
|
||||
self.cover_changed = True
|
||||
self.cpixmap = pix
|
||||
self.cover_data = cdata
|
||||
|
||||
def trim_cover(self, *args):
|
||||
from calibre.utils.magick import Image
|
||||
cdata = self.cover_data
|
||||
if not cdata:
|
||||
return
|
||||
im = Image()
|
||||
im.load(cdata)
|
||||
im.trim(10)
|
||||
cdata = im.export('png')
|
||||
pix = QPixmap()
|
||||
pix.loadFromData(cdata)
|
||||
self.cover.setPixmap(pix)
|
||||
self.cover_changed = True
|
||||
self.cpixmap = pix
|
||||
self.cover_data = cdata
|
||||
|
||||
|
||||
|
||||
def sync_formats(self):
|
||||
old_extensions, new_extensions, paths = set(), set(), {}
|
||||
for row in range(self.formats.count()):
|
||||
@ -338,11 +425,14 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
if ext not in extensions:
|
||||
self.db.remove_format(self.row, ext, notify=False)
|
||||
|
||||
def do_cancel_all(self):
|
||||
self.cancel_all = True
|
||||
self.reject()
|
||||
def show_format(self, item, *args):
|
||||
fmt = item.ext
|
||||
self.view_format.emit(fmt)
|
||||
|
||||
def __init__(self, window, row, db, accepted_callback=None, cancel_all=False):
|
||||
# }}}
|
||||
|
||||
def __init__(self, window, row, db, prev=None,
|
||||
next_=None):
|
||||
ResizableDialog.__init__(self, window)
|
||||
self.bc_box.layout().setAlignment(self.cover, Qt.AlignCenter|Qt.AlignHCenter)
|
||||
self.cancel_all = False
|
||||
@ -354,16 +444,27 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
_(' The red color indicates that the current '
|
||||
'author sort does not match the current author'))
|
||||
|
||||
if cancel_all:
|
||||
self.__abort_button = self.button_box.addButton(self.button_box.Abort)
|
||||
self.__abort_button.setToolTip(_('Abort the editing of all remaining books'))
|
||||
self.connect(self.__abort_button, SIGNAL('clicked()'),
|
||||
self.do_cancel_all)
|
||||
self.row_delta = 0
|
||||
if prev:
|
||||
self.prev_button = QPushButton(QIcon(I('back.png')), _('Previous'),
|
||||
self)
|
||||
self.button_box.addButton(self.prev_button, self.button_box.ActionRole)
|
||||
tip = _('Save changes and edit the metadata of %s')%prev
|
||||
self.prev_button.setToolTip(tip)
|
||||
self.prev_button.clicked.connect(partial(self.next_triggered,
|
||||
-1))
|
||||
if next_:
|
||||
self.next_button = QPushButton(QIcon(I('forward.png')), _('Next'),
|
||||
self)
|
||||
self.button_box.addButton(self.next_button, self.button_box.ActionRole)
|
||||
tip = _('Save changes and edit the metadata of %s')%next_
|
||||
self.next_button.setToolTip(tip)
|
||||
self.next_button.clicked.connect(partial(self.next_triggered, 1))
|
||||
|
||||
self.splitter.setStretchFactor(100, 1)
|
||||
self.read_state()
|
||||
self.db = db
|
||||
self.pi = ProgressIndicator(self)
|
||||
self.accepted_callback = accepted_callback
|
||||
self.id = db.id(row)
|
||||
self.row = row
|
||||
self.cover_data = None
|
||||
@ -412,6 +513,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
self.connect(self.reset_cover, SIGNAL('clicked()'), self.do_reset_cover)
|
||||
self.connect(self.swap_button, SIGNAL('clicked()'), self.swap_title_author)
|
||||
self.timeout = float(prefs['network_timeout'])
|
||||
|
||||
|
||||
self.title.setText(db.title(row))
|
||||
isbn = db.isbn(self.id, index_is_id=True)
|
||||
if not isbn:
|
||||
@ -472,6 +575,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
else:
|
||||
self.cover_data = cover
|
||||
self.cover.setPixmap(pm)
|
||||
self.update_cover_tooltip()
|
||||
self.original_series_name = unicode(self.series.text()).strip()
|
||||
if len(db.custom_column_label_map) == 0:
|
||||
self.central_widget.tabBar().setVisible(False)
|
||||
@ -479,6 +583,9 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
self.create_custom_column_editors()
|
||||
self.generate_cover_button.clicked.connect(self.generate_cover)
|
||||
|
||||
self.original_author = unicode(self.authors.text()).strip()
|
||||
self.original_title = unicode(self.title.text()).strip()
|
||||
|
||||
def create_custom_column_editors(self):
|
||||
w = self.central_widget.widget(1)
|
||||
layout = w.layout()
|
||||
@ -531,10 +638,6 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
self.isbn.setStyleSheet('QLineEdit { background-color: rgba(255,0,0,20%) }')
|
||||
self.isbn.setToolTip(_('This ISBN number is invalid'))
|
||||
|
||||
def show_format(self, item, *args):
|
||||
fmt = item.ext
|
||||
self.view_format.emit(fmt)
|
||||
|
||||
def deduce_author_sort(self):
|
||||
au = unicode(self.authors.text())
|
||||
au = re.sub(r'\s+et al\.$', '', au)
|
||||
@ -547,9 +650,6 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
self.authors.setText(title)
|
||||
self.author_sort.setText('')
|
||||
|
||||
def cover_dropped(self, cover_data):
|
||||
self.cover_changed = True
|
||||
self.cover_data = cover_data
|
||||
|
||||
def initialize_combos(self):
|
||||
self.initalize_authors()
|
||||
@ -625,66 +725,6 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
self.tags.setText(tag_string)
|
||||
self.tags.update_tags_cache(self.db.all_tags())
|
||||
|
||||
def fetch_cover(self):
|
||||
isbn = re.sub(r'[^0-9a-zA-Z]', '', unicode(self.isbn.text())).strip()
|
||||
self.fetch_cover_button.setEnabled(False)
|
||||
self.setCursor(Qt.WaitCursor)
|
||||
title, author = map(unicode, (self.title.text(), self.authors.text()))
|
||||
self.cover_fetcher = CoverFetcher(None, None, isbn,
|
||||
self.timeout, title, author)
|
||||
self.cover_fetcher.start()
|
||||
self._hangcheck = QTimer(self)
|
||||
self.connect(self._hangcheck, SIGNAL('timeout()'), self.hangcheck)
|
||||
self.cf_start_time = time.time()
|
||||
self.pi.start(_('Downloading cover...'))
|
||||
self._hangcheck.start(100)
|
||||
|
||||
def hangcheck(self):
|
||||
if not self.cover_fetcher.isFinished() and \
|
||||
time.time()-self.cf_start_time < self.COVER_FETCH_TIMEOUT:
|
||||
return
|
||||
|
||||
self._hangcheck.stop()
|
||||
try:
|
||||
if self.cover_fetcher.isRunning():
|
||||
self.cover_fetcher.terminate()
|
||||
error_dialog(self, _('Cannot fetch cover'),
|
||||
_('<b>Could not fetch cover.</b><br/>')+
|
||||
_('The download timed out.')).exec_()
|
||||
return
|
||||
if self.cover_fetcher.needs_isbn:
|
||||
error_dialog(self, _('Cannot fetch cover'),
|
||||
_('Could not find cover for this book. Try '
|
||||
'specifying the ISBN first.')).exec_()
|
||||
return
|
||||
if self.cover_fetcher.exception is not None:
|
||||
err = self.cover_fetcher.exception
|
||||
error_dialog(self, _('Cannot fetch cover'),
|
||||
_('<b>Could not fetch cover.</b><br/>')+unicode(err)).exec_()
|
||||
return
|
||||
if self.cover_fetcher.errors and self.cover_fetcher.cover_data is None:
|
||||
details = u'\n\n'.join([e[-1] + ': ' + e[1] for e in self.cover_fetcher.errors])
|
||||
error_dialog(self, _('Cannot fetch cover'),
|
||||
_('<b>Could not fetch cover.</b><br/>') +
|
||||
_('For the error message from each cover source, '
|
||||
'click Show details below.'), det_msg=details, show=True)
|
||||
return
|
||||
|
||||
pix = QPixmap()
|
||||
pix.loadFromData(self.cover_fetcher.cover_data)
|
||||
if pix.isNull():
|
||||
error_dialog(self, _('Bad cover'),
|
||||
_('The cover is not a valid picture')).exec_()
|
||||
else:
|
||||
self.cover.setPixmap(pix)
|
||||
self.cover_changed = True
|
||||
self.cpixmap = pix
|
||||
self.cover_data = self.cover_fetcher.cover_data
|
||||
finally:
|
||||
self.fetch_cover_button.setEnabled(True)
|
||||
self.unsetCursor()
|
||||
self.pi.stop()
|
||||
|
||||
|
||||
def fetch_metadata(self):
|
||||
isbn = re.sub(r'[^0-9a-zA-Z]', '', unicode(self.isbn.text()))
|
||||
@ -778,6 +818,10 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
unicode(self.tags.text()).split(',')],
|
||||
notify=notify, commit=commit)
|
||||
|
||||
def next_triggered(self, row_delta, *args):
|
||||
self.row_delta = row_delta
|
||||
self.accept()
|
||||
|
||||
def accept(self):
|
||||
cf = getattr(self, 'cover_fetcher', None)
|
||||
if cf is not None and hasattr(cf, 'terminate'):
|
||||
@ -787,9 +831,10 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
if self.formats_changed:
|
||||
self.sync_formats()
|
||||
title = unicode(self.title.text()).strip()
|
||||
self.db.set_title(self.id, title, notify=False)
|
||||
if title != self.original_title:
|
||||
self.db.set_title(self.id, title, notify=False)
|
||||
au = unicode(self.authors.text()).strip()
|
||||
if au:
|
||||
if au and au != self.original_author:
|
||||
self.db.set_authors(self.id, string_to_authors(au), notify=False)
|
||||
aus = unicode(self.author_sort.text()).strip()
|
||||
if aus:
|
||||
@ -839,8 +884,6 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
raise
|
||||
self.save_state()
|
||||
QDialog.accept(self)
|
||||
if callable(self.accepted_callback):
|
||||
self.accepted_callback(self.id)
|
||||
|
||||
def reject(self, *args):
|
||||
cf = getattr(self, 'cover_fetcher', None)
|
||||
|
@ -254,6 +254,38 @@ class CcTextDelegate(QStyledItemDelegate): # {{{
|
||||
|
||||
# }}}
|
||||
|
||||
class CcEnumDelegate(QStyledItemDelegate): # {{{
|
||||
'''
|
||||
Delegate for text/int/float data.
|
||||
'''
|
||||
|
||||
def createEditor(self, parent, option, index):
|
||||
m = index.model()
|
||||
col = m.column_map[index.column()]
|
||||
editor = QComboBox(parent)
|
||||
editor.addItem('')
|
||||
for v in m.custom_columns[col]['display']['enum_values']:
|
||||
editor.addItem(v)
|
||||
return editor
|
||||
|
||||
def setModelData(self, editor, model, index):
|
||||
val = unicode(editor.currentText())
|
||||
if not val:
|
||||
val = None
|
||||
model.setData(index, QVariant(val), Qt.EditRole)
|
||||
|
||||
def setEditorData(self, editor, index):
|
||||
m = index.model()
|
||||
val = m.db.data[index.row()][m.custom_columns[m.column_map[index.column()]]['rec_index']]
|
||||
if val is None:
|
||||
val = ''
|
||||
idx = editor.findText(val)
|
||||
if idx < 0:
|
||||
editor.setCurrentIndex(0)
|
||||
else:
|
||||
editor.setCurrentIndex(idx)
|
||||
# }}}
|
||||
|
||||
class CcCommentsDelegate(QStyledItemDelegate): # {{{
|
||||
'''
|
||||
Delegate for comments data.
|
||||
|
@ -634,7 +634,7 @@ class BooksModel(QAbstractTableModel): # {{{
|
||||
for col in self.custom_columns:
|
||||
idx = self.custom_columns[col]['rec_index']
|
||||
datatype = self.custom_columns[col]['datatype']
|
||||
if datatype in ('text', 'comments', 'composite'):
|
||||
if datatype in ('text', 'comments', 'composite', 'enumeration'):
|
||||
self.dc[col] = functools.partial(text_type, idx=idx,
|
||||
mult=self.custom_columns[col]['is_multiple'])
|
||||
elif datatype in ('int', 'float'):
|
||||
@ -722,7 +722,11 @@ class BooksModel(QAbstractTableModel): # {{{
|
||||
if typ in ('text', 'comments'):
|
||||
val = unicode(value.toString()).strip()
|
||||
val = val if val else None
|
||||
if typ == 'bool':
|
||||
elif typ == 'enumeration':
|
||||
val = unicode(value.toString()).strip()
|
||||
if not val:
|
||||
val = None
|
||||
elif typ == 'bool':
|
||||
val = value.toPyObject()
|
||||
elif typ == 'rating':
|
||||
val = value.toInt()[0]
|
||||
@ -730,7 +734,7 @@ class BooksModel(QAbstractTableModel): # {{{
|
||||
val *= 2
|
||||
elif typ in ('int', 'float'):
|
||||
val = unicode(value.toString()).strip()
|
||||
if val is None or not val:
|
||||
if not val:
|
||||
val = None
|
||||
elif typ == 'datetime':
|
||||
val = value.toDate()
|
||||
|
@ -14,7 +14,8 @@ from PyQt4.Qt import QTableView, Qt, QAbstractItemView, QMenu, pyqtSignal, \
|
||||
|
||||
from calibre.gui2.library.delegates import RatingDelegate, PubDateDelegate, \
|
||||
TextDelegate, DateDelegate, TagsDelegate, CcTextDelegate, \
|
||||
CcBoolDelegate, CcCommentsDelegate, CcDateDelegate, CcTemplateDelegate
|
||||
CcBoolDelegate, CcCommentsDelegate, CcDateDelegate, CcTemplateDelegate, \
|
||||
CcEnumDelegate
|
||||
from calibre.gui2.library.models import BooksModel, DeviceBooksModel
|
||||
from calibre.utils.config import tweaks, prefs
|
||||
from calibre.gui2 import error_dialog, gprefs
|
||||
@ -76,6 +77,7 @@ class BooksView(QTableView): # {{{
|
||||
self.publisher_delegate = TextDelegate(self)
|
||||
self.text_delegate = TextDelegate(self)
|
||||
self.cc_text_delegate = CcTextDelegate(self)
|
||||
self.cc_enum_delegate = CcEnumDelegate(self)
|
||||
self.cc_bool_delegate = CcBoolDelegate(self)
|
||||
self.cc_comments_delegate = CcCommentsDelegate(self)
|
||||
self.cc_template_delegate = CcTemplateDelegate(self)
|
||||
@ -427,6 +429,8 @@ class BooksView(QTableView): # {{{
|
||||
self.setItemDelegateForColumn(cm.index(colhead), self.rating_delegate)
|
||||
elif cc['datatype'] == 'composite':
|
||||
self.setItemDelegateForColumn(cm.index(colhead), self.cc_template_delegate)
|
||||
elif cc['datatype'] == 'enumeration':
|
||||
self.setItemDelegateForColumn(cm.index(colhead), self.cc_enum_delegate)
|
||||
else:
|
||||
dattr = colhead+'_delegate'
|
||||
delegate = colhead if hasattr(self, dattr) else 'text'
|
||||
|
@ -27,18 +27,20 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
|
||||
3:{'datatype':'series',
|
||||
'text':_('Text column for keeping series-like information'),
|
||||
'is_multiple':False},
|
||||
4:{'datatype':'datetime',
|
||||
4:{'datatype':'enumeration',
|
||||
'text':_('Text, but with a fixed set of permitted values'), 'is_multiple':False},
|
||||
5:{'datatype':'datetime',
|
||||
'text':_('Date'), 'is_multiple':False},
|
||||
5:{'datatype':'float',
|
||||
6:{'datatype':'float',
|
||||
'text':_('Floating point numbers'), 'is_multiple':False},
|
||||
6:{'datatype':'int',
|
||||
7:{'datatype':'int',
|
||||
'text':_('Integers'), 'is_multiple':False},
|
||||
7:{'datatype':'rating',
|
||||
8:{'datatype':'rating',
|
||||
'text':_('Ratings, shown with stars'),
|
||||
'is_multiple':False},
|
||||
8:{'datatype':'bool',
|
||||
9:{'datatype':'bool',
|
||||
'text':_('Yes/No'), 'is_multiple':False},
|
||||
9:{'datatype':'composite',
|
||||
10:{'datatype':'composite',
|
||||
'text':_('Column built from other columns'), 'is_multiple':False},
|
||||
}
|
||||
|
||||
@ -59,6 +61,7 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
|
||||
self.editing_col = editing
|
||||
self.standard_colheads = standard_colheads
|
||||
self.standard_colnames = standard_colnames
|
||||
self.column_type_box.setMaxVisibleItems(len(self.column_types))
|
||||
for t in self.column_types:
|
||||
self.column_type_box.addItem(self.column_types[t]['text'])
|
||||
self.column_type_box.currentIndexChanged.connect(self.datatype_changed)
|
||||
@ -91,6 +94,8 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
|
||||
self.date_format_box.setText(c['display'].get('date_format', ''))
|
||||
elif ct == 'composite':
|
||||
self.composite_box.setText(c['display'].get('composite_template', ''))
|
||||
elif ct == 'enumeration':
|
||||
self.enum_box.setText(','.join(c['display'].get('enum_values', [])))
|
||||
self.datatype_changed()
|
||||
self.exec_()
|
||||
|
||||
@ -103,7 +108,8 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
|
||||
getattr(self, 'date_format_'+x).setVisible(col_type == 'datetime')
|
||||
for x in ('box', 'default_label', 'label'):
|
||||
getattr(self, 'composite_'+x).setVisible(col_type == 'composite')
|
||||
|
||||
for x in ('box', 'default_label', 'label'):
|
||||
getattr(self, 'enum_'+x).setVisible(col_type == 'enumeration')
|
||||
|
||||
def accept(self):
|
||||
col = unicode(self.column_name_box.text())
|
||||
@ -145,17 +151,31 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
|
||||
return self.simple_error('', _('The heading %s is already used')%col_heading)
|
||||
|
||||
display_dict = {}
|
||||
|
||||
if col_type == 'datetime':
|
||||
if self.date_format_box.text():
|
||||
display_dict = {'date_format':unicode(self.date_format_box.text())}
|
||||
else:
|
||||
display_dict = {'date_format': None}
|
||||
|
||||
if col_type == 'composite':
|
||||
elif col_type == 'composite':
|
||||
if not self.composite_box.text():
|
||||
return self.simple_error('', _('You must enter a template for'
|
||||
' composite columns'))
|
||||
display_dict = {'composite_template':unicode(self.composite_box.text())}
|
||||
elif col_type == 'enumeration':
|
||||
if not self.enum_box.text():
|
||||
return self.simple_error('', _('You must enter at least one'
|
||||
' value for enumeration columns'))
|
||||
l = [v.strip() for v in unicode(self.enum_box.text()).split(',')]
|
||||
for v in l:
|
||||
if not v:
|
||||
return self.simple_error('', _('You cannot provide the empty '
|
||||
'value, as it is included by default'))
|
||||
for i in range(0, len(l)-1):
|
||||
if l[i] in l[i+1:]:
|
||||
return self.simple_error('', _('The value "{0}" is in the '
|
||||
'list more than once').format(l[i]))
|
||||
display_dict = {'enum_values': l}
|
||||
|
||||
db = self.parent.gui.library_view.model().db
|
||||
key = db.field_metadata.custom_field_prefix+col
|
||||
|
@ -10,7 +10,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>528</width>
|
||||
<height>199</height>
|
||||
<height>212</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
@ -24,7 +24,7 @@
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QGridLayout" name="gridLayout_2" rowstretch="0,0,0,0">
|
||||
<layout class="QGridLayout" name="gridLayout_2" rowstretch="0,0,0,0,0,0,0,0,0,0,0,0">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetDefaultConstraint</enum>
|
||||
</property>
|
||||
@ -56,7 +56,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<item row="0" column="2">
|
||||
<widget class="QLineEdit" name="column_name_box">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
@ -69,7 +69,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<item row="1" column="2">
|
||||
<widget class="QLineEdit" name="column_heading_box">
|
||||
<property name="toolTip">
|
||||
<string>Column heading in the library view and category name in the tag browser</string>
|
||||
@ -86,7 +86,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<item row="2" column="2">
|
||||
<widget class="QComboBox" name="column_type_box">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
|
||||
@ -105,7 +105,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<item row="4" column="2">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="date_format_box">
|
||||
@ -147,7 +147,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
<item row="5" column="2">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="composite_box">
|
||||
@ -158,7 +158,7 @@
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string><p>Field template. Uses the same syntax as save templates.</string>
|
||||
<string>Field template. Uses the same syntax as save templates.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@ -184,7 +184,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="10" column="0" colspan="3">
|
||||
<item row="11" column="0" colspan="4">
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
@ -197,6 +197,45 @@
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
<widget class="QLabel" name="enum_label">
|
||||
<property name="text">
|
||||
<string>Values</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>enum_box</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="2">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="enum_box">
|
||||
<property name="toolTip">
|
||||
<string>A comma-separated list of permitted values. The empty value is always
|
||||
included, and is the default. For example, the list 'one,two,three' has
|
||||
four values, the first of them being the empty value.</string>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="enum_default_label">
|
||||
<property name="text">
|
||||
<string>Default: (nothing)</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>The empty string is always the first value</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="11" column="0">
|
||||
|
@ -384,7 +384,7 @@ class SearchBoxMixin(object): # {{{
|
||||
def do_advanced_search(self, *args):
|
||||
d = SearchDialog(self, self.library_view.model().db)
|
||||
if d.exec_() == QDialog.Accepted:
|
||||
self.search.set_search_string(d.search_string())
|
||||
self.search.set_search_string(d.search_string(), store_in_history=True)
|
||||
|
||||
def do_search_button(self):
|
||||
self.search.do_search()
|
||||
|
@ -106,10 +106,13 @@ class TagsView(QTreeView): # {{{
|
||||
self.refresh_required.connect(self.recount, type=Qt.QueuedConnection)
|
||||
self.sort_by.currentIndexChanged.connect(self.sort_changed)
|
||||
self.made_connections = True
|
||||
self.refresh_signal_processed = True
|
||||
db.add_listener(self.database_changed)
|
||||
|
||||
def database_changed(self, event, ids):
|
||||
self.refresh_required.emit()
|
||||
if self.refresh_signal_processed:
|
||||
self.refresh_signal_processed = False
|
||||
self.refresh_required.emit()
|
||||
|
||||
@property
|
||||
def match_all(self):
|
||||
@ -295,6 +298,7 @@ class TagsView(QTreeView): # {{{
|
||||
return self.isExpanded(idx)
|
||||
|
||||
def recount(self, *args):
|
||||
self.refresh_signal_processed = True
|
||||
ci = self.currentIndex()
|
||||
if not ci.isValid():
|
||||
ci = self.indexAt(QPoint(10, 10))
|
||||
@ -937,7 +941,9 @@ class TagBrowserMixin(object): # {{{
|
||||
if old_author != new_author:
|
||||
# The id might change if the new author already exists
|
||||
id = db.rename_author(id, new_author)
|
||||
db.set_sort_field_for_author(id, unicode(new_sort))
|
||||
db.set_sort_field_for_author(id, unicode(new_sort),
|
||||
commit=False, notify=False)
|
||||
db.commit()
|
||||
self.library_view.model().refresh()
|
||||
self.tags_view.recount()
|
||||
|
||||
|
@ -614,7 +614,7 @@ class DocumentView(QWebView):
|
||||
|
||||
def search(self, text, backwards=False):
|
||||
if backwards:
|
||||
return self.findText(text, self.document.FindBackwards)
|
||||
return self.findText(text, self.document.FindBackward)
|
||||
return self.findText(text)
|
||||
|
||||
def path(self):
|
||||
|
@ -172,6 +172,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
||||
self.iterator = None
|
||||
self.current_page = None
|
||||
self.pending_search = None
|
||||
self.pending_search_dir= None
|
||||
self.pending_anchor = None
|
||||
self.pending_reference = None
|
||||
self.pending_bookmark = None
|
||||
@ -435,7 +436,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
||||
if not text:
|
||||
self.view.search('')
|
||||
return self.search.search_done(False)
|
||||
if self.view.search(text):
|
||||
if self.view.search(text, backwards=backwards):
|
||||
self.scrolled(self.view.scroll_fraction)
|
||||
return self.search.search_done(True)
|
||||
index = self.iterator.search(text, self.current_index,
|
||||
@ -449,11 +450,13 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
||||
return self.search.search_done(True)
|
||||
return self.search.search_done(True)
|
||||
self.pending_search = text
|
||||
self.pending_search_dir = 'backwards' if backwards else 'forwards'
|
||||
self.load_path(self.iterator.spine[index])
|
||||
|
||||
def do_search(self, text):
|
||||
def do_search(self, text, backwards):
|
||||
self.pending_search = None
|
||||
if self.view.search(text):
|
||||
self.pending_search_dir = None
|
||||
if self.view.search(text, backwards=backwards):
|
||||
self.scrolled(self.view.scroll_fraction)
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
@ -499,8 +502,10 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
||||
self.current_index = index
|
||||
self.set_page_number(self.view.scroll_fraction)
|
||||
if self.pending_search is not None:
|
||||
self.do_search(self.pending_search)
|
||||
self.do_search(self.pending_search,
|
||||
self.pending_search_dir=='backwards')
|
||||
self.pending_search = None
|
||||
self.pending_search_dir = None
|
||||
if self.pending_anchor is not None:
|
||||
self.view.scroll_to(self.pending_anchor)
|
||||
self.pending_anchor = None
|
||||
|
@ -520,7 +520,7 @@ class ResultCache(SearchQueryParser): # {{{
|
||||
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 \
|
||||
['composite', 'text', 'comments', 'series']:
|
||||
['composite', 'text', 'comments', 'series', 'enumeration']:
|
||||
exclude_fields.append(db_col[x])
|
||||
col_datatype[db_col[x]] = self.field_metadata[x]['datatype']
|
||||
is_multiple_cols[db_col[x]] = self.field_metadata[x]['is_multiple']
|
||||
@ -828,7 +828,7 @@ class SortKeyGenerator(object):
|
||||
sidx = record[sidx_fm['rec_index']]
|
||||
val = (val, sidx)
|
||||
|
||||
elif dt in ('text', 'comments', 'composite'):
|
||||
elif dt in ('text', 'comments', 'composite', 'enumeration'):
|
||||
if val is None:
|
||||
val = ''
|
||||
val = val.lower()
|
||||
|
@ -565,8 +565,9 @@ datatype is one of: {0}
|
||||
'applies if datatype is text.'))
|
||||
parser.add_option('--display', default='{}',
|
||||
help=_('A dictionary of options to customize how '
|
||||
'the data in this column will be interpreted.'))
|
||||
|
||||
'the data in this column will be interpreted. This is a JSON '
|
||||
' string. For enumeration columns, use '
|
||||
'--display=\'{"enum_values":["val1", "val2"]}\''))
|
||||
return parser
|
||||
|
||||
|
||||
|
@ -18,7 +18,7 @@ from calibre.utils.date import parse_date
|
||||
class CustomColumns(object):
|
||||
|
||||
CUSTOM_DATA_TYPES = frozenset(['rating', 'text', 'comments', 'datetime',
|
||||
'int', 'float', 'bool', 'series', 'composite'])
|
||||
'int', 'float', 'bool', 'series', 'composite', 'enumeration'])
|
||||
|
||||
def custom_table_names(self, num):
|
||||
return 'custom_column_%d'%num, 'books_custom_column_%d_link'%num
|
||||
@ -136,6 +136,12 @@ class CustomColumns(object):
|
||||
x = bool(int(x))
|
||||
return x
|
||||
|
||||
def adapt_enum(x, d):
|
||||
v = adapt_text(x, d)
|
||||
if not v:
|
||||
v = None
|
||||
return v
|
||||
|
||||
self.custom_data_adapters = {
|
||||
'float': lambda x,d : x if x is None else float(x),
|
||||
'int': lambda x,d : x if x is None else int(x),
|
||||
@ -144,7 +150,8 @@ class CustomColumns(object):
|
||||
'comments': lambda x,d: adapt_text(x, {'is_multiple':False}),
|
||||
'datetime' : adapt_datetime,
|
||||
'text':adapt_text,
|
||||
'series':adapt_text
|
||||
'series':adapt_text,
|
||||
'enumeration': adapt_enum
|
||||
}
|
||||
|
||||
# Create Tag Browser categories for custom columns
|
||||
@ -439,6 +446,9 @@ class CustomColumns(object):
|
||||
val = self.custom_data_adapters[data['datatype']](val, data)
|
||||
|
||||
if data['normalized']:
|
||||
if data['datatype'] == 'enumeration' and (
|
||||
val and val not in data['display']['enum_values']):
|
||||
return None
|
||||
if not append or not data['is_multiple']:
|
||||
self.conn.execute('DELETE FROM %s WHERE book=?'%lt, (id_,))
|
||||
self.conn.execute(
|
||||
@ -558,7 +568,7 @@ class CustomColumns(object):
|
||||
|
||||
if datatype in ('rating', 'int'):
|
||||
dt = 'INT'
|
||||
elif datatype in ('text', 'comments', 'series', 'composite'):
|
||||
elif datatype in ('text', 'comments', 'series', 'composite', 'enumeration'):
|
||||
dt = 'TEXT'
|
||||
elif datatype in ('float',):
|
||||
dt = 'REAL'
|
||||
|
@ -1639,15 +1639,16 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
return []
|
||||
return result
|
||||
|
||||
def set_sort_field_for_author(self, old_id, new_sort):
|
||||
def set_sort_field_for_author(self, old_id, new_sort, commit=True, notify=False):
|
||||
self.conn.execute('UPDATE authors SET sort=? WHERE id=?', \
|
||||
(new_sort.strip(), old_id))
|
||||
self.conn.commit()
|
||||
if commit:
|
||||
self.conn.commit()
|
||||
# Now change all the author_sort fields in books by this author
|
||||
bks = self.conn.get('SELECT book from books_authors_link WHERE author=?', (old_id,))
|
||||
for (book_id,) in bks:
|
||||
ss = self.author_sort_from_book(book_id, index_is_id=True)
|
||||
self.set_author_sort(book_id, ss)
|
||||
self.set_author_sort(book_id, ss, notify=notify, commit=commit)
|
||||
|
||||
def rename_author(self, old_id, new_name):
|
||||
# Make sure that any commas in new_name are changed to '|'!
|
||||
|
@ -83,7 +83,7 @@ class FieldMetadata(dict):
|
||||
'''
|
||||
|
||||
VALID_DATA_TYPES = frozenset([None, 'rating', 'text', 'comments', 'datetime',
|
||||
'int', 'float', 'bool', 'series', 'composite'])
|
||||
'int', 'float', 'bool', 'series', 'composite', 'enumeration'])
|
||||
|
||||
# Builtin metadata {{{
|
||||
|
||||
@ -177,7 +177,7 @@ class FieldMetadata(dict):
|
||||
'is_multiple':None,
|
||||
'kind':'field',
|
||||
'name':None,
|
||||
'search_terms':[],
|
||||
'search_terms':['author_sort'],
|
||||
'is_custom':False,
|
||||
'is_category':False}),
|
||||
('comments', {'table':None,
|
||||
|
@ -115,7 +115,6 @@ def pynocase(one, two, encoding='utf-8'):
|
||||
pass
|
||||
return cmp(one.lower(), two.lower())
|
||||
|
||||
|
||||
def load_c_extensions(conn, debug=DEBUG):
|
||||
try:
|
||||
conn.enable_load_extension(True)
|
||||
|
@ -119,10 +119,11 @@ The functions available are:
|
||||
* ``ifempty(text)`` -- if the field is not empty, return the value of the field. Otherwise return `text`.
|
||||
* ``test(text if not empty, text if empty)`` -- return `text if not empty` if the field is not empty, otherwise return `text if empty`.
|
||||
* ``contains(pattern, text if match, text if not match`` -- checks if field contains matches for the regular expression `pattern`. Returns `text if match` if matches are found, otherwise it returns `text if no match`.
|
||||
* ``switch(pattern, value, pattern, value, ..., else_value)`` -- for each ``pattern, value`` pair, checks if the field matches the regular expression ``pattern`` and if so, returns that ``value``. If no ``pattern`` matches, then ``else_value`` is returned. You can have as many ``pattern, value`` pairs as you want.
|
||||
* ``count(separator)`` -- interprets the value as a list of items separated by `separator`, returning the number of items in the list. Most lists use a comma as the separator, but authors uses an ampersand. Examples: `{tags:count(,)}`, `{authors:count(&)}`
|
||||
* ``lookup(pattern, field, pattern, field, ..., else_field)`` -- like switch, except the arguments are field (metadata) names, not text. The value of the appropriate field will be fetched and used. Note that because composite columns are fields, you can use this function in one composite field to use the value of some other composite field. This is extremely useful when constructing variable save paths (more later).
|
||||
* ``re(pattern, replacement)`` -- return the field after applying the regular expression. All instances of `pattern` are replaced with `replacement`. As in all of |app|, these are python-compatible regular expressions.
|
||||
* ``shorten(left chars, middle text, right chars)`` -- Return a shortened version of the field, consisting of `left chars` characters from the beginning of the field, followed by `middle text`, followed by `right chars` characters from the end of the string. `Left chars` and `right chars` must be integers. For example, assume the title of the book is `Ancient English Laws in the Times of Ivanhoe`, and you want it to fit in a space of at most 15 characters. If you use ``{title:shorten(9,-,5)}``, the result will be `Ancient E-nhoe`. If the field's length is less than ``left chars`` + ``right chars`` + the length of ``middle text``, then the field will be used intact. For example, the title `The Dome` would not be changed.
|
||||
* ``lookup(pattern, field, pattern, field, ..., else_field)`` -- like switch, except the arguments are field (metadata) names, not text. The value of the appropriate field will be fetched and used. Note that because composite columns are fields, you can use this function in one composite field to use the value of some other composite field. This is extremely useful when constructing variable save paths (more later).
|
||||
* ``switch(pattern, value, pattern, value, ..., else_value)`` -- for each ``pattern, value`` pair, checks if the field matches the regular expression ``pattern`` and if so, returns that ``value``. If no ``pattern`` matches, then ``else_value`` is returned. You can have as many ``pattern, value`` pairs as you want.
|
||||
|
||||
|
||||
Now, about using functions and formatting in the same field. Suppose you have an integer custom column called ``#myint`` that you want to see with leading zeros, as in ``003``. To do this, you would use a format of ``0>3s``. However, by default, if a number (integer or float) equals zero then the field produces the empty value, so zero values will produce nothing, not ``000``. If you really want to see ``000`` values, then you use both the format string and the ``ifempty`` function to change the empty value back to a zero. The field reference would be::
|
||||
|
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
@ -79,6 +79,9 @@ class TemplateFormatter(string.Formatter):
|
||||
else:
|
||||
return val
|
||||
|
||||
def _count(self, val, sep):
|
||||
return unicode(len(val.split(sep)))
|
||||
|
||||
functions = {
|
||||
'uppercase' : (0, lambda s,x: x.upper()),
|
||||
'lowercase' : (0, lambda s,x: x.lower()),
|
||||
@ -91,6 +94,7 @@ class TemplateFormatter(string.Formatter):
|
||||
'shorten' : (3, _shorten),
|
||||
'switch' : (-1, _switch),
|
||||
'test' : (2, _test),
|
||||
'count' : (1, _count),
|
||||
}
|
||||
|
||||
format_string_re = re.compile(r'^(.*)\|(.*)\|(.*)$')
|
||||
@ -136,8 +140,13 @@ class TemplateFormatter(string.Formatter):
|
||||
if fmt[colon:p] in self.functions:
|
||||
field = fmt[colon:p]
|
||||
func = self.functions[field]
|
||||
args = self.arg_parser.scan(fmt[p+1:])[0]
|
||||
args = [self.backslash_comma_to_comma.sub(',', a) for a in args]
|
||||
if func[0] == 1:
|
||||
# only one arg expected. Don't bother to scan. Avoids need
|
||||
# for escaping characters
|
||||
args = [fmt[p+1:-1]]
|
||||
else:
|
||||
args = self.arg_parser.scan(fmt[p+1:])[0]
|
||||
args = [self.backslash_comma_to_comma.sub(',', a) for a in args]
|
||||
if (func[0] == 0 and (len(args) != 1 or args[0])) or \
|
||||
(func[0] > 0 and func[0] != len(args)):
|
||||
raise ValueError('Incorrect number of arguments for function '+ fmt[0:p])
|
||||
|
Loading…
x
Reference in New Issue
Block a user