Sync to trunk.

This commit is contained in:
John Schember 2011-02-03 18:38:16 -05:00
commit babe65c41a
32 changed files with 577 additions and 351 deletions

View File

@ -52,6 +52,17 @@ p.formats {
text-indent: 0.0in; text-indent: 0.0in;
} }
/*
* Minimize widows and orphans by logically grouping chunks
* Some reports of problems with Sony (ADE) ereaders
* ADE: page-break-inside:avoid;
* iBooks: display:inline-block;
* width:100%;
*/
div.author_logical_group {
page-break-inside:avoid;
}
div.description > p:first-child { div.description > p:first-child {
margin: 0 0 0 0; margin: 0 0 0 0;
text-indent: 0em; text-indent: 0em;
@ -62,27 +73,19 @@ div.description {
text-indent: 1em; text-indent: 1em;
} }
/* div.initial_letter {
* Attempt to minimize widows and orphans by logically grouping chunks page-break-before:always;
* Recommend enabling for iPad
* Some reports of problems with Sony ereaders, presumably ADE engines
*/
/*
div.logical_group {
display:inline-block;
width:100%;
} }
*/
p.date_index { p.author_title_letter_index {
font-size:x-large; font-size:x-large;
text-align:center; text-align:center;
font-weight:bold; font-weight:bold;
margin-top:1em; margin-top:0px;
margin-bottom:0px; margin-bottom:0px;
} }
p.letter_index { p.date_index {
font-size:x-large; font-size:x-large;
text-align:center; text-align:center;
font-weight:bold; font-weight:bold;
@ -99,6 +102,14 @@ p.series {
text-indent:-2em; text-indent:-2em;
} }
p.series_letter_index {
font-size:x-large;
text-align:center;
font-weight:bold;
margin-top:1em;
margin-bottom:0px;
}
p.read_book { p.read_book {
text-align:left; text-align:left;
margin-top:0px; margin-top:0px;

View File

@ -2,24 +2,23 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__author__ = 'Luis Hernandez' __author__ = 'Luis Hernandez'
__copyright__ = 'Luis Hernandez<tolyluis@gmail.com>' __copyright__ = 'Luis Hernandez<tolyluis@gmail.com>'
description = 'Diario local de Talavera de la Reina - v1.2 - 27 Jan 2011' __version__ = 'v1.0'
__date__ = '01 Feb 2011'
''' '''
http://www.latribunadetalavera.es/ http://www.promecal.es/
''' '''
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1294946868(BasicNewsRecipe): class AdvancedUserRecipe1294946868(BasicNewsRecipe):
title = u'La Tribuna de Talavera' title = u'La Tribuna de'
publisher = u'Grupo PROMECAL' publisher = u'Grupo PROMECAL'
__author__ = 'Luis Hernández' __author__ = 'Luis Hernández'
description = 'Diario local de Talavera de la Reina' description = 'Varios diarios locales del grupo PROMECAL'
cover_url = 'http://www.latribunadetalavera.es/entorno/mancheta.gif'
oldest_article = 5 oldest_article = 3
max_articles_per_feed = 50 max_articles_per_feed = 50
remove_javascript = True remove_javascript = True
@ -27,7 +26,7 @@ class AdvancedUserRecipe1294946868(BasicNewsRecipe):
use_embedded_content = False use_embedded_content = False
encoding = 'utf-8' encoding = 'utf-8'
language = 'es' language = 'es_ES'
timefmt = '[%a, %d %b, %Y]' timefmt = '[%a, %d %b, %Y]'
keep_only_tags = [ keep_only_tags = [
@ -39,7 +38,20 @@ class AdvancedUserRecipe1294946868(BasicNewsRecipe):
remove_tags_before = dict(name='div' , attrs={'class':['comparte']}) remove_tags_before = dict(name='div' , attrs={'class':['comparte']})
remove_tags_after = dict(name='div' , attrs={'id':['relacionadas']}) remove_tags_after = dict(name='div' , attrs={'id':['relacionadas']})
extra_css = ' p{text-align: justify; font-size: 100%} body{ text-align: left; font-family: serif; font-size: 100% } h1{ font-family: sans-serif; font-size:150%; font-weight: 700; text-align: justify; } h2{ font-family: sans-serif; font-size:120%; font-weight: 600; text-align: justify } h3{ font-family: sans-serif; font-size:60%; font-weight: 600; text-align: left } h4{ font-family: sans-serif; font-size:80%; font-weight: 600; text-align: left } h5{ font-family: sans-serif; font-size:70%; font-weight: 600; text-align: left }img{margin-bottom: 0.4em} ' remove_tags = [
dict(name='div', attrs={'id':['relacionadas']})
,dict(name='h3')
,dict(name='h5')
]
extra_css = """
p{text-align: justify; font-size: 100%}
body{text-align: left; font-family: serif; font-size: 100%}
h1{font-family: sans; font-size:150%; font-weight: bold; text-align: justify;}
h2{font-family: sans-serif; font-size:85%; font-style: italic; text-align: justify;}
h4{font-family: sans; font-size:75%; font-weight: bold; text-align: center;}
img{margin-bottom: 0.4em}
"""
def preprocess_html(self, soup): def preprocess_html(self, soup):
for alink in soup.findAll('a'): for alink in soup.findAll('a'):
@ -48,4 +60,15 @@ class AdvancedUserRecipe1294946868(BasicNewsRecipe):
alink.replaceWith(tstr) alink.replaceWith(tstr)
return soup return soup
feeds = [(u'Portada', u'http://www.latribunadetalavera.es/rss.html')]
feeds = [
(u'Albacete', u'http://www.latribunadealbacete.es/rss.html')
,(u'Avila', u'http://www.diariodeavila.es/rss.html')
,(u'Burgos', u'http://www.diariodeburgos.es/rss.html')
,(u'Ciudad Real', u'http://www.latribunadeciudadreal.es/rss.html')
,(u'Palencia', u'http://www.diariopalentino.es/rss.html')
,(u'Puertollano', u'http://www.latribunadepuertollano.es/rss.html')
,(u'Talavera de la Reina', u'http://www.latribunadetalavera.es/rss.html')
,(u'Toledo', u'http://www.latribunadetoledo.es/rss.html')
,(u'Valladolid', u'http://www.eldiadevalladolid.com/rss.html')
]

View File

@ -15,12 +15,26 @@ class LeTemps(BasicNewsRecipe):
oldest_article = 7 oldest_article = 7
max_articles_per_feed = 100 max_articles_per_feed = 100
__author__ = 'Sujata Raman' __author__ = 'Sujata Raman'
description = 'French news. Needs a subscription from http://www.letemps.ch'
no_stylesheets = True no_stylesheets = True
remove_javascript = True remove_javascript = True
recursions = 1 recursions = 1
encoding = 'UTF-8' encoding = 'UTF-8'
match_regexps = [r'http://www.letemps.ch/Page/Uuid/[-0-9a-f]+\|[1-9]'] match_regexps = [r'http://www.letemps.ch/Page/Uuid/[-0-9a-f]+\|[1-9]']
language = 'fr' language = 'fr'
needs_subscription = True
def get_browser(self):
br = BasicNewsRecipe.get_browser(self)
br.open('http://www.letemps.ch/login')
br['username'] = self.username
br['password'] = self.password
raw = br.submit().read()
if '>Login' in raw:
raise ValueError('Failed to login to letemp.ch. Check '
'your username and password')
return br
keep_only_tags = [dict(name='div', attrs={'id':'content'}), keep_only_tags = [dict(name='div', attrs={'id':'content'}),
dict(name='div', attrs={'class':'story'}) dict(name='div', attrs={'class':'story'})

View File

@ -13,15 +13,12 @@ class MSNSankeiNewsProduct(BasicNewsRecipe):
description = 'Products release from Japan' description = 'Products release from Japan'
oldest_article = 7 oldest_article = 7
max_articles_per_feed = 100 max_articles_per_feed = 100
encoding = 'Shift_JIS' encoding = 'utf-8'
language = 'ja' language = 'ja'
cover_url = 'http://sankei.jp.msn.com/images/common/sankeShinbunLogo.jpg' cover_url = 'http://sankei.jp.msn.com/images/common/sankeShinbunLogo.jpg'
masthead_url = 'http://sankei.jp.msn.com/images/common/sankeiNewsLogo.gif' masthead_url = 'http://sankei.jp.msn.com/images/common/sankeiNewsLogo.gif'
feeds = [(u'\u65b0\u5546\u54c1', u'http://sankei.jp.msn.com/rss/news/release.xml')] feeds = [(u'\u65b0\u5546\u54c1', u'http://sankei.jp.msn.com/rss/news/release.xml')]
remove_tags_before = dict(id="__r_article_title__") remove_tags_before = dict(id="NewsTitle")
remove_tags_after = dict(id="ajax_release_news") remove_tags_after = dict(id="RelatedTitle")
remove_tags = [{'class':"parent chromeCustom6G"},
dict(id="RelatedImg")
]

View File

@ -1,7 +1,5 @@
#!/usr/bin/env python
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>' __copyright__ = '2009-2011, Darko Miletic <darko.miletic at gmail.com>'
''' '''
theonion.com theonion.com
@ -15,26 +13,39 @@ class TheOnion(BasicNewsRecipe):
description = "America's finest news source" description = "America's finest news source"
oldest_article = 2 oldest_article = 2
max_articles_per_feed = 100 max_articles_per_feed = 100
publisher = u'Onion, Inc.' publisher = 'Onion, Inc.'
category = u'humor, news, USA' category = 'humor, news, USA'
language = 'en' language = 'en'
no_stylesheets = True no_stylesheets = True
use_embedded_content = False use_embedded_content = False
encoding = 'utf-8' encoding = 'utf-8'
remove_javascript = True publication_type = 'newsportal'
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"' masthead_url = 'http://o.onionstatic.com/img/headers/onion_190.png'
extra_css = """
body{font-family: Helvetica,Arial,sans-serif}
.section_title{color: gray; text-transform: uppercase}
.title{font-family: Georgia,serif}
.meta{color: gray; display: inline}
.has_caption{display: block}
.caption{font-size: x-small; color: gray; margin-bottom: 0.8em}
"""
html2lrf_options = [ conversion_options = {
'--comment' , description 'comment' : description
, '--category' , category , 'tags' : category
, '--publisher' , publisher , 'publisher': publisher
, 'language' : language
}
keep_only_tags = [
dict(name='h2', attrs={'class':['section_title','title']})
,dict(attrs={'class':['main_image','meta','article_photo_lead','article_body']})
,dict(attrs={'id':['entries']})
] ]
remove_attributes=['lang','rel']
keep_only_tags = [dict(name='div', attrs={'id':'main'})] remove_tags_after = dict(attrs={'class':['article_body','feature_content']})
remove_tags = [ remove_tags = [
dict(name=['object','link','iframe','base']) dict(name=['object','link','iframe','base','meta'])
,dict(name='div', attrs={'class':['toolbar_side','graphical_feature','toolbar_bottom']}) ,dict(name='div', attrs={'class':['toolbar_side','graphical_feature','toolbar_bottom']})
,dict(name='div', attrs={'id':['recent_slider','sidebar','pagination','related_media']}) ,dict(name='div', attrs={'id':['recent_slider','sidebar','pagination','related_media']})
] ]
@ -44,3 +55,28 @@ class TheOnion(BasicNewsRecipe):
(u'Daily' , u'http://feeds.theonion.com/theonion/daily' ) (u'Daily' , u'http://feeds.theonion.com/theonion/daily' )
,(u'Sports' , u'http://feeds.theonion.com/theonion/sports' ) ,(u'Sports' , u'http://feeds.theonion.com/theonion/sports' )
] ]
def get_article_url(self, article):
artl = BasicNewsRecipe.get_article_url(self, article)
if artl.startswith('http://www.theonion.com/audio/'):
artl = None
return artl
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 = []
if not limg.has_key('alt'):
limg['alt'] = 'image'
else:
str = self.tag_to_string(item)
item.replaceWith(str)
return soup

View File

@ -89,21 +89,21 @@ class NOOK_COLOR(NOOK):
BCD = [0x216] BCD = [0x216]
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'EBOOK_DISK' WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'EBOOK_DISK'
EBOOK_DIR_MAIN = 'My Files/Books' EBOOK_DIR_MAIN = 'My Files'
'''
def create_upload_path(self, path, mdata, fname, create_dirs=True): def create_upload_path(self, path, mdata, fname, create_dirs=True):
filepath = NOOK.create_upload_path(self, path, mdata, fname, filepath = NOOK.create_upload_path(self, path, mdata, fname,
create_dirs=create_dirs) create_dirs=False)
edm = self.EBOOK_DIR_MAIN.replace('/', os.sep) edm = self.EBOOK_DIR_MAIN
npath = os.path.join(edm, _('News')) + os.sep subdir = 'Books'
if npath in filepath: if mdata.tags:
filepath = filepath.replace(npath, os.sep.join('My Files', if _('News') in mdata.tags:
'Magazines')+os.sep) subdir = 'Magazines'
filepath = filepath.replace(os.sep+edm+os.sep,
os.sep+edm+os.sep+subdir+os.sep)
filedir = os.path.dirname(filepath) filedir = os.path.dirname(filepath)
if create_dirs and not os.path.exists(filedir): if create_dirs and not os.path.exists(filedir):
os.makedirs(filedir) os.makedirs(filedir)
return filepath return filepath
'''

View File

@ -490,11 +490,12 @@ class HeuristicProcessor(object):
applied to wrapping divs. This is because many ebook devices don't support margin:auto applied to wrapping divs. This is because many ebook devices don't support margin:auto
All other html is converted to text. All other html is converted to text.
''' '''
hr_open = '<div id="scenebreak" style="margin-left: 45%; margin-right: 45%; margin-top:1.5em; margin-bottom:1.5em">' hr_open = '<div id="scenebreak" style="margin-left: 45%; margin-right: 45%; margin-top:1.5em; margin-bottom:1.5em; page-break-before:avoid">'
if re.findall('(<|>)', replacement_break): if re.findall('(<|>)', replacement_break):
if re.match('^<hr', replacement_break): if re.match('^<hr', replacement_break):
if replacement_break.find('width') != -1: if replacement_break.find('width') != -1:
width = int(re.sub('.*?width(:|=)(?P<wnum>\d+).*', '\g<wnum>', replacement_break)) width = int(re.sub('.*?width(:|=)(?P<wnum>\d+).*', '\g<wnum>', replacement_break))
replacement_break = re.sub('(?i)(width=\d+\%?|width:\s*\d+(\%|px|pt|em)?;?)', '', replacement_break)
divpercent = (100 - width) / 2 divpercent = (100 - width) / 2
hr_open = re.sub('45', str(divpercent), hr_open) hr_open = re.sub('45', str(divpercent), hr_open)
scene_break = hr_open+replacement_break+'</div>' scene_break = hr_open+replacement_break+'</div>'
@ -642,7 +643,7 @@ class HeuristicProcessor(object):
# or 'hard' scene breaks are replaced, depending on which is in use # or 'hard' scene breaks are replaced, depending on which is in use
# Otherwise separator lines are centered, use a bit larger margin in this case # Otherwise separator lines are centered, use a bit larger margin in this case
replacement_break = getattr(self.extra_opts, 'replace_scene_breaks', None) replacement_break = getattr(self.extra_opts, 'replace_scene_breaks', None)
if replacement_break is not None: if replacement_break:
replacement_break = self.markup_user_break(replacement_break) replacement_break = self.markup_user_break(replacement_break)
if len(scene_break.findall(html)) >= 1: if len(scene_break.findall(html)) >= 1:
html = scene_break.sub(replacement_break, html) html = scene_break.sub(replacement_break, html)

View File

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

View File

@ -74,23 +74,29 @@ class ShareConnMenu(QMenu): # {{{
opts = email_config().parse() opts = email_config().parse()
if opts.accounts: if opts.accounts:
self.email_to_menu = QMenu(_('Email to')+'...', self) self.email_to_menu = QMenu(_('Email to')+'...', self)
ac = self.addMenu(self.email_to_menu)
self.email_actions.append(ac)
self.email_to_and_delete_menu = QMenu(
_('Email to and delete from library')+'...', self)
keys = sorted(opts.accounts.keys()) keys = sorted(opts.accounts.keys())
for account in keys: for account in keys:
formats, auto, default = opts.accounts[account] formats, auto, default = opts.accounts[account]
dest = 'mail:'+account+';'+formats dest = 'mail:'+account+';'+formats
action1 = DeviceAction(dest, False, False, I('mail.png'), action1 = DeviceAction(dest, False, False, I('mail.png'),
_('Email to')+' '+account) account)
action2 = DeviceAction(dest, True, False, I('mail.png'), action2 = DeviceAction(dest, True, False, I('mail.png'),
_('Email to')+' '+account+ _(' and delete from library')) account + ' ' + _('(delete from library)'))
map(self.email_to_menu.addAction, (action1, action2)) self.email_to_menu.addAction(action1)
self.email_to_and_delete_menu.addAction(action2)
map(self.memory.append, (action1, action2)) map(self.memory.append, (action1, action2))
if default: if default:
map(self.addAction, (action1, action2)) ac = DeviceAction(dest, False, False,
map(self.email_actions.append, (action1, action2)) I('mail.png'), _('Email to') + ' ' +account)
self.email_to_menu.addSeparator() self.addAction(ac)
self.email_actions.append(ac)
action1.a_s.connect(sync_menu.action_triggered) action1.a_s.connect(sync_menu.action_triggered)
action2.a_s.connect(sync_menu.action_triggered) action2.a_s.connect(sync_menu.action_triggered)
ac = self.addMenu(self.email_to_menu) ac = self.addMenu(self.email_to_and_delete_menu)
self.email_actions.append(ac) self.email_actions.append(ac)
else: else:
ac = self.addAction(_('Setup email based sharing of books')) ac = self.addAction(_('Setup email based sharing of books'))

View File

@ -160,6 +160,7 @@ class EditMetadataAction(InterfaceAction):
break break
changed.add(d.id) changed.add(d.id)
self.gui.library_view.model().refresh_ids(list(d.books_to_refresh))
if d.row_delta == 0: if d.row_delta == 0:
break break
current_row += d.row_delta current_row += d.row_delta

View File

@ -64,8 +64,6 @@ class CompleteWindow(QListView): # {{{
def do_selected(self, idx=None): def do_selected(self, idx=None):
idx = self.currentIndex() if idx is None else idx idx = self.currentIndex() if idx is None else idx
if not idx.isValid() and self.model().rowCount() > 0:
idx = self.model().index(0)
if idx.isValid(): if idx.isValid():
data = unicode(self.model().data(idx, Qt.DisplayRole)) data = unicode(self.model().data(idx, Qt.DisplayRole))
self.completion_selected.emit(data) self.completion_selected.emit(data)
@ -81,6 +79,9 @@ class CompleteWindow(QListView): # {{{
self.hide() self.hide()
return True return True
elif key in (Qt.Key_Enter, Qt.Key_Return, Qt.Key_Tab): elif key in (Qt.Key_Enter, Qt.Key_Return, Qt.Key_Tab):
if key == Qt.Key_Tab and not self.currentIndex().isValid():
if self.model().rowCount() > 0:
self.setCurrentIndex(self.model().index(0))
self.do_selected() self.do_selected()
return True return True
elif key in (Qt.Key_Up, Qt.Key_Down, Qt.Key_PageUp, elif key in (Qt.Key_Up, Qt.Key_Down, Qt.Key_PageUp,
@ -175,9 +176,9 @@ class MultiCompleteLineEdit(QLineEdit):
self._model = CompleteModel(parent=self) self._model = CompleteModel(parent=self)
self.complete_window = CompleteWindow(self, self._model) self.complete_window = CompleteWindow(self, self._model)
self.textChanged.connect(self.text_changed) self.textEdited.connect(self.text_edited)
self.cursorPositionChanged.connect(self.cursor_position_changed)
self.complete_window.completion_selected.connect(self.completion_selected) self.complete_window.completion_selected.connect(self.completion_selected)
self.installEventFilter(self)
# Interface {{{ # Interface {{{
def update_items_cache(self, complete_items): def update_items_cache(self, complete_items):
@ -197,15 +198,17 @@ class MultiCompleteLineEdit(QLineEdit):
return True # Filter this event since the cw is visible return True # Filter this event since the cw is visible
return QLineEdit.eventFilter(self, o, e) return QLineEdit.eventFilter(self, o, e)
def hide_completion_window(self):
self.complete_window.hide()
def text_changed(self, *args):
self.update_completions()
def cursor_position_changed(self, *args): def text_edited(self, *args):
self.update_completions() self.update_completions()
def update_completions(self): def update_completions(self):
' Update the list of completions ' ' Update the list of completions '
if not self.complete_window.isVisible() and not self.hasFocus():
return
cpos = self.cursorPosition() cpos = self.cursorPosition()
text = unicode(self.text()) text = unicode(self.text())
prefix = text[:cpos] prefix = text[:cpos]
@ -223,7 +226,7 @@ class MultiCompleteLineEdit(QLineEdit):
text text
''' '''
if self.sep is None: if self.sep is None:
return text return -1, text
else: else:
cursor_pos = self.cursorPosition() cursor_pos = self.cursorPosition()
before_text = unicode(self.text())[:cursor_pos] before_text = unicode(self.text())[:cursor_pos]
@ -232,24 +235,18 @@ class MultiCompleteLineEdit(QLineEdit):
if len(after_parts) < 3 and not after_parts[-1].strip(): if len(after_parts) < 3 and not after_parts[-1].strip():
after_text = u'' after_text = u''
prefix_len = len(before_text.split(self.sep)[-1].lstrip()) prefix_len = len(before_text.split(self.sep)[-1].lstrip())
if self.space_before_sep: return prefix_len, \
complete_text_pat = '%s%s %s %s' before_text[:cursor_pos - prefix_len] + text + after_text
len_extra = 3
else:
complete_text_pat = '%s%s%s %s'
len_extra = 2
return prefix_len, len_extra, complete_text_pat % (
before_text[:cursor_pos - prefix_len], text, self.sep, after_text)
def completion_selected(self, text): def completion_selected(self, text):
prefix_len, len_extra, ctext = self.get_completed_text(text) prefix_len, ctext = self.get_completed_text(text)
if self.sep is None: if self.sep is None:
self.setText(ctext) self.setText(ctext)
self.setCursorPosition(len(ctext)) self.setCursorPosition(len(ctext))
else: else:
cursor_pos = self.cursorPosition() cursor_pos = self.cursorPosition()
self.setText(ctext) self.setText(ctext)
self.setCursorPosition(cursor_pos - prefix_len + len(text) + len_extra) self.setCursorPosition(cursor_pos - prefix_len + len(text))
def update_complete_window(self, matches): def update_complete_window(self, matches):
self._model.update_matches(matches) self._model.update_matches(matches)
@ -334,6 +331,11 @@ class MultiCompleteComboBox(EnComboBox):
def __init__(self, *args): def __init__(self, *args):
EnComboBox.__init__(self, *args) EnComboBox.__init__(self, *args)
self.setLineEdit(MultiCompleteLineEdit(self)) self.setLineEdit(MultiCompleteLineEdit(self))
# Needed to allow changing the case of an existing item
# otherwise on focus out, the text is changed to the
# item that matches case insensitively
c = self.lineEdit().completer()
c.setCaseSensitivity(Qt.CaseSensitive)
def update_items_cache(self, complete_items): def update_items_cache(self, complete_items):
self.lineEdit().update_items_cache(complete_items) self.lineEdit().update_items_cache(complete_items)

View File

@ -27,8 +27,8 @@ class HeuristicsWidget(Widget, Ui_Form):
'dehyphenate', 'renumber_headings'] 'dehyphenate', 'renumber_headings']
) )
self.db, self.book_id = db, book_id self.db, self.book_id = db, book_id
self.rssb_defaults = [u'', u'<hr />', u'* * *', u'• • •', u'✦ ✦ ✦', self.rssb_defaults = [u'', u'<hr />', u' ', u'• • •', u'♦ ♦ ♦',
u'✮ ✮ ✮', u'☆ ☆ ☆', u'❂ ❂ ❂', u'✣ ✣ ✣', u'❖ ❖ ❖', u'☼ ☼ ☼', u'✠ ✠ ✠'] u'† †', u'‡ ‡ ‡', u'∞ ∞ ∞', u'¤ ¤ ¤', u'§']
self.initialize_options(get_option, get_help, db, book_id) self.initialize_options(get_option, get_help, db, book_id)
self.load_histories() self.load_histories()

View File

@ -70,9 +70,6 @@ class MetadataWidget(Widget, Ui_Form):
def initialize_metadata_options(self): def initialize_metadata_options(self):
self.initialize_combos() self.initialize_combos()
self.author.editTextChanged.connect(self.deduce_author_sort) self.author.editTextChanged.connect(self.deduce_author_sort)
self.author.set_separator('&')
self.author.set_space_before_sep(True)
self.author.update_items_cache(self.db.all_author_names())
mi = self.db.get_metadata(self.book_id, index_is_id=True) mi = self.db.get_metadata(self.book_id, index_is_id=True)
self.title.setText(mi.title) self.title.setText(mi.title)
@ -109,6 +106,9 @@ class MetadataWidget(Widget, Ui_Form):
def initalize_authors(self): def initalize_authors(self):
all_authors = self.db.all_authors() all_authors = self.db.all_authors()
all_authors.sort(key=lambda x : sort_key(x[1])) all_authors.sort(key=lambda x : sort_key(x[1]))
self.author.set_separator('&')
self.author.set_space_before_sep(True)
self.author.update_items_cache(self.db.all_author_names())
for i in all_authors: for i in all_authors:
id, name = i id, name = i
@ -124,6 +124,8 @@ class MetadataWidget(Widget, Ui_Form):
def initialize_series(self): def initialize_series(self):
all_series = self.db.all_series() all_series = self.db.all_series()
all_series.sort(key=lambda x : sort_key(x[1])) all_series.sort(key=lambda x : sort_key(x[1]))
self.series.set_separator(None)
self.series.update_items_cache([x[1] for x in all_series])
for i in all_series: for i in all_series:
id, name = i id, name = i
@ -133,6 +135,8 @@ class MetadataWidget(Widget, Ui_Form):
def initialize_publisher(self): def initialize_publisher(self):
all_publishers = self.db.all_publishers() all_publishers = self.db.all_publishers()
all_publishers.sort(key=lambda x : sort_key(x[1])) all_publishers.sort(key=lambda x : sort_key(x[1]))
self.publisher.set_separator(None)
self.publisher.update_items_cache([x[1] for x in all_publishers])
for i in all_publishers: for i in all_publishers:
id, name = i id, name = i

View File

@ -190,7 +190,7 @@
</widget> </widget>
</item> </item>
<item row="4" column="1"> <item row="4" column="1">
<widget class="CompleteLineEdit" name="tags"> <widget class="MultiCompleteLineEdit" name="tags">
<property name="toolTip"> <property name="toolTip">
<string>Tags categorize the book. This is particularly useful while searching. &lt;br&gt;&lt;br&gt;They can be any words or phrases, separated by commas.</string> <string>Tags categorize the book. This is particularly useful while searching. &lt;br&gt;&lt;br&gt;They can be any words or phrases, separated by commas.</string>
</property> </property>
@ -213,7 +213,7 @@
</widget> </widget>
</item> </item>
<item row="5" column="1"> <item row="5" column="1">
<widget class="EnComboBox" name="series"> <widget class="MultiCompleteComboBox" name="series">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed"> <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>10</horstretch> <horstretch>10</horstretch>
@ -248,14 +248,14 @@
</widget> </widget>
</item> </item>
<item row="3" column="1"> <item row="3" column="1">
<widget class="EnComboBox" name="publisher"> <widget class="MultiCompleteComboBox" name="publisher">
<property name="editable"> <property name="editable">
<bool>true</bool> <bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="1"> <item row="1" column="1">
<widget class="CompleteComboBox" name="author"> <widget class="MultiCompleteComboBox" name="author">
<property name="editable"> <property name="editable">
<bool>true</bool> <bool>true</bool>
</property> </property>
@ -277,19 +277,14 @@
<header>widgets.h</header> <header>widgets.h</header>
</customwidget> </customwidget>
<customwidget> <customwidget>
<class>EnComboBox</class> <class>MultiCompleteComboBox</class>
<extends>QComboBox</extends> <extends>QComboBox</extends>
<header>widgets.h</header> <header>calibre/gui2/complete.h</header>
</customwidget> </customwidget>
<customwidget> <customwidget>
<class>CompleteComboBox</class> <class>MultiCompleteLineEdit</class>
<extends>QComboBox</extends>
<header>widgets.h</header>
</customwidget>
<customwidget>
<class>CompleteLineEdit</class>
<extends>QLineEdit</extends> <extends>QLineEdit</extends>
<header>widgets.h</header> <header>calibre/gui2/complete.h</header>
</customwidget> </customwidget>
<customwidget> <customwidget>
<class>ImageView</class> <class>ImageView</class>

View File

@ -14,7 +14,7 @@ from PyQt4.Qt import QComboBox, QLabel, QSpinBox, QDoubleSpinBox, QDateEdit, \
QPushButton QPushButton
from calibre.utils.date import qt_to_dt, now from calibre.utils.date import qt_to_dt, now
from calibre.gui2.widgets import CompleteLineEdit, EnComboBox from calibre.gui2.complete import MultiCompleteLineEdit, MultiCompleteComboBox
from calibre.gui2.comments_editor import Editor as CommentsEditor from calibre.gui2.comments_editor import Editor as CommentsEditor
from calibre.gui2 import UNDEFINED_QDATE, error_dialog from calibre.gui2 import UNDEFINED_QDATE, error_dialog
from calibre.utils.config import tweaks from calibre.utils.config import tweaks
@ -44,8 +44,10 @@ class Base(object):
val = self.gui_val val = self.gui_val
val = self.normalize_ui_val(val) val = self.normalize_ui_val(val)
if val != self.initial_val: if val != self.initial_val:
self.db.set_custom(book_id, val, num=self.col_id, notify=notify, return self.db.set_custom(book_id, val, num=self.col_id,
commit=False) notify=notify, commit=False, allow_case_change=True)
else:
return set()
def normalize_db_val(self, val): def normalize_db_val(self, val):
return val return val
@ -228,10 +230,12 @@ class Text(Base):
values = self.all_values = list(self.db.all_custom(num=self.col_id)) values = self.all_values = list(self.db.all_custom(num=self.col_id))
values.sort(key=sort_key) values.sort(key=sort_key)
if self.col_metadata['is_multiple']: if self.col_metadata['is_multiple']:
w = CompleteLineEdit(parent, values) w = MultiCompleteLineEdit(parent)
w.update_items_cache(values)
w.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) w.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
else: else:
w = EnComboBox(parent) w = MultiCompleteComboBox(parent)
w.set_separator(None)
w.setSizeAdjustPolicy(w.AdjustToMinimumContentsLengthWithIcon) w.setSizeAdjustPolicy(w.AdjustToMinimumContentsLengthWithIcon)
w.setMinimumContentsLength(25) w.setMinimumContentsLength(25)
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent), w] self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent), w]
@ -240,9 +244,10 @@ class Text(Base):
val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True) val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
self.initial_val = val self.initial_val = val
val = self.normalize_db_val(val) val = self.normalize_db_val(val)
self.widgets[1].update_items_cache(self.all_values)
if self.col_metadata['is_multiple']: if self.col_metadata['is_multiple']:
self.setter(val) self.setter(val)
self.widgets[1].update_items_cache(self.all_values)
else: else:
idx = None idx = None
for i, c in enumerate(self.all_values): for i, c in enumerate(self.all_values):
@ -276,7 +281,7 @@ class Series(Base):
def setup_ui(self, parent): def setup_ui(self, parent):
values = self.all_values = list(self.db.all_custom(num=self.col_id)) values = self.all_values = list(self.db.all_custom(num=self.col_id))
values.sort(key=sort_key) values.sort(key=sort_key)
w = EnComboBox(parent) w = MultiCompleteComboBox(parent)
w.setSizeAdjustPolicy(w.AdjustToMinimumContentsLengthWithIcon) w.setSizeAdjustPolicy(w.AdjustToMinimumContentsLengthWithIcon)
w.setMinimumContentsLength(25) w.setMinimumContentsLength(25)
self.name_widget = w self.name_widget = w
@ -305,6 +310,7 @@ class Series(Base):
if c == val: if c == val:
idx = i idx = i
self.name_widget.addItem(c) self.name_widget.addItem(c)
self.name_widget.update_items_cache(self.all_values)
self.name_widget.setEditText('') self.name_widget.setEditText('')
if idx is not None: if idx is not None:
self.widgets[1].setCurrentIndex(idx) self.widgets[1].setCurrentIndex(idx)
@ -326,8 +332,10 @@ class Series(Base):
num=self.col_id) num=self.col_id)
else: else:
s_index = None s_index = None
self.db.set_custom(book_id, val, extra=s_index, return self.db.set_custom(book_id, val, extra=s_index, num=self.col_id,
num=self.col_id, notify=notify, commit=False) notify=notify, commit=False, allow_case_change=True)
else:
return set()
class Enumeration(Base): class Enumeration(Base):
@ -670,7 +678,7 @@ class BulkDateTime(BulkBase):
class BulkSeries(BulkBase): class BulkSeries(BulkBase):
def setup_ui(self, parent): def setup_ui(self, parent):
self.make_widgets(parent, EnComboBox) self.make_widgets(parent, MultiCompleteComboBox)
values = self.all_values = list(self.db.all_custom(num=self.col_id)) values = self.all_values = list(self.db.all_custom(num=self.col_id))
values.sort(key=sort_key) values.sort(key=sort_key)
self.main_widget.setSizeAdjustPolicy(self.main_widget.AdjustToMinimumContentsLengthWithIcon) self.main_widget.setSizeAdjustPolicy(self.main_widget.AdjustToMinimumContentsLengthWithIcon)
@ -705,6 +713,8 @@ class BulkSeries(BulkBase):
def initialize(self, book_id): def initialize(self, book_id):
self.idx_widget.setChecked(False) self.idx_widget.setChecked(False)
self.main_widget.set_separator(None)
self.main_widget.update_items_cache(self.all_values)
for c in self.all_values: for c in self.all_values:
self.main_widget.addItem(c) self.main_widget.addItem(c)
self.main_widget.setEditText('') self.main_widget.setEditText('')
@ -795,7 +805,8 @@ class RemoveTags(QWidget):
layout.setSpacing(5) layout.setSpacing(5)
layout.setContentsMargins(0, 0, 0, 0) layout.setContentsMargins(0, 0, 0, 0)
self.tags_box = CompleteLineEdit(parent, values) self.tags_box = MultiCompleteLineEdit(parent)
self.tags_box.update_items_cache(values)
layout.addWidget(self.tags_box, stretch=3) layout.addWidget(self.tags_box, stretch=3)
self.checkbox = QCheckBox(_('Remove all tags'), parent) self.checkbox = QCheckBox(_('Remove all tags'), parent)
layout.addWidget(self.checkbox) layout.addWidget(self.checkbox)
@ -816,7 +827,7 @@ class BulkText(BulkBase):
values = self.all_values = list(self.db.all_custom(num=self.col_id)) values = self.all_values = list(self.db.all_custom(num=self.col_id))
values.sort(key=sort_key) values.sort(key=sort_key)
if self.col_metadata['is_multiple']: if self.col_metadata['is_multiple']:
self.make_widgets(parent, CompleteLineEdit, self.make_widgets(parent, MultiCompleteLineEdit,
extra_label_text=_('tags to add')) extra_label_text=_('tags to add'))
self.main_widget.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) self.main_widget.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
self.adding_widget = self.main_widget self.adding_widget = self.main_widget
@ -829,16 +840,16 @@ class BulkText(BulkBase):
w.tags_box.textChanged.connect(self.a_c_checkbox_changed) w.tags_box.textChanged.connect(self.a_c_checkbox_changed)
w.checkbox.stateChanged.connect(self.a_c_checkbox_changed) w.checkbox.stateChanged.connect(self.a_c_checkbox_changed)
else: else:
self.make_widgets(parent, EnComboBox) self.make_widgets(parent, MultiCompleteComboBox)
self.main_widget.set_separator(None)
self.main_widget.setSizeAdjustPolicy( self.main_widget.setSizeAdjustPolicy(
self.main_widget.AdjustToMinimumContentsLengthWithIcon) self.main_widget.AdjustToMinimumContentsLengthWithIcon)
self.main_widget.setMinimumContentsLength(25) self.main_widget.setMinimumContentsLength(25)
self.ignore_change_signals = False self.ignore_change_signals = False
def initialize(self, book_ids): def initialize(self, book_ids):
if self.col_metadata['is_multiple']:
self.main_widget.update_items_cache(self.all_values) self.main_widget.update_items_cache(self.all_values)
else: if not self.col_metadata['is_multiple']:
val = self.get_initial_value(book_ids) val = self.get_initial_value(book_ids)
self.initial_val = val = self.normalize_db_val(val) self.initial_val = val = self.normalize_db_val(val)
idx = None idx = None

