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;
}
/*
* 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 {
margin: 0 0 0 0;
text-indent: 0em;
@ -62,27 +73,19 @@ div.description {
text-indent: 1em;
}
/*
* Attempt to minimize widows and orphans by logically grouping chunks
* Recommend enabling for iPad
* Some reports of problems with Sony ereaders, presumably ADE engines
*/
/*
div.logical_group {
display:inline-block;
width:100%;
div.initial_letter {
page-break-before:always;
}
*/
p.date_index {
p.author_title_letter_index {
font-size:x-large;
text-align:center;
font-weight:bold;
margin-top:1em;
margin-top:0px;
margin-bottom:0px;
}
p.letter_index {
p.date_index {
font-size:x-large;
text-align:center;
font-weight:bold;
@ -99,6 +102,14 @@ p.series {
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 {
text-align:left;
margin-top:0px;

View File

@ -2,24 +2,23 @@
__license__ = 'GPL v3'
__author__ = 'Luis Hernandez'
__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
class AdvancedUserRecipe1294946868(BasicNewsRecipe):
title = u'La Tribuna de Talavera'
title = u'La Tribuna de'
publisher = u'Grupo PROMECAL'
__author__ = 'Luis Hernández'
description = 'Diario local de Talavera de la Reina'
cover_url = 'http://www.latribunadetalavera.es/entorno/mancheta.gif'
description = 'Varios diarios locales del grupo PROMECAL'
oldest_article = 5
oldest_article = 3
max_articles_per_feed = 50
remove_javascript = True
@ -27,7 +26,7 @@ class AdvancedUserRecipe1294946868(BasicNewsRecipe):
use_embedded_content = False
encoding = 'utf-8'
language = 'es'
language = 'es_ES'
timefmt = '[%a, %d %b, %Y]'
keep_only_tags = [
@ -39,7 +38,20 @@ class AdvancedUserRecipe1294946868(BasicNewsRecipe):
remove_tags_before = dict(name='div' , attrs={'class':['comparte']})
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):
for alink in soup.findAll('a'):
@ -48,4 +60,15 @@ class AdvancedUserRecipe1294946868(BasicNewsRecipe):
alink.replaceWith(tstr)
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
max_articles_per_feed = 100
__author__ = 'Sujata Raman'
description = 'French news. Needs a subscription from http://www.letemps.ch'
no_stylesheets = True
remove_javascript = True
recursions = 1
encoding = 'UTF-8'
match_regexps = [r'http://www.letemps.ch/Page/Uuid/[-0-9a-f]+\|[1-9]']
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'}),
dict(name='div', attrs={'class':'story'})

View File

@ -13,15 +13,12 @@ class MSNSankeiNewsProduct(BasicNewsRecipe):
description = 'Products release from Japan'
oldest_article = 7
max_articles_per_feed = 100
encoding = 'Shift_JIS'
encoding = 'utf-8'
language = 'ja'
cover_url = 'http://sankei.jp.msn.com/images/common/sankeShinbunLogo.jpg'
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')]
remove_tags_before = dict(id="__r_article_title__")
remove_tags_after = dict(id="ajax_release_news")
remove_tags = [{'class':"parent chromeCustom6G"},
dict(id="RelatedImg")
]
remove_tags_before = dict(id="NewsTitle")
remove_tags_after = dict(id="RelatedTitle")

View File

@ -1,7 +1,5 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>'
__copyright__ = '2009-2011, Darko Miletic <darko.miletic at gmail.com>'
'''
theonion.com
@ -12,35 +10,73 @@ from calibre.web.feeds.news import BasicNewsRecipe
class TheOnion(BasicNewsRecipe):
title = 'The Onion'
__author__ = 'Darko Miletic'
description = "America's finest news source"
oldest_article = 2
description = "America's finest news source"
oldest_article = 2
max_articles_per_feed = 100
publisher = u'Onion, Inc.'
category = u'humor, news, USA'
language = 'en'
publisher = 'Onion, Inc.'
category = 'humor, news, USA'
language = 'en'
no_stylesheets = True
use_embedded_content = False
encoding = 'utf-8'
remove_javascript = True
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"'
html2lrf_options = [
'--comment' , description
, '--category' , category
, '--publisher' , publisher
]
publication_type = 'newsportal'
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}
"""
keep_only_tags = [dict(name='div', attrs={'id':'main'})]
conversion_options = {
'comment' : description
, 'tags' : category
, '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']
remove_tags_after = dict(attrs={'class':['article_body','feature_content']})
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={'id':['recent_slider','sidebar','pagination','related_media']})
]
feeds = [
(u'Daily' , u'http://feeds.theonion.com/theonion/daily' )
,(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]
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):
filepath = NOOK.create_upload_path(self, path, mdata, fname,
create_dirs=create_dirs)
edm = self.EBOOK_DIR_MAIN.replace('/', os.sep)
npath = os.path.join(edm, _('News')) + os.sep
if npath in filepath:
filepath = filepath.replace(npath, os.sep.join('My Files',
'Magazines')+os.sep)
filedir = os.path.dirname(filepath)
if create_dirs and not os.path.exists(filedir):
os.makedirs(filedir)
create_dirs=False)
edm = self.EBOOK_DIR_MAIN
subdir = 'Books'
if mdata.tags:
if _('News') in mdata.tags:
subdir = 'Magazines'
filepath = filepath.replace(os.sep+edm+os.sep,
os.sep+edm+os.sep+subdir+os.sep)
filedir = os.path.dirname(filepath)
if create_dirs and not os.path.exists(filedir):
os.makedirs(filedir)
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
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.match('^<hr', replacement_break):
if replacement_break.find('width') != -1:
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
hr_open = re.sub('45', str(divpercent), hr_open)
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
# Otherwise separator lines are centered, use a bit larger margin in this case
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)
if len(scene_break.findall(html)) >= 1:
html = scene_break.sub(replacement_break, html)

View File

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

View File

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

View File

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

View File

@ -64,8 +64,6 @@ class CompleteWindow(QListView): # {{{
def do_selected(self, idx=None):
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():
data = unicode(self.model().data(idx, Qt.DisplayRole))
self.completion_selected.emit(data)
@ -81,6 +79,9 @@ class CompleteWindow(QListView): # {{{
self.hide()
return True
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()
return True
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.complete_window = CompleteWindow(self, self._model)
self.textChanged.connect(self.text_changed)
self.cursorPositionChanged.connect(self.cursor_position_changed)
self.textEdited.connect(self.text_edited)
self.complete_window.completion_selected.connect(self.completion_selected)
self.installEventFilter(self)
# Interface {{{
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 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()
def update_completions(self):
' Update the list of completions '
if not self.complete_window.isVisible() and not self.hasFocus():
return
cpos = self.cursorPosition()
text = unicode(self.text())
prefix = text[:cpos]
@ -223,7 +226,7 @@ class MultiCompleteLineEdit(QLineEdit):
text
'''
if self.sep is None:
return text
return -1, text
else:
cursor_pos = self.cursorPosition()
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():
after_text = u''
prefix_len = len(before_text.split(self.sep)[-1].lstrip())
if self.space_before_sep:
complete_text_pat = '%s%s %s %s'
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)
return prefix_len, \
before_text[:cursor_pos - prefix_len] + text + after_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:
self.setText(ctext)
self.setCursorPosition(len(ctext))
else:
cursor_pos = self.cursorPosition()
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):
self._model.update_matches(matches)
@ -334,6 +331,11 @@ class MultiCompleteComboBox(EnComboBox):
def __init__(self, *args):
EnComboBox.__init__(self, *args)
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):
self.lineEdit().update_items_cache(complete_items)

View File

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

View File

@ -70,9 +70,6 @@ class MetadataWidget(Widget, Ui_Form):
def initialize_metadata_options(self):
self.initialize_combos()
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)
self.title.setText(mi.title)
@ -109,6 +106,9 @@ class MetadataWidget(Widget, Ui_Form):
def initalize_authors(self):
all_authors = self.db.all_authors()
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:
id, name = i
@ -124,6 +124,8 @@ class MetadataWidget(Widget, Ui_Form):
def initialize_series(self):
all_series = self.db.all_series()
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:
id, name = i
@ -133,6 +135,8 @@ class MetadataWidget(Widget, Ui_Form):
def initialize_publisher(self):
all_publishers = self.db.all_publishers()
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:
id, name = i

View File

@ -190,7 +190,7 @@
</widget>
</item>
<item row="4" column="1">
<widget class="CompleteLineEdit" name="tags">
<widget class="MultiCompleteLineEdit" name="tags">
<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>
</property>
@ -213,7 +213,7 @@
</widget>
</item>
<item row="5" column="1">
<widget class="EnComboBox" name="series">
<widget class="MultiCompleteComboBox" name="series">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>10</horstretch>
@ -248,14 +248,14 @@
</widget>
</item>
<item row="3" column="1">
<widget class="EnComboBox" name="publisher">
<widget class="MultiCompleteComboBox" name="publisher">
<property name="editable">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="CompleteComboBox" name="author">
<widget class="MultiCompleteComboBox" name="author">
<property name="editable">
<bool>true</bool>
</property>
@ -277,19 +277,14 @@
<header>widgets.h</header>
</customwidget>
<customwidget>
<class>EnComboBox</class>
<class>MultiCompleteComboBox</class>
<extends>QComboBox</extends>
<header>widgets.h</header>
<header>calibre/gui2/complete.h</header>
</customwidget>
<customwidget>
<class>CompleteComboBox</class>
<extends>QComboBox</extends>
<header>widgets.h</header>
</customwidget>
<customwidget>
<class>CompleteLineEdit</class>
<class>MultiCompleteLineEdit</class>
<extends>QLineEdit</extends>
<header>widgets.h</header>
<header>calibre/gui2/complete.h</header>
</customwidget>
<customwidget>
<class>ImageView</class>

View File

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

View File

@ -7,8 +7,8 @@ __license__ = 'GPL v3'
from PyQt4.Qt import QDialog, QGridLayout, QLabel, QDialogButtonBox, \
QApplication, QSpinBox, QToolButton, QIcon
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.gui2.complete import MultiCompleteComboBox
class AddEmptyBookDialog(QDialog):
@ -32,7 +32,7 @@ class AddEmptyBookDialog(QDialog):
self.author_label = QLabel(_('Set the author of the new books to:'))
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.AdjustToMinimumContentsLengthWithIcon)
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.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.meta import get_metadata
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_remove_conv, do_auto_author, series, do_series_restart, \
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
@ -159,6 +159,9 @@ class MyBlockingBusy(QDialog): # {{{
if do_title_case and not title_set:
title = self.db.title(id, index_is_id=True)
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:
self.db.set_authors(id, string_to_authors(au), notify=False)
if cover_action == 'remove':
@ -360,11 +363,11 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
if (f in ['author_sort'] or
(fm[f]['datatype'] in ['text', 'series', 'enumeration']
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'] ):
self.all_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.sort()
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.search_mode.currentIndexChanged[int].connect(self.s_r_search_mode_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_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.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):
if field:
if field == '{template}':
@ -508,7 +521,7 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
for i in range(0, self.s_r_number_of_books):
w = getattr(self, 'book_%d_text'%(i+1))
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)
if len(t) > 1:
t = t[self.starting_from.value()-1:
@ -518,13 +531,13 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
if self.search_mode.currentIndex() == 0:
self.destination_field.setCurrentIndex(idx)
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)
def s_r_destination_field_changed(self, txt):
txt = unicode(txt)
def s_r_destination_field_changed(self, idx):
txt = self.s_r_df_itemdata(idx)
if not txt:
txt = unicode(self.search_field.currentText())
txt = self.s_r_sf_itemdata(None)
if txt and txt in self.writable_fields:
self.destination_field_fm = self.db.metadata_for_field(txt)
self.s_r_paint_results(None)
@ -533,8 +546,9 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
self.search_field.clear()
self.destination_field.clear()
if val == 0:
self.search_field.addItems(self.writable_fields)
self.destination_field.addItems(self.writable_fields)
for f in 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.setVisible(False)
self.destination_field_label.setVisible(False)
@ -544,8 +558,14 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
self.comma_separated.setVisible(False)
self.s_r_heading.setText('<p>'+self.main_heading + self.character_heading)
else:
self.search_field.addItems(self.all_fields)
self.destination_field.addItems(self.writable_fields)
self.search_field.blockSignals(True)
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_label.setVisible(True)
self.replace_mode.setVisible(True)
@ -575,7 +595,7 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
return rfunc(rtext)
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)
result = []
rfunc = self.s_r_functions[unicode(self.replace_func.currentText())]
@ -587,10 +607,10 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
return result
def s_r_do_destination(self, mi, val):
src = unicode(self.search_field.currentText())
src = self.s_r_sf_itemdata(None)
if src == '':
return ''
dest = unicode(self.destination_field.currentText())
dest = self.s_r_df_itemdata(None)
if dest == '':
if self.db.metadata_for_field(src)['datatype'] == 'composite':
raise Exception(_('You must specify a destination when source is a composite field'))
@ -680,10 +700,10 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
break
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:
return
dest = unicode(self.destination_field.currentText())
dest = self.s_r_df_itemdata(None)
if not dest:
dest = source
dfm = self.db.field_metadata[dest]
@ -717,6 +737,8 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
else:
if dest == 'comments':
setter = self.db.set_comment
elif dest == 'sort':
setter = self.db.set_title_sort
else:
setter = getattr(self.db, 'set_'+dest)
if dest in ['title', 'authors']:
@ -764,6 +786,8 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
def initialize_series(self):
all_series = self.db.all_series()
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:
id, name = i
@ -773,6 +797,8 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
def initialize_publisher(self):
all_publishers = self.db.all_publishers()
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:
id, name = i
@ -840,6 +866,7 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
do_remove_conv = self.remove_conversion_settings.isChecked()
do_auto_author = self.auto_author_sort.isChecked()
do_title_case = self.change_title_to_title_case.isChecked()
do_title_sort = self.update_title_sort.isChecked()
pubdate = adddate = None
if self.apply_pubdate.isChecked():
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_remove_conv, do_auto_author, series, do_series_restart,
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}%%.')
%len(self.ids), args, self.db, self.ids,

