mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Merge from trunk
This commit is contained in:
commit
ff17ffd514
114
Changelog.yaml
114
Changelog.yaml
@ -4,6 +4,120 @@
|
|||||||
# for important features/bug fixes.
|
# for important features/bug fixes.
|
||||||
# Also, each release can have new and improved recipes.
|
# Also, each release can have new and improved recipes.
|
||||||
|
|
||||||
|
- version: 0.7.41
|
||||||
|
date: 2011-01-21
|
||||||
|
|
||||||
|
new features:
|
||||||
|
- title: "Conversions: Replace the remove header/footer options with a more geenric search replace option, that allows you to not only remove but also replace text"
|
||||||
|
|
||||||
|
- title: "Conversion: The preprocess html option has now become a new 'Heuristic Processing' option which allows you to control exactly which heuristics are used"
|
||||||
|
|
||||||
|
- title: "Conversion: Various improvements to Heuristic Processing (used to be preprocess HTML)"
|
||||||
|
|
||||||
|
- title: "When adding empty books to calibre, optionally set the author to the author of the currently selected book"
|
||||||
|
tickets: [7702]
|
||||||
|
|
||||||
|
- title: "Device drivers for the Archos 101, SmatQ T7 and Acer Lumiread"
|
||||||
|
|
||||||
|
- title: "Catalog generation: Make By Authors optional"
|
||||||
|
|
||||||
|
- title: "Allow bulk editing of Date and Published columns."
|
||||||
|
|
||||||
|
- title: "Add a little button to clear date and published values to the edit metadata dialogs"
|
||||||
|
|
||||||
|
- title: "When adding books by ISBN, allow the specification of special tags that will be added to the new book entries"
|
||||||
|
tickets: [8436]
|
||||||
|
|
||||||
|
- title: "Completion on multiple authors"
|
||||||
|
tickets: [8405]
|
||||||
|
|
||||||
|
- title: "Add AZW to default list of internally viewed formats, a I am tired of getting tickets about it"
|
||||||
|
|
||||||
|
- title: "Nicer error message when catalog generation fails"
|
||||||
|
|
||||||
|
- title: "Add capitalize option to context menus in the edit metadata dialog"
|
||||||
|
|
||||||
|
bug fixes:
|
||||||
|
- title: "RTF Input: Fix regression in 0.7.40 that broke conversion of some old style RTF files"
|
||||||
|
|
||||||
|
- title: "Fix Tag editor forgets position"
|
||||||
|
tickets: [8271]
|
||||||
|
|
||||||
|
- title: "When converting books in the calibre GUI, override metadata from the input document, even when empty."
|
||||||
|
description: >
|
||||||
|
"So if you have removed all the tags and comments in the calibre GUI for the book in the calibre GUI, but the actual file that is being converted still has tags and comments, they are ignored. This affects only conversions in the calibre GUI, not from the command line via ebook-convert."
|
||||||
|
tickets: [8390]
|
||||||
|
|
||||||
|
- title: "Fix memory leak when switching libraries"
|
||||||
|
|
||||||
|
- title: "RTF Output: Fix incorrent spacing between letters."
|
||||||
|
tickets: [8422]
|
||||||
|
|
||||||
|
- title: "Catalog generation: Add composite columns to Merge Comments eligible types"
|
||||||
|
|
||||||
|
- title: "Add a confirmation when closing the add a custom news source dialog."
|
||||||
|
tickets: [8460]
|
||||||
|
|
||||||
|
- title: "Another workaround for LibraryThing UA sniffing that was preventing series metadata download, sigh."
|
||||||
|
tickets: [8477]
|
||||||
|
|
||||||
|
- title: "PD Novel driver: Put books on the SD card into the eBooks folder"
|
||||||
|
|
||||||
|
- title: "When shortening filepaths to conform to windows path length limitations, remove text from the middle of each component instead of the ends."
|
||||||
|
tickets: [8451]
|
||||||
|
|
||||||
|
- title: "Make completion in most places case insensitive"
|
||||||
|
tickets: [8441]
|
||||||
|
|
||||||
|
- title: "Fix regression that caused the N key to stop working when editing a Yes/no column"
|
||||||
|
tickets: [8417]
|
||||||
|
|
||||||
|
- title: "Email: Fix bug when connecting to SMTP relays that use MD5 auth"
|
||||||
|
|
||||||
|
- title: "MOBI Output: Fix bug that could cause a link pointing to the start of a section to go to a point later in the section is the section contained an empty id attribute"
|
||||||
|
|
||||||
|
- title: "When auto converting books and the device is unplugged, do not raise an error."
|
||||||
|
tickets: [8426]
|
||||||
|
|
||||||
|
- title: "Ebook-viewer: Display cover when viewing FB2 files"
|
||||||
|
|
||||||
|
- title: "MOBI Input: Special case handling of emptu div tags with a defined height used as paragraph separators."
|
||||||
|
tickets: [8391]
|
||||||
|
|
||||||
|
- title: "Fix sorting of author names into sub categories by first letter in the Tag Browser when the first letter has diacritics"
|
||||||
|
tickets: [8378]
|
||||||
|
|
||||||
|
- title: "Fix regression in 0.7.40 that caused commas in author names to become | when converting/saving to disk"
|
||||||
|
|
||||||
|
- title: "Fix view specific format on a book with no formats gives an error"
|
||||||
|
tickets: [8352]
|
||||||
|
|
||||||
|
|
||||||
|
improved recipes:
|
||||||
|
- Blic
|
||||||
|
- Las Vegas Review Journal
|
||||||
|
- La Vanguardia
|
||||||
|
- New York Times
|
||||||
|
- El Pais
|
||||||
|
- Seattle Times
|
||||||
|
- Ars Technica
|
||||||
|
- Dilbert
|
||||||
|
- Nature News
|
||||||
|
|
||||||
|
new recipes:
|
||||||
|
- title: "kath.net"
|
||||||
|
author: "Bobus"
|
||||||
|
|
||||||
|
- title: "iHNed"
|
||||||
|
author: "Karel Bilek"
|
||||||
|
|
||||||
|
- title: "Gulf News"
|
||||||
|
author: "Darko Miletic"
|
||||||
|
|
||||||
|
- title: "South Africa Mail and Guardian"
|
||||||
|
author: "77ja65"
|
||||||
|
|
||||||
|
|
||||||
- version: 0.7.40
|
- version: 0.7.40
|
||||||
date: 2011-01-14
|
date: 2011-01-14
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ class AdvancedUserRecipe1293122276(BasicNewsRecipe):
|
|||||||
__author__ = 'Jack Mason'
|
__author__ = 'Jack Mason'
|
||||||
author = 'IBM Global Business Services'
|
author = 'IBM Global Business Services'
|
||||||
publisher = 'IBM'
|
publisher = 'IBM'
|
||||||
|
language = 'en'
|
||||||
category = 'news, technology, IT, internet of things, analytics'
|
category = 'news, technology, IT, internet of things, analytics'
|
||||||
oldest_article = 7
|
oldest_article = 7
|
||||||
max_articles_per_feed = 30
|
max_articles_per_feed = 30
|
||||||
|
@ -6,6 +6,7 @@ class KANewsRecipe(BasicNewsRecipe):
|
|||||||
description = u'Nachrichten aus Karlsruhe, Deutschland und der Welt.'
|
description = u'Nachrichten aus Karlsruhe, Deutschland und der Welt.'
|
||||||
__author__ = 'tfeld'
|
__author__ = 'tfeld'
|
||||||
lang='de'
|
lang='de'
|
||||||
|
language = 'de'
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
|
|
||||||
oldest_article = 7
|
oldest_article = 7
|
||||||
|
@ -4,6 +4,7 @@ class AdvancedUserRecipe1295262156(BasicNewsRecipe):
|
|||||||
title = u'kath.net'
|
title = u'kath.net'
|
||||||
__author__ = 'Bobus'
|
__author__ = 'Bobus'
|
||||||
oldest_article = 7
|
oldest_article = 7
|
||||||
|
language = 'en'
|
||||||
max_articles_per_feed = 100
|
max_articles_per_feed = 100
|
||||||
|
|
||||||
feeds = [(u'kath.net', u'http://www.kath.net/2005/xml/index.xml')]
|
feeds = [(u'kath.net', u'http://www.kath.net/2005/xml/index.xml')]
|
||||||
|
@ -10,6 +10,7 @@ import re
|
|||||||
class NationalGeographicNews(BasicNewsRecipe):
|
class NationalGeographicNews(BasicNewsRecipe):
|
||||||
title = u'National Geographic News'
|
title = u'National Geographic News'
|
||||||
oldest_article = 7
|
oldest_article = 7
|
||||||
|
language = 'en'
|
||||||
max_articles_per_feed = 100
|
max_articles_per_feed = 100
|
||||||
remove_javascript = True
|
remove_javascript = True
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
|
@ -43,8 +43,9 @@ class Stage3(Command):
|
|||||||
|
|
||||||
description = 'Stage 3 of the publish process'
|
description = 'Stage 3 of the publish process'
|
||||||
sub_commands = ['upload_user_manual', 'upload_demo', 'sdist',
|
sub_commands = ['upload_user_manual', 'upload_demo', 'sdist',
|
||||||
'upload_to_google_code', 'tag_release', 'upload_to_server',
|
'upload_to_google_code', 'upload_to_sourceforge',
|
||||||
'upload_to_sourceforge', 'upload_to_mobileread',
|
'tag_release', 'upload_to_server',
|
||||||
|
'upload_to_mobileread',
|
||||||
]
|
]
|
||||||
|
|
||||||
class Stage4(Command):
|
class Stage4(Command):
|
||||||
|
@ -2,7 +2,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
__appname__ = 'calibre'
|
__appname__ = 'calibre'
|
||||||
__version__ = '0.7.40'
|
__version__ = '0.7.41'
|
||||||
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
@ -72,7 +72,8 @@ class Plumber(object):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, input, output, log, report_progress=DummyReporter(),
|
def __init__(self, input, output, log, report_progress=DummyReporter(),
|
||||||
dummy=False, merge_plugin_recs=True, abort_after_input_dump=False):
|
dummy=False, merge_plugin_recs=True, abort_after_input_dump=False,
|
||||||
|
override_input_metadata=False):
|
||||||
'''
|
'''
|
||||||
:param input: Path to input file.
|
:param input: Path to input file.
|
||||||
:param output: Path to output file/directory
|
:param output: Path to output file/directory
|
||||||
@ -87,6 +88,7 @@ class Plumber(object):
|
|||||||
self.log = log
|
self.log = log
|
||||||
self.ui_reporter = report_progress
|
self.ui_reporter = report_progress
|
||||||
self.abort_after_input_dump = abort_after_input_dump
|
self.abort_after_input_dump = abort_after_input_dump
|
||||||
|
self.override_input_metadata = override_input_metadata
|
||||||
|
|
||||||
# Pipeline options {{{
|
# Pipeline options {{{
|
||||||
# Initialize the conversion options that are independent of input and
|
# Initialize the conversion options that are independent of input and
|
||||||
@ -924,7 +926,8 @@ OptionRecommendation(name='sr3_replace',
|
|||||||
self.opts.dest = self.opts.output_profile
|
self.opts.dest = self.opts.output_profile
|
||||||
|
|
||||||
from calibre.ebooks.oeb.transforms.metadata import MergeMetadata
|
from calibre.ebooks.oeb.transforms.metadata import MergeMetadata
|
||||||
MergeMetadata()(self.oeb, self.user_metadata, self.opts)
|
MergeMetadata()(self.oeb, self.user_metadata, self.opts,
|
||||||
|
override_input_metadata=self.override_input_metadata)
|
||||||
pr(0.2)
|
pr(0.2)
|
||||||
self.flush()
|
self.flush()
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ import os
|
|||||||
from calibre.utils.date import isoformat, now
|
from calibre.utils.date import isoformat, now
|
||||||
from calibre import guess_type
|
from calibre import guess_type
|
||||||
|
|
||||||
def meta_info_to_oeb_metadata(mi, m, log):
|
def meta_info_to_oeb_metadata(mi, m, log, override_input_metadata=False):
|
||||||
from calibre.ebooks.oeb.base import OPF
|
from calibre.ebooks.oeb.base import OPF
|
||||||
if not mi.is_null('title'):
|
if not mi.is_null('title'):
|
||||||
m.clear('title')
|
m.clear('title')
|
||||||
@ -29,15 +29,23 @@ def meta_info_to_oeb_metadata(mi, m, log):
|
|||||||
if not mi.is_null('book_producer'):
|
if not mi.is_null('book_producer'):
|
||||||
m.filter('contributor', lambda x : x.role.lower() == 'bkp')
|
m.filter('contributor', lambda x : x.role.lower() == 'bkp')
|
||||||
m.add('contributor', mi.book_producer, role='bkp')
|
m.add('contributor', mi.book_producer, role='bkp')
|
||||||
|
elif override_input_metadata:
|
||||||
|
m.filter('contributor', lambda x : x.role.lower() == 'bkp')
|
||||||
if not mi.is_null('comments'):
|
if not mi.is_null('comments'):
|
||||||
m.clear('description')
|
m.clear('description')
|
||||||
m.add('description', mi.comments)
|
m.add('description', mi.comments)
|
||||||
|
elif override_input_metadata:
|
||||||
|
m.clear('description')
|
||||||
if not mi.is_null('publisher'):
|
if not mi.is_null('publisher'):
|
||||||
m.clear('publisher')
|
m.clear('publisher')
|
||||||
m.add('publisher', mi.publisher)
|
m.add('publisher', mi.publisher)
|
||||||
|
elif override_input_metadata:
|
||||||
|
m.clear('publisher')
|
||||||
if not mi.is_null('series'):
|
if not mi.is_null('series'):
|
||||||
m.clear('series')
|
m.clear('series')
|
||||||
m.add('series', mi.series)
|
m.add('series', mi.series)
|
||||||
|
elif override_input_metadata:
|
||||||
|
m.clear('series')
|
||||||
if not mi.is_null('isbn'):
|
if not mi.is_null('isbn'):
|
||||||
has = False
|
has = False
|
||||||
for x in m.identifier:
|
for x in m.identifier:
|
||||||
@ -46,19 +54,27 @@ def meta_info_to_oeb_metadata(mi, m, log):
|
|||||||
has = True
|
has = True
|
||||||
if not has:
|
if not has:
|
||||||
m.add('identifier', mi.isbn, scheme='ISBN')
|
m.add('identifier', mi.isbn, scheme='ISBN')
|
||||||
|
elif override_input_metadata:
|
||||||
|
m.filter('identifier', lambda x: x.scheme.lower() == 'isbn')
|
||||||
if not mi.is_null('language'):
|
if not mi.is_null('language'):
|
||||||
m.clear('language')
|
m.clear('language')
|
||||||
m.add('language', mi.language)
|
m.add('language', mi.language)
|
||||||
if not mi.is_null('series_index'):
|
if not mi.is_null('series_index'):
|
||||||
m.clear('series_index')
|
m.clear('series_index')
|
||||||
m.add('series_index', mi.format_series_index())
|
m.add('series_index', mi.format_series_index())
|
||||||
|
elif override_input_metadata:
|
||||||
|
m.clear('series_index')
|
||||||
if not mi.is_null('rating'):
|
if not mi.is_null('rating'):
|
||||||
m.clear('rating')
|
m.clear('rating')
|
||||||
m.add('rating', '%.2f'%mi.rating)
|
m.add('rating', '%.2f'%mi.rating)
|
||||||
|
elif override_input_metadata:
|
||||||
|
m.clear('rating')
|
||||||
if not mi.is_null('tags'):
|
if not mi.is_null('tags'):
|
||||||
m.clear('subject')
|
m.clear('subject')
|
||||||
for t in mi.tags:
|
for t in mi.tags:
|
||||||
m.add('subject', t)
|
m.add('subject', t)
|
||||||
|
elif override_input_metadata:
|
||||||
|
m.clear('subject')
|
||||||
if not mi.is_null('pubdate'):
|
if not mi.is_null('pubdate'):
|
||||||
m.clear('date')
|
m.clear('date')
|
||||||
m.add('date', isoformat(mi.pubdate))
|
m.add('date', isoformat(mi.pubdate))
|
||||||
@ -68,9 +84,14 @@ def meta_info_to_oeb_metadata(mi, m, log):
|
|||||||
if not mi.is_null('rights'):
|
if not mi.is_null('rights'):
|
||||||
m.clear('rights')
|
m.clear('rights')
|
||||||
m.add('rights', mi.rights)
|
m.add('rights', mi.rights)
|
||||||
|
elif override_input_metadata:
|
||||||
|
m.clear('rights')
|
||||||
if not mi.is_null('publication_type'):
|
if not mi.is_null('publication_type'):
|
||||||
m.clear('publication_type')
|
m.clear('publication_type')
|
||||||
m.add('publication_type', mi.publication_type)
|
m.add('publication_type', mi.publication_type)
|
||||||
|
elif override_input_metadata:
|
||||||
|
m.clear('publication_type')
|
||||||
|
|
||||||
if not m.timestamp:
|
if not m.timestamp:
|
||||||
m.add('timestamp', isoformat(now()))
|
m.add('timestamp', isoformat(now()))
|
||||||
|
|
||||||
@ -78,11 +99,12 @@ def meta_info_to_oeb_metadata(mi, m, log):
|
|||||||
class MergeMetadata(object):
|
class MergeMetadata(object):
|
||||||
'Merge in user metadata, including cover'
|
'Merge in user metadata, including cover'
|
||||||
|
|
||||||
def __call__(self, oeb, mi, opts):
|
def __call__(self, oeb, mi, opts, override_input_metadata=False):
|
||||||
self.oeb, self.log = oeb, oeb.log
|
self.oeb, self.log = oeb, oeb.log
|
||||||
m = self.oeb.metadata
|
m = self.oeb.metadata
|
||||||
self.log('Merging user specified metadata...')
|
self.log('Merging user specified metadata...')
|
||||||
meta_info_to_oeb_metadata(mi, m, oeb.log)
|
meta_info_to_oeb_metadata(mi, m, oeb.log,
|
||||||
|
override_input_metadata=override_input_metadata)
|
||||||
cover_id = self.set_cover(mi, opts.prefer_metadata_cover)
|
cover_id = self.set_cover(mi, opts.prefer_metadata_cover)
|
||||||
m.clear('cover')
|
m.clear('cover')
|
||||||
if cover_id is not None:
|
if cover_id is not None:
|
||||||
|
@ -262,7 +262,7 @@ class RTFMLizer(object):
|
|||||||
|
|
||||||
if hasattr(elem, 'tail') and elem.tail != None and elem.tail.strip() != '':
|
if hasattr(elem, 'tail') and elem.tail != None and elem.tail.strip() != '':
|
||||||
if 'block' in tag_stack:
|
if 'block' in tag_stack:
|
||||||
text += '%s ' % txt2rtf(elem.tail)
|
text += '%s' % txt2rtf(elem.tail)
|
||||||
else:
|
else:
|
||||||
text += '{\\par \\pard \\hyphpar %s}' % txt2rtf(elem.tail)
|
text += '{\\par \\pard \\hyphpar %s}' % txt2rtf(elem.tail)
|
||||||
|
|
||||||
|
@ -384,7 +384,14 @@ class ChooseLibraryAction(InterfaceAction):
|
|||||||
return
|
return
|
||||||
|
|
||||||
prefs['library_path'] = loc
|
prefs['library_path'] = loc
|
||||||
|
#from calibre.utils.mem import memory
|
||||||
|
#import weakref, gc
|
||||||
|
#ref = weakref.ref(self.gui.library_view.model().db)
|
||||||
|
#before = memory()/1024**2
|
||||||
self.gui.library_moved(loc)
|
self.gui.library_moved(loc)
|
||||||
|
#print gc.get_referrers(ref)[0]
|
||||||
|
#for i in xrange(3): gc.collect()
|
||||||
|
#print 'leaked:', memory()/1024**2 - before
|
||||||
|
|
||||||
def qs_requested(self, idx, *args):
|
def qs_requested(self, idx, *args):
|
||||||
self.switch_requested(self.qs_locations[idx])
|
self.switch_requested(self.qs_locations[idx])
|
||||||
|
@ -144,6 +144,9 @@ class PluginWidget(QWidget,Ui_Form):
|
|||||||
# Hook changes to thumb_width
|
# Hook changes to thumb_width
|
||||||
self.thumb_width.valueChanged.connect(self.thumb_width_changed)
|
self.thumb_width.valueChanged.connect(self.thumb_width_changed)
|
||||||
|
|
||||||
|
# Hook changes to Description section
|
||||||
|
self.generate_descriptions.stateChanged.connect(self.generate_descriptions_changed)
|
||||||
|
|
||||||
def options(self):
|
def options(self):
|
||||||
# Save/return the current options
|
# Save/return the current options
|
||||||
# exclude_genre stores literally
|
# exclude_genre stores literally
|
||||||
@ -265,7 +268,7 @@ class PluginWidget(QWidget,Ui_Form):
|
|||||||
custom_fields = {}
|
custom_fields = {}
|
||||||
for custom_field in all_custom_fields:
|
for custom_field in all_custom_fields:
|
||||||
field_md = self.db.metadata_for_field(custom_field)
|
field_md = self.db.metadata_for_field(custom_field)
|
||||||
if field_md['datatype'] in ['text','comments']:
|
if field_md['datatype'] in ['text','comments','composite']:
|
||||||
custom_fields[field_md['name']] = {'field':custom_field,
|
custom_fields[field_md['name']] = {'field':custom_field,
|
||||||
'datatype':field_md['datatype']}
|
'datatype':field_md['datatype']}
|
||||||
# Blank field first
|
# Blank field first
|
||||||
@ -324,6 +327,28 @@ class PluginWidget(QWidget,Ui_Form):
|
|||||||
else:
|
else:
|
||||||
self.exclude_pattern.setEnabled(False)
|
self.exclude_pattern.setEnabled(False)
|
||||||
|
|
||||||
|
def generate_descriptions_changed(self,new_state):
|
||||||
|
'''
|
||||||
|
Process changes to Descriptions section
|
||||||
|
0: unchecked
|
||||||
|
2: checked
|
||||||
|
'''
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
if new_state == 0:
|
||||||
|
# unchecked
|
||||||
|
self.merge_source_field.setEnabled(False)
|
||||||
|
self.merge_before.setEnabled(False)
|
||||||
|
self.merge_after.setEnabled(False)
|
||||||
|
self.include_hr.setEnabled(False)
|
||||||
|
elif new_state == 2:
|
||||||
|
# checked
|
||||||
|
self.merge_source_field.setEnabled(True)
|
||||||
|
self.merge_before.setEnabled(True)
|
||||||
|
self.merge_after.setEnabled(True)
|
||||||
|
self.include_hr.setEnabled(True)
|
||||||
|
|
||||||
def header_note_source_field_changed(self,new_index):
|
def header_note_source_field_changed(self,new_index):
|
||||||
'''
|
'''
|
||||||
Process changes in the header_note_source_field combo box
|
Process changes in the header_note_source_field combo box
|
||||||
|
@ -12,7 +12,8 @@ from lxml.html import soupparser
|
|||||||
|
|
||||||
from PyQt4.Qt import QApplication, QFontInfo, QSize, QWidget, QPlainTextEdit, \
|
from PyQt4.Qt import QApplication, QFontInfo, QSize, QWidget, QPlainTextEdit, \
|
||||||
QToolBar, QVBoxLayout, QAction, QIcon, Qt, QTabWidget, QUrl, \
|
QToolBar, QVBoxLayout, QAction, QIcon, Qt, QTabWidget, QUrl, \
|
||||||
QSyntaxHighlighter, QColor, QChar, QColorDialog, QMenu, QInputDialog
|
QSyntaxHighlighter, QColor, QChar, QColorDialog, QMenu, QInputDialog, \
|
||||||
|
QHBoxLayout
|
||||||
from PyQt4.QtWebKit import QWebView, QWebPage
|
from PyQt4.QtWebKit import QWebView, QWebPage
|
||||||
|
|
||||||
from calibre.ebooks.chardet import xml_to_unicode
|
from calibre.ebooks.chardet import xml_to_unicode
|
||||||
@ -488,7 +489,7 @@ class Highlighter(QSyntaxHighlighter):
|
|||||||
|
|
||||||
class Editor(QWidget): # {{{
|
class Editor(QWidget): # {{{
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None, one_line_toolbar=False):
|
||||||
QWidget.__init__(self, parent)
|
QWidget.__init__(self, parent)
|
||||||
self.toolbar1 = QToolBar(self)
|
self.toolbar1 = QToolBar(self)
|
||||||
self.toolbar2 = QToolBar(self)
|
self.toolbar2 = QToolBar(self)
|
||||||
@ -508,9 +509,14 @@ class Editor(QWidget): # {{{
|
|||||||
self.wyswyg.layout = l = QVBoxLayout(self.wyswyg)
|
self.wyswyg.layout = l = QVBoxLayout(self.wyswyg)
|
||||||
self.setLayout(self._layout)
|
self.setLayout(self._layout)
|
||||||
l.setContentsMargins(0, 0, 0, 0)
|
l.setContentsMargins(0, 0, 0, 0)
|
||||||
l.addWidget(self.toolbar1)
|
if one_line_toolbar:
|
||||||
l.addWidget(self.toolbar2)
|
tb = QHBoxLayout()
|
||||||
l.addWidget(self.toolbar3)
|
l.addLayout(tb)
|
||||||
|
else:
|
||||||
|
tb = l
|
||||||
|
tb.addWidget(self.toolbar1)
|
||||||
|
tb.addWidget(self.toolbar2)
|
||||||
|
tb.addWidget(self.toolbar3)
|
||||||
l.addWidget(self.editor)
|
l.addWidget(self.editor)
|
||||||
self._layout.addWidget(self.tabs)
|
self._layout.addWidget(self.tabs)
|
||||||
self.tabs.addTab(self.wyswyg, _('Normal view'))
|
self.tabs.addTab(self.wyswyg, _('Normal view'))
|
||||||
|
@ -12,17 +12,24 @@ from calibre.customize.ui import plugin_for_catalog_format
|
|||||||
from calibre.utils.logging import Log
|
from calibre.utils.logging import Log
|
||||||
|
|
||||||
def gui_convert(input, output, recommendations, notification=DummyReporter(),
|
def gui_convert(input, output, recommendations, notification=DummyReporter(),
|
||||||
abort_after_input_dump=False, log=None):
|
abort_after_input_dump=False, log=None, override_input_metadata=False):
|
||||||
recommendations = list(recommendations)
|
recommendations = list(recommendations)
|
||||||
recommendations.append(('verbose', 2, OptionRecommendation.HIGH))
|
recommendations.append(('verbose', 2, OptionRecommendation.HIGH))
|
||||||
if log is None:
|
if log is None:
|
||||||
log = Log()
|
log = Log()
|
||||||
plumber = Plumber(input, output, log, report_progress=notification,
|
plumber = Plumber(input, output, log, report_progress=notification,
|
||||||
abort_after_input_dump=abort_after_input_dump)
|
abort_after_input_dump=abort_after_input_dump,
|
||||||
|
override_input_metadata=override_input_metadata)
|
||||||
plumber.merge_ui_recommendations(recommendations)
|
plumber.merge_ui_recommendations(recommendations)
|
||||||
|
|
||||||
plumber.run()
|
plumber.run()
|
||||||
|
|
||||||
|
def gui_convert_override(input, output, recommendations, notification=DummyReporter(),
|
||||||
|
abort_after_input_dump=False, log=None):
|
||||||
|
gui_convert(input, output, recommendations, notification=notification,
|
||||||
|
abort_after_input_dump=abort_after_input_dump, log=log,
|
||||||
|
override_input_metadata=True)
|
||||||
|
|
||||||
def gui_catalog(fmt, title, dbspec, ids, out_file_name, sync, fmt_options, connected_device,
|
def gui_catalog(fmt, title, dbspec, ids, out_file_name, sync, fmt_options, connected_device,
|
||||||
notification=DummyReporter(), log=None):
|
notification=DummyReporter(), log=None):
|
||||||
if log is None:
|
if log is None:
|
||||||
|
@ -47,6 +47,8 @@ class RegexBuilder(QDialog, Ui_RegexBuilder):
|
|||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
self.regex.setStyleSheet('QLineEdit { color: black; background-color: white; }')
|
self.regex.setStyleSheet('QLineEdit { color: black; background-color: white; }')
|
||||||
|
self.preview.setExtraSelections([])
|
||||||
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def do_test(self):
|
def do_test(self):
|
||||||
|
@ -79,6 +79,8 @@ class TagEditor(QDialog, Ui_TagEditor):
|
|||||||
|
|
||||||
def apply_tags(self, item=None):
|
def apply_tags(self, item=None):
|
||||||
items = self.available_tags.selectedItems() if item is None else [item]
|
items = self.available_tags.selectedItems() if item is None else [item]
|
||||||
|
rows = [self.available_tags.row(i) for i in items]
|
||||||
|
row = max(rows)
|
||||||
for item in items:
|
for item in items:
|
||||||
tag = unicode(item.text())
|
tag = unicode(item.text())
|
||||||
self.tags.append(tag)
|
self.tags.append(tag)
|
||||||
@ -89,6 +91,12 @@ class TagEditor(QDialog, Ui_TagEditor):
|
|||||||
for tag in self.tags:
|
for tag in self.tags:
|
||||||
self.applied_tags.addItem(tag)
|
self.applied_tags.addItem(tag)
|
||||||
|
|
||||||
|
if row >= self.available_tags.count():
|
||||||
|
row = self.available_tags.count() - 1
|
||||||
|
|
||||||
|
if row > 2:
|
||||||
|
item = self.available_tags.item(row)
|
||||||
|
self.available_tags.scrollToItem(item)
|
||||||
|
|
||||||
|
|
||||||
def unapply_tags(self, item=None):
|
def unapply_tags(self, item=None):
|
||||||
|
@ -77,9 +77,9 @@ class TitleEdit(EnLineEdit):
|
|||||||
def commit(self, db, id_):
|
def commit(self, db, id_):
|
||||||
title = self.current_val
|
title = self.current_val
|
||||||
if self.COMMIT:
|
if self.COMMIT:
|
||||||
getattr(db, 'set_', self.TITLE_ATTR)(id_, title, notify=False)
|
getattr(db, 'set_'+ self.TITLE_ATTR)(id_, title, notify=False)
|
||||||
else:
|
else:
|
||||||
getattr(db, 'set_', self.TITLE_ATTR)(id_, title, notify=False,
|
getattr(db, 'set_'+ self.TITLE_ATTR)(id_, title, notify=False,
|
||||||
commit=False)
|
commit=False)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ from functools import partial
|
|||||||
from PyQt4.Qt import Qt, QVBoxLayout, QHBoxLayout, QWidget, QPushButton, \
|
from PyQt4.Qt import Qt, QVBoxLayout, QHBoxLayout, QWidget, QPushButton, \
|
||||||
QGridLayout, pyqtSignal, QDialogButtonBox, QScrollArea, QFont, \
|
QGridLayout, pyqtSignal, QDialogButtonBox, QScrollArea, QFont, \
|
||||||
QTabWidget, QIcon, QToolButton, QSplitter, QGroupBox, QSpacerItem, \
|
QTabWidget, QIcon, QToolButton, QSplitter, QGroupBox, QSpacerItem, \
|
||||||
QSizePolicy
|
QSizePolicy, QPalette, QFrame, QSize
|
||||||
|
|
||||||
from calibre.ebooks.metadata import authors_to_string, string_to_authors
|
from calibre.ebooks.metadata import authors_to_string, string_to_authors
|
||||||
from calibre.gui2 import ResizableDialog, error_dialog, gprefs
|
from calibre.gui2 import ResizableDialog, error_dialog, gprefs
|
||||||
@ -22,9 +22,11 @@ from calibre.gui2.metadata.basic_widgets import TitleEdit, AuthorsEdit, \
|
|||||||
from calibre.gui2.custom_column_widgets import populate_metadata_page
|
from calibre.gui2.custom_column_widgets import populate_metadata_page
|
||||||
from calibre.utils.config import tweaks
|
from calibre.utils.config import tweaks
|
||||||
|
|
||||||
class MetadataSingleDialog(ResizableDialog):
|
class MetadataSingleDialogBase(ResizableDialog):
|
||||||
|
|
||||||
view_format = pyqtSignal(object)
|
view_format = pyqtSignal(object)
|
||||||
|
cc_two_column = tweaks['metadata_single_use_2_cols_for_custom_fields']
|
||||||
|
one_line_comments_toolbar = False
|
||||||
|
|
||||||
def __init__(self, db, parent=None):
|
def __init__(self, db, parent=None):
|
||||||
self.db = db
|
self.db = db
|
||||||
@ -61,13 +63,11 @@ class MetadataSingleDialog(ResizableDialog):
|
|||||||
self.l.addWidget(self.button_box)
|
self.l.addWidget(self.button_box)
|
||||||
|
|
||||||
self.setWindowIcon(QIcon(I('edit_input.png')))
|
self.setWindowIcon(QIcon(I('edit_input.png')))
|
||||||
self.setWindowTitle(_('Edit Meta Information'))
|
self.setWindowTitle(_('Edit Metadata'))
|
||||||
|
|
||||||
self.create_basic_metadata_widgets()
|
self.create_basic_metadata_widgets()
|
||||||
|
|
||||||
if len(self.db.custom_column_label_map) == 0:
|
if len(self.db.custom_column_label_map):
|
||||||
self.central_widget.tabBar().setVisible(False)
|
|
||||||
else:
|
|
||||||
self.create_custom_metadata_widgets()
|
self.create_custom_metadata_widgets()
|
||||||
|
|
||||||
|
|
||||||
@ -101,7 +101,7 @@ class MetadataSingleDialog(ResizableDialog):
|
|||||||
'Using this button to create author sort will change author sort from'
|
'Using this button to create author sort will change author sort from'
|
||||||
' red to green.'))
|
' red to green.'))
|
||||||
self.author_sort = AuthorSortEdit(self, self.authors,
|
self.author_sort = AuthorSortEdit(self, self.authors,
|
||||||
self.deduce_author_sort_button, db)
|
self.deduce_author_sort_button, self.db)
|
||||||
self.basic_metadata_widgets.extend([self.authors, self.author_sort])
|
self.basic_metadata_widgets.extend([self.authors, self.author_sort])
|
||||||
|
|
||||||
self.swap_title_author_button = QToolButton(self)
|
self.swap_title_author_button = QToolButton(self)
|
||||||
@ -127,7 +127,7 @@ class MetadataSingleDialog(ResizableDialog):
|
|||||||
self.cover = Cover(self)
|
self.cover = Cover(self)
|
||||||
self.basic_metadata_widgets.append(self.cover)
|
self.basic_metadata_widgets.append(self.cover)
|
||||||
|
|
||||||
self.comments = CommentsEdit(self)
|
self.comments = CommentsEdit(self, self.one_line_comments_toolbar)
|
||||||
self.basic_metadata_widgets.append(self.comments)
|
self.basic_metadata_widgets.append(self.comments)
|
||||||
|
|
||||||
self.rating = RatingEdit(self)
|
self.rating = RatingEdit(self)
|
||||||
@ -166,19 +166,213 @@ class MetadataSingleDialog(ResizableDialog):
|
|||||||
w.setLayout(layout)
|
w.setLayout(layout)
|
||||||
self.custom_metadata_widgets, self.__cc_spacers = \
|
self.custom_metadata_widgets, self.__cc_spacers = \
|
||||||
populate_metadata_page(layout, self.db, None, parent=w, bulk=False,
|
populate_metadata_page(layout, self.db, None, parent=w, bulk=False,
|
||||||
two_column=tweaks['metadata_single_use_2_cols_for_custom_fields'])
|
two_column=self.cc_two_column)
|
||||||
self.__custom_col_layouts = [layout]
|
self.__custom_col_layouts = [layout]
|
||||||
ans = self.custom_metadata_widgets
|
|
||||||
for i in range(len(ans)-1):
|
|
||||||
if len(ans[i+1].widgets) == 2:
|
|
||||||
w.setTabOrder(ans[i].widgets[-1], ans[i+1].widgets[1])
|
|
||||||
else:
|
|
||||||
w.setTabOrder(ans[i].widgets[-1], ans[i+1].widgets[0])
|
|
||||||
for c in range(2, len(ans[i].widgets), 2):
|
|
||||||
w.setTabOrder(ans[i].widgets[c-1], ans[i].widgets[c+1])
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
def do_layout(self): # {{{
|
def set_custom_metadata_tab_order(self, before=None, after=None): # {{{
|
||||||
|
sto = QWidget.setTabOrder
|
||||||
|
if getattr(self, 'custom_metadata_widgets', []):
|
||||||
|
ans = self.custom_metadata_widgets
|
||||||
|
for i in range(len(ans)-1):
|
||||||
|
if before is not None and i == 0:
|
||||||
|
pass# Do something
|
||||||
|
if len(ans[i+1].widgets) == 2:
|
||||||
|
sto(ans[i].widgets[-1], ans[i+1].widgets[1])
|
||||||
|
else:
|
||||||
|
sto(ans[i].widgets[-1], ans[i+1].widgets[0])
|
||||||
|
for c in range(2, len(ans[i].widgets), 2):
|
||||||
|
sto(ans[i].widgets[c-1], ans[i].widgets[c+1])
|
||||||
|
if after is not None:
|
||||||
|
pass # Do something
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
def do_layout(self):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def __call__(self, id_):
|
||||||
|
self.book_id = id_
|
||||||
|
for widget in self.basic_metadata_widgets:
|
||||||
|
widget.initialize(self.db, id_)
|
||||||
|
for widget in self.custom_metadata_widgets:
|
||||||
|
widget.initialize(id_)
|
||||||
|
# Commented out as it doesn't play nice with Next, Prev buttons
|
||||||
|
#self.fetch_metadata_button.setFocus(Qt.OtherFocusReason)
|
||||||
|
|
||||||
|
|
||||||
|
# Miscellaneous interaction methods {{{
|
||||||
|
def update_window_title(self, *args):
|
||||||
|
title = self.title.current_val
|
||||||
|
if len(title) > 50:
|
||||||
|
title = title[:50] + u'\u2026'
|
||||||
|
self.setWindowTitle(_('Edit Metadata') + ' - ' +
|
||||||
|
title)
|
||||||
|
|
||||||
|
def swap_title_author(self, *args):
|
||||||
|
title = self.title.current_val
|
||||||
|
self.title.current_val = authors_to_string(self.authors.current_val)
|
||||||
|
self.authors.current_val = string_to_authors(title)
|
||||||
|
self.title_sort.auto_generate()
|
||||||
|
self.author_sort.auto_generate()
|
||||||
|
|
||||||
|
def remove_unused_series(self, *args):
|
||||||
|
self.db.remove_unused_series()
|
||||||
|
idx = self.series.current_val
|
||||||
|
self.series.clear()
|
||||||
|
self.series.initialize(self.db, self.book_id)
|
||||||
|
if idx:
|
||||||
|
for i in range(self.series.count()):
|
||||||
|
if unicode(self.series.itemText(i)) == idx:
|
||||||
|
self.series.setCurrentIndex(i)
|
||||||
|
break
|
||||||
|
|
||||||
|
def tags_editor(self, *args):
|
||||||
|
self.tags.edit(self.db, self.book_id)
|
||||||
|
|
||||||
|
def metadata_from_format(self, *args):
|
||||||
|
mi, ext = self.formats_manager.get_selected_format_metadata(self.db,
|
||||||
|
self.book_id)
|
||||||
|
if mi is not None:
|
||||||
|
self.update_from_mi(mi)
|
||||||
|
|
||||||
|
def cover_from_format(self, *args):
|
||||||
|
mi, ext = self.formats_manager.get_selected_format_metadata(self.db,
|
||||||
|
self.book_id)
|
||||||
|
if mi is None:
|
||||||
|
return
|
||||||
|
cdata = None
|
||||||
|
if mi.cover and os.access(mi.cover, os.R_OK):
|
||||||
|
cdata = open(mi.cover).read()
|
||||||
|
elif mi.cover_data[1] is not None:
|
||||||
|
cdata = mi.cover_data[1]
|
||||||
|
if cdata is None:
|
||||||
|
error_dialog(self, _('Could not read cover'),
|
||||||
|
_('Could not read cover from %s format')%ext).exec_()
|
||||||
|
return
|
||||||
|
orig = self.cover.current_val
|
||||||
|
self.cover.current_val = cdata
|
||||||
|
if self.cover.current_val is None:
|
||||||
|
self.cover.current_val = orig
|
||||||
|
return error_dialog(self, _('Could not read cover'),
|
||||||
|
_('The cover in the %s format is invalid')%ext,
|
||||||
|
show=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
def update_from_mi(self, mi):
|
||||||
|
if not mi.is_null('title'):
|
||||||
|
self.title.current_val = mi.title
|
||||||
|
if not mi.is_null('authors'):
|
||||||
|
self.authors.current_val = mi.authors
|
||||||
|
if not mi.is_null('author_sort'):
|
||||||
|
self.author_sort.current_val = mi.author_sort
|
||||||
|
if not mi.is_null('rating'):
|
||||||
|
try:
|
||||||
|
self.rating.current_val = mi.rating
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if not mi.is_null('publisher'):
|
||||||
|
self.publisher.current_val = mi.publisher
|
||||||
|
if not mi.is_null('tags'):
|
||||||
|
self.tags.current_val = mi.tags
|
||||||
|
if not mi.is_null('isbn'):
|
||||||
|
self.isbn.current_val = mi.isbn
|
||||||
|
if not mi.is_null('pubdate'):
|
||||||
|
self.pubdate.current_val = mi.pubdate
|
||||||
|
if not mi.is_null('series') and mi.series.strip():
|
||||||
|
self.series.current_val = mi.series
|
||||||
|
if mi.series_index is not None:
|
||||||
|
self.series_index.current_val = float(mi.series_index)
|
||||||
|
if mi.comments and mi.comments.strip():
|
||||||
|
self.comments.current_val = mi.comments
|
||||||
|
|
||||||
|
def fetch_metadata(self, *args):
|
||||||
|
pass # TODO: fetch metadata
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
def apply_changes(self):
|
||||||
|
self.changed.add(self.book_id)
|
||||||
|
for widget in self.basic_metadata_widgets:
|
||||||
|
try:
|
||||||
|
if not widget.commit(self.db, self.book_id):
|
||||||
|
return False
|
||||||
|
except IOError, err:
|
||||||
|
if err.errno == 13: # Permission denied
|
||||||
|
import traceback
|
||||||
|
fname = err.filename if err.filename else 'file'
|
||||||
|
error_dialog(self, _('Permission denied'),
|
||||||
|
_('Could not open %s. Is it being used by another'
|
||||||
|
' program?')%fname, det_msg=traceback.format_exc(),
|
||||||
|
show=True)
|
||||||
|
return False
|
||||||
|
raise
|
||||||
|
for widget in getattr(self, 'custom_metadata_widgets', []):
|
||||||
|
widget.commit(self.book_id)
|
||||||
|
|
||||||
|
self.db.commit()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def accept(self):
|
||||||
|
self.save_state()
|
||||||
|
if not self.apply_changes():
|
||||||
|
return
|
||||||
|
ResizableDialog.accept(self)
|
||||||
|
|
||||||
|
def reject(self):
|
||||||
|
self.save_state()
|
||||||
|
ResizableDialog.reject(self)
|
||||||
|
|
||||||
|
def save_state(self):
|
||||||
|
gprefs['metasingle_window_geometry3'] = bytearray(self.saveGeometry())
|
||||||
|
|
||||||
|
# Dialog use methods {{{
|
||||||
|
def start(self, row_list, current_row, view_slot=None):
|
||||||
|
self.row_list = row_list
|
||||||
|
self.current_row = current_row
|
||||||
|
if view_slot is not None:
|
||||||
|
self.view_format.connect(view_slot)
|
||||||
|
self.do_one()
|
||||||
|
ret = self.exec_()
|
||||||
|
self.break_cycles()
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def do_one(self, delta=0):
|
||||||
|
self.current_row += delta
|
||||||
|
prev = next_ = None
|
||||||
|
if self.current_row > 0:
|
||||||
|
prev = self.db.title(self.row_list[self.current_row-1])
|
||||||
|
if self.current_row < len(self.row_list) - 1:
|
||||||
|
next_ = self.db.title(self.row_list[self.current_row+1])
|
||||||
|
|
||||||
|
if next_ is not None:
|
||||||
|
tip = _('Save changes and edit the metadata of %s')%next_
|
||||||
|
self.next_button.setToolTip(tip)
|
||||||
|
self.next_button.setVisible(next_ is not None)
|
||||||
|
if prev is not None:
|
||||||
|
tip = _('Save changes and edit the metadata of %s')%prev
|
||||||
|
self.prev_button.setToolTip(tip)
|
||||||
|
self.prev_button.setVisible(prev is not None)
|
||||||
|
self(self.db.id(self.row_list[self.current_row]))
|
||||||
|
|
||||||
|
def break_cycles(self):
|
||||||
|
# Break any reference cycles that could prevent python
|
||||||
|
# from garbage collecting this dialog
|
||||||
|
def disconnect(signal):
|
||||||
|
try:
|
||||||
|
signal.disconnect()
|
||||||
|
except:
|
||||||
|
pass # Fails if view format was never connected
|
||||||
|
disconnect(self.view_format)
|
||||||
|
for b in ('next_button', 'prev_button'):
|
||||||
|
x = getattr(self, b, None)
|
||||||
|
if x is not None:
|
||||||
|
disconnect(x.clicked)
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class MetadataSingleDialog(MetadataSingleDialogBase): # {{{
|
||||||
|
|
||||||
|
def do_layout(self):
|
||||||
|
if len(self.db.custom_column_label_map) == 0:
|
||||||
|
self.central_widget.tabBar().setVisible(False)
|
||||||
self.central_widget.clear()
|
self.central_widget.clear()
|
||||||
self.tabs = []
|
self.tabs = []
|
||||||
self.labels = []
|
self.labels = []
|
||||||
@ -283,181 +477,143 @@ class MetadataSingleDialog(ResizableDialog):
|
|||||||
l.addWidget(self.comments)
|
l.addWidget(self.comments)
|
||||||
self.splitter.addWidget(gb)
|
self.splitter.addWidget(gb)
|
||||||
|
|
||||||
# }}}
|
self.set_custom_metadata_tab_order()
|
||||||
|
|
||||||
def __call__(self, id_):
|
# }}}
|
||||||
self.book_id = id_
|
|
||||||
for widget in self.basic_metadata_widgets:
|
|
||||||
widget.initialize(self.db, id_)
|
|
||||||
for widget in self.custom_metadata_widgets:
|
|
||||||
widget.initialize(id_)
|
|
||||||
# Commented out as it doesn't play nice with Next, Prev buttons
|
|
||||||
#self.fetch_metadata_button.setFocus(Qt.OtherFocusReason)
|
|
||||||
|
|
||||||
|
class MetadataSingleDialogAlt(MetadataSingleDialogBase): # {{{
|
||||||
|
|
||||||
def update_window_title(self, *args):
|
cc_two_column = False
|
||||||
title = self.title.current_val
|
one_line_comments_toolbar = True
|
||||||
if len(title) > 50:
|
|
||||||
title = title[:50] + u'\u2026'
|
|
||||||
self.setWindowTitle(_('Edit Meta Information') + ' - ' +
|
|
||||||
title)
|
|
||||||
|
|
||||||
def swap_title_author(self, *args):
|
def do_layout(self):
|
||||||
title = self.title.current_val
|
self.central_widget.clear()
|
||||||
self.title.current_val = authors_to_string(self.authors.current_val)
|
self.tabs = []
|
||||||
self.authors.current_val = string_to_authors(title)
|
self.labels = []
|
||||||
self.title_sort.auto_generate()
|
sto = QWidget.setTabOrder
|
||||||
self.author_sort.auto_generate()
|
|
||||||
|
|
||||||
def remove_unused_series(self, *args):
|
self.tabs.append(QWidget(self))
|
||||||
self.db.remove_unused_series()
|
self.central_widget.addTab(self.tabs[0], _("&Metadata"))
|
||||||
idx = self.series.current_val
|
self.tabs[0].l = QGridLayout()
|
||||||
self.series.clear()
|
self.tabs[0].setLayout(self.tabs[0].l)
|
||||||
self.series.initialize(self.db, self.book_id)
|
|
||||||
if idx:
|
|
||||||
for i in range(self.series.count()):
|
|
||||||
if unicode(self.series.itemText(i)) == idx:
|
|
||||||
self.series.setCurrentIndex(i)
|
|
||||||
break
|
|
||||||
|
|
||||||
def tags_editor(self, *args):
|
self.tabs.append(QWidget(self))
|
||||||
self.tags.edit(self.db, self.book_id)
|
self.central_widget.addTab(self.tabs[1], _("&Cover and formats"))
|
||||||
|
self.tabs[1].l = QGridLayout()
|
||||||
|
self.tabs[1].setLayout(self.tabs[1].l)
|
||||||
|
|
||||||
def metadata_from_format(self, *args):
|
# Tab 0
|
||||||
mi, ext = self.formats_manager.get_selected_format_metadata(self.db,
|
tab0 = self.tabs[0]
|
||||||
self.book_id)
|
|
||||||
if mi is not None:
|
|
||||||
self.update_from_mi(mi)
|
|
||||||
|
|
||||||
def cover_from_format(self, *args):
|
tl = QGridLayout()
|
||||||
mi, ext = self.formats_manager.get_selected_format_metadata(self.db,
|
gb = QGroupBox(_('&Basic metadata'), self.tabs[0])
|
||||||
self.book_id)
|
self.tabs[0].l.addWidget(gb, 0, 0, 1, 1)
|
||||||
if mi is None:
|
gb.setLayout(tl)
|
||||||
return
|
|
||||||
cdata = None
|
|
||||||
if mi.cover and os.access(mi.cover, os.R_OK):
|
|
||||||
cdata = open(mi.cover).read()
|
|
||||||
elif mi.cover_data[1] is not None:
|
|
||||||
cdata = mi.cover_data[1]
|
|
||||||
if cdata is None:
|
|
||||||
error_dialog(self, _('Could not read cover'),
|
|
||||||
_('Could not read cover from %s format')%ext).exec_()
|
|
||||||
return
|
|
||||||
orig = self.cover.current_val
|
|
||||||
self.cover.current_val = cdata
|
|
||||||
if self.cover.current_val is None:
|
|
||||||
self.cover.current_val = orig
|
|
||||||
return error_dialog(self, _('Could not read cover'),
|
|
||||||
_('The cover in the %s format is invalid')%ext,
|
|
||||||
show=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
def update_from_mi(self, mi):
|
sto(self.button_box, self.title)
|
||||||
if not mi.is_null('title'):
|
|
||||||
self.title.current_val = mi.title
|
|
||||||
if not mi.is_null('authors'):
|
|
||||||
self.authors.current_val = mi.authors
|
|
||||||
if not mi.is_null('author_sort'):
|
|
||||||
self.author_sort.current_val = mi.author_sort
|
|
||||||
if not mi.is_null('rating'):
|
|
||||||
try:
|
|
||||||
self.rating.current_val = mi.rating
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
if not mi.is_null('publisher'):
|
|
||||||
self.publisher.current_val = mi.publisher
|
|
||||||
if not mi.is_null('tags'):
|
|
||||||
self.tags.current_val = mi.tags
|
|
||||||
if not mi.is_null('isbn'):
|
|
||||||
self.isbn.current_val = mi.isbn
|
|
||||||
if not mi.is_null('pubdate'):
|
|
||||||
self.pubdate.current_val = mi.pubdate
|
|
||||||
if not mi.is_null('series') and mi.series.strip():
|
|
||||||
self.series.current_val = mi.series
|
|
||||||
if mi.series_index is not None:
|
|
||||||
self.series_index.current_val = float(mi.series_index)
|
|
||||||
if mi.comments and mi.comments.strip():
|
|
||||||
self.comments.current_val = mi.comments
|
|
||||||
|
|
||||||
def fetch_metadata(self, *args):
|
def create_row(row, widget, tab_to, button=None, icon=None, span=1):
|
||||||
pass # TODO: fetch metadata
|
ql = BuddyLabel(widget)
|
||||||
|
tl.addWidget(ql, row, 1, 1, 1)
|
||||||
|
tl.addWidget(widget, row, 2, 1, 1)
|
||||||
|
if button is not None:
|
||||||
|
tl.addWidget(button, row, 3, span, 1)
|
||||||
|
if icon is not None:
|
||||||
|
button.setIcon(QIcon(I(icon)))
|
||||||
|
if tab_to is not None:
|
||||||
|
if button is not None:
|
||||||
|
sto(widget, button)
|
||||||
|
sto(button, tab_to)
|
||||||
|
else:
|
||||||
|
sto(widget, tab_to)
|
||||||
|
|
||||||
def apply_changes(self):
|
tl.addWidget(self.swap_title_author_button, 0, 0, 2, 1)
|
||||||
self.changed.add(self.book_id)
|
|
||||||
for widget in self.basic_metadata_widgets:
|
|
||||||
try:
|
|
||||||
if not widget.commit(self.db, self.book_id):
|
|
||||||
return False
|
|
||||||
except IOError, err:
|
|
||||||
if err.errno == 13: # Permission denied
|
|
||||||
import traceback
|
|
||||||
fname = err.filename if err.filename else 'file'
|
|
||||||
error_dialog(self, _('Permission denied'),
|
|
||||||
_('Could not open %s. Is it being used by another'
|
|
||||||
' program?')%fname, det_msg=traceback.format_exc(),
|
|
||||||
show=True)
|
|
||||||
return False
|
|
||||||
raise
|
|
||||||
for widget in getattr(self, 'custom_metadata_widgets', []):
|
|
||||||
widget.commit(self.book_id)
|
|
||||||
|
|
||||||
self.db.commit()
|
create_row(0, self.title, self.title_sort,
|
||||||
return True
|
button=self.deduce_title_sort_button, span=2,
|
||||||
|
icon='auto_author_sort.png')
|
||||||
|
create_row(1, self.title_sort, self.authors)
|
||||||
|
create_row(2, self.authors, self.author_sort,
|
||||||
|
button=self.deduce_author_sort_button,
|
||||||
|
span=2, icon='auto_author_sort.png')
|
||||||
|
create_row(3, self.author_sort, self.series)
|
||||||
|
create_row(4, self.series, self.series_index,
|
||||||
|
button=self.remove_unused_series_button, icon='trash.png')
|
||||||
|
create_row(5, self.series_index, self.tags)
|
||||||
|
create_row(6, self.tags, self.rating, button=self.tags_editor_button)
|
||||||
|
create_row(7, self.rating, self.pubdate)
|
||||||
|
create_row(8, self.pubdate, self.publisher,
|
||||||
|
button=self.pubdate.clear_button, icon='trash.png')
|
||||||
|
create_row(9, self.publisher, self.timestamp)
|
||||||
|
create_row(10, self.timestamp, self.isbn,
|
||||||
|
button=self.timestamp.clear_button, icon='trash.png')
|
||||||
|
create_row(11, self.isbn, self.comments)
|
||||||
|
tl.addItem(QSpacerItem(1, 1, QSizePolicy.Fixed, QSizePolicy.Expanding),
|
||||||
|
12, 1, 1 ,1)
|
||||||
|
|
||||||
def accept(self):
|
w = getattr(self, 'custom_metadata_widgets_parent', None)
|
||||||
self.save_state()
|
if w is not None:
|
||||||
if not self.apply_changes():
|
gb = QGroupBox(_('C&ustom metadata'), tab0)
|
||||||
return
|
gbl = QVBoxLayout()
|
||||||
ResizableDialog.accept(self)
|
gb.setLayout(gbl)
|
||||||
|
sr = QScrollArea(tab0)
|
||||||
|
sr.setWidgetResizable(True)
|
||||||
|
sr.setBackgroundRole(QPalette.Base)
|
||||||
|
sr.setFrameStyle(QFrame.NoFrame)
|
||||||
|
sr.setWidget(w)
|
||||||
|
gbl.addWidget(sr)
|
||||||
|
self.tabs[0].l.addWidget(gb, 0, 1, 1, 1)
|
||||||
|
sto(self.isbn, gb)
|
||||||
|
|
||||||
def reject(self):
|
w = QGroupBox(_('&Comments'), tab0)
|
||||||
self.save_state()
|
sp = QSizePolicy()
|
||||||
ResizableDialog.reject(self)
|
sp.setVerticalStretch(10)
|
||||||
|
sp.setHorizontalPolicy(QSizePolicy.Expanding)
|
||||||
|
sp.setVerticalPolicy(QSizePolicy.Expanding)
|
||||||
|
w.setSizePolicy(sp)
|
||||||
|
l = QHBoxLayout()
|
||||||
|
w.setLayout(l)
|
||||||
|
l.addWidget(self.comments)
|
||||||
|
tab0.l.addWidget(w, 1, 0, 1, 2)
|
||||||
|
|
||||||
def save_state(self):
|
# Tab 1
|
||||||
gprefs['metasingle_window_geometry3'] = bytearray(self.saveGeometry())
|
tab1 = self.tabs[1]
|
||||||
|
|
||||||
def start(self, row_list, current_row, view_slot=None):
|
wsp = QWidget(tab1)
|
||||||
self.row_list = row_list
|
wgl = QVBoxLayout()
|
||||||
self.current_row = current_row
|
wsp.setLayout(wgl)
|
||||||
if view_slot is not None:
|
|
||||||
self.view_format.connect(view_slot)
|
|
||||||
self.do_one()
|
|
||||||
ret = self.exec_()
|
|
||||||
self.break_cycles()
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def do_one(self, delta=0):
|
# right-hand side of splitter
|
||||||
self.current_row += delta
|
gb = QGroupBox(_('Change cover'), tab1)
|
||||||
prev = next_ = None
|
l = QGridLayout()
|
||||||
if self.current_row > 0:
|
gb.setLayout(l)
|
||||||
prev = self.db.title(self.row_list[self.current_row-1])
|
sto(self.swap_title_author_button, self.cover.buttons[0])
|
||||||
if self.current_row < len(self.row_list) - 1:
|
for i, b in enumerate(self.cover.buttons[:3]):
|
||||||
next_ = self.db.title(self.row_list[self.current_row+1])
|
l.addWidget(b, 0, i, 1, 1)
|
||||||
|
sto(b, self.cover.buttons[i+1])
|
||||||
|
hl = QHBoxLayout()
|
||||||
|
for b in self.cover.buttons[3:]:
|
||||||
|
hl.addWidget(b)
|
||||||
|
sto(self.cover.buttons[-2], self.cover.buttons[-1])
|
||||||
|
l.addLayout(hl, 1, 0, 1, 3)
|
||||||
|
wgl.addWidget(gb)
|
||||||
|
wgl.addItem(QSpacerItem(10, 10, QSizePolicy.Expanding,
|
||||||
|
QSizePolicy.Expanding))
|
||||||
|
wgl.addWidget(self.fetch_metadata_button)
|
||||||
|
wgl.addItem(QSpacerItem(10, 10, QSizePolicy.Expanding,
|
||||||
|
QSizePolicy.Expanding))
|
||||||
|
wgl.addWidget(self.formats_manager)
|
||||||
|
|
||||||
if next_ is not None:
|
self.splitter = QSplitter(Qt.Horizontal, tab1)
|
||||||
tip = _('Save changes and edit the metadata of %s')%next_
|
tab1.l.addWidget(self.splitter)
|
||||||
self.next_button.setToolTip(tip)
|
self.splitter.addWidget(self.cover)
|
||||||
self.next_button.setVisible(next_ is not None)
|
self.splitter.addWidget(wsp)
|
||||||
if prev is not None:
|
|
||||||
tip = _('Save changes and edit the metadata of %s')%prev
|
self.formats_manager.formats.setMaximumWidth(10000)
|
||||||
self.prev_button.setToolTip(tip)
|
self.formats_manager.formats.setIconSize(QSize(64, 64))
|
||||||
self.prev_button.setVisible(prev is not None)
|
|
||||||
self(self.db.id(self.row_list[self.current_row]))
|
# }}}
|
||||||
|
|
||||||
def break_cycles(self):
|
|
||||||
# Break any reference cycles that could prevent python
|
|
||||||
# from garbage collecting this dialog
|
|
||||||
def disconnect(signal):
|
|
||||||
try:
|
|
||||||
signal.disconnect()
|
|
||||||
except:
|
|
||||||
pass # Fails if view format was never connected
|
|
||||||
disconnect(self.view_format)
|
|
||||||
for b in ('next_button', 'prev_button'):
|
|
||||||
x = getattr(self, b, None)
|
|
||||||
if x is not None:
|
|
||||||
disconnect(x.clicked)
|
|
||||||
|
|
||||||
def edit_metadata(db, row_list, current_row, parent=None, view_slot=None):
|
def edit_metadata(db, row_list, current_row, parent=None, view_slot=None):
|
||||||
d = MetadataSingleDialog(db, parent)
|
d = MetadataSingleDialog(db, parent)
|
||||||
@ -467,8 +623,8 @@ def edit_metadata(db, row_list, current_row, parent=None, view_slot=None):
|
|||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
from PyQt4.Qt import QApplication
|
from PyQt4.Qt import QApplication
|
||||||
app = QApplication([])
|
app = QApplication([])
|
||||||
from calibre.library import db
|
from calibre.library import db as db_
|
||||||
db = db()
|
db = db_()
|
||||||
row_list = list(range(len(db.data)))
|
row_list = list(range(len(db.data)))
|
||||||
edit_metadata(db, row_list, 0)
|
edit_metadata(db, row_list, 0)
|
||||||
|
|
||||||
|
@ -75,7 +75,7 @@ def convert_single_ebook(parent, db, book_ids, auto_conversion=False, out_format
|
|||||||
temp_files.append(d.cover_file)
|
temp_files.append(d.cover_file)
|
||||||
args = [in_file, out_file.name, recs]
|
args = [in_file, out_file.name, recs]
|
||||||
temp_files.append(out_file)
|
temp_files.append(out_file)
|
||||||
jobs.append(('gui_convert', args, desc, d.output_format.upper(), book_id, temp_files))
|
jobs.append(('gui_convert_override', args, desc, d.output_format.upper(), book_id, temp_files))
|
||||||
|
|
||||||
changed = True
|
changed = True
|
||||||
d.break_cycles()
|
d.break_cycles()
|
||||||
@ -185,7 +185,7 @@ class QueueBulk(QProgressDialog):
|
|||||||
|
|
||||||
args = [in_file, out_file.name, lrecs]
|
args = [in_file, out_file.name, lrecs]
|
||||||
temp_files.append(out_file)
|
temp_files.append(out_file)
|
||||||
self.jobs.append(('gui_convert', args, desc, self.output_format.upper(), book_id, temp_files))
|
self.jobs.append(('gui_convert_override', args, desc, self.output_format.upper(), book_id, temp_files))
|
||||||
|
|
||||||
self.changed = True
|
self.changed = True
|
||||||
self.setValue(self.i)
|
self.setValue(self.i)
|
||||||
|
@ -440,6 +440,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
|||||||
except:
|
except:
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
olddb.break_cycles()
|
||||||
if self.device_connected:
|
if self.device_connected:
|
||||||
self.set_books_in_library(self.booklists(), reset=True)
|
self.set_books_in_library(self.booklists(), reset=True)
|
||||||
self.refresh_ondevice()
|
self.refresh_ondevice()
|
||||||
|
@ -361,6 +361,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
self.refresh()
|
self.refresh()
|
||||||
self.last_update_check = self.last_modified()
|
self.last_update_check = self.last_modified()
|
||||||
|
|
||||||
|
def break_cycles(self):
|
||||||
|
self.data = self.field_metadata = self.prefs = self.listeners = None
|
||||||
|
|
||||||
def initialize_database(self):
|
def initialize_database(self):
|
||||||
metadata_sqlite = open(P('metadata_sqlite.sql'), 'rb').read()
|
metadata_sqlite = open(P('metadata_sqlite.sql'), 'rb').read()
|
||||||
|
@ -486,6 +486,8 @@ Calibre has several keyboard shortcuts to save you time and mouse movement. Thes
|
|||||||
- Download metadata and shortcuts
|
- Download metadata and shortcuts
|
||||||
* - :kbd:`Ctrl+R`
|
* - :kbd:`Ctrl+R`
|
||||||
- Restart calibre
|
- Restart calibre
|
||||||
|
* - :kbd:`Shift+Ctrl+E`
|
||||||
|
- Add empty books to calibre
|
||||||
* - :kbd:`Ctrl+Q`
|
* - :kbd:`Ctrl+Q`
|
||||||
- Quit calibre
|
- Quit calibre
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -28,6 +28,9 @@ PARALLEL_FUNCS = {
|
|||||||
'gui_convert' :
|
'gui_convert' :
|
||||||
('calibre.gui2.convert.gui_conversion', 'gui_convert', 'notification'),
|
('calibre.gui2.convert.gui_conversion', 'gui_convert', 'notification'),
|
||||||
|
|
||||||
|
'gui_convert_override' :
|
||||||
|
('calibre.gui2.convert.gui_conversion', 'gui_convert_override', 'notification'),
|
||||||
|
|
||||||
'gui_catalog' :
|
'gui_catalog' :
|
||||||
('calibre.gui2.convert.gui_conversion', 'gui_catalog', 'notification'),
|
('calibre.gui2.convert.gui_conversion', 'gui_catalog', 'notification'),
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user