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.

This commit is contained in:
Kovid Goyal 2014-01-30 22:17:18 +05:30
parent 4763a8f9af
commit 83d0b29938
3 changed files with 132 additions and 4 deletions

View File

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

View File

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

View File

@ -8,6 +8,14 @@ __copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
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)