From 6ff2d3c346b57681ce3e2620f6f86999c77bbf1a Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 23 May 2011 13:58:56 +0100 Subject: [PATCH 1/2] Add syntax highlighting and parenthesis matching to the template editor --- src/calibre/gui2/dialogs/template_dialog.py | 203 +++++++++++++++++++- 1 file changed, 202 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/dialogs/template_dialog.py b/src/calibre/gui2/dialogs/template_dialog.py index 174056ef80..da2b730444 100644 --- a/src/calibre/gui2/dialogs/template_dialog.py +++ b/src/calibre/gui2/dialogs/template_dialog.py @@ -5,10 +5,190 @@ __license__ = 'GPL v3' import json -from PyQt4.Qt import Qt, QDialog, QDialogButtonBox +from PyQt4.Qt import (Qt, QDialog, QDialogButtonBox, QSyntaxHighlighter, + QRegExp, QSettings, QVariant, QApplication, + QTextCharFormat, QFont, QColor, QCursor) + from calibre.gui2.dialogs.template_dialog_ui import Ui_TemplateDialog from calibre.utils.formatter_functions import formatter_functions +class ParenPosition: + + def __init__(self, block, pos, paren): + self.block = block + self.pos = pos + self.paren = paren + self.highlight = False + + def set_highlight(self, to_what): + self.highlight = to_what + +class TemplateHighlighter(QSyntaxHighlighter): + + Config = {} + Rules = [] + Formats = {} + + KEYWORDS = ["program"] + + def __init__(self, parent=None): + super(TemplateHighlighter, self).__init__(parent) + + self.initializeFormats() + + TemplateHighlighter.Rules.append((QRegExp( + "|".join([r"\b%s\b" % keyword for keyword in self.KEYWORDS])), + "keyword")) + TemplateHighlighter.Rules.append((QRegExp( + "|".join([r"\b%s\b" % builtin for builtin in + formatter_functions.get_builtins()])), + "builtin")) + + TemplateHighlighter.Rules.append((QRegExp( + r"\b[+-]?[0-9]+[lL]?\b" + r"|\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b" + r"|\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b"), + "number")) + + stringRe = QRegExp(r"""(?:[^:]'[^']*'|"[^"]*")""") + stringRe.setMinimal(True) + TemplateHighlighter.Rules.append((stringRe, "string")) + + lparenRe = QRegExp(r'\(') + lparenRe.setMinimal(True) + TemplateHighlighter.Rules.append((lparenRe, "lparen")) + rparenRe = QRegExp(r'\)') + rparenRe.setMinimal(True) + TemplateHighlighter.Rules.append((rparenRe, "rparen")) + + self.regenerate_paren_positions() + self.highlighted_paren = False + + def initializeFormats(self): + Config = self.Config + Config["fontfamily"] = "Bitstream Vera Sans Mono" + Config["fontsize"] = 10 + for name, color, bold, italic in ( + ("normal", "#000000", False, False), + ("keyword", "#000080", True, False), + ("builtin", "#0000A0", False, False), + ("comment", "#007F00", False, True), + ("string", "#808000", False, False), + ("number", "#924900", False, False), + ("lparen", "#000000", True, True), + ("rparen", "#000000", True, True)): + Config["%sfontcolor" % name] = color + Config["%sfontbold" % name] = bold + Config["%sfontitalic" % name] = italic + + baseFormat = QTextCharFormat() + baseFormat.setFontFamily(Config["fontfamily"]) + baseFormat.setFontPointSize(Config["fontsize"]) + + for name in ("normal", "keyword", "builtin", "comment", + "string", "number", "lparen", "rparen"): + format = QTextCharFormat(baseFormat) + format.setForeground(QColor(Config["%sfontcolor" % name])) + if Config["%sfontbold" % name]: + format.setFontWeight(QFont.Bold) + format.setFontItalic(Config["%sfontitalic" % name]) + self.Formats[name] = format + + def find_paren(self, bn, pos): + for pp in self.paren_positions: + if pp.block == bn and pp.pos == pos: + return pp + return None + + def highlightBlock(self, text): + bn = self.currentBlock().blockNumber() + textLength = text.length() + + self.setFormat(0, textLength, self.Formats["normal"]) + + if text.isEmpty(): + pass + elif text[0] == "#": + self.setFormat(0, text.length(), self.Formats["comment"]) + return + + for regex, format in TemplateHighlighter.Rules: + i = regex.indexIn(text) + while i >= 0: + length = regex.matchedLength() + if format in ['lparen', 'rparen']: + pp = self.find_paren(bn, i) + if pp and pp.highlight: + self.setFormat(i, length, self.Formats[format]) + else: + self.setFormat(i, length, self.Formats[format]) + i = regex.indexIn(text, i + length) + + if self.generate_paren_positions: + t = unicode(text) + i = 0 + first = True + while i < len(t): + c = t[i] + if c == ':': + if first and i+1 < len(t) and t[i+1] == "'": + i += 2 + elif c in ["'", '"']: + first = False + i += 1 + j = t[i:].find(c) + if j < 0: + i = len(t) + else: + i = i + j + elif c == '(': + self.paren_positions.append(ParenPosition(bn, i, '(')) + elif c == ')': + self.paren_positions.append(ParenPosition(bn, i, ')')) + i += 1 + + def rehighlight(self): + QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) + QSyntaxHighlighter.rehighlight(self) + QApplication.restoreOverrideCursor() + + def check_cursor_pos(self, chr, block, pos_in_block): + found_pp = -1 + for i, pp in enumerate(self.paren_positions): + pp.set_highlight(False) + if pp.block == block and pp.pos == pos_in_block: + found_pp = i + + if chr not in ['(', ')']: + if self.highlighted_paren: + self.rehighlight() + self.highlighted_paren = False + return + + if found_pp >= 0: + stack = 0 + if chr == '(': + list = self.paren_positions[found_pp+1:] + else: + list = reversed(self.paren_positions[0:found_pp]) + for pp in list: + if pp.paren == chr: + stack += 1; + elif stack: + stack -= 1 + else: + pp.set_highlight(True) + self.paren_positions[found_pp].set_highlight(True) + break + self.highlighted_paren = True + self.rehighlight() + + def regenerate_paren_positions(self): + self.generate_paren_positions = True + self.paren_positions = [] + self.rehighlight() + self.generate_paren_positions = False + class TemplateDialog(QDialog, Ui_TemplateDialog): def __init__(self, parent, text): @@ -20,6 +200,11 @@ class TemplateDialog(QDialog, Ui_TemplateDialog): self.setWindowFlags(self.windowFlags()&(~Qt.WindowContextHelpButtonHint)) self.setWindowIcon(icon) + self.last_text = '' + self.highlighter = TemplateHighlighter(self.textbox.document()) + self.textbox.cursorPositionChanged.connect(self.text_cursor_changed) + self.textbox.textChanged.connect(self.textbox_changed) + self.textbox.setTabStopWidth(10) self.source_code.setTabStopWidth(10) self.documentation.setReadOnly(True) @@ -46,6 +231,22 @@ class TemplateDialog(QDialog, Ui_TemplateDialog): self.function.setCurrentIndex(0) self.function.currentIndexChanged[str].connect(self.function_changed) + def textbox_changed(self): + cur_text = unicode(self.textbox.toPlainText()) + if self.last_text != cur_text: + self.last_text = cur_text + self.highlighter.regenerate_paren_positions() + + def text_cursor_changed(self): + cursor = self.textbox.textCursor() + block_number = cursor.blockNumber() + pos_in_block = cursor.positionInBlock() + position = cursor.position() + t = unicode(self.textbox.toPlainText()) + if position < len(t): + self.highlighter.check_cursor_pos(t[position], block_number, + pos_in_block) + def function_changed(self, toWhat): name = unicode(toWhat) self.source_code.clear() From e624da286e8bd3f47990750815bab6b9f1fb6842 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 23 May 2011 16:12:02 +0100 Subject: [PATCH 2/2] Improvements to paren matching algorithm. --- src/calibre/gui2/dialogs/template_dialog.py | 25 ++++++++++++--------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/calibre/gui2/dialogs/template_dialog.py b/src/calibre/gui2/dialogs/template_dialog.py index da2b730444..b82e8b00bd 100644 --- a/src/calibre/gui2/dialogs/template_dialog.py +++ b/src/calibre/gui2/dialogs/template_dialog.py @@ -28,6 +28,7 @@ class TemplateHighlighter(QSyntaxHighlighter): Config = {} Rules = [] Formats = {} + BN_FACTOR = 1000 KEYWORDS = ["program"] @@ -95,10 +96,8 @@ class TemplateHighlighter(QSyntaxHighlighter): self.Formats[name] = format def find_paren(self, bn, pos): - for pp in self.paren_positions: - if pp.block == bn and pp.pos == pos: - return pp - return None + dex = bn * self.BN_FACTOR + pos + return self.paren_pos_map.get(dex, None) def highlightBlock(self, text): bn = self.currentBlock().blockNumber() @@ -127,24 +126,27 @@ class TemplateHighlighter(QSyntaxHighlighter): if self.generate_paren_positions: t = unicode(text) i = 0 - first = True + foundQuote = False while i < len(t): c = t[i] if c == ':': - if first and i+1 < len(t) and t[i+1] == "'": + # Deal with the funky syntax of template program mode. + # This won't work if there are more than one template + # expression in the document. + if not foundQuote and i+1 < len(t) and t[i+1] == "'": i += 2 elif c in ["'", '"']: - first = False + foundQuote = True i += 1 j = t[i:].find(c) if j < 0: i = len(t) else: i = i + j - elif c == '(': - self.paren_positions.append(ParenPosition(bn, i, '(')) - elif c == ')': - self.paren_positions.append(ParenPosition(bn, i, ')')) + elif c in ['(', ')']: + pp = ParenPosition(bn, i, c) + self.paren_positions.append(pp) + self.paren_pos_map[bn*self.BN_FACTOR+i] = pp i += 1 def rehighlight(self): @@ -186,6 +188,7 @@ class TemplateHighlighter(QSyntaxHighlighter): def regenerate_paren_positions(self): self.generate_paren_positions = True self.paren_positions = [] + self.paren_pos_map = {} self.rehighlight() self.generate_paren_positions = False