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.check_book.fix_requested.connect(self.fix_requested)
self.gui.toc_view.navigate_requested.connect(self.link_clicked) self.gui.toc_view.navigate_requested.connect(self.link_clicked)
self.gui.image_browser.image_activated.connect(self.image_activated) 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): def preferences(self):
p = Preferences(self.gui) p = Preferences(self.gui)
@ -411,11 +413,11 @@ class Boss(QObject):
d.line_activated.connect(line_activated) d.line_activated.connect(line_activated)
return d 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() self.commit_all_editors_to_container()
d = self.create_diff_dialog() d = self.create_diff_dialog()
d.revert_requested.connect(partial(self.revert_requested, self.global_undo.previous_container)) 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): def compare_book(self):
self.commit_all_editors_to_container() self.commit_all_editors_to_container()
@ -434,6 +436,9 @@ class Boss(QObject):
set_current_container(nc) set_current_container(nc)
self.apply_container_update_to_gui() self.apply_container_update_to_gui()
def compare_requested(self, container):
self.show_current_diff(to_container=container)
# Renaming {{{ # Renaming {{{
def rationalize_folders(self): 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.file_list import FileListWidget
from calibre.gui2.tweak_book.job import BlockingJob from calibre.gui2.tweak_book.job import BlockingJob
from calibre.gui2.tweak_book.boss import Boss 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.preview import Preview
from calibre.gui2.tweak_book.search import SearchPanel from calibre.gui2.tweak_book.search import SearchPanel
from calibre.gui2.tweak_book.check import Check from calibre.gui2.tweak_book.check import Check
@ -581,6 +582,13 @@ class Main(MainWindow):
d.close() # Hidden by default d.close() # Hidden by default
d.visibilityChanged.connect(self.toc_view.visibility_changed) 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): def resizeEvent(self, ev):
self.blocking_job.resize(ev.size()) self.blocking_job.resize(ev.size())
return super(Main, self).resizeEvent(ev) return super(Main, self).resizeEvent(ev)

View File

@ -8,6 +8,14 @@ __copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
import shutil 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 MAX_SAVEPOINTS = 100
def cleanup(containers): def cleanup(containers):
@ -23,12 +31,35 @@ class State(object):
self.container = container self.container = container
self.message = None self.message = None
class GlobalUndoHistory(object): class GlobalUndoHistory(QAbstractListModel):
def __init__(self): def __init__(self, parent=None):
QAbstractListModel.__init__(self, parent)
self.states = [] self.states = []
self.pos = 0 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 @property
def current_container(self): def current_container(self):
return self.states[self.pos].container return self.states[self.pos].container
@ -40,6 +71,7 @@ class GlobalUndoHistory(object):
def open_book(self, container): def open_book(self, container):
self.states = [State(container)] self.states = [State(container)]
self.pos = 0 self.pos = 0
self.reset()
def add_savepoint(self, new_container, message): def add_savepoint(self, new_container, message):
try: 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' % ( 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))) self.pos, len(self.states)))
extra = self.states[self.pos+1:] extra = self.states[self.pos+1:]
if extra:
self.beginRemoveRows(ROOT, self.pos+1, len(self.states) - 1)
cleanup(extra) cleanup(extra)
self.states = self.states[:self.pos+1] 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.states.append(State(new_container))
self.pos += 1 self.pos += 1
self.endInsertRows()
self.dataChanged.emit(self.index(self.pos-1), self.index(self.pos))
if len(self.states) > MAX_SAVEPOINTS: if len(self.states) > MAX_SAVEPOINTS:
num = len(self.states) - MAX_SAVEPOINTS num = len(self.states) - MAX_SAVEPOINTS
self.beginRemoveRows(ROOT, 0, num - 1)
cleanup(self.states[:num]) cleanup(self.states[:num])
self.states = self.states[num:] self.states = self.states[num:]
self.endRemoveRows()
def rewind_savepoint(self): def rewind_savepoint(self):
''' Revert back to the last save point, should only be used immediately ''' 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 where you create savepoint, perform some operation, operation fails, so
revert to state before creating savepoint. ''' revert to state before creating savepoint. '''
if self.pos > 0 and self.pos == len(self.states) - 1: if self.pos > 0 and self.pos == len(self.states) - 1:
self.beginRemoveRows(ROOT, self.pos, self.pos)
self.pos -= 1 self.pos -= 1
cleanup([self.states.pop().container]) cleanup([self.states.pop().container])
self.endRemoveRows()
self.dataChanged.emit(self.index(self.pos))
ans = self.current_container ans = self.current_container
ans.message = None ans.message = None
return ans return ans
@ -73,17 +117,22 @@ class GlobalUndoHistory(object):
def undo(self): def undo(self):
if self.pos > 0: if self.pos > 0:
self.pos -= 1 self.pos -= 1
self.dataChanged.emit(self.index(self.pos), self.index(self.pos+1))
return self.current_container return self.current_container
def redo(self): def redo(self):
if self.pos < len(self.states) - 1: if self.pos < len(self.states) - 1:
self.pos += 1 self.pos += 1
self.dataChanged.emit(self.index(self.pos-1), self.index(self.pos))
return self.current_container return self.current_container
def revert_to(self, container): def revert_to(self, container):
for i, state in enumerate(self.states): for i, state in enumerate(self.states):
if state.container is container: if state.container is container:
opos = self.pos
self.pos = i self.pos = i
for x in (i, opos):
self.dataChanged.emit(self.index(x), self.index(x))
return container return container
@property @property
@ -106,3 +155,69 @@ class GlobalUndoHistory(object):
return '' return ''
return self.states[self.pos].message or '' 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)