Merge pull request #224 from vkbo/fixes

Various fixes for 0.6
This commit is contained in:
Veronica K. Berglyd Olsen
2020-05-24 18:41:47 +02:00
committed by GitHub
43 changed files with 345 additions and 107 deletions
+6
View File
@@ -99,6 +99,12 @@ python3 -m pip install lxml
python3 -m pip install pyenchant
```
PyQt/Qt should be at least 5.2.1, but ideally 5.10 or higher for nearly all features to work.
Exporting to markdown requires PyQt/Qt 5.14.
There are no known minimum for lxml, but the code was originally written with 4.2.
The optional spell check library must be at least 3.0.0 to work with Windows.
On Linux, 2.0.0 also works fine.
If no external spell checking tool is installed, novelWriter will use a basic spell checker based on standard Python package `difflib`.
Currently, only English dictionaries are available for this spell checker, but more can be added to the `nw/assets/dict` folder.
See the [nw/assets/dict/README.md](README.md) file in that folder for how to generate more dictionaries.
+5
View File
@@ -35,6 +35,11 @@ The following are optional, but recommended:
* ``pyenchant`` for spell checking
PyQt/Qt should be at least 5.2.1, but ideally 5.10 or higher for nearly all features to work.
Exporting to markdown requires PyQt/Qt 5.14.
There are no known minimum for lxml, but the code was originally written with 4.2.
The optional spell check library must be at least 3.0.0 to work with Windows.
On Linux, 2.0.0 also works fine.
Running novelWriter
===================
+2 -2
View File
@@ -136,7 +136,6 @@ class NWDoc():
"""Save the document via temp file in case of save failure, and
in any case keep a backup of the file.
"""
if self.docHandle is None or not self.docEditable:
return False
@@ -202,7 +201,6 @@ class NWDoc():
"""Parses the document meta tag and returns the path and name as
a list and a string.
"""
if len(self.docMeta) < 14:
# Not enough information
return "", []
@@ -232,6 +230,8 @@ class NWDoc():
@staticmethod
def _assemblePath(tHandle, docExt):
"""Assemble the file path for a given handle.
"""
if tHandle is None:
return None, None
docDir = "data_"+tHandle[0]
+7 -3
View File
@@ -244,7 +244,6 @@ class NWIndex():
and text as separate inputs as we want to primarily scan the
files before we save them, unless we're rebuilding the index.
"""
theItem = self.theProject.projTree[tHandle]
if theItem is None:
return False
@@ -529,13 +528,18 @@ class NWIndex():
# Extract Data
##
def getNovelStructure(self):
def getNovelStructure(self, skipExcluded=True):
"""Builds a list of all titles in the novel, in the correct
order as they appear in the tree view and in the respective
document files, but skipping all note files.
"""
theStructure = []
for tHandle in self.theProject.projTree.handles():
for tItem in self.theProject.projTree:
if tItem is None:
continue
if not tItem.isExported and skipExcluded:
continue
tHandle = tItem.itemHandle
if tHandle not in self.novelIndex:
continue
for sTitle in sorted(self.novelIndex[tHandle].keys()):
+70 -17
View File
@@ -181,7 +181,6 @@ class NWProject():
"""Clear the data for the current project, and set them to
default values.
"""
# Project Status
self.projOpened = 0
self.projChanged = False
@@ -236,7 +235,6 @@ class NWProject():
parse the XML of the file and populate the project variables and
build the tree of project items.
"""
if not path.isfile(fileName):
fileName = path.join(fileName, nwFiles.PROJ_FILE)
if not path.isfile(fileName):
@@ -404,7 +402,6 @@ class NWProject():
make sure if the save fails, we're not left with a truncated
file.
"""
if self.projPath is None:
self.makeAlert("Project path not set, cannot save.", nwAlert.ERROR)
return False
@@ -522,7 +519,6 @@ class NWProject():
def zipIt(self, doNotify):
"""Create a zip file of the entire project.
"""
logger.info("Backing up project")
self.theParent.statusBar.setStatus("Backing up project ...")
@@ -836,7 +832,6 @@ class NWProject():
def _readLockFile(self):
"""Reads the lock file in the project folder.
"""
if self.projPath is None:
return ["ERROR"]
@@ -863,7 +858,6 @@ class NWProject():
def _writeLockFile(self):
"""Writes a lock file to the project folder.
"""
if self.projPath is None:
return False
@@ -901,6 +895,8 @@ class NWProject():
return None
def _checkFolder(self, thePath):
"""Check if a folder exists, and if it doesn't, create it.
"""
if not path.isdir(thePath):
try:
mkdir(thePath)
@@ -911,6 +907,8 @@ class NWProject():
return True
def _packProjectValue(self, xParent, theName, theValue, allowNone=True):
"""Pack a list of values into an xml element.
"""
if not isinstance(theValue, list):
theValue = [theValue]
for aValue in theValue:
@@ -927,7 +925,6 @@ class NWProject():
orphaned files so the user can either delete them, or put them
back into the project tree.
"""
if self.projPath is None:
return
@@ -992,7 +989,6 @@ class NWProject():
def _appendSessionStats(self):
"""Append session statistics to the sessions log file.
"""
if self.projMeta is None:
return False
@@ -1234,9 +1230,13 @@ class NWTree():
##
def __len__(self):
"""Return the length counter. Does not check that it is correct!
"""
return self._theLength
def __bool__(self):
"""Returns True if the tree has any entries.
"""
return self._theLength > 0
##
@@ -1382,7 +1382,6 @@ class NWItem():
def unpackXML(self, xItem):
"""Sets the values from an XML entry of type 'item'.
"""
if xItem.tag != "item":
logger.error("XML entry is not an NWItem")
return False
@@ -1420,9 +1419,11 @@ class NWItem():
@staticmethod
def _subPack(xParent, name, attrib=None, text=None, none=True):
"""Packs the values into an xml element.
"""
if not none and (text == None or text == "None"):
return None
xSub = etree.SubElement(xParent,name,attrib=attrib)
xSub = etree.SubElement(xParent, name, attrib=attrib)
if text is not None:
xSub.text = text
return xSub
@@ -1432,10 +1433,14 @@ class NWItem():
##
def setName(self, theName):
"""Set the item name.
"""
self.itemName = theName.strip()
return
def setHandle(self, theHandle):
"""Set the item handle, and ensure it is valid.
"""
if isinstance(theHandle, str):
if len(theHandle) == 13:
self.itemHandle = theHandle
@@ -1446,6 +1451,8 @@ class NWItem():
return
def setParent(self, theParent):
"""Set the parent handle, and ensure that it is valid.
"""
if theParent is None:
self.parHandle = None
elif isinstance(theParent, str):
@@ -1458,10 +1465,16 @@ class NWItem():
return
def setOrder(self, theOrder):
"""Set the item order, and ensure that it is valid. This value
is purely a meta value, not actually used by novelWriter.
"""
self.itemOrder = checkInt(theOrder, 0)
return
def setType(self, theType):
"""Set the item type from either a proper nwItemType, or set it
from a string representing a nwItemType.
"""
if isinstance(theType, nwItemType):
self.itemType = theType
elif theType in nwItemType.__members__:
@@ -1472,6 +1485,9 @@ class NWItem():
return
def setClass(self, theClass):
"""Set the item class from either a proper nwItemClass, or set
it from a string representing a nwItemClass.
"""
if isinstance(theClass, nwItemClass):
self.itemClass = theClass
elif theClass in nwItemClass.__members__:
@@ -1482,6 +1498,9 @@ class NWItem():
return
def setLayout(self, theLayout):
"""Set the item layout from either a proper nwItemLayout, or set
it from a string representing a nwItemLayout.
"""
if isinstance(theLayout, nwItemLayout):
self.itemLayout = theLayout
elif theLayout in nwItemLayout.__members__:
@@ -1492,6 +1511,9 @@ class NWItem():
return
def setStatus(self, theStatus):
"""Set the item status by looking it up in the valid status
items of the current project.
"""
if self.itemClass == nwItemClass.NOVEL:
self.itemStatus = self.theProject.statusItems.checkEntry(theStatus)
else:
@@ -1499,6 +1521,8 @@ class NWItem():
return
def setExpanded(self, expState):
"""Save the expanded status of an item in the project tree.
"""
if isinstance(expState, str):
self.isExpanded = expState == str(True)
else:
@@ -1506,6 +1530,8 @@ class NWItem():
return
def setExported(self, expState):
"""Save the export flag.
"""
if isinstance(expState, str):
self.isExported = expState == str(True)
else:
@@ -1517,19 +1543,27 @@ class NWItem():
##
def setCharCount(self, theCount):
self.charCount = checkInt(theCount,0)
"""Set the character count, and ensure that it is an integer.
"""
self.charCount = checkInt(theCount, 0)
return
def setWordCount(self, theCount):
self.wordCount = checkInt(theCount,0)
"""Set the word count, and ensure that it is an integer.
"""
self.wordCount = checkInt(theCount, 0)
return
def setParaCount(self, theCount):
self.paraCount = checkInt(theCount,0)
"""Set the paragraph count, and ensure that it is an integer.
"""
self.paraCount = checkInt(theCount, 0)
return
def setCursorPos(self, thePosition):
self.cursorPos = checkInt(thePosition,0)
"""Set the cursor position, and ensure that it is an integer.
"""
self.cursorPos = checkInt(thePosition, 0)
return
# END Class NWItem
@@ -1551,6 +1585,9 @@ class NWStatus():
return
def addEntry(self, theLabel, theColours):
"""Add a status entry to the status object, but ensure it isn't
a duplicate.
"""
theLabel = theLabel.strip()
if self.lookupEntry(theLabel) is None:
self.theLabels.append(theLabel)
@@ -1561,6 +1598,9 @@ class NWStatus():
return True
def lookupEntry(self, theLabel):
"""Look up a status entry in the object lists, and return it if
it exists.
"""
if theLabel is None:
return None
theLabel = theLabel.strip()
@@ -1569,6 +1609,9 @@ class NWStatus():
return None
def checkEntry(self, theStatus):
"""Check if a status value is valid, and returns the safe
reference to be used internally.
"""
if isinstance(theStatus, str):
theStatus = theStatus.strip()
if self.lookupEntry(theStatus) is not None:
@@ -1578,11 +1621,12 @@ class NWStatus():
return self.theLabels[theStatus]
def setNewEntries(self, newList):
"""Update the list of entries after they have been modified by
the GUI tool.
"""
replaceMap = {}
if newList is not None:
self.theLabels = []
self.theColours = []
self.theCounts = []
@@ -1598,10 +1642,14 @@ class NWStatus():
return replaceMap
def resetCounts(self):
"""Clear the counts of references to the status entries.
"""
self.theCounts = [0]*self.theLength
return
def countEntry(self, theLabel):
"""Lookup the usage count of a given entry.
"""
theIndex = self.lookupEntry(theLabel)
if theIndex is not None:
self.theCounts[theIndex] += 1
@@ -1623,7 +1671,6 @@ class NWStatus():
def unpackEntries(self, xParent):
"""Unpack an XML tree and set the class values.
"""
theLabels = []
theColours = []
@@ -1661,15 +1708,21 @@ class NWStatus():
##
def __getitem__(self, n):
"""Return an entry by its index.
"""
if n >= 0 and n < self.theLength:
return self.theLabels[n], self.theColours[n], self.theCounts[n]
return None, None, None
def __iter__(self):
"""Initialise the iterator.
"""
self.theIndex = 0
return self
def __next__(self):
"""Return the next entry for the iterator.
"""
if self.theIndex < self.theLength:
theLabel, theColour, theCount = self.__getitem__(self.theIndex)
self.theIndex += 1
+36 -2
View File
@@ -55,15 +55,23 @@ class NWSpellCheck():
return
def setLanguage(self, theLang, projectDict=None):
"""Dummy function.
"""
return
def checkWord(self, theWord):
"""Dummy function.
"""
return True
def suggestWords(self, theWord):
"""Dummy function.
"""
return []
def addWord(self, newWord):
"""Add a word to the project dictionary.
"""
if self.projectDict is not None and newWord not in self.PROJW:
newWord = newWord.strip()
self.PROJW.append(newWord)
@@ -76,10 +84,14 @@ class NWSpellCheck():
return
def listDictionaries(self):
"""Dummy function.
"""
return []
@staticmethod
def expandLanguage(spTag):
"""Translate a language tag to something more suer friendly.
"""
spBits = spTag.split("_")
if spBits[0] in isoLanguage.ISO_639_1:
spLang = isoLanguage.ISO_639_1[spBits[0]]
@@ -94,6 +106,9 @@ class NWSpellCheck():
##
def _readProjectDictionary(self, projectDict):
"""Read the content of the project dictionary, and add it to the
lookup lists.
"""
self.PROJW = []
if projectDict is not None:
self.projectDict = projectDict
@@ -147,17 +162,25 @@ class NWSpellEnchant(NWSpellCheck):
return
def checkWord(self, theWord):
"""Wrapper function for pyenchant.
"""
return self.theDict.check(theWord)
def suggestWords(self, theWord):
"""Wrapper function for pyenchant.
"""
return self.theDict.suggest(theWord)
def addWord(self, newWord):
"""Wrapper function for pyenchant.
"""
self.theDict.add_to_session(newWord)
NWSpellCheck.addWord(self, newWord)
return
def listDictionaries(self):
"""Wrapper function for pyenchant.
"""
retList = []
try:
import enchant
@@ -171,6 +194,8 @@ class NWSpellEnchant(NWSpellCheck):
# END Class NWSpellEnchant
class NWSpellEnchantDummy:
"""Fallback for when Enchant is selected, but not installed.
"""
def __init__(self):
return
@@ -191,6 +216,11 @@ class NWSpellEnchantDummy:
# ================================================================================================ #
class NWSpellSimple(NWSpellCheck):
"""Internal spell check tool that uses standard Python packages with
no other external dependencies. This is the fallback spell checker
when no other is available. This method is fairly slow compared to
other implementations.
"""
WORDS = []
@@ -200,7 +230,8 @@ class NWSpellSimple(NWSpellCheck):
return
def setLanguage(self, theLang, projectDict=None):
"""Load a dictionary as a list from the app assets folder.
"""
self.WORDS = []
dictFile = path.join(self.mainConf.dictPath,theLang+".dict")
try:
@@ -259,6 +290,8 @@ class NWSpellSimple(NWSpellCheck):
return theOptions
def addWord(self, newWord):
"""Wrapper for the internal project dictionary feature.
"""
newWord = newWord.strip().lower()
if newWord not in self.WORDS:
self.WORDS.append(newWord)
@@ -266,7 +299,8 @@ class NWSpellSimple(NWSpellCheck):
return
def listDictionaries(self):
"""Lists the dictionary files in the app assets folder.
"""
retList = []
for dictFile in listdir(self.mainConf.dictPath):
+16
View File
@@ -540,6 +540,22 @@ class Tokenizer():
tToken[0], tToken[1], tToken[2], tToken[3] | self.A_PBA
)
# A single page is always left-aligned and starts on a fresh
# page, unless it's empty.
if self.isPage:
for n, tToken in enumerate(self.theTokens):
tType = tToken[0]
tText = tToken[1]
tFormat = tToken[2]
if n == 0:
self.theTokens[n] = (
tType, tText, tFormat, self.A_LEFT | self.A_PBB
)
else:
self.theTokens[n] = (
tType, tText, tFormat, self.A_LEFT
)
return
##
-3
View File
@@ -39,7 +39,6 @@ def countWords(theText):
"""Count words in a piece of text, skipping special syntax and
comments.
"""
charCount = 0
wordCount = 0
paraCount = 0
@@ -86,7 +85,6 @@ def projectMaintenance(theProject):
"""Wrapper class for handling various tasks related to managing old
projects with content from older versions of novelWriter.
"""
# Remove no longer used project cache folder
if path.isdir(theProject.projPath):
cacheDir = path.join(theProject.projPath, "cache")
@@ -141,7 +139,6 @@ def numberToWord(numVal, theLanguage):
def _numberToWordEN(numVal):
"""Convert numbers to English words.
"""
numWord = ""
oneWord = ""
tenWord = ""
+9 -1
View File
@@ -69,10 +69,14 @@ class PagedDialog(QDialog):
return
def addTab(self, tabWidget, tabLabel):
"""Forwards the adding of tabs to the QTabWidget.
"""
self._tabBox.addTab(tabWidget, tabLabel)
return
def addControls(self, buttonBar):
"""Adds a button bar to the dialog.
"""
self._buttonBox.addWidget(buttonBar)
return
@@ -85,12 +89,16 @@ class VerticalTabBar(QTabBar):
return
def tabSizeHint(self, theIndex):
"""Returns a transposed size hint for the rotated bar.
"""
tSize = QTabBar.tabSizeHint(self, theIndex)
tSize.transpose()
return tSize
def paintEvent(self, theEvent):
"""Custom implementation of the label painter that rotates the
label 90 degrees.
"""
pObj = QStylePainter(self)
oObj = QStyleOptionTab()
+4 -2
View File
@@ -69,11 +69,15 @@ class QConfigLayout(QGridLayout):
return
def setHelpText(self, intRow, theText):
"""Set the text for the help label.
"""
if intRow in self._itemMap:
self._itemMap[intRow]["help"].setText(theText)
return
def setLabelText(self, intRow, theText):
"""Set the text for the main label.
"""
if intRow in self._itemMap:
self._itemMap[intRow]["label"].setText(theText)
return
@@ -85,7 +89,6 @@ class QConfigLayout(QGridLayout):
def addGroupLabel(self, theLabel):
"""Adds a text label to separate groups of settings.
"""
if isinstance(theLabel, QLabel):
qLabel = theLabel
elif isinstance(theLabel, str):
@@ -107,7 +110,6 @@ class QConfigLayout(QGridLayout):
def addRow(self, theLabel, theWidget, helpText=None, theUnit=None):
"""Add a label and a widget as a new row of the grid.
"""
thisEntry = {
"label" : None,
"help" : None,
-1
View File
@@ -97,7 +97,6 @@ class QSwitch(QAbstractButton):
def paintEvent(self, event):
"""Drawing the switch itself.
"""
qPaint = QPainter(self)
qPaint.setRenderHint(QPainter.Antialiasing, True)
qPaint.setPen(Qt.NoPen)
+20 -19
View File
@@ -143,31 +143,33 @@ class GuiDocDetails(QFrame):
self.pCountData.setAlignment(Qt.AlignRight)
# Assemble
self.mainBox.addWidget(self.labelName, 0, 0, 1, 1)
self.mainBox.addWidget(self.labelFlag, 0, 1, 1, 1)
self.mainBox.addWidget(self.labelData, 0, 2, 1, 3)
self.mainBox.addWidget(self.labelName, 0, 0, 1, 1, Qt.AlignTop)
self.mainBox.addWidget(self.labelFlag, 0, 1, 1, 1, Qt.AlignTop)
self.mainBox.addWidget(self.labelData, 0, 2, 1, 3, Qt.AlignTop)
self.mainBox.addWidget(self.statusName, 1, 0, 1, 1)
self.mainBox.addWidget(self.statusFlag, 1, 1, 1, 1)
self.mainBox.addWidget(self.statusData, 1, 2, 1, 1)
self.mainBox.addWidget(self.cCountName, 1, 3, 1, 1)
self.mainBox.addWidget(self.cCountData, 1, 4, 1, 1)
self.mainBox.addWidget(self.statusName, 1, 0, 1, 1, Qt.AlignTop)
self.mainBox.addWidget(self.statusFlag, 1, 1, 1, 1, Qt.AlignTop)
self.mainBox.addWidget(self.statusData, 1, 2, 1, 1, Qt.AlignTop)
self.mainBox.addWidget(self.cCountName, 1, 3, 1, 1, Qt.AlignTop)
self.mainBox.addWidget(self.cCountData, 1, 4, 1, 1, Qt.AlignTop)
self.mainBox.addWidget(self.className, 2, 0, 1, 1)
self.mainBox.addWidget(self.classFlag, 2, 1, 1, 1)
self.mainBox.addWidget(self.classData, 2, 2, 1, 1)
self.mainBox.addWidget(self.wCountName, 2, 3, 1, 1)
self.mainBox.addWidget(self.wCountData, 2, 4, 1, 1)
self.mainBox.addWidget(self.className, 2, 0, 1, 1, Qt.AlignTop)
self.mainBox.addWidget(self.classFlag, 2, 1, 1, 1, Qt.AlignTop)
self.mainBox.addWidget(self.classData, 2, 2, 1, 1, Qt.AlignTop)
self.mainBox.addWidget(self.wCountName, 2, 3, 1, 1, Qt.AlignTop)
self.mainBox.addWidget(self.wCountData, 2, 4, 1, 1, Qt.AlignTop)
self.mainBox.addWidget(self.layoutName, 3, 0, 1, 1)
self.mainBox.addWidget(self.layoutFlag, 3, 1, 1, 1)
self.mainBox.addWidget(self.layoutData, 3, 2, 1, 1)
self.mainBox.addWidget(self.pCountName, 3, 3, 1, 1)
self.mainBox.addWidget(self.pCountData, 3, 4, 1, 1)
self.mainBox.addWidget(self.layoutName, 3, 0, 1, 1, Qt.AlignTop)
self.mainBox.addWidget(self.layoutFlag, 3, 1, 1, 1, Qt.AlignTop)
self.mainBox.addWidget(self.layoutData, 3, 2, 1, 1, Qt.AlignTop)
self.mainBox.addWidget(self.pCountName, 3, 3, 1, 1, Qt.AlignTop)
self.mainBox.addWidget(self.pCountData, 3, 4, 1, 1, Qt.AlignTop)
self.mainBox.setColumnStretch(0,0)
self.mainBox.setColumnStretch(1,0)
self.mainBox.setColumnStretch(2,1)
self.mainBox.setColumnStretch(3,0)
self.mainBox.setColumnStretch(4,0)
logger.debug("DocDetails initialisation complete")
@@ -180,7 +182,6 @@ class GuiDocDetails(QFrame):
def updateViewBox(self, tHandle):
"""Populate the details box from a given handle.
"""
nwItem = self.theProject.projTree[tHandle]
if nwItem is None:
+28 -12
View File
@@ -141,7 +141,6 @@ class GuiDocEditor(QTextEdit):
"""Clear the current document and reset all document related
flags and counters.
"""
self.nwDocument.clearDocument()
self.setReadOnly(True)
self.clear()
@@ -167,7 +166,6 @@ class GuiDocEditor(QTextEdit):
settings. This function is both called when the editor is
created, and when the user changes the main editor preferences.
"""
# Some Constants
self.nonWord = "\"'"
self.nonWord += "".join(self.mainConf.fmtDoubleQuotes)
@@ -241,7 +239,6 @@ class GuiDocEditor(QTextEdit):
document is new (empty string), we set up the editor for editing
the file.
"""
theDoc = self.nwDocument.openDocument(tHandle, showStatus=showStatus)
if theDoc is None:
# There was an io error
@@ -319,7 +316,6 @@ class GuiDocEditor(QTextEdit):
Config.textFixedW is enabled or we're in Zen mode. Otherwise,
just ensure the margins are set correctly.
"""
if self.mainConf.textFixedW or self.theParent.isZenMode:
vBar = self.verticalScrollBar()
if vBar.isVisible():
@@ -376,6 +372,10 @@ class GuiDocEditor(QTextEdit):
##
def setDocumentChanged(self, bValue):
"""Keeps track of the document changed variable, and ensures
that the corresponding icon on the status bar shows the same
status.
"""
self.docChanged = bValue
self.theParent.statusBar.setDocumentStatus(self.docChanged)
return self.docChanged
@@ -437,7 +437,6 @@ class GuiDocEditor(QTextEdit):
If the spell check mode (theMode) is not defined (None), then
toggle the current status saved in this class.
"""
if theMode is None:
theMode = not self.spellCheck
@@ -460,7 +459,6 @@ class GuiDocEditor(QTextEdit):
currently loaded text. The fastest way to do this, at least as
of Qt 5.13, is to clear the text and put it back.
"""
logger.verbose("Running spell checker")
if self.spellCheck:
bfTime = time()
@@ -481,6 +479,12 @@ class GuiDocEditor(QTextEdit):
##
def docAction(self, theAction):
"""Perform an action on the current document based on an action
flag. This is just a single entry point wrapper function to
ensure all the feature functions get the correct information
passed to it without having to consider the internal logic of
this class when calling these actions from other classes.
"""
logger.verbose("Requesting action: %s" % theAction.name)
if not self.theParent.hasProject:
logger.error("No project open")
@@ -537,9 +541,14 @@ class GuiDocEditor(QTextEdit):
return True
def isEmpty(self):
"""Wrapper function to check if the current document is empty.
"""
return self.qDocument.isEmpty()
def revealLocation(self):
"""Tell the user where on the file system the file in the editor
is saved.
"""
if self.theHandle is not None:
msgBox = QMessageBox()
msgBox.information(self, "File Location", (
@@ -568,7 +577,6 @@ class GuiDocEditor(QTextEdit):
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:
@@ -628,7 +636,6 @@ class GuiDocEditor(QTextEdit):
"""Triggered by right click to open the context menu. Also
triggered by the Ctrl+. shortcut.
"""
if not self.spellCheck:
return
@@ -667,7 +674,11 @@ class GuiDocEditor(QTextEdit):
return
@pyqtSlot("QTextCursor", str)
def _correctWord(self, theCursor, theWord):
"""Slot for the spell check context menu triggering the
replacement of a word with the word from the dictionary.
"""
xPos = theCursor.selectionStart()
theCursor.beginEditBlock()
theCursor.removeSelectedText()
@@ -677,7 +688,11 @@ class GuiDocEditor(QTextEdit):
self.setTextCursor(theCursor)
return
@pyqtSlot("QTextCursor")
def _addWord(self, theCursor):
"""Slot for the spell check context menu triggered when the user
wants to add a word to the project dictionary.
"""
theWord = theCursor.selectedText().strip().strip(self.nonWord)
logger.debug("Added '%s' to project dictionary" % theWord)
self.theDict.addWord(theWord)
@@ -729,7 +744,6 @@ class GuiDocEditor(QTextEdit):
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()
@@ -772,13 +786,15 @@ class GuiDocEditor(QTextEdit):
return
def _openSpellContext(self):
"""Opens the spell check context menu at the current point of
the cursor.
"""
self._openContextMenu(self.cursorRect().center())
return
def _docAutoReplace(self, theBlock):
"""Autoreplace text elements based on main configuration.
"""
if not theBlock.isValid():
return
@@ -870,7 +886,6 @@ class GuiDocEditor(QTextEdit):
def _formatBlock(self, docAction):
"""Changes the block format of the block under the cursor.
"""
theCursor = self.textCursor()
theBlock = theCursor.block()
if not theBlock.isValid():
@@ -949,6 +964,8 @@ class GuiDocEditor(QTextEdit):
return
def _makeSelection(self, selMode):
"""Wrapper function to select a word based on a selection mode.
"""
theCursor = self.textCursor()
theCursor.clearSelection()
theCursor.select(selMode)
@@ -1025,7 +1042,6 @@ class GuiDocEditor(QTextEdit):
"""Create the spell checking object based on the spellTool
setting in config.
"""
if self.mainConf.spellTool == "enchant":
from nw.core.spellcheck import NWSpellEnchant
self.theDict = NWSpellEnchant()
-1
View File
@@ -79,7 +79,6 @@ class GuiDocTitleBar(QLabel):
"""Sets the document title from the handle, or alternatively,
set the whole document path.
"""
self.setText("")
self.theHandle = tHandle
if tHandle is None:
+42 -5
View File
@@ -105,13 +105,19 @@ class GuiDocTree(QTreeWidget):
##
def clearTree(self):
"""Clear the GUI content and the related maps.
"""
self.clear()
self.theMap = {}
self.orphRoot = None
return
def newTreeItem(self, itemType, itemClass):
"""Add new item to the tree, with a given itemType and
itemClass, and attach it to the selected handle. Also make sure
the item is added in a place it can be added, and that other
meta data is set correctly to ensure a valid project tree.
"""
pHandle = self.getSelectedHandle()
if not self.theParent.hasProject:
@@ -274,7 +280,6 @@ class GuiDocTree(QTreeWidget):
function only asks for confirmation once, and calls the regular
deleteItem function for each document in the Trash folder.
"""
trashHandle = self.theProject.projTree.trashRoot()
logger.debug("Emptying Trash folder")
@@ -315,7 +320,6 @@ class GuiDocTree(QTreeWidget):
that to save memory. Items not in the tree are not saved to the
project file, so a loaded project will be clean anyway.
"""
if tHandle is None:
tHandle = self.getSelectedHandle()
@@ -446,6 +450,13 @@ class GuiDocTree(QTreeWidget):
return
def propagateCount(self, tHandle, theCount, nDepth=0):
"""Recursive function setting the word count for a given item,
and propagating that count upwards in the tree until reaching a
root item. This function is more efficient than recalculating
everything each time the word count is updated, but is also
prone to diverging from the true values if the counts are not
properly reported to the function.
"""
tItem = self._getTreeItem(tHandle)
if tItem is not None:
tItem.setText(self.C_COUNT,str(theCount))
@@ -460,6 +471,12 @@ class GuiDocTree(QTreeWidget):
return
def projectWordCount(self):
"""Sum up the word counts for all root items and set the
relevant values in the project and on the status bar. This call
is a fast way of getting this number, and depends on the
propagateCount function being called when it should to maintain
the correct count.
"""
nWords = 0
for n in range(self.topLevelItemCount()):
tItem = self.topLevelItem(n)
@@ -472,6 +489,11 @@ class GuiDocTree(QTreeWidget):
return
def buildTree(self):
"""Build the entire project tree from scratch. This depends on
the save project item iterator in the project class which will
always make sure items with a parent have had their parent item
sent first.
"""
self.clear()
for nwItem in self.theProject.getProjectItems():
self._addTreeItem(nwItem)
@@ -517,11 +539,16 @@ class GuiDocTree(QTreeWidget):
##
def _getTreeItem(self, tHandle):
"""Returns the QTreeWidgetItem of a given item handle.
"""
if tHandle in self.theMap.keys():
return self.theMap[tHandle]
return None
def _scanChildren(self, theList, theItem, theIndex):
"""This is a recursive function returning all items in a tree
starting at a given QTreeWidgetItem.
"""
tHandle = theItem.text(self.C_HANDLE)
nwItem = self.theProject.projTree[tHandle]
nwItem.setExpanded(theItem.isExpanded())
@@ -532,7 +559,9 @@ class GuiDocTree(QTreeWidget):
return theList
def _addTreeItem(self, nwItem):
"""Create a QTreeWidgetItem from an NWItem and add it to the
project tree.
"""
tHandle = nwItem.itemHandle
pHandle = nwItem.parHandle
tClass = nwItem.itemClass
@@ -591,6 +620,9 @@ class GuiDocTree(QTreeWidget):
return trItem
def _addOrphanedRoot(self):
"""Add the special Orphaned Files root item to hold non-root
items with no parent set.
"""
if self.orphRoot is None:
newItem = QTreeWidgetItem([""]*4)
newItem.setText(self.C_NAME, "Orphaned Files")
@@ -604,6 +636,8 @@ class GuiDocTree(QTreeWidget):
return
def _cleanOrphanedRoot(self):
"""Remove the special Orphaned Files root folder if it is empty.
"""
if self.orphRoot is not None:
if self.orphRoot.childCount() == 0:
self.takeTopLevelItem(self.indexOfTopLevelItem(self.orphRoot))
@@ -615,7 +649,6 @@ class GuiDocTree(QTreeWidget):
in the project is consistent with the treeView. Also move the
word count over to the new parent tree.
"""
trItemS = self._getTreeItem(tHandle)
nwItemS = self.theProject.projTree[tHandle]
trItemP = trItemS.parent()
@@ -636,6 +669,10 @@ class GuiDocTree(QTreeWidget):
return True
def _moveOrphanedItem(self, tHandle, dHandle):
"""Move an Orphaned Item to a new dHandle parent item. This
function will set all the missing meta data based on the meta
data of the destination item.
"""
trItemS = self._getTreeItem(tHandle)
nwItemS = self.theProject.projTree[tHandle]
nwItemD = self.theProject.projTree[dHandle]
+17 -5
View File
@@ -88,7 +88,6 @@ class GuiDocViewer(QTextBrowser):
def initViewer(self):
"""Set editor settings from main config.
"""
self._makeStyleSheet()
# Set Font
@@ -122,7 +121,6 @@ class GuiDocViewer(QTextBrowser):
def loadText(self, tHandle):
"""Load text into the viewer from an item handle.
"""
tItem = self.theProject.projTree[tHandle]
if tItem is None:
logger.warning("Item not found")
@@ -153,11 +151,16 @@ class GuiDocViewer(QTextBrowser):
return True
def reloadText(self):
"""Reload the text in the current document.
"""
self.loadText(self.theHandle)
return
def loadFromTag(self, theTag):
"""Load text in the document from a reference given by a meta
tag rather than a known handle. This function depends on the
index being up to date.
"""
logger.debug("Loading document from tag '%s'" % theTag)
if theTag in self.theParent.theIndex.tagIndex.keys():
@@ -175,6 +178,9 @@ class GuiDocViewer(QTextBrowser):
return True
def docAction(self, theAction):
"""Wrapper function for various document actions on the current
document.
"""
logger.verbose("Requesting action: %s" % theAction.name)
if self.theHandle is None:
logger.error("No document open")
@@ -225,6 +231,9 @@ class GuiDocViewer(QTextBrowser):
##
def _makeSelection(self, selMode):
"""Wrapper function for making a selection based on a specific
selection mode.
"""
theCursor = self.textCursor()
theCursor.clearSelection()
theCursor.select(selMode)
@@ -232,7 +241,8 @@ class GuiDocViewer(QTextBrowser):
return
def _linkClicked(self, theURL):
"""Slot for a link in the document being clicked.
"""
theLink = theURL.url()
tHandle = None
onLine = 0
@@ -256,7 +266,9 @@ class GuiDocViewer(QTextBrowser):
return
def _makeStyleSheet(self):
"""Generate an appropriate style sheet for the document viewer,
based on the current syntax highlighter theme,
"""
styleSheet = (
"body {{"
" color: rgb({tColR},{tColG},{tColB});"
+4
View File
@@ -67,11 +67,15 @@ class GuiNoticeBar(QFrame):
return
def showNote(self, theNote):
"""Show the note on the noticebar.
"""
self.noteLabel.setText("<b>Note:</b> %s" % theNote)
self.setVisible(True)
return
def hideNote(self):
"""Clear the noticebar and hide it.
"""
self.noteLabel.setText("")
self.setVisible(False)
return
+1 -7
View File
@@ -125,7 +125,6 @@ class GuiProjectOutline(QTreeWidget):
"""Clear the tree and header and set the default values for the
columns arrays.
"""
self.clear()
self.setColumnCount(1)
self.setHeaderLabel(nwLabels.OUTLINE_COLS[nwOutline.TITLE])
@@ -150,7 +149,6 @@ class GuiProjectOutline(QTreeWidget):
what data to load, and if necessary, force a rebuild of the
tree.
"""
# If it's the first time, we always build
if self.firstView or self.firstView and overRide:
self._loadHeaderState()
@@ -229,7 +227,6 @@ class GuiProjectOutline(QTreeWidget):
"""Load the state of the main tree header, that is, column order
and column width.
"""
# Load whatever we saved last time, regardless of wether it
# contains the correct names or number of columns. The names
# must be valid though.
@@ -280,7 +277,6 @@ class GuiProjectOutline(QTreeWidget):
save the current width of hidden columns though. This preserves
the last known width in case they're unhidden again.
"""
# If we haven't built the tree, there is nothing to save.
if self.lastBuild == 0:
return
@@ -319,7 +315,6 @@ class GuiProjectOutline(QTreeWidget):
if they are hidden. This ensures that showing and hiding columns
is fast and doesn't require a rebuild of the tree.
"""
self.clear()
if self.firstView:
@@ -347,7 +342,7 @@ class GuiProjectOutline(QTreeWidget):
currChapter = None
currScene = None
for titleKey in self.theIndex.getNovelStructure():
for titleKey in self.theIndex.getNovelStructure(skipExcluded=True):
if len(titleKey) < 16:
continue
@@ -404,7 +399,6 @@ class GuiProjectOutline(QTreeWidget):
def _createTreeItem(self, tHandle, sTitle):
"""Populate a tree item with all the column values.
"""
nwItem = self.theProject.projTree[tHandle]
novIdx = self.theIndex.novelIndex[tHandle][sTitle]
+17
View File
@@ -98,6 +98,9 @@ class GuiSearchBar(QFrame):
##
def setSearchText(self, theText):
"""Open the search bar and set the search text to the text
provided, if any.
"""
if not self.isVisible():
self.setVisible(True)
self.searchBox.setText(theText)
@@ -106,15 +109,21 @@ class GuiSearchBar(QFrame):
return True
def setReplaceText(self, theText):
"""Set the replace text.
"""
self._replaceVisible(True)
self.replaceBox.setFocus()
self.replaceBox.setText(theText)
return True
def getSearchText(self):
"""Return the current search text.
"""
return self.searchBox.text()
def getReplaceText(self):
"""Return the current replace text.
"""
return self.replaceBox.text()
##
@@ -122,11 +131,15 @@ class GuiSearchBar(QFrame):
##
def _doClose(self):
"""Hide the search/replace bar.
"""
self._replaceVisible(False)
self.setVisible(False)
return
def _doSearch(self):
"""Call the search action function for the document editor.
"""
modKey = qApp.keyboardModifiers()
if modKey == Qt.ShiftModifier:
self.theParent.docEditor.docAction(nwDocAction.GO_PREV)
@@ -135,10 +148,14 @@ class GuiSearchBar(QFrame):
return
def _doReplace(self):
"""Call the replace action function for the document editor.
"""
self.theParent.docEditor.docAction(nwDocAction.REPL_NEXT)
return
def _replaceVisible(self, isVisible):
"""Set the visibility of all the replace widgets.
"""
self.replaceLabel.setVisible(isVisible)
self.replaceBox.setVisible(isVisible)
self.replaceButton.setVisible(isVisible)
+10 -1
View File
@@ -94,7 +94,9 @@ class GuiDocViewDetails(QWidget):
return
def refreshReferences(self, tHandle):
"""Update the current list of document references from the
project index.
"""
self.currHandle = tHandle
if self.isSticky.isChecked():
@@ -117,12 +119,17 @@ class GuiDocViewDetails(QWidget):
##
def _linkClicked(self, theLink):
"""Capture the link-click and forward it to the document viewer
class for handling.
"""
if len(theLink) == 18:
tHandle = theLink[-13:]
self.theParent.viewDocument(tHandle)
return
def _doShowHide(self, chState):
"""Toggle the expand/collapse of the panel.
"""
self.scrollBox.setVisible(chState)
self.mainConf.setShowRefPanel(chState)
if chState:
@@ -132,6 +139,8 @@ class GuiDocViewDetails(QWidget):
return
def _doSticky(self, chState):
"""Toggle the sticky feature of the references.
"""
if not chState and self.currHandle is not None:
self.refreshReferences(self.currHandle)
return
+10 -2
View File
@@ -75,6 +75,9 @@ class GuiDocHighlighter(QSyntaxHighlighter):
return
def initHighlighter(self):
"""Initialise the syntax highlighter, setting all the colour
rules and building the regexes.
"""
logger.debug("Setting up highlighting rules")
@@ -205,14 +208,21 @@ class GuiDocHighlighter(QSyntaxHighlighter):
##
def setDict(self, theDict):
"""Set the dictionary object for spell check underlines lookup.
"""
self.theDict = theDict
return True
def setSpellCheck(self, theMode):
"""Enable/disable the real time spell checker.
"""
self.spellCheck = theMode
return True
def setHandle(self, theHandle):
"""Set the handle of the currently highlighted document. This is
needed for the index lookup for validating tags and references.
"""
self.theHandle = theHandle
return True
@@ -226,7 +236,6 @@ class GuiDocHighlighter(QSyntaxHighlighter):
is significantly faster than running the regex checks we use for
text paragraphs.
"""
if self.theHandle is None or not theText:
return
@@ -317,7 +326,6 @@ class GuiDocHighlighter(QSyntaxHighlighter):
"""Generate a valid character format to be applied to the text
that is to be highlighted.
"""
theFormat = QTextCharFormat()
if fmtCol is not None:
-2
View File
@@ -51,7 +51,6 @@ class OptionState():
def loadSettings(self):
"""Load the options dictionary from the project settings file.
"""
if self.theProject.projMeta is None:
return False
@@ -76,7 +75,6 @@ class OptionState():
def saveSettings(self):
"""Save the options dictionary to the project settings file.
"""
if self.theProject.projMeta is None:
return False
+3 -1
View File
@@ -45,7 +45,9 @@ class WordCounter(QThread):
return
def run(self):
"""Overloaded run function for the word counter, forwarding the
call to the function that does the actual counting.
"""
theText = self.theParent.getText()
cC, wC, pC = countWords(theText)
+1 -1
View File
@@ -57,7 +57,7 @@ class GuiMain(QMainWindow):
logger.info("Starting %s" % nw.__package__)
logger.debug("Initialising GUI ...")
self.mainConf = nw.CONFIG
self.mainConf = nw.CONFIG
# Some runtime info useful for debugging
logger.info("OS: %s" % self.mainConf.osType)
+3 -3
View File
@@ -1,3 +1,3 @@
pyqt5
lxml
pyenchant
pyqt5>=5.2.1
lxml>=4.2.0
pyenchant>=3.0.0
+4
View File
@@ -0,0 +1,4 @@
%%~ 974e400180a99:7031beac91f75:Page
This is a plain page with some text on it.
This file should receive no special formatting, but the text will always be left aligned and the content will always start on a fresh page when the project is exported.
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.5.2" hexVersion="0x000502f0" fileVersion="1.0" saveCount="158" autoCount="21" timeStamp="2020-05-23 21:12:53">
<novelWriterXML appVersion="0.6" hexVersion="0x000600f0" fileVersion="1.0" saveCount="168" autoCount="26" timeStamp="2020-05-24 18:33:34">
<project>
<name>Sample Project</name>
<title>Sample Project</title>
@@ -11,8 +11,8 @@
<spellCheck>True</spellCheck>
<autoOutline>True</autoOutline>
<lastEdited>636b6aa9b697b</lastEdited>
<lastViewed>6a2d6d5f4f401</lastViewed>
<lastWordCount>875</lastWordCount>
<lastViewed>ba8a28a246524</lastViewed>
<lastWordCount>914</lastWordCount>
<autoReplace>
<A>B</A>
<B>E</B>
@@ -44,7 +44,7 @@
<entry blue="175" green="0" red="117">Main</entry>
</importance>
</settings>
<content count="21">
<content count="22">
<item handle="7031beac91f75" order="0" parent="None">
<name>Novel</name>
<type>ROOT</type>
@@ -65,20 +65,33 @@
<paraCount>2</paraCount>
<cursorPos>78</cursorPos>
</item>
<item handle="edca4be2fcaf8" order="1" parent="7031beac91f75">
<name>Part 1</name>
<item handle="974e400180a99" order="1" parent="7031beac91f75">
<name>Page</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>PAGE</layout>
<charCount>208</charCount>
<wordCount>40</wordCount>
<paraCount>2</paraCount>
<cursorPos>213</cursorPos>
</item>
<item handle="edca4be2fcaf8" order="2" parent="7031beac91f75">
<name>Part One</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>PARTITION</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<charCount>23</charCount>
<wordCount>5</wordCount>
<paraCount>1</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="e7ded148d6e4a" order="2" parent="7031beac91f75">
<item handle="e7ded148d6e4a" order="3" parent="7031beac91f75">
<name>A Folder</name>
<type>FOLDER</type>
<class>NOVEL</class>
@@ -109,7 +122,7 @@
<charCount>1199</charCount>
<wordCount>216</wordCount>
<paraCount>7</paraCount>
<cursorPos>0</cursorPos>
<cursorPos>527</cursorPos>
</item>
<item handle="bc0cbd2a407f3" order="2" parent="e7ded148d6e4a">
<name>Another Scene</name>
@@ -277,9 +290,9 @@
<expanded>False</expanded>
<exported>True</exported>
<layout>SCENE</layout>
<charCount>30</charCount>
<wordCount>6</wordCount>
<paraCount>1</paraCount>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>36</cursorPos>
</item>
</content>
+3 -3
View File
@@ -37,8 +37,8 @@ setuptools.setup(
],
python_requires = ">=3.6",
install_requires = [
"pyqt5",
"lxml",
"pyenchant",
"pyqt5>=5.2.1",
"lxml>=4.2.0",
"pyenchant>=3.0.0",
],
)