Merge from trunk

This commit is contained in:
Charles Haley 2011-02-08 18:41:42 +00:00
commit a3c28f669b
22 changed files with 399 additions and 182 deletions

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Lionel Bergeret <lbergeret at gmail.com>' __copyright__ = '2008-2011, Lionel Bergeret <lbergeret at gmail.com>'
''' '''
cinebel.be cinebel.be
''' '''
@ -14,14 +14,14 @@ class Cinebel(BasicNewsRecipe):
description = u'Cinema news from Belgium in French' description = u'Cinema news from Belgium in French'
publisher = u'cinebel.be' publisher = u'cinebel.be'
category = 'news, cinema, movie, Belgium' category = 'news, cinema, movie, Belgium'
oldest_article = 3 oldest_article = 15
encoding = 'utf8' language = 'fr'
language = 'fr_BE'
max_articles_per_feed = 20 max_articles_per_feed = 20
no_stylesheets = True no_stylesheets = True
use_embedded_content = False use_embedded_content = False
timefmt = ' [%d %b %Y]' timefmt = ' [%d %b %Y]'
filterDuplicates = True
keep_only_tags = [ keep_only_tags = [
dict(name = 'span', attrs = {'class': 'movieMainTitle'}) 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' ) ,(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): def get_cover_url(self):
cover_url = 'http://www.cinebel.be/portal/resources/common/logo_index.gif' cover_url = 'http://www.cinebel.be/portal/resources/common/logo_index.gif'
return cover_url return cover_url

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Lionel Bergeret <lbergeret at gmail.com>' __copyright__ = '2008-2011, Lionel Bergeret <lbergeret at gmail.com>'
''' '''
dhnet.be dhnet.be
''' '''
@ -16,7 +16,8 @@ class DHNetBe(BasicNewsRecipe):
publisher = u'dhnet.be' publisher = u'dhnet.be'
category = 'news, Belgium' category = 'news, Belgium'
oldest_article = 3 oldest_article = 3
language = 'fr_BE' language = 'fr'
masthead_url = 'http://www.dhnet.be/images/homepage_logo_dh.gif'
max_articles_per_feed = 20 max_articles_per_feed = 20
no_stylesheets = True no_stylesheets = True
@ -34,6 +35,13 @@ class DHNetBe(BasicNewsRecipe):
,(u'La Une Info' , u'http://www.dhnet.be/rss/dhinfos/' ) ,(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): 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') 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 return cover_url

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Lionel Bergeret <lbergeret at gmail.com>' __copyright__ = '2008-2011, Lionel Bergeret <lbergeret at gmail.com>'
''' '''
lalibre.be lalibre.be
''' '''
@ -16,18 +16,18 @@ class LaLibre(BasicNewsRecipe):
publisher = u'lalibre.be' publisher = u'lalibre.be'
category = 'news, Belgium' category = 'news, Belgium'
oldest_article = 3 oldest_article = 3
language = 'fr_BE' language = 'fr'
masthead_url = 'http://www.lalibre.be/img/logoLaLibre.gif'
max_articles_per_feed = 20 max_articles_per_feed = 20
no_stylesheets = True no_stylesheets = True
use_embedded_content = False use_embedded_content = False
timefmt = ' [%d %b %Y]' timefmt = ' [%d %b %Y]'
keep_only_tags = [ remove_tags_before = dict(name = 'div', attrs = {'class': 'extraMainContent'})
dict(name = 'div', attrs = {'id': 'articleHat'}) remove_tags_after = dict(name = 'div', attrs = {'id': 'articleText'})
,dict(name = 'p', attrs = {'id': 'publicationDate'})
,dict(name = 'div', attrs = {'id': 'articleText'}) remove_tags = [dict(name = 'div', attrs = {'id': 'strongArticleLinks'})]
]
feeds = [ feeds = [
(u'L\'actu' , u'http://www.lalibre.be/rss/?section=10' ) (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' ) ,(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): 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') 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 return cover_url

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Lionel Bergeret <lbergeret at gmail.com>' __copyright__ = '2008-2011, Lionel Bergeret <lbergeret at gmail.com>'
''' '''
lameuse.be lameuse.be
''' '''
@ -16,8 +16,8 @@ class LaMeuse(BasicNewsRecipe):
publisher = u'lameuse.be' publisher = u'lameuse.be'
category = 'news, Belgium' category = 'news, Belgium'
oldest_article = 3 oldest_article = 3
encoding = 'utf8' language = 'fr'
language = 'fr_BE' masthead_url = 'http://www.lameuse.be/images/SPV3/logo_header_LM.gif'
max_articles_per_feed = 20 max_articles_per_feed = 20
no_stylesheets = True no_stylesheets = True
@ -32,6 +32,11 @@ class LaMeuse(BasicNewsRecipe):
dict(name = 'div', attrs = {'class': 'sb-group'}) dict(name = 'div', attrs = {'class': 'sb-group'})
,dict(name = 'div', attrs = {'id': 'share'}) ,dict(name = 'div', attrs = {'id': 'share'})
,dict(name = 'div', attrs = {'id': 'commentaires'}) ,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 = [ feeds = [

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Lionel Bergeret <lbergeret at gmail.com>' __copyright__ = '2008-2011, Lionel Bergeret <lbergeret at gmail.com>'
''' '''
lavenir.net lavenir.net
''' '''
@ -15,8 +15,7 @@ class LAvenir(BasicNewsRecipe):
publisher = u'lavenir.net' publisher = u'lavenir.net'
category = 'news, Belgium' category = 'news, Belgium'
oldest_article = 3 oldest_article = 3
encoding = 'utf8' language = 'fr'
language = 'fr_BE'
max_articles_per_feed = 20 max_articles_per_feed = 20
no_stylesheets = True 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' ) ,(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): def get_cover_url(self):
cover_url = 'http://www.lavenir.net/extra/Static/journal/Pdf/1/UNE_Nationale.PDF' cover_url = 'http://www.lavenir.net/extra/Static/journal/Pdf/1/UNE_Nationale.PDF'
return cover_url return cover_url

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Lionel Bergeret <lbergeret at gmail.com>' __copyright__ = '2008-2011, Lionel Bergeret <lbergeret at gmail.com>'
''' '''
lesoir.be lesoir.be
''' '''
@ -16,7 +16,8 @@ class LeSoirBe(BasicNewsRecipe):
publisher = u'lesoir.be' publisher = u'lesoir.be'
category = 'news, Belgium' category = 'news, Belgium'
oldest_article = 3 oldest_article = 3
language = 'fr_BE' language = 'fr'
masthead_url = 'http://pdf.lesoir.be/pdf/images/SOIR//logo.gif'
max_articles_per_feed = 20 max_articles_per_feed = 20
no_stylesheets = True no_stylesheets = True

View File

@ -791,6 +791,17 @@ class Toolbar(PreferencesPlugin):
description = _('Customize the toolbars and context menus, changing which' description = _('Customize the toolbars and context menus, changing which'
' actions are available in each') ' 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): class InputOptions(PreferencesPlugin):
name = 'Input Options' name = 'Input Options'
icon = I('arrow-down.png') icon = I('arrow-down.png')
@ -941,7 +952,7 @@ class Misc(PreferencesPlugin):
config_widget = 'calibre.gui2.preferences.misc' config_widget = 'calibre.gui2.preferences.misc'
description = _('Miscellaneous advanced configuration') description = _('Miscellaneous advanced configuration')
plugins += [LookAndFeel, Behavior, Columns, Toolbar, InputOptions, plugins += [LookAndFeel, Behavior, Columns, Toolbar, Search, InputOptions,
CommonOptions, OutputOptions, Adding, Saving, Sending, Plugboard, CommonOptions, OutputOptions, Adding, Saving, Sending, Plugboard,
Email, Server, Plugins, Tweaks, Misc, TemplateFunctions] Email, Server, Plugins, Tweaks, Misc, TemplateFunctions]

View File

@ -11,6 +11,7 @@ from calibre.ebooks.conversion.preprocess import DocAnalysis, Dehyphenator
from calibre.utils.logging import default_log from calibre.utils.logging import default_log
from calibre.utils.wordcount import get_wordcount_obj from calibre.utils.wordcount import get_wordcount_obj
class HeuristicProcessor(object): class HeuristicProcessor(object):
def __init__(self, extra_opts=None, log=None): def __init__(self, extra_opts=None, log=None):
@ -40,6 +41,9 @@ class HeuristicProcessor(object):
def is_pdftohtml(self, src): def is_pdftohtml(self, src):
return '<!-- created by calibre\'s pdftohtml -->' in src[:1000] 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): def chapter_head(self, match):
from calibre.utils.html2text import html2text from calibre.utils.html2text import html2text
chap = match.group('chap') chap = match.group('chap')
@ -518,6 +522,111 @@ class HeuristicProcessor(object):
return scene_break 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): def __call__(self, html):
self.log.debug("********* Heuristic processing HTML *********") self.log.debug("********* Heuristic processing HTML *********")
@ -532,6 +641,10 @@ class HeuristicProcessor(object):
self.log.warn("flow is too short, not running heuristics") self.log.warn("flow is too short, not running heuristics")
return html 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 # Arrange line feeds and </p> tags so the line_length and no_markup functions work correctly
html = self.arrange_htm_line_endings(html) html = self.arrange_htm_line_endings(html)
#self.dump(html, 'after_arrange_line_endings') #self.dump(html, 'after_arrange_line_endings')

View File

@ -106,9 +106,13 @@ def _config():
'clicked')) 'clicked'))
c.add_opt('asked_library_thing_password', default=False, c.add_opt('asked_library_thing_password', default=False,
help='Asked library thing password at least once.') help='Asked library thing password at least once.')
c.add_opt('search_as_you_type', default=True, c.add_opt('search_as_you_type', default=False,
help='Start searching as you type. If this is disabled then search will ' help=_('Start searching as you type. If this is disabled then search will '
'only take place when the Enter or Return key is pressed.') '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=[], c.add_opt('save_to_disk_template_history', default=[],
help='Previously used Save to Disk templates') help='Previously used Save to Disk templates')
c.add_opt('send_to_device_template_history', default=[], 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.gui.addAction(self.p_action)
self.p_action.triggered.connect(self.move_backward) self.p_action.triggered.connect(self.move_backward)
def gui_layout_complete(self):
self.gui.search_options_button.setVisible(True)
def location_selected(self, loc): def location_selected(self, loc):
self.can_move = loc == 'library' self.can_move = loc == 'library'
try:
self.gui.search_options_button.setVisible(self.can_move)
except:
import traceback
traceback.print_exc()
def move_forward(self): def move_forward(self):
if self.can_move is None: if self.can_move is None:
self.can_move = self.gui.current_view() is self.gui.library_view self.can_move = self.gui.current_view() is self.gui.library_view
self.gui.search_options_button.setVisible(self.can_move)
if self.can_move: if self.can_move:
self.gui.current_view().move_highlighted_row(forward=True) self.gui.current_view().move_highlighted_row(forward=True)
@ -50,7 +41,6 @@ class NextMatchAction(InterfaceAction):
def move_backward(self): def move_backward(self):
if self.can_move is None: if self.can_move is None:
self.can_move = self.gui.current_view() is self.gui.library_view self.can_move = self.gui.current_view() is self.gui.library_view
self.gui.search_options_button.setVisible(self.can_move)
if self.can_move: if self.can_move:
self.gui.current_view().move_highlighted_row(forward=False) self.gui.current_view().move_highlighted_row(forward=False)

View File

@ -33,7 +33,8 @@ class PreferencesAction(InterfaceAction):
x.triggered.connect(self.do_config) 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(): if self.gui.job_manager.has_jobs():
d = error_dialog(self.gui, _('Cannot configure'), d = error_dialog(self.gui, _('Cannot configure'),
_('Cannot configure while there are running jobs.')) _('Cannot configure while there are running jobs.'))
@ -44,7 +45,8 @@ class PreferencesAction(InterfaceAction):
_('Cannot configure before calibre is restarted.')) _('Cannot configure before calibre is restarted.'))
d.exec_() d.exec_()
return 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.show()
d.run_wizard_requested.connect(self.gui.run_wizard, d.run_wizard_requested.connect(self.gui.run_wizard,
type=Qt.QueuedConnection) type=Qt.QueuedConnection)

