manuskript/manuskript/ui/revisions.py
2017-11-15 20:58:12 +01:00

299 lines
12 KiB
Python

#!/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 = "<br>"
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 += "<br><span style='color: blue;'>{}</span><br>".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 += "<span style='color:red; background:yellow;'>{}</span>".format(_format(txt[i1:i2]))
elif tag == "insert":
newline += "<span style='color:green; background:yellow;'>{}</span>".format(
_format(txt2[j1:j2]))
elif tag == "replace":
newline += "<span style='color:red; background:yellow;'>{}</span>".format(_format(txt[i1:i2]))
newline += "<span style='color:green; background:yellow;'>{}</span>".format(
_format(txt2[j1:j2]))
# Few ugly tweaks for html diffs
newline = re.sub(r"(<span style='color.*?><span.*?>)</span>(.*)<span style='color:.*?>(</span></span>)",
"\\1\\2\\3", newline)
newline = re.sub(
r"<p align=\"<span style='color:red; background:yellow;'>cen</span><span style='color:green; background:yellow;'>righ</span>t<span style='color:red; background:yellow;'>er</span>\" style=\" -qt-block-indent:0; -qt-user-state:0; \">(.*?)</p>",
"<p align=\"right\"><span style='color:green; background:yellow;'>\\1</span></p>", newline)
newline = re.sub(
r"<p align=\"<span style='color:green; background:yellow;'>cente</span>r<span style='color:red; background:yellow;'>ight</span>\" style=\" -qt-block-indent:0; -qt-user-state:0; \">(.*)</p>",
"<p align=\"center\"><span style='color:green; background:yellow;'>\\1</span></p>", newline)
newline = re.sub(r"<p(<span.*?>)(.*?)(</span>)(.*?)>(.*?)</p>",
"<p\\2\\4>\\1\\5\\3</p>", newline)
mydiff += newline + extra
skip = True
elif op == "- ":
if self.actDiffOnly.isChecked():
mydiff += "<br>{}:<br>".format(str(n))
mydiff += "<span style='color:red;'>{}</span>{}".format(txt, extra)
elif op == "+ ":
if self.actDiffOnly.isChecked():
mydiff += "<br>{}:<br>".format(str(n))
mydiff += "<span style='color:green;'>{}</span>{}".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()