mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Edit metadata dialog: Show a confirmation dialog on cancel if some changes have been made. Fixes #1786544 [[Enhancement] Cancelling out of edit metadata dialog box should prompt user whether to save changes](https://bugs.launchpad.net/calibre/+bug/1786544)
This commit is contained in:
parent
f8e4062d9b
commit
ed6184f628
@ -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)
|
||||
|
@ -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]
|
||||
|
@ -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. <br><br>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
|
||||
|
@ -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):
|
||||
|
Loading…
x
Reference in New Issue
Block a user