diff --git a/docs/source/tech_technical.rst b/docs/source/tech_technical.rst
index 7298f904..50209ecc 100644
--- a/docs/source/tech_technical.rst
+++ b/docs/source/tech_technical.rst
@@ -49,8 +49,8 @@ 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, select :guilabel:`Show File Details` from the :guilabel:`Document` menu when
-having the document open, or look in one of the ``ToC`` files in the root of the project folder.
-The ``ToC`` files have a list of all document files in the project and where they are saved.
+having the document open, or look in the ``ToC.txt`` file in the root of the project folder.
+The ``ToC.txt`` file has a list of all document files in the project and where they are saved.
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
diff --git a/nw/constants/constants.py b/nw/constants/constants.py
index e237b662..a32e60c4 100644
--- a/nw/constants/constants.py
+++ b/nw/constants/constants.py
@@ -59,7 +59,6 @@ class nwFiles():
PROJ_DICT = "wordlist.txt"
PROJ_LOCK = "nwProject.lock"
TOC_TXT = "ToC.txt"
- TOC_JSON = "ToC.json"
SESS_STATS = "sessionStats.log"
INDEX_FILE = "tagsIndex.json"
OPTS_FILE = "guiOptions.json"
diff --git a/nw/core/project.py b/nw/core/project.py
index 1bf53151..f8e45194 100644
--- a/nw/core/project.py
+++ b/nw/core/project.py
@@ -743,7 +743,7 @@ class NWProject():
"""Close the current project and clear all meta data.
"""
self.optState.saveSettings()
- self.projTree.writeToCFiles()
+ self.projTree.writeToCFile()
self._appendSessionStats()
self._clearLockFile()
self.clearProject()
@@ -781,6 +781,10 @@ class NWProject():
def zipIt(self, doNotify):
"""Create a zip file of the entire project.
"""
+ if not self.theParent.hasProject:
+ logger.error("No project open")
+ return False
+
logger.info("Backing up project")
self.theParent.setStatus("Backing up project ...")
@@ -1476,6 +1480,7 @@ class NWProject():
os.path.join(self.projMeta, "timelineOptions.json"),
os.path.join(self.projMeta, "docMergeOptions.json"),
os.path.join(self.projMeta, "sessionLogOptions.json"),
+ os.path.join(self.projPath, "ToC.json"),
]
for rmFile in rmList:
diff --git a/nw/core/tree.py b/nw/core/tree.py
index 2973d462..ac3ab45c 100644
--- a/nw/core/tree.py
+++ b/nw/core/tree.py
@@ -26,7 +26,6 @@
"""
import logging
-import json
import os
from lxml import etree
@@ -139,46 +138,42 @@ 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.
+ def writeToCFile(self):
+ """Write the convenience table of contents file in the root of
+ the project directory.
"""
- tocText = os.path.join(self.theProject.projPath, nwFiles.TOC_TXT)
- tocJson = os.path.join(self.theProject.projPath, nwFiles.TOC_JSON)
+ tocList = []
+ tocLen = 0
+ for tHandle in self._treeOrder:
+ tItem = self.__getitem__(tHandle)
+ if tItem is None:
+ continue
+ tFile = tHandle+".nwd"
+ if os.path.isfile(os.path.join(self.theProject.projContent, tFile)):
+ tocLine = "%-25s %-9s %-10s %s" % (
+ os.path.join("content", tFile),
+ tItem.itemClass.name,
+ tItem.itemLayout.name,
+ tItem.itemName,
+ )
+ tocList.append(tocLine)
+ tocLen = max(tocLen, len(tocLine))
- jsonData = []
try:
# Dump the text
+ tocText = os.path.join(self.theProject.projPath, nwFiles.TOC_TXT)
with open(tocText, mode="w", encoding="utf8") as outFile:
outFile.write("\n")
- outFile.write(" Table of Contents\n")
- 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 os.path.isfile(os.path.join(self.theProject.projContent, tFile)):
- outFile.write(" %-25s %-9s %s\n" % (
- os.path.join("content", tFile),
- tItem.itemClass.name,
- tItem.itemName,
- ))
- jsonData.append([
- os.path.join("content", tFile),
- tItem.itemClass.name,
- tItem.itemName,
- ])
+ outFile.write("%-25s %-9s %-10s %s\n" % (
+ "File Name", "Class", "Layout", "Document Label"
+ ))
+ outFile.write("-"*tocLen + "\n")
+ outFile.write("\n".join(tocList))
outFile.write("\n")
- # Dump the JSON
- with open(tocJson, mode="w+", encoding="utf8") as outFile:
- json.dump(jsonData, outFile, indent=2)
-
except Exception as e:
logger.error(str(e))
diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py
index 85a3f75d..8759d86e 100644
--- a/nw/gui/doceditor.py
+++ b/nw/gui/doceditor.py
@@ -620,9 +620,10 @@ class GuiDocEditor(QTextEdit):
this class when calling these actions from other classes.
"""
logger.verbose("Requesting action: %s" % theAction.name)
- if not self.theParent.hasProject:
- logger.error("No project open")
+ if self.theHandle is None:
+ logger.error("No document open")
return False
+
self._allowAutoReplace(False)
if theAction == nwDocAction.UNDO:
self.undo()
@@ -678,7 +679,9 @@ class GuiDocEditor(QTextEdit):
logger.debug("Unknown or unsupported document action %s" % str(theAction))
self._allowAutoReplace(True)
return False
+
self._allowAutoReplace(True)
+
return True
def isEmpty(self):
@@ -690,21 +693,29 @@ class GuiDocEditor(QTextEdit):
"""Tell the user where on the file system the file in the editor
is saved.
"""
- if self.theHandle is not None:
- msgBox = QMessageBox()
- msgBox.information(self, "File Location", (
- "File details for the currently open file
"
- "Handle: {handle:s}
"
- "Location: {fileLoc:s}"
- ).format(
- handle = self.theHandle,
- fileLoc = str(self.nwDocument.getFileLocation())
- ))
+ if self.theHandle is None:
+ logger.error("No document open")
+ return False
+
+ msgBox = QMessageBox()
+ msgBox.information(self, "File Location", (
+ "File details for the currently open file
"
+ "Handle: {handle:s}
"
+ "Location: {fileLoc:s}"
+ ).format(
+ handle = self.theHandle,
+ fileLoc = str(self.nwDocument.getFileLocation())
+ ))
+
return
def insertText(self, theInsert):
"""Insert a specific type of text at the cursor position.
"""
+ if self.theHandle is None:
+ logger.error("No document open")
+ return False
+
if isinstance(theInsert, str):
theText = theInsert
elif isinstance(theInsert, nwDocInsert):
diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py
index f0d0bca9..4116a863 100644
--- a/nw/gui/projtree.py
+++ b/nw/gui/projtree.py
@@ -163,6 +163,7 @@ class GuiProjectTree(QTreeWidget):
nHandle = None
if not self.theParent.hasProject:
+ logger.error("No project open")
return False
# The item needs to be assigned an item class, so one must be
@@ -281,6 +282,10 @@ class GuiProjectTree(QTreeWidget):
"""Move an item up or down in the tree, but only if the treeView
has focus. This also applies when the menu is used.
"""
+ if not self.theParent.hasProject:
+ logger.error("No project open")
+ return False
+
hasFocus = qApp.focusWidget() == self or not self.mainConf.showGUI
if hasFocus and self.theParent.hasProject:
@@ -364,6 +369,10 @@ class GuiProjectTree(QTreeWidget):
function only asks for confirmation once, and calls the regular
deleteItem function for each document in the Trash folder.
"""
+ if not self.theParent.hasProject:
+ logger.error("No project open")
+ return False
+
trashHandle = self.theProject.projTree.trashRoot()
logger.debug("Emptying Trash folder")
@@ -409,6 +418,10 @@ class GuiProjectTree(QTreeWidget):
delete the files on disk. Folders are deleted if they're empty only,
and the deletion is always permanent.
"""
+ if not self.theParent.hasProject:
+ logger.error("No project open")
+ return False
+
if tHandle is None:
tHandle = self.getSelectedHandle()
diff --git a/nw/guimain.py b/nw/guimain.py
index 1a6b5a16..4a7c6b1d 100644
--- a/nw/guimain.py
+++ b/nw/guimain.py
@@ -316,7 +316,8 @@ class GuiMain(QMainWindow):
if not isYes:
msgBox = QMessageBox()
msgRes = msgBox.question(
- self, "Close Project", "Save changes and close the current project?"
+ self, "Close Project",
+ "Close the current project?
Changes are saved automatically."
)
if msgRes != QMessageBox.Yes:
return False
@@ -442,6 +443,7 @@ class GuiMain(QMainWindow):
"""Save the current project.
"""
if not self.hasProject:
+ logger.error("No project open")
return False
# If the project is new, it may not have a path, so we need one
@@ -465,27 +467,33 @@ class GuiMain(QMainWindow):
def closeDocument(self):
"""Close the document and clear the editor and title field.
"""
- if self.hasProject:
- self.docEditor.saveCursorPosition()
- if self.docEditor.docChanged:
- self.saveDocument()
- self.docEditor.clearEditor()
+ if not self.hasProject:
+ logger.error("No project open")
+ return False
+
+ self.docEditor.saveCursorPosition()
+ if self.docEditor.docChanged:
+ self.saveDocument()
+ self.docEditor.clearEditor()
return True
def openDocument(self, tHandle, tLine=None, changeFocus=True, doScroll=False):
"""Open a specific document, optionally at a given line.
"""
- if self.hasProject:
- self.closeDocument()
- self.tabWidget.setCurrentWidget(self.splitDocs)
- if self.docEditor.loadText(tHandle, tLine):
- if changeFocus:
- self.docEditor.setFocus()
- self.theProject.setLastEdited(tHandle)
- self.treeView.setSelectedHandle(tHandle, doScroll=doScroll)
- else:
- return False
+ if not self.hasProject:
+ logger.error("No project open")
+ return False
+
+ self.closeDocument()
+ self.tabWidget.setCurrentWidget(self.splitDocs)
+ if self.docEditor.loadText(tHandle, tLine):
+ if changeFocus:
+ self.docEditor.setFocus()
+ self.theProject.setLastEdited(tHandle)
+ self.treeView.setSelectedHandle(tHandle, doScroll=doScroll)
+ else:
+ return False
return True
@@ -493,43 +501,54 @@ class GuiMain(QMainWindow):
"""Opens the next document in the project tree, following the
document with the given handle. Stops when reaching the end.
"""
- if self.hasProject:
- self.treeView.flushTreeOrder()
- nHandle = None # The next handle after tHandle
- fHandle = None # The first file handle we encounter
- foundIt = False # We've found tHandle, pick the next we see
- for tItem in self.theProject.projTree:
- if tItem is None:
- continue
- if tItem.itemType != nwItemType.FILE:
- continue
- if fHandle is None:
- fHandle = tItem.itemHandle
- if tItem.itemHandle == tHandle:
- foundIt = True
- elif foundIt:
- nHandle = tItem.itemHandle
- break
+ if not self.hasProject:
+ logger.error("No project open")
+ return False
- if nHandle is not None:
- self.openDocument(nHandle, tLine=0, doScroll=True)
- return True
- elif wrapAround:
- self.openDocument(fHandle, tLine=0, doScroll=True)
- return False
+ self.treeView.flushTreeOrder()
+ nHandle = None # The next handle after tHandle
+ fHandle = None # The first file handle we encounter
+ foundIt = False # We've found tHandle, pick the next we see
+ for tItem in self.theProject.projTree:
+ if tItem is None:
+ continue
+ if tItem.itemType != nwItemType.FILE:
+ continue
+ if fHandle is None:
+ fHandle = tItem.itemHandle
+ if tItem.itemHandle == tHandle:
+ foundIt = True
+ elif foundIt:
+ nHandle = tItem.itemHandle
+ break
+
+ if nHandle is not None:
+ self.openDocument(nHandle, tLine=0, doScroll=True)
+ return True
+ elif wrapAround:
+ self.openDocument(fHandle, tLine=0, doScroll=True)
+ return False
return False
def saveDocument(self):
"""Save the current documents.
"""
- if self.hasProject:
- self.docEditor.saveText()
+ if not self.hasProject:
+ logger.error("No project open")
+ return False
+
+ self.docEditor.saveText()
+
return True
def viewDocument(self, tHandle=None, tAnchor=None):
"""Load a document for viewing in the view panel.
"""
+ if not self.hasProject:
+ logger.error("No project open")
+ return False
+
if tHandle is None:
logger.debug("Viewing document, but no handle provided")
@@ -573,8 +592,11 @@ class GuiMain(QMainWindow):
"""Import the text contained in an out-of-project text file, and
insert the text into the currently open document.
"""
- lastPath = self.mainConf.lastPath
+ if not self.hasProject:
+ logger.error("No project open")
+ return False
+ lastPath = self.mainConf.lastPath
extFilter = [
"Text files (*.txt)",
"Markdown files (*.md)",
@@ -627,16 +649,26 @@ class GuiMain(QMainWindow):
def mergeDocuments(self):
"""Merge multiple documents to one single new document.
"""
+ if not self.hasProject:
+ logger.error("No project open")
+ return False
+
dlgMerge = GuiDocMerge(self, self.theProject)
dlgMerge.exec_()
- return
+
+ return True
def splitDocument(self):
"""Split a single document into multiple documents.
"""
+ if not self.hasProject:
+ logger.error("No project open")
+ return False
+
dlgSplit = GuiDocSplit(self, self.theProject)
dlgSplit.exec_()
- return
+
+ return True
def passDocumentAction(self, theAction):
"""Pass on document action theAction to the document viewer if
@@ -655,6 +687,10 @@ class GuiMain(QMainWindow):
def openSelectedItem(self):
"""Open the selected documents.
"""
+ if not self.hasProject:
+ logger.error("No project open")
+ return False
+
tHandle = self.treeView.getSelectedHandle()
if tHandle is None:
logger.warning("No item selected")
@@ -673,6 +709,10 @@ class GuiMain(QMainWindow):
def editItem(self, tHandle=None):
"""Open the edit item dialog.
"""
+ if not self.hasProject:
+ logger.error("No project open")
+ return False
+
if tHandle is None:
tHandle = self.treeView.getSelectedHandle()
if tHandle is None:
@@ -703,6 +743,7 @@ class GuiMain(QMainWindow):
"""Rebuild the entire index.
"""
if not self.hasProject:
+ logger.error("No project open")
return False
logger.debug("Rebuilding index ...")
@@ -748,9 +789,14 @@ class GuiMain(QMainWindow):
def rebuildOutline(self):
"""Force a rebuild of the Outline view.
"""
+ if not self.hasProject:
+ logger.error("No project open")
+ return False
+
logger.verbose("Forcing a rebuild of the Project Outline")
self.tabWidget.setCurrentWidget(self.splitOutline)
self.projView.refreshTree(overRide=True)
+
return True
##
@@ -943,7 +989,8 @@ class GuiMain(QMainWindow):
if self.hasProject:
msgBox = QMessageBox()
msgRes = msgBox.question(
- self, "Exit", "Do you want to save changes and exit?"
+ self, "Exit",
+ "Do you want to exit novelWriter?
Changes are saved automatically."
)
if msgRes != QMessageBox.Yes:
return False
@@ -1008,6 +1055,7 @@ class GuiMain(QMainWindow):
"""
if self.docEditor.theHandle is None:
logger.error("No document open, so not activating Focus Mode")
+ self.mainMenu.aFocusMode.setChecked(self.isFocusMode)
return False
self.isFocusMode = not self.isFocusMode
diff --git a/sample/ToC.json b/sample/ToC.json
deleted file mode 100644
index c00f8735..00000000
--- a/sample/ToC.json
+++ /dev/null
@@ -1,87 +0,0 @@
-[
- [
- "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/8a5deb88c0e97.nwd",
- "NOVEL",
- "Old File"
- ],
- [
- "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"
- ]
-]
\ No newline at end of file
diff --git a/sample/ToC.txt b/sample/ToC.txt
index 92237107..b106e892 100644
--- a/sample/ToC.txt
+++ b/sample/ToC.txt
@@ -1,24 +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/8a5deb88c0e97.nwd NOVEL Old File
- 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
+Table of Contents
+=================
+File Name Class Layout Document Label
+---------------------------------------------------------------------
+content/53b69b83cdafc.nwd NOVEL TITLE Title Page
+content/974e400180a99.nwd NOVEL PAGE Page
+content/edca4be2fcaf8.nwd NOVEL PARTITION Part One
+content/6a2d6d5f4f401.nwd NOVEL CHAPTER Chapter One
+content/636b6aa9b697b.nwd NOVEL SCENE Making a Scene
+content/bc0cbd2a407f3.nwd NOVEL SCENE Another Scene
+content/ba8a28a246524.nwd NOVEL UNNUMBERED Interlude
+content/96b68994dfa3d.nwd NOVEL NOTE A Note on Structure
+content/88706ddc78b1b.nwd NOVEL CHAPTER Chapter Two
+content/ae7339df26ded.nwd NOVEL SCENE We Found John!
+content/14298de4d9524.nwd CHARACTER NOTE John Smith
+content/bb2c23b3c42cc.nwd CHARACTER NOTE Jane Smith
+content/b3e74dbc1f584.nwd WORLD NOTE Earth
+content/f1471bef9f2ae.nwd WORLD NOTE Space
+content/5eaea4e8cdee8.nwd WORLD NOTE Mars
+content/8a5deb88c0e97.nwd NOVEL SCENE Old File
+content/b8136a5a774a0.nwd NOVEL SCENE Delete Me!
diff --git a/tests/nwdummy.py b/tests/nwdummy.py
index 313af767..4b5302b7 100644
--- a/tests/nwdummy.py
+++ b/tests/nwdummy.py
@@ -6,6 +6,7 @@ class DummyMain():
def __init__(self):
self.mainConf = None
+ self.hasProject = True
self.statusBar = StatusBar()
return
diff --git a/tests/test_project.py b/tests/test_project.py
index b8df3c0e..5ed9d261 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -627,7 +627,6 @@ def testProjectOldFormat(nwDummy, nwOldProj):
# Check that new files have been created
assert os.path.isfile(os.path.join(nwOldProj, "meta", "guiOptions.json"))
assert os.path.isfile(os.path.join(nwOldProj, "meta", "sessionStats.log"))
- assert os.path.isfile(os.path.join(nwOldProj, "ToC.json"))
assert os.path.isfile(os.path.join(nwOldProj, "ToC.txt"))
@pytest.mark.project