View File

@ -76,7 +76,7 @@
</widget>
</item>
<item row="0" column="1">
<widget class="CompleteComboBox" name="authors">
<widget class="MultiCompleteComboBox" name="authors">
<property name="editable">
<bool>true</bool>
</property>
@ -175,7 +175,7 @@
</widget>
</item>
<item row="4" column="1">
<widget class="EnComboBox" name="publisher">
<widget class="MultiCompleteComboBox" name="publisher">
<property name="editable">
<bool>true</bool>
</property>
@ -195,7 +195,7 @@
</widget>
</item>
<item row="5" column="1">
<widget class="CompleteLineEdit" name="tags">
<widget class="MultiCompleteLineEdit" name="tags">
<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>
</property>
@ -229,7 +229,7 @@
</widget>
</item>
<item row="6" column="1">
<widget class="CompleteLineEdit" name="remove_tags">
<widget class="MultiCompleteLineEdit" name="remove_tags">
<property name="toolTip">
<string>Comma separated list of tags to remove from the books. </string>
</property>
@ -262,7 +262,7 @@
</widget>
</item>
<item row="7" column="1">
<widget class="EnComboBox" name="series">
<widget class="MultiCompleteComboBox" name="series">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
@ -489,6 +489,16 @@ title and author are swapped before the title case is set</string>
</property>
</widget>
</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>
<spacer name="horizontalSpacer">
<property name="orientation">
@ -1072,19 +1082,14 @@ not multiple and the destination field is multiple</string>
<header>widgets.h</header>
</customwidget>
<customwidget>
<class>EnComboBox</class>
<class>MultiCompleteComboBox</class>
<extends>QComboBox</extends>
<header>widgets.h</header>
<header>calibre/gui2/complete.h</header>
</customwidget>
<customwidget>
<class>CompleteComboBox</class>
<extends>QComboBox</extends>
<header>widgets.h</header>
</customwidget>
<customwidget>
<class>CompleteLineEdit</class>
<class>MultiCompleteLineEdit</class>
<extends>QLineEdit</extends>
<header>widgets.h</header>
<header>calibre/gui2/complete.h</header>
</customwidget>
<customwidget>
<class>HistoryLineEdit</class>

