manuskript/manuskript/ui/views/textEditView.py

500 lines
17 KiB
Python
Raw Normal View History

#!/usr/bin/env python
2016-02-07 00:34:22 +13:00
# --!-- coding: utf8 --!--
2015-06-25 23:41:55 +12:00
import re
2015-06-15 22:18:42 +12:00
2016-02-07 00:34:22 +13:00
from PyQt5.QtCore import QTimer, QModelIndex, Qt, QEvent, pyqtSignal, QRegExp
from PyQt5.QtGui import QTextBlockFormat, QTextCharFormat, QFont, QColor, QMouseEvent, QTextCursor
from PyQt5.QtWidgets import QTextEdit, qApp, QAction, QMenu
from manuskript import settings
from manuskript.enums import Outline
from manuskript.functions import AUC
from manuskript.functions import toString
from manuskript.models.outlineModel import outlineModel
2016-03-30 22:36:25 +13:00
from manuskript.ui.editors.MDFunctions import MDFormatSelection
from manuskript.ui.editors.MMDHighlighter import MMDHighlighter
2016-02-07 00:34:22 +13:00
from manuskript.ui.editors.basicHighlighter import basicHighlighter
from manuskript.ui.editors.textFormat import textFormat
try:
import enchant
except ImportError:
enchant = None
2016-02-07 00:34:22 +13:00
2015-06-15 22:18:42 +12:00
class textEditView(QTextEdit):
2016-02-07 00:34:22 +13:00
def __init__(self, parent=None, index=None, html=None, spellcheck=True, highlighting=False, dict="",
autoResize=False):
QTextEdit.__init__(self, parent)
2015-06-15 22:18:42 +12:00
self._column = Outline.text.value
self._index = None
self._indexes = None
self._model = None
self._placeholderText = self.placeholderText()
2015-06-15 22:18:42 +12:00
self._updating = False
self._item = None
self._highlighting = highlighting
2015-06-25 05:53:51 +12:00
self._textFormat = "text"
self.setAcceptRichText(False)
2015-06-26 03:06:07 +12:00
# When setting up a theme, this becomes true.
2016-02-07 00:34:22 +13:00
self._fromTheme = False
2015-06-07 05:10:44 +12:00
self.spellcheck = spellcheck
2015-06-30 00:21:57 +12:00
self.currentDict = dict if dict else settings.dict
2015-06-08 12:07:08 +12:00
self.highlighter = None
2015-07-03 23:59:41 +12:00
self.setAutoResize(autoResize)
2015-06-20 04:47:45 +12:00
self._defaultBlockFormat = QTextBlockFormat()
2015-06-26 03:06:07 +12:00
self._defaultCharFormat = QTextCharFormat()
2015-06-20 04:47:45 +12:00
self.highlightWord = ""
self.highligtCS = False
self.defaultFontPointSize = qApp.font().pointSize()
self._dict = None
2016-02-07 00:34:22 +13:00
# self.document().contentsChanged.connect(self.submit, AUC)
# Submit text changed only after 500ms without modifications
self.updateTimer = QTimer()
self.updateTimer.setInterval(500)
self.updateTimer.setSingleShot(True)
self.updateTimer.timeout.connect(self.submit)
2016-02-07 00:34:22 +13:00
# self.updateTimer.timeout.connect(lambda: print("Timeout"))
2015-06-25 05:53:51 +12:00
self.updateTimer.stop()
self.document().contentsChanged.connect(self.updateTimer.start, AUC)
2016-02-07 00:34:22 +13:00
# self.document().contentsChanged.connect(lambda: print("Document changed"))
# self.document().contentsChanged.connect(lambda: print(self.objectName(), "Contents changed"))
self.setEnabled(False)
2016-02-07 00:34:22 +13:00
2015-06-07 05:10:44 +12:00
if index:
self.setCurrentModelIndex(index)
2016-02-07 00:34:22 +13:00
2015-06-07 05:10:44 +12:00
elif html:
self.document().setHtml(html)
self.setReadOnly(True)
2016-02-07 00:34:22 +13:00
2015-06-20 04:47:45 +12:00
# Spellchecking
if enchant and self.spellcheck:
try:
self._dict = enchant.Dict(self.currentDict if self.currentDict else enchant.get_default_language())
except enchant.errors.DictNotFoundError:
self.spellcheck = False
2015-06-20 04:47:45 +12:00
else:
self.spellcheck = False
2016-02-07 00:34:22 +13:00
2015-06-30 00:21:57 +12:00
if self._highlighting and not self.highlighter:
self.highlighter = basicHighlighter(self)
2015-06-30 00:21:57 +12:00
self.highlighter.setDefaultBlockFormat(self._defaultBlockFormat)
2016-02-07 00:34:22 +13:00
2015-06-15 22:18:42 +12:00
def setModel(self, model):
self._model = model
try:
self._model.dataChanged.connect(self.update, AUC)
except TypeError:
pass
try:
self._model.rowsAboutToBeRemoved.connect(self.rowsAboutToBeRemoved, AUC)
except TypeError:
pass
2016-02-07 00:34:22 +13:00
2015-06-15 22:18:42 +12:00
def setColumn(self, col):
self._column = col
2016-02-07 00:34:22 +13:00
2015-06-15 22:18:42 +12:00
def setHighlighting(self, val):
self._highlighting = val
2016-02-07 00:34:22 +13:00
2015-06-20 04:47:45 +12:00
def setDefaultBlockFormat(self, bf):
self._defaultBlockFormat = bf
if self.highlighter:
self.highlighter.setDefaultBlockFormat(bf)
2016-02-07 00:34:22 +13:00
2015-06-07 05:10:44 +12:00
def setCurrentModelIndex(self, index):
2015-06-15 22:18:42 +12:00
self._indexes = None
2015-06-07 05:10:44 +12:00
if index.isValid():
self.setEnabled(True)
2015-06-15 22:18:42 +12:00
if index.column() != self._column:
index = index.sibling(index.row(), self._column)
self._index = index
2016-02-07 00:34:22 +13:00
self.setPlaceholderText(self._placeholderText)
2016-02-07 00:34:22 +13:00
if not self._model:
self.setModel(index.model())
2015-06-25 05:53:51 +12:00
self.setupEditorForIndex(self._index)
2015-06-26 03:06:07 +12:00
self.loadFontSettings()
2015-06-07 05:10:44 +12:00
self.updateText()
2015-06-22 23:11:45 +12:00
else:
self._index = QModelIndex()
2015-06-22 23:11:45 +12:00
self.setPlainText("")
self.setEnabled(False)
2016-02-07 00:34:22 +13:00
def setCurrentModelIndexes(self, indexes):
self._index = None
self._indexes = []
2016-02-07 00:34:22 +13:00
for i in indexes:
if i.isValid():
self.setEnabled(True)
if i.column() != self._column:
i = i.sibling(i.row(), self._column)
self._indexes.append(i)
2016-02-07 00:34:22 +13:00
if not self._model:
self.setModel(i.model())
2016-02-07 00:34:22 +13:00
self.updateText()
2016-02-07 00:34:22 +13:00
def setupEditorForIndex(self, index):
2016-03-30 21:30:41 +13:00
# In which model are we editing?
2015-06-25 05:53:51 +12:00
if type(index.model()) != outlineModel:
self._textFormat = "text"
return
2016-02-07 00:34:22 +13:00
2016-03-30 21:30:41 +13:00
# what type of text are we editing?
if self._column not in [Outline.text.value, Outline.notes.value]:
2015-06-25 05:53:51 +12:00
self._textFormat = "text"
2016-02-07 00:34:22 +13:00
2015-06-30 00:33:30 +12:00
else:
2016-03-30 21:30:41 +13:00
self._textFormat = "md"
2016-02-07 00:34:22 +13:00
# Setting highlighter
if self._highlighting:
item = index.internalPointer()
2016-03-30 21:30:41 +13:00
if self._column in [Outline.text.value, Outline.notes.value]:
self.highlighter = MMDHighlighter(self)
2016-03-30 21:30:41 +13:00
else:
self.highlighter = basicHighlighter(self)
2016-02-07 00:34:22 +13:00
self.highlighter.setDefaultBlockFormat(self._defaultBlockFormat)
2016-02-07 00:34:22 +13:00
2015-06-26 03:06:07 +12:00
def loadFontSettings(self):
if self._fromTheme or \
2016-02-07 00:34:22 +13:00
not self._index or \
type(self._index.model()) != outlineModel or \
self._column != Outline.text.value:
2015-06-26 03:06:07 +12:00
return
2016-02-07 00:34:22 +13:00
2015-06-26 03:06:07 +12:00
opt = settings.textEditor
f = QFont()
f.fromString(opt["font"])
2016-02-07 00:34:22 +13:00
# self.setFont(f)
self.setStyleSheet("""QTextEdit{{
2015-07-02 00:51:43 +12:00
background: {bg};
color: {foreground};
font-family: {ff};
font-size: {fs};
}}
""".format(
2015-07-02 00:51:43 +12:00
bg=opt["background"],
foreground=opt["fontColor"],
ff=f.family(),
fs="{}pt".format(str(f.pointSize()))))
2016-02-07 00:34:22 +13:00
2015-06-26 03:06:07 +12:00
cf = QTextCharFormat()
2016-02-07 00:34:22 +13:00
# cf.setFont(f)
# cf.setForeground(QColor(opt["fontColor"]))
2015-06-26 03:06:07 +12:00
bf = QTextBlockFormat()
bf.setLineHeight(opt["lineSpacing"], bf.ProportionalHeight)
bf.setTextIndent(opt["tabWidth"] * 1 if opt["indent"] else 0)
bf.setTopMargin(opt["spacingAbove"])
bf.setBottomMargin(opt["spacingBelow"])
2016-02-07 00:34:22 +13:00
2015-06-26 03:06:07 +12:00
self._defaultCharFormat = cf
self._defaultBlockFormat = bf
2016-02-07 00:34:22 +13:00
2015-06-26 03:06:07 +12:00
if self.highlighter:
self.highlighter.setMisspelledColor(QColor(opt["misspelled"]))
self.highlighter.setDefaultCharFormat(self._defaultCharFormat)
self.highlighter.setDefaultBlockFormat(self._defaultBlockFormat)
2016-02-07 00:34:22 +13:00
2015-06-07 05:10:44 +12:00
def update(self, topLeft, bottomRight):
2015-06-15 22:38:38 +12:00
if self._updating:
2015-06-15 22:18:42 +12:00
return
2016-02-07 00:34:22 +13:00
2015-06-15 22:18:42 +12:00
elif self._index:
2016-02-07 00:34:22 +13:00
if topLeft.parent() != self._index.parent():
return
2016-02-07 00:34:22 +13:00
# print("Model changed: ({}:{}), ({}:{}/{}), ({}:{}) for {} of {}".format(
# topLeft.row(), topLeft.column(),
# self._index.row(), self._index.row(), self._column,
# bottomRight.row(), bottomRight.column(),
# self.objectName(), self.parent().objectName()))
2015-06-15 22:18:42 +12:00
if topLeft.row() <= self._index.row() <= bottomRight.row():
2016-03-30 21:30:41 +13:00
if topLeft.column() <= self._column <= bottomRight.column():
self.updateText()
2016-02-07 00:34:22 +13:00
2015-06-15 22:18:42 +12:00
elif self._indexes:
update = False
for i in self._indexes:
if topLeft.row() <= i.row() <= bottomRight.row():
update = True
if update:
self.updateText()
2016-02-07 00:34:22 +13:00
def rowsAboutToBeRemoved(self, parent, first, last):
if self._index:
if self._index.parent() == parent and \
2016-02-07 00:34:22 +13:00
first <= self._index.row() <= last:
self._index = None
self.setEnabled(False)
2016-02-07 00:34:22 +13:00
# FIXME: self._indexes
2015-06-25 05:53:51 +12:00
def disconnectDocument(self):
try:
self.document().contentsChanged.disconnect(self.updateTimer.start)
except:
pass
2016-02-07 00:34:22 +13:00
2015-06-25 05:53:51 +12:00
def reconnectDocument(self):
self.document().contentsChanged.connect(self.updateTimer.start, AUC)
2016-02-07 00:34:22 +13:00
2015-06-07 05:10:44 +12:00
def updateText(self):
if self._updating:
return
2016-02-07 00:34:22 +13:00
# print("Updating", self.objectName())
2015-06-15 22:38:38 +12:00
self._updating = True
2015-06-15 22:18:42 +12:00
if self._index:
2015-06-25 05:53:51 +12:00
self.disconnectDocument()
2016-03-30 21:30:41 +13:00
if self.toPlainText() != toString(self._model.data(self._index)):
# print(" Updating plaintext")
self.document().setPlainText(toString(self._model.data(self._index)))
2015-06-25 05:53:51 +12:00
self.reconnectDocument()
2016-02-07 00:34:22 +13:00
2015-06-15 22:18:42 +12:00
elif self._indexes:
self.disconnectDocument()
2015-06-15 22:18:42 +12:00
t = []
same = True
for i in self._indexes:
item = i.internalPointer()
t.append(toString(item.data(self._column)))
2016-02-07 00:34:22 +13:00
2015-06-15 22:18:42 +12:00
for t2 in t[1:]:
if t2 != t[0]:
same = False
break
2016-02-07 00:34:22 +13:00
2015-06-15 22:18:42 +12:00
if same:
self.document().setPlainText(t[0])
else:
self.document().setPlainText("")
2016-02-07 00:34:22 +13:00
2015-06-15 22:18:42 +12:00
if not self._placeholderText:
self._placeholderText = self.placeholderText()
2016-02-07 00:34:22 +13:00
2015-06-15 22:18:42 +12:00
self.setPlaceholderText(self.tr("Various"))
self.reconnectDocument()
2015-06-15 22:38:38 +12:00
self._updating = False
2016-02-07 00:34:22 +13:00
2015-06-15 22:18:42 +12:00
def submit(self):
2015-07-04 04:41:18 +12:00
self.updateTimer.stop()
2015-06-15 22:38:38 +12:00
if self._updating:
return
2016-02-07 00:34:22 +13:00
# print("Submitting", self.objectName())
if self._index and self._index.isValid():
2016-02-07 00:34:22 +13:00
# item = self._index.internalPointer()
2016-03-30 21:30:41 +13:00
if self.toPlainText() != self._model.data(self._index):
# print(" Submitting plain text")
self._updating = True
self._model.setData(self._index, self.toPlainText())
self._updating = False
2016-02-07 00:34:22 +13:00
2015-06-15 22:18:42 +12:00
elif self._indexes:
self._updating = True
for i in self._indexes:
item = i.internalPointer()
if self.toPlainText() != toString(item.data(self._column)):
print("Submitting many indexes")
2015-06-15 22:18:42 +12:00
self._model.setData(i, self.toPlainText())
self._updating = False
2016-02-07 00:34:22 +13:00
2015-06-28 00:11:26 +12:00
def keyPressEvent(self, event):
2015-07-04 04:41:18 +12:00
QTextEdit.keyPressEvent(self, event)
2015-06-28 00:11:26 +12:00
if event.key() == Qt.Key_Space:
self.submit()
2016-02-07 00:34:22 +13:00
2015-06-15 22:18:42 +12:00
# -----------------------------------------------------------------------------------------------------
# Resize stuff
2016-02-07 00:34:22 +13:00
2015-06-07 05:10:44 +12:00
def resizeEvent(self, e):
QTextEdit.resizeEvent(self, e)
2015-06-15 22:18:42 +12:00
if self._autoResize:
self.sizeChange()
2015-06-07 05:10:44 +12:00
def sizeChange(self):
docHeight = self.document().size().height()
if self.heightMin <= docHeight <= self.heightMax:
self.setMinimumHeight(docHeight)
2016-02-07 00:34:22 +13:00
2015-06-15 22:18:42 +12:00
def setAutoResize(self, val):
self._autoResize = val
if self._autoResize:
self.document().contentsChanged.connect(self.sizeChange)
self.heightMin = 0
self.heightMax = 65000
self.sizeChange()
2016-02-07 00:34:22 +13:00
###############################################################################
# SPELLCHECKING
###############################################################################
# Based on http://john.nachtimwald.com/2009/08/22/qplaintextedit-with-in-line-spell-check/
2015-06-07 05:10:44 +12:00
def setDict(self, d):
self.currentDict = d
2015-06-20 04:47:45 +12:00
self._dict = enchant.Dict(d)
if self.highlighter:
self.highlighter.rehighlight()
2016-02-07 00:34:22 +13:00
2015-06-07 05:10:44 +12:00
def toggleSpellcheck(self, v):
self.spellcheck = v
2015-06-20 04:47:45 +12:00
if enchant and self.spellcheck and not self._dict:
2016-03-30 04:31:33 +13:00
if self.currentDict:
self._dict = enchant.Dict(self.currentDict)
2016-03-31 02:47:52 +13:00
elif enchant.dict_exists(enchant.get_default_language()):
2016-03-30 04:31:33 +13:00
self._dict = enchant.Dict(enchant.get_default_language())
else:
self.spellcheck = False
2015-06-15 22:18:42 +12:00
if self.highlighter:
self.highlighter.rehighlight()
2015-06-20 04:47:45 +12:00
else:
self.spellcheck = False
2015-06-07 05:10:44 +12:00
def mousePressEvent(self, event):
if event.button() == Qt.RightButton:
# Rewrite the mouse event to a left button event so the cursor is
# moved to the location of the pointer.
event = QMouseEvent(QEvent.MouseButtonPress, event.pos(),
2016-02-07 00:34:22 +13:00
Qt.LeftButton, Qt.LeftButton, Qt.NoModifier)
2015-06-07 05:10:44 +12:00
QTextEdit.mousePressEvent(self, event)
2016-02-07 00:34:22 +13:00
2015-06-07 05:10:44 +12:00
class SpellAction(QAction):
2016-02-07 00:34:22 +13:00
"""A special QAction that returns the text in a signal. Used for spellckech."""
2015-06-08 08:06:57 +12:00
correct = pyqtSignal(str)
2016-02-07 00:34:22 +13:00
2015-06-07 05:10:44 +12:00
def __init__(self, *args):
QAction.__init__(self, *args)
2016-02-07 00:34:22 +13:00
2015-06-07 05:10:44 +12:00
self.triggered.connect(lambda x: self.correct.emit(
2016-02-07 00:34:22 +13:00
str(self.text())))
2015-06-07 05:10:44 +12:00
def contextMenuEvent(self, event):
# Based on http://john.nachtimwald.com/2009/08/22/qplaintextedit-with-in-line-spell-check/
popup_menu = self.createStandardContextMenu()
2015-06-27 20:27:52 +12:00
popup_menu.exec_(event.globalPos())
2016-02-07 00:34:22 +13:00
2015-06-27 20:27:52 +12:00
def createStandardContextMenu(self):
popup_menu = QTextEdit.createStandardContextMenu(self)
2016-02-07 00:34:22 +13:00
2015-06-27 20:27:52 +12:00
if not self.spellcheck:
return popup_menu
2016-02-07 00:34:22 +13:00
2015-06-07 05:10:44 +12:00
# Select the word under the cursor.
# But only if there is no selection (otherwise it's impossible to select more text to copy/cut)
2015-06-07 05:10:44 +12:00
cursor = self.textCursor()
if not cursor.hasSelection():
cursor.select(QTextCursor.WordUnderCursor)
self.setTextCursor(cursor)
2015-06-07 05:10:44 +12:00
# Check if the selected word is misspelled and offer spelling
# suggestions if it is.
2015-06-27 20:27:52 +12:00
if cursor.hasSelection():
text = str(cursor.selectedText())
valid = self._dict.check(text)
selectedWord = cursor.selectedText()
if not valid:
2015-06-27 20:27:52 +12:00
spell_menu = QMenu(self.tr('Spelling Suggestions'), self)
2015-06-20 04:47:45 +12:00
for word in self._dict.suggest(text):
2015-06-07 05:10:44 +12:00
action = self.SpellAction(word, spell_menu)
action.correct.connect(self.correctWord)
spell_menu.addAction(action)
# Only add the spelling suggests to the menu if there are
# suggestions.
if len(spell_menu.actions()) != 0:
popup_menu.insertSeparator(popup_menu.actions()[0])
# Adds: add to dictionary
addAction = QAction(self.tr("&Add to dictionary"), popup_menu)
addAction.triggered.connect(self.addWordToDict)
addAction.setData(selectedWord)
popup_menu.insertAction(popup_menu.actions()[0], addAction)
# Adds: suggestions
2015-06-07 05:10:44 +12:00
popup_menu.insertMenu(popup_menu.actions()[0], spell_menu)
# popup_menu.insertSeparator(popup_menu.actions()[0])
# If word was added to custom dict, give the possibility to remove it
elif valid and self._dict.is_added(selectedWord):
popup_menu.insertSeparator(popup_menu.actions()[0])
# Adds: remove from dictionary
rmAction = QAction(self.tr("&Remove from custom dictionary"), popup_menu)
rmAction.triggered.connect(self.rmWordFromDict)
rmAction.setData(selectedWord)
popup_menu.insertAction(popup_menu.actions()[0], rmAction)
2015-06-27 20:27:52 +12:00
return popup_menu
2016-02-07 00:34:22 +13:00
2015-06-07 05:10:44 +12:00
def correctWord(self, word):
2016-02-07 00:34:22 +13:00
"""
2015-06-07 05:10:44 +12:00
Replaces the selected text with word.
2016-02-07 00:34:22 +13:00
"""
2015-06-07 05:10:44 +12:00
cursor = self.textCursor()
cursor.beginEditBlock()
2016-02-07 00:34:22 +13:00
2015-06-07 05:10:44 +12:00
cursor.removeSelectedText()
cursor.insertText(word)
2016-02-07 00:34:22 +13:00
2015-06-07 05:10:44 +12:00
cursor.endEditBlock()
2016-02-07 00:34:22 +13:00
def addWordToDict(self):
word = self.sender().data()
self._dict.add(word)
self.highlighter.rehighlight()
def rmWordFromDict(self):
word = self.sender().data()
self._dict.remove(word)
self.highlighter.rehighlight()
2016-02-07 00:34:22 +13:00
###############################################################################
# FORMATTING
###############################################################################
2015-06-25 06:41:23 +12:00
def focusOutEvent(self, event):
2016-02-07 00:34:22 +13:00
"""Submit changes just before focusing out."""
2015-06-25 06:41:23 +12:00
QTextEdit.focusOutEvent(self, event)
self.submit()
2016-02-07 00:34:22 +13:00
def focusInEvent(self, event):
2016-02-07 00:34:22 +13:00
"""Finds textFormatter and attach them to that view."""
QTextEdit.focusInEvent(self, event)
2016-02-07 00:34:22 +13:00
p = self.parent()
while p.parent():
p = p.parent()
2016-02-07 00:34:22 +13:00
if self._index:
for tF in p.findChildren(textFormat, QRegExp(".*"), Qt.FindChildrenRecursively):
tF.updateFromIndex(self._index)
tF.setTextEdit(self)
2016-02-07 00:34:22 +13:00
def applyFormat(self, _format):
2016-02-07 00:34:22 +13:00
2016-03-30 21:30:41 +13:00
if self._textFormat == "md":
2016-03-30 22:36:25 +13:00
if _format == "Bold":
MDFormatSelection(self, 0)
elif _format == "Italic":
MDFormatSelection(self, 1)
elif _format == "Code":
MDFormatSelection(self, 2)
elif _format == "Clear":
MDFormatSelection(self)