Merge branch 'dev' into view_details

This commit is contained in:
Veronica K. B. Olsen
2020-05-28 19:34:22 +02:00
41 changed files with 358 additions and 242 deletions
-49
View File
@@ -1,49 +0,0 @@
<h1>Help!</h1>
<p><i>A brief guide to make the most out of the Build Novel Project tool.</i></p>
<h2>Novel Title Formats</h2>
<p>The format of the various title levels in the files under the Novel folder can be customised in
these settings. The actual title given in the headings of your files will for instance replace
all occurrences of the keyword <mark>%title%</mark>. Any static text will be left as-is in the
final title. An empty field means the title isn't written out at all.</p>
<p>The available formatting keywords are:</p>
<p><mark>%title%</mark> &ndash; This is replaced with the text you put in your headings in your
documents</p>
<p><mark>%chnum%</mark> &ndash; This is replaced with the chapter number of your chapter type
headings. These are generated automaticall starting from 1, but ignoring chapter headings in
files with "Unnumbered" layout.</p>
<p><mark>%chnumword%</mark> &ndash; This is replaced with the chapter number, but instead of an
arabic number, the word for it is used, e.g. One, Two, Fifteen, Twenty-Five, etc.</p>
<p><mark>%scnum%</mark> &ndash; This is replaced with the scene number. The number is reset to one
for each new chapter, so it is the scene number within the current chapter.</p>
<p><mark>%scabsnum%</mark> &ndash; This is replaced with the absolute scene number. That is, the
number is counted from the first scene in the novel, and not reset for each chapter.</p>
<p><mark>\\</mark> &ndash; Two backslashes are replaced by a line break.</p>
<p><b>Note:</b> The Scene and Section formats are treated slightly differently than the other title
formats. If the format is a constant text, that is, contains no <mark>%keyword%</mark> tags, it
will be treated as a separator instead. Scene and Section separators are centred, and for
scenes, not shown if placed directly after the chapter heading. For instance, it you want the
classic three asterisk <mark>* * *</mark> separator between scenes, just put that into the
scene format box, and nothing else.</p>
<h2>Build Overrides</h2>
<p><b>Novel Outline Mode:</b> This option will build an outline version of the novel rather than the
full thing. It overrides the title format settings without changing them. Each title will be
written out, and the synopsis text will appear instead of the body text of the files. Some of
the other options are still available in Outline Mode.</p>
<h2>Include Non-Text Elements</h2>
<p><b>Include Synopsis:</b> This will add the synopsis comment as the first paragraph after each
heading.</p>
<p><b>Include Comments:</b> This will include any comments as additional paragraphs in the text.</p>
<p><b>Include Keywords:</b> This will include any keywords and tags as clickable links after each
heading.</p>
<h2>Additional Options</h2>
<p><b>Include Novel Files:</b> This means all files that don't have a layout of type "Note" will be
included. This is the normal mode when exporting the novel itself without the notes.</p>
<p><b>Include Note Files:</b> This means all files with a layout of type "Note" <i>will</i> be
included. Titles in note files are always left as they appear.</p>
<p><b>Ignore Export Flag:</b> Each file in the project tree has an "Include when building project"
option set, which is indicated by a little check mark in the "Flags" column. Files without This
tick will normally be skipped during build, but can be included if this option is enabled.</p>
+8 -27
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,8 +91,9 @@ 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)
docDir = "content"
docFile = self.docHandle+".nwd"
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)
@@ -139,8 +138,9 @@ 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))
docDir = "content"
docFile = self.docHandle+".nwd"
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):
@@ -159,12 +159,6 @@ class NWDoc():
self.makeAlert(["Could not save document.",str(e)], nwAlert.ERROR)
return False
# Remove bak files from old file save method, if one exists
# This part can eventually be removed
docBack = path.join(dataPath, docFile[:-3]+"bak")
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):
@@ -179,7 +173,8 @@ class NWDoc():
"""Permanently delete a document source file and its backups
from the project data folder.
"""
docDir, docFile = self._assemblePath(tHandle, self.FILE_MN)
docDir = "content"
docFile = self.docHandle+".nwd"
dataPath = path.join(self.theProject.projPath, docDir)
chkList = []
chkList.append(path.join(dataPath, docFile))
@@ -226,18 +221,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
+104 -25
View File
@@ -33,7 +33,7 @@
import logging
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
@@ -74,6 +74,7 @@ class NWProject():
# 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.projData = None # The full path to the project's data folder
self.projDict = None # The spell check dictionary
self.projFile = None # The file name of the project main XML file
@@ -195,6 +196,7 @@ class NWProject():
# Project Settings
self.projPath = None
self.projMeta = None
self.projData = None
self.projDict = None
self.projFile = nwFiles.PROJ_FILE
self.projName = ""
@@ -247,10 +249,13 @@ class NWProject():
logger.debug("Opening project: %s" % self.projPath)
self.projMeta = path.join(self.projPath,"meta")
self.projData = path.join(self.projPath,"content")
self.projDict = path.join(self.projMeta, nwFiles.PROJ_DICT)
if not self._checkFolder(self.projMeta):
return False
if not self._checkFolder(self.projData):
return False
if overrideLock:
self._clearLockFile()
@@ -291,7 +296,7 @@ class NWProject():
self.clearProject()
return False
xRoot = nwXML.getroot()
xRoot = nwXML.getroot()
nwxRoot = xRoot.tag
appVersion = "Unknown"
@@ -314,13 +319,40 @@ class NWProject():
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 novelWriter any more, so make sure you have a recent backup."
) % 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 +365,8 @@ class NWProject():
if msgRes != QMessageBox.Yes:
return False
# Start Parsing XML
# =================
for xChild in xRoot:
if xChild.tag == "project":
logger.debug("Found project meta")
@@ -404,16 +438,21 @@ 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")
self.projMeta = path.join(self.projPath, "meta")
self.projData = path.join(self.projPath, "content")
saveTime = time()
if not self._checkFolder(self.projPath):
return False
if not self._checkFolder(self.projMeta):
return False
if not self._checkFolder(self.projData):
return False
logger.debug("Saving project: %s" % self.projPath)
@@ -427,7 +466,7 @@ 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),
@@ -937,29 +976,20 @@ 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.projData):
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
@@ -1016,6 +1046,55 @@ class NWProject():
return True
def _updateStorage(self):
"""Updates the project storage folder from 1.0 to 1.1.
"""
contDir = path.join(self.projPath, "content")
self._checkFolder(contDir)
errList = []
for projItem in listdir(self.projPath):
itemPath = path.join(self.projPath, projItem)
if not path.isdir(itemPath) or not projItem.startswith("data_"):
continue
for dataFile in listdir(itemPath):
dataPath = path.join(itemPath, dataFile)
if dataFile.endswith(".bak"):
try:
unlink(dataPath)
logger.info("Deleted file: %s" % dataPath)
except:
errList.append("Failed to delete: %s" % dataPath)
elif dataFile.endswith(".nwd") and len(dataFile) == 21:
tHandle = projItem[-1]+dataFile[:12]
newPath = path.join(contDir, tHandle+".nwd")
try:
rename(dataPath, newPath)
logger.info("Moved file: %s" % dataPath)
logger.info("New location: %s" % newPath)
except:
errList.append("Failed to move: %s" % dataPath)
else:
newPath = path.join(self.projPath, "unknown_"+dataFile)
try:
rename(dataPath, newPath)
logger.info("Moved file: %s" % dataPath)
logger.info("New location: %s" % newPath)
except:
errList.append("Failed to move: %s" % dataPath)
try:
rmdir(itemPath)
logger.info("Removed folder: %s" % itemPath)
except:
errList.append("Failed to delete: %s" % itemPath)
if errList:
self.makeAlert(errList, nwAlert.ERROR)
return
# END Class NWProject
# ================================================================================================ #
+4 -4
View File
@@ -577,10 +577,10 @@ class Tokenizer():
"""Replaces the %keyword% strings.
"""
theTitle = theTitle.replace(r"%title%", theText)
theTitle = theTitle.replace(r"%chnum%", str(self.numChapter))
theTitle = theTitle.replace(r"%scnum%", str(self.numChScene))
theTitle = theTitle.replace(r"%scabsnum%", str(self.numAbsScene))
theTitle = theTitle.replace(r"%chnumword%", 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))
theTitle = theTitle.replace(r"%chw%", numberToWord(self.numChapter,"en"))
return theTitle
# END Class Tokenizer
+1 -6
View File
@@ -89,7 +89,7 @@ def projectMaintenance(theProject):
if path.isdir(theProject.projPath):
cacheDir = path.join(theProject.projPath, "cache")
if path.isdir(cacheDir):
logger.info("Deprecated cache folder found")
logger.info("Deprecated cache folder content found")
rmList = []
for i in range(10):
rmList.append(path.join(cacheDir, "nwProject.nwx.%d" % i))
@@ -101,11 +101,6 @@ def projectMaintenance(theProject):
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 = []
+77 -34
View File
@@ -39,7 +39,7 @@ from PyQt5.QtGui import (
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QTextBrowser, QPushButton, QLabel,
QLineEdit, QGroupBox, QGridLayout, QProgressBar, QMenu, QAction,
QFileDialog, QFontComboBox, QSpinBox
QFileDialog, QFontComboBox, QSpinBox, QDialogButtonBox
)
from nw.gui.additions import QSwitch
@@ -75,11 +75,11 @@ class GuiBuildNovel(QDialog):
self.nwdText = [] # List of markdown documents
self.setWindowTitle("Build Novel Project")
self.setMinimumWidth(800)
self.setMinimumWidth(900)
self.setMinimumHeight(800)
self.resize(
self.optState.getInt("GuiBuildNovel", "winWidth", 800),
self.optState.getInt("GuiBuildNovel", "winWidth", 900),
self.optState.getInt("GuiBuildNovel", "winHeight", 800)
)
@@ -95,30 +95,61 @@ class GuiBuildNovel(QDialog):
self.titleForm = QGridLayout(self)
self.titleGroup.setLayout(self.titleForm)
fmtHelp = (
r"<b>Formatting Codes:</b><br>"
r"%title% for the title as set in the document<br>"
r"%ch% for chapter number (1, 2, 3)<br>"
r"%chw% for chapter number as a word (one, two)<br>"
r"%sc% for scene number within chapter<br>"
r"%sca% for scene number within novel"
)
fmtScHelp = (
r"<br><br>"
r"Leave blank to skip this heading, or set to a static text, like "
r"for instance '* * *', to make a separator. The separator will "
r"be centred automatically and only appear between sections of "
r"the same type."
)
self.fmtTitle = QLineEdit()
self.fmtTitle.setMaxLength(200)
self.fmtTitle.setFixedWidth(200)
self.fmtTitle.setText(self.theProject.titleFormat["title"])
self.fmtTitle.setFixedWidth(220)
self.fmtTitle.setToolTip(fmtHelp)
self.fmtTitle.setText(
self._reFmtCodes(self.theProject.titleFormat["title"])
)
self.fmtChapter = QLineEdit()
self.fmtChapter.setMaxLength(200)
self.fmtChapter.setFixedWidth(200)
self.fmtChapter.setText(self.theProject.titleFormat["chapter"])
self.fmtChapter.setFixedWidth(220)
self.fmtChapter.setToolTip(fmtHelp)
self.fmtChapter.setText(
self._reFmtCodes(self.theProject.titleFormat["chapter"])
)
self.fmtUnnumbered = QLineEdit()
self.fmtUnnumbered.setMaxLength(200)
self.fmtUnnumbered.setFixedWidth(200)
self.fmtUnnumbered.setText(self.theProject.titleFormat["unnumbered"])
self.fmtUnnumbered.setFixedWidth(220)
self.fmtUnnumbered.setToolTip(fmtHelp)
self.fmtUnnumbered.setText(
self._reFmtCodes(self.theProject.titleFormat["unnumbered"])
)
self.fmtScene = QLineEdit()
self.fmtScene.setMaxLength(200)
self.fmtScene.setFixedWidth(200)
self.fmtScene.setText(self.theProject.titleFormat["scene"])
self.fmtScene.setFixedWidth(220)
self.fmtScene.setToolTip(fmtHelp + fmtScHelp)
self.fmtScene.setText(
self._reFmtCodes(self.theProject.titleFormat["scene"])
)
self.fmtSection = QLineEdit()
self.fmtSection.setMaxLength(200)
self.fmtSection.setFixedWidth(200)
self.fmtSection.setText(self.theProject.titleFormat["section"])
self.fmtSection.setFixedWidth(220)
self.fmtSection.setToolTip(fmtHelp + fmtScHelp)
self.fmtSection.setText(
self._reFmtCodes(self.theProject.titleFormat["section"])
)
self.titleForm.addWidget(QLabel("Title"), 0, 0, 1, 1, Qt.AlignLeft)
self.titleForm.addWidget(self.fmtTitle, 0, 1, 1, 1, Qt.AlignRight)
@@ -141,7 +172,8 @@ class GuiBuildNovel(QDialog):
self.textGroup.setLayout(self.textForm)
self.textFont = QFontComboBox()
self.textFont.setFixedWidth(200)
self.textFont.setFixedWidth(220)
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))
)
@@ -151,11 +183,13 @@ 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.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.setChecked(
self.optState.getBool("GuiBuildNovel", "justifyText", False)
)
@@ -177,12 +211,15 @@ class GuiBuildNovel(QDialog):
self.includeGroup.setLayout(self.includeForm)
self.includeSynopsis = QSwitch()
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.setChecked(self.theProject.titleFormat["withComments"])
self.includeKeywords = QSwitch()
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)
@@ -202,21 +239,34 @@ class GuiBuildNovel(QDialog):
self.addsGroup.setLayout(self.addsForm)
self.novelFiles = QSwitch()
self.novelFiles.setToolTip(
"Include files with layouts 'Book', 'Page', 'Partition', "
"'Chapter', 'Unnumbered', and 'Scene'."
)
self.novelFiles.setChecked(
self.optState.getBool("GuiBuildNovel", "addNovel", True)
)
self.noteFiles = QSwitch()
self.noteFiles.setToolTip("Include files with layout 'Note'.")
self.noteFiles.setChecked(
self.optState.getBool("GuiBuildNovel", "addNotes", False)
)
self.ignoreFlag = QSwitch()
self.ignoreFlag.setToolTip(
"Ignore the 'Include when building project' setting and include "
"all files in the output."
)
self.ignoreFlag.setChecked(
self.optState.getBool("GuiBuildNovel", "ignoreFlag", False)
)
self.excludeBody = QSwitch()
self.excludeBody.setToolTip(
"Exclude body text in the output. Combine with 'Include synopsis' "
"for making outline."
)
self.excludeBody.setChecked(
self.optState.getBool("GuiBuildNovel", "excludeBody", False)
)
@@ -279,13 +329,12 @@ class GuiBuildNovel(QDialog):
self.saveTXT.triggered.connect(lambda: self._saveDocument(self.FMT_TXT))
self.saveMenu.addAction(self.saveTXT)
self.btnClose = QPushButton("Close")
self.btnClose.clicked.connect(self._doClose)
self.buttonForm.addWidget(self.btnHelp, 0, 0)
self.buttonForm.addWidget(self.btnSave, 0, 0)
self.buttonForm.addWidget(self.btnPrint, 0, 1)
self.buttonForm.addWidget(self.btnSave, 1, 0)
self.buttonForm.addWidget(self.btnClose, 1, 1)
# Buttons
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close)
self.buttonBox.rejected.connect(self._doClose)
# Assemble GUI
# ============
@@ -303,6 +352,7 @@ class GuiBuildNovel(QDialog):
self.innerBox.addWidget(self.docView)
self.outerBox.addLayout(self.innerBox)
self.outerBox.addWidget(self.buttonBox)
self.setLayout(self.outerBox)
self.innerBox.setStretch(0, 0)
@@ -661,21 +711,14 @@ class GuiBuildNovel(QDialog):
return
def _showHelp(self):
"""Generate a help text and show it in the document window.
def _reFmtCodes(self, theFormat):
"""Translates old formatting codes to new ones.
"""
docName = "exportHelp_%s.htm" % self.mainConf.guiLang
docPath = path.join(self.mainConf.assetPath, "text", docName)
if path.isfile(docPath):
with open(docPath, mode="r", encoding="utf8") as inFile:
helpText = inFile.read()
self.docView.setStyleSheet()
self.docView.setContent(helpText)
else:
self.theParent.makeAlert(
"Could not open help text file for Build Project.", nwAlert.ERROR
)
return
theFormat = theFormat.replace(r"%chnum%", r"%ch%")
theFormat = theFormat.replace(r"%scnum%", r"%sc%")
theFormat = theFormat.replace(r"%scabsnum%", r"%sca%")
theFormat = theFormat.replace(r"%chnumword%", r"%chw%")
return theFormat
# END Class GuiBuildNovel
+1 -1
View File
@@ -93,7 +93,7 @@ class GuiProjectLoad(QDialog):
self.lblRecent = QLabel("<b>Recently Opened Projects</b>")
self.lblPath = QLabel("<b>Path</b>")
self.selPath = QLineEdit("")
self.selPath.setEnabled(False)
self.selPath.setReadOnly(True)
self.browseButton = QPushButton("...")
self.browseButton.setMaximumWidth(30)