View File

@ -622,6 +622,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.original_author = unicode(self.authors.text()).strip()
self.original_title = unicode(self.title.text()).strip()
self.books_to_refresh = set()
self.show()
@ -739,6 +740,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.series.setSizeAdjustPolicy(self.series.AdjustToContentsOnFirstShow)
all_series = self.db.all_series()
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)
idx, c = None, 0
for i in all_series:
@ -756,6 +759,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
def initialize_publisher(self):
all_publishers = self.db.all_publishers()
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)
idx, c = None, 0
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'
' editor, you must either discard or apply these '
'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())
else:
self.tags.setText(self.original_tags)
@ -882,9 +888,9 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
break
def apply_tags(self, commit=False, notify=False):
self.db.set_tags(self.id, [x.strip() for x in
unicode(self.tags.text()).split(',')],
notify=notify, commit=commit)
return self.db.set_tags(self.id, [x.strip() for x in
unicode(self.tags.text()).split(',')],
notify=notify, commit=commit, allow_case_change=True)
def next_triggered(self, row_delta, *args):
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)
au = unicode(self.authors.text()).strip()
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()
if aus:
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)
self.db.set_rating(self.id, 2*self.rating.value(), notify=False,
commit=False)
self.apply_tags()
self.db.set_publisher(self.id,
unicode(self.publisher.currentText()).strip(),
notify=False, commit=False)
self.db.set_series(self.id,
self.books_to_refresh |= self.apply_tags()
self.books_to_refresh |= self.db.set_publisher(self.id,
unicode(self.publisher.currentText()).strip(),
notify=False, commit=False, allow_case_change=True)
self.books_to_refresh |= self.db.set_series(self.id,
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(),
notify=False, commit=False)
self.db.set_comment(self.id,
@ -940,7 +949,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
else:
self.db.remove_cover(self.id)
for w in getattr(self, 'custom_column_widgets', []):
w.commit(self.id)
self.books_to_refresh |= w.commit(self.id)
self.db.commit()
except IOError, err:
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>
</item>
<item row="2" column="1">
<widget class="CompleteComboBox" name="authors">
<widget class="MultiCompleteComboBox" name="authors">
<property name="editable">
<bool>true</bool>
</property>
@ -313,7 +313,7 @@ If the box is colored green, then text matches the individual author's sort stri
</widget>
</item>
<item row="5" column="1" colspan="2">
<widget class="EnComboBox" name="publisher">
<widget class="MultiCompleteComboBox" name="publisher">
<property name="editable">
<bool>true</bool>
</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">
<layout class="QHBoxLayout" name="_2">
<item>
<widget class="CompleteLineEdit" name="tags">
<widget class="MultiCompleteLineEdit" name="tags">
<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>
</property>
@ -379,7 +379,7 @@ If the box is colored green, then text matches the individual author's sort stri
<number>5</number>
</property>
<item>
<widget class="EnComboBox" name="series">
<widget class="MultiCompleteComboBox" name="series">
<property name="toolTip">
<string>List of known series. You can add new series.</string>
</property>
@ -837,19 +837,14 @@ If the box is colored green, then text matches the individual author's sort stri
<header>widgets.h</header>
</customwidget>
<customwidget>
<class>EnComboBox</class>
<extends>QComboBox</extends>
<header>widgets.h</header>
</customwidget>
<customwidget>
<class>CompleteLineEdit</class>
<class>MultiCompleteLineEdit</class>
<extends>QLineEdit</extends>
<header>widgets.h</header>
<header>calibre/gui2/complete.h</header>
</customwidget>
<customwidget>
<class>CompleteComboBox</class>
<class>MultiCompleteComboBox</class>
<extends>QComboBox</extends>
<header>widgets.h</header>
<header>calibre/gui2/complete.h</header>
</customwidget>
<customwidget>
<class>FormatList</class>

View File

@ -3,7 +3,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
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.library.caches import CONTAINS_MATCH, EQUALS_MATCH
@ -29,20 +29,18 @@ class SearchDialog(QDialog, Ui_Dialog):
name = name.strip().replace('|', ',')
self.authors_box.addItem(name)
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_space_before_sep(True)
self.authors_box.update_items_cache(db.all_author_names())
all_series = db.all_series()
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:
id, name = i
self.series_box.addItem(name)
self.series_box.setEditText('')
self.series_box.completer().setCompletionMode(QCompleter.PopupCompletion)
self.series_box.setAutoCompletionCaseSensitivity(Qt.CaseInsensitive)
all_tags = db.all_tags()
self.tags_box.update_items_cache(all_tags)

View File

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

View File

@ -12,11 +12,11 @@ from PyQt4.Qt import QColor, Qt, QModelIndex, QSize, \
QPainterPath, QLinearGradient, QBrush, \
QPen, QStyle, QPainter, QStyleOptionViewItemV4, \
QIcon, QDoubleSpinBox, QVariant, QSpinBox, \
QStyledItemDelegate, QCompleter, \
QComboBox, QTextDocument
QStyledItemDelegate, QComboBox, QTextDocument
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.config import tweaks
from calibre.utils.formatter import validation_formatter
@ -151,38 +151,15 @@ class TextDelegate(QStyledItemDelegate): # {{{
self.auto_complete_function = f
def createEditor(self, parent, option, index):
editor = EnLineEdit(parent)
if self.auto_complete_function:
editor = MultiCompleteLineEdit(parent)
editor.set_separator(None)
complete_items = [i[1] for i in self.auto_complete_function()]
completer = QCompleter(complete_items, self)
completer.setCaseSensitivity(Qt.CaseInsensitive)
completer.setCompletionMode(QCompleter.PopupCompletion)
editor.setCompleter(completer)
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
editor.update_items_cache(complete_items)
else:
editor = EnLineEdit(parent)
return editor
# }}}
#}}}
class CompleteDelegate(QStyledItemDelegate): # {{{
def __init__(self, parent, sep, items_func_name, space_before_sep=False):
@ -197,13 +174,15 @@ class CompleteDelegate(QStyledItemDelegate): # {{{
def createEditor(self, parent, option, index):
if self.db and hasattr(self.db, self.items_func_name):
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):
editor = CompleteLineEdit(parent, getattr(self.db, self.items_func_name)(),
self.sep, self.space_before_sep)
all_items = getattr(self.db, self.items_func_name)()
else:
editor = CompleteLineEdit(parent,
sorted(list(self.db.all_custom(label=self.db.field_metadata.key_to_label(col))),
key=sort_key), self.sep, self.space_before_sep)
all_items = list(self.db.all_custom(
label=self.db.field_metadata.key_to_label(col)))
editor.update_items_cache(all_items)
else:
editor = EnLineEdit(parent)
return editor
@ -273,13 +252,11 @@ class CcTextDelegate(QStyledItemDelegate): # {{{
editor.setRange(-100., float(sys.maxint))
editor.setDecimals(2)
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))),
key=sort_key)
completer = QCompleter(complete_items, self)
completer.setCaseSensitivity(Qt.CaseInsensitive)
completer.setCompletionMode(QCompleter.PopupCompletion)
editor.setCompleter(completer)
editor.update_items_cache(complete_items)
return editor
# }}}

