Cleaned up the source files in the gui folder a bit.
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter GUI Document Details
|
||||
|
||||
novelWriter – GUI Document Details
|
||||
====================================
|
||||
Class holding the left side document details panel
|
||||
|
||||
File History:
|
||||
Created: 2019-04-24 [0.0.1]
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
import nw
|
||||
|
||||
from PyQt5.QtGui import QFont
|
||||
from PyQt5.QtWidgets import QFrame, QGridLayout, QLabel
|
||||
|
||||
from nw.constants import nwLabels
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class GuiDocDetails(QFrame):
|
||||
|
||||
C_NAME = 0
|
||||
C_COUNT = 1
|
||||
C_FLAGS = 2
|
||||
C_HANDLE = 3
|
||||
|
||||
def __init__(self, theParent, theProject):
|
||||
QFrame.__init__(self, theParent)
|
||||
|
||||
logger.debug("Initialising DocDetails ...")
|
||||
self.mainConf = nw.CONFIG
|
||||
self.debugGUI = self.mainConf.debugGUI
|
||||
self.theParent = theParent
|
||||
self.theProject = theProject
|
||||
|
||||
self.mainBox = QGridLayout(self)
|
||||
self.mainBox.setVerticalSpacing(1)
|
||||
self.mainBox.setHorizontalSpacing(15)
|
||||
self.setLayout(self.mainBox)
|
||||
|
||||
self.fntOne = QFont()
|
||||
self.fntOne.setPointSize(10)
|
||||
self.fntOne.setBold(True)
|
||||
|
||||
self.fntTwo = QFont()
|
||||
self.fntTwo.setPointSize(10)
|
||||
self.colTwo = [
|
||||
QLabel(""),
|
||||
QLabel(""),
|
||||
QLabel(""),
|
||||
QLabel("")
|
||||
]
|
||||
|
||||
colOne = ["Label","Status","Class","Layout"]
|
||||
for nRow in range(4):
|
||||
lblOne = QLabel(colOne[nRow])
|
||||
lblOne.setFont(self.fntOne)
|
||||
self.mainBox.addWidget(lblOne,nRow,0)
|
||||
self.mainBox.addWidget(self.colTwo[nRow],nRow,1)
|
||||
|
||||
self.mainBox.setColumnStretch(0,0)
|
||||
self.mainBox.setColumnStretch(1,1)
|
||||
|
||||
logger.debug("DocDetails initialisation complete")
|
||||
|
||||
return
|
||||
|
||||
def buildViewBox(self, tHandle):
|
||||
|
||||
nwItem = self.theProject.getItem(tHandle)
|
||||
|
||||
if nwItem is None:
|
||||
colTwo = [""]*4
|
||||
else:
|
||||
colTwo = [
|
||||
nwItem.itemName,
|
||||
nwItem.itemStatus,
|
||||
nwLabels.CLASS_NAME[nwItem.itemClass],
|
||||
nwLabels.LAYOUT_NAME[nwItem.itemLayout],
|
||||
]
|
||||
|
||||
for nRow in range(4):
|
||||
self.colTwo[nRow].setText(colTwo[nRow])
|
||||
|
||||
return
|
||||
|
||||
# END Class GuiDocDetails
|
||||
@@ -0,0 +1,664 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter GUI Document Editor
|
||||
|
||||
novelWriter – GUI Document Editor
|
||||
===================================
|
||||
Class holding the document editor
|
||||
|
||||
File History:
|
||||
Created: 2018-09-29 [0.0.1]
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
import nw
|
||||
|
||||
from time import time
|
||||
|
||||
from PyQt5.QtCore import Qt, QTimer, QSizeF
|
||||
from PyQt5.QtWidgets import qApp, QTextEdit, QAction, QMenu, QShortcut
|
||||
from PyQt5.QtGui import (
|
||||
QTextCursor, QTextOption, QIcon, QKeySequence, QFont, QColor, QPalette, QTextDocument
|
||||
)
|
||||
|
||||
from nw.project.document import NWDoc
|
||||
from nw.gui.tools.dochighlight import GuiDocHighlighter
|
||||
from nw.gui.tools.wordcounter import WordCounter
|
||||
from nw.tools.spellcheck import NWSpellCheck
|
||||
from nw.constants import nwFiles, nwUnicode
|
||||
from nw.enum import nwDocAction, nwAlert
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class GuiDocEditor(QTextEdit):
|
||||
|
||||
def __init__(self, theParent, theProject):
|
||||
QTextEdit.__init__(self)
|
||||
|
||||
logger.debug("Initialising DocEditor ...")
|
||||
|
||||
# Class Variables
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theProject = theProject
|
||||
self.theParent = theParent
|
||||
self.theTheme = theParent.theTheme
|
||||
self.docChanged = False
|
||||
self.spellCheck = False
|
||||
self.nwDocument = NWDoc(self.theProject, self.theParent)
|
||||
self.theHandle = None
|
||||
|
||||
# Document Variables
|
||||
self.charCount = 0
|
||||
self.wordCount = 0
|
||||
self.paraCount = 0
|
||||
self.lastEdit = 0
|
||||
|
||||
# Typography
|
||||
self.typDQOpen = self.mainConf.fmtDoubleQuotes[0]
|
||||
self.typDQClose = self.mainConf.fmtDoubleQuotes[1]
|
||||
self.typSQOpen = self.mainConf.fmtSingleQuotes[0]
|
||||
self.typSQClose = self.mainConf.fmtSingleQuotes[1]
|
||||
|
||||
# Core Elements
|
||||
self.qDocument = self.document()
|
||||
self.qDocument.setDocumentMargin(self.mainConf.textMargin)
|
||||
self.qDocument.contentsChange.connect(self._docChange)
|
||||
if self.mainConf.spellTool == "enchant":
|
||||
from nw.tools.spellenchant import NWSpellEnchant
|
||||
self.theDict = NWSpellEnchant()
|
||||
else:
|
||||
self.theDict = NWSpellCheck()
|
||||
|
||||
self.hLight = GuiDocHighlighter(self.qDocument, self.theParent)
|
||||
self.hLight.setDict(self.theDict)
|
||||
|
||||
# Context Menu
|
||||
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
self.customContextMenuRequested.connect(self._openContextMenu)
|
||||
|
||||
# Editor State
|
||||
self.hasSelection = False
|
||||
self.setMinimumWidth(300)
|
||||
self.setAcceptRichText(False)
|
||||
|
||||
# Custom Shortcuts
|
||||
QShortcut(QKeySequence("Ctrl+."), self, context=Qt.WidgetShortcut, activated=self._openSpellContext)
|
||||
QShortcut(Qt.Key_Return | Qt.ControlModifier, self, context=Qt.WidgetShortcut, activated=self._followTag)
|
||||
QShortcut(Qt.Key_Enter | Qt.ControlModifier, self, context=Qt.WidgetShortcut, activated=self._followTag)
|
||||
|
||||
# Set Up Word Count Thread and Timer
|
||||
self.wcInterval = self.mainConf.wordCountTimer
|
||||
self.wcTimer = QTimer()
|
||||
self.wcTimer.setInterval(int(self.wcInterval*1000))
|
||||
self.wcTimer.timeout.connect(self._runCounter)
|
||||
|
||||
self.wCounter = WordCounter(self)
|
||||
self.wCounter.finished.connect(self._updateCounts)
|
||||
|
||||
self.initEditor()
|
||||
|
||||
logger.debug("DocEditor initialisation complete")
|
||||
|
||||
return
|
||||
|
||||
def clearEditor(self):
|
||||
|
||||
self.nwDocument.clearDocument()
|
||||
self.setReadOnly(True)
|
||||
self.clear()
|
||||
self.wcTimer.stop()
|
||||
|
||||
self.theHandle = None
|
||||
self.charCount = 0
|
||||
self.wordCount = 0
|
||||
self.paraCount = 0
|
||||
self.lastEdit = 0
|
||||
self.hasSelection = False
|
||||
|
||||
self.setDocumentChanged(False)
|
||||
|
||||
return True
|
||||
|
||||
def initEditor(self):
|
||||
"""Initialise or re-initialise the editor with the user's settings.
|
||||
This function is both called when the editor is created, and when the user changes the
|
||||
main editor preferences.
|
||||
"""
|
||||
|
||||
# Reload dictionaries
|
||||
self.setDictionaries()
|
||||
|
||||
# Set font
|
||||
theFont = QFont()
|
||||
if self.mainConf.textFont is None:
|
||||
# If none is defined, set the default back to config
|
||||
self.mainConf.textFont = self.qDocument.defaultFont().family()
|
||||
theFont.setFamily(self.mainConf.textFont)
|
||||
theFont.setPointSize(self.mainConf.textSize)
|
||||
self.setFont(theFont)
|
||||
|
||||
docPalette = self.palette()
|
||||
docPalette.setColor(QPalette.Base, QColor(*self.theTheme.colBack))
|
||||
docPalette.setColor(QPalette.Text, QColor(*self.theTheme.colText))
|
||||
self.setPalette(docPalette)
|
||||
|
||||
# Set default text margins
|
||||
self.qDocument.setDocumentMargin(self.mainConf.textMargin)
|
||||
|
||||
# Also set the document text options for the document text flow
|
||||
theOpt = QTextOption()
|
||||
|
||||
if self.mainConf.tabWidth is not None:
|
||||
if self.mainConf.verQtValue >= 51000:
|
||||
theOpt.setTabStopDistance(self.mainConf.tabWidth)
|
||||
if self.mainConf.doJustify:
|
||||
theOpt.setAlignment(Qt.AlignJustify)
|
||||
if self.mainConf.showTabsNSpaces:
|
||||
theOpt.setFlags(theOpt.flags() | QTextOption.ShowTabsAndSpaces)
|
||||
if self.mainConf.showLineEndings:
|
||||
theOpt.setFlags(theOpt.flags() | QTextOption.ShowLineAndParagraphSeparators)
|
||||
|
||||
self.qDocument.setDefaultTextOption(theOpt)
|
||||
|
||||
# Initialise the syntax highlighter
|
||||
self.hLight.initHighlighter()
|
||||
|
||||
# If we have a document open, we should reload it in case the font changed, otherwise
|
||||
# we just clear the editor entirely, which makes it read only.
|
||||
if self.theHandle is not None:
|
||||
# We must save the current handle as clearEditor() sets it to None
|
||||
tHandle = self.theHandle
|
||||
self.clearEditor()
|
||||
self.loadText(tHandle)
|
||||
self.changeWidth()
|
||||
else:
|
||||
self.clearEditor()
|
||||
|
||||
return True
|
||||
|
||||
def loadText(self, tHandle):
|
||||
"""Load text from a document into the editor. If we have an io error, we must handle this
|
||||
and clear the editor so that we don't risk overwriting the file if it exists. This can for
|
||||
instance happen of the file contains binary elements or an encoding that novelWriter does
|
||||
not support. If load is successful, ot the document is new (empty string) we set up the
|
||||
editor for editing the file.
|
||||
"""
|
||||
|
||||
theDoc = self.nwDocument.openDocument(tHandle)
|
||||
if theDoc is None:
|
||||
# There was an io error
|
||||
self.clearEditor()
|
||||
return False
|
||||
|
||||
self.hLight.setHandle(tHandle)
|
||||
self.setPlainText(theDoc)
|
||||
self.setCursorPosition(self.nwDocument.theItem.cursorPos)
|
||||
self.lastEdit = time()
|
||||
self._runCounter()
|
||||
self.wcTimer.start()
|
||||
self.setDocumentChanged(False)
|
||||
self.theHandle = tHandle
|
||||
|
||||
if self.nwDocument.docEditable:
|
||||
self.setReadOnly(False)
|
||||
|
||||
return True
|
||||
|
||||
def saveText(self):
|
||||
|
||||
if self.nwDocument.theItem is None:
|
||||
return False
|
||||
|
||||
docText = self.getText()
|
||||
cursPos = self.getCursorPosition()
|
||||
theItem = self.nwDocument.theItem
|
||||
theItem.setCharCount(self.charCount)
|
||||
theItem.setWordCount(self.wordCount)
|
||||
theItem.setParaCount(self.paraCount)
|
||||
theItem.setCursorPos(cursPos)
|
||||
self.nwDocument.saveDocument(docText)
|
||||
self.setDocumentChanged(False)
|
||||
|
||||
self.theParent.theIndex.scanText(theItem.itemHandle, docText)
|
||||
|
||||
return True
|
||||
|
||||
##
|
||||
# Setters and Getters
|
||||
##
|
||||
|
||||
def setDocumentChanged(self, bValue):
|
||||
self.docChanged = bValue
|
||||
self.theParent.statusBar.setDocumentStatus(self.docChanged)
|
||||
return self.docChanged
|
||||
|
||||
def getText(self):
|
||||
"""Get the text content of the current document. This method uses QTextEdit->toPlainText for
|
||||
Qt versions lower than 5.9, and the QDocument->toRawText for higher version. The latter
|
||||
preserves non-breaking spaces, which the former does not.
|
||||
"""
|
||||
if self.mainConf.verQtValue >= 50900:
|
||||
theText = self.qDocument.toRawText().replace(nwUnicode.U_PARA,"\n")
|
||||
else:
|
||||
theText = self.toPlainText()
|
||||
return theText
|
||||
|
||||
def setCursorPosition(self, thePosition):
|
||||
theCursor = self.textCursor()
|
||||
theCursor.setPosition(thePosition)
|
||||
self.setTextCursor(theCursor)
|
||||
return True
|
||||
|
||||
def getCursorPosition(self):
|
||||
theCursor = self.textCursor()
|
||||
return theCursor.position()
|
||||
|
||||
##
|
||||
# Spell Checking
|
||||
##
|
||||
|
||||
def setDictionaries(self):
|
||||
self.theDict.setLanguage(self.mainConf.spellLanguage, self.theProject.projDict)
|
||||
return True
|
||||
|
||||
def setSpellCheck(self, theMode):
|
||||
self.spellCheck = theMode
|
||||
self.hLight.setSpellCheck(theMode)
|
||||
self.hLight.rehighlight()
|
||||
return True
|
||||
|
||||
def updateSpellCheck(self):
|
||||
if self.spellCheck:
|
||||
self.hLight.rehighlight()
|
||||
return True
|
||||
|
||||
##
|
||||
# General Class Methods
|
||||
##
|
||||
|
||||
def changeWidth(self):
|
||||
"""Automatically adjust the margins so the text is centred, but only if Config.textFixedW is
|
||||
set to True.
|
||||
"""
|
||||
if self.mainConf.textFixedW:
|
||||
vBar = self.verticalScrollBar()
|
||||
if vBar.isVisible():
|
||||
sW = vBar.width()
|
||||
else:
|
||||
sW = 0
|
||||
tW = self.mainConf.textWidth
|
||||
wW = self.width()
|
||||
tM = int((wW - sW - tW)/2)
|
||||
if tM < 0:
|
||||
tM = 0
|
||||
docFormat = self.qDocument.rootFrame().frameFormat()
|
||||
docFormat.setLeftMargin(tM)
|
||||
docFormat.setRightMargin(tM)
|
||||
self.qDocument.rootFrame().setFrameFormat(docFormat)
|
||||
|
||||
return
|
||||
|
||||
def docAction(self, theAction):
|
||||
logger.verbose("Requesting action: %s" % theAction.name)
|
||||
if not self.theParent.hasProject:
|
||||
logger.error("No project open")
|
||||
return False
|
||||
if theAction == nwDocAction.UNDO: self.undo()
|
||||
elif theAction == nwDocAction.REDO: self.redo()
|
||||
elif theAction == nwDocAction.CUT: self.cut()
|
||||
elif theAction == nwDocAction.COPY: self.copy()
|
||||
elif theAction == nwDocAction.PASTE: self.paste()
|
||||
elif theAction == nwDocAction.BOLD: self._wrapSelection("**","**")
|
||||
elif theAction == nwDocAction.ITALIC: self._wrapSelection("_","_")
|
||||
elif theAction == nwDocAction.U_LINE: self._wrapSelection("__","__")
|
||||
elif theAction == nwDocAction.S_QUOTE: self._wrapSelection(self.typSQOpen,self.typSQClose)
|
||||
elif theAction == nwDocAction.D_QUOTE: self._wrapSelection(self.typDQOpen,self.typDQClose)
|
||||
elif theAction == nwDocAction.SEL_ALL: self._makeSelection(QTextCursor.Document)
|
||||
elif theAction == nwDocAction.SEL_PARA: self._makeSelection(QTextCursor.BlockUnderCursor)
|
||||
elif theAction == nwDocAction.FIND: self._beginSearch()
|
||||
elif theAction == nwDocAction.REPLACE: self._beginReplace()
|
||||
elif theAction == nwDocAction.GO_NEXT: self._findNext()
|
||||
elif theAction == nwDocAction.GO_PREV: self._findPrev()
|
||||
elif theAction == nwDocAction.REPL_NEXT: self._replaceNext()
|
||||
else:
|
||||
logger.error("Unknown or unsupported document action %s" % str(theAction))
|
||||
return False
|
||||
return True
|
||||
|
||||
def isEmpty(self):
|
||||
return self.qDocument.isEmpty()
|
||||
|
||||
##
|
||||
# Document Events and Maintenance
|
||||
##
|
||||
|
||||
def keyPressEvent(self, keyEvent):
|
||||
"""Intercept key press events.
|
||||
We need to intercept key presses briefly to record the state of selection. This is in order
|
||||
to know whether we had a selection prior to triggering the _docChange slot, as we do not
|
||||
want to trigger autoreplace on selections. Autoreplace on selections messes with undo/redo
|
||||
history.
|
||||
We also need to intercept the Shift key modifier for certain key combinations that modifies
|
||||
standard keys like enter and space. However, we don't want to spend a lot of time in this
|
||||
function as it is triggered on every keypress when typing.
|
||||
"""
|
||||
|
||||
self.hasSelection = self.textCursor().hasSelection()
|
||||
|
||||
if keyEvent.modifiers() == Qt.ShiftModifier:
|
||||
theKey = keyEvent.key()
|
||||
if theKey == Qt.Key_Return:
|
||||
self._insertHardBreak()
|
||||
elif theKey == Qt.Key_Enter:
|
||||
self._insertHardBreak()
|
||||
elif theKey == Qt.Key_Space:
|
||||
self._insertNonBreakingSpace()
|
||||
else:
|
||||
QTextEdit.keyPressEvent(self, keyEvent)
|
||||
else:
|
||||
QTextEdit.keyPressEvent(self, keyEvent)
|
||||
|
||||
return
|
||||
|
||||
def mouseReleaseEvent(self, mEvent):
|
||||
"""If the mouse button is released and the control key is pressed, check if we're clicking
|
||||
on a tag, and trigger the follow tag function.
|
||||
"""
|
||||
if qApp.keyboardModifiers() == Qt.ControlModifier:
|
||||
theCursor = self.cursorForPosition(mEvent.pos())
|
||||
self._followTag(theCursor)
|
||||
QTextEdit.mouseReleaseEvent(self, mEvent)
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _followTag(self, theCursor=None):
|
||||
"""Activated by Ctrl+Enter. Checks that we're in a block starting with '@'. We then find the
|
||||
word under the cursor and check that it is after the ':'. If all this is fine, we have a tag
|
||||
and can tell the document viewer to try and find and load the file where the tag is defined.
|
||||
"""
|
||||
|
||||
if theCursor is None:
|
||||
theCursor = self.textCursor()
|
||||
|
||||
theBlock = theCursor.block()
|
||||
theText = theBlock.text()
|
||||
|
||||
if len(theText) == 0:
|
||||
return False
|
||||
|
||||
if theText.startswith("@"):
|
||||
|
||||
theCursor.select(QTextCursor.WordUnderCursor)
|
||||
theWord = theCursor.selectedText()
|
||||
cPos = theText.find(":")
|
||||
wPos = theCursor.selectionStart() - theBlock.position()
|
||||
if wPos <= cPos:
|
||||
return False
|
||||
|
||||
logger.verbose("Attempting to follow tag '%s'" % theWord)
|
||||
self.theParent.docViewer.loadFromTag(theWord)
|
||||
|
||||
return True
|
||||
|
||||
def _insertHardBreak(self):
|
||||
theCursor = self.textCursor()
|
||||
theCursor.beginEditBlock()
|
||||
theCursor.insertText(" \n")
|
||||
theCursor.endEditBlock()
|
||||
return
|
||||
|
||||
def _insertNonBreakingSpace(self):
|
||||
theCursor = self.textCursor()
|
||||
theCursor.beginEditBlock()
|
||||
theCursor.insertText(nwUnicode.U_NBSP)
|
||||
theCursor.endEditBlock()
|
||||
return
|
||||
|
||||
def _openSpellContext(self):
|
||||
self._openContextMenu(self.cursorRect().center())
|
||||
return
|
||||
|
||||
def _openContextMenu(self, thePos):
|
||||
|
||||
if not self.spellCheck:
|
||||
return
|
||||
|
||||
theCursor = self.cursorForPosition(thePos)
|
||||
theCursor.select(QTextCursor.WordUnderCursor)
|
||||
theWord = theCursor.selectedText()
|
||||
if theWord == "":
|
||||
return
|
||||
if self.theDict.checkWord(theWord):
|
||||
return
|
||||
|
||||
mnuSuggest = QMenu()
|
||||
mnuHead = QAction("Spelling Suggestion(s)", mnuSuggest)
|
||||
mnuSuggest.addAction(mnuHead)
|
||||
mnuSuggest.addSeparator()
|
||||
theSuggest = self.theDict.suggestWords(theWord)
|
||||
if len(theSuggest) > 0:
|
||||
for aWord in theSuggest:
|
||||
mnuWord = QAction(aWord, mnuSuggest)
|
||||
mnuWord.triggered.connect(lambda thePos, aWord=aWord : self._correctWord(theCursor, aWord))
|
||||
mnuSuggest.addAction(mnuWord)
|
||||
mnuSuggest.addSeparator()
|
||||
mnuAdd = QAction("Add Word to Dictionary", mnuSuggest)
|
||||
mnuAdd.triggered.connect(lambda thePos : self._addWord(theCursor))
|
||||
mnuSuggest.addAction(mnuAdd)
|
||||
else:
|
||||
mnuHead = QAction("No Suggestions", mnuSuggest)
|
||||
mnuSuggest.addAction(mnuHead)
|
||||
|
||||
mnuSuggest.exec_(self.viewport().mapToGlobal(thePos))
|
||||
|
||||
return
|
||||
|
||||
def _correctWord(self, theCursor, theWord):
|
||||
xPos = theCursor.selectionStart()
|
||||
theCursor.beginEditBlock()
|
||||
theCursor.removeSelectedText()
|
||||
theCursor.insertText(theWord)
|
||||
theCursor.endEditBlock()
|
||||
theCursor.setPosition(xPos)
|
||||
self.setTextCursor(theCursor)
|
||||
return
|
||||
|
||||
def _addWord(self, theCursor):
|
||||
theWord = theCursor.selectedText().strip()
|
||||
logger.info("Added '%s' to project dictionary" % theWord)
|
||||
self.theDict.addWord(theWord)
|
||||
self.hLight.setDict(self.theDict)
|
||||
self.hLight.rehighlightBlock(theCursor.block())
|
||||
return
|
||||
|
||||
def _docChange(self, thePos, charsRemoved, charsAdded):
|
||||
self.lastEdit = time()
|
||||
if not self.docChanged:
|
||||
self.setDocumentChanged(True)
|
||||
if not self.wcTimer.isActive():
|
||||
self.wcTimer.start()
|
||||
if self.mainConf.doReplace and not self.hasSelection:
|
||||
self._docAutoReplace(self.qDocument.findBlock(thePos))
|
||||
return
|
||||
|
||||
def _docAutoReplace(self, theBlock):
|
||||
"""Autoreplace text elements based on main configuration.
|
||||
"""
|
||||
|
||||
if not theBlock.isValid():
|
||||
return
|
||||
|
||||
theText = theBlock.text()
|
||||
theCursor = self.textCursor()
|
||||
thePos = theCursor.positionInBlock()
|
||||
theLen = len(theText)
|
||||
|
||||
if theLen < 1 or thePos-1 > theLen:
|
||||
return
|
||||
|
||||
theOne = theText[thePos-1:thePos]
|
||||
theTwo = theText[thePos-2:thePos]
|
||||
theThree = theText[thePos-3:thePos]
|
||||
|
||||
if self.mainConf.doReplaceDQuote and theTwo == " \"":
|
||||
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 1)
|
||||
theCursor.insertText(self.typDQOpen)
|
||||
|
||||
elif self.mainConf.doReplaceDQuote and theOne == "\"":
|
||||
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 1)
|
||||
if thePos == 1:
|
||||
theCursor.insertText(self.typDQOpen)
|
||||
else:
|
||||
theCursor.insertText(self.typDQClose)
|
||||
|
||||
elif self.mainConf.doReplaceSQuote and theTwo == " '":
|
||||
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 1)
|
||||
theCursor.insertText(self.typSQOpen)
|
||||
|
||||
elif self.mainConf.doReplaceSQuote and theOne == "'":
|
||||
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 1)
|
||||
if thePos == 1:
|
||||
theCursor.insertText(self.typSQOpen)
|
||||
else:
|
||||
theCursor.insertText(self.typSQClose)
|
||||
|
||||
elif self.mainConf.doReplaceDash and theTwo == "--":
|
||||
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 2)
|
||||
theCursor.insertText(nwUnicode.U_ENDASH)
|
||||
|
||||
elif self.mainConf.doReplaceDash and theTwo == nwUnicode.U_ENDASH+"-":
|
||||
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 2)
|
||||
theCursor.insertText(nwUnicode.U_EMDASH)
|
||||
|
||||
elif self.mainConf.doReplaceDots and theThree == "...":
|
||||
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 3)
|
||||
theCursor.insertText(nwUnicode.U_HELLIP)
|
||||
|
||||
return
|
||||
|
||||
def _runCounter(self):
|
||||
"""Decide whether to run the word counter, or stop the timer due to inactivity.
|
||||
"""
|
||||
sinceActive = time()-self.lastEdit
|
||||
if sinceActive > 5*self.wcInterval:
|
||||
logger.debug("Stopping word count timer: no activity last %.1f seconds" % sinceActive)
|
||||
self.wcTimer.stop()
|
||||
elif self.wCounter.isRunning():
|
||||
logger.verbose("Word counter thread is busy")
|
||||
else:
|
||||
logger.verbose("Starting word counter")
|
||||
self.wCounter.start()
|
||||
return
|
||||
|
||||
def _updateCounts(self):
|
||||
"""Slot for the word counter's finished signal
|
||||
"""
|
||||
logger.verbose("Updating word count")
|
||||
|
||||
tHandle = self.nwDocument.docHandle
|
||||
self.charCount = self.wCounter.charCount
|
||||
self.wordCount = self.wCounter.wordCount
|
||||
self.paraCount = self.wCounter.paraCount
|
||||
self.theParent.statusBar.setCounts(self.charCount,self.wordCount,self.paraCount)
|
||||
self.theParent.treeView.propagateCount(tHandle, self.wordCount)
|
||||
self.theParent.treeView.projectWordCount()
|
||||
|
||||
return
|
||||
|
||||
def _wrapSelection(self, tBefore, tAfter):
|
||||
"""Wraps the selected text in whatever is in tBefore and tAfter. If there is no selection,
|
||||
the autoSelect setting decides the action. AutoSelect will select the word under the cursor
|
||||
before wrapping it. If this feature is disabled, nothing is done.
|
||||
"""
|
||||
theCursor = self.textCursor()
|
||||
if self.mainConf.autoSelect and not theCursor.hasSelection():
|
||||
theCursor.select(QTextCursor.WordUnderCursor)
|
||||
if theCursor.hasSelection():
|
||||
posS = theCursor.selectionStart()
|
||||
posE = theCursor.selectionEnd()
|
||||
theCursor.clearSelection()
|
||||
theCursor.beginEditBlock()
|
||||
theCursor.setPosition(posE)
|
||||
theCursor.insertText(tAfter)
|
||||
theCursor.setPosition(posS)
|
||||
theCursor.insertText(tBefore)
|
||||
theCursor.endEditBlock()
|
||||
else:
|
||||
logger.warning("No selection made, nothing to do")
|
||||
return
|
||||
|
||||
def _makeSelection(self, selMode):
|
||||
theCursor = self.textCursor()
|
||||
theCursor.clearSelection()
|
||||
theCursor.select(selMode)
|
||||
self.setTextCursor(theCursor)
|
||||
return
|
||||
|
||||
def _beginSearch(self):
|
||||
"""Sets the selected text as the search text for the search bar.
|
||||
"""
|
||||
theCursor = self.textCursor()
|
||||
if theCursor.hasSelection():
|
||||
selText = theCursor.selectedText()
|
||||
else:
|
||||
selText = ""
|
||||
self.theParent.searchBar.setSearchText(selText)
|
||||
return
|
||||
|
||||
def _beginReplace(self):
|
||||
"""Opens the replace line of the search bar and sets the replace text.
|
||||
"""
|
||||
self._beginSearch()
|
||||
self.theParent.searchBar.setReplaceText("")
|
||||
return
|
||||
|
||||
def _findNext(self):
|
||||
"""Searches for the next occurrence of the search bar text in the document.
|
||||
Wraps back to the top if not found.
|
||||
"""
|
||||
searchFor = self.theParent.searchBar.getSearchText()
|
||||
wasFound = self.find(searchFor)
|
||||
if not wasFound:
|
||||
theCursor = self.textCursor()
|
||||
theCursor.movePosition(QTextCursor.Start)
|
||||
self.setTextCursor(theCursor)
|
||||
return
|
||||
|
||||
def _findPrev(self):
|
||||
"""Searches for the previous occurrence of the search bar text in the document.
|
||||
Wraps back to the end if not found.
|
||||
"""
|
||||
searchFor = self.theParent.searchBar.getSearchText()
|
||||
wasFound = self.find(searchFor, QTextDocument.FindBackward)
|
||||
if not wasFound:
|
||||
theCursor = self.textCursor()
|
||||
theCursor.movePosition(QTextCursor.End)
|
||||
self.setTextCursor(theCursor)
|
||||
return
|
||||
|
||||
def _replaceNext(self):
|
||||
"""Searches for the next occurrence of the search bar text in the document and replaces it
|
||||
with the replace text. Wraps back to the top if not found.
|
||||
"""
|
||||
theCursor = self.textCursor()
|
||||
searchFor = self.theParent.searchBar.getSearchText()
|
||||
replWith = self.theParent.searchBar.getReplaceText()
|
||||
if theCursor.hasSelection() and theCursor.selectedText() == searchFor:
|
||||
xPos = theCursor.selectionStart()
|
||||
theCursor.beginEditBlock()
|
||||
theCursor.removeSelectedText()
|
||||
theCursor.insertText(replWith)
|
||||
theCursor.endEditBlock()
|
||||
theCursor.setPosition(xPos)
|
||||
self.setTextCursor(theCursor)
|
||||
logger.verbose("Replaced occurrence of '%s' with '%s' on line %d" % (
|
||||
searchFor, replWith, theCursor.blockNumber()
|
||||
))
|
||||
if searchFor != "":
|
||||
self._findNext()
|
||||
return
|
||||
|
||||
# END Class GuiDocEditor
|
||||
@@ -0,0 +1,529 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter GUI Document Tree
|
||||
|
||||
novelWriter – GUI Document Tree
|
||||
=================================
|
||||
Class holding the left side document tree view
|
||||
|
||||
File History:
|
||||
Created: 2018-09-29 [0.0.1]
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
import nw
|
||||
|
||||
from PyQt5.QtCore import Qt, QSize
|
||||
from PyQt5.QtGui import QIcon, QFont, QColor
|
||||
from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QAbstractItemView, QApplication
|
||||
|
||||
from nw.project.item import NWItem
|
||||
from nw.enum import nwItemType, nwItemClass, nwAlert
|
||||
from nw.constants import nwLabels
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class GuiDocTree(QTreeWidget):
|
||||
|
||||
C_NAME = 0
|
||||
C_COUNT = 1
|
||||
C_FLAGS = 2
|
||||
C_HANDLE = 3
|
||||
|
||||
def __init__(self, theParent, theProject):
|
||||
QTreeWidget.__init__(self, theParent)
|
||||
|
||||
logger.debug("Initialising DocTree ...")
|
||||
self.mainConf = nw.CONFIG
|
||||
self.debugGUI = self.mainConf.debugGUI
|
||||
self.theParent = theParent
|
||||
self.theTheme = theParent.theTheme
|
||||
self.theProject = theProject
|
||||
|
||||
# Tree Settings
|
||||
self.theMap = None
|
||||
self.orphRoot = None
|
||||
|
||||
self.clearTree()
|
||||
|
||||
# Build GUI
|
||||
self.setIconSize(QSize(13,13))
|
||||
self.setExpandsOnDoubleClick(True)
|
||||
self.setIndentation(13)
|
||||
self.setColumnCount(4)
|
||||
self.setHeaderLabels(["Label","Words","Flags","Handle"])
|
||||
if not self.debugGUI:
|
||||
self.hideColumn(self.C_HANDLE)
|
||||
|
||||
treeHead = self.headerItem()
|
||||
treeHead.setTextAlignment(self.C_COUNT,Qt.AlignRight)
|
||||
|
||||
# Allow Move by Drag & Drop
|
||||
self.setDragEnabled(True)
|
||||
self.setDragDropMode(QAbstractItemView.InternalMove)
|
||||
|
||||
# Set Multiple Selection by CTRL
|
||||
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
||||
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
|
||||
for colN in range(len(self.mainConf.treeColWidth)):
|
||||
self.setColumnWidth(colN,self.mainConf.treeColWidth[colN])
|
||||
|
||||
self.fontFlags = QFont("Monospace",10)
|
||||
self.fontCount = QFont("Monospace",10)
|
||||
|
||||
logger.debug("DocTree initialisation complete")
|
||||
|
||||
# Internal Mapping
|
||||
self.makeAlert = self.theParent.makeAlert
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Class Methods
|
||||
##
|
||||
|
||||
def clearTree(self):
|
||||
self.clear()
|
||||
self.theMap = {}
|
||||
self.orphRoot = None
|
||||
return
|
||||
|
||||
def newTreeItem(self, itemType, itemClass):
|
||||
|
||||
pHandle = self.getSelectedHandle()
|
||||
|
||||
if not self.theParent.hasProject:
|
||||
return False
|
||||
|
||||
if itemClass is None and pHandle is not None:
|
||||
pItem = self.theProject.getItem(pHandle)
|
||||
if pItem is not None:
|
||||
itemClass = pItem.itemClass
|
||||
|
||||
if itemClass is None:
|
||||
if itemType is not None:
|
||||
if itemType == nwItemType.FILE:
|
||||
self.makeAlert(
|
||||
"Please select a valid location in the tree to add a document.",
|
||||
nwAlert.ERROR
|
||||
)
|
||||
return False
|
||||
elif itemType == nwItemType.FOLDER:
|
||||
self.makeAlert(
|
||||
"Please select a valid location in the tree to add a folder.",
|
||||
nwAlert.ERROR
|
||||
)
|
||||
return False
|
||||
self.makeAlert("Failed to add new item.", nwAlert.BUG)
|
||||
return False
|
||||
|
||||
logger.verbose("Adding new item of type %s and class %s to handle %s" % (
|
||||
itemType.name, itemClass.name, str(pHandle))
|
||||
)
|
||||
|
||||
if itemType == nwItemType.ROOT:
|
||||
tHandle = self.theProject.newRoot(nwLabels.CLASS_NAME[itemClass], itemClass)
|
||||
|
||||
else:
|
||||
# If no parent has been selected, make the new file under the root NOVEL item.
|
||||
if pHandle is None:
|
||||
pHandle = self.theProject.findRootItem(nwItemClass.NOVEL)
|
||||
|
||||
# If still nothing, give up
|
||||
if pHandle is None:
|
||||
logger.error("Did not find anywhere to add the item!")
|
||||
return False
|
||||
|
||||
# Now check if the selected item is a file, in which case the new file will be a sibling
|
||||
pItem = self.theProject.getItem(pHandle)
|
||||
if pItem.itemType == nwItemType.FILE:
|
||||
pHandle = pItem.parHandle
|
||||
|
||||
# If we again has no home, give up
|
||||
if pHandle is None:
|
||||
self.makeAlert("Did not find anywhere to add the file or folder!", nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
if pHandle == self.theProject.trashRoot:
|
||||
self.makeAlert("Cannot add new files or folders to the trash folder.", nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
# If we're still here, add the file or folder
|
||||
if itemType == nwItemType.FILE:
|
||||
tHandle = self.theProject.newFile("New File", itemClass, pHandle)
|
||||
elif itemType == nwItemType.FOLDER:
|
||||
tHandle = self.theProject.newFolder("New Folder", itemClass, pHandle)
|
||||
else:
|
||||
logger.error("Failed to add new item")
|
||||
return False
|
||||
|
||||
# Add the new item to the tree
|
||||
nwItem = self.theProject.getItem(tHandle)
|
||||
trItem = self._addTreeItem(nwItem)
|
||||
if pHandle is not None and pHandle in self.theMap.keys():
|
||||
self.theMap[pHandle].setExpanded(True)
|
||||
self.clearSelection()
|
||||
trItem.setSelected(True)
|
||||
self.theParent.editItem()
|
||||
|
||||
return True
|
||||
|
||||
def moveTreeItem(self, nStep):
|
||||
"""Move an item up or down in the tree, but only if the treeView has focus. This also
|
||||
applies when the menu is used.
|
||||
"""
|
||||
if QApplication.focusWidget() == self and self.theParent.hasProject:
|
||||
tHandle = self.getSelectedHandle()
|
||||
tItem = self._getTreeItem(tHandle)
|
||||
pItem = tItem.parent()
|
||||
if pItem is None:
|
||||
tIndex = self.indexOfTopLevelItem(tItem)
|
||||
nChild = self.topLevelItemCount()
|
||||
nIndex = tIndex + nStep
|
||||
if nIndex < 0 or nIndex >= nChild:
|
||||
return False
|
||||
cItem = self.takeTopLevelItem(tIndex)
|
||||
self.insertTopLevelItem(nIndex, cItem)
|
||||
else:
|
||||
tIndex = pItem.indexOfChild(tItem)
|
||||
nChild = pItem.childCount()
|
||||
nIndex = tIndex + nStep
|
||||
if nIndex < 0 or nIndex >= nChild:
|
||||
return False
|
||||
cItem = pItem.takeChild(tIndex)
|
||||
pItem.insertChild(nIndex, cItem)
|
||||
self.clearSelection()
|
||||
cItem.setSelected(True)
|
||||
self.theProject.setProjectChanged(True)
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
def saveTreeOrder(self):
|
||||
theList = []
|
||||
for i in range(self.topLevelItemCount()):
|
||||
if self.topLevelItem(i) == self.orphRoot:
|
||||
continue
|
||||
theList = self._scanChildren(theList, self.topLevelItem(i), i)
|
||||
self.theProject.setTreeOrder(theList)
|
||||
return True
|
||||
|
||||
def getColumnSizes(self):
|
||||
retVals = [
|
||||
self.columnWidth(0),
|
||||
self.columnWidth(1),
|
||||
self.columnWidth(2),
|
||||
]
|
||||
return retVals
|
||||
|
||||
def deleteItem(self, tHandle=None):
|
||||
"""Delete items from the tree. Note that this does not delete the item from the item tree in
|
||||
the project object. However, since this is only meta data, there isn't really a need to do
|
||||
that to save memory. As items not in the tree are not saved to the project file, a loaded
|
||||
project will be clean anyway.
|
||||
"""
|
||||
|
||||
if tHandle is None:
|
||||
tHandle = self.getSelectedHandle()
|
||||
|
||||
if tHandle is None:
|
||||
return False
|
||||
|
||||
trItemS = self._getTreeItem(tHandle)
|
||||
nwItemS = self.theProject.getItem(tHandle)
|
||||
|
||||
if nwItemS.itemType == nwItemType.FILE:
|
||||
logger.debug("User requested file %s moved to trash" % tHandle)
|
||||
trItemP = trItemS.parent()
|
||||
trItemT = self._addTrashRoot()
|
||||
if trItemP is None or trItemT is None:
|
||||
logger.error("Could not move item to trash")
|
||||
return False
|
||||
tIndex = trItemP.indexOfChild(trItemS)
|
||||
trItemC = trItemP.takeChild(tIndex)
|
||||
trItemT.addChild(trItemC)
|
||||
nwItemS.setParent(self.theProject.trashRoot)
|
||||
self.clearSelection()
|
||||
trItemP.setSelected(True)
|
||||
self.theProject.setProjectChanged(True)
|
||||
|
||||
elif nwItemS.itemType == nwItemType.FOLDER:
|
||||
logger.debug("User requested folder %s deleted" % tHandle)
|
||||
trItemP = trItemS.parent()
|
||||
if trItemP is None:
|
||||
logger.error("Could not delete folder")
|
||||
return False
|
||||
tIndex = trItemP.indexOfChild(trItemS)
|
||||
if trItemS.childCount() == 0:
|
||||
trItemP.takeChild(tIndex)
|
||||
self.clearSelection()
|
||||
trItemP.setSelected(True)
|
||||
self.theProject.deleteItem(tHandle)
|
||||
else:
|
||||
self.makeAlert(["Cannot delete folder.","It is not empty."], nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
elif nwItemS.itemType == nwItemType.ROOT:
|
||||
logger.debug("User requested root folder %s deleted" % tHandle)
|
||||
tIndex = self.indexOfTopLevelItem(trItemS)
|
||||
if trItemS.childCount() == 0:
|
||||
self.takeTopLevelItem(tIndex)
|
||||
self.theParent.mainMenu.setAvailableRoot()
|
||||
self.theProject.setProjectChanged(True)
|
||||
else:
|
||||
self.makeAlert(["Cannot delete root folder.","It is not empty."], nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def setTreeItemValues(self, tHandle):
|
||||
|
||||
trItem = self._getTreeItem(tHandle)
|
||||
nwItem = self.theProject.getItem(tHandle)
|
||||
tName = nwItem.itemName
|
||||
tClass = nwItem.itemClass
|
||||
tHandle = nwItem.itemHandle
|
||||
pHandle = nwItem.parHandle
|
||||
|
||||
tStatus = nwLabels.CLASS_FLAG[nwItem.itemClass]
|
||||
if nwItem.itemType == nwItemType.FILE:
|
||||
tStatus += "."+nwLabels.LAYOUT_FLAG[nwItem.itemLayout]
|
||||
iStatus = nwItem.itemStatus
|
||||
if tClass == nwItemClass.NOVEL:
|
||||
iStatus = self.theProject.statusItems.checkEntry(iStatus) # Make sure it's a valid index
|
||||
flagIcon = self.theParent.statusIcons[iStatus]
|
||||
else:
|
||||
iStatus = self.theProject.importItems.checkEntry(iStatus) # Make sure it's a valid index
|
||||
flagIcon = self.theParent.importIcons[iStatus]
|
||||
|
||||
trItem.setText(self.C_NAME, tName)
|
||||
trItem.setText(self.C_FLAGS,tStatus)
|
||||
trItem.setIcon(self.C_FLAGS,flagIcon)
|
||||
|
||||
return
|
||||
|
||||
def propagateCount(self, tHandle, theCount, nDepth=0):
|
||||
tItem = self._getTreeItem(tHandle)
|
||||
if tItem is not None:
|
||||
tItem.setText(self.C_COUNT,str(theCount))
|
||||
pItem = tItem.parent()
|
||||
if pItem is not None:
|
||||
pCount = 0
|
||||
for i in range(pItem.childCount()):
|
||||
pCount += int(pItem.child(i).text(self.C_COUNT))
|
||||
pHandle = pItem.text(self.C_HANDLE)
|
||||
if not nDepth > 200 and pHandle != "":
|
||||
self.propagateCount(pHandle, pCount, nDepth+1)
|
||||
return
|
||||
|
||||
def projectWordCount(self):
|
||||
nWords = 0
|
||||
for n in range(self.topLevelItemCount()):
|
||||
tItem = self.topLevelItem(n)
|
||||
if tItem == self.orphRoot:
|
||||
continue
|
||||
nWords += int(tItem.text(self.C_COUNT))
|
||||
self.theProject.setProjectWordCount(nWords)
|
||||
sWords = self.theProject.getSessionWordCount()
|
||||
self.theParent.statusBar.setStats(nWords,sWords)
|
||||
return
|
||||
|
||||
def buildTree(self):
|
||||
self.clear()
|
||||
for nwItem in self.theProject.getProjectItems():
|
||||
self._addTreeItem(nwItem)
|
||||
return True
|
||||
|
||||
def getSelectedHandle(self):
|
||||
selItem = self.selectedItems()
|
||||
if len(selItem) == 0:
|
||||
return None
|
||||
if isinstance(selItem[0], QTreeWidgetItem):
|
||||
return selItem[0].text(self.C_HANDLE)
|
||||
return None
|
||||
|
||||
def getSelectedHandles(self):
|
||||
selItems = self.selectedItems()
|
||||
selHandles = []
|
||||
for n in range(len(selItems)):
|
||||
if isinstance(selItems[n], QTreeWidgetItem):
|
||||
selHandles.append(selItems[n].text(self.C_HANDLE))
|
||||
return selHandles
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _getTreeItem(self, tHandle):
|
||||
if tHandle in self.theMap.keys():
|
||||
return self.theMap[tHandle]
|
||||
return None
|
||||
|
||||
def _scanChildren(self, theList, theItem, theIndex):
|
||||
tHandle = theItem.text(self.C_HANDLE)
|
||||
nwItem = self.theProject.projTree[tHandle]
|
||||
nwItem.setExpanded(theItem.isExpanded())
|
||||
nwItem.setOrder(theIndex)
|
||||
theList.append(tHandle)
|
||||
for i in range(theItem.childCount()):
|
||||
self._scanChildren(theList, theItem.child(i), i)
|
||||
return theList
|
||||
|
||||
def _addTreeItem(self, nwItem):
|
||||
|
||||
tHandle = nwItem.itemHandle
|
||||
pHandle = nwItem.parHandle
|
||||
newItem = QTreeWidgetItem([""]*4)
|
||||
|
||||
newItem.setText(self.C_NAME, "")
|
||||
newItem.setText(self.C_COUNT, "0")
|
||||
newItem.setText(self.C_FLAGS, "")
|
||||
newItem.setText(self.C_HANDLE, tHandle)
|
||||
|
||||
# newItem.setForeground(self.C_COUNT,QColor(*self.theParent.theTheme.treeWCount))
|
||||
newItem.setTextAlignment(self.C_COUNT,Qt.AlignRight)
|
||||
newItem.setFont(self.C_FLAGS,self.fontFlags)
|
||||
|
||||
self.theMap[tHandle] = newItem
|
||||
if pHandle is None:
|
||||
if nwItem.itemType == nwItemType.ROOT:
|
||||
self.addTopLevelItem(newItem)
|
||||
self.theParent.mainMenu.setAvailableRoot()
|
||||
elif nwItem.itemType == nwItemType.TRASH:
|
||||
self.addTopLevelItem(newItem)
|
||||
else:
|
||||
self._addOrphanedRoot()
|
||||
self.orphRoot.addChild(newItem)
|
||||
else:
|
||||
self.theMap[pHandle].addChild(newItem)
|
||||
self.propagateCount(tHandle, nwItem.wordCount)
|
||||
|
||||
self.setTreeItemValues(tHandle)
|
||||
newItem.setExpanded(nwItem.isExpanded)
|
||||
|
||||
if nwItem.itemType == nwItemType.ROOT:
|
||||
newItem.setIcon(self.C_NAME, self.theTheme.getIcon("root"))
|
||||
elif nwItem.itemType == nwItemType.FOLDER:
|
||||
newItem.setIcon(self.C_NAME, self.theTheme.getIcon("folder"))
|
||||
elif nwItem.itemType == nwItemType.FILE:
|
||||
newItem.setIcon(self.C_NAME, self.theTheme.getIcon("document"))
|
||||
elif nwItem.itemType == nwItemType.TRASH:
|
||||
newItem.setIcon(self.C_NAME, self.theTheme.getIcon("trash"))
|
||||
|
||||
return newItem
|
||||
|
||||
def _addTrashRoot(self):
|
||||
if self.theProject.trashRoot is None:
|
||||
self.theProject.addTrash()
|
||||
trItem = self._addTreeItem(
|
||||
self.theProject.getItem(self.theProject.trashRoot)
|
||||
)
|
||||
trItem.setExpanded(True)
|
||||
else:
|
||||
trItem = self._getTreeItem(self.theProject.trashRoot)
|
||||
return trItem
|
||||
|
||||
def _addOrphanedRoot(self):
|
||||
if self.orphRoot is None:
|
||||
newItem = QTreeWidgetItem([""]*4)
|
||||
newItem.setText(self.C_NAME, "Orphaned Files")
|
||||
newItem.setText(self.C_COUNT, "")
|
||||
newItem.setText(self.C_FLAGS, "")
|
||||
newItem.setText(self.C_HANDLE, "")
|
||||
self.addTopLevelItem(newItem)
|
||||
self.orphRoot = newItem
|
||||
newItem.setExpanded(True)
|
||||
newItem.setIcon(self.C_NAME, self.theTheme.getIcon("orphan"))
|
||||
return
|
||||
|
||||
def _cleanOrphanedRoot(self):
|
||||
if self.orphRoot is not None:
|
||||
if self.orphRoot.childCount() == 0:
|
||||
self.takeTopLevelItem(self.indexOfTopLevelItem(self.orphRoot))
|
||||
self.orphRoot = None
|
||||
return
|
||||
|
||||
def _updateItemParent(self, tHandle):
|
||||
trItemS = self._getTreeItem(tHandle)
|
||||
nwItemS = self.theProject.getItem(tHandle)
|
||||
trItemP = trItemS.parent()
|
||||
if trItemP is None:
|
||||
logger.error("Failed to find new parent item of %s" % tHandle)
|
||||
return
|
||||
pHandle = trItemP.text(self.C_HANDLE)
|
||||
nwItemS.setParent(pHandle)
|
||||
self.setTreeItemValues(tHandle)
|
||||
self.theProject.setProjectChanged(True)
|
||||
return
|
||||
|
||||
def _moveOrphanedItem(self, tHandle, dHandle):
|
||||
trItemS = self._getTreeItem(tHandle)
|
||||
nwItemS = self.theProject.getItem(tHandle)
|
||||
nwItemD = self.theProject.getItem(dHandle)
|
||||
trItemP = trItemS.parent()
|
||||
nwItemS.setClass(nwItemD.itemClass)
|
||||
if trItemP is None:
|
||||
logger.error("Failed to find new parent item of %s" % tHandle)
|
||||
return
|
||||
pHandle = trItemP.text(self.C_HANDLE)
|
||||
nwItemS.setParent(pHandle)
|
||||
self.setTreeItemValues(tHandle)
|
||||
self.theProject.setProjectChanged(True)
|
||||
return
|
||||
|
||||
##
|
||||
# Event Overloading
|
||||
##
|
||||
|
||||
def mousePressEvent(self, theEvent):
|
||||
"""Overload mousePressEvent to clear selection if clicking the mouse in a blank
|
||||
area of the tree view.
|
||||
"""
|
||||
QTreeWidget.mousePressEvent(self, theEvent)
|
||||
selItem = self.indexAt(theEvent.pos())
|
||||
if not selItem.isValid():
|
||||
self.clearSelection()
|
||||
return
|
||||
|
||||
def dropEvent(self, theEvent):
|
||||
"""Overload the drop of dragged item event to check whether the drop is allowed
|
||||
or not. Disallowed drops are cancelled.
|
||||
"""
|
||||
sHandle = self.getSelectedHandle()
|
||||
if sHandle is None:
|
||||
return
|
||||
|
||||
dIndex = self.indexAt(theEvent.pos())
|
||||
if not dIndex.isValid():
|
||||
return
|
||||
|
||||
dItem = self.itemFromIndex(dIndex)
|
||||
dHandle = dItem.text(self.C_HANDLE)
|
||||
snItem = self.theProject.getItem(sHandle)
|
||||
dnItem = self.theProject.getItem(dHandle)
|
||||
isSame = snItem.itemClass == dnItem.itemClass
|
||||
isNone = snItem.itemClass == nwItemClass.NO_CLASS
|
||||
onFile = dnItem.itemType == nwItemType.FILE
|
||||
isRoot = snItem.itemType == nwItemType.ROOT
|
||||
onRoot = dnItem.itemType == nwItemType.ROOT
|
||||
isOnTop = self.dropIndicatorPosition() == QAbstractItemView.OnItem
|
||||
isAbove = self.dropIndicatorPosition() == QAbstractItemView.AboveItem
|
||||
isBelow = self.dropIndicatorPosition() == QAbstractItemView.BelowItem
|
||||
if (isSame or isNone) and not (onFile and isOnTop) and not isRoot:
|
||||
logger.verbose("Drag'n'drop of item %s accepted" % sHandle)
|
||||
QTreeWidget.dropEvent(self, theEvent)
|
||||
if isNone:
|
||||
self._moveOrphanedItem(sHandle, dHandle)
|
||||
self._cleanOrphanedRoot()
|
||||
else:
|
||||
self._updateItemParent(sHandle)
|
||||
elif isRoot and (isAbove or isBelow) and onRoot:
|
||||
logger.verbose("Drag'n'drop of item %s accepted" % sHandle)
|
||||
QTreeWidget.dropEvent(self, theEvent)
|
||||
else:
|
||||
logger.verbose("Drag'n'drop of item %s not accepted" % sHandle)
|
||||
|
||||
return
|
||||
|
||||
# END Class GuiDocTree
|
||||
@@ -0,0 +1,188 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter GUI Document Viewer
|
||||
|
||||
novelWriter – GUI Document Viewer
|
||||
===================================
|
||||
Class holding the document html viewer
|
||||
|
||||
File History:
|
||||
Created: 2019-05-10 [0.0.1]
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
import nw
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QTextBrowser
|
||||
from PyQt5.QtGui import QTextOption, QFont, QPalette, QColor
|
||||
|
||||
from nw.convert.tokenizer import Tokenizer
|
||||
from nw.convert.text.tohtml import ToHtml
|
||||
from nw.enum import nwItemType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class GuiDocViewer(QTextBrowser):
|
||||
|
||||
def __init__(self, theParent, theProject):
|
||||
QTextBrowser.__init__(self)
|
||||
|
||||
logger.debug("Initialising DocViewer ...")
|
||||
|
||||
# Class Variables
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theProject = theProject
|
||||
self.theParent = theParent
|
||||
self.theTheme = theParent.theTheme
|
||||
self.theHandle = None
|
||||
|
||||
self.qDocument = self.document()
|
||||
self.setMinimumWidth(300)
|
||||
self.initViewer()
|
||||
|
||||
theOpt = QTextOption()
|
||||
if self.mainConf.doJustify:
|
||||
theOpt.setAlignment(Qt.AlignJustify)
|
||||
self.qDocument.setDefaultTextOption(theOpt)
|
||||
|
||||
logger.debug("DocViewer initialisation complete")
|
||||
|
||||
return
|
||||
|
||||
def clearViewer(self):
|
||||
self.clear()
|
||||
self.setSearchPaths([""])
|
||||
return True
|
||||
|
||||
def initViewer(self):
|
||||
"""Set editor settings from main config.
|
||||
"""
|
||||
|
||||
self._makeStyleSheet()
|
||||
|
||||
# Set Font
|
||||
theFont = QFont()
|
||||
if self.mainConf.textFont is None:
|
||||
# If none is defined, set the default back to config
|
||||
self.mainConf.textFont = self.qDocument.defaultFont().family()
|
||||
theFont.setFamily(self.mainConf.textFont)
|
||||
theFont.setPointSize(self.mainConf.textSize)
|
||||
self.setFont(theFont)
|
||||
|
||||
docPalette = self.palette()
|
||||
docPalette.setColor(QPalette.Base, QColor(*self.theTheme.colBack))
|
||||
docPalette.setColor(QPalette.Text, QColor(*self.theTheme.colText))
|
||||
self.setPalette(docPalette)
|
||||
|
||||
self.qDocument.setDocumentMargin(self.mainConf.textMargin)
|
||||
theOpt = QTextOption()
|
||||
if self.mainConf.doJustify:
|
||||
theOpt.setAlignment(Qt.AlignJustify)
|
||||
self.qDocument.setDefaultTextOption(theOpt)
|
||||
|
||||
# If we have a document open, we should reload it in case the font changed
|
||||
if self.theHandle is not None:
|
||||
tHandle = self.theHandle
|
||||
self.clearViewer()
|
||||
self.loadText(tHandle)
|
||||
|
||||
return True
|
||||
|
||||
def loadText(self, tHandle):
|
||||
|
||||
tItem = self.theProject.getItem(tHandle)
|
||||
if tItem is None:
|
||||
logger.warning("Item not found")
|
||||
return False
|
||||
|
||||
if tItem.itemType != nwItemType.FILE:
|
||||
return False
|
||||
|
||||
logger.debug("Generating preview for item %s" % tHandle)
|
||||
sPos = self.verticalScrollBar().value()
|
||||
aDoc = ToHtml(self.theProject, self.theParent)
|
||||
aDoc.setText(tHandle)
|
||||
aDoc.doAutoReplace()
|
||||
aDoc.tokenizeText()
|
||||
aDoc.doConvert()
|
||||
aDoc.doPostProcessing()
|
||||
self.setHtml(aDoc.theResult)
|
||||
if self.theHandle == tHandle:
|
||||
self.verticalScrollBar().setValue(sPos)
|
||||
self.theHandle = tHandle
|
||||
self.theProject.setLastViewed(tHandle)
|
||||
|
||||
return True
|
||||
|
||||
def loadFromTag(self, theTag):
|
||||
|
||||
logger.debug("Loading document from tag '%s'" % theTag)
|
||||
|
||||
if theTag in self.theParent.theIndex.tagIndex.keys():
|
||||
theTarget = self.theParent.theIndex.tagIndex[theTag]
|
||||
else:
|
||||
logger.debug("The tag was not found in the index")
|
||||
return False
|
||||
|
||||
if len(theTarget) != 3:
|
||||
# Just to make sure the index is not messed up
|
||||
return False
|
||||
|
||||
self.loadText(theTarget[1])
|
||||
|
||||
return True
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _makeStyleSheet(self):
|
||||
|
||||
self.qDocument.setDefaultStyleSheet((
|
||||
"body {{"
|
||||
" font-size: {textSize}pt;"
|
||||
" color: rgb({tColR},{tColG},{tColB});"
|
||||
"}}\n"
|
||||
"h1, h2, h3, h4 {{"
|
||||
" color: rgb({hColR},{hColG},{hColB});"
|
||||
"}}\n"
|
||||
"a {{"
|
||||
" color: rgb({aColR},{aColG},{aColB});"
|
||||
"}}\n"
|
||||
"pre {{"
|
||||
" color: rgb({cColR},{cColG},{cColB});"
|
||||
" font-size: {preSize}pt;"
|
||||
"}}\n"
|
||||
"mark {{"
|
||||
" color: rgb({eColR},{eColG},{eColB});"
|
||||
"}}\n"
|
||||
"table {{"
|
||||
" margin: 10px 0px;"
|
||||
"}}\n"
|
||||
"td {{"
|
||||
" padding: 0px 4px;"
|
||||
"}}\n"
|
||||
).format(
|
||||
textSize = self.mainConf.textSize,
|
||||
preSize = self.mainConf.textSize*0.9,
|
||||
tColR = self.theTheme.colText[0],
|
||||
tColG = self.theTheme.colText[1],
|
||||
tColB = self.theTheme.colText[2],
|
||||
hColR = self.theTheme.colHead[0],
|
||||
hColG = self.theTheme.colHead[1],
|
||||
hColB = self.theTheme.colHead[2],
|
||||
cColR = self.theTheme.colComm[0],
|
||||
cColG = self.theTheme.colComm[1],
|
||||
cColB = self.theTheme.colComm[2],
|
||||
eColR = self.theTheme.colEmph[0],
|
||||
eColG = self.theTheme.colEmph[1],
|
||||
eColB = self.theTheme.colEmph[2],
|
||||
aColR = self.theTheme.colLink[0],
|
||||
aColG = self.theTheme.colLink[1],
|
||||
aColB = self.theTheme.colLink[2],
|
||||
))
|
||||
|
||||
return True
|
||||
|
||||
# END Class GuiDocViewer
|
||||
@@ -0,0 +1,132 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter GUI Main Window SearchBar
|
||||
|
||||
novelWriter – GUI Main Window SearchBar
|
||||
=========================================
|
||||
Class holding the main window search bar
|
||||
|
||||
File History:
|
||||
Created: 2019-09-29 [0.2.1]
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
import nw
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWidgets import QFrame, QGridLayout, QLabel, QLineEdit, QPushButton, QApplication
|
||||
|
||||
from nw.enum import nwDocAction
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class GuiSearchBar(QFrame):
|
||||
|
||||
def __init__(self, theParent):
|
||||
QFrame.__init__(self, theParent)
|
||||
|
||||
logger.debug("Initialising GuiSearchBar ...")
|
||||
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theParent = theParent
|
||||
self.theTheme = theParent.theTheme
|
||||
self.repVisible = False
|
||||
|
||||
self.setContentsMargins(0,0,0,0)
|
||||
|
||||
self.mainBox = QGridLayout(self)
|
||||
self.setLayout(self.mainBox)
|
||||
|
||||
self.searchBox = QLineEdit()
|
||||
self.replaceBox = QLineEdit()
|
||||
self.searchLabel = QLabel("Search")
|
||||
self.replaceLabel = QLabel("Replace")
|
||||
self.closeButton = QPushButton(self.theTheme.getIcon("close"),"")
|
||||
self.searchButton = QPushButton(self.theTheme.getIcon("search"),"")
|
||||
self.replaceButton = QPushButton(self.theTheme.getIcon("replace"),"")
|
||||
|
||||
self.closeButton.clicked.connect(self._doClose)
|
||||
self.searchButton.clicked.connect(self._doSearch)
|
||||
self.replaceButton.clicked.connect(self._doReplace)
|
||||
self.searchBox.returnPressed.connect(self._doSearch)
|
||||
self.replaceBox.returnPressed.connect(self._doSearch)
|
||||
|
||||
self.mainBox.addWidget(QLabel(""), 0,0)
|
||||
self.mainBox.addWidget(self.searchLabel, 0,1)
|
||||
self.mainBox.addWidget(self.searchBox, 0,2)
|
||||
self.mainBox.addWidget(self.searchButton, 0,3)
|
||||
self.mainBox.addWidget(self.closeButton, 0,4)
|
||||
self.mainBox.addWidget(self.replaceLabel, 1,1)
|
||||
self.mainBox.addWidget(self.replaceBox, 1,2)
|
||||
self.mainBox.addWidget(self.replaceButton, 1,3)
|
||||
|
||||
self.mainBox.setColumnStretch(0,1)
|
||||
self.mainBox.setColumnStretch(1,0)
|
||||
self.mainBox.setColumnStretch(2,0)
|
||||
self.mainBox.setColumnStretch(3,0)
|
||||
self.mainBox.setColumnStretch(4,0)
|
||||
self.mainBox.setContentsMargins(0,0,0,0)
|
||||
|
||||
self.searchBox.setMinimumWidth(180)
|
||||
self.replaceBox.setMinimumWidth(180)
|
||||
|
||||
self._replaceVisible(False)
|
||||
|
||||
logger.debug("GuiSearchBar initialisation complete")
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Get and Set Functions
|
||||
##
|
||||
|
||||
def setSearchText(self, theText):
|
||||
if not self.isVisible():
|
||||
self.setVisible(True)
|
||||
self.searchBox.setText(theText)
|
||||
self.searchBox.setFocus(True)
|
||||
logger.verbose("Setting search text to '%s'" % theText)
|
||||
return True
|
||||
|
||||
def setReplaceText(self, theText):
|
||||
self._replaceVisible(True)
|
||||
self.replaceBox.setFocus(True)
|
||||
self.replaceBox.setText(theText)
|
||||
return True
|
||||
|
||||
def getSearchText(self):
|
||||
return self.searchBox.text()
|
||||
|
||||
def getReplaceText(self):
|
||||
return self.replaceBox.text()
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _doClose(self):
|
||||
self._replaceVisible(False)
|
||||
self.setVisible(False)
|
||||
return
|
||||
|
||||
def _doSearch(self):
|
||||
modKey = QApplication.keyboardModifiers()
|
||||
if modKey == Qt.ShiftModifier:
|
||||
self.theParent.docEditor.docAction(nwDocAction.GO_PREV)
|
||||
else:
|
||||
self.theParent.docEditor.docAction(nwDocAction.GO_NEXT)
|
||||
return
|
||||
|
||||
def _doReplace(self):
|
||||
self.theParent.docEditor.docAction(nwDocAction.REPL_NEXT)
|
||||
return
|
||||
|
||||
def _replaceVisible(self, isVisible):
|
||||
self.replaceLabel.setVisible(isVisible)
|
||||
self.replaceBox.setVisible(isVisible)
|
||||
self.replaceButton.setVisible(isVisible)
|
||||
self.repVisible = isVisible
|
||||
return True
|
||||
|
||||
# END Class GuiSearchBar
|
||||
Reference in New Issue
Block a user