View File

@ -19,7 +19,6 @@ from calibre.gui2.widgets import Splitter
from calibre.gui2.tag_view import TagBrowserWidget from calibre.gui2.tag_view import TagBrowserWidget
from calibre.gui2.book_details import BookDetails from calibre.gui2.book_details import BookDetails
from calibre.gui2.notify import get_notifier from calibre.gui2.notify import get_notifier
from calibre.utils.config import dynamic
_keep_refs = [] _keep_refs = []
@ -65,8 +64,7 @@ class LibraryViewMixin(object): # {{{
view.verticalHeader().sectionDoubleClicked.connect(self.iactions['View'].view_specific_book) view.verticalHeader().sectionDoubleClicked.connect(self.iactions['View'].view_specific_book)
self.build_context_menus() self.build_context_menus()
highlight_cbox=dynamic.get('search_highlight_only', False) self.library_view.model().set_highlight_only(config['highlight_search_matches'])
self.library_view.model().set_highlight_only(highlight_cbox)
def build_context_menus(self): def build_context_menus(self):
lm = QMenu(self) lm = QMenu(self)

View File

@ -7,8 +7,8 @@ __docformat__ = 'restructuredtext en'
from functools import partial from functools import partial
from PyQt4.Qt import QIcon, Qt, QWidget, QToolBar, QSize, QDialogButtonBox, \ from PyQt4.Qt import QIcon, Qt, QWidget, QToolBar, QSize, \
pyqtSignal, QToolButton, QMenu, QCheckBox, QDialog, QGridLayout, QFrame, \ pyqtSignal, QToolButton, QMenu, \
QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup
@ -17,7 +17,6 @@ from calibre.gui2.search_box import SearchBox2, SavedSearchBox
from calibre.gui2.throbber import ThrobbingButton from calibre.gui2.throbber import ThrobbingButton
from calibre.gui2 import gprefs from calibre.gui2 import gprefs
from calibre.gui2.widgets import ComboBoxWithHelp from calibre.gui2.widgets import ComboBoxWithHelp
from calibre.gui2.complete import MultiCompleteLineEdit
from calibre import human_readable from calibre import human_readable
class LocationManager(QObject): # {{{ class LocationManager(QObject): # {{{
@ -201,8 +200,7 @@ class SearchBar(QWidget): # {{{
x.setIcon(QIcon(I('config.png'))) x.setIcon(QIcon(I('config.png')))
x.setObjectName("search_option_button") x.setObjectName("search_option_button")
l.addWidget(x) l.addWidget(x)
x.setToolTip(_("Change search highlighting and column limit options")) x.setToolTip(_("Change the way searching for books works"))
x.setVisible(False)
x = parent.saved_search = SavedSearchBox(self) x = parent.saved_search = SavedSearchBox(self)
x.setMaximumSize(QSize(150, 16777215)) x.setMaximumSize(QSize(150, 16777215))
@ -229,90 +227,6 @@ class SearchBar(QWidget): # {{{
x.setToolTip(_("Delete current saved search")) x.setToolTip(_("Delete current saved search"))
class SearchOptions(QDialog):
def __init__(self, parent, limit_to_fields, limit_field_list,
limit_cbox, highlight_cbox):
QDialog.__init__(self, parent=parent)
self.setWindowTitle(_('Search options'))
l = QGridLayout()
self.setLayout(l)
x = QLabel('<p>'+_('Use this box to change search options related to how '
'results are displayed and which columns are searched. '
'Changes will be remembered across calibre restarts. '
'When you press OK, the last search will be redone using '
'the new option values.')+'<p>'+_('Note: the limit option '
'below affects all searches, including saved searches '
'and, by extension, search restrictions. For this reason '
'it is usually better to use prefixes in saved searches, '
'for example series:someword instead of simply someword.'),
parent=self)
x.setWordWrap(True)
l.addWidget(x, 0, 0, 1, 2)
line = QFrame(self)
line.setFrameShape(QFrame.HLine)
line.setFrameShadow(QFrame.Sunken)
l.addWidget(line, 1, 0, 1, 2)
x = self.search_highlight_only = QCheckBox(self)
x.setToolTip('<p>'+_('When searching, show all books with search results '
'highlight instead of showing only the matches.<p> You can use the '
'N or F3 keys to go to the next match.'))
x.setChecked(highlight_cbox)
l.addWidget(x, 2, 1, 1, 1)
x = QLabel(_('Check this box if you want to see all books with search '
'results &highlighted instead of only the matched books'),
parent=self)
x.setBuddy(self.search_highlight_only)
l.addWidget(x, 2, 0, 1, 1)
x = self.search_limit_checkbox = QCheckBox(self)
x.setToolTip('<p>'+_('When searching for text without using lookup '
'prefixes, as for example someword instead of title:someword, '
'limit the columns searched to those named in the text box below.'))
x.setChecked(limit_cbox)
l.addWidget(x, 3, 1, 1, 1)
x = QLabel(_('Check this box if you want non-&prefixed searches to be '
'limited to certain columns/lookup names'), parent=self)
x.setBuddy(self.search_limit_checkbox)
l.addWidget(x, 3, 0, 1, 1)
x = self.search_box_limit_to = MultiCompleteLineEdit(parent=self)
x.setToolTip(_('Choose columns to be searched when not using prefixes, '
'as for example when searching for someword instead of '
'title:someword. Enter a list of search/lookup names '
'separated by commas. You must check the Limit box '
'above for this option to take effect.'))
x.setMinimumWidth(200)
x.set_separator(',')
x.update_items_cache(limit_field_list)
x.setText(limit_to_fields)
l.addWidget(x, 4, 1, 1, 1)
x = QLabel(_('Enter the list of &columns that non-prefixed searches '
'are limited to'), parent=self)
x.setBuddy(self.search_box_limit_to)
l.addWidget(x, 4, 0, 1, 1)
buttons = QDialogButtonBox()
buttons.addButton(QDialogButtonBox.Ok)
buttons.addButton(QDialogButtonBox.Cancel)
l.addWidget(buttons, 5, 0, 1, 2)
buttons.accepted.connect(self.search_options_accepted)
buttons.rejected.connect(self.search_options_rejected)
def search_options_accepted(self):
QDialog.accept(self)
def search_options_rejected(self):
QDialog.reject(self)
def values(self):
return (unicode(self.search_box_limit_to.text()),
bool(self.search_limit_checkbox.checkState()),
bool(self.search_highlight_only.checkState()))
# }}} # }}}
class Spacer(QWidget): # {{{ class Spacer(QWidget): # {{{