View File

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

View File

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

View File

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

View File

@ -420,7 +420,8 @@ class ResultCache(SearchQueryParser): # {{{
return candidates - 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([])
if candidates is None:
candidates = self.universal_set()
@ -434,8 +435,8 @@ class ResultCache(SearchQueryParser): # {{{
if isinstance(location, list):
if allow_recursion:
for loc in location:
matches |= self.get_matches(loc, query, candidates,
allow_recursion=False)
matches |= self.get_matches(loc, query,
candidates=candidates, allow_recursion=False)
return matches
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)
btc += 1
# <p class="letter_index">
# <p class="book_title">
divTag = Tag(soup, "div")
dtc = 0
current_letter = ""
@ -1870,11 +1868,12 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
divTag.insert(dtc, divRunningTag)
dtc += 1
divRunningTag = Tag(soup, 'div')
divRunningTag['class'] = "logical_group"
if dtc > 0:
divRunningTag['class'] = "initial_letter"
drtc = 0
current_letter = self.letter_or_symbol(book['title_sort'][0])
pIndexTag = Tag(soup, "p")
pIndexTag['class'] = "letter_index"
pIndexTag['class'] = "author_title_letter_index"
aTag = Tag(soup, "a")
aTag['name'] = "%s" % self.letter_or_symbol(book['title_sort'][0])
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)
btc += 1
# <p class="letter_index">
# <p class="author_index">
divTag = Tag(soup, "div")
dtc = 0
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())
author_count = 0
divOpeningTag = Tag(soup, 'div')
divOpeningTag['class'] = "logical_group"
if dtc > 0:
divOpeningTag['class'] = "initial_letter"
dotc = 0
pIndexTag = Tag(soup, "p")
pIndexTag['class'] = "letter_index"
pIndexTag['class'] = "author_title_letter_index"
aTag = Tag(soup, "a")
aTag['name'] = "%sauthors" % self.letter_or_symbol(current_letter)
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
current_author = book['author']
author_count += 1
if author_count == 2:
if author_count >= 2:
# Add divOpeningTag to divTag, kill divOpeningTag
divTag.insert(dtc, divOpeningTag)
dtc += 1
divOpeningTag = None
dotc = 0
if divOpeningTag:
divTag.insert(dtc, divOpeningTag)
dtc += 1
divOpeningTag = None
dotc = 0
# Create a divRunningTag for the next author
if author_count > 2:
divTag.insert(dtc, divRunningTag)
dtc += 1
# Create a divRunningTag for the rest of the authors in this letter
divRunningTag = Tag(soup, 'div')
divRunningTag['class'] = "logical_group"
divRunningTag['class'] = "author_logical_group"
drtc = 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)
btc += 1
# <p class="letter_index">
# <p class="author_index">
divTag = Tag(soup, "div")
dtc = 0
@ -2558,8 +2559,6 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
body.insert(btc, aTag)
btc += 1
# <p class="letter_index">
# <p class="author_index">
divTag = Tag(soup, "div")
dtc = 0
@ -2661,8 +2660,6 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
body.insert(btc, aTag)
btc += 1
# <p class="letter_index">
# <p class="author_index">
divTag = Tag(soup, "div")
dtc = 0
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
current_letter = self.letter_or_symbol(sort_title[0].upper())
pIndexTag = Tag(soup, "p")
pIndexTag['class'] = "letter_index"
pIndexTag['class'] = "series_letter_index"
aTag = Tag(soup, "a")
aTag['name'] = "%s_series" % self.letter_or_symbol(current_letter)
pIndexTag.insert(0,aTag)

