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):