diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py index 893c8b6b6a..81088da520 100644 --- a/resources/default_tweaks.py +++ b/resources/default_tweaks.py @@ -32,9 +32,10 @@ series_index_auto_increment = 'next' # Should the completion separator be append # to the end of the completed text to -# automatically begin a new completion operation. +# automatically begin a new completion operation +# for authors. # Can be either True or False -completer_append_separator = False +authors_completer_append_separator = False # The algorithm used to copy author to author_sort diff --git a/resources/recipes/cinebel_be.recipe b/resources/recipes/cinebel_be.recipe index ec76bfc894..024050eb67 100644 --- a/resources/recipes/cinebel_be.recipe +++ b/resources/recipes/cinebel_be.recipe @@ -1,7 +1,7 @@ #!/usr/bin/env python __license__ = 'GPL v3' -__copyright__ = '2008, Lionel Bergeret ' +__copyright__ = '2008-2011, Lionel Bergeret ' ''' cinebel.be ''' @@ -14,14 +14,14 @@ class Cinebel(BasicNewsRecipe): description = u'Cinema news from Belgium in French' publisher = u'cinebel.be' category = 'news, cinema, movie, Belgium' - oldest_article = 3 - encoding = 'utf8' - language = 'fr_BE' + oldest_article = 15 + language = 'fr' max_articles_per_feed = 20 no_stylesheets = True use_embedded_content = False timefmt = ' [%d %b %Y]' + filterDuplicates = True keep_only_tags = [ dict(name = 'span', attrs = {'class': 'movieMainTitle'}) @@ -35,6 +35,13 @@ class Cinebel(BasicNewsRecipe): ,(u'Top 10' , u'http://www.cinebel.be/Servlets/RssServlet?languageCode=fr&rssType=2' ) ] + def preprocess_html(self, soup): + for alink in soup.findAll('a'): + if alink.has_key('href'): + tstr = "Site officiel: " + alink['href'] + alink.replaceWith(tstr) + return soup + def get_cover_url(self): cover_url = 'http://www.cinebel.be/portal/resources/common/logo_index.gif' return cover_url diff --git a/resources/recipes/dhnet_be.recipe b/resources/recipes/dhnet_be.recipe index ef4d1736e3..d55470a765 100644 --- a/resources/recipes/dhnet_be.recipe +++ b/resources/recipes/dhnet_be.recipe @@ -1,7 +1,7 @@ #!/usr/bin/env python __license__ = 'GPL v3' -__copyright__ = '2008, Lionel Bergeret ' +__copyright__ = '2008-2011, Lionel Bergeret ' ''' dhnet.be ''' @@ -16,7 +16,8 @@ class DHNetBe(BasicNewsRecipe): publisher = u'dhnet.be' category = 'news, Belgium' oldest_article = 3 - language = 'fr_BE' + language = 'fr' + masthead_url = 'http://www.dhnet.be/images/homepage_logo_dh.gif' max_articles_per_feed = 20 no_stylesheets = True @@ -34,6 +35,13 @@ class DHNetBe(BasicNewsRecipe): ,(u'La Une Info' , u'http://www.dhnet.be/rss/dhinfos/' ) ] + def preprocess_html(self, soup): + for alink in soup.findAll('a'): + if alink.string is not None: + tstr = alink.string + alink.replaceWith(tstr) + return soup + def get_cover_url(self): cover_url = strftime('http://pdf-online.dhnet.be/pdfonline/image/%Y%m%d/dh_%Y%m%d_nam_infoge_001.pdf.L.jpg') return cover_url diff --git a/resources/recipes/lalibre_be.recipe b/resources/recipes/lalibre_be.recipe index 53e346bf12..a6356be828 100644 --- a/resources/recipes/lalibre_be.recipe +++ b/resources/recipes/lalibre_be.recipe @@ -1,7 +1,7 @@ #!/usr/bin/env python __license__ = 'GPL v3' -__copyright__ = '2008, Lionel Bergeret ' +__copyright__ = '2008-2011, Lionel Bergeret ' ''' lalibre.be ''' @@ -16,18 +16,18 @@ class LaLibre(BasicNewsRecipe): publisher = u'lalibre.be' category = 'news, Belgium' oldest_article = 3 - language = 'fr_BE' + language = 'fr' + masthead_url = 'http://www.lalibre.be/img/logoLaLibre.gif' max_articles_per_feed = 20 no_stylesheets = True use_embedded_content = False timefmt = ' [%d %b %Y]' - keep_only_tags = [ - dict(name = 'div', attrs = {'id': 'articleHat'}) - ,dict(name = 'p', attrs = {'id': 'publicationDate'}) - ,dict(name = 'div', attrs = {'id': 'articleText'}) - ] + remove_tags_before = dict(name = 'div', attrs = {'class': 'extraMainContent'}) + remove_tags_after = dict(name = 'div', attrs = {'id': 'articleText'}) + + remove_tags = [dict(name = 'div', attrs = {'id': 'strongArticleLinks'})] feeds = [ (u'L\'actu' , u'http://www.lalibre.be/rss/?section=10' ) @@ -38,6 +38,13 @@ class LaLibre(BasicNewsRecipe): ,(u'Societe' , u'http://www.lalibre.be/rss/?section=12' ) ] + def preprocess_html(self, soup): + for alink in soup.findAll('a'): + if alink.string is not None: + tstr = alink.string + alink.replaceWith(tstr) + return soup + def get_cover_url(self): cover_url = strftime('http://pdf-online.lalibre.be/pdfonline/image/%Y%m%d/llb_%Y%m%d_nam_libre_001.pdf.L.jpg') return cover_url diff --git a/resources/recipes/lameuse_be.recipe b/resources/recipes/lameuse_be.recipe index 03b7f84a5f..7166d01103 100644 --- a/resources/recipes/lameuse_be.recipe +++ b/resources/recipes/lameuse_be.recipe @@ -1,7 +1,7 @@ #!/usr/bin/env python __license__ = 'GPL v3' -__copyright__ = '2008, Lionel Bergeret ' +__copyright__ = '2008-2011, Lionel Bergeret ' ''' lameuse.be ''' @@ -16,8 +16,8 @@ class LaMeuse(BasicNewsRecipe): publisher = u'lameuse.be' category = 'news, Belgium' oldest_article = 3 - encoding = 'utf8' - language = 'fr_BE' + language = 'fr' + masthead_url = 'http://www.lameuse.be/images/SPV3/logo_header_LM.gif' max_articles_per_feed = 20 no_stylesheets = True @@ -32,6 +32,11 @@ class LaMeuse(BasicNewsRecipe): dict(name = 'div', attrs = {'class': 'sb-group'}) ,dict(name = 'div', attrs = {'id': 'share'}) ,dict(name = 'div', attrs = {'id': 'commentaires'}) + ,dict(name = 'ul', attrs = {'class': 'right liensutiles'}) + ,dict(name = 'ul', attrs = {'class': 'bas liensutiles'}) + ,dict(name = 'p', attrs = {'class': 'ariane'}) + ,dict(name = 'div', attrs = {'class': 'inner-bloc'}) + ,dict(name = 'div', attrs = {'class': 'block-01'}) ] feeds = [ diff --git a/resources/recipes/lavenir_be.recipe b/resources/recipes/lavenir_be.recipe index 68be449ae5..4c2c8a00a2 100644 --- a/resources/recipes/lavenir_be.recipe +++ b/resources/recipes/lavenir_be.recipe @@ -1,7 +1,7 @@ #!/usr/bin/env python __license__ = 'GPL v3' -__copyright__ = '2008, Lionel Bergeret ' +__copyright__ = '2008-2011, Lionel Bergeret ' ''' lavenir.net ''' @@ -15,8 +15,7 @@ class LAvenir(BasicNewsRecipe): publisher = u'lavenir.net' category = 'news, Belgium' oldest_article = 3 - encoding = 'utf8' - language = 'fr_BE' + language = 'fr' max_articles_per_feed = 20 no_stylesheets = True @@ -35,6 +34,13 @@ class LAvenir(BasicNewsRecipe): ,(u'Societe' , u'http://www.lavenir.net/rss.aspx?foto=1&intro=1§ion=info&info=12e1a2f4-7e03-4cf1-afec-016869072317' ) ] + def preprocess_html(self, soup): + for alink in soup.findAll('a'): + if alink.string is not None: + tstr = alink.string + alink.replaceWith(tstr) + return soup + def get_cover_url(self): cover_url = 'http://www.lavenir.net/extra/Static/journal/Pdf/1/UNE_Nationale.PDF' return cover_url diff --git a/resources/recipes/lesoir_be.recipe b/resources/recipes/lesoir_be.recipe index 6b6891c3b8..64fd2fa65c 100644 --- a/resources/recipes/lesoir_be.recipe +++ b/resources/recipes/lesoir_be.recipe @@ -1,7 +1,7 @@ #!/usr/bin/env python __license__ = 'GPL v3' -__copyright__ = '2008, Lionel Bergeret ' +__copyright__ = '2008-2011, Lionel Bergeret ' ''' lesoir.be ''' @@ -16,7 +16,8 @@ class LeSoirBe(BasicNewsRecipe): publisher = u'lesoir.be' category = 'news, Belgium' oldest_article = 3 - language = 'fr_BE' + language = 'fr' + masthead_url = 'http://pdf.lesoir.be/pdf/images/SOIR//logo.gif' max_articles_per_feed = 20 no_stylesheets = True diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 32c512fe39..bd8e07fb58 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -791,6 +791,17 @@ class Toolbar(PreferencesPlugin): description = _('Customize the toolbars and context menus, changing which' ' actions are available in each') +class Search(PreferencesPlugin): + name = 'Search' + icon = I('search.png') + gui_name = _('Customize searching') + category = 'Interface' + gui_category = _('Interface') + category_order = 1 + name_order = 5 + config_widget = 'calibre.gui2.preferences.search' + description = _('Customize the way searching for books works in calibre') + class InputOptions(PreferencesPlugin): name = 'Input Options' icon = I('arrow-down.png') @@ -941,7 +952,7 @@ class Misc(PreferencesPlugin): config_widget = 'calibre.gui2.preferences.misc' description = _('Miscellaneous advanced configuration') -plugins += [LookAndFeel, Behavior, Columns, Toolbar, InputOptions, +plugins += [LookAndFeel, Behavior, Columns, Toolbar, Search, InputOptions, CommonOptions, OutputOptions, Adding, Saving, Sending, Plugboard, Email, Server, Plugins, Tweaks, Misc, TemplateFunctions] diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index 11d636791b..e9021461eb 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -62,6 +62,9 @@ class ANDROID(USBMS): # Archos 0x0e79 : { 0x1419: [0x0216], 0x1420 : [0x0216], 0x1422 : [0x0216]}, + # Huawei + 0x45e : { 0x00e1 : [0x007], }, + } EBOOK_DIR_MAIN = ['eBooks/import', 'wordplayer/calibretransfer', 'Books'] EXTRA_CUSTOMIZATION_MESSAGE = _('Comma separated list of directories to ' @@ -71,12 +74,13 @@ class ANDROID(USBMS): VENDOR_NAME = ['HTC', 'MOTOROLA', 'GOOGLE_', 'ANDROID', 'ACER', 'GT-I5700', 'SAMSUNG', 'DELL', 'LINUX', 'GOOGLE', 'ARCHOS', - 'TELECHIP'] + 'TELECHIP', 'HUAWEI', ] WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE', '__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897', 'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'SCH-I500_CARD', 'SPH-D700_CARD', 'MB810', 'GT-P1000', 'DESIRE', - 'SGH-T849', '_MB300', 'A70S', 'S_ANDROID', 'A101IT', 'A70H'] + 'SGH-T849', '_MB300', 'A70S', 'S_ANDROID', 'A101IT', 'A70H', + 'IDEOS_TABLET'] WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD', 'A70S', 'A101IT'] diff --git a/src/calibre/ebooks/conversion/utils.py b/src/calibre/ebooks/conversion/utils.py index d075390e8e..e58bbca1bd 100644 --- a/src/calibre/ebooks/conversion/utils.py +++ b/src/calibre/ebooks/conversion/utils.py @@ -11,6 +11,7 @@ from calibre.ebooks.conversion.preprocess import DocAnalysis, Dehyphenator from calibre.utils.logging import default_log from calibre.utils.wordcount import get_wordcount_obj + class HeuristicProcessor(object): def __init__(self, extra_opts=None, log=None): @@ -40,6 +41,9 @@ class HeuristicProcessor(object): def is_pdftohtml(self, src): return '' in src[:1000] + def is_abbyy(self, src): + return '' + return '<'+tag+' '+pstyle+'>' else: - return '

