mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Merge from trunk
This commit is contained in:
commit
a3c28f669b
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 = [
|
||||
|
@ -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§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
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
|
||||
|
@ -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')
|
||||
@ -518,6 +522,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 +641,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')
|
||||
|
@ -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=[],
|
||||
|
@ -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_options_button.setVisible(True)
|
||||
|
||||
def location_selected(self, loc):
|
||||
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):
|
||||
if self.can_move is None:
|
||||
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:
|
||||
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_options_button.setVisible(self.can_move)
|
||||
|
||||
if self.can_move:
|
||||
self.gui.current_view().move_highlighted_row(forward=False)
|
||||
|
@ -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)
|
||||
|
@ -19,7 +19,6 @@ from calibre.gui2.widgets import Splitter
|
||||
from calibre.gui2.tag_view import TagBrowserWidget
|
||||
from calibre.gui2.book_details import BookDetails
|
||||
from calibre.gui2.notify import get_notifier
|
||||
from calibre.utils.config import dynamic
|
||||
|
||||
_keep_refs = []
|
||||
|
||||
@ -65,8 +64,7 @@ class LibraryViewMixin(object): # {{{
|
||||
view.verticalHeader().sectionDoubleClicked.connect(self.iactions['View'].view_specific_book)
|
||||
|
||||
self.build_context_menus()
|
||||
highlight_cbox=dynamic.get('search_highlight_only', False)
|
||||
self.library_view.model().set_highlight_only(highlight_cbox)
|
||||
self.library_view.model().set_highlight_only(config['highlight_search_matches'])
|
||||
|
||||
def build_context_menus(self):
|
||||
lm = QMenu(self)
|
||||
|
@ -7,8 +7,8 @@ __docformat__ = 'restructuredtext en'
|
||||
|
||||
from functools import partial
|
||||
|
||||
from PyQt4.Qt import QIcon, Qt, QWidget, QToolBar, QSize, QDialogButtonBox, \
|
||||
pyqtSignal, QToolButton, QMenu, QCheckBox, QDialog, QGridLayout, QFrame, \
|
||||
from PyQt4.Qt import QIcon, Qt, QWidget, QToolBar, QSize, \
|
||||
pyqtSignal, QToolButton, QMenu, \
|
||||
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 import gprefs
|
||||
from calibre.gui2.widgets import ComboBoxWithHelp
|
||||
from calibre.gui2.complete import MultiCompleteLineEdit
|
||||
from calibre import human_readable
|
||||
|
||||
class LocationManager(QObject): # {{{
|
||||
@ -201,8 +200,7 @@ class SearchBar(QWidget): # {{{
|
||||
x.setIcon(QIcon(I('config.png')))
|
||||
x.setObjectName("search_option_button")
|
||||
l.addWidget(x)
|
||||
x.setToolTip(_("Change search highlighting and column limit options"))
|
||||
x.setVisible(False)
|
||||
x.setToolTip(_("Change the way searching for books works"))
|
||||
|
||||
x = parent.saved_search = SavedSearchBox(self)
|
||||
x.setMaximumSize(QSize(150, 16777215))
|
||||
@ -229,90 +227,6 @@ class SearchBar(QWidget): # {{{
|
||||
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): # {{{
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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 &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>
|
||||
|
@ -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,11 +332,14 @@ 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):
|
||||
if self.close_after_initial:
|
||||
self.close()
|
||||
else:
|
||||
self.hide_plugin()
|
||||
|
||||
def restore_defaults(self, *args):
|
||||
|
38
src/calibre/gui2/preferences/search.py
Normal file
38
src/calibre/gui2/preferences/search.py
Normal 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')
|
||||
|
130
src/calibre/gui2/preferences/search.ui
Normal file
130
src/calibre/gui2/preferences/search.ui
Normal 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 &type</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QCheckBox" name="opt_highlight_search_matches">
|
||||
<property name="text">
|
||||
<string>&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, "asimov" 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>&Limit the searched metadata</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>&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 "series:Foundation" rather than just "Foundation" 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>
|
@ -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, prefs
|
||||
from calibre.utils.search_query_parser import saved_searches
|
||||
from calibre.utils.icu import sort_key
|
||||
|
||||
@ -401,24 +400,8 @@ class SearchBoxMixin(object): # {{{
|
||||
self.focus_to_library()
|
||||
|
||||
def search_options_button_clicked(self):
|
||||
from calibre.gui2.layout import SearchOptions
|
||||
|
||||
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()
|
||||
self.iactions['Preferences'].do_config(initial_plugin=('Interface',
|
||||
'Search'), close_after_initial=True)
|
||||
|
||||
def focus_to_library(self):
|
||||
self.current_view().setFocus(Qt.OtherFocusReason)
|
||||
|
@ -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()
|
||||
|
@ -447,15 +447,15 @@ class ResultCache(SearchQueryParser): # {{{
|
||||
raise ParseException(query, len(query), 'Recursive query group detected', self)
|
||||
|
||||
# apply the limit if appropriate
|
||||
if location == 'all' and prefs['use_search_box_limit'] and \
|
||||
prefs['search_box_limit_to']:
|
||||
for l in prefs['search_box_limit_to'].split(','):
|
||||
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 not l or l == 'all':
|
||||
continue
|
||||
if l not in self.all_search_locations:
|
||||
raise ParseException(l, len(l),
|
||||
'Unknown field "%s" in search column limit'%l, self)
|
||||
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
|
||||
|
@ -728,11 +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('search_box_limit_to', default='title, authors, series',
|
||||
help=_('Comma-separated list of fields to search when no prefix'))
|
||||
c.add_opt('use_search_box_limit', default=False,
|
||||
help=_('Set to true to apply the search box limit'))
|
||||
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
|
||||
|
Loading…
x
Reference in New Issue
Block a user