#!/usr/bin/env python # --!-- coding: utf8 --!-- import datetime import difflib import re from PyQt5.QtCore import Qt, QTimer, QRect from PyQt5.QtGui import QPalette, QFontMetrics from PyQt5.QtWidgets import QWidget, QMenu, QActionGroup, QAction, QListWidgetItem, QStyledItemDelegate, QStyle from manuskript.enums import Outline from manuskript.ui.revisions_ui import Ui_revisions from manuskript.models import references as Ref class revisions(QWidget, Ui_revisions): def __init__(self, parent=None): QWidget.__init__(self, parent) self.setupUi(self) self.splitter.setStretchFactor(0, 5) self.splitter.setStretchFactor(1, 70) self.listDelegate = listCompleterDelegate(self) self.list.setItemDelegate(self.listDelegate) self.list.itemClicked.connect(self.showDiff) self.list.setContextMenuPolicy(Qt.CustomContextMenu) self.list.customContextMenuRequested.connect(self.popupMenu) self.btnDelete.setEnabled(False) self.btnDelete.clicked.connect(self.delete) self.btnRestore.clicked.connect(self.restore) self.btnRestore.setEnabled(False) # self.list.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.updateTimer = QTimer() self.updateTimer.setSingleShot(True) self.updateTimer.setInterval(500) self.updateTimer.timeout.connect(self.update) self.updateTimer.stop() self.menu = QMenu(self) self.actGroup = QActionGroup(self) self.actShowDiff = QAction(self.tr("Show modifications"), self.menu) self.actShowDiff.setCheckable(True) self.actShowDiff.setChecked(True) self.actShowDiff.triggered.connect(self.showDiff) self.menu.addAction(self.actShowDiff) self.actGroup.addAction(self.actShowDiff) self.actShowVersion = QAction(self.tr("Show ancient version"), self.menu) self.actShowVersion.setCheckable(True) self.actShowVersion.setChecked(False) self.actShowVersion.triggered.connect(self.showDiff) self.menu.addAction(self.actShowVersion) self.actGroup.addAction(self.actShowVersion) self.menu.addSeparator() self.actShowSpaces = QAction(self.tr("Show spaces"), self.menu) self.actShowSpaces.setCheckable(True) self.actShowSpaces.setChecked(False) self.actShowSpaces.triggered.connect(self.showDiff) self.menu.addAction(self.actShowSpaces) self.actDiffOnly = QAction(self.tr("Show modifications only"), self.menu) self.actDiffOnly.setCheckable(True) self.actDiffOnly.setChecked(True) self.actDiffOnly.triggered.connect(self.showDiff) self.menu.addAction(self.actDiffOnly) self.btnOptions.setMenu(self.menu) self._model = None self._index = None def setModel(self, model): self._model = model self._model.dataChanged.connect(self.updateMaybe) def setCurrentModelIndex(self, index): self._index = index self.view.setText("") self.update() def updateMaybe(self, topLeft, bottomRight): if self._index and \ topLeft.column() <= Outline.revisions <= bottomRight.column() and \ topLeft.row() <= self._index.row() <= bottomRight.row(): # self.update() self.updateTimer.start() def update(self): self.list.clear() item = self._index.internalPointer() rev = item.revisions() # Sort revisions rev = sorted(rev, key=lambda x: x[0], reverse=True) for r in rev: timestamp = datetime.datetime.fromtimestamp(r[0]).strftime('%Y-%m-%d %H:%M:%S') readable = self.readableDelta(r[0]) i = QListWidgetItem(readable) i.setData(Qt.UserRole, r[0]) i.setData(Qt.UserRole + 1, timestamp) self.list.addItem(i) def readableDelta(self, timestamp): now = datetime.datetime.now() delta = now - datetime.datetime.fromtimestamp(timestamp) if delta.days > 365: return self.tr("{} years ago").format(str(int(delta.days / 365))) elif delta.days > 30: return self.tr("{} months ago").format(str(int(delta.days / 30.5))) elif delta.days > 0: return self.tr("{} days ago").format(str(delta.days)) if delta.days == 1: return self.tr("1 day ago") elif delta.seconds > 60 * 60: return self.tr("{} hours ago").format(str(int(delta.seconds / 60 / 60))) elif delta.seconds > 60: return self.tr("{} minutes ago").format(str(int(delta.seconds / 60))) else: return self.tr("{} seconds ago").format(str(delta.seconds)) def showDiff(self): # UI stuff self.actShowSpaces.setEnabled(self.actShowDiff.isChecked()) self.actDiffOnly.setEnabled(self.actShowDiff.isChecked()) # FIXME: Errors in line number i = self.list.currentItem() if not i: self.btnDelete.setEnabled(False) self.btnRestore.setEnabled(False) return self.btnDelete.setEnabled(True) self.btnRestore.setEnabled(True) ts = i.data(Qt.UserRole) item = self._index.internalPointer() textNow = item.text() textBefore = [r[1] for r in item.revisions() if r[0] == ts][0] if self.actShowVersion.isChecked(): self.view.setText(textBefore) return textNow = textNow.splitlines() textBefore = textBefore.splitlines() d = difflib.Differ() diff = list(d.compare(textBefore, textNow)) if self.actShowSpaces.isChecked(): _format = lambda x: x.replace(" ", "␣ ") else: _format = lambda x: x extra = "
" diff = [d for d in diff if d and not d[:2] == "? "] mydiff = "" skip = False for n, l in enumerate(diff): l = diff[n] op = l[:2] txt = l[2:] op2 = diff[n + 1][:2] if n + 1 < len(diff) else None txt2 = diff[n + 1][2:] if n + 1 < len(diff) else None if skip: skip = False continue # Same line if op == " " and not self.actDiffOnly.isChecked(): mydiff += "{}{}".format(txt, extra) elif op == "- " and op2 == "+ ": if self.actDiffOnly.isChecked(): mydiff += "
{}
".format( self.tr("Line {}:").format(str(n))) s = difflib.SequenceMatcher(None, txt, txt2, autojunk=True) newline = "" for tag, i1, i2, j1, j2 in s.get_opcodes(): if tag == "equal": newline += txt[i1:i2] elif tag == "delete": newline += "{}".format(_format(txt[i1:i2])) elif tag == "insert": newline += "{}".format( _format(txt2[j1:j2])) elif tag == "replace": newline += "{}".format(_format(txt[i1:i2])) newline += "{}".format( _format(txt2[j1:j2])) # Few ugly tweaks for html diffs newline = re.sub(r"(()", "\\1\\2\\3", newline) newline = re.sub( r"

cenrighter\" style=\" -qt-block-indent:0; -qt-user-state:0; \">(.*?)

", "

\\1

", newline) newline = re.sub( r"

centeright\" style=\" -qt-block-indent:0; -qt-user-state:0; \">(.*)

", "

\\1

", newline) newline = re.sub(r")(.*?)()(.*?)>(.*?)