'+span + return '<'+tag+' '+pstyle+'>'+span else: if not span: - return '

' + return '<'+tag+' style="text-indent:3%">' else: - return '

'+span + return '<'+tag+' style="text-indent:3%">'+span def no_markup(self, raw, percent): ''' @@ -365,7 +370,7 @@ class HeuristicProcessor(object): return html def fix_nbsp_indents(self, html): - txtindent = re.compile(ur'[^>]*)>\s*(?P(]*>\s*)+)?\s*(\u00a0){2,}', re.IGNORECASE) + txtindent = re.compile(ur'<(?Pp|div)(?P[^>]*)>\s*(?P(]*>\s*)+)?\s*(\u00a0){2,}', re.IGNORECASE) html = txtindent.sub(self.insert_indent, html) if self.found_indents > 1: self.log.debug("replaced "+unicode(self.found_indents)+ " nbsp indents with inline styles") @@ -518,6 +523,111 @@ class HeuristicProcessor(object): return scene_break + def abbyy_processor(self, html): + abbyy_line = re.compile('((?P[^\"]*?);?">)(?P.*?)(?P

)|(?P]*>))', re.IGNORECASE) + empty_paragraph = '\n

\n' + self.in_blockquote = False + self.previous_was_paragraph = False + html = re.sub(']*>', '', html) + + def check_paragraph(content): + content = re.sub('\s*]*>\s*', '', content) + if re.match('.*[\"\'.!?:]$', content): + #print "detected this as a paragraph" + return True + else: + return False + + def convert_styles(match): + #print "raw styles are: "+match.group('styles') + content = match.group('content') + #print "raw content is: "+match.group('content') + image = match.group('image') + + is_paragraph = False + text_align = '' + text_indent = '' + paragraph_before = '' + paragraph_after = '' + blockquote_open = '\n
\n' + blockquote_close = '
\n' + indented_text = 'text-indent:3%;' + blockquote_open_loop = '' + blockquote_close_loop = '' + debugabby = False + + if image: + debugabby = True + if self.in_blockquote: + self.in_blockquote = False + blockquote_close_loop = blockquote_close + self.previous_was_paragraph = False + return blockquote_close_loop+'\n'+image+'\n' + else: + styles = match.group('styles').split(';') + is_paragraph = check_paragraph(content) + #print "styles for this line are: "+str(styles) + split_styles = [] + for style in styles: + #print "style is: "+str(style) + newstyle = style.split(':') + #print "newstyle is: "+str(newstyle) + split_styles.append(newstyle) + styles = split_styles + for style, setting in styles: + if style == 'text-align' and setting != 'left': + text_align = style+':'+setting+';' + if style == 'text-indent': + setting = int(re.sub('\s*pt\s*', '', setting)) + if 9 < setting < 14: + text_indent = indented_text + else: + text_indent = style+':'+str(setting)+'pt;' + if style == 'padding': + setting = re.sub('pt', '', setting).split(' ') + if int(setting[1]) < 16 and int(setting[3]) < 16: + if self.in_blockquote: + debugabby = True + if is_paragraph: + self.in_blockquote = False + blockquote_close_loop = blockquote_close + if int(setting[3]) > 8 and text_indent == '': + text_indent = indented_text + if int(setting[0]) > 5: + paragraph_before = empty_paragraph + if int(setting[2]) > 5: + paragraph_after = empty_paragraph + elif not self.in_blockquote and self.previous_was_paragraph: + debugabby = True + self.in_blockquote = True + blockquote_open_loop = blockquote_open + if debugabby: + self.log.debug('\n\n******\n') + self.log.debug('padding top is: '+str(setting[0])) + self.log.debug('padding right is:' + +str(setting[1])) + self.log.debug('padding bottom is: ' + + str(setting[2])) + self.log.debug('padding left is: ' + +str(setting[3])) + + #print "text-align is: "+str(text_align) + #print "\n***\nline is:\n "+str(match.group(0))+'\n' + if debugabby: + #print "this line is a paragraph = "+str(is_paragraph)+", previous line was "+str(self.previous_was_paragraph) + self.log.debug("styles for this line were:", styles) + self.log.debug('newline is:') + self.log.debug(blockquote_open_loop+blockquote_close_loop+ + paragraph_before+'

'+content+'

'+paragraph_after+'\n\n\n\n\n') + #print "is_paragraph is "+str(is_paragraph)+", previous_was_paragraph is "+str(self.previous_was_paragraph) + self.previous_was_paragraph = is_paragraph + #print "previous_was_paragraph is now set to "+str(self.previous_was_paragraph)+"\n\n\n" + return blockquote_open_loop+blockquote_close_loop+paragraph_before+'

'+content+'

'+paragraph_after + + html = abbyy_line.sub(convert_styles, html) + return html + def __call__(self, html): self.log.debug("********* Heuristic processing HTML *********") @@ -532,6 +642,10 @@ class HeuristicProcessor(object): self.log.warn("flow is too short, not running heuristics") return html + is_abbyy = self.is_abbyy(html) + if is_abbyy: + html = self.abbyy_processor(html) + # Arrange line feeds and

tags so the line_length and no_markup functions work correctly html = self.arrange_htm_line_endings(html) #self.dump(html, 'after_arrange_line_endings') diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 92a68fa840..b33166dd33 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -106,9 +106,13 @@ def _config(): 'clicked')) c.add_opt('asked_library_thing_password', default=False, help='Asked library thing password at least once.') - c.add_opt('search_as_you_type', default=True, - help='Start searching as you type. If this is disabled then search will ' - 'only take place when the Enter or Return key is pressed.') + c.add_opt('search_as_you_type', default=False, + help=_('Start searching as you type. If this is disabled then search will ' + 'only take place when the Enter or Return key is pressed.')) + c.add_opt('highlight_search_matches', default=False, + help=_('When searching, show all books with search results ' + 'highlighted instead of showing only the matches. You can use the ' + 'N or F3 keys to go to the next match.')) c.add_opt('save_to_disk_template_history', default=[], help='Previously used Save to Disk templates') c.add_opt('send_to_device_template_history', default=[], diff --git a/src/calibre/gui2/actions/next_match.py b/src/calibre/gui2/actions/next_match.py index 79de6a2d9b..1c74719674 100644 --- a/src/calibre/gui2/actions/next_match.py +++ b/src/calibre/gui2/actions/next_match.py @@ -28,21 +28,12 @@ class NextMatchAction(InterfaceAction): self.gui.addAction(self.p_action) self.p_action.triggered.connect(self.move_backward) - def gui_layout_complete(self): - self.gui.search_highlight_only.setVisible(True) - def location_selected(self, loc): self.can_move = loc == 'library' - try: - self.gui.search_highlight_only.setVisible(self.can_move) - except: - import traceback - traceback.print_exc() def move_forward(self): if self.can_move is None: self.can_move = self.gui.current_view() is self.gui.library_view - self.gui.search_highlight_only.setVisible(self.can_move) if self.can_move: self.gui.current_view().move_highlighted_row(forward=True) @@ -50,7 +41,6 @@ class NextMatchAction(InterfaceAction): def move_backward(self): if self.can_move is None: self.can_move = self.gui.current_view() is self.gui.library_view - self.gui.search_highlight_only.setVisible(self.can_move) if self.can_move: self.gui.current_view().move_highlighted_row(forward=False) diff --git a/src/calibre/gui2/actions/preferences.py b/src/calibre/gui2/actions/preferences.py index f1fe06c43d..ee52f06aac 100644 --- a/src/calibre/gui2/actions/preferences.py +++ b/src/calibre/gui2/actions/preferences.py @@ -33,7 +33,8 @@ class PreferencesAction(InterfaceAction): x.triggered.connect(self.do_config) - def do_config(self, checked=False, initial_plugin=None): + def do_config(self, checked=False, initial_plugin=None, + close_after_initial=False): if self.gui.job_manager.has_jobs(): d = error_dialog(self.gui, _('Cannot configure'), _('Cannot configure while there are running jobs.')) @@ -44,7 +45,8 @@ class PreferencesAction(InterfaceAction): _('Cannot configure before calibre is restarted.')) d.exec_() return - d = Preferences(self.gui, initial_plugin=initial_plugin) + d = Preferences(self.gui, initial_plugin=initial_plugin, + close_after_initial=close_after_initial) d.show() d.run_wizard_requested.connect(self.gui.run_wizard, type=Qt.QueuedConnection) diff --git a/src/calibre/gui2/complete.py b/src/calibre/gui2/complete.py index 58020f924a..2eb97b128d 100644 --- a/src/calibre/gui2/complete.py +++ b/src/calibre/gui2/complete.py @@ -9,7 +9,6 @@ __docformat__ = 'restructuredtext en' from PyQt4.Qt import QLineEdit, QAbstractListModel, Qt, \ QApplication, QCompleter -from calibre.utils.config import tweaks from calibre.utils.icu import sort_key, lower from calibre.gui2 import NONE from calibre.gui2.widgets import EnComboBox @@ -55,6 +54,8 @@ class MultiCompleteLineEdit(QLineEdit): self.sep = ',' self.space_before_sep = False + self.add_separator = True + self.original_cursor_pos = None self._model = CompleteModel(parent=self) self._completer = c = QCompleter(self._model, self) @@ -82,6 +83,9 @@ class MultiCompleteLineEdit(QLineEdit): def set_space_before_sep(self, space_before): self.space_before_sep = space_before + def set_add_separator(self, what): + self.add_separator = bool(what) + # }}} def item_entered(self, idx): @@ -93,7 +97,7 @@ class MultiCompleteLineEdit(QLineEdit): def update_completions(self): ' Update the list of completions ' - cpos = self.cursorPosition() + self.original_cursor_pos = cpos = self.cursorPosition() text = unicode(self.text()) prefix = text[:cpos] self.current_prefix = prefix @@ -103,38 +107,38 @@ class MultiCompleteLineEdit(QLineEdit): self._completer.setCompletionPrefix(complete_prefix) def get_completed_text(self, text): - ''' - Get completed text from current cursor position and the completion - text - ''' + 'Get completed text in before and after parts' if self.sep is None: - return -1, text + return text, '' else: - cursor_pos = self.cursorPosition() - before_text = unicode(self.text())[:cursor_pos] - after_text = unicode(self.text())[cursor_pos:] - prefix_len = len(before_text.split(self.sep)[-1].lstrip()) - if tweaks['completer_append_separator']: - prefix_len = len(before_text.split(self.sep)[-1].lstrip()) - completed_text = before_text[:cursor_pos - prefix_len] + text + self.sep + ' ' + after_text - prefix_len = prefix_len - len(self.sep) - 1 - if prefix_len < 0: - prefix_len = 0 + cursor_pos = self.original_cursor_pos + if cursor_pos is None: + cursor_pos = self.cursorPosition() + self.original_cursor_pos = None + # Split text + curtext = unicode(self.text()) + before_text = curtext[:cursor_pos] + after_text = curtext[cursor_pos:].rstrip() + # Remove the completion prefix from the before text + before_text = self.sep.join(before_text.split(self.sep)[:-1]).rstrip() + if before_text: + # Add the separator to the end of before_text + if self.space_before_sep: + before_text += ' ' + before_text += self.sep + ' ' + if self.add_separator or after_text: + # Add separator to the end of completed text + if self.space_before_sep: + text = text.rstrip() + ' ' + completed_text = text + self.sep + ' ' else: - prefix_len = len(before_text.split(self.sep)[-1].lstrip()) - completed_text = before_text[:cursor_pos - prefix_len] + text + after_text - return prefix_len, completed_text - + completed_text = text + return before_text + completed_text, after_text def completion_selected(self, text): - prefix_len, ctext = self.get_completed_text(unicode(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)) + before_text, after_text = self.get_completed_text(unicode(text)) + self.setText(before_text + after_text) + self.setCursorPosition(len(before_text)) @dynamic_property def all_items(self): @@ -164,6 +168,9 @@ class MultiCompleteComboBox(EnComboBox): def set_space_before_sep(self, space_before): self.lineEdit().set_space_before_sep(space_before) + def set_add_separator(self, what): + self.lineEdit().set_add_separator(what) + if __name__ == '__main__': diff --git a/src/calibre/gui2/convert/metadata.py b/src/calibre/gui2/convert/metadata.py index 81274f25a8..95dd7623c9 100644 --- a/src/calibre/gui2/convert/metadata.py +++ b/src/calibre/gui2/convert/metadata.py @@ -19,6 +19,7 @@ from calibre.ptempfile import PersistentTemporaryFile from calibre.gui2.convert import Widget from calibre.utils.icu import sort_key from calibre.library.comments import comments_to_html +from calibre.utils.config import tweaks def create_opf_file(db, book_id): mi = db.get_metadata(book_id, index_is_id=True) @@ -108,6 +109,7 @@ class MetadataWidget(Widget, Ui_Form): all_authors.sort(key=lambda x : sort_key(x[1])) self.author.set_separator('&') self.author.set_space_before_sep(True) + self.author.set_add_separator(tweaks['authors_completer_append_separator']) self.author.update_items_cache(self.db.all_author_names()) for i in all_authors: diff --git a/src/calibre/gui2/dialogs/add_empty_book.py b/src/calibre/gui2/dialogs/add_empty_book.py index 9e5fb07308..d4990e14d4 100644 --- a/src/calibre/gui2/dialogs/add_empty_book.py +++ b/src/calibre/gui2/dialogs/add_empty_book.py @@ -9,6 +9,7 @@ from PyQt4.Qt import QDialog, QGridLayout, QLabel, QDialogButtonBox, \ from calibre.ebooks.metadata import authors_to_string, string_to_authors from calibre.utils.icu import sort_key from calibre.gui2.complete import MultiCompleteComboBox +from calibre.utils.config import tweaks class AddEmptyBookDialog(QDialog): @@ -69,6 +70,7 @@ class AddEmptyBookDialog(QDialog): self.authors_combo.set_separator('&') self.authors_combo.set_space_before_sep(True) + self.authors_combo.set_add_separator(tweaks['authors_completer_append_separator']) self.authors_combo.update_items_cache(db.all_author_names()) @property diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index e355144544..9ad61d515b 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -781,6 +781,7 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog): self.authors.set_separator('&') self.authors.set_space_before_sep(True) + self.authors.set_add_separator(tweaks['authors_completer_append_separator']) self.authors.update_items_cache(self.db.all_author_names()) def initialize_series(self): diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py index 52d263fe36..d95c905f42 100644 --- a/src/calibre/gui2/dialogs/metadata_single.py +++ b/src/calibre/gui2/dialogs/metadata_single.py @@ -735,6 +735,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): self.authors.set_separator('&') self.authors.set_space_before_sep(True) + self.authors.set_add_separator(tweaks['authors_completer_append_separator']) self.authors.update_items_cache(self.db.all_author_names()) def initialize_series(self): diff --git a/src/calibre/gui2/dialogs/search.py b/src/calibre/gui2/dialogs/search.py index 9c91446f3c..b4976e2657 100644 --- a/src/calibre/gui2/dialogs/search.py +++ b/src/calibre/gui2/dialogs/search.py @@ -9,6 +9,7 @@ from calibre.gui2.dialogs.search_ui import Ui_Dialog from calibre.library.caches import CONTAINS_MATCH, EQUALS_MATCH from calibre.gui2 import gprefs from calibre.utils.icu import sort_key +from calibre.utils.config import tweaks box_values = {} @@ -31,6 +32,7 @@ class SearchDialog(QDialog, Ui_Dialog): self.authors_box.setEditText('') self.authors_box.set_separator('&') self.authors_box.set_space_before_sep(True) + self.authors_box.set_add_separator(tweaks['authors_completer_append_separator']) self.authors_box.update_items_cache(db.all_author_names()) all_series = db.all_series() diff --git a/src/calibre/gui2/init.py b/src/calibre/gui2/init.py index ebd670c8fa..0ca58582b6 100644 --- a/src/calibre/gui2/init.py +++ b/src/calibre/gui2/init.py @@ -64,6 +64,7 @@ class LibraryViewMixin(object): # {{{ view.verticalHeader().sectionDoubleClicked.connect(self.iactions['View'].view_specific_book) self.build_context_menus() + self.library_view.model().set_highlight_only(config['highlight_search_matches']) def build_context_menus(self): lm = QMenu(self) diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index c1d9498075..e8a4e79384 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en' from functools import partial from PyQt4.Qt import QIcon, Qt, QWidget, QToolBar, QSize, \ - pyqtSignal, QToolButton, QMenu, QCheckBox, \ + pyqtSignal, QToolButton, QMenu, \ QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup @@ -156,7 +156,8 @@ class SearchBar(QWidget): # {{{ x = ComboBoxWithHelp(self) x.setMaximumSize(QSize(150, 16777215)) x.setObjectName("search_restriction") - x.setToolTip(_("Books display will be restricted to those matching the selected saved search")) + x.setToolTip(_('Books display will be restricted to those matching the ' + 'selected saved search')) l.addWidget(x) parent.search_restriction = x @@ -175,7 +176,8 @@ class SearchBar(QWidget): # {{{ x = parent.search = SearchBox2(self) x.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) x.setObjectName("search") - x.setToolTip(_("

Search the list of books by title, author, publisher, tags, comments, etc.

Words separated by spaces are ANDed")) + x.setToolTip(_("

Search the list of books by title, author, publisher, " + "tags, comments, etc.

Words separated by spaces are ANDed")) l.addWidget(x) self.search_button = QToolButton() @@ -194,13 +196,11 @@ class SearchBar(QWidget): # {{{ l.addWidget(x) x.setToolTip(_("Reset Quick Search")) - x = parent.search_highlight_only = QCheckBox() - x.setText(_('&Highlight')) - x.setToolTip('

'+_('When searching, highlight matched books, instead ' - 'of restricting the book list to the matches.

You can use the ' - 'N or F3 keys to go to the next match.')) + x = parent.search_options_button = QToolButton(self) + x.setIcon(QIcon(I('config.png'))) + x.setObjectName("search_option_button") l.addWidget(x) - x.setVisible(False) + x.setToolTip(_("Change the way searching for books works")) x = parent.saved_search = SavedSearchBox(self) x.setMaximumSize(QSize(150, 16777215)) @@ -227,7 +227,6 @@ class SearchBar(QWidget): # {{{ x.setToolTip(_("Delete current saved search")) - # }}} class Spacer(QWidget): # {{{ diff --git a/src/calibre/gui2/library/delegates.py b/src/calibre/gui2/library/delegates.py index fed2e42470..87da6818eb 100644 --- a/src/calibre/gui2/library/delegates.py +++ b/src/calibre/gui2/library/delegates.py @@ -177,6 +177,8 @@ class CompleteDelegate(QStyledItemDelegate): # {{{ editor = MultiCompleteLineEdit(parent) editor.set_separator(self.sep) editor.set_space_before_sep(self.space_before_sep) + if self.sep == '&': + editor.set_add_separator(tweaks['authors_completer_append_separator']) if not index.model().is_custom_column(col): all_items = getattr(self.db, self.items_func_name)() else: diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index 2f8a747c39..48668d3376 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -238,8 +238,6 @@ class BooksModel(QAbstractTableModel): # {{{ def set_highlight_only(self, toWhat): self.highlight_only = toWhat - if self.last_search: - self.research() def get_current_highlighted_id(self): if len(self.ids_to_highlight) == 0 or self.current_highlighted_idx is None: diff --git a/src/calibre/gui2/metadata/basic_widgets.py b/src/calibre/gui2/metadata/basic_widgets.py index f9058fc333..a135176daf 100644 --- a/src/calibre/gui2/metadata/basic_widgets.py +++ b/src/calibre/gui2/metadata/basic_widgets.py @@ -177,6 +177,7 @@ class AuthorsEdit(MultiCompleteComboBox): self.set_separator('&') self.set_space_before_sep(True) + self.set_add_separator(tweaks['authors_completer_append_separator']) self.update_items_cache(db.all_author_names()) au = db.authors(id_, index_is_id=True) diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py index 37ed90cc61..196ef16b08 100644 --- a/src/calibre/gui2/preferences/look_feel.py +++ b/src/calibre/gui2/preferences/look_feel.py @@ -46,7 +46,6 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): r('disable_tray_notification', config) r('use_roman_numerals_for_series_number', config) r('separate_cover_flow', config, restart_required=True) - r('search_as_you_type', config) r('show_child_bar', gprefs) choices = [(_('Small'), 'small'), (_('Medium'), 'medium'), @@ -116,7 +115,6 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): def refresh_gui(self, gui): - gui.search.search_as_you_type(config['search_as_you_type']) self.update_font_display() gui.tags_view.reread_collapse_parameters() diff --git a/src/calibre/gui2/preferences/look_feel.ui b/src/calibre/gui2/preferences/look_feel.ui index 2223167068..3f2bb3e145 100644 --- a/src/calibre/gui2/preferences/look_feel.ui +++ b/src/calibre/gui2/preferences/look_feel.ui @@ -124,23 +124,13 @@ - + Show cover &browser in a separate window (needs restart) - - - - Search as you type - - - true - - - @@ -177,7 +167,7 @@ if you never want subcategories - If a Tag Browser category has more than this number of items, it is divided + If a Tag Browser category has more than this number of items, it is divided up into sub-categories. If the partition method is set to disable, this value is ignored. diff --git a/src/calibre/gui2/preferences/main.py b/src/calibre/gui2/preferences/main.py index f7d49427c8..f25cc85dce 100644 --- a/src/calibre/gui2/preferences/main.py +++ b/src/calibre/gui2/preferences/main.py @@ -157,11 +157,12 @@ class Preferences(QMainWindow): run_wizard_requested = pyqtSignal() - def __init__(self, gui, initial_plugin=None): + def __init__(self, gui, initial_plugin=None, close_after_initial=False): QMainWindow.__init__(self, gui) self.gui = gui self.must_restart = False self.committed = False + self.close_after_initial = close_after_initial self.resize(900, 720) nh, nw = min_available_height()-25, available_width()-10 @@ -306,7 +307,7 @@ class Preferences(QMainWindow): def esc(self, *args): if self.stack.currentIndex() == 1: - self.hide_plugin() + self.cancel() elif self.stack.currentIndex() == 0: self.close() @@ -331,12 +332,15 @@ class Preferences(QMainWindow): show_copy_button=False) self.showing_widget.refresh_gui(self.gui) self.hide_plugin() - if must_restart and rc: + if self.close_after_initial or (must_restart and rc): self.close() def cancel(self, *args): - self.hide_plugin() + if self.close_after_initial: + self.close() + else: + self.hide_plugin() def restore_defaults(self, *args): self.showing_widget.restore_defaults() diff --git a/src/calibre/gui2/preferences/search.py b/src/calibre/gui2/preferences/search.py new file mode 100644 index 0000000000..81bc603df4 --- /dev/null +++ b/src/calibre/gui2/preferences/search.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +__license__ = 'GPL v3' +__copyright__ = '2010, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +from PyQt4.Qt import QApplication + +from calibre.gui2.preferences import ConfigWidgetBase, test_widget, \ + CommaSeparatedList +from calibre.gui2.preferences.search_ui import Ui_Form +from calibre.gui2 import config +from calibre.utils.config import prefs + +class ConfigWidget(ConfigWidgetBase, Ui_Form): + + def genesis(self, gui): + self.gui = gui + + r = self.register + + r('search_as_you_type', config) + r('highlight_search_matches', config) + r('limit_search_columns', prefs) + r('limit_search_columns_to', prefs, setting=CommaSeparatedList) + fl = gui.library_view.model().db.field_metadata.get_search_terms() + self.opt_limit_search_columns_to.update_items_cache(fl) + + def refresh_gui(self, gui): + gui.search.search_as_you_type(config['search_as_you_type']) + gui.library_view.model().set_highlight_only(config['highlight_search_matches']) + gui.search.do_search() + +if __name__ == '__main__': + app = QApplication([]) + test_widget('Interface', 'Search') + diff --git a/src/calibre/gui2/preferences/search.ui b/src/calibre/gui2/preferences/search.ui new file mode 100644 index 0000000000..360059ce56 --- /dev/null +++ b/src/calibre/gui2/preferences/search.ui @@ -0,0 +1,104 @@ + + + Form + + + + 0 + 0 + 670 + 392 + + + + Form + + + + + + Search as you &type + + + + + + + &Highlight search results instead of restricting the book list to the results + + + + + + + What to search by default + + + + + + When you enter a search term without a prefix, by default calibre will search all metadata for matches. For example, entering, "asimov" will search not just authors but title/tags/series/comments/etc. Use these options if you would like to change this behavior. + + + true + + + + + + + &Limit the searched metadata + + + + + + + &Columns that non-prefixed searches are limited to: + + + opt_limit_search_columns_to + + + + + + + + + + Note that this option affects all searches, including saved searches and restrictions. Therefore, if you use this option, it is best to ensure that you always use prefixes in your saved searches. For example, use "series:Foundation" rather than just "Foundation" in a saved search + + + true + + + + + + + + + + Qt::Vertical + + + + 0 + 0 + + + + + + + + + MultiCompleteLineEdit + QLineEdit +

calibre/gui2.complete.h
+ + + + + diff --git a/src/calibre/gui2/search_box.py b/src/calibre/gui2/search_box.py index e4073a01c9..900c882adc 100644 --- a/src/calibre/gui2/search_box.py +++ b/src/calibre/gui2/search_box.py @@ -16,7 +16,6 @@ from calibre.gui2 import config from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.dialogs.saved_search_editor import SavedSearchEditor from calibre.gui2.dialogs.search import SearchDialog -from calibre.utils.config import dynamic from calibre.utils.search_query_parser import saved_searches from calibre.utils.icu import sort_key @@ -271,7 +270,7 @@ class SavedSearchBox(QComboBox): # {{{ def initialize(self, _search_box, colorize=False, help_text=_('Search')): self.search_box = _search_box try: - self.line_edit.setPlaceholderText(help_text) + self.line_edit.setPlaceholderText(help_text) except: # Using Qt < 4.7 pass @@ -376,9 +375,7 @@ class SearchBoxMixin(object): # {{{ unicode(self.search.toolTip()))) self.advanced_search_button.setStatusTip(self.advanced_search_button.toolTip()) self.clear_button.setStatusTip(self.clear_button.toolTip()) - self.search_highlight_only.stateChanged.connect(self.highlight_only_changed) - self.search_highlight_only.setChecked( - dynamic.get('search_highlight_only', False)) + self.search_options_button.clicked.connect(self.search_options_button_clicked) def focus_search_box(self, *args): self.search.setFocus(Qt.OtherFocusReason) @@ -402,14 +399,13 @@ class SearchBoxMixin(object): # {{{ self.search.do_search() self.focus_to_library() + def search_options_button_clicked(self): + self.iactions['Preferences'].do_config(initial_plugin=('Interface', + 'Search'), close_after_initial=True) + def focus_to_library(self): self.current_view().setFocus(Qt.OtherFocusReason) - def highlight_only_changed(self, toWhat): - dynamic.set('search_highlight_only', toWhat) - self.current_view().model().set_highlight_only(toWhat) - self.focus_to_library() - # }}} class SavedSearchBoxMixin(object): # {{{ diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index fd3530d333..79199c6881 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -1214,7 +1214,7 @@ class TagBrowserMixin(object): # {{{ db.field_metadata.remove_user_categories() for k in d.categories: db.field_metadata.add_user_category('@' + k, k) - db.data.sqp_change_locations(db.field_metadata.get_search_terms()) + db.data.change_search_locations(db.field_metadata.get_search_terms()) self.tags_view.set_new_model() self.tags_view.recount() diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 907dd577b8..5ac7e6a45d 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -483,8 +483,10 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ action.location_selected(location) if location == 'library': self.search_restriction.setEnabled(True) + self.search_options_button.setEnabled(True) else: self.search_restriction.setEnabled(False) + self.search_options_button.setEnabled(False) # Reset the view in case something changed while it was invisible self.current_view().reset() self.set_number_of_books_shown() diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index e818e6a3c0..1330d10e59 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -11,7 +11,7 @@ from itertools import repeat from datetime import timedelta from threading import Thread -from calibre.utils.config import tweaks +from calibre.utils.config import tweaks, prefs from calibre.utils.date import parse_date, now, UNDEFINED_DATE from calibre.utils.search_query_parser import SearchQueryParser from calibre.utils.pyparsing import ParseException @@ -182,15 +182,16 @@ class ResultCache(SearchQueryParser): # {{{ self.first_sort = True self.search_restriction = '' self.field_metadata = field_metadata - all_search_locations = field_metadata.get_search_terms() - SearchQueryParser.__init__(self, all_search_locations, optimize=True) + self.all_search_locations = field_metadata.get_search_terms() + SearchQueryParser.__init__(self, self.all_search_locations, optimize=True) self.build_date_relop_dict() self.build_numeric_relop_dict() def break_cycles(self): self._data = self.field_metadata = self.FIELD_MAP = \ self.numeric_search_relops = self.date_search_relops = \ - self.db_prefs = None + self.db_prefs = self.all_search_locations = None + self.sqp_change_locations([]) def __getitem__(self, row): @@ -218,6 +219,10 @@ class ResultCache(SearchQueryParser): # {{{ def universal_set(self): return set([i[0] for i in self._data if i is not None]) + def change_search_locations(self, locations): + self.sqp_change_locations(locations) + self.all_search_locations = locations + def build_date_relop_dict(self): ''' Because the database dates have time in them, we can't use direct @@ -432,6 +437,7 @@ class ResultCache(SearchQueryParser): # {{{ # get metadata key associated with the search term. Eliminates # dealing with plurals and other aliases location = self.field_metadata.search_term_to_field_key(icu_lower(location.strip())) + # grouped search terms if isinstance(location, list): if allow_recursion: for loc in location: @@ -440,6 +446,20 @@ class ResultCache(SearchQueryParser): # {{{ return matches raise ParseException(query, len(query), 'Recursive query group detected', self) + # apply the limit if appropriate + if location == 'all' and prefs['limit_search_columns'] and \ + prefs['limit_search_columns_to']: + terms = set([]) + for l in prefs['limit_search_columns_to']: + l = icu_lower(l.strip()) + if l and l != 'all' and l in self.all_search_locations: + terms.add(l) + if terms: + for l in terms: + matches |= self.get_matches(l, query, + candidates=candidates, allow_recursion=allow_recursion) + return matches + if location in self.field_metadata: fm = self.field_metadata[location] # take care of dates special case diff --git a/src/calibre/utils/config.py b/src/calibre/utils/config.py index 11c58f7769..a2ceaced68 100644 --- a/src/calibre/utils/config.py +++ b/src/calibre/utils/config.py @@ -728,6 +728,17 @@ def _prefs(): c.add_opt('user_categories', default={}, help=_('User-created tag browser categories')) c.add_opt('manage_device_metadata', default='manual', help=_('How and when calibre updates metadata on the device.')) + c.add_opt('limit_search_columns', default=False, + help=_('When searching for text without using lookup ' + 'prefixes, as for example, Red instead of title:Red, ' + 'limit the columns searched to those named below.')) + c.add_opt('limit_search_columns_to', + default=['title', 'authors', 'tags', 'series', 'publisher'], + help=_('Choose columns to be searched when not using prefixes, ' + 'as for example, when searching for Redd instead of ' + 'title:Red. Enter a list of search/lookup names ' + 'separated by commas. Only takes effect if you set the option ' + 'to limit search columns above.')) c.add_opt('migrated', default=False, help='For Internal use. Don\'t modify.') return c