diff --git a/src/calibre/gui2/comments_editor.py b/src/calibre/gui2/comments_editor.py
index 50de42a3be..dbef2c5707 100644
--- a/src/calibre/gui2/comments_editor.py
+++ b/src/calibre/gui2/comments_editor.py
@@ -13,7 +13,7 @@ from PyQt5.Qt import (QApplication, QFontInfo, QSize, QWidget, QPlainTextEdit,
QToolBar, QVBoxLayout, QAction, QIcon, Qt, QTabWidget, QUrl, QFormLayout,
QSyntaxHighlighter, QColor, QColorDialog, QMenu, QDialog, QLabel,
QHBoxLayout, QKeySequence, QLineEdit, QDialogButtonBox, QPushButton,
- QCheckBox)
+ pyqtSignal, QCheckBox)
from PyQt5.QtWebKitWidgets import QWebView, QWebPage
try:
from PyQt5 import sip
@@ -74,6 +74,8 @@ class BlockStyleAction(QAction): # {{{
class EditorWidget(QWebView, LineEditECM): # {{{
+ data_changed = pyqtSignal()
+
def __init__(self, parent=None):
QWebView.__init__(self, parent)
self.base_url = None
@@ -184,6 +186,7 @@ class EditorWidget(QWebView, LineEditECM): # {{{
self.setHtml('')
self.set_readonly(False)
+ self.page().contentsChanged.connect(self.data_changed)
def update_link_action(self):
wac = self.pageAction(QWebPage.ToggleBold).isEnabled()
@@ -660,6 +663,7 @@ class Highlighter(QSyntaxHighlighter):
class Editor(QWidget): # {{{
toolbar_prefs_name = None
+ data_changed = pyqtSignal()
def __init__(self, parent=None, one_line_toolbar=False, toolbar_prefs_name=None):
QWidget.__init__(self, parent)
@@ -671,6 +675,7 @@ class Editor(QWidget): # {{{
t = getattr(self, 'toolbar%d'%i)
t.setIconSize(QSize(18, 18))
self.editor = EditorWidget(self)
+ self.editor.data_changed.connect(self.data_changed)
self.set_base_url = self.editor.set_base_url
self.set_html = self.editor.set_html
self.tabs = QTabWidget(self)
diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py
index 5d226605e0..6be662bc81 100644
--- a/src/calibre/gui2/custom_column_widgets.py
+++ b/src/calibre/gui2/custom_column_widgets.py
@@ -25,12 +25,20 @@ from calibre.gui2.library.delegates import ClearingDoubleSpinBox, ClearingSpinBo
from calibre.gui2.widgets2 import RatingEditor
+def safe_disconnect(signal):
+ try:
+ signal.disconnect()
+ except Exception:
+ pass
+
+
class Base(object):
def __init__(self, db, col_id, parent=None):
self.db, self.col_id = db, col_id
self.col_metadata = db.custom_column_num_map[col_id]
self.initial_val = self.widgets = None
+ self.signals_to_disconnect = []
self.setup_ui(parent)
def initialize(self, book_id):
@@ -66,6 +74,12 @@ class Base(object):
def break_cycles(self):
self.db = self.widgets = self.initial_val = None
+ for signal in self.signals_to_disconnect:
+ safe_disconnect(signal)
+ self.signals_to_disconnect = []
+
+ def connect_data_changed(self, slot):
+ pass
class SimpleText(Base):
@@ -79,6 +93,10 @@ class SimpleText(Base):
def getter(self):
return self.widgets[1].text().strip()
+ def connect_data_changed(self, slot):
+ self.widgets[1].textChanged.connect(slot)
+ self.signals_to_disconnect.append(self.widgets[1].textChanged)
+
class LongText(Base):
@@ -98,6 +116,10 @@ class LongText(Base):
def getter(self):
return self._tb.toPlainText()
+ def connect_data_changed(self, slot):
+ self._tb.textChanged.connect(slot)
+ self.signals_to_disconnect.append(self._tb.textChanged)
+
class Bool(Base):
@@ -165,6 +187,10 @@ class Bool(Base):
def set_to_cleared(self):
self.combobox.setCurrentIndex(2)
+ def connect_data_changed(self, slot):
+ self.combobox.currentTextChanged.connect(slot)
+ self.signals_to_disconnect.append(self.combobox.currentTextChanged)
+
class Int(Base):
@@ -195,6 +221,10 @@ class Int(Base):
self.setter(0)
self.was_none = to_what == self.widgets[1].minimum()
+ def connect_data_changed(self, slot):
+ self.widgets[1].valueChanged.connect(slot)
+ self.signals_to_disconnect.append(self.widgets[1].valueChanged)
+
class Float(Int):
@@ -223,6 +253,10 @@ class Rating(Base):
def getter(self):
return self.widgets[1].rating_value or None
+ def connect_data_changed(self, slot):
+ self.widgets[1].currentTextChanged.connect(slot)
+ self.signals_to_disconnect.append(self.widgets[1].currentTextChanged)
+
class DateTimeEdit(QDateTimeEdit):
@@ -302,6 +336,10 @@ class DateTime(Base):
def normalize_ui_val(self, val):
return as_utc(val) if val is not None else None
+ def connect_data_changed(self, slot):
+ self.widgets[1].dateTimeChanged.connect(slot)
+ self.signals_to_disconnect.append(self.widgets[1].dateTimeChanged)
+
class Comments(Base):
@@ -345,6 +383,10 @@ class Comments(Base):
self._tb.tab = val
return property(fget=fget, fset=fset)
+ def connect_data_changed(self, slot):
+ self._tb.data_changed.connect(slot)
+ self.signals_to_disconnect.append(self._tb.data_changed)
+
class MultipleWidget(QWidget):
@@ -481,6 +523,14 @@ class Text(Base):
if d.exec_() == TagEditor.Accepted:
self.setter(d.tags)
+ def connect_data_changed(self, slot):
+ if self.col_metadata['is_multiple']:
+ s = self.widgets[1].tags_box.currentTextChanged
+ else:
+ s = self.widgets[1].currentTextChanged
+ s.connect(slot)
+ self.signals_to_disconnect.append(s)
+
class Series(Base):
@@ -554,6 +604,11 @@ class Series(Base):
val, s_index = self.current_val
mi.set('#' + self.col_metadata['label'], val, extra=s_index)
+ def connect_data_changed(self, slot):
+ for s in self.widgets[1].editTextChanged, self.widgets[3].valueChanged:
+ s.connect(slot)
+ self.signals_to_disconnect.append(s)
+
class Enumeration(Base):
@@ -598,6 +653,10 @@ class Enumeration(Base):
val = None
return val
+ def connect_data_changed(self, slot):
+ self.widgets[1].currentIndexChanged.connect(slot)
+ self.signals_to_disconnect.append(self.widgets[1].currentIndexChanged)
+
def comments_factory(db, key, parent):
fm = db.custom_column_num_map[key]
diff --git a/src/calibre/gui2/metadata/basic_widgets.py b/src/calibre/gui2/metadata/basic_widgets.py
index 930435c4fd..bdd5380633 100644
--- a/src/calibre/gui2/metadata/basic_widgets.py
+++ b/src/calibre/gui2/metadata/basic_widgets.py
@@ -195,12 +195,14 @@ class TitleEdit(EnLineEdit, ToMetadataMixin):
TITLE_ATTR = FIELD_NAME = 'title'
TOOLTIP = _('Change the title of this book')
LABEL = _('&Title:')
+ data_changed = pyqtSignal()
def __init__(self, parent):
self.dialog = parent
EnLineEdit.__init__(self, parent)
self.setToolTip(self.TOOLTIP)
self.setWhatsThis(self.TOOLTIP)
+ self.textChanged.connect(self.data_changed)
def get_default(self):
return _('Unknown')
@@ -331,6 +333,7 @@ class AuthorsEdit(EditWithComplete, ToMetadataMixin):
TOOLTIP = ''
LABEL = _('&Author(s):')
FIELD_NAME = 'authors'
+ data_changed = pyqtSignal()
def __init__(self, parent, manage_authors):
self.dialog = parent
@@ -343,6 +346,7 @@ class AuthorsEdit(EditWithComplete, ToMetadataMixin):
self.manage_authors_signal = manage_authors
manage_authors.triggered.connect(self.manage_authors)
self.lineEdit().createStandardContextMenu = self.createStandardContextMenu
+ self.lineEdit().textChanged.connect(self.data_changed)
def createStandardContextMenu(self):
menu = QLineEdit.createStandardContextMenu(self.lineEdit())
@@ -444,6 +448,7 @@ class AuthorSortEdit(EnLineEdit, ToMetadataMixin):
'red, then the authors and this text do not match.')
LABEL = _('Author s&ort:')
FIELD_NAME = 'author_sort'
+ data_changed = pyqtSignal()
def __init__(self, parent, authors_edit, autogen_button, db,
copy_a_to_as_action, copy_as_to_a_action, a_to_as, as_to_a):
@@ -463,6 +468,7 @@ class AuthorSortEdit(EnLineEdit, ToMetadataMixin):
self.authors_edit.editTextChanged.connect(self.update_state_and_val, type=Qt.QueuedConnection)
self.textChanged.connect(self.update_state)
+ self.textChanged.connect(self.data_changed)
self.autogen_button = autogen_button
self.copy_a_to_as_action = copy_a_to_as_action
@@ -591,6 +597,7 @@ class SeriesEdit(EditWithComplete, ToMetadataMixin):
TOOLTIP = _('List of known series. You can add new series.')
LABEL = _('&Series:')
FIELD_NAME = 'series'
+ data_changed = pyqtSignal()
def __init__(self, parent):
EditWithComplete.__init__(self, parent)
@@ -602,6 +609,7 @@ class SeriesEdit(EditWithComplete, ToMetadataMixin):
self.setWhatsThis(self.TOOLTIP)
self.setEditable(True)
self.books_to_refresh = set([])
+ self.lineEdit().textChanged.connect(self.data_changed)
@dynamic_property
def current_val(self):
@@ -645,9 +653,11 @@ class SeriesIndexEdit(make_undoable(QDoubleSpinBox), ToMetadataMixin):
TOOLTIP = ''
LABEL = _('&Number:')
FIELD_NAME = 'series_index'
+ data_changed = pyqtSignal()
def __init__(self, parent, series_edit):
super(SeriesIndexEdit, self).__init__(parent)
+ self.valueChanged.connect(self.data_changed)
self.dialog = parent
self.db = self.original_series_name = None
self.setMaximum(10000000)
@@ -795,11 +805,23 @@ class FormatList(_FormatList):
class FormatsManager(QWidget):
+ data_changed = pyqtSignal()
+
+ @property
+ def changed(self):
+ return self._changed
+
+ @changed.setter
+ def changed(self, val):
+ self._changed = val
+ if val:
+ self.data_changed.emit()
+
def __init__(self, parent, copy_fmt):
QWidget.__init__(self, parent)
self.dialog = parent
self.copy_fmt = copy_fmt
- self.changed = False
+ self._changed = False
self.l = l = QGridLayout()
self.setLayout(l)
@@ -1031,6 +1053,7 @@ class FormatsManager(QWidget):
class Cover(ImageView): # {{{
download_cover = pyqtSignal()
+ data_changed = pyqtSignal()
def __init__(self, parent):
ImageView.__init__(self, parent, show_size_pref_name='edit_metadata_cover_widget', default_show_size=True)
@@ -1202,6 +1225,7 @@ class Cover(ImageView): # {{{
tt = _('Cover size: %(width)d x %(height)d pixels') % \
dict(width=pm.width(), height=pm.height())
self.setToolTip(tt)
+ self.data_changed.emit()
return property(fget=fget, fset=fset)
@@ -1245,6 +1269,7 @@ class CommentsEdit(Editor, ToMetadataMixin): # {{{
val = comments_to_html(val)
self.set_html(val, self.allow_undo)
self.wyswyg_dirtied()
+ self.data_changed.emit()
return property(fget=fget, fset=fset)
def initialize(self, db, id_):
@@ -1265,11 +1290,13 @@ class RatingEdit(RatingEditor, ToMetadataMixin): # {{{
LABEL = _('&Rating:')
TOOLTIP = _('Rating of this book. 0-5 stars')
FIELD_NAME = 'rating'
+ data_changed = pyqtSignal()
def __init__(self, parent):
super(RatingEdit, self).__init__(parent)
self.setToolTip(self.TOOLTIP)
self.setWhatsThis(self.TOOLTIP)
+ self.currentTextChanged.connect(self.data_changed)
@dynamic_property
def current_val(self):
@@ -1301,9 +1328,11 @@ class TagsEdit(EditWithComplete, ToMetadataMixin): # {{{
'useful while searching.
They can be any words '
'or phrases, separated by commas.')
FIELD_NAME = 'tags'
+ data_changed = pyqtSignal()
def __init__(self, parent):
EditWithComplete.__init__(self, parent)
+ self.currentTextChanged.connect(self.data_changed)
self.lineEdit().setMaxLength(655360) # see https://bugs.launchpad.net/bugs/1630944
self.books_to_refresh = set([])
self.setToolTip(self.TOOLTIP)
@@ -1366,9 +1395,11 @@ class LanguagesEdit(LE, ToMetadataMixin): # {{{
LABEL = _('&Languages:')
TOOLTIP = _('A comma separated list of languages for this book')
FIELD_NAME = 'languages'
+ data_changed = pyqtSignal()
def __init__(self, *args, **kwargs):
LE.__init__(self, *args, **kwargs)
+ self.textChanged.connect(self.data_changed)
self.setToolTip(self.TOOLTIP)
@dynamic_property
@@ -1458,11 +1489,13 @@ class IdentifiersEdit(QLineEdit, ToMetadataMixin):
'For example: \n\n%s\n\nIf an identifier value contains a comma, you can use the | character to represent it.')%(
'isbn:1565927249, doi:10.1000/182, amazon:1565927249')
FIELD_NAME = 'identifiers'
+ data_changed = pyqtSignal()
def __init__(self, parent):
QLineEdit.__init__(self, parent)
self.pat = re.compile(r'[^0-9a-zA-Z]')
self.textChanged.connect(self.validate)
+ self.textChanged.connect(self.data_changed)
def contextMenuEvent(self, ev):
m = self.createStandardContextMenu()
@@ -1631,9 +1664,11 @@ class ISBNDialog(QDialog): # {{{
class PublisherEdit(EditWithComplete, ToMetadataMixin): # {{{
LABEL = _('&Publisher:')
FIELD_NAME = 'publisher'
+ data_changed = pyqtSignal()
def __init__(self, parent):
EditWithComplete.__init__(self, parent)
+ self.currentTextChanged.connect(self.data_changed)
self.set_separator(None)
self.setSizeAdjustPolicy(
self.AdjustToMinimumContentsLengthWithIcon)
@@ -1694,11 +1729,13 @@ class DateEdit(make_undoable(QDateTimeEdit), ToMetadataMixin):
FMT = 'dd MMM yyyy hh:mm:ss'
ATTR = FIELD_NAME = 'timestamp'
TWEAK = 'gui_timestamp_display_format'
+ data_changed = pyqtSignal()
def __init__(self, parent, create_clear_button=True):
super(DateEdit, self).__init__(parent)
self.setToolTip(self.TOOLTIP)
self.setWhatsThis(self.TOOLTIP)
+ self.dateTimeChanged.connect(self.data_changed)
fmt = tweaks[self.TWEAK]
if fmt is None:
fmt = self.FMT
diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py
index 037a032a4c..6020a671e5 100644
--- a/src/calibre/gui2/metadata/single.py
+++ b/src/calibre/gui2/metadata/single.py
@@ -17,6 +17,7 @@ from PyQt5.Qt import (Qt, QVBoxLayout, QHBoxLayout, QWidget, QPushButton,
QSizePolicy, QFrame, QSize, QKeySequence, QMenu, QShortcut, QDialog)
from calibre.constants import isosx
+from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.ebooks.metadata import authors_to_string, string_to_authors
from calibre.gui2 import error_dialog, gprefs, pixmap_to_data
from calibre.gui2.metadata.basic_widgets import (TitleEdit, AuthorsEdit,
@@ -56,6 +57,7 @@ class MetadataSingleDialogBase(QDialog):
def __init__(self, db, parent=None, editing_multiple=False):
self.db = db
+ self.was_data_edited = False
self.changed = set()
self.books_to_refresh = set()
self.rows_to_refresh = set()
@@ -292,6 +294,8 @@ class MetadataSingleDialogBase(QDialog):
self.config_metadata_button.clicked.connect(self.configure_metadata)
self.config_metadata_button.setToolTip(
_('Change how calibre downloads metadata'))
+ for w in self.basic_metadata_widgets:
+ w.data_changed.connect(self.data_changed)
# }}}
@@ -320,6 +324,7 @@ class MetadataSingleDialogBase(QDialog):
two_column=self.cc_two_column)
self.__custom_col_layouts = [layout]
for widget in self.custom_metadata_widgets:
+ widget.connect_data_changed(self.data_changed)
if isinstance(widget, Comments):
self.comments_edit_state_at_apply[widget] = None
# }}}
@@ -353,6 +358,9 @@ class MetadataSingleDialogBase(QDialog):
def do_layout(self):
raise NotImplementedError()
+ def data_changed(self):
+ self.was_data_edited = True
+
def __call__(self, id_):
self.book_id = id_
self.books_to_refresh = set([])
@@ -363,6 +371,7 @@ class MetadataSingleDialogBase(QDialog):
widget.initialize(id_)
if callable(self.set_current_callback):
self.set_current_callback(id_)
+ self.was_data_edited = False
# Commented out as it doesn't play nice with Next, Prev buttons
# self.fetch_metadata_button.setFocus(Qt.OtherFocusReason)
@@ -620,6 +629,10 @@ class MetadataSingleDialogBase(QDialog):
def reject(self):
self.save_state()
+ if self.was_data_edited and not confirm(
+ title=_('Are you sure?'), name='confirm-cancel-edit-single-metadata', msg=_(
+ 'You will lose all unsaved changes, are you sure?'), parent=self):
+ return
QDialog.reject(self)
def save_state(self):