View File

@ -7,8 +7,8 @@ __license__ = 'GPL v3'
from PyQt4.Qt import QDialog, QGridLayout, QLabel, QDialogButtonBox, \ from PyQt4.Qt import QDialog, QGridLayout, QLabel, QDialogButtonBox, \
QApplication, QSpinBox, QToolButton, QIcon QApplication, QSpinBox, QToolButton, QIcon
from calibre.ebooks.metadata import authors_to_string, string_to_authors from calibre.ebooks.metadata import authors_to_string, string_to_authors
from calibre.gui2.widgets import CompleteComboBox
from calibre.utils.icu import sort_key from calibre.utils.icu import sort_key
from calibre.gui2.complete import MultiCompleteComboBox
class AddEmptyBookDialog(QDialog): class AddEmptyBookDialog(QDialog):
@ -32,7 +32,7 @@ class AddEmptyBookDialog(QDialog):
self.author_label = QLabel(_('Set the author of the new books to:')) self.author_label = QLabel(_('Set the author of the new books to:'))
self._layout.addWidget(self.author_label, 2, 0, 1, 2) self._layout.addWidget(self.author_label, 2, 0, 1, 2)
self.authors_combo = CompleteComboBox(self) self.authors_combo = MultiCompleteComboBox(self)
self.authors_combo.setSizeAdjustPolicy( self.authors_combo.setSizeAdjustPolicy(
self.authors_combo.AdjustToMinimumContentsLengthWithIcon) self.authors_combo.AdjustToMinimumContentsLengthWithIcon)
self.authors_combo.setEditable(True) self.authors_combo.setEditable(True)

