%PDF- %PDF-
| Direktori : /lib/calibre/calibre/gui2/tweak_book/ |
| Current File : //lib/calibre/calibre/gui2/tweak_book/undo.py |
#!/usr/bin/env python3
__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
import shutil
from qt.core import (
QAbstractListModel, Qt, QModelIndex, QApplication, QWidget,
QGridLayout, QListView, QStyledItemDelegate, pyqtSignal, QPushButton, QIcon, QItemSelectionModel)
from calibre.gui2 import error_dialog
ROOT = QModelIndex()
MAX_SAVEPOINTS = 100
def cleanup(containers):
for container in containers:
try:
shutil.rmtree(container.root, ignore_errors=True)
except:
pass
class State:
def __init__(self, container):
self.container = container
self.message = None
self.rewind_message = None
class GlobalUndoHistory(QAbstractListModel):
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.ItemDataRole.DisplayRole):
if role == Qt.ItemDataRole.DisplayRole:
return self.label_for_row(index.row())
if role == Qt.ItemDataRole.FontRole and index.row() == self.pos:
f = QApplication.instance().font()
f.setBold(True)
return f
if role == Qt.ItemDataRole.UserRole:
return self.states[index.row()]
return None
def label_for_row(self, row):
msg = self.states[row].message
if self.pos == row:
msg = _('Current state') + ('' if not msg else _(' [was %s]') % msg)
elif not msg:
msg = _('[Unnamed state]')
else:
msg = msg
return msg
def label_for_container(self, container):
for i, state in enumerate(self.states):
if state.container is container:
return self.label_for_row(i)
@property
def current_container(self):
return self.states[self.pos].container
@property
def previous_container(self):
return self.states[self.pos - 1].container
def open_book(self, container):
self.beginResetModel()
self.states = [State(container)]
self.pos = 0
self.endResetModel()
def truncate(self):
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()
def add_savepoint(self, new_container, message):
try:
self.states[self.pos].rewind_message = self.states[self.pos].message
self.states[self.pos].message = message
except IndexError:
raise IndexError('The checkpoint stack has an incorrect position pointer.'
f' This should never happen: pos={self.pos!r}, len_states={len(self.states)=}')
self.truncate()
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.pos -= num
self.endRemoveRows()
def rewind_savepoint(self):
''' Revert back to the last save point, should only be used immediately
after a call to add_savepoint. If there are intervening calls to undo
or redo, behavior is undefined. This is intended to be used in the case
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), self.index(self.pos))
ans = self.current_container
self.states[self.pos].message = self.states[self.pos].rewind_message
return ans
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
def can_undo(self):
return self.pos > 0
@property
def can_redo(self):
return self.pos < len(self.states) - 1
@property
def undo_msg(self):
if not self.can_undo:
return ''
return self.states[self.pos - 1].message or ''
@property
def redo_msg(self):
if not self.can_redo:
return ''
return self.states[self.pos + 1].message or _('[Unnamed state]')
def update_path_to_ebook(self, path):
for state in self.states:
state.container.path_to_ebook = path
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), QItemSelectionModel.SelectionFlag.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)