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:
Kovid Goyal 2014-09-21 15:22:04 +05:30
parent 9f529a74fa
commit 55288fcfdc
4 changed files with 170 additions and 42 deletions

View File

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

View File

@ -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 = []

View File

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

View File

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