Merge pull request #257 from vkbo/dev

Dev to Master for Release 0.7
This commit is contained in:
Veronica K. Berglyd Olsen
2020-05-30 00:31:51 +02:00
committed by GitHub
64 changed files with 866 additions and 425 deletions
+4 -6
View File
@@ -14,12 +14,10 @@ docs/source/_*
__pycache__
# Sample Project
sample/**/cache
sample/**/wordlist.txt
sample/**/sessionInfo.log
sample/**/*.bak
sample/**/*.json
sample/**/*.lock
sample/cache
sample/meta
sample/*.bak
sample/*.lock
# PyTest
tests/temp
+22 -1
View File
@@ -1,5 +1,25 @@
# novelWriter ChangeLog
## Version 0.7 RC1 [2020-xx-xx]
**User Interface**
* The back-references list now shows references to any tag in the open document, not just the first tag. Issue #227, PR #234.
* Clicking a tag now tries to scroll to the header where the tag is set. The index needed a couple of minor changes for this feature, so this will invalidate the old index for a project, and require a new to be built. This is done automatically. PR #234.
* Moved the Close button on the "Build Novel project" dialog to the area with the other buttons since we anyway increased the size of that area. PR #256.
**Project Structure**
* The project folder structure has been simplified and cleaned up. We also now freeze the main entry values in the main XML file. The XML file is now given version 1.1, and no further core changes to its structure will be made without bumping this version. We're also locking it to only be opened by version 0.7 or later. An old project file is converted on first open. PRs #253 and #261.
* When a project is closed, two table of contents files are written to the project folder. They are named `ToC.txt` and `ToC.json` and are there for the user's convenience if they want to find a specific file from the project in the data folders. As discussed in Issue #259, PR #261.
* The expanded node flag from the project tree was also saved for file entries, which cannot actually be expanded. These flags are no longer saved in the XML file. PR #261.
**Other Changes**
* Dropped the usage of .bak copies of document files. This was the old method to ensure the document data was written successfully, but it uses twice the storage space. Instead, writing via a temp file is the safe way to save files. PR #248.
* The project class now records the accumulated time in seconds a project has been opened. This data is not yet displayed anywhere, but it is being tracked in the project XML file. PR #261.
## Version 0.6.4 [2020-xx-xx]
**User Interface**
@@ -27,7 +47,8 @@
## Version 0.6.2 [2020-05-28]
* Botched release. Replaced with 0.6.3. Crashes when Build Novel project is opened.
* Botched release. Replaced with 0.6.3. Crashes when Build Novel Project is opened.
## Version 0.6.1 [2020-05-25]
+2 -2
View File
@@ -24,9 +24,9 @@ copyright = "2018-2020, Veronica Berglyd Olsen"
author = "Veronica Berglyd Olsen"
# The short X.Y version
version = "0.6.3"
version = "0.7.0"
# The full version, including alpha/beta/rc tags
release = "0.6.3"
release = "0.7.0rc1"
# -- General configuration ---------------------------------------------------
+2 -2
View File
@@ -30,9 +30,9 @@ The project XML file is indent-formatted, suitable for diff tools and version co
Project Documents
-----------------
The project documents are saved in folders starting with ``data_``.
The project documents are saved in a folder in the main project folder named ``content``.
Each document has a file handle taken from the first 13 characters of a SHA256 hash of the system time when the file was first created.
The documents are saved with a folder and filename derived from this hash.
The documents are saved with a filename assembled from this hash and the file extension ``.nwd``.
If you wish to find the physical location of a file in the project, you can either look it up in the project XML file, or select :menuselection:`Document --> Show File Details` in the menu when having the document open.
The reason for this cryptic file naming is to avoid issues with file naming conventions and restrictions on different operating systems, and also to have a file name that does not depend on what the user names the files, or changes it to.
+2 -2
View File
@@ -40,8 +40,8 @@ __package__ = "novelWriter"
__author__ = "Veronica Berglyd Olsen"
__copyright__ = "Copyright 20182020, Veronica Berglyd Olsen"
__license__ = "GPLv3"
__version__ = "0.6.3"
__hexversion__ = "0x000603f0"
__version__ = "0.7.0rc1"
__hexversion__ = "0x000700c1"
__date__ = "2020-05-28"
__maintainer__ = "Veronica Berglyd Olsen"
__email__ = "code@vkbo.net"
+3 -1
View File
@@ -533,7 +533,9 @@ class Config:
# Write config file
try:
cnfParse.write(open(path.join(self.confPath,self.confFile),mode="w",encoding="utf8"))
cnfParse.write(
open(path.join(self.confPath, self.confFile), mode="w", encoding="utf8")
)
self.confChanged = False
except Exception as e:
logger.error("Could not save config file")
+2
View File
@@ -39,6 +39,8 @@ class nwFiles():
PROJ_FILE = "nwProject.nwx"
PROJ_DICT = "wordlist.txt"
PROJ_LOCK = "nwProject.lock"
TOC_TXT = "ToC.txt"
TOC_JSON = "ToC.json"
SESS_INFO = "sessionInfo.log"
INDEX_FILE = "tagsIndex.json"
OPTS_FILE = "guiOptions.json"
-2
View File
@@ -9,7 +9,6 @@ from nw.core.spellcheck import NWSpellSimple
from nw.core.tokenizer import Tokenizer
from nw.core.tohtml import ToHtml
from nw.core.tools import countWords
from nw.core.tools import projectMaintenance
from nw.core.tools import numberToWord
__all__ = [
@@ -22,6 +21,5 @@ __all__ = [
"Tokenizer",
"ToHtml",
"countWords",
"projectMaintenance",
"numberToWord",
]
+24 -44
View File
@@ -37,8 +37,6 @@ logger = logging.getLogger(__name__)
class NWDoc():
FILE_MN = "main.nwd"
def __init__(self, theProject, theParent):
self.mainConf = nw.CONFIG
@@ -93,17 +91,17 @@ class NWDoc():
if self.theItem.parHandle == self.theProject.projTree.trashRoot():
self.docEditable = False
docDir, docFile = self._assemblePath(self.docHandle, self.FILE_MN)
self.fileLoc = path.join(docDir,docFile)
logger.debug("Opening document %s" % self.fileLoc)
dataDir = path.join(self.theProject.projPath, docDir)
docPath = path.join(dataDir, docFile)
docFile = self.docHandle+".nwd"
logger.debug("Opening document %s" % docFile)
docPath = path.join(self.theProject.projContent, docFile)
self.fileLoc = docPath
theText = ""
self.docMeta = ""
if path.isfile(docPath):
try:
with open(docPath,mode="r",encoding="utf8") as inFile:
with open(docPath, mode="r", encoding="utf8") as inFile:
fstLine = inFile.readline()
if fstLine.startswith("%%~ "):
# This is the meta line
@@ -113,7 +111,7 @@ class NWDoc():
theText += inFile.read()
except Exception as e:
self.makeAlert(["Failed to open document file.",str(e)], nwAlert.ERROR)
self.makeAlert(["Failed to open document file.", str(e)], nwAlert.ERROR)
# Note: Document must be cleared in case of an io error,
# or else the auto-save or save will try to overwrite it
# with an empty file. Return None to alert the caller.
@@ -139,34 +137,29 @@ class NWDoc():
if self.docHandle is None or not self.docEditable:
return False
docDir, docFile = self._assemblePath(self.docHandle, self.FILE_MN)
logger.debug("Saving document %s" % path.join(docDir,docFile))
dataPath = path.join(self.theProject.projPath, docDir)
docPath = path.join(dataPath, docFile)
if not path.isdir(dataPath):
mkdir(dataPath)
logger.debug("Created folder %s" % dataPath)
self.theProject.ensureFolderStructure()
docTemp = path.join(dataPath, docFile+"~")
docBack = path.join(dataPath, docFile[:-3]+"bak")
docFile = self.docHandle+".nwd"
logger.debug("Saving document %s" % docFile)
docPath = path.join(self.theProject.projContent, docFile)
docTemp = path.join(self.theProject.projContent, docFile+"~")
itemPath = self.theProject.projTree.getItemPath(self.docHandle)
docMeta = "%%~ "+":".join(itemPath)+":"+self.theItem.itemName+"\n"
try:
with open(docTemp,mode="w",encoding="utf8") as outFile:
with open(docTemp, mode="w", encoding="utf8") as outFile:
outFile.write(docMeta)
outFile.write(docText)
except Exception as e:
self.makeAlert(["Could not save document.",str(e)], nwAlert.ERROR)
self.makeAlert(["Could not save document.", str(e)], nwAlert.ERROR)
return False
# If we're here, the file was successfully saved,
# so let's sort out the temps and backups
if path.isfile(docBack):
unlink(docBack)
# If we're here, the file was successfully saved, so we can
# replace the temp file with the actual file
if path.isfile(docPath):
rename(docPath, docBack)
unlink(docPath)
rename(docTemp, docPath)
self.theParent.statusBar.setStatus("Saved Document: %s" % self.theItem.itemName)
@@ -177,12 +170,12 @@ class NWDoc():
"""Permanently delete a document source file and its backups
from the project data folder.
"""
docDir, docFile = self._assemblePath(tHandle, self.FILE_MN)
dataPath = path.join(self.theProject.projPath, docDir)
docFile = self.docHandle+".nwd"
chkList = []
chkList.append(path.join(dataPath, docFile))
chkList.append(path.join(dataPath, docFile+"~"))
chkList.append(path.join(dataPath, docFile[:-3]+"bak"))
chkList.append(path.join(self.theProject.projContent, docFile))
chkList.append(path.join(self.theProject.projContent, docFile+"~"))
for chkFile in chkList:
if path.isfile(chkFile):
try:
@@ -191,6 +184,7 @@ class NWDoc():
except Exception as e:
self.makeAlert(["Could not delete document file.",str(e)], nwAlert.ERROR)
return False
return True
##
@@ -224,18 +218,4 @@ class NWDoc():
return theMeta, thePath
##
# Internal Functions
##
@staticmethod
def _assemblePath(tHandle, docExt):
"""Assemble the file path for a given handle.
"""
if tHandle is None:
return None, None
docDir = "data_"+tHandle[0]
docFile = tHandle[1:13]+"_"+docExt
return docDir, docFile
# END Class NWDoc
+17 -17
View File
@@ -199,7 +199,7 @@ class NWIndex():
try:
for tTag in self.tagIndex:
if len(self.tagIndex[tTag]) != 3:
if len(self.tagIndex[tTag]) != 4:
self.indexBroken = True
for tHandle in self.refIndex:
@@ -228,7 +228,7 @@ class NWIndex():
if self.indexBroken:
self.clearIndex()
self.theParent.makeAlert(
"The index loaded from project cache contains errors. Rebuilding index.",
"The project index is outdated or broken. Rebuilding index.",
nwAlert.WARN
)
@@ -260,9 +260,9 @@ class NWIndex():
logger.debug("Indexing item with handle %s" % tHandle)
# Check file type, and reset its old index
# Also add a dummy entry for T0 in case the file has no title
# Also add a dummy entry T000000 in case the file has no title
self.refIndex[tHandle] = {}
self.refIndex[tHandle]["T0"] = {
self.refIndex[tHandle]["T000000"] = {
"tags" : [],
"updated" : time(),
}
@@ -301,7 +301,7 @@ class NWIndex():
elif aLine.startswith(r"@"):
self._indexNoteRef(tHandle, aLine, nLine, nTitle)
self._indexTag(tHandle, aLine, nLine, itemClass)
self._indexTag(tHandle, aLine, nLine, nTitle, itemClass)
elif aLine.startswith(r"%"):
if nTitle > 0:
@@ -436,7 +436,7 @@ class NWIndex():
return True
def _indexTag(self, tHandle, aLine, nLine, itemClass):
def _indexTag(self, tHandle, aLine, nLine, nTitle, itemClass):
"""Validate and save the information from a tag.
"""
isValid, theBits, thePos = self.scanThis(aLine)
@@ -444,7 +444,8 @@ class NWIndex():
return False
if theBits[0] == nwKeyWords.TAG_KEY:
self.tagIndex[theBits[1]] = [nLine, tHandle, itemClass.name]
sTitle = "T%06d" % nTitle
self.tagIndex[theBits[1]] = [nLine, tHandle, itemClass.name, sTitle]
return True
@@ -606,18 +607,17 @@ class NWIndex():
if tHandle is None:
return theRefs
theTag = None
theTags = set()
for tTag in self.tagIndex:
if tHandle == self.tagIndex[tTag][1]:
theTag = tTag
break
theTags.add(tTag)
if theTag is not None:
if theTags:
for tHandle in self.refIndex:
for sTitle in self.refIndex[tHandle]:
for nLine, tKey, tTag in self.refIndex[tHandle][sTitle]["tags"]:
if tTag == theTag:
theRefs[tHandle] = nLine
for _, _, tTag in self.refIndex[tHandle][sTitle]["tags"]:
if tTag in theTags and tHandle not in theRefs:
theRefs[tHandle] = sTitle
return theRefs
@@ -626,8 +626,8 @@ class NWIndex():
"""
if theTag in self.tagIndex:
theRef = self.tagIndex[theTag]
if len(theRef) == 3:
return theRef[1], theRef[0]
return None, 0
if len(theRef) == 4:
return theRef[1], theRef[0], theRef[3]
return None, 0, "T000000"
# END Class NWIndex
+282 -54
View File
@@ -31,9 +31,10 @@
"""
import logging
import json
import nw
from os import path, mkdir, listdir, unlink, rename
from os import path, mkdir, listdir, unlink, rename, rmdir
from lxml import etree
from hashlib import sha256
from time import time
@@ -42,7 +43,6 @@ from shutil import make_archive
from PyQt5.QtWidgets import QMessageBox
from nw.gui.tools import OptionState
from nw.core.tools import projectMaintenance
from nw.core.document import NWDoc
from nw.common import checkString, checkBool, checkInt, formatTimeStamp
from nw.constants import (
@@ -70,12 +70,15 @@ class NWProject():
self.lockedBy = None # Data on which computer has the project open
self.saveCount = 0 # Meta data: number of saves
self.autoCount = 0 # Meta data: number of automatic saves
self.editTime = 0 # The accumulated edit time read from the project file
# Class Settings
self.projPath = None # The full path to where the currently open project is saved
self.projMeta = None # The full path to the project's meta data folder
self.projDict = None # The spell check dictionary
self.projFile = None # The file name of the project main XML file
self.projPath = None # The full path to where the currently open project is saved
self.projMeta = None # The full path to the project's meta data folder
self.projCache = None # The full path to the project's cache folder
self.projContent = None # The full path to the project's content folder
self.projDict = None # The spell check dictionary
self.projFile = None # The file name of the project main XML file
# Project Meta
self.projName = "" # Project name (working title)
@@ -112,7 +115,7 @@ class NWProject():
CUSTOM, and always have parent handle set to None.
"""
if not self.projTree.checkRootUnique(rootClass):
self.makeAlert("Duplicate root item detected!", nwAlert.ERROR)
self.makeAlert("Duplicate root item detected.", nwAlert.ERROR)
return None
newItem = NWItem(self)
newItem.setName(rootName)
@@ -195,6 +198,8 @@ class NWProject():
# Project Settings
self.projPath = None
self.projMeta = None
self.projCache = None
self.projContent = None
self.projDict = None
self.projFile = nwFiles.PROJ_FILE
self.projName = ""
@@ -203,7 +208,7 @@ class NWProject():
self.autoReplace = {}
self.titleFormat = {
"title" : r"%title%",
"chapter" : r"Chapter %num%\\%title%",
"chapter" : r"Chapter %ch%: %title%",
"unnumbered" : r"%title%",
"scene" : r"* * *",
"section" : r"",
@@ -246,11 +251,30 @@ class NWProject():
self.projPath = path.abspath(path.dirname(fileName))
logger.debug("Opening project: %s" % self.projPath)
self.projMeta = path.join(self.projPath,"meta")
# Standard Folders and Files
# ==========================
if not self.ensureFolderStructure():
return False
self.projDict = path.join(self.projMeta, nwFiles.PROJ_DICT)
if not self._checkFolder(self.projMeta):
return False
# Check for Old Legacy Data
# =========================
errList = []
for projItem in listdir(self.projPath):
logger.verbose("Project contains: %s" % projItem)
if projItem.startswith("data_"):
self._legacyDataFolder(projItem)
if errList:
self.makeAlert(errList, nwAlert.ERROR)
self._deprecatedFiles()
# Project Lock
# ============
if overrideLock:
self._clearLockFile()
@@ -267,10 +291,8 @@ class NWProject():
else:
logger.verbose("Project is not locked")
try:
projectMaintenance(self)
except Exception as E:
logger.error(str(E))
# Open The Project XML File
# =========================
try:
nwXML = etree.parse(fileName)
@@ -291,7 +313,7 @@ class NWProject():
self.clearProject()
return False
xRoot = nwXML.getroot()
xRoot = nwXML.getroot()
nwxRoot = xRoot.tag
appVersion = "Unknown"
@@ -310,17 +332,51 @@ class NWProject():
self.saveCount = checkInt(xRoot.attrib["saveCount"], 0, False)
if "autoCount" in xRoot.attrib:
self.autoCount = checkInt(xRoot.attrib["autoCount"], 0, False)
if "editTime" in xRoot.attrib:
self.editTime = checkInt(xRoot.attrib["editTime"], 0, False)
logger.verbose("XML root is %s" % nwxRoot)
logger.verbose("File version is %s" % fileVersion)
if not nwxRoot == "novelWriterXML" or not fileVersion == "1.0":
# Check File Type
# ===============
if not nwxRoot == "novelWriterXML":
self.makeAlert(
"Project file does not appear to be a novelWriterXML file version 1.0",
"Project file does not appear to be a novelWriterXML file.",
nwAlert.ERROR
)
return False
# Check Project Storage Version
# =============================
if fileVersion == "1.0":
msgBox = QMessageBox()
msgRes = msgBox.question(self.theParent, "Old Project Version", (
"The project file and data is created by a %s version lower than 0.7. "
"Do you want to upgrade the project to the most recent format?<br><br>"
"Note that after the upgrade, you cannot open the project with an older "
"version of %s any more, so make sure you have a recent backup."
) % (
nw.__package__, nw.__package__
))
if msgRes == QMessageBox.Yes:
self._updateStorage()
else:
return False
elif fileVersion != "1.1":
self.makeAlert((
"Unknown or unsupported %s project format. "
"The project cannot be opened by this version of %s."
) % (
nw.__package__, nw.__package__
), nwAlert.ERROR)
return False
# Check novelWriter Version
# =========================
if int(hexVersion, 16) > int(nw.__hexversion__, 16) and self.mainConf.showGUI:
msgBox = QMessageBox()
msgRes = msgBox.question(self.theParent, "Version Conflict", (
@@ -333,6 +389,9 @@ class NWProject():
if msgRes != QMessageBox.Yes:
return False
# Start Parsing XML
# =================
for xChild in xRoot:
if xChild.tag == "project":
logger.debug("Found project meta")
@@ -350,6 +409,7 @@ class NWProject():
self.bookAuthors.append(xItem.text)
elif xItem.tag == "backup":
self.doBackup = checkBool(xItem.text, False)
elif xChild.tag == "settings":
logger.debug("Found project settings")
for xItem in xChild:
@@ -377,6 +437,7 @@ class NWProject():
for xEntry in xItem:
titleFormat[xEntry.tag] = checkString(xEntry.text, "", False)
self.setTitleFormat(titleFormat)
elif xChild.tag == "content":
logger.debug("Found project content")
self.projTree.unpackXML(xChild)
@@ -404,15 +465,13 @@ class NWProject():
file.
"""
if self.projPath is None:
self.makeAlert("Project path not set, cannot save.", nwAlert.ERROR)
self.makeAlert(
"Project path not set, cannot save project.", nwAlert.ERROR
)
return False
self.projMeta = path.join(self.projPath,"meta")
saveTime = time()
if not self._checkFolder(self.projPath):
return False
if not self._checkFolder(self.projMeta):
if not self.ensureFolderStructure():
return False
logger.debug("Saving project: %s" % self.projPath)
@@ -427,10 +486,11 @@ class NWProject():
nwXML = etree.Element("novelWriterXML",attrib={
"appVersion" : str(nw.__version__),
"hexVersion" : str(nw.__hexversion__),
"fileVersion" : "1.0",
"fileVersion" : "1.1",
"saveCount" : str(self.saveCount),
"autoCount" : str(self.autoCount),
"timeStamp" : formatTimeStamp(saveTime),
"editTime" : str(int(self.editTime + saveTime - self.projOpened)),
})
# Save Project Meta
@@ -480,7 +540,7 @@ class NWProject():
xml_declaration = True
))
except Exception as e:
self.makeAlert(["Failed to save project.",str(e)], nwAlert.ERROR)
self.makeAlert(["Failed to save project.", str(e)], nwAlert.ERROR)
return False
# If we're here, the file was successfully saved,
@@ -507,12 +567,33 @@ class NWProject():
def closeProject(self):
"""Close the current project and clear all meta data.
"""
self.projTree.writeToCFiles()
self._appendSessionStats()
self._clearLockFile()
self.clearProject()
self.lockedBy = None
return True
def ensureFolderStructure(self):
"""Ensure that all necessary folders exist in the project
folder.
"""
if self.projPath is None or self.projPath == "":
return False
self.projMeta = path.join(self.projPath, "meta")
self.projCache = path.join(self.projPath, "cache")
self.projContent = path.join(self.projPath, "content")
if not self._checkFolder(self.projMeta):
return False
if not self._checkFolder(self.projCache):
return False
if not self._checkFolder(self.projContent):
return False
return True
##
# Backup Project
##
@@ -937,35 +1018,26 @@ class NWProject():
if self.projPath is None:
return
# First, scan the project data folders
itemList = []
for subItem in listdir(self.projPath):
if subItem[:5] != "data_":
continue
dataDir = path.join(self.projPath,subItem)
for subFile in listdir(dataDir):
if subFile[-4:] == ".nwd":
newItem = path.join(subItem,subFile)
itemList.append(newItem)
# Then check the valid files
# Then check the files in the data folder
orphanFiles = []
for fileItem in itemList:
if len(fileItem) != 28:
# Just to be safe, shouldn't happen
for fileItem in listdir(self.projContent):
if not fileItem.endswith(".nwd"):
logger.warning("Skipping file %s" % fileItem)
continue
fHandle = fileItem[5]+fileItem[7:19]
if len(fileItem) != 17:
logger.warning("Skipping file %s" % fileItem)
continue
fHandle = fileItem[:13]
if fHandle in self.projTree:
logger.debug("Checking file %s, handle %s: OK" % (fileItem,fHandle))
logger.debug("Checking file %s, handle %s: OK" % (fileItem, fHandle))
else:
logger.debug("Checking file %s, handle %s: Orphaned" % (fileItem,fHandle))
logger.debug("Checking file %s, handle %s: Orphaned" % (fileItem, fHandle))
orphanFiles.append(fHandle)
# Report status
if len(orphanFiles) > 0:
self.makeAlert(
"Found %d orphaned file(s) in project folder!" % len(orphanFiles),
"Found %d orphaned file(s) in project folder." % len(orphanFiles),
nwAlert.WARN
)
else:
@@ -998,7 +1070,7 @@ class NWProject():
def _appendSessionStats(self):
"""Append session statistics to the sessions log file.
"""
if self.projMeta is None:
if not self.ensureFolderStructure():
return False
sessionFile = path.join(self.projMeta, nwFiles.SESS_INFO)
@@ -1016,12 +1088,122 @@ class NWProject():
return True
##
# Legacy Data Structure Handlers
##
def _legacyDataFolder(self, theFolder):
"""Clean up legacy data folders.
"""
errList = []
theData = path.join(self.projPath, theFolder)
if not path.isdir(theData):
errList.append("Not a folder: %s" % theData)
return errList
logger.info("Old data folder %s found" % theFolder)
# Move Documents to Content
# =========================
for dataItem in listdir(theData):
theFile = path.join(theData, dataItem)
if not path.isfile(theFile):
theErr = self._moveUnknownItem(theData, dataItem)
if theErr:
errList.append(theErr)
continue
if len(dataItem) == 21 and dataItem.endswith("_main.nwd"):
tHandle = theFolder[-1]+dataItem[:12]
newPath = path.join(self.projContent, tHandle+".nwd")
try:
rename(theFile, newPath)
logger.info("Moved file: %s" % theFile)
logger.info("New location: %s" % newPath)
except Exception as e:
logger.error(str(e))
errList.append("Could not move: %s" % theFile)
elif len(dataItem) == 21 and dataItem.endswith("_main.bak"):
try:
unlink(theFile)
logger.info("Deleted file: %s" % theFile)
except Exception as e:
logger.error(str(e))
errList.append("Could not delete: %s" % theFile)
else:
theErr = self._moveUnknownItem(theData, dataItem)
if theErr:
errList.append(theErr)
# Remove Data Folder
# ==================
try:
rmdir(theData)
logger.info("Removed folder: %s" % theFolder)
except:
errList.append("Failed to remove: %s" % theFolder)
return errList
def _moveUnknownItem(self, theDir, theItem):
"""Move an item that doesn't belong in the project folder to
a junk folder.
"""
theJunk = path.join(self.projPath, "junk")
if not self._checkFolder(theJunk):
return "Could not make folder: %s" % theJunk
theSrc = path.join(theDir, theItem)
theDst = path.join(theJunk, theItem)
try:
rename(theSrc, theDst)
logger.info("Moved to junk: %s" % theSrc)
except Exception as e:
logger.error(str(e))
return "Could not move item %s to junk." % theSrc
return ""
def _deprecatedFiles(self):
"""Delete files that are no longer used by novelWriter.
"""
rmList = []
rmList.append(path.join(self.projCache, "nwProject.nwx.0"))
rmList.append(path.join(self.projCache, "nwProject.nwx.1"))
rmList.append(path.join(self.projCache, "nwProject.nwx.2"))
rmList.append(path.join(self.projCache, "nwProject.nwx.3"))
rmList.append(path.join(self.projCache, "nwProject.nwx.4"))
rmList.append(path.join(self.projCache, "nwProject.nwx.5"))
rmList.append(path.join(self.projCache, "nwProject.nwx.6"))
rmList.append(path.join(self.projCache, "nwProject.nwx.7"))
rmList.append(path.join(self.projCache, "nwProject.nwx.8"))
rmList.append(path.join(self.projCache, "nwProject.nwx.9"))
rmList.append(path.join(self.projMeta, "mainOptions.json"))
rmList.append(path.join(self.projMeta, "exportOptions.json"))
rmList.append(path.join(self.projMeta, "outlineOptions.json"))
rmList.append(path.join(self.projMeta, "timelineOptions.json"))
rmList.append(path.join(self.projMeta, "docMergeOptions.json"))
rmList.append(path.join(self.projMeta, "sessionLogOptions.json"))
for rmFile in rmList:
if path.isfile(rmFile):
logger.info("Deleting: %s" % rmFile)
try:
unlink(rmFile)
except Exception as e:
logger.error(str(e))
return
# END Class NWProject
# ================================================================================================ #
# =============================================================================================== #
# NWTree
# Class holding the project tree for the NWProject
# ================================================================================================ #
# =============================================================================================== #
class NWTree():
@@ -1102,7 +1284,7 @@ class NWTree():
for tHandle in self._treeOrder:
tItem = self.__getitem__(tHandle)
tItem.packXML(xContent)
return
return
def unpackXML(self, xContent):
"""Iterate through all items of a content XML object and add
@@ -1120,6 +1302,51 @@ class NWTree():
return True
def writeToCFiles(self):
"""Write the convenience table of contents files in the root of
the project directory. These files are there to assist the user
if they wish to browse the stored files.
"""
tocText = path.join(self.theProject.projPath, nwFiles.TOC_TXT)
tocJson = path.join(self.theProject.projPath, nwFiles.TOC_JSON)
jsonData = []
try:
# Dump the text
with open(tocText, mode="w", encoding="utf8") as outFile:
outFile.write("\n")
outFile.write(" Table of Contents\n")
outFile.write("===================\n")
outFile.write("\n")
outFile.write(" %-25s %-9s %s\n" %("File Name","Class","Document Label"))
outFile.write("-"*80+"\n")
for tHandle in sorted(self._treeOrder):
tItem = self.__getitem__(tHandle)
if tItem is None:
continue
tFile = tHandle+".nwd"
if path.isfile(path.join(self.theProject.projContent, tFile)):
outFile.write(" %-25s %-9s %s\n" %(
path.join("content", tFile),
tItem.itemClass.name,
tItem.itemName,
))
jsonData.append([
path.join("content", tFile),
tItem.itemClass.name,
tItem.itemName,
])
outFile.write("\n")
# Dump the JSON
with open(tocJson, mode="w+", encoding="utf8") as outFile:
outFile.write(json.dumps(jsonData, indent=2))
except Exception as e:
logger.error(str(e))
return
##
# Tree Structure Methods
##
@@ -1356,10 +1583,10 @@ class NWTree():
# END Class NWTree
# ================================================================================================ #
# =============================================================================================== #
# NWItem
# Class holding the project items making up the NWProject
# ================================================================================================ #
# =============================================================================================== #
class NWItem():
@@ -1402,7 +1629,6 @@ class NWItem():
xSub = self._subPack(xPack,"type", text=str(self.itemType.name))
xSub = self._subPack(xPack,"class", text=str(self.itemClass.name))
xSub = self._subPack(xPack,"status", text=str(self.itemStatus))
xSub = self._subPack(xPack,"expanded", text=str(self.isExpanded))
if self.itemType == nwItemType.FILE:
xSub = self._subPack(xPack,"exported", text=str(self.isExported))
xSub = self._subPack(xPack,"layout", text=str(self.itemLayout.name))
@@ -1410,6 +1636,8 @@ class NWItem():
xSub = self._subPack(xPack,"wordCount", text=str(self.wordCount), none=False)
xSub = self._subPack(xPack,"paraCount", text=str(self.paraCount), none=False)
xSub = self._subPack(xPack,"cursorPos", text=str(self.cursorPos), none=False)
else:
xSub = self._subPack(xPack,"expanded", text=str(self.isExpanded))
return
def unpackXML(self, xItem):
@@ -1601,10 +1829,10 @@ class NWItem():
# END Class NWItem
# ================================================================================================ #
# =============================================================================================== #
# NWStatus
# Class holding the item status values stored in the NWProject
# ================================================================================================ #
# =============================================================================================== #
class NWStatus():
+6 -6
View File
@@ -35,9 +35,9 @@ from nw.constants import isoLanguage
logger = logging.getLogger(__name__)
# ================================================================================================ #
# =============================================================================================== #
# SpellChecking SuperClass
# ================================================================================================ #
# =============================================================================================== #
class NWSpellCheck():
@@ -129,9 +129,9 @@ class NWSpellCheck():
# END Class NWSpellCheck
# ================================================================================================ #
# =============================================================================================== #
# Enchant Based SpellChecking
# ================================================================================================ #
# =============================================================================================== #
class NWSpellEnchant(NWSpellCheck):
@@ -211,9 +211,9 @@ class NWSpellEnchantDummy:
# END Class NWSpellEnchantDummy
# ================================================================================================ #
# =============================================================================================== #
# Fallback SpellChecking Using difflib
# ================================================================================================ #
# =============================================================================================== #
class NWSpellSimple(NWSpellCheck):
"""Internal spell check tool that uses standard Python packages with
+21 -16
View File
@@ -143,7 +143,7 @@ class ToHtml(Tokenizer):
parStyle = None
tmpResult = []
hasHardBreak = False
for tType, tText, tFormat, tStyle in self.theTokens:
for tType, tLine, tText, tFormat, tStyle in self.theTokens:
# Styles
aStyle = []
@@ -174,6 +174,11 @@ class ToHtml(Tokenizer):
else:
hStyle = ""
if self.linkHeaders:
aNm = "<a name='head_%s:T%06d'></a>" % (self.theHandle, tLine)
else:
aNm = ""
# Process TextType
if tType == self.T_EMPTY:
if parStyle is None:
@@ -191,23 +196,23 @@ class ToHtml(Tokenizer):
elif tType == self.T_TITLE:
tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<h1 class='title'%s>%s</h1>\n" % (hStyle, tHead))
tmpResult.append("<h1 class='title'%s>%s%s</h1>\n" % (hStyle, aNm, tHead))
elif tType == self.T_HEAD1:
tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<%s%s>%s</%s>\n" % (h1, hStyle, tHead, h1))
tmpResult.append("<%s%s>%s%s</%s>\n" % (h1, hStyle, aNm, tHead, h1))
elif tType == self.T_HEAD2:
tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<%s%s>%s</%s>\n" % (h2, hStyle, tHead, h2))
tmpResult.append("<%s%s>%s%s</%s>\n" % (h2, hStyle, aNm, tHead, h2))
elif tType == self.T_HEAD3:
tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<%s%s>%s</%s>\n" % (h3, hStyle, tHead, h3))
tmpResult.append("<%s%s>%s%s</%s>\n" % (h3, hStyle, aNm, tHead, h3))
elif tType == self.T_HEAD4:
tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<%s%s>%s</%s>\n" % (h4, hStyle, tHead, h4))
tmpResult.append("<%s%s>%s%s</%s>\n" % (h4, hStyle, aNm, tHead, h4))
elif tType == self.T_SEP:
tmpResult.append("<p class='sep'>%s</p>\n" % tText)
@@ -296,17 +301,17 @@ class ToHtml(Tokenizer):
refTags = []
if theBits[0] in nwLabels.KEY_NAME:
retText += "<span class='tags'>%s:</span>&nbsp;" % nwLabels.KEY_NAME[theBits[0]]
if self.genMode == self.M_PREVIEW:
for tTag in theBits[1:]:
refTags.append("<a href='#%s=%s'>%s</a>" % (
theBits[0][1:], tTag, tTag
))
retText += ", ".join(refTags)
if theBits[0] == nwKeyWords.TAG_KEY:
retText += "<a name='tag_%s'>%s</a>" % (
theBits[1], theBits[1]
)
else:
if theBits[0] == nwKeyWords.TAG_KEY:
retText += "<a name='tag_%s'/>%s" % (
theBits[1], theBits[1]
)
if self.genMode == self.M_PREVIEW:
for tTag in theBits[1:]:
refTags.append("<a href='#%s=%s'>%s</a>" % (
theBits[0][1:], tTag, tTag
))
retText += ", ".join(refTags)
else:
for tTag in theBits[1:]:
refTags.append("<a href='#tag_%s'>%s</a>" % (
+144 -51
View File
@@ -102,6 +102,8 @@ class Tokenizer():
self.hideScene = False # Do not include scene headers
self.hideSection = False # Do not include section headers
self.linkHeaders = False # Add an anchor before headers
# Instance Variables
self.numChapter = 0 # Counter for chapter numbers
self.numChScene = 0 # Counter for scene number within chapter
@@ -174,6 +176,10 @@ class Tokenizer():
self.hideSection = hideSection
return
def setLinkHeaders(self, linkHeaders):
self.linkHeaders = linkHeaders
return
def setBodyText(self, doBodyText):
self.doBodyText = doBodyText
return
@@ -307,12 +313,16 @@ class Tokenizer():
self.theTokens = []
self.theMarkdown = ""
tmpMarkdown = []
nLine = 0
for aLine in self.theText.splitlines():
nLine += 1
# Tag lines starting with specific characters
if len(aLine.strip()) == 0:
self.theTokens.append((
self.T_EMPTY, "", None, self.A_NONE
self.T_EMPTY, nLine,
"", None,
self.A_NONE
))
tmpMarkdown.append("\n")
@@ -320,45 +330,59 @@ class Tokenizer():
cLine = aLine[1:].strip()
if cLine.lower().startswith("synopsis:"):
self.theTokens.append((
self.T_SYNOPSIS, cLine[9:].strip(), None, self.A_NONE
self.T_SYNOPSIS, nLine,
cLine[9:].strip(), None,
self.A_NONE
))
if self.doSynopsis:
tmpMarkdown.append("%s\n" % aLine)
else:
self.theTokens.append((
self.T_COMMENT, aLine[1:].strip(), None, self.A_NONE
self.T_COMMENT, nLine,
aLine[1:].strip(), None,
self.A_NONE
))
if self.doComments:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[0] == "@":
self.theTokens.append((
self.T_KEYWORD, aLine[1:].strip(), None, self.A_NONE
self.T_KEYWORD, nLine,
aLine[1:].strip(), None,
self.A_NONE
))
if self.doKeywords:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:2] == "# ":
self.theTokens.append((
self.T_HEAD1, aLine[2:].strip(), None, self.A_NONE
self.T_HEAD1, nLine,
aLine[2:].strip(), None,
self.A_NONE
))
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:3] == "## ":
self.theTokens.append((
self.T_HEAD2, aLine[3:].strip(), None, self.A_NONE
self.T_HEAD2, nLine,
aLine[3:].strip(), None,
self.A_NONE
))
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:4] == "### ":
self.theTokens.append((
self.T_HEAD3, aLine[4:].strip(), None, self.A_NONE
self.T_HEAD3, nLine,
aLine[4:].strip(), None,
self.A_NONE
))
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:5] == "#### ":
self.theTokens.append((
self.T_HEAD4, aLine[5:].strip(), None, self.A_NONE
self.T_HEAD4, nLine,
aLine[5:].strip(), None,
self.A_NONE
))
tmpMarkdown.append("%s\n" % aLine)
@@ -383,13 +407,17 @@ class Tokenizer():
# sorted by position
fmtPos = sorted(fmtPos, key=itemgetter(0))
self.theTokens.append((
self.T_TEXT, aLine, fmtPos, self.A_NONE
self.T_TEXT, nLine,
aLine, fmtPos,
self.A_NONE
))
tmpMarkdown.append("%s\n" % aLine)
# Always add an empty line at the end
self.theTokens.append((
self.T_EMPTY, "", None, self.A_NONE
self.T_EMPTY, nLine,
"", None,
self.A_NONE
))
tmpMarkdown.append("\n")
@@ -413,100 +441,146 @@ class Tokenizer():
for n in range(len(self.theTokens)):
tToken = self.theTokens[n]
tType = tToken[0]
tText = tToken[1]
# In case we see text before a scene, we reset the flag
if tType == self.T_TEXT:
if tToken[0] == self.T_TEXT:
self.firstScene = False
elif tType == self.T_HEAD1:
elif tToken[0] == self.T_HEAD1:
# Main Title
# ==========
tText = self._formatHeading(self.fmtTitle, tText)
tTemp = self._formatHeading(self.fmtTitle, tToken[2])
self.theTokens[n] = (
tType, tText, None, self.A_NONE
tToken[0],
tToken[1],
tTemp,
None,
self.A_NONE
)
elif tType == self.T_HEAD2:
elif tToken[0] == self.T_HEAD2:
# Novel Chapter
# =============
# Numbered or Unnumbered
if self.isUnNum:
tText = self._formatHeading(self.fmtUnNum, tText)
tTemp = self._formatHeading(self.fmtUnNum, tToken[2])
else:
self.numChapter += 1
tText = self._formatHeading(self.fmtChapter, tText)
tTemp = self._formatHeading(self.fmtChapter, tToken[2])
# Format the chapter header
self.theTokens[n] = (
tType, tText, None, self.A_PBB
tToken[0],
tToken[1],
tTemp,
None,
self.A_PBB
)
# Set scene variables
self.firstScene = True
self.numChScene = 0
elif tType == self.T_HEAD3:
elif tToken[0] == self.T_HEAD3:
# Novel Scene
# ===========
self.numChScene += 1
self.numAbsScene += 1
tTemp = self._formatHeading(self.fmtScene, tText)
tTemp = self._formatHeading(self.fmtScene, tToken[2])
if tTemp == "" and self.hideScene:
self.theTokens[n] = (
self.T_EMPTY, "", None, self.A_NONE
self.T_EMPTY,
tToken[1],
"",
None,
self.A_NONE
)
elif tTemp == "" and not self.hideScene:
if self.firstScene:
self.theTokens[n] = (
self.T_EMPTY, "", None, self.A_NONE
self.T_EMPTY,
tToken[1],
"",
None,
self.A_NONE
)
else:
self.theTokens[n] = (
self.T_SKIP, "", None, self.A_NONE
self.T_SKIP,
tToken[1],
"",
None,
self.A_NONE
)
elif tTemp == self.fmtScene:
if self.firstScene:
self.theTokens[n] = (
self.T_EMPTY, "", None, self.A_NONE
self.T_EMPTY,
tToken[1],
"",
None,
self.A_NONE
)
else:
self.theTokens[n] = (
self.T_SEP, tTemp, None, self.A_CENTRE
self.T_SEP,
tToken[1],
tTemp,
None,
self.A_CENTRE
)
else:
self.theTokens[n] = (
tType, tTemp, None, self.A_NONE
tToken[0],
tToken[1],
tTemp,
None,
self.A_NONE
)
# Definitely no longer the first scene
self.firstScene = False
elif tType == self.T_HEAD4:
elif tToken[0] == self.T_HEAD4:
# Novel Section
# =============
tTemp = self._formatHeading(self.fmtSection, tText)
tTemp = self._formatHeading(self.fmtSection, tToken[2])
if tTemp == "" and self.hideSection:
self.theTokens[n] = (
self.T_EMPTY, "", None, self.A_NONE
self.T_EMPTY,
tToken[1],
"",
None,
self.A_NONE
)
elif tTemp == "" and not self.hideSection:
self.theTokens[n] = (
self.T_SKIP, "", None, self.A_NONE
self.T_SKIP,
tToken[1],
"",
None,
self.A_NONE
)
elif tTemp == self.fmtSection:
self.theTokens[n] = (
self.T_SEP, tTemp, None, self.A_CENTRE
self.T_SEP,
tToken[1],
tTemp,
None,
self.A_CENTRE
)
else:
self.theTokens[n] = (
tType, tTemp, None, self.A_NONE
tToken[0],
tToken[1],
tTemp,
None,
self.A_NONE
)
# For title page and partitions, we need to centre all text.
@@ -515,21 +589,30 @@ class Tokenizer():
# We also swap header level 1 with a title type instead.
if self.isTitle or self.isPart:
for n, tToken in enumerate(self.theTokens):
tType = tToken[0]
tText = tToken[1]
tFormat = tToken[2]
if tType == self.T_HEAD1:
if tToken[0] == self.T_HEAD1:
if self.isTitle:
self.theTokens[n] = (
self.T_TITLE, tText, tFormat, self.A_PBB_NO | self.A_CENTRE
self.T_TITLE,
tToken[1],
tToken[2],
tToken[3],
self.A_PBB_NO | self.A_CENTRE
)
else:
self.theTokens[n] = (
tType, tText, tFormat, self.A_PBB | self.A_CENTRE
tToken[0],
tToken[1],
tToken[2],
tToken[3],
self.A_PBB | self.A_CENTRE
)
else:
self.theTokens[n] = (
tType, tText, tFormat, self.A_CENTRE
tToken[0],
tToken[1],
tToken[2],
tToken[3],
self.A_CENTRE
)
# Add a page break after the last entry
@@ -537,23 +620,32 @@ class Tokenizer():
if n >= 0:
tToken = self.theTokens[n]
self.theTokens[n] = (
tToken[0], tToken[1], tToken[2], tToken[3] | self.A_PBA
tToken[0],
tToken[1],
tToken[2],
tToken[3],
tToken[4] | self.A_PBA
)
# A single page is always left-aligned and starts on a fresh
# page, unless it's empty.
if self.isPage:
for n, tToken in enumerate(self.theTokens):
tType = tToken[0]
tText = tToken[1]
tFormat = tToken[2]
if n == 0:
self.theTokens[n] = (
tType, tText, tFormat, self.A_LEFT | self.A_PBB
tToken[0],
tToken[1],
tToken[2],
tToken[3],
self.A_LEFT | self.A_PBB
)
else:
self.theTokens[n] = (
tType, tText, tFormat, self.A_LEFT
tToken[0],
tToken[1],
tToken[2],
tToken[3],
self.A_LEFT
)
return
@@ -566,10 +658,11 @@ class Tokenizer():
"""Replaces the %keyword% strings.
"""
theTitle = theTitle.replace(r"%title%", theText)
theTitle = theTitle.replace(r"%ch%", str(self.numChapter))
theTitle = theTitle.replace(r"%sc%", str(self.numChScene))
theTitle = theTitle.replace(r"%sca%", str(self.numAbsScene))
theTitle = theTitle.replace(r"%chw%", numberToWord(self.numChapter,"en"))
theTitle = theTitle.replace(r"%ch%", str(self.numChapter))
theTitle = theTitle.replace(r"%sc%", str(self.numChScene))
theTitle = theTitle.replace(r"%sca%", str(self.numAbsScene))
if r"%chw%" in theTitle:
theTitle = theTitle.replace(r"%chw%", numberToWord(self.numChapter,"en"))
return theTitle
# END Class Tokenizer
-45
View File
@@ -8,7 +8,6 @@
File History:
Created: 2019-04-22 [0.0.1] countWords
Created: 2019-10-13 [0.2.3] numberToWord, _numberToWordEN
Created: 2020-02-13 [0.4.3] projectMaintenance
Merged: 2020-05-08 [0.4.5] All of the above into this file
This file is a part of novelWriter
@@ -81,50 +80,6 @@ def countWords(theText):
return charCount, wordCount, paraCount
def projectMaintenance(theProject):
"""Wrapper class for handling various tasks related to managing old
projects with content from older versions of novelWriter.
"""
# Remove no longer used project cache folder
if path.isdir(theProject.projPath):
cacheDir = path.join(theProject.projPath, "cache")
if path.isdir(cacheDir):
logger.info("Deprecated cache folder found")
rmList = []
for i in range(10):
rmList.append(path.join(cacheDir, "nwProject.nwx.%d" % i))
rmList.append(path.join(cacheDir, "projCount.txt"))
for rmFile in rmList:
if path.isfile(rmFile):
logger.info("Deleting: %s" % rmFile)
try:
unlink(rmFile)
except Exception as e:
logger.error(str(e))
logger.info("Deleting: %s" % cacheDir)
try:
rmdir(cacheDir)
except Exception as e:
logger.error(str(e))
# Remove no longer used meta files
rmList = []
rmList.append(path.join(theProject.projMeta, "mainOptions.json"))
rmList.append(path.join(theProject.projMeta, "exportOptions.json"))
rmList.append(path.join(theProject.projMeta, "outlineOptions.json"))
rmList.append(path.join(theProject.projMeta, "timelineOptions.json"))
rmList.append(path.join(theProject.projMeta, "docMergeOptions.json"))
rmList.append(path.join(theProject.projMeta, "sessionLogOptions.json"))
for rmFile in rmList:
if path.isfile(rmFile):
logger.info("Deleting: %s" % rmFile)
try:
unlink(rmFile)
except Exception as e:
logger.error(str(e))
return
def numberToWord(numVal, theLanguage):
"""Wrapper for converting numbers to words for chapter headings.
"""
+2 -2
View File
@@ -9,7 +9,7 @@ from nw.gui.theme import GuiTheme
# Dialogs
from nw.gui.dialogs.about import GuiAbout
from nw.gui.dialogs.configeditor import GuiConfigEditor
from nw.gui.dialogs.preferences import GuiPreferences
from nw.gui.dialogs.docmerge import GuiDocMerge
from nw.gui.dialogs.docsplit import GuiDocSplit
from nw.gui.dialogs.itemeditor import GuiItemEditor
@@ -40,7 +40,7 @@ __all__ = [
"GuiMainStatus",
"GuiTheme",
"GuiAbout",
"GuiConfigEditor",
"GuiPreferences",
"GuiDocMerge",
"GuiDocSplit",
"GuiItemEditor",
+33 -24
View File
@@ -34,12 +34,12 @@ from time import time
from PyQt5.QtCore import Qt, QByteArray
from PyQt5.QtPrintSupport import QPrinter, QPrintPreviewDialog
from PyQt5.QtGui import (
QTextOption, QPalette, QColor, QTextDocumentWriter, QFont
QPalette, QColor, QTextDocumentWriter, QFont
)
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QTextBrowser, QPushButton, QLabel,
QLineEdit, QGroupBox, QGridLayout, QProgressBar, QMenu, QAction,
QFileDialog, QFontComboBox, QSpinBox, QDialogButtonBox
QFileDialog, QFontComboBox, QSpinBox
)
from nw.gui.additions import QSwitch
@@ -83,8 +83,7 @@ class GuiBuildNovel(QDialog):
self.optState.getInt("GuiBuildNovel", "winHeight", 800)
)
self.outerBox = QVBoxLayout()
self.innerBox = QHBoxLayout()
self.outerBox = QHBoxLayout()
self.toolsBox = QVBoxLayout()
self.docView = GuiBuildNovelDocView(self, self.theProject)
@@ -173,7 +172,9 @@ class GuiBuildNovel(QDialog):
self.textFont = QFontComboBox()
self.textFont.setFixedWidth(220)
self.textFont.setToolTip("The font is used for PDF and printing. Other formats have no font set.")
self.textFont.setToolTip(
"The font is used for PDF and printing. Other formats have no font set."
)
self.textFont.setCurrentFont(
QFont(self.optState.getString("GuiBuildNovel", "textFont", self.mainConf.textFont))
)
@@ -183,13 +184,17 @@ class GuiBuildNovel(QDialog):
self.textSize.setMinimum(5)
self.textSize.setMaximum(48)
self.textSize.setSingleStep(1)
self.textSize.setToolTip("The size is used for PDF and printing. Other formats have no size set.")
self.textSize.setToolTip(
"The size is used for PDF and printing. Other formats have no size set."
)
self.textSize.setValue(
self.optState.getInt("GuiBuildNovel", "textSize", self.mainConf.textSize)
)
self.justifyText = QSwitch()
self.justifyText.setToolTip("Applies to PDF, printing, HTML, and Open Document exports.")
self.justifyText.setToolTip(
"Applies to PDF, printing, HTML, and Open Document exports."
)
self.justifyText.setChecked(
self.optState.getBool("GuiBuildNovel", "justifyText", False)
)
@@ -211,15 +216,21 @@ class GuiBuildNovel(QDialog):
self.includeGroup.setLayout(self.includeForm)
self.includeSynopsis = QSwitch()
self.includeSynopsis.setToolTip("Include synopsis type comments in the output.")
self.includeSynopsis.setToolTip(
"Include synopsis type comments in the output."
)
self.includeSynopsis.setChecked(self.theProject.titleFormat["withSynopsis"])
self.includeComments = QSwitch()
self.includeComments.setToolTip("Include plain comments in the output.")
self.includeComments.setToolTip(
"Include plain comments in the output."
)
self.includeComments.setChecked(self.theProject.titleFormat["withComments"])
self.includeKeywords = QSwitch()
self.includeKeywords.setToolTip("Include meta keywords (tags, references) in the output.")
self.includeKeywords.setToolTip(
"Include meta keywords (tags, references) in the output."
)
self.includeKeywords.setChecked(self.theProject.titleFormat["withKeywords"])
self.includeForm.addWidget(QLabel("Include synopsis"), 0, 0, 1, 1, Qt.AlignLeft)
@@ -292,7 +303,7 @@ class GuiBuildNovel(QDialog):
# Action Buttons
# ==============
self.buttonForm = QGridLayout()
self.buttonBox = QHBoxLayout()
self.btnPrint = QPushButton("Print")
self.btnPrint.clicked.connect(self._printDocument)
@@ -326,12 +337,13 @@ class GuiBuildNovel(QDialog):
self.saveTXT.triggered.connect(lambda: self._saveDocument(self.FMT_TXT))
self.saveMenu.addAction(self.saveTXT)
self.buttonForm.addWidget(self.btnSave, 0, 0)
self.buttonForm.addWidget(self.btnPrint, 0, 1)
self.btnClose = QPushButton("Close")
self.btnClose.clicked.connect(self._doClose)
# Buttons
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close)
self.buttonBox.rejected.connect(self._doClose)
self.buttonBox.addWidget(self.btnSave)
self.buttonBox.addWidget(self.btnPrint)
self.buttonBox.addWidget(self.btnClose)
self.buttonBox.setSpacing(4)
# Assemble GUI
# ============
@@ -343,18 +355,15 @@ class GuiBuildNovel(QDialog):
self.toolsBox.addWidget(self.buildProgress)
self.toolsBox.addWidget(self.buildNovel)
self.toolsBox.addSpacing(8)
self.toolsBox.addLayout(self.buttonForm)
self.toolsBox.addLayout(self.buttonBox)
self.innerBox.addLayout(self.toolsBox)
self.innerBox.addWidget(self.docView)
self.outerBox.addLayout(self.toolsBox)
self.outerBox.addWidget(self.docView)
self.outerBox.setStretch(0, 0)
self.outerBox.setStretch(1, 1)
self.outerBox.addLayout(self.innerBox)
self.outerBox.addWidget(self.buttonBox)
self.setLayout(self.outerBox)
self.innerBox.setStretch(0, 0)
self.innerBox.setStretch(1, 1)
self.show()
logger.debug("GuiBuildNovel initialisation complete")
+2 -2
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from nw.gui.dialogs.about import GuiAbout
from nw.gui.dialogs.configeditor import GuiConfigEditor
from nw.gui.dialogs.preferences import GuiPreferences
from nw.gui.dialogs.docmerge import GuiDocMerge
from nw.gui.dialogs.docsplit import GuiDocSplit
from nw.gui.dialogs.itemeditor import GuiItemEditor
@@ -11,7 +11,7 @@ from nw.gui.dialogs.sessionlog import GuiSessionLogView
__all__ = [
"GuiAbout",
"GuiConfigEditor",
"GuiPreferences",
"GuiDocMerge",
"GuiDocSplit",
"GuiItemEditor",
+13 -10
View File
@@ -119,16 +119,19 @@ class GuiAbout(QDialog):
"<h2>About {name:s}</h2>"
"<p>{copyright:s}.</p>"
"<p>Website: <a href='{website:s}'>{domain:s}</a></p>"
"<p>{name:s} is a markdown-like text editor designed for organising and writing "
"novels. It is written in Python 3 with a Qt5 GUI, using PyQt5.</p>"
"<p>{name:s} is free software: you can redistribute it and/or modify it under the "
"terms of the GNU General Public License as published by the Free Software Foundation, "
"either version 3 of the License, or (at your option) any later version.</p>"
"<p>{name:s} is distributed in the hope that it will be useful, but WITHOUT ANY "
"WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A "
"PARTICULAR PURPOSE.</p>"
"<p>See the License tab for the full text, or visit the GNU website at "
"<a href='https://www.gnu.org/licenses/gpl-3.0.html'>GPL v3.0</a> for more details.</p>"
"<p>{name:s} is a markdown-like text editor designed for "
"organising and writing novels. It is written in Python 3 with a "
"Qt5 GUI, using PyQt5.</p>"
"<p>{name:s} is free software: you can redistribute it and/or "
"modify it under the terms of the GNU General Public License as "
"published by the Free Software Foundation, either version 3 of "
"the License, or (at your option) any later version.</p>"
"<p>{name:s} is distributed in the hope that it will be useful, "
"but WITHOUT ANY WARRANTY; without even the implied warranty of "
"MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.</p>"
"<p>See the License tab for the full text, or visit the GNU website "
"at <a href='https://www.gnu.org/licenses/gpl-3.0.html'>GPL v3.0</a> "
"for more details.</p>"
"<h3>Credits</h3>"
"<p>{credits:s}</p>"
).format(
+10 -4
View File
@@ -150,7 +150,9 @@ class GuiDocSplit(QDialog):
), nwAlert.ERROR)
return
fHandle = self.theProject.newFolder(srcItem.itemName, srcItem.itemClass, srcItem.parHandle)
fHandle = self.theProject.newFolder(
srcItem.itemName, srcItem.itemClass, srcItem.parHandle
)
self.theParent.treeView.revealTreeItem(fHandle)
logger.verbose("Creating folder %s" % fHandle)
@@ -165,7 +167,7 @@ class GuiDocSplit(QDialog):
elif wTitle.startswith("### "):
itemLayout = nwItemLayout.SCENE
elif wTitle.startswith("#### "):
itemLayout = nwItemLayout.PAGE
itemLayout = nwItemLayout.SCENE
wTitle = wTitle.lstrip("#")
wTitle = wTitle.strip()
@@ -174,7 +176,9 @@ class GuiDocSplit(QDialog):
newItem = self.theProject.projTree[nHandle]
newItem.setLayout(itemLayout)
logger.verbose(
"Creating new document %s with text from line %d to %d" % (nHandle, iStart, iEnd-1)
"Creating new document %s with text from line %d to %d" % (
nHandle, iStart, iEnd-1
)
)
theText = "\n".join(theLines[iStart:iEnd])
@@ -227,7 +231,9 @@ class GuiDocSplit(QDialog):
spLevel = self.splitLevel.currentData()
self.optState.setValue("GuiDocSplit", "spLevel", spLevel)
logger.debug("Scanning document %s for headings level <= %d" % (self.sourceItem, spLevel))
logger.debug(
"Scanning document %s for headings level <= %d" % (self.sourceItem, spLevel)
)
lineNo = 0
for aLine in theText.splitlines():
@@ -44,12 +44,12 @@ from nw.constants import nwAlert, nwQuotes
logger = logging.getLogger(__name__)
class GuiConfigEditor(PagedDialog):
class GuiPreferences(PagedDialog):
def __init__(self, theParent, theProject):
PagedDialog.__init__(self, theParent)
logger.debug("Initialising ConfigEditor ...")
logger.debug("Initialising GuiPreferences ...")
self.mainConf = nw.CONFIG
self.theParent = theParent
@@ -74,7 +74,7 @@ class GuiConfigEditor(PagedDialog):
self.show()
logger.debug("ConfigEditor initialisation complete")
logger.debug("GuiPreferences initialisation complete")
return
@@ -122,7 +122,7 @@ class GuiConfigEditor(PagedDialog):
self.close()
return
# END Class GuiConfigEditor
# END Class GuiPreferences
class GuiConfigEditGeneralTab(QWidget):
@@ -228,7 +228,7 @@ class GuiConfigEditGeneralTab(QWidget):
## Backup Path
self.backupPath = self.mainConf.backupPath
self.backupGetPath = QPushButton(self.theTheme.getIcon("folder-open"),"Select Folder")
self.backupGetPath = QPushButton("Browse")
self.backupGetPath.clicked.connect(self._backupFolder)
self.backupPathRow = self.mainForm.addRow(
"Backup storage location",
+3 -1
View File
@@ -362,7 +362,9 @@ class GuiProjectEditStatus(QWidget):
self.listBox.takeItem(iRow)
self.colChanged = True
else:
self.theParent.makeAlert("Cannot delete status item that is in use.",nwAlert.ERROR)
self.theParent.makeAlert(
"Cannot delete status item that is in use.", nwAlert.ERROR
)
return
def _saveItem(self):
+14 -6
View File
@@ -180,11 +180,15 @@ class GuiSessionLogView(QDialog):
inData = inLine.split()
if len(inData) != 8:
continue
dStart = datetime.strptime("%s %s" % (inData[1],inData[2]), nwConst.tStampFmt)
dEnd = datetime.strptime("%s %s" % (inData[4],inData[5]), nwConst.tStampFmt)
dStart = datetime.strptime(
"%s %s" % (inData[1],inData[2]), nwConst.tStampFmt
)
dEnd = datetime.strptime(
"%s %s" % (inData[4],inData[5]), nwConst.tStampFmt
)
nWords = int(inData[7])
tDiff = dEnd - dStart
sDiff = tDiff.total_seconds()
tDiff = dEnd - dStart
sDiff = tDiff.total_seconds()
self.timeTotal += sDiff
if abs(nWords) > 0:
@@ -196,7 +200,9 @@ class GuiSessionLogView(QDialog):
if hideNegative and nWords < 0:
continue
newItem = QTreeWidgetItem([str(dStart),self._formatTime(sDiff),str(nWords),""])
newItem = QTreeWidgetItem(
[str(dStart), self._formatTime(sDiff), str(nWords), ""]
)
newItem.setTextAlignment(1,Qt.AlignRight)
newItem.setTextAlignment(2,Qt.AlignRight)
@@ -208,7 +214,9 @@ class GuiSessionLogView(QDialog):
self.listBox.addTopLevelItem(newItem)
except Exception as e:
self.theParent.makeAlert(["Failed to read session log file.",str(e)], nwAlert.ERROR)
self.theParent.makeAlert(
["Failed to read session log file.",str(e)], nwAlert.ERROR
)
return False
self.labelFilter.setText(self._formatTime(self.timeFilter))
+13 -4
View File
@@ -405,6 +405,8 @@ class GuiDocEditor(QTextEdit):
def setCursorPosition(self, thePosition):
"""Move the cursor to a given position in the document.
"""
if not isinstance(thePosition, int):
return False
if thePosition >= 0:
theCursor = self.textCursor()
theCursor.setPosition(thePosition)
@@ -420,12 +422,13 @@ class GuiDocEditor(QTextEdit):
def setCursorLine(self, theLine):
"""Move the cursor to a given line in the document.
"""
if theLine is None:
if not isinstance(theLine, int):
return False
if theLine >= 0:
theBlock = self.qDocument.findBlockByLineNumber(theLine)
if theBlock:
self.setCursorPosition(theBlock.position())
logger.verbose("Cursor moved to line %d" % theLine)
return True
##
@@ -480,7 +483,9 @@ class GuiDocEditor(QTextEdit):
self.hLight.rehighlight()
qApp.restoreOverrideCursor()
afTime = time()
logger.debug("Document re-highlighted in %.3f milliseconds" % (1000*(afTime-bfTime)))
logger.debug(
"Document re-highlighted in %.3f milliseconds" % (1000*(afTime-bfTime))
)
return True
@@ -717,7 +722,9 @@ class GuiDocEditor(QTextEdit):
"""
sinceActive = time()-self.lastEdit
if sinceActive > 5*self.wcInterval:
logger.debug("Stopping word count timer: no activity last %.1f seconds" % sinceActive)
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")
@@ -953,7 +960,9 @@ class GuiDocEditor(QTextEdit):
theText = newText
cOffset -= 0
else:
logger.error("Unknown or unsupported block format requested: %s" % str(docAction))
logger.error(
"Unknown or unsupported block format requested: %s" % str(docAction)
)
return
# Replace the block text
+2 -2
View File
@@ -396,7 +396,7 @@ class GuiDocTree(QTreeWidget):
trItemP.takeChild(tIndex)
del self.theProject.projTree[tHandle]
else:
self.makeAlert(["Cannot delete folder.","It is not empty."], nwAlert.ERROR)
self.makeAlert("Cannot delete folder. It is not empty.", nwAlert.ERROR)
return False
elif nwItemS.itemType == nwItemType.ROOT:
@@ -407,7 +407,7 @@ class GuiDocTree(QTreeWidget):
self.theParent.mainMenu.setAvailableRoot()
self.theProject.setProjectChanged(True)
else:
self.makeAlert(["Cannot delete root folder.","It is not empty."], nwAlert.ERROR)
self.makeAlert("Cannot delete root folder. It is not empty.", nwAlert.ERROR)
return False
return True
+50 -29
View File
@@ -28,7 +28,7 @@
import logging
import nw
from PyQt5.QtCore import Qt
from PyQt5.QtCore import Qt, QUrl
from PyQt5.QtWidgets import QTextBrowser
from PyQt5.QtGui import QTextOption, QFont, QPalette, QColor, QTextCursor
@@ -134,6 +134,7 @@ class GuiDocViewer(QTextBrowser):
sPos = self.verticalScrollBar().value()
aDoc = ToHtml(self.theProject, self.theParent)
aDoc.setPreview(True, self.mainConf.viewComments)
aDoc.setLinkHeaders(True)
aDoc.setText(tHandle)
aDoc.doAutoReplace()
aDoc.tokenizeText()
@@ -164,19 +165,17 @@ class GuiDocViewer(QTextBrowser):
index being up to date.
"""
logger.debug("Loading document from tag '%s'" % theTag)
if theTag in self.theParent.theIndex.tagIndex.keys():
theTarget = self.theParent.theIndex.tagIndex[theTag]
tHandle, onLine, sTitle = self.theParent.theIndex.getTagSource(theTag)
if tHandle is None:
self.theParent.makeAlert((
"Could not find the reference for tag '%s'. It either doesn't "
"exist, or the index is out of date. The index can be updated "
"from the Tools menu, or by pressing F9."
) % theTag, nwAlert.ERROR)
return
else:
logger.debug("The tag was not found in the index")
return False
if len(theTarget) != 3:
# Just to make sure the index is not messed up
return False
self.loadText(theTarget[1])
self.loadText(tHandle)
self.navigateTo("#head_%s:%s" % (tHandle, sTitle))
return True
def docAction(self, theAction):
@@ -200,6 +199,15 @@ class GuiDocViewer(QTextBrowser):
return False
return True
def navigateTo(self, navLink):
"""Go to a specific #link in the document.
"""
if not isinstance(navLink, str):
return False
if navLink.startswith("#"):
self.setSource(QUrl(navLink))
return True
def updateDocMargins(self):
"""Automatically adjust the margins so the text is centred if
Config.textFixedW is enabled or we're in Zen mode. Otherwise,
@@ -233,6 +241,33 @@ class GuiDocViewer(QTextBrowser):
self.updateDocMargins()
return
##
# Setters
##
def setCursorPosition(self, thePosition):
"""Move the cursor to a given position in the document.
"""
if not isinstance(thePosition, int):
return False
if thePosition >= 0:
theCursor = self.textCursor()
theCursor.setPosition(thePosition)
self.setTextCursor(theCursor)
return True
def setCursorLine(self, theLine):
"""Move the cursor to a given line in the document.
"""
if not isinstance(theLine, int):
return False
if theLine >= 0:
theBlock = self.qDocument.findBlockByLineNumber(theLine)
if theBlock:
self.setCursorPosition(theBlock.position())
logger.verbose("Cursor moved to line %d" % theLine)
return True
##
# Events
##
@@ -262,25 +297,11 @@ class GuiDocViewer(QTextBrowser):
"""Slot for a link in the document being clicked.
"""
theLink = theURL.url()
tHandle = None
onLine = 0
theTag = ""
logger.verbose("Clicked link: '%s'" % theLink)
if len(theLink) > 0:
theBits = theLink.split("=")
if len(theBits) == 2:
theTag = theBits[1]
tHandle, onLine = self.theParent.theIndex.getTagSource(theBits[1])
if tHandle is None:
self.theParent.makeAlert((
"Could not find the reference for tag '%s'. It either doesn't exist, or the index "
"is out of date. The index can be updated from the Tools menu.") % theTag,
nwAlert.ERROR
)
return
else:
self.loadText(tHandle)
self.loadFromTag(theBits[1])
return
def _makeStyleSheet(self):
+7 -4
View File
@@ -107,7 +107,9 @@ class GuiDocViewDetails(QWidget):
for tHandle in theRefs:
tItem = self.theProject.projTree[tHandle]
if tItem is not None:
theList.append("<a href='#tag=%s'>%s</a>" % (tHandle,tItem.itemName))
theList.append("<a href='#head_%s:%s'>%s</a>" % (
tHandle, theRefs[tHandle], tItem.itemName
))
self.refList.setText(", ".join(theList))
self.refList.adjustSize()
@@ -122,9 +124,10 @@ class GuiDocViewDetails(QWidget):
"""Capture the link-click and forward it to the document viewer
class for handling.
"""
if len(theLink) == 18:
tHandle = theLink[-13:]
self.theParent.viewDocument(tHandle)
logger.verbose("Clicked link: '%s'" % theLink)
if len(theLink) == 27:
tHandle = theLink[6:19]
self.theParent.viewDocument(tHandle, theLink)
return
def _doShowHide(self, chState):
+6 -2
View File
@@ -249,7 +249,9 @@ class GuiIcons:
try:
confParser.read_file(open(themeConf, mode="r", encoding="utf8"))
except Exception as e:
self.theParent.makeAlert(["Could not load theme config file.",str(e)],nwAlert.ERROR)
self.theParent.makeAlert(
["Could not load theme config file.",str(e)], nwAlert.ERROR
)
continue
themeName = ""
if confParser.has_section("Main"):
@@ -295,7 +297,9 @@ class GuiIcons:
# Finally. we check if we have a fallback icon
if self.mainConf.guiDark:
fbackIcon = path.join(self.mainConf.iconPath, self.fbackName, "%s-dark.svg" % iconKey)
fbackIcon = path.join(
self.mainConf.iconPath, self.fbackName, "%s-dark.svg" % iconKey
)
if path.isfile(fbackIcon):
logger.verbose("Loading icon '%s' from fallback theme" % iconKey)
return QIcon(fbackIcon)
+5 -1
View File
@@ -137,13 +137,17 @@ class GuiMainStatus(QStatusBar):
self._updateTime()
return True
##
# Setters
##
def setRefTime(self, theTime):
"""Set the reference time for the status bar clock.
"""
self.refTime = theTime
return
def setStatus(self, theMessage, timeOut=10.0):
def setStatus(self, theMessage, timeOut=20.0):
"""Set the status bar message to display for 'timeOut' seconds.
"""
self.showMessage(theMessage, int(timeOut*1000))
+6 -2
View File
@@ -284,7 +284,9 @@ class GuiTheme:
try:
confParser.read_file(open(themeConf, mode="r", encoding="utf8"))
except Exception as e:
self.theParent.makeAlert(["Could not load theme config file.",str(e)],nwAlert.ERROR)
self.theParent.makeAlert(
["Could not load theme config file.",str(e)], nwAlert.ERROR
)
continue
themeName = ""
if confParser.has_section("Main"):
@@ -314,7 +316,9 @@ class GuiTheme:
try:
confParser.read_file(open(syntaxPath, mode="r", encoding="utf8"))
except Exception as e:
self.theParent.makeAlert(["Could not load syntax file.",str(e)],nwAlert.ERROR)
self.theParent.makeAlert(
["Could not load syntax file.",str(e)], nwAlert.ERROR
)
return []
syntaxName = ""
if confParser.has_section("Main"):
+25 -19
View File
@@ -42,7 +42,7 @@ from PyQt5.QtWidgets import (
from nw.gui import (
GuiMainMenu, GuiMainStatus, GuiTheme, GuiDocTree, GuiDocEditor,
GuiDocViewer, GuiDocDetails, GuiSearchBar, GuiNoticeBar, GuiDocViewDetails,
GuiConfigEditor, GuiProjectSettings, GuiItemEditor, GuiProjectOutline,
GuiPreferences, GuiProjectSettings, GuiItemEditor, GuiProjectOutline,
GuiSessionLogView, GuiDocMerge, GuiDocSplit, GuiProjectLoad, GuiBuildNovel
)
from nw.core import NWProject, NWDoc, NWIndex
@@ -485,7 +485,7 @@ class GuiMain(QMainWindow):
self.docEditor.saveText()
return True
def viewDocument(self, tHandle=None):
def viewDocument(self, tHandle=None, navLink=None):
"""Load a document for viewing in the view panel.
"""
if tHandle is None:
@@ -503,13 +503,15 @@ class GuiMain(QMainWindow):
# Make sure main tab is in Editor view
self.tabWidget.setCurrentWidget(self.splitView)
if self.docViewer.loadText(tHandle) and not self.viewPane.isVisible():
bPos = self.splitMain.sizes()
self.viewPane.setVisible(True)
vPos = [0,0]
vPos[0] = int(bPos[1]/2)
vPos[1] = bPos[1]-vPos[0]
self.splitView.setSizes(vPos)
if self.docViewer.loadText(tHandle):
if not self.viewPane.isVisible():
bPos = self.splitMain.sizes()
self.viewPane.setVisible(True)
vPos = [0,0]
vPos[0] = int(bPos[1]/2)
vPos[1] = bPos[1]-vPos[0]
self.splitView.setSizes(vPos)
self.docViewer.navigateTo(navLink)
return True
@@ -551,7 +553,7 @@ class GuiMain(QMainWindow):
if self.docEditor.theHandle is None:
self.makeAlert(
["Please open a document to import the text file into."],
"Please open a document to import the text file into.",
nwAlert.ERROR
)
return False
@@ -666,6 +668,12 @@ class GuiMain(QMainWindow):
theDoc = NWDoc(self.theProject, self)
for nDone, tItem in enumerate(self.theProject.projTree):
if tItem is not None:
self.statusBar.setStatus("Indexing: '%s'" % tItem.itemName)
else:
self.statusBar.setStatus("Indexing: Unknown item")
if tItem is not None and tItem.itemType == nwItemType.FILE:
logger.verbose("Scanning: %s" % tItem.itemName)
theText = theDoc.openDocument(tItem.itemHandle, showStatus=False)
@@ -681,16 +689,14 @@ class GuiMain(QMainWindow):
self.treeView.propagateCount(tItem.itemHandle, wC)
self.treeView.projectWordCount()
self.statusBar.setStatus("Building index: %.2f%%" % (100.0*(nDone + 1)/nItems))
self.docEditor.reloadText()
qApp.restoreOverrideCursor()
tEnd = time()
self.statusBar.setStatus("Indexing completed in %.1f ms" % ((tEnd - tStart)*1000.0))
self.docEditor.reloadText()
qApp.restoreOverrideCursor()
if self.mainConf.showGUI:
self.makeAlert(
"Project index rebuilt in %.3f seconds." % (tEnd - tStart), nwAlert.INFO
)
self.makeAlert("The project index has been successfully rebuilt.", nwAlert.INFO)
return True
@@ -735,7 +741,7 @@ class GuiMain(QMainWindow):
def editConfigDialog(self):
"""Open the preferences dialog.
"""
dlgConf = GuiConfigEditor(self, self.theProject)
dlgConf = GuiPreferences(self, self.theProject)
if dlgConf.exec_() == QDialog.Accepted:
logger.debug("Applying new preferences")
self.initMain()
@@ -776,7 +782,7 @@ class GuiMain(QMainWindow):
0 = info, 1 = warning, and 2 = error.
"""
if isinstance(theMessage, list):
popMsg = " ".join(theMessage)
popMsg = "<br>".join(theMessage)
logMsg = theMessage
else:
popMsg = theMessage
+82
View File
@@ -0,0 +1,82 @@
[
[
"content/14298de4d9524.nwd",
"CHARACTER",
"John Smith"
],
[
"content/53b69b83cdafc.nwd",
"NOVEL",
"Title Page"
],
[
"content/5eaea4e8cdee8.nwd",
"WORLD",
"Mars"
],
[
"content/636b6aa9b697b.nwd",
"NOVEL",
"Making a Scene"
],
[
"content/6a2d6d5f4f401.nwd",
"NOVEL",
"Chapter One"
],
[
"content/88706ddc78b1b.nwd",
"NOVEL",
"Chapter Two"
],
[
"content/96b68994dfa3d.nwd",
"NOVEL",
"A Note on Structure"
],
[
"content/974e400180a99.nwd",
"NOVEL",
"Page"
],
[
"content/ae7339df26ded.nwd",
"NOVEL",
"We Found John!"
],
[
"content/b3e74dbc1f584.nwd",
"WORLD",
"Earth"
],
[
"content/b8136a5a774a0.nwd",
"NOVEL",
"Delete Me!"
],
[
"content/ba8a28a246524.nwd",
"NOVEL",
"Interlude"
],
[
"content/bb2c23b3c42cc.nwd",
"CHARACTER",
"Jane Smith"
],
[
"content/bc0cbd2a407f3.nwd",
"NOVEL",
"Another Scene"
],
[
"content/edca4be2fcaf8.nwd",
"NOVEL",
"Part One"
],
[
"content/f1471bef9f2ae.nwd",
"WORLD",
"Space"
]
]
+23
View File
@@ -0,0 +1,23 @@
Table of Contents
===================
File Name Class Document Label
--------------------------------------------------------------------------------
content/14298de4d9524.nwd CHARACTER John Smith
content/53b69b83cdafc.nwd NOVEL Title Page
content/5eaea4e8cdee8.nwd WORLD Mars
content/636b6aa9b697b.nwd NOVEL Making a Scene
content/6a2d6d5f4f401.nwd NOVEL Chapter One
content/88706ddc78b1b.nwd NOVEL Chapter Two
content/96b68994dfa3d.nwd NOVEL A Note on Structure
content/974e400180a99.nwd NOVEL Page
content/ae7339df26ded.nwd NOVEL We Found John!
content/b3e74dbc1f584.nwd WORLD Earth
content/b8136a5a774a0.nwd NOVEL Delete Me!
content/ba8a28a246524.nwd NOVEL Interlude
content/bb2c23b3c42cc.nwd CHARACTER Jane Smith
content/bc0cbd2a407f3.nwd NOVEL Another Scene
content/edca4be2fcaf8.nwd NOVEL Part One
content/f1471bef9f2ae.nwd WORLD Space
+4 -20
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.6.1" hexVersion="0x000601f0" fileVersion="1.0" saveCount="186" autoCount="28" timeStamp="2020-05-28 09:59:15">
<novelWriterXML appVersion="0.7.0rc1" hexVersion="0x000700c1" fileVersion="1.1" saveCount="202" autoCount="29" timeStamp="2020-05-30 00:19:45" editTime="35">
<project>
<name>Sample Project</name>
<title>Sample Project</title>
@@ -11,7 +11,7 @@
<spellCheck>True</spellCheck>
<autoOutline>True</autoOutline>
<lastEdited>636b6aa9b697b</lastEdited>
<lastViewed>ba8a28a246524</lastViewed>
<lastViewed>b3e74dbc1f584</lastViewed>
<lastWordCount>914</lastWordCount>
<autoReplace>
<A>B</A>
@@ -57,7 +57,6 @@
<type>FILE</type>
<class>NOVEL</class>
<status>Started</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>TITLE</layout>
<charCount>72</charCount>
@@ -70,10 +69,9 @@
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>PAGE</layout>
<charCount>208</charCount>
<charCount>210</charCount>
<wordCount>40</wordCount>
<paraCount>2</paraCount>
<cursorPos>213</cursorPos>
@@ -83,7 +81,6 @@
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>PARTITION</layout>
<charCount>23</charCount>
@@ -103,7 +100,6 @@
<type>FILE</type>
<class>NOVEL</class>
<status>Notes</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>CHAPTER</layout>
<charCount>12</charCount>
@@ -116,20 +112,18 @@
<type>FILE</type>
<class>NOVEL</class>
<status>1st Draft</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>SCENE</layout>
<charCount>1199</charCount>
<wordCount>216</wordCount>
<paraCount>7</paraCount>
<cursorPos>527</cursorPos>
<cursorPos>1266</cursorPos>
</item>
<item handle="bc0cbd2a407f3" order="2" parent="e7ded148d6e4a">
<name>Another Scene</name>
<type>FILE</type>
<class>NOVEL</class>
<status>1st Draft</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>SCENE</layout>
<charCount>476</charCount>
@@ -142,7 +136,6 @@
<type>FILE</type>
<class>NOVEL</class>
<status>Finished</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>UNNUMBERED</layout>
<charCount>633</charCount>
@@ -155,7 +148,6 @@
<type>FILE</type>
<class>NOVEL</class>
<status>2nd Draft</status>
<expanded>False</expanded>
<exported>False</exported>
<layout>NOTE</layout>
<charCount>1692</charCount>
@@ -168,7 +160,6 @@
<type>FILE</type>
<class>NOVEL</class>
<status>1st Draft</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>CHAPTER</layout>
<charCount>139</charCount>
@@ -181,7 +172,6 @@
<type>FILE</type>
<class>NOVEL</class>
<status>1st Draft</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>SCENE</layout>
<charCount>189</charCount>
@@ -208,7 +198,6 @@
<type>FILE</type>
<class>CHARACTER</class>
<status>Minor</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>NOTE</layout>
<charCount>49</charCount>
@@ -221,7 +210,6 @@
<type>FILE</type>
<class>CHARACTER</class>
<status>Major</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>NOTE</layout>
<charCount>55</charCount>
@@ -241,7 +229,6 @@
<type>FILE</type>
<class>WORLD</class>
<status>Main</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>NOTE</layout>
<charCount>76</charCount>
@@ -254,7 +241,6 @@
<type>FILE</type>
<class>WORLD</class>
<status>Minor</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>NOTE</layout>
<charCount>115</charCount>
@@ -267,7 +253,6 @@
<type>FILE</type>
<class>WORLD</class>
<status>Major</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>NOTE</layout>
<charCount>28</charCount>
@@ -287,7 +272,6 @@
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>SCENE</layout>
<charCount>0</charCount>
+5 -2
View File
@@ -6,7 +6,7 @@ with open("README.md", "r") as inFile:
setuptools.setup(
name = "novelWriter",
version = "0.6.3",
version = "0.7.0rc1",
author = "Veronica Berglyd Olsen",
author_email = "code@vkbo.net",
description = "A markdown-like document editor for writing novels",
@@ -27,7 +27,10 @@ setuptools.setup(
"Source Code": "https://github.com/vkbo/novelWriter",
},
classifiers = [
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Development Status :: 3 - Alpha",
"Operating System :: OS Independent",
+2 -3
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.6.1" hexVersion="0x000601f0" fileVersion="1.0" saveCount="2" autoCount="0" timeStamp="2020-05-27 18:25:48">
<novelWriterXML appVersion="0.7.0rc1" hexVersion="0x000700c1" fileVersion="1.1" saveCount="2" autoCount="0" timeStamp="2020-05-29 23:05:28">
<project>
<name>New Project</name>
<title></title>
@@ -14,7 +14,7 @@
<autoReplace/>
<titleFormat>
<title>%title%</title>
<chapter>Chapter %num%\\%title%</chapter>
<chapter>Chapter %ch%: %title%</chapter>
<unnumbered>%title%</unnumbered>
<scene>* * *</scene>
<section></section>
@@ -55,7 +55,6 @@
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>SCENE</layout>
<charCount>0</charCount>
+2 -6
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.6.1" hexVersion="0x000601f0" fileVersion="1.0" saveCount="4" autoCount="0" timeStamp="2020-05-27 18:27:20">
<novelWriterXML appVersion="0.7.0rc1" hexVersion="0x000700c1" fileVersion="1.1" saveCount="4" autoCount="0" timeStamp="2020-05-29 23:06:02">
<project>
<name>New Project</name>
<title></title>
@@ -14,7 +14,7 @@
<autoReplace/>
<titleFormat>
<title>%title%</title>
<chapter>Chapter %num%\\%title%</chapter>
<chapter>Chapter %ch%: %title%</chapter>
<unnumbered>%title%</unnumbered>
<scene>* * *</scene>
<section></section>
@@ -55,7 +55,6 @@
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>SCENE</layout>
<charCount>331</charCount>
@@ -75,7 +74,6 @@
<type>FILE</type>
<class>CHARACTER</class>
<status>New</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>NOTE</layout>
<charCount>34</charCount>
@@ -95,7 +93,6 @@
<type>FILE</type>
<class>PLOT</class>
<status>New</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>NOTE</layout>
<charCount>48</charCount>
@@ -115,7 +112,6 @@
<type>FILE</type>
<class>WORLD</class>
<status>New</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>NOTE</layout>
<charCount>51</charCount>
+2 -3
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.5" fileVersion="1.0" saveCount="2" autoCount="0" timeStamp="2020-05-10 23:19:00">
<novelWriterXML appVersion="0.7.0rc1" hexVersion="0x000700c1" fileVersion="1.1" saveCount="2" autoCount="0" timeStamp="2020-05-29 23:07:05">
<project>
<name>Project Name</name>
<title>Project Title</title>
@@ -18,7 +18,7 @@
</autoReplace>
<titleFormat>
<title>%title%</title>
<chapter>Chapter %num%\\%title%</chapter>
<chapter>Chapter %ch%: %title%</chapter>
<unnumbered>%title%</unnumbered>
<scene>* * *</scene>
<section></section>
@@ -59,7 +59,6 @@
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>SCENE</layout>
<charCount>0</charCount>
+2 -3
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.6.1" hexVersion="0x000601f0" fileVersion="1.0" saveCount="2" autoCount="0" timeStamp="2020-05-27 18:30:26">
<novelWriterXML appVersion="0.7.0rc1" hexVersion="0x000700c1" fileVersion="1.1" saveCount="2" autoCount="0" timeStamp="2020-05-29 23:07:37">
<project>
<name>New Project</name>
<title></title>
@@ -14,7 +14,7 @@
<autoReplace/>
<titleFormat>
<title>%title%</title>
<chapter>Chapter %num%\\%title%</chapter>
<chapter>Chapter %ch%: %title%</chapter>
<unnumbered>%title%</unnumbered>
<scene>* * *</scene>
<section></section>
@@ -55,7 +55,6 @@
<type>FILE</type>
<class>NOVEL</class>
<status>Note</status>
<expanded>False</expanded>
<exported>False</exported>
<layout>PAGE</layout>
<charCount>0</charCount>
+2 -3
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.6.1" hexVersion="0x000601f0" fileVersion="1.0" saveCount="1" autoCount="0" timeStamp="2020-05-27 18:23:46">
<novelWriterXML appVersion="0.7.0rc1" hexVersion="0x000700c1" fileVersion="1.1" saveCount="1" autoCount="0" timeStamp="2020-05-29 23:03:58">
<project>
<name>New Project</name>
<title></title>
@@ -14,7 +14,7 @@
<autoReplace/>
<titleFormat>
<title>%title%</title>
<chapter>Chapter %num%\\%title%</chapter>
<chapter>Chapter %ch%: %title%</chapter>
<unnumbered>%title%</unnumbered>
<scene>* * *</scene>
<section></section>
@@ -76,7 +76,6 @@
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>SCENE</layout>
<charCount>0</charCount>
+2 -3
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.6.1" hexVersion="0x000601f0" fileVersion="1.0" saveCount="4" autoCount="0" timeStamp="2020-05-27 18:24:35">
<novelWriterXML appVersion="0.7.0rc1" hexVersion="0x000700c1" fileVersion="1.1" saveCount="4" autoCount="0" timeStamp="2020-05-29 23:04:31">
<project>
<name>New Project</name>
<title></title>
@@ -14,7 +14,7 @@
<autoReplace/>
<titleFormat>
<title>%title%</title>
<chapter>Chapter %num%\\%title%</chapter>
<chapter>Chapter %ch%: %title%</chapter>
<unnumbered>%title%</unnumbered>
<scene>* * *</scene>
<section></section>
@@ -76,7 +76,6 @@
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>SCENE</layout>
<charCount>0</charCount>
+2 -5
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.6.1" hexVersion="0x000601f0" fileVersion="1.0" saveCount="5" autoCount="0" timeStamp="2020-05-27 18:24:56">
<novelWriterXML appVersion="0.7.0rc1" hexVersion="0x000700c1" fileVersion="1.1" saveCount="5" autoCount="0" timeStamp="2020-05-29 23:04:50">
<project>
<name>New Project</name>
<title></title>
@@ -14,7 +14,7 @@
<autoReplace/>
<titleFormat>
<title>%title%</title>
<chapter>Chapter %num%\\%title%</chapter>
<chapter>Chapter %ch%: %title%</chapter>
<unnumbered>%title%</unnumbered>
<scene>* * *</scene>
<section></section>
@@ -76,7 +76,6 @@
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>SCENE</layout>
<charCount>0</charCount>
@@ -117,7 +116,6 @@
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>SCENE</layout>
<charCount>0</charCount>
@@ -130,7 +128,6 @@
<type>FILE</type>
<class>CHARACTER</class>
<status>New</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>NOTE</layout>
<charCount>0</charCount>
+8 -8
View File
@@ -237,14 +237,14 @@ def testMainWindows(qtbot, nwTempGUI, nwRef, nwTemp):
# Check the files
refFile = path.join(nwTempGUI,"nwProject.nwx")
assert cmpFiles(refFile, path.join(nwRef,"gui","1_nwProject.nwx"), [2])
refFile = path.join(nwTempGUI,"data_0","e17daca5f3e1_main.nwd")
assert cmpFiles(refFile, path.join(nwRef,"gui","1_e17daca5f3e1_main.nwd"))
refFile = path.join(nwTempGUI,"data_9","8010bd9270f9_main.nwd")
assert cmpFiles(refFile, path.join(nwRef,"gui","1_8010bd9270f9_main.nwd"))
refFile = path.join(nwTempGUI,"data_3","1489056e0916_main.nwd")
assert cmpFiles(refFile, path.join(nwRef,"gui","1_1489056e0916_main.nwd"))
refFile = path.join(nwTempGUI,"data_1","a6562590ef19_main.nwd")
assert cmpFiles(refFile, path.join(nwRef,"gui","1_a6562590ef19_main.nwd"))
refFile = path.join(nwTempGUI,"content","0e17daca5f3e1.nwd")
assert cmpFiles(refFile, path.join(nwRef,"gui","1_0e17daca5f3e1.nwd"))
refFile = path.join(nwTempGUI,"content","98010bd9270f9.nwd")
assert cmpFiles(refFile, path.join(nwRef,"gui","1_98010bd9270f9.nwd"))
refFile = path.join(nwTempGUI,"content","31489056e0916.nwd")
assert cmpFiles(refFile, path.join(nwRef,"gui","1_31489056e0916.nwd"))
refFile = path.join(nwTempGUI,"content","1a6562590ef19.nwd")
assert cmpFiles(refFile, path.join(nwRef,"gui","1_1a6562590ef19.nwd"))
nwGUI.closeMain()
# qtbot.stopForInteraction()
+3 -3
View File
@@ -155,7 +155,7 @@ def testIndexCheckThese(nwTempProj):
"# Hello World!\n"
"@pov: Jane"
))
assert str(theIndex.tagIndex) == "{'Jane': [2, '2858dcd1057d3', 'CHARACTER']}"
assert str(theIndex.tagIndex) == "{'Jane': [2, '2858dcd1057d3', 'CHARACTER', 'T000001']}"
assert theIndex.novelIndex[nHandle]["T000001"]["title"] == "Hello World!"
assert str(theIndex.checkThese(["@tag", "Jane"], cItem)) == "[True, True]"
@@ -195,7 +195,7 @@ def testIndexMeta(nwTempProj):
"\n"
"Well, not really.\n"
))
assert str(theIndex.tagIndex) == "{'Jane': [2, '2858dcd1057d3', 'CHARACTER']}"
assert str(theIndex.tagIndex) == "{'Jane': [2, '2858dcd1057d3', 'CHARACTER', 'T000001']}"
assert theIndex.novelIndex[nHandle]["T000001"]["title"] == "Hello World!"
# The novel structure should contain the pointer to the novel file header
@@ -214,6 +214,6 @@ def testIndexMeta(nwTempProj):
# The character file should have a record of the reference from the novel file
theRefs = theIndex.getBackReferenceList(cHandle)
assert str(theRefs) == "{'41cfc0d1f2d12': 3}"
assert str(theRefs) == "{'41cfc0d1f2d12': 'T000001'}"
assert theProject.closeProject()