Merge branch 'main' into release_1.0b4

This commit is contained in:
Veronica K. B. Olsen
2020-10-08 00:14:23 +02:00
45 changed files with 1373 additions and 582 deletions
+16 -6
View File
@@ -199,28 +199,28 @@ def main(sysArgs=None):
# Check Packages and Versions
errorData = []
errorCode = 0
if sys.hexversion < 0x030403f0:
errorData.append(
"At least Python 3.4.3 is required, but 3.6 is highly recommended."
)
errorCode |= 4
if CONFIG.verQtValue < 50200:
errorData.append(
"At least Qt5 version 5.2 is required, found %s." % CONFIG.verQtString
)
errorCode |= 8
if CONFIG.verPyQtValue < 50200:
errorData.append(
"At least PyQt5 version 5.2 is required, found %s." % CONFIG.verPyQtString
)
try:
import PyQt5.QtSvg # noqa: F401
except ImportError:
errorData.append("Python module 'PyQt5.QtSvg' is missing.")
errorCode |= 16
try:
import lxml # noqa: F401
except ImportError:
errorData.append("Python module 'lxml' is missing.")
errorCode |= 32
if errorData:
if not testMode:
@@ -236,11 +236,21 @@ def main(sysArgs=None):
"<br>&nbsp;-&nbsp;".join(errorData)
))
errApp.exec_()
sys.exit(10 + len(errorData))
sys.exit(errorCode)
# Finish initialising config
CONFIG.initConfig(confPath, dataPath)
if CONFIG.osDarwin:
try:
from Foundation import NSBundle
bundle = NSBundle.mainBundle()
info = bundle.localizedInfoDictionary() or bundle.infoDictionary()
info["CFBundleName"] = "novelWriter"
except ImportError as e:
logger.error("Failed to set application name")
logger.error(str(e))
# Import GUI (after dependency checks), and launch
from nw.guimain import GuiMain
if testMode:
+14 -4
View File
@@ -29,6 +29,8 @@ import logging
from datetime import datetime
from PyQt5.QtWidgets import qApp
from nw.constants import nwConst, nwUnicode
logger = logging.getLogger(__name__)
@@ -201,12 +203,12 @@ def transferCase(theSource, theTarget):
if len(theTarget) < 1 or len(theSource) < 1:
return theResult
if theSource[0] == theSource[0].upper():
theResult = theTarget[0].upper() + theTarget[1:]
if theSource.istitle():
theResult = theTarget.title()
if theSource == theSource.upper():
if theSource.isupper():
theResult = theTarget.upper()
elif theSource == theSource.lower():
elif theSource.islower():
theResult = theTarget.lower()
return theResult
@@ -251,3 +253,11 @@ def makeFileNameSafe(theText):
if c.isalpha() or c.isdigit() or c == " ":
cleanName += c
return cleanName
def getGuiItem(theName):
"""Returns a QtWidget based on its objectName.
"""
for qWidget in qApp.topLevelWidgets():
if qWidget.objectName() == theName:
return qWidget
return None
+2 -4
View File
@@ -68,7 +68,6 @@ class Config:
self.confPath = None # Folder where the config is saved
self.confFile = None # The config file name
self.dataPath = None # Folder where app data is stored
self.homePath = None # The user's home folder
self.lastPath = None # The last user-selected folder (browse dialogs)
self.appPath = None # The full path to the novelwriter package folder
self.appRoot = None # The full path to the novelwriter root folder
@@ -257,8 +256,7 @@ class Config:
logger.verbose("Data path: %s" % self.dataPath)
self.confFile = self.appHandle+".conf"
self.homePath = os.path.expanduser("~")
self.lastPath = self.homePath
self.lastPath = os.path.expanduser("~")
self.appPath = getattr(sys, "_MEIPASS", os.path.abspath(os.path.dirname(__file__)))
self.appRoot = os.path.join(self.appPath, os.path.pardir)
self.assetPath = os.path.join(self.appPath, "assets")
@@ -268,7 +266,7 @@ class Config:
self.appIcon = os.path.join(self.iconPath, "novelwriter.svg")
logger.verbose("App path: %s" % self.appPath)
logger.verbose("Home path: %s" % self.homePath)
logger.verbose("Last path: %s" % self.lastPath)
# If config folder does not exist, make it.
# This assumes that the os config folder itself exists.
+3 -1
View File
@@ -33,7 +33,9 @@ class nwConst():
fStampFmt = "%Y-%m-%d %H.%M.%S" # FileName safe format
dStampFmt = "%Y-%m-%d" # Date only format
maxDepth = 30 # Maximum folder depth of a project
maxDepth = 30 # Maximum folder depth of a project
maxDocSize = 5000000 # Maxium size of a single document
maxBuildSize = 10000000 # Maxium size of a project build
# END Class nwConst
+6 -6
View File
@@ -271,16 +271,16 @@ class NWIndex():
theRoot = self.theProject.projTree.getRootItem(tHandle)
if theItem is None:
logger.error("Not indexing unknown item %s" % tHandle)
logger.info("Not indexing unknown item %s" % tHandle)
return False
if theItem.itemType != nwItemType.FILE:
logger.error("Not indexing non-file item %s" % tHandle)
logger.info("Not indexing non-file item %s" % tHandle)
return False
if theItem.itemLayout == nwItemLayout.NO_LAYOUT:
logger.error("Not indexing no-layout item %s" % tHandle)
logger.info("Not indexing no-layout item %s" % tHandle)
return False
if theItem.parHandle is None:
logger.error("Not indexing orphaned item %s" % tHandle)
logger.info("Not indexing orphaned item %s" % tHandle)
return False
# Run word counter for the whole text
@@ -289,10 +289,10 @@ class NWIndex():
# If the file is archived or trashed, we don't index the file itself
if self.theProject.projTree.isTrashRoot(theItem.parHandle):
logger.error("Not indexing trash item %s" % tHandle)
logger.info("Not indexing trash item %s" % tHandle)
return False
if theRoot.itemClass == nwItemClass.ARCHIVE:
logger.error("Not indexing archived item %s" % tHandle)
logger.info("Not indexing archived item %s" % tHandle)
return False
itemClass = theItem.itemClass
+3
View File
@@ -62,6 +62,8 @@ class OptionState():
"GuiBuildNovel": {
"winWidth",
"winHeight",
"boxWidth",
"docWidth",
"addNovel",
"addNotes",
"ignoreFlag",
@@ -74,6 +76,7 @@ class OptionState():
"incComments",
"incKeywords",
"incBodyText",
"replaceTabs",
},
"GuiOutline": {
"headerOrder",
+4
View File
@@ -757,6 +757,10 @@ class NWProject():
if self.projPath is None or self.projPath == "":
return False
if self.projPath == os.path.expanduser("~"):
# Don't make a mess in the user's home folder
return False
self.projMeta = os.path.join(self.projPath, "meta")
self.projCache = os.path.join(self.projPath, "cache")
self.projContent = os.path.join(self.projPath, "content")
+31 -6
View File
@@ -87,6 +87,11 @@ class NWSpellCheck():
"""
return []
def describeDict(self):
"""Dummy function.
"""
return "", ""
@staticmethod
def expandLanguage(spTag):
"""Translate a language tag to something more user friendly.
@@ -187,6 +192,21 @@ class NWSpellEnchant(NWSpellCheck):
logger.error("Failed to list languages for enchant spell checking")
return retList
def describeDict(self):
"""Return the tag and provider of the currently loaded
dictionary.
"""
try:
spTag = self.theDict.tag
spName = self.theDict.provider.name
except Exception as e:
logger.error("Failed to extract information about the dictionary")
logger.error(str(e))
spTag = ""
spName = ""
return spTag, spName
# END Class NWSpellEnchant
class NWSpellEnchantDummy:
@@ -221,12 +241,14 @@ class NWSpellSimple(NWSpellCheck):
def __init__(self):
NWSpellCheck.__init__(self)
self.theLang = ""
logger.debug("Simple spell checking activated")
return
def setLanguage(self, theLang, projectDict=None):
"""Load a dictionary as a list from the app assets folder.
"""
self.theLang = theLang
self.WORDS = []
dictFile = os.path.join(self.mainConf.dictPath, theLang+".dict")
try:
@@ -269,15 +291,12 @@ class NWSpellSimple(NWSpellCheck):
if len(theWord) == 0:
return []
firstUp = theWord[0] == theWord[0].upper()
theWord = theWord.lower()
theMatches = get_close_matches(theWord, self.WORDS, n=10, cutoff=0.75)
theMatches = get_close_matches(theWord.lower(), self.WORDS, n=10, cutoff=0.75)
theOptions = []
for aWord in theMatches:
if len(aWord) == 0:
continue
if firstUp:
if theWord[0].isupper():
aWord = aWord[0].upper() + aWord[1:]
aWord = aWord.replace("'", self.mainConf.fmtApostrophe)
theOptions.append(aWord)
@@ -305,9 +324,15 @@ class NWSpellSimple(NWSpellCheck):
if theBits[1] != ".dict":
continue
spName = "%s [Internal]" % self.expandLanguage(theBits[0])
spName = "%s [internal]" % self.expandLanguage(theBits[0])
retList.append((theBits[0], spName))
return retList
def describeDict(self):
"""Return the tag and provider of the currently loaded
dictionary.
"""
return self.theLang, "internal"
# END Class NWSpellSimple
+1 -2
View File
@@ -77,7 +77,6 @@ class ToHtml(Tokenizer):
self.doKeywords = True
self.doComments = doComments
self.doSynopsis = doSynopsis
self._buildRegEx()
return
def setStyles(self, cssStyles):
@@ -316,7 +315,7 @@ class ToHtml(Tokenizer):
retText = ""
refTags = []
if theBits[0] in nwLabels.KEY_NAME:
retText += "<span class='tags'>%s:</span>&nbsp;" % nwLabels.KEY_NAME[theBits[0]]
retText += "<span class='tags'>%s:</span> " % nwLabels.KEY_NAME[theBits[0]]
if len(theBits) > 1:
if theBits[0] == nwKeyWords.TAG_KEY:
retText += "<a name='tag_%s'>%s</a>" % (
+17 -1
View File
@@ -33,7 +33,7 @@ from PyQt5.QtCore import QRegularExpression
from nw.core.document import NWDoc
from nw.core.tools import numberToWord, numberToRoman
from nw.constants import nwItemLayout, nwItemType, nwRegEx
from nw.constants import nwConst, nwItemLayout, nwItemType, nwRegEx
logger = logging.getLogger(__name__)
@@ -120,6 +120,9 @@ class Tokenizer():
self.isNote = False
self.isNovel = False
# Error Handling
self.errData = []
return
##
@@ -212,6 +215,14 @@ class Tokenizer():
theDocument = NWDoc(self.theProject, self.theParent)
self.theText = theDocument.openDocument(theHandle)
docSize = len(self.theText)
if docSize > nwConst.maxDocSize:
errVal = "Document '%s' is too big (%.2f MB). Skipping." % (
self.theItem.itemName, docSize/1.0e6
)
self.theText = "# ERROR\n\n%s\n\n" % errVal
self.errData.append(errVal)
self.isNone = self.theItem.itemLayout == nwItemLayout.NO_LAYOUT
self.isTitle = self.theItem.itemLayout == nwItemLayout.TITLE
self.isBook = self.theItem.itemLayout == nwItemLayout.BOOK
@@ -230,6 +241,11 @@ class Tokenizer():
"""
return self.theResult
def getResultSize(self):
"""Return the size of the result from the conversion.
"""
return len(self.theResult)
def getFilteredMarkdown(self):
"""Return the novelWriter markdown after the filters have been applied.
"""
+12 -2
View File
@@ -29,6 +29,8 @@
import logging
from nw.constants import nwUnicode
logger = logging.getLogger(__name__)
# =============================================================================================== #
@@ -44,6 +46,15 @@ def countWords(theText):
paraCount = 0
prevEmpty = True
# We need to treat dashes as word separators for counting words.
# The check+replace apprach is much faster that direct replace for
# large texts, and a bit slower for small texts, but in the latter
# case it doesn't matter.
if nwUnicode.U_ENDASH in theText:
theText = theText.replace(nwUnicode.U_ENDASH, " ")
if nwUnicode.U_EMDASH in theText:
theText = theText.replace(nwUnicode.U_EMDASH, " ")
for aLine in theText.splitlines():
countPara = True
@@ -72,8 +83,7 @@ def countWords(theText):
charCount -= 2
countPara = False
theBuff = aLine.replace("", " ").replace("", " ")
wordCount += len(theBuff.split())
wordCount += len(aLine.split())
charCount += theLen
if countPara and prevEmpty:
paraCount += 1
+223 -66
View File
@@ -36,19 +36,20 @@ from datetime import datetime
from PyQt5.QtCore import Qt, QByteArray, QTimer
from PyQt5.QtPrintSupport import QPrinter, QPrintPreviewDialog
from PyQt5.QtGui import (
QPalette, QColor, QTextDocumentWriter, QFont
QPalette, QColor, QTextDocumentWriter, QFont, QCursor
)
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QTextBrowser, QPushButton, QLabel,
qApp, QDialog, QVBoxLayout, QHBoxLayout, QTextBrowser, QPushButton, QLabel,
QLineEdit, QGroupBox, QGridLayout, QProgressBar, QMenu, QAction,
QFileDialog, QFontDialog, QSpinBox
QFileDialog, QFontDialog, QSpinBox, QScrollArea, QSplitter, QWidget,
QSizePolicy
)
from nw.common import fuzzyTime, makeFileNameSafe
from nw.gui.custom import QSwitch
from nw.core import ToHtml
from nw.constants import (
nwAlert, nwFiles, nwItemType, nwItemLayout, nwItemClass
nwConst, nwAlert, nwFiles, nwItemType, nwItemLayout, nwItemClass
)
logger = logging.getLogger(__name__)
@@ -76,27 +77,25 @@ class GuiBuildNovel(QDialog):
self.theTheme = theParent.theTheme
self.optState = self.theProject.optState
self.htmlText = [] # List of html document
self.htmlText = [] # List of html documents
self.htmlStyle = [] # List of html styles
self.nwdText = [] # List of markdown documents
self.buildTime = 0 # The timestamp of the last build
self.setWindowTitle("Build Novel Project")
self.setMinimumWidth(self.mainConf.pxInt(900))
self.setMinimumHeight(self.mainConf.pxInt(800))
self.setMinimumWidth(self.mainConf.pxInt(700))
self.setMinimumHeight(self.mainConf.pxInt(600))
self.resize(
self.mainConf.pxInt(self.optState.getInt("GuiBuildNovel", "winWidth", 900)),
self.mainConf.pxInt(self.optState.getInt("GuiBuildNovel", "winHeight", 800))
)
self.outerBox = QHBoxLayout()
self.toolsBox = QVBoxLayout()
self.docView = GuiBuildNovelDocView(self, self.theProject)
# Title Formats
# =============
self.titleGroup = QGroupBox("Title Formats for Novel Files", self)
self.titleForm = QGridLayout(self)
self.titleGroup.setLayout(self.titleForm)
@@ -118,11 +117,11 @@ class GuiBuildNovel(QDialog):
r"be centred automatically and only appear between sections of "
r"the same type."
)
xFmt = self.mainConf.pxInt(220)
xFmt = self.mainConf.pxInt(100)
self.fmtTitle = QLineEdit()
self.fmtTitle.setMaxLength(200)
self.fmtTitle.setFixedWidth(xFmt)
self.fmtTitle.setMinimumWidth(xFmt)
self.fmtTitle.setToolTip(fmtHelp)
self.fmtTitle.setText(
self._reFmtCodes(self.theProject.titleFormat["title"])
@@ -130,7 +129,7 @@ class GuiBuildNovel(QDialog):
self.fmtChapter = QLineEdit()
self.fmtChapter.setMaxLength(200)
self.fmtChapter.setFixedWidth(xFmt)
self.fmtChapter.setMinimumWidth(xFmt)
self.fmtChapter.setToolTip(fmtHelp)
self.fmtChapter.setText(
self._reFmtCodes(self.theProject.titleFormat["chapter"])
@@ -138,7 +137,7 @@ class GuiBuildNovel(QDialog):
self.fmtUnnumbered = QLineEdit()
self.fmtUnnumbered.setMaxLength(200)
self.fmtUnnumbered.setFixedWidth(xFmt)
self.fmtUnnumbered.setMinimumWidth(xFmt)
self.fmtUnnumbered.setToolTip(fmtHelp)
self.fmtUnnumbered.setText(
self._reFmtCodes(self.theProject.titleFormat["unnumbered"])
@@ -146,7 +145,7 @@ class GuiBuildNovel(QDialog):
self.fmtScene = QLineEdit()
self.fmtScene.setMaxLength(200)
self.fmtScene.setFixedWidth(xFmt)
self.fmtScene.setMinimumWidth(xFmt)
self.fmtScene.setToolTip(fmtHelp + fmtScHelp)
self.fmtScene.setText(
self._reFmtCodes(self.theProject.titleFormat["scene"])
@@ -154,28 +153,41 @@ class GuiBuildNovel(QDialog):
self.fmtSection = QLineEdit()
self.fmtSection.setMaxLength(200)
self.fmtSection.setFixedWidth(xFmt)
self.fmtSection.setMinimumWidth(xFmt)
self.fmtSection.setToolTip(fmtHelp + fmtScHelp)
self.fmtSection.setText(
self._reFmtCodes(self.theProject.titleFormat["section"])
)
self.titleForm.addWidget(QLabel("Title"), 0, 0, 1, 1, Qt.AlignLeft)
self.titleForm.addWidget(self.fmtTitle, 0, 1, 1, 1, Qt.AlignRight)
self.titleForm.addWidget(QLabel("Chapter"), 1, 0, 1, 1, Qt.AlignLeft)
self.titleForm.addWidget(self.fmtChapter, 1, 1, 1, 1, Qt.AlignRight)
self.titleForm.addWidget(QLabel("Unnumbered"), 2, 0, 1, 1, Qt.AlignLeft)
self.titleForm.addWidget(self.fmtUnnumbered, 2, 1, 1, 1, Qt.AlignRight)
self.titleForm.addWidget(QLabel("Scene"), 3, 0, 1, 1, Qt.AlignLeft)
self.titleForm.addWidget(self.fmtScene, 3, 1, 1, 1, Qt.AlignRight)
self.titleForm.addWidget(QLabel("Section"), 4, 0, 1, 1, Qt.AlignLeft)
self.titleForm.addWidget(self.fmtSection, 4, 1, 1, 1, Qt.AlignRight)
# Dummy boxes due to QGridView and QLineEdit expand bug
self.boxTitle = QHBoxLayout()
self.boxTitle.addWidget(self.fmtTitle)
self.boxChapter = QHBoxLayout()
self.boxChapter.addWidget(self.fmtChapter)
self.boxUnnumbered = QHBoxLayout()
self.boxUnnumbered.addWidget(self.fmtUnnumbered)
self.boxScene = QHBoxLayout()
self.boxScene.addWidget(self.fmtScene)
self.boxSection = QHBoxLayout()
self.boxSection.addWidget(self.fmtSection)
self.titleForm.setColumnStretch(0, 1)
self.titleForm.setColumnStretch(1, 0)
self.titleForm.addWidget(QLabel("Title"), 0, 0, 1, 1, Qt.AlignLeft)
self.titleForm.addLayout(self.boxTitle, 0, 1, 1, 1, Qt.AlignRight)
self.titleForm.addWidget(QLabel("Chapter"), 1, 0, 1, 1, Qt.AlignLeft)
self.titleForm.addLayout(self.boxChapter, 1, 1, 1, 1, Qt.AlignRight)
self.titleForm.addWidget(QLabel("Unnumbered"), 2, 0, 1, 1, Qt.AlignLeft)
self.titleForm.addLayout(self.boxUnnumbered, 2, 1, 1, 1, Qt.AlignRight)
self.titleForm.addWidget(QLabel("Scene"), 3, 0, 1, 1, Qt.AlignLeft)
self.titleForm.addLayout(self.boxScene, 3, 1, 1, 1, Qt.AlignRight)
self.titleForm.addWidget(QLabel("Section"), 4, 0, 1, 1, Qt.AlignLeft)
self.titleForm.addLayout(self.boxSection, 4, 1, 1, 1, Qt.AlignRight)
self.titleForm.setColumnStretch(0, 0)
self.titleForm.setColumnStretch(1, 1)
# Text Options
# =============
self.formatGroup = QGroupBox("Formatting Options", self)
self.formatForm = QGridLayout(self)
self.formatGroup.setLayout(self.formatForm)
@@ -183,7 +195,7 @@ class GuiBuildNovel(QDialog):
## Font Family
self.textFont = QLineEdit()
self.textFont.setReadOnly(True)
self.textFont.setFixedWidth(self.mainConf.pxInt(182))
self.textFont.setMinimumWidth(xFmt)
self.textFont.setText(
self.optState.getString("GuiBuildNovel", "textFont", self.mainConf.textFont)
)
@@ -219,8 +231,12 @@ class GuiBuildNovel(QDialog):
self.optState.getBool("GuiBuildNovel", "noStyling", False)
)
# Dummy box due to QGridView and QLineEdit expand bug
self.boxFont = QHBoxLayout()
self.boxFont.addWidget(self.textFont)
self.formatForm.addWidget(QLabel("Font family"), 0, 0, 1, 1, Qt.AlignLeft)
self.formatForm.addWidget(self.textFont, 0, 1, 1, 1, Qt.AlignRight)
self.formatForm.addLayout(self.boxFont, 0, 1, 1, 1, Qt.AlignRight)
self.formatForm.addWidget(self.fontButton, 0, 2, 1, 1, Qt.AlignRight)
self.formatForm.addWidget(QLabel("Font size"), 1, 0, 1, 1, Qt.AlignLeft)
self.formatForm.addWidget(self.textSize, 1, 1, 1, 2, Qt.AlignRight)
@@ -229,12 +245,13 @@ class GuiBuildNovel(QDialog):
self.formatForm.addWidget(QLabel("Disable styling"), 3, 0, 1, 1, Qt.AlignLeft)
self.formatForm.addWidget(self.noStyling, 3, 1, 1, 2, Qt.AlignRight)
self.formatForm.setColumnStretch(0, 1)
self.formatForm.setColumnStretch(1, 0)
self.formatForm.setColumnStretch(0, 0)
self.formatForm.setColumnStretch(1, 1)
self.formatForm.setColumnStretch(2, 0)
# Include Switches
# ================
self.textGroup = QGroupBox("Text Options", self)
self.textForm = QGridLayout(self)
self.textGroup.setLayout(self.textForm)
@@ -283,9 +300,10 @@ class GuiBuildNovel(QDialog):
self.textForm.setColumnStretch(0, 1)
self.textForm.setColumnStretch(1, 0)
# Additional Options
# ==================
self.fileGroup = QGroupBox("File Options", self)
# File Filter Options
# ===================
self.fileGroup = QGroupBox("File Filter Options", self)
self.fileForm = QGridLayout(self)
self.fileGroup.setLayout(self.fileForm)
@@ -323,8 +341,31 @@ class GuiBuildNovel(QDialog):
self.fileForm.setColumnStretch(0, 1)
self.fileForm.setColumnStretch(1, 0)
# Export Options
# ==============
self.exportGroup = QGroupBox("Export Options", self)
self.exportForm = QGridLayout(self)
self.exportGroup.setLayout(self.exportForm)
self.replaceTabs = QSwitch()
self.replaceTabs.setToolTip(
"Replace all tabs with eight spaces."
)
self.replaceTabs.setChecked(
self.optState.getBool("GuiBuildNovel", "replaceTabs", False)
)
self.exportForm.addWidget(QLabel("Replace tabs with spaces"), 0, 0, 1, 1, Qt.AlignLeft)
self.exportForm.addWidget(self.replaceTabs, 0, 1, 1, 1, Qt.AlignRight)
self.exportForm.setColumnStretch(0, 1)
self.exportForm.setColumnStretch(1, 0)
# Build Button
# ============
self.buildProgress = QProgressBar()
self.buildProgress = QProgressBar()
self.buildNovel = QPushButton("Build Project")
@@ -332,6 +373,7 @@ class GuiBuildNovel(QDialog):
# Action Buttons
# ==============
self.buttonBox = QHBoxLayout()
self.btnPrint = QPushButton("Print")
@@ -349,28 +391,28 @@ class GuiBuildNovel(QDialog):
self.savePDF.triggered.connect(lambda: self._saveDocument(self.FMT_PDF))
self.saveMenu.addAction(self.savePDF)
self.saveHTM = QAction("%s HTML (.htm)" % nw.__package__, self)
self.saveHTM = QAction("%s HTML (.htm)" % self.mainConf.appName, self)
self.saveHTM.triggered.connect(lambda: self._saveDocument(self.FMT_HTM))
self.saveMenu.addAction(self.saveHTM)
self.saveNWD = QAction("%s Markdown (.nwd)" % self.mainConf.appName, self)
self.saveNWD.triggered.connect(lambda: self._saveDocument(self.FMT_NWD))
self.saveMenu.addAction(self.saveNWD)
if self.mainConf.verQtValue >= 51400:
self.saveMD = QAction("Markdown (.md)", self)
self.saveMD.triggered.connect(lambda: self._saveDocument(self.FMT_MD))
self.saveMenu.addAction(self.saveMD)
self.saveNWD = QAction("%s Markdown (.nwd)" % nw.__package__, self)
self.saveNWD.triggered.connect(lambda: self._saveDocument(self.FMT_NWD))
self.saveMenu.addAction(self.saveNWD)
self.saveTXT = QAction("Plain Text (.txt)", self)
self.saveTXT.triggered.connect(lambda: self._saveDocument(self.FMT_TXT))
self.saveMenu.addAction(self.saveTXT)
self.saveJsonH = QAction("JSON + %s HTML (.json)" % nw.__package__, self)
self.saveJsonH = QAction("JSON + %s HTML (.json)" % self.mainConf.appName, self)
self.saveJsonH.triggered.connect(lambda: self._saveDocument(self.FMT_JSON_H))
self.saveMenu.addAction(self.saveJsonH)
self.saveJsonM = QAction("JSON + %s Markdown (.json)" % nw.__package__, self)
self.saveJsonM = QAction("JSON + %s Markdown (.json)" % self.mainConf.appName, self)
self.saveJsonM.triggered.connect(lambda: self._saveDocument(self.FMT_JSON_M))
self.saveMenu.addAction(self.saveJsonM)
@@ -384,27 +426,68 @@ class GuiBuildNovel(QDialog):
# Assemble GUI
# ============
# Splitter Position
boxWidth = self.mainConf.pxInt(350)
boxWidth = self.optState.getInt("GuiBuildNovel", "boxWidth", boxWidth)
docWidth = max(self.width() - boxWidth, 100)
docWidth = self.optState.getInt("GuiBuildNovel", "docWidth", docWidth)
# The Tool Box
self.toolsBox = QVBoxLayout()
self.toolsBox.addWidget(self.titleGroup)
self.toolsBox.addWidget(self.formatGroup)
self.toolsBox.addWidget(self.textGroup)
self.toolsBox.addWidget(self.fileGroup)
self.toolsBox.addWidget(self.exportGroup)
self.toolsBox.addStretch(1)
self.toolsBox.addWidget(self.buildProgress)
self.toolsBox.addWidget(self.buildNovel)
self.toolsBox.addSpacing(8)
self.toolsBox.addLayout(self.buttonBox)
self.outerBox.addLayout(self.toolsBox)
self.outerBox.addWidget(self.docView)
self.outerBox.setStretch(0, 0)
self.outerBox.setStretch(1, 1)
# Tool Box Wrapper Widget
self.toolsWidget = QWidget()
self.toolsWidget.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum)
self.toolsWidget.setLayout(self.toolsBox)
# Tool Box Scroll Area
self.toolsArea = QScrollArea()
self.toolsArea.setMinimumWidth(self.mainConf.pxInt(250))
self.toolsArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.toolsArea.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.toolsArea.setWidgetResizable(True)
self.toolsArea.setWidget(self.toolsWidget)
# Tools and Buttons Layout
self.innerBox = QVBoxLayout()
self.innerBox.addWidget(self.toolsArea)
self.innerBox.addSpacing(8)
self.innerBox.addWidget(self.buildProgress)
self.innerBox.addWidget(self.buildNovel)
self.innerBox.addSpacing(8)
self.innerBox.addLayout(self.buttonBox)
# Tools and Buttons Wrapper Widget
self.innerWidget = QWidget()
self.innerWidget.setLayout(self.innerBox)
# Main Dialog Splitter
self.mainSplit = QSplitter(Qt.Horizontal)
self.mainSplit.addWidget(self.innerWidget)
self.mainSplit.addWidget(self.docView)
self.mainSplit.setSizes([boxWidth, docWidth])
# Outer Layout
self.outerBox = QHBoxLayout()
self.outerBox.addWidget(self.mainSplit)
self.setLayout(self.outerBox)
self.buildNovel.setFocus()
logger.debug("GuiBuildNovel initialisation complete")
# Load from Cache
return
def viewCachedDoc(self):
"""Load the previously generated document from cache.
"""
if self._loadCache():
textFont = self.textFont.text()
textSize = self.textSize.value()
@@ -415,14 +498,25 @@ class GuiBuildNovel(QDialog):
self.docView.clearStyleSheet()
else:
self.docView.setStyleSheet(self.htmlStyle)
self.docView.setContent(self.htmlText, self.buildTime)
htmlSize = sum([len(x) for x in self.htmlText])
if htmlSize < nwConst.maxBuildSize:
qApp.processEvents()
self.docView.setContent(self.htmlText, self.buildTime)
else:
self.docView.setText(
"Failed to generate preview. The result is too big."
)
self._enableQtSave(False)
else:
self.htmlText = []
self.htmlStyle = []
self.nwdText = []
self.buildTime = 0
return False
return
return True
##
# Slots
@@ -448,6 +542,7 @@ class GuiBuildNovel(QDialog):
noteFiles = self.noteFiles.isChecked()
ignoreFlag = self.ignoreFlag.isChecked()
includeBody = self.includeBody.isChecked()
replaceTabs = self.replaceTabs.isChecked()
makeHtml = ToHtml(self.theProject, self.theParent)
makeHtml.setTitleFormat(fmtTitle)
@@ -474,6 +569,8 @@ class GuiBuildNovel(QDialog):
self.htmlStyle = []
self.nwdText = []
htmlSize = 0
for nItt, tItem in enumerate(self.theProject.projTree):
noteRoot = noteFiles
@@ -488,6 +585,7 @@ class GuiBuildNovel(QDialog):
makeHtml.doConvert()
self.htmlText.append(makeHtml.getResult())
self.nwdText.append(makeHtml.getFilteredMarkdown())
htmlSize += makeHtml.getResultSize()
elif self._checkInclude(tItem, noteFiles, novelFiles, ignoreFlag):
makeHtml.setText(tItem.itemHandle)
@@ -498,6 +596,7 @@ class GuiBuildNovel(QDialog):
makeHtml.doPostProcessing()
self.htmlText.append(makeHtml.getResult())
self.nwdText.append(makeHtml.getFilteredMarkdown())
htmlSize += makeHtml.getResultSize()
except Exception as e:
logger.error("Failed to generate html of document '%s'" % tItem.itemHandle)
@@ -511,6 +610,24 @@ class GuiBuildNovel(QDialog):
# Update progress bar, also for skipped items
self.buildProgress.setValue(nItt+1)
if makeHtml.errData:
self.theParent.makeAlert((
"There were problems when building the project:"
"<br>-&nbsp;%s"
) % "<br>-&nbsp;".join(makeHtml.errData), nwAlert.ERROR)
if replaceTabs:
htmlText = []
eightSpace = "&nbsp;"*8
for aLine in self.htmlText:
htmlText.append(aLine.replace("\t", eightSpace))
self.htmlText = htmlText
nwdText = []
for aLine in self.nwdText:
nwdText.append(aLine.replace("\t", " "))
self.nwdText = nwdText
tEnd = int(time())
logger.debug("Built project in %.3f ms" % (1000*(tEnd-tStart)))
self.htmlStyle = makeHtml.getStyleSheet()
@@ -523,7 +640,15 @@ class GuiBuildNovel(QDialog):
self.docView.clearStyleSheet()
else:
self.docView.setStyleSheet(self.htmlStyle)
self.docView.setContent(self.htmlText, self.buildTime)
if htmlSize < nwConst.maxBuildSize:
self.docView.setContent(self.htmlText, self.buildTime)
self._enableQtSave(True)
else:
self.docView.setText(
"Failed to generate preview. The result is too big."
)
self._enableQtSave(False)
self._saveCache()
@@ -637,12 +762,10 @@ class GuiBuildNovel(QDialog):
if self.mainConf.showGUI:
dlgOpt = QFileDialog.Options()
dlgOpt |= QFileDialog.DontUseNativeDialog
saveTo = QFileDialog.getSaveFileName(
savePath, _ = QFileDialog.getSaveFileName(
self, "Save Document As", savePath, options=dlgOpt
)
if saveTo[0]:
savePath = saveTo[0]
else:
if not savePath:
return False
self.mainConf.setLastPath(savePath)
@@ -666,6 +789,9 @@ class GuiBuildNovel(QDialog):
# Write novelWriter HTML data
theStyle = self.htmlStyle.copy()
theStyle.append(r"article {width: 800px; margin: 40px auto;}")
bodyText = "".join(self.htmlText)
bodyText = bodyText.replace("\t", "&#09;")
theHtml = (
"<!DOCTYPE html>\n"
"<html>\n"
@@ -685,7 +811,7 @@ class GuiBuildNovel(QDialog):
).format(
projTitle = self.theProject.projName,
htmlStyle = "\n".join(theStyle),
bodyText = "".join(self.htmlText),
bodyText = bodyText,
)
outFile.write(theHtml)
@@ -773,8 +899,10 @@ class GuiBuildNovel(QDialog):
def _doPrintPreview(self, thePrinter):
"""Connect the print preview painter to the document viewer.
"""
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
thePrinter.setOrientation(QPrinter.Portrait)
self.docView.qDocument.print(thePrinter)
qApp.restoreOverrideCursor()
return
def _selectFont(self):
@@ -787,6 +915,9 @@ class GuiBuildNovel(QDialog):
if theStatus:
self.textFont.setText(theFont.family())
self.textSize.setValue(theFont.pointSize())
self.raise_() # Move the dialog to front (fixes a bug on macOS)
return
def _loadCache(self):
@@ -860,13 +991,24 @@ class GuiBuildNovel(QDialog):
"""Capture the user closing the window so we can save settings.
"""
self._saveSettings()
QDialog.closeEvent(self, theEvent)
self.docView.clear()
theEvent.accept()
return
##
# Internal Functions
##
def _enableQtSave(self, theState):
"""Set the enabled status of Save menu entries that depend on
the QTextDocument.
"""
self.saveODT.setEnabled(theState)
self.savePDF.setEnabled(theState)
self.saveMD.setEnabled(theState)
self.saveTXT.setEnabled(theState)
return
def _saveSettings(self):
"""Save the various user settings.
"""
@@ -894,10 +1036,21 @@ class GuiBuildNovel(QDialog):
incComments = self.includeComments.isChecked()
incKeywords = self.includeKeywords.isChecked()
incBodyText = self.includeBody.isChecked()
replaceTabs = self.replaceTabs.isChecked()
mainSplit = self.mainSplit.sizes()
if len(mainSplit) == 2:
boxWidth = self.mainConf.rpxInt(mainSplit[0])
docWidth = self.mainConf.rpxInt(mainSplit[1])
else:
boxWidth = 100
docWidth = 100
# GUI Settings
self.optState.setValue("GuiBuildNovel", "winWidth", winWidth)
self.optState.setValue("GuiBuildNovel", "winHeight", winHeight)
self.optState.setValue("GuiBuildNovel", "boxWidth", boxWidth)
self.optState.setValue("GuiBuildNovel", "docWidth", docWidth)
self.optState.setValue("GuiBuildNovel", "justifyText", justifyText)
self.optState.setValue("GuiBuildNovel", "noStyling", noStyling)
self.optState.setValue("GuiBuildNovel", "textFont", textFont)
@@ -909,6 +1062,7 @@ class GuiBuildNovel(QDialog):
self.optState.setValue("GuiBuildNovel", "incComments", incComments)
self.optState.setValue("GuiBuildNovel", "incKeywords", incKeywords)
self.optState.setValue("GuiBuildNovel", "incBodyText", incBodyText)
self.optState.setValue("GuiBuildNovel", "replaceTabs", replaceTabs)
self.optState.saveSettings()
return
@@ -956,6 +1110,12 @@ class GuiBuildNovelDocView(QTextBrowser):
theFont.setPointSize(self.mainConf.textSize)
self.setFont(theFont)
# Set the tab stops
if self.mainConf.verQtValue >= 51000:
self.setTabStopDistance(self.mainConf.getTabWidth())
else:
self.setTabStopWidth(self.mainConf.getTabWidth())
docPalette = self.palette()
docPalette.setColor(QPalette.Base, QColor(255, 255, 255))
docPalette.setColor(QPalette.Text, QColor(0, 0, 0))
@@ -1019,17 +1179,13 @@ class GuiBuildNovelDocView(QTextBrowser):
self.buildTime = timeStamp
sPos = self.verticalScrollBar().value()
# Refresh the tab stops
if self.mainConf.verQtValue >= 51000:
self.setTabStopDistance(self.mainConf.getTabWidth())
else:
self.setTabStopWidth(self.mainConf.getTabWidth())
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
theText = theText.replace("\t", "!!tab!!")
theText = theText.replace("<del>", "<span style='text-decoration: line-through;'>")
theText = theText.replace("</del>", "</span>")
self.setHtml(theText)
qApp.processEvents()
while self.find("!!tab!!"):
theCursor = self.textCursor()
@@ -1041,6 +1197,7 @@ class GuiBuildNovelDocView(QTextBrowser):
# Since we change the content while it may still be rendering, we mark
# the document dirty again to make sure it's re-rendered properly.
self.qDocument.markContentsDirty(0, self.qDocument.characterCount())
qApp.restoreOverrideCursor()
return
+190 -90
View File
@@ -12,6 +12,7 @@
Created: 2020-04-25 [0.4.5] GuiDocEditHeader
Rewritten: 2020-06-15 [0.9.0] GuiDocEditSearch
Created: 2020-06-27 [0.10.0] GuiDocEditFooter
Rewritten: 2020-10-07 [1.0b3] BackgroundWordCounter
This file is a part of novelWriter
Copyright 20182020, Veronica Berglyd Olsen
@@ -36,7 +37,8 @@ import logging
from time import time
from PyQt5.QtCore import (
Qt, QSize, QThread, QTimer, pyqtSlot, QRegExp, QRegularExpression
Qt, QSize, QTimer, pyqtSlot, pyqtSignal, QRegExp, QRegularExpression,
QPointF, QObject, QRunnable
)
from PyQt5.QtGui import (
QTextCursor, QTextOption, QKeySequence, QFont, QColor, QPalette,
@@ -52,7 +54,7 @@ from nw.core import NWDoc, NWSpellCheck, NWSpellSimple, countWords
from nw.gui.dochighlight import GuiDocHighlighter
from nw.common import transferCase
from nw.constants import (
nwAlert, nwUnicode, nwDocAction, nwDocInsert, nwItemClass
nwConst, nwAlert, nwUnicode, nwDocAction, nwDocInsert, nwItemClass
)
logger = logging.getLogger(__name__)
@@ -69,21 +71,23 @@ class GuiDocEditor(QTextEdit):
self.theParent = theParent
self.theTheme = theParent.theTheme
self.theProject = theParent.theProject
self.docChanged = False
self.spellCheck = False
self.nwDocument = NWDoc(self.theProject, self.theParent)
self.theHandle = None
self.theDict = None
self.docChanged = False # Flag for changed status of document
self.spellCheck = False # Flag for spell checking enabled
self.theHandle = None # The handle of the open file
self.theDict = None # The current spell check dictionary
self.nonWord = "\"'" # Characters to not include in spell checking
# Document Variables
self.charCount = 0
self.wordCount = 0
self.paraCount = 0
self.lastEdit = 0
self.lastFind = None
self.bigDoc = False
self.doReplace = False
self.nonWord = "\"'"
self.charCount = 0 # Character count
self.wordCount = 0 # Word count
self.paraCount = 0 # Paragraph count
self.lastEdit = 0 # Time stamp of last edit
self.lastFind = None # Position of the last found search word
self.bigDoc = False # Flag for very large document size
self.doReplace = False # Switch to temporarily disable auto-replace
self.queuePos = None # Used for delayed change of cursor position
# Typography
self.typDQOpen = self.mainConf.fmtDoubleQuotes[0]
@@ -94,6 +98,7 @@ class GuiDocEditor(QTextEdit):
# Core Elements and Signals
self.qDocument = self.document()
self.qDocument.contentsChange.connect(self._docChange)
self.qDocument.documentLayout().documentSizeChanged.connect(self._docSizeChanged)
# Document Title
self.docHeader = GuiDocEditHeader(self)
@@ -132,14 +137,15 @@ class GuiDocEditor(QTextEdit):
activated=self._followTag
)
# Set Up Word Count Thread and Timer
# Set Up Word Counter
self.wcInterval = self.mainConf.wordCountTimer
self.wcTimer = QTimer()
self.wcTimer.setInterval(int(self.wcInterval*1000))
self.wcTimer.timeout.connect(self._runCounter)
self.wCounter = BackgroundWordCounter(self)
self.wCounter.finished.connect(self._updateCounts)
self.wCounter.setAutoDelete(False)
self.wCounter.signals.countsReady.connect(self._updateCounts)
self.initEditor()
@@ -161,8 +167,10 @@ class GuiDocEditor(QTextEdit):
self.wordCount = 0
self.paraCount = 0
self.lastEdit = 0
self.lastFind = None
self.bigDoc = False
self.doReplace = False
self.queuePos = None
self.setDocumentChanged(False)
self.docHeader.setTitleFromHandle(self.theHandle)
@@ -216,6 +224,12 @@ class GuiDocEditor(QTextEdit):
self.qDocument.setDefaultTextOption(theOpt)
# Refresh the tab stops
if self.mainConf.verQtValue >= 51000:
self.setTabStopDistance(self.mainConf.getTabWidth())
else:
self.setTabStopWidth(self.mainConf.getTabWidth())
# Initialise the syntax highlighter
self.hLight.initHighlighter()
@@ -223,21 +237,12 @@ class GuiDocEditor(QTextEdit):
# font changed, otherwise we just clear the editor entirely,
# which makes it read only.
if self.theHandle is not None:
self.reloadText()
self.redrawText()
else:
self.clearEditor()
return True
def reloadText(self):
"""Reloads the document currently being edited.
"""
if self.theHandle is not None:
tHandle = self.theHandle
self.clearEditor()
self.loadText(tHandle, showStatus=False)
return
def loadText(self, tHandle, tLine=None, showStatus=True):
"""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
@@ -253,12 +258,22 @@ class GuiDocEditor(QTextEdit):
self.clearEditor()
return False
docSize = len(theDoc)
if docSize > nwConst.maxDocSize:
self.theParent.makeAlert((
"The document you are trying to open is too big. "
"The document size is %.2f\u202fMB. "
"The maximum size allowed is %.2f\u202fMB."
) % (docSize/1.0e6, nwConst.maxDocSize/1.0e6), nwAlert.ERROR)
self.clearEditor()
return False
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
self.hLight.setHandle(tHandle)
# Check that the document is not too big for full, initial spell
# checking. If it is too big, we switch to only check as we type
self._checkDocSize(len(theDoc))
self._checkDocSize(docSize)
spTemp = self.hLight.spellCheck
if self.bigDoc:
self.hLight.spellCheck = False
@@ -266,15 +281,11 @@ class GuiDocEditor(QTextEdit):
bfTime = time()
self._allowAutoReplace(False)
self.setPlainText(theDoc)
qApp.processEvents()
self._allowAutoReplace(True)
afTime = time()
logger.debug("Document highlighted in %.3f milliseconds" % (1000*(afTime-bfTime)))
theItem = self.nwDocument.getCurrentItem()
if tLine is None and theItem is not None:
self.setCursorPosition(theItem.cursorPos)
else:
self.setCursorLine(tLine)
logger.debug("Document highlighted in %.3f ms" % (1000*(afTime-bfTime)))
self.lastEdit = time()
self._runCounter()
@@ -287,24 +298,56 @@ class GuiDocEditor(QTextEdit):
self.docFooter.setHandle(self.theHandle)
self.updateDocMargins()
self.hLight.spellCheck = spTemp
theItem = self.nwDocument.getCurrentItem()
if tLine is None and theItem is not None:
# For large documents we queue the repositioning until the
# document layout has grown past the point we want to move
# the cursor to. This makes the loading significantly
# faster.
if docSize > 50000:
self.queuePos = theItem.cursorPos
else:
self.setCursorPosition(theItem.cursorPos)
else:
self.setCursorLine(tLine)
qApp.restoreOverrideCursor()
# Refresh the tab stops
if self.mainConf.verQtValue >= 51000:
self.setTabStopDistance(self.mainConf.getTabWidth())
else:
self.setTabStopWidth(self.mainConf.getTabWidth())
return True
def updateTagHighLighting(self, forceBigDoc=False):
"""Rerun the syntax highlighter on all meta data lines.
"""
self.hLight.rehighlightByType(GuiDocHighlighter.BLOCK_META)
return
def redrawText(self):
"""Redraw the text by marking the document content as "dirty".
"""
self.qDocument.markContentsDirty(0, self.qDocument.characterCount())
return
def replaceText(self, theText):
"""Replaces the text of the current document with the provided
text. This also clears undo history.
"""
docSize = len(theText)
if docSize > nwConst.maxDocSize:
self.theParent.makeAlert((
"The text you are trying to add is too big. "
"The text size is %.2f\u202fMB. "
"The maximum size allowed is %.2f\u202fMB."
) % (docSize/1.0e6, nwConst.maxDocSize/1.0e6), nwAlert.ERROR)
return False
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
self.setPlainText(theText)
self.setDocumentChanged(True)
self.updateDocMargins()
return
qApp.restoreOverrideCursor()
return True
def saveText(self):
"""Save the text currently in the editor to the NWDoc object,
@@ -315,11 +358,10 @@ class GuiDocEditor(QTextEdit):
return False
docText = self.getText()
cursPos = self.getCursorPosition()
theItem.setCharCount(self.charCount)
theItem.setWordCount(self.wordCount)
theItem.setParaCount(self.paraCount)
theItem.setCursorPos(cursPos)
self.saveCursorPosition()
self.nwDocument.saveDocument(docText)
self.setDocumentChanged(False)
@@ -428,6 +470,15 @@ class GuiDocEditor(QTextEdit):
"""
return self.textCursor().selectionEnd()
def saveCursorPosition(self):
"""Save the cursor position to the current project item object.
"""
theItem = self.nwDocument.getCurrentItem()
if theItem is not None:
cursPos = self.getCursorPosition()
theItem.setCursorPos(cursPos)
return
def setCursorLine(self, theLine):
"""Move the cursor to a given line in the document.
"""
@@ -455,7 +506,11 @@ class GuiDocEditor(QTextEdit):
theLang = self.theProject.projLang
self.theDict.setLanguage(theLang, self.theProject.projDict)
self.theParent.statusBar.setLanguage(self.theDict.spellLanguage)
aLang, aName = self.theDict.describeDict()
self.theParent.statusBar.setLanguage(
aLang, "%s [%s]" % (self.mainConf.spellTool.title(), aName.title())
)
if not self.bigDoc:
self.spellCheckDocument()
@@ -502,9 +557,8 @@ class GuiDocEditor(QTextEdit):
qApp.restoreOverrideCursor()
afTime = time()
logger.debug(
"Document re-highlighted in %.3f milliseconds" % (1000*(afTime-bfTime))
"Document highlighted in %.3f ms" % (1000*(afTime-bfTime))
)
self.theParent.statusBar.showMessage("Spell check complete")
return True
@@ -727,6 +781,13 @@ class GuiDocEditor(QTextEdit):
"""
self.lastEdit = time()
self.lastFind = None
if self.qDocument.characterCount() > nwConst.maxDocSize:
self.theParent.makeAlert((
"The document has grown too big and you cannot add more text to it. "
"The maximum size of a single novelWriter document is %.2f\u202fMB."
) % (nwConst.maxDocSize/1.0e6), nwAlert.ERROR)
self.undo()
return
if not self.docChanged:
self.setDocumentChanged(True)
if not self.wcTimer.isActive():
@@ -802,7 +863,7 @@ class GuiDocEditor(QTextEdit):
mnuHead = QAction("Spelling Suggestion(s)", mnuContext)
mnuContext.addAction(mnuHead)
theSuggest = self.theDict.suggestWords(theWord)
theSuggest = self.theDict.suggestWords(theWord)[:15]
if len(theSuggest) > 0:
for aWord in theSuggest:
mnuWord = QAction("%s %s" % (nwUnicode.U_ENDASH, aWord), mnuContext)
@@ -855,21 +916,18 @@ class GuiDocEditor(QTextEdit):
"""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()
if self.wCounter.isRunning():
logger.verbose("Word counter is busy")
return
if time() - self.lastEdit < 5*self.wcInterval:
logger.verbose("Running word counter")
self.theParent.threadPool.start(self.wCounter)
return
@pyqtSlot()
def _updateCounts(self):
@pyqtSlot(int, int, int)
def _updateCounts(self, cCount, wCount, pCount):
"""Slot for the word counter's finished signal
"""
theItem = self.nwDocument.getCurrentItem()
@@ -878,23 +936,44 @@ class GuiDocEditor(QTextEdit):
logger.verbose("Updating word count")
self.charCount = self.wCounter.charCount
self.wordCount = self.wCounter.wordCount
self.paraCount = self.wCounter.paraCount
theItem.setCharCount(self.charCount)
theItem.setWordCount(self.wordCount)
theItem.setParaCount(self.paraCount)
self.charCount = cCount
self.wordCount = wCount
self.paraCount = pCount
theItem.setCharCount(cCount)
theItem.setWordCount(wCount)
theItem.setParaCount(pCount)
self.theParent.treeView.propagateCount(self.theHandle, self.wordCount)
self.theParent.treeView.propagateCount(self.theHandle, wCount)
self.theParent.treeView.projectWordCount()
self.theParent.treeMeta.updateCounts(
self.theHandle, self.charCount, self.wordCount, self.paraCount
)
self._checkDocSize(self.charCount)
self.theParent.treeMeta.updateCounts(self.theHandle, cCount, wCount, pCount)
self._checkDocSize(self.qDocument.characterCount())
self.docFooter.updateCounts()
return
@pyqtSlot("QSizeF")
def _docSizeChanged(self, theSize):
"""Called whenever the underlying document layout size changes.
This is used to queue the repositioning of the cursor for very
large documents to ensure the region where the cursor is being
moved to has been drawn before the move is made.
"""
if self.queuePos is not None:
thePos = self.qDocument.documentLayout().hitTest(
QPointF(theSize.width(), theSize.height()), Qt.FuzzyHit
)
if self.queuePos <= thePos:
logger.verbose(
"Allowed cursor move to %d <= %d" % (self.queuePos, thePos)
)
self.setCursorPosition(self.queuePos)
self.queuePos = None
else:
logger.verbose(
"Denied cursor move to %d > %d" % (self.queuePos, thePos)
)
return
##
# Internal Functions
##
@@ -1050,15 +1129,24 @@ class GuiDocEditor(QTextEdit):
"""Check if document size crosses the big document limit set in
config. If so, we will set the big document flag to True.
"""
if theSize > self.mainConf.bigDocLimit*1000:
logger.info(
"The document size is %d > %d, big doc mode is enabled" % (
theSize, self.mainConf.bigDocLimit*1000
newState = theSize > self.mainConf.bigDocLimit*1000
if newState != self.bigDoc:
if newState:
logger.info(
"The document size is {:n} > {:n}, big doc mode has been enabled".format(
theSize, self.mainConf.bigDocLimit*1000
)
)
)
self.bigDoc = True
else:
self.bigDoc = False
else:
logger.info(
"The document size is {:n} <= {:n}, big doc mode has been disabled".format(
theSize, self.mainConf.bigDocLimit*1000
)
)
self.bigDoc = newState
return
def _wrapSelection(self, tBefore, tAfter=None):
@@ -1432,33 +1520,42 @@ class GuiDocEditor(QTextEdit):
# END Class GuiDocEditor
# =============================================================================================== #
# The Off GUI Thread Word Counter
# Runs the word counter in the background for the DocEditor
# The Off-GUI Thread Word Counter
# A runnable for the word counter to be run in the thread pool off the main GUI thread.
# =============================================================================================== #
class BackgroundWordCounter(QThread):
class BackgroundWordCounter(QRunnable):
def __init__(self, docEditor):
QThread.__init__(self, docEditor)
QRunnable.__init__(self)
self.docEditor = docEditor
self.charCount = 0
self.wordCount = 0
self.paraCount = 0
self.signals = BackgroundWordCounterSignals()
self._isRunning = False
return
def isRunning(self):
return self._isRunning
@pyqtSlot()
def run(self):
"""Overloaded run function for the word counter, forwarding the
call to the function that does the actual counting.
"""
self._isRunning = True
theText = self.docEditor.getText()
cC, wC, pC = countWords(theText)
self.charCount = cC
self.wordCount = wC
self.paraCount = pC
self.signals.countsReady.emit(cC, wC, pC)
self._isRunning = False
return
## END Class BackgroundWordCounter
class BackgroundWordCounterSignals(QObject):
countsReady = pyqtSignal(int, int, int)
# END Class BackgroundWordCounterSignals
# =============================================================================================== #
# The Embedded Document Search/Replace Feature
# Only used by DocEditor, and is at a fixed position in the QTextEdit's viewport
@@ -2176,7 +2273,10 @@ class GuiDocEditFooter(QWidget):
wCount = self.theItem.wordCount
wDiff = wCount - self.theItem.initCount
self.wordsText.setText("Words: {:n} ({:+n})".format(wCount, wDiff))
self.wordsText.setText(f"Words: {wCount:n} ({wDiff:+n})")
byteSize = self.docEditor.qDocument.characterCount()
self.wordsText.setToolTip(f"Document size is {byteSize:n} bytes")
return
+38 -2
View File
@@ -28,6 +28,8 @@
import nw
import logging
from time import time
from PyQt5.QtCore import Qt, QRegularExpression
from PyQt5.QtGui import (
QColor, QTextCharFormat, QFont, QSyntaxHighlighter, QBrush
@@ -39,6 +41,11 @@ logger = logging.getLogger(__name__)
class GuiDocHighlighter(QSyntaxHighlighter):
BLOCK_NONE = 0
BLOCK_TEXT = 1
BLOCK_META = 2
BLOCK_TITLE = 4
def __init__(self, theDoc, theParent):
QSyntaxHighlighter.__init__(self, theDoc)
@@ -198,7 +205,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
# Build a QRegExp for spell checker
# Include additional characters that the highlighter should
# consider to be word separators
wordSep = r"_\+/"
wordSep = r"\-_\+/"
wordSep += nwUnicode.U_ENDASH
wordSep += nwUnicode.U_EMDASH
self.spellRx = QRegularExpression(r"\b[^\s"+wordSep+r"]+\b")
@@ -229,6 +236,27 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self.theHandle = theHandle
return True
##
# Methods
##
def rehighlightByType(self, theType):
"""Loop through all blocks and rehighlight those of a given
content type.
"""
qDocument = self.document()
nBlocks = qDocument.blockCount()
bfTime = time()
for i in range(nBlocks):
theBlock = qDocument.findBlockByNumber(i)
if theBlock.userState() & theType > 0:
self.rehighlightBlock(theBlock)
afTime = time()
logger.debug(
"Document highlighted in %.3f ms" % (1000*(afTime-bfTime))
)
return
##
# Highlight Block
##
@@ -239,10 +267,12 @@ class GuiDocHighlighter(QSyntaxHighlighter):
is significantly faster than running the regex checks used for
text paragraphs.
"""
self.setCurrentBlockState(self.BLOCK_NONE)
if self.theHandle is None or not theText:
return
if theText.startswith("@"): # Keywords and commands
self.setCurrentBlockState(self.BLOCK_META)
tItem = self.theParent.theProject.projTree[self.theHandle]
isValid, theBits, thePos = self.theIndex.scanThis(theText)
isGood = self.theIndex.checkThese(theBits, tItem)
@@ -266,22 +296,27 @@ class GuiDocHighlighter(QSyntaxHighlighter):
return
elif theText.startswith("# "): # Header 1
self.setCurrentBlockState(self.BLOCK_TITLE)
self.setFormat(0, 1, self.hStyles["header1h"])
self.setFormat(1, len(theText), self.hStyles["header1"])
elif theText.startswith("## "): # Header 2
self.setCurrentBlockState(self.BLOCK_TITLE)
self.setFormat(0, 2, self.hStyles["header2h"])
self.setFormat(2, len(theText), self.hStyles["header2"])
elif theText.startswith("### "): # Header 3
self.setCurrentBlockState(self.BLOCK_TITLE)
self.setFormat(0, 3, self.hStyles["header3h"])
self.setFormat(3, len(theText), self.hStyles["header3"])
elif theText.startswith("#### "): # Header 4
self.setCurrentBlockState(self.BLOCK_TITLE)
self.setFormat(0, 4, self.hStyles["header4h"])
self.setFormat(4, len(theText), self.hStyles["header4"])
elif theText.startswith("%"): # Comments
self.setCurrentBlockState(self.BLOCK_TEXT)
toCheck = theText[1:].lstrip()
synTag = toCheck[:9].lower()
tLen = len(theText)
@@ -294,6 +329,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self.setFormat(0, tLen, self.hStyles["hidden"])
else: # Text Paragraph
self.setCurrentBlockState(self.BLOCK_TEXT)
for rX, xFmt in self.rxRules:
rxItt = rX.globalMatch(theText, 0)
while rxItt.hasNext():
@@ -314,7 +350,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
while rxSpell.hasNext():
rxMatch = rxSpell.next()
if not self.theDict.checkWord(rxMatch.captured(0)):
if rxMatch.captured(0) == rxMatch.captured(0).upper():
if rxMatch.captured(0).isupper() or rxMatch.captured(0).isnumeric():
continue
xPos = rxMatch.capturedStart(0)
xLen = rxMatch.capturedLength(0)
+19 -12
View File
@@ -34,10 +34,10 @@ import logging
from PyQt5.QtCore import Qt, QUrl, QSize, pyqtSlot
from PyQt5.QtGui import (
QTextOption, QFont, QPalette, QColor, QTextCursor, QIcon
QTextOption, QFont, QPalette, QColor, QTextCursor, QIcon, QCursor
)
from PyQt5.QtWidgets import (
QTextBrowser, QWidget, QScrollArea, QLabel, QHBoxLayout, QToolButton,
qApp, QTextBrowser, QWidget, QScrollArea, QLabel, QHBoxLayout, QToolButton,
QAction, QMenu
)
@@ -124,11 +124,15 @@ class GuiDocViewer(QTextBrowser):
theOpt.setAlignment(Qt.AlignJustify)
self.qDocument.setDefaultTextOption(theOpt)
# Refresh the tab stops
if self.mainConf.verQtValue >= 51000:
self.setTabStopDistance(self.mainConf.getTabWidth())
else:
self.setTabStopWidth(self.mainConf.getTabWidth())
# 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)
self.redrawText()
return True
@@ -144,6 +148,8 @@ class GuiDocViewer(QTextBrowser):
return False
logger.debug("Generating preview for item %s" % tHandle)
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
sPos = self.verticalScrollBar().value()
aDoc = ToHtml(self.theProject, self.theParent)
aDoc.setPreview(True, self.mainConf.viewComments, self.mainConf.viewSynopsis)
@@ -195,7 +201,8 @@ class GuiDocViewer(QTextBrowser):
# Since we change the content while it may still be rendering, we mark
# the document dirty again to make sure it's re-rendered properly.
self.qDocument.markContentsDirty(0, self.qDocument.characterCount())
self.redrawText()
qApp.restoreOverrideCursor()
return True
@@ -205,6 +212,12 @@ class GuiDocViewer(QTextBrowser):
self.loadText(self.theHandle, updateHistory=False)
return
def redrawText(self):
"""Redraw the text by marking the document content as "dirty".
"""
self.qDocument.markContentsDirty(0, self.qDocument.characterCount())
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
@@ -489,12 +502,6 @@ class GuiDocViewer(QTextBrowser):
"mark {{"
" color: rgb({eColR},{eColG},{eColB});"
"}}\n"
"table {{"
" margin: 10px 0px;"
"}}\n"
"td {{"
" padding: 0px 4px;"
"}}\n"
".tags {{"
" color: rgb({kColR},{kColG},{kColB});"
" font-wright: bold;"
+6 -6
View File
@@ -195,9 +195,9 @@ class GuiItemDetails(QWidget):
we're already showing.
"""
if tHandle == self.theHandle:
self.cCountData.setText("{:n}".format(cC))
self.wCountData.setText("{:n}".format(wC))
self.pCountData.setText("{:n}".format(pC))
self.cCountData.setText(f"{cC:n}")
self.wCountData.setText(f"{wC:n}")
self.pCountData.setText(f"{pC:n}")
return
def updateViewBox(self, tHandle):
@@ -252,9 +252,9 @@ class GuiItemDetails(QWidget):
self.layoutData.setText(nwLabels.LAYOUT_NAME[nwItem.itemLayout])
if nwItem.itemType == nwItemType.FILE:
self.cCountData.setText("{:n}".format(nwItem.charCount))
self.wCountData.setText("{:n}".format(nwItem.wordCount))
self.pCountData.setText("{:n}".format(nwItem.paraCount))
self.cCountData.setText(f"{nwItem.charCount:n}")
self.wCountData.setText(f"{nwItem.wordCount:n}")
self.pCountData.setText(f"{nwItem.paraCount:n}")
else:
self.cCountData.setText("")
self.wCountData.setText("")
+4
View File
@@ -276,6 +276,7 @@ class GuiMainMenu(QMenuBar):
self.aExitNW = QAction("Exit", self)
self.aExitNW.setStatusTip("Exit %s" % self.mainConf.appName)
self.aExitNW.setShortcut("Ctrl+Q")
self.aExitNW.setMenuRole(QAction.QuitRole)
self.aExitNW.triggered.connect(lambda: self.theParent.closeMain())
self.projMenu.addAction(self.aExitNW)
@@ -843,6 +844,7 @@ class GuiMainMenu(QMenuBar):
self.aPreferences = QAction("Preferences", self)
self.aPreferences.setStatusTip("Preferences")
self.aPreferences.setShortcut("Ctrl+,")
self.aPreferences.setMenuRole(QAction.PreferencesRole)
self.aPreferences.triggered.connect(lambda: self.theParent.showPreferencesDialog())
self.toolsMenu.addAction(self.aPreferences)
@@ -857,12 +859,14 @@ class GuiMainMenu(QMenuBar):
# Help > About
self.aAboutNW = QAction("About %s" % self.mainConf.appName, self)
self.aAboutNW.setStatusTip("About %s" % self.mainConf.appName)
self.aAboutNW.setMenuRole(QAction.AboutRole)
self.aAboutNW.triggered.connect(lambda: self.theParent.showAboutNWDialog())
self.helpMenu.addAction(self.aAboutNW)
# Help > About Qt5
self.aAboutQt = QAction("About Qt5", self)
self.aAboutQt.setStatusTip("About Qt5")
self.aAboutQt.setMenuRole(QAction.AboutQtRole)
self.aAboutQt.triggered.connect(lambda: self.theParent.showAboutQtDialog())
self.helpMenu.addAction(self.aAboutQt)
+3 -3
View File
@@ -431,9 +431,9 @@ class GuiOutline(QTreeWidget):
newItem.setText(self.colIndex[nwOutline.LINE], sTitle[1:].lstrip("0"))
newItem.setData(self.colIndex[nwOutline.LINE], Qt.UserRole, sTitle)
newItem.setText(self.colIndex[nwOutline.SYNOP], novIdx["synopsis"])
newItem.setText(self.colIndex[nwOutline.CCOUNT], str(novIdx["cCount"]))
newItem.setText(self.colIndex[nwOutline.WCOUNT], str(novIdx["wCount"]))
newItem.setText(self.colIndex[nwOutline.PCOUNT], str(novIdx["pCount"]))
newItem.setText(self.colIndex[nwOutline.CCOUNT], "{:n}".format(novIdx["cCount"]))
newItem.setText(self.colIndex[nwOutline.WCOUNT], "{:n}".format(novIdx["wCount"]))
newItem.setText(self.colIndex[nwOutline.PCOUNT], "{:n}".format(novIdx["pCount"]))
newItem.setTextAlignment(self.colIndex[nwOutline.CCOUNT], Qt.AlignRight)
newItem.setTextAlignment(self.colIndex[nwOutline.WCOUNT], Qt.AlignRight)
newItem.setTextAlignment(self.colIndex[nwOutline.PCOUNT], Qt.AlignRight)
+1 -1
View File
@@ -340,7 +340,7 @@ class GuiConfigEditGeneralTab(QWidget):
dlgOpt = QFileDialog.Options()
dlgOpt |= QFileDialog.ShowDirsOnly
dlgOpt |= QFileDialog.DontUseNativeDialog
newDir = QFileDialog.getExistingDirectory(
newDir = QFileDialog.getExistingDirectory(
self, "Backup Directory", currDir, options=dlgOpt
)
if newDir:
+18 -6
View File
@@ -125,7 +125,7 @@ class GuiProjectLoad(QDialog):
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Open | QDialogButtonBox.Cancel)
self.buttonBox.accepted.connect(self._doOpenRecent)
self.buttonBox.rejected.connect(self._doClose)
self.buttonBox.rejected.connect(self._doCancel)
self.newButton = self.buttonBox.addButton("New", QDialogButtonBox.ActionRole)
self.newButton.clicked.connect(self._doNewProject)
@@ -153,7 +153,7 @@ class GuiProjectLoad(QDialog):
"""Close the dialog window with a recent project selected.
"""
logger.verbose("GuiProjectLoad open button clicked")
self._saveDialogState()
self._saveSettings()
selItems = self.listBox.selectedItems()
if selItems:
@@ -194,11 +194,12 @@ class GuiProjectLoad(QDialog):
return
def _doClose(self):
def _doCancel(self):
"""Close the dialog window without doing anything.
"""
logger.verbose("GuiProjectLoad close button clicked")
self._saveDialogState()
self.openPath = None
self.openState = self.NONE_STATE
self.close()
return
@@ -206,7 +207,7 @@ class GuiProjectLoad(QDialog):
"""Create a new project.
"""
logger.verbose("GuiProjectLoad new project button clicked")
self._saveDialogState()
self._saveSettings()
self.openPath = None
self.openState = self.NEW_STATE
self.accept()
@@ -230,11 +231,22 @@ class GuiProjectLoad(QDialog):
return
##
# Events
##
def closeEvent(self, theEvent):
"""Capture the user closing the dialog so we can save settings.
"""
self._saveSettings()
theEvent.accept()
return
##
# Internal Functions
##
def _saveDialogState(self):
def _saveSettings(self):
"""Save the changes made to the dialog.
"""
colWidths = [0, 0, 0]
+3 -3
View File
@@ -282,15 +282,15 @@ class GuiProjectEditMeta(QWidget):
self.nRootLabel = QLabel("Root folders:")
self.nRootLabel.setIndent(xInd)
self.nRootValue = QLabel("{:n}".format(nR))
self.nRootValue = QLabel(f"{nR:n}")
self.nDirLabel = QLabel("Folders:")
self.nDirLabel.setIndent(xInd)
self.nDirValue = QLabel("{:n}".format(nD))
self.nDirValue = QLabel(f"{nD:n}")
self.nFileLabel = QLabel("Documents:")
self.nFileLabel.setIndent(xInd)
self.nFileValue = QLabel("{:n}".format(nF))
self.nFileValue = QLabel(f"{nF:n}")
self.wordsLabel = QLabel("Word count:")
self.wordsLabel.setIndent(xInd)
+8 -5
View File
@@ -401,7 +401,7 @@ class GuiProjectTree(QTreeWidget):
if nwItemS is None:
return False
wCount = int(trItemS.text(self.C_COUNT))
wCount = int(trItemS.data(self.C_COUNT, Qt.UserRole))
if nwItemS.itemType == nwItemType.FILE:
logger.debug("User requested file %s moved to trash" % tHandle)
trItemP = trItemS.parent()
@@ -550,12 +550,13 @@ class GuiProjectTree(QTreeWidget):
"""
tItem = self._getTreeItem(tHandle)
if tItem is not None:
tItem.setText(self.C_COUNT, str(theCount))
tItem.setText(self.C_COUNT, f"{theCount:n}")
tItem.setData(self.C_COUNT, Qt.UserRole, int(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))
pCount += int(pItem.child(i).data(self.C_COUNT, Qt.UserRole))
pHandle = pItem.data(self.C_NAME, Qt.UserRole)
if not nDepth > nwConst.maxDepth + 1 and pHandle != "":
@@ -575,7 +576,7 @@ class GuiProjectTree(QTreeWidget):
tItem = self.topLevelItem(n)
if tItem == self.orphRoot:
continue
nWords += int(tItem.text(self.C_COUNT))
nWords += int(tItem.data(self.C_COUNT, Qt.UserRole))
self.theProject.setProjectWordCount(nWords)
sWords = self.theProject.getSessionWordCount()
@@ -715,7 +716,7 @@ class GuiProjectTree(QTreeWidget):
self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR)
return
wCount = int(sItem.text(self.C_COUNT))
wCount = int(sItem.data(self.C_COUNT, Qt.UserRole))
isSame = snItem.itemClass == dnItem.itemClass
isNone = snItem.itemClass == nwItemClass.NO_CLASS
isNote = snItem.itemLayout == nwItemLayout.NOTE
@@ -809,6 +810,7 @@ class GuiProjectTree(QTreeWidget):
newItem.setTextAlignment(self.C_FLAGS, Qt.AlignLeft | Qt.AlignVCenter)
newItem.setData(self.C_NAME, Qt.UserRole, tHandle)
newItem.setData(self.C_COUNT, Qt.UserRole, 0)
self.theMap[tHandle] = newItem
if pHandle is None:
@@ -881,6 +883,7 @@ class GuiProjectTree(QTreeWidget):
self.orphRoot = newItem
newItem.setExpanded(True)
newItem.setData(self.C_NAME, Qt.UserRole, "")
newItem.setData(self.C_COUNT, Qt.UserRole, 0)
newItem.setIcon(self.C_NAME, self.theTheme.getIcon("proj_orphan"))
return
+5 -1
View File
@@ -152,13 +152,17 @@ class GuiMainStatus(QStatusBar):
qApp.processEvents()
return
def setLanguage(self, theLanguage):
def setLanguage(self, theLanguage, theProvider=""):
"""Set the language code for the spell checker.
"""
if theLanguage is None:
self.langText.setText("None")
self.langText.setToolTip("")
else:
self.langText.setText(NWSpellCheck.expandLanguage(theLanguage))
self.langText.setToolTip(
"Provider: %s" % (theProvider if theProvider else "unknown")
)
return
def setProjectStatus(self, isChanged):
+21 -15
View File
@@ -33,7 +33,7 @@ import os
from datetime import datetime
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPixmap
from PyQt5.QtGui import QPixmap, QCursor
from PyQt5.QtWidgets import (
qApp, QDialog, QTreeWidget, QTreeWidgetItem, QDialogButtonBox, QGridLayout,
QLabel, QGroupBox, QMenu, QAction, QFileDialog, QSpinBox, QHBoxLayout
@@ -253,10 +253,15 @@ class GuiWritingStats(QDialog):
logger.debug("GuiWritingStats initialisation complete")
qApp.processEvents()
return
def populateGUI(self):
"""Populate list box with data from the log file.
"""
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
self._loadLogFile()
self._updateListBox()
qApp.restoreOverrideCursor()
return
##
@@ -320,20 +325,20 @@ class GuiWritingStats(QDialog):
# Generate the file name
if fileExt:
fileName = "sessionStats.%s" % fileExt
saveDir = self.mainConf.lastPath
savePath = os.path.join(saveDir, fileName)
saveDir = self.mainConf.lastPath
if not os.path.isdir(saveDir):
saveDir = self.mainConf.homePath
saveDir = os.path.expanduser("~")
fileName = "sessionStats.%s" % fileExt
savePath = os.path.join(saveDir, fileName)
dlgOpt = QFileDialog.Options()
dlgOpt |= QFileDialog.DontUseNativeDialog
saveTo = QFileDialog.getSaveFileName(
savePath, _ = QFileDialog.getSaveFileName(
self, "Save Document As", savePath, options=dlgOpt
)
if saveTo:
savePath = saveTo[0]
else:
if not savePath:
return False
self.mainConf.setLastPath(savePath)
@@ -449,10 +454,11 @@ class GuiWritingStats(QDialog):
)
return False
ttWords = ttNovel + ttNotes
self.labelTotal.setText(self._formatTime(ttTime))
self.novelWords.setText("{:n}".format(ttNovel))
self.notesWords.setText("{:n}".format(ttNotes))
self.totalWords.setText("{:n}".format(ttNovel + ttNotes))
self.novelWords.setText(f"{ttNovel:n}")
self.notesWords.setText(f"{ttNotes:n}")
self.totalWords.setText(f"{ttWords:n}")
return True
@@ -539,7 +545,7 @@ class GuiWritingStats(QDialog):
newItem = QTreeWidgetItem()
newItem.setText(self.C_TIME, sStart)
newItem.setText(self.C_LENGTH, self._formatTime(sDiff))
newItem.setText(self.C_COUNT, "{:n}".format(nWords))
newItem.setText(self.C_COUNT, f"{nWords:n}")
if nWords > 0 and listMax > 0:
theBar = self.barImage.scaled(
+22 -9
View File
@@ -32,7 +32,7 @@ import os
from datetime import datetime
from time import time
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtCore import Qt, QTimer, QThreadPool
from PyQt5.QtGui import QIcon, QPixmap, QColor, QKeySequence, QCursor
from PyQt5.QtWidgets import (
qApp, QMainWindow, QVBoxLayout, QWidget, QSplitter, QFileDialog, QShortcut,
@@ -48,6 +48,7 @@ from nw.gui import (
)
from nw.core import NWProject, NWDoc, NWIndex
from nw.constants import nwItemType, nwItemClass, nwAlert
from nw.common import getGuiItem
logger = logging.getLogger(__name__)
@@ -59,6 +60,7 @@ class GuiMain(QMainWindow):
logger.debug("Initialising GUI ...")
self.setObjectName("GuiMain")
self.mainConf = nw.CONFIG
self.threadPool = QThreadPool()
# Some runtime info useful for debugging
logger.info("OS: %s" % self.mainConf.osType)
@@ -448,6 +450,7 @@ class GuiMain(QMainWindow):
"""Close the document and clear the editor and title field.
"""
if self.hasProject:
self.docEditor.saveCursorPosition()
if self.docEditor.docChanged:
self.saveDocument()
self.docEditor.clearEditor()
@@ -556,16 +559,15 @@ class GuiMain(QMainWindow):
extFilter = [
"Text files (*.txt)",
"Markdown files (*.md)",
"novelWriter files (*.nwd)",
"All files (*.*)",
]
dlgOpt = QFileDialog.Options()
dlgOpt |= QFileDialog.DontUseNativeDialog
inPath = QFileDialog.getOpenFileName(
loadFile, _ = QFileDialog.getOpenFileName(
self, "Import File", lastPath, options=dlgOpt, filter=";;".join(extFilter)
)
if inPath:
loadFile = inPath[0]
else:
if not loadFile:
return False
if loadFile.strip() == "":
@@ -716,8 +718,7 @@ class GuiMain(QMainWindow):
tEnd = time()
self.statusBar.setStatus("Indexing completed in %.1f ms" % ((tEnd - tStart)*1000.0))
self.docEditor.reloadText()
self.docEditor.updateTagHighLighting()
qApp.restoreOverrideCursor()
if not beQuiet:
@@ -816,9 +817,15 @@ class GuiMain(QMainWindow):
logger.error("No project open")
return
dlgBuild = GuiBuildNovel(self, self.theProject)
dlgBuild = getGuiItem("GuiBuildNovel")
if dlgBuild is None:
dlgBuild = GuiBuildNovel(self, self.theProject)
dlgBuild.setModal(False)
dlgBuild.show()
qApp.processEvents()
dlgBuild.viewCachedDoc()
return
def showWritingStatsDialog(self):
@@ -828,9 +835,15 @@ class GuiMain(QMainWindow):
logger.error("No project open")
return
dlgStats = GuiWritingStats(self, self.theProject)
dlgStats = getGuiItem("GuiWritingStats")
if dlgStats is None:
dlgStats = GuiWritingStats(self, self.theProject)
dlgStats.setModal(False)
dlgStats.show()
qApp.processEvents()
dlgStats.populateGUI()
return
def showAboutNWDialog(self):