18
18
from PyQt4.Qt import (
19
QSplitter, QApplication, QPlainTextDocumentLayout, QTextDocument, QTimer,
19
QSplitter, QApplication, QTimer,
20
20
QTextCursor, QTextCharFormat, Qt, QRect, QPainter, QPalette, QPen, QBrush,
21
21
QColor, QTextLayout, QCursor, QFont, QSplitterHandle, QPainterPath,
22
22
QHBoxLayout, QWidget, QScrollBar, QEventLoop, pyqtSignal, QImage, QPixmap,
25
25
from calibre import human_readable, fit_image
26
26
from calibre.gui2 import info_dialog
27
27
from calibre.gui2.tweak_book import tprefs
28
from calibre.gui2.tweak_book.editor.text import PlainTextEdit, get_highlighter, default_font_family, LineNumbers
29
from calibre.gui2.tweak_book.editor.themes import THEMES, default_theme, theme_color
28
from calibre.gui2.tweak_book.editor.text import PlainTextEdit, default_font_family, LineNumbers
29
from calibre.gui2.tweak_book.editor.themes import theme_color
30
30
from calibre.gui2.tweak_book.diff import get_sequence_matcher
31
from calibre.gui2.tweak_book.diff.highlight import get_theme, get_highlighter
32
33
Change = namedtuple('Change', 'ltop lbot rtop rbot kind')
39
40
def __exit__(self, *args):
40
41
QApplication.restoreOverrideCursor()
43
theme = THEMES.get(tprefs['editor_theme'], None)
45
theme = THEMES[default_theme()]
48
43
def beautify_text(raw, syntax):
49
44
from lxml import etree
50
45
from calibre.ebooks.oeb.polish.parsing import parse
165
160
def show_context_menu(self, pos):
168
i = unicode(self.textCursor().selectedText())
163
i = unicode(self.textCursor().selectedText()).rstrip('\0')
170
165
a(QIcon(I('edit-copy.png')), _('Copy to clipboard'), self.copy).setShortcut(QKeySequence.Copy)
198
193
for i, (num, text) in enumerate(self.headers):
199
194
if num > block_number:
200
195
name = text if i == 0 else self.headers[i - 1][1]
202
name = self.headers[0][1]
198
name = self.headers[-1][1]
203
199
self.line_activated.emit(name, lnum, bool(self.right))
205
201
def search(self, query, reverse=False):
395
class Highlight(QTextDocument): # {{{
397
def __init__(self, parent, text, syntax):
398
QTextDocument.__init__(self, parent)
399
self.l = QPlainTextDocumentLayout(self)
400
self.setDocumentLayout(self.l)
401
self.highlighter = get_highlighter(syntax)(self)
402
self.highlighter.apply_theme(get_theme())
403
self.highlighter.setDocument(self)
404
self.setPlainText(text)
406
def copy_lines(self, lo, hi, cursor):
407
''' Copy specified lines from the syntax highlighted buffer into the
408
destination cursor, preserving all formatting created by the syntax
412
block = self.findBlockByNumber(lo)
415
cursor.insertText(block.text())
416
dest_block = cursor.block()
417
c = QTextCursor(dest_block)
418
for af in block.layout().additionalFormats():
419
start = dest_block.position() + af.start
420
c.setPosition(start), c.setPosition(start + af.length, c.KeepAnchor)
421
c.setCharFormat(af.format)
423
cursor.setCharFormat(QTextCharFormat())
427
391
class DiffSplitHandle(QSplitterHandle): # {{{
692
656
right_text = unicodedata.normalize('NFC', right_text)
693
657
if beautify and syntax in {'xml', 'html', 'css'}:
694
658
left_text, right_text = beautify_text(left_text, syntax), beautify_text(right_text, syntax)
659
if len(left_text) == len(right_text) and left_text == right_text:
660
for v in (self.left, self.right):
662
c.movePosition(c.End)
663
c.insertText('[%s]\n\n' % _('The files are identical after beautifying'))
695
666
left_lines = self.left_lines = left_text.splitlines()
696
667
right_lines = self.right_lines = right_text.splitlines()
698
669
cruncher = get_sequence_matcher()(None, left_lines, right_lines)
700
left_highlight, right_highlight = Highlight(self, left_text, syntax), Highlight(self, right_text, syntax)
671
left_highlight, right_highlight = get_highlighter(self.left, left_text, syntax), get_highlighter(self.right, right_text, syntax)
701
672
cl, cr = self.left_cursor, self.right_cursor = self.left.textCursor(), self.right.textCursor()
702
673
cl.beginEditBlock(), cr.beginEditBlock()
703
674
cl.movePosition(cl.End), cr.movePosition(cr.End)
718
689
self.left.line_number_map[self.changes[-1].ltop] = '-'
719
690
self.right.line_number_map[self.changes[-1].rtop] = '-'
721
693
for i, group in enumerate(cruncher.get_grouped_opcodes(context)):
722
694
for j, (tag, alo, ahi, blo, bhi) in enumerate(group):
723
695
if j == 0 and (i > 0 or min(alo, blo) > 0):
778
750
self.changes.append(Change(
779
751
rtop=start_block, rbot=current_block, ltop=l, lbot=l, kind='insert'))
753
def trim_identical_leading_lines(self, alo, ahi, blo, bhi):
754
''' The patience diff algorithm sometimes results in a block of replace
755
lines with identical leading lines. Remove these. This can cause extra
756
lines of context, but that is better than having extra lines of diff
757
with no actual changes. '''
758
a, b = self.left_lines, self.right_lines
760
while alo < ahi and blo < bhi and a[alo] == b[blo]:
765
self.equal(alo - leading, alo, blo - leading, blo)
766
return alo, ahi, blo, bhi
781
768
def replace(self, alo, ahi, blo, bhi):
782
769
''' When replacing one block of lines with another, search the blocks
783
770
for *similar* lines; the best-matching pair (if any) is used as a synch
784
771
point, and intraline difference marking is done on the similar pair.
785
772
Lots of work, but often worth it. '''
773
alo, ahi, blo, bhi = self.trim_identical_leading_lines(alo, ahi, blo, bhi)
774
if alo == ahi and blo == bhi:
786
776
if ahi + bhi - alo - blo > 100:
787
777
# Too many lines, this will be too slow
788
778
# http://bugs.python.org/issue6931
968
958
changes = self.changes[which]
969
959
bar = self.bars[which]
970
960
syncpos = self.syncpos + bar.value()
972
962
for i, (top, bot, kind) in enumerate(changes):
973
963
if syncpos <= bot:
974
964
if top <= syncpos:
980
970
return 'in', i, ratio
982
# syncpos is after the change
983
offset = syncpos - prev[1]
972
# syncpos is after the previous change
973
offset = syncpos - prev
984
974
return 'after', i - 1, offset
987
prev = (top, bot, kind)
989
offset = syncpos - prev[1]
990
return 'after', len(self.changes) - 1, offset
976
# syncpos is after the current change
978
offset = syncpos - prev
979
return 'after', len(changes) - 1, offset
992
981
def scroll_to(self, which, position):
993
982
changes = self.changes[which]