diff --git a/.gitignore b/.gitignore
index 2905634a..c87816d0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 750409b9..e5f313e1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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]
diff --git a/docs/source/conf.py b/docs/source/conf.py
index cce19db2..02f7ed85 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -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 ---------------------------------------------------
diff --git a/docs/source/technical.rst b/docs/source/technical.rst
index 32d79545..027d2aaf 100644
--- a/docs/source/technical.rst
+++ b/docs/source/technical.rst
@@ -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.
diff --git a/nw/__init__.py b/nw/__init__.py
index 1eff781f..9956c0d0 100644
--- a/nw/__init__.py
+++ b/nw/__init__.py
@@ -40,8 +40,8 @@ __package__ = "novelWriter"
__author__ = "Veronica Berglyd Olsen"
__copyright__ = "Copyright 2018–2020, 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"
diff --git a/nw/config.py b/nw/config.py
index bc40259b..18529f61 100644
--- a/nw/config.py
+++ b/nw/config.py
@@ -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")
diff --git a/nw/constants/constants.py b/nw/constants/constants.py
index bc20af31..cf0ab4eb 100644
--- a/nw/constants/constants.py
+++ b/nw/constants/constants.py
@@ -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"
diff --git a/nw/core/__init__.py b/nw/core/__init__.py
index 6dd65fd2..0107dac2 100644
--- a/nw/core/__init__.py
+++ b/nw/core/__init__.py
@@ -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",
]
diff --git a/nw/core/document.py b/nw/core/document.py
index 4c502bea..6ab4d500 100644
--- a/nw/core/document.py
+++ b/nw/core/document.py
@@ -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
diff --git a/nw/core/index.py b/nw/core/index.py
index e585a03a..456b415c 100644
--- a/nw/core/index.py
+++ b/nw/core/index.py
@@ -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
diff --git a/nw/core/project.py b/nw/core/project.py
index 0c303f74..13fa1f08 100644
--- a/nw/core/project.py
+++ b/nw/core/project.py
@@ -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?
"
+ "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():
diff --git a/nw/core/spellcheck.py b/nw/core/spellcheck.py
index 2af06f42..6296bc60 100644
--- a/nw/core/spellcheck.py
+++ b/nw/core/spellcheck.py
@@ -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
diff --git a/nw/core/tohtml.py b/nw/core/tohtml.py
index 53503ba5..cb2a4baf 100644
--- a/nw/core/tohtml.py
+++ b/nw/core/tohtml.py
@@ -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 = "" % (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"\\", "
")
- tmpResult.append("
%s
\n" % tText) @@ -296,17 +301,17 @@ class ToHtml(Tokenizer): refTags = [] if theBits[0] in nwLabels.KEY_NAME: retText += " " % nwLabels.KEY_NAME[theBits[0]] - if self.genMode == self.M_PREVIEW: - for tTag in theBits[1:]: - refTags.append("%s" % ( - theBits[0][1:], tTag, tTag - )) - retText += ", ".join(refTags) + if theBits[0] == nwKeyWords.TAG_KEY: + retText += "%s" % ( + theBits[1], theBits[1] + ) else: - if theBits[0] == nwKeyWords.TAG_KEY: - retText += "%s" % ( - theBits[1], theBits[1] - ) + if self.genMode == self.M_PREVIEW: + for tTag in theBits[1:]: + refTags.append("%s" % ( + theBits[0][1:], tTag, tTag + )) + retText += ", ".join(refTags) else: for tTag in theBits[1:]: refTags.append("%s" % ( diff --git a/nw/core/tokenizer.py b/nw/core/tokenizer.py index 8de64118..1dc573ca 100644 --- a/nw/core/tokenizer.py +++ b/nw/core/tokenizer.py @@ -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 diff --git a/nw/core/tools.py b/nw/core/tools.py index a6eb244b..8115105d 100644 --- a/nw/core/tools.py +++ b/nw/core/tools.py @@ -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. """ diff --git a/nw/gui/__init__.py b/nw/gui/__init__.py index 393f501d..8360a460 100644 --- a/nw/gui/__init__.py +++ b/nw/gui/__init__.py @@ -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", diff --git a/nw/gui/build.py b/nw/gui/build.py index b73f1017..f191d063 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -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") diff --git a/nw/gui/dialogs/__init__.py b/nw/gui/dialogs/__init__.py index 271a7a1d..5f4e4a03 100644 --- a/nw/gui/dialogs/__init__.py +++ b/nw/gui/dialogs/__init__.py @@ -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", diff --git a/nw/gui/dialogs/about.py b/nw/gui/dialogs/about.py index f71f6e4c..653f87ae 100644 --- a/nw/gui/dialogs/about.py +++ b/nw/gui/dialogs/about.py @@ -119,16 +119,19 @@ class GuiAbout(QDialog): "{copyright:s}.
" "Website: {domain:s}
" - "{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.
" - "{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.
" - "{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.
" - "See the License tab for the full text, or visit the GNU website at " - "GPL v3.0 for more details.
" + "{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.
" + "{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.
" + "{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.
" + "See the License tab for the full text, or visit the GNU website " + "at GPL v3.0 " + "for more details.
" "{credits:s}
" ).format( diff --git a/nw/gui/dialogs/docsplit.py b/nw/gui/dialogs/docsplit.py index 25cc2f1d..f95e0041 100644 --- a/nw/gui/dialogs/docsplit.py +++ b/nw/gui/dialogs/docsplit.py @@ -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(): diff --git a/nw/gui/dialogs/configeditor.py b/nw/gui/dialogs/preferences.py similarity index 99% rename from nw/gui/dialogs/configeditor.py rename to nw/gui/dialogs/preferences.py index a088d090..eaa034d1 100644 --- a/nw/gui/dialogs/configeditor.py +++ b/nw/gui/dialogs/preferences.py @@ -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", diff --git a/nw/gui/dialogs/projectsettings.py b/nw/gui/dialogs/projectsettings.py index 8d501585..a49193d1 100644 --- a/nw/gui/dialogs/projectsettings.py +++ b/nw/gui/dialogs/projectsettings.py @@ -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): diff --git a/nw/gui/dialogs/sessionlog.py b/nw/gui/dialogs/sessionlog.py index 59750dce..fdda4166 100644 --- a/nw/gui/dialogs/sessionlog.py +++ b/nw/gui/dialogs/sessionlog.py @@ -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)) diff --git a/nw/gui/elements/doceditor.py b/nw/gui/elements/doceditor.py index f3b1f5af..aa3ea1c0 100644 --- a/nw/gui/elements/doceditor.py +++ b/nw/gui/elements/doceditor.py @@ -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 diff --git a/nw/gui/elements/doctree.py b/nw/gui/elements/doctree.py index 2c81cf91..02db9bde 100644 --- a/nw/gui/elements/doctree.py +++ b/nw/gui/elements/doctree.py @@ -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 diff --git a/nw/gui/elements/docviewer.py b/nw/gui/elements/docviewer.py index 3eefed49..66f2a5d3 100644 --- a/nw/gui/elements/docviewer.py +++ b/nw/gui/elements/docviewer.py @@ -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): diff --git a/nw/gui/elements/viewdetails.py b/nw/gui/elements/viewdetails.py index 589ffee5..d4b2621c 100644 --- a/nw/gui/elements/viewdetails.py +++ b/nw/gui/elements/viewdetails.py @@ -107,7 +107,9 @@ class GuiDocViewDetails(QWidget): for tHandle in theRefs: tItem = self.theProject.projTree[tHandle] if tItem is not None: - theList.append("%s" % (tHandle,tItem.itemName)) + theList.append("%s" % ( + 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): diff --git a/nw/gui/icons.py b/nw/gui/icons.py index a57f3029..db5d6e9e 100644 --- a/nw/gui/icons.py +++ b/nw/gui/icons.py @@ -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) diff --git a/nw/gui/statusbar.py b/nw/gui/statusbar.py index c7ee28df..d268ab74 100644 --- a/nw/gui/statusbar.py +++ b/nw/gui/statusbar.py @@ -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)) diff --git a/nw/gui/theme.py b/nw/gui/theme.py index 5b4893a6..04a00c0e 100644 --- a/nw/gui/theme.py +++ b/nw/gui/theme.py @@ -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"): diff --git a/nw/guimain.py b/nw/guimain.py index 234de395..a3b7d46e 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -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 = "