Sync to trunk.

This commit is contained in:
John Schember 2011-02-08 17:47:27 -05:00
commit 5cb9505ee1
34 changed files with 460 additions and 121 deletions

View File

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

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2008, Lionel Bergeret <lbergeret at gmail.com>'
__copyright__ = '2008-2011, Lionel Bergeret <lbergeret at gmail.com>'
'''
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

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2008, Lionel Bergeret <lbergeret at gmail.com>'
__copyright__ = '2008-2011, Lionel Bergeret <lbergeret at gmail.com>'
'''
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

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2008, Lionel Bergeret <lbergeret at gmail.com>'
__copyright__ = '2008-2011, Lionel Bergeret <lbergeret at gmail.com>'
'''
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

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2008, Lionel Bergeret <lbergeret at gmail.com>'
__copyright__ = '2008-2011, Lionel Bergeret <lbergeret at gmail.com>'
'''
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 = [

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2008, Lionel Bergeret <lbergeret at gmail.com>'
__copyright__ = '2008-2011, Lionel Bergeret <lbergeret at gmail.com>'
'''
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&section=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

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2008, Lionel Bergeret <lbergeret at gmail.com>'
__copyright__ = '2008-2011, Lionel Bergeret <lbergeret at gmail.com>'
'''
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

View File

@ -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]

View File

@ -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']

View File

@ -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 '<!-- created by calibre\'s pdftohtml -->' in src[:1000]
def is_abbyy(self, src):
return '<meta name="generator" content="ABBYY FineReader' in src[:1000]
def chapter_head(self, match):
from calibre.utils.html2text import html2text
chap = match.group('chap')
@ -77,22 +81,23 @@ class HeuristicProcessor(object):
def insert_indent(self, match):
pstyle = match.group('formatting')
tag = match.group('tagtype')
span = match.group('span')
self.found_indents = self.found_indents + 1
if pstyle:
if pstyle.lower().find('style'):
if pstyle.lower().find('style') != -1:
pstyle = re.sub(r'"$', '; text-indent:3%"', pstyle)
else:
pstyle = pstyle+' style="text-indent:3%"'
if not span:
return '<p '+pstyle+'>'
return '<'+tag+' '+pstyle+'>'
else:
return '<p '+pstyle+'>'+span
return '<'+tag+' '+pstyle+'>'+span
else:
if not span:
return '<p style="text-indent:3%">'
return '<'+tag+' style="text-indent:3%">'
else:
return '<p style="text-indent:3%">'+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'<p(?P<formatting>[^>]*)>\s*(?P<span>(<span[^>]*>\s*)+)?\s*(\u00a0){2,}', re.IGNORECASE)
txtindent = re.compile(ur'<(?P<tagtype>p|div)(?P<formatting>[^>]*)>\s*(?P<span>(<span[^>]*>\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<linestart><p\sstyle="(?P<styles>[^\"]*?);?">)(?P<content>.*?)(?P<lineend></p>)|(?P<image><img[^>]*>))', re.IGNORECASE)
empty_paragraph = '\n<p> </p>\n'
self.in_blockquote = False
self.previous_was_paragraph = False
html = re.sub('</?a[^>]*>', '', html)
def check_paragraph(content):
content = re.sub('\s*</?span[^>]*>\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<blockquote>\n'
blockquote_close = '</blockquote>\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+'<p style="'+text_indent+text_align+
'">'+content+'</p>'+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+'<p style="'+text_indent+text_align+'">'+content+'</p>'+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 </p> 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')

View File

@ -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=[],

View File

@ -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)

View File

@ -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)

View File

@ -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__':

View File

@ -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:

View File

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

View File

@ -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):

View File

@ -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):

View File

@ -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()

View File

@ -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)

View File

@ -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(_("<p>Search the list of books by title, author, publisher, tags, comments, etc.<br><br>Words separated by spaces are ANDed"))
x.setToolTip(_("<p>Search the list of books by title, author, publisher, "
"tags, comments, etc.<br><br>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('<p>'+_('When searching, highlight matched books, instead '
'of restricting the book list to the matches.<p> 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): # {{{

View File

@ -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:

View File

@ -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:

View File

@ -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)

View File

@ -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()

View File

@ -124,23 +124,13 @@
</property>
</widget>
</item>
<item row="6" column="0">
<item row="6" column="0" colspan="2">
<widget class="QCheckBox" name="opt_separate_cover_flow">
<property name="text">
<string>Show cover &amp;browser in a separate window (needs restart)</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QCheckBox" name="opt_search_as_you_type">
<property name="text">
<string>Search as you type</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="7" column="0" colspan="2">
<layout class="QHBoxLayout">
<item>
@ -177,7 +167,7 @@ if you never want subcategories</string>
<item>
<widget class="QSpinBox" name="opt_tags_browser_collapse_at">
<property name="toolTip">
<string>If a Tag Browser category has more than this number of items, it is divided
<string>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.</string>
</property>
<property name="maximum">

View File

@ -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()

View File

@ -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 <kovid@kovidgoyal.net>'
__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')

View File

@ -0,0 +1,104 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>670</width>
<height>392</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QCheckBox" name="opt_search_as_you_type">
<property name="text">
<string>Search as you &amp;type</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="opt_highlight_search_matches">
<property name="text">
<string>&amp;Highlight search results instead of restricting the book list to the results</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>What to search by default</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="label">
<property name="text">
<string>When you enter a search term without a prefix, by default calibre will search all metadata for matches. For example, entering, &quot;asimov&quot; will search not just authors but title/tags/series/comments/etc. Use these options if you would like to change this behavior.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QCheckBox" name="opt_limit_search_columns">
<property name="text">
<string>&amp;Limit the searched metadata</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>&amp;Columns that non-prefixed searches are limited to:</string>
</property>
<property name="buddy">
<cstring>opt_limit_search_columns_to</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="MultiCompleteLineEdit" name="opt_limit_search_columns_to"/>
</item>
<item row="5" column="0" colspan="2">
<widget class="QLabel" name="label_3">
<property name="text">
<string>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 &quot;series:Foundation&quot; rather than just &quot;Foundation&quot; in a saved search</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="3" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>MultiCompleteLineEdit</class>
<extends>QLineEdit</extends>
<header>calibre/gui2.complete.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@ -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): # {{{

View File

@ -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()

View File

@ -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()

View File

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

View File

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