Merge branch 'main' into release_1.0b4
This commit is contained in:
+16
-6
@@ -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> - ".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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
@@ -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> " % 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
@@ -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
@@ -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
@@ -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>- %s"
|
||||
) % "<br>- ".join(makeHtml.errData), nwAlert.ERROR)
|
||||
|
||||
if replaceTabs:
|
||||
htmlText = []
|
||||
eightSpace = " "*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", "	")
|
||||
|
||||
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
@@ -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 2018–2020, 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
@@ -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
@@ -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;"
|
||||
|
||||
@@ -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("–")
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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]
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user