diff --git a/resources/catalog/stylesheet.css b/resources/catalog/stylesheet.css index 336d015e44..4b32056400 100644 --- a/resources/catalog/stylesheet.css +++ b/resources/catalog/stylesheet.css @@ -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; diff --git a/resources/recipes/la_tribuna.recipe b/resources/recipes/la_tribuna.recipe index 739d11cc8d..c6d3f5cb28 100644 --- a/resources/recipes/la_tribuna.recipe +++ b/resources/recipes/la_tribuna.recipe @@ -2,24 +2,23 @@ __license__ = 'GPL v3' __author__ = 'Luis Hernandez' __copyright__ = 'Luis Hernandez' -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') + ] diff --git a/resources/recipes/le_temps.recipe b/resources/recipes/le_temps.recipe index c33d9a51d2..7e320fe710 100644 --- a/resources/recipes/le_temps.recipe +++ b/resources/recipes/le_temps.recipe @@ -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'}) diff --git a/resources/recipes/msnsankei.recipe b/resources/recipes/msnsankei.recipe index ae195559d5..59664d055f 100644 --- a/resources/recipes/msnsankei.recipe +++ b/resources/recipes/msnsankei.recipe @@ -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") diff --git a/resources/recipes/theonion.recipe b/resources/recipes/theonion.recipe index 3be4ae4e04..b0eacbb5e0 100644 --- a/resources/recipes/theonion.recipe +++ b/resources/recipes/theonion.recipe @@ -1,7 +1,5 @@ -#!/usr/bin/env python - __license__ = 'GPL v3' -__copyright__ = '2009, Darko Miletic ' +__copyright__ = '2009-2011, Darko Miletic ' ''' 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 diff --git a/src/calibre/devices/nook/driver.py b/src/calibre/devices/nook/driver.py index ca05885645..39d0763735 100644 --- a/src/calibre/devices/nook/driver.py +++ b/src/calibre/devices/nook/driver.py @@ -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 - ''' diff --git a/src/calibre/ebooks/conversion/utils.py b/src/calibre/ebooks/conversion/utils.py index 8a339afe4c..edd4d54cba 100644 --- a/src/calibre/ebooks/conversion/utils.py +++ b/src/calibre/ebooks/conversion/utils.py @@ -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 = '
' + hr_open = '
' if re.findall('(<|>)', replacement_break): if re.match('^\d+).*', '\g', 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+'
' @@ -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) diff --git a/src/calibre/ebooks/oeb/stylizer.py b/src/calibre/ebooks/oeb/stylizer.py index abad5afcb3..849d161228 100644 --- a/src/calibre/ebooks/oeb/stylizer.py +++ b/src/calibre/ebooks/oeb/stylizer.py @@ -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 diff --git a/src/calibre/gui2/actions/device.py b/src/calibre/gui2/actions/device.py index fb3e627789..b32568f8fd 100644 --- a/src/calibre/gui2/actions/device.py +++ b/src/calibre/gui2/actions/device.py @@ -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')) diff --git a/src/calibre/gui2/actions/edit_metadata.py b/src/calibre/gui2/actions/edit_metadata.py index f50251e700..6c2cfb8126 100644 --- a/src/calibre/gui2/actions/edit_metadata.py +++ b/src/calibre/gui2/actions/edit_metadata.py @@ -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 diff --git a/src/calibre/gui2/complete.py b/src/calibre/gui2/complete.py index ce8609fc99..f589b30679 100644 --- a/src/calibre/gui2/complete.py +++ b/src/calibre/gui2/complete.py @@ -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) diff --git a/src/calibre/gui2/convert/heuristics.py b/src/calibre/gui2/convert/heuristics.py index 77fadf059c..5e7e4aa506 100644 --- a/src/calibre/gui2/convert/heuristics.py +++ b/src/calibre/gui2/convert/heuristics.py @@ -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'
', u'* * *', u'• • •', u'✦ ✦ ✦', - u'✮ ✮ ✮', u'☆ ☆ ☆', u'❂ ❂ ❂', u'✣ ✣ ✣', u'❖ ❖ ❖', u'☼ ☼ ☼', u'✠ ✠ ✠'] + self.rssb_defaults = [u'', u'
', u'∗ ∗ ∗', u'• • •', u'♦ ♦ ♦', + u'† †', u'‡ ‡ ‡', u'∞ ∞ ∞', u'¤ ¤ ¤', u'§'] self.initialize_options(get_option, get_help, db, book_id) self.load_histories() diff --git a/src/calibre/gui2/convert/metadata.py b/src/calibre/gui2/convert/metadata.py index 23cac74cf8..81274f25a8 100644 --- a/src/calibre/gui2/convert/metadata.py +++ b/src/calibre/gui2/convert/metadata.py @@ -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 diff --git a/src/calibre/gui2/convert/metadata.ui b/src/calibre/gui2/convert/metadata.ui index 61c27594c4..95ccac6890 100644 --- a/src/calibre/gui2/convert/metadata.ui +++ b/src/calibre/gui2/convert/metadata.ui @@ -190,7 +190,7 @@ - + Tags categorize the book. This is particularly useful while searching. <br><br>They can be any words or phrases, separated by commas. @@ -213,7 +213,7 @@ - + 10 @@ -248,14 +248,14 @@ - + true - + true @@ -277,19 +277,14 @@
widgets.h
- EnComboBox + MultiCompleteComboBox QComboBox -
widgets.h
+
calibre/gui2/complete.h
- CompleteComboBox - QComboBox -
widgets.h
-
- - CompleteLineEdit + MultiCompleteLineEdit QLineEdit -
widgets.h
+
calibre/gui2/complete.h
ImageView diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py index 360a5bcd0a..eae6dc79c3 100644 --- a/src/calibre/gui2/custom_column_widgets.py +++ b/src/calibre/gui2/custom_column_widgets.py @@ -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 diff --git a/src/calibre/gui2/dialogs/add_empty_book.py b/src/calibre/gui2/dialogs/add_empty_book.py index b8339f95f5..9e5fb07308 100644 --- a/src/calibre/gui2/dialogs/add_empty_book.py +++ b/src/calibre/gui2/dialogs/add_empty_book.py @@ -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) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index 533a344de5..e355144544 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -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('

'+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, diff --git a/src/calibre/gui2/dialogs/metadata_bulk.ui b/src/calibre/gui2/dialogs/metadata_bulk.ui index b0f2c144fc..2ab37bcbc6 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.ui +++ b/src/calibre/gui2/dialogs/metadata_bulk.ui @@ -76,7 +76,7 @@ - + true @@ -175,7 +175,7 @@ - + true @@ -195,7 +195,7 @@ - + Tags categorize the book. This is particularly useful while searching. <br><br>They can be any words or phrases, separated by commas. @@ -229,7 +229,7 @@ - + Comma separated list of tags to remove from the books. @@ -262,7 +262,7 @@ - + 0 @@ -489,6 +489,16 @@ title and author are swapped before the title case is set + + + + Update title sort based on the current title. This will be applied only after other changes to title. + + + Update &title sort + + + @@ -1072,19 +1082,14 @@ not multiple and the destination field is multiple

widgets.h
- EnComboBox + MultiCompleteComboBox QComboBox -
widgets.h
+
calibre/gui2/complete.h
- CompleteComboBox - QComboBox -
widgets.h
-
- - CompleteLineEdit + MultiCompleteLineEdit QLineEdit -
widgets.h
+
calibre/gui2/complete.h
HistoryLineEdit diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py index fa20658c12..aec8c4fd60 100644 --- a/src/calibre/gui2/dialogs/metadata_single.py +++ b/src/calibre/gui2/dialogs/metadata_single.py @@ -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 diff --git a/src/calibre/gui2/dialogs/metadata_single.ui b/src/calibre/gui2/dialogs/metadata_single.ui index 23efc45399..5bcf268aaa 100644 --- a/src/calibre/gui2/dialogs/metadata_single.ui +++ b/src/calibre/gui2/dialogs/metadata_single.ui @@ -240,7 +240,7 @@ Using this button to create author sort will change author sort from red to gree
- + true @@ -313,7 +313,7 @@ If the box is colored green, then text matches the individual author's sort stri - + true @@ -335,7 +335,7 @@ If the box is colored green, then text matches the individual author's sort stri - + Tags categorize the book. This is particularly useful while searching. <br><br>They can be any words or phrases, separated by commas. @@ -379,7 +379,7 @@ If the box is colored green, then text matches the individual author's sort stri 5 - + List of known series. You can add new series. @@ -837,19 +837,14 @@ If the box is colored green, then text matches the individual author's sort stri
widgets.h
- EnComboBox - QComboBox -
widgets.h
-
- - CompleteLineEdit + MultiCompleteLineEdit QLineEdit -
widgets.h
+
calibre/gui2/complete.h
- CompleteComboBox + MultiCompleteComboBox QComboBox -
widgets.h
+
calibre/gui2/complete.h
FormatList diff --git a/src/calibre/gui2/dialogs/search.py b/src/calibre/gui2/dialogs/search.py index ab3fd3ec4e..9c91446f3c 100644 --- a/src/calibre/gui2/dialogs/search.py +++ b/src/calibre/gui2/dialogs/search.py @@ -3,7 +3,7 @@ __copyright__ = '2008, Kovid Goyal ' 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) diff --git a/src/calibre/gui2/dialogs/search.ui b/src/calibre/gui2/dialogs/search.ui index 1d013a1e9f..eb6fffdb60 100644 --- a/src/calibre/gui2/dialogs/search.ui +++ b/src/calibre/gui2/dialogs/search.ui @@ -265,21 +265,21 @@
- + Enter an author's name. Only one author can be used. - + Enter a series name, without an index. Only one series name can be used. - + Enter tags separated by spaces @@ -355,19 +355,14 @@
widgets.h
- EnComboBox - QComboBox -
widgets.h
-
- - CompleteLineEdit + MultiCompleteLineEdit QLineEdit -
widgets.h
+
calibre/gui2/complete.h
- CompleteComboBox + MultiCompleteComboBox QComboBox -
widgets.h
+
calibre/gui2/complete.h
diff --git a/src/calibre/gui2/library/delegates.py b/src/calibre/gui2/library/delegates.py index ae9d6e2f71..fed2e42470 100644 --- a/src/calibre/gui2/library/delegates.py +++ b/src/calibre/gui2/library/delegates.py @@ -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 # }}} diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index 31b8cf46bf..0b6991665b 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -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 diff --git a/src/calibre/gui2/metadata/basic_widgets.py b/src/calibre/gui2/metadata/basic_widgets.py index 8ec037278e..f9058fc333 100644 --- a/src/calibre/gui2/metadata/basic_widgets.py +++ b/src/calibre/gui2/metadata/basic_widgets.py @@ -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 # }}} diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index 3b163d84f7..1be954155c 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -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 diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index dd4509acea..e818e6a3c0 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -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) diff --git a/src/calibre/library/catalog.py b/src/calibre/library/catalog.py index 460bf79c87..2ff1c4b731 100644 --- a/src/calibre/library/catalog.py +++ b/src/calibre/library/catalog.py @@ -1841,8 +1841,6 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) body.insert(btc,pTag) btc += 1 - #

- #

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 - #

- #

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 - #

- #

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 - #

- #

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 - #

- #

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) diff --git a/src/calibre/library/custom_columns.py b/src/calibre/library/custom_columns.py index f94081f046..467a3f309e 100644 --- a/src/calibre/library/custom_columns.py +++ b/src/calibre/library/custom_columns.py @@ -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' diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index bfe54df36e..6ec06e96f0 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -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: diff --git a/src/calibre/manual/conversion.rst b/src/calibre/manual/conversion.rst index ecd8609ecc..60f8a10fc6 100644 --- a/src/calibre/manual/conversion.rst +++ b/src/calibre/manual/conversion.rst @@ -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.


- 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.
+ tags, i.e. horizontal rules, and 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): + + + Example horizontal rule with styles: +
:guilabel:`Remove unnecessary hyphens` |app| will analyze all hyphenated content in the document when this option is enabled. The document itself is used diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py index 2e5852df89..518f2ed140 100644 --- a/src/calibre/utils/formatter_functions.py +++ b/src/calibre/utils/formatter_functions.py @@ -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'