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

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

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

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

View File

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

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

View File

@ -6,6 +6,7 @@ __license__ = 'GPL v3'
__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 @@ 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():

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

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

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

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

View File

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

View File

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

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

View File

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

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