Cleaned up the source files in the gui folder a bit.

This commit is contained in:
Veronica K. B. Olsen
2019-10-31 13:56:00 +01:00
parent 9fea45b7df
commit 6f893bf321
16 changed files with 45 additions and 41 deletions
+90
View File
@@ -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
+664
View File
@@ -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
+529
View File
@@ -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
+188
View File
@@ -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
+132
View File
@@ -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