View File

@ -46,7 +46,6 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
r('disable_tray_notification', config) r('disable_tray_notification', config)
r('use_roman_numerals_for_series_number', config) r('use_roman_numerals_for_series_number', config)
r('separate_cover_flow', config, restart_required=True) r('separate_cover_flow', config, restart_required=True)
r('search_as_you_type', config)
r('show_child_bar', gprefs) r('show_child_bar', gprefs)
choices = [(_('Small'), 'small'), (_('Medium'), 'medium'), choices = [(_('Small'), 'small'), (_('Medium'), 'medium'),
@ -116,7 +115,6 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
def refresh_gui(self, gui): def refresh_gui(self, gui):
gui.search.search_as_you_type(config['search_as_you_type'])
self.update_font_display() self.update_font_display()
gui.tags_view.reread_collapse_parameters() gui.tags_view.reread_collapse_parameters()

View File

@ -124,23 +124,13 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="6" column="0"> <item row="6" column="0" colspan="2">
<widget class="QCheckBox" name="opt_separate_cover_flow"> <widget class="QCheckBox" name="opt_separate_cover_flow">
<property name="text"> <property name="text">
<string>Show cover &amp;browser in a separate window (needs restart)</string> <string>Show cover &amp;browser in a separate window (needs restart)</string>
</property> </property>
</widget> </widget>
</item> </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"> <item row="7" column="0" colspan="2">
<layout class="QHBoxLayout"> <layout class="QHBoxLayout">
<item> <item>

View File

@ -157,11 +157,12 @@ class Preferences(QMainWindow):
run_wizard_requested = pyqtSignal() 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) QMainWindow.__init__(self, gui)
self.gui = gui self.gui = gui
self.must_restart = False self.must_restart = False
self.committed = False self.committed = False
self.close_after_initial = close_after_initial
self.resize(900, 720) self.resize(900, 720)
nh, nw = min_available_height()-25, available_width()-10 nh, nw = min_available_height()-25, available_width()-10
@ -306,7 +307,7 @@ class Preferences(QMainWindow):
def esc(self, *args): def esc(self, *args):
if self.stack.currentIndex() == 1: if self.stack.currentIndex() == 1:
self.hide_plugin() self.cancel()
elif self.stack.currentIndex() == 0: elif self.stack.currentIndex() == 0:
self.close() self.close()
@ -331,11 +332,14 @@ class Preferences(QMainWindow):
show_copy_button=False) show_copy_button=False)
self.showing_widget.refresh_gui(self.gui) self.showing_widget.refresh_gui(self.gui)
self.hide_plugin() self.hide_plugin()
if must_restart and rc: if self.close_after_initial or (must_restart and rc):
self.close() self.close()
def cancel(self, *args): def cancel(self, *args):
if self.close_after_initial:
self.close()
else:
self.hide_plugin() self.hide_plugin()
def restore_defaults(self, *args): def restore_defaults(self, *args):

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,130 @@
<?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="2" 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="3" 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="3" 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>
<item row="1" column="0" colspan="2">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="4" column="0" colspan="2">
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</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>20</width>
<height>40</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.confirm_delete import confirm
from calibre.gui2.dialogs.saved_search_editor import SavedSearchEditor from calibre.gui2.dialogs.saved_search_editor import SavedSearchEditor
from calibre.gui2.dialogs.search import SearchDialog from calibre.gui2.dialogs.search import SearchDialog
from calibre.utils.config import dynamic, prefs
from calibre.utils.search_query_parser import saved_searches from calibre.utils.search_query_parser import saved_searches
from calibre.utils.icu import sort_key from calibre.utils.icu import sort_key
@ -401,24 +400,8 @@ class SearchBoxMixin(object): # {{{
self.focus_to_library() self.focus_to_library()
def search_options_button_clicked(self): def search_options_button_clicked(self):
from calibre.gui2.layout import SearchOptions self.iactions['Preferences'].do_config(initial_plugin=('Interface',
'Search'), close_after_initial=True)
fm = self.library_view.model().db.field_metadata
ll = fm.get_search_terms()
ll = [l for l in ll if not l.startswith('@') and l not in fm.search_items]
options_box = SearchOptions(parent=self,
limit_to_fields=prefs['search_box_limit_to'],
limit_field_list=ll,
limit_cbox=prefs['use_search_box_limit'],
highlight_cbox=dynamic.get('search_highlight_only', False))
r = options_box.exec_()
if r:
limit_list, limit_cbox, highlight_cbox = options_box.values()
prefs['search_box_limit_to'] = limit_list
prefs['use_search_box_limit'] = limit_cbox
dynamic.set('search_highlight_only', highlight_cbox)
self.current_view().model().set_highlight_only(highlight_cbox)
self.search.do_search()
def focus_to_library(self): def focus_to_library(self):
self.current_view().setFocus(Qt.OtherFocusReason) self.current_view().setFocus(Qt.OtherFocusReason)