View File

@ -11,7 +11,7 @@ from PyQt4.Qt import Qt, QDialog, QGridLayout, QVBoxLayout, QFont, QLabel, \
from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog
from calibre.gui2.dialogs.tag_editor import TagEditor from calibre.gui2.dialogs.tag_editor import TagEditor
from calibre.ebooks.metadata import string_to_authors, authors_to_string from calibre.ebooks.metadata import string_to_authors, authors_to_string, title_sort
from calibre.ebooks.metadata.book.base import composite_formatter from calibre.ebooks.metadata.book.base import composite_formatter
from calibre.ebooks.metadata.meta import get_metadata from calibre.ebooks.metadata.meta import get_metadata
from calibre.gui2.custom_column_widgets import populate_metadata_page from calibre.gui2.custom_column_widgets import populate_metadata_page
@ -134,7 +134,7 @@ class MyBlockingBusy(QDialog): # {{{
do_autonumber, do_remove_format, remove_format, do_swap_ta, \ do_autonumber, do_remove_format, remove_format, do_swap_ta, \
do_remove_conv, do_auto_author, series, do_series_restart, \ do_remove_conv, do_auto_author, series, do_series_restart, \
series_start_value, do_title_case, cover_action, clear_series, \ series_start_value, do_title_case, cover_action, clear_series, \
pubdate, adddate = self.args pubdate, adddate, do_title_sort = self.args
# first loop: do author and title. These will commit at the end of each # first loop: do author and title. These will commit at the end of each
@ -159,6 +159,9 @@ class MyBlockingBusy(QDialog): # {{{
if do_title_case and not title_set: if do_title_case and not title_set:
title = self.db.title(id, index_is_id=True) title = self.db.title(id, index_is_id=True)
self.db.set_title(id, titlecase(title), notify=False) self.db.set_title(id, titlecase(title), notify=False)
if do_title_sort:
title = self.db.title(id, index_is_id=True)
self.db.set_title_sort(id, title_sort(title), notify=False)
if au: if au:
self.db.set_authors(id, string_to_authors(au), notify=False) self.db.set_authors(id, string_to_authors(au), notify=False)
if cover_action == 'remove': if cover_action == 'remove':
@ -360,11 +363,11 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
if (f in ['author_sort'] or if (f in ['author_sort'] or
(fm[f]['datatype'] in ['text', 'series', 'enumeration'] (fm[f]['datatype'] in ['text', 'series', 'enumeration']
and fm[f].get('search_terms', None) and fm[f].get('search_terms', None)
and f not in ['formats', 'ondevice', 'sort']) or and f not in ['formats', 'ondevice']) or
fm[f]['datatype'] in ['int', 'float', 'bool'] ): fm[f]['datatype'] in ['int', 'float', 'bool'] ):
self.all_fields.append(f) self.all_fields.append(f)
self.writable_fields.append(f) self.writable_fields.append(f)
if f in ['sort'] or fm[f]['datatype'] == 'composite': if fm[f]['datatype'] == 'composite':
self.all_fields.append(f) self.all_fields.append(f)
self.all_fields.sort() self.all_fields.sort()
self.all_fields.insert(1, '{template}') self.all_fields.insert(1, '{template}')
@ -437,7 +440,7 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
self.replace_func.addItems(sorted(self.s_r_functions.keys())) self.replace_func.addItems(sorted(self.s_r_functions.keys()))
self.search_mode.currentIndexChanged[int].connect(self.s_r_search_mode_changed) self.search_mode.currentIndexChanged[int].connect(self.s_r_search_mode_changed)
self.search_field.currentIndexChanged[int].connect(self.s_r_search_field_changed) self.search_field.currentIndexChanged[int].connect(self.s_r_search_field_changed)
self.destination_field.currentIndexChanged[str].connect(self.s_r_destination_field_changed) self.destination_field.currentIndexChanged[int].connect(self.s_r_destination_field_changed)
self.replace_mode.currentIndexChanged[int].connect(self.s_r_paint_results) self.replace_mode.currentIndexChanged[int].connect(self.s_r_paint_results)
self.replace_func.currentIndexChanged[str].connect(self.s_r_paint_results) self.replace_func.currentIndexChanged[str].connect(self.s_r_paint_results)
@ -469,6 +472,16 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
self.query_field.currentIndexChanged[str].connect(self.s_r_query_change) self.query_field.currentIndexChanged[str].connect(self.s_r_query_change)
self.query_field.setCurrentIndex(0) self.query_field.setCurrentIndex(0)
def s_r_sf_itemdata(self, idx):
if idx is None:
idx = self.search_field.currentIndex()
return unicode(self.search_field.itemData(idx).toString())
def s_r_df_itemdata(self, idx):
if idx is None:
idx = self.destination_field.currentIndex()
return unicode(self.destination_field.itemData(idx).toString())
def s_r_get_field(self, mi, field): def s_r_get_field(self, mi, field):
if field: if field:
if field == '{template}': if field == '{template}':
@ -508,7 +521,7 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
for i in range(0, self.s_r_number_of_books): for i in range(0, self.s_r_number_of_books):
w = getattr(self, 'book_%d_text'%(i+1)) w = getattr(self, 'book_%d_text'%(i+1))
mi = self.db.get_metadata(self.ids[i], index_is_id=True) mi = self.db.get_metadata(self.ids[i], index_is_id=True)
src = unicode(self.search_field.currentText()) src = self.s_r_sf_itemdata(idx)
t = self.s_r_get_field(mi, src) t = self.s_r_get_field(mi, src)
if len(t) > 1: if len(t) > 1:
t = t[self.starting_from.value()-1: t = t[self.starting_from.value()-1:
@ -518,13 +531,13 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
if self.search_mode.currentIndex() == 0: if self.search_mode.currentIndex() == 0:
self.destination_field.setCurrentIndex(idx) self.destination_field.setCurrentIndex(idx)
else: else:
self.s_r_destination_field_changed(self.destination_field.currentText()) self.s_r_destination_field_changed(self.destination_field.currentIndex())
self.s_r_paint_results(None) self.s_r_paint_results(None)
def s_r_destination_field_changed(self, txt): def s_r_destination_field_changed(self, idx):
txt = unicode(txt) txt = self.s_r_df_itemdata(idx)
if not txt: if not txt:
txt = unicode(self.search_field.currentText()) txt = self.s_r_sf_itemdata(None)
if txt and txt in self.writable_fields: if txt and txt in self.writable_fields:
self.destination_field_fm = self.db.metadata_for_field(txt) self.destination_field_fm = self.db.metadata_for_field(txt)
self.s_r_paint_results(None) self.s_r_paint_results(None)
@ -533,8 +546,9 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
self.search_field.clear() self.search_field.clear()
self.destination_field.clear() self.destination_field.clear()
if val == 0: if val == 0:
self.search_field.addItems(self.writable_fields) for f in self.writable_fields:
self.destination_field.addItems(self.writable_fields) self.search_field.addItem(f if f != 'sort' else 'title_sort', f)
self.destination_field.addItem(f if f != 'sort' else 'title_sort', f)
self.destination_field.setCurrentIndex(0) self.destination_field.setCurrentIndex(0)
self.destination_field.setVisible(False) self.destination_field.setVisible(False)
self.destination_field_label.setVisible(False) self.destination_field_label.setVisible(False)
@ -544,8 +558,14 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
self.comma_separated.setVisible(False) self.comma_separated.setVisible(False)
self.s_r_heading.setText('<p>'+self.main_heading + self.character_heading) self.s_r_heading.setText('<p>'+self.main_heading + self.character_heading)
else: else:
self.search_field.addItems(self.all_fields) self.search_field.blockSignals(True)
self.destination_field.addItems(self.writable_fields) self.destination_field.blockSignals(True)
for f in self.all_fields:
self.search_field.addItem(f if f != 'sort' else 'title_sort', f)
for f in self.writable_fields:
self.destination_field.addItem(f if f != 'sort' else 'title_sort', f)
self.search_field.blockSignals(False)
self.destination_field.blockSignals(False)
self.destination_field.setVisible(True) self.destination_field.setVisible(True)
self.destination_field_label.setVisible(True) self.destination_field_label.setVisible(True)
self.replace_mode.setVisible(True) self.replace_mode.setVisible(True)
@ -575,7 +595,7 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
return rfunc(rtext) return rfunc(rtext)
def s_r_do_regexp(self, mi): def s_r_do_regexp(self, mi):
src_field = unicode(self.search_field.currentText()) src_field = self.s_r_sf_itemdata(None)
src = self.s_r_get_field(mi, src_field) src = self.s_r_get_field(mi, src_field)
result = [] result = []
rfunc = self.s_r_functions[unicode(self.replace_func.currentText())] rfunc = self.s_r_functions[unicode(self.replace_func.currentText())]
@ -587,10 +607,10 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
return result return result
def s_r_do_destination(self, mi, val): def s_r_do_destination(self, mi, val):
src = unicode(self.search_field.currentText()) src = self.s_r_sf_itemdata(None)
if src == '': if src == '':
return '' return ''
dest = unicode(self.destination_field.currentText()) dest = self.s_r_df_itemdata(None)
if dest == '': if dest == '':
if self.db.metadata_for_field(src)['datatype'] == 'composite': if self.db.metadata_for_field(src)['datatype'] == 'composite':
raise Exception(_('You must specify a destination when source is a composite field')) raise Exception(_('You must specify a destination when source is a composite field'))
@ -680,10 +700,10 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
break break
def do_search_replace(self, id): def do_search_replace(self, id):
source = unicode(self.search_field.currentText()) source = self.s_r_sf_itemdata(None)
if not source or not self.s_r_obj: if not source or not self.s_r_obj:
return return
dest = unicode(self.destination_field.currentText()) dest = self.s_r_df_itemdata(None)
if not dest: if not dest:
dest = source dest = source
dfm = self.db.field_metadata[dest] dfm = self.db.field_metadata[dest]
@ -717,6 +737,8 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
else: else:
if dest == 'comments': if dest == 'comments':
setter = self.db.set_comment setter = self.db.set_comment
elif dest == 'sort':
setter = self.db.set_title_sort
else: else:
setter = getattr(self.db, 'set_'+dest) setter = getattr(self.db, 'set_'+dest)
if dest in ['title', 'authors']: if dest in ['title', 'authors']:
@ -764,6 +786,8 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
def initialize_series(self): def initialize_series(self):
all_series = self.db.all_series() all_series = self.db.all_series()
all_series.sort(key=lambda x : sort_key(x[1])) all_series.sort(key=lambda x : sort_key(x[1]))
self.series.set_separator(None)
self.series.update_items_cache([x[1] for x in all_series])
for i in all_series: for i in all_series:
id, name = i id, name = i
@ -773,6 +797,8 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
def initialize_publisher(self): def initialize_publisher(self):
all_publishers = self.db.all_publishers() all_publishers = self.db.all_publishers()
all_publishers.sort(key=lambda x : sort_key(x[1])) all_publishers.sort(key=lambda x : sort_key(x[1]))
self.publisher.set_separator(None)
self.publisher.update_items_cache([x[1] for x in all_publishers])
for i in all_publishers: for i in all_publishers:
id, name = i id, name = i
@ -840,6 +866,7 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
do_remove_conv = self.remove_conversion_settings.isChecked() do_remove_conv = self.remove_conversion_settings.isChecked()
do_auto_author = self.auto_author_sort.isChecked() do_auto_author = self.auto_author_sort.isChecked()
do_title_case = self.change_title_to_title_case.isChecked() do_title_case = self.change_title_to_title_case.isChecked()
do_title_sort = self.update_title_sort.isChecked()
pubdate = adddate = None pubdate = adddate = None
if self.apply_pubdate.isChecked(): if self.apply_pubdate.isChecked():
pubdate = qt_to_dt(self.pubdate.date()) pubdate = qt_to_dt(self.pubdate.date())
@ -858,7 +885,7 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
do_autonumber, do_remove_format, remove_format, do_swap_ta, do_autonumber, do_remove_format, remove_format, do_swap_ta,
do_remove_conv, do_auto_author, series, do_series_restart, do_remove_conv, do_auto_author, series, do_series_restart,
series_start_value, do_title_case, cover_action, clear_series, series_start_value, do_title_case, cover_action, clear_series,
pubdate, adddate) pubdate, adddate, do_title_sort)
bb = MyBlockingBusy(_('Applying changes to %d books.\nPhase {0} {1}%%.') bb = MyBlockingBusy(_('Applying changes to %d books.\nPhase {0} {1}%%.')
%len(self.ids), args, self.db, self.ids, %len(self.ids), args, self.db, self.ids,

View File

@ -76,7 +76,7 @@
</widget> </widget>
</item> </item>
<item row="0" column="1"> <item row="0" column="1">
<widget class="CompleteComboBox" name="authors"> <widget class="MultiCompleteComboBox" name="authors">
<property name="editable"> <property name="editable">
<bool>true</bool> <bool>true</bool>
</property> </property>
@ -175,7 +175,7 @@
</widget> </widget>
</item> </item>
<item row="4" column="1"> <item row="4" column="1">
<widget class="EnComboBox" name="publisher"> <widget class="MultiCompleteComboBox" name="publisher">
<property name="editable"> <property name="editable">
<bool>true</bool> <bool>true</bool>
</property> </property>
@ -195,7 +195,7 @@
</widget> </widget>
</item> </item>
<item row="5" column="1"> <item row="5" column="1">
<widget class="CompleteLineEdit" name="tags"> <widget class="MultiCompleteLineEdit" name="tags">
<property name="toolTip"> <property name="toolTip">
<string>Tags categorize the book. This is particularly useful while searching. &lt;br&gt;&lt;br&gt;They can be any words or phrases, separated by commas.</string> <string>Tags categorize the book. This is particularly useful while searching. &lt;br&gt;&lt;br&gt;They can be any words or phrases, separated by commas.</string>
</property> </property>
@ -229,7 +229,7 @@
</widget> </widget>
</item> </item>
<item row="6" column="1"> <item row="6" column="1">
<widget class="CompleteLineEdit" name="remove_tags"> <widget class="MultiCompleteLineEdit" name="remove_tags">
<property name="toolTip"> <property name="toolTip">
<string>Comma separated list of tags to remove from the books. </string> <string>Comma separated list of tags to remove from the books. </string>
</property> </property>
@ -262,7 +262,7 @@
</widget> </widget>
</item> </item>
<item row="7" column="1"> <item row="7" column="1">
<widget class="EnComboBox" name="series"> <widget class="MultiCompleteComboBox" name="series">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed"> <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch> <horstretch>0</horstretch>
@ -489,6 +489,16 @@ title and author are swapped before the title case is set</string>
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QCheckBox" name="update_title_sort">
<property name="toolTip">
<string>Update title sort based on the current title. This will be applied only after other changes to title.</string>
</property>
<property name="text">
<string>Update &amp;title sort</string>
</property>
</widget>
</item>
<item> <item>
<spacer name="horizontalSpacer"> <spacer name="horizontalSpacer">
<property name="orientation"> <property name="orientation">
@ -1072,19 +1082,14 @@ not multiple and the destination field is multiple</string>
<header>widgets.h</header> <header>widgets.h</header>
</customwidget> </customwidget>
<customwidget> <customwidget>
<class>EnComboBox</class> <class>MultiCompleteComboBox</class>
<extends>QComboBox</extends> <extends>QComboBox</extends>
<header>widgets.h</header> <header>calibre/gui2/complete.h</header>
</customwidget> </customwidget>
<customwidget> <customwidget>
<class>CompleteComboBox</class> <class>MultiCompleteLineEdit</class>
<extends>QComboBox</extends>
<header>widgets.h</header>
</customwidget>
<customwidget>
<class>CompleteLineEdit</class>
<extends>QLineEdit</extends> <extends>QLineEdit</extends>
<header>widgets.h</header> <header>calibre/gui2/complete.h</header>
</customwidget> </customwidget>
<customwidget> <customwidget>
<class>HistoryLineEdit</class> <class>HistoryLineEdit</class>

View File

@ -622,6 +622,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.original_author = unicode(self.authors.text()).strip() self.original_author = unicode(self.authors.text()).strip()
self.original_title = unicode(self.title.text()).strip() self.original_title = unicode(self.title.text()).strip()
self.books_to_refresh = set()
self.show() self.show()
@ -739,6 +740,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.series.setSizeAdjustPolicy(self.series.AdjustToContentsOnFirstShow) self.series.setSizeAdjustPolicy(self.series.AdjustToContentsOnFirstShow)
all_series = self.db.all_series() all_series = self.db.all_series()
all_series.sort(key=lambda x : sort_key(x[1])) all_series.sort(key=lambda x : sort_key(x[1]))
self.series.set_separator(None)
self.series.update_items_cache([x[1] for x in all_series])
series_id = self.db.series_id(self.row) series_id = self.db.series_id(self.row)
idx, c = None, 0 idx, c = None, 0
for i in all_series: for i in all_series:
@ -756,6 +759,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
def initialize_publisher(self): def initialize_publisher(self):
all_publishers = self.db.all_publishers() all_publishers = self.db.all_publishers()
all_publishers.sort(key=lambda x : sort_key(x[1])) all_publishers.sort(key=lambda x : sort_key(x[1]))
self.publisher.set_separator(None)
self.publisher.update_items_cache([x[1] for x in all_publishers])
publisher_id = self.db.publisher_id(self.row) publisher_id = self.db.publisher_id(self.row)
idx, c = None, 0 idx, c = None, 0
for i in all_publishers: for i in all_publishers:
@ -775,7 +780,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
_('You have changed the tags. In order to use the tags' _('You have changed the tags. In order to use the tags'
' editor, you must either discard or apply these ' ' editor, you must either discard or apply these '
'changes. Apply changes?'), show_copy_button=False): 'changes. Apply changes?'), show_copy_button=False):
self.apply_tags(commit=True, notify=True) self.books_to_refresh |= self.apply_tags(commit=True, notify=True,
allow_case_change=True)
self.original_tags = unicode(self.tags.text()) self.original_tags = unicode(self.tags.text())
else: else:
self.tags.setText(self.original_tags) self.tags.setText(self.original_tags)
@ -882,9 +888,9 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
break break
def apply_tags(self, commit=False, notify=False): def apply_tags(self, commit=False, notify=False):
self.db.set_tags(self.id, [x.strip() for x in return self.db.set_tags(self.id, [x.strip() for x in
unicode(self.tags.text()).split(',')], unicode(self.tags.text()).split(',')],
notify=notify, commit=commit) notify=notify, commit=commit, allow_case_change=True)
def next_triggered(self, row_delta, *args): def next_triggered(self, row_delta, *args):
self.row_delta = row_delta self.row_delta = row_delta
@ -903,7 +909,10 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.db.set_title_sort(self.id, ts, notify=False, commit=False) self.db.set_title_sort(self.id, ts, notify=False, commit=False)
au = unicode(self.authors.text()).strip() au = unicode(self.authors.text()).strip()
if au and au != self.original_author: if au and au != self.original_author:
self.db.set_authors(self.id, string_to_authors(au), notify=False) self.books_to_refresh |= self.db.set_authors(self.id,
string_to_authors(au),
notify=False,
allow_case_change=True)
aus = unicode(self.author_sort.text()).strip() aus = unicode(self.author_sort.text()).strip()
if aus: if aus:
self.db.set_author_sort(self.id, aus, notify=False, commit=False) self.db.set_author_sort(self.id, aus, notify=False, commit=False)
@ -913,13 +922,13 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
notify=False, commit=False) notify=False, commit=False)
self.db.set_rating(self.id, 2*self.rating.value(), notify=False, self.db.set_rating(self.id, 2*self.rating.value(), notify=False,
commit=False) commit=False)
self.apply_tags() self.books_to_refresh |= self.apply_tags()
self.db.set_publisher(self.id, self.books_to_refresh |= self.db.set_publisher(self.id,
unicode(self.publisher.currentText()).strip(), unicode(self.publisher.currentText()).strip(),
notify=False, commit=False) notify=False, commit=False, allow_case_change=True)
self.db.set_series(self.id, self.books_to_refresh |= self.db.set_series(self.id,
unicode(self.series.currentText()).strip(), notify=False, unicode(self.series.currentText()).strip(), notify=False,
commit=False) commit=False, allow_case_change=True)
self.db.set_series_index(self.id, self.series_index.value(), self.db.set_series_index(self.id, self.series_index.value(),
notify=False, commit=False) notify=False, commit=False)
self.db.set_comment(self.id, self.db.set_comment(self.id,
@ -940,7 +949,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
else: else:
self.db.remove_cover(self.id) self.db.remove_cover(self.id)
for w in getattr(self, 'custom_column_widgets', []): for w in getattr(self, 'custom_column_widgets', []):
w.commit(self.id) self.books_to_refresh |= w.commit(self.id)
self.db.commit() self.db.commit()
except IOError, err: except IOError, err:
if err.errno == 13: # Permission denied if err.errno == 13: # Permission denied

View File

@ -240,7 +240,7 @@ Using this button to create author sort will change author sort from red to gree
</widget> </widget>
</item> </item>
<item row="2" column="1"> <item row="2" column="1">
<widget class="CompleteComboBox" name="authors"> <widget class="MultiCompleteComboBox" name="authors">
<property name="editable"> <property name="editable">
<bool>true</bool> <bool>true</bool>
</property> </property>
@ -313,7 +313,7 @@ If the box is colored green, then text matches the individual author's sort stri
</widget> </widget>
</item> </item>
<item row="5" column="1" colspan="2"> <item row="5" column="1" colspan="2">
<widget class="EnComboBox" name="publisher"> <widget class="MultiCompleteComboBox" name="publisher">
<property name="editable"> <property name="editable">
<bool>true</bool> <bool>true</bool>
</property> </property>
@ -335,7 +335,7 @@ If the box is colored green, then text matches the individual author's sort stri
<item row="6" column="1"> <item row="6" column="1">
<layout class="QHBoxLayout" name="_2"> <layout class="QHBoxLayout" name="_2">
<item> <item>
<widget class="CompleteLineEdit" name="tags"> <widget class="MultiCompleteLineEdit" name="tags">
<property name="toolTip"> <property name="toolTip">
<string>Tags categorize the book. This is particularly useful while searching. &lt;br&gt;&lt;br&gt;They can be any words or phrases, separated by commas.</string> <string>Tags categorize the book. This is particularly useful while searching. &lt;br&gt;&lt;br&gt;They can be any words or phrases, separated by commas.</string>
</property> </property>
@ -379,7 +379,7 @@ If the box is colored green, then text matches the individual author's sort stri
<number>5</number> <number>5</number>
</property> </property>
<item> <item>
<widget class="EnComboBox" name="series"> <widget class="MultiCompleteComboBox" name="series">
<property name="toolTip"> <property name="toolTip">
<string>List of known series. You can add new series.</string> <string>List of known series. You can add new series.</string>
</property> </property>
@ -837,19 +837,14 @@ If the box is colored green, then text matches the individual author's sort stri
<header>widgets.h</header> <header>widgets.h</header>
</customwidget> </customwidget>
<customwidget> <customwidget>
<class>EnComboBox</class> <class>MultiCompleteLineEdit</class>
<extends>QComboBox</extends>
<header>widgets.h</header>
</customwidget>
<customwidget>
<class>CompleteLineEdit</class>
<extends>QLineEdit</extends> <extends>QLineEdit</extends>
<header>widgets.h</header> <header>calibre/gui2/complete.h</header>
</customwidget> </customwidget>
<customwidget> <customwidget>
<class>CompleteComboBox</class> <class>MultiCompleteComboBox</class>
<extends>QComboBox</extends> <extends>QComboBox</extends>
<header>widgets.h</header> <header>calibre/gui2/complete.h</header>
</customwidget> </customwidget>
<customwidget> <customwidget>
<class>FormatList</class> <class>FormatList</class>

View File

@ -3,7 +3,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import re, copy import re, copy
from PyQt4.Qt import QDialog, QDialogButtonBox, QCompleter, Qt from PyQt4.Qt import QDialog, QDialogButtonBox
from calibre.gui2.dialogs.search_ui import Ui_Dialog from calibre.gui2.dialogs.search_ui import Ui_Dialog
from calibre.library.caches import CONTAINS_MATCH, EQUALS_MATCH from calibre.library.caches import CONTAINS_MATCH, EQUALS_MATCH
@ -29,20 +29,18 @@ class SearchDialog(QDialog, Ui_Dialog):
name = name.strip().replace('|', ',') name = name.strip().replace('|', ',')
self.authors_box.addItem(name) self.authors_box.addItem(name)
self.authors_box.setEditText('') self.authors_box.setEditText('')
self.authors_box.completer().setCompletionMode(QCompleter.PopupCompletion)
self.authors_box.setAutoCompletionCaseSensitivity(Qt.CaseInsensitive)
self.authors_box.set_separator('&') self.authors_box.set_separator('&')
self.authors_box.set_space_before_sep(True) self.authors_box.set_space_before_sep(True)
self.authors_box.update_items_cache(db.all_author_names()) self.authors_box.update_items_cache(db.all_author_names())
all_series = db.all_series() all_series = db.all_series()
all_series.sort(key=lambda x : sort_key(x[1])) all_series.sort(key=lambda x : sort_key(x[1]))
self.series_box.set_separator(None)
self.series_box.update_items_cache([x[1] for x in all_series])
for i in all_series: for i in all_series:
id, name = i id, name = i
self.series_box.addItem(name) self.series_box.addItem(name)
self.series_box.setEditText('') self.series_box.setEditText('')
self.series_box.completer().setCompletionMode(QCompleter.PopupCompletion)
self.series_box.setAutoCompletionCaseSensitivity(Qt.CaseInsensitive)
all_tags = db.all_tags() all_tags = db.all_tags()
self.tags_box.update_items_cache(all_tags) self.tags_box.update_items_cache(all_tags)

View File

@ -265,21 +265,21 @@
</widget> </widget>
</item> </item>
<item row="2" column="1"> <item row="2" column="1">
<widget class="CompleteComboBox" name="authors_box"> <widget class="MultiCompleteComboBox" name="authors_box">
<property name="toolTip"> <property name="toolTip">
<string>Enter an author's name. Only one author can be used.</string> <string>Enter an author's name. Only one author can be used.</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="3" column="1"> <item row="3" column="1">
<widget class="EnComboBox" name="series_box"> <widget class="MultiCompleteComboBox" name="series_box">
<property name="toolTip"> <property name="toolTip">
<string>Enter a series name, without an index. Only one series name can be used.</string> <string>Enter a series name, without an index. Only one series name can be used.</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="1"> <item row="4" column="1">
<widget class="CompleteLineEdit" name="tags_box"> <widget class="MultiCompleteLineEdit" name="tags_box">
<property name="toolTip"> <property name="toolTip">
<string>Enter tags separated by spaces</string> <string>Enter tags separated by spaces</string>
</property> </property>
@ -355,19 +355,14 @@
<header>widgets.h</header> <header>widgets.h</header>
</customwidget> </customwidget>
<customwidget> <customwidget>
<class>EnComboBox</class> <class>MultiCompleteLineEdit</class>
<extends>QComboBox</extends>
<header>widgets.h</header>
</customwidget>
<customwidget>
<class>CompleteLineEdit</class>
<extends>QLineEdit</extends> <extends>QLineEdit</extends>
<header>widgets.h</header> <header>calibre/gui2/complete.h</header>
</customwidget> </customwidget>
<customwidget> <customwidget>
<class>CompleteComboBox</class> <class>MultiCompleteComboBox</class>
<extends>QComboBox</extends> <extends>QComboBox</extends>
<header>widgets.h</header> <header>calibre/gui2/complete.h</header>
</customwidget> </customwidget>
</customwidgets> </customwidgets>
<tabstops> <tabstops>

View File

@ -12,11 +12,11 @@ from PyQt4.Qt import QColor, Qt, QModelIndex, QSize, \
QPainterPath, QLinearGradient, QBrush, \ QPainterPath, QLinearGradient, QBrush, \
QPen, QStyle, QPainter, QStyleOptionViewItemV4, \ QPen, QStyle, QPainter, QStyleOptionViewItemV4, \
QIcon, QDoubleSpinBox, QVariant, QSpinBox, \ QIcon, QDoubleSpinBox, QVariant, QSpinBox, \
QStyledItemDelegate, QCompleter, \ QStyledItemDelegate, QComboBox, QTextDocument
QComboBox, QTextDocument
from calibre.gui2 import UNDEFINED_QDATE, error_dialog from calibre.gui2 import UNDEFINED_QDATE, error_dialog
from calibre.gui2.widgets import EnLineEdit, CompleteLineEdit from calibre.gui2.widgets import EnLineEdit
from calibre.gui2.complete import MultiCompleteLineEdit
from calibre.utils.date import now, format_date from calibre.utils.date import now, format_date
from calibre.utils.config import tweaks from calibre.utils.config import tweaks
from calibre.utils.formatter import validation_formatter from calibre.utils.formatter import validation_formatter
@ -151,39 +151,16 @@ class TextDelegate(QStyledItemDelegate): # {{{
self.auto_complete_function = f self.auto_complete_function = f
def createEditor(self, parent, option, index): def createEditor(self, parent, option, index):
editor = EnLineEdit(parent)
if self.auto_complete_function: if self.auto_complete_function:
editor = MultiCompleteLineEdit(parent)
editor.set_separator(None)
complete_items = [i[1] for i in self.auto_complete_function()] complete_items = [i[1] for i in self.auto_complete_function()]
completer = QCompleter(complete_items, self) editor.update_items_cache(complete_items)
completer.setCaseSensitivity(Qt.CaseInsensitive) else:
completer.setCompletionMode(QCompleter.PopupCompletion) editor = EnLineEdit(parent)
editor.setCompleter(completer)
return editor return editor
#}}} #}}}
class TagsDelegate(QStyledItemDelegate): # {{{
def __init__(self, parent):
QStyledItemDelegate.__init__(self, parent)
self.db = None
def set_database(self, db):
self.db = db
def createEditor(self, parent, option, index):
if self.db:
col = index.model().column_map[index.column()]
if not index.model().is_custom_column(col):
editor = CompleteLineEdit(parent, self.db.all_tags())
else:
editor = CompleteLineEdit(parent,
sorted(list(self.db.all_custom(label=self.db.field_metadata.key_to_label(col))),
key=sort_key))
return editor
else:
editor = EnLineEdit(parent)
return editor
# }}}
class CompleteDelegate(QStyledItemDelegate): # {{{ class CompleteDelegate(QStyledItemDelegate): # {{{
def __init__(self, parent, sep, items_func_name, space_before_sep=False): def __init__(self, parent, sep, items_func_name, space_before_sep=False):
QStyledItemDelegate.__init__(self, parent) QStyledItemDelegate.__init__(self, parent)
@ -197,13 +174,15 @@ class CompleteDelegate(QStyledItemDelegate): # {{{
def createEditor(self, parent, option, index): def createEditor(self, parent, option, index):
if self.db and hasattr(self.db, self.items_func_name): if self.db and hasattr(self.db, self.items_func_name):
col = index.model().column_map[index.column()] col = index.model().column_map[index.column()]
editor = MultiCompleteLineEdit(parent)
editor.set_separator(self.sep)
editor.set_space_before_sep(self.space_before_sep)
if not index.model().is_custom_column(col): if not index.model().is_custom_column(col):
editor = CompleteLineEdit(parent, getattr(self.db, self.items_func_name)(), all_items = getattr(self.db, self.items_func_name)()
self.sep, self.space_before_sep)
else: else:
editor = CompleteLineEdit(parent, all_items = list(self.db.all_custom(
sorted(list(self.db.all_custom(label=self.db.field_metadata.key_to_label(col))), label=self.db.field_metadata.key_to_label(col)))
key=sort_key), self.sep, self.space_before_sep) editor.update_items_cache(all_items)
else: else:
editor = EnLineEdit(parent) editor = EnLineEdit(parent)
return editor return editor
@ -273,13 +252,11 @@ class CcTextDelegate(QStyledItemDelegate): # {{{
editor.setRange(-100., float(sys.maxint)) editor.setRange(-100., float(sys.maxint))
editor.setDecimals(2) editor.setDecimals(2)
else: else:
editor = EnLineEdit(parent) editor = MultiCompleteLineEdit(parent)
editor.set_separator(None)
complete_items = sorted(list(m.db.all_custom(label=m.db.field_metadata.key_to_label(col))), complete_items = sorted(list(m.db.all_custom(label=m.db.field_metadata.key_to_label(col))),
key=sort_key) key=sort_key)
completer = QCompleter(complete_items, self) editor.update_items_cache(complete_items)
completer.setCaseSensitivity(Qt.CaseInsensitive)
completer.setCompletionMode(QCompleter.PopupCompletion)
editor.setCompleter(completer)
return editor return editor
# }}} # }}}

View File

@ -800,9 +800,10 @@ class BooksModel(QAbstractTableModel): # {{{
return True return True
id = self.db.id(row) id = self.db.id(row)
self.db.set_custom(id, val, extra=s_index, books_to_refresh = set([id])
books_to_refresh |= self.db.set_custom(id, val, extra=s_index,
label=label, num=None, append=False, notify=True) label=label, num=None, append=False, notify=True)
self.refresh_ids([id], current_row=row) self.refresh_ids(list(books_to_refresh), current_row=row)
return True return True
def setData(self, index, value, role): def setData(self, index, value, role):
@ -819,6 +820,7 @@ class BooksModel(QAbstractTableModel): # {{{
value.toDate() if column in ('timestamp', 'pubdate') else \ value.toDate() if column in ('timestamp', 'pubdate') else \
unicode(value.toString()) unicode(value.toString())
id = self.db.id(row) id = self.db.id(row)
books_to_refresh = set([id])
if column == 'rating': if column == 'rating':
val = 0 if val < 0 else 5 if val > 5 else val val = 0 if val < 0 else 5 if val > 5 else val
val *= 2 val *= 2
@ -826,7 +828,8 @@ class BooksModel(QAbstractTableModel): # {{{
elif column == 'series': elif column == 'series':
val = val.strip() val = val.strip()
if not val: if not val:
self.db.set_series(id, val) books_to_refresh |= self.db.set_series(id, val,
allow_case_change=True)
self.db.set_series_index(id, 1.0) self.db.set_series_index(id, 1.0)
else: else:
pat = re.compile(r'\[([.0-9]+)\]') pat = re.compile(r'\[([.0-9]+)\]')
@ -840,7 +843,8 @@ class BooksModel(QAbstractTableModel): # {{{
if ni != 1: if ni != 1:
self.db.set_series_index(id, ni) self.db.set_series_index(id, ni)
if val: if val:
self.db.set_series(id, val) books_to_refresh |= self.db.set_series(id, val,
allow_case_change=True)
elif column == 'timestamp': elif column == 'timestamp':
if val.isNull() or not val.isValid(): if val.isNull() or not val.isValid():
return False return False
@ -850,8 +854,9 @@ class BooksModel(QAbstractTableModel): # {{{
return False return False
self.db.set_pubdate(id, qt_to_dt(val, as_utc=False)) self.db.set_pubdate(id, qt_to_dt(val, as_utc=False))
else: else:
self.db.set(row, column, val) books_to_refresh |= self.db.set(row, column, val,
self.refresh_ids([id], row) allow_case_change=True)
self.refresh_ids(list(books_to_refresh), row)
self.dataChanged.emit(index, index) self.dataChanged.emit(index, index)
return True return True

View File

@ -12,7 +12,7 @@ from PyQt4.Qt import Qt, QDateEdit, QDate, \
QDoubleSpinBox, QListWidgetItem, QSize, QPixmap, \ QDoubleSpinBox, QListWidgetItem, QSize, QPixmap, \
QPushButton, QSpinBox, QLineEdit QPushButton, QSpinBox, QLineEdit
from calibre.gui2.widgets import EnLineEdit, EnComboBox, FormatList, ImageView from calibre.gui2.widgets import EnLineEdit, FormatList, ImageView
from calibre.gui2.complete import MultiCompleteLineEdit, MultiCompleteComboBox from calibre.gui2.complete import MultiCompleteLineEdit, MultiCompleteComboBox
from calibre.utils.icu import sort_key from calibre.utils.icu import sort_key
from calibre.utils.config import tweaks, prefs from calibre.utils.config import tweaks, prefs
@ -156,6 +156,7 @@ class AuthorsEdit(MultiCompleteComboBox):
def __init__(self, parent): def __init__(self, parent):
self.dialog = parent self.dialog = parent
self.books_to_refresh = set([])
MultiCompleteComboBox.__init__(self, parent) MultiCompleteComboBox.__init__(self, parent)
self.setToolTip(self.TOOLTIP) self.setToolTip(self.TOOLTIP)
self.setWhatsThis(self.TOOLTIP) self.setWhatsThis(self.TOOLTIP)
@ -166,6 +167,7 @@ class AuthorsEdit(MultiCompleteComboBox):
return _('Unknown') return _('Unknown')
def initialize(self, db, id_): def initialize(self, db, id_):
self.books_to_refresh = set([])
all_authors = db.all_authors() all_authors = db.all_authors()
all_authors.sort(key=lambda x : sort_key(x[1])) all_authors.sort(key=lambda x : sort_key(x[1]))
for i in all_authors: for i in all_authors:
@ -185,7 +187,8 @@ class AuthorsEdit(MultiCompleteComboBox):
def commit(self, db, id_): def commit(self, db, id_):
authors = self.current_val authors = self.current_val
db.set_authors(id_, authors, notify=False) self.books_to_refresh |= db.set_authors(id_, authors, notify=False,
allow_case_change=True)
return True return True
@dynamic_property @dynamic_property
@ -283,19 +286,21 @@ class AuthorSortEdit(EnLineEdit):
# }}} # }}}
# Series {{{ # Series {{{
class SeriesEdit(EnComboBox): class SeriesEdit(MultiCompleteComboBox):
TOOLTIP = _('List of known series. You can add new series.') TOOLTIP = _('List of known series. You can add new series.')
LABEL = _('&Series:') LABEL = _('&Series:')
def __init__(self, parent): def __init__(self, parent):
EnComboBox.__init__(self, parent) MultiCompleteComboBox.__init__(self, parent)
self.set_separator(None)
self.dialog = parent self.dialog = parent
self.setSizeAdjustPolicy( self.setSizeAdjustPolicy(
self.AdjustToMinimumContentsLengthWithIcon) self.AdjustToMinimumContentsLengthWithIcon)
self.setToolTip(self.TOOLTIP) self.setToolTip(self.TOOLTIP)
self.setWhatsThis(self.TOOLTIP) self.setWhatsThis(self.TOOLTIP)
self.setEditable(True) self.setEditable(True)
self.books_to_refresh = set([])
@dynamic_property @dynamic_property
def current_val(self): def current_val(self):
@ -312,8 +317,10 @@ class SeriesEdit(EnComboBox):
return property(fget=fget, fset=fset) return property(fget=fget, fset=fset)
def initialize(self, db, id_): def initialize(self, db, id_):
self.books_to_refresh = set([])
all_series = db.all_series() all_series = db.all_series()
all_series.sort(key=lambda x : sort_key(x[1])) all_series.sort(key=lambda x : sort_key(x[1]))
self.update_items_cache([x[1] for x in all_series])
series_id = db.series_id(id_, index_is_id=True) series_id = db.series_id(id_, index_is_id=True)
idx, c = None, 0 idx, c = None, 0
for i in all_series: for i in all_series:
@ -330,7 +337,8 @@ class SeriesEdit(EnComboBox):
def commit(self, db, id_): def commit(self, db, id_):
series = self.current_val series = self.current_val
db.set_series(id_, series, notify=False, commit=True) self.books_to_refresh |= db.set_series(id_, series, notify=False,
commit=True, allow_case_change=True)
return True return True
class SeriesIndexEdit(QDoubleSpinBox): class SeriesIndexEdit(QDoubleSpinBox):
@ -822,6 +830,7 @@ class TagsEdit(MultiCompleteLineEdit): # {{{
def __init__(self, parent): def __init__(self, parent):
MultiCompleteLineEdit.__init__(self, parent) MultiCompleteLineEdit.__init__(self, parent)
self.books_to_refresh = set([])
self.setToolTip(self.TOOLTIP) self.setToolTip(self.TOOLTIP)
self.setWhatsThis(self.TOOLTIP) self.setWhatsThis(self.TOOLTIP)
@ -836,6 +845,7 @@ class TagsEdit(MultiCompleteLineEdit): # {{{
return property(fget=fget, fset=fset) return property(fget=fget, fset=fset)
def initialize(self, db, id_): def initialize(self, db, id_):
self.books_to_refresh = set([])
tags = db.tags(id_, index_is_id=True) tags = db.tags(id_, index_is_id=True)
tags = tags.split(',') if tags else [] tags = tags.split(',') if tags else []
self.current_val = tags self.current_val = tags
@ -864,7 +874,9 @@ class TagsEdit(MultiCompleteLineEdit): # {{{
def commit(self, db, id_): def commit(self, db, id_):
db.set_tags(id_, self.current_val, notify=False, commit=False) self.books_to_refresh |= db.set_tags(
id_, self.current_val, notify=False, commit=False,
allow_case_change=True)
return True return True
# }}} # }}}
@ -910,13 +922,15 @@ class ISBNEdit(QLineEdit): # {{{
# }}} # }}}
class PublisherEdit(EnComboBox): # {{{ class PublisherEdit(MultiCompleteComboBox): # {{{
LABEL = _('&Publisher:') LABEL = _('&Publisher:')
def __init__(self, parent): def __init__(self, parent):
EnComboBox.__init__(self, parent) MultiCompleteComboBox.__init__(self, parent)
self.set_separator(None)
self.setSizeAdjustPolicy( self.setSizeAdjustPolicy(
self.AdjustToMinimumContentsLengthWithIcon) self.AdjustToMinimumContentsLengthWithIcon)
self.books_to_refresh = set([])
@dynamic_property @dynamic_property
def current_val(self): def current_val(self):
@ -933,8 +947,10 @@ class PublisherEdit(EnComboBox): # {{{
return property(fget=fget, fset=fset) return property(fget=fget, fset=fset)
def initialize(self, db, id_): def initialize(self, db, id_):
self.books_to_refresh = set([])
all_publishers = db.all_publishers() all_publishers = db.all_publishers()
all_publishers.sort(key=lambda x : sort_key(x[1])) all_publishers.sort(key=lambda x : sort_key(x[1]))
self.update_items_cache([x[1] for x in all_publishers])
publisher_id = db.publisher_id(id_, index_is_id=True) publisher_id = db.publisher_id(id_, index_is_id=True)
idx, c = None, 0 idx, c = None, 0
for i in all_publishers: for i in all_publishers:
@ -949,7 +965,8 @@ class PublisherEdit(EnComboBox): # {{{
self.setCurrentIndex(idx) self.setCurrentIndex(idx)
def commit(self, db, id_): def commit(self, db, id_):
db.set_publisher(id_, self.current_val, notify=False, commit=False) self.books_to_refresh |= db.set_publisher(id_, self.current_val,
notify=False, commit=False, allow_case_change=True)
return True return True
# }}} # }}}

View File

@ -31,6 +31,8 @@ class MetadataSingleDialogBase(ResizableDialog):
def __init__(self, db, parent=None): def __init__(self, db, parent=None):
self.db = db self.db = db
self.changed = set([]) self.changed = set([])
self.books_to_refresh = set([])
self.rows_to_refresh = set([])
ResizableDialog.__init__(self, parent) ResizableDialog.__init__(self, parent)
def setupUi(self, *args): # {{{ def setupUi(self, *args): # {{{
@ -192,6 +194,7 @@ class MetadataSingleDialogBase(ResizableDialog):
def __call__(self, id_): def __call__(self, id_):
self.book_id = id_ self.book_id = id_
self.books_to_refresh = set([])
for widget in self.basic_metadata_widgets: for widget in self.basic_metadata_widgets:
widget.initialize(self.db, id_) widget.initialize(self.db, id_)
for widget in self.custom_metadata_widgets: for widget in self.custom_metadata_widgets:
@ -295,6 +298,8 @@ class MetadataSingleDialogBase(ResizableDialog):
try: try:
if not widget.commit(self.db, self.book_id): if not widget.commit(self.db, self.book_id):
return False return False
self.books_to_refresh |= getattr(widget, 'books_to_refresh',
set([]))
except IOError, err: except IOError, err:
if err.errno == 13: # Permission denied if err.errno == 13: # Permission denied
import traceback import traceback
@ -306,9 +311,13 @@ class MetadataSingleDialogBase(ResizableDialog):
return False return False
raise raise
for widget in getattr(self, 'custom_metadata_widgets', []): for widget in getattr(self, 'custom_metadata_widgets', []):
widget.commit(self.book_id) self.books_to_refresh |= widget.commit(self.book_id)
self.db.commit() self.db.commit()
rows = self.db.refresh_ids(list(self.books_to_refresh))
if rows:
self.rows_to_refresh |= set(rows)
return True return True
def accept(self): def accept(self):
@ -330,12 +339,14 @@ class MetadataSingleDialogBase(ResizableDialog):
self.current_row = current_row self.current_row = current_row
if view_slot is not None: if view_slot is not None:
self.view_format.connect(view_slot) self.view_format.connect(view_slot)
self.do_one() self.do_one(apply_changes=False)
ret = self.exec_() ret = self.exec_()
self.break_cycles() self.break_cycles()
return ret return ret
def do_one(self, delta=0): def do_one(self, delta=0, apply_changes=True):
if apply_changes:
self.apply_changes()
self.current_row += delta self.current_row += delta
prev = next_ = None prev = next_ = None
if self.current_row > 0: if self.current_row > 0:
@ -353,6 +364,7 @@ class MetadataSingleDialogBase(ResizableDialog):
self.prev_button.setVisible(prev is not None) self.prev_button.setVisible(prev is not None)
self(self.db.id(self.row_list[self.current_row])) self(self.db.id(self.row_list[self.current_row]))
def break_cycles(self): def break_cycles(self):
# Break any reference cycles that could prevent python # Break any reference cycles that could prevent python
# from garbage collecting this dialog # from garbage collecting this dialog
@ -618,7 +630,7 @@ class MetadataSingleDialogAlt(MetadataSingleDialogBase): # {{{
def edit_metadata(db, row_list, current_row, parent=None, view_slot=None): def edit_metadata(db, row_list, current_row, parent=None, view_slot=None):
d = MetadataSingleDialog(db, parent) d = MetadataSingleDialog(db, parent)
d.start(row_list, current_row, view_slot=view_slot) d.start(row_list, current_row, view_slot=view_slot)
return d.changed return d.changed, d.rows_to_refresh
if __name__ == '__main__': if __name__ == '__main__':
from PyQt4.Qt import QApplication from PyQt4.Qt import QApplication

View File

@ -420,7 +420,8 @@ class ResultCache(SearchQueryParser): # {{{
return candidates - res return candidates - res
return res return res
def get_matches(self, location, query, allow_recursion=True, candidates=None): def get_matches(self, location, query, candidates=None,
allow_recursion=True):
matches = set([]) matches = set([])
if candidates is None: if candidates is None:
candidates = self.universal_set() candidates = self.universal_set()
@ -434,8 +435,8 @@ class ResultCache(SearchQueryParser): # {{{
if isinstance(location, list): if isinstance(location, list):
if allow_recursion: if allow_recursion:
for loc in location: for loc in location:
matches |= self.get_matches(loc, query, candidates, matches |= self.get_matches(loc, query,
allow_recursion=False) candidates=candidates, allow_recursion=False)
return matches return matches
raise ParseException(query, len(query), 'Recursive query group detected', self) raise ParseException(query, len(query), 'Recursive query group detected', self)

View File

@ -1841,8 +1841,6 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
body.insert(btc,pTag) body.insert(btc,pTag)
btc += 1 btc += 1
# <p class="letter_index">
# <p class="book_title">
divTag = Tag(soup, "div") divTag = Tag(soup, "div")
dtc = 0 dtc = 0
current_letter = "" current_letter = ""
@ -1870,11 +1868,12 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
divTag.insert(dtc, divRunningTag) divTag.insert(dtc, divRunningTag)
dtc += 1 dtc += 1
divRunningTag = Tag(soup, 'div') divRunningTag = Tag(soup, 'div')
divRunningTag['class'] = "logical_group" if dtc > 0:
divRunningTag['class'] = "initial_letter"
drtc = 0 drtc = 0
current_letter = self.letter_or_symbol(book['title_sort'][0]) current_letter = self.letter_or_symbol(book['title_sort'][0])
pIndexTag = Tag(soup, "p") pIndexTag = Tag(soup, "p")
pIndexTag['class'] = "letter_index" pIndexTag['class'] = "author_title_letter_index"
aTag = Tag(soup, "a") aTag = Tag(soup, "a")
aTag['name'] = "%s" % self.letter_or_symbol(book['title_sort'][0]) aTag['name'] = "%s" % self.letter_or_symbol(book['title_sort'][0])
pIndexTag.insert(0,aTag) pIndexTag.insert(0,aTag)
@ -1982,8 +1981,6 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
body.insert(btc, aTag) body.insert(btc, aTag)
btc += 1 btc += 1
# <p class="letter_index">
# <p class="author_index">
divTag = Tag(soup, "div") divTag = Tag(soup, "div")
dtc = 0 dtc = 0
divOpeningTag = None divOpeningTag = None
@ -2017,10 +2014,11 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
current_letter = self.letter_or_symbol(book['author_sort'][0].upper()) current_letter = self.letter_or_symbol(book['author_sort'][0].upper())
author_count = 0 author_count = 0
divOpeningTag = Tag(soup, 'div') divOpeningTag = Tag(soup, 'div')
divOpeningTag['class'] = "logical_group" if dtc > 0:
divOpeningTag['class'] = "initial_letter"
dotc = 0 dotc = 0
pIndexTag = Tag(soup, "p") pIndexTag = Tag(soup, "p")
pIndexTag['class'] = "letter_index" pIndexTag['class'] = "author_title_letter_index"
aTag = Tag(soup, "a") aTag = Tag(soup, "a")
aTag['name'] = "%sauthors" % self.letter_or_symbol(current_letter) aTag['name'] = "%sauthors" % self.letter_or_symbol(current_letter)
pIndexTag.insert(0,aTag) pIndexTag.insert(0,aTag)
@ -2032,16 +2030,21 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
# Start a new author # Start a new author
current_author = book['author'] current_author = book['author']
author_count += 1 author_count += 1
if author_count == 2: if author_count >= 2:
# Add divOpeningTag to divTag, kill divOpeningTag # Add divOpeningTag to divTag, kill divOpeningTag
if divOpeningTag:
divTag.insert(dtc, divOpeningTag) divTag.insert(dtc, divOpeningTag)
dtc += 1 dtc += 1
divOpeningTag = None divOpeningTag = None
dotc = 0 dotc = 0
# Create a divRunningTag for the rest of the authors in this letter # Create a divRunningTag for the next author
if author_count > 2:
divTag.insert(dtc, divRunningTag)
dtc += 1
divRunningTag = Tag(soup, 'div') divRunningTag = Tag(soup, 'div')
divRunningTag['class'] = "logical_group" divRunningTag['class'] = "author_logical_group"
drtc = 0 drtc = 0
non_series_books = 0 non_series_books = 0
@ -2373,8 +2376,6 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
body.insert(btc,pTag) body.insert(btc,pTag)
btc += 1 btc += 1
# <p class="letter_index">
# <p class="author_index">
divTag = Tag(soup, "div") divTag = Tag(soup, "div")
dtc = 0 dtc = 0
@ -2558,8 +2559,6 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
body.insert(btc, aTag) body.insert(btc, aTag)
btc += 1 btc += 1
# <p class="letter_index">
# <p class="author_index">
divTag = Tag(soup, "div") divTag = Tag(soup, "div")
dtc = 0 dtc = 0
@ -2661,8 +2660,6 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
body.insert(btc, aTag) body.insert(btc, aTag)
btc += 1 btc += 1
# <p class="letter_index">
# <p class="author_index">
divTag = Tag(soup, "div") divTag = Tag(soup, "div")
dtc = 0 dtc = 0
current_letter = "" current_letter = ""
@ -2677,7 +2674,7 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
# Start a new letter with Index letter # Start a new letter with Index letter
current_letter = self.letter_or_symbol(sort_title[0].upper()) current_letter = self.letter_or_symbol(sort_title[0].upper())
pIndexTag = Tag(soup, "p") pIndexTag = Tag(soup, "p")
pIndexTag['class'] = "letter_index" pIndexTag['class'] = "series_letter_index"
aTag = Tag(soup, "a") aTag = Tag(soup, "a")
aTag['name'] = "%s_series" % self.letter_or_symbol(current_letter) aTag['name'] = "%s_series" % self.letter_or_symbol(current_letter)
pIndexTag.insert(0,aTag) pIndexTag.insert(0,aTag)

View File

@ -440,22 +440,24 @@ class CustomColumns(object):
self.dirtied(ids, commit=False) self.dirtied(ids, commit=False)
self.conn.commit() self.conn.commit()
def set_custom(self, id, val, label=None, num=None, def set_custom(self, id, val, label=None, num=None, append=False,
append=False, notify=True, extra=None, commit=True): notify=True, extra=None, commit=True, allow_case_change=False):
self._set_custom(id, val, label=label, num=num, append=append, rv = self._set_custom(id, val, label=label, num=num, append=append,
notify=notify, extra=extra) notify=notify, extra=extra,
allow_case_change=allow_case_change)
self.dirtied([id], commit=False) self.dirtied([id], commit=False)
if commit: if commit:
self.conn.commit() self.conn.commit()
return rv
def _set_custom(self, id_, val, label=None, num=None, def _set_custom(self, id_, val, label=None, num=None, append=False,
append=False, notify=True, extra=None): notify=True, extra=None, allow_case_change=False):
if label is not None: if label is not None:
data = self.custom_column_label_map[label] data = self.custom_column_label_map[label]
if num is not None: if num is not None:
data = self.custom_column_num_map[num] data = self.custom_column_num_map[num]
if data['datatype'] == 'composite': if data['datatype'] == 'composite':
return None return set([])
if not data['editable']: if not data['editable']:
raise ValueError('Column %r is not editable'%data['label']) raise ValueError('Column %r is not editable'%data['label'])
table, lt = self.custom_table_names(data['num']) table, lt = self.custom_table_names(data['num'])
@ -466,10 +468,11 @@ class CustomColumns(object):
if data['datatype'] == 'series' and extra is None: if data['datatype'] == 'series' and extra is None:
(val, extra) = self._get_series_values(val) (val, extra) = self._get_series_values(val)
books_to_refresh = set([])
if data['normalized']: if data['normalized']:
if data['datatype'] == 'enumeration' and ( if data['datatype'] == 'enumeration' and (
val and val not in data['display']['enum_values']): val and val not in data['display']['enum_values']):
return None return books_to_refresh
if not append or not data['is_multiple']: if not append or not data['is_multiple']:
self.conn.execute('DELETE FROM %s WHERE book=?'%lt, (id_,)) self.conn.execute('DELETE FROM %s WHERE book=?'%lt, (id_,))
self.conn.execute( self.conn.execute(
@ -483,6 +486,7 @@ class CustomColumns(object):
for x in set(set_val) - set(existing): for x in set(set_val) - set(existing):
if x is None: if x is None:
continue continue
case_change = False
existing = list(self.all_custom(num=data['num'])) existing = list(self.all_custom(num=data['num']))
lx = [t.lower() if hasattr(t, 'lower') else t for t in existing] lx = [t.lower() if hasattr(t, 'lower') else t for t in existing]
try: try:
@ -493,7 +497,8 @@ class CustomColumns(object):
ex = existing[idx] ex = existing[idx]
xid = self.conn.get( xid = self.conn.get(
'SELECT id FROM %s WHERE value=?'%table, (ex,), all=False) 'SELECT id FROM %s WHERE value=?'%table, (ex,), all=False)
if ex != x: if allow_case_change and ex != x:
case_change = True
self.conn.execute( self.conn.execute(
'UPDATE %s SET value=? WHERE id=?'%table, (x, xid)) 'UPDATE %s SET value=? WHERE id=?'%table, (x, xid))
else: else:
@ -512,6 +517,10 @@ class CustomColumns(object):
self.conn.execute( self.conn.execute(
'''INSERT INTO %s(book, value) '''INSERT INTO %s(book, value)
VALUES (?,?)'''%lt, (id_, xid)) VALUES (?,?)'''%lt, (id_, xid))
if case_change:
bks = self.conn.get('SELECT book FROM %s WHERE value=?'%lt,
(xid,))
books_to_refresh |= set([bk[0] for bk in bks])
nval = self.conn.get( nval = self.conn.get(
'SELECT custom_%s FROM meta2 WHERE id=?'%data['num'], 'SELECT custom_%s FROM meta2 WHERE id=?'%data['num'],
(id_,), all=False) (id_,), all=False)
@ -530,7 +539,7 @@ class CustomColumns(object):
row_is_id=True) row_is_id=True)
if notify: if notify:
self.notify('metadata', [id_]) self.notify('metadata', [id_])
return nval return books_to_refresh
def clean_custom(self): def clean_custom(self):
st = ('DELETE FROM {table} WHERE (SELECT COUNT(id) FROM {lt} WHERE' st = ('DELETE FROM {table} WHERE (SELECT COUNT(id) FROM {lt} WHERE'

View File

@ -1479,29 +1479,34 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return float(tweaks['series_index_auto_increment']) return float(tweaks['series_index_auto_increment'])
return 1.0 return 1.0
def set(self, row, column, val): def set(self, row, column, val, allow_case_change=False):
''' '''
Convenience method for setting the title, authors, publisher or rating Convenience method for setting the title, authors, publisher or rating
''' '''
id = self.data[row][0] id = self.data[row][0]
col = {'title':1, 'authors':2, 'publisher':3, 'rating':4, 'tags':7}[column] col = {'title':1, 'authors':2, 'publisher':3, 'rating':4, 'tags':7}[column]
books_to_refresh = set()
self.data.set(row, col, val) self.data.set(row, col, val)
if column == 'authors': if column == 'authors':
val = string_to_authors(val) val = string_to_authors(val)
self.set_authors(id, val, notify=False) books_to_refresh |= self.set_authors(id, val, notify=False,
allow_case_change=allow_case_change)
elif column == 'title': elif column == 'title':
self.set_title(id, val, notify=False) self.set_title(id, val, notify=False)
elif column == 'publisher': elif column == 'publisher':
self.set_publisher(id, val, notify=False) books_to_refresh |= self.set_publisher(id, val, notify=False,
allow_case_change=allow_case_change)
elif column == 'rating': elif column == 'rating':
self.set_rating(id, val, notify=False) self.set_rating(id, val, notify=False)
elif column == 'tags': elif column == 'tags':
books_to_refresh |= \
self.set_tags(id, [x.strip() for x in val.split(',') if x.strip()], self.set_tags(id, [x.strip() for x in val.split(',') if x.strip()],
append=False, notify=False) append=False, notify=False, allow_case_change=allow_case_change)
self.data.refresh_ids(self, [id]) self.data.refresh_ids(self, [id])
self.set_path(id, True) self.set_path(id, True)
self.notify('metadata', [id]) self.notify('metadata', [id])
return books_to_refresh
def set_metadata(self, id, mi, ignore_errors=False, def set_metadata(self, id, mi, ignore_errors=False,
set_title=True, set_authors=True, commit=True): set_title=True, set_authors=True, commit=True):
@ -1627,54 +1632,73 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
result.append(r) result.append(r)
return ' & '.join(result).replace('|', ',') return ' & '.join(result).replace('|', ',')
def _set_authors(self, id, authors): def _set_authors(self, id, authors, allow_case_change=False):
if not authors: if not authors:
authors = [_('Unknown')] authors = [_('Unknown')]
self.conn.execute('DELETE FROM books_authors_link WHERE book=?',(id,)) self.conn.execute('DELETE FROM books_authors_link WHERE book=?',(id,))
books_to_refresh = set([])
final_authors = []
for a in authors: for a in authors:
case_change = False
if not a: if not a:
continue continue
a = a.strip().replace(',', '|') a = a.strip().replace(',', '|')
if not isinstance(a, unicode): if not isinstance(a, unicode):
a = a.decode(preferred_encoding, 'replace') a = a.decode(preferred_encoding, 'replace')
author = self.conn.get('SELECT id from authors WHERE name=?', (a,), all=False) aus = self.conn.get('SELECT id, name FROM authors WHERE name=?', (a,))
if author: if aus:
aid = author aid, name = aus[0]
# Handle change of case # Handle change of case
self.conn.execute('UPDATE authors SET name=? WHERE id=?', (a, aid)) if name != a:
if allow_case_change:
self.conn.execute('''UPDATE authors
SET name=? WHERE id=?''', (a, aid))
case_change = True
else: else:
aid = self.conn.execute('INSERT INTO authors(name) VALUES (?)', (a,)).lastrowid a = name
else:
aid = self.conn.execute('''INSERT INTO authors(name)
VALUES (?)''', (a,)).lastrowid
final_authors.append(a.replace('|', ','))
try: try:
self.conn.execute('INSERT INTO books_authors_link(book, author) VALUES (?,?)', self.conn.execute('''INSERT INTO books_authors_link(book, author)
(id, aid)) VALUES (?,?)''', (id, aid))
except IntegrityError: # Sometimes books specify the same author twice in their metadata except IntegrityError: # Sometimes books specify the same author twice in their metadata
pass pass
if case_change:
bks = self.conn.get('''SELECT book FROM books_authors_link
WHERE author=?''', (aid,))
books_to_refresh |= set([bk[0] for bk in bks])
ss = self.author_sort_from_book(id, index_is_id=True) ss = self.author_sort_from_book(id, index_is_id=True)
self.conn.execute('UPDATE books SET author_sort=? WHERE id=?', self.conn.execute('UPDATE books SET author_sort=? WHERE id=?',
(ss, id)) (ss, id))
self.data.set(id, self.FIELD_MAP['authors'], self.data.set(id, self.FIELD_MAP['authors'],
','.join([a.replace(',', '|') for a in authors]), ','.join([a.replace(',', '|') for a in final_authors]),
row_is_id=True) row_is_id=True)
self.data.set(id, self.FIELD_MAP['author_sort'], ss, row_is_id=True) self.data.set(id, self.FIELD_MAP['author_sort'], ss, row_is_id=True)
aum = self.authors_with_sort_strings(id, index_is_id=True) aum = self.authors_with_sort_strings(id, index_is_id=True)
self.data.set(id, self.FIELD_MAP['au_map'], self.data.set(id, self.FIELD_MAP['au_map'],
':#:'.join([':::'.join((au.replace(',', '|'), aus)) for (au, aus) in aum]), ':#:'.join([':::'.join((au.replace(',', '|'), aus)) for (au, aus) in aum]),
row_is_id=True) row_is_id=True)
return books_to_refresh
def set_authors(self, id, authors, notify=True, commit=True): def set_authors(self, id, authors, notify=True, commit=True,
allow_case_change=False):
''' '''
Note that even if commit is False, the db will still be committed to Note that even if commit is False, the db will still be committed to
because this causes the location of files to change because this causes the location of files to change
:param authors: A list of authors. :param authors: A list of authors.
''' '''
self._set_authors(id, authors) books_to_refresh = self._set_authors(id, authors,
allow_case_change=allow_case_change)
self.dirtied([id], commit=False) self.dirtied([id], commit=False)
if commit: if commit:
self.conn.commit() self.conn.commit()
self.set_path(id, index_is_id=True) self.set_path(id, index_is_id=True)
if notify: if notify:
self.notify('metadata', [id]) self.notify('metadata', [id])
return books_to_refresh
def set_title_sort(self, id, title_sort_, notify=True, commit=True): def set_title_sort(self, id, title_sort_, notify=True, commit=True):
if not title_sort_: if not title_sort_:
@ -1697,10 +1721,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
title = title.decode(preferred_encoding, 'replace') title = title.decode(preferred_encoding, 'replace')
self.conn.execute('UPDATE books SET title=? WHERE id=?', (title, id)) self.conn.execute('UPDATE books SET title=? WHERE id=?', (title, id))
self.data.set(id, self.FIELD_MAP['title'], title, row_is_id=True) self.data.set(id, self.FIELD_MAP['title'], title, row_is_id=True)
if tweaks['title_series_sorting'] == 'library_order': ts = self.conn.get('SELECT sort FROM books WHERE id=?', (id,),
self.data.set(id, self.FIELD_MAP['sort'], title_sort(title), row_is_id=True) all=False)
else: if ts:
self.data.set(id, self.FIELD_MAP['sort'], title, row_is_id=True) self.data.set(id, self.FIELD_MAP['sort'], ts, row_is_id=True)
return True return True
def set_title(self, id, title, notify=True, commit=True): def set_title(self, id, title, notify=True, commit=True):
@ -1738,24 +1762,44 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.notify('metadata', [id]) self.notify('metadata', [id])
def set_publisher(self, id, publisher, notify=True, commit=True): def set_publisher(self, id, publisher, notify=True, commit=True,
allow_case_change=False):
self.conn.execute('DELETE FROM books_publishers_link WHERE book=?',(id,)) self.conn.execute('DELETE FROM books_publishers_link WHERE book=?',(id,))
self.conn.execute('DELETE FROM publishers WHERE (SELECT COUNT(id) FROM books_publishers_link WHERE publisher=publishers.id) < 1') self.conn.execute('''DELETE FROM publishers WHERE (SELECT COUNT(id)
FROM books_publishers_link
WHERE publisher=publishers.id) < 1''')
books_to_refresh = set()
if publisher: if publisher:
case_change = False
if not isinstance(publisher, unicode): if not isinstance(publisher, unicode):
publisher = publisher.decode(preferred_encoding, 'replace') publisher = publisher.decode(preferred_encoding, 'replace')
pub = self.conn.get('SELECT id from publishers WHERE name=?', (publisher,), all=False) pubx = self.conn.get('''SELECT id,name from publishers
if pub: WHERE name=?''', (publisher,))
aid = pub if pubx:
aid, cur_name = pubx[0]
if publisher != cur_name:
if allow_case_change:
self.conn.execute('''UPDATE publishers SET name=?
WHERE id=?''', (publisher, aid))
case_change = True
else: else:
aid = self.conn.execute('INSERT INTO publishers(name) VALUES (?)', (publisher,)).lastrowid publisher = cur_name
self.conn.execute('INSERT INTO books_publishers_link(book, publisher) VALUES (?,?)', (id, aid)) else:
aid = self.conn.execute('''INSERT INTO publishers(name)
VALUES (?)''', (publisher,)).lastrowid
self.conn.execute('''INSERT INTO books_publishers_link(book, publisher)
VALUES (?,?)''', (id, aid))
if case_change:
bks = self.conn.get('''SELECT book FROM books_publishers_link
WHERE publisher=?''', (aid,))
books_to_refresh |= set([bk[0] for bk in bks])
self.dirtied([id], commit=False) self.dirtied([id], commit=False)
if commit: if commit:
self.conn.commit() self.conn.commit()
self.data.set(id, self.FIELD_MAP['publisher'], publisher, row_is_id=True) self.data.set(id, self.FIELD_MAP['publisher'], publisher, row_is_id=True)
if notify: if notify:
self.notify('metadata', [id]) self.notify('metadata', [id])
return books_to_refresh
def set_uuid(self, id, uuid, notify=True, commit=True): def set_uuid(self, id, uuid, notify=True, commit=True):
if uuid: if uuid:
@ -2119,17 +2163,21 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
def commit(self): def commit(self):
self.conn.commit() self.conn.commit()
def set_tags(self, id, tags, append=False, notify=True, commit=True): def set_tags(self, id, tags, append=False, notify=True, commit=True,
allow_case_change=False):
''' '''
@param tags: list of strings @param tags: list of strings
@param append: If True existing tags are not removed @param append: If True existing tags are not removed
''' '''
if not append: if not append:
self.conn.execute('DELETE FROM books_tags_link WHERE book=?', (id,)) self.conn.execute('DELETE FROM books_tags_link WHERE book=?', (id,))
self.conn.execute('DELETE FROM tags WHERE (SELECT COUNT(id) FROM books_tags_link WHERE tag=tags.id) < 1') self.conn.execute('''DELETE FROM tags WHERE (SELECT COUNT(id)
FROM books_tags_link WHERE tag=tags.id) < 1''')
otags = self.get_tags(id) otags = self.get_tags(id)
tags = self.cleanup_tags(tags) tags = self.cleanup_tags(tags)
books_to_refresh = set([])
for tag in (set(tags)-otags): for tag in (set(tags)-otags):
case_changed = False
tag = tag.strip() tag = tag.strip()
if not tag: if not tag:
continue continue
@ -2144,15 +2192,20 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if idx > -1: if idx > -1:
etag = existing_tags[idx] etag = existing_tags[idx]
tid = self.conn.get('SELECT id FROM tags WHERE name=?', (etag,), all=False) tid = self.conn.get('SELECT id FROM tags WHERE name=?', (etag,), all=False)
if etag != tag: if allow_case_change and etag != tag:
self.conn.execute('UPDATE tags SET name=? WHERE id=?', (tag, tid)) self.conn.execute('UPDATE tags SET name=? WHERE id=?', (tag, tid))
case_changed = True
else: else:
tid = self.conn.execute('INSERT INTO tags(name) VALUES(?)', (tag,)).lastrowid tid = self.conn.execute('INSERT INTO tags(name) VALUES(?)', (tag,)).lastrowid
if not self.conn.get('SELECT book FROM books_tags_link WHERE book=? AND tag=?', if not self.conn.get('''SELECT book FROM books_tags_link
(id, tid), all=False): WHERE book=? AND tag=?''', (id, tid), all=False):
self.conn.execute('INSERT INTO books_tags_link(book, tag) VALUES (?,?)', self.conn.execute('''INSERT INTO books_tags_link(book, tag)
(id, tid)) VALUES (?,?)''', (id, tid))
if case_changed:
bks = self.conn.get('SELECT book FROM books_tags_link WHERE tag=?',
(tid,))
books_to_refresh |= set([bk[0] for bk in bks])
self.dirtied([id], commit=False) self.dirtied([id], commit=False)
if commit: if commit:
self.conn.commit() self.conn.commit()
@ -2160,12 +2213,14 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.data.set(id, self.FIELD_MAP['tags'], tags, row_is_id=True) self.data.set(id, self.FIELD_MAP['tags'], tags, row_is_id=True)
if notify: if notify:
self.notify('metadata', [id]) self.notify('metadata', [id])
return books_to_refresh
def unapply_tags(self, book_id, tags, notify=True): def unapply_tags(self, book_id, tags, notify=True):
for tag in tags: for tag in tags:
id = self.conn.get('SELECT id FROM tags WHERE name=?', (tag,), all=False) id = self.conn.get('SELECT id FROM tags WHERE name=?', (tag,), all=False)
if id: if id:
self.conn.execute('DELETE FROM books_tags_link WHERE tag=? AND book=?', (id, book_id)) self.conn.execute('''DELETE FROM books_tags_link
WHERE tag=? AND book=?''', (id, book_id))
self.conn.commit() self.conn.commit()
self.data.refresh_ids(self, [book_id]) self.data.refresh_ids(self, [book_id])
if notify: if notify:
@ -2209,31 +2264,44 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
pass pass
return (val, None) return (val, None)
def set_series(self, id, series, notify=True, commit=True): def set_series(self, id, series, notify=True, commit=True, allow_case_change=True):
self.conn.execute('DELETE FROM books_series_link WHERE book=?',(id,)) self.conn.execute('DELETE FROM books_series_link WHERE book=?',(id,))
self.conn.execute('''DELETE FROM series self.conn.execute('''DELETE FROM series
WHERE (SELECT COUNT(id) FROM books_series_link WHERE (SELECT COUNT(id) FROM books_series_link
WHERE series=series.id) < 1''') WHERE series=series.id) < 1''')
(series, idx) = self._get_series_values(series) (series, idx) = self._get_series_values(series)
books_to_refresh = set([])
if series: if series:
case_change = False
if not isinstance(series, unicode): if not isinstance(series, unicode):
series = series.decode(preferred_encoding, 'replace') series = series.decode(preferred_encoding, 'replace')
series = series.strip() series = series.strip()
series = u' '.join(series.split()) series = u' '.join(series.split())
s = self.conn.get('SELECT id from series WHERE name=?', (series,), all=False) sx = self.conn.get('SELECT id,name from series WHERE name=?', (series,))
if s: if sx:
aid = s aid, cur_name = sx[0]
if cur_name != series:
if allow_case_change:
self.conn.execute('UPDATE series SET name=? WHERE id=?', (series, aid))
case_change = True
else:
series = cur_name
else: else:
aid = self.conn.execute('INSERT INTO series(name) VALUES (?)', (series,)).lastrowid aid = self.conn.execute('INSERT INTO series(name) VALUES (?)', (series,)).lastrowid
self.conn.execute('INSERT INTO books_series_link(book, series) VALUES (?,?)', (id, aid)) self.conn.execute('INSERT INTO books_series_link(book, series) VALUES (?,?)', (id, aid))
if idx: if idx:
self.set_series_index(id, idx, notify=notify, commit=commit) self.set_series_index(id, idx, notify=notify, commit=commit)
if case_change:
bks = self.conn.get('SELECT book FROM books_series_link WHERE series=?',
(aid,))
books_to_refresh |= set([bk[0] for bk in bks])
self.dirtied([id], commit=False) self.dirtied([id], commit=False)
if commit: if commit:
self.conn.commit() self.conn.commit()
self.data.set(id, self.FIELD_MAP['series'], series, row_is_id=True) self.data.set(id, self.FIELD_MAP['series'], series, row_is_id=True)
if notify: if notify:
self.notify('metadata', [id]) self.notify('metadata', [id])
return books_to_refresh
def set_series_index(self, id, idx, notify=True, commit=True): def set_series_index(self, id, idx, notify=True, commit=True):
if idx is None: if idx is None:

View File

@ -316,9 +316,19 @@ remove all non-breaking-space entities, or may include false positive matches re
:guilabel:`Replace scene breaks` :guilabel:`Replace scene breaks`
If this option is configured then |app| will replace scene break markers it finds with the replacement text specified by the If this option is configured then |app| will replace scene break markers it finds with the replacement text specified by the
user. In general you should avoid using html tags, |app| will discard any tags and use pre-defined markup. <hr /> user. Please note that some ornamental characters may not be supported across all reading devices.
tags, i.e. horizontal rules, are an exception. These can optionally be specified with styles, if you choose to add your own
style be sure to include the 'width' setting, otherwise the style information will be discarded. In general you should avoid using html tags, |app| will discard any tags and use pre-defined markup. <hr />
tags, i.e. horizontal rules, and <img> tags are exceptions. Horizontal rules can optionally be specified with styles, if you
choose to add your own style be sure to include the 'width' setting, otherwise the style information will be discarded. Image
tags can used, but |app| does not provide the ability to add the image during conversion, this must be done after the fact using
the 'Tweak Epub' feature, or Sigil.
Example image tag (place the image within an 'Images' folder inside the epub after conversion):
<img style="width:10%" src="../Images/scenebreak.png" />
Example horizontal rule with styles:
<hr style="width:20%;padding-top: 1px;border-top: 2px ridge black;border-bottom: 2px groove black;"/>
:guilabel:`Remove unnecessary hyphens` :guilabel:`Remove unnecessary hyphens`
|app| will analyze all hyphenated content in the document when this option is enabled. The document itself is used |app| will analyze all hyphenated content in the document when this option is enabled. The document itself is used

View File

@ -186,7 +186,7 @@ class BuiltinTemplate(BuiltinFormatterFunction):
def evaluate(self, formatter, kwargs, mi, locals, template): def evaluate(self, formatter, kwargs, mi, locals, template):
template = template.replace('[[', '{').replace(']]', '}') template = template.replace('[[', '{').replace(']]', '}')
return formatter.safe_format(template, kwargs, 'TEMPLATE', mi) return formatter.__class__().safe_format(template, kwargs, 'TEMPLATE', mi)
class BuiltinEval(BuiltinFormatterFunction): class BuiltinEval(BuiltinFormatterFunction):
name = 'eval' name = 'eval'