Pull from driver-dev

This commit is contained in:
Kovid Goyal 2009-07-08 18:29:49 -06:00
commit a9f3765632
29 changed files with 752 additions and 278 deletions

View file

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

View file

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

View file

@ -7,7 +7,8 @@
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 @@ def __init__(self, input, output, log, report_progress=DummyReporter()):
)
),
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)(?<=<hr>)((\s*<a name=\d+></a>((<img.+?>)*<br>\s*)?\d+<br>\s*.*?\s*)|(\s*<a name=\d+></a>((<img.+?>)*<br>\s*)?.*?<br>\s*\d+))(?=<br>)',
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)(?<=<hr>)((\s*<a name=\d+></a>((<img.+?>)*<br>\s*)?\d+<br>\s*.*?\s*)|(\s*<a name=\d+></a>((<img.+?>)*<br>\s*)?.*?<br>\s*\d+))(?=<br>)',
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 @@ def __init__(self, input, output, log, report_progress=DummyReporter()):
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 @@ def find_html_index(self, files):
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 @@ def setup_options(self):
'''
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:

View file

@ -140,8 +140,6 @@ class HTMLPreProcessor(object):
(re.compile(u'(?<=[\.,;\?!”"\'])[\s^ ]*(?=<)'), lambda match: ' '),
# Connect paragraphs split by -
(re.compile(u'(?<=[^\s][-])[\s]*(</p>)*[\s]*(<p>)*\s*(?=[^\s])'), lambda match: ''),
# Remove - that splits words
(re.compile(u'(?<=[^\s])[-]+(?=[^\s])'), lambda match: ''),
# Add space before and after italics
(re.compile(u'(?<!“)<i>'), lambda match: ' <i>'),
(re.compile(r'</i>(?=\w)'), lambda match: '</i> '),
@ -163,10 +161,10 @@ class HTMLPreProcessor(object):
lambda match : '<h3 class="subtitle">%s</h3>'%(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'<meta\s+name="Publisher"\s+content=".*?Baen.*?"',
@ -187,18 +185,30 @@ def __call__(self, html, remove_special_chars=None):
elif self.is_book_designer(html):
rules = self.BOOK_DESIGNER
elif self.is_pdftohtml(html):
length = line_length(html, self.pdf_line_length)
line_length_rules = []
if length:
line_length_rules = [
# Un wrap using punctuation
(re.compile(r'(?<=.{%i}[a-z\.,;:)-IA])\s*(?P<ital></(i|b|u)>)?\s*(<p.*?>)\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<ital></(i|b|u)>)?\s*(<p.*?>)\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)

View file

@ -35,7 +35,7 @@ def __call__(self, oeb, opts):
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)

View file

@ -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 @@ def convert(self, stream, options, file_ext, log,
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)

View file

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

View file

@ -15,7 +15,8 @@
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 @@ def setup_output_formats(self, db, preferred_output_format):
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 @@ def accept(self):
self._recommendations = recs
ResizableDialog.accept(self)

View file

@ -35,21 +35,17 @@ def __init__(self, parent, get_option, get_help, db=None, book_id=None):
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 @@ def initialize_metadata_options(self):
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:

View file

@ -143,19 +143,6 @@
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="EnLineEdit" name="author">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Change the author(s) of this book. Multiple authors should be separated by an &amp;. If the author name contains an &amp;, use &amp;&amp; to represent it.</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
@ -195,13 +182,6 @@
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="EnLineEdit" name="publisher">
<property name="toolTip">
<string>Change the publisher of this book</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
@ -216,7 +196,7 @@
</widget>
</item>
<item row="4" column="1">
<widget class="EnLineEdit" name="tags">
<widget class="TagsLineEdit" name="tags">
<property name="toolTip">
<string>Tags categorize the book. This is particularly useful while searching. &lt;br&gt;&lt;br&gt;They can be any words or phrases, separated by commas.</string>
</property>
@ -276,6 +256,20 @@
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="EnComboBox" name="publisher">
<property name="editable">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="EnComboBox" name="author">
<property name="editable">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
@ -329,11 +323,16 @@
<extends>QComboBox</extends>
<header>widgets.h</header>
</customwidget>
<customwidget>
<class>TagsLineEdit</class>
<extends>QLineEdit</extends>
<header>widgets.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="../images.qrc"/>
<include location="../images.qrc"/>
<include location="../../../../../gui2/images.qrc"/>
<include location="../images.qrc"/>
</resources>
<connections/>
</ui>

View file

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

View file

@ -14,14 +14,14 @@
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="0">
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Line Un-Wrapping Factor:</string>
</property>
</widget>
</item>
<item row="3" column="0">
<item row="2" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
@ -34,8 +34,8 @@
</property>
</spacer>
</item>
<item row="1" column="1">
<widget class="QDoubleSpinBox" name="opt_pdf_line_length">
<item row="0" column="1">
<widget class="QDoubleSpinBox" name="opt_unwrap_factor">
<property name="maximum">
<double>1.000000000000000</double>
</property>
@ -47,7 +47,7 @@
</property>
</widget>
</item>
<item row="2" column="0">
<item row="1" column="0">
<widget class="QCheckBox" name="opt_no_images">
<property name="text">
<string>No Images</string>

View file

@ -20,11 +20,10 @@
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 @@ def setup_input_output_formats(self, db, book_id, preferred_input_format,
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

View file

@ -6,6 +6,7 @@
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__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 @@ def __init__(self, parent, get_option, get_help, db=None, book_id=None):
['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 @@ def __init__(self, parent, get_option, get_help, db=None, book_id=None):
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():

View file

@ -14,6 +14,9 @@
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" colspan="2">
<widget class="XPathEdit" name="opt_chapter" native="true"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
@ -62,20 +65,27 @@
</property>
</widget>
</item>
<item row="4" column="0" colspan="2">
<item row="8" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>&amp;Footer regular expression:</string>
</property>
<property name="buddy">
<cstring>opt_footer_regex</cstring>
</property>
</widget>
</item>
<item row="10" column="0" colspan="2">
<widget class="QCheckBox" name="opt_preprocess_html">
<property name="text">
<string>&amp;Preprocess input file to possibly improve structure detection</string>
</property>
</widget>
</item>
<item row="5" column="0" colspan="2">
<item row="11" column="0" colspan="2">
<widget class="XPathEdit" name="opt_page_breaks_before" native="true"/>
</item>
<item row="0" column="0" colspan="2">
<widget class="XPathEdit" name="opt_chapter" native="true"/>
</item>
<item row="6" column="0">
<item row="12" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
@ -88,6 +98,36 @@
</property>
</spacer>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>&amp;Header regular expression:</string>
</property>
<property name="buddy">
<cstring>opt_header_regex</cstring>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QCheckBox" name="opt_remove_footer">
<property name="text">
<string>Remove F&amp;ooter</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QCheckBox" name="opt_remove_header">
<property name="text">
<string>Remove H&amp;eader</string>
</property>
</widget>
</item>
<item row="9" column="0" colspan="2">
<widget class="QLineEdit" name="opt_footer_regex"/>
</item>
<item row="6" column="0" colspan="2">
<widget class="QLineEdit" name="opt_header_regex"/>
</item>
</layout>
</widget>
<customwidgets>

View file

@ -22,7 +22,8 @@
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 @@ def __init__(self, parent):
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 @@ def __init__(self, window, db, server=None):
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 @@ def __init__(self, window, db, server=None):
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 @@ def modify_plugin(self, op=''):
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 @@ def accept(self):
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 @@ def accept(self):
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:

View file

@ -8,7 +8,7 @@
<x>0</x>
<y>0</y>
<width>800</width>
<height>557</height>
<height>583</height>
</rect>
</property>
<property name="windowTitle">
@ -232,6 +232,68 @@
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="groupBox_5">
<property name="title">
<string>Preferred &amp;input format order:</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_11">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_10">
<item>
<widget class="QListWidget" name="input_order">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_10">
<item>
<widget class="QToolButton" name="input_up">
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../images.qrc">
<normaloff>:/images/arrow-up.svg</normaloff>:/images/arrow-up.svg</iconset>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_5">
<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>
<widget class="QToolButton" name="input_down">
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../images.qrc">
<normaloff>:/images/arrow-down.svg</normaloff>:/images/arrow-down.svg</iconset>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="dirs_box">
<property name="title">
@ -364,6 +426,16 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="search_as_you_type">
<property name="text">
<string>Search as you type</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="sync_news">
<property name="text">
@ -529,15 +601,6 @@
</layout>
</item>
</layout>
<zorder>roman_numerals</zorder>
<zorder>groupBox_2</zorder>
<zorder>systray_icon</zorder>
<zorder>sync_news</zorder>
<zorder>delete_news</zorder>
<zorder>separate_cover_flow</zorder>
<zorder>systray_notifications</zorder>
<zorder></zorder>
<zorder></zorder>
</widget>
<widget class="QWidget" name="page_6">
<layout class="QGridLayout" name="gridLayout_6">

View file

@ -9,7 +9,8 @@
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 @@ def __init__(self, window, rows, db):
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:

View file

@ -45,16 +45,6 @@
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>authors</cstring>
</property>
</widget>
</item>
<item row="0" column="1" colspan="2">
<widget class="EnLineEdit" name="authors">
<property name="toolTip">
<string>Change the author(s) of this book. Multiple authors should be separated by an &amp;. If the author name contains an &amp;, use &amp;&amp; to represent it.</string>
</property>
</widget>
</item>
<item row="2" column="0">
@ -65,9 +55,6 @@
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>authors</cstring>
</property>
</widget>
</item>
<item row="2" column="1" colspan="2">
@ -117,16 +104,6 @@
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>publisher</cstring>
</property>
</widget>
</item>
<item row="4" column="1" colspan="2">
<widget class="EnLineEdit" name="publisher">
<property name="toolTip">
<string>Change the publisher of this book</string>
</property>
</widget>
</item>
<item row="5" column="0">
@ -143,7 +120,7 @@
</widget>
</item>
<item row="5" column="1">
<widget class="EnLineEdit" name="tags">
<widget class="TagsLineEdit" name="tags">
<property name="toolTip">
<string>Tags categorize the book. This is particularly useful while searching. &lt;br&gt;&lt;br&gt;They can be any words or phrases, separated by commas.</string>
</property>
@ -174,7 +151,7 @@
</widget>
</item>
<item row="6" column="1" colspan="2">
<widget class="EnLineEdit" name="remove_tags">
<widget class="TagsLineEdit" name="remove_tags">
<property name="toolTip">
<string>Comma separated list of tags to remove from the books. </string>
</property>
@ -235,6 +212,20 @@
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="EnComboBox" name="authors">
<property name="editable">
<bool>true</bool>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="EnComboBox" name="publisher">
<property name="editable">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
@ -265,6 +256,11 @@
<extends>QComboBox</extends>
<header>widgets.h</header>
</customwidget>
<customwidget>
<class>TagsLineEdit</class>
<extends>QLineEdit</extends>
<header>widgets.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="../images.qrc"/>

View file

@ -13,7 +13,7 @@
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 @@ def __init__(self, parent, ext, size, path=None):
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 @@ def __init__(self, window, row, db, accepted_callback=None):
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 @@ def __init__(self, window, row, db, accepted_callback=None):
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 @@ def __init__(self, window, row, db, accepted_callback=None):
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 @@ def swap_title_author(self):
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 @@ def initialize_series(self):
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 @@ def initialize_series_and_publisher(self):
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()

View file

@ -121,9 +121,6 @@
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>authors</cstring>
</property>
</widget>
</item>
<item row="2" column="0">
@ -225,7 +222,7 @@
<item row="5" column="1" colspan="2">
<layout class="QHBoxLayout" name="_2">
<item>
<widget class="EnLineEdit" name="tags">
<widget class="TagsLineEdit" name="tags">
<property name="toolTip">
<string>Tags categorize the book. This is particularly useful while searching. &lt;br&gt;&lt;br&gt;They can be any words or phrases, separated by commas.</string>
</property>
@ -345,9 +342,6 @@
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="EnLineEdit" name="authors"/>
</item>
<item row="7" column="1">
<widget class="QDoubleSpinBox" name="series_index">
<property name="enabled">
@ -371,6 +365,13 @@
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="EnComboBox" name="authors">
<property name="editable">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
@ -651,11 +652,15 @@
<extends>QComboBox</extends>
<header>widgets.h</header>
</customwidget>
<customwidget>
<class>TagsLineEdit</class>
<extends>QLineEdit</extends>
<header>widgets.h</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>title</tabstop>
<tabstop>swap_button</tabstop>
<tabstop>authors</tabstop>
<tabstop>author_sort</tabstop>
<tabstop>auto_author_sort</tabstop>
<tabstop>rating</tabstop>

View file

@ -9,7 +9,8 @@
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.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 @@ def createEditor(self, parent, option, index):
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 @@ def read_config(self):
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 @@ def __init__(self, parent, modelcls=BooksModel):
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 @@ def dropEvent(self, event):
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 @@ def __init__(self, parent):
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 @@ def __init__(self, parent, help_text=_('Search (For Advanced Search click the bu
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 @@ def keyPressEvent(self, event):
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 @@ def mouseReleaseEvent(self, event):
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 @@ def set_search_string(self, txt):
self.end(False)
self.initial_state = False
def search_as_you_type(self, enabled):
self.as_you_type = enabled

View file

@ -147,6 +147,7 @@ def __init__(self, listener, opts, actions, parent=None):
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 @@ def __init__(self, listener, opts, actions, parent=None):
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 @@ def get_books_for_conversion(self):
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 @@ def view_folder(self, *args):
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 @@ def do_config(self, *args):
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 \

View file

@ -10,7 +10,8 @@
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 @@ def __init__(self, *args):
QComboBox.__init__(self, *args)
self.setLineEdit(EnLineEdit(self))
def text(self):
return qstring_to_unicode(self.currentText())
class PythonHighlighter(QSyntaxHighlighter):

View file

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

View file

@ -928,6 +928,10 @@ def authors(self, index, index_is_id=False):
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)

View file

@ -51,7 +51,7 @@ def delete_tree(path):
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 @@ def get_matches(self, location, query):
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 @@ def has_id(self, id):
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 @@ def refresh_ids(self, conn, ids):
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 @@ def refresh(self, db, field=None, ascending=True):
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 @@ def __init__(self, library_path, row_factory=False):
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 @@ def set(self, row, column, val):
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 @@ def unapply_tags(self, book_id, tags, notify=True):
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 @@ def add_news(self, path, recipe):
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 @@ def add_news(self, path, recipe):
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 @@ def create_book_entry(self, mi, cover=None, add_duplicates=True):
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 @@ def add_books(self, paths, formats, metadata, add_duplicates=True):
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 @@ def add_books(self, paths, formats, metadata, add_duplicates=True):
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 @@ def import_book(self, mi, formats, notify=True):
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 @@ def import_book(self, mi, formats, notify=True):
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])

View file

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

View file

@ -50,6 +50,8 @@ class SearchQueryParser(object):
'author',
'publisher',
'series',
'rating',
'cover',
'comments',
'format',
'isbn',