mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
...
This commit is contained in:
parent
daeaa71812
commit
631bd5d63d
927
src/calibre/gui2/metadata/basic_widgets.py
Normal file
927
src/calibre/gui2/metadata/basic_widgets.py
Normal file
@ -0,0 +1,927 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import textwrap, re, os
|
||||||
|
|
||||||
|
from PyQt4.Qt import Qt, \
|
||||||
|
QIcon, QToolButton, QWidget, QLabel, QGridLayout, \
|
||||||
|
QDoubleSpinBox, QListWidgetItem, QSize, QPixmap, \
|
||||||
|
QPushButton, QSpinBox, \
|
||||||
|
QMessageBox, QLineEdit
|
||||||
|
|
||||||
|
from calibre.gui2.widgets import EnLineEdit, CompleteComboBox, \
|
||||||
|
EnComboBox, FormatList, ImageView, CompleteLineEdit
|
||||||
|
from calibre.utils.icu import sort_key
|
||||||
|
from calibre.utils.config import tweaks
|
||||||
|
from calibre.ebooks.metadata import title_sort, authors_to_string, \
|
||||||
|
string_to_authors, check_isbn
|
||||||
|
from calibre.gui2 import file_icon_provider, \
|
||||||
|
choose_files, error_dialog, choose_images, question_dialog
|
||||||
|
from calibre.utils.date import local_tz
|
||||||
|
from calibre import strftime
|
||||||
|
from calibre.ebooks import BOOK_EXTENSIONS
|
||||||
|
from calibre.customize.ui import run_plugins_on_import
|
||||||
|
from calibre.utils.date import utcfromtimestamp
|
||||||
|
from calibre.gui2.comments_editor import Editor
|
||||||
|
from calibre.library.comments import comments_to_html
|
||||||
|
from calibre.gui2.dialogs.tag_editor import TagEditor
|
||||||
|
|
||||||
|
'''
|
||||||
|
The interface common to all widgets used to set basic metadata
|
||||||
|
class BasicMetadataWidget(object):
|
||||||
|
|
||||||
|
LABEL = "label text"
|
||||||
|
|
||||||
|
def initialize(self, db, id_):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def commit(self, db, id_):
|
||||||
|
return True
|
||||||
|
|
||||||
|
@dynamic_property
|
||||||
|
def current_val(self):
|
||||||
|
# Present in most but not all basic metadata widgets
|
||||||
|
def fget(self):
|
||||||
|
return None
|
||||||
|
def fset(self, val):
|
||||||
|
pass
|
||||||
|
return property(fget=fget, fset=fset)
|
||||||
|
'''
|
||||||
|
|
||||||
|
# Title {{{
|
||||||
|
class TitleEdit(EnLineEdit):
|
||||||
|
|
||||||
|
TITLE_ATTR = 'title'
|
||||||
|
COMMIT = True
|
||||||
|
TOOLTIP = _('Change the title of this book')
|
||||||
|
LABEL = _('&Title:')
|
||||||
|
|
||||||
|
def __init__(self, parent):
|
||||||
|
self.dialog = parent
|
||||||
|
EnLineEdit.__init__(self, parent)
|
||||||
|
self.setToolTip(self.TOOLTIP)
|
||||||
|
self.setWhatsThis(self.TOOLTIP)
|
||||||
|
|
||||||
|
def get_default(self):
|
||||||
|
return _('Unknown')
|
||||||
|
|
||||||
|
def initialize(self, db, id_):
|
||||||
|
title = getattr(db, self.TITLE_ATTR)(id_, index_is_id=True)
|
||||||
|
self.current_val = title
|
||||||
|
self.original_val = self.current_val
|
||||||
|
|
||||||
|
def commit(self, db, id_):
|
||||||
|
title = self.current_val
|
||||||
|
if self.COMMIT:
|
||||||
|
getattr(db, 'set_', self.TITLE_ATTR)(id_, title, notify=False)
|
||||||
|
else:
|
||||||
|
getattr(db, 'set_', self.TITLE_ATTR)(id_, title, notify=False,
|
||||||
|
commit=False)
|
||||||
|
return True
|
||||||
|
|
||||||
|
@dynamic_property
|
||||||
|
def current_val(self):
|
||||||
|
|
||||||
|
def fget(self):
|
||||||
|
title = unicode(self.text()).strip()
|
||||||
|
if not title:
|
||||||
|
title = self.get_default()
|
||||||
|
return title
|
||||||
|
|
||||||
|
def fset(self, val):
|
||||||
|
if hasattr(val, 'strip'):
|
||||||
|
val = val.strip()
|
||||||
|
if not val:
|
||||||
|
val = self.get_default()
|
||||||
|
self.setText(val)
|
||||||
|
self.setCursorPosition(0)
|
||||||
|
|
||||||
|
return property(fget=fget, fset=fset)
|
||||||
|
|
||||||
|
class TitleSortEdit(TitleEdit):
|
||||||
|
|
||||||
|
TITLE_ATTR = 'title_sort'
|
||||||
|
COMMIT = False
|
||||||
|
TOOLTIP = _('Specify how this book should be sorted when by title.'
|
||||||
|
' For example, The Exorcist might be sorted as Exorcist, The.')
|
||||||
|
LABEL = _('Title &sort:')
|
||||||
|
|
||||||
|
def __init__(self, parent, title_edit, autogen_button):
|
||||||
|
TitleEdit.__init__(self, parent)
|
||||||
|
self.title_edit = title_edit
|
||||||
|
|
||||||
|
base = self.TOOLTIP
|
||||||
|
ok_tooltip = '<p>' + textwrap.fill(base+'<br><br>'+
|
||||||
|
_(' The green color indicates that the current '
|
||||||
|
'title sort matches the current title'))
|
||||||
|
bad_tooltip = '<p>'+textwrap.fill(base + '<br><br>'+
|
||||||
|
_(' The red color warns that the current '
|
||||||
|
'title sort does not match the current title. '
|
||||||
|
'No action is required if this is what you want.'))
|
||||||
|
self.tooltips = (ok_tooltip, bad_tooltip)
|
||||||
|
|
||||||
|
self.title_edit.textChanged.connect(self.update_state)
|
||||||
|
self.textChanged.connect(self.update_state)
|
||||||
|
|
||||||
|
autogen_button.clicked.connect(self.auto_generate)
|
||||||
|
self.update_state()
|
||||||
|
|
||||||
|
def update_state(self, *args):
|
||||||
|
ts = title_sort(self.title_edit.current_val)
|
||||||
|
normal = ts == self.current_val
|
||||||
|
if normal:
|
||||||
|
col = 'rgb(0, 255, 0, 20%)'
|
||||||
|
else:
|
||||||
|
col = 'rgb(255, 0, 0, 20%)'
|
||||||
|
self.setStyleSheet('QLineEdit { color: black; '
|
||||||
|
'background-color: %s; }'%col)
|
||||||
|
tt = self.tooltips[0 if normal else 1]
|
||||||
|
self.setToolTip(tt)
|
||||||
|
self.setWhatsThis(tt)
|
||||||
|
|
||||||
|
def auto_generate(self, *args):
|
||||||
|
self.current_val = title_sort(self.title_edit.current_val)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# Authors {{{
|
||||||
|
class AuthorsEdit(CompleteComboBox):
|
||||||
|
|
||||||
|
TOOLTIP = ''
|
||||||
|
LABEL = _('&Author(s):')
|
||||||
|
|
||||||
|
def __init__(self, parent):
|
||||||
|
self.dialog = parent
|
||||||
|
CompleteComboBox.__init__(self, parent)
|
||||||
|
self.setToolTip(self.TOOLTIP)
|
||||||
|
self.setWhatsThis(self.TOOLTIP)
|
||||||
|
self.setEditable(True)
|
||||||
|
self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon)
|
||||||
|
|
||||||
|
def get_default(self):
|
||||||
|
return _('Unknown')
|
||||||
|
|
||||||
|
def initialize(self, db, id_):
|
||||||
|
all_authors = db.all_authors()
|
||||||
|
all_authors.sort(key=lambda x : sort_key(x[1]))
|
||||||
|
for i in all_authors:
|
||||||
|
id, name = i
|
||||||
|
name = [name.strip().replace('|', ',') for n in name.split(',')]
|
||||||
|
self.addItem(authors_to_string(name))
|
||||||
|
|
||||||
|
self.set_separator('&')
|
||||||
|
self.set_space_before_sep(True)
|
||||||
|
self.update_items_cache(db.all_author_names())
|
||||||
|
|
||||||
|
au = db.authors(id_, index_is_id=True)
|
||||||
|
if not au:
|
||||||
|
au = _('Unknown')
|
||||||
|
self.current_val = [a.strip().replace('|', ',') for a in au.split(',')]
|
||||||
|
self.original_val = self.current_val
|
||||||
|
|
||||||
|
def commit(self, db, id_):
|
||||||
|
authors = self.current_val
|
||||||
|
db.set_authors(id_, authors, notify=False)
|
||||||
|
return True
|
||||||
|
|
||||||
|
@dynamic_property
|
||||||
|
def current_val(self):
|
||||||
|
|
||||||
|
def fget(self):
|
||||||
|
au = unicode(self.text()).strip()
|
||||||
|
if not au:
|
||||||
|
au = self.get_default()
|
||||||
|
return string_to_authors(au)
|
||||||
|
|
||||||
|
def fset(self, val):
|
||||||
|
if not val:
|
||||||
|
val = [self.get_default()]
|
||||||
|
self.setEditText(' & '.join([x.strip() for x in val]))
|
||||||
|
self.lineEdit().setCursorPosition(0)
|
||||||
|
|
||||||
|
|
||||||
|
return property(fget=fget, fset=fset)
|
||||||
|
|
||||||
|
class AuthorSortEdit(EnLineEdit):
|
||||||
|
|
||||||
|
TOOLTIP = _('Specify how the author(s) of this book should be sorted. '
|
||||||
|
'For example Charles Dickens should be sorted as Dickens, '
|
||||||
|
'Charles.\nIf the box is colored green, then text matches '
|
||||||
|
'the individual author\'s sort strings. If it is colored '
|
||||||
|
'red, then the authors and this text do not match.')
|
||||||
|
LABEL = _('Author s&ort:')
|
||||||
|
|
||||||
|
def __init__(self, parent, authors_edit, autogen_button, db):
|
||||||
|
EnLineEdit.__init__(self, parent)
|
||||||
|
self.authors_edit = authors_edit
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
base = self.TOOLTIP
|
||||||
|
ok_tooltip = '<p>' + textwrap.fill(base+'<br><br>'+
|
||||||
|
_(' The green color indicates that the current '
|
||||||
|
'author sort matches the current author'))
|
||||||
|
bad_tooltip = '<p>'+textwrap.fill(base + '<br><br>'+
|
||||||
|
_(' The red color indicates that the current '
|
||||||
|
'author sort does not match the current author. '
|
||||||
|
'No action is required if this is what you want.'))
|
||||||
|
self.tooltips = (ok_tooltip, bad_tooltip)
|
||||||
|
|
||||||
|
self.authors_edit.editTextChanged.connect(self.update_state)
|
||||||
|
self.textChanged.connect(self.update_state)
|
||||||
|
|
||||||
|
autogen_button.clicked.connect(self.auto_generate)
|
||||||
|
self.update_state()
|
||||||
|
|
||||||
|
@dynamic_property
|
||||||
|
def current_val(self):
|
||||||
|
|
||||||
|
def fget(self):
|
||||||
|
return unicode(self.text()).strip()
|
||||||
|
|
||||||
|
def fset(self, val):
|
||||||
|
if not val:
|
||||||
|
val = ''
|
||||||
|
self.setText(val.strip())
|
||||||
|
self.setCursorPosition(0)
|
||||||
|
|
||||||
|
return property(fget=fget, fset=fset)
|
||||||
|
|
||||||
|
def update_state(self, *args):
|
||||||
|
au = unicode(self.authors_edit.text())
|
||||||
|
au = re.sub(r'\s+et al\.$', '', au)
|
||||||
|
au = self.db.author_sort_from_authors(string_to_authors(au))
|
||||||
|
|
||||||
|
normal = au == self.current_val
|
||||||
|
if normal:
|
||||||
|
col = 'rgb(0, 255, 0, 20%)'
|
||||||
|
else:
|
||||||
|
col = 'rgb(255, 0, 0, 20%)'
|
||||||
|
self.setStyleSheet('QLineEdit { color: black; '
|
||||||
|
'background-color: %s; }'%col)
|
||||||
|
tt = self.tooltips[0 if normal else 1]
|
||||||
|
self.setToolTip(tt)
|
||||||
|
self.setWhatsThis(tt)
|
||||||
|
|
||||||
|
def auto_generate(self, *args):
|
||||||
|
au = unicode(self.authors_edit.text())
|
||||||
|
au = re.sub(r'\s+et al\.$', '', au)
|
||||||
|
authors = string_to_authors(au)
|
||||||
|
self.current_val = self.db.author_sort_from_authors(authors)
|
||||||
|
|
||||||
|
def initialize(self, db, id_):
|
||||||
|
self.current_val = db.author_sort(id_, index_is_id=True)
|
||||||
|
|
||||||
|
def commit(self, db, id_):
|
||||||
|
aus = self.current_val
|
||||||
|
db.set_author_sort(id_, aus, notify=False, commit=False)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# Series {{{
|
||||||
|
class SeriesEdit(EnComboBox):
|
||||||
|
|
||||||
|
TOOLTIP = _('List of known series. You can add new series.')
|
||||||
|
LABEL = _('&Series:')
|
||||||
|
|
||||||
|
def __init__(self, parent):
|
||||||
|
EnComboBox.__init__(self, parent)
|
||||||
|
self.dialog = parent
|
||||||
|
self.setSizeAdjustPolicy(
|
||||||
|
self.AdjustToMinimumContentsLengthWithIcon)
|
||||||
|
self.setToolTip(self.TOOLTIP)
|
||||||
|
self.setWhatsThis(self.TOOLTIP)
|
||||||
|
self.setEditable(True)
|
||||||
|
|
||||||
|
@dynamic_property
|
||||||
|
def current_val(self):
|
||||||
|
|
||||||
|
def fget(self):
|
||||||
|
return unicode(self.currentText()).strip()
|
||||||
|
|
||||||
|
def fset(self, val):
|
||||||
|
if not val:
|
||||||
|
val = ''
|
||||||
|
self.setEditText(val.strip())
|
||||||
|
self.setCursorPosition(0)
|
||||||
|
|
||||||
|
return property(fget=fget, fset=fset)
|
||||||
|
|
||||||
|
def initialize(self, db, id_):
|
||||||
|
all_series = db.all_series()
|
||||||
|
all_series.sort(key=lambda x : sort_key(x[1]))
|
||||||
|
series_id = db.series_id(id_, index_is_id=True)
|
||||||
|
idx, c = None, 0
|
||||||
|
for i in all_series:
|
||||||
|
id, name = i
|
||||||
|
if id == series_id:
|
||||||
|
idx = c
|
||||||
|
self.addItem(name)
|
||||||
|
c += 1
|
||||||
|
|
||||||
|
self.lineEdit().setText('')
|
||||||
|
if idx is not None:
|
||||||
|
self.setCurrentIndex(idx)
|
||||||
|
self.original_val = self.current_val
|
||||||
|
|
||||||
|
def commit(self, db, id_):
|
||||||
|
series = self.current_val
|
||||||
|
db.set_series(id_, series, notify=False, commit=True)
|
||||||
|
return True
|
||||||
|
|
||||||
|
class SeriesIndexEdit(QDoubleSpinBox):
|
||||||
|
|
||||||
|
TOOLTIP = ''
|
||||||
|
LABEL = _('&Number:')
|
||||||
|
|
||||||
|
def __init__(self, parent, series_edit):
|
||||||
|
QDoubleSpinBox.__init__(self, parent)
|
||||||
|
self.dialog = parent
|
||||||
|
self.db = self.original_series_name = None
|
||||||
|
self.setMaximum(1000000)
|
||||||
|
self.series_edit = series_edit
|
||||||
|
series_edit.currentIndexChanged.connect(self.enable)
|
||||||
|
series_edit.editTextChanged.connect(self.enable)
|
||||||
|
series_edit.lineEdit().editingFinished.connect(self.increment)
|
||||||
|
self.enable()
|
||||||
|
|
||||||
|
def enable(self, *args):
|
||||||
|
self.setEnabled(bool(self.series_edit.current_val))
|
||||||
|
|
||||||
|
@dynamic_property
|
||||||
|
def current_val(self):
|
||||||
|
|
||||||
|
def fget(self):
|
||||||
|
return self.value()
|
||||||
|
|
||||||
|
def fset(self, val):
|
||||||
|
if val is None:
|
||||||
|
val = 1.0
|
||||||
|
val = float(val)
|
||||||
|
self.setValue(val)
|
||||||
|
|
||||||
|
return property(fget=fget, fset=fset)
|
||||||
|
|
||||||
|
def initialize(self, db, id_):
|
||||||
|
self.db = db
|
||||||
|
if self.series_edit.current_val:
|
||||||
|
val = db.series_index(id_, index_is_id=True)
|
||||||
|
else:
|
||||||
|
val = 1.0
|
||||||
|
self.current_val = val
|
||||||
|
self.original_val = self.current_val
|
||||||
|
self.original_series_name = self.series_edit.original_val
|
||||||
|
|
||||||
|
def commit(self, db, id_):
|
||||||
|
db.set_series_index(id_, self.current_val, notify=False, commit=False)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def increment(self):
|
||||||
|
if self.db is not None:
|
||||||
|
try:
|
||||||
|
series = self.series_edit.current_val
|
||||||
|
if series and series != self.original_series_name:
|
||||||
|
ns = 1.0
|
||||||
|
if tweaks['series_index_auto_increment'] != 'const':
|
||||||
|
ns = self.db.get_next_series_num_for(series)
|
||||||
|
self.current_val = ns
|
||||||
|
self.original_series_name = series
|
||||||
|
except:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class BuddyLabel(QLabel): # {{{
|
||||||
|
|
||||||
|
def __init__(self, buddy):
|
||||||
|
QLabel.__init__(self, buddy.LABEL)
|
||||||
|
self.setBuddy(buddy)
|
||||||
|
self.setAlignment(Qt.AlignRight|Qt.AlignVCenter)
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class Format(QListWidgetItem): # {{{
|
||||||
|
|
||||||
|
def __init__(self, parent, ext, size, path=None, timestamp=None):
|
||||||
|
self.path = path
|
||||||
|
self.ext = ext
|
||||||
|
self.size = float(size)/(1024*1024)
|
||||||
|
text = '%s (%.2f MB)'%(self.ext.upper(), self.size)
|
||||||
|
QListWidgetItem.__init__(self, file_icon_provider().icon_from_ext(ext),
|
||||||
|
text, parent, QListWidgetItem.UserType)
|
||||||
|
if timestamp is not None:
|
||||||
|
ts = timestamp.astimezone(local_tz)
|
||||||
|
t = strftime('%a, %d %b %Y [%H:%M:%S]', ts.timetuple())
|
||||||
|
text = _('Last modified: %s')%t
|
||||||
|
self.setToolTip(text)
|
||||||
|
self.setStatusTip(text)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class FormatsManager(QWidget): # {{{
|
||||||
|
|
||||||
|
def __init__(self, parent):
|
||||||
|
QWidget.__init__(self, parent)
|
||||||
|
self.dialog = parent
|
||||||
|
self.changed = False
|
||||||
|
|
||||||
|
self.l = l = QGridLayout()
|
||||||
|
self.setLayout(l)
|
||||||
|
self.cover_from_format_button = QToolButton(self)
|
||||||
|
self.cover_from_format_button.setToolTip(
|
||||||
|
_('Set the cover for the book from the selected format'))
|
||||||
|
self.cover_from_format_button.setIcon(QIcon(I('book.png')))
|
||||||
|
self.cover_from_format_button.setIconSize(QSize(32, 32))
|
||||||
|
|
||||||
|
self.metadata_from_format_button = QToolButton(self)
|
||||||
|
self.metadata_from_format_button.setIcon(QIcon(I('edit_input.png')))
|
||||||
|
self.metadata_from_format_button.setIconSize(QSize(32, 32))
|
||||||
|
# TODO: Implement the *_from_format buttons
|
||||||
|
|
||||||
|
self.add_format_button = QToolButton(self)
|
||||||
|
self.add_format_button.setIcon(QIcon(I('add_book.png')))
|
||||||
|
self.add_format_button.setIconSize(QSize(32, 32))
|
||||||
|
self.add_format_button.clicked.connect(self.add_format)
|
||||||
|
|
||||||
|
self.remove_format_button = QToolButton(self)
|
||||||
|
self.remove_format_button.setIcon(QIcon(I('trash.png')))
|
||||||
|
self.remove_format_button.setIconSize(QSize(32, 32))
|
||||||
|
self.remove_format_button.clicked.connect(self.remove_format)
|
||||||
|
|
||||||
|
self.formats = FormatList(self)
|
||||||
|
self.formats.setAcceptDrops(True)
|
||||||
|
self.formats.formats_dropped.connect(self.formats_dropped)
|
||||||
|
self.formats.delete_format.connect(self.remove_format)
|
||||||
|
self.formats.itemDoubleClicked.connect(self.show_format)
|
||||||
|
self.formats.setDragDropMode(self.formats.DropOnly)
|
||||||
|
self.formats.setIconSize(QSize(32, 32))
|
||||||
|
self.formats.setMaximumWidth(200)
|
||||||
|
|
||||||
|
l.addWidget(self.cover_from_format_button, 0, 0, 1, 1)
|
||||||
|
l.addWidget(self.metadata_from_format_button, 2, 0, 1, 1)
|
||||||
|
l.addWidget(self.add_format_button, 0, 2, 1, 1)
|
||||||
|
l.addWidget(self.remove_format_button, 2, 2, 1, 1)
|
||||||
|
l.addWidget(self.formats, 0, 1, 3, 1)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(self, db, id_):
|
||||||
|
self.changed = False
|
||||||
|
exts = db.formats(id_, index_is_id=True)
|
||||||
|
if exts:
|
||||||
|
exts = exts.split(',')
|
||||||
|
for ext in exts:
|
||||||
|
if not ext:
|
||||||
|
ext = ''
|
||||||
|
size = db.sizeof_format(id_, ext, index_is_id=True)
|
||||||
|
timestamp = db.format_last_modified(id_, ext)
|
||||||
|
if size is None:
|
||||||
|
continue
|
||||||
|
Format(self.formats, ext, size, timestamp=timestamp)
|
||||||
|
|
||||||
|
def commit(self, db, id_):
|
||||||
|
if not self.changed:
|
||||||
|
return True
|
||||||
|
old_extensions, new_extensions, paths = set(), set(), {}
|
||||||
|
for row in range(self.formats.count()):
|
||||||
|
fmt = self.formats.item(row)
|
||||||
|
ext, path = fmt.ext.lower(), fmt.path
|
||||||
|
if 'unknown' in ext.lower():
|
||||||
|
ext = None
|
||||||
|
if path:
|
||||||
|
new_extensions.add(ext)
|
||||||
|
paths[ext] = path
|
||||||
|
else:
|
||||||
|
old_extensions.add(ext)
|
||||||
|
for ext in new_extensions:
|
||||||
|
db.add_format(id_, ext, open(paths[ext], 'rb'), notify=False,
|
||||||
|
index_is_id=True)
|
||||||
|
db_extensions = set([f.lower() for f in db.formats(id_,
|
||||||
|
index_is_id=True).split(',')])
|
||||||
|
extensions = new_extensions.union(old_extensions)
|
||||||
|
for ext in db_extensions:
|
||||||
|
if ext not in extensions:
|
||||||
|
db.remove_format(id_, ext, notify=False, index_is_id=True)
|
||||||
|
|
||||||
|
self.changed = False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def add_format(self, *args):
|
||||||
|
files = choose_files(self, 'add formats dialog',
|
||||||
|
_("Choose formats for ") +
|
||||||
|
self.dialog.title.current_val,
|
||||||
|
[(_('Books'), BOOK_EXTENSIONS)])
|
||||||
|
self._add_formats(files)
|
||||||
|
|
||||||
|
def _add_formats(self, paths):
|
||||||
|
added = False
|
||||||
|
if not paths:
|
||||||
|
return added
|
||||||
|
bad_perms = []
|
||||||
|
for _file in paths:
|
||||||
|
_file = os.path.abspath(_file)
|
||||||
|
if not os.access(_file, os.R_OK):
|
||||||
|
bad_perms.append(_file)
|
||||||
|
continue
|
||||||
|
|
||||||
|
nfile = run_plugins_on_import(_file)
|
||||||
|
if nfile is not None:
|
||||||
|
_file = nfile
|
||||||
|
stat = os.stat(_file)
|
||||||
|
size = stat.st_size
|
||||||
|
ext = os.path.splitext(_file)[1].lower().replace('.', '')
|
||||||
|
timestamp = utcfromtimestamp(stat.st_mtime)
|
||||||
|
for row in range(self.formats.count()):
|
||||||
|
fmt = self.formats.item(row)
|
||||||
|
if fmt.ext.lower() == ext:
|
||||||
|
self.formats.takeItem(row)
|
||||||
|
break
|
||||||
|
Format(self.formats, ext, size, path=_file, timestamp=timestamp)
|
||||||
|
self.changed = True
|
||||||
|
added = True
|
||||||
|
if bad_perms:
|
||||||
|
error_dialog(self, _('No permission'),
|
||||||
|
_('You do not have '
|
||||||
|
'permission to read the following files:'),
|
||||||
|
det_msg='\n'.join(bad_perms), show=True)
|
||||||
|
|
||||||
|
return added
|
||||||
|
|
||||||
|
def formats_dropped(self, event, paths):
|
||||||
|
if self._add_formats(paths):
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
def remove_format(self, *args):
|
||||||
|
rows = self.formats.selectionModel().selectedRows(0)
|
||||||
|
for row in rows:
|
||||||
|
self.formats.takeItem(row.row())
|
||||||
|
self.changed = True
|
||||||
|
|
||||||
|
def show_format(self, item, *args):
|
||||||
|
fmt = item.ext
|
||||||
|
self.dialog.view_format.emit(fmt)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class Cover(ImageView): # {{{
|
||||||
|
|
||||||
|
def __init__(self, parent):
|
||||||
|
ImageView.__init__(self, parent)
|
||||||
|
self.dialog = parent
|
||||||
|
self._cdata = None
|
||||||
|
self.cover_changed.connect(self.set_pixmap_from_data)
|
||||||
|
|
||||||
|
self.select_cover_button = QPushButton(QIcon(I('document_open.png')),
|
||||||
|
_('&Browse'), parent)
|
||||||
|
self.trim_cover_button = QPushButton(QIcon(I('trim.png')),
|
||||||
|
_('T&rim'), parent)
|
||||||
|
self.remove_cover_button = QPushButton(QIcon(I('trash.png')),
|
||||||
|
_('&Remove'), parent)
|
||||||
|
|
||||||
|
self.select_cover_button.clicked.connect(self.select_cover)
|
||||||
|
self.remove_cover_button.clicked.connect(self.remove_cover)
|
||||||
|
self.trim_cover_button.clicked.connect(self.trim_cover)
|
||||||
|
|
||||||
|
self.download_cover_button = QPushButton(_('Download co&ver'), parent)
|
||||||
|
self.generate_cover_button = QPushButton(_('&Generate cover'), parent)
|
||||||
|
|
||||||
|
self.download_cover_button.clicked.connect(self.download_cover)
|
||||||
|
self.generate_cover_button.clicked.connect(self.generate_cover)
|
||||||
|
|
||||||
|
self.buttons = [self.select_cover_button, self.remove_cover_button,
|
||||||
|
self.trim_cover_button, self.download_cover_button,
|
||||||
|
self.generate_cover_button]
|
||||||
|
|
||||||
|
def select_cover(self, *args):
|
||||||
|
files = choose_images(self, 'change cover dialog',
|
||||||
|
_('Choose cover for ') +
|
||||||
|
self.dialog.title.current_val)
|
||||||
|
if not files:
|
||||||
|
return
|
||||||
|
_file = files[0]
|
||||||
|
if _file:
|
||||||
|
_file = os.path.abspath(_file)
|
||||||
|
if not os.access(_file, os.R_OK):
|
||||||
|
d = error_dialog(self, _('Cannot read'),
|
||||||
|
_('You do not have permission to read the file: ') + _file)
|
||||||
|
d.exec_()
|
||||||
|
return
|
||||||
|
cf, cover = None, None
|
||||||
|
try:
|
||||||
|
cf = open(_file, "rb")
|
||||||
|
cover = cf.read()
|
||||||
|
except IOError, e:
|
||||||
|
d = error_dialog(self, _('Error reading file'),
|
||||||
|
_("<p>There was an error reading from file: <br /><b>")
|
||||||
|
+ _file + "</b></p><br />"+str(e))
|
||||||
|
d.exec_()
|
||||||
|
if cover:
|
||||||
|
orig = self.current_val
|
||||||
|
self.current_val = cover
|
||||||
|
if self.current_val is None:
|
||||||
|
self.current_val = orig
|
||||||
|
error_dialog(self,
|
||||||
|
_("Not a valid picture"),
|
||||||
|
_file + _(" is not a valid picture"), show=True)
|
||||||
|
|
||||||
|
def remove_cover(self, *args):
|
||||||
|
self.current_val = None
|
||||||
|
|
||||||
|
def trim_cover(self, *args):
|
||||||
|
from calibre.utils.magick import Image
|
||||||
|
cdata = self.current_val
|
||||||
|
if not cdata:
|
||||||
|
return
|
||||||
|
im = Image()
|
||||||
|
im.load(cdata)
|
||||||
|
im.trim(10)
|
||||||
|
cdata = im.export('png')
|
||||||
|
self.current_val = cdata
|
||||||
|
|
||||||
|
def download_cover(self, *args):
|
||||||
|
pass # TODO: Implement this
|
||||||
|
|
||||||
|
def generate_cover(self, *args):
|
||||||
|
from calibre.ebooks import calibre_cover
|
||||||
|
from calibre.ebooks.metadata import fmt_sidx
|
||||||
|
from calibre.gui2 import config
|
||||||
|
title = self.dialog.title.current_val
|
||||||
|
author = authors_to_string(self.dialog.authors.current_val)
|
||||||
|
if not title or not author:
|
||||||
|
return error_dialog(self, _('Specify title and author'),
|
||||||
|
_('You must specify a title and author before generating '
|
||||||
|
'a cover'), show=True)
|
||||||
|
series = self.dialog.series.current_val
|
||||||
|
series_string = None
|
||||||
|
if series:
|
||||||
|
series_string = _('Book %s of %s')%(
|
||||||
|
fmt_sidx(self.dialog.series_index.current_val,
|
||||||
|
use_roman=config['use_roman_numerals_for_series_number']), series)
|
||||||
|
self.current_val = calibre_cover(title, author,
|
||||||
|
series_string=series_string)
|
||||||
|
|
||||||
|
def set_pixmap_from_data(self, data):
|
||||||
|
if not data:
|
||||||
|
self.current_val = None
|
||||||
|
return
|
||||||
|
orig = self.current_val
|
||||||
|
self.current_val = data
|
||||||
|
if self.current_val is None:
|
||||||
|
error_dialog(self, _('Invalid cover'),
|
||||||
|
_('Could not change cover as the image is invalid.'),
|
||||||
|
show=True)
|
||||||
|
self.current_val = orig
|
||||||
|
|
||||||
|
def initialize(self, db, id_):
|
||||||
|
self._cdata = None
|
||||||
|
self.current_val = db.cover(id_, index_is_id=True)
|
||||||
|
self.original_val = self.current_val
|
||||||
|
|
||||||
|
@property
|
||||||
|
def changed(self):
|
||||||
|
return self.current_val != self.original_val
|
||||||
|
|
||||||
|
@dynamic_property
|
||||||
|
def current_val(self):
|
||||||
|
def fget(self):
|
||||||
|
return self._cdata
|
||||||
|
def fset(self, cdata):
|
||||||
|
self._cdata = None
|
||||||
|
pm = QPixmap()
|
||||||
|
if cdata:
|
||||||
|
pm.loadFromData(cdata)
|
||||||
|
if pm.isNull():
|
||||||
|
pm = QPixmap(I('default_cover.png'))
|
||||||
|
else:
|
||||||
|
self._cdata = cdata
|
||||||
|
self.setPixmap(pm)
|
||||||
|
tt = _('This book has no cover')
|
||||||
|
if self._cdata:
|
||||||
|
tt = _('Cover size: %dx%d pixels') % \
|
||||||
|
(pm.width(), pm.height())
|
||||||
|
self.setToolTip(tt)
|
||||||
|
|
||||||
|
return property(fget=fget, fset=fset)
|
||||||
|
|
||||||
|
def commit(self, db, id_):
|
||||||
|
if self.changed:
|
||||||
|
if self.current_val:
|
||||||
|
db.set_cover(id_, self.current_val, notify=False, commit=False)
|
||||||
|
else:
|
||||||
|
db.remove_cover(id_, notify=False, commit=False)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class CommentsEdit(Editor): # {{{
|
||||||
|
|
||||||
|
@dynamic_property
|
||||||
|
def current_val(self):
|
||||||
|
def fget(self):
|
||||||
|
return self.html
|
||||||
|
def fset(self, val):
|
||||||
|
if not val or not val.strip():
|
||||||
|
val = ''
|
||||||
|
else:
|
||||||
|
val = comments_to_html(val)
|
||||||
|
self.html = val
|
||||||
|
return property(fget=fget, fset=fset)
|
||||||
|
|
||||||
|
def initialize(self, db, id_):
|
||||||
|
self.current_val = db.comments(id_, index_is_id=True)
|
||||||
|
self.original_val = self.current_val
|
||||||
|
|
||||||
|
def commit(self, db, id_):
|
||||||
|
db.set_comment(id_, self.current_val, notify=False, commit=False)
|
||||||
|
return True
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class RatingEdit(QSpinBox): # {{{
|
||||||
|
LABEL = _('&Rating:')
|
||||||
|
TOOLTIP = _('Rating of this book. 0-5 stars')
|
||||||
|
|
||||||
|
def __init__(self, parent):
|
||||||
|
QSpinBox.__init__(self, parent)
|
||||||
|
self.setToolTip(self.TOOLTIP)
|
||||||
|
self.setWhatsThis(self.TOOLTIP)
|
||||||
|
self.setMaximum(5)
|
||||||
|
self.setSuffix(' ' + _('stars'))
|
||||||
|
|
||||||
|
@dynamic_property
|
||||||
|
def current_val(self):
|
||||||
|
def fget(self):
|
||||||
|
return self.value()
|
||||||
|
def fset(self, val):
|
||||||
|
if val is None:
|
||||||
|
val = 0
|
||||||
|
val = int(val)
|
||||||
|
if val < 0:
|
||||||
|
val = 0
|
||||||
|
if val > 5:
|
||||||
|
val = 5
|
||||||
|
self.setValue(val)
|
||||||
|
return property(fget=fget, fset=fset)
|
||||||
|
|
||||||
|
def initialize(self, db, id_):
|
||||||
|
val = db.rating(id_, index_is_id=True)
|
||||||
|
if val > 0:
|
||||||
|
val = int(val/2.)
|
||||||
|
else:
|
||||||
|
val = 0
|
||||||
|
self.current_val = val
|
||||||
|
self.original_val = self.current_val
|
||||||
|
|
||||||
|
def commit(self, db, id_):
|
||||||
|
db.set_rating(id_, 2*self.current_val, notify=False, commit=False)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class TagsEdit(CompleteLineEdit): # {{{
|
||||||
|
LABEL = _('Ta&gs:')
|
||||||
|
TOOLTIP = '<p>'+_('Tags categorize the book. This is particularly '
|
||||||
|
'useful while searching. <br><br>They can be any words'
|
||||||
|
'or phrases, separated by commas.')
|
||||||
|
|
||||||
|
def __init__(self, parent):
|
||||||
|
CompleteLineEdit.__init__(self, parent)
|
||||||
|
self.setToolTip(self.TOOLTIP)
|
||||||
|
self.setWhatsThis(self.TOOLTIP)
|
||||||
|
|
||||||
|
@dynamic_property
|
||||||
|
def current_val(self):
|
||||||
|
def fget(self):
|
||||||
|
return [x.strip() for x in unicode(self.text()).split(',')]
|
||||||
|
def fset(self, val):
|
||||||
|
if not val:
|
||||||
|
val = []
|
||||||
|
self.setText(', '.join([x.strip() for x in val]))
|
||||||
|
return property(fget=fget, fset=fset)
|
||||||
|
|
||||||
|
def initialize(self, db, id_):
|
||||||
|
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.original_val = self.current_val
|
||||||
|
|
||||||
|
@property
|
||||||
|
def changed(self):
|
||||||
|
return self.current_val != self.original_val
|
||||||
|
|
||||||
|
def edit(self, db, id_):
|
||||||
|
if self.changed:
|
||||||
|
if question_dialog(self, _('Tags changed'),
|
||||||
|
_('You have changed the tags. In order to use the tags'
|
||||||
|
' editor, you must either discard or apply these '
|
||||||
|
'changes'), show_copy_button=False,
|
||||||
|
buttons=QMessageBox.Apply|QMessageBox.Discard,
|
||||||
|
yes_button=QMessageBox.Apply):
|
||||||
|
self.commit(db, id_)
|
||||||
|
db.commit()
|
||||||
|
self.original_val = self.current_val
|
||||||
|
else:
|
||||||
|
self.current_val = self.original_val
|
||||||
|
d = TagEditor(self, db, id_)
|
||||||
|
if d.exec_() == TagEditor.Accepted:
|
||||||
|
self.current_val = d.tags
|
||||||
|
self.update_items_cache(db.all_tags())
|
||||||
|
|
||||||
|
|
||||||
|
def commit(self, db, id_):
|
||||||
|
db.set_tags(id_, self.current_val, notify=False, commit=False)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class ISBNEdit(QLineEdit): # {{{
|
||||||
|
LABEL = _('IS&BN:')
|
||||||
|
|
||||||
|
def __init__(self, parent):
|
||||||
|
QLineEdit.__init__(self, parent)
|
||||||
|
self.pat = re.compile(r'[^0-9a-zA-Z]')
|
||||||
|
self.textChanged.connect(self.validate)
|
||||||
|
|
||||||
|
@dynamic_property
|
||||||
|
def current_val(self):
|
||||||
|
def fget(self):
|
||||||
|
return self.pat.sub('', unicode(self.text()).strip())
|
||||||
|
def fset(self, val):
|
||||||
|
if not val:
|
||||||
|
val = ''
|
||||||
|
self.setText(val.strip())
|
||||||
|
return property(fget=fget, fset=fset)
|
||||||
|
|
||||||
|
def initialize(self, db, id_):
|
||||||
|
self.current_val = db.isbn(id_, index_is_id=True)
|
||||||
|
self.original_val = self.current_val
|
||||||
|
|
||||||
|
def commit(self, db, id_):
|
||||||
|
db.set_isbn(id_, self.current_val, notify=False, commit=False)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def validate(self, *args):
|
||||||
|
isbn = self.current_val
|
||||||
|
tt = _('This ISBN number is valid')
|
||||||
|
if not isbn:
|
||||||
|
col = 'rgba(0,255,0,0%)'
|
||||||
|
elif check_isbn(isbn) is not None:
|
||||||
|
col = 'rgba(0,255,0,20%)'
|
||||||
|
else:
|
||||||
|
col = 'rgba(255,0,0,20%)'
|
||||||
|
tt = _('This ISBN number is invalid')
|
||||||
|
self.setToolTip(tt)
|
||||||
|
self.setStyleSheet('QLineEdit { background-color: %s }'%col)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class PublisherEdit(EnComboBox): # {{{
|
||||||
|
LABEL = _('&Publisher:')
|
||||||
|
|
||||||
|
def __init__(self, parent):
|
||||||
|
EnComboBox.__init__(self, parent)
|
||||||
|
self.setSizeAdjustPolicy(
|
||||||
|
self.AdjustToMinimumContentsLengthWithIcon)
|
||||||
|
|
||||||
|
@dynamic_property
|
||||||
|
def current_val(self):
|
||||||
|
|
||||||
|
def fget(self):
|
||||||
|
return unicode(self.currentText()).strip()
|
||||||
|
|
||||||
|
def fset(self, val):
|
||||||
|
if not val:
|
||||||
|
val = ''
|
||||||
|
self.setEditText(val.strip())
|
||||||
|
self.setCursorPosition(0)
|
||||||
|
|
||||||
|
return property(fget=fget, fset=fset)
|
||||||
|
|
||||||
|
def initialize(self, db, id_):
|
||||||
|
all_publishers = db.all_publishers()
|
||||||
|
all_publishers.sort(key=lambda x : sort_key(x[1]))
|
||||||
|
publisher_id = db.publisher_id(id_, index_is_id=True)
|
||||||
|
idx, c = None, 0
|
||||||
|
for i in all_publishers:
|
||||||
|
id, name = i
|
||||||
|
if id == publisher_id:
|
||||||
|
idx = c
|
||||||
|
self.addItem(name)
|
||||||
|
c += 1
|
||||||
|
|
||||||
|
self.setEditText('')
|
||||||
|
if idx is not None:
|
||||||
|
self.setCurrentIndex(idx)
|
||||||
|
|
||||||
|
def commit(self, db, id_):
|
||||||
|
db.set_publisher(id_, self.current_val, notify=False, commit=False)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
|
@ -5,924 +5,17 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import textwrap, re, os
|
|
||||||
|
|
||||||
from PyQt4.Qt import QDialogButtonBox, Qt, QTabWidget, QScrollArea, \
|
from PyQt4.Qt import Qt, QVBoxLayout, QHBoxLayout, QWidget, \
|
||||||
QVBoxLayout, QIcon, QToolButton, QWidget, QLabel, QGridLayout, \
|
QGridLayout, pyqtSignal, QDialogButtonBox, QScrollArea, \
|
||||||
QDoubleSpinBox, QListWidgetItem, QSize, pyqtSignal, QPixmap, \
|
QTabWidget, QIcon, QToolButton, QSplitter, QGroupBox
|
||||||
QSplitter, QPushButton, QGroupBox, QHBoxLayout, QSpinBox, \
|
|
||||||
QMessageBox, QLineEdit
|
|
||||||
|
|
||||||
from calibre.gui2 import ResizableDialog, file_icon_provider, \
|
from calibre.ebooks.metadata import authors_to_string, string_to_authors
|
||||||
choose_files, error_dialog, choose_images, question_dialog
|
from calibre.gui2 import ResizableDialog
|
||||||
from calibre.utils.icu import sort_key
|
from calibre.gui2.metadata.basic_widgets import TitleEdit, AuthorsEdit, \
|
||||||
from calibre.utils.config import tweaks
|
AuthorSortEdit, TitleSortEdit, SeriesEdit, SeriesIndexEdit, ISBNEdit, \
|
||||||
from calibre.gui2.widgets import EnLineEdit, CompleteComboBox, \
|
RatingEdit, PublisherEdit, TagsEdit, FormatsManager, Cover, CommentsEdit, \
|
||||||
EnComboBox, FormatList, ImageView, CompleteLineEdit
|
BuddyLabel
|
||||||
from calibre.ebooks.metadata import title_sort, authors_to_string, \
|
|
||||||
string_to_authors, check_isbn
|
|
||||||
from calibre.utils.date import local_tz
|
|
||||||
from calibre import strftime
|
|
||||||
from calibre.ebooks import BOOK_EXTENSIONS
|
|
||||||
from calibre.customize.ui import run_plugins_on_import
|
|
||||||
from calibre.utils.date import utcfromtimestamp
|
|
||||||
from calibre.gui2.comments_editor import Editor
|
|
||||||
from calibre.library.comments import comments_to_html
|
|
||||||
from calibre.gui2.dialogs.tag_editor import TagEditor
|
|
||||||
|
|
||||||
'''
|
|
||||||
The interface common to all widgets used to set basic metadata
|
|
||||||
class BasicMetadataWidget(object):
|
|
||||||
|
|
||||||
LABEL = "label text"
|
|
||||||
|
|
||||||
def initialize(self, db, id_):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def commit(self, db, id_):
|
|
||||||
return True
|
|
||||||
|
|
||||||
@dynamic_property
|
|
||||||
def current_val(self):
|
|
||||||
# Present in most but not all basic metadata widgets
|
|
||||||
def fget(self):
|
|
||||||
return None
|
|
||||||
def fset(self, val):
|
|
||||||
pass
|
|
||||||
return property(fget=fget, fset=fset)
|
|
||||||
'''
|
|
||||||
|
|
||||||
# Title {{{
|
|
||||||
class TitleEdit(EnLineEdit):
|
|
||||||
|
|
||||||
TITLE_ATTR = 'title'
|
|
||||||
COMMIT = True
|
|
||||||
TOOLTIP = _('Change the title of this book')
|
|
||||||
LABEL = _('&Title:')
|
|
||||||
|
|
||||||
def __init__(self, parent):
|
|
||||||
self.dialog = parent
|
|
||||||
EnLineEdit.__init__(self, parent)
|
|
||||||
self.setToolTip(self.TOOLTIP)
|
|
||||||
self.setWhatsThis(self.TOOLTIP)
|
|
||||||
|
|
||||||
def get_default(self):
|
|
||||||
return _('Unknown')
|
|
||||||
|
|
||||||
def initialize(self, db, id_):
|
|
||||||
title = getattr(db, self.TITLE_ATTR)(id_, index_is_id=True)
|
|
||||||
self.current_val = title
|
|
||||||
self.original_val = self.current_val
|
|
||||||
|
|
||||||
def commit(self, db, id_):
|
|
||||||
title = self.current_val
|
|
||||||
if self.COMMIT:
|
|
||||||
getattr(db, 'set_', self.TITLE_ATTR)(id_, title, notify=False)
|
|
||||||
else:
|
|
||||||
getattr(db, 'set_', self.TITLE_ATTR)(id_, title, notify=False,
|
|
||||||
commit=False)
|
|
||||||
return True
|
|
||||||
|
|
||||||
@dynamic_property
|
|
||||||
def current_val(self):
|
|
||||||
|
|
||||||
def fget(self):
|
|
||||||
title = unicode(self.text()).strip()
|
|
||||||
if not title:
|
|
||||||
title = self.get_default()
|
|
||||||
return title
|
|
||||||
|
|
||||||
def fset(self, val):
|
|
||||||
if hasattr(val, 'strip'):
|
|
||||||
val = val.strip()
|
|
||||||
if not val:
|
|
||||||
val = self.get_default()
|
|
||||||
self.setText(val)
|
|
||||||
self.setCursorPosition(0)
|
|
||||||
|
|
||||||
return property(fget=fget, fset=fset)
|
|
||||||
|
|
||||||
class TitleSortEdit(TitleEdit):
|
|
||||||
|
|
||||||
TITLE_ATTR = 'title_sort'
|
|
||||||
COMMIT = False
|
|
||||||
TOOLTIP = _('Specify how this book should be sorted when by title.'
|
|
||||||
' For example, The Exorcist might be sorted as Exorcist, The.')
|
|
||||||
LABEL = _('Title &sort:')
|
|
||||||
|
|
||||||
def __init__(self, parent, title_edit, autogen_button):
|
|
||||||
TitleEdit.__init__(self, parent)
|
|
||||||
self.title_edit = title_edit
|
|
||||||
|
|
||||||
base = self.TOOLTIP
|
|
||||||
ok_tooltip = '<p>' + textwrap.fill(base+'<br><br>'+
|
|
||||||
_(' The green color indicates that the current '
|
|
||||||
'title sort matches the current title'))
|
|
||||||
bad_tooltip = '<p>'+textwrap.fill(base + '<br><br>'+
|
|
||||||
_(' The red color warns that the current '
|
|
||||||
'title sort does not match the current title. '
|
|
||||||
'No action is required if this is what you want.'))
|
|
||||||
self.tooltips = (ok_tooltip, bad_tooltip)
|
|
||||||
|
|
||||||
self.title_edit.textChanged.connect(self.update_state)
|
|
||||||
self.textChanged.connect(self.update_state)
|
|
||||||
|
|
||||||
autogen_button.clicked.connect(self.auto_generate)
|
|
||||||
self.update_state()
|
|
||||||
|
|
||||||
def update_state(self, *args):
|
|
||||||
ts = title_sort(self.title_edit.current_val)
|
|
||||||
normal = ts == self.current_val
|
|
||||||
if normal:
|
|
||||||
col = 'rgb(0, 255, 0, 20%)'
|
|
||||||
else:
|
|
||||||
col = 'rgb(255, 0, 0, 20%)'
|
|
||||||
self.setStyleSheet('QLineEdit { color: black; '
|
|
||||||
'background-color: %s; }'%col)
|
|
||||||
tt = self.tooltips[0 if normal else 1]
|
|
||||||
self.setToolTip(tt)
|
|
||||||
self.setWhatsThis(tt)
|
|
||||||
|
|
||||||
def auto_generate(self, *args):
|
|
||||||
self.current_val = title_sort(self.title_edit.current_val)
|
|
||||||
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
# Authors {{{
|
|
||||||
class AuthorsEdit(CompleteComboBox):
|
|
||||||
|
|
||||||
TOOLTIP = ''
|
|
||||||
LABEL = _('&Author(s):')
|
|
||||||
|
|
||||||
def __init__(self, parent):
|
|
||||||
self.dialog = parent
|
|
||||||
CompleteComboBox.__init__(self, parent)
|
|
||||||
self.setToolTip(self.TOOLTIP)
|
|
||||||
self.setWhatsThis(self.TOOLTIP)
|
|
||||||
self.setEditable(True)
|
|
||||||
self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon)
|
|
||||||
|
|
||||||
def get_default(self):
|
|
||||||
return _('Unknown')
|
|
||||||
|
|
||||||
def initialize(self, db, id_):
|
|
||||||
all_authors = db.all_authors()
|
|
||||||
all_authors.sort(key=lambda x : sort_key(x[1]))
|
|
||||||
for i in all_authors:
|
|
||||||
id, name = i
|
|
||||||
name = [name.strip().replace('|', ',') for n in name.split(',')]
|
|
||||||
self.addItem(authors_to_string(name))
|
|
||||||
|
|
||||||
self.set_separator('&')
|
|
||||||
self.set_space_before_sep(True)
|
|
||||||
self.update_items_cache(db.all_author_names())
|
|
||||||
|
|
||||||
au = db.authors(id_, index_is_id=True)
|
|
||||||
if not au:
|
|
||||||
au = _('Unknown')
|
|
||||||
self.current_val = [a.strip().replace('|', ',') for a in au.split(',')]
|
|
||||||
self.original_val = self.current_val
|
|
||||||
|
|
||||||
def commit(self, db, id_):
|
|
||||||
authors = self.current_val
|
|
||||||
db.set_authors(id_, authors, notify=False)
|
|
||||||
return True
|
|
||||||
|
|
||||||
@dynamic_property
|
|
||||||
def current_val(self):
|
|
||||||
|
|
||||||
def fget(self):
|
|
||||||
au = unicode(self.text()).strip()
|
|
||||||
if not au:
|
|
||||||
au = self.get_default()
|
|
||||||
return string_to_authors(au)
|
|
||||||
|
|
||||||
def fset(self, val):
|
|
||||||
if not val:
|
|
||||||
val = [self.get_default()]
|
|
||||||
self.setEditText(' & '.join([x.strip() for x in val]))
|
|
||||||
self.lineEdit().setCursorPosition(0)
|
|
||||||
|
|
||||||
|
|
||||||
return property(fget=fget, fset=fset)
|
|
||||||
|
|
||||||
class AuthorSortEdit(EnLineEdit):
|
|
||||||
|
|
||||||
TOOLTIP = _('Specify how the author(s) of this book should be sorted. '
|
|
||||||
'For example Charles Dickens should be sorted as Dickens, '
|
|
||||||
'Charles.\nIf the box is colored green, then text matches '
|
|
||||||
'the individual author\'s sort strings. If it is colored '
|
|
||||||
'red, then the authors and this text do not match.')
|
|
||||||
LABEL = _('Author s&ort:')
|
|
||||||
|
|
||||||
def __init__(self, parent, authors_edit, autogen_button, db):
|
|
||||||
EnLineEdit.__init__(self, parent)
|
|
||||||
self.authors_edit = authors_edit
|
|
||||||
self.db = db
|
|
||||||
|
|
||||||
base = self.TOOLTIP
|
|
||||||
ok_tooltip = '<p>' + textwrap.fill(base+'<br><br>'+
|
|
||||||
_(' The green color indicates that the current '
|
|
||||||
'author sort matches the current author'))
|
|
||||||
bad_tooltip = '<p>'+textwrap.fill(base + '<br><br>'+
|
|
||||||
_(' The red color indicates that the current '
|
|
||||||
'author sort does not match the current author. '
|
|
||||||
'No action is required if this is what you want.'))
|
|
||||||
self.tooltips = (ok_tooltip, bad_tooltip)
|
|
||||||
|
|
||||||
self.authors_edit.editTextChanged.connect(self.update_state)
|
|
||||||
self.textChanged.connect(self.update_state)
|
|
||||||
|
|
||||||
autogen_button.clicked.connect(self.auto_generate)
|
|
||||||
self.update_state()
|
|
||||||
|
|
||||||
@dynamic_property
|
|
||||||
def current_val(self):
|
|
||||||
|
|
||||||
def fget(self):
|
|
||||||
return unicode(self.text()).strip()
|
|
||||||
|
|
||||||
def fset(self, val):
|
|
||||||
if not val:
|
|
||||||
val = ''
|
|
||||||
self.setText(val.strip())
|
|
||||||
self.setCursorPosition(0)
|
|
||||||
|
|
||||||
return property(fget=fget, fset=fset)
|
|
||||||
|
|
||||||
def update_state(self, *args):
|
|
||||||
au = unicode(self.authors_edit.text())
|
|
||||||
au = re.sub(r'\s+et al\.$', '', au)
|
|
||||||
au = self.db.author_sort_from_authors(string_to_authors(au))
|
|
||||||
|
|
||||||
normal = au == self.current_val
|
|
||||||
if normal:
|
|
||||||
col = 'rgb(0, 255, 0, 20%)'
|
|
||||||
else:
|
|
||||||
col = 'rgb(255, 0, 0, 20%)'
|
|
||||||
self.setStyleSheet('QLineEdit { color: black; '
|
|
||||||
'background-color: %s; }'%col)
|
|
||||||
tt = self.tooltips[0 if normal else 1]
|
|
||||||
self.setToolTip(tt)
|
|
||||||
self.setWhatsThis(tt)
|
|
||||||
|
|
||||||
def auto_generate(self, *args):
|
|
||||||
au = unicode(self.authors_edit.text())
|
|
||||||
au = re.sub(r'\s+et al\.$', '', au)
|
|
||||||
authors = string_to_authors(au)
|
|
||||||
self.current_val = self.db.author_sort_from_authors(authors)
|
|
||||||
|
|
||||||
def initialize(self, db, id_):
|
|
||||||
self.current_val = db.author_sort(id_, index_is_id=True)
|
|
||||||
|
|
||||||
def commit(self, db, id_):
|
|
||||||
aus = self.current_val
|
|
||||||
db.set_author_sort(id_, aus, notify=False, commit=False)
|
|
||||||
return True
|
|
||||||
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
# Series {{{
|
|
||||||
class SeriesEdit(EnComboBox):
|
|
||||||
|
|
||||||
TOOLTIP = _('List of known series. You can add new series.')
|
|
||||||
LABEL = _('&Series:')
|
|
||||||
|
|
||||||
def __init__(self, parent):
|
|
||||||
EnComboBox.__init__(self, parent)
|
|
||||||
self.dialog = parent
|
|
||||||
self.setSizeAdjustPolicy(
|
|
||||||
self.AdjustToMinimumContentsLengthWithIcon)
|
|
||||||
self.setToolTip(self.TOOLTIP)
|
|
||||||
self.setWhatsThis(self.TOOLTIP)
|
|
||||||
self.setEditable(True)
|
|
||||||
|
|
||||||
@dynamic_property
|
|
||||||
def current_val(self):
|
|
||||||
|
|
||||||
def fget(self):
|
|
||||||
return unicode(self.currentText()).strip()
|
|
||||||
|
|
||||||
def fset(self, val):
|
|
||||||
if not val:
|
|
||||||
val = ''
|
|
||||||
self.setEditText(val.strip())
|
|
||||||
self.setCursorPosition(0)
|
|
||||||
|
|
||||||
return property(fget=fget, fset=fset)
|
|
||||||
|
|
||||||
def initialize(self, db, id_):
|
|
||||||
all_series = db.all_series()
|
|
||||||
all_series.sort(key=lambda x : sort_key(x[1]))
|
|
||||||
series_id = db.series_id(id_, index_is_id=True)
|
|
||||||
idx, c = None, 0
|
|
||||||
for i in all_series:
|
|
||||||
id, name = i
|
|
||||||
if id == series_id:
|
|
||||||
idx = c
|
|
||||||
self.addItem(name)
|
|
||||||
c += 1
|
|
||||||
|
|
||||||
self.lineEdit().setText('')
|
|
||||||
if idx is not None:
|
|
||||||
self.setCurrentIndex(idx)
|
|
||||||
self.original_val = self.current_val
|
|
||||||
|
|
||||||
def commit(self, db, id_):
|
|
||||||
series = self.current_val
|
|
||||||
db.set_series(id_, series, notify=False, commit=True)
|
|
||||||
return True
|
|
||||||
|
|
||||||
class SeriesIndexEdit(QDoubleSpinBox):
|
|
||||||
|
|
||||||
TOOLTIP = ''
|
|
||||||
LABEL = _('&Number:')
|
|
||||||
|
|
||||||
def __init__(self, parent, series_edit):
|
|
||||||
QDoubleSpinBox.__init__(self, parent)
|
|
||||||
self.dialog = parent
|
|
||||||
self.db = self.original_series_name = None
|
|
||||||
self.setMaximum(1000000)
|
|
||||||
self.series_edit = series_edit
|
|
||||||
series_edit.currentIndexChanged.connect(self.enable)
|
|
||||||
series_edit.editTextChanged.connect(self.enable)
|
|
||||||
series_edit.lineEdit().editingFinished.connect(self.increment)
|
|
||||||
self.enable()
|
|
||||||
|
|
||||||
def enable(self, *args):
|
|
||||||
self.setEnabled(bool(self.series_edit.current_val))
|
|
||||||
|
|
||||||
@dynamic_property
|
|
||||||
def current_val(self):
|
|
||||||
|
|
||||||
def fget(self):
|
|
||||||
return self.value()
|
|
||||||
|
|
||||||
def fset(self, val):
|
|
||||||
if val is None:
|
|
||||||
val = 1.0
|
|
||||||
val = float(val)
|
|
||||||
self.setValue(val)
|
|
||||||
|
|
||||||
return property(fget=fget, fset=fset)
|
|
||||||
|
|
||||||
def initialize(self, db, id_):
|
|
||||||
self.db = db
|
|
||||||
if self.series_edit.current_val:
|
|
||||||
val = db.series_index(id_, index_is_id=True)
|
|
||||||
else:
|
|
||||||
val = 1.0
|
|
||||||
self.current_val = val
|
|
||||||
self.original_val = self.current_val
|
|
||||||
self.original_series_name = self.series_edit.original_val
|
|
||||||
|
|
||||||
def commit(self, db, id_):
|
|
||||||
db.set_series_index(id_, self.current_val, notify=False, commit=False)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def increment(self):
|
|
||||||
if self.db is not None:
|
|
||||||
try:
|
|
||||||
series = self.series_edit.current_val
|
|
||||||
if series and series != self.original_series_name:
|
|
||||||
ns = 1.0
|
|
||||||
if tweaks['series_index_auto_increment'] != 'const':
|
|
||||||
ns = self.db.get_next_series_num_for(series)
|
|
||||||
self.current_val = ns
|
|
||||||
self.original_series_name = series
|
|
||||||
except:
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
|
|
||||||
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
class BuddyLabel(QLabel): # {{{
|
|
||||||
|
|
||||||
def __init__(self, buddy):
|
|
||||||
QLabel.__init__(self, buddy.LABEL)
|
|
||||||
self.setBuddy(buddy)
|
|
||||||
self.setAlignment(Qt.AlignRight|Qt.AlignVCenter)
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
class Format(QListWidgetItem): # {{{
|
|
||||||
|
|
||||||
def __init__(self, parent, ext, size, path=None, timestamp=None):
|
|
||||||
self.path = path
|
|
||||||
self.ext = ext
|
|
||||||
self.size = float(size)/(1024*1024)
|
|
||||||
text = '%s (%.2f MB)'%(self.ext.upper(), self.size)
|
|
||||||
QListWidgetItem.__init__(self, file_icon_provider().icon_from_ext(ext),
|
|
||||||
text, parent, QListWidgetItem.UserType)
|
|
||||||
if timestamp is not None:
|
|
||||||
ts = timestamp.astimezone(local_tz)
|
|
||||||
t = strftime('%a, %d %b %Y [%H:%M:%S]', ts.timetuple())
|
|
||||||
text = _('Last modified: %s')%t
|
|
||||||
self.setToolTip(text)
|
|
||||||
self.setStatusTip(text)
|
|
||||||
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
class FormatsManager(QWidget): # {{{
|
|
||||||
|
|
||||||
def __init__(self, parent):
|
|
||||||
QWidget.__init__(self, parent)
|
|
||||||
self.dialog = parent
|
|
||||||
self.changed = False
|
|
||||||
|
|
||||||
self.l = l = QGridLayout()
|
|
||||||
self.setLayout(l)
|
|
||||||
self.cover_from_format_button = QToolButton(self)
|
|
||||||
self.cover_from_format_button.setToolTip(
|
|
||||||
_('Set the cover for the book from the selected format'))
|
|
||||||
self.cover_from_format_button.setIcon(QIcon(I('book.png')))
|
|
||||||
self.cover_from_format_button.setIconSize(QSize(32, 32))
|
|
||||||
|
|
||||||
self.metadata_from_format_button = QToolButton(self)
|
|
||||||
self.metadata_from_format_button.setIcon(QIcon(I('edit_input.png')))
|
|
||||||
self.metadata_from_format_button.setIconSize(QSize(32, 32))
|
|
||||||
# TODO: Implement the *_from_format buttons
|
|
||||||
|
|
||||||
self.add_format_button = QToolButton(self)
|
|
||||||
self.add_format_button.setIcon(QIcon(I('add_book.png')))
|
|
||||||
self.add_format_button.setIconSize(QSize(32, 32))
|
|
||||||
self.add_format_button.clicked.connect(self.add_format)
|
|
||||||
|
|
||||||
self.remove_format_button = QToolButton(self)
|
|
||||||
self.remove_format_button.setIcon(QIcon(I('trash.png')))
|
|
||||||
self.remove_format_button.setIconSize(QSize(32, 32))
|
|
||||||
self.remove_format_button.clicked.connect(self.remove_format)
|
|
||||||
|
|
||||||
self.formats = FormatList(self)
|
|
||||||
self.formats.setAcceptDrops(True)
|
|
||||||
self.formats.formats_dropped.connect(self.formats_dropped)
|
|
||||||
self.formats.delete_format.connect(self.remove_format)
|
|
||||||
self.formats.itemDoubleClicked.connect(self.show_format)
|
|
||||||
self.formats.setDragDropMode(self.formats.DropOnly)
|
|
||||||
self.formats.setIconSize(QSize(32, 32))
|
|
||||||
self.formats.setMaximumWidth(200)
|
|
||||||
|
|
||||||
l.addWidget(self.cover_from_format_button, 0, 0, 1, 1)
|
|
||||||
l.addWidget(self.metadata_from_format_button, 2, 0, 1, 1)
|
|
||||||
l.addWidget(self.add_format_button, 0, 2, 1, 1)
|
|
||||||
l.addWidget(self.remove_format_button, 2, 2, 1, 1)
|
|
||||||
l.addWidget(self.formats, 0, 1, 3, 1)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def initialize(self, db, id_):
|
|
||||||
self.changed = False
|
|
||||||
exts = db.formats(id_, index_is_id=True)
|
|
||||||
if exts:
|
|
||||||
exts = exts.split(',')
|
|
||||||
for ext in exts:
|
|
||||||
if not ext:
|
|
||||||
ext = ''
|
|
||||||
size = db.sizeof_format(id_, ext, index_is_id=True)
|
|
||||||
timestamp = db.format_last_modified(id_, ext)
|
|
||||||
if size is None:
|
|
||||||
continue
|
|
||||||
Format(self.formats, ext, size, timestamp=timestamp)
|
|
||||||
|
|
||||||
def commit(self, db, id_):
|
|
||||||
if not self.changed:
|
|
||||||
return True
|
|
||||||
old_extensions, new_extensions, paths = set(), set(), {}
|
|
||||||
for row in range(self.formats.count()):
|
|
||||||
fmt = self.formats.item(row)
|
|
||||||
ext, path = fmt.ext.lower(), fmt.path
|
|
||||||
if 'unknown' in ext.lower():
|
|
||||||
ext = None
|
|
||||||
if path:
|
|
||||||
new_extensions.add(ext)
|
|
||||||
paths[ext] = path
|
|
||||||
else:
|
|
||||||
old_extensions.add(ext)
|
|
||||||
for ext in new_extensions:
|
|
||||||
db.add_format(id_, ext, open(paths[ext], 'rb'), notify=False,
|
|
||||||
index_is_id=True)
|
|
||||||
db_extensions = set([f.lower() for f in db.formats(id_,
|
|
||||||
index_is_id=True).split(',')])
|
|
||||||
extensions = new_extensions.union(old_extensions)
|
|
||||||
for ext in db_extensions:
|
|
||||||
if ext not in extensions:
|
|
||||||
db.remove_format(id_, ext, notify=False, index_is_id=True)
|
|
||||||
|
|
||||||
self.changed = False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def add_format(self, *args):
|
|
||||||
files = choose_files(self, 'add formats dialog',
|
|
||||||
_("Choose formats for ") +
|
|
||||||
self.dialog.title.current_val,
|
|
||||||
[(_('Books'), BOOK_EXTENSIONS)])
|
|
||||||
self._add_formats(files)
|
|
||||||
|
|
||||||
def _add_formats(self, paths):
|
|
||||||
added = False
|
|
||||||
if not paths:
|
|
||||||
return added
|
|
||||||
bad_perms = []
|
|
||||||
for _file in paths:
|
|
||||||
_file = os.path.abspath(_file)
|
|
||||||
if not os.access(_file, os.R_OK):
|
|
||||||
bad_perms.append(_file)
|
|
||||||
continue
|
|
||||||
|
|
||||||
nfile = run_plugins_on_import(_file)
|
|
||||||
if nfile is not None:
|
|
||||||
_file = nfile
|
|
||||||
stat = os.stat(_file)
|
|
||||||
size = stat.st_size
|
|
||||||
ext = os.path.splitext(_file)[1].lower().replace('.', '')
|
|
||||||
timestamp = utcfromtimestamp(stat.st_mtime)
|
|
||||||
for row in range(self.formats.count()):
|
|
||||||
fmt = self.formats.item(row)
|
|
||||||
if fmt.ext.lower() == ext:
|
|
||||||
self.formats.takeItem(row)
|
|
||||||
break
|
|
||||||
Format(self.formats, ext, size, path=_file, timestamp=timestamp)
|
|
||||||
self.changed = True
|
|
||||||
added = True
|
|
||||||
if bad_perms:
|
|
||||||
error_dialog(self, _('No permission'),
|
|
||||||
_('You do not have '
|
|
||||||
'permission to read the following files:'),
|
|
||||||
det_msg='\n'.join(bad_perms), show=True)
|
|
||||||
|
|
||||||
return added
|
|
||||||
|
|
||||||
def formats_dropped(self, event, paths):
|
|
||||||
if self._add_formats(paths):
|
|
||||||
event.accept()
|
|
||||||
|
|
||||||
def remove_format(self, *args):
|
|
||||||
rows = self.formats.selectionModel().selectedRows(0)
|
|
||||||
for row in rows:
|
|
||||||
self.formats.takeItem(row.row())
|
|
||||||
self.changed = True
|
|
||||||
|
|
||||||
def show_format(self, item, *args):
|
|
||||||
fmt = item.ext
|
|
||||||
self.dialog.view_format.emit(fmt)
|
|
||||||
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
class Cover(ImageView): # {{{
|
|
||||||
|
|
||||||
def __init__(self, parent):
|
|
||||||
ImageView.__init__(self, parent)
|
|
||||||
self.dialog = parent
|
|
||||||
self._cdata = None
|
|
||||||
self.cover_changed.connect(self.set_pixmap_from_data)
|
|
||||||
|
|
||||||
self.select_cover_button = QPushButton(QIcon(I('document_open.png')),
|
|
||||||
_('&Browse'), parent)
|
|
||||||
self.trim_cover_button = QPushButton(QIcon(I('trim.png')),
|
|
||||||
_('T&rim'), parent)
|
|
||||||
self.remove_cover_button = QPushButton(QIcon(I('trash.png')),
|
|
||||||
_('&Remove'), parent)
|
|
||||||
|
|
||||||
self.select_cover_button.clicked.connect(self.select_cover)
|
|
||||||
self.remove_cover_button.clicked.connect(self.remove_cover)
|
|
||||||
self.trim_cover_button.clicked.connect(self.trim_cover)
|
|
||||||
|
|
||||||
self.download_cover_button = QPushButton(_('Download co&ver'), parent)
|
|
||||||
self.generate_cover_button = QPushButton(_('&Generate cover'), parent)
|
|
||||||
|
|
||||||
self.download_cover_button.clicked.connect(self.download_cover)
|
|
||||||
self.generate_cover_button.clicked.connect(self.generate_cover)
|
|
||||||
|
|
||||||
self.buttons = [self.select_cover_button, self.remove_cover_button,
|
|
||||||
self.trim_cover_button, self.download_cover_button,
|
|
||||||
self.generate_cover_button]
|
|
||||||
|
|
||||||
def select_cover(self, *args):
|
|
||||||
files = choose_images(self, 'change cover dialog',
|
|
||||||
_('Choose cover for ') +
|
|
||||||
self.dialog.title.current_val)
|
|
||||||
if not files:
|
|
||||||
return
|
|
||||||
_file = files[0]
|
|
||||||
if _file:
|
|
||||||
_file = os.path.abspath(_file)
|
|
||||||
if not os.access(_file, os.R_OK):
|
|
||||||
d = error_dialog(self, _('Cannot read'),
|
|
||||||
_('You do not have permission to read the file: ') + _file)
|
|
||||||
d.exec_()
|
|
||||||
return
|
|
||||||
cf, cover = None, None
|
|
||||||
try:
|
|
||||||
cf = open(_file, "rb")
|
|
||||||
cover = cf.read()
|
|
||||||
except IOError, e:
|
|
||||||
d = error_dialog(self, _('Error reading file'),
|
|
||||||
_("<p>There was an error reading from file: <br /><b>")
|
|
||||||
+ _file + "</b></p><br />"+str(e))
|
|
||||||
d.exec_()
|
|
||||||
if cover:
|
|
||||||
orig = self.current_val
|
|
||||||
self.current_val = cover
|
|
||||||
if self.current_val is None:
|
|
||||||
self.current_val = orig
|
|
||||||
error_dialog(self,
|
|
||||||
_("Not a valid picture"),
|
|
||||||
_file + _(" is not a valid picture"), show=True)
|
|
||||||
|
|
||||||
def remove_cover(self, *args):
|
|
||||||
self.current_val = None
|
|
||||||
|
|
||||||
def trim_cover(self, *args):
|
|
||||||
from calibre.utils.magick import Image
|
|
||||||
cdata = self.current_val
|
|
||||||
if not cdata:
|
|
||||||
return
|
|
||||||
im = Image()
|
|
||||||
im.load(cdata)
|
|
||||||
im.trim(10)
|
|
||||||
cdata = im.export('png')
|
|
||||||
self.current_val = cdata
|
|
||||||
|
|
||||||
def download_cover(self, *args):
|
|
||||||
pass # TODO: Implement this
|
|
||||||
|
|
||||||
def generate_cover(self, *args):
|
|
||||||
from calibre.ebooks import calibre_cover
|
|
||||||
from calibre.ebooks.metadata import fmt_sidx
|
|
||||||
from calibre.gui2 import config
|
|
||||||
title = self.dialog.title.current_val
|
|
||||||
author = authors_to_string(self.dialog.authors.current_val)
|
|
||||||
if not title or not author:
|
|
||||||
return error_dialog(self, _('Specify title and author'),
|
|
||||||
_('You must specify a title and author before generating '
|
|
||||||
'a cover'), show=True)
|
|
||||||
series = self.dialog.series.current_val
|
|
||||||
series_string = None
|
|
||||||
if series:
|
|
||||||
series_string = _('Book %s of %s')%(
|
|
||||||
fmt_sidx(self.dialog.series_index.current_val,
|
|
||||||
use_roman=config['use_roman_numerals_for_series_number']), series)
|
|
||||||
self.current_val = calibre_cover(title, author,
|
|
||||||
series_string=series_string)
|
|
||||||
|
|
||||||
def set_pixmap_from_data(self, data):
|
|
||||||
if not data:
|
|
||||||
self.current_val = None
|
|
||||||
return
|
|
||||||
orig = self.current_val
|
|
||||||
self.current_val = data
|
|
||||||
if self.current_val is None:
|
|
||||||
error_dialog(self, _('Invalid cover'),
|
|
||||||
_('Could not change cover as the image is invalid.'),
|
|
||||||
show=True)
|
|
||||||
self.current_val = orig
|
|
||||||
|
|
||||||
def initialize(self, db, id_):
|
|
||||||
self._cdata = None
|
|
||||||
self.current_val = db.cover(id_, index_is_id=True)
|
|
||||||
self.original_val = self.current_val
|
|
||||||
|
|
||||||
@property
|
|
||||||
def changed(self):
|
|
||||||
return self.current_val != self.original_val
|
|
||||||
|
|
||||||
@dynamic_property
|
|
||||||
def current_val(self):
|
|
||||||
def fget(self):
|
|
||||||
return self._cdata
|
|
||||||
def fset(self, cdata):
|
|
||||||
self._cdata = None
|
|
||||||
pm = QPixmap()
|
|
||||||
if cdata:
|
|
||||||
pm.loadFromData(cdata)
|
|
||||||
if pm.isNull():
|
|
||||||
pm = QPixmap(I('default_cover.png'))
|
|
||||||
else:
|
|
||||||
self._cdata = cdata
|
|
||||||
self.setPixmap(pm)
|
|
||||||
tt = _('This book has no cover')
|
|
||||||
if self._cdata:
|
|
||||||
tt = _('Cover size: %dx%d pixels') % \
|
|
||||||
(pm.width(), pm.height())
|
|
||||||
self.setToolTip(tt)
|
|
||||||
|
|
||||||
return property(fget=fget, fset=fset)
|
|
||||||
|
|
||||||
def commit(self, db, id_):
|
|
||||||
if self.changed:
|
|
||||||
if self.current_val:
|
|
||||||
db.set_cover(id_, self.current_val, notify=False, commit=False)
|
|
||||||
else:
|
|
||||||
db.remove_cover(id_, notify=False, commit=False)
|
|
||||||
return True
|
|
||||||
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
class CommentsEdit(Editor): # {{{
|
|
||||||
|
|
||||||
@dynamic_property
|
|
||||||
def current_val(self):
|
|
||||||
def fget(self):
|
|
||||||
return self.html
|
|
||||||
def fset(self, val):
|
|
||||||
if not val or not val.strip():
|
|
||||||
val = ''
|
|
||||||
else:
|
|
||||||
val = comments_to_html(val)
|
|
||||||
self.html = val
|
|
||||||
return property(fget=fget, fset=fset)
|
|
||||||
|
|
||||||
def initialize(self, db, id_):
|
|
||||||
self.current_val = db.comments(id_, index_is_id=True)
|
|
||||||
self.original_val = self.current_val
|
|
||||||
|
|
||||||
def commit(self, db, id_):
|
|
||||||
db.set_comment(id_, self.current_val, notify=False, commit=False)
|
|
||||||
return True
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
class RatingEdit(QSpinBox): # {{{
|
|
||||||
LABEL = _('&Rating:')
|
|
||||||
TOOLTIP = _('Rating of this book. 0-5 stars')
|
|
||||||
|
|
||||||
def __init__(self, parent):
|
|
||||||
QSpinBox.__init__(self, parent)
|
|
||||||
self.setToolTip(self.TOOLTIP)
|
|
||||||
self.setWhatsThis(self.TOOLTIP)
|
|
||||||
self.setMaximum(5)
|
|
||||||
self.setSuffix(' ' + _('stars'))
|
|
||||||
|
|
||||||
@dynamic_property
|
|
||||||
def current_val(self):
|
|
||||||
def fget(self):
|
|
||||||
return self.value()
|
|
||||||
def fset(self, val):
|
|
||||||
if val is None:
|
|
||||||
val = 0
|
|
||||||
val = int(val)
|
|
||||||
if val < 0:
|
|
||||||
val = 0
|
|
||||||
if val > 5:
|
|
||||||
val = 5
|
|
||||||
self.setValue(val)
|
|
||||||
return property(fget=fget, fset=fset)
|
|
||||||
|
|
||||||
def initialize(self, db, id_):
|
|
||||||
val = db.rating(id_, index_is_id=True)
|
|
||||||
if val > 0:
|
|
||||||
val = int(val/2.)
|
|
||||||
else:
|
|
||||||
val = 0
|
|
||||||
self.current_val = val
|
|
||||||
self.original_val = self.current_val
|
|
||||||
|
|
||||||
def commit(self, db, id_):
|
|
||||||
db.set_rating(id_, 2*self.current_val, notify=False, commit=False)
|
|
||||||
return True
|
|
||||||
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
class TagsEdit(CompleteLineEdit): # {{{
|
|
||||||
LABEL = _('Ta&gs:')
|
|
||||||
TOOLTIP = '<p>'+_('Tags categorize the book. This is particularly '
|
|
||||||
'useful while searching. <br><br>They can be any words'
|
|
||||||
'or phrases, separated by commas.')
|
|
||||||
|
|
||||||
def __init__(self, parent):
|
|
||||||
CompleteLineEdit.__init__(self, parent)
|
|
||||||
self.setToolTip(self.TOOLTIP)
|
|
||||||
self.setWhatsThis(self.TOOLTIP)
|
|
||||||
|
|
||||||
@dynamic_property
|
|
||||||
def current_val(self):
|
|
||||||
def fget(self):
|
|
||||||
return [x.strip() for x in unicode(self.text()).split(',')]
|
|
||||||
def fset(self, val):
|
|
||||||
if not val:
|
|
||||||
val = []
|
|
||||||
self.setText(', '.join([x.strip() for x in val]))
|
|
||||||
return property(fget=fget, fset=fset)
|
|
||||||
|
|
||||||
def initialize(self, db, id_):
|
|
||||||
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.original_val = self.current_val
|
|
||||||
|
|
||||||
@property
|
|
||||||
def changed(self):
|
|
||||||
return self.current_val != self.original_val
|
|
||||||
|
|
||||||
def edit(self, db, id_):
|
|
||||||
if self.changed:
|
|
||||||
if question_dialog(self, _('Tags changed'),
|
|
||||||
_('You have changed the tags. In order to use the tags'
|
|
||||||
' editor, you must either discard or apply these '
|
|
||||||
'changes'), show_copy_button=False,
|
|
||||||
buttons=QMessageBox.Apply|QMessageBox.Discard,
|
|
||||||
yes_button=QMessageBox.Apply):
|
|
||||||
self.commit(db, id_)
|
|
||||||
db.commit()
|
|
||||||
self.original_val = self.current_val
|
|
||||||
else:
|
|
||||||
self.current_val = self.original_val
|
|
||||||
d = TagEditor(self, db, id_)
|
|
||||||
if d.exec_() == TagEditor.Accepted:
|
|
||||||
self.current_val = d.tags
|
|
||||||
self.update_items_cache(db.all_tags())
|
|
||||||
|
|
||||||
|
|
||||||
def commit(self, db, id_):
|
|
||||||
db.set_tags(id_, self.current_val, notify=False, commit=False)
|
|
||||||
return True
|
|
||||||
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
class ISBNEdit(QLineEdit): # {{{
|
|
||||||
LABEL = _('IS&BN:')
|
|
||||||
|
|
||||||
def __init__(self, parent):
|
|
||||||
QLineEdit.__init__(self, parent)
|
|
||||||
self.pat = re.compile(r'[^0-9a-zA-Z]')
|
|
||||||
self.textChanged.connect(self.validate)
|
|
||||||
|
|
||||||
@dynamic_property
|
|
||||||
def current_val(self):
|
|
||||||
def fget(self):
|
|
||||||
return self.pat.sub('', unicode(self.text()).strip())
|
|
||||||
def fset(self, val):
|
|
||||||
if not val:
|
|
||||||
val = ''
|
|
||||||
self.setText(val.strip())
|
|
||||||
return property(fget=fget, fset=fset)
|
|
||||||
|
|
||||||
def initialize(self, db, id_):
|
|
||||||
self.current_val = db.isbn(id_, index_is_id=True)
|
|
||||||
self.original_val = self.current_val
|
|
||||||
|
|
||||||
def commit(self, db, id_):
|
|
||||||
db.set_isbn(id_, self.current_val, notify=False, commit=False)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def validate(self, *args):
|
|
||||||
isbn = self.current_val
|
|
||||||
tt = _('This ISBN number is valid')
|
|
||||||
if not isbn:
|
|
||||||
col = 'rgba(0,255,0,0%)'
|
|
||||||
elif check_isbn(isbn) is not None:
|
|
||||||
col = 'rgba(0,255,0,20%)'
|
|
||||||
else:
|
|
||||||
col = 'rgba(255,0,0,20%)'
|
|
||||||
tt = _('This ISBN number is invalid')
|
|
||||||
self.setToolTip(tt)
|
|
||||||
self.setStyleSheet('QLineEdit { background-color: %s }'%col)
|
|
||||||
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
class PublisherEdit(EnComboBox): # {{{
|
|
||||||
LABEL = _('&Publisher:')
|
|
||||||
|
|
||||||
def __init__(self, parent):
|
|
||||||
EnComboBox.__init__(self, parent)
|
|
||||||
self.setSizeAdjustPolicy(
|
|
||||||
self.AdjustToMinimumContentsLengthWithIcon)
|
|
||||||
|
|
||||||
@dynamic_property
|
|
||||||
def current_val(self):
|
|
||||||
|
|
||||||
def fget(self):
|
|
||||||
return unicode(self.currentText()).strip()
|
|
||||||
|
|
||||||
def fset(self, val):
|
|
||||||
if not val:
|
|
||||||
val = ''
|
|
||||||
self.setEditText(val.strip())
|
|
||||||
self.setCursorPosition(0)
|
|
||||||
|
|
||||||
return property(fget=fget, fset=fset)
|
|
||||||
|
|
||||||
def initialize(self, db, id_):
|
|
||||||
all_publishers = db.all_publishers()
|
|
||||||
all_publishers.sort(key=lambda x : sort_key(x[1]))
|
|
||||||
publisher_id = db.publisher_id(id_, index_is_id=True)
|
|
||||||
idx, c = None, 0
|
|
||||||
for i in all_publishers:
|
|
||||||
id, name = i
|
|
||||||
if id == publisher_id:
|
|
||||||
idx = c
|
|
||||||
self.addItem(name)
|
|
||||||
c += 1
|
|
||||||
|
|
||||||
self.setEditText('')
|
|
||||||
if idx is not None:
|
|
||||||
self.setCurrentIndex(idx)
|
|
||||||
|
|
||||||
def commit(self, db, id_):
|
|
||||||
db.set_publisher(id_, self.current_val, notify=False, commit=False)
|
|
||||||
return True
|
|
||||||
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
class MetadataSingleDialog(ResizableDialog):
|
class MetadataSingleDialog(ResizableDialog):
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user