", "\\1\\5\\3

", newline) mydiff += newline + extra skip = True elif op == "- ": if self.actDiffOnly.isChecked(): mydiff += "
{}:
".format(str(n)) mydiff += "{}{}".format(txt, extra) elif op == "+ ": if self.actDiffOnly.isChecked(): mydiff += "
{}:
".format(str(n)) mydiff += "{}{}".format(txt, extra) self.view.setText(mydiff) def restore(self): i = self.list.currentItem() if not i: return ts = i.data(Qt.UserRole) item = self._index.internalPointer() textBefore = [r[1] for r in item.revisions() if r[0] == ts][0] index = self._index.sibling(self._index.row(), Outline.text) self._index.model().setData(index, textBefore) # item.setData(Outline.text, textBefore) def delete(self): i = self.list.currentItem() if not i: return ts = i.data(Qt.UserRole) self._index.internalPointer().deleteRevision(ts) def clearAll(self): self._index.internalPointer().clearAllRevisions() def saveState(self): return [ self.actShowDiff.isChecked(), self.actShowVersion.isChecked(), self.actShowSpaces.isChecked(), self.actDiffOnly.isChecked(), ] def popupMenu(self, pos): i = self.list.itemAt(pos) m = QMenu(self) if i: m.addAction(self.tr("Restore")).triggered.connect(self.restore) m.addAction(self.tr("Delete")).triggered.connect(self.delete) m.addSeparator() if self.list.count(): m.addAction(self.tr("Clear all")).triggered.connect(self.clearAll) m.popup(self.list.mapToGlobal(pos)) def restoreState(self, state): self.actShowDiff.setChecked(state[0]) self.actShowVersion.setChecked(state[1]) self.actShowSpaces.setChecked(state[2]) self.actDiffOnly.setChecked(state[3]) self.actShowSpaces.setEnabled(self.actShowDiff.isChecked()) self.actDiffOnly.setEnabled(self.actShowDiff.isChecked()) class listCompleterDelegate(QStyledItemDelegate): def __init__(self, parent=None): QStyledItemDelegate.__init__(self, parent) def paint(self, painter, option, index): extra = index.data(Qt.UserRole + 1) if not extra: return QStyledItemDelegate.paint(self, painter, option, index) else: if option.state & QStyle.State_Selected: painter.fillRect(option.rect, option.palette.color(QPalette.Inactive, QPalette.Highlight)) title = index.data() extra = " - {}".format(extra) painter.drawText(option.rect, Qt.AlignLeft, title) fm = QFontMetrics(option.font) w = fm.width(title) r = QRect(option.rect) r.setLeft(r.left() + w) painter.save() painter.setPen(Qt.gray) painter.drawText(r, Qt.AlignLeft, extra) painter.restore()