%PDF- %PDF-
Direktori : /proc/thread-self/root/usr/lib/calibre/calibre/gui2/tweak_book/ |
Current File : //proc/thread-self/root/usr/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)