From 83d0b29938c5646f0fe1d1a913bb012d3b0d4cd8 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 30 Jan 2014 22:17:18 +0530 Subject: [PATCH] Edit book: Add a tool to view the list of checkpoints and compare the current state of the book to the state at the specified checkpoint. --- src/calibre/gui2/tweak_book/boss.py | 9 ++- src/calibre/gui2/tweak_book/ui.py | 8 ++ src/calibre/gui2/tweak_book/undo.py | 119 +++++++++++++++++++++++++++- 3 files changed, 132 insertions(+), 4 deletions(-) diff --git a/src/calibre/gui2/tweak_book/boss.py b/src/calibre/gui2/tweak_book/boss.py index 9ebf961962..158aaa67ed 100644 --- a/src/calibre/gui2/tweak_book/boss.py +++ b/src/calibre/gui2/tweak_book/boss.py @@ -109,6 +109,8 @@ class Boss(QObject): self.gui.check_book.fix_requested.connect(self.fix_requested) self.gui.toc_view.navigate_requested.connect(self.link_clicked) self.gui.image_browser.image_activated.connect(self.image_activated) + self.gui.checkpoints.revert_requested.connect(self.revert_requested) + self.gui.checkpoints.compare_requested.connect(self.compare_requested) def preferences(self): p = Preferences(self.gui) @@ -411,11 +413,11 @@ class Boss(QObject): d.line_activated.connect(line_activated) return d - def show_current_diff(self, allow_revert=True): + def show_current_diff(self, allow_revert=True, to_container=None): self.commit_all_editors_to_container() d = self.create_diff_dialog() d.revert_requested.connect(partial(self.revert_requested, self.global_undo.previous_container)) - d.container_diff(self.global_undo.previous_container, self.global_undo.current_container) + d.container_diff(to_container or self.global_undo.previous_container, self.global_undo.current_container) def compare_book(self): self.commit_all_editors_to_container() @@ -434,6 +436,9 @@ class Boss(QObject): set_current_container(nc) self.apply_container_update_to_gui() + def compare_requested(self, container): + self.show_current_diff(to_container=container) + # Renaming {{{ def rationalize_folders(self): diff --git a/src/calibre/gui2/tweak_book/ui.py b/src/calibre/gui2/tweak_book/ui.py index 191f470543..d0308de87d 100644 --- a/src/calibre/gui2/tweak_book/ui.py +++ b/src/calibre/gui2/tweak_book/ui.py @@ -25,6 +25,7 @@ from calibre.gui2.tweak_book import current_container, tprefs, actions, capitali from calibre.gui2.tweak_book.file_list import FileListWidget from calibre.gui2.tweak_book.job import BlockingJob from calibre.gui2.tweak_book.boss import Boss +from calibre.gui2.tweak_book.undo import CheckpointView from calibre.gui2.tweak_book.preview import Preview from calibre.gui2.tweak_book.search import SearchPanel from calibre.gui2.tweak_book.check import Check @@ -581,6 +582,13 @@ class Main(MainWindow): d.close() # Hidden by default d.visibilityChanged.connect(self.toc_view.visibility_changed) + d = create(_('Checkpoints'), 'checkpoints') + d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea | Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea) + self.checkpoints = CheckpointView(self.boss.global_undo, parent=d) + d.setWidget(self.checkpoints) + self.addDockWidget(Qt.LeftDockWidgetArea, d) + d.close() # Hidden by default + def resizeEvent(self, ev): self.blocking_job.resize(ev.size()) return super(Main, self).resizeEvent(ev) diff --git a/src/calibre/gui2/tweak_book/undo.py b/src/calibre/gui2/tweak_book/undo.py index 39d59ff691..c7269f0cf3 100644 --- a/src/calibre/gui2/tweak_book/undo.py +++ b/src/calibre/gui2/tweak_book/undo.py @@ -8,6 +8,14 @@ __copyright__ = '2013, Kovid Goyal ' import shutil +from PyQt4.Qt import ( + QAbstractListModel, Qt, QModelIndex, QVariant, QApplication, QWidget, + QGridLayout, QListView, QStyledItemDelegate, pyqtSignal, QPushButton, QIcon) + +from calibre.gui2 import NONE, error_dialog + +ROOT = QModelIndex() + MAX_SAVEPOINTS = 100 def cleanup(containers): @@ -23,12 +31,35 @@ class State(object): self.container = container self.message = None -class GlobalUndoHistory(object): +class GlobalUndoHistory(QAbstractListModel): - def __init__(self): + def __init__(self, parent=None): + QAbstractListModel.__init__(self, parent) self.states = [] self.pos = 0 + def rowCount(self, parent=ROOT): + return len(self.states) + + def data(self, index, role=Qt.DisplayRole): + if role == Qt.DisplayRole: + row = index.row() + msg = self.states[row].message + if self.pos == row: + msg = _('Current state') + elif not msg: + msg = _('[Unnamed state]') + else: + msg = _('Before %s') % msg + return QVariant(msg) + if role == Qt.FontRole and index.row() == self.pos: + f = QApplication.instance().font() + f.setBold(True) + return QVariant(f) + if role == Qt.UserRole: + return QVariant(self.states[index.row()]) + return NONE + @property def current_container(self): return self.states[self.pos].container @@ -40,6 +71,7 @@ class GlobalUndoHistory(object): def open_book(self, container): self.states = [State(container)] self.pos = 0 + self.reset() def add_savepoint(self, new_container, message): try: @@ -48,14 +80,23 @@ class GlobalUndoHistory(object): raise IndexError('The checkpoint stack has an incorrect position pointer. This should never happen: self.pos = %r, len(self.states) = %r' % ( self.pos, len(self.states))) extra = self.states[self.pos+1:] + if extra: + self.beginRemoveRows(ROOT, self.pos+1, len(self.states) - 1) cleanup(extra) self.states = self.states[:self.pos+1] + if extra: + self.endRemoveRows() + self.beginInsertRows(ROOT, self.pos+1, self.pos+1) self.states.append(State(new_container)) self.pos += 1 + self.endInsertRows() + self.dataChanged.emit(self.index(self.pos-1), self.index(self.pos)) if len(self.states) > MAX_SAVEPOINTS: num = len(self.states) - MAX_SAVEPOINTS + self.beginRemoveRows(ROOT, 0, num - 1) cleanup(self.states[:num]) self.states = self.states[num:] + self.endRemoveRows() def rewind_savepoint(self): ''' Revert back to the last save point, should only be used immediately @@ -64,8 +105,11 @@ class GlobalUndoHistory(object): where you create savepoint, perform some operation, operation fails, so revert to state before creating savepoint. ''' if self.pos > 0 and self.pos == len(self.states) - 1: + self.beginRemoveRows(ROOT, self.pos, self.pos) self.pos -= 1 cleanup([self.states.pop().container]) + self.endRemoveRows() + self.dataChanged.emit(self.index(self.pos)) ans = self.current_container ans.message = None return ans @@ -73,17 +117,22 @@ class GlobalUndoHistory(object): def undo(self): if self.pos > 0: self.pos -= 1 + self.dataChanged.emit(self.index(self.pos), self.index(self.pos+1)) return self.current_container def redo(self): if self.pos < len(self.states) - 1: self.pos += 1 + self.dataChanged.emit(self.index(self.pos-1), self.index(self.pos)) return self.current_container def revert_to(self, container): for i, state in enumerate(self.states): if state.container is container: + opos = self.pos self.pos = i + for x in (i, opos): + self.dataChanged.emit(self.index(x), self.index(x)) return container @property @@ -106,3 +155,69 @@ class GlobalUndoHistory(object): return '' return self.states[self.pos].message or '' +class SpacedDelegate(QStyledItemDelegate): + + def sizeHint(self, *args): + ans = QStyledItemDelegate.sizeHint(self, *args) + ans.setHeight(ans.height() + 4) + return ans + +class CheckpointView(QWidget): + + revert_requested = pyqtSignal(object) + compare_requested = pyqtSignal(object) + + def __init__(self, model, parent=None): + QWidget.__init__(self, parent) + self.l = l = QGridLayout(self) + self.setLayout(l) + self.setContentsMargins(0, 0, 0, 0) + + self.view = v = QListView(self) + self.d = SpacedDelegate(v) + v.doubleClicked.connect(self.double_clicked) + v.setItemDelegate(self.d) + v.setModel(model) + l.addWidget(v, 0, 0, 1, -1) + model.dataChanged.connect(self.data_changed) + + self.rb = b = QPushButton(QIcon(I('edit-undo.png')), _('&Revert to'), self) + b.setToolTip(_('Revert the book to the selected checkpoint')) + b.clicked.connect(self.revert_clicked) + l.addWidget(b, 1, 1) + + self.cb = b = QPushButton(QIcon(I('diff.png')), _('&Compare'), self) + b.setToolTip(_('Compare the state of the book at the selected checkpoint with the current state')) + b.clicked.connect(self.compare_clicked) + l.addWidget(b, 1, 0) + + def data_changed(self, *args): + self.view.clearSelection() + m = self.view.model() + sm = self.view.selectionModel() + sm.select(m.index(m.pos), sm.ClearAndSelect) + self.view.setCurrentIndex(m.index(m.pos)) + + def double_clicked(self, index): + pass # Too much danger of accidental double click + + def revert_clicked(self): + m = self.view.model() + row = self.view.currentIndex().row() + if row < 0: + return + if row == m.pos: + return error_dialog(self, _('Cannot revert'), _( + 'Cannot revert to the current state'), show=True) + self.revert_requested.emit(m.states[row].container) + + def compare_clicked(self): + m = self.view.model() + row = self.view.currentIndex().row() + if row < 0: + return + if row == m.pos: + return error_dialog(self, _('Cannot compare'), _( + 'There is no point comparing the current state to itself'), show=True) + self.compare_requested.emit(m.states[row].container) +