mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Edit metadata dialog: Allow undoing the changes to individual fields by right clicking on the field and selecting Undo after a metadata download. Fixes #1223367 [Feature Request: Don't "destroy" undo/ctrl-z and redo/ctrl-y for the fields, after metadata download](https://bugs.launchpad.net/calibre/+bug/1223367)
This commit is contained in:
parent
9f529a74fa
commit
55288fcfdc
@ -360,6 +360,15 @@ class EditorWidget(QWebView): # {{{
|
||||
self.set_font_style()
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
def set_html(self, val, allow_undo=True):
|
||||
if not allow_undo or self.readonly:
|
||||
self.html = val
|
||||
return
|
||||
mf = self.page().mainFrame()
|
||||
mf.evaluateJavaScript('document.execCommand("selectAll", false, null)')
|
||||
mf.evaluateJavaScript('document.execCommand("insertHTML", false, %s)' % json.dumps(unicode(val)))
|
||||
self.set_font_style()
|
||||
|
||||
def set_font_style(self):
|
||||
fi = QFontInfo(QApplication.font(self))
|
||||
f = fi.pixelSize() + 1 + int(tweaks['change_book_details_font_size_by'])
|
||||
@ -622,6 +631,7 @@ class Editor(QWidget): # {{{
|
||||
t = getattr(self, 'toolbar%d'%i)
|
||||
t.setIconSize(QSize(18, 18))
|
||||
self.editor = EditorWidget(self)
|
||||
self.set_html = self.editor.set_html
|
||||
self.tabs = QTabWidget(self)
|
||||
self.tabs.setTabPosition(self.tabs.South)
|
||||
self.wyswyg = QWidget(self.tabs)
|
||||
|
@ -58,15 +58,27 @@ class LanguagesEdit(EditWithComplete):
|
||||
return ans
|
||||
|
||||
def fset(self, lang_codes):
|
||||
ans = []
|
||||
for lc in lang_codes:
|
||||
name = self._lang_map.get(lc, None)
|
||||
if name is not None:
|
||||
ans.append(name)
|
||||
self.setEditText(', '.join(ans))
|
||||
self.set_lang_codes(lang_codes, allow_undo=False)
|
||||
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
def set_lang_codes(self, lang_codes, allow_undo=True):
|
||||
ans = []
|
||||
for lc in lang_codes:
|
||||
name = self._lang_map.get(lc, None)
|
||||
if name is not None:
|
||||
ans.append(name)
|
||||
ans = ', '.join(ans)
|
||||
|
||||
if allow_undo:
|
||||
orig, self.disable_popup = self.disable_popup, True
|
||||
try:
|
||||
self.lineEdit().selectAll(), self.lineEdit().insert(ans)
|
||||
finally:
|
||||
self.disable_popup = orig
|
||||
else:
|
||||
self.setEditText(ans)
|
||||
|
||||
def validate(self):
|
||||
vals = self.vals
|
||||
bad = []
|
||||
|
@ -7,13 +7,14 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import textwrap, re, os, shutil
|
||||
import textwrap, re, os, shutil, weakref
|
||||
|
||||
from PyQt5.Qt import (
|
||||
Qt, QDateTimeEdit, pyqtSignal, QMessageBox, QIcon, QToolButton, QWidget,
|
||||
QLabel, QGridLayout, QApplication, QDoubleSpinBox, QListWidgetItem, QSize,
|
||||
QPixmap, QDialog, QMenu, QSpinBox, QLineEdit, QSizePolicy,
|
||||
QDialogButtonBox, QAction, QCalendarWidget, QDate, QDateTime)
|
||||
QPixmap, QDialog, QMenu, QSpinBox, QLineEdit, QSizePolicy, QKeySequence,
|
||||
QDialogButtonBox, QAction, QCalendarWidget, QDate, QDateTime, QUndoCommand,
|
||||
QUndoStack)
|
||||
|
||||
from calibre.gui2.widgets import EnLineEdit, FormatList as _FormatList, ImageView
|
||||
from calibre.utils.icu import sort_key
|
||||
@ -76,10 +77,113 @@ class BasicMetadataWidget(object):
|
||||
class ToMetadataMixin(object):
|
||||
|
||||
FIELD_NAME = None
|
||||
allow_undo = False
|
||||
|
||||
def apply_to_metadata(self, mi):
|
||||
mi.set(self.FIELD_NAME, self.current_val)
|
||||
|
||||
def set_value(self, val, allow_undo=True):
|
||||
self.allow_undo = allow_undo
|
||||
try:
|
||||
self.current_val = val
|
||||
finally:
|
||||
self.allow_undo = False
|
||||
|
||||
def set_text(self, text):
|
||||
if self.allow_undo:
|
||||
self.selectAll(), self.insert(text)
|
||||
else:
|
||||
self.setText(text)
|
||||
|
||||
def set_edit_text(self, text):
|
||||
if self.allow_undo:
|
||||
orig, self.disable_popup = self.disable_popup, True
|
||||
try:
|
||||
self.lineEdit().selectAll(), self.lineEdit().insert(text)
|
||||
finally:
|
||||
self.disable_popup = orig
|
||||
else:
|
||||
self.setEditText(text)
|
||||
|
||||
def make_undoable(spinbox):
|
||||
'Add a proper undo/redo capability to spinbox which must be a sub-class of QAbstractSpinBox'
|
||||
|
||||
def access_key(k):
|
||||
if QKeySequence.keyBindings(k):
|
||||
return '\t' + QKeySequence(k).toString(QKeySequence.NativeText)
|
||||
return ''
|
||||
|
||||
class UndoCommand(QUndoCommand):
|
||||
|
||||
def __init__(self, widget, val):
|
||||
QUndoCommand.__init__(self)
|
||||
self.widget = weakref.ref(widget)
|
||||
if hasattr(widget, 'dateTime'):
|
||||
self.undo_val = widget.dateTime()
|
||||
elif hasattr(widget, 'value'):
|
||||
self.undo_val = widget.value()
|
||||
self.redo_val = val
|
||||
|
||||
def undo(self):
|
||||
w = self.widget()
|
||||
if hasattr(w, 'setDateTime'):
|
||||
w.setDateTime(self.undo_val)
|
||||
elif hasattr(w, 'setValue'):
|
||||
w.setValue(self.undo_val)
|
||||
|
||||
def redo(self):
|
||||
w = self.widget()
|
||||
if hasattr(w, 'setDateTime'):
|
||||
w.setDateTime(self.redo_val)
|
||||
elif hasattr(w, 'setValue'):
|
||||
w.setValue(self.redo_val)
|
||||
|
||||
class UndoableSpinbox(spinbox):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
spinbox.__init__(self, parent)
|
||||
self.undo_stack = QUndoStack(self)
|
||||
self.undo, self.redo = self.undo_stack.undo, self.undo_stack.redo
|
||||
|
||||
def keyPressEvent(self, ev):
|
||||
if ev == QKeySequence.Undo:
|
||||
self.undo()
|
||||
return ev.accept()
|
||||
if ev == QKeySequence.Redo:
|
||||
self.redo()
|
||||
return ev.accept()
|
||||
return spinbox.keyPressEvent(self, ev)
|
||||
|
||||
def contextMenuEvent(self, ev):
|
||||
m = QMenu(self)
|
||||
m.addAction(_('&Undo') + access_key(QKeySequence.Undo), self.undo).setEnabled(self.undo_stack.canUndo())
|
||||
m.addAction(_('&Redo') + access_key(QKeySequence.Redo), self.redo).setEnabled(self.undo_stack.canRedo())
|
||||
m.addSeparator()
|
||||
le = self.lineEdit()
|
||||
m.addAction(_('Cu&t') + access_key(QKeySequence.Cut), le.cut).setEnabled(not le.isReadOnly() and le.hasSelectedText())
|
||||
m.addAction(_('&Copy') + access_key(QKeySequence.Copy), le.copy).setEnabled(le.hasSelectedText())
|
||||
m.addAction(_('&Paste') + access_key(QKeySequence.Paste), le.paste).setEnabled(not le.isReadOnly())
|
||||
m.addAction(_('Delete') + access_key(QKeySequence.Delete), le.del_).setEnabled(not le.isReadOnly() and le.hasSelectedText())
|
||||
m.addSeparator()
|
||||
m.addAction(_('Select &All') + access_key(QKeySequence.SelectAll), self.selectAll)
|
||||
m.addSeparator()
|
||||
m.addAction(_('&Step up'), self.stepUp)
|
||||
m.addAction(_('Step &down'), self.stepDown)
|
||||
m.setAttribute(Qt.WA_DeleteOnClose)
|
||||
m.popup(ev.globalPos())
|
||||
|
||||
def set_spinbox_value(self, val):
|
||||
if self.allow_undo:
|
||||
cmd = UndoCommand(self, val)
|
||||
self.undo_stack.push(cmd)
|
||||
if hasattr(self, 'setDateTime'):
|
||||
self.setDateTime(val)
|
||||
elif hasattr(self, 'setValue'):
|
||||
self.setValue(val)
|
||||
|
||||
return UndoableSpinbox
|
||||
|
||||
|
||||
# Title {{{
|
||||
class TitleEdit(EnLineEdit, ToMetadataMixin):
|
||||
|
||||
@ -126,7 +230,7 @@ class TitleEdit(EnLineEdit, ToMetadataMixin):
|
||||
val = val.strip()
|
||||
if not val:
|
||||
val = self.get_default()
|
||||
self.setText(val)
|
||||
self.set_text(val)
|
||||
self.setCursorPosition(0)
|
||||
|
||||
return property(fget=fget, fset=fset)
|
||||
@ -311,7 +415,7 @@ class AuthorsEdit(EditWithComplete, ToMetadataMixin):
|
||||
def fset(self, val):
|
||||
if not val:
|
||||
val = [self.get_default()]
|
||||
self.setEditText(' & '.join([x.strip() for x in val]))
|
||||
self.set_edit_text(' & '.join([x.strip() for x in val]))
|
||||
self.lineEdit().setCursorPosition(0)
|
||||
|
||||
return property(fget=fget, fset=fset)
|
||||
@ -373,7 +477,7 @@ class AuthorSortEdit(EnLineEdit, ToMetadataMixin):
|
||||
def fset(self, val):
|
||||
if not val:
|
||||
val = ''
|
||||
self.setText(val.strip())
|
||||
self.set_text(val.strip())
|
||||
self.setCursorPosition(0)
|
||||
|
||||
return property(fget=fget, fset=fset)
|
||||
@ -493,7 +597,7 @@ class SeriesEdit(EditWithComplete, ToMetadataMixin):
|
||||
def fset(self, val):
|
||||
if not val:
|
||||
val = ''
|
||||
self.setEditText(val.strip())
|
||||
self.set_edit_text(val.strip())
|
||||
self.lineEdit().setCursorPosition(0)
|
||||
|
||||
return property(fget=fget, fset=fset)
|
||||
@ -521,14 +625,14 @@ class SeriesEdit(EditWithComplete, ToMetadataMixin):
|
||||
self.dialog = None
|
||||
|
||||
|
||||
class SeriesIndexEdit(QDoubleSpinBox, ToMetadataMixin):
|
||||
class SeriesIndexEdit(make_undoable(QDoubleSpinBox), ToMetadataMixin):
|
||||
|
||||
TOOLTIP = ''
|
||||
LABEL = _('&Number:')
|
||||
FIELD_NAME = 'series_index'
|
||||
|
||||
def __init__(self, parent, series_edit):
|
||||
QDoubleSpinBox.__init__(self, parent)
|
||||
super(SeriesIndexEdit, self).__init__(parent)
|
||||
self.dialog = parent
|
||||
self.db = self.original_series_name = None
|
||||
self.setMaximum(10000000)
|
||||
@ -551,7 +655,7 @@ class SeriesIndexEdit(QDoubleSpinBox, ToMetadataMixin):
|
||||
if val is None:
|
||||
val = 1.0
|
||||
val = float(val)
|
||||
self.setValue(val)
|
||||
self.set_spinbox_value(val)
|
||||
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
@ -1103,7 +1207,7 @@ class CommentsEdit(Editor, ToMetadataMixin): # {{{
|
||||
val = ''
|
||||
else:
|
||||
val = comments_to_html(val)
|
||||
self.html = val
|
||||
self.set_html(val, self.allow_undo)
|
||||
self.wyswyg_dirtied()
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
@ -1117,13 +1221,13 @@ class CommentsEdit(Editor, ToMetadataMixin): # {{{
|
||||
db.set_comment(id_, self.current_val, notify=False, commit=False)
|
||||
# }}}
|
||||
|
||||
class RatingEdit(QSpinBox, ToMetadataMixin): # {{{
|
||||
class RatingEdit(make_undoable(QSpinBox), ToMetadataMixin): # {{{
|
||||
LABEL = _('&Rating:')
|
||||
TOOLTIP = _('Rating of this book. 0-5 stars')
|
||||
FIELD_NAME = 'rating'
|
||||
|
||||
def __init__(self, parent):
|
||||
QSpinBox.__init__(self, parent)
|
||||
super(RatingEdit, self).__init__(parent)
|
||||
self.setToolTip(self.TOOLTIP)
|
||||
self.setWhatsThis(self.TOOLTIP)
|
||||
self.setMaximum(5)
|
||||
@ -1141,7 +1245,7 @@ class RatingEdit(QSpinBox, ToMetadataMixin): # {{{
|
||||
val = 0
|
||||
if val > 5:
|
||||
val = 5
|
||||
self.setValue(val)
|
||||
self.set_spinbox_value(val)
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
def initialize(self, db, id_):
|
||||
@ -1182,7 +1286,7 @@ class TagsEdit(EditWithComplete, ToMetadataMixin): # {{{
|
||||
def fset(self, val):
|
||||
if not val:
|
||||
val = []
|
||||
self.setText(', '.join([x.strip() for x in val]))
|
||||
self.set_edit_text(', '.join([x.strip() for x in val]))
|
||||
self.setCursorPosition(0)
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
@ -1240,7 +1344,7 @@ class LanguagesEdit(LE, ToMetadataMixin): # {{{
|
||||
def fget(self):
|
||||
return self.lang_codes
|
||||
def fset(self, val):
|
||||
self.lang_codes = val
|
||||
self.set_lang_codes(val, self.allow_undo)
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
def initialize(self, db, id_):
|
||||
@ -1309,9 +1413,8 @@ class IdentifiersEdit(QLineEdit, ToMetadataMixin): # {{{
|
||||
val[k] = v
|
||||
ids = sorted(val.iteritems(), key=keygen)
|
||||
txt = ', '.join(['%s:%s'%(k.lower(), vl) for k, vl in ids])
|
||||
# Use clear + insert instead of setText so that undo works
|
||||
self.clear()
|
||||
self.insert(txt.strip())
|
||||
# Use selectAll + insert instead of setText so that undo works
|
||||
self.selectAll(), self.insert(txt.strip())
|
||||
self.setCursorPosition(0)
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
@ -1426,7 +1529,7 @@ class PublisherEdit(EditWithComplete, ToMetadataMixin): # {{{
|
||||
def fset(self, val):
|
||||
if not val:
|
||||
val = ''
|
||||
self.setEditText(val.strip())
|
||||
self.set_edit_text(val.strip())
|
||||
self.lineEdit().setCursorPosition(0)
|
||||
|
||||
return property(fget=fget, fset=fset)
|
||||
@ -1459,7 +1562,7 @@ class CalendarWidget(QCalendarWidget):
|
||||
if self.selectedDate().year() == UNDEFINED_DATE.year:
|
||||
self.setSelectedDate(QDate.currentDate())
|
||||
|
||||
class DateEdit(QDateTimeEdit, ToMetadataMixin):
|
||||
class DateEdit(make_undoable(QDateTimeEdit), ToMetadataMixin):
|
||||
|
||||
TOOLTIP = ''
|
||||
LABEL = _('&Date:')
|
||||
@ -1468,7 +1571,7 @@ class DateEdit(QDateTimeEdit, ToMetadataMixin):
|
||||
TWEAK = 'gui_timestamp_display_format'
|
||||
|
||||
def __init__(self, parent, create_clear_button=True):
|
||||
QDateTimeEdit.__init__(self, parent)
|
||||
super(DateEdit, self).__init__(parent)
|
||||
self.setToolTip(self.TOOLTIP)
|
||||
self.setWhatsThis(self.TOOLTIP)
|
||||
fmt = tweaks[self.TWEAK]
|
||||
@ -1499,7 +1602,7 @@ class DateEdit(QDateTimeEdit, ToMetadataMixin):
|
||||
val = UNDEFINED_DATE
|
||||
else:
|
||||
val = as_local_time(val)
|
||||
self.setDateTime(val)
|
||||
self.set_spinbox_value(val)
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
def initialize(self, db, id_):
|
||||
@ -1525,7 +1628,7 @@ class DateEdit(QDateTimeEdit, ToMetadataMixin):
|
||||
ev.accept()
|
||||
self.setDateTime(QDateTime.currentDateTime())
|
||||
else:
|
||||
return QDateTimeEdit.keyPressEvent(self, ev)
|
||||
return super(DateEdit, self).keyPressEvent(ev)
|
||||
|
||||
class PubdateEdit(DateEdit):
|
||||
LABEL = _('Publishe&d:')
|
||||
|
@ -394,23 +394,24 @@ class MetadataSingleDialogBase(ResizableDialog):
|
||||
return
|
||||
|
||||
def update_from_mi(self, mi, update_sorts=True, merge_tags=True, merge_comments=False):
|
||||
fw = self.focusWidget()
|
||||
if not mi.is_null('title'):
|
||||
self.title.current_val = mi.title
|
||||
self.title.set_value(mi.title)
|
||||
if update_sorts:
|
||||
self.title_sort.auto_generate()
|
||||
if not mi.is_null('authors'):
|
||||
self.authors.current_val = mi.authors
|
||||
self.authors.set_value(mi.authors)
|
||||
if not mi.is_null('author_sort'):
|
||||
self.author_sort.current_val = mi.author_sort
|
||||
self.author_sort.set_value(mi.author_sort)
|
||||
elif update_sorts:
|
||||
self.author_sort.auto_generate()
|
||||
if not mi.is_null('rating'):
|
||||
try:
|
||||
self.rating.current_val = mi.rating
|
||||
self.rating.set_value(mi.rating)
|
||||
except:
|
||||
pass
|
||||
if not mi.is_null('publisher'):
|
||||
self.publisher.current_val = mi.publisher
|
||||
self.publisher.set_value(mi.publisher)
|
||||
if not mi.is_null('tags'):
|
||||
old_tags = self.tags.current_val
|
||||
tags = mi.tags if mi.tags else []
|
||||
@ -418,30 +419,32 @@ class MetadataSingleDialogBase(ResizableDialog):
|
||||
ltags, lotags = {t.lower() for t in tags}, {t.lower() for t in
|
||||
old_tags}
|
||||
tags = [t for t in tags if t.lower() in ltags-lotags] + old_tags
|
||||
self.tags.current_val = tags
|
||||
self.tags.set_value(tags)
|
||||
if not mi.is_null('identifiers'):
|
||||
current = self.identifiers.current_val
|
||||
current.update(mi.identifiers)
|
||||
self.identifiers.current_val = current
|
||||
self.identifiers.set_value(current)
|
||||
if not mi.is_null('pubdate'):
|
||||
self.pubdate.current_val = mi.pubdate
|
||||
self.pubdate.set_value(mi.pubdate)
|
||||
if not mi.is_null('series') and mi.series.strip():
|
||||
self.series.current_val = mi.series
|
||||
self.series.set_value(mi.series)
|
||||
if mi.series_index is not None:
|
||||
self.series_index.reset_original()
|
||||
self.series_index.current_val = float(mi.series_index)
|
||||
self.series_index.set_value(float(mi.series_index))
|
||||
if not mi.is_null('languages'):
|
||||
langs = [canonicalize_lang(x) for x in mi.languages]
|
||||
langs = [x for x in langs if x is not None]
|
||||
if langs:
|
||||
self.languages.current_val = langs
|
||||
self.languages.set_value(langs)
|
||||
if mi.comments and mi.comments.strip():
|
||||
val = mi.comments
|
||||
if val and merge_comments:
|
||||
cval = self.comments.current_val
|
||||
if cval:
|
||||
val = merge_two_comments(cval, val)
|
||||
self.comments.current_val = val
|
||||
self.comments.set_value(val)
|
||||
if fw is not None:
|
||||
fw.setFocus(Qt.OtherFocusReason)
|
||||
|
||||
def fetch_metadata(self, *args):
|
||||
d = FullFetch(self.cover.pixmap(), self)
|
||||
|
Loading…
x
Reference in New Issue
Block a user