mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 10:44:09 -04:00
Pull from driver-dev
This commit is contained in:
commit
a9f3765632
@ -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'
|
||||
|
@ -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():
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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 &. If the author name contains an &, use && 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. <br><br>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>
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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():
|
||||
|
@ -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>&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>&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>&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&ooter</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QCheckBox" name="opt_remove_header">
|
||||
<property name="text">
|
||||
<string>Remove H&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>
|
||||
|
@ -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:
|
||||
|
@ -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 &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">
|
||||
|
@ -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:
|
||||
|
@ -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 &. If the author name contains an &, use && 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. <br><br>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"/>
|
||||
|
@ -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()
|
||||
|
@ -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. <br><br>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>
|
||||
|
@ -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
|
||||
|
||||
|
@ -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 \
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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'
|
||||
|
@ -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)
|
||||
|
@ -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])
|
||||
|
||||
|
@ -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',
|
||||
|
@ -50,6 +50,8 @@ class SearchQueryParser(object):
|
||||
'author',
|
||||
'publisher',
|
||||
'series',
|
||||
'rating',
|
||||
'cover',
|
||||
'comments',
|
||||
'format',
|
||||
'isbn',
|
||||
|
Loading…
x
Reference in New Issue
Block a user