mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Implement drawing colored backgrounds for change blocks
This commit is contained in:
parent
78dab06e96
commit
45abaf8ce4
@ -8,16 +8,19 @@ __copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
from PyQt4.Qt import (
|
from PyQt4.Qt import (
|
||||||
QSplitter, QApplication, QPlainTextDocumentLayout, QTextDocument,
|
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 import tprefs
|
||||||
from calibre.gui2.tweak_book.editor.text import PlainTextEdit, get_highlighter, default_font_family, LineNumbers
|
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.gui2.tweak_book.editor.themes import THEMES, default_theme, theme_color
|
||||||
from calibre.utils.diff import get_sequence_matcher
|
from calibre.utils.diff import get_sequence_matcher
|
||||||
|
|
||||||
|
Change = namedtuple('Change', 'ltop lbot rtop rbot kind')
|
||||||
|
|
||||||
def get_theme():
|
def get_theme():
|
||||||
theme = THEMES.get(tprefs['editor_theme'], None)
|
theme = THEMES.get(tprefs['editor_theme'], None)
|
||||||
if theme is None:
|
if theme is None:
|
||||||
@ -26,8 +29,9 @@ def get_theme():
|
|||||||
|
|
||||||
class TextBrowser(PlainTextEdit): # {{{
|
class TextBrowser(PlainTextEdit): # {{{
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, right=False, parent=None):
|
||||||
PlainTextEdit.__init__(self, parent)
|
PlainTextEdit.__init__(self, parent)
|
||||||
|
self.right = right
|
||||||
self.setReadOnly(True)
|
self.setReadOnly(True)
|
||||||
w = self.fontMetrics()
|
w = self.fontMetrics()
|
||||||
self.number_width = max(map(lambda x:w.width(str(x)), xrange(10)))
|
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.Text, theme_color(theme, 'LineNr', 'fg'))
|
||||||
pal.setColor(pal.BrightText, theme_color(theme, 'LineNrC', 'fg'))
|
pal.setColor(pal.BrightText, theme_color(theme, 'LineNrC', 'fg'))
|
||||||
self.line_number_map = {}
|
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):
|
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):
|
def line_number_area_width(self):
|
||||||
digits = 1
|
digits = 1
|
||||||
@ -82,7 +106,10 @@ class TextBrowser(PlainTextEdit): # {{{
|
|||||||
def resizeEvent(self, ev):
|
def resizeEvent(self, ev):
|
||||||
PlainTextEdit.resizeEvent(self, ev)
|
PlainTextEdit.resizeEvent(self, ev)
|
||||||
cr = self.contentsRect()
|
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):
|
def paint_line_numbers(self, ev):
|
||||||
painter = QPainter(self.line_number_area)
|
painter = QPainter(self.line_number_area)
|
||||||
@ -95,13 +122,45 @@ class TextBrowser(PlainTextEdit): # {{{
|
|||||||
painter.setPen(self.line_number_palette.color(QPalette.Text))
|
painter.setPen(self.line_number_palette.color(QPalette.Text))
|
||||||
|
|
||||||
while block.isValid() and top <= ev.rect().bottom():
|
while block.isValid() and top <= ev.rect().bottom():
|
||||||
if block.isVisible() and bottom >= ev.rect().top():
|
r = ev.rect()
|
||||||
painter.drawText(0, top, self.line_number_area.width() - 5, self.fontMetrics().height(),
|
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, '')))
|
Qt.AlignRight, unicode(self.line_number_map.get(num, '')))
|
||||||
block = block.next()
|
block = block.next()
|
||||||
top = bottom
|
top = bottom
|
||||||
bottom = top + int(self.blockBoundingRect(block).height())
|
bottom = top + int(self.blockBoundingRect(block).height())
|
||||||
num += 1
|
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): # {{{
|
class Highlight(QTextDocument): # {{{
|
||||||
@ -141,7 +200,7 @@ class TextDiffView(QSplitter):
|
|||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
QSplitter.__init__(self, parent)
|
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)
|
self.addWidget(self.left), self.addWidget(self.right)
|
||||||
|
|
||||||
def __call__(self, left_text, right_text, context=None, syntax=None):
|
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.left_highlight, self.right_highlight = Highlight(self, left_text, syntax), Highlight(self, right_text, syntax)
|
||||||
self.context = context
|
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.cruncher = get_sequence_matcher()(None, left_lines, right_lines)
|
||||||
self.do_layout(context)
|
self.do_layout(context)
|
||||||
|
|
||||||
@ -160,7 +217,12 @@ class TextDiffView(QSplitter):
|
|||||||
|
|
||||||
def do_layout(self, context=None):
|
def do_layout(self, context=None):
|
||||||
self.left.clear(), self.right.clear()
|
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:
|
if context is None:
|
||||||
for tag, alo, ahi, blo, bhi in self.cruncher.get_opcodes():
|
for tag, alo, ahi, blo, bhi in self.cruncher.get_opcodes():
|
||||||
getattr(self, tag)(alo, ahi, blo, bhi)
|
getattr(self, tag)(alo, ahi, blo, bhi)
|
||||||
@ -168,38 +230,55 @@ class TextDiffView(QSplitter):
|
|||||||
for group in self.cruncher.get_grouped_opcodes():
|
for group in self.cruncher.get_grouped_opcodes():
|
||||||
for tag, alo, ahi, blo, bhi in group:
|
for tag, alo, ahi, blo, bhi in group:
|
||||||
getattr(self, tag)(alo, ahi, blo, bhi)
|
getattr(self, tag)(alo, ahi, blo, bhi)
|
||||||
|
cl.endEditBlock(), cr.endEditBlock()
|
||||||
|
|
||||||
for v in (self.left, self.right):
|
for v in (self.left, self.right):
|
||||||
c = v.textCursor()
|
c = v.textCursor()
|
||||||
c.movePosition(c.Start)
|
c.movePosition(c.Start)
|
||||||
v.setTextCursor(c)
|
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):
|
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)
|
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
|
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):
|
def equal(self, alo, ahi, blo, bhi):
|
||||||
self.left_insert(alo, ahi), self.right_insert(blo, bhi)
|
self.left_insert(alo, ahi), self.right_insert(blo, bhi)
|
||||||
|
|
||||||
def delete(self, alo, ahi, 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):
|
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):
|
def replace(self, alo, ahi, blo, bhi):
|
||||||
self.left_insert(alo, ahi)
|
lsb, lcb = self.left_insert(alo, ahi)
|
||||||
self.right_insert(blo, bhi)
|
rsb, rcb = self.right_insert(blo, bhi)
|
||||||
|
self.changes.append(Change(
|
||||||
|
rtop=rsb, rbot=rcb, ltop=lsb, lbot=lcb, kind='replace'))
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app = QApplication([])
|
app = QApplication([])
|
||||||
raw1 = open(sys.argv[-2], 'rb').read().decode('utf-8')
|
raw1 = open(sys.argv[-2], 'rb').read().decode('utf-8')
|
||||||
raw2 = open(sys.argv[-1], 'rb').read().decode('utf-8')
|
raw2 = open(sys.argv[-1], 'rb').read().decode('utf-8')
|
||||||
w = TextDiffView()
|
w = TextDiffView()
|
||||||
w(raw1, raw2, syntax='html', context=None)
|
|
||||||
w.show()
|
w.show()
|
||||||
|
w(raw1, raw2, syntax='html', context=None)
|
||||||
app.exec_()
|
app.exec_()
|
||||||
|
|
||||||
|
# TODO: Add diff colors for other color schemes
|
||||||
|
# TODO: Handle scroll wheel and key up/down events
|
||||||
|
@ -137,6 +137,11 @@ THEMES = {
|
|||||||
SpecialCharacter bg={cursor_loc}
|
SpecialCharacter bg={cursor_loc}
|
||||||
Error us=wave uc=red
|
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(
|
'''.format(
|
||||||
cursor_loc='F8DE7E',
|
cursor_loc='F8DE7E',
|
||||||
identifier='7b5694',
|
identifier='7b5694',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user