View File

@ -440,22 +440,24 @@ class CustomColumns(object):
self.dirtied(ids, commit=False)
self.conn.commit()
def set_custom(self, id, val, label=None, num=None,
append=False, notify=True, extra=None, commit=True):
self._set_custom(id, val, label=label, num=num, append=append,
notify=notify, extra=extra)
def set_custom(self, id, val, label=None, num=None, append=False,
notify=True, extra=None, commit=True, allow_case_change=False):
rv = self._set_custom(id, val, label=label, num=num, append=append,
notify=notify, extra=extra,
allow_case_change=allow_case_change)
self.dirtied([id], commit=False)
if commit:
self.conn.commit()
return rv
def _set_custom(self, id_, val, label=None, num=None,
append=False, notify=True, extra=None):
def _set_custom(self, id_, val, label=None, num=None, append=False,
notify=True, extra=None, allow_case_change=False):
if label is not None:
data = self.custom_column_label_map[label]
if num is not None:
data = self.custom_column_num_map[num]
if data['datatype'] == 'composite':
return None
return set([])
if not data['editable']:
raise ValueError('Column %r is not editable'%data['label'])
table, lt = self.custom_table_names(data['num'])
@ -466,10 +468,11 @@ class CustomColumns(object):
if data['datatype'] == 'series' and extra is None:
(val, extra) = self._get_series_values(val)
books_to_refresh = set([])
if data['normalized']:
if data['datatype'] == 'enumeration' and (
val and val not in data['display']['enum_values']):
return None
return books_to_refresh
if not append or not data['is_multiple']:
self.conn.execute('DELETE FROM %s WHERE book=?'%lt, (id_,))
self.conn.execute(
@ -483,6 +486,7 @@ class CustomColumns(object):
for x in set(set_val) - set(existing):
if x is None:
continue
case_change = False
existing = list(self.all_custom(num=data['num']))
lx = [t.lower() if hasattr(t, 'lower') else t for t in existing]
try:
@ -492,13 +496,14 @@ class CustomColumns(object):
if idx > -1:
ex = existing[idx]
xid = self.conn.get(
'SELECT id FROM %s WHERE value=?'%table, (ex,), all=False)
if ex != x:
'SELECT id FROM %s WHERE value=?'%table, (ex,), all=False)
if allow_case_change and ex != x:
case_change = True
self.conn.execute(
'UPDATE %s SET value=? WHERE id=?'%table, (x, xid))
'UPDATE %s SET value=? WHERE id=?'%table, (x, xid))
else:
xid = self.conn.execute(
'INSERT INTO %s(value) VALUES(?)'%table, (x,)).lastrowid
'INSERT INTO %s(value) VALUES(?)'%table, (x,)).lastrowid
if not self.conn.get(
'SELECT book FROM %s WHERE book=? AND value=?'%lt,
(id_, xid), all=False):
@ -512,6 +517,10 @@ class CustomColumns(object):
self.conn.execute(
'''INSERT INTO %s(book, value)
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(
'SELECT custom_%s FROM meta2 WHERE id=?'%data['num'],
(id_,), all=False)
@ -530,7 +539,7 @@ class CustomColumns(object):
row_is_id=True)
if notify:
self.notify('metadata', [id_])
return nval
return books_to_refresh
def clean_custom(self):
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 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
'''
id = self.data[row][0]
col = {'title':1, 'authors':2, 'publisher':3, 'rating':4, 'tags':7}[column]
books_to_refresh = set()
self.data.set(row, col, val)
if column == 'authors':
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':
self.set_title(id, val, notify=False)
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':
self.set_rating(id, val, notify=False)
elif column == 'tags':
self.set_tags(id, [x.strip() for x in val.split(',') if x.strip()],
append=False, notify=False)
books_to_refresh |= \
self.set_tags(id, [x.strip() for x in val.split(',') if x.strip()],
append=False, notify=False, allow_case_change=allow_case_change)
self.data.refresh_ids(self, [id])
self.set_path(id, True)
self.notify('metadata', [id])
return books_to_refresh
def set_metadata(self, id, mi, ignore_errors=False,
set_title=True, set_authors=True, commit=True):
@ -1627,54 +1632,73 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
result.append(r)
return ' & '.join(result).replace('|', ',')
def _set_authors(self, id, authors):
def _set_authors(self, id, authors, allow_case_change=False):
if not authors:
authors = [_('Unknown')]
self.conn.execute('DELETE FROM books_authors_link WHERE book=?',(id,))
books_to_refresh = set([])
final_authors = []
for a in authors:
case_change = False
if not a:
continue
a = a.strip().replace(',', '|')
if not isinstance(a, unicode):
a = a.decode(preferred_encoding, 'replace')
author = self.conn.get('SELECT id from authors WHERE name=?', (a,), all=False)
if author:
aid = author
aus = self.conn.get('SELECT id, name FROM authors WHERE name=?', (a,))
if aus:
aid, name = aus[0]
# 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:
a = name
else:
aid = self.conn.execute('INSERT INTO authors(name) VALUES (?)', (a,)).lastrowid
aid = self.conn.execute('''INSERT INTO authors(name)
VALUES (?)''', (a,)).lastrowid
final_authors.append(a.replace('|', ','))
try:
self.conn.execute('INSERT INTO books_authors_link(book, author) VALUES (?,?)',
(id, aid))
self.conn.execute('''INSERT INTO books_authors_link(book, author)
VALUES (?,?)''', (id, aid))
except IntegrityError: # Sometimes books specify the same author twice in their metadata
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)
self.conn.execute('UPDATE books SET author_sort=? WHERE id=?',
(ss, id))
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)
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)
self.data.set(id, self.FIELD_MAP['au_map'],
':#:'.join([':::'.join((au.replace(',', '|'), aus)) for (au, aus) in aum]),
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
because this causes the location of files to change
: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)
if commit:
self.conn.commit()
self.set_path(id, index_is_id=True)
if notify:
self.notify('metadata', [id])
return books_to_refresh
def set_title_sort(self, id, title_sort_, notify=True, commit=True):
if not title_sort_:
@ -1697,10 +1721,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
title = title.decode(preferred_encoding, 'replace')
self.conn.execute('UPDATE books SET title=? WHERE id=?', (title, id))
self.data.set(id, self.FIELD_MAP['title'], title, row_is_id=True)
if tweaks['title_series_sorting'] == 'library_order':
self.data.set(id, self.FIELD_MAP['sort'], title_sort(title), row_is_id=True)
else:
self.data.set(id, self.FIELD_MAP['sort'], title, row_is_id=True)
ts = self.conn.get('SELECT sort FROM books WHERE id=?', (id,),
all=False)
if ts:
self.data.set(id, self.FIELD_MAP['sort'], ts, row_is_id=True)
return True
def set_title(self, id, title, notify=True, commit=True):
@ -1738,24 +1762,44 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
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 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:
case_change = False
if not isinstance(publisher, unicode):
publisher = publisher.decode(preferred_encoding, 'replace')
pub = self.conn.get('SELECT id from publishers WHERE name=?', (publisher,), all=False)
if pub:
aid = pub
pubx = self.conn.get('''SELECT id,name from publishers
WHERE name=?''', (publisher,))
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:
publisher = cur_name
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))
self.dirtied([id], commit=False)
if commit:
self.conn.commit()
self.data.set(id, self.FIELD_MAP['publisher'], publisher, row_is_id=True)
if notify:
self.notify('metadata', [id])
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)
if commit:
self.conn.commit()
self.data.set(id, self.FIELD_MAP['publisher'], publisher, row_is_id=True)
if notify:
self.notify('metadata', [id])
return books_to_refresh
def set_uuid(self, id, uuid, notify=True, commit=True):
if uuid:
@ -2119,17 +2163,21 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
def commit(self):
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 append: If True existing tags are not removed
'''
if not append:
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)
tags = self.cleanup_tags(tags)
books_to_refresh = set([])
for tag in (set(tags)-otags):
case_changed = False
tag = tag.strip()
if not tag:
continue
@ -2144,15 +2192,20 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if idx > -1:
etag = existing_tags[idx]
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))
case_changed = True
else:
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=?',
(id, tid), all=False):
self.conn.execute('INSERT INTO books_tags_link(book, tag) VALUES (?,?)',
(id, tid))
if not self.conn.get('''SELECT book FROM books_tags_link
WHERE book=? AND tag=?''', (id, tid), all=False):
self.conn.execute('''INSERT INTO books_tags_link(book, tag)
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)
if 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)
if notify:
self.notify('metadata', [id])
return books_to_refresh
def unapply_tags(self, book_id, tags, notify=True):
for tag in tags:
id = self.conn.get('SELECT id FROM tags WHERE name=?', (tag,), all=False)
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.data.refresh_ids(self, [book_id])
if notify:
@ -2209,31 +2264,44 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
pass
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 series
WHERE (SELECT COUNT(id) FROM books_series_link
WHERE series=series.id) < 1''')
(series, idx) = self._get_series_values(series)
books_to_refresh = set([])
if series:
case_change = False
if not isinstance(series, unicode):
series = series.decode(preferred_encoding, 'replace')
series = series.strip()
series = u' '.join(series.split())
s = self.conn.get('SELECT id from series WHERE name=?', (series,), all=False)
if s:
aid = s
sx = self.conn.get('SELECT id,name from series WHERE name=?', (series,))
if sx:
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:
aid = self.conn.execute('INSERT INTO series(name) VALUES (?)', (series,)).lastrowid
self.conn.execute('INSERT INTO books_series_link(book, series) VALUES (?,?)', (id, aid))
if idx:
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)
if commit:
self.conn.commit()
self.data.set(id, self.FIELD_MAP['series'], series, row_is_id=True)
if notify:
self.notify('metadata', [id])
return books_to_refresh
def set_series_index(self, id, idx, notify=True, commit=True):
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`
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 />
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.
user. Please note that some ornamental characters may not be supported across all reading devices.
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`
|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):
template = template.replace('[[', '{').replace(']]', '}')
return formatter.safe_format(template, kwargs, 'TEMPLATE', mi)
return formatter.__class__().safe_format(template, kwargs, 'TEMPLATE', mi)
class BuiltinEval(BuiltinFormatterFunction):
name = 'eval'