diff --git a/resources/mime.types b/resources/mime.types
index ab98b3bf4a..a2a67c38f9 100644
--- a/resources/mime.types
+++ b/resources/mime.types
@@ -585,7 +585,6 @@ application/vnd.osa.netdeploy
application/vnd.osgi.bundle
application/vnd.osgi.dp dp
application/vnd.otps.ct-kip+xml
-application/vnd.palm oprc pdb pqa
application/vnd.paos.xml
application/vnd.pg.format str
application/vnd.pg.osasli ei6
@@ -1082,7 +1081,6 @@ chemical/x-ncbi-asn1 asn
chemical/x-ncbi-asn1-ascii ent prt
chemical/x-ncbi-asn1-binary aso val
chemical/x-ncbi-asn1-spec asn
-chemical/x-pdb ent pdb
chemical/x-rosdal ros
chemical/x-swissprot sw
chemical/x-vamas-iso14976 vms
@@ -1379,3 +1377,5 @@ application/x-cbr cbr
application/x-cb7 cb7
application/x-koboreader-ebook kobo
image/wmf wmf
+application/ereader pdb
+
diff --git a/resources/recipes/wsj.recipe b/resources/recipes/wsj.recipe
index 4ce315200c..eb473f1121 100644
--- a/resources/recipes/wsj.recipe
+++ b/resources/recipes/wsj.recipe
@@ -35,7 +35,7 @@ class WallStreetJournal(BasicNewsRecipe):
remove_tags_before = dict(name='h1')
remove_tags = [
- dict(id=["articleTabs_tab_article", "articleTabs_tab_comments", "articleTabs_tab_interactive","articleTabs_tab_video","articleTabs_tab_map","articleTabs_tab_slideshow"]),
+ dict(id=["articleTabs_tab_article", "articleTabs_tab_comments", "articleTabs_tab_interactive","articleTabs_tab_video","articleTabs_tab_map","articleTabs_tab_slideshow","articleTabs_tab_quotes","articleTabs_tab_document"]),
{'class':['footer_columns','network','insetCol3wide','interactive','video','slideshow','map','insettip','insetClose','more_in', "insetContent", 'articleTools_bottom', 'aTools', "tooltip", "adSummary", "nav-inline"]},
dict(rel='shortcut icon'),
]
@@ -101,7 +101,7 @@ class WallStreetJournal(BasicNewsRecipe):
title = 'Front Section'
url = 'http://online.wsj.com' + a['href']
feeds = self.wsj_add_feed(feeds,title,url)
- title = 'What''s News'
+ title = "What's News"
url = url.replace('pageone','whatsnews')
feeds = self.wsj_add_feed(feeds,title,url)
else:
diff --git a/resources/recipes/wsj_free.recipe b/resources/recipes/wsj_free.recipe
index df8234e8e2..a4a957fc90 100644
--- a/resources/recipes/wsj_free.recipe
+++ b/resources/recipes/wsj_free.recipe
@@ -10,7 +10,10 @@ class WallStreetJournal(BasicNewsRecipe):
title = 'Wall Street Journal (free)'
__author__ = 'Kovid Goyal, Sujata Raman, Joshua Oster-Morris, Starson17'
- description = 'News and current affairs'
+ description = '''News and current affairs. This recipe only fetches complete
+ versions of the articles that are available free on the wsj.com website.
+ To get the rest of the articles, subscribe to the WSJ and use the other WSJ
+ recipe.'''
language = 'en'
cover_url = 'http://dealbreaker.com/images/thumbs/Wall%20Street%20Journal%20A1.JPG'
max_articles_per_feed = 1000
@@ -151,6 +154,4 @@ class WallStreetJournal(BasicNewsRecipe):
return articles
- def cleanup(self):
- self.browser.open('http://online.wsj.com/logout?url=http://online.wsj.com')
diff --git a/src/calibre/debug.py b/src/calibre/debug.py
index e1c3e1809e..3a080fc57b 100644
--- a/src/calibre/debug.py
+++ b/src/calibre/debug.py
@@ -22,13 +22,15 @@ Run an embedded python interpreter.
parser.add_option('-d', '--debug-device-driver', default=False, action='store_true',
help='Debug the specified device driver.')
parser.add_option('-g', '--gui', default=False, action='store_true',
- help='Run the GUI',)
+ help='Run the GUI with debugging enabled. Debug output is '
+ 'printed to stdout and stderr.')
parser.add_option('--gui-debug', default=None,
help='Run the GUI with a debug console, logging to the'
- ' specified path',)
+ ' specified path. For internal use only, use the -g'
+ ' option to run the GUI in debug mode',)
parser.add_option('--show-gui-debug', default=None,
- help='Display the specified log file.',)
-
+ help='Display the specified log file. For internal use'
+ ' only.',)
parser.add_option('-w', '--viewer', default=False, action='store_true',
help='Run the ebook viewer',)
parser.add_option('--paths', default=False, action='store_true',
diff --git a/src/calibre/devices/interface.py b/src/calibre/devices/interface.py
index 2a92f46e8d..bc442f5853 100644
--- a/src/calibre/devices/interface.py
+++ b/src/calibre/devices/interface.py
@@ -35,6 +35,16 @@ class DevicePlugin(Plugin):
#: Height for thumbnails on the device
THUMBNAIL_HEIGHT = 68
+ #: Width for thumbnails on the device. Setting this will force thumbnails
+ #: to this size, not preserving aspect ratio. If it is not set, then
+ #: the aspect ratio will be preserved and the thumbnail will be no higher
+ #: than THUMBNAIL_HEIGHT
+ # THUMBNAIL_WIDTH = 68
+
+ #: Set this to True if the device supports updating cover thumbnails during
+ #: sync_booklists. Setting it to true will ask device.py to refresh the
+ #: cover thumbnails during book matching
+ WANTS_UPDATED_THUMBNAILS = False
#: Whether the metadata on books can be set via the GUI.
CAN_SET_METADATA = ['title', 'authors', 'collections']
diff --git a/src/calibre/devices/prs505/__init__.py b/src/calibre/devices/prs505/__init__.py
index 48b7d98123..1a59cb81a6 100644
--- a/src/calibre/devices/prs505/__init__.py
+++ b/src/calibre/devices/prs505/__init__.py
@@ -8,5 +8,5 @@ CACHE_XML = 'Sony Reader/database/cache.xml'
CACHE_EXT = 'Sony Reader/database/cacheExt.xml'
MEDIA_THUMBNAIL = 'database/thumbnail'
-CACHE_THUMBNAIL = 'Sony Reader/database/thumbnail'
+CACHE_THUMBNAIL = 'Sony Reader/thumbnail'
diff --git a/src/calibre/devices/prs505/driver.py b/src/calibre/devices/prs505/driver.py
index 0f6668891a..3768b8be62 100644
--- a/src/calibre/devices/prs505/driver.py
+++ b/src/calibre/devices/prs505/driver.py
@@ -81,12 +81,19 @@ class PRS505(USBMS):
_('Set this option to have separate book covers uploaded '
'every time you connect your device. Unset this option if '
'you have so many books on the reader that performance is '
- 'unacceptable.')
+ 'unacceptable.'),
+ _('Preserve cover aspect ratio when building thumbnails') +
+ ':::' +
+ _('Set this option if you want the cover thumbnails to have '
+ 'the same aspect ratio (width to height) as the cover. '
+ 'Unset it if you want the thumbnail to be the maximum size, '
+ 'ignoring aspect ratio.')
]
EXTRA_CUSTOMIZATION_DEFAULT = [
', '.join(['series', 'tags']),
False,
- False
+ False,
+ True
]
OPT_COLLECTIONS = 0
@@ -96,7 +103,7 @@ class PRS505(USBMS):
plugboard = None
plugboard_func = None
- THUMBNAIL_HEIGHT = 200
+ THUMBNAIL_HEIGHT = 217
MAX_PATH_LEN = 201 # 250 - (max(len(CACHE_THUMBNAIL), len(MEDIA_THUMBNAIL)) +
# len('main_thumbnail.jpg') + 1)
@@ -138,6 +145,13 @@ class PRS505(USBMS):
if not write_cache(self._card_b_prefix):
self._card_b_prefix = None
self.booklist_class.rebuild_collections = self.rebuild_collections
+ # Set the thumbnail width to the theoretical max if the user has asked
+ # that we do not preserve aspect ratio
+ if not self.settings().extra_customization[3]:
+ self.THUMBNAIL_WIDTH = 168
+ # Set WANTS_UPDATED_THUMBNAILS if the user has asked that thumbnails be
+ # updated on every connect
+ self.WANTS_UPDATED_THUMBNAILS = self.settings().extra_customization[2]
def get_device_information(self, end_session=True):
return (self.gui_name, '', '', '')
diff --git a/src/calibre/ebooks/conversion/cli.py b/src/calibre/ebooks/conversion/cli.py
index 278d599378..975507e2a7 100644
--- a/src/calibre/ebooks/conversion/cli.py
+++ b/src/calibre/ebooks/conversion/cli.py
@@ -46,7 +46,8 @@ HEURISTIC_OPTIONS = ['markup_chapter_headings',
'italicize_common_cases', 'fix_indents',
'html_unwrap_factor', 'unwrap_lines',
'delete_blank_paragraphs', 'format_scene_breaks',
- 'dehyphenate', 'renumber_headings']
+ 'dehyphenate', 'renumber_headings',
+ 'replace_scene_breaks']
def print_help(parser, log):
help = parser.format_help().encode(preferred_encoding, 'replace')
@@ -143,7 +144,7 @@ def add_pipeline_options(parser, plumber):
' patterns. Disabled by default. Use %s to enable. '
' Individual actions can be disabled with the %s options.')
% ('--enable-heuristics', '--disable-*'),
- ['enable_heuristics', 'replace_scene_breaks'] + HEURISTIC_OPTIONS
+ ['enable_heuristics'] + HEURISTIC_OPTIONS
),
'SEARCH AND REPLACE' : (
diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py
index a4708d398c..70b6ca657e 100644
--- a/src/calibre/ebooks/conversion/plumber.py
+++ b/src/calibre/ebooks/conversion/plumber.py
@@ -530,10 +530,11 @@ OptionRecommendation(name='format_scene_breaks',
help=_('Left aligned scene break markers are center aligned. '
'Replace soft scene breaks that use multiple blank lines with'
'horizontal rules.')),
-
+
OptionRecommendation(name='replace_scene_breaks',
recommended_value='', level=OptionRecommendation.LOW,
- help=_('Replace scene breaks with the specified text.')),
+ help=_('Replace scene breaks with the specified text. By default, the '
+ 'text from the input document is used.')),
OptionRecommendation(name='dehyphenate',
recommended_value=True, level=OptionRecommendation.LOW,
diff --git a/src/calibre/ebooks/conversion/utils.py b/src/calibre/ebooks/conversion/utils.py
index 21c6063f63..8a339afe4c 100644
--- a/src/calibre/ebooks/conversion/utils.py
+++ b/src/calibre/ebooks/conversion/utils.py
@@ -423,11 +423,11 @@ class HeuristicProcessor(object):
if getattr(self.extra_opts, option, False):
return True
return False
-
+
def merge_blanks(self, html, blanks_count=None):
base_em = .5 # Baseline is 1.5em per blank line, 1st line is .5 em css and 1em for the nbsp
em_per_line = 1.5 # Add another 1.5 em for each additional blank
-
+
def merge_matches(match):
to_merge = match.group(0)
lines = float(len(self.single_blank.findall(to_merge))) - 1.
@@ -437,17 +437,17 @@ class HeuristicProcessor(object):
else:
newline = self.any_multi_blank.sub('\n
', match.group(0))
return newline
-
+
html = self.any_multi_blank.sub(merge_matches, html)
return html
def detect_whitespace(self, html):
- blanks_around_headings = re.compile(r'(?P(]*>\s*
\s*){1,}\s*)?(?P\d+)[^>]*>.*?)(?P\s*(]*>\s*
\s*){1,})?', re.IGNORECASE)
+ blanks_around_headings = re.compile(r'(?P(]*>\s*
\s*){1,}\s*)?(?P\d+)[^>]*>.*?)(?P\s*(]*>\s*
\s*){1,})?', re.IGNORECASE)
blanks_n_nopunct = re.compile(r'(?P(]*>\s*
\s*){1,}\s*)?]*>\s*(<(span|[ibu]|em|strong|font)[^>]*>\s*)*.{1,100}?[^\W]((span|[ibu]|em|strong|font)>\s*)*
(?P\s*(]*>\s*
\s*){1,})?', re.IGNORECASE)
-
+
def merge_header_whitespace(match):
initblanks = match.group('initparas')
- endblanks = match.group('initparas')
+ endblanks = match.group('initparas')
heading = match.group('heading')
top_margin = ''
bottom_margin = ''
@@ -484,7 +484,7 @@ class HeuristicProcessor(object):
def markup_user_break(self, replacement_break):
'''
- Takes string a user supplies and wraps it in markup that will be centered with
+ Takes string a user supplies and wraps it in markup that will be centered with
appropriate margins.
and
tags are allowed. If the user specifies
a style with width attributes in the
tag then the appropriate margins are
applied to wrapping divs. This is because many ebook devices don't support margin:auto
@@ -499,10 +499,11 @@ class HeuristicProcessor(object):
hr_open = re.sub('45', str(divpercent), hr_open)
scene_break = hr_open+replacement_break+''
else:
- scene_break = hr_open+'
'
+ scene_break = hr_open+'
'
elif re.match('^
'
else:
+ from calibre.utils.html2text import html2text
replacement_break = html2text(replacement_break)
replacement_break = re.sub('\s', ' ', replacement_break)
scene_break = self.scene_break_open+replacement_break+''
@@ -646,11 +647,11 @@ class HeuristicProcessor(object):
if len(scene_break.findall(html)) >= 1:
html = scene_break.sub(replacement_break, html)
else:
- html = re.sub(']*>\s*
', replacement_break, html)
+ html = re.sub(']*>\s*
', replacement_break, html)
else:
html = scene_break.sub(self.scene_break_open+'\g'+'', html)
if self.deleted_nbsps:
# put back non-breaking spaces in empty paragraphs so they render correctly
html = self.anyblank.sub('\n'+r'\g'+u'\u00a0'+r'\g', html)
- return html
\ No newline at end of file
+ return html
diff --git a/src/calibre/ebooks/metadata/opf2.py b/src/calibre/ebooks/metadata/opf2.py
index 62d57f2251..dfb902b5b9 100644
--- a/src/calibre/ebooks/metadata/opf2.py
+++ b/src/calibre/ebooks/metadata/opf2.py
@@ -422,6 +422,33 @@ class MetadataField(object):
elem = obj.create_metadata_element(self.name, is_dc=self.is_dc)
obj.set_text(elem, self.renderer(val))
+class TitleSortField(MetadataField):
+
+ def __get__(self, obj, type=None):
+ c = self.__real_get__(obj, type)
+ if c is None:
+ matches = obj.title_path(obj.metadata)
+ if matches:
+ for match in matches:
+ ans = match.get('{%s}file-as'%obj.NAMESPACES['opf'], None)
+ if not ans:
+ ans = match.get('file-as', None)
+ if ans:
+ c = ans
+ if not c:
+ c = self.none_is
+ else:
+ c = c.strip()
+ return c
+
+ def __set__(self, obj, val):
+ MetadataField.__set__(self, obj, val)
+ matches = obj.title_path(obj.metadata)
+ if matches:
+ for match in matches:
+ for attr in list(match.attrib):
+ if attr.endswith('file-as'):
+ del match.attrib[attr]
def serialize_user_metadata(metadata_elem, all_user_metadata, tail='\n'+(' '*8)):
from calibre.utils.config import to_json
@@ -490,6 +517,7 @@ class OPF(object): # {{{
rights = MetadataField('rights')
series = MetadataField('series', is_dc=False)
series_index = MetadataField('series_index', is_dc=False, formatter=float, none_is=1)
+ title_sort = TitleSortField('title_sort', is_dc=False)
rating = MetadataField('rating', is_dc=False, formatter=int)
pubdate = MetadataField('date', formatter=parse_date,
renderer=isoformat)
@@ -776,30 +804,6 @@ class OPF(object): # {{{
return property(fget=fget, fset=fset)
- @dynamic_property
- def title_sort(self):
-
- def fget(self):
- matches = self.title_path(self.metadata)
- if matches:
- for match in matches:
- ans = match.get('{%s}file-as'%self.NAMESPACES['opf'], None)
- if not ans:
- ans = match.get('file-as', None)
- if ans:
- return ans
-
- def fset(self, val):
- matches = self.title_path(self.metadata)
- if matches:
- for key in matches[0].attrib:
- if key.endswith('file-as'):
- matches[0].attrib.pop(key)
- matches[0].set('{%s}file-as'%self.NAMESPACES['opf'], unicode(val))
-
- return property(fget=fget, fset=fset)
-
-
@dynamic_property
def tags(self):
@@ -1129,8 +1133,6 @@ class OPFCreator(Metadata):
metadata = M.metadata()
a = metadata.append
role = {}
- if self.title_sort:
- role = {'file-as':self.title_sort}
a(DC_ELEM('title', self.title if self.title else _('Unknown'),
opf_attrs=role))
for i, author in enumerate(self.authors):
@@ -1165,6 +1167,8 @@ class OPFCreator(Metadata):
a(CAL_ELEM('calibre:series', self.series))
if self.series_index is not None:
a(CAL_ELEM('calibre:series_index', self.format_series_index()))
+ if self.title_sort:
+ a(CAL_ELEM('calibre:title_sort', self.title_sort))
if self.rating is not None:
a(CAL_ELEM('calibre:rating', str(self.rating)))
if self.timestamp is not None:
@@ -1320,7 +1324,6 @@ def test_m2o():
mi.author_sort = 'author sort'
mi.pubdate = nowf()
mi.language = 'en'
- mi.category = 'test'
mi.comments = 'what a fun book\n\n'
mi.publisher = 'publisher'
mi.isbn = 'boooo'
@@ -1335,11 +1338,11 @@ def test_m2o():
opf = metadata_to_opf(mi)
print opf
newmi = MetaInformation(OPF(StringIO(opf)))
- for attr in ('author_sort', 'title_sort', 'comments', 'category',
+ for attr in ('author_sort', 'title_sort', 'comments',
'publisher', 'series', 'series_index', 'rating',
'isbn', 'tags', 'cover_data', 'application_id',
'language', 'cover',
- 'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc',
+ 'book_producer', 'timestamp',
'pubdate', 'rights', 'publication_type'):
o, n = getattr(mi, attr), getattr(newmi, attr)
if o != n and o.strip() != n.strip():
@@ -1441,4 +1444,6 @@ def test_user_metadata():
print opf.render()
if __name__ == '__main__':
- test_user_metadata()
+ #test_user_metadata()
+ #test_m2o()
+ test()
diff --git a/src/calibre/ebooks/metadata/sources/google.py b/src/calibre/ebooks/metadata/sources/google.py
index 1a3bf6d516..d9efb65ae0 100644
--- a/src/calibre/ebooks/metadata/sources/google.py
+++ b/src/calibre/ebooks/metadata/sources/google.py
@@ -65,7 +65,7 @@ def to_metadata(browser, log, entry_):
mi = Metadata(title_, authors)
try:
- raw = browser.open(id_url).read()
+ raw = browser.open_novisit(id_url).read()
feed = etree.fromstring(raw)
extra = entry(feed)[0]
except:
@@ -129,7 +129,7 @@ class Worker(Thread):
for i in self.entries:
try:
ans = to_metadata(self.browser, self.log, i)
- if ans is not None:
+ if isinstance(ans, Metadata):
self.result_queue.put(ans)
except:
self.log.exception(
diff --git a/src/calibre/ebooks/mobi/reader.py b/src/calibre/ebooks/mobi/reader.py
index 0ae3c9ac9d..9576ccb637 100644
--- a/src/calibre/ebooks/mobi/reader.py
+++ b/src/calibre/ebooks/mobi/reader.py
@@ -103,6 +103,8 @@ class EXTHHeader(object):
pass
elif id == 108:
pass # Producer
+ elif id == 113:
+ pass # ASIN or UUID
#else:
# print 'unhandled metadata record', id, repr(content)
diff --git a/src/calibre/ebooks/mobi/writer.py b/src/calibre/ebooks/mobi/writer.py
index 2a71ecd43b..abba173d69 100644
--- a/src/calibre/ebooks/mobi/writer.py
+++ b/src/calibre/ebooks/mobi/writer.py
@@ -1547,6 +1547,31 @@ class MobiWriter(object):
rights = 'Unknown'
exth.write(pack('>II', EXTH_CODES['rights'], len(rights) + 8))
exth.write(rights)
+ nrecs += 1
+
+ # Write UUID as ASIN
+ uuid = None
+ from calibre.ebooks.oeb.base import OPF
+ for x in oeb.metadata['identifier']:
+ if x.get(OPF('scheme'), None).lower() == 'uuid' or unicode(x).startswith('urn:uuid:'):
+ uuid = unicode(x).split(':')[-1]
+ break
+ if uuid is None:
+ from uuid import uuid4
+ uuid = str(uuid4())
+
+ if isinstance(uuid, unicode):
+ uuid = uuid.encode('utf-8')
+ exth.write(pack('>II', 113, len(uuid) + 8))
+ exth.write(uuid)
+ nrecs += 1
+
+ # Write cdetype
+ if not self.opts.mobi_periodical:
+ data = 'EBOK'
+ exth.write(pack('>II', 501, len(data)+8))
+ exth.write(data)
+ nrecs += 1
# Add a publication date entry
if oeb.metadata['date'] != [] :
diff --git a/src/calibre/gui2/complete.py b/src/calibre/gui2/complete.py
new file mode 100644
index 0000000000..ce8609fc99
--- /dev/null
+++ b/src/calibre/gui2/complete.py
@@ -0,0 +1,357 @@
+#!/usr/bin/env python
+# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
+
+__license__ = 'GPL v3'
+__copyright__ = '2011, Kovid Goyal '
+__docformat__ = 'restructuredtext en'
+
+
+from PyQt4.Qt import QLineEdit, QListView, QAbstractListModel, Qt, QTimer, \
+ QApplication, QPoint, QItemDelegate, QStyleOptionViewItem, \
+ QStyle, QEvent, pyqtSignal
+
+from calibre.utils.icu import sort_key, lower
+from calibre.gui2 import NONE
+from calibre.gui2.widgets import EnComboBox
+
+class CompleterItemDelegate(QItemDelegate): # {{{
+
+ ''' Renders the current item as thought it were selected '''
+
+ def __init__(self, view):
+ self.view = view
+ QItemDelegate.__init__(self, view)
+
+ def paint(self, p, opt, idx):
+ opt = QStyleOptionViewItem(opt)
+ opt.showDecorationSelected = True
+ if self.view.currentIndex() == idx:
+ opt.state |= QStyle.State_HasFocus
+ QItemDelegate.paint(self, p, opt, idx)
+
+# }}}
+
+class CompleteWindow(QListView): # {{{
+
+ '''
+ The completion popup. For keyboard and mouse handling see
+ :meth:`eventFilter`.
+ '''
+
+ #: This signal is emitted when the user selects one of the listed
+ #: completions, by mouse or keyboard
+ completion_selected = pyqtSignal(object)
+
+ def __init__(self, widget, model):
+ self.widget = widget
+ QListView.__init__(self)
+ self.setVisible(False)
+ self.setParent(None, Qt.Popup)
+ self.setAlternatingRowColors(True)
+ self.setFocusPolicy(Qt.NoFocus)
+ self._d = CompleterItemDelegate(self)
+ self.setItemDelegate(self._d)
+ self.setModel(model)
+ self.setFocusProxy(widget)
+ self.installEventFilter(self)
+ self.clicked.connect(self.do_selected)
+ self.entered.connect(self.do_entered)
+ self.setMouseTracking(True)
+
+ def do_entered(self, idx):
+ if idx.isValid():
+ self.setCurrentIndex(idx)
+
+ def do_selected(self, idx=None):
+ idx = self.currentIndex() if idx is None else idx
+ if not idx.isValid() and self.model().rowCount() > 0:
+ idx = self.model().index(0)
+ if idx.isValid():
+ data = unicode(self.model().data(idx, Qt.DisplayRole))
+ self.completion_selected.emit(data)
+ self.hide()
+
+ def eventFilter(self, o, e):
+ if o is not self:
+ return False
+ if e.type() == e.KeyPress:
+ key = e.key()
+ if key in (Qt.Key_Escape, Qt.Key_Backtab) or \
+ (key == Qt.Key_F4 and (e.modifiers() & Qt.AltModifier)):
+ self.hide()
+ return True
+ elif key in (Qt.Key_Enter, Qt.Key_Return, Qt.Key_Tab):
+ self.do_selected()
+ return True
+ elif key in (Qt.Key_Up, Qt.Key_Down, Qt.Key_PageUp,
+ Qt.Key_PageDown):
+ return False
+ # Send key event to associated line edit
+ self.widget.eat_focus_out = False
+ try:
+ self.widget.event(e)
+ finally:
+ self.widget.eat_focus_out = True
+ if not self.widget.hasFocus():
+ # Line edit lost focus
+ self.hide()
+ if e.isAccepted():
+ # Line edit consumed event
+ return True
+ elif e.type() == e.MouseButtonPress:
+ # Hide popup if user clicks outside it, otherwise
+ # pass event to popup
+ if not self.underMouse():
+ self.hide()
+ return True
+ elif e.type() in (e.InputMethod, e.ShortcutOverride):
+ QApplication.sendEvent(self.widget, e)
+
+ return False # Do not filter this event
+
+# }}}
+
+class CompleteModel(QAbstractListModel):
+
+ def __init__(self, parent=None):
+ QAbstractListModel.__init__(self, parent)
+ self.sep = ','
+ self.space_before_sep = False
+ self.items = []
+ self.lowered_items = []
+ self.matches = []
+
+ def set_items(self, items):
+ items = [unicode(x.strip()) for x in items]
+ self.items = list(sorted(items, key=lambda x: sort_key(x)))
+ self.lowered_items = [lower(x) for x in self.items]
+ self.matches = []
+ self.reset()
+
+ def rowCount(self, *args):
+ return len(self.matches)
+
+ def data(self, index, role):
+ if role == Qt.DisplayRole:
+ r = index.row()
+ try:
+ return self.matches[r]
+ except IndexError:
+ pass
+ return NONE
+
+ def get_matches(self, prefix):
+ '''
+ Return all matches that (case insensitively) start with prefix
+ '''
+ prefix = lower(prefix)
+ ans = []
+ if prefix:
+ for i, test in enumerate(self.lowered_items):
+ if test.startswith(prefix):
+ ans.append(self.items[i])
+ return ans
+
+ def update_matches(self, matches):
+ self.matches = matches
+ self.reset()
+
+class MultiCompleteLineEdit(QLineEdit):
+ '''
+ A line edit that completes on multiple items separated by a
+ separator. Use the :meth:`update_items_cache` to set the list of
+ all possible completions. Separator can be controlled with the
+ :meth:`set_separator` and :meth:`set_space_before_sep` methods.
+
+ A call to self.set_separator(None) will allow this widget to be used
+ to complete non multiple fields as well.
+ '''
+
+ def __init__(self, parent=None):
+ self.eat_focus_out = True
+ self.max_visible_items = 7
+ self.current_prefix = None
+ QLineEdit.__init__(self, parent)
+
+ self._model = CompleteModel(parent=self)
+ self.complete_window = CompleteWindow(self, self._model)
+ self.textChanged.connect(self.text_changed)
+ self.cursorPositionChanged.connect(self.cursor_position_changed)
+ self.complete_window.completion_selected.connect(self.completion_selected)
+
+ # Interface {{{
+ def update_items_cache(self, complete_items):
+ self.all_items = complete_items
+
+ def set_separator(self, sep):
+ self.sep = sep
+
+ def set_space_before_sep(self, space_before):
+ self.space_before_sep = space_before
+
+ # }}}
+
+ def eventFilter(self, o, e):
+ if self.eat_focus_out and o is self and e.type() == QEvent.FocusOut:
+ if self.complete_window.isVisible():
+ return True # Filter this event since the cw is visible
+ return QLineEdit.eventFilter(self, o, e)
+
+
+ def text_changed(self, *args):
+ self.update_completions()
+
+ def cursor_position_changed(self, *args):
+ self.update_completions()
+
+ def update_completions(self):
+ ' Update the list of completions '
+ cpos = self.cursorPosition()
+ text = unicode(self.text())
+ prefix = text[:cpos]
+ self.current_prefix = prefix
+ complete_prefix = prefix.lstrip()
+ if self.sep:
+ complete_prefix = prefix = prefix.split(self.sep)[-1].lstrip()
+
+ matches = self._model.get_matches(complete_prefix)
+ self.update_complete_window(matches)
+
+ def get_completed_text(self, text):
+ '''
+ Get completed text from current cursor position and the completion
+ text
+ '''
+ if self.sep is None:
+ return text
+ else:
+ cursor_pos = self.cursorPosition()
+ before_text = unicode(self.text())[:cursor_pos]
+ after_text = unicode(self.text())[cursor_pos:]
+ after_parts = after_text.split(self.sep)
+ if len(after_parts) < 3 and not after_parts[-1].strip():
+ after_text = u''
+ prefix_len = len(before_text.split(self.sep)[-1].lstrip())
+ if self.space_before_sep:
+ complete_text_pat = '%s%s %s %s'
+ len_extra = 3
+ else:
+ complete_text_pat = '%s%s%s %s'
+ len_extra = 2
+ return prefix_len, len_extra, complete_text_pat % (
+ before_text[:cursor_pos - prefix_len], text, self.sep, after_text)
+
+ def completion_selected(self, text):
+ prefix_len, len_extra, ctext = self.get_completed_text(text)
+ if self.sep is None:
+ self.setText(ctext)
+ self.setCursorPosition(len(ctext))
+ else:
+ cursor_pos = self.cursorPosition()
+ self.setText(ctext)
+ self.setCursorPosition(cursor_pos - prefix_len + len(text) + len_extra)
+
+ def update_complete_window(self, matches):
+ self._model.update_matches(matches)
+ if matches:
+ self.show_complete_window()
+ else:
+ self.complete_window.hide()
+
+
+ def position_complete_window(self):
+ popup = self.complete_window
+ screen = QApplication.desktop().availableGeometry(self)
+ h = (popup.sizeHintForRow(0) * min(self.max_visible_items,
+ popup.model().rowCount()) + 3) + 3
+ hsb = popup.horizontalScrollBar()
+ if hsb and hsb.isVisible():
+ h += hsb.sizeHint().height()
+
+ rh = self.height()
+ pos = self.mapToGlobal(QPoint(0, self.height() - 2))
+ w = self.width()
+
+ if w > screen.width():
+ w = screen.width()
+ if (pos.x() + w) > (screen.x() + screen.width()):
+ pos.setX(screen.x() + screen.width() - w)
+ if (pos.x() < screen.x()):
+ pos.setX(screen.x())
+
+ top = pos.y() - rh - screen.top() + 2
+ bottom = screen.bottom() - pos.y()
+ h = max(h, popup.minimumHeight())
+ if h > bottom:
+ h = min(max(top, bottom), h)
+ if top > bottom:
+ pos.setY(pos.y() - h - rh + 2)
+
+ popup.setGeometry(pos.x(), pos.y(), w, h)
+
+
+ def show_complete_window(self):
+ self.position_complete_window()
+ self.complete_window.show()
+
+ def moveEvent(self, ev):
+ ret = QLineEdit.moveEvent(self, ev)
+ QTimer.singleShot(0, self.position_complete_window)
+ return ret
+
+ def resizeEvent(self, ev):
+ ret = QLineEdit.resizeEvent(self, ev)
+ QTimer.singleShot(0, self.position_complete_window)
+ return ret
+
+
+ @dynamic_property
+ def all_items(self):
+ def fget(self):
+ return self._model.items
+ def fset(self, items):
+ self._model.set_items(items)
+ return property(fget=fget, fset=fset)
+
+ @dynamic_property
+ def sep(self):
+ def fget(self):
+ return self._model.sep
+ def fset(self, val):
+ self._model.sep = val
+ return property(fget=fget, fset=fset)
+
+ @dynamic_property
+ def space_before_sep(self):
+ def fget(self):
+ return self._model.space_before_sep
+ def fset(self, val):
+ self._model.space_before_sep = val
+ return property(fget=fget, fset=fset)
+
+class MultiCompleteComboBox(EnComboBox):
+
+ def __init__(self, *args):
+ EnComboBox.__init__(self, *args)
+ self.setLineEdit(MultiCompleteLineEdit(self))
+
+ def update_items_cache(self, complete_items):
+ self.lineEdit().update_items_cache(complete_items)
+
+ def set_separator(self, sep):
+ self.lineEdit().set_separator(sep)
+
+ def set_space_before_sep(self, space_before):
+ self.lineEdit().set_space_before_sep(space_before)
+
+
+
+if __name__ == '__main__':
+ from PyQt4.Qt import QDialog, QVBoxLayout
+ app = QApplication([])
+ d = QDialog()
+ d.setLayout(QVBoxLayout())
+ le = MultiCompleteLineEdit(d)
+ d.layout().addWidget(le)
+ le.all_items = ['one', 'otwo', 'othree', 'ooone', 'ootwo', 'oothree']
+ d.exec_()
diff --git a/src/calibre/gui2/convert/heuristics.py b/src/calibre/gui2/convert/heuristics.py
index 18a6b8b5fe..77fadf059c 100644
--- a/src/calibre/gui2/convert/heuristics.py
+++ b/src/calibre/gui2/convert/heuristics.py
@@ -27,7 +27,8 @@ class HeuristicsWidget(Widget, Ui_Form):
'dehyphenate', 'renumber_headings']
)
self.db, self.book_id = db, book_id
- self.rssb_defaults = [u'', u'
', u'* * *', u'• • •', u'✦ ✦ ✦', u'✮ ✮ ✮', 'u☆ ☆ ☆', u'❂ ❂ ❂', u'✣ ✣ ✣', u'❖ ❖ ❖', u'☼ ☼ ☼', u'✠ ✠ ✠']
+ self.rssb_defaults = [u'', u'
', u'* * *', u'• • •', u'✦ ✦ ✦',
+ u'✮ ✮ ✮', u'☆ ☆ ☆', u'❂ ❂ ❂', u'✣ ✣ ✣', u'❖ ❖ ❖', u'☼ ☼ ☼', u'✠ ✠ ✠']
self.initialize_options(get_option, get_help, db, book_id)
self.load_histories()
@@ -39,7 +40,7 @@ class HeuristicsWidget(Widget, Ui_Form):
def restore_defaults(self, get_option):
Widget.restore_defaults(self, get_option)
-
+
self.save_histories()
rssb_hist = gprefs['replace_scene_breaks_history']
for x in self.rssb_defaults:
@@ -50,7 +51,7 @@ class HeuristicsWidget(Widget, Ui_Form):
def commit_options(self, save_defaults=False):
self.save_histories()
-
+
return Widget.commit_options(self, save_defaults)
def break_cycles(self):
@@ -73,7 +74,7 @@ class HeuristicsWidget(Widget, Ui_Form):
def load_histories(self):
self.opt_replace_scene_breaks.clear()
self.opt_replace_scene_breaks.lineEdit().setText('')
-
+
val = unicode(self.opt_replace_scene_breaks.currentText())
rssb_hist = gprefs.get('replace_scene_breaks_history', self.rssb_defaults)
if val in rssb_hist:
diff --git a/src/calibre/gui2/convert/heuristics.ui b/src/calibre/gui2/convert/heuristics.ui
index 4f7cf5ea6e..46d62061af 100644
--- a/src/calibre/gui2/convert/heuristics.ui
+++ b/src/calibre/gui2/convert/heuristics.ui
@@ -164,7 +164,10 @@
- Replace soft scene breaks:
+ Replace soft scene &breaks:
+
+
+ opt_replace_scene_breaks
diff --git a/src/calibre/gui2/convert/structure_detection.ui b/src/calibre/gui2/convert/structure_detection.ui
index ef0677a67c..f80e6f8182 100644
--- a/src/calibre/gui2/convert/structure_detection.ui
+++ b/src/calibre/gui2/convert/structure_detection.ui
@@ -48,10 +48,10 @@
- -
+
-
- -
+
-
Qt::Vertical
@@ -77,6 +77,16 @@
+ -
+
+
+ The header and footer removal options have been replaced by the Search & Replace options. Click the Search & Replace category in the bar to the left to use these options. Leave the replace field blank and enter your header/footer removal regexps into the search field.
+
+
+ true
+
+
+
diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py
index a5066a99ef..8efa7f154c 100644
--- a/src/calibre/gui2/device.py
+++ b/src/calibre/gui2/device.py
@@ -838,9 +838,10 @@ class DeviceMixin(object): # {{{
format_count[f] = 1
for f in self.device_manager.device.settings().format_map:
if f in format_count.keys():
- formats.append((f, _('%i of %i Books') % (format_count[f], len(rows))), True if f in aval_out_formats else False)
+ formats.append((f, _('%i of %i Books') % (format_count[f],
+ len(rows)), True if f in aval_out_formats else False))
elif f in aval_out_formats:
- formats.append((f, _('0 of %i Books') % len(rows)), True)
+ formats.append((f, _('0 of %i Books') % len(rows), True))
d = ChooseFormatDeviceDialog(self, _('Choose format to send to device'), formats)
if d.exec_() != QDialog.Accepted:
return
@@ -871,6 +872,16 @@ class DeviceMixin(object): # {{{
self.send_by_mail(to, fmts, delete)
def cover_to_thumbnail(self, data):
+ if self.device_manager.device and \
+ hasattr(self.device_manager.device, 'THUMBNAIL_WIDTH'):
+ try:
+ return thumbnail(data,
+ self.device_manager.device.THUMBNAIL_WIDTH,
+ self.device_manager.device.THUMBNAIL_HEIGHT,
+ preserve_aspect_ratio=False)
+ except:
+ pass
+ return
ht = self.device_manager.device.THUMBNAIL_HEIGHT \
if self.device_manager else DevicePlugin.THUMBNAIL_HEIGHT
try:
@@ -1272,6 +1283,8 @@ class DeviceMixin(object): # {{{
x = x.lower() if x else ''
return string_pat.sub('', x)
+ update_metadata = prefs['manage_device_metadata'] == 'on_connect'
+
# Force a reset if the caches are not initialized
if reset or not hasattr(self, 'db_book_title_cache'):
# Build a cache (map) of the library, so the search isn't On**2
@@ -1284,8 +1297,13 @@ class DeviceMixin(object): # {{{
except:
return False
+ get_covers = False
+ if update_metadata and self.device_manager.is_device_connected:
+ if self.device_manager.device.WANTS_UPDATED_THUMBNAILS:
+ get_covers = True
+
for id in db.data.iterallids():
- mi = db.get_metadata(id, index_is_id=True)
+ mi = db.get_metadata(id, index_is_id=True, get_cover=get_covers)
title = clean_string(mi.title)
if title not in db_book_title_cache:
db_book_title_cache[title] = \
@@ -1311,7 +1329,6 @@ class DeviceMixin(object): # {{{
# the application_id to the db_id of the matching book. This value
# will be used by books_on_device to indicate matches.
- update_metadata = prefs['manage_device_metadata'] == 'on_connect'
for booklist in booklists:
for book in booklist:
book.in_library = None
@@ -1382,6 +1399,12 @@ class DeviceMixin(object): # {{{
if update_metadata:
if self.device_manager.is_device_connected:
+ if self.device_manager.device.WANTS_UPDATED_THUMBNAILS:
+ for blist in booklists:
+ for book in blist:
+ if book.cover and os.access(book.cover, os.R_OK):
+ book.thumbnail = \
+ self.cover_to_thumbnail(open(book.cover, 'rb').read())
plugboards = self.library_view.model().db.prefs.get('plugboards', {})
self.device_manager.sync_booklists(
Dispatcher(self.metadata_synced), booklists,
diff --git a/src/calibre/gui2/email.py b/src/calibre/gui2/email.py
index 6b2ed81413..426747e044 100644
--- a/src/calibre/gui2/email.py
+++ b/src/calibre/gui2/email.py
@@ -264,8 +264,9 @@ class EmailMixin(object): # {{{
if _auto_ids != []:
for id in _auto_ids:
if specific_format == None:
- formats = [f.lower() for f in self.library_view.model().db.formats(id, index_is_id=True).split(',')]
- formats = formats if formats != None else []
+ dbfmts = self.library_view.model().db.formats(id, index_is_id=True)
+ formats = [f.lower() for f in (dbfmts.split(',') if fmts else
+ [])]
if list(set(formats).intersection(available_input_formats())) != [] and list(set(fmts).intersection(available_output_formats())) != []:
auto.append(id)
else:
diff --git a/src/calibre/gui2/metadata/basic_widgets.py b/src/calibre/gui2/metadata/basic_widgets.py
index d3fa5958ab..8ec037278e 100644
--- a/src/calibre/gui2/metadata/basic_widgets.py
+++ b/src/calibre/gui2/metadata/basic_widgets.py
@@ -12,8 +12,8 @@ from PyQt4.Qt import Qt, QDateEdit, QDate, \
QDoubleSpinBox, QListWidgetItem, QSize, QPixmap, \
QPushButton, QSpinBox, QLineEdit
-from calibre.gui2.widgets import EnLineEdit, CompleteComboBox, \
- EnComboBox, FormatList, ImageView, CompleteLineEdit
+from calibre.gui2.widgets import EnLineEdit, EnComboBox, FormatList, ImageView
+from calibre.gui2.complete import MultiCompleteLineEdit, MultiCompleteComboBox
from calibre.utils.icu import sort_key
from calibre.utils.config import tweaks, prefs
from calibre.ebooks.metadata import title_sort, authors_to_string, \
@@ -149,14 +149,14 @@ class TitleSortEdit(TitleEdit):
# }}}
# Authors {{{
-class AuthorsEdit(CompleteComboBox):
+class AuthorsEdit(MultiCompleteComboBox):
TOOLTIP = ''
LABEL = _('&Author(s):')
def __init__(self, parent):
self.dialog = parent
- CompleteComboBox.__init__(self, parent)
+ MultiCompleteComboBox.__init__(self, parent)
self.setToolTip(self.TOOLTIP)
self.setWhatsThis(self.TOOLTIP)
self.setEditable(True)
@@ -814,14 +814,14 @@ class RatingEdit(QSpinBox): # {{{
# }}}
-class TagsEdit(CompleteLineEdit): # {{{
+class TagsEdit(MultiCompleteLineEdit): # {{{
LABEL = _('Ta&gs:')
TOOLTIP = ''+_('Tags categorize the book. This is particularly '
'useful while searching.
They can be any words'
'or phrases, separated by commas.')
def __init__(self, parent):
- CompleteLineEdit.__init__(self, parent)
+ MultiCompleteLineEdit.__init__(self, parent)
self.setToolTip(self.TOOLTIP)
self.setWhatsThis(self.TOOLTIP)
@@ -839,7 +839,7 @@ class TagsEdit(CompleteLineEdit): # {{{
tags = db.tags(id_, index_is_id=True)
tags = tags.split(',') if tags else []
self.current_val = tags
- self.update_items_cache(db.all_tags())
+ self.all_items = db.all_tags()
self.original_val = self.current_val
@property
@@ -860,7 +860,7 @@ class TagsEdit(CompleteLineEdit): # {{{
d = TagEditor(self, db, id_)
if d.exec_() == TagEditor.Accepted:
self.current_val = d.tags
- self.update_items_cache(db.all_tags())
+ self.all_items = db.all_tags()
def commit(self, db, id_):
diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py
index 3fc16e99b4..bfe54df36e 100644
--- a/src/calibre/library/database2.py
+++ b/src/calibre/library/database2.py
@@ -430,8 +430,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
authors = self.authors(id, index_is_id=True)
if not authors:
authors = _('Unknown')
- author = ascii_filename(authors.split(',')[0][:self.PATH_LIMIT]).decode(filesystem_encoding, 'ignore')
- title = ascii_filename(self.title(id, index_is_id=True)[:self.PATH_LIMIT]).decode(filesystem_encoding, 'ignore')
+ author = ascii_filename(authors.split(',')[0])[:self.PATH_LIMIT].decode(filesystem_encoding, 'replace')
+ title = ascii_filename(self.title(id, index_is_id=True))[:self.PATH_LIMIT].decode(filesystem_encoding, 'replace')
path = author + '/' + title + ' (%d)'%id
return path
@@ -442,8 +442,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
authors = self.authors(id, index_is_id=True)
if not authors:
authors = _('Unknown')
- author = ascii_filename(authors.split(',')[0][:self.PATH_LIMIT]).decode(filesystem_encoding, 'replace')
- title = ascii_filename(self.title(id, index_is_id=True)[:self.PATH_LIMIT]).decode(filesystem_encoding, 'replace')
+ author = ascii_filename(authors.split(',')[0])[:self.PATH_LIMIT].decode(filesystem_encoding, 'replace')
+ title = ascii_filename(self.title(id, index_is_id=True))[:self.PATH_LIMIT].decode(filesystem_encoding, 'replace')
name = title + ' - ' + author
while name.endswith('.'):
name = name[:-1]
diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst
index 59f6a9b88d..18c53ade5d 100644
--- a/src/calibre/manual/faq.rst
+++ b/src/calibre/manual/faq.rst
@@ -391,7 +391,7 @@ Take your pick:
* A tribute to the SONY Librie which was the first e-ink based e-book reader
* My wife chose it ;-)
-|app| is pronounced as cal-i-ber *not* ca-libre. If you're wondering, |app| is the British/commonwealth spelling for caliber. Being Indian, that's the natural spelling for me.
+|app| is pronounced as cal-i-ber *not* ca-li-bre. If you're wondering, |app| is the British/commonwealth spelling for caliber. Being Indian, that's the natural spelling for me.
Why does |app| show only some of my fonts on OS X?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/src/calibre/utils/magick/draw.py b/src/calibre/utils/magick/draw.py
index ad4b681b43..04cce5efe3 100644
--- a/src/calibre/utils/magick/draw.py
+++ b/src/calibre/utils/magick/draw.py
@@ -72,11 +72,17 @@ def save_cover_data_to(data, path, bgcolor='#ffffff', resize_to=None,
f.write(data)
return ret
-def thumbnail(data, width=120, height=120, bgcolor='#ffffff', fmt='jpg'):
+def thumbnail(data, width=120, height=120, bgcolor='#ffffff', fmt='jpg',
+ preserve_aspect_ratio=True):
img = Image()
img.load(data)
owidth, oheight = img.size
- scaled, nwidth, nheight = fit_image(owidth, oheight, width, height)
+ if not preserve_aspect_ratio:
+ scaled = owidth > width or oheight > height
+ nwidth = width
+ nheight = height
+ else:
+ scaled, nwidth, nheight = fit_image(owidth, oheight, width, height)
if scaled:
img.size = (nwidth, nheight)
canvas = create_canvas(img.size[0], img.size[1], bgcolor)