From 3089c798dc3cf58d0d72a48306f1dd60663360b5 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Sat, 9 May 2020 19:26:28 +0200
Subject: [PATCH 01/24] Added the Build Project dialog and connected it to the
GUI in place of the Export tool
---
nw/gui/__init__.py | 14 ++++++-----
nw/gui/build.py | 58 ++++++++++++++++++++++++++++++++++++++++++++++
nw/gui/mainmenu.py | 10 ++++----
nw/guimain.py | 8 +++----
4 files changed, 75 insertions(+), 15 deletions(-)
create mode 100644 nw/gui/build.py
diff --git a/nw/gui/__init__.py b/nw/gui/__init__.py
index c59bfed9..7d4633b5 100644
--- a/nw/gui/__init__.py
+++ b/nw/gui/__init__.py
@@ -1,15 +1,16 @@
# -*- coding: utf-8 -*-
-# Qt Additions
-from nw.gui.additions.qconfiglayout import QConfigLayout
-from nw.gui.additions.qswitch import QSwitch
-
# Main Window Elements
+from nw.gui.build import GuiBuildNovel
from nw.gui.icons import GuiIcons
from nw.gui.mainmenu import GuiMainMenu
from nw.gui.statusbar import GuiMainStatus
from nw.gui.theme import GuiTheme
+# Qt Additions
+from nw.gui.additions.qconfiglayout import QConfigLayout
+from nw.gui.additions.qswitch import QSwitch
+
# Dialogs
from nw.gui.dialogs.configeditor import GuiConfigEditor
from nw.gui.dialogs.docmerge import GuiDocMerge
@@ -37,12 +38,13 @@ from nw.gui.tools.optionstate import OptionState
from nw.gui.tools.wordcounter import WordCounter
__all__ = [
- "QConfigLayout",
- "QSwitch",
+ "GuiBuildNovel",
"GuiIcons",
"GuiMainMenu",
"GuiMainStatus",
"GuiTheme",
+ "QConfigLayout",
+ "QSwitch",
"GuiConfigEditor",
"GuiDocMerge",
"GuiDocSplit",
diff --git a/nw/gui/build.py b/nw/gui/build.py
new file mode 100644
index 00000000..f08ae287
--- /dev/null
+++ b/nw/gui/build.py
@@ -0,0 +1,58 @@
+# -*- coding: utf-8 -*-
+"""novelWriter GUI Build Novel
+
+ novelWriter – GUI Build Novel
+===============================
+ Class holding the build novel window
+
+ File History:
+ Created: 2020-05-09 [0.5]
+
+ This file is a part of novelWriter
+ Copyright 2020, Veronica Berglyd Olsen
+
+ This program 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.
+
+ This program 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 GNU
+ General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see
Some text ...
") + + self.show() + + logger.debug("GuiBuildNovelDocView initialisation complete") + + return + +# END Class GuiBuildNovelDocView From 42b819acd9e92a74dcc111fa470900d9b0af6212 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 9 May 2020 20:05:42 +0200 Subject: [PATCH 03/24] Added exported option to NWItem --- nw/core/project.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/nw/core/project.py b/nw/core/project.py index ca1d7a55..7011a7da 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -1288,6 +1288,7 @@ class NWItem(): self.itemLayout = nwItemLayout.NO_LAYOUT self.itemStatus = None self.isExpanded = False + self.isExported = True # Document Meta Data self.charCount = 0 @@ -1314,6 +1315,7 @@ class NWItem(): 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)) + xSub = self._subPack(xPack,"exported", text=str(self.isExported)) if self.itemType == nwItemType.FILE: xSub = self._subPack(xPack,"layout", text=str(self.itemLayout.name)) xSub = self._subPack(xPack,"charCount", text=str(self.charCount), none=False) @@ -1347,6 +1349,7 @@ class NWItem(): "layout" : self.setLayout, "status" : self.setStatus, "expanded" : self.setExpanded, + "exported" : self.setExported, "charCount" : self.setCharCount, "wordCount" : self.setWordCount, "paraCount" : self.setParaCount, @@ -1447,6 +1450,13 @@ class NWItem(): self.isExpanded = expState == True return + def setExported(self, expState): + if isinstance(expState, str): + self.isExported = expState == str(True) + else: + self.isExported = expState == True + return + ## # Set Document Meta Data ## From ef40d4f2de9db2cc1106f9adcceee06f98a68ae1 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 9 May 2020 20:51:27 +0200 Subject: [PATCH 04/24] Added the new exported option to project tree and item editor --- nw/gui/dialogs/configeditor.py | 1 + nw/gui/dialogs/itemeditor.py | 57 +++++++++++++++++++++----------- nw/gui/dialogs/projecteditor.py | 5 +-- nw/gui/elements/doctree.py | 13 ++++++-- sample/sampleNovel/nwProject.nwx | 24 ++++++++++++-- 5 files changed, 74 insertions(+), 26 deletions(-) diff --git a/nw/gui/dialogs/configeditor.py b/nw/gui/dialogs/configeditor.py index 581d7307..e3bf4f7b 100644 --- a/nw/gui/dialogs/configeditor.py +++ b/nw/gui/dialogs/configeditor.py @@ -59,6 +59,7 @@ class GuiConfigEditor(QDialog): self.setWindowTitle("Preferences") self.guiDeco = self.theParent.theTheme.loadDecoration("settings",(64,64)) + self.outerBox.setSpacing(16) self.tabGeneral = GuiConfigEditGeneralTab(self.theParent) self.tabLayout = GuiConfigEditLayoutTab(self.theParent) diff --git a/nw/gui/dialogs/itemeditor.py b/nw/gui/dialogs/itemeditor.py index 30c4721c..fbd7d4dd 100644 --- a/nw/gui/dialogs/itemeditor.py +++ b/nw/gui/dialogs/itemeditor.py @@ -30,10 +30,11 @@ import nw from PyQt5.QtCore import Qt from PyQt5.QtWidgets import ( - QDialog, QHBoxLayout, QVBoxLayout, QGroupBox, QFormLayout, QLineEdit, - QPushButton, QComboBox + QDialog, QHBoxLayout, QVBoxLayout, QGroupBox, QGridLayout, QLineEdit, + QPushButton, QComboBox, QLabel, QSpacerItem, QSizePolicy, QDialogButtonBox ) +from nw.gui.additions import QSwitch from nw.constants import nwLabels, nwItemLayout, nwItemClass, nwItemType logger = logging.getLogger(__name__) @@ -48,22 +49,26 @@ class GuiItemEditor(QDialog): self.mainConf = nw.CONFIG self.theProject = theProject self.theParent = theParent - self.theItem = self.theProject.projTree[tHandle] - self.outerBox = QHBoxLayout() self.innerBox = QVBoxLayout() + self.theItem = self.theProject.projTree[tHandle] + if self.theItem is None: + self._doClose() + self.setWindowTitle("Item Settings") self.guiDeco = self.theParent.theTheme.loadDecoration("settings",(64,64)) + self.outerBox.setSpacing(16) self.setLayout(self.outerBox) self.outerBox.addWidget(self.guiDeco, 0, Qt.AlignTop) self.outerBox.addLayout(self.innerBox) self.mainGroup = QGroupBox("Item Settings") - self.mainForm = QFormLayout() + self.mainForm = QGridLayout() self.editName = QLineEdit() + self.editName.setMinimumWidth(220) self.editName.setMaxLength(200) self.editStatus = QComboBox() @@ -100,11 +105,21 @@ class GuiItemEditor(QDialog): if itemLayout in self.validLayouts: self.editLayout.addItem(nwLabels.LAYOUT_NAME[itemLayout],itemLayout) - self.mainForm.addRow("Label", self.editName) - self.mainForm.addRow("Status", self.editStatus) - self.mainForm.addRow("Layout", self.editLayout) + self.editExport = QSwitch() + self.editExport.setChecked(self.theItem.isExported) + self.textExport = QLabel("Include when building project") - self.editName.setMinimumWidth(200) + self.mainForm.addWidget(QLabel("Label"), 0, 0) + self.mainForm.addWidget(self.editName, 0, 1, 1, 2) + self.mainForm.addWidget(QLabel("Status"), 1, 0) + self.mainForm.addWidget(self.editStatus, 1, 1, 1, 2) + self.mainForm.addWidget(QLabel("Layout"), 2, 0) + self.mainForm.addWidget(self.editLayout, 2, 1, 1, 2) + self.mainForm.addWidget(self.textExport, 4, 0, 1, 2) + self.mainForm.addWidget(self.editExport, 4, 2) + + self.spacerItem = QSpacerItem(12, 12, QSizePolicy.Fixed, QSizePolicy.Fixed) + self.mainForm.addItem(self.spacerItem, 3, 0) self.editName.setText(self.theItem.itemName) statusIdx = self.editStatus.findData(self.theItem.itemStatus) @@ -114,19 +129,13 @@ class GuiItemEditor(QDialog): if layoutIdx != -1: self.editLayout.setCurrentIndex(layoutIdx) - self.buttonBox = QHBoxLayout() - self.closeButton = QPushButton("Close") - self.closeButton.clicked.connect(self._doClose) - self.saveButton = QPushButton("Save") - self.saveButton.setDefault(True) - self.saveButton.clicked.connect(self._doSave) - self.buttonBox.addStretch(1) - self.buttonBox.addWidget(self.closeButton) - self.buttonBox.addWidget(self.saveButton) + self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + self.buttonBox.accepted.connect(self._doSave) + self.buttonBox.rejected.connect(self._doClose) self.mainGroup.setLayout(self.mainForm) self.innerBox.addWidget(self.mainGroup) - self.innerBox.addLayout(self.buttonBox) + self.innerBox.addWidget(self.buttonBox) self.show() @@ -137,16 +146,26 @@ class GuiItemEditor(QDialog): return def _doSave(self): + """Save the setting to the item. + """ + logger.verbose("ItemEditor save button clicked") + itemName = self.editName.text() itemStatus = self.editStatus.currentData() itemLayout = self.editLayout.currentData() + isExported = self.editExport.isChecked() + self.theItem.setName(itemName) self.theItem.setStatus(itemStatus) self.theItem.setLayout(itemLayout) + self.theItem.setExported(isExported) + self.theProject.setProjectChanged(True) + self.accept() self.close() + return def _doClose(self): diff --git a/nw/gui/dialogs/projecteditor.py b/nw/gui/dialogs/projecteditor.py index ffcd2516..3df14269 100644 --- a/nw/gui/dialogs/projecteditor.py +++ b/nw/gui/dialogs/projecteditor.py @@ -54,10 +54,10 @@ class GuiProjectEditor(QDialog): self.outerBox = QHBoxLayout() self.innerBox = QVBoxLayout() - self.setWindowTitle("Project Settings") - self.setLayout(self.outerBox) + self.setWindowTitle("Project Settings") self.guiDeco = self.theParent.theTheme.loadDecoration("settings",(64,64)) + self.outerBox.setSpacing(16) self.theProject.countStatus() self.tabMain = GuiProjectEditMain(self.theParent, self.theProject) @@ -78,6 +78,7 @@ class GuiProjectEditor(QDialog): self.buttonBox.accepted.connect(self._doSave) self.buttonBox.rejected.connect(self._doClose) + self.setLayout(self.outerBox) self.innerBox.addWidget(self.tabWidget) self.innerBox.addWidget(self.buttonBox) diff --git a/nw/gui/elements/doctree.py b/nw/gui/elements/doctree.py index 92a006de..193295e1 100644 --- a/nw/gui/elements/doctree.py +++ b/nw/gui/elements/doctree.py @@ -36,7 +36,7 @@ from PyQt5.QtWidgets import ( from nw.core import NWDoc from nw.constants import ( - nwLabels, nwItemType, nwItemClass, nwItemLayout, nwAlert + nwLabels, nwItemType, nwItemClass, nwItemLayout, nwAlert, nwUnicode ) logger = logging.getLogger(__name__) @@ -254,6 +254,8 @@ class GuiDocTree(QTreeWidget): return theList def getColumnSizes(self): + """Return the column widths for the tree columns. + """ retVals = [ self.columnWidth(0), self.columnWidth(1), @@ -401,7 +403,8 @@ class GuiDocTree(QTreeWidget): return True def setTreeItemValues(self, tHandle): - + """Set the name and flag values for a tree item. + """ trItem = self._getTreeItem(tHandle) nwItem = self.theProject.projTree[tHandle] tName = nwItem.itemName @@ -409,7 +412,11 @@ class GuiDocTree(QTreeWidget): tHandle = nwItem.itemHandle pHandle = nwItem.parHandle - tStatus = nwLabels.CLASS_FLAG[nwItem.itemClass] + if nwItem.isExported: + tStatus = nwUnicode.U_CHECK + else: + tStatus = " " + tStatus += " "+nwLabels.CLASS_FLAG[nwItem.itemClass] if nwItem.itemType == nwItemType.FILE: tStatus += "."+nwLabels.LAYOUT_FLAG[nwItem.itemLayout] iStatus = nwItem.itemStatus diff --git a/sample/sampleNovel/nwProject.nwx b/sample/sampleNovel/nwProject.nwx index a452021d..68ac9522 100644 --- a/sample/sampleNovel/nwProject.nwx +++ b/sample/sampleNovel/nwProject.nwx @@ -1,5 +1,5 @@ -@%s\n" % tText @@ -167,6 +183,8 @@ class ToHtml(Tokenizer): return "
A brief guide to make the most out of the Build Project tool.
+ +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 replace all + occurrences of the keyword %title%. Any static text will be left as-is in the + final title. An empty field means the title isn't written out at all.
+The available formatting keywords are:
+%title% – This is replaced with the text you put in your headings in your + documents
+%num% – This is replaced with the chapter number of your chapter type + headings. These are generated automaticall starting from 1.
+%numword% – This is replaced with the chapter number of your chapter type + headings, but instead of an arabic number, the word for it is used instead, e.g. One, Two, + Fifteen, Twenty-Five, etc.
+\\ – Two backslashes are replaced by a line break.
+Note: The Scene format is treated slightly differently than the other title formats. If a + scene format is a constant text, that is, contains no %title%, it will be treated + as a scene separator instead. Scene separators are centred, and not shown if the chapter starts + directly on the first scene. If the field is blank, a large space between the scenes is added + instead.
+ +Novel Outline Mode: 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.
+ +Include Synopsis: This will add the synopsis comment as the first paragraph after each + heading.
+Include Comments: This will include any comments as additional paragraphs in the text.
+Include Keywords: This will include any keywords and tags as clickable links after each + heading.
+ +Include Novel Files: 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.
+Include Note Files: This means all files with a layout of type "Note" will be + included. Titles in note files are always left as they appear.
+Ignore Export Flag: 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.
diff --git a/nw/config.py b/nw/config.py index f0f21f32..e031cb79 100644 --- a/nw/config.py +++ b/nw/config.py @@ -84,6 +84,7 @@ class Config: self.guiTheme = "default" self.guiSyntax = "default_light" self.guiDark = False + self.guiLang = "en" # Hardcoded for now ## Sizes self.winGeometry = [1100, 650] diff --git a/nw/core/project.py b/nw/core/project.py index 8986aeb3..c964c75b 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -200,13 +200,14 @@ class NWProject(): self.bookAuthors = [] self.autoReplace = {} self.titleFormat = { - "title": "%title%", - "chapter": "Chapter %num%", - "chapterSub": "%title%", - "unnumbered": "%title%", - "scene": "", - "sceneSep": "* * *", - "section": "", + "title" : r"%title%", + "chapter" : r"Chapter %num%\\%title%", + "unnumbered" : r"%title%", + "scene" : r"* * *", + "section" : r"", + "withSynopsis" : False, + "withComments" : False, + "withKeywords" : False, } self.spellCheck = False self.autoOutline = True @@ -717,9 +718,11 @@ class NWProject(): def setTitleFormat(self, titleFormat): """Set the formatting of titles in the project. """ - for valKey in titleFormat: - if valKey in self.titleFormat: - self.titleFormat[valKey] = titleFormat[valKey] + for valKey, valEntry in titleFormat.items(): + if valKey in ("title","chapter","unnumbered","scene","section"): + self.titleFormat[valKey] = checkString(valEntry, self.titleFormat[valKey], False) + elif valKey in ("withSynopsis","withComments","withKeywords"): + self.titleFormat[valKey] = checkBool(valEntry, False, False) return def setProjectChanged(self, bValue): diff --git a/nw/core/tokenizer.py b/nw/core/tokenizer.py index f9b0264c..1cf2a40e 100644 --- a/nw/core/tokenizer.py +++ b/nw/core/tokenizer.py @@ -165,6 +165,11 @@ class Tokenizer(): return + def getResult(self): + """Return the result from the conversion. + """ + return self.theResult + def doAutoReplace(self): """Run through the user's auto-replace dictionary. """ diff --git a/nw/gui/build.py b/nw/gui/build.py index 82525d6f..401bf58e 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -28,18 +28,29 @@ import logging import nw +from os import path + from PyQt5.QtCore import Qt from PyQt5.QtGui import QTextOption, QPalette, QColor from PyQt5.QtWidgets import ( - QDialog, QVBoxLayout, QHBoxLayout, QTextBrowser + QDialog, QVBoxLayout, QHBoxLayout, QTextBrowser, QPushButton, + QLabel, QLineEdit, QGroupBox, QGridLayout, QComboBox, QProgressBar, + QMenu, QAction ) -from nw.constants import nwConst, nwFiles, nwAlert +from nw.gui.additions import QSwitch +from nw.core import ToHtml +from nw.constants import nwConst, nwFiles, nwAlert, nwItemType logger = logging.getLogger(__name__) class GuiBuildNovel(QDialog): + FMT_ODT = 1 + FMT_PDF = 2 + FMT_HTM = 3 + FMT_MD = 4 + def __init__(self, theParent, theProject): QDialog.__init__(self, theParent) @@ -48,28 +59,310 @@ class GuiBuildNovel(QDialog): self.mainConf = nw.CONFIG self.theProject = theProject self.theParent = theParent + self.theTheme = theParent.theTheme self.optState = self.theProject.optState self.setWindowTitle("Build Project") - self.setMinimumWidth(400) - self.setMinimumHeight(400) + self.setMinimumWidth(800) + self.setMinimumHeight(700) + + self.resize( + self.optState.getInt("GuiBuildNovel", "winWidth", 800), + self.optState.getInt("GuiBuildNovel", "winHeight", 700) + ) self.outerBox = QVBoxLayout() self.innerBox = QHBoxLayout() + self.toolsBox = QVBoxLayout() self.docView = GuiBuildNovelDocView(self, self.theProject) + # Title Formats + # ============= + self.titleGroup = QGroupBox("Novel Title Formats", self) + self.titleForm = QGridLayout(self) + self.titleGroup.setLayout(self.titleForm) + + self.fmtTitle = QLineEdit() + self.fmtTitle.setMaxLength(200) + self.fmtTitle.setFixedWidth(200) + self.fmtTitle.setText(self.theProject.titleFormat["title"]) + + self.fmtChapter = QLineEdit() + self.fmtChapter.setMaxLength(200) + self.fmtChapter.setFixedWidth(200) + self.fmtChapter.setText(self.theProject.titleFormat["chapter"]) + + self.fmtUnnumbered = QLineEdit() + self.fmtUnnumbered.setMaxLength(200) + self.fmtUnnumbered.setFixedWidth(200) + self.fmtUnnumbered.setText(self.theProject.titleFormat["unnumbered"]) + + self.fmtScene = QLineEdit() + self.fmtScene.setMaxLength(200) + self.fmtScene.setFixedWidth(200) + self.fmtScene.setText(self.theProject.titleFormat["scene"]) + + self.fmtSection = QLineEdit() + self.fmtSection.setMaxLength(200) + self.fmtSection.setFixedWidth(200) + self.fmtSection.setText(self.theProject.titleFormat["section"]) + + self.titleForm.addWidget(QLabel("Title"), 0, 0) + self.titleForm.addWidget(self.fmtTitle, 0, 1) + self.titleForm.addWidget(QLabel("Chapter"), 1, 0) + self.titleForm.addWidget(self.fmtChapter, 1, 1) + self.titleForm.addWidget(QLabel("Unnumbered"), 2, 0) + self.titleForm.addWidget(self.fmtUnnumbered, 2, 1) + self.titleForm.addWidget(QLabel("Scene"), 3, 0) + self.titleForm.addWidget(self.fmtScene, 3, 1) + self.titleForm.addWidget(QLabel("Section"), 4, 0) + self.titleForm.addWidget(self.fmtSection, 4, 1) + + self.titleForm.setColumnStretch(0, 1) + self.titleForm.setColumnStretch(1, 0) + + # Build Settings + # ============== + self.buildGroup = QGroupBox("Build Overrides", self) + self.buildForm = QGridLayout(self) + self.buildGroup.setLayout(self.buildForm) + + self.outlineMode = QSwitch() + self.outlineMode.setChecked(self.optState.getBool("GuiBuildNovel", "outlineMode", False)) + + self.buildForm.addWidget(QLabel("Novel Outline Mode"), 0, 0) + self.buildForm.addWidget(self.outlineMode, 0, 1) + + self.buildForm.setColumnStretch(0, 1) + self.buildForm.setColumnStretch(1, 0) + + # Include Switches + # ================ + self.includeGroup = QGroupBox("Include Non-Text Elements", self) + self.includeForm = QGridLayout(self) + self.includeGroup.setLayout(self.includeForm) + + self.includeSynopsis = QSwitch() + self.includeSynopsis.setChecked(self.theProject.titleFormat["withSynopsis"]) + self.includeComments = QSwitch() + self.includeComments.setChecked(self.theProject.titleFormat["withComments"]) + self.includeKeywords = QSwitch() + self.includeKeywords.setChecked(self.theProject.titleFormat["withKeywords"]) + + self.includeForm.addWidget(QLabel("Include Synopsis"), 0, 0) + self.includeForm.addWidget(self.includeSynopsis, 0, 1) + self.includeForm.addWidget(QLabel("Include Comments"), 1, 0) + self.includeForm.addWidget(self.includeComments, 1, 1) + self.includeForm.addWidget(QLabel("Include Keywords"), 2, 0) + self.includeForm.addWidget(self.includeKeywords, 2, 1) + + self.includeForm.setColumnStretch(0, 1) + self.includeForm.setColumnStretch(1, 0) + + # Additional Options + # ================== + self.addsGroup = QGroupBox("Additional Options", self) + self.addsForm = QGridLayout(self) + self.addsGroup.setLayout(self.addsForm) + + self.novelFiles = QSwitch() + self.novelFiles.setChecked(self.optState.getBool("GuiBuildNovel", "addNovel", True)) + self.noteFiles = QSwitch() + self.noteFiles.setChecked(self.optState.getBool("GuiBuildNovel", "addNotes", False)) + self.ignoreFlag = QSwitch() + self.ignoreFlag.setChecked(self.optState.getBool("GuiBuildNovel", "ignoreFlag", False)) + + self.addsForm.addWidget(QLabel("Include Novel Files"), 0, 0) + self.addsForm.addWidget(self.novelFiles, 0, 1) + self.addsForm.addWidget(QLabel("Include Note Files"), 1, 0) + self.addsForm.addWidget(self.noteFiles, 1, 1) + self.addsForm.addWidget(QLabel("Ignore Export Flag"), 2, 0) + self.addsForm.addWidget(self.ignoreFlag, 2, 1) + + self.addsForm.setColumnStretch(0, 1) + self.addsForm.setColumnStretch(1, 0) + + # Build Button + # ============ + self.buildProgress = QProgressBar() + + self.genPreview = QPushButton("Generate Preview") + self.genPreview.clicked.connect(self._buildPreview) + + # Action Buttons + # ============== + self.buttonForm = QGridLayout() + + self.btnHelp = QPushButton("Help") + self.btnHelp.clicked.connect(self._showHelp) + + self.btnPrint = QPushButton("Print") + self.btnPrint.clicked.connect(self._printDocument) + + self.btnSave = QPushButton("Save As") + self.saveMenu = QMenu(self) + self.saveODT = QAction("Open Document (.odt)") + self.savePDF = QAction("Portable Document (.pdf)") + self.saveHTM = QAction("HTML5 (.htm)") + self.saveMD = QAction("Markdown (.md)") + self.saveODT.triggered.connect(lambda: self._saveDocument(self.FMT_ODT)) + self.savePDF.triggered.connect(lambda: self._saveDocument(self.FMT_PDF)) + self.saveHTM.triggered.connect(lambda: self._saveDocument(self.FMT_HTM)) + self.saveMD.triggered.connect(lambda: self._saveDocument(self.FMT_MD)) + self.saveMenu.addAction(self.saveODT) + self.saveMenu.addAction(self.savePDF) + self.saveMenu.addAction(self.saveHTM) + self.saveMenu.addAction(self.saveMD) + self.btnSave.setMenu(self.saveMenu) + + self.btnClose = QPushButton("Close") + self.btnClose.clicked.connect(self._doClose) + + self.buttonForm.addWidget(self.btnHelp, 0, 0) + self.buttonForm.addWidget(self.btnPrint, 0, 1) + self.buttonForm.addWidget(self.btnSave, 1, 0) + self.buttonForm.addWidget(self.btnClose, 1, 1) + # Assemble GUI + # ============ + self.toolsBox.addWidget(self.titleGroup) + self.toolsBox.addWidget(self.buildGroup) + self.toolsBox.addWidget(self.includeGroup) + self.toolsBox.addWidget(self.addsGroup) + self.toolsBox.addStretch(1) + self.toolsBox.addWidget(self.buildProgress) + self.toolsBox.addWidget(self.genPreview) + self.toolsBox.addSpacing(8) + self.toolsBox.addLayout(self.buttonForm) + + self.innerBox.addLayout(self.toolsBox) self.innerBox.addWidget(self.docView) + self.outerBox.addLayout(self.innerBox) self.setLayout(self.outerBox) + self.innerBox.setStretch(0, 0) + self.innerBox.setStretch(1, 1) + + self.outlineMode.toggled.connect(self._toggelOutlineMode) + self._toggelOutlineMode(self.outlineMode.isChecked()) + self.show() logger.debug("GuiBuildNovel initialisation complete") return + ## + # Slots + ## + + def _buildPreview(self): + """Build a preview of the project in the document viewer. + """ + + makeHtml = ToHtml(self.theProject, self.theParent) + theText = "" + + for tItem in self.theProject.projTree: + if tItem is not None and tItem.itemType == nwItemType.FILE: + makeHtml.setText(tItem.itemHandle) + makeHtml.doAutoReplace() + makeHtml.tokenizeText() + makeHtml.doHeaders() + makeHtml.doConvert() + makeHtml.doPostProcessing() + theText += makeHtml.getResult() + + self.docView.setHtml(theText) + + return + + def _saveDocument(self, theFormat): + return + + def _printDocument(self): + return + + def _toggelOutlineMode(self, theState): + """Enables or disables the options that are overridden in# + outline mode. + """ + self.fmtTitle.setEnabled(not theState) + self.fmtChapter.setEnabled(not theState) + self.fmtUnnumbered.setEnabled(not theState) + self.fmtScene.setEnabled(not theState) + self.fmtSection.setEnabled(not theState) + self.includeSynopsis.setEnabled(not theState) + self.novelFiles.setEnabled(not theState) + self.noteFiles.setEnabled(not theState) + return + + def _doClose(self): + """Close button was clicked. + """ + self.close() + return + + ## + # Events + ## + + def closeEvent(self, theEvent): + """Capture the user closing the window so we can save settings. + """ + self._saveSettings() + QDialog.closeEvent(self, theEvent) + return + + ## + # Internal Functions + ## + + def _saveSettings(self): + """Save the various user settings. + """ + logger.debug("Saving GuiBuildNovel settings") + + # Formatting + self.theProject.setTitleFormat({ + "title" : self.fmtTitle.text().strip(), + "chapter" : self.fmtChapter.text().strip(), + "unnumbered" : self.fmtUnnumbered.text().strip(), + "scene" : self.fmtScene.text().strip(), + "section" : self.fmtSection.text().strip(), + "withSynopsis" : self.includeSynopsis.isChecked(), + "withComments" : self.includeComments.isChecked(), + "withKeywords" : self.includeKeywords.isChecked(), + }) + + # GUI Settings + self.optState.setValue("GuiBuildNovel", "winWidth", self.width()) + self.optState.setValue("GuiBuildNovel", "winHeight", self.height()) + self.optState.setValue("GuiBuildNovel", "outlineMode", self.outlineMode.isChecked()) + self.optState.setValue("GuiBuildNovel", "addNovel", self.novelFiles.isChecked()) + self.optState.setValue("GuiBuildNovel", "addNotes", self.noteFiles.isChecked()) + self.optState.setValue("GuiBuildNovel", "ignoreFlag", self.ignoreFlag.isChecked()) + self.optState.saveSettings() + + return + + def _showHelp(self): + """Generate a help text and show it in the document window. + """ + 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.setText(helpText) + else: + self.theParent.makeAlert( + "Could not open help text file for Build Project.", nwAlert.ERROR + ) + return + # END Class GuiBuildNovel class GuiBuildNovelDocView(QTextBrowser): @@ -83,10 +376,12 @@ class GuiBuildNovelDocView(QTextBrowser): self.theProject = theProject self.theParent = theParent - self.qDocument = self.document() - self.setMinimumWidth(300) + self.setMinimumWidth(400) self.setOpenExternalLinks(False) + self.qDocument = self.document() + self.qDocument.setDocumentMargin(self.mainConf.textMargin) + theOpt = QTextOption() if self.mainConf.doJustify: theOpt.setAlignment(Qt.AlignJustify) @@ -97,7 +392,7 @@ class GuiBuildNovelDocView(QTextBrowser): docPalette.setColor(QPalette.Text, QColor( 0, 0, 0)) self.setPalette(docPalette) - self.setHtml("Some text ...
") + self._makeStyleSheet() self.show() @@ -105,4 +400,32 @@ class GuiBuildNovelDocView(QTextBrowser): return + def setText(self, theText): + self.setHtml(theText) + return + + ## + # Internal Functions + ## + + def _makeStyleSheet(self): + + styleSheet = ( + "h1, h2 {" + " color: rgb(66, 113, 174);" + "}\n" + "h3, h4 {" + " color: rgb(50, 50, 50);" + "}\n" + "a {" + " color: rgb(137, 89, 168);" + "}\n" + "mark {" + " background-color: rgb(240, 198, 116);" + "}\n" + ) + self.qDocument.setDefaultStyleSheet(styleSheet) + + return + # END Class GuiBuildNovelDocView diff --git a/sample/sampleNovel/nwProject.nwx b/sample/sampleNovel/nwProject.nwx index c911b0a3..05445b4b 100644 --- a/sample/sampleNovel/nwProject.nwx +++ b/sample/sampleNovel/nwProject.nwx @@ -1,5 +1,5 @@ -%s
\n" % (hStyle,tText) + self.theResult += "%s
\n" % (hStyle, tText) elif tType == self.T_SKIP: self.theResult += "\n" + elif tType == self.T_PBREAK: + self.theResult += "
\n" + elif tType == self.T_TEXT: tTemp = tText for xPos, xLen, xFmt in reversed(tFormat): @@ -146,6 +162,9 @@ class ToHtml(Tokenizer): else: thisPar.append(tTemp.rstrip()+" ") + elif tType == self.T_SYNOPSIS and self.doSynopsis: + self.theResult += self._formatSynopsis(tText) + elif tType == self.T_COMMENT and self.doComments: self.theResult += self._formatComments(tText) @@ -158,12 +177,27 @@ class ToHtml(Tokenizer): # Internal Functions ## - def _formatKeywords(self, tText): - """Apply HTML formatting to keywords. + def _formatSynopsis(self, tText): + """Apply HTML formatting to synopsis. """ if not self.forPreview: - return "
@%s\n" % tText + return "
Synopsis: %s
\n" % tText + + return "%s
\n" % tText + + def _formatComments(self, tText): + """Apply HTML formatting to comments. + """ + + if not self.forPreview: + return "Comment: %s
\n" % tText + + return "%s
\n" % tText + + def _formatKeywords(self, tText): + """Apply HTML formatting to keywords. + """ tText = "@"+tText isValid, theBits, thePos = self.theParent.theIndex.scanThis(tText) @@ -182,13 +216,4 @@ class ToHtml(Tokenizer): return "%s
\n" % tText - # END Class ToHtml diff --git a/nw/core/tokenizer.py b/nw/core/tokenizer.py index 1cf2a40e..a39d12f2 100644 --- a/nw/core/tokenizer.py +++ b/nw/core/tokenizer.py @@ -40,29 +40,30 @@ logger = logging.getLogger(__name__) class Tokenizer(): - FMT_B_B = 1 # Begin bold - FMT_B_E = 2 # End bold - FMT_I_B = 3 # Begin italics - FMT_I_E = 4 # End italics - FMT_U_B = 5 # Begin underline - FMT_U_E = 6 # End underline + FMT_B_B = 1 # Begin bold + FMT_B_E = 2 # End bold + FMT_I_B = 3 # Begin italics + FMT_I_E = 4 # End italics + FMT_U_B = 5 # Begin underline + FMT_U_E = 6 # End underline - T_EMPTY = 1 # Empty line (new paragraph) - T_COMMENT = 2 # Comment line - T_KEYWORD = 3 # Command line - T_HEAD1 = 4 # Header 1 (title) - T_HEAD2 = 5 # Header 2 (chapter) - T_HEAD3 = 6 # Header 3 (scene) - T_HEAD4 = 7 # Header 4 - T_TEXT = 8 # Text line - T_SEP = 9 # Scene separator - T_SKIP = 10 # Paragraph break - T_PBREAK = 11 # Page break + T_EMPTY = 1 # Empty line (new paragraph) + T_SYNOPSIS = 2 # Synopsis comment + T_COMMENT = 3 # Comment line + T_KEYWORD = 4 # Command line + T_HEAD1 = 5 # Header 1 (title) + T_HEAD2 = 6 # Header 2 (chapter) + T_HEAD3 = 7 # Header 3 (scene) + T_HEAD4 = 8 # Header 4 + T_TEXT = 9 # Text line + T_SEP = 10 # Scene separator + T_SKIP = 11 # Paragraph break + T_PBREAK = 12 # Page break - A_LEFT = 1 # Left aligned - A_RIGHT = 2 # Right aligned - A_CENTRE = 3 # Centred - A_JUSTIFY = 4 # Justified + A_LEFT = 1 # Left aligned + A_RIGHT = 2 # Right aligned + A_CENTRE = 3 # Centred + A_JUSTIFY = 4 # Justified def __init__(self, theProject, theParent): @@ -78,8 +79,11 @@ class Tokenizer(): self.theResult = None # The result text after conversion # User Settings + self.doBodyText = True # Include body text + self.doSynopsis = False # Also process synopsis comments self.doComments = False # Also process comments self.doKeywords = False # Also process keywords like tags and references + self.doJustify = False # Justify text self.fmtTitle = "%title%" # Formatting for titles self.fmtChapter = "%title%" # Formatting for numbered chapters @@ -113,14 +117,6 @@ class Tokenizer(): # Setters ## - def setComments(self, doComments): - self.doComments = doComments - return - - def setKeywords(self, doKeywords): - self.doKeywords = doKeywords - return - def setTitleFormat(self, fmtTitle): self.fmtTitle = fmtTitle return @@ -143,6 +139,26 @@ class Tokenizer(): self.hideSection = hideSection return + def setBodyText(self, doBodyText): + self.doBodyText = doBodyText + return + + def setSynopsis(self, doSynopsis): + self.doSynopsis = doSynopsis + return + + def setComments(self, doComments): + self.doComments = doComments + return + + def setKeywords(self, doKeywords): + self.doKeywords = doKeywords + return + + def setJustify(self, doJustify): + self.doJustify = doJustify + return + ## # Class Methods ## @@ -207,25 +223,38 @@ class Tokenizer(): [None, self.FMT_U_B, None, self.FMT_U_E] )] + if self.doJustify: + defAlign = self.A_JUSTIFY + else: + defAlign = self.A_LEFT + self.theTokens = [] for aLine in self.theText.splitlines(): # Tag lines starting with specific characters if len(aLine.strip()) == 0: - self.theTokens.append((self.T_EMPTY,"",None,self.A_LEFT)) + self.theTokens.append((self.T_EMPTY, "", None, self.A_LEFT)) elif aLine[0] == "%": - self.theTokens.append((self.T_COMMENT,aLine[1:].strip(),None,self.A_LEFT)) + cLine = aLine[1:].strip() + if cLine.lower().startswith("synopsis:"): + self.theTokens.append((self.T_SYNOPSIS, cLine[9:].strip(), None, defAlign)) + else: + self.theTokens.append((self.T_COMMENT, aLine[1:].strip(), None, defAlign)) elif aLine[0] == "@": - self.theTokens.append((self.T_KEYWORD,aLine[1:].strip(),None,self.A_LEFT)) + self.theTokens.append((self.T_KEYWORD, aLine[1:].strip(), None, self.A_LEFT)) elif aLine[:2] == "# ": - self.theTokens.append((self.T_HEAD1,aLine[2:].strip(),None,self.A_LEFT)) + self.theTokens.append((self.T_HEAD1, aLine[2:].strip(), None, self.A_LEFT)) elif aLine[:3] == "## ": - self.theTokens.append((self.T_HEAD2,aLine[3:].strip(),None,self.A_LEFT)) + self.theTokens.append((self.T_HEAD2, aLine[3:].strip(), None, self.A_LEFT)) elif aLine[:4] == "### ": - self.theTokens.append((self.T_HEAD3,aLine[4:].strip(),None,self.A_LEFT)) + self.theTokens.append((self.T_HEAD3, aLine[4:].strip(), None, self.A_LEFT)) elif aLine[:5] == "#### ": - self.theTokens.append((self.T_HEAD4,aLine[5:].strip(),None,self.A_LEFT)) + self.theTokens.append((self.T_HEAD4, aLine[5:].strip(), None, self.A_LEFT)) else: + if not self.doBodyText: + # Skip all body text + continue + # Otherwise we use RegEx to find formatting tags within a line of text fmtPos = [] for theRX, theKeys in rxFormats: @@ -240,11 +269,11 @@ class Tokenizer(): # Save the line as is, but append the array of formatting locations # sorted by position - fmtPos = sorted(fmtPos,key=itemgetter(0)) - self.theTokens.append((self.T_TEXT,aLine,fmtPos,self.A_LEFT)) + fmtPos = sorted(fmtPos, key=itemgetter(0)) + self.theTokens.append((self.T_TEXT, aLine, fmtPos, defAlign)) # Always add an empty line at the end - self.theTokens.append((self.T_EMPTY,"",None,self.A_LEFT)) + self.theTokens.append((self.T_EMPTY, "", None, self.A_LEFT)) return @@ -283,37 +312,37 @@ class Tokenizer(): if not isUnNum: self.numChapter += 1 tText = self._formatChapter(tText,isUnNum) - self.theTokens[n] = (tType,tText,None,self.A_LEFT) + self.theTokens[n] = (tType, tText, None, self.A_LEFT) self.firstScene = True elif tType == self.T_HEAD3: tTemp = self._formatScene(tText) if tTemp == "" and self.hideScene: - self.theTokens[n] = (self.T_EMPTY,"",None,self.A_LEFT) + self.theTokens[n] = (self.T_EMPTY, "", None, self.A_LEFT) elif tTemp == "" and not self.hideScene: if self.firstScene: - self.theTokens[n] = (self.T_EMPTY,"",None,self.A_LEFT) + self.theTokens[n] = (self.T_EMPTY, "", None, self.A_LEFT) else: - self.theTokens[n] = (self.T_SKIP,"",None,self.A_LEFT) + self.theTokens[n] = (self.T_SKIP, "", None, self.A_LEFT) elif tTemp == self.fmtScene: if self.firstScene: - self.theTokens[n] = (self.T_EMPTY,"",None,self.A_LEFT) + self.theTokens[n] = (self.T_EMPTY, "", None, self.A_LEFT) else: - self.theTokens[n] = (self.T_SEP,tTemp,None,self.A_CENTRE) + self.theTokens[n] = (self.T_SEP, tTemp, None, self.A_CENTRE) else: - self.theTokens[n] = (tType,tTemp,None,self.A_LEFT) + self.theTokens[n] = (tType, tTemp, None, self.A_LEFT) self.firstScene = False elif tType == self.T_HEAD4: tTemp = self._formatSection(tText) if tTemp == "" and self.hideSection: - self.theTokens[n] = (self.T_EMPTY,"",None,self.A_LEFT) + self.theTokens[n] = (self.T_EMPTY, "", None, self.A_LEFT) elif tTemp == "" and not self.hideSection: - self.theTokens[n] = (self.T_SKIP,"",None,self.A_LEFT) + self.theTokens[n] = (self.T_SKIP, "", None, self.A_LEFT) elif tTemp == self.fmtSection: - self.theTokens[n] = (self.T_SEP,tTemp,None,self.A_CENTRE) + self.theTokens[n] = (self.T_SEP, tTemp, None, self.A_CENTRE) else: - self.theTokens[n] = (tType,tTemp,None,self.A_LEFT) + self.theTokens[n] = (tType, tTemp, None, self.A_LEFT) # For title page and partitions, we need to centre all text # and for some formats, we need a page break @@ -323,9 +352,9 @@ class Tokenizer(): tType = tToken[0] tText = tToken[1] tFormat = tToken[2] - self.theTokens[n] = (tType,tText,tFormat,self.A_CENTRE) + self.theTokens[n] = (tType, tText, tFormat, self.A_CENTRE) - self.theTokens.append((self.T_PBREAK,"",None,self.A_LEFT)) + self.theTokens.append((self.T_PBREAK, "", None, self.A_LEFT)) return diff --git a/nw/gui/build.py b/nw/gui/build.py index 94c82019..2e1e5f0f 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -42,7 +42,9 @@ from PyQt5.QtWidgets import ( from nw.gui.additions import QSwitch from nw.core import ToHtml -from nw.constants import nwConst, nwFiles, nwAlert, nwItemType +from nw.constants import ( + nwConst, nwFiles, nwAlert, nwItemType, nwItemLayout, nwItemClass +) logger = logging.getLogger(__name__) @@ -70,11 +72,11 @@ class GuiBuildNovel(QDialog): self.setWindowTitle("Build Project") self.setMinimumWidth(800) - self.setMinimumHeight(700) + self.setMinimumHeight(800) self.resize( self.optState.getInt("GuiBuildNovel", "winWidth", 800), - self.optState.getInt("GuiBuildNovel", "winHeight", 700) + self.optState.getInt("GuiBuildNovel", "winHeight", 800) ) self.outerBox = QVBoxLayout() @@ -128,6 +130,21 @@ class GuiBuildNovel(QDialog): self.titleForm.setColumnStretch(0, 1) self.titleForm.setColumnStretch(1, 0) + # Text Options + # ============= + self.textGroup = QGroupBox("Text Options", self) + self.textForm = QGridLayout(self) + self.textGroup.setLayout(self.textForm) + + self.justifyText = QSwitch() + self.justifyText.setChecked(self.optState.getBool("GuiBuildNovel", "justifyText", False)) + + self.textForm.addWidget(QLabel("Justify text"), 0, 0) + self.textForm.addWidget(self.justifyText, 0, 1) + + self.textForm.setColumnStretch(0, 1) + self.textForm.setColumnStretch(1, 0) + # Build Settings # ============== self.buildGroup = QGroupBox("Build Overrides", self) @@ -156,11 +173,11 @@ class GuiBuildNovel(QDialog): self.includeKeywords = QSwitch() self.includeKeywords.setChecked(self.theProject.titleFormat["withKeywords"]) - self.includeForm.addWidget(QLabel("Include Synopsis"), 0, 0) + self.includeForm.addWidget(QLabel("Include synopsis"), 0, 0) self.includeForm.addWidget(self.includeSynopsis, 0, 1) - self.includeForm.addWidget(QLabel("Include Comments"), 1, 0) + self.includeForm.addWidget(QLabel("Include comments"), 1, 0) self.includeForm.addWidget(self.includeComments, 1, 1) - self.includeForm.addWidget(QLabel("Include Keywords"), 2, 0) + self.includeForm.addWidget(QLabel("Include keywords"), 2, 0) self.includeForm.addWidget(self.includeKeywords, 2, 1) self.includeForm.setColumnStretch(0, 1) @@ -179,11 +196,11 @@ class GuiBuildNovel(QDialog): self.ignoreFlag = QSwitch() self.ignoreFlag.setChecked(self.optState.getBool("GuiBuildNovel", "ignoreFlag", False)) - self.addsForm.addWidget(QLabel("Include Novel Files"), 0, 0) + self.addsForm.addWidget(QLabel("Include novel files"), 0, 0) self.addsForm.addWidget(self.novelFiles, 0, 1) - self.addsForm.addWidget(QLabel("Include Note Files"), 1, 0) + self.addsForm.addWidget(QLabel("Include note files"), 1, 0) self.addsForm.addWidget(self.noteFiles, 1, 1) - self.addsForm.addWidget(QLabel("Ignore Export Flag"), 2, 0) + self.addsForm.addWidget(QLabel("Ignore export flag"), 2, 0) self.addsForm.addWidget(self.ignoreFlag, 2, 1) self.addsForm.setColumnStretch(0, 1) @@ -245,6 +262,7 @@ class GuiBuildNovel(QDialog): # Assemble GUI # ============ self.toolsBox.addWidget(self.titleGroup) + self.toolsBox.addWidget(self.textGroup) self.toolsBox.addWidget(self.buildGroup) self.toolsBox.addWidget(self.includeGroup) self.toolsBox.addWidget(self.addsGroup) @@ -280,11 +298,50 @@ class GuiBuildNovel(QDialog): """Build a preview of the project in the document viewer. """ - makeHtml = ToHtml(self.theProject, self.theParent) - self.htmlText = "" + # Get Settings + fmtTitle = self.fmtTitle.text().strip() + fmtChapter = self.fmtChapter.text().strip() + fmtUnnumbered = self.fmtUnnumbered.text().strip() + fmtScene = self.fmtScene.text().strip() + fmtSection = self.fmtSection.text().strip() + justifyText = self.justifyText.isChecked() + outlineMode = self.outlineMode.isChecked() + incSynopsis = self.includeSynopsis.isChecked() + incComments = self.includeComments.isChecked() + incKeywords = self.includeKeywords.isChecked() + novelFiles = self.novelFiles.isChecked() + noteFiles = self.noteFiles.isChecked() + ignoreFlag = self.ignoreFlag.isChecked() + doBodyText = True - for tItem in self.theProject.projTree: - if tItem is not None and tItem.itemType == nwItemType.FILE: + if outlineMode: + fmtTitle = "%title%" + fmtChapter = "Chapter: %title%" + fmtUnnumbered = "Chapter: %title%" + fmtScene = "Scene: %title%" + fmtSection = "Section: %title%" + doBodyText = False + incSynopsis = True + novelFiles = True + noteFiles = False + + makeHtml = ToHtml(self.theProject, self.theParent) + makeHtml.setTitleFormat(fmtTitle) + makeHtml.setChapterFormat(fmtChapter) + makeHtml.setUnNumberedFormat(fmtUnnumbered) + makeHtml.setSceneFormat(fmtScene, fmtScene == "") + makeHtml.setSectionFormat(fmtSection, fmtSection == "") + makeHtml.setBodyText(doBodyText) + makeHtml.setSynopsis(incSynopsis) + makeHtml.setComments(incComments) + makeHtml.setKeywords(incKeywords) + makeHtml.setJustify(justifyText) + + self.htmlText = "" + self.buildProgress.setMaximum(len(self.theProject.projTree)) + self.buildProgress.setValue(0) + for nItt, tItem in enumerate(self.theProject.projTree): + if self._checkInclude(tItem, noteFiles, novelFiles, ignoreFlag): makeHtml.setText(tItem.itemHandle) makeHtml.doAutoReplace() makeHtml.tokenizeText() @@ -292,11 +349,49 @@ class GuiBuildNovel(QDialog): makeHtml.doConvert() makeHtml.doPostProcessing() self.htmlText += makeHtml.getResult() + self.buildProgress.setValue(nItt+1) self.docView.setHtml(self.htmlText) return + def _checkInclude(self, theItem, noteFiles, novelFiles, ignoreFlag): + """This function checks whether a file should be included in the + export or not. For standard note and novel files, this is + controlled by the options selected by the user. For other files + classified as non-exportable, a few checks must be made, and the + following are not: + * Items that are not actual files. + * Items that have been orphaned which are tagged as NO_LAYOUT + and NO_CLASS. + * Items that appear in the TRASH folder or have parent set to + None (orphaned files). + """ + + if theItem is None: + return False + + if not theItem.isExported and not ignoreFlag: + return False + + isNone = theItem.itemType != nwItemType.FILE + isNone |= theItem.itemLayout == nwItemLayout.NO_LAYOUT + isNone |= theItem.itemClass == nwItemClass.NO_CLASS + isNone |= theItem.itemClass == nwItemClass.TRASH + isNone |= theItem.parHandle == self.theProject.projTree.trashRoot() + isNone |= theItem.parHandle is None + isNote = theItem.itemLayout == nwItemLayout.NOTE + isNovel = not isNone and not isNote + + if isNone: + return False + if isNote and not noteFiles: + return False + if isNovel and not novelFiles: + return False + + return True + def _saveDocument(self, theFormat): """Save the document to various formats. """ @@ -474,6 +569,7 @@ class GuiBuildNovel(QDialog): # GUI Settings self.optState.setValue("GuiBuildNovel", "winWidth", self.width()) self.optState.setValue("GuiBuildNovel", "winHeight", self.height()) + self.optState.setValue("GuiBuildNovel", "justifyText", self.justifyText.isChecked()) self.optState.setValue("GuiBuildNovel", "outlineMode", self.outlineMode.isChecked()) self.optState.setValue("GuiBuildNovel", "addNovel", self.novelFiles.isChecked()) self.optState.setValue("GuiBuildNovel", "addNotes", self.noteFiles.isChecked()) @@ -557,6 +653,10 @@ class GuiBuildNovelDocView(QTextBrowser): "mark {" " background-color: rgb(240, 198, 116);" "}\n" + ".tags {" + " color: rgb(245, 135, 31);" + " font-wright: bold;" + "}\n" ) self.qDocument.setDefaultStyleSheet(styleSheet) diff --git a/sample/sampleNovel/nwProject.nwx b/sample/sampleNovel/nwProject.nwx index 69ac815d..7b76aea0 100644 --- a/sample/sampleNovel/nwProject.nwx +++ b/sample/sampleNovel/nwProject.nwx @@ -1,5 +1,5 @@ -A brief guide to make the most out of the Build Project tool.
+A brief guide to make the most out of the Build Novel Project tool.
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 replace all - occurrences of the keyword %title%. Any static text will be left as-is in the + these settings. The actual title given in the headings of your files will for instance replace + all occurrences of the keyword %title%. Any static text will be left as-is in the final title. An empty field means the title isn't written out at all.
The available formatting keywords are:
%title% – This is replaced with the text you put in your headings in your documents
-%num% – This is replaced with the chapter number of your chapter type - headings. These are generated automaticall starting from 1.
-%numword% – This is replaced with the chapter number of your chapter type - headings, but instead of an arabic number, the word for it is used instead, e.g. One, Two, - Fifteen, Twenty-Five, etc.
+%chnum% – 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.
+%chnumword% – 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.
+%scnum% – 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.
+%scabsnum% – 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.
\\ – Two backslashes are replaced by a line break.
-Note: The Scene format is treated slightly differently than the other title formats. If a - scene format is a constant text, that is, contains no %title%, it will be treated - as a scene separator instead. Scene separators are centred, and not shown if the chapter starts - directly on the first scene. If the field is blank, a large space between the scenes is added - instead.
+Note: 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 %keyword% 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 * * * separator between scenes, just put that into the + scene format box, and nothing else.
Novel Outline Mode: This option will build an outline version of the novel rather than the
diff --git a/nw/core/tokenizer.py b/nw/core/tokenizer.py
index 84fbb100..f9d64d45 100644
--- a/nw/core/tokenizer.py
+++ b/nw/core/tokenizer.py
@@ -104,6 +104,8 @@ class Tokenizer():
# Instance Variables
self.numChapter = 0 # Counter for chapter numbers
+ self.numChScene = 0 # Counter for scene number within chapter
+ self.numAbsScene = 0 # Counter for scene number within novel
self.firstScene = False # Flag to indicate that the first scene of the chapter
return
@@ -370,23 +372,53 @@ class Tokenizer():
tType = tToken[0]
tText = tToken[1]
+ # In case we see text before a scene, we reset the flag
if tType == self.T_TEXT:
self.firstScene = False
- elif tType == self.T_HEAD2: # Novel Chapter
- if not isUnNum:
- self.numChapter += 1
- tText = self._formatChapter(tText,isUnNum)
+ elif tType == self.T_HEAD1:
+ # Main Title
+ # ==========
+
+ tText = self._formatHeading(self.fmtTitle, tText)
self.theTokens[n] = (
tType,
tText,
None,
self.A_LEFT | self.A_PBB_R
)
- self.firstScene = True
- elif tType == self.T_HEAD3: # Novel Scene
- tTemp = self._formatScene(tText)
+ elif tType == self.T_HEAD2:
+ # Novel Chapter
+ # =============
+
+ # Numbered or Unnumbered
+ if isUnNum:
+ tText = self._formatHeading(self.fmtUnNum, tText)
+ else:
+ self.numChapter += 1
+ tText = self._formatHeading(self.fmtChapter, tText)
+
+ # Format the chapter header
+ self.theTokens[n] = (
+ tType,
+ tText,
+ None,
+ self.A_LEFT | self.A_PBB_R
+ )
+
+ # Set scene variables
+ self.firstScene = True
+ self.numChScene = 0
+
+ elif tType == self.T_HEAD3:
+ # Novel Scene
+ # ===========
+
+ self.numChScene += 1
+ self.numAbsScene += 1
+
+ tTemp = self._formatHeading(self.fmtScene, tText)
if tTemp == "" and self.hideScene:
self.theTokens[n] = (
self.T_EMPTY,
@@ -431,10 +463,15 @@ class Tokenizer():
None,
self.A_LEFT | self.A_PBA_AV
)
+
+ # Definitely no longer the first scene
self.firstScene = False
- elif tType == self.T_HEAD4: # Novel Section
- tTemp = self._formatSection(tText)
+ elif tType == self.T_HEAD4:
+ # Novel Section
+ # =============
+
+ tTemp = self._formatHeading(self.fmtSection, tText)
if tTemp == "" and self.hideSection:
self.theTokens[n] = (
self.T_EMPTY,
@@ -500,38 +537,14 @@ class Tokenizer():
# Internal Functions
##
- def _formatTitle(self, theText):
- """Replace tokens for headers level 1.
+ def _formatHeading(self, theTitle, theText):
+ """Replaces the %keyword% strings.
"""
- theTitle = self.fmtTitle
- theTitle = theTitle.replace("%title%", theText)
- return theTitle
-
- def _formatChapter(self, theText, noNum):
- """Replace tokens for headers level 2.
- """
- if noNum:
- theTitle = self.fmtUnNum
- theTitle = theTitle.replace("%title%", theText)
- else:
- theTitle = self.fmtChapter
- theTitle = theTitle.replace("%title%", theText)
- theTitle = theTitle.replace("%num%", str(self.numChapter))
- theTitle = theTitle.replace("%numword%", numberToWord(self.numChapter,"en"))
- return theTitle
-
- def _formatScene(self, theText):
- """Replace tokens for headers level 3.
- """
- theTitle = self.fmtScene
- theTitle = theTitle.replace("%title%", theText)
- return theTitle
-
- def _formatSection(self, theText):
- """Replace tokens for headers level 4.
- """
- theTitle = self.fmtSection
- theTitle = theTitle.replace("%title%", theText)
+ 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"))
return theTitle
# END Class Tokenizer
diff --git a/nw/gui/build.py b/nw/gui/build.py
index 0cca7332..bac21b87 100644
--- a/nw/gui/build.py
+++ b/nw/gui/build.py
@@ -69,7 +69,7 @@ class GuiBuildNovel(QDialog):
self.htmlText = ""
- self.setWindowTitle("Build Project")
+ self.setWindowTitle("Build Novel Project")
self.setMinimumWidth(800)
self.setMinimumHeight(800)
@@ -315,11 +315,11 @@ class GuiBuildNovel(QDialog):
doBodyText = True
if outlineMode:
- fmtTitle = "%title%"
- fmtChapter = "Chapter: %title%"
- fmtUnnumbered = "Chapter: %title%"
- fmtScene = "Scene: %title%"
- fmtSection = "Section: %title%"
+ fmtTitle = r"%title%"
+ fmtChapter = r"Chapter %chnum%: %title%"
+ fmtUnnumbered = r"%title%"
+ fmtScene = r"Scene %chnum%.%scnum%: %title%"
+ fmtSection = r"Section: %title%"
doBodyText = False
incSynopsis = True
novelFiles = True
diff --git a/sample/sampleNovel/nwProject.nwx b/sample/sampleNovel/nwProject.nwx
index eb89f47c..97d54bc9 100644
--- a/sample/sampleNovel/nwProject.nwx
+++ b/sample/sampleNovel/nwProject.nwx
@@ -1,5 +1,5 @@
- %s %s %s Synopsis: %s %s %s Comment: %s %s %s %s %s %s %s
")
@@ -164,13 +170,11 @@ class ToHtml(Tokenizer):
self.theResult += "
")
- self.theResult += "%s
\n" % (hStyle, tHead)
+ tmpResult.append("%s
\n" % (hStyle, tHead))
elif tType == self.T_HEAD2:
tHead = tText.replace(r"\\", "
")
- self.theResult += "%s
\n" % (hStyle, tHead)
+ tmpResult.append("%s
\n" % (hStyle, tHead))
elif tType == self.T_HEAD3:
tHead = tText.replace(r"\\", "
")
- self.theResult += "%s
\n" % (hStyle, tHead)
+ tmpResult.append("%s
\n" % (hStyle, tHead))
elif tType == self.T_HEAD4:
tHead = tText.replace(r"\\", "
")
- self.theResult += "%s
\n" % (hStyle, tHead)
+ tmpResult.append("%s
\n" % (hStyle, tHead))
elif tType == self.T_SEP:
- self.theResult += "