diff --git a/src/calibre/gui2/comments_editor.py b/src/calibre/gui2/comments_editor.py index ff7682d314..6badbe6c5f 100644 --- a/src/calibre/gui2/comments_editor.py +++ b/src/calibre/gui2/comments_editor.py @@ -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) diff --git a/src/calibre/gui2/languages.py b/src/calibre/gui2/languages.py index f234a9c56b..5b5ad6bb07 100644 --- a/src/calibre/gui2/languages.py +++ b/src/calibre/gui2/languages.py @@ -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 = [] diff --git a/src/calibre/gui2/metadata/basic_widgets.py b/src/calibre/gui2/metadata/basic_widgets.py index 9868142646..af2b772724 100644 --- a/src/calibre/gui2/metadata/basic_widgets.py +++ b/src/calibre/gui2/metadata/basic_widgets.py @@ -7,13 +7,14 @@ __license__ = 'GPL v3' __copyright__ = '2011, Kovid Goyal ' __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:') diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index e639d4ebf4..83fe645520 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -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)