From 03a0124093df654d793a616b91a622d02fff2169 Mon Sep 17 00:00:00 2001 From: Olivier Keshavjee Date: Sat, 6 Jun 2015 16:21:08 +0200 Subject: [PATCH] Adds txt2tags highlighter, from an other project -- probably buggy --- makefile | 2 +- src/enums.py | 1 + src/ui/editors/__init__.py | 0 src/ui/editors/blockUserData.py | 68 +++ src/ui/editors/customTextEdit.py | 31 ++ src/ui/{ => editors}/editorWidget.py | 2 +- src/ui/{ => editors}/editorWidget_ui.py | 7 +- src/ui/{ => editors}/editorWidget_ui.ui | 11 +- src/ui/editors/t2tFunctions.py | 264 +++++++++++ src/ui/editors/t2tHighlighter.py | 553 ++++++++++++++++++++++++ src/ui/editors/t2tHighlighterStyle.py | 245 +++++++++++ src/ui/mainWindow.py | 10 +- src/ui/mainWindow.ui | 4 +- test_project/outline.xml | 8 +- 14 files changed, 1188 insertions(+), 18 deletions(-) create mode 100644 src/ui/editors/__init__.py create mode 100644 src/ui/editors/blockUserData.py create mode 100644 src/ui/editors/customTextEdit.py rename src/ui/{ => editors}/editorWidget.py (99%) rename src/ui/{ => editors}/editorWidget_ui.py (93%) rename src/ui/{ => editors}/editorWidget_ui.ui (86%) create mode 100644 src/ui/editors/t2tFunctions.py create mode 100644 src/ui/editors/t2tHighlighter.py create mode 100644 src/ui/editors/t2tHighlighterStyle.py diff --git a/makefile b/makefile index 1c82d523..18910059 100644 --- a/makefile +++ b/makefile @@ -1,4 +1,4 @@ -UI := $(wildcard src/ui/*.ui) $(wildcard src/ui/*.qrc) +UI := $(wildcard src/ui/*.ui) $(wildcard src/ui/*/*.ui) $(wildcard src/ui/*.qrc) UIs= $(UI:.ui=.py) $(UI:.qrc=_rc.py) diff --git a/src/enums.py b/src/enums.py index 40f5166b..42cdb299 100644 --- a/src/enums.py +++ b/src/enums.py @@ -41,3 +41,4 @@ class Outline(Enum): goalPercentage = 12 setGoal = 13 # The goal set by the user, if any. Can be different from goal which can be computed # (sum of all sub-items' goals) + textFormat = 14 diff --git a/src/ui/editors/__init__.py b/src/ui/editors/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/ui/editors/blockUserData.py b/src/ui/editors/blockUserData.py new file mode 100644 index 00000000..c25b1c92 --- /dev/null +++ b/src/ui/editors/blockUserData.py @@ -0,0 +1,68 @@ +#!/usr/bin/python +# -*- coding: utf8 -*- + +from qt import * + + +class blockUserData (QTextBlockUserData): + + @staticmethod + def getUserData(block): + "Returns userData if it exists, or a blank one." + data = block.userData() + if data is None: + data = blockUserData() + return data + + @staticmethod + def getUserState(block): + "Returns the block state." + state = block.userState() + while state >= 100: + state -= 100 # +100 means in a list + return state + + def __init__(self): + QTextBlockUserData.__init__(self) + self._listLevel = 0 + self._leadingSpaces = 0 + self._emptyLinesBefore = 0 + self._listSymbol = "" + + def isList(self): + return self._listLevel > 0 + + def listLevel(self): + return self._listLevel + + def setListLevel(self, level): + self._listLevel = level + + def listSymbol(self): + return self._listSymbol + + def setListSymbol(self, s): + self._listSymbol = s + + def leadingSpaces(self): + return self._leadingSpaces + + def setLeadingSpaces(self, n): + self._leadingSpaces = n + + def emptyLinesBefore(self): + return self._emptyLinesBefore + + def setEmptyLinesBefore(self, n): + self._emptyLinesBefore = n + + def text(self): + return str(self.listLevel()) + "|" + str(self.leadingSpaces()) + "|" + str(self.emptyLinesBefore()) + + def __eq__(self, b): + return self._listLevel == b._listLevel and \ + self._leadingSpaces == b._leadingSpaces and \ + self._emptyLinesBefore == b._emptyLinesBefore + + def __ne__(self, b): + return not self == b diff --git a/src/ui/editors/customTextEdit.py b/src/ui/editors/customTextEdit.py new file mode 100644 index 00000000..13b529ee --- /dev/null +++ b/src/ui/editors/customTextEdit.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +#--!-- coding: utf8 --!-- + +from __future__ import print_function +from __future__ import unicode_literals + +from qt import * +from enums import * +from ui.editors.t2tHighlighter import * +try: + import enchant +except ImportError: + enchant = None + +class customTextEdit(QTextEdit): + + def __init__(self, parent=None): + QTextEdit.__init__(self, parent) + + self.defaultFontPointSize = 9 + self.highlightWord = "" + self.highligtCS = False + + self.highlighter = t2tHighlighter(self) + + # Spellchecking + if enchant: + self.dict = enchant.Dict("fr_CH") + self.spellcheck = True + else: + self.spellcheck = False \ No newline at end of file diff --git a/src/ui/editorWidget.py b/src/ui/editors/editorWidget.py similarity index 99% rename from src/ui/editorWidget.py rename to src/ui/editors/editorWidget.py index 4ebafcd2..55defd02 100644 --- a/src/ui/editorWidget.py +++ b/src/ui/editors/editorWidget.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals from qt import * from enums import * -from ui.editorWidget_ui import * +from ui.editors.editorWidget_ui import * class GrowingTextEdit(QTextEdit): diff --git a/src/ui/editorWidget_ui.py b/src/ui/editors/editorWidget_ui.py similarity index 93% rename from src/ui/editorWidget_ui.py rename to src/ui/editors/editorWidget_ui.py index ca63579c..21c14ed3 100644 --- a/src/ui/editorWidget_ui.py +++ b/src/ui/editors/editorWidget_ui.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'src/ui/editorWidget_ui.ui' +# Form implementation generated from reading ui file 'src/ui/editors/editorWidget_ui.ui' # # Created by: PyQt5 UI code generator 5.4.1 # @@ -22,7 +22,7 @@ class Ui_editorWidget_ui(object): self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.scene) self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0) self.horizontalLayout_2.setObjectName("horizontalLayout_2") - self.txtRedacText = QtWidgets.QPlainTextEdit(self.scene) + self.txtRedacText = customTextEdit(self.scene) self.txtRedacText.setObjectName("txtRedacText") self.horizontalLayout_2.addWidget(self.txtRedacText) self.stack.addWidget(self.scene) @@ -45,10 +45,11 @@ class Ui_editorWidget_ui(object): self.horizontalLayout.addWidget(self.stack) self.retranslateUi(editorWidget_ui) - self.stack.setCurrentIndex(1) + self.stack.setCurrentIndex(0) QtCore.QMetaObject.connectSlotsByName(editorWidget_ui) def retranslateUi(self, editorWidget_ui): _translate = QtCore.QCoreApplication.translate editorWidget_ui.setWindowTitle(_translate("editorWidget_ui", "Form")) +from ui.editors.customTextEdit import customTextEdit diff --git a/src/ui/editorWidget_ui.ui b/src/ui/editors/editorWidget_ui.ui similarity index 86% rename from src/ui/editorWidget_ui.ui rename to src/ui/editors/editorWidget_ui.ui index 9528ceb9..ea72f10a 100644 --- a/src/ui/editorWidget_ui.ui +++ b/src/ui/editors/editorWidget_ui.ui @@ -20,7 +20,7 @@ - 1 + 0 @@ -28,7 +28,7 @@ 0 - + @@ -66,6 +66,13 @@ + + + customTextEdit + QTextEdit +
ui.editors.customTextEdit.h
+
+
diff --git a/src/ui/editors/t2tFunctions.py b/src/ui/editors/t2tFunctions.py new file mode 100644 index 00000000..262bc36f --- /dev/null +++ b/src/ui/editors/t2tFunctions.py @@ -0,0 +1,264 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from qt import * + + +def textToFormatArray(text): + """ + Take some text and returns an array of array containing informations + about how the text is formatted: + r = [ [0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1], + ... + ] + Each sub-array is for one of the beautifier: + 0: bold + 1: italic + 2: underline + 3: strike + 4: code + 5: tagged + + Each sub-array contains an element for each character of the text, with the + value 1 if it is formatted in the specific format, -1 if it is markup, and + 0 otherwise. + + removeMarks returns a both the array and a string, in which all of the + formatting marks have been removed. + """ + + result = [] + + for markup in ["\*", "/", "_", "-", "`", "\'"]: + + rList = [] + + r = QRegExp(r'(' + markup * 2 + ')(.+)(' + markup * 2 + ')') + r.setMinimal(True) + pos = r.indexIn(text, 0) + lastPos = 0 + while pos >= 0: + #We have a winner + rList += [0] * (pos - lastPos) + rList += [2] * 2 + rList += [1] * len(r.cap(2)) + rList += [2] * 2 + lastPos = pos + len(r.cap(0)) + pos = r.indexIn(text, len(rList)) + + if len(rList) < len(text): + rList += [0] * (len(text) - len(rList)) + + result.append(rList) + + return result + + +def textToFormatArrayNoMarkup(text): + """ + Same as textToFormatArray, except that it removes all the markup from the + text and returns two elements: + the array + the text without markup + """ + + r = textToFormatArray(text) + result = [[], [], [], [], [], []] + rText = "" + + for i in range(len(text)): + t = max([k[i] for k in r]) # kind of flattens all the format array + if t != 2: + rText += text[i] + [result[k].append(r[k][i]) for k in range(len(r))] + + return rText, result + + +def translateSelectionToUnformattedText(text, start, end): + """ + Translate the start / end of selection from a formatted text to an + unformatted one. + """ + r = textToFormatArray(text) + + rStart, rEnd = start, end + + for i in range(len(text)): + t = max([k[i] for k in r]) # kind of flattens all the format array + if t == 2: # t == 2 means this character is markup + if i <= start: rStart -= 1 + if i < end: rEnd -= 1 + + return rStart, rEnd + + +def translateSelectionToFormattedText(text, start, end): + """ + Translate the start / end of selection from a formatted text to an + unformatted one. + """ + r = textToFormatArray(text) + + rStart, rEnd = start, end + + for i in range(len(text)): + t = max([k[i] for k in r]) # kind of flattens all the format array + if t == 2: # t == 2 means this character is markup + if i <= start: rStart -= 1 + if i < end: rEnd -= 1 + + return rStart, rEnd + + +def printArray(array): + print("".join([str(j) for j in array])) + + +def printArrays(arrays): + for i in arrays: printArray(i) + + +def reformatText(text, markupArray): + """ + Takes a text without formatting markup, and an array generated by + textToFormatArray, and adds the propper markup. + """ + + rText = "" + markup = ["**", "//", "__", "--", "``", "''"] + + for k in range(len(markupArray)): + m = markupArray[k] + open = False # Are we in an openned markup + d = 0 + alreadySeen = [] + for i in range(len(text)): + insert = False + if not open and m[i] == 1: + insert = True + open = True + + if open and m[i] == 0: + insert = True + open = False + if open and m[i] > 1: + z = i + while m[z] == m[i]: z += 1 + if m[z] != 1 and not m[i] in alreadySeen: + insert = True + open = False + alreadySeen.append(m[i]) + if insert: + rText += markup[k] + for j in range(len(markupArray)): + # The other array still have the same length + if j > k: + #Insert 2 for bold, 3 for italic, etc. + markupArray[j].insert(i + d, k + 2) + markupArray[j].insert(i + d, k + 2) + alreadySeen = [] + d += 2 + rText += text[i] + if open: + rText += markup[k] + for j in range(len(markupArray)): + # The other array still have the same length + if j > k: + #Insert 2 for bold, 3 for italic, etc. + markupArray[j].insert(i + d, k + 2) + markupArray[j].insert(i + d, k + 2) + text = rText + rText = "" + + ## Clean up + # Exclude first and last space of the markup + for markup in ["\*", "/", "_", "-", "`", "\'"]: + r = QRegExp(r'(' + markup * 2 + ')(\s+)(.+)(' + markup * 2 + ')') + r.setMinimal(True) + text.replace(r, "\\2\\1\\3\\4") + r = QRegExp(r'(' + markup * 2 + ')(.+)(\s+)(' + markup * 2 + ')') + r.setMinimal(True) + text.replace(r, "\\1\\2\\4\\3") + + return text + + +def cleanFormat(text): + "Makes markup clean (removes doubles, etc.)" + t, a = textToFormatArrayNoMarkup(text) + return reformatText(t, a) + + +class State: + NORMAL = 0 + TITLE_1 = 1 + TITLE_2 = 2 + TITLE_3 = 3 + TITLE_4 = 4 + TITLE_5 = 5 + NUMBERED_TITLE_1 = 6 + NUMBERED_TITLE_2 = 7 + NUMBERED_TITLE_3 = 8 + NUMBERED_TITLE_4 = 9 + NUMBERED_TITLE_5 = 10 + TITLES = [TITLE_1, TITLE_2, TITLE_3, TITLE_4, TITLE_5, NUMBERED_TITLE_1, + NUMBERED_TITLE_2, NUMBERED_TITLE_3, NUMBERED_TITLE_4, + NUMBERED_TITLE_5] + # AREA + COMMENT_AREA = 11 + CODE_AREA = 12 + RAW_AREA = 13 + TAGGED_AREA = 14 + # AREA MARKUP + COMMENT_AREA_BEGINS = 15 + COMMENT_AREA_ENDS = 16 + CODE_AREA_BEGINS = 17 + CODE_AREA_ENDS = 18 + RAW_AREA_BEGINS = 19 + RAW_AREA_ENDS = 20 + TAGGED_AREA_BEGINS = 21 + TAGGED_AREA_ENDS = 22 + #LINE + COMMENT_LINE = 30 + CODE_LINE = 31 + RAW_LINE = 32 + TAGGED_LINE = 33 + SETTINGS_LINE = 34 + BLOCKQUOTE_LINE = 35 + HORIZONTAL_LINE = 36 + HEADER_LINE = 37 + # LIST + LIST_BEGINS = 40 + LIST_ENDS = 41 + LIST_EMPTY = 42 + LIST_BULLET = 43 + LIST_BULLET_ENDS = 44 + LIST = [40, 41, 42] + range(100, 201) + # TABLE + TABLE_LINE = 50 + TABLE_HEADER = 51 + #OTHER + MARKUP = 60 + LINKS = 61 + MACRO = 62 + DEFAULT = 63 + + @staticmethod + def titleLevel(state): + """ + Returns the level of the title, from the block state. + """ + return { + State.TITLE_1: 1, + State.TITLE_2: 2, + State.TITLE_3: 3, + State.TITLE_4: 4, + State.TITLE_5: 5, + State.NUMBERED_TITLE_1: 1, + State.NUMBERED_TITLE_2: 2, + State.NUMBERED_TITLE_3: 3, + State.NUMBERED_TITLE_4: 4, + State.NUMBERED_TITLE_5: 5, + }.get(state, -1) \ No newline at end of file diff --git a/src/ui/editors/t2tHighlighter.py b/src/ui/editors/t2tHighlighter.py new file mode 100644 index 00000000..50471677 --- /dev/null +++ b/src/ui/editors/t2tHighlighter.py @@ -0,0 +1,553 @@ +#!/usr/bin/python +# -*- coding: utf8 -*- + +from qt import * +from ui.editors.t2tFunctions import * +from ui.editors.blockUserData import blockUserData +from ui.editors.t2tHighlighterStyle import t2tHighlighterStyle +import re + +# This is aiming at implementing every rule from www.txt2tags.org/rules.html +# But we're not there yet. + +#FIXME: macro words not hilighted properly if at the begining of a line. + +#TODO: parse %!postproc et !%preproc, et si la ligne se termine par une couleur en commentaire (%#FF00FF), utiliser cette couleur pour highlighter. Permet des règles customisées par document, facilement. + + +class t2tHighlighter (QSyntaxHighlighter): + """Syntax highlighter for the Txt2Tags language. + """ + + def __init__(self, editor, style="Default"): + QSyntaxHighlighter.__init__(self, editor.document()) + + self.editor = editor + + # Stupid variable that fixes the loss of QTextBlockUserData. + self.thisDocument = editor.document() + self.style = t2tHighlighterStyle(self.editor, style) + + self.inDocRules = [] + + rules = [ + (r'^\s*[-=_]{20,}\s*$', State.HORIZONTAL_LINE), + (r'^\s*(\+{1})([^\+].*[^\+])(\+{1})(\[[A-Za-z0-9_-]*\])?\s*$', State.NUMBERED_TITLE_1), + (r'^\s*(\+{2})([^\+].*[^\+])(\+{2})(\[[A-Za-z0-9_-]*\])?\s*$', State.NUMBERED_TITLE_2), + (r'^\s*(\+{3})([^\+].*[^\+])(\+{3})(\[[A-Za-z0-9_-]*\])?\s*$', State.NUMBERED_TITLE_3), + (r'^\s*(\+{4})([^\+].*[^\+])(\+{4})(\[[A-Za-z0-9_-]*\])?\s*$', State.NUMBERED_TITLE_4), + (r'^\s*(\+{5})([^\+].*[^\+])(\+{5})(\[[A-Za-z0-9_-]*\])?\s*$', State.NUMBERED_TITLE_5), + (r'^\s*(={1})([^=].*[^=])(={1})(\[[A-Za-z0-9_-]*\])?\s*$', State.TITLE_1), + (r'^\s*(={2})([^=].*[^=])(={2})(\[[A-Za-z0-9_-]*\])?\s*$', State.TITLE_2), + (r'^\s*(={3})([^=].*[^=])(={3})(\[[A-Za-z0-9_-]*\])?\s*$', State.TITLE_3), + (r'^\s*(={4})([^=].*[^=])(={4})(\[[A-Za-z0-9_-]*\])?\s*$', State.TITLE_4), + (r'^\s*(={5})([^=].*[^=])(={5})(\[[A-Za-z0-9_-]*\])?\s*$', State.TITLE_5), + (r'^%!.*$', State.SETTINGS_LINE), + (r'^%[^!]?.*$', State.COMMENT_LINE), + (r'^\t.+$', State.BLOCKQUOTE_LINE), + (r'^(```)(.+)$', State.CODE_LINE), + (r'^(""")(.+)$', State.RAW_LINE), + (r'^(\'\'\')(.+)$', State.TAGGED_LINE), + (r'^\s*[-+:] [^ ].*$', State.LIST_BEGINS), + (r'^\s*[-+:]\s*$', State.LIST_ENDS), + (r'^ *\|\| .*$', State.TABLE_HEADER), + (r'^ *\| .*$', State.TABLE_LINE) + ] + + # Generate rules to identify blocks + State.Rules = [(QRegExp(pattern), state) + for (pattern, state) in rules] + State.Recursion = 0 + + def highlightBlock(self, text): + """Apply syntax highlighting to the given block of text. + """ + + # Check if syntax highlighting is enabled + if self.style is None: + default = QTextBlockFormat() + QTextCursor(self.currentBlock()).setBlockFormat(default) + return + + block = self.currentBlock() + oldState = blockUserData.getUserState(block) + self.identifyBlock(block) + # formatBlock prevent undo/redo from working + # TODO: find a todo/undo compatible way of formatting block + #self.formatBlock(block) + + state = blockUserData.getUserState(block) + data = blockUserData.getUserData(block) + inList = self.isList(block) + + op = self.style.format(State.MARKUP) + + self.setFormat(0, len(text), self.style.format(State.DEFAULT)) + + # InDocRules: is it a settings which might have a specific rule, + # a comment which contains color infos, or a include conf? + # r'^%!p[or][se]t?proc[^\s]*\s*:\s*\'(.*)\'\s*\'.*\'' + rlist = [QRegExp(r'^%!p[or][se]t?proc[^\s]*\s*:\s*((\'[^\']*\'|\"[^\"]*\")\s*(\'[^\']*\'|\"[^\"]*\"))'), # pre/postproc + QRegExp(r'^%.*\s\((.*)\)'), # comment + QRegExp(r'^%!includeconf:\s*([^\s]*)\s*')] # includeconf + for r in rlist: + if r.indexIn(text) != -1: + self.parseInDocRules() + + # Format the whole line: + for lineState in [ + State.BLOCKQUOTE_LINE, + State.HORIZONTAL_LINE, + State.HEADER_LINE, + ]: + if not inList and state == lineState: + self.setFormat(0, len(text), self.style.format(lineState)) + + for (lineState, marker) in [ + (State.COMMENT_LINE, "%"), + (State.CODE_LINE, "```"), + (State.RAW_LINE, "\"\"\""), + (State.TAGGED_LINE, "'''"), + (State.SETTINGS_LINE, "%!") + ]: + if state == lineState and \ + not (inList and state == State.SETTINGS_LINE): + n = 0 + # If it's a comment, we want to highlight all '%'. + if state == State.COMMENT_LINE: + while text[n:n + 1] == "%": + n += 1 + n -= 1 + + # Apply Format + self.setFormat(0, len(marker) + n, op) + self.setFormat(len(marker) + n, + len(text) - len(marker) - n, + self.style.format(lineState)) + + # If it's a setting, we might do something + if state == State.SETTINGS_LINE: + # Target + r = QRegExp(r'^%!([^\s]+)\s*:\s*(\b\w*\b)$') + if r.indexIn(text) != -1: + setting = r.cap(1) + val = r.cap(2) + if setting == "target" and \ + val in self.editor.main.targetsNames: + self.editor.fileWidget.preview.setPreferredTarget(val) + + # Pre/postproc + r = QRegExp(r'^%!p[or][se]t?proc[^\s]*\s*:\s*((\'[^\']*\'|\"[^\"]*\")\s*(\'[^\']*\'|\"[^\"]*\"))') + if r.indexIn(text) != -1: + p = r.pos(1) + length = len(r.cap(1)) + self.setFormat(p, length, self.style.makeFormat(base=self.format(p), + fixedPitch=True)) + + # Tables + for lineState in [State.TABLE_LINE, State.TABLE_HEADER]: + if state == lineState: + for i in range(len(text)): + if text[i] == "|": + self.setFormat(i, 1, op) + else: + self.setFormat(i, 1, self.style.format(lineState)) + + # Lists + #if text == " p": print(data.isList()) + if data.isList(): + r = QRegExp(r'^\s*[\+\-\:]? ?') + r.indexIn(text) + self.setFormat(0, r.matchedLength(), self.style.format(State.LIST_BULLET)) + #if state == State.LIST_BEGINS: + #r = QRegExp(r'^\s*[+-:] ') + #r.indexIn(text) + #self.setFormat(0, r.matchedLength(), self.style.format(State.LIST_BULLET)) + + if state == State.LIST_ENDS: + self.setFormat(0, len(text), self.style.format(State.LIST_BULLET_ENDS)) + + # Titles + if not inList and state in State.TITLES: + r = [i for (i, s) in State.Rules if s == state][0] + pos = r.indexIn(text) + if pos >= 0: + f = self.style.format(state) + # Uncomment for markup to be same size as title + #op = self.formats(preset="markup", + #base=self.formats(preset=state)) + self.setFormat(r.pos(2), r.cap(2).length(), f) + self.setFormat(r.pos(1), r.cap(1).length(), op) + self.setFormat(r.pos(3), r.cap(3).length(), op) + + # Areas: comment, code, raw tagged + for (begins, middle, ends) in [ + (State.COMMENT_AREA_BEGINS, State.COMMENT_AREA, State.COMMENT_AREA_ENDS), + (State.CODE_AREA_BEGINS, State.CODE_AREA, State.CODE_AREA_ENDS), + (State.RAW_AREA_BEGINS, State.RAW_AREA, State.RAW_AREA_ENDS), + (State.TAGGED_AREA_BEGINS, State.TAGGED_AREA, State.TAGGED_AREA_ENDS), + ]: + + if state == middle: + self.setFormat(0, len(text), self.style.format(middle)) + elif state in [begins, ends]: + self.setFormat(0, len(text), op) + + # Inline formatting + if state not in [ + #State.COMMENT_AREA, + #State.COMMENT_LINE, + State.RAW_AREA, + State.RAW_LINE, + State.CODE_AREA, + State.CODE_LINE, + State.TAGGED_AREA, + State.TAGGED_LINE, + State.SETTINGS_LINE, + State.HORIZONTAL_LINE, + ] and state not in State.TITLES: + formatArray = textToFormatArray(text) + + # InDocRules + for (r, c) in self.inDocRules: + i = re.finditer(r.decode('utf8'), text, re.UNICODE) + for m in i: + f = self.format(m.start()) + l = m.end() - m.start() + if "," in c: + c1, c2 = c.split(",") + self.setFormat(m.start(), l, + self.style.makeFormat(color=c1, bgcolor=c2, base=f)) + else: + self.setFormat(m.start(), l, + self.style.makeFormat(color=c, base=f)) + + # Links + if state not in [State.COMMENT_LINE, State.COMMENT_AREA]: + r = QRegExp(r'\[(\[[^\]]*\])?[^\]]*\s*([^\s]+)\]') + r.setMinimal(False) + pos = r.indexIn(text) + links = [] + while pos >= 0: + #TODO: The text should not be formatted if [**not bold**] + #if max([k[pos] for k in formatArray]) == 0 or 1 == 1: + self.setFormat(pos, 1, + self.style.format(State.MARKUP)) + self.setFormat(pos + 1, r.cap(0).length() - 1, + self.style.format(State.LINKS)) + self.setFormat(pos + r.cap(0).length() - 1, 1, + self.style.format(State.MARKUP)) + if r.pos(2) > 0: + _f = QTextCharFormat(self.style.format(State.LINKS)) + _f.setForeground(QBrush(_f.foreground() + .color().lighter())) + _f.setFontUnderline(True) + self.setFormat(r.pos(2), r.cap(2).length(), _f) + + links.append([pos, r.cap(0).length()]) # To remember for the next highlighter (single links) + pos = r.indexIn(text, pos + 1) + + # Links like www.theologeek.ch, http://www.fsf.org, ... + # FIXME: - "http://adresse et http://adresse" is detected also as italic + # - some error, like "http://adress.htm." also color the final "." + # - also: adresse@email.com, ftp://, www2, www3, etc. + # - But for now, does the job + r = QRegExp(r'http://[^\s]*|www\.[a-zA-Z0-9-_]+\.[a-zA-Z0-9-_]+[^\s]*') + #r.setMinimal(True) + pos = r.indexIn(text) + while pos >= 0: + for k in links: + #print pos, k[0], k[1] + if pos > k[0] and pos < k[0] + k[1]: # already highlighted + break + else: + self.setFormat(pos, r.cap(0).length(), self.style.format(State.LINKS)) + + pos = r.indexIn(text, pos + 1) + + # Bold, Italic, Underline, Code, Tagged, Strikeout + for i in range(len(text)): + f = self.format(i) + beautifiers = [k[i] for k in formatArray] + self.setFormat(i, 1, self.style.beautifyFormat(f, beautifiers)) + + # Macro words + for r in [r'(%%)\b\w+\b', r'(%%)\b\w+\b\(.+\)']: + r = QRegExp(r) + r.setMinimal(True) + pos = r.indexIn(text) + while pos >= 0: + if max([k[pos] for k in formatArray]) == 0: + self.setFormat(pos, r.cap(0).length(), + self.style.format(State.MACRO)) + pos = r.indexIn(text, pos + 1) + + # Highlighted word (for search) + if self.editor.highlightWord: + if self.editor.highligtCS and self.editor.highlightWord in text or \ + not self.editor.highlightCs and self.editor.highlightWord.lower() in text.lower(): + #if self.editor.highlightCS: + #s = self.editor.highlightWord + #else: + #s = self.editor.highlightWord.toLower() + #print(s) + p = text.indexOf(self.editor.highlightWord, cs=self.editor.highlightCS) + while p >= 0: + self.setFormat(p, len(self.editor.highlightWord), + self.style.makeFormat(preset="higlighted", base=self.format(p))) + p = text.indexOf(self.editor.highlightWord, p + 1, cs=self.editor.highlightCS) + + + ### Highlight Selection + ### TODO: way to slow, find another way. + ##sel = self.editor.textCursor().selectedText() + ##if len(sel) > 5: self.keywordRules.append((QRegExp(sel), "selected")) + + ## Do keyword formatting + #for expression, style in self.keywordRules: + #expression.setMinimal( True ) + #index = expression.indexIn(text, 0) + + ## There might be more than one on the same line + #while index >= 0: + #length = expression.cap(0).length() + #f = self.formats(preset=style, base=self.formats(index)) + #self.setFormat(index, length, f) + #index = expression.indexIn(text, index + length) + + # Spell checking + # Based on http://john.nachtimwald.com/2009/08/22/qplaintextedit-with-in-line-spell-check/ + WORDS = u'(?iu)[\w\']+' + if state not in [State.SETTINGS_LINE]: + if self.editor.spellcheck: + for word_object in re.finditer(WORDS, text): + if not self.editor.dict.check(word_object.group()): + format = self.format(word_object.start()) + format.setUnderlineColor(Qt.red) + format.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline) + self.setFormat(word_object.start(), + word_object.end() - word_object.start(), format) + + # If a title was changed, we emit the corresponding signal + if oldState in State.TITLES or \ + self.currentBlockState() in State.TITLES: + self.editor.structureChanged.emit() + #FIXME: si du texte est supprimé et qu'il y a un titre dedans, + # cela n'est pas détecté et le signal pas émis. + + def identifyBlock(self, block): + """Identifies what block type it is, and set userState and userData + accordingly.""" + + text = block.text() + data = blockUserData.getUserData(block) + + # Header Lines + if block.blockNumber() == 0: + block.setUserState(State.HEADER_LINE) + return + elif block.blockNumber() in [1, 2] and \ + self.document().findBlockByNumber(0).text(): + block.setUserState(State.HEADER_LINE) + return + + state = 0 + inList = False + blankLinesBefore = 0 + + #if text.contains(QRegExp(r'^\s*[-+:] [^ ].*[^-+]{1}\s*$')): + if QRegExp(r'^\s*[-+:] [^ ].*[^-+]{1}\s*$').indexIn(text) <> -1: + state = State.LIST_BEGINS + + # List stuff + if self.isList(block.previous()) or state == State.LIST_BEGINS: + inList = True + + # listLevel and leadingSpaces + #FIXME: not behaving exactly correctly... + lastData = blockUserData.getUserData(block.previous()) + if state == State.LIST_BEGINS: + leadingSpaces = QRegExp(r'[-+:]').indexIn(text, 0) + data.setLeadingSpaces(leadingSpaces) + + data.setListSymbol(text[leadingSpaces]) + if self.isList(block.previous()): + # The last block was also a list. + # We need to check if this is the same level, or a sublist + if leadingSpaces > lastData.leadingSpaces(): + # This is a sublevel list + data.setListLevel(lastData.listLevel() + 1) + else: + # This is same level + data.setListLevel(lastData.listLevel()) + else: + data.setListLevel(1) + else: + data.setListLevel(lastData.listLevel()) + data.setLeadingSpaces(lastData.leadingSpaces()) + data.setListSymbol(lastData.listSymbol()) + + # Blank lines before (two = end of list) + blankLinesBefore = self.getBlankLines(block.previous()) + if not QRegExp(r'^\s*$').indexIn(block.previous().text()) <> -1 and \ + not blockUserData.getUserState(block.previous()) in [State.COMMENT_LINE, + State.COMMENT_AREA, State.COMMENT_AREA_BEGINS, + State.COMMENT_AREA_ENDS]: + blankLinesBefore = 0 + elif not blockUserData.getUserState(block.previous()) in \ + [State.COMMENT_LINE, State.COMMENT_AREA, + State.COMMENT_AREA_BEGINS, State.COMMENT_AREA_ENDS]: + blankLinesBefore += 1 + if blankLinesBefore == 2: + # End of list. + blankLinesBefore = 0 + inList = False + if inList and QRegExp(r'^\s*$').indexIn(text) <> -1: + state = State.LIST_EMPTY + + # Areas + for (begins, middle, ends, marker) in [ + (State.COMMENT_AREA_BEGINS, State.COMMENT_AREA, State.COMMENT_AREA_ENDS, "^%%%\s*$"), + (State.CODE_AREA_BEGINS, State.CODE_AREA, State.CODE_AREA_ENDS, "^```\s*$"), + (State.RAW_AREA_BEGINS, State.RAW_AREA, State.RAW_AREA_ENDS, "^\"\"\"\s*$"), + (State.TAGGED_AREA_BEGINS, State.TAGGED_AREA, State.TAGGED_AREA_ENDS, '^\'\'\'\s*$'), + ]: + + if QRegExp(marker).indexIn(text) <> -1: + if blockUserData.getUserState(block.previous()) in [begins, middle]: + state = ends + break + else: + state = begins + break + if blockUserData.getUserState(block.previous()) in [middle, begins]: + state = middle + break + + # Patterns (for lines) + if not state: + for (pattern, lineState) in State.Rules: + pos = pattern.indexIn(text) + if pos >= 0: + state = lineState + break + + if state in [State.BLOCKQUOTE_LINE, State.LIST_ENDS]: + #FIXME: doesn't work exactly. Closes only the current level, not + #FIXME: the whole list. + inList = False + + if inList and not state == State.LIST_BEGINS: + state += 100 + if blankLinesBefore: + state += 100 + + block.setUserState(state) + block.setUserData(data) + + def formatBlock(self, block): + """ + Formats the block according to its state. + """ + #TODO: Use QTextDocument format presets, and QTextBlock's + #TODO: blockFormatIndex. And move that in t2tHighlighterStyle. + state = block.userState() + blockFormat = QTextBlockFormat() + + if state in [State.BLOCKQUOTE_LINE, + State.HEADER_LINE] + State.LIST: + blockFormat = self.style.formatBlock(block, state) + + QTextCursor(block).setBlockFormat(blockFormat) + + def getBlankLines(self, block): + "Returns if there is a blank line before in the list." + state = block.userState() + if state >= 200: + return 1 + else: + return 0 + + def isList(self, block): + "Returns TRUE if the block is in a list." + if block.userState() == State.LIST_BEGINS or\ + block.userState() >= 100: + return True + + def setStyle(self, style): + if style in t2tHighlighterStyle.validStyles: + self.style = t2tHighlighterStyle(self.editor, style) + else: + self.style = None + self.rehighlight() + + def setFontPointSize(self, size): + self.defaultFontPointSize = size + self.style = t2tHighlighterStyle(self.editor, self.style.name) + self.rehighlight() + + def parseInDocRules(self): + oldRules = self.inDocRules + self.inDocRules = [] + + t = self.thisDocument.toPlainText() + + # Get all conf files + confs = [] + lines = t.split("\n") + for l in lines: + r = QRegExp(r'^%!includeconf:\s*([^\s]*)\s*') + if r.indexIn(l) != -1: + confs.append(r.cap(1)) + + # Try to load conf files + for c in confs: + try: + import codecs + f = self.editor.fileWidget.file + d = QDir.cleanPath(QFileInfo(f).absoluteDir().absolutePath()+"/"+c) + file = codecs.open(d, 'r', "utf-8") + except: + print("Error: cannot open {}.".format(c)) + continue + # We add the content to the current lines of the current document + lines += file.readlines() #lines.extend(file.readlines()) + + #b = self.thisDocument.firstBlock() + lastColor = "" + + #while b.isValid(): + for l in lines: + text = l #b.text() + r = QRegExp(ur'^%!p[or][se]t?proc[^\s]*\s*:\s*(\'[^\']*\'|\"[^\"]*\")\s*(\'[^\']*\'|\"[^\"]*\")') + if r.indexIn(text) != -1: + rule = r.cap(1)[1:-1] + # Check if there was a color-comment above that post/preproc bloc + if lastColor: + self.inDocRules.append((str(rule), lastColor)) + # Check if previous block is a comment like it should + else: + previousText = lines[lines.indexOf(l)-1] #b.previous().text() + r = QRegExp(r'^%.*\s\((.*)\)') + if r.indexIn(previousText) != -1: + lastColor = r.cap(1) + self.inDocRules.append((str(rule), lastColor)) + else: + lastColor = "" + #b = b.next() + + if oldRules != self.inDocRules: + #Rules have changed, we need to rehighlight + #print("Rules have changed.", len(self.inDocRules)) + #self.rehighlight() # Doesn't work (seg fault), why? + pass + #b = self.thisDocument.firstBlock() + #while b.isValid(): + #for (r, c) in self.inDocRules: + #r = QRegExp(r) + #pos = r.indexIn(b.text()) + #if pos >= 0: + #print("rehighlighting:", b.text()) + #self.rehighlightBlock(b) + #break + #b = b.next() diff --git a/src/ui/editors/t2tHighlighterStyle.py b/src/ui/editors/t2tHighlighterStyle.py new file mode 100644 index 00000000..3b3c4eab --- /dev/null +++ b/src/ui/editors/t2tHighlighterStyle.py @@ -0,0 +1,245 @@ +#!/usr/bin/python +# -*- coding: utf8 -*- + +from qt import * +from ui.editors.t2tFunctions import * +from ui.editors.blockUserData import blockUserData + +#TODO: creates a general way to generate styles (and edit/import/export) + + +class t2tHighlighterStyle (): + """Style for the Syntax highlighter for the Txt2Tags language. + """ + + validStyles = ["Default", "Monospace"] + + def __init__(self, editor, name="Default"): + + self.editor = editor + self.name = name + + # Defaults + self.defaultFontPointSize = self.editor.defaultFontPointSize + self.defaultFontFamily = "" + self.tabStopWidth = 40 + + self.setupEditor() + + if self.name == "Default": + self.initDefaults() + #Temporary other theme + elif self.name == "Monospace": + self.defaultFontFamily = "Monospace" + self.initDefaults() + for i in self.styles: + f = self.styles[i] + f.setFontFixedPitch(True) + f.setFontFamily(self.defaultFontFamily) + f.setFontPointSize(self.defaultFontPointSize) + self.styles[i] = f + + def setupEditor(self): + self.editor.setTabStopWidth(self.tabStopWidth) + + def initDefaults(self): + self.styles = {} + for i in [State.CODE_AREA, + State.CODE_LINE, + State.COMMENT_AREA, + State.COMMENT_LINE, + State.SETTINGS_LINE, + State.BLOCKQUOTE_LINE, + State.RAW_AREA, + State.RAW_LINE, + State.TAGGED_AREA, + State.TAGGED_LINE, + State.TITLE_1, + State.TITLE_2, + State.TITLE_3, + State.TITLE_4, + State.TITLE_5, + State.NUMBERED_TITLE_1, + State.NUMBERED_TITLE_2, + State.NUMBERED_TITLE_3, + State.NUMBERED_TITLE_4, + State.NUMBERED_TITLE_5, + State.TABLE_HEADER, + State.TABLE_LINE, + State.HORIZONTAL_LINE, + State.MARKUP, + State.LIST_BULLET, + State.LIST_BULLET_ENDS, + State.LINKS, + State.MACRO, + State.DEFAULT, + State.HEADER_LINE]: + self.styles[i] = self.makeFormat(preset=i) + + def format(self, state): + return self.styles[state] + + def beautifyFormat(self, base, beautifiers): + "Apply beautifiers given in beautifiers array to format" + if max(beautifiers) == 2: + return self.makeFormat(preset=State.MARKUP, base=base) + else: + if beautifiers[0]: # bold + base.setFontWeight(QFont.Bold) + if beautifiers[1]: # italic + base.setFontItalic(True) + if beautifiers[2]: # underline + base.setFontUnderline(True) + if beautifiers[3]: # strikeout + base.setFontStrikeOut(True) + if beautifiers[4]: # code + base = self.makeFormat(base=base, preset=State.CODE_LINE) + if beautifiers[5]: # tagged + base = self.makeFormat(base=base, preset=State.TAGGED_LINE) + return base + + def formatBlock(self, block, state): + "Apply transformation to given block." + blockFormat = QTextBlockFormat() + + if state == State.BLOCKQUOTE_LINE: + # Number of tabs + n = block.text().indexOf(QRegExp(r'[^\t]'), 0) + blockFormat.setIndent(0) + blockFormat.setTextIndent(-self.tabStopWidth * n) + blockFormat.setLeftMargin(self.tabStopWidth * n) + #blockFormat.setRightMargin(self.editor.contentsRect().width() + # - self.editor.lineNumberAreaWidth() + # - fm.width("X") * self.editor.LimitLine + #+ self.editor.tabStopWidth()) + blockFormat.setAlignment(Qt.AlignJustify) + if self.name == "Default" : + blockFormat.setTopMargin(5) + blockFormat.setBottomMargin(5) + elif state == State.HEADER_LINE: + blockFormat.setBackground(QColor("#EEEEEE")) + elif state in State.LIST: + data = blockUserData.getUserData(block) + if str(data.listSymbol()) in "+-": + blockFormat.setBackground(QColor("#EEFFEE")) + else: + blockFormat.setBackground(QColor("#EEEEFA")) + n = blockUserData.getUserData(block).leadingSpaces() + 1 + f = QFontMetrics(QFont(self.defaultFontFamily, + self.defaultFontPointSize)) + fm = f.width(" " * n + + blockUserData.getUserData(block).listSymbol()) + blockFormat.setTextIndent(-fm) + blockFormat.setLeftMargin(fm) + if blockUserData.getUserState(block) == State.LIST_BEGINS and\ + self.name == "Default": + blockFormat.setTopMargin(5) + return blockFormat + + def makeFormat(self, color='', style='', size='', base='', fixedPitch='', + preset='', title_level='', bgcolor=''): + """ + Returns a QTextCharFormat with the given attributes, using presets. + """ + + _color = QColor() + _format = QTextCharFormat() + size = self.defaultFontPointSize + _format.setFontFamily(self.defaultFontFamily) + + # Base + if base: _format = base + + # Presets + if preset in [State.CODE_AREA, State.CODE_LINE, "code"]: + style = "bold" + color = "black" + fixedPitch = True + _format.setBackground(QColor("#EEEEEE")) + + if preset in [State.COMMENT_AREA, State.COMMENT_LINE, "comment"]: + style = "italic" + color = "darkGreen" + + if preset in [State.SETTINGS_LINE, "setting", State.MACRO]: + #style = "italic" + color = "magenta" + + if preset in [State.BLOCKQUOTE_LINE]: + color = "red" + + if preset in [State.HEADER_LINE]: + size = size * 2 + print size + + if preset in [State.RAW_AREA, State.RAW_LINE, "raw"]: + color = "blue" + + if preset in [State.TAGGED_AREA, State.TAGGED_LINE, "tagged"]: + color = "purple" + + if preset in State.TITLES: + style = "bold" + color = "darkRed" if State.titleLevel(preset) % 2 == 1 else "blue" + size = (self.defaultFontPointSize + + 11 - 2 * State.titleLevel(preset)) + + if preset == State.TABLE_HEADER: + style = "bold" + color = "darkMagenta" + + if preset == State.TABLE_LINE: + color = "darkMagenta" + + if preset == State.LIST_BULLET: + color = "red" + style = "bold" + fixedPitch = True + + if preset == State.LIST_BULLET_ENDS: + color = "darkGray" + fixedPitch = True + + if preset in [State.MARKUP, "markup"]: + color = "darkGray" + + if preset in [State.HORIZONTAL_LINE]: + color = "cyan" + fixedPitch = True + + if preset == State.LINKS: + color="blue" + #style="underline" + + if preset == "selected": + _format.setBackground(QColor("yellow")) + + if preset == "higlighted": + bgcolor = "yellow" + + if preset == State.DEFAULT: + size = self.defaultFontPointSize + _format.setFontFamily(self.defaultFontFamily) + + # Manual formatting + if color: + _color.setNamedColor(color) + _format.setForeground(_color) + if bgcolor: + _color.setNamedColor(bgcolor) + _format.setBackground(_color) + + if 'bold' in style: + _format.setFontWeight(QFont.Bold) + if 'italic' in style: + _format.setFontItalic(True) + if 'strike' in style: + _format.setFontStrikeOut(True) + if 'underline' in style: + _format.setFontUnderline(True) + if size: + _format.setFontPointSize(size) + if fixedPitch: + _format.setFontFixedPitch(True) + + return _format diff --git a/src/ui/mainWindow.py b/src/ui/mainWindow.py index 8df07adf..34460344 100644 --- a/src/ui/mainWindow.py +++ b/src/ui/mainWindow.py @@ -1103,7 +1103,7 @@ class Ui_MainWindow(object): self.menubar.addAction(self.menu_Aide.menuAction()) self.retranslateUi(MainWindow) - self.tabMain.setCurrentIndex(5) + self.tabMain.setCurrentIndex(6) self.tabSummary.setCurrentIndex(0) self.tabPersos.setCurrentIndex(0) self.tabPlot.setCurrentIndex(0) @@ -1273,9 +1273,9 @@ class Ui_MainWindow(object): self.actShowHelp.setText(_translate("MainWindow", "Afficher les &bulles d\'aide")) self.actShowHelp.setShortcut(_translate("MainWindow", "Ctrl+Shift+B")) -from ui.collapsibleGroupBox2 import collapsibleGroupBox2 -from ui.cmbOutlinePersoChoser import cmbOutlinePersoChoser -from ui.sldImportance import sldImportance from ui.cmbOutlineStatusChoser import cmbOutlineStatusChoser +from ui.cmbOutlinePersoChoser import cmbOutlinePersoChoser +from ui.collapsibleGroupBox2 import collapsibleGroupBox2 from ui.chkOutlineCompile import chkOutlineCompile -from ui.editorWidget import editorWidget +from ui.sldImportance import sldImportance +from ui.editors.editorWidget import editorWidget diff --git a/src/ui/mainWindow.ui b/src/ui/mainWindow.ui index 6f888868..b328e988 100644 --- a/src/ui/mainWindow.ui +++ b/src/ui/mainWindow.ui @@ -18,7 +18,7 @@ - 5 + 6 true @@ -2185,7 +2185,7 @@ editorWidget QWidget -
ui.editorWidget.h
+
ui.editors.editorWidget.h
1
diff --git a/test_project/outline.xml b/test_project/outline.xml index 9eb5373a..fb53a2da 100644 --- a/test_project/outline.xml +++ b/test_project/outline.xml @@ -1,9 +1,9 @@ - + - + @@ -14,12 +14,12 @@ - + - +