diff --git a/src/calibre/gui2/tweak_book/diff.py b/src/calibre/gui2/tweak_book/diff.py index 6c91258482..7a602f3ad5 100644 --- a/src/calibre/gui2/tweak_book/diff.py +++ b/src/calibre/gui2/tweak_book/diff.py @@ -8,16 +8,19 @@ __copyright__ = '2014, Kovid Goyal ' import sys from functools import partial +from collections import namedtuple from PyQt4.Qt import ( QSplitter, QApplication, QPlainTextDocumentLayout, QTextDocument, - QTextCursor, QTextCharFormat, Qt, QRect, QPainter, QPalette) + QTextCursor, QTextCharFormat, Qt, QRect, QPainter, QPalette, QPen) from calibre.gui2.tweak_book import tprefs from calibre.gui2.tweak_book.editor.text import PlainTextEdit, get_highlighter, default_font_family, LineNumbers from calibre.gui2.tweak_book.editor.themes import THEMES, default_theme, theme_color from calibre.utils.diff import get_sequence_matcher +Change = namedtuple('Change', 'ltop lbot rtop rbot kind') + def get_theme(): theme = THEMES.get(tprefs['editor_theme'], None) if theme is None: @@ -26,8 +29,9 @@ def get_theme(): class TextBrowser(PlainTextEdit): # {{{ - def __init__(self, parent=None): + def __init__(self, right=False, parent=None): PlainTextEdit.__init__(self, parent) + self.right = right self.setReadOnly(True) w = self.fontMetrics() self.number_width = max(map(lambda x:w.width(str(x)), xrange(10))) @@ -58,9 +62,29 @@ class TextBrowser(PlainTextEdit): # {{{ pal.setColor(pal.Text, theme_color(theme, 'LineNr', 'fg')) pal.setColor(pal.BrightText, theme_color(theme, 'LineNrC', 'fg')) self.line_number_map = {} + self.changes = [] + self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.diff_backgrounds = { + 'replace' : theme_color(theme, 'DiffReplace', 'bg'), + 'insert' : theme_color(theme, 'DiffInsert', 'bg'), + 'delete' : theme_color(theme, 'DiffDelete', 'bg'), + } + self.diff_foregrounds = { + 'replace' : theme_color(theme, 'DiffReplace', 'fg'), + 'insert' : theme_color(theme, 'DiffInsert', 'fg'), + 'delete' : theme_color(theme, 'DiffDelete', 'fg'), + } + + def clear(self): + PlainTextEdit.clear(self) + self.line_number_map.clear() + del self.changes[:] def update_line_number_area_width(self, block_count=0): - self.setViewportMargins(self.line_number_area_width(), 0, 0, 0) + if self.right: + self.setViewportMargins(0, 0, self.line_number_area_width(), 0) + else: + self.setViewportMargins(self.line_number_area_width(), 0, 0, 0) def line_number_area_width(self): digits = 1 @@ -82,7 +106,10 @@ class TextBrowser(PlainTextEdit): # {{{ def resizeEvent(self, ev): PlainTextEdit.resizeEvent(self, ev) cr = self.contentsRect() - self.line_number_area.setGeometry(QRect(cr.left(), cr.top(), self.line_number_area_width(), cr.height())) + if self.right: + self.line_number_area.setGeometry(QRect(cr.right() - self.line_number_area_width(), cr.top(), cr.right(), cr.height())) + else: + self.line_number_area.setGeometry(QRect(cr.left(), cr.top(), self.line_number_area_width(), cr.height())) def paint_line_numbers(self, ev): painter = QPainter(self.line_number_area) @@ -95,13 +122,45 @@ class TextBrowser(PlainTextEdit): # {{{ painter.setPen(self.line_number_palette.color(QPalette.Text)) while block.isValid() and top <= ev.rect().bottom(): - if block.isVisible() and bottom >= ev.rect().top(): - painter.drawText(0, top, self.line_number_area.width() - 5, self.fontMetrics().height(), + r = ev.rect() + if block.isVisible() and bottom >= r.top(): + if self.right: + painter.drawText(r.left() + 3, top, r.right(), self.fontMetrics().height(), + Qt.AlignLeft, unicode(self.line_number_map.get(num, ''))) + else: + painter.drawText(r.left(), top, r.right() - 5, self.fontMetrics().height(), Qt.AlignRight, unicode(self.line_number_map.get(num, ''))) block = block.next() top = bottom bottom = top + int(self.blockBoundingRect(block).height()) num += 1 + + def paintEvent(self, event): + w = self.width() + painter = QPainter(self.viewport()) + painter.setClipRect(event.rect()) + floor = event.rect().bottom() + ceiling = event.rect().top() + fv = self.firstVisibleBlock().blockNumber() + origin = self.contentOffset() + doc = self.document() + + for top, bot, kind in self.changes: + if bot < fv: + continue + y_top = self.blockBoundingGeometry(doc.findBlockByNumber(top)).translated(origin).y() - 1 + y_bot = self.blockBoundingGeometry(doc.findBlockByNumber(bot)).translated(origin).y() + 1 + if max(y_top, y_bot) < ceiling: + continue + if min(y_top, y_bot) > floor: + break + painter.fillRect(0, y_top, w, y_bot - y_top, self.diff_backgrounds[kind]) + painter.setPen(QPen(self.diff_foregrounds[kind], 1)) + painter.drawLine(0, y_top, w, y_top) + painter.drawLine(0, y_bot - 1, w, y_bot - 1) + painter.end() + PlainTextEdit.paintEvent(self, event) + # }}} class Highlight(QTextDocument): # {{{ @@ -141,7 +200,7 @@ class TextDiffView(QSplitter): def __init__(self, parent=None): QSplitter.__init__(self, parent) - self.left, self.right = TextBrowser(self), TextBrowser(self) + self.left, self.right = TextBrowser(parent=self), TextBrowser(right=True, parent=self) self.addWidget(self.left), self.addWidget(self.right) def __call__(self, left_text, right_text, context=None, syntax=None): @@ -150,8 +209,6 @@ class TextDiffView(QSplitter): self.left_highlight, self.right_highlight = Highlight(self, left_text, syntax), Highlight(self, right_text, syntax) self.context = context - self.left_insert = partial(self.do_insert, self.left.textCursor(), self.left_highlight, self.left.line_number_map) - self.right_insert = partial(self.do_insert, self.right.textCursor(), self.right_highlight, self.right.line_number_map) self.cruncher = get_sequence_matcher()(None, left_lines, right_lines) self.do_layout(context) @@ -160,7 +217,12 @@ class TextDiffView(QSplitter): def do_layout(self, context=None): self.left.clear(), self.right.clear() - self.left.line_number_map.clear(), self.right.line_number_map.clear() + cl, cr = self.left_cursor, self.right_cursor = self.left.textCursor(), self.right.textCursor() + cl.beginEditBlock(), cr.beginEditBlock() + self.changes = [] + self.left_insert = partial(self.do_insert, cl, self.left_highlight, self.left.line_number_map) + self.right_insert = partial(self.do_insert, cr, self.right_highlight, self.right.line_number_map) + if context is None: for tag, alo, ahi, blo, bhi in self.cruncher.get_opcodes(): getattr(self, tag)(alo, ahi, blo, bhi) @@ -168,38 +230,55 @@ class TextDiffView(QSplitter): for group in self.cruncher.get_grouped_opcodes(): for tag, alo, ahi, blo, bhi in group: getattr(self, tag)(alo, ahi, blo, bhi) + cl.endEditBlock(), cr.endEditBlock() for v in (self.left, self.right): c = v.textCursor() c.movePosition(c.Start) v.setTextCursor(c) + for ltop, lbot, rtop, rbot, kind in self.changes: + self.left.changes.append((ltop, lbot, kind)) + self.right.changes.append((rtop, rbot, kind)) + + self.update() + def do_insert(self, cursor, highlighter, line_number_map, lo, hi): - start_block = cursor.blockNumber() + start_block = cursor.block() highlighter.copy_lines(lo, hi, cursor) - for num, i in enumerate(xrange(start_block, cursor.blockNumber())): + for num, i in enumerate(xrange(start_block.blockNumber(), cursor.blockNumber())): line_number_map[i] = lo + num + 1 - return start_block, cursor.blockNumber() + return start_block.blockNumber(), cursor.block().blockNumber() def equal(self, alo, ahi, blo, bhi): self.left_insert(alo, ahi), self.right_insert(blo, bhi) def delete(self, alo, ahi, blo, bhi): - self.left_insert(alo, ahi) + start_block, current_block = self.left_insert(alo, ahi) + r = self.right_cursor.block().blockNumber() + self.changes.append(Change( + ltop=start_block, lbot=current_block, rtop=r, rbot=r, kind='delete')) def insert(self, alo, ahi, blo, bhi): - self.right_insert(blo, bhi) + start_block, current_block = self.right_insert(blo, bhi) + l = self.left_cursor.block().blockNumber() + self.changes.append(Change( + rtop=start_block, rbot=current_block, ltop=l, lbot=l, kind='insert')) def replace(self, alo, ahi, blo, bhi): - self.left_insert(alo, ahi) - self.right_insert(blo, bhi) + lsb, lcb = self.left_insert(alo, ahi) + rsb, rcb = self.right_insert(blo, bhi) + self.changes.append(Change( + rtop=rsb, rbot=rcb, ltop=lsb, lbot=lcb, kind='replace')) if __name__ == '__main__': app = QApplication([]) raw1 = open(sys.argv[-2], 'rb').read().decode('utf-8') raw2 = open(sys.argv[-1], 'rb').read().decode('utf-8') w = TextDiffView() - w(raw1, raw2, syntax='html', context=None) w.show() + w(raw1, raw2, syntax='html', context=None) app.exec_() +# TODO: Add diff colors for other color schemes +# TODO: Handle scroll wheel and key up/down events diff --git a/src/calibre/gui2/tweak_book/editor/themes.py b/src/calibre/gui2/tweak_book/editor/themes.py index fef80d57f3..929a94a456 100644 --- a/src/calibre/gui2/tweak_book/editor/themes.py +++ b/src/calibre/gui2/tweak_book/editor/themes.py @@ -137,6 +137,11 @@ THEMES = { SpecialCharacter bg={cursor_loc} Error us=wave uc=red + DiffDelete bg=rgb(255,160,180) fg=rgb(200,60,90) + DiffInsert bg=rgb(180,255,180) fg=rgb(80,210,80) + DiffReplace bg=rgb(206,226,250) fg=rgb(90,130,180) + DiffReplaceReplace bg=rgb(180,210,250) + '''.format( cursor_loc='F8DE7E', identifier='7b5694',