View File

@ -483,8 +483,10 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
action.location_selected(location) action.location_selected(location)
if location == 'library': if location == 'library':
self.search_restriction.setEnabled(True) self.search_restriction.setEnabled(True)
self.search_options_button.setEnabled(True)
else: else:
self.search_restriction.setEnabled(False) self.search_restriction.setEnabled(False)
self.search_options_button.setEnabled(False)
# Reset the view in case something changed while it was invisible # Reset the view in case something changed while it was invisible
self.current_view().reset() self.current_view().reset()
self.set_number_of_books_shown() self.set_number_of_books_shown()

View File

@ -447,15 +447,15 @@ class ResultCache(SearchQueryParser): # {{{
raise ParseException(query, len(query), 'Recursive query group detected', self) raise ParseException(query, len(query), 'Recursive query group detected', self)
# apply the limit if appropriate # apply the limit if appropriate
if location == 'all' and prefs['use_search_box_limit'] and \ if location == 'all' and prefs['limit_search_columns'] and \
prefs['search_box_limit_to']: prefs['limit_search_columns_to']:
for l in prefs['search_box_limit_to'].split(','): terms = set([])
for l in prefs['limit_search_columns_to']:
l = icu_lower(l.strip()) l = icu_lower(l.strip())
if not l or l == 'all': if l and l != 'all' and l in self.all_search_locations:
continue terms.add(l)
if l not in self.all_search_locations: if terms:
raise ParseException(l, len(l), for l in terms:
'Unknown field "%s" in search column limit'%l, self)
matches |= self.get_matches(l, query, matches |= self.get_matches(l, query,
candidates=candidates, allow_recursion=allow_recursion) candidates=candidates, allow_recursion=allow_recursion)
return matches return matches

View File

@ -728,11 +728,17 @@ def _prefs():
c.add_opt('user_categories', default={}, help=_('User-created tag browser categories')) c.add_opt('user_categories', default={}, help=_('User-created tag browser categories'))
c.add_opt('manage_device_metadata', default='manual', c.add_opt('manage_device_metadata', default='manual',
help=_('How and when calibre updates metadata on the device.')) help=_('How and when calibre updates metadata on the device.'))
c.add_opt('limit_search_columns', default=False,
c.add_opt('search_box_limit_to', default='title, authors, series', help=_('When searching for text without using lookup '
help=_('Comma-separated list of fields to search when no prefix')) 'prefixes, as for example, Red instead of title:Red, '
c.add_opt('use_search_box_limit', default=False, 'limit the columns searched to those named below.'))
help=_('Set to true to apply the search box limit')) 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.') c.add_opt('migrated', default=False, help='For Internal use. Don\'t modify.')
return c return c