diff --git a/src/calibre/customize/profiles.py b/src/calibre/customize/profiles.py
index af2694ccff..69afd77a24 100644
--- a/src/calibre/customize/profiles.py
+++ b/src/calibre/customize/profiles.py
@@ -110,6 +110,18 @@ class CybookG3Input(InputProfile):
fbase = 16
fsizes = [12, 12, 14, 16, 18, 20, 22, 24]
+class CybookOpusInput(InputProfile):
+
+ name = 'Cybook Opus'
+ short_name = 'cybook_opus'
+ description = _('This profile is intended for the Cybook Opus.')
+
+ # Screen size is a best guess
+ screen_size = (600, 800)
+ dpi = 200
+ fbase = 16
+ fsizes = [12, 12, 14, 16, 18, 20, 22, 24]
+
class KindleInput(InputProfile):
name = 'Kindle'
@@ -222,6 +234,18 @@ class CybookG3Output(OutputProfile):
fbase = 16
fsizes = [12, 12, 14, 16, 18, 20, 22, 24]
+class CybookOpusOutput(OutputProfile):
+
+ name = 'Cybook Opus'
+ short_name = 'cybook_opus'
+ description = _('This profile is intended for the Cybook Opus.')
+
+ # Screen size is a best guess
+ screen_size = (600, 800)
+ dpi = 200
+ fbase = 16
+ fsizes = [12, 12, 14, 16, 18, 20, 22, 24]
+
class KindleOutput(OutputProfile):
name = 'Kindle'
diff --git a/src/calibre/customize/ui.py b/src/calibre/customize/ui.py
index 78afa3be15..04f9b80529 100644
--- a/src/calibre/customize/ui.py
+++ b/src/calibre/customize/ui.py
@@ -276,6 +276,13 @@ def plugin_for_input_format(fmt):
if fmt.lower() in plugin.file_types:
return plugin
+def all_input_formats():
+ formats = set([])
+ for plugin in input_format_plugins():
+ for format in plugin.file_types:
+ formats.add(format)
+ return formats
+
def available_input_formats():
formats = set([])
for plugin in input_format_plugins():
diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py
index b2e2958ec0..3c52ec2d7b 100644
--- a/src/calibre/ebooks/conversion/plumber.py
+++ b/src/calibre/ebooks/conversion/plumber.py
@@ -7,7 +7,8 @@ import os, re, sys
from calibre.customize.conversion import OptionRecommendation, DummyReporter
from calibre.customize.ui import input_profiles, output_profiles, \
- plugin_for_input_format, plugin_for_output_format
+ plugin_for_input_format, plugin_for_output_format, \
+ available_input_formats, available_output_formats
from calibre.ebooks.conversion.preprocess import HTMLPreProcessor
from calibre.ptempfile import PersistentTemporaryDirectory
from calibre import extract, walk
@@ -19,10 +20,6 @@ def supported_input_formats():
fmts.add(x)
return fmts
-INPUT_FORMAT_PREFERENCES = ['cbr', 'cbz', 'cbc', 'lit', 'mobi', 'prc', 'azw', 'fb2', 'html',
- 'rtf', 'pdf', 'txt', 'pdb']
-OUTPUT_FORMAT_PREFERENCES = ['epub', 'mobi', 'lit', 'pdf', 'pdb', 'txt']
-
class OptionValues(object):
pass
@@ -50,7 +47,7 @@ class Plumber(object):
'tags', 'book_producer', 'language'
]
- def __init__(self, input, output, log, report_progress=DummyReporter()):
+ def __init__(self, input, output, log, report_progress=DummyReporter(), dummy=False):
'''
:param input: Path to input file.
:param output: Path to output file/directory
@@ -318,6 +315,31 @@ OptionRecommendation(name='preprocess_html',
)
),
+OptionRecommendation(name='remove_header',
+ recommended_value=False, level=OptionRecommendation.LOW,
+ help=_('Use a regular expression to try and remove the header.'
+ )
+ ),
+
+OptionRecommendation(name='header_regex',
+ recommended_value='(?i)(?<=
)((\s*(()*
\s*)?\d+
\s*.*?\s*)|(\s*(()*
\s*)?.*?
\s*\d+))(?=
)',
+ level=OptionRecommendation.LOW,
+ help=_('The regular expression to use to remove the header.'
+ )
+ ),
+
+OptionRecommendation(name='remove_footer',
+ recommended_value=False, level=OptionRecommendation.LOW,
+ help=_('Use a regular expression to try and remove the footer.'
+ )
+ ),
+
+OptionRecommendation(name='footer_regex',
+ recommended_value='(?i)(?<=
)((\s*(()*
\s*)?\d+
\s*.*?\s*)|(\s*(()*
\s*)?.*?
\s*\d+))(?=
)',
+ level=OptionRecommendation.LOW,
+ help=_('The regular expression to use to remove the footer.'
+ )
+ ),
OptionRecommendation(name='read_metadata_from_opf',
recommended_value=None, level=OptionRecommendation.LOW,
@@ -419,12 +441,28 @@ OptionRecommendation(name='list_recipes',
self.input_fmt = input_fmt
self.output_fmt = output_fmt
+
+ self.all_format_options = set()
+ self.input_options = set()
+ self.output_options = set()
# Build set of all possible options. Two options are equal if their
# names are the same.
- self.input_options = self.input_plugin.options.union(
- self.input_plugin.common_options)
- self.output_options = self.output_plugin.options.union(
+ if not dummy:
+ self.input_options = self.input_plugin.options.union(
+ self.input_plugin.common_options)
+ self.output_options = self.output_plugin.options.union(
self.output_plugin.common_options)
+ else:
+ for fmt in available_input_formats():
+ input_plugin = plugin_for_input_format(fmt)
+ if input_plugin:
+ self.all_format_options = self.all_format_options.union(
+ input_plugin.options.union(input_plugin.common_options))
+ for fmt in available_output_formats():
+ output_plugin = plugin_for_output_format(fmt)
+ if output_plugin:
+ self.all_format_options = self.all_format_options.union(
+ output_plugin.options.union(output_plugin.common_options))
# Remove the options that have been disabled by recommendations from the
# plugins.
@@ -469,7 +507,7 @@ OptionRecommendation(name='list_recipes',
def get_option_by_name(self, name):
for group in (self.input_options, self.pipeline_options,
- self.output_options):
+ self.output_options, self.all_format_options):
for rec in group:
if rec.option == name:
return rec
@@ -535,7 +573,7 @@ OptionRecommendation(name='list_recipes',
'''
self.opts = OptionValues()
for group in (self.input_options, self.pipeline_options,
- self.output_options):
+ self.output_options, self.all_format_options):
for rec in group:
setattr(self.opts, rec.option.name, rec.recommended_value)
@@ -696,7 +734,7 @@ def create_oebbook(log, path_or_stream, opts, input_plugin, reader=None,
'''
from calibre.ebooks.oeb.base import OEBBook
html_preprocessor = HTMLPreProcessor(input_plugin.preprocess_html,
- opts.preprocess_html, getattr(opts, 'pdf_line_length', 0.5))
+ opts.preprocess_html, opts)
oeb = OEBBook(log, html_preprocessor,
pretty_print=opts.pretty_print, input_encoding=encoding)
if not populate:
diff --git a/src/calibre/ebooks/conversion/preprocess.py b/src/calibre/ebooks/conversion/preprocess.py
index 43bb52b8ad..69d6f1e511 100644
--- a/src/calibre/ebooks/conversion/preprocess.py
+++ b/src/calibre/ebooks/conversion/preprocess.py
@@ -140,8 +140,6 @@ class HTMLPreProcessor(object):
(re.compile(u'(?<=[\.,;\?!”"\'])[\s^ ]*(?=<)'), lambda match: ' '),
# Connect paragraphs split by -
(re.compile(u'(?<=[^\s][-–])[\s]*()*[\s]*()*\s*(?=[^\s])'), lambda match: ''),
- # Remove - that splits words
- (re.compile(u'(?<=[^\s])[-–]+(?=[^\s])'), lambda match: ''),
# Add space before and after italics
(re.compile(u'(?'), lambda match: ' '),
(re.compile(r'(?=\w)'), lambda match: ' '),
@@ -163,10 +161,10 @@ class HTMLPreProcessor(object):
lambda match : '
%s
'%(match.group(1),)),
]
def __init__(self, input_plugin_preprocess, plugin_preprocess,
- pdf_line_length):
+ extra_opts=None):
self.input_plugin_preprocess = input_plugin_preprocess
self.plugin_preprocess = plugin_preprocess
- self.pdf_line_length = pdf_line_length
+ self.extra_opts = extra_opts
def is_baen(self, src):
return re.compile(r'(i|b|u)>)?\s*()\s*(?=(<(i|b|u)>)?\s*[\w\d(])' % length, re.UNICODE), wrap_lines),
- ]
+ end_rules = []
+ if getattr(self.extra_opts, 'unwrap_factor', None):
+ length = line_length(html, getattr(self.extra_opts, 'unwrap_factor'))
+ if length:
+ end_rules.append(
+ # Un wrap using punctuation
+ (re.compile(r'(?<=.{%i}[a-z\.,;:)-IA])\s*(?P(i|b|u)>)?\s*()\s*(?=(<(i|b|u)>)?\s*[\w\d(])' % length, re.UNICODE), wrap_lines),
+ )
- rules = self.PDFTOHTML + line_length_rules
+ rules = self.PDFTOHTML + end_rules
else:
rules = []
- for rule in self.PREPROCESS + rules:
+
+ pre_rules = []
+ if getattr(self.extra_opts, 'remove_header', None):
+ pre_rules.append(
+ (re.compile(getattr(self.extra_opts, 'header_regex')), lambda match : '')
+ )
+ if getattr(self.extra_opts, 'remove_footer', None):
+ pre_rules.append(
+ (re.compile(getattr(self.extra_opts, 'footer_regex')), lambda match : '')
+ )
+
+ for rule in self.PREPROCESS + pre_rules + rules:
html = rule[0].sub(rule[1], html)
# Handle broken XHTML w/ SVG (ugh)
diff --git a/src/calibre/ebooks/oeb/transforms/guide.py b/src/calibre/ebooks/oeb/transforms/guide.py
index c1f0dd6669..0764dd650a 100644
--- a/src/calibre/ebooks/oeb/transforms/guide.py
+++ b/src/calibre/ebooks/oeb/transforms/guide.py
@@ -35,7 +35,7 @@ class Clean(object):
for x in list(self.oeb.guide):
href = urldefrag(self.oeb.guide[x].href)[0]
if x.lower() not in ('cover', 'titlepage', 'masthead', 'toc',
- 'title-page', 'copyright-page'):
+ 'title-page', 'copyright-page', 'start'):
self.oeb.guide.remove(x)
diff --git a/src/calibre/ebooks/pdf/input.py b/src/calibre/ebooks/pdf/input.py
index 3b82becc1f..58abbd635c 100644
--- a/src/calibre/ebooks/pdf/input.py
+++ b/src/calibre/ebooks/pdf/input.py
@@ -20,7 +20,7 @@ class PDFInput(InputFormatPlugin):
options = set([
OptionRecommendation(name='no_images', recommended_value=False,
help=_('Do not extract images from the document')),
- OptionRecommendation(name='pdf_line_length', recommended_value=0.5,
+ OptionRecommendation(name='unwrap_factor', recommended_value=0.5,
help=_('Scale used to determine the length at which a line should '
'be unwrapped. Valid values are a decimal between 0 and 1. The '
'default is 0.5, this is the median line length.')),
@@ -42,12 +42,7 @@ class PDFInput(InputFormatPlugin):
images = os.listdir(os.getcwd())
images.remove('index.html')
for i in images:
- # Remove the - from the file name because it causes problems.
- # The reference to the image with the - will be changed to not
- # include it later in the conversion process.
- new_i = i.replace('-', '')
- os.rename(i, new_i)
- manifest.append((new_i, None))
+ manifest.append((i, None))
log.debug('Generating manifest...')
opf.create_manifest(manifest)
diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py
index af4ca16eac..f5dcdcfebe 100644
--- a/src/calibre/gui2/__init__.py
+++ b/src/calibre/gui2/__init__.py
@@ -71,6 +71,9 @@ def _config():
help='Show donation button')
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.')
return ConfigProxy(c)
config = _config()
diff --git a/src/calibre/gui2/convert/bulk.py b/src/calibre/gui2/convert/bulk.py
index 0b48f2521b..393e005e5c 100644
--- a/src/calibre/gui2/convert/bulk.py
+++ b/src/calibre/gui2/convert/bulk.py
@@ -15,7 +15,8 @@ from calibre.gui2.convert.page_setup import PageSetupWidget
from calibre.gui2.convert.structure_detection import StructureDetectionWidget
from calibre.gui2.convert.toc import TOCWidget
from calibre.gui2.convert import GuiRecommendations
-from calibre.ebooks.conversion.plumber import Plumber, OUTPUT_FORMAT_PREFERENCES
+from calibre.ebooks.conversion.plumber import Plumber
+from calibre.utils.config import prefs
from calibre.utils.logging import Log
class BulkConfig(Config):
@@ -102,7 +103,7 @@ class BulkConfig(Config):
preferred_output_format = preferred_output_format if \
preferred_output_format and preferred_output_format \
in output_formats else sort_formats_by_preference(output_formats,
- OUTPUT_FORMAT_PREFERENCES)[0]
+ prefs['output_format'])[0]
self.output_formats.addItems(list(map(QString, [x.upper() for x in
output_formats])))
self.output_formats.setCurrentIndex(output_formats.index(preferred_output_format))
@@ -117,4 +118,3 @@ class BulkConfig(Config):
self._recommendations = recs
ResizableDialog.accept(self)
-
diff --git a/src/calibre/gui2/convert/metadata.py b/src/calibre/gui2/convert/metadata.py
index 01eb5bee1c..82e7b21148 100644
--- a/src/calibre/gui2/convert/metadata.py
+++ b/src/calibre/gui2/convert/metadata.py
@@ -35,21 +35,17 @@ class MetadataWidget(Widget, Ui_Form):
self.connect(self.cover_button, SIGNAL("clicked()"), self.select_cover)
def initialize_metadata_options(self):
- all_series = self.db.all_series()
- all_series.sort(cmp=lambda x, y : cmp(x[1], y[1]))
- for series in all_series:
- self.series.addItem(series[1])
- self.series.setCurrentIndex(-1)
+ self.initialize_combos()
mi = self.db.get_metadata(self.book_id, index_is_id=True)
self.title.setText(mi.title)
if mi.authors:
- self.author.setText(authors_to_string(mi.authors))
- else:
- self.author.setText('')
- self.publisher.setText(mi.publisher if mi.publisher else '')
+ self.author.setCurrentIndex(self.author.findText(authors_to_string(mi.authors)))
+ if mi.publisher:
+ self.publisher.setCurrentIndex(self.publisher.findText(mi.publisher))
self.author_sort.setText(mi.author_sort if mi.author_sort else '')
self.tags.setText(', '.join(mi.tags if mi.tags else []))
+ self.tags.update_tags_cache(self.db.all_tags())
self.comment.setText(mi.comments if mi.comments else '')
if mi.series:
self.series.setCurrentIndex(self.series.findText(mi.series))
@@ -66,6 +62,39 @@ class MetadataWidget(Widget, Ui_Form):
if not pm.isNull():
self.cover.setPixmap(pm)
+ def initialize_combos(self):
+ self.initalize_authors()
+ self.initialize_series()
+ self.initialize_publisher()
+
+ def initalize_authors(self):
+ all_authors = self.db.all_authors()
+ all_authors.sort(cmp=lambda x, y : cmp(x[1], y[1]))
+
+ for i in all_authors:
+ id, name = i
+ name = authors_to_string([name.strip().replace('|', ',') for n in name.split(',')])
+ self.author.addItem(name)
+ self.author.setCurrentIndex(-1)
+
+ def initialize_series(self):
+ all_series = self.db.all_series()
+ all_series.sort(cmp=lambda x, y : cmp(x[1], y[1]))
+
+ for i in all_series:
+ id, name = i
+ self.series.addItem(name)
+ self.series.setCurrentIndex(-1)
+
+ def initialize_publisher(self):
+ all_publishers = self.db.all_publishers()
+ all_publishers.sort(cmp=lambda x, y : cmp(x[1], y[1]))
+
+ for i in all_publishers:
+ id, name = i
+ self.publisher.addItem(name)
+ self.publisher.setCurrentIndex(-1)
+
def get_title_and_authors(self):
title = unicode(self.title.text()).strip()
if not title:
diff --git a/src/calibre/gui2/convert/metadata.ui b/src/calibre/gui2/convert/metadata.ui
index e4533de24c..3abe8ece55 100644
--- a/src/calibre/gui2/convert/metadata.ui
+++ b/src/calibre/gui2/convert/metadata.ui
@@ -143,19 +143,6 @@
- -
-
-
-
- 1
- 0
-
-
-
- Change the author(s) of this book. Multiple authors should be separated by an &. If the author name contains an &, use && to represent it.
-
-
-
-
@@ -195,13 +182,6 @@
- -
-
-
- Change the publisher of this book
-
-
-
-
@@ -216,7 +196,7 @@
-
-
+
Tags categorize the book. This is particularly useful while searching. <br><br>They can be any words or phrases, separated by commas.
@@ -276,6 +256,20 @@
+ -
+
+
+ true
+
+
+
+ -
+
+
+ true
+
+
+
-
@@ -329,11 +323,16 @@
QComboBox
+
+ TagsLineEdit
+ QLineEdit
+
+
-
+
diff --git a/src/calibre/gui2/convert/pdf_input.py b/src/calibre/gui2/convert/pdf_input.py
index 71e4bc0ef3..e4a9541823 100644
--- a/src/calibre/gui2/convert/pdf_input.py
+++ b/src/calibre/gui2/convert/pdf_input.py
@@ -14,6 +14,6 @@ class PluginWidget(Widget, Ui_Form):
def __init__(self, parent, get_option, get_help, db=None, book_id=None):
Widget.__init__(self, parent, 'pdf_input',
- ['no_images', 'pdf_line_length'])
+ ['no_images', 'unwrap_factor'])
self.db, self.book_id = db, book_id
self.initialize_options(get_option, get_help, db, book_id)
diff --git a/src/calibre/gui2/convert/pdf_input.ui b/src/calibre/gui2/convert/pdf_input.ui
index 35b840ded0..40f480b15d 100644
--- a/src/calibre/gui2/convert/pdf_input.ui
+++ b/src/calibre/gui2/convert/pdf_input.ui
@@ -14,14 +14,14 @@
Form
-
-
+
-
Line Un-Wrapping Factor:
- -
+
-
Qt::Vertical
@@ -34,8 +34,8 @@
- -
-
+
-
+
1.000000000000000
@@ -47,7 +47,7 @@
- -
+
-
No Images
diff --git a/src/calibre/gui2/convert/single.py b/src/calibre/gui2/convert/single.py
index ea839e6e80..b995c3e3a2 100644
--- a/src/calibre/gui2/convert/single.py
+++ b/src/calibre/gui2/convert/single.py
@@ -20,11 +20,10 @@ from calibre.gui2.convert.page_setup import PageSetupWidget
from calibre.gui2.convert.structure_detection import StructureDetectionWidget
from calibre.gui2.convert.toc import TOCWidget
-
-from calibre.ebooks.conversion.plumber import Plumber, supported_input_formats, \
- INPUT_FORMAT_PREFERENCES, OUTPUT_FORMAT_PREFERENCES
+from calibre.ebooks.conversion.plumber import Plumber, supported_input_formats
from calibre.customize.ui import available_output_formats
from calibre.customize.conversion import OptionRecommendation
+from calibre.utils.config import prefs
from calibre.utils.logging import Log
class NoSupportedInputFormats(Exception):
@@ -33,11 +32,11 @@ class NoSupportedInputFormats(Exception):
def sort_formats_by_preference(formats, prefs):
def fcmp(x, y):
try:
- x = prefs.index(x)
+ x = prefs.index(x.upper())
except ValueError:
x = sys.maxint
try:
- y = prefs.index(y)
+ y = prefs.index(y.upper())
except ValueError:
y = sys.maxint
return cmp(x, y)
@@ -206,11 +205,11 @@ class Config(ResizableDialog, Ui_Dialog):
preferred_input_format = preferred_input_format if \
preferred_input_format in input_formats else \
sort_formats_by_preference(input_formats,
- INPUT_FORMAT_PREFERENCES)[0]
+ prefs['input_format_order'])[0]
preferred_output_format = preferred_output_format if \
preferred_output_format in output_formats else \
sort_formats_by_preference(output_formats,
- OUTPUT_FORMAT_PREFERENCES)[0]
+ prefs['output_format'])[0]
self.input_formats.addItems(list(map(QString, [x.upper() for x in
input_formats])))
self.output_formats.addItems(list(map(QString, [x.upper() for x in
diff --git a/src/calibre/gui2/convert/structure_detection.py b/src/calibre/gui2/convert/structure_detection.py
index 66dff86aca..506ee37c45 100644
--- a/src/calibre/gui2/convert/structure_detection.py
+++ b/src/calibre/gui2/convert/structure_detection.py
@@ -6,6 +6,7 @@ __license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal '
__docformat__ = 'restructuredtext en'
+import re
from calibre.gui2.convert.structure_detection_ui import Ui_Form
from calibre.gui2.convert import Widget
@@ -23,7 +24,8 @@ class StructureDetectionWidget(Widget, Ui_Form):
['chapter', 'chapter_mark',
'remove_first_image',
'insert_metadata', 'page_breaks_before',
- 'preprocess_html']
+ 'preprocess_html', 'remove_header', 'header_regex',
+ 'remove_footer', 'footer_regex']
)
self.db, self.book_id = db, book_id
self.initialize_options(get_option, get_help, db, book_id)
@@ -31,8 +33,16 @@ class StructureDetectionWidget(Widget, Ui_Form):
self.opt_page_breaks_before.set_msg(_('Insert page breaks before '
'(XPath expression):'))
-
def pre_commit_check(self):
+ for x in ('header_regex', 'footer_regex'):
+ x = getattr(self, 'opt_'+x)
+ try:
+ pat = unicode(x.text())
+ re.compile(pat)
+ except Exception, err:
+ error_dialog(self, _('Invalid regular expression'),
+ _('Invalid regular expression: %s')%err).exec_()
+ return False
for x in ('chapter', 'page_breaks_before'):
x = getattr(self, 'opt_'+x)
if not x.check():
diff --git a/src/calibre/gui2/convert/structure_detection.ui b/src/calibre/gui2/convert/structure_detection.ui
index 768b430c5a..eebc0f0d53 100644
--- a/src/calibre/gui2/convert/structure_detection.ui
+++ b/src/calibre/gui2/convert/structure_detection.ui
@@ -14,6 +14,9 @@
Form
+
-
+
+
-
@@ -62,20 +65,27 @@
- -
+
-
+
+
+ &Footer regular expression:
+
+
+ opt_footer_regex
+
+
+
+ -
&Preprocess input file to possibly improve structure detection
- -
+
-
- -
-
-
- -
+
-
Qt::Vertical
@@ -88,6 +98,36 @@
+ -
+
+
+ &Header regular expression:
+
+
+ opt_header_regex
+
+
+
+ -
+
+
+ Remove F&ooter
+
+
+
+ -
+
+
+ Remove H&eader
+
+
+
+ -
+
+
+ -
+
+
diff --git a/src/calibre/gui2/dialogs/config.py b/src/calibre/gui2/dialogs/config.py
index 2b5e9d4cf2..52b2caeaea 100644
--- a/src/calibre/gui2/dialogs/config.py
+++ b/src/calibre/gui2/dialogs/config.py
@@ -22,7 +22,8 @@ from calibre.library import server_config
from calibre.customize.ui import initialized_plugins, is_disabled, enable_plugin, \
disable_plugin, customize_plugin, \
plugin_customization, add_plugin, \
- remove_plugin, input_format_plugins, \
+ remove_plugin, all_input_formats, \
+ input_format_plugins, \
output_format_plugins, available_output_formats
from calibre.utils.smtp import config as smtp_prefs
from calibre.gui2.convert.look_and_feel import LookAndFeelWidget
@@ -39,7 +40,7 @@ class ConfigTabs(QTabWidget):
log = Log()
log.outputs = []
- self.plumber = Plumber('dummt.epub', 'dummy.epub', log)
+ self.plumber = Plumber('dummy.epub', 'dummy.epub', log, dummy=True)
def widget_factory(cls):
return cls(self, self.plumber.get_option_by_name,
@@ -337,6 +338,18 @@ class ConfigDialog(QDialog, Ui_Dialog):
self.connect(self.browse_button, SIGNAL('clicked(bool)'), self.browse)
self.connect(self.compact_button, SIGNAL('clicked(bool)'), self.compact)
+ input_map = prefs['input_format_order']
+ all_formats = set()
+ for fmt in all_input_formats():
+ all_formats.add(fmt.upper())
+ for format in input_map + list(all_formats.difference(input_map)):
+ item = QListWidgetItem(format, self.input_order)
+ item.setData(Qt.UserRole, QVariant(format))
+ item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsSelectable)
+
+ self.connect(self.input_up, SIGNAL('clicked()'), self.up_input)
+ self.connect(self.input_down, SIGNAL('clicked()'), self.down_input)
+
dirs = config['frequently_used_directories']
rn = config['use_roman_numerals_for_series_number']
self.timeout.setValue(prefs['network_timeout'])
@@ -424,6 +437,7 @@ class ConfigDialog(QDialog, Ui_Dialog):
self.password.setText(opts.password if opts.password else '')
self.auto_launch.setChecked(config['autolaunch_server'])
self.systray_icon.setChecked(config['systray_icon'])
+ self.search_as_you_type.setChecked(config['search_as_you_type'])
self.sync_news.setChecked(config['upload_news_to_device'])
self.delete_news.setChecked(config['delete_news_from_library_on_upload'])
p = {'normal':0, 'high':1, 'low':2}[prefs['worker_process_priority']]
@@ -553,6 +567,17 @@ class ConfigDialog(QDialog, Ui_Dialog):
plugin.name + _(' cannot be removed. It is a '
'builtin plugin. Try disabling it instead.')).exec_()
+ def up_input(self):
+ idx = self.input_order.currentRow()
+ if idx > 0:
+ self.input_order.insertItem(idx-1, self.input_order.takeItem(idx))
+ self.input_order.setCurrentRow(idx-1)
+
+ def down_input(self):
+ idx = self.input_order.currentRow()
+ if idx < self.input_order.count()-1:
+ self.input_order.insertItem(idx+1, self.input_order.takeItem(idx))
+ self.input_order.setCurrentRow(idx+1)
def up_column(self):
idx = self.columns.currentRow()
@@ -656,6 +681,8 @@ class ConfigDialog(QDialog, Ui_Dialog):
config['new_version_notification'] = bool(self.new_version_notification.isChecked())
prefs['network_timeout'] = int(self.timeout.value())
path = qstring_to_unicode(self.location.text())
+ input_cols = [unicode(self.input_order.item(i).data(Qt.UserRole).toString()) for i in range(self.input_order.count())]
+ prefs['input_format_order'] = input_cols
cols = [unicode(self.columns.item(i).data(Qt.UserRole).toString()) for i in range(self.columns.count()) if self.columns.item(i).checkState()==Qt.Checked]
if not cols:
cols = ['title']
@@ -681,6 +708,7 @@ class ConfigDialog(QDialog, Ui_Dialog):
sc.set('max_cover', mcs)
config['delete_news_from_library_on_upload'] = self.delete_news.isChecked()
config['upload_news_to_device'] = self.sync_news.isChecked()
+ config['search_as_you_type'] = self.search_as_you_type.isChecked()
fmts = []
for i in range(self.viewer.count()):
if self.viewer.item(i).checkState() == Qt.Checked:
diff --git a/src/calibre/gui2/dialogs/config.ui b/src/calibre/gui2/dialogs/config.ui
index 90a53364ca..55e5ce0552 100644
--- a/src/calibre/gui2/dialogs/config.ui
+++ b/src/calibre/gui2/dialogs/config.ui
@@ -8,7 +8,7 @@
0
0
800
- 557
+ 583
@@ -232,6 +232,68 @@
+ -
+
+
+ Preferred &input format order:
+
+
+
-
+
+
-
+
+
+ true
+
+
+ QAbstractItemView::SelectRows
+
+
+
+ -
+
+
-
+
+
+ ...
+
+
+
+ :/images/arrow-up.svg:/images/arrow-up.svg
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+ -
+
+
+ ...
+
+
+
+ :/images/arrow-down.svg:/images/arrow-down.svg
+
+
+
+
+
+
+
+
+
+
-
@@ -364,6 +426,16 @@
+ -
+
+
+ Search as you type
+
+
+ true
+
+
+
-
@@ -529,15 +601,6 @@
- roman_numerals
- groupBox_2
- systray_icon
- sync_news
- delete_news
- separate_cover_flow
- systray_notifications
-
-
diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py
index 9b8810b3a4..622ea95a2b 100644
--- a/src/calibre/gui2/dialogs/metadata_bulk.py
+++ b/src/calibre/gui2/dialogs/metadata_bulk.py
@@ -9,7 +9,8 @@ from PyQt4.QtGui import QDialog
from calibre.gui2 import qstring_to_unicode
from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog
from calibre.gui2.dialogs.tag_editor import TagEditor
-from calibre.ebooks.metadata import string_to_authors, authors_to_sort_string
+from calibre.ebooks.metadata import string_to_authors, authors_to_sort_string, \
+ authors_to_string
class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
@@ -25,29 +26,63 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
QObject.connect(self.button_box, SIGNAL("accepted()"), self.sync)
QObject.connect(self.rating, SIGNAL('valueChanged(int)'), self.rating_changed)
- all_series = self.db.all_series()
+ self.tags.update_tags_cache(self.db.all_tags())
+ self.remove_tags.update_tags_cache(self.db.all_tags())
- for i in all_series:
- id, name = i
- self.series.addItem(name)
+ self.initialize_combos()
for f in self.db.all_formats():
self.remove_format.addItem(f)
self.remove_format.setCurrentIndex(-1)
- self.series.lineEdit().setText('')
QObject.connect(self.series, SIGNAL('currentIndexChanged(int)'), self.series_changed)
QObject.connect(self.series, SIGNAL('editTextChanged(QString)'), self.series_changed)
QObject.connect(self.tag_editor_button, SIGNAL('clicked()'), self.tag_editor)
+
self.exec_()
+ def initialize_combos(self):
+ self.initalize_authors()
+ self.initialize_series()
+ self.initialize_publisher()
+
+ def initalize_authors(self):
+ all_authors = self.db.all_authors()
+ all_authors.sort(cmp=lambda x, y : cmp(x[1], y[1]))
+
+ for i in all_authors:
+ id, name = i
+ name = authors_to_string([name.strip().replace('|', ',') for n in name.split(',')])
+ self.authors.addItem(name)
+ self.authors.setEditText('')
+
+ def initialize_series(self):
+ all_series = self.db.all_series()
+ all_series.sort(cmp=lambda x, y : cmp(x[1], y[1]))
+
+ for i in all_series:
+ id, name = i
+ self.series.addItem(name)
+ self.series.setEditText('')
+
+ def initialize_publisher(self):
+ all_publishers = self.db.all_publishers()
+ all_publishers.sort(cmp=lambda x, y : cmp(x[1], y[1]))
+
+ for i in all_publishers:
+ id, name = i
+ self.publisher.addItem(name)
+ self.publisher.setEditText('')
+
def tag_editor(self):
d = TagEditor(self, self.db, None)
d.exec_()
if d.result() == QDialog.Accepted:
tag_string = ', '.join(d.tags)
self.tags.setText(tag_string)
+ self.tags.update_tags_cache(self.db.all_tags())
+ self.remove_tags.update_tags_cache(self.db.all_tags())
def sync(self):
for id in self.ids:
diff --git a/src/calibre/gui2/dialogs/metadata_bulk.ui b/src/calibre/gui2/dialogs/metadata_bulk.ui
index beea2cace3..f2bcc3cd93 100644
--- a/src/calibre/gui2/dialogs/metadata_bulk.ui
+++ b/src/calibre/gui2/dialogs/metadata_bulk.ui
@@ -45,16 +45,6 @@
Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
- authors
-
-
-
- -
-
-
- Change the author(s) of this book. Multiple authors should be separated by an &. If the author name contains an &, use && to represent it.
-
-
@@ -65,9 +55,6 @@
Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
- authors
-
-
@@ -117,16 +104,6 @@
Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
- publisher
-
-
-
- -
-
-
- Change the publisher of this book
-
-
@@ -143,7 +120,7 @@
-
-
+
Tags categorize the book. This is particularly useful while searching. <br><br>They can be any words or phrases, separated by commas.
@@ -174,7 +151,7 @@
-
-
+
Comma separated list of tags to remove from the books.
@@ -235,6 +212,20 @@
+ -
+
+
+ true
+
+
+
+ -
+
+
+ true
+
+
+
@@ -265,6 +256,11 @@
QComboBox
+
+ TagsLineEdit
+ QLineEdit
+
+
diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py
index d25d0609c8..37252f17cd 100644
--- a/src/calibre/gui2/dialogs/metadata_single.py
+++ b/src/calibre/gui2/dialogs/metadata_single.py
@@ -13,7 +13,7 @@ import traceback
from datetime import datetime
from PyQt4.QtCore import SIGNAL, QObject, QCoreApplication, Qt, QTimer, QThread, QDate
-from PyQt4.QtGui import QPixmap, QListWidgetItem, QErrorMessage, QDialog, QCompleter
+from PyQt4.QtGui import QPixmap, QListWidgetItem, QErrorMessage, QDialog
from calibre.gui2 import qstring_to_unicode, error_dialog, file_icon_provider, \
choose_files, pixmap_to_data, choose_images, ResizableDialog
@@ -80,13 +80,6 @@ class Format(QListWidgetItem):
QListWidgetItem.__init__(self, file_icon_provider().icon_from_ext(ext),
text, parent, QListWidgetItem.UserType)
-class AuthorCompleter(QCompleter):
-
- def __init__(self, db):
- all_authors = db.all_authors()
- all_authors.sort(cmp=lambda x, y : cmp(x[1], y[1]))
- QCompleter.__init__(self, [x[1] for x in all_authors])
-
class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
COVER_FETCH_TIMEOUT = 240 # seconds
@@ -233,8 +226,6 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.cover_changed = False
self.cpixmap = None
self.cover.setAcceptDrops(True)
- self._author_completer = AuthorCompleter(self.db)
- self.authors.setCompleter(self._author_completer)
self.pubdate.setMinimumDate(QDate(100,1,1))
self.connect(self.cover, SIGNAL('cover_changed()'), self.cover_dropped)
QObject.connect(self.cover_button, SIGNAL("clicked(bool)"), \
@@ -265,16 +256,11 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
if not isbn:
isbn = ''
self.isbn.setText(isbn)
- au = self.db.authors(row)
- if au:
- au = [a.strip().replace('|', ',') for a in au.split(',')]
- self.authors.setText(authors_to_string(au))
- else:
- self.authors.setText('')
aus = self.db.author_sort(row)
self.author_sort.setText(aus if aus else '')
tags = self.db.tags(row)
- self.tags.setText(tags if tags else '')
+ self.tags.setText(', '.join(tags.split(',')) if tags else '')
+ self.tags.update_tags_cache(self.db.all_tags())
rating = self.db.rating(row)
if rating > 0:
self.rating.setValue(int(rating/2.))
@@ -295,7 +281,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
Format(self.formats, ext, size)
- self.initialize_series_and_publisher()
+ self.initialize_combos()
self.series_index.setValue(self.db.series_index(row))
QObject.connect(self.series, SIGNAL('currentIndexChanged(int)'), self.enable_series_index)
@@ -331,6 +317,30 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
def cover_dropped(self):
self.cover_changed = True
+ def initialize_combos(self):
+ self.initalize_authors()
+ self.initialize_series()
+ self.initialize_publisher()
+
+ self.layout().activate()
+
+ def initalize_authors(self):
+ all_authors = self.db.all_authors()
+ all_authors.sort(cmp=lambda x, y : cmp(x[1], y[1]))
+ author_id = self.db.author_id(self.row)
+ idx, c = None, 0
+ for i in all_authors:
+ id, name = i
+ if id == author_id:
+ idx = c
+ name = [name.strip().replace('|', ',') for n in name.split(',')]
+ self.authors.addItem(authors_to_string(name))
+ c += 1
+
+ self.authors.setEditText('')
+ if idx is not None:
+ self.authors.setCurrentIndex(idx)
+
def initialize_series(self):
self.series.setSizeAdjustPolicy(self.series.AdjustToContentsOnFirstShow)
all_series = self.db.all_series()
@@ -349,8 +359,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.series.setCurrentIndex(idx)
self.enable_series_index()
- def initialize_series_and_publisher(self):
- self.initialize_series()
+ def initialize_publisher(self):
all_publishers = self.db.all_publishers()
all_publishers.sort(cmp=lambda x, y : cmp(x[1], y[1]))
publisher_id = self.db.publisher_id(self.row)
@@ -366,15 +375,13 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
if idx is not None:
self.publisher.setCurrentIndex(idx)
-
- self.layout().activate()
-
def edit_tags(self):
d = TagEditor(self, self.db, self.row)
d.exec_()
if d.result() == QDialog.Accepted:
tag_string = ', '.join(d.tags)
self.tags.setText(tag_string)
+ self.tags.update_tags_cache(self.db.all_tags())
def fetch_cover(self):
isbn = unicode(self.isbn.text()).strip()
diff --git a/src/calibre/gui2/dialogs/metadata_single.ui b/src/calibre/gui2/dialogs/metadata_single.ui
index bbf1bb0f7b..14191f2851 100644
--- a/src/calibre/gui2/dialogs/metadata_single.ui
+++ b/src/calibre/gui2/dialogs/metadata_single.ui
@@ -121,9 +121,6 @@
Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
- authors
-
-
@@ -225,7 +222,7 @@
-
-
-
+
Tags categorize the book. This is particularly useful while searching. <br><br>They can be any words or phrases, separated by commas.
@@ -345,9 +342,6 @@
- -
-
-
-
@@ -371,6 +365,13 @@
+ -
+
+
+ true
+
+
+
@@ -651,11 +652,15 @@
QComboBox
+
+ TagsLineEdit
+ QLineEdit
+
+
title
swap_button
- authors
author_sort
auto_author_sort
rating
diff --git a/src/calibre/gui2/library.py b/src/calibre/gui2/library.py
index c1705cb420..c38af0cbf7 100644
--- a/src/calibre/gui2/library.py
+++ b/src/calibre/gui2/library.py
@@ -9,7 +9,8 @@ from math import cos, sin, pi
from PyQt4.QtGui import QTableView, QAbstractItemView, QColor, \
QItemDelegate, QPainterPath, QLinearGradient, QBrush, \
QPen, QStyle, QPainter, QLineEdit, \
- QPalette, QImage, QApplication, QMenu, QStyledItemDelegate
+ QPalette, QImage, QApplication, QMenu, \
+ QStyledItemDelegate, QCompleter
from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, QString, \
SIGNAL, QObject, QSize, QModelIndex, QDate
@@ -19,6 +20,7 @@ from calibre.utils.pyparsing import ParseException
from calibre.library.database2 import FIELD_MAP
from calibre.gui2 import NONE, TableView, qstring_to_unicode, config, \
error_dialog
+from calibre.gui2.widgets import EnLineEdit, TagsLineEdit
from calibre.utils.search_query_parser import SearchQueryParser
from calibre.ebooks.metadata.meta import set_metadata as _set_metadata
from calibre.ebooks.metadata import string_to_authors, fmt_sidx
@@ -111,6 +113,45 @@ class PubDateDelegate(QStyledItemDelegate):
qde.setCalendarPopup(True)
return qde
+class TextDelegate(QStyledItemDelegate):
+
+ def __init__(self, parent):
+ '''
+ Delegate for text data. If auto_complete_function needs to return a list
+ of text items to auto-complete with. The funciton is None no
+ auto-complete will be used.
+ '''
+ QStyledItemDelegate.__init__(self, parent)
+ self.auto_complete_function = None
+
+ def set_auto_complete_function(self, f):
+ self.auto_complete_function = f
+
+ def createEditor(self, parent, option, index):
+ editor = EnLineEdit(parent)
+ if self.auto_complete_function:
+ complete_items = [i[1] for i in self.auto_complete_function()]
+ completer = QCompleter(complete_items, self)
+ completer.setCaseSensitivity(Qt.CaseInsensitive)
+ completer.setCompletionMode(QCompleter.InlineCompletion)
+ editor.setCompleter(completer)
+ return editor
+
+class TagsDelegate(QStyledItemDelegate):
+
+ def __init__(self, parent):
+ QStyledItemDelegate.__init__(self, parent)
+ self.db = None
+
+ def set_database(self, db):
+ self.db = db
+
+ def createEditor(self, parent, option, index):
+ if self.db:
+ editor = TagsLineEdit(parent, self.db.all_tags())
+ else:
+ editor = EnLineEdit(parent)
+ return editor
class BooksModel(QAbstractTableModel):
headers = {
@@ -148,21 +189,7 @@ class BooksModel(QAbstractTableModel):
if cols != self.column_map:
self.column_map = cols
self.reset()
- try:
- idx = self.column_map.index('rating')
- except ValueError:
- idx = -1
- try:
- tidx = self.column_map.index('timestamp')
- except ValueError:
- tidx = -1
- try:
- pidx = self.column_map.index('pubdate')
- except ValueError:
- pidx = -1
-
- self.emit(SIGNAL('columns_sorted(int,int,int)'), idx, tidx, pidx)
-
+ self.emit(SIGNAL('columns_sorted()'))
def set_database(self, db):
self.db = db
@@ -649,34 +676,45 @@ class BooksView(TableView):
self.rating_delegate = LibraryDelegate(self)
self.timestamp_delegate = DateDelegate(self)
self.pubdate_delegate = PubDateDelegate(self)
+ self.tags_delegate = TagsDelegate(self)
+ self.authors_delegate = TextDelegate(self)
+ self.series_delegate = TextDelegate(self)
+ self.publisher_delegate = TextDelegate(self)
self.display_parent = parent
self._model = modelcls(self)
self.setModel(self._model)
self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setSortingEnabled(True)
- try:
- cm = self._model.column_map
- self.columns_sorted(cm.index('rating') if 'rating' in cm else -1,
- cm.index('timestamp') if 'timestamp' in cm else -1,
- cm.index('pubdate') if 'pubdate' in cm else -1)
- except ValueError:
- pass
+ for i in range(10):
+ self.setItemDelegateForColumn(i, TextDelegate(self))
+ self.columns_sorted()
QObject.connect(self.selectionModel(), SIGNAL('currentRowChanged(QModelIndex, QModelIndex)'),
self._model.current_changed)
- self.connect(self._model, SIGNAL('columns_sorted(int,int,int)'),
+ self.connect(self._model, SIGNAL('columns_sorted()'),
self.columns_sorted, Qt.QueuedConnection)
- def columns_sorted(self, rating_col, timestamp_col, pubdate_col):
+ def columns_sorted(self):
for i in range(self.model().columnCount(None)):
if self.itemDelegateForColumn(i) in (self.rating_delegate,
self.timestamp_delegate, self.pubdate_delegate):
self.setItemDelegateForColumn(i, self.itemDelegate())
- if rating_col > -1:
- self.setItemDelegateForColumn(rating_col, self.rating_delegate)
- if timestamp_col > -1:
- self.setItemDelegateForColumn(timestamp_col, self.timestamp_delegate)
- if pubdate_col > -1:
- self.setItemDelegateForColumn(pubdate_col, self.pubdate_delegate)
+
+ cm = self._model.column_map
+
+ if 'rating' in cm:
+ self.setItemDelegateForColumn(cm.index('rating'), self.rating_delegate)
+ if 'timestamp' in cm:
+ self.setItemDelegateForColumn(cm.index('timestamp'), self.timestamp_delegate)
+ if 'pubdate' in cm:
+ self.setItemDelegateForColumn(cm.index('pubdate'), self.pubdate_delegate)
+ if 'tags' in cm:
+ self.setItemDelegateForColumn(cm.index('tags'), self.tags_delegate)
+ if 'authors' in cm:
+ self.setItemDelegateForColumn(cm.index('authors'), self.authors_delegate)
+ if 'publisher' in cm:
+ self.setItemDelegateForColumn(cm.index('publisher'), self.publisher_delegate)
+ if 'series' in cm:
+ self.setItemDelegateForColumn(cm.index('series'), self.series_delegate)
def set_context_menu(self, edit_metadata, send_to_device, convert, view,
save, open_folder, book_details, similar_menu=None):
@@ -739,6 +777,10 @@ class BooksView(TableView):
def set_database(self, db):
self._model.set_database(db)
+ self.tags_delegate.set_database(db)
+ self.authors_delegate.set_auto_complete_function(db.all_authors)
+ self.series_delegate.set_auto_complete_function(db.all_series)
+ self.publisher_delegate.set_auto_complete_function(db.all_publishers)
def close(self):
self._model.close()
@@ -769,10 +811,13 @@ class DeviceBooksView(BooksView):
self.resize_on_select = False
self.rating_delegate = None
for i in range(10):
- self.setItemDelegateForColumn(i, self.itemDelegate())
+ self.setItemDelegateForColumn(i, TextDelegate(self))
self.setDragDropMode(self.NoDragDrop)
self.setAcceptDrops(False)
+ def set_database(self, db):
+ self._model.set_database(db)
+
def resizeColumnsToContents(self):
QTableView.resizeColumnsToContents(self)
self.columns_resized = True
@@ -1062,6 +1107,7 @@ class SearchBox(QLineEdit):
QLineEdit.__init__(self, parent)
self.help_text = help_text
self.initial_state = True
+ self.as_you_type = True
self.default_palette = QApplication.palette(self)
self.gray = QPalette(self.default_palette)
self.gray.setBrush(QPalette.Text, QBrush(QColor('gray')))
@@ -1094,6 +1140,9 @@ class SearchBox(QLineEdit):
if self.initial_state:
self.normalize_state()
self.initial_state = False
+ if not self.as_you_type:
+ if event.key() in (Qt.Key_Return, Qt.Key_Enter):
+ self.do_search()
QLineEdit.keyPressEvent(self, event)
def mouseReleaseEvent(self, event):
@@ -1103,17 +1152,21 @@ class SearchBox(QLineEdit):
QLineEdit.mouseReleaseEvent(self, event)
def text_edited_slot(self, text):
- text = qstring_to_unicode(text) if isinstance(text, QString) else unicode(text)
- self.prev_text = text
- self.timer = self.startTimer(self.__class__.INTERVAL)
+ if self.as_you_type:
+ text = qstring_to_unicode(text) if isinstance(text, QString) else unicode(text)
+ self.prev_text = text
+ self.timer = self.startTimer(self.__class__.INTERVAL)
def timerEvent(self, event):
self.killTimer(event.timerId())
if event.timerId() == self.timer:
- text = qstring_to_unicode(self.text())
- refinement = text.startswith(self.prev_search) and ':' not in text
- self.prev_search = text
- self.emit(SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), text, refinement)
+ self.do_search()
+
+ def do_search(self):
+ text = qstring_to_unicode(self.text())
+ refinement = text.startswith(self.prev_search) and ':' not in text
+ self.prev_search = text
+ self.emit(SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), text, refinement)
def search_from_tokens(self, tokens, all):
ans = u' '.join([u'%s:%s'%x for x in tokens])
@@ -1132,3 +1185,6 @@ class SearchBox(QLineEdit):
self.end(False)
self.initial_state = False
+ def search_as_you_type(self, enabled):
+ self.as_you_type = enabled
+
diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py
index c1597c5979..ff797d8aaa 100644
--- a/src/calibre/gui2/main.py
+++ b/src/calibre/gui2/main.py
@@ -147,6 +147,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.system_tray_icon.hide()
else:
self.system_tray_icon.show()
+ self.search.search_as_you_type(config['search_as_you_type'])
self.system_tray_menu = QMenu(self)
self.restore_action = self.system_tray_menu.addAction(
QIcon(':/images/page.svg'), _('&Restore'))
@@ -311,12 +312,14 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
cm.addAction(_('Convert individually'))
cm.addAction(_('Bulk convert'))
self.action_convert.setMenu(cm)
+ self._convert_single_hook = partial(self.convert_ebook, bulk=False)
QObject.connect(cm.actions()[0],
- SIGNAL('triggered(bool)'), self.convert_single)
+ SIGNAL('triggered(bool)'), self._convert_single_hook)
+ self._convert_bulk_hook = partial(self.convert_ebook, bulk=True)
QObject.connect(cm.actions()[1],
- SIGNAL('triggered(bool)'), self.convert_bulk)
+ SIGNAL('triggered(bool)'), self._convert_bulk_hook)
QObject.connect(self.action_convert,
- SIGNAL('triggered(bool)'), self.convert_single)
+ SIGNAL('triggered(bool)'), self.convert_ebook)
self.convert_menu = cm
pm = QMenu()
@@ -1161,32 +1164,17 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
return None
return [self.library_view.model().db.id(r) for r in rows]
- def convert_bulk(self, checked):
+ def convert_ebook(self, checked, bulk=None):
book_ids = self.get_books_for_conversion()
if book_ids is None: return
previous = self.library_view.currentIndex()
rows = [x.row() for x in \
self.library_view.selectionModel().selectedRows()]
- jobs, changed, bad = convert_bulk_ebook(self,
+ if bulk or (bulk is None and len(book_ids) > 1):
+ jobs, changed, bad = convert_bulk_ebook(self,
self.library_view.model().db, book_ids, out_format=prefs['output_format'])
- for func, args, desc, fmt, id, temp_files in jobs:
- if id not in bad:
- job = self.job_manager.run_job(Dispatcher(self.book_converted),
- func, args=args, description=desc)
- self.conversion_jobs[job] = (temp_files, fmt, id)
-
- if changed:
- self.library_view.model().refresh_rows(rows)
- current = self.library_view.currentIndex()
- self.library_view.model().current_changed(current, previous)
-
- def convert_single(self, checked):
- book_ids = self.get_books_for_conversion()
- if book_ids is None: return
- previous = self.library_view.currentIndex()
- rows = [x.row() for x in \
- self.library_view.selectionModel().selectedRows()]
- jobs, changed, bad = convert_single_ebook(self,
+ else:
+ jobs, changed, bad = convert_single_ebook(self,
self.library_view.model().db, book_ids, out_format=prefs['output_format'])
for func, args, desc, fmt, id, temp_files in jobs:
if id not in bad:
@@ -1369,51 +1357,51 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
def view_book(self, triggered):
rows = self.current_view().selectionModel().selectedRows()
- if self.current_view() is self.library_view:
- if not rows or len(rows) == 0:
- self._launch_viewer()
- return
- row = rows[0].row()
- formats = self.library_view.model().db.formats(row).upper()
- formats = formats.split(',')
- title = self.library_view.model().db.title(row)
- id = self.library_view.model().db.id(row)
- format = None
- if len(formats) == 1:
- format = formats[0]
- if 'LRF' in formats:
- format = 'LRF'
- if 'EPUB' in formats:
- format = 'EPUB'
- if 'MOBI' in formats:
- format = 'MOBI'
- if not formats:
- d = error_dialog(self, _('Cannot view'),
- _('%s has no available formats.')%(title,))
- d.exec_()
- return
- if format is None:
- d = ChooseFormatDialog(self, _('Choose the format to view'),
- formats)
- d.exec_()
- if d.result() == QDialog.Accepted:
- format = d.format()
- else:
+ if not rows or len(rows) == 0:
+ self._launch_viewer()
+ return
+
+ if len(rows) >= 3:
+ if not question_dialog(self, _('Multiple Books Selected'),
+ _('You are attempting to open %d books. Opening too many '
+ 'books at once can be slow and have a negative effect on the '
+ 'responsiveness of your computer. Once started the process '
+ 'cannot be stopped until complete. Do you wish to continue?'
+ % len(rows))):
return
- self.view_format(row, format)
+ if self.current_view() is self.library_view:
+ for row in rows:
+ row = row.row()
+
+ formats = self.library_view.model().db.formats(row).upper()
+ formats = formats.split(',')
+ title = self.library_view.model().db.title(row)
+
+ if not formats:
+ error_dialog(self, _('Cannot view'),
+ _('%s has no available formats.')%(title,), show=True)
+ continue
+
+ in_prefs = False
+ for format in prefs['input_format_order']:
+ if format in formats:
+ in_prefs = True
+ self.view_format(row, format)
+ break
+ if not in_prefs:
+ self.view_format(row, format[0])
else:
paths = self.current_view().model().paths(rows)
- if paths:
+ for path in paths:
pt = PersistentTemporaryFile('_viewer_'+\
- os.path.splitext(paths[0])[1])
+ os.path.splitext(path)[1])
self.persistent_files.append(pt)
pt.close()
self.device_manager.view_book(\
Dispatcher(self.book_downloaded_for_viewing),
- paths[0], pt.name)
-
+ path, pt.name)
############################################################################
@@ -1441,6 +1429,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.content_server = d.server
if d.result() == d.Accepted:
self.tool_bar.setIconSize(config['toolbar_icon_size'])
+ self.search.search_as_you_type(config['search_as_you_type'])
self.tool_bar.setToolButtonStyle(
Qt.ToolButtonTextUnderIcon if \
config['show_text_in_toolbar'] else \
diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py
index 3f7734f8c9..f4b9130fc8 100644
--- a/src/calibre/gui2/widgets.py
+++ b/src/calibre/gui2/widgets.py
@@ -10,7 +10,8 @@ from PyQt4.Qt import QListView, QIcon, QFont, QLabel, QListWidget, \
QPixmap, QMovie, QPalette, QTimer, QDialog, \
QAbstractListModel, QVariant, Qt, SIGNAL, \
QRegExp, QSettings, QSize, QModelIndex, \
- QAbstractButton, QPainter, QLineEdit, QComboBox
+ QAbstractButton, QPainter, QLineEdit, QComboBox, \
+ QMenu, QStringListModel, QCompleter
from calibre.gui2 import human_readable, NONE, TableView, \
qstring_to_unicode, error_dialog
@@ -460,12 +461,30 @@ class LineEditECM(object):
def contextMenuEvent(self, event):
menu = self.createStandardContextMenu()
menu.addSeparator()
- action_title_case = menu.addAction('Title Case')
+ case_menu = QMenu(_('Change Case'))
+ action_upper_case = case_menu.addAction(_('Upper Case'))
+ action_lower_case = case_menu.addAction(_('Lower Case'))
+ action_swap_case = case_menu.addAction(_('Swap Case'))
+ action_title_case = case_menu.addAction(_('Title Case'))
+
+ self.connect(action_upper_case, SIGNAL('triggered()'), self.upper_case)
+ self.connect(action_lower_case, SIGNAL('triggered()'), self.lower_case)
+ self.connect(action_swap_case, SIGNAL('triggered()'), self.swap_case)
self.connect(action_title_case, SIGNAL('triggered()'), self.title_case)
+ menu.addMenu(case_menu)
menu.exec_(event.globalPos())
+ def upper_case(self):
+ self.setText(qstring_to_unicode(self.text()).upper())
+
+ def lower_case(self):
+ self.setText(qstring_to_unicode(self.text()).lower())
+
+ def swap_case(self):
+ self.setText(qstring_to_unicode(self.text()).swapcase())
+
def title_case(self):
self.setText(qstring_to_unicode(self.text()).title())
@@ -481,6 +500,84 @@ class EnLineEdit(LineEditECM, QLineEdit):
pass
+class TagsCompleter(QCompleter):
+
+ '''
+ A completer object that completes a list of tags. It is used in conjunction
+ with a CompleterLineEdit.
+ '''
+
+ def __init__(self, parent, all_tags):
+ QCompleter.__init__(self, all_tags, parent)
+ self.all_tags = set(all_tags)
+
+ def update(self, text_tags, completion_prefix):
+ tags = list(self.all_tags.difference(text_tags))
+ model = QStringListModel(tags, self)
+ self.setModel(model)
+
+ self.setCompletionPrefix(completion_prefix)
+ if completion_prefix.strip() != '':
+ self.complete()
+
+ def update_tags_cache(self, tags):
+ self.all_tags = set(tags)
+ model = QStringListModel(tags, self)
+ self.setModel(model)
+
+
+class TagsLineEdit(EnLineEdit):
+
+ '''
+ A QLineEdit that can complete parts of text separated by separator.
+ '''
+
+ def __init__(self, parent=0, tags=[]):
+ EnLineEdit.__init__(self, parent)
+
+ self.separator = ','
+
+ self.connect(self, SIGNAL('textChanged(QString)'), self.text_changed)
+
+ self.completer = TagsCompleter(self, tags)
+ self.completer.setCaseSensitivity(Qt.CaseInsensitive)
+
+ self.connect(self,
+ SIGNAL('text_changed(PyQt_PyObject, PyQt_PyObject)'),
+ self.completer.update)
+ self.connect(self.completer, SIGNAL('activated(QString)'),
+ self.complete_text)
+
+ self.completer.setWidget(self)
+
+ def update_tags_cache(self, tags):
+ self.completer.update_tags_cache(tags)
+
+ def text_changed(self, text):
+ all_text = qstring_to_unicode(text)
+ text = all_text[:self.cursorPosition()]
+ prefix = text.split(',')[-1].strip()
+
+ text_tags = []
+ for t in all_text.split(self.separator):
+ t1 = qstring_to_unicode(t).strip()
+ if t1 != '':
+ text_tags.append(t)
+ text_tags = list(set(text_tags))
+
+ self.emit(SIGNAL('text_changed(PyQt_PyObject, PyQt_PyObject)'),
+ text_tags, prefix)
+
+ def complete_text(self, text):
+ cursor_pos = self.cursorPosition()
+ before_text = qstring_to_unicode(self.text())[:cursor_pos]
+ after_text = qstring_to_unicode(self.text())[cursor_pos:]
+ prefix_len = len(before_text.split(',')[-1].strip())
+ self.setText('%s%s%s %s' % (before_text[:cursor_pos - prefix_len],
+ text, self.separator, after_text))
+ self.setCursorPosition(cursor_pos - prefix_len + len(text) + 2)
+
+
class EnComboBox(QComboBox):
'''
@@ -493,6 +590,8 @@ class EnComboBox(QComboBox):
QComboBox.__init__(self, *args)
self.setLineEdit(EnLineEdit(self))
+ def text(self):
+ return qstring_to_unicode(self.currentText())
class PythonHighlighter(QSyntaxHighlighter):
diff --git a/src/calibre/gui2/wizard/__init__.py b/src/calibre/gui2/wizard/__init__.py
index bfd01f58fd..29ceda68fd 100644
--- a/src/calibre/gui2/wizard/__init__.py
+++ b/src/calibre/gui2/wizard/__init__.py
@@ -92,6 +92,12 @@ class CybookG3(Device):
manufacturer = 'Booken'
id = 'cybookg3'
+class CybookOpus(CybookG3):
+
+ name = 'Cybook Opus'
+ output_format = 'EPUB'
+ id = 'cybook_opus'
+
class BeBook(Device):
name = 'BeBook or BeBook Mini'
diff --git a/src/calibre/library/database.py b/src/calibre/library/database.py
index 72b629db0b..ed92853df2 100644
--- a/src/calibre/library/database.py
+++ b/src/calibre/library/database.py
@@ -928,6 +928,10 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
except:
pass
+ def author_id(self, index, index_is_id=False):
+ id = index if index_is_id else self.id(index)
+ return self.conn.get('SELECT author from books_authors_link WHERE book=?', (id,), all=False)
+
def isbn(self, idx, index_is_id=False):
id = idx if index_is_id else self.id(idx)
return self.conn.get('SELECT isbn FROM books WHERE id=?',(id,), all=False)
diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py
index f7780aa2a6..41e4387e1e 100644
--- a/src/calibre/library/database2.py
+++ b/src/calibre/library/database2.py
@@ -51,7 +51,7 @@ copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
FIELD_MAP = {'id':0, 'title':1, 'authors':2, 'publisher':3, 'rating':4, 'timestamp':5,
'size':6, 'tags':7, 'comments':8, 'series':9, 'series_index':10,
'sort':11, 'author_sort':12, 'formats':13, 'isbn':14, 'path':15,
- 'lccn':16, 'pubdate':17, 'flags':18}
+ 'lccn':16, 'pubdate':17, 'flags':18, 'cover':19}
INDEX_MAP = dict(zip(FIELD_MAP.values(), FIELD_MAP.keys()))
@@ -198,19 +198,40 @@ class ResultCache(SearchQueryParser):
query = query.decode('utf-8')
if location in ('tag', 'author', 'format'):
location += 's'
- all = ('title', 'authors', 'publisher', 'tags', 'comments', 'series', 'formats', 'isbn')
+ all = ('title', 'authors', 'publisher', 'tags', 'comments', 'series', 'formats', 'isbn', 'rating', 'cover')
MAP = {}
for x in all:
MAP[x] = FIELD_MAP[x]
+ EXCLUDE_FIELDS = [MAP['rating'], MAP['cover']]
location = [location] if location != 'all' else list(MAP.keys())
for i, loc in enumerate(location):
location[i] = MAP[loc]
+ try:
+ rating_query = int(query) * 2
+ except:
+ rating_query = None
for item in self._data:
if item is None: continue
for loc in location:
- if item[loc] and query in item[loc].lower():
+ if query == 'false' and not item[loc]:
+ if isinstance(item[loc], basestring):
+ if item[loc].strip() != '':
+ continue
matches.add(item[0])
break
+ if query == 'true' and item[loc]:
+ if isinstance(item[loc], basestring):
+ if item[loc].strip() == '':
+ continue
+ matches.add(item[0])
+ break
+ if rating_query and item[loc] and loc == MAP['rating'] and rating_query == int(item[loc]):
+ matches.add(item[0])
+ break
+ if item[loc] and loc not in EXCLUDE_FIELDS and query in item[loc].lower():
+ matches.add(item[0])
+ break
+
return matches
def remove(self, id):
@@ -242,15 +263,16 @@ class ResultCache(SearchQueryParser):
pass
return False
- def refresh_ids(self, conn, ids):
+ def refresh_ids(self, db, ids):
'''
Refresh the data in the cache for books identified by ids.
Returns a list of affected rows or None if the rows are filtered.
'''
for id in ids:
try:
- self._data[id] = conn.get('SELECT * from meta WHERE id=?',
+ self._data[id] = db.conn.get('SELECT * from meta WHERE id=?',
(id,))[0]
+ self._data[id].append(db.has_cover(id, index_is_id=True))
except IndexError:
return None
try:
@@ -259,12 +281,13 @@ class ResultCache(SearchQueryParser):
pass
return None
- def books_added(self, ids, conn):
+ def books_added(self, ids, db):
if not ids:
return
self._data.extend(repeat(None, max(ids)-len(self._data)+2))
for id in ids:
- self._data[id] = conn.get('SELECT * from meta WHERE id=?', (id,))[0]
+ self._data[id] = db.conn.get('SELECT * from meta WHERE id=?', (id,))[0]
+ self._data[id].append(db.has_cover(id, index_is_id=True))
self._map[0:0] = ids
self._map_filtered[0:0] = ids
@@ -282,6 +305,9 @@ class ResultCache(SearchQueryParser):
self._data = list(itertools.repeat(None, temp[-1][0]+2)) if temp else []
for r in temp:
self._data[r[0]] = r
+ for item in self._data:
+ if item is not None:
+ item.append(db.has_cover(item[0], index_is_id=True))
self._map = [i[0] for i in self._data if i is not None]
if field is not None:
self.sort(field, ascending)
@@ -400,7 +426,7 @@ class LibraryDatabase2(LibraryDatabase):
self.refresh = functools.partial(self.data.refresh, self)
self.sort = self.data.sort
self.index = self.data.index
- self.refresh_ids = functools.partial(self.data.refresh_ids, self.conn)
+ self.refresh_ids = functools.partial(self.data.refresh_ids, self)
self.row = self.data.row
self.has_id = self.data.has_id
self.count = self.data.count
@@ -1014,7 +1040,7 @@ class LibraryDatabase2(LibraryDatabase):
self.set_rating(id, val, notify=False)
elif column == 'tags':
self.set_tags(id, val.split(','), append=False, notify=False)
- self.data.refresh_ids(self.conn, [id])
+ self.data.refresh_ids(self, [id])
self.set_path(id, True)
self.notify('metadata', [id])
@@ -1195,7 +1221,7 @@ class LibraryDatabase2(LibraryDatabase):
if id:
self.conn.execute('DELETE FROM books_tags_link WHERE tag=? AND book=?', (id, book_id))
self.conn.commit()
- self.data.refresh_ids(self.conn, [book_id])
+ self.data.refresh_ids(self, [book_id])
if notify:
self.notify('metadata', [id])
@@ -1300,7 +1326,7 @@ class LibraryDatabase2(LibraryDatabase):
obj = self.conn.execute('INSERT INTO books(title, author_sort) VALUES (?, ?)',
(mi.title, mi.authors[0]))
id = obj.lastrowid
- self.data.books_added([id], self.conn)
+ self.data.books_added([id], self)
self.set_path(id, index_is_id=True)
self.conn.commit()
self.set_metadata(id, mi)
@@ -1309,7 +1335,7 @@ class LibraryDatabase2(LibraryDatabase):
if not hasattr(path, 'read'):
stream.close()
self.conn.commit()
- self.data.refresh_ids(self.conn, [id]) # Needed to update format list and size
+ self.data.refresh_ids(self, [id]) # Needed to update format list and size
return id
def run_import_plugins(self, path_or_stream, format):
@@ -1337,7 +1363,7 @@ class LibraryDatabase2(LibraryDatabase):
obj = self.conn.execute('INSERT INTO books(title, series_index, author_sort) VALUES (?, ?, ?)',
(title, series_index, aus))
id = obj.lastrowid
- self.data.books_added([id], self.conn)
+ self.data.books_added([id], self)
self.set_path(id, True)
self.conn.commit()
self.set_metadata(id, mi)
@@ -1370,7 +1396,7 @@ class LibraryDatabase2(LibraryDatabase):
obj = self.conn.execute('INSERT INTO books(title, series_index, author_sort) VALUES (?, ?, ?)',
(title, series_index, aus))
id = obj.lastrowid
- self.data.books_added([id], self.conn)
+ self.data.books_added([id], self)
ids.append(id)
self.set_path(id, True)
self.conn.commit()
@@ -1381,7 +1407,7 @@ class LibraryDatabase2(LibraryDatabase):
self.add_format(id, format, stream, index_is_id=True)
stream.close()
self.conn.commit()
- self.data.refresh_ids(self.conn, ids) # Needed to update format list and size
+ self.data.refresh_ids(self, ids) # Needed to update format list and size
if duplicates:
paths = list(duplicate[0] for duplicate in duplicates)
formats = list(duplicate[1] for duplicate in duplicates)
@@ -1403,7 +1429,7 @@ class LibraryDatabase2(LibraryDatabase):
obj = self.conn.execute('INSERT INTO books(title, series_index, author_sort) VALUES (?, ?, ?)',
(title, series_index, aus))
id = obj.lastrowid
- self.data.books_added([id], self.conn)
+ self.data.books_added([id], self)
self.set_path(id, True)
self.set_metadata(id, mi)
for path in formats:
@@ -1412,7 +1438,7 @@ class LibraryDatabase2(LibraryDatabase):
continue
self.add_format_with_hooks(id, ext, path, index_is_id=True)
self.conn.commit()
- self.data.refresh_ids(self.conn, [id]) # Needed to update format list and size
+ self.data.refresh_ids(self, [id]) # Needed to update format list and size
if notify:
self.notify('add', [id])
diff --git a/src/calibre/utils/config.py b/src/calibre/utils/config.py
index 0925d8667d..73db896e17 100644
--- a/src/calibre/utils/config.py
+++ b/src/calibre/utils/config.py
@@ -548,6 +548,10 @@ def _prefs():
help=_('The language in which to display the user interface'))
c.add_opt('output_format', default='EPUB',
help=_('The default output format for ebook conversions.'))
+ c.add_opt('input_format_order', default=['EPUB', 'MOBI', 'LIT', 'PRC',
+ 'FB2', 'HTML', 'HTM', 'XHTM', 'SHTML', 'XHTML', 'ODT', 'RTF', 'PDF',
+ 'TXT'],
+ help=_('Ordered list of formats to prefer for input.'))
c.add_opt('read_file_metadata', default=True,
help=_('Read metadata from files'))
c.add_opt('worker_process_priority', default='normal',
diff --git a/src/calibre/utils/search_query_parser.py b/src/calibre/utils/search_query_parser.py
index 35241c89c4..425b4c2d49 100644
--- a/src/calibre/utils/search_query_parser.py
+++ b/src/calibre/utils/search_query_parser.py
@@ -50,6 +50,8 @@ class SearchQueryParser(object):
'author',
'publisher',
'series',
+ 'rating',
+ 'cover',
'comments',
'format',
'isbn',