diff --git a/manuskript/exporter/manuskript/markdown.py b/manuskript/exporter/manuskript/markdown.py
index c706650f..f338cb2d 100644
--- a/manuskript/exporter/manuskript/markdown.py
+++ b/manuskript/exporter/manuskript/markdown.py
@@ -5,7 +5,7 @@ from PyQt5.QtWidgets import QPlainTextEdit, QGroupBox, qApp, QVBoxLayout, QCheck
from manuskript.exporter.manuskript.plainText import plainText
from manuskript.functions import mainWindow
-from manuskript.ui.editors.MMDHighlighter import MMDHighlighter
+from manuskript.ui.highlighters import MMDHighlighter
from manuskript.ui.exporters.manuskript.plainTextSettings import exporterSettings
@@ -72,4 +72,4 @@ class markdownSettings(exporterSettings):
self.settings = exporterSettings.getSettings(self)
self.settings["Preview"]["MarkdownHighlighter"] = self.chkMarkdownHighlighter.isChecked()
- return self.settings
\ No newline at end of file
+ return self.settings
diff --git a/manuskript/functions.py b/manuskript/functions.py
index 0be46436..0b3451e6 100644
--- a/manuskript/functions.py
+++ b/manuskript/functions.py
@@ -6,7 +6,7 @@ import re
from random import *
from PyQt5.QtCore import Qt, QRect, QStandardPaths, QObject, QRegExp, QDir
-from PyQt5.QtCore import QUrl
+from PyQt5.QtCore import QUrl, QTimer
from PyQt5.QtGui import QBrush, QIcon, QPainter, QColor, QImage, QPixmap
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtWidgets import qApp, QTextEdit
@@ -357,7 +357,9 @@ def statusMessage(message, duration=5000):
"""
Shows a message in MainWindow's status bar.
"""
+ mainWindow().statusBar().show()
mainWindow().statusBar().showMessage(message, duration)
+ QTimer.singleShot(duration, mainWindow().statusBar().hide)
def openURL(url):
"""
diff --git a/manuskript/mainWindow.py b/manuskript/mainWindow.py
index 1615c8d4..a3e12559 100644
--- a/manuskript/mainWindow.py
+++ b/manuskript/mainWindow.py
@@ -492,9 +492,8 @@ class MainWindow(QMainWindow, Ui_MainWindow):
It assumes that the datas have been populated in a different way."""
if loadFromFile and not os.path.exists(project):
print(self.tr("The file {} does not exist. Try again.").format(project))
- self.statusBar().showMessage(
- self.tr("The file {} does not exist. Try again.").format(project),
- 5000)
+ F.statusMessage(
+ self.tr("The file {} does not exist. Try again.").format(project))
return
if loadFromFile:
@@ -712,7 +711,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
# Giving some feedback
print(feedback)
- self.statusBar().showMessage(feedback, 5000)
+ F.statusMessage(feedback)
def loadEmptyDatas(self):
self.mdlFlatData = QStandardItemModel(self)
@@ -732,13 +731,13 @@ class MainWindow(QMainWindow, Ui_MainWindow):
# Giving some feedback
if not errors:
print(self.tr("Project {} loaded.").format(project))
- self.statusBar().showMessage(
+ F.statusMessage(
self.tr("Project {} loaded.").format(project), 5000)
else:
print(self.tr("Project {} loaded with some errors:").format(project))
for e in errors:
print(self.tr(" * {} wasn't found in project file.").format(e))
- self.statusBar().showMessage(
+ F.statusMessage(
self.tr("Project {} loaded with some errors.").format(project), 5000)
###############################################################################
diff --git a/manuskript/settings.py b/manuskript/settings.py
index efb8cef4..8d5044ba 100644
--- a/manuskript/settings.py
+++ b/manuskript/settings.py
@@ -64,15 +64,15 @@ textEditor = {
"misspelled": "#F00",
"lineSpacing": 100,
"tabWidth": 20,
- "indent": True,
+ "indent": False,
"spacingAbove": 5,
"spacingBelow": 5,
"textAlignment": 0, # 0: left, 1: center, 2: right, 3: justify
"cursorWidth": 1,
"cursorNotBlinking": False,
- "maxWidth": 0,
+ "maxWidth": 600,
"marginsLR": 0,
- "marginsTB": 0,
+ "marginsTB": 20,
"backgroundTransparent": False,
}
@@ -185,6 +185,8 @@ def load(string, fromString=False, protocol=None):
#print("Loading:")
#pp.pprint(allSettings)
+ # FIXME: use dict.update(dict) to update settings in newer versions.
+
if "viewSettings" in allSettings:
global viewSettings
viewSettings = allSettings["viewSettings"]
@@ -267,9 +269,9 @@ def load(string, fromString=False, protocol=None):
"textAlignment": 0, # Added in 0.5.0
"cursorWidth": 1,
"cursorNotBlinking": False, # Added in 0.6.0
- "maxWidth": 0,
+ "maxWidth": 600,
"marginsLR": 0,
- "marginsTB": 0,
+ "marginsTB": 20,
"backgroundTransparent": False, # Added in 0.6.0
}
diff --git a/manuskript/ui/editors/editorWidget.py b/manuskript/ui/editors/editorWidget.py
index b23d349d..0d00bb8c 100644
--- a/manuskript/ui/editors/editorWidget.py
+++ b/manuskript/ui/editors/editorWidget.py
@@ -2,12 +2,13 @@
# --!-- coding: utf8 --!--
from PyQt5.QtCore import pyqtSignal, QModelIndex
from PyQt5.QtGui import QPalette
-from PyQt5.QtWidgets import QWidget, QFrame, QSpacerItem, QSizePolicy, QVBoxLayout
+from PyQt5.QtWidgets import QWidget, QFrame, QSpacerItem, QSizePolicy
+from PyQt5.QtWidgets import QVBoxLayout, qApp, QStyle
from manuskript import settings
from manuskript.functions import AUC, mainWindow
from manuskript.ui.editors.editorWidget_ui import Ui_editorWidget_ui
-from manuskript.ui.views.textEditView import textEditView
+from manuskript.ui.views.MDEditView import MDEditView
from manuskript.ui.tools.splitDialog import splitDialog
@@ -60,10 +61,37 @@ class editorWidget(QWidget, Ui_editorWidget_ui):
self._model = None
+ # Capture textEdit scrollbar, so that we can put it outside the margins.
+ self.txtEditScrollBar = self.txtRedacText.verticalScrollBar()
+ self.txtEditScrollBar.setParent(self)
+ self.stack.currentChanged.connect(self.setScrollBarVisibility)
+
# def setModel(self, model):
# self._model = model
# self.setView()
+ def resizeEvent(self, event):
+ """
+ textEdit's scrollBar has been reparented to self. So we need to
+ update it's geomtry when self is resized, and put it where we want it
+ to be.
+ """
+ # Update scrollbar geometry
+ r = self.geometry()
+ w = 10 # Cf. style.mainEditorTabSS
+ r.setWidth(w)
+ r.moveRight(self.geometry().width())
+ self.txtEditScrollBar.setGeometry(r)
+
+ QWidget.resizeEvent(self, event)
+
+ def setScrollBarVisibility(self):
+ """
+ Since the texteEdit scrollBar has been reparented to self, it is not
+ hidden when stack changes. We have to do it manually.
+ """
+ self.txtEditScrollBar.setVisible(self.stack.currentIndex() == 0)
+
def setFolderView(self, v):
oldV = self.folderView
if v == "cork":
@@ -150,7 +178,7 @@ class editorWidget(QWidget, Ui_editorWidget_ui):
self.updateTabTitle()
def addTitle(itm):
- edt = textEditView(self, html="{t}".format(l=min(itm.level() + 1, 5), t=itm.title()),
+ edt = MDEditView(self, html="{t}".format(l=min(itm.level() + 1, 5), t=itm.title()),
autoResize=True)
edt.setFrameShape(QFrame.NoFrame)
self.txtEdits.append(edt)
@@ -163,7 +191,7 @@ class editorWidget(QWidget, Ui_editorWidget_ui):
l.addWidget(line)
def addText(itm):
- edt = textEditView(self,
+ edt = MDEditView(self,
index=itm.index(),
spellcheck=self.spellcheck,
dict=settings.dict,
@@ -214,7 +242,12 @@ class editorWidget(QWidget, Ui_editorWidget_ui):
w = QWidget()
w.setObjectName("editorWidgetFolderText")
l = QVBoxLayout(w)
- w.setStyleSheet("background: {};".format(settings.textEditor["background"]))
+ opt = settings.textEditor
+ background = (opt["background"] if not opt["backgroundTransparent"]
+ else "transparent")
+ w.setStyleSheet("background: {};".format(background))
+ self.stack.widget(1).setStyleSheet("background: {}"
+ .format(background))
# self.scroll.setWidgetResizable(False)
self.txtEdits = []
diff --git a/manuskript/ui/editors/editorWidget_ui.py b/manuskript/ui/editors/editorWidget_ui.py
index 9787c8a2..c0c5faf6 100644
--- a/manuskript/ui/editors/editorWidget_ui.py
+++ b/manuskript/ui/editors/editorWidget_ui.py
@@ -2,8 +2,7 @@
# Form implementation generated from reading ui file 'manuskript/ui/editors/editorWidget_ui.ui'
#
-# Created: Fri Apr 8 20:03:08 2016
-# by: PyQt5 UI code generator 5.2.1
+# Created by: PyQt5 UI code generator 5.5.1
#
# WARNING! All changes made in this file will be lost!
@@ -23,7 +22,7 @@ class Ui_editorWidget_ui(object):
self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.text)
self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0)
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
- self.txtRedacText = textEditView(self.text)
+ self.txtRedacText = MDEditView(self.text)
self.txtRedacText.setFrameShape(QtWidgets.QFrame.NoFrame)
self.txtRedacText.setObjectName("txtRedacText")
self.horizontalLayout_2.addWidget(self.txtRedacText)
@@ -31,8 +30,8 @@ class Ui_editorWidget_ui(object):
self.folder = QtWidgets.QWidget()
self.folder.setObjectName("folder")
self.verticalLayout = QtWidgets.QVBoxLayout(self.folder)
- self.verticalLayout.setSpacing(0)
self.verticalLayout.setContentsMargins(0, 0, 0, 0)
+ self.verticalLayout.setSpacing(0)
self.verticalLayout.setObjectName("verticalLayout")
self.scroll = QtWidgets.QScrollArea(self.folder)
self.scroll.setAutoFillBackground(True)
@@ -75,6 +74,6 @@ class Ui_editorWidget_ui(object):
_translate = QtCore.QCoreApplication.translate
editorWidget_ui.setWindowTitle(_translate("editorWidget_ui", "Form"))
-from manuskript.ui.views.outlineView import outlineView
-from manuskript.ui.views.textEditView import textEditView
+from manuskript.ui.views.MDEditView import MDEditView
from manuskript.ui.views.corkView import corkView
+from manuskript.ui.views.outlineView import outlineView
diff --git a/manuskript/ui/editors/editorWidget_ui.ui b/manuskript/ui/editors/editorWidget_ui.ui
index 04054d26..b52abce5 100644
--- a/manuskript/ui/editors/editorWidget_ui.ui
+++ b/manuskript/ui/editors/editorWidget_ui.ui
@@ -46,7 +46,7 @@
0
-
-
+
QFrame::NoFrame
@@ -147,12 +147,12 @@
-
- textEditView
- QTextEdit
- manuskript.ui.views.textEditView.h
-
-
+
+ MDEditView
+ QTextEdit
+ manuskript.ui.views.MDEditView.h
+
+
outlineView
QTreeView
manuskript.ui.views.outlineView.h
diff --git a/manuskript/ui/editors/fullScreenEditor.py b/manuskript/ui/editors/fullScreenEditor.py
index fe39dcb6..4de5ba80 100644
--- a/manuskript/ui/editors/fullScreenEditor.py
+++ b/manuskript/ui/editors/fullScreenEditor.py
@@ -13,10 +13,9 @@ from manuskript import settings
from manuskript.enums import Outline
from manuskript.functions import allPaths, drawProgress
from manuskript.ui.editors.locker import locker
-from manuskript.ui.editors.textFormat import textFormat
from manuskript.ui.editors.themes import findThemePath, generateTheme, setThemeEditorDatas
from manuskript.ui.editors.themes import loadThemeDatas
-from manuskript.ui.views.textEditView import textEditView
+from manuskript.ui.views.MDEditView import MDEditView
try:
import enchant
@@ -35,11 +34,11 @@ class fullScreenEditor(QWidget):
self._geometries = {}
# Text editor
- self.editor = textEditView(self,
- index=index,
- spellcheck=settings.spellcheck,
- highlighting=True,
- dict=settings.dict)
+ self.editor = MDEditView(self,
+ index=index,
+ spellcheck=settings.spellcheck,
+ highlighting=True,
+ dict=settings.dict)
self.editor.setFrameStyle(QFrame.NoFrame)
self.editor.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.editor.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
@@ -65,11 +64,7 @@ class fullScreenEditor(QWidget):
self.topPanel.layout().addStretch(1)
- # Formatting
- self.textFormat = textFormat(self)
- self.topPanel.layout().addWidget(self.textFormat)
- self.topPanel.layout().addStretch(1)
-
+ # Close
self.btnClose = QPushButton(self)
self.btnClose.setIcon(qApp.style().standardIcon(QStyle.SP_DialogCloseButton))
self.btnClose.clicked.connect(self.close)
diff --git a/manuskript/ui/editors/mainEditor.py b/manuskript/ui/editors/mainEditor.py
index c5a034e3..1633359a 100644
--- a/manuskript/ui/editors/mainEditor.py
+++ b/manuskript/ui/editors/mainEditor.py
@@ -270,9 +270,6 @@ class mainEditor(QWidget, Ui_mainEditor):
else:
visible = True
- # Hides / show textFormat
- self.textFormat.updateFromIndex(index)
-
self.btnRedacFolderText.setVisible(visible)
self.btnRedacFolderCork.setVisible(visible)
self.btnRedacFolderOutline.setVisible(visible)
diff --git a/manuskript/ui/editors/mainEditor_ui.py b/manuskript/ui/editors/mainEditor_ui.py
index 45c8e6ff..d7f94e38 100644
--- a/manuskript/ui/editors/mainEditor_ui.py
+++ b/manuskript/ui/editors/mainEditor_ui.py
@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file 'manuskript/ui/editors/mainEditor_ui.ui'
#
-# Created by: PyQt5 UI code generator 5.9
+# Created by: PyQt5 UI code generator 5.5.1
#
# WARNING! All changes made in this file will be lost!
@@ -65,17 +65,6 @@ class Ui_mainEditor(object):
self.horizontalLayout_19.addWidget(self.sldCorkSizeFactor)
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_19.addItem(spacerItem)
- self.textFormat = textFormat(mainEditor)
- sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
- sizePolicy.setHorizontalStretch(0)
- sizePolicy.setVerticalStretch(0)
- sizePolicy.setHeightForWidth(self.textFormat.sizePolicy().hasHeightForWidth())
- self.textFormat.setSizePolicy(sizePolicy)
- self.textFormat.setMinimumSize(QtCore.QSize(20, 20))
- self.textFormat.setObjectName("textFormat")
- self.horizontalLayout_19.addWidget(self.textFormat)
- spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
- self.horizontalLayout_19.addItem(spacerItem1)
self.lblRedacWC = QtWidgets.QLabel(mainEditor)
self.lblRedacWC.setMinimumSize(QtCore.QSize(10, 0))
self.lblRedacWC.setText("")
@@ -110,4 +99,3 @@ class Ui_mainEditor(object):
self.btnRedacFullscreen.setShortcut(_translate("mainEditor", "F11"))
from manuskript.ui.editors.tabSplitter import tabSplitter
-from manuskript.ui.editors.textFormat import textFormat
diff --git a/manuskript/ui/editors/mainEditor_ui.ui b/manuskript/ui/editors/mainEditor_ui.ui
index 1110a34f..2950db94 100644
--- a/manuskript/ui/editors/mainEditor_ui.ui
+++ b/manuskript/ui/editors/mainEditor_ui.ui
@@ -51,8 +51,7 @@
-
-
+ ..
Alt+Up
@@ -141,35 +140,6 @@
- -
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 20
- 20
-
-
-
-
-
@@ -237,12 +207,6 @@
-
- textFormat
- QWidget
- manuskript.ui.editors.textFormat.h
- 1
-
tabSplitter
QWidget
diff --git a/manuskript/ui/editors/themes.py b/manuskript/ui/editors/themes.py
index 47f96f32..6b1222da 100644
--- a/manuskript/ui/editors/themes.py
+++ b/manuskript/ui/editors/themes.py
@@ -10,7 +10,7 @@ from PyQt5.QtGui import QPixmap, QPainter, QColor, QBrush, QImage, QTextBlockFor
from PyQt5.QtWidgets import qApp, QFrame
from manuskript.functions import allPaths, appPath, findBackground, findFirstFile
-from manuskript.ui.views.textEditView import textEditView
+from manuskript.ui.views.MDEditView import MDEditView
_thumbCache = {}
@@ -89,13 +89,13 @@ def themeTextRect(themeDatas, screenRect):
def createThemePreview(theme, screenRect, size=QSize(200, 120)):
"""
Generates a QPixmap preview for given theme.
-
+
Theme can be either a string containing the filename of the ini
file with the theme settings, or it can be a dict with the settings.
-
+
If theme is a filename, the result is cached.
"""
-
+
# Checking whether theme is a string or dict
if type(theme) == str and os.path.exists(theme):
# Theme is the path to an ini file
@@ -126,7 +126,7 @@ def createThemePreview(theme, screenRect, size=QSize(200, 120)):
painter.setPen(Qt.white)
painter.drawRect(QRect(w, h, w * 4, h * 5))
painter.end()
-
+
# If theme is a themefile, we keep it in cache
if fromFile:
_thumbCache[theme] = [themeDatas, px]
@@ -265,11 +265,12 @@ def setThemeEditorDatas(editor, themeDatas, pixmap, screenRect):
)
editor._fromTheme = True
-
+ editor._themeData = themeDatas
+ editor.highlighter.updateColorScheme()
def addThemePreviewText(pixmap, themeDatas, screenRect):
# Text
- previewText = textEditView(highlighting=True)
+ previewText = MDEditView(highlighting=True)
previewText.setFrameStyle(QFrame.NoFrame)
previewText.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
previewText.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
diff --git a/manuskript/ui/editors/MMDHighlighter.py b/manuskript/ui/highlighters/MMDHighlighter.py
similarity index 92%
rename from manuskript/ui/editors/MMDHighlighter.py
rename to manuskript/ui/highlighters/MMDHighlighter.py
index a6740f3d..cea62555 100644
--- a/manuskript/ui/editors/MMDHighlighter.py
+++ b/manuskript/ui/highlighters/MMDHighlighter.py
@@ -5,10 +5,10 @@ import re
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QTextCharFormat, QFont, QTextCursor, QFontMetrics
-from manuskript.ui.editors.basicHighlighter import basicHighlighter
+from manuskript.ui.highlighters import BasicHighlighter
-class MMDHighlighter(basicHighlighter):
+class MMDHighlighter(BasicHighlighter):
MARKDOWN_REGEX = {
'Bold': '(\*\*)(.+?)(\*\*)',
@@ -27,7 +27,7 @@ class MMDHighlighter(basicHighlighter):
}
def __init__(self, editor, style="Default"):
- basicHighlighter.__init__(self, editor)
+ BasicHighlighter.__init__(self, editor)
self.editor = editor
@@ -35,13 +35,6 @@ class MMDHighlighter(basicHighlighter):
for key in self.MARKDOWN_REGEX:
self.rules[key] = re.compile(self.MARKDOWN_REGEX[key])
- def highlightBlock(self, text):
- basicHighlighter.highlightBlockBefore(self, text)
-
- self.doHighlightBlock(text)
-
- basicHighlighter.highlightBlockAfter(self, text)
-
def doHighlightBlock(self, text):
"""
A quick-n-dirty very basic highlighter, that fails in most non-trivial cases. And is ugly.
diff --git a/manuskript/ui/highlighters/__init__.py b/manuskript/ui/highlighters/__init__.py
new file mode 100644
index 00000000..c3b1ed33
--- /dev/null
+++ b/manuskript/ui/highlighters/__init__.py
@@ -0,0 +1,12 @@
+#!/usr/bin/python
+# -*- coding: utf8 -*-
+
+from manuskript.ui.highlighters.basicHighlighter import BasicHighlighter
+from manuskript.ui.highlighters.MMDHighlighter import MMDHighlighter
+
+# Markdown highlighter
+from manuskript.ui.highlighters.markdownEnums import MarkdownState
+from manuskript.ui.highlighters.markdownEnums import MarkdownTokenType
+from manuskript.ui.highlighters.markdownEnums import BlockquoteStyle
+from manuskript.ui.highlighters.markdownTokenizer import MarkdownTokenizer
+from manuskript.ui.highlighters.markdownHighlighter import MarkdownHighlighter
diff --git a/manuskript/ui/editors/basicHighlighter.py b/manuskript/ui/highlighters/basicHighlighter.py
similarity index 55%
rename from manuskript/ui/editors/basicHighlighter.py
rename to manuskript/ui/highlighters/basicHighlighter.py
index a09b53a4..960b7201 100644
--- a/manuskript/ui/editors/basicHighlighter.py
+++ b/manuskript/ui/highlighters/basicHighlighter.py
@@ -4,12 +4,16 @@
import re
from PyQt5.QtCore import Qt
-from PyQt5.QtGui import QBrush, QTextCursor, QColor, QFont, QSyntaxHighlighter, QTextBlockFormat, QTextCharFormat
+from PyQt5.QtGui import QBrush, QTextCursor, QColor, QFont, QSyntaxHighlighter
+from PyQt5.QtGui import QTextBlockFormat, QTextCharFormat
import manuskript.models.references as Ref
+import manuskript.ui.style as S
+from manuskript import settings
+from manuskript import functions as F
-class basicHighlighter(QSyntaxHighlighter):
+class BasicHighlighter(QSyntaxHighlighter):
def __init__(self, editor):
QSyntaxHighlighter.__init__(self, editor.document())
@@ -17,6 +21,11 @@ class basicHighlighter(QSyntaxHighlighter):
self._misspelledColor = Qt.red
self._defaultBlockFormat = QTextBlockFormat()
self._defaultCharFormat = QTextCharFormat()
+ self.defaultTextColor = QColor(S.text)
+ self.backgroundColor = QColor(S.base)
+ self.markupColor = QColor(S.textLight)
+ self.linkColor = QColor(S.link)
+ self.spellingErrorColor = QColor(Qt.red)
def setDefaultBlockFormat(self, bf):
self._defaultBlockFormat = bf
@@ -29,17 +38,63 @@ class basicHighlighter(QSyntaxHighlighter):
def setMisspelledColor(self, color):
self._misspelledColor = color
+ def updateColorScheme(self, rehighlight=True):
+ """
+ Generates a base set of colors that will take account of user
+ preferences, and use system style.
+ """
+
+ # Reading user settings
+ opt = settings.textEditor
+
+ if not self.editor._fromTheme or not self.editor._themeData:
+
+ self.defaultTextColor = QColor(opt["fontColor"])
+ self.backgroundColor = (QColor(opt["background"])
+ if not opt["backgroundTransparent"]
+ else QColor(S.window))
+ self.markupColor = F.mixColors(self.defaultTextColor,
+ self.backgroundColor,
+ .3)
+ self.linkColor = QColor(S.link)
+ self.spellingErrorColor = QColor(opt["misspelled"])
+ self._defaultCharFormat.setForeground(QBrush(self.defaultTextColor))
+
+ # FullscreenEditor probably
+ else:
+ opt = self.editor._themeData
+ self.defaultTextColor = QColor(opt["Text/Color"])
+ self.backgroundColor = F.mixColors(
+ QColor(opt["Foreground/Color"]),
+ QColor(opt["Background/Color"]),
+ int(opt["Foreground/Opacity"])/100.)
+ self.markupColor = F.mixColors(self.defaultTextColor,
+ self.backgroundColor,
+ .3)
+ self.linkColor = QColor(S.link)
+ self.spellingErrorColor = QColor(opt["Text/Misspelled"])
+
+ if rehighlight:
+ self.rehighlight()
+
def highlightBlock(self, text):
"""Apply syntax highlighting to the given block of text.
"""
self.highlightBlockBefore(text)
+ self.doHighlightBlock(text)
self.highlightBlockAfter(text)
+ def doHighlightBlock(self, text):
+ """
+ Virtual funtion to subclass.
+ """
+ pass
+
def highlightBlockBefore(self, text):
"""Highlighting to do before anything else.
- When subclassing basicHighlighter, you must call highlightBlockBefore
- before you do any custom highlighting.
+ When subclassing BasicHighlighter, you must call highlightBlockBefore
+ before you do any custom highlighting. Or implement doHighlightBlock.
"""
#print(">", self.currentBlock().document().availableUndoSteps())
@@ -56,8 +111,8 @@ class basicHighlighter(QSyntaxHighlighter):
def highlightBlockAfter(self, text):
"""Highlighting to do after everything else.
- When subclassing basicHighlighter, you must call highlightBlockAfter
- after your custom highlighting.
+ When subclassing BasicHighlighter, you must call highlightBlockAfter
+ after your custom highlighting. Or implement doHighlightBlock.
"""
# References
@@ -91,13 +146,16 @@ class basicHighlighter(QSyntaxHighlighter):
textedText = text + " "
# Based on http://john.nachtimwald.com/2009/08/22/qplaintextedit-with-in-line-spell-check/
- WORDS = '(?iu)([\w\']+)[^\'\w]' # (?iu) means case insensitive and unicode
+ WORDS = r'(?iu)([\w\']+)[^\'\w]'
+ # (?iu) means case insensitive and unicode
if hasattr(self.editor, "spellcheck") and self.editor.spellcheck:
for word_object in re.finditer(WORDS, textedText):
- if self.editor._dict and not self.editor._dict.check(word_object.group(1)):
+ if (self.editor._dict
+ and not self.editor._dict.check(word_object.group(1))):
format = self.format(word_object.start(1))
format.setUnderlineColor(self._misspelledColor)
# SpellCheckUnderline fails with some fonts
format.setUnderlineStyle(QTextCharFormat.WaveUnderline)
self.setFormat(word_object.start(1),
- word_object.end(1) - word_object.start(1), format)
+ word_object.end(1) - word_object.start(1),
+ format)
diff --git a/manuskript/ui/highlighters/markdownEnums.py b/manuskript/ui/highlighters/markdownEnums.py
new file mode 100644
index 00000000..76cb32b4
--- /dev/null
+++ b/manuskript/ui/highlighters/markdownEnums.py
@@ -0,0 +1,100 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+#==============================================================================
+# MARKDOWN STATES
+#==============================================================================
+
+class MarkdownState:
+ MarkdownStateUnknown = -1
+ MarkdownStateParagraphBreak = 0
+ MarkdownStateListLineBreak = 1
+ MarkdownStateParagraph = 2
+ MarkdownStateAtxHeading1 = 3
+ MarkdownStateAtxHeading2 = 4
+ MarkdownStateAtxHeading3 = 5
+ MarkdownStateAtxHeading4 = 6
+ MarkdownStateAtxHeading5 = 7
+ MarkdownStateAtxHeading6 = 8
+ MarkdownStateBlockquote = 9
+ MarkdownStateCodeBlock = 10
+ MarkdownStateInGithubCodeFence = 11
+ MarkdownStateInPandocCodeFence = 12
+ MarkdownStateCodeFenceEnd = 13
+ MarkdownStateComment = 14
+ MarkdownStateHorizontalRule = 15
+ MarkdownStateNumberedList = 16
+ MarkdownStateBulletPointList = 17
+ MarkdownStateSetextHeading1Line1 = 18
+ MarkdownStateSetextHeading1Line2 = 19
+ MarkdownStateSetextHeading2Line1 = 20
+ MarkdownStateSetextHeading2Line2 = 21
+ MarkdownStatePipeTableHeader = 22
+ MarkdownStatePipeTableDivider = 23
+ MarkdownStatePipeTableRow = 24
+
+#==============================================================================
+# MARKDOWN TOKEN TYPE
+#==============================================================================
+
+class MarkdownTokenType:
+ TokenUnknown = -1
+
+ # Titles
+ TokenAtxHeading1 = 0
+ TokenAtxHeading2 = 1
+ TokenAtxHeading3 = 2
+ TokenAtxHeading4 = 3
+ TokenAtxHeading5 = 4
+ TokenAtxHeading6 = 5
+ TokenSetextHeading1Line1 = 6
+ TokenSetextHeading1Line2 = 7
+ TokenSetextHeading2Line1 = 8
+ TokenSetextHeading2Line2 = 9
+
+ TokenEmphasis = 10
+ TokenStrong = 11
+ TokenStrikethrough = 12
+ TokenVerbatim = 13
+ TokenHtmlTag = 14
+ TokenHtmlEntity = 15
+ TokenAutomaticLink = 16
+ TokenInlineLink = 17
+ TokenReferenceLink = 18
+ TokenReferenceDefinition = 19
+ TokenImage = 20
+ TokenHtmlComment = 21
+ TokenNumberedList = 22
+ TokenBulletPointList = 23
+ TokenHorizontalRule = 24
+ TokenLineBreak = 25
+ TokenBlockquote = 26
+ TokenCodeBlock = 27
+ TokenGithubCodeFence = 28
+ TokenPandocCodeFence = 29
+ TokenCodeFenceEnd = 30
+ TokenMention = 31
+ TokenTableHeader = 32
+ TokenTableDivider = 33
+ TokenTablePipe = 34
+ TokenSuperScript = 35
+ TokenSubScript = 36
+ # CriticMarkup
+ TokenCMAddition = 37 # {++ ++}
+ TokenCMDeletion = 38 # {-- --}
+ TokenCMSubstitution = 39 #{~~ ~> ~~}
+ TokenCMComment = 40 # {>> <<}
+ TokenCMHighlight = 41 # {== ==}{>> <<}
+ TokenLast = 42
+
+ TITLES = [TokenAtxHeading1, TokenAtxHeading2, TokenAtxHeading3,
+ TokenAtxHeading4, TokenAtxHeading5, TokenAtxHeading6,
+ TokenSetextHeading1Line1, TokenSetextHeading1Line2,
+ TokenSetextHeading2Line1, TokenSetextHeading2Line2]
+
+
+
+class BlockquoteStyle:
+ BlockquoteStylePlain = 0
+ BlockquoteStyleItalic = 1
+ BlockquoteStyleFancy = 2
diff --git a/manuskript/ui/highlighters/markdownHighlighter.py b/manuskript/ui/highlighters/markdownHighlighter.py
new file mode 100644
index 00000000..52b46a2e
--- /dev/null
+++ b/manuskript/ui/highlighters/markdownHighlighter.py
@@ -0,0 +1,665 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+"""
+A QSyntaxHighlighter for markdown, using tokenizer. More accurate than simple
+regexp, but not yet perfect.
+"""
+
+import re
+from PyQt5.QtCore import Qt, pyqtSignal, qWarning, QRegExp
+from PyQt5.QtGui import (QSyntaxHighlighter, QTextBlock, QColor, QFont,
+ QTextCharFormat, QBrush, QPalette)
+from PyQt5.QtWidgets import qApp, QStyle
+
+from manuskript.ui.highlighters import BasicHighlighter
+from manuskript.ui.highlighters import MarkdownTokenizer
+from manuskript.ui.highlighters import MarkdownState as MS
+from manuskript.ui.highlighters import MarkdownTokenType as MTT
+from manuskript.ui.highlighters import BlockquoteStyle as BS
+from manuskript.ui import style as S
+from manuskript import settings
+from manuskript import functions as F
+
+# Un longue ligne. Un longue ligne. Un longue ligne. Un longue ligne.asdasdasda
+
+GW_FADE_ALPHA = 140
+
+# Highlighter based on GhostWriter (http://wereturtle.github.io/ghostwriter/).
+# GPLV3+.
+
+#FIXME: Setext heading don't work anymore
+
+class MarkdownHighlighter(BasicHighlighter):
+
+ highlightBlockAtPosition = pyqtSignal(int)
+ headingFound = pyqtSignal(int, str, QTextBlock)
+ headingRemoved = pyqtSignal(int)
+
+ def __init__(self, editor):
+ BasicHighlighter.__init__(self, editor)
+
+ #default values
+ self.editor = editor
+ self.tokenizer = MarkdownTokenizer()
+
+ self.spellCheckEnabled = False
+ #self.typingPaused = True
+ self.inBlockquote = False
+ self.blockquoteStyle = BS.BlockquoteStyleFancy
+
+ # Settings
+ self.useUndlerlineForEmphasis = False
+ self.highlightLineBreaks = True
+
+ self.highlightBlockAtPosition.connect(self.onHighlightBlockAtPosition,
+ Qt.QueuedConnection)
+
+ self.theme = self.defaultTheme()
+ self.setupHeadingFontSize(True)
+
+ self.highlightedWords = []
+ self.highlightedTags = []
+ self.searchExpression = ""
+ self.searchExpressionRegExp = False
+ self.searchExpressionCase = False
+
+ #f = self.document().defaultFont()
+ #f.setFamily("monospace")
+ #self.document().setDefaultFont(f)
+
+ def doHighlightBlock(self, text):
+ """
+ Note: Never set the QTextBlockFormat for a QTextBlock from within
+ the highlighter. Depending on how the block format is modified,
+ a recursive call to the highlighter may be triggered, which will
+ cause the application to crash.
+
+ Likewise, don't try to set the QTextBlockFormat outside the highlighter
+ (i.e., from within the text editor). While the application will not
+ crash, the format change will be added to the undo stack. Attempting
+ to undo from that point on will cause the undo stack to be virtually
+ frozen, since undoing the format operation causes the text to be
+ considered changed, thus triggering the slot that changes the text
+ formatting to be triggered yet again.
+ """
+
+ lastState = self.currentBlockState()
+ self.setFormat(0, len(text), self._defaultCharFormat)
+
+ if self.tokenizer != None:
+ self.tokenizer.clear()
+ block = self.currentBlock()
+ nextState = MS.MarkdownStateUnknown
+ previousState = self.previousBlockState()
+
+ if block.next().isValid():
+ nextState = block.next().userState()
+
+ self.tokenizer.tokenize(text, lastState, previousState, nextState)
+ self.setCurrentBlockState(self.tokenizer.getState())
+
+ self.inBlockquote = self.tokenizer.getState() == MS.MarkdownStateBlockquote
+
+ # STATE FORMATTING
+ # FIXME: generic
+ if self.currentBlockState() in [
+ MS.MarkdownStatePipeTableHeader,
+ MS.MarkdownStatePipeTableDivider,
+ MS.MarkdownStatePipeTableRow]:
+ fmt = QTextCharFormat()
+ f = fmt.font()
+ f.setFamily("Monospace")
+ fmt.setFont(f)
+ self.setFormat(0, len(text), fmt)
+
+ # Monospace the blank chars
+ i = 0
+ while i <= len(text)-1 and text[i] in [" ", "\t"]:
+ fmt = self.format(i)
+ fmt.setFontFamily("Monospace")
+ self.setFormat(i, 1, fmt)
+ i += 1
+
+ #if self.currentBlockState() == MS.MarkdownStateBlockquote:
+ #fmt = QTextCharFormat(self._defaultCharFormat)
+ #fmt.setForeground(Qt.lightGray)
+ #self.setFormat(0, len(text), fmt)
+
+ tokens = self.tokenizer.getTokens()
+
+ for token in tokens:
+ if token.type == MTT.TokenUnknown:
+ qWarning("Highlighter found unknown token type in text block.")
+ continue
+
+ if token.type in [
+ MTT.TokenAtxHeading1,
+ MTT.TokenAtxHeading2,
+ MTT.TokenAtxHeading3,
+ MTT.TokenAtxHeading4,
+ MTT.TokenAtxHeading5,
+ MTT.TokenAtxHeading6,
+ MTT.TokenSetextHeading1Line1,
+ MTT.TokenSetextHeading2Line1,
+ ]:
+ self.storeHeadingData(token, text)
+
+ self.applyFormattingForToken(token, text)
+
+ if self.tokenizer.backtrackRequested():
+ previous = self.currentBlock().previous()
+ self.highlightBlockAtPosition.emit(previous.position())
+
+ if self.spellCheckEnabled:
+ self.spellCheck(text)
+
+ # If the block has transitioned from previously being a heading to now
+ # being a non-heading, signal that the position in the document no
+ # longer contains a heading.
+
+ if self.isHeadingBlockState(lastState) and \
+ not self.isHeadingBlockState(self.currentBlockState()):
+ self.headingRemoved.emit(self.currentBlock().position())
+
+
+ ###########################################################################
+ # COLORS & FORMATTING
+ ###########################################################################
+
+ def updateColorScheme(self, rehighlight=True):
+ BasicHighlighter.updateColorScheme(self, rehighlight)
+ self.theme = self.defaultTheme()
+ self.setEnableLargeHeadingSizes(True)
+
+ def defaultTheme(self):
+
+ # Base Colors
+ background = self.backgroundColor
+ text = self.defaultTextColor
+ highlightedText = QColor(S.highlightedText)
+ highlightedTextDark = QColor(S.highlightedTextDark)
+ highlightedTextLight = QColor(S.highlightedTextLight)
+ highlight = QColor(S.highlight)
+ link = self.linkColor
+ linkVisited = QColor(S.linkVisited)
+
+ # titleColor = highlight
+ titleColor = QColor(S.highlightedTextDark)
+
+ # FullscreenEditor probably
+ if self.editor._fromTheme and self.editor._themeData:
+ text = QColor(self.editor._themeData["Text/Color"])
+ background = QColor(self.editor._themeData["Background/Color"])
+ titleColor = text
+
+ # Compositions
+ light = F.mixColors(text, background, .75)
+ markup = F.mixColors(text, background, .5)
+ veryLight = F.mixColors(text, background, .25)
+ listToken = F.mixColors(highlight, background, .4)
+ titleMarkupColor = F.mixColors(titleColor, background, .3)
+
+
+ theme = {
+ "markup": markup}
+
+ #Exemple:
+ #"color": Qt.red,
+ #"deltaSize": 10,
+ #"background": Qt.yellow,
+ #"monospace": True,
+ #"bold": True,
+ #"italic": True,
+ #"underline": True,
+ #"overline": True,
+ #"strike": True,
+ #"formatMarkup": True,
+ #"markupBold": True,
+ #"markupColor": Qt.blue,
+ #"markupBackground": Qt.green,
+ #"markupMonospace": True,
+ #"super":True,
+ #"sub":True
+
+ for i in MTT.TITLES:
+ theme[i] = {
+ "formatMarkup":True,
+ "bold": True,
+ # "monospace": True,
+ "markupColor": titleMarkupColor
+ }
+
+ theme[MTT.TokenAtxHeading1]["color"] = titleColor
+ theme[MTT.TokenAtxHeading2]["color"] = F.mixColors(titleColor,
+ background, .9)
+ theme[MTT.TokenAtxHeading3]["color"] = F.mixColors(titleColor,
+ background, .8)
+ theme[MTT.TokenAtxHeading4]["color"] = F.mixColors(titleColor,
+ background, .7)
+ theme[MTT.TokenAtxHeading5]["color"] = F.mixColors(titleColor,
+ background, .6)
+ theme[MTT.TokenAtxHeading6]["color"] = F.mixColors(titleColor,
+ background, .5)
+
+ theme[MTT.TokenSetextHeading1Line1]["color"] = titleColor
+ theme[MTT.TokenSetextHeading2Line1]["color"] = F.mixColors(titleColor,
+ background,
+ .9)
+
+ for i in [MTT.TokenSetextHeading1Line1, MTT.TokenSetextHeading2Line1]:
+ theme[i]["monospace"] = True
+
+ for i in [MTT.TokenSetextHeading1Line2, MTT.TokenSetextHeading2Line2]:
+ theme[i] = {
+ "color": titleMarkupColor,
+ "monospace":True}
+
+ # Beautifiers
+ theme[MTT.TokenEmphasis] = {
+ "italic":True}
+ theme[MTT.TokenStrong] = {
+ "bold":True}
+ theme[MTT.TokenStrikethrough] = {
+ "strike":True}
+ theme[MTT.TokenVerbatim] = {
+ "monospace":True,
+ "background": veryLight,
+ "formatMarkup": True,
+ "markupColor": markup,
+ "deltaSize": -1}
+ theme[MTT.TokenSuperScript] = {
+ "super":True,
+ "formatMarkup":True}
+ theme[MTT.TokenSubScript] = {
+ "sub":True,
+ "formatMarkup":True}
+ theme[MTT.TokenHtmlTag] = {
+ "color": linkVisited}
+ theme[MTT.TokenHtmlEntity] = { #
+ "color": linkVisited}
+ theme[MTT.TokenAutomaticLink] = {
+ "color": link}
+ theme[MTT.TokenInlineLink] = {
+ "color": link}
+ theme[MTT.TokenReferenceLink] = {
+ "color": link}
+ theme[MTT.TokenReferenceDefinition] = {
+ "color": link}
+ theme[MTT.TokenImage] = {
+ "color": highlightedTextDark}
+ theme[MTT.TokenHtmlComment] = {
+ "color": markup}
+ theme[MTT.TokenNumberedList] = {
+ "markupColor": listToken,
+ "markupBold": True,
+ "markupMonospace": True,}
+ theme[MTT.TokenBulletPointList] = {
+ "markupColor": listToken,
+ "markupBold": True,
+ "markupMonospace": True,}
+ theme[MTT.TokenHorizontalRule] = {
+ "overline": True,
+ "underline": True,
+ "monospace": True,
+ "color": markup}
+ theme[MTT.TokenLineBreak] = {
+ "background": markup}
+ theme[MTT.TokenBlockquote] = {
+ "color": light,
+ "markupColor": veryLight,
+ "markupBackground": veryLight}
+ theme[MTT.TokenCodeBlock] = {
+ "color": light,
+ "markupBackground": veryLight,
+ "formatMarkup": True,
+ "monospace":True,
+ "deltaSize":-1}
+ theme[MTT.TokenGithubCodeFence] = {
+ "color": markup}
+ theme[MTT.TokenPandocCodeFence] = {
+ "color": markup}
+ theme[MTT.TokenCodeFenceEnd] = {
+ "color": markup}
+ theme[MTT.TokenMention] = {} # FIXME
+ theme[MTT.TokenTableHeader] = {
+ "color": light, "monospace":True}
+ theme[MTT.TokenTableDivider] = {
+ "color": markup, "monospace":True}
+ theme[MTT.TokenTablePipe] = {
+ "color": markup, "monospace":True}
+
+ # CriticMarkup
+ theme[MTT.TokenCMAddition] = {
+ "color": QColor("#00bb00"),
+ "markupColor": QColor(F.mixColors("#00bb00", background, .4)),
+ "markupMonospace": True,}
+ theme[MTT.TokenCMDeletion] = {
+ "color": QColor("#dd0000"),
+ "markupColor": QColor(F.mixColors("#dd0000", background, .4)),
+ "markupMonospace": True,
+ "strike": True}
+ theme[MTT.TokenCMSubstitution] = {
+ "color": QColor("#ff8600"),
+ "markupColor": QColor(F.mixColors("#ff8600", background, .4)),
+ "markupMonospace": True,}
+ theme[MTT.TokenCMComment] = {
+ "color": QColor("#0000bb"),
+ "markupColor": QColor(F.mixColors("#0000bb", background, .4)),
+ "markupMonospace": True,}
+ theme[MTT.TokenCMHighlight] = {
+ "color": QColor("#aa53a9"),
+ "background": QColor(F.mixColors("#aa53a9", background, .1)),
+ "markupBackground": QColor(F.mixColors("#aa53a9", background, .1)),
+ "markupColor": QColor(F.mixColors("#aa53a9", background, .5)),
+ "markupMonospace": True,}
+
+ return theme
+
+ ###########################################################################
+ # ACTUAL FORMATTING
+ ###########################################################################
+
+ def applyFormattingForToken(self, token, text):
+ if token.type != MTT.TokenUnknown:
+ fmt = self.format(token.position + token.openingMarkupLength)
+ markupFormat = self.format(token.position)
+ if self.theme.get("markup"):
+ markupFormat.setForeground(self.theme["markup"])
+
+ ## Debug
+ def debug():
+ print("{}\n{}{}{}{} (state:{})".format(
+ text,
+ " "*token.position,
+ "^"*token.openingMarkupLength,
+ str(token.type).center(token.length
+ - token.openingMarkupLength
+ - token.closingMarkupLength, "-"),
+ "^" * token.closingMarkupLength,
+ self.currentBlockState(),)
+ )
+
+ # if token.type in range(6, 10):
+ # debug()
+
+ theme = self.theme.get(token.type)
+ if theme:
+ fmt, markupFormat = self.formatsFromTheme(theme,
+ fmt,
+ markupFormat)
+
+ # Format openning Markup
+ self.setFormat(token.position, token.openingMarkupLength,
+ markupFormat)
+
+ # Format Text
+ self.setFormat(
+ token.position + token.openingMarkupLength,
+ token.length - token.openingMarkupLength - token.closingMarkupLength,
+ fmt)
+
+ # Format closing Markup
+ if token.closingMarkupLength > 0:
+ self.setFormat(
+ token.position + token.length - token.closingMarkupLength,
+ token.closingMarkupLength,
+ markupFormat)
+
+ else:
+ qWarning("MarkdownHighlighter.applyFormattingForToken() was passed"
+ " in a token of unknown type.")
+
+ def formatsFromTheme(self, theme, format=None,
+ markupFormat=QTextCharFormat()):
+ # Token
+ if theme.get("color"):
+ format.setForeground(theme["color"])
+ if theme.get("deltaSize"):
+ size = self.editor._defaultFontSize + theme["deltaSize"]
+ if size >= 0:
+ f = format.font()
+ f.setPointSize(size)
+ format.setFont(f)
+ if theme.get("background"):
+ format.setBackground(theme["background"])
+ if theme.get("monospace"):
+ format.setFontFamily("Monospace")
+ if theme.get("bold"):
+ format.setFontWeight(QFont.Bold)
+ if theme.get("italic"):
+ format.setFontItalic(theme["italic"])
+ if theme.get("underline"):
+ format.setFontUnderline(theme["underline"])
+ if theme.get("overline"):
+ format.setFontOverline(theme["overline"])
+ if theme.get("strike"):
+ format.setFontStrikeOut(theme["strike"])
+ if theme.get("super"):
+ format.setVerticalAlignment(QTextCharFormat.AlignSuperScript)
+ if theme.get("sub"):
+ format.setVerticalAlignment(QTextCharFormat.AlignSubScript)
+
+ # Markup
+ if theme.get("formatMarkup"):
+ c = markupFormat.foreground()
+ markupFormat = QTextCharFormat(format)
+ markupFormat.setForeground(c)
+ if theme.get("markupBold"):
+ markupFormat.setFontWeight(QFont.Bold)
+ if theme.get("markupColor"):
+ markupFormat.setForeground(theme["markupColor"])
+ if theme.get("markupBackground"):
+ markupFormat.setBackground(theme["markupBackground"])
+ if theme.get("markupMonospace"):
+ markupFormat.setFontFamily("Monospace")
+
+ return format, markupFormat
+
+ ###########################################################################
+ # SETTINGS
+ ###########################################################################
+
+ def setHighlighted(self, words, tags):
+ rehighlight = (self.highlightedWords != words
+ or self.highlightedTags != tags)
+ self.highlightedWords = words
+ self.highlightedTags = tags
+ if rehighlight:
+ self.rehighlight()
+
+ def setSearched(self, expression, regExp=False, caseSensitivity=False):
+ """
+ Define an expression currently searched, to be highlighted.
+ Can be regExp.
+ """
+ rehighlight = self.searchExpression != expression or \
+ self.searchExpressionRegExp != regExp or \
+ self.searchExpressionCase != caseSensitivity
+ self.searchExpression = expression
+ self.searchExpressionRegExp = regExp
+ self.searchExpressionCase = caseSensitivity
+ if rehighlight:
+ self.rehighlight()
+
+ def setDictionary(self, dictionary):
+ self.dictionary = dictionary
+ if self.spellCheckEnabled:
+ self.rehighlight()
+
+ def increaseFontSize(self):
+ self._defaultCharFormat.setFontPointSize(
+ self._defaultCharFormat.font().pointSize() + 1.0)
+ self.rehighlight()
+
+ def decreaseFontSize(self):
+ self._defaultCharFormat.setFontPointSize(
+ self._defaultCharFormat.font().pointSize() - 1.0)
+ self.rehighlight()
+
+ def setEnableLargeHeadingSizes(self, enable):
+ self.setupHeadingFontSize(enable)
+ self.rehighlight()
+
+ def setupHeadingFontSize(self, useLargeHeadings):
+ if useLargeHeadings:
+ self.theme[MTT.TokenSetextHeading1Line1]["deltaSize"] = 7
+ self.theme[MTT.TokenSetextHeading2Line1]["deltaSize"] = 5
+ self.theme[MTT.TokenSetextHeading1Line2]["deltaSize"] = 7
+ self.theme[MTT.TokenSetextHeading2Line2]["deltaSize"] = 5
+ self.theme[MTT.TokenAtxHeading1]["deltaSize"] = 7
+ self.theme[MTT.TokenAtxHeading2]["deltaSize"] = 5
+ self.theme[MTT.TokenAtxHeading3]["deltaSize"] = 3
+ self.theme[MTT.TokenAtxHeading4]["deltaSize"] = 2
+ self.theme[MTT.TokenAtxHeading5]["deltaSize"] = 1
+ self.theme[MTT.TokenAtxHeading6]["deltaSize"] = 0
+
+ else:
+ for i in MTT.TITLES:
+ self.theme[i]["deltaSize"] = 0
+
+ def setUseUnderlineForEmphasis(self, enable):
+ self.useUndlerlineForEmphasis = enable
+ self.rehighlight()
+
+ def setFont(self, fontFamily, fontSize):
+ font = QFont(family=fontFamily, pointSize=fontSize,
+ weight=QFont.Normal, italic=False)
+ self._defaultCharFormat.setFont(font)
+ self.rehighlight()
+
+ def setSpellCheckEnabled(self, enabled):
+ self.spellCheckEnabled = enabled
+ self.rehighlight()
+
+ def setBlockquoteStyle(self, style):
+ self.blockquoteStyle = style
+
+ if style == BS.BlockquoteStyleItalic:
+ self.emphasizeToken[MTT.TokenBlockquote] = True
+ else:
+ self.emphasizeToken[MTT.TokenBlockquote] = False
+
+ self.rehighlight()
+
+ def setHighlightLineBreaks(self, enable):
+ self.highlightLineBreaks = enable
+ self.rehighlight()
+
+ ###########################################################################
+ # GHOSTWRITER SPECIFIC?
+ ###########################################################################
+
+ def onTypingResumed(self):
+ self.typingPaused = False
+
+ def onTypingPaused(self):
+ self.typingPaused = True
+ block = self.document().findBlock(self.editor.textCursor().position())
+ self.rehighlightBlock(block)
+
+ def onHighlightBlockAtPosition(self, position):
+ block = self.document().findBlock(position)
+ self.rehighlightBlock(block)
+
+ def onTextBlockRemoved(self, block):
+ if self.isHeadingBlockState(block.userState):
+ self.headingRemoved.emit(block.position())
+
+ ###########################################################################
+ # SPELLCHECK
+ ###########################################################################
+
+ def spellCheck(self, text):
+ cursorPosition = self.editor.textCursor().position()
+ cursorPosBlock = self.document().findBlock(cursorPosition)
+ cursorPosInBlock = -1
+
+ if self.currentBlock() == cursorPosBlock:
+ cursorPosInBlock = cursorPosition - cursorPosBlock.position()
+
+ misspelledWord = self.dictionary.check(text, 0)
+
+ while not misspelledWord.isNull():
+ startIndex = misspelledWord.position()
+ length = misspelledWord.length()
+
+ if self.typingPaused or cursorPosInBlock != startIndex + length:
+ spellingErrorFormat = self.format(startIndex)
+ spellingErrorFormat.setUnderlineColor(self.spellingErrorColor)
+ spellingErrorFormat.setUnderlineStyle(
+ qApp.stlye().styleHint(QStyle.SH_SpellCheckUnderlineStyle))
+
+ self.setFormat(startIndex, length, spellingErrorFormat)
+
+ startIndex += length
+ misspelledWord = self.dictionary.check(text, startIndex)
+
+ def storeHeadingData(self, token, text):
+ if token.type in [
+ MTT.TokenAtxHeading1,
+ MTT.TokenAtxHeading2,
+ MTT.TokenAtxHeading3,
+ MTT.TokenAtxHeading4,
+ MTT.TokenAtxHeading5,
+ MTT.TokenAtxHeading6]:
+ level = token.type - MTT.TokenAtxHeading1 + 1
+ s = token.position + token.openingMarkupLength
+ l = (token.length
+ - token.openingMarkupLength
+ - token.closingMarkupLength)
+ headingText = text[s:s+l].strip()
+
+ elif token.type == MTT.TokenSetextHeading1Line1:
+ level = 1
+ headingText = text
+
+ elif token.type == MTT.TokenSetextHeading2Line1:
+ level = 2
+ headingText = text
+
+ else:
+ qWarning("MarkdownHighlighter.storeHeadingData() encountered" +
+ " unexpected token: {}".format(token.getType()))
+ return
+
+ # FIXME: TypeError: could not convert 'TextBlockData' to 'QTextBlockUserData'
+ # blockData = self.currentBlockUserData()
+ # if blockData is None:
+ # blockData = TextBlockData(self.document(), self.currentBlock())
+ #
+ # self.setCurrentBlockUserData(blockData)
+ self.headingFound.emit(level, headingText, self.currentBlock())
+
+ def isHeadingBlockState(self, state):
+ return state in [
+ MS.MarkdownStateAtxHeading1,
+ MS.MarkdownStateAtxHeading2,
+ MS.MarkdownStateAtxHeading3,
+ MS.MarkdownStateAtxHeading4,
+ MS.MarkdownStateAtxHeading5,
+ MS.MarkdownStateAtxHeading6,
+ MS.MarkdownStateSetextHeading1Line1,
+ MS.MarkdownStateSetextHeading2Line1,]
+
+
+def getLuminance(color):
+ return (0.30 * color.redF()) + \
+ (0.59 * color.greenF()) + \
+ (0.11 * color.blueF())
+
+
+def applyAlphaToChannel(foreground, background, alpha):
+ return (foreground * alpha) + (background * (1.0 - alpha))
+
+
+def applyAlpha(foreground, background, alpha):
+ blendedColor = QColor(0, 0, 0)
+ normalizedAlpha = alpha / 255.0
+ blendedColor.setRed(applyAlphaToChannel(
+ foreground.red(), background.red(), normalizedAlpha))
+ blendedColor.setGreen(applyAlphaToChannel(
+ foreground.green(), background.green(), normalizedAlpha))
+ blendedColor.setBlue(applyAlphaToChannel(
+ foreground.blue(), background.blue(), normalizedAlpha))
+ return blendedColor
diff --git a/manuskript/ui/highlighters/markdownTokenizer.py b/manuskript/ui/highlighters/markdownTokenizer.py
new file mode 100644
index 00000000..ffb4d7ba
--- /dev/null
+++ b/manuskript/ui/highlighters/markdownTokenizer.py
@@ -0,0 +1,902 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+import re
+from PyQt5.QtCore import *
+from PyQt5.QtGui import *
+from PyQt5.QtWidgets import *
+
+from manuskript.ui.highlighters import MarkdownState as MS
+from manuskript.ui.highlighters import MarkdownTokenType as MTT
+
+# This file is simply a python translation of GhostWriter's Tokenizer.
+# http://wereturtle.github.io/ghostwriter/
+# GPLV3+.
+
+# ==============================================================================
+# TOKEN
+# ==============================================================================
+
+class Token:
+ def __init__(self):
+ self.type = -1
+ self.position = 0
+ self.length = 0
+ self.openingMarkupLength = 0
+ self.closingMarkupLength = 0
+
+# ==============================================================================
+# HIGHLIGHT TOKENIZER
+# ==============================================================================
+
+class HighlightTokenizer:
+ def __init__(self):
+ self.tokens = []
+
+ def tokenize(text, currentState, previousState, nextState):
+ # Subclass me
+ return 0
+
+ def getTokens(self):
+ self.tokens = sorted(self.tokens, key=lambda t: t.position)
+ return self.tokens
+
+ def getState(self):
+ return self.state
+
+ def backtrackRequested(self):
+ return self.backtrack
+
+ def clear(self):
+ self.tokens = []
+ self.backtrack = False
+ self.state = -1
+
+ def addToken(self, token):
+ self.tokens.append(token)
+
+ if token.type == -1:
+ print("Error here", token.position, token.length)
+
+ def setState(self, state):
+ self.state = state
+
+ def requestBacktrack(self):
+ self.backtrack = True
+
+ def tokenLessThan(self, t1, t2):
+ return t1.getPosition() < t2.getPosition()
+
+
+class MarkdownTokenizer(HighlightTokenizer):
+
+ DUMMY_CHAR = "$"
+ MAX_MARKDOWN_HEADING_LEVEL = 6
+
+ paragraphBreakRegex = QRegExp("^\\s*$")
+ heading1SetextRegex = QRegExp("^===+\\s*$")
+ heading2SetextRegex = QRegExp("^---+\\s*$")
+ blockquoteRegex = QRegExp("^ {0,3}>.*$")
+ githubCodeFenceStartRegex = QRegExp("^```+.*$")
+ githubCodeFenceEndRegex = QRegExp("^```+\\s*$")
+ pandocCodeFenceStartRegex = QRegExp("^~~~+.*$")
+ pandocCodeFenceEndRegex = QRegExp("^~~~+\\s*$")
+ numberedListRegex = QRegExp("^ {0,3}[0-9a-z]+[.)]\\s+.*$")
+ numberedNestedListRegex = QRegExp("^\\s*[0-9a-z]+[.)]\\s+.*$")
+ hruleRegex = QRegExp("\\s*(\\*\\s*){3,}|(\\s*(_\\s*){3,})|((\\s*(-\\s*){3,}))")
+ lineBreakRegex = QRegExp(".*\\s{2,}$")
+ emphasisRegex = QRegExp("(\\*(?![\\s*]).*[^\\s*]\\*)|_(?![\\s_]).*[^\\s_]_")
+ emphasisRegex.setMinimal(True)
+ strongRegex = QRegExp("\\*\\*(?=\\S).*\\S\\*\\*(?!\\*)|__(?=\\S).*\\S__(?!_)")
+ strongRegex.setMinimal(True)
+ strikethroughRegex = QRegExp("~~[^\\s]+.*[^\\s]+~~")
+ strikethroughRegex.setMinimal(True)
+ superScriptRegex = QRegExp("\^([^\\s]|(\\\\\\s))+\^") # Spaces must be escaped "\ "
+ superScriptRegex.setMinimal(True)
+ subScriptRegex = QRegExp("~([^\\s]|(\\\\\\s))+~") # Spaces must be escaped "\ "
+ subScriptRegex.setMinimal(True)
+ verbatimRegex = QRegExp("`+")
+ htmlTagRegex = QRegExp("<[^<>]+>")
+ htmlTagRegex.setMinimal(True)
+ htmlEntityRegex = QRegExp("&[a-zA-Z]+;|?[0-9]+;")
+ automaticLinkRegex = QRegExp("(<[a-zA-Z]+\\:.+>)|(<.+@.+>)")
+ automaticLinkRegex.setMinimal(True)
+ inlineLinkRegex = QRegExp("\\[.+\\]\\(.+\\)")
+ inlineLinkRegex.setMinimal(True)
+ referenceLinkRegex = QRegExp("\\[(.+)\\]")
+ referenceLinkRegex.setMinimal(True)
+ referenceDefinitionRegex = QRegExp("^\\s*\\[.+\\]:")
+ imageRegex = QRegExp("!\\[.*\\]\\(.+\\)")
+ imageRegex.setMinimal(True)
+ htmlInlineCommentRegex = QRegExp("")
+ htmlInlineCommentRegex.setMinimal(True)
+ mentionRegex = QRegExp("\\B@\\w+(\\-\\w+)*(/\\w+(\\-\\w+)*)?")
+ pipeTableDividerRegex = QRegExp("^ {0,3}(\\|[ :]?)?-{3,}([ :]?\\|[ :]?-{3,}([ :]?\\|)?)+\\s*$")
+ CMAdditionRegex = QRegExp("(\\{\\+\\+.*\\+\\+\\})")
+ CMAdditionRegex.setMinimal(True)
+ CMDeletionRegex = QRegExp("(\\{--.*--\\})")
+ CMDeletionRegex.setMinimal(True)
+ CMSubstitutionRegex = QRegExp("(\\{~~.*~>.*~~\\})")
+ CMSubstitutionRegex.setMinimal(True)
+ CMCommentRegex = QRegExp("(\\{>>.*<<\\})")
+ CMCommentRegex.setMinimal(True)
+ CMHighlightRegex = QRegExp("(\\{==.*==\\})")
+ CMHighlightRegex.setMinimal(True)
+
+ def __init__(self):
+ HighlightTokenizer.__init__(self)
+
+ def tokenize(self, text, currentState, previousState, nextState):
+ self.currentState = currentState
+ self.previousState = previousState
+ self.nextState = nextState
+
+ if (self.previousState == MS.MarkdownStateInGithubCodeFence or \
+ self.previousState == MS.MarkdownStateInPandocCodeFence) and \
+ self.tokenizeCodeBlock(text):
+ # No further tokenizing required
+ pass
+
+ elif self.previousState != MS.MarkdownStateComment \
+ and self.paragraphBreakRegex.exactMatch(text):
+
+ if previousState in [MS.MarkdownStateListLineBreak,
+ MS.MarkdownStateNumberedList,
+ MS.MarkdownStateBulletPointList]:
+ self.setState(MS.MarkdownStateListLineBreak)
+ elif previousState != MS.MarkdownStateCodeBlock or \
+ (text[:1] != "\t" and text[-4:] != " "):
+ self.setState(MS.MarkdownStateParagraphBreak)
+
+ elif self.tokenizeSetextHeadingLine2(text) or \
+ self.tokenizeCodeBlock(text) or \
+ self.tokenizeMultilineComment(text) or \
+ self.tokenizeHorizontalRule(text) or \
+ self.tokenizeTableDivider(text):
+ # No further tokenizing required
+ pass
+
+ elif self.tokenizeSetextHeadingLine1(text) or \
+ self.tokenizeAtxHeading(text) or \
+ self.tokenizeBlockquote(text) or \
+ self.tokenizeNumberedList(text) or \
+ self.tokenizeBulletPointList(text):
+ self.tokenizeLineBreak(text)
+ self.tokenizeInline(text)
+
+ else:
+ if previousState in [MS.MarkdownStateListLineBreak,
+ MS.MarkdownStateNumberedList,
+ MS.MarkdownStateNumberedList]:
+ if not self.tokenizeNumberedList(text) and \
+ not self.tokenizeBulletPointList(text) and \
+ (text[:1] == "\t" or text[:4] == " "):
+ self.setState(previousState)
+ else:
+ self.setState(MS.MarkdownStateParagraph)
+ else:
+ self.setState(MS.MarkdownStateParagraph)
+ self.tokenizeLineBreak(text)
+ self.tokenizeInline(text)
+
+ # Make sure that if the second line of a setext heading is removed the
+ # first line is reprocessed. Otherwise, it will still show up in the
+ # document as a heading.
+ if (previousState == MS.MarkdownStateSetextHeading1Line1 and \
+ self.getState() != MS.MarkdownStateSetextHeading1Line2) or \
+ (previousState == MS.MarkdownStateSetextHeading2Line1 and \
+ self.getState() != MS.MarkdownStateSetextHeading2Line2):
+ self.requestBacktrack()
+
+ def tokenizeSetextHeadingLine1(self, text):
+ #Check the next line's state to see if this is a setext-style heading.
+ level = 0
+ token = Token()
+ nextState = self.nextState
+
+ if MS.MarkdownStateSetextHeading1Line2 == nextState:
+ level = 1
+ self.setState(MS.MarkdownStateSetextHeading1Line1)
+ token.type = MTT.TokenSetextHeading1Line1
+
+ elif MS.MarkdownStateSetextHeading2Line2 == nextState:
+ level = 2
+ self.setState(MS.MarkdownStateSetextHeading2Line1)
+ token.type = MTT.TokenSetextHeading2Line1
+
+ if level > 0:
+ token.length = len(text)
+ token.position = 0
+ self.addToken(token)
+ return True
+
+ return False
+
+ def tokenizeSetextHeadingLine2(self, text):
+ level = 0
+ setextMatch = False
+ token = Token()
+ previousState = self.previousState
+ if previousState == MS.MarkdownStateSetextHeading1Line1:
+ level = 1
+ setextMatch = self.heading1SetextRegex.exactMatch(text)
+ self.setState(MS.MarkdownStateSetextHeading1Line2)
+ token.type = MTT.TokenSetextHeading1Line2
+
+ elif previousState == MS.MarkdownStateSetextHeading2Line1:
+ level = 2
+ setextMatch = self.heading2SetextRegex.exactMatch(text)
+ self.setState(MS.MarkdownStateSetextHeading2Line2)
+ token.type = MTT.TokenSetextHeading2Line2
+
+ elif previousState == MS.MarkdownStateParagraph:
+ h1Line2 = self.heading1SetextRegex.exactMatch(text)
+ h2Line2 = self.heading2SetextRegex.exactMatch(text)
+
+ if h1Line2 or h2Line2:
+ # Restart tokenizing on the previous line.
+ self.requestBacktrack()
+ token.length = len(text)
+ token.position = 0
+
+ if h1Line2:
+ self.setState(MS.MarkdownStateSetextHeading1Line2)
+ token.type = MTT.TokenSetextHeading1Line2
+
+ else:
+ self.setState(MS.MarkdownStateSetextHeading2Line2)
+ token.type = MTT.TokenSetextHeading2Line2
+
+ self.addToken(token)
+ return True
+
+ if level > 0:
+ if setextMatch:
+ token.length = len(text)
+ token.position = 0
+ self.addToken(token)
+ return True
+
+ else:
+ # Restart tokenizing on the previous line.
+ self.requestBacktrack()
+ False
+
+ return False
+
+ def tokenizeAtxHeading(self, text):
+ escapedText = self.dummyOutEscapeCharacters(text)
+ trailingPoundCount = 0
+ level = 0
+
+ #Count the number of pound signs at the front of the string,
+ #up to the maximum allowed, to determine the heading level.
+
+ while escapedText[level] == "#":
+ level += 1
+ if level >= len(escapedText) or level >= self.MAX_MARKDOWN_HEADING_LEVEL:
+ break
+
+ if level > 0 and level < len(text):
+ # Count how many pound signs are at the end of the text.
+ while escapedText[-trailingPoundCount -1] == "#":
+ trailingPoundCount += 1
+
+ token = Token()
+ token.position = 0
+ token.length = len(text)
+ token.type = MTT.TokenAtxHeading1 + level -1
+ token.openingMarkupLength = level
+ token.closingMarkupLength = trailingPoundCount
+ self.addToken(token)
+ self.setState(MS.MarkdownStateAtxHeading1 + level -1)
+ return True
+ return False
+
+ def tokenizeNumberedList(self, text):
+ previousState = self.previousState
+ if (previousState in [MS.MarkdownStateParagraphBreak,
+ MS.MarkdownStateUnknown,
+ MS.MarkdownStateCodeBlock,
+ MS.MarkdownStateCodeFenceEnd,] and \
+ self.numberedListRegex.exactMatch(text)) or \
+ (previousState in [MS.MarkdownStateListLineBreak,
+ MS.MarkdownStateNumberedList,
+ MS.MarkdownStateBulletPointList,] and \
+ self.numberedNestedListRegex.exactMatch(text)):
+ periodIndex = text.find(".")
+ parenthIndex = text.find(")")
+
+ if periodIndex < 0:
+ index = parenthIndex
+ elif parenthIndex < 0:
+ index = periodIndex
+ elif parenthIndex > periodIndex:
+ index = periodIndex
+ else:
+ index = parenthIndex
+
+ if index > 0:
+ token = Token()
+ token.type = MTT.TokenNumberedList
+ token.position = 0
+ token.length = len(text)
+ token.openingMarkupLength = index + 2
+ self.addToken(token)
+ self.setState(MS.MarkdownStateNumberedList)
+ return True
+
+ return False
+
+ return False
+
+ def tokenizeBulletPointList(self, text):
+ foundBulletChar = False
+ bulletCharIndex = -1
+ spaceCount = 0
+ whitespaceFoundAfterBulletChar = False
+ previousState = self.previousState
+
+ if previousState not in [MS.MarkdownStateUnknown,
+ MS.MarkdownStateParagraphBreak,
+ MS.MarkdownStateListLineBreak,
+ MS.MarkdownStateNumberedList,
+ MS.MarkdownStateBulletPointList,
+ MS.MarkdownStateCodeBlock,
+ MS.MarkdownStateCodeFenceEnd]:
+ return False
+
+ # Search for the bullet point character, which can
+ # be either a '+', '-', or '*'.
+
+ for i in range(len(text)):
+ if text[i] == " ":
+ if foundBulletChar:
+ # We've confirmed it's a bullet point by the whitespace that
+ # follows the bullet point character, and can now exit the
+ # loop.
+
+ whitespaceFoundAfterBulletChar = True
+ break
+
+ else:
+ spaceCount += 1
+
+ # If this list item is the first in the list, ensure the
+ # number of spaces preceeding the bullet point does not
+ # exceed three, as that would indicate a code block rather
+ # than a bullet point list.
+
+ if spaceCount > 3 and previousState not in [
+ MS.MarkdownStateNumberedList,
+ MS.MarkdownStateBulletPointList,
+ MS.MarkdownStateListLineBreak,] and \
+ previousState in [
+ MS.MarkdownStateParagraphBreak,
+ MS.MarkdownStateUnknown,
+ MS.MarkdownStateCodeBlock,
+ MS.MarkdownStateCodeFenceEnd,]:
+ return False
+
+ elif text[i] == "\t":
+ if foundBulletChar:
+ # We've confirmed it's a bullet point by the whitespace that
+ # follows the bullet point character, and can now exit the
+ # loop.
+
+ whitespaceFoundAfterBulletChar = True
+ break
+
+ elif previousState in [
+ MS.MarkdownStateParagraphBreak,
+ MS.MarkdownStateUnknown]:
+
+ # If this list item is the first in the list, ensure that
+ # no tab character preceedes the bullet point, as that would
+ # indicate a code block rather than a bullet point list.
+
+ return False
+
+ elif text[i] in ["+", "-", "*"]:
+ foundBulletChar = True
+ bulletCharIndex = i
+
+ else:
+ return False
+
+ if bulletCharIndex >= 0 and whitespaceFoundAfterBulletChar:
+ token = Token()
+ token.type = MTT.TokenBulletPointList
+ token.position = 0
+ token.length = len(text)
+ token.openingMarkupLength = bulletCharIndex + 2
+ self.addToken(token)
+ self.setState(MS.MarkdownStateBulletPointList)
+ return True
+
+ return False
+
+ def tokenizeHorizontalRule (self, text):
+ if self.hruleRegex.exactMatch(text):
+ token = Token()
+ token.type = MTT.TokenHorizontalRule
+ token.position = 0
+ token.length = len(text)
+ self.addToken(token)
+ self.setState(MS.MarkdownStateHorizontalRule)
+ return True
+
+ return False
+
+ def tokenizeLineBreak(self, text):
+ currentState = self.currentState
+ previousState = self.previousState
+ nextState = self.nextState
+
+ if currentState in [
+ MS.MarkdownStateParagraph,
+ MS.MarkdownStateBlockquote,
+ MS.MarkdownStateNumberedList,
+ MS.MarkdownStateBulletPointList,]:
+ if previousState in [
+ MS.MarkdownStateParagraph,
+ MS.MarkdownStateBlockquote,
+ MS.MarkdownStateNumberedList,
+ MS.MarkdownStateBulletPointList,]:
+ self.requestBacktrack()
+
+ if nextState in [
+ MS.MarkdownStateParagraph,
+ MS.MarkdownStateBlockquote,
+ MS.MarkdownStateNumberedList,
+ MS.MarkdownStateBulletPointList,]:
+ self.requestBacktrack()
+ if self.lineBreakRegex.exactMatch(text):
+ token = Token()
+ token.type = MTT.TokenLineBreak
+ token.position = len(text) - 1
+ token.length = 1
+ self.addToken(token)
+ return True
+
+ return False
+
+ def tokenizeBlockquote(self, text):
+ previousState = self.previousState
+ if previousState == MS.MarkdownStateBlockquote or \
+ self.blockquoteRegex.exactMatch(text):
+
+ # Find any '>' characters at the front of the line.
+ markupLength = 0
+
+ for i in range(len(text)):
+ if text[i] == ">":
+ markupLength = i + 1
+ elif text[i] != " ":
+ # There are no more '>' characters at the front of the line,
+ # so stop processing.
+ break
+
+ token = Token()
+ token.type = MTT.TokenBlockquote
+ token.position = 0
+ token.length = len(text)
+
+ if markupLength > 0:
+ token.openingMarkupLength = markupLength
+
+ self.addToken(token)
+ self.setState(MS.MarkdownStateBlockquote)
+ return True
+ return False
+
+ def tokenizeCodeBlock(self, text):
+ previousState = self.previousState
+ if previousState in [
+ MS.MarkdownStateInGithubCodeFence,
+ MS.MarkdownStateInPandocCodeFence]:
+ self.setState(previousState)
+
+ if (previousState == MS.MarkdownStateInGithubCodeFence and \
+ self.githubCodeFenceEndRegex.exactMatch(text)) or \
+ (previousState == MS.MarkdownStateInPandocCodeFence and \
+ self.pandocCodeFenceEndRegex.exactMatch(text)):
+ token = Token()
+ token.type = MTT.TokenCodeFenceEnd
+ token.position = 0
+ token.length = len(text)
+ self.addToken(token)
+ self.setState(MS.MarkdownStateCodeFenceEnd)
+
+ else:
+ token = Token()
+ token.type = MTT.TokenCodeBlock
+ token.position = 0
+ token.length = len(text)
+ self.addToken(token)
+
+ return True
+
+ elif previousState in [
+ MS.MarkdownStateCodeBlock,
+ MS.MarkdownStateParagraphBreak,
+ MS.MarkdownStateUnknown,] and \
+ (text[:1] == "\t" or text[:4] == " "):
+ token = Token()
+ token.type = MTT.TokenCodeBlock
+ token.position = 0
+ token.length = len(text)
+ token.openingMarkupLength = len(text) - len(text.lstrip())
+ self.addToken(token)
+ self.setState(MS.MarkdownStateCodeBlock)
+ return True
+
+ elif previousState in [
+ MS.MarkdownStateParagraphBreak,
+ MS.MarkdownStateParagraph,
+ MS.MarkdownStateUnknown,
+ MS.MarkdownStateListLineBreak,]:
+ foundCodeFenceStart = False
+ token = Token()
+ if self.githubCodeFenceStartRegex.exactMatch(text):
+ foundCodeFenceStart = True
+ token.type = MTT.TokenGithubCodeFence
+ self.setState(MS.MarkdownStateInGithubCodeFence)
+
+ elif self.pandocCodeFenceStartRegex.exactMatch(text):
+ foundCodeFenceStart = True
+ token.type = MTT.TokenPandocCodeFence
+ self.setState(MS.MarkdownStateInPandocCodeFence)
+
+ if foundCodeFenceStart:
+ token.position = 0
+ token.length = len(text)
+ self.addToken(token)
+ return True
+
+ return False
+
+ def tokenizeMultilineComment(self, text):
+ previousState = self.previousState
+
+ if previousState == MS.MarkdownStateComment:
+ # Find the end of the comment, if any.
+ index = text.find("-->")
+ token = Token()
+ token.type = MTT.TokenHtmlComment
+ token.position = 0
+
+ if index >= 0:
+ token.length = index + 3
+ self.addToken(token)
+
+ # Return false so that the rest of the line that isn't within
+ # the commented segment can be highlighted as normal paragraph
+ # text.
+
+ else:
+ token.length = len(text)
+ self.addToken(token)
+ self.setState(MS.MarkdownStateComment)
+ return True
+
+ return False
+
+ def tokenizeInline(self, text):
+ escapedText = self.dummyOutEscapeCharacters(text)
+
+ # Check if the line is a reference definition.
+ if self.referenceDefinitionRegex.exactMatch(text):
+ colonIndex = escapedText.find(":")
+ token = Token()
+ token.type = MTT.TokenReferenceDefinition
+ token.position = 0
+ token.length = colonIndex + 1
+ self.addToken(token)
+
+ # Replace the first bracket so that the '[...]:' reference definition
+ # start doesn't get highlighted as a reference link.
+
+ firstBracketIndex = escapedText.find("[")
+ if firstBracketIndex >= 0:
+ i = firstBracketIndex
+ escapedText = escapedText[:i] + self.DUMMY_CHAR + escapedText[i+1:]
+
+ escapedText = self.tokenizeVerbatim(escapedText)
+ escapedText = self.tokenizeHtmlComments(escapedText)
+ escapedText = self.tokenizeTableHeaderRow(escapedText)
+ escapedText = self.tokenizeTableRow(escapedText)
+ escapedText = self.tokenizeMatches(MTT.TokenImage, escapedText, self.imageRegex, 0, 0, False, True)
+ escapedText = self.tokenizeMatches(MTT.TokenInlineLink, escapedText, self.inlineLinkRegex, 0, 0, False, True)
+ escapedText = self.tokenizeMatches(MTT.TokenReferenceLink, escapedText, self.referenceLinkRegex, 0, 0, False, True)
+ escapedText = self.tokenizeMatches(MTT.TokenHtmlEntity, escapedText, self.htmlEntityRegex)
+ escapedText = self.tokenizeMatches(MTT.TokenAutomaticLink, escapedText, self.automaticLinkRegex, 0, 0, False, True)
+ escapedText = self.tokenizeMatches(MTT.TokenStrong, escapedText, self.strongRegex, 2, 2, True)
+ escapedText = self.tokenizeMatches(MTT.TokenEmphasis, escapedText, self.emphasisRegex, 1, 1, True)
+ escapedText = self.tokenizeMatches(MTT.TokenMention, escapedText, self.mentionRegex, 0, 0, False, True)
+ escapedText = self.tokenizeMatches(MTT.TokenCMAddition, escapedText, self.CMAdditionRegex, 3, 3, True)
+ escapedText = self.tokenizeMatches(MTT.TokenCMDeletion, escapedText, self.CMDeletionRegex, 3, 3, True)
+ escapedText = self.tokenizeMatches(MTT.TokenCMSubstitution, escapedText, self.CMSubstitutionRegex, 3, 3, True)
+ escapedText = self.tokenizeMatches(MTT.TokenCMComment, escapedText, self.CMCommentRegex, 3, 3, True)
+ escapedText = self.tokenizeMatches(MTT.TokenCMHighlight, escapedText, self.CMHighlightRegex, 3, 3, True)
+ escapedText = self.tokenizeMatches(MTT.TokenStrikethrough, escapedText, self.strikethroughRegex, 2, 2, True)
+ escapedText = self.tokenizeMatches(MTT.TokenHtmlTag, escapedText, self.htmlTagRegex)
+ escapedText = self.tokenizeMatches(MTT.TokenSubScript, escapedText, self.subScriptRegex, 1, 1, True)
+ escapedText = self.tokenizeMatches(MTT.TokenSuperScript, escapedText, self.superScriptRegex, 1, 1, True)
+
+ return True
+
+ def tokenizeVerbatim(self, text):
+ index = self.verbatimRegex.indexIn(text)
+
+ while index >= 0:
+ end = ""
+ count = self.verbatimRegex.matchedLength()
+
+ # Search for the matching end, which should have the same number
+ # of back ticks as the start.
+ for i in range(count):
+ end += '`'
+
+ endIndex = text.find(end, index + count)
+
+ # If the end was found, add the verbatim token.
+ if endIndex >= 0:
+ token = Token()
+ token.type = MTT.TokenVerbatim
+ token.position = index
+ token.length = endIndex + count - index
+ token.openingMarkupLength = count
+ token.closingMarkupLength = count
+ self.addToken(token)
+
+ # Fill out the token match in the string with the dummy
+ # character so that searches for other Markdown elements
+ # don't find anything within this token's range in the string.
+
+ for i in range(index, index + token.length):
+ text = text[:i] + self.DUMMY_CHAR + text[i+1:]
+
+ index += token.length
+
+ # Else start searching again at the very next character.
+ else:
+ index += 1
+
+ index = self.verbatimRegex.indexIn(text, index)
+ return text
+
+ def tokenizeHtmlComments(self, text):
+ previousState = self.previousState
+
+ # Check for the end of a multiline comment so that it doesn't get further
+ # tokenized. Don't bother formatting the comment itself, however, because
+ # it should have already been tokenized in tokenizeMultilineComment().
+ if previousState == MS.MarkdownStateComment:
+ commentEnd = text.find("-->")
+ for i in range(commentEnd + 3):
+ text = text[:i] + self.DUMMY_CHAR + text[i+1:]
+
+ # Now check for inline comments (non-multiline).
+ commentStart = self.htmlInlineCommentRegex.indexIn(text)
+
+ while commentStart >= 0:
+ commentLength = self.htmlInlineCommentRegex.matchedLength()
+ token = Token()
+ token.type = MTT.TokenHtmlComment
+ token.position = commentStart
+ token.length = commentLength
+ self.addToken(token)
+
+ # Replace comment segment with dummy characters so that it doesn't
+ # get tokenized again.
+
+ for i in range(commentStart, commentStart + commentLength):
+ text = text[:i] + self.DUMMY_CHAR + text[i+1:]
+
+ commentStart = self.htmlInlineCommentRegex.indexIn(text, commentStart + commentLength)
+
+ # Find multiline comment start, if any.
+ commentStart = text.find("")
+ else:
+ cursor.insertText("")
+ cursor.movePosition(QTextCursor.PreviousCharacter,
+ QTextCursor.MoveAnchor, 4)
+ self.setTextCursor(cursor)
+
+ def commentLine(self):
+ cursor = self.textCursor()
+
+ start = cursor.selectionStart()
+ end = cursor.selectionEnd()
+ block = self.document().findBlock(start)
+ block2 = self.document().findBlock(end)
+
+ if True:
+ # Method 1
+ cursor.beginEditBlock()
+ while block.isValid():
+ self.commentBlock(block)
+ if block == block2: break
+ block = block.next()
+ cursor.endEditBlock()
+
+ else:
+ # Method 2
+ cursor.beginEditBlock()
+ cursor.setPosition(block.position())
+ cursor.insertText("")
+ cursor.endEditBlock()
+
+ def commentBlock(self, block):
+ cursor = QTextCursor(block)
+ text = block.text()
+ if text[:5] == "":
+ text2 = text[5:-4]
+ else:
+ text2 = ""
+ self.selectBlock(cursor)
+ cursor.insertText(text2)
+
+ def insertFormattingMarkup(self, markup):
+ cursor = self.textCursor()
+
+ # Select begining and end of words
+ self.selectWord(cursor)
+
+ if cursor.hasSelection():
+ start = cursor.selectionStart()
+ end = cursor.selectionEnd() + len(markup)
+ cursor.beginEditBlock()
+ cursor.setPosition(start)
+ cursor.insertText(markup)
+ cursor.setPosition(end)
+ cursor.insertText(markup)
+ cursor.endEditBlock()
+ cursor.movePosition(QTextCursor.PreviousCharacter,
+ QTextCursor.KeepAnchor, len(markup))
+ #self.setTextCursor(cursor)
+
+ else:
+ # Insert markup twice (for opening and closing around the cursor),
+ # and then move the cursor to be between the pair.
+ cursor.beginEditBlock()
+ cursor.insertText(markup)
+ cursor.insertText(markup)
+ cursor.movePosition(QTextCursor.PreviousCharacter,
+ QTextCursor.MoveAnchor, len(markup))
+ cursor.endEditBlock()
+ self.setTextCursor(cursor)
+
+ def clearFormat(self):
+ cursor = self.textCursor()
+ text = cursor.selectedText()
+ if not text:
+ self.selectBlock(cursor)
+ text = cursor.selectedText()
+ text = self.clearedFormat(text)
+ cursor.insertText(text)
+
+ def clearedFormat(self, text):
+ # FIXME: clear also block formats
+ for reg, rep, flags in [
+ ("\*\*(.*?)\*\*", "\\1", None), # bold
+ ("__(.*?)__", "\\1", None), # bold
+ ("\*(.*?)\*", "\\1", None), # emphasis
+ ("_(.*?)_", "\\1", None), # emphasis
+ ("`(.*?)`", "\\1", None), # verbatim
+ ("~~(.*?)~~", "\\1", None), # strike
+ ("\^(.*?)\^", "\\1", None), # superscript
+ ("~(.*?)~", "\\1", None), # subscript
+ ("", "\\1", re.S), # comments
+
+
+ # LINES OR BLOCKS
+ (r"^#*\s*(.+?)\s*", "\\1", re.M), # ATX
+ (r"^[=-]*$", "", re.M), # Setext
+ (r"^`*$", "", re.M), # Code block fenced
+ (r"^\s*[-+*]\s*(.*?)\s*$", "\\1", re.M), # Bullet List
+ (r"^\s*[0-9a-z](\.|\))\s*(.*?)\s*$", "\\2", re.M), # Bullet List
+ (r"\s*[>\s]*(.*?)\s*$", "\\1", re.M), # Code block and blockquote
+
+ ]:
+ text = re.sub(reg, rep, text, flags if flags else 0)
+ return text
+
+ def clearedFormatForStats(self, text):
+ # Remove stuff that musn't be counted
+ # FIXME: clear also block formats
+ for reg, rep, flags in [
+ ("", "", re.S), # comments
+ ]:
+ text = re.sub(reg, rep, text, flags if flags else 0)
+ return text
+
+ def titleSetext(self, level):
+ cursor = self.textCursor()
+
+ cursor.beginEditBlock()
+ # Is it already a Setext header?
+ if cursor.block().userState() in [
+ MS.MarkdownStateSetextHeading1Line2,
+ MS.MarkdownStateSetextHeading2Line2]:
+ cursor.movePosition(QTextCursor.PreviousBlock)
+
+ text = cursor.block().text()
+
+ if cursor.block().userState() in [
+ MS.MarkdownStateSetextHeading1Line1,
+ MS.MarkdownStateSetextHeading2Line1]:
+ # Need to remove line below
+ c = QTextCursor(cursor.block().next())
+ self.selectBlock(c)
+ c.insertText("")
+
+ char = "=" if level == 1 else "-"
+ text = re.sub("^#*\s*(.*)\s*#*", "\\1", text) # Removes #
+ sub = char * len(text)
+ text = text + "\n" + sub
+
+ self.selectBlock(cursor)
+ cursor.insertText(text)
+ cursor.endEditBlock()
+
+ def titleATX(self, level):
+ cursor = self.textCursor()
+ text = cursor.block().text()
+
+ # Are we in a Setext Header?
+ if cursor.block().userState() in [
+ MS.MarkdownStateSetextHeading1Line1,
+ MS.MarkdownStateSetextHeading2Line1]:
+ # Need to remove line below
+ cursor.beginEditBlock()
+ c = QTextCursor(cursor.block().next())
+ self.selectBlock(c)
+ c.insertText("")
+
+ self.selectBlock(cursor)
+ cursor.insertText(text)
+ cursor.endEditBlock()
+ return
+
+ elif cursor.block().userState() in [
+ MS.MarkdownStateSetextHeading1Line2,
+ MS.MarkdownStateSetextHeading2Line2]:
+ cursor.movePosition(QTextCursor.PreviousBlock)
+ self.setTextCursor(cursor)
+ self.titleATX(level)
+ return
+
+ m = re.match("^(#+)(\s*)(.+)", text)
+ if m:
+ pre = m.group(1)
+ space = m.group(2)
+ txt = m.group(3)
+
+ if len(pre) == level:
+ # Remove title
+ text = txt
+ else:
+ text = "#" * level + space + txt
+
+ else:
+ text = "#" * level + " " + text
+
+ self.selectBlock(cursor)
+ cursor.insertText(text)
diff --git a/manuskript/ui/views/basicItemView_ui.py b/manuskript/ui/views/basicItemView_ui.py
index f0594fad..91e09124 100644
--- a/manuskript/ui/views/basicItemView_ui.py
+++ b/manuskript/ui/views/basicItemView_ui.py
@@ -2,8 +2,7 @@
# Form implementation generated from reading ui file 'manuskript/ui/views/basicItemView_ui.ui'
#
-# Created: Thu Mar 3 17:26:11 2016
-# by: PyQt5 UI code generator 5.2.1
+# Created by: PyQt5 UI code generator 5.5.1
#
# WARNING! All changes made in this file will be lost!
@@ -51,7 +50,7 @@ class Ui_basicItemView(object):
self.label_9 = QtWidgets.QLabel(basicItemView)
self.label_9.setObjectName("label_9")
self.verticalLayout.addWidget(self.label_9)
- self.txtSummaryFull = textEditView(basicItemView)
+ self.txtSummaryFull = MDEditCompleter(basicItemView)
self.txtSummaryFull.setObjectName("txtSummaryFull")
self.verticalLayout.addWidget(self.txtSummaryFull)
@@ -67,6 +66,6 @@ class Ui_basicItemView(object):
self.txtSummarySentence.setPlaceholderText(_translate("basicItemView", "One line summary"))
self.label_9.setText(_translate("basicItemView", "Few sentences summary:"))
+from manuskript.ui.views.MDEditCompleter import MDEditCompleter
from manuskript.ui.views.cmbOutlineCharacterChoser import cmbOutlineCharacterChoser
from manuskript.ui.views.lineEditView import lineEditView
-from manuskript.ui.views.textEditView import textEditView
diff --git a/manuskript/ui/views/basicItemView_ui.ui b/manuskript/ui/views/basicItemView_ui.ui
index 6466cc73..efccc995 100644
--- a/manuskript/ui/views/basicItemView_ui.ui
+++ b/manuskript/ui/views/basicItemView_ui.ui
@@ -101,15 +101,15 @@
-
-
+
- textEditView
+ MDEditCompleter
QTextEdit
- manuskript.ui.views.textEditView.h
+ manuskript.ui.views.MDEditCompleter.h
cmbOutlineCharacterChoser
diff --git a/manuskript/ui/views/metadataView_ui.py b/manuskript/ui/views/metadataView_ui.py
index 2069a06d..ba1b6591 100644
--- a/manuskript/ui/views/metadataView_ui.py
+++ b/manuskript/ui/views/metadataView_ui.py
@@ -2,8 +2,7 @@
# Form implementation generated from reading ui file 'manuskript/ui/views/metadataView_ui.ui'
#
-# Created: Fri Apr 8 14:24:47 2016
-# by: PyQt5 UI code generator 5.2.1
+# Created by: PyQt5 UI code generator 5.5.1
#
# WARNING! All changes made in this file will be lost!
@@ -14,8 +13,8 @@ class Ui_metadataView(object):
metadataView.setObjectName("metadataView")
metadataView.resize(400, 537)
self.verticalLayout = QtWidgets.QVBoxLayout(metadataView)
- self.verticalLayout.setSpacing(0)
self.verticalLayout.setContentsMargins(0, 0, 0, 0)
+ self.verticalLayout.setSpacing(0)
self.verticalLayout.setObjectName("verticalLayout")
self.grpProperties = collapsibleGroupBox2(metadataView)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
@@ -27,8 +26,8 @@ class Ui_metadataView(object):
self.grpProperties.setCheckable(True)
self.grpProperties.setObjectName("grpProperties")
self.verticalLayout_28 = QtWidgets.QVBoxLayout(self.grpProperties)
- self.verticalLayout_28.setSpacing(0)
self.verticalLayout_28.setContentsMargins(0, 0, 0, 0)
+ self.verticalLayout_28.setSpacing(0)
self.verticalLayout_28.setObjectName("verticalLayout_28")
self.properties = propertiesView(self.grpProperties)
self.properties.setMinimumSize(QtCore.QSize(0, 50))
@@ -40,8 +39,8 @@ class Ui_metadataView(object):
self.grpSummary.setCheckable(True)
self.grpSummary.setObjectName("grpSummary")
self.verticalLayout_22 = QtWidgets.QVBoxLayout(self.grpSummary)
- self.verticalLayout_22.setSpacing(0)
self.verticalLayout_22.setContentsMargins(0, 0, 0, 0)
+ self.verticalLayout_22.setSpacing(0)
self.verticalLayout_22.setObjectName("verticalLayout_22")
self.txtSummarySentence = lineEditView(self.grpSummary)
self.txtSummarySentence.setInputMask("")
@@ -53,10 +52,9 @@ class Ui_metadataView(object):
self.line.setLineWidth(0)
self.line.setMidLineWidth(0)
self.line.setFrameShape(QtWidgets.QFrame.HLine)
- self.line.setFrameShadow(QtWidgets.QFrame.Sunken)
self.line.setObjectName("line")
self.verticalLayout_22.addWidget(self.line)
- self.txtSummaryFull = textEditView(self.grpSummary)
+ self.txtSummaryFull = MDEditCompleter(self.grpSummary)
self.txtSummaryFull.setFrameShape(QtWidgets.QFrame.NoFrame)
self.txtSummaryFull.setObjectName("txtSummaryFull")
self.verticalLayout_22.addWidget(self.txtSummaryFull)
@@ -66,10 +64,10 @@ class Ui_metadataView(object):
self.grpNotes.setCheckable(True)
self.grpNotes.setObjectName("grpNotes")
self.horizontalLayout_29 = QtWidgets.QHBoxLayout(self.grpNotes)
- self.horizontalLayout_29.setSpacing(0)
self.horizontalLayout_29.setContentsMargins(0, 0, 0, 0)
+ self.horizontalLayout_29.setSpacing(0)
self.horizontalLayout_29.setObjectName("horizontalLayout_29")
- self.txtNotes = textEditCompleter(self.grpNotes)
+ self.txtNotes = MDEditCompleter(self.grpNotes)
self.txtNotes.setFrameShape(QtWidgets.QFrame.NoFrame)
self.txtNotes.setObjectName("txtNotes")
self.horizontalLayout_29.addWidget(self.txtNotes)
@@ -79,8 +77,8 @@ class Ui_metadataView(object):
self.grpRevisions.setCheckable(True)
self.grpRevisions.setObjectName("grpRevisions")
self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.grpRevisions)
- self.verticalLayout_2.setSpacing(0)
self.verticalLayout_2.setContentsMargins(0, 0, 0, 0)
+ self.verticalLayout_2.setSpacing(0)
self.verticalLayout_2.setObjectName("verticalLayout_2")
self.revisions = revisions(self.grpRevisions)
self.revisions.setMinimumSize(QtCore.QSize(0, 50))
@@ -103,8 +101,7 @@ class Ui_metadataView(object):
self.grpRevisions.setTitle(_translate("metadataView", "Revisions"))
from manuskript.ui.collapsibleGroupBox2 import collapsibleGroupBox2
-from manuskript.ui.views.textEditView import textEditView
-from manuskript.ui.views.textEditCompleter import textEditCompleter
-from manuskript.ui.views.propertiesView import propertiesView
-from manuskript.ui.views.lineEditView import lineEditView
from manuskript.ui.revisions import revisions
+from manuskript.ui.views.MDEditCompleter import MDEditCompleter
+from manuskript.ui.views.lineEditView import lineEditView
+from manuskript.ui.views.propertiesView import propertiesView
diff --git a/manuskript/ui/views/metadataView_ui.ui b/manuskript/ui/views/metadataView_ui.ui
index 13b97c1f..1733dadf 100644
--- a/manuskript/ui/views/metadataView_ui.ui
+++ b/manuskript/ui/views/metadataView_ui.ui
@@ -132,7 +132,7 @@
-
-
+
QFrame::NoFrame
@@ -172,7 +172,7 @@
0
-
-
+
QFrame::NoFrame
@@ -228,9 +228,9 @@
- textEditView
+ MDEditCompleter
QTextEdit
- manuskript.ui.views.textEditView.h
+ manuskript.ui.views.MDEditCompleter.h
lineEditView
@@ -249,11 +249,6 @@
manuskript.ui.views.propertiesView.h
1
-
- textEditCompleter
- QTextEdit
- manuskript.ui.views.textEditCompleter.h
-
revisions
QWidget
diff --git a/manuskript/ui/views/textEditView.py b/manuskript/ui/views/textEditView.py
index 2d10c30f..091f05f0 100644
--- a/manuskript/ui/views/textEditView.py
+++ b/manuskript/ui/views/textEditView.py
@@ -7,13 +7,10 @@ from PyQt5.QtGui import QTextBlockFormat, QTextCharFormat, QFont, QColor, QIcon,
from PyQt5.QtWidgets import QWidget, QTextEdit, qApp, QAction, QMenu
from manuskript import settings
-from manuskript.enums import Outline
+from manuskript.enums import Outline, World, Character, Plot
from manuskript import functions as F
from manuskript.models.outlineModel import outlineModel
-from manuskript.ui.editors.MDFunctions import MDFormatSelection
-from manuskript.ui.editors.MMDHighlighter import MMDHighlighter
-from manuskript.ui.editors.basicHighlighter import basicHighlighter
-from manuskript.ui.editors.textFormat import textFormat
+from manuskript.ui.highlighters import BasicHighlighter
from manuskript.ui import style as S
try:
@@ -23,8 +20,8 @@ except ImportError:
class textEditView(QTextEdit):
- def __init__(self, parent=None, index=None, html=None, spellcheck=True, highlighting=False, dict="",
- autoResize=False):
+ def __init__(self, parent=None, index=None, html=None, spellcheck=True,
+ highlighting=False, dict="", autoResize=False):
QTextEdit.__init__(self, parent)
self._column = Outline.text
self._index = None
@@ -42,16 +39,18 @@ class textEditView(QTextEdit):
# position, so we only have it's ID as reference. We store it to
# update at the propper time.
self._updateIndexFromID = None
+ self._themeData = None
+ self._highlighterClass = BasicHighlighter
self.spellcheck = spellcheck
self.currentDict = dict if dict else settings.dict
+ self._defaultFontSize = qApp.font().pointSize()
self.highlighter = None
self.setAutoResize(autoResize)
self._defaultBlockFormat = QTextBlockFormat()
self._defaultCharFormat = QTextCharFormat()
self.highlightWord = ""
self.highligtCS = False
- self.defaultFontPointSize = qApp.font().pointSize()
self._dict = None
# self.document().contentsChanged.connect(self.submit, F.AUC)
@@ -80,7 +79,8 @@ class textEditView(QTextEdit):
# Spellchecking
if enchant and self.spellcheck:
try:
- self._dict = enchant.Dict(self.currentDict if self.currentDict else self.getDefaultLocale())
+ self._dict = enchant.Dict(self.currentDict if self.currentDict
+ else self.getDefaultLocale())
except enchant.errors.DictNotFoundError:
self.spellcheck = False
@@ -88,7 +88,7 @@ class textEditView(QTextEdit):
self.spellcheck = False
if self._highlighting and not self.highlighter:
- self.highlighter = basicHighlighter(self)
+ self.highlighter = self._highlighterClass(self)
self.highlighter.setDefaultBlockFormat(self._defaultBlockFormat)
def getDefaultLocale(self):
@@ -177,27 +177,11 @@ class textEditView(QTextEdit):
self.updateText()
def setupEditorForIndex(self, index):
- # In which model are we editing?
- if type(index.model()) != outlineModel:
- self._textFormat = "text"
- return
-
- # what type of text are we editing?
- if self._column not in [Outline.text, Outline.notes]:
- self._textFormat = "text"
-
- else:
- self._textFormat = "md"
-
# Setting highlighter
if self._highlighting:
- item = index.internalPointer()
- if self._column in [Outline.text, Outline.notes]:
- self.highlighter = MMDHighlighter(self)
- else:
- self.highlighter = basicHighlighter(self)
-
+ self.highlighter = self._highlighterClass(self)
self.highlighter.setDefaultBlockFormat(self._defaultBlockFormat)
+ self.highlighter.updateColorScheme()
def loadFontSettings(self):
if self._fromTheme or \
@@ -209,8 +193,10 @@ class textEditView(QTextEdit):
opt = settings.textEditor
f = QFont()
f.fromString(opt["font"])
- background = opt["background"] if not opt["backgroundTransparent"] else "transparent"
- foreground = opt["fontColor"] if not opt["backgroundTransparent"] else S.text
+ background = (opt["background"] if not opt["backgroundTransparent"]
+ else "transparent")
+ foreground = opt["fontColor"] # if not opt["backgroundTransparent"]
+ # else S.text
# self.setFont(f)
self.setStyleSheet("""QTextEdit{{
background: {bg};
@@ -230,6 +216,7 @@ class textEditView(QTextEdit):
maxWidth = "max-width: {}px;".format(opt["maxWidth"]) if opt["maxWidth"] else "",
)
)
+ self._defaultFontSize = f.pointSize()
# We set the parent background to the editor's background in case
# there are margins. We check that the parent class is a QWidget because
@@ -266,6 +253,7 @@ class textEditView(QTextEdit):
self._defaultBlockFormat = bf
if self.highlighter:
+ self.highlighter.updateColorScheme()
self.highlighter.setMisspelledColor(QColor(opt["misspelled"]))
self.highlighter.setDefaultCharFormat(self._defaultCharFormat)
self.highlighter.setDefaultBlockFormat(self._defaultBlockFormat)
@@ -415,7 +403,8 @@ class textEditView(QTextEdit):
self.sizeChange()
def sizeChange(self):
- docHeight = self.document().size().height()
+ opt = settings.textEditor
+ docHeight = self.document().size().height() + 2 * opt["marginsTB"]
if self.heightMin <= docHeight <= self.heightMax:
self.setMinimumHeight(docHeight)
@@ -461,6 +450,31 @@ class textEditView(QTextEdit):
Qt.LeftButton, Qt.LeftButton, Qt.NoModifier)
QTextEdit.mousePressEvent(self, event)
+ def wheelEvent(self, event):
+ """
+ We catch wheelEvent if key modifier is CTRL to change font size.
+ Note: this should be in a class specific for main textEditView (#TODO).
+ """
+ if event.modifiers() & Qt.ControlModifier:
+ # Get the wheel angle.
+ d = event.angleDelta().y() / 120
+
+ # Update settings
+ f = QFont()
+ f.fromString(settings.textEditor["font"])
+ f.setPointSizeF(f.pointSizeF() + d)
+ settings.textEditor["font"] = f.toString()
+
+ # Update font to all textEditView. Drastically.
+ for w in F.mainWindow().findChildren(textEditView, QRegExp(".*")):
+ w.loadFontSettings()
+
+ # We tell the world that we accepted this event
+ event.accept()
+ return
+
+ QTextEdit.wheelEvent(self, event)
+
class SpellAction(QAction):
"""A special QAction that returns the text in a signal. Used for spellckech."""
@@ -560,32 +574,6 @@ class textEditView(QTextEdit):
QTextEdit.focusOutEvent(self, event)
self.submit()
- def focusInEvent(self, event):
- """Finds textFormatter and attach them to that view."""
- QTextEdit.focusInEvent(self, event)
-
- p = self.parent()
- while p.parent():
- p = p.parent()
-
- if self._index:
- for tF in p.findChildren(textFormat, QRegExp(".*"), Qt.FindChildrenRecursively):
- tF.updateFromIndex(self._index)
- tF.setTextEdit(self)
-
- def applyFormat(self, _format):
-
- if self._textFormat == "md":
-
- if _format == "Bold":
- MDFormatSelection(self, 0)
- elif _format == "Italic":
- MDFormatSelection(self, 1)
- elif _format == "Code":
- MDFormatSelection(self, 2)
- elif _format == "Clear":
- MDFormatSelection(self)
-
###############################################################################
# KEYBOARD SHORTCUTS
###############################################################################
@@ -596,7 +584,7 @@ class textEditView(QTextEdit):
edit that has focus. So we can pass it the call for documents
edits like: duplicate, move up, etc.
"""
- if self._index and self._column == Outline.text.value:
+ if self._index and self._column == Outline.text:
function = getattr(F.mainWindow().treeRedacOutline, functionName)
function()