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 . +""" + +import logging +import nw + +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import ( + QDialog +) + +from nw.constants import nwConst, nwFiles, nwAlert + +logger = logging.getLogger(__name__) + +class GuiBuildNovel(QDialog): + + def __init__(self, theParent, theProject): + QDialog.__init__(self, theParent) + + logger.debug("Initialising GuiBuildNovel ...") + + self.mainConf = nw.CONFIG + self.theProject = theProject + self.theParent = theParent + self.optState = self.theProject.optState + + self.show() + + logger.debug("GuiBuildNovel initialisation complete") + + return + +# END Class GuiBuildNovel diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py index a37ddf0c..ddc60daa 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -225,11 +225,11 @@ class GuiMainMenu(QMenuBar): self.projMenu.addAction(self.aProjectSettings) # Project > Export Project - self.aExportProject = QAction("Export Project", self) - self.aExportProject.setStatusTip("Export project") - self.aExportProject.setShortcut("F5") - self.aExportProject.triggered.connect(self.theParent.exportProjectDialog) - self.projMenu.addAction(self.aExportProject) + self.aBuildProject = QAction("Build Project", self) + self.aBuildProject.setStatusTip("Build project") + self.aBuildProject.setShortcut("F5") + self.aBuildProject.triggered.connect(self.theParent.buildProjectDialog) + self.projMenu.addAction(self.aBuildProject) # Project > Session Log self.aSessionLog = QAction("Session Log", self) diff --git a/nw/guimain.py b/nw/guimain.py index 2f1380bd..bbf047b0 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -40,10 +40,10 @@ from PyQt5.QtWidgets import ( ) from nw.gui import ( - GuiMainMenu, GuiMainStatus, GuiTheme, GuiDocTree, GuiDocEditor, GuiExport, + GuiMainMenu, GuiMainStatus, GuiTheme, GuiDocTree, GuiDocEditor, GuiDocViewer, GuiDocDetails, GuiSearchBar, GuiNoticeBar, GuiDocViewDetails, GuiConfigEditor, GuiProjectEditor, GuiItemEditor, GuiProjectOutline, - GuiSessionLogView, GuiDocMerge, GuiDocSplit, GuiProjectLoad + GuiSessionLogView, GuiDocMerge, GuiDocSplit, GuiProjectLoad, GuiBuildNovel ) from nw.core import NWProject, NWDoc, NWIndex, countWords from nw.constants import nwFiles, nwItemType, nwAlert @@ -755,9 +755,9 @@ class GuiMain(QMainWindow): self._setWindowTitle(self.theProject.projName) return True - def exportProjectDialog(self): + def buildProjectDialog(self): if self.hasProject: - dlgExport = GuiExport(self, self.theProject) + dlgExport = GuiBuildNovel(self, self.theProject) dlgExport.exec_() return True From 3b136bddbb0dfd8d29292acf4df50efb48cb2486 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 9 May 2020 20:02:19 +0200 Subject: [PATCH 02/24] Added document to build GUI --- nw/gui/build.py | 52 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/nw/gui/build.py b/nw/gui/build.py index f08ae287..82525d6f 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -29,8 +29,9 @@ import logging import nw from PyQt5.QtCore import Qt +from PyQt5.QtGui import QTextOption, QPalette, QColor from PyQt5.QtWidgets import ( - QDialog + QDialog, QVBoxLayout, QHBoxLayout, QTextBrowser ) from nw.constants import nwConst, nwFiles, nwAlert @@ -49,6 +50,20 @@ class GuiBuildNovel(QDialog): self.theParent = theParent self.optState = self.theProject.optState + self.setWindowTitle("Build Project") + self.setMinimumWidth(400) + self.setMinimumHeight(400) + + self.outerBox = QVBoxLayout() + self.innerBox = QHBoxLayout() + + self.docView = GuiBuildNovelDocView(self, self.theProject) + + # Assemble GUI + self.innerBox.addWidget(self.docView) + self.outerBox.addLayout(self.innerBox) + self.setLayout(self.outerBox) + self.show() logger.debug("GuiBuildNovel initialisation complete") @@ -56,3 +71,38 @@ class GuiBuildNovel(QDialog): return # END Class GuiBuildNovel + +class GuiBuildNovelDocView(QTextBrowser): + + def __init__(self, theParent, theProject): + QTextBrowser.__init__(self, theParent) + + logger.debug("Initialising GuiBuildNovelDocView ...") + + self.mainConf = nw.CONFIG + self.theProject = theProject + self.theParent = theParent + + self.qDocument = self.document() + self.setMinimumWidth(300) + self.setOpenExternalLinks(False) + + theOpt = QTextOption() + if self.mainConf.doJustify: + theOpt.setAlignment(Qt.AlignJustify) + self.qDocument.setDefaultTextOption(theOpt) + + docPalette = self.palette() + docPalette.setColor(QPalette.Base, QColor(255, 255, 255)) + docPalette.setColor(QPalette.Text, QColor( 0, 0, 0)) + self.setPalette(docPalette) + + self.setHtml("

Hello World

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 @@ - + Sample Project Sample Project @@ -10,7 +10,7 @@ True True - 96b68994dfa3d + 6a2d6d5f4f401 b3e74dbc1f584 875 @@ -41,6 +41,7 @@ NOVEL Started True + True Title Page @@ -48,6 +49,7 @@ NOVEL Started False + True TITLE 72 15 @@ -60,6 +62,7 @@ NOVEL 1st Draft True + True Chapter One @@ -67,6 +70,7 @@ NOVEL Notes False + True CHAPTER 12 3 @@ -79,6 +83,7 @@ NOVEL 1st Draft False + True SCENE 1199 216 @@ -91,6 +96,7 @@ NOVEL 1st Draft False + True SCENE 476 93 @@ -103,6 +109,7 @@ NOVEL Finished False + True UNNUMBERED 633 101 @@ -115,6 +122,7 @@ NOVEL 2nd Draft False + False NOTE 1692 313 @@ -127,6 +135,7 @@ NOVEL 1st Draft False + True CHAPTER 139 28 @@ -139,6 +148,7 @@ NOVEL 1st Draft False + True SCENE 189 37 @@ -151,6 +161,7 @@ CHARACTER None True + True Main Characters @@ -158,6 +169,7 @@ CHARACTER None True + True John Smith @@ -165,6 +177,7 @@ CHARACTER Minor False + True NOTE 49 9 @@ -177,6 +190,7 @@ CHARACTER Major False + True NOTE 55 9 @@ -189,6 +203,7 @@ WORLD None True + True Earth @@ -196,6 +211,7 @@ WORLD Main False + True NOTE 76 15 @@ -208,6 +224,7 @@ WORLD Minor False + True NOTE 115 24 @@ -220,6 +237,7 @@ WORLD Major False + True NOTE 28 6 @@ -232,6 +250,7 @@ TRASH None True + True Delete Me! @@ -239,6 +258,7 @@ NOVEL New False + True SCENE 30 6 From 91dd15ab57ed681fb394b46d7492678837705bfa Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 9 May 2020 21:48:35 +0200 Subject: [PATCH 05/24] Rewritten the tree details pane to show more info --- nw/gui/elements/docdetails.py | 140 +++++++++++++++++++++++++++------- 1 file changed, 112 insertions(+), 28 deletions(-) diff --git a/nw/gui/elements/docdetails.py b/nw/gui/elements/docdetails.py index a55e44c2..de547c06 100644 --- a/nw/gui/elements/docdetails.py +++ b/nw/gui/elements/docdetails.py @@ -32,7 +32,7 @@ from PyQt5.QtCore import Qt from PyQt5.QtGui import QFont from PyQt5.QtWidgets import QFrame, QGridLayout, QLabel -from nw.constants import nwLabels +from nw.constants import nwLabels, nwItemClass, nwUnicode logger = logging.getLogger(__name__) @@ -48,7 +48,7 @@ class GuiDocDetails(QFrame): self.mainBox = QGridLayout(self) self.mainBox.setVerticalSpacing(1) - self.mainBox.setHorizontalSpacing(15) + self.mainBox.setHorizontalSpacing(6) self.setLayout(self.mainBox) self.fntOne = QFont() @@ -56,50 +56,134 @@ class GuiDocDetails(QFrame): self.fntOne.setBold(True) self.fntTwo = QFont() + self.fntTwo.setFamily("Monospace") self.fntTwo.setPointSize(10) - self.colTwo = [ - QLabel(""), - QLabel(""), - QLabel(""), - QLabel("") - ] - colOne = ["Label","Status","Class","Layout"] - for nRow in range(4): - lblOne = QLabel(colOne[nRow]) - lblOne.setFont(self.fntOne) - lblOne.setAlignment(Qt.AlignTop) - self.mainBox.addWidget(lblOne,nRow,0) - self.mainBox.addWidget(self.colTwo[nRow],nRow,1) - self.colTwo[nRow].setWordWrap(True) - self.colTwo[nRow].setAlignment(Qt.AlignTop) + self.fntThree = QFont() + self.fntThree.setPointSize(10) + + # Label + self.labelName = QLabel("Label ") + self.labelName.setFont(self.fntOne) + self.labelName.setAlignment(Qt.AlignLeft | Qt.AlignBaseline) + + self.labelFlag = QLabel("") + self.labelFlag.setFont(self.fntTwo) + self.labelFlag.setAlignment(Qt.AlignRight | Qt.AlignBaseline) + + self.labelData = QLabel("") + self.labelData.setFont(self.fntThree) + self.labelData.setAlignment(Qt.AlignLeft | Qt.AlignBaseline) + self.labelData.setWordWrap(True) + + # Status + self.statusName = QLabel("Status ") + self.statusName.setFont(self.fntOne) + self.statusName.setAlignment(Qt.AlignLeft) + + self.statusFlag = QLabel("") + self.statusFlag.setFont(self.fntTwo) + self.statusFlag.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + + self.statusData = QLabel("") + self.statusData.setFont(self.fntThree) + self.statusData.setAlignment(Qt.AlignLeft) + + # Class + self.className = QLabel("Class ") + self.className.setFont(self.fntOne) + self.className.setAlignment(Qt.AlignLeft) + + self.classFlag = QLabel("") + self.classFlag.setFont(self.fntTwo) + self.classFlag.setAlignment(Qt.AlignRight) + + self.classData = QLabel("") + self.classData.setFont(self.fntThree) + self.classData.setAlignment(Qt.AlignLeft) + + # Layout + self.layoutName = QLabel("Layout ") + self.layoutName.setFont(self.fntOne) + self.layoutName.setAlignment(Qt.AlignLeft) + + self.layoutFlag = QLabel("") + self.layoutFlag.setFont(self.fntTwo) + self.layoutFlag.setAlignment(Qt.AlignRight) + + self.layoutData = QLabel("") + self.layoutData.setFont(self.fntThree) + self.layoutData.setAlignment(Qt.AlignLeft) + + # Assemble + self.mainBox.addWidget(self.labelName, 0, 0) + self.mainBox.addWidget(self.statusName, 1, 0) + self.mainBox.addWidget(self.className, 2, 0) + self.mainBox.addWidget(self.layoutName, 3, 0) + self.mainBox.addWidget(self.labelFlag, 0, 1) + self.mainBox.addWidget(self.statusFlag, 1, 1) + self.mainBox.addWidget(self.classFlag, 2, 1) + self.mainBox.addWidget(self.layoutFlag, 3, 1) + self.mainBox.addWidget(self.labelData, 0, 2) + self.mainBox.addWidget(self.statusData, 1, 2) + self.mainBox.addWidget(self.classData, 2, 2) + self.mainBox.addWidget(self.layoutData, 3, 2) self.mainBox.setColumnStretch(0,0) - self.mainBox.setColumnStretch(1,1) + self.mainBox.setColumnStretch(1,0) + self.mainBox.setColumnStretch(2,1) logger.debug("DocDetails initialisation complete") return - def buildViewBox(self, tHandle): + ### + # Class Methods + ## + + def updateViewBox(self, tHandle): + """Populate the details box from a given handle. + """ nwItem = self.theProject.projTree[tHandle] if nwItem is None: - colTwo = [""]*4 + self.labelFlag.setText("") + self.statusFlag.setText("") + self.classFlag.setText("") + self.layoutFlag.setText("") + self.labelData.setText("") + self.statusData.setText("") + self.classData.setText("") + self.layoutData.setText("") + else: theLabel = nwItem.itemName if len(theLabel) > 100: theLabel = theLabel[:96].rstrip()+" ..." - colTwo = [ - theLabel, - nwItem.itemStatus, - nwLabels.CLASS_NAME[nwItem.itemClass], - nwLabels.LAYOUT_NAME[nwItem.itemLayout], - ] - for nRow in range(4): - self.colTwo[nRow].setText(colTwo[nRow]) + iStatus = nwItem.itemStatus + if nwItem.itemClass == nwItemClass.NOVEL: + iStatus = self.theProject.statusItems.checkEntry(iStatus) # Make sure it's valid + flagIcon = self.theParent.statusIcons[iStatus] + else: + iStatus = self.theProject.importItems.checkEntry(iStatus) # Make sure it's valid + flagIcon = self.theParent.importIcons[iStatus] + + if nwItem.isExported: + exportFlag = nwUnicode.U_CHECK + else: + exportFlag = " " + + self.labelFlag.setText(exportFlag) + self.statusFlag.setPixmap(flagIcon.pixmap(10, 10)) + self.classFlag.setText(nwLabels.CLASS_FLAG[nwItem.itemClass]) + self.layoutFlag.setText(nwLabels.LAYOUT_FLAG[nwItem.itemLayout]) + + self.labelData.setText(theLabel) + self.statusData.setText(nwItem.itemStatus) + self.classData.setText(nwLabels.CLASS_NAME[nwItem.itemClass]) + self.layoutData.setText(nwLabels.LAYOUT_NAME[nwItem.itemLayout]) return From 1ad156054c4895f7e5a01b07e03c4ff23c42aa1f Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 9 May 2020 21:49:05 +0200 Subject: [PATCH 06/24] Make sure information is synced properly when item label is changed --- nw/gui/dialogs/itemeditor.py | 4 ++-- nw/gui/elements/doceditor.py | 8 ++++++++ nw/gui/elements/doctree.py | 4 ++-- nw/gui/elements/docviewer.py | 8 ++++++++ nw/guimain.py | 5 ++++- 5 files changed, 24 insertions(+), 5 deletions(-) diff --git a/nw/gui/dialogs/itemeditor.py b/nw/gui/dialogs/itemeditor.py index fbd7d4dd..432ddb08 100644 --- a/nw/gui/dialogs/itemeditor.py +++ b/nw/gui/dialogs/itemeditor.py @@ -44,7 +44,7 @@ class GuiItemEditor(QDialog): def __init__(self, theParent, theProject, tHandle): QDialog.__init__(self, theParent) - logger.debug("Initialising ItemEditor ...") + logger.debug("Initialising GuiItemEditor ...") self.mainConf = nw.CONFIG self.theProject = theProject @@ -141,7 +141,7 @@ class GuiItemEditor(QDialog): self.editName.selectAll() - logger.debug("ItemEditor initialisation complete") + logger.debug("GuiItemEditor initialisation complete") return diff --git a/nw/gui/elements/doceditor.py b/nw/gui/elements/doceditor.py index 96966ddd..8aa583da 100644 --- a/nw/gui/elements/doceditor.py +++ b/nw/gui/elements/doceditor.py @@ -352,6 +352,14 @@ class GuiDocEditor(QTextEdit): return + def updateDocTitle(self, tHandle): + """Called when an item label is changed to check if the document + title bar needs updating, + """ + if tHandle == self.theHandle: + self.docTitle.setTitleFromHandle(self.theHandle) + return + ## # Setters and Getters ## diff --git a/nw/gui/elements/doctree.py b/nw/gui/elements/doctree.py index 193295e1..0c6b8bb2 100644 --- a/nw/gui/elements/doctree.py +++ b/nw/gui/elements/doctree.py @@ -526,8 +526,8 @@ class GuiDocTree(QTreeWidget): newItem.setText(self.C_HANDLE, tHandle) # newItem.setForeground(self.C_COUNT,QColor(*self.theParent.theTheme.treeWCount)) - newItem.setTextAlignment(self.C_COUNT,Qt.AlignRight) - newItem.setFont(self.C_FLAGS,self.fontFlags) + newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight) + newItem.setFont(self.C_FLAGS, self.fontFlags) self.theMap[tHandle] = newItem if pHandle is None: diff --git a/nw/gui/elements/docviewer.py b/nw/gui/elements/docviewer.py index af51e908..9f3d65ff 100644 --- a/nw/gui/elements/docviewer.py +++ b/nw/gui/elements/docviewer.py @@ -192,6 +192,14 @@ class GuiDocViewer(QTextBrowser): return False return True + def updateDocTitle(self, tHandle): + """Called when an item label is changed to check if the document + title bar needs updating, + """ + if tHandle == self.theHandle: + self.docTitle.setTitleFromHandle(self.theHandle) + return + ## # Events ## diff --git a/nw/guimain.py b/nw/guimain.py index bbf047b0..c76e9c6f 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -629,6 +629,9 @@ class GuiMain(QMainWindow): dlgProj = GuiItemEditor(self, self.theProject, tHandle) if dlgProj.exec_(): self.treeView.setTreeItemValues(tHandle) + self.treeMeta.updateViewBox(tHandle) + self.docEditor.updateDocTitle(tHandle) + self.docViewer.updateDocTitle(tHandle) return @@ -1006,7 +1009,7 @@ class GuiMain(QMainWindow): def _treeSingleClick(self): sHandle = self.treeView.getSelectedHandle() if sHandle is not None: - self.treeMeta.buildViewBox(sHandle) + self.treeMeta.updateViewBox(sHandle) return def _treeDoubleClick(self, tItem, colNo): From 33f4626aec977ada9e36c34c3d1cb6471c4c13f9 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 10 May 2020 13:51:22 +0200 Subject: [PATCH 07/24] Moved the convert classes we want to keep to the core folder. The rest will be deleted. --- nw/convert/__init__.py | 27 --------------------------- nw/core/__init__.py | 4 ++++ nw/{convert/text => core}/tohtml.py | 2 +- nw/{convert => core}/tokenizer.py | 0 nw/gui/__init__.py | 2 -- nw/gui/dialogs/__init__.py | 2 -- nw/gui/elements/docviewer.py | 2 +- 7 files changed, 6 insertions(+), 33 deletions(-) delete mode 100644 nw/convert/__init__.py rename nw/{convert/text => core}/tohtml.py (99%) rename nw/{convert => core}/tokenizer.py (100%) diff --git a/nw/convert/__init__.py b/nw/convert/__init__.py deleted file mode 100644 index ea50119a..00000000 --- a/nw/convert/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- coding: utf-8 -*- - -from nw.convert.tokenizer import Tokenizer - -from nw.convert.file.concat import ConcatFile -from nw.convert.file.html import HtmlFile -from nw.convert.file.latex import LaTeXFile -from nw.convert.file.markdown import MarkdownFile -from nw.convert.file.text import TextFile - -from nw.convert.text.tohtml import ToHtml -from nw.convert.text.tolatex import ToLaTeX -from nw.convert.text.tomarkdown import ToMarkdown -from nw.convert.text.totext import ToText - -__all__ = [ - "Tokenizer", - "ConcatFile", - "HtmlFile", - "LaTeXFile", - "MarkdownFile", - "TextFile", - "ToHtml", - "ToLaTeX", - "ToMarkdown", - "ToText", -] diff --git a/nw/core/__init__.py b/nw/core/__init__.py index a608acd7..6dd65fd2 100644 --- a/nw/core/__init__.py +++ b/nw/core/__init__.py @@ -6,6 +6,8 @@ from nw.core.project import NWProject from nw.core.spellcheck import NWSpellCheck from nw.core.spellcheck import NWSpellEnchant from nw.core.spellcheck import NWSpellSimple +from nw.core.tokenizer import Tokenizer +from nw.core.tohtml import ToHtml from nw.core.tools import countWords from nw.core.tools import projectMaintenance from nw.core.tools import numberToWord @@ -17,6 +19,8 @@ __all__ = [ "NWSpellCheck", "NWSpellEnchant", "NWSpellSimple", + "Tokenizer", + "ToHtml", "countWords", "projectMaintenance", "numberToWord", diff --git a/nw/convert/text/tohtml.py b/nw/core/tohtml.py similarity index 99% rename from nw/convert/text/tohtml.py rename to nw/core/tohtml.py index 330e7bc8..aa63eaff 100644 --- a/nw/convert/text/tohtml.py +++ b/nw/core/tohtml.py @@ -29,7 +29,7 @@ import logging import re import nw -from nw.convert.tokenizer import Tokenizer +from nw.core.tokenizer import Tokenizer from nw.constants import nwUnicode, nwLabels logger = logging.getLogger(__name__) diff --git a/nw/convert/tokenizer.py b/nw/core/tokenizer.py similarity index 100% rename from nw/convert/tokenizer.py rename to nw/core/tokenizer.py diff --git a/nw/gui/__init__.py b/nw/gui/__init__.py index 7d4633b5..c209b8d8 100644 --- a/nw/gui/__init__.py +++ b/nw/gui/__init__.py @@ -15,7 +15,6 @@ from nw.gui.additions.qswitch import QSwitch from nw.gui.dialogs.configeditor import GuiConfigEditor from nw.gui.dialogs.docmerge import GuiDocMerge from nw.gui.dialogs.docsplit import GuiDocSplit -from nw.gui.dialogs.export import GuiExport from nw.gui.dialogs.itemeditor import GuiItemEditor from nw.gui.dialogs.projecteditor import GuiProjectEditor from nw.gui.dialogs.projectload import GuiProjectLoad @@ -48,7 +47,6 @@ __all__ = [ "GuiConfigEditor", "GuiDocMerge", "GuiDocSplit", - "GuiExport", "GuiItemEditor", "GuiProjectEditor", "GuiProjectLoad", diff --git a/nw/gui/dialogs/__init__.py b/nw/gui/dialogs/__init__.py index 2dbc503e..110b5b5e 100644 --- a/nw/gui/dialogs/__init__.py +++ b/nw/gui/dialogs/__init__.py @@ -3,7 +3,6 @@ from nw.gui.dialogs.configeditor import GuiConfigEditor from nw.gui.dialogs.docmerge import GuiDocMerge from nw.gui.dialogs.docsplit import GuiDocSplit -from nw.gui.dialogs.export import GuiExport from nw.gui.dialogs.itemeditor import GuiItemEditor from nw.gui.dialogs.projecteditor import GuiProjectEditor from nw.gui.dialogs.projectload import GuiProjectLoad @@ -13,7 +12,6 @@ __all__ = [ "GuiConfigEditor", "GuiDocMerge", "GuiDocSplit", - "GuiExport", "GuiItemEditor", "GuiProjectEditor", "GuiProjectLoad", diff --git a/nw/gui/elements/docviewer.py b/nw/gui/elements/docviewer.py index 9f3d65ff..c99b1de1 100644 --- a/nw/gui/elements/docviewer.py +++ b/nw/gui/elements/docviewer.py @@ -32,7 +32,7 @@ from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QTextBrowser from PyQt5.QtGui import QTextOption, QFont, QPalette, QColor, QTextCursor -from nw.convert import ToHtml +from nw.core import ToHtml from nw.constants import nwAlert, nwItemType, nwDocAction from nw.gui.elements.doctitlebar import GuiDocTitleBar From 2eaa707f4ed22b70ae81f6fcf202d8a80255201d Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 10 May 2020 14:44:44 +0200 Subject: [PATCH 08/24] Some cleanup of the tokenizer and tohtml classes --- nw/convert/file/text.py | 2 +- nw/core/tohtml.py | 22 ++++++++- nw/core/tokenizer.py | 98 +++++++++++++++++++++++++---------------- 3 files changed, 80 insertions(+), 42 deletions(-) diff --git a/nw/convert/file/text.py b/nw/convert/file/text.py index ceab8d55..afcadb10 100644 --- a/nw/convert/file/text.py +++ b/nw/convert/file/text.py @@ -143,7 +143,7 @@ class TextFile(): self.theConv.setText(tHandle) self.theConv.doAutoReplace() self.theConv.tokenizeText() - self.theConv.doHeaders() + self.theConv.formatHeaders() self.theConv.doConvert() self.theConv.doPostProcessing() diff --git a/nw/core/tohtml.py b/nw/core/tohtml.py index aa63eaff..0b786ad6 100644 --- a/nw/core/tohtml.py +++ b/nw/core/tohtml.py @@ -41,6 +41,10 @@ class ToHtml(Tokenizer): self.forPreview = False return + ## + # Setters + ## + def setPreview(self, forPreview, doComments): """If we're using this class to generate markdown preview, we need to make a few changes to formatting, which is selected by @@ -52,7 +56,14 @@ class ToHtml(Tokenizer): self.doComments = doComments return + ## + # Class Methods + ## + def doAutoReplace(self): + """Extend the auto-replace to also properly encode some unicode + characters into their respective HTML entities. + """ Tokenizer.doAutoReplace(self) if self.forPreview: @@ -76,6 +87,9 @@ class ToHtml(Tokenizer): return def doConvert(self): + """Convert the list of text tokens into a HTML document saved + to theResult. + """ htmlTags = { self.FMT_B_B : "", @@ -136,7 +150,7 @@ class ToHtml(Tokenizer): self.theResult += self._formatComments(tText) elif tType == self.T_KEYWORD and self.doKeywords: - self.theResult += self._formatTags(tText) + self.theResult += self._formatKeywords(tText) return @@ -144,7 +158,9 @@ class ToHtml(Tokenizer): # Internal Functions ## - def _formatTags(self, tText): + def _formatKeywords(self, tText): + """Apply HTML formatting to keywords. + """ if not self.forPreview: return "
@%s
\n" % tText @@ -167,6 +183,8 @@ class ToHtml(Tokenizer): return "
%s
" % retText def _formatComments(self, tText): + """Apply HTML formatting to comments. + """ if not self.forPreview: return "
%s
\n" % tText diff --git a/nw/core/tokenizer.py b/nw/core/tokenizer.py index 4ac99377..f9b0264c 100644 --- a/nw/core/tokenizer.py +++ b/nw/core/tokenizer.py @@ -70,30 +70,45 @@ class Tokenizer(): self.theProject = theProject self.theParent = theParent - self.theText = None - self.theHandle = None - self.theItem = None - self.theTokens = None - self.theResult = None + # Data Variables + self.theText = None # The raw text to be tokenized + self.theHandle = None # The handle associated with the text + self.theItem = None # The NWItem associated with the handle + self.theTokens = None # The list of the processed tokens + self.theResult = None # The result text after conversion - self.wordWrap = 0 - self.doComments = False - self.doKeywords = False + # User Settings + self.doComments = False # Also process comments + self.doKeywords = False # Also process keywords like tags and references - self.fmtTitle = "%title%" - self.fmtChapter = "%title%" - self.fmtUnNum = "%title%" - self.fmtScene = "%title%" - self.fmtSection = "%title%" + self.fmtTitle = "%title%" # Formatting for titles + self.fmtChapter = "%title%" # Formatting for numbered chapters + self.fmtUnNum = "%title%" # Formatting for unnumbered chapters + self.fmtScene = "%title%" # Formatting for scenes + self.fmtSection = "%title%" # Formatting for sections - self.hideScene = False - self.hideSection = False + self.hideScene = False # Do not include scene headers + self.hideSection = False # Do not include section headers - self.numChapter = 0 - self.firstScene = False + # Instance Variables + self.numChapter = 0 # Counter for chapter numbers + self.firstScene = False # Flag to indicate that the first scene of the chapter return + def clearData(self): + """Clear the data arrays and variables, but not settings, so the class + can be reused for multiple documents. + """ + self.theText = None + self.theHandle = None + self.theItem = None + self.theTokens = None + self.theResult = None + self.numChapter = 0 + self.firstScene = False + return + ## # Setters ## @@ -106,13 +121,6 @@ class Tokenizer(): self.doKeywords = doKeywords return - def setWordWrap(self, wordWrap): - if wordWrap >= 0: - self.wordWrap = wordWrap - else: - self.wordWrap = 0 - return - def setTitleFormat(self, fmtTitle): self.fmtTitle = fmtTitle return @@ -140,6 +148,9 @@ class Tokenizer(): ## def setText(self, theHandle, theText=None): + """Set the text for the tokenizer from a handle. If theText is + not set, load it from the file. + """ self.theHandle = theHandle self.theItem = self.theProject.projTree[theHandle] @@ -155,12 +166,16 @@ class Tokenizer(): return def doAutoReplace(self): + """Run through the user's auto-replace dictionary. + """ + if len(self.theProject.autoReplace) > 0: repDict = {} for aKey, aVal in self.theProject.autoReplace.items(): repDict["<%s>" % aKey] = aVal xRep = re.compile("|".join([re.escape(k) for k in repDict.keys()]), flags=re.DOTALL) self.theText = xRep.sub(lambda x: repDict[x.group(0)], self.theText) + return def doPostProcessing(self): @@ -229,6 +244,9 @@ class Tokenizer(): return def doHeaders(self): + """Apply formatting to the text headers according to document + layout and user settings. + """ isNone = self.theItem.itemLayout == nwItemLayout.NO_LAYOUT isTitle = self.theItem.itemLayout == nwItemLayout.TITLE @@ -241,8 +259,8 @@ class Tokenizer(): isNote = self.theItem.itemLayout == nwItemLayout.NOTE # No special header formatting for notes and no-layout files - if isNone: return - if isNote: return + if isNone or isNote: + return # For novel files, we need to handle chapter numbering and scene # breaks @@ -259,12 +277,12 @@ class Tokenizer(): elif tType == self.T_HEAD2: if not isUnNum: self.numChapter += 1 - tText = self._doFormatChapter(tText,isUnNum) + tText = self._formatChapter(tText,isUnNum) self.theTokens[n] = (tType,tText,None,self.A_LEFT) self.firstScene = True elif tType == self.T_HEAD3: - tTemp = self._doFormatScene(tText) + tTemp = self._formatScene(tText) if tTemp == "" and self.hideScene: self.theTokens[n] = (self.T_EMPTY,"",None,self.A_LEFT) elif tTemp == "" and not self.hideScene: @@ -282,7 +300,7 @@ class Tokenizer(): self.firstScene = False elif tType == self.T_HEAD4: - tTemp = self._doFormatSection(tText) + tTemp = self._formatSection(tText) if tTemp == "" and self.hideSection: self.theTokens[n] = (self.T_EMPTY,"",None,self.A_LEFT) elif tTemp == "" and not self.hideSection: @@ -310,12 +328,16 @@ class Tokenizer(): # Internal Functions ## - def _doFormatTitle(self, theText): + def _formatTitle(self, theText): + """Replace tokens for headers level 1. + """ theTitle = self.fmtTitle theTitle = theTitle.replace("%title%", theText) return theTitle - def _doFormatChapter(self, theText, noNum): + def _formatChapter(self, theText, noNum): + """Replace tokens for headers level 2. + """ if noNum: theTitle = self.fmtUnNum theTitle = theTitle.replace("%title%", theText) @@ -326,20 +348,18 @@ class Tokenizer(): theTitle = theTitle.replace("%numword%", numberToWord(self.numChapter,"en")) return theTitle - def _doFormatScene(self, theText): + def _formatScene(self, theText): + """Replace tokens for headers level 3. + """ theTitle = self.fmtScene theTitle = theTitle.replace("%title%", theText) return theTitle - def _doFormatSection(self, theText): + def _formatSection(self, theText): + """Replace tokens for headers level 4. + """ theTitle = self.fmtSection theTitle = theTitle.replace("%title%", theText) return theTitle - def _centreText(self, theText, theWidth): - tLen = len(theText) - if tLen < theWidth: - return " "*int((theWidth-tLen)/2) + theText - return theText - # END Class Tokenizer From cf3fc2ef4f7591b31e93b6258b9070c84f6ff37b Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 10 May 2020 15:57:50 +0200 Subject: [PATCH 09/24] Added title formatting settings to project file --- nw/core/project.py | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/nw/core/project.py b/nw/core/project.py index 7011a7da..c2ce2902 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -80,10 +80,9 @@ class NWProject(): self.bookTitle = "" # The final title; should only be used for exports self.bookAuthors = [] # A list of book authors - # Various - self.autoReplace = {} # Text to auto-replace on exports - # Project Settings + self.autoReplace = {} # Text to auto-replace on exports + self.titleFormat = {} # The formatting of titles for exports self.spellCheck = False # Controls the spellcheck-as-you-type feature self.autoOutline = True # If true, the Project Outline is updated automatically self.statusItems = None # Novel file progress status values @@ -200,6 +199,15 @@ class NWProject(): self.bookTitle = "" self.bookAuthors = [] self.autoReplace = {} + self.titleFormat = { + "title": "%title%", + "chapter": "Chapter %num%", + "chapterSub": "%title%", + "unnumbered": "%title%", + "scene": "", + "sceneSep": "* * *", + "section": "", + } self.spellCheck = False self.autoOutline = True self.statusItems = NWStatus() @@ -347,6 +355,11 @@ class NWProject(): elif xItem.tag == "autoReplace": for xEntry in xItem: self.autoReplace[xEntry.tag] = checkString(xEntry.text, None, False) + elif xItem.tag == "titleFormat": + titleFormat = self.titleFormat.copy() + for xEntry in xItem: + titleFormat[xEntry.tag] = checkString(xEntry.text, None, False) + self.setTitleFormat(titleFormat) elif xChild.tag == "content": logger.debug("Found project content") self.projTree.unpackXML(xChild) @@ -417,10 +430,16 @@ class NWProject(): self._packProjectValue(xSettings, "lastEdited", self.lastEdited) self._packProjectValue(xSettings, "lastViewed", self.lastViewed) self._packProjectValue(xSettings, "lastWordCount", self.currWCount) + xAutoRep = etree.SubElement(xSettings, "autoReplace") for aKey, aValue in self.autoReplace.items(): if len(aKey) > 0: - self._packProjectValue(xAutoRep,aKey,aValue) + self._packProjectValue(xAutoRep, aKey, aValue) + + xTitleFmt = etree.SubElement(xSettings, "titleFormat") + for aKey, aValue in self.titleFormat.items(): + if len(aKey) > 0: + self._packProjectValue(xTitleFmt, aKey, aValue) xStatus = etree.SubElement(xSettings,"status") self.statusItems.packEntries(xStatus) @@ -695,6 +714,14 @@ class NWProject(): self.autoReplace = autoReplace return + 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] + return + def setProjectChanged(self, bValue): """Toggle the project changed flag, and propagate the information to the GUI statusbar. From 5ab1402854d94b034c92ff2713f5cf6aba0d1c25 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 10 May 2020 16:12:47 +0200 Subject: [PATCH 10/24] Fix tests --- nw/core/project.py | 2 +- sample/sampleNovel/nwProject.nwx | 11 ++++++++++- tests/reference/gui/0_nwProject.nwx | 17 ++++++++++++++++- tests/reference/gui/1_nwProject.nwx | 20 +++++++++++++++++++- tests/reference/gui/2_nwProject.nwx | 17 ++++++++++++++++- tests/reference/gui/3_nwProject.nwx | 17 ++++++++++++++++- tests/reference/proj/1_nwProject.nwx | 17 ++++++++++++++++- tests/reference/proj/2_nwProject.nwx | 21 ++++++++++++++++++++- tests/test_gui.py | 6 +++--- tests/test_item.py | 3 ++- 10 files changed, 119 insertions(+), 12 deletions(-) diff --git a/nw/core/project.py b/nw/core/project.py index c2ce2902..8986aeb3 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -358,7 +358,7 @@ class NWProject(): elif xItem.tag == "titleFormat": titleFormat = self.titleFormat.copy() for xEntry in xItem: - titleFormat[xEntry.tag] = checkString(xEntry.text, None, False) + titleFormat[xEntry.tag] = checkString(xEntry.text, "", False) self.setTitleFormat(titleFormat) elif xChild.tag == "content": logger.debug("Found project content") diff --git a/sample/sampleNovel/nwProject.nwx b/sample/sampleNovel/nwProject.nwx index 68ac9522..c911b0a3 100644 --- a/sample/sampleNovel/nwProject.nwx +++ b/sample/sampleNovel/nwProject.nwx @@ -1,5 +1,5 @@ - + Sample Project Sample Project @@ -18,6 +18,15 @@ E D
+ + %title% + Chapter %num% + %title% + %title% + None + * * * +
None
+
New Notes diff --git a/tests/reference/gui/0_nwProject.nwx b/tests/reference/gui/0_nwProject.nwx index e5368533..d91e4c30 100644 --- a/tests/reference/gui/0_nwProject.nwx +++ b/tests/reference/gui/0_nwProject.nwx @@ -1,5 +1,5 @@ - + @@ -12,6 +12,15 @@ None 0 + + %title% + Chapter %num% + %title% + %title% + + * * * +
+
New Note @@ -32,6 +41,7 @@ NOVEL New False + True New Chapter @@ -39,6 +49,7 @@ NOVEL New False + True New Scene @@ -46,6 +57,7 @@ NOVEL New False + True SCENE 0 0 @@ -58,6 +70,7 @@ CHARACTER New False + True Plot @@ -65,6 +78,7 @@ PLOT New False + True World @@ -72,6 +86,7 @@ WORLD New False + True
diff --git a/tests/reference/gui/1_nwProject.nwx b/tests/reference/gui/1_nwProject.nwx index c55ff681..0cb958d1 100644 --- a/tests/reference/gui/1_nwProject.nwx +++ b/tests/reference/gui/1_nwProject.nwx @@ -1,5 +1,5 @@ - + @@ -12,6 +12,15 @@ 31489056e0916 86 + + %title% + Chapter %num% + %title% + %title% + + * * * +
+
New Note @@ -32,6 +41,7 @@ NOVEL New True + True New Chapter @@ -39,6 +49,7 @@ NOVEL New True + True New Scene @@ -46,6 +57,7 @@ NOVEL New False + True SCENE 331 59 @@ -58,6 +70,7 @@ CHARACTER New True + True New File @@ -65,6 +78,7 @@ CHARACTER New False + True NOTE 34 8 @@ -77,6 +91,7 @@ PLOT New True + True New File @@ -84,6 +99,7 @@ PLOT New False + True NOTE 48 10 @@ -96,6 +112,7 @@ WORLD New True + True New File @@ -103,6 +120,7 @@ WORLD New False + True NOTE 51 9 diff --git a/tests/reference/gui/2_nwProject.nwx b/tests/reference/gui/2_nwProject.nwx index ca896ec2..0ab86944 100644 --- a/tests/reference/gui/2_nwProject.nwx +++ b/tests/reference/gui/2_nwProject.nwx @@ -1,5 +1,5 @@ - + Project Name Project Title @@ -16,6 +16,15 @@ With This Stuff + + %title% + Chapter %num% + %title% + %title% + + * * * +
+
New Note @@ -36,6 +45,7 @@ NOVEL New False + True
New Chapter @@ -43,6 +53,7 @@ NOVEL New False + True New Scene @@ -50,6 +61,7 @@ NOVEL New False + True SCENE 0 0 @@ -62,6 +74,7 @@ CHARACTER New False + True Plot @@ -69,6 +82,7 @@ PLOT New False + True World @@ -76,6 +90,7 @@ WORLD New False + True
diff --git a/tests/reference/gui/3_nwProject.nwx b/tests/reference/gui/3_nwProject.nwx index f0bda720..3d80a294 100644 --- a/tests/reference/gui/3_nwProject.nwx +++ b/tests/reference/gui/3_nwProject.nwx @@ -1,5 +1,5 @@ - + @@ -12,6 +12,15 @@ None 0 + + %title% + Chapter %num% + %title% + %title% + + * * * +
+
New Note @@ -32,6 +41,7 @@ NOVEL New False + True New Chapter @@ -39,6 +49,7 @@ NOVEL New False + True Just a Page @@ -46,6 +57,7 @@ NOVEL Note False + True PAGE 0 0 @@ -58,6 +70,7 @@ CHARACTER New False + True Plot @@ -65,6 +78,7 @@ PLOT New False + True World @@ -72,6 +86,7 @@ WORLD New False + True
diff --git a/tests/reference/proj/1_nwProject.nwx b/tests/reference/proj/1_nwProject.nwx index 319d24b3..76bfb4ae 100644 --- a/tests/reference/proj/1_nwProject.nwx +++ b/tests/reference/proj/1_nwProject.nwx @@ -1,5 +1,5 @@ - + @@ -12,6 +12,15 @@ None 0 + + %title% + Chapter %num% + %title% + %title% + + * * * +
+
New Note @@ -32,6 +41,7 @@ NOVEL New False + True Characters @@ -39,6 +49,7 @@ CHARACTER New False + True Plot @@ -46,6 +57,7 @@ PLOT New False + True World @@ -53,6 +65,7 @@ WORLD New False + True New Chapter @@ -60,6 +73,7 @@ NOVEL New False + True New Scene @@ -67,6 +81,7 @@ NOVEL New False + True SCENE 0 0 diff --git a/tests/reference/proj/2_nwProject.nwx b/tests/reference/proj/2_nwProject.nwx index 92bd9df4..585e7fb3 100644 --- a/tests/reference/proj/2_nwProject.nwx +++ b/tests/reference/proj/2_nwProject.nwx @@ -1,5 +1,5 @@ - + @@ -12,6 +12,15 @@ None 0 + + %title% + Chapter %num% + %title% + %title% + + * * * +
+
New Note @@ -32,6 +41,7 @@ NOVEL New False + True
Characters @@ -39,6 +49,7 @@ CHARACTER New False + True Plot @@ -46,6 +57,7 @@ PLOT New False + True World @@ -53,6 +65,7 @@ WORLD New False + True New Chapter @@ -60,6 +73,7 @@ NOVEL New False + True New Scene @@ -67,6 +81,7 @@ NOVEL New False + True SCENE 0 0 @@ -79,6 +94,7 @@ TIMELINE New False + True Object @@ -86,6 +102,7 @@ OBJECT New False + True Custom1 @@ -93,6 +110,7 @@ CUSTOM New False + True Custom2 @@ -100,6 +118,7 @@ CUSTOM New False + True
diff --git a/tests/test_gui.py b/tests/test_gui.py index e2291fff..9e2cd53d 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -13,7 +13,7 @@ from nw.gui.dialogs.itemeditor import GuiItemEditor from nw.constants import * -keyDelay = 10 +keyDelay = 5 stepDelay = 50 @pytest.mark.gui @@ -361,7 +361,7 @@ def testItemEditor(qtbot, nwTempGUI, nwRef, nwTemp): layoutIdx = itemEdit.editLayout.findData(nwItemLayout.PAGE) itemEdit.editLayout.setCurrentIndex(layoutIdx) - qtbot.mouseClick(itemEdit.saveButton, Qt.LeftButton) + itemEdit._doSave() itemEdit = GuiItemEditor(nwGUI, nwGUI.theProject, "31489056e0916") qtbot.addWidget(itemEdit) @@ -369,7 +369,7 @@ def testItemEditor(qtbot, nwTempGUI, nwRef, nwTemp): assert itemEdit.editStatus.currentData() == "Note" assert itemEdit.editLayout.currentData() == nwItemLayout.PAGE - qtbot.mouseClick(itemEdit.closeButton, Qt.LeftButton) + itemEdit._doClose() qtbot.wait(stepDelay) assert nwGUI.saveProject() diff --git a/tests/test_item.py b/tests/test_item.py index 64268e62..19f663f6 100644 --- a/tests/test_item.py +++ b/tests/test_item.py @@ -197,7 +197,8 @@ def testItemXMLPackUnpack(): assert etree.tostring(xContent, pretty_print=False, encoding="utf-8") == ( b"" b"" - b"A NameTRASHTRASHMainTrue" + b"A NameTRASHTRASHMain" + b"TrueTrue" b"" b"" ) From 4d9e2d03a37e81c27967479705aba8cb64201636 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 10 May 2020 19:31:53 +0200 Subject: [PATCH 11/24] Done building the buttons etc for BuildNovel dialog --- nw/assets/text/exportHelp_en.htm | 44 ++++ nw/config.py | 1 + nw/core/project.py | 23 ++- nw/core/tokenizer.py | 5 + nw/gui/build.py | 337 ++++++++++++++++++++++++++++++- sample/sampleNovel/nwProject.nwx | 13 +- 6 files changed, 400 insertions(+), 23 deletions(-) create mode 100644 nw/assets/text/exportHelp_en.htm diff --git a/nw/assets/text/exportHelp_en.htm b/nw/assets/text/exportHelp_en.htm new file mode 100644 index 00000000..df8eca2a --- /dev/null +++ b/nw/assets/text/exportHelp_en.htm @@ -0,0 +1,44 @@ +

Help!

+

A brief guide to make the most out of the Build Project tool.

+ +

Novel Title Formats

+

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.

+ +

Build Overrides

+

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 Non-Text Elements

+

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.

+ +

Additional Options

+

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("

Hello World

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 @@ - + Sample Project Sample Project @@ -20,12 +20,13 @@ %title% - Chapter %num% - %title% + Chapter %num%\\%title% %title% - None - * * * -
None
+ * * * +
+ True + False + False
New From 26b8379ada3db00e9973257fddc0fa9dd3a28da7 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 10 May 2020 19:37:38 +0200 Subject: [PATCH 12/24] Updated tests again --- tests/reference/gui/0_nwProject.nwx | 11 ++++++----- tests/reference/gui/1_nwProject.nwx | 11 ++++++----- tests/reference/gui/2_nwProject.nwx | 11 ++++++----- tests/reference/gui/3_nwProject.nwx | 11 ++++++----- tests/reference/proj/1_nwProject.nwx | 11 ++++++----- tests/reference/proj/2_nwProject.nwx | 11 ++++++----- 6 files changed, 36 insertions(+), 30 deletions(-) diff --git a/tests/reference/gui/0_nwProject.nwx b/tests/reference/gui/0_nwProject.nwx index d91e4c30..3007ae93 100644 --- a/tests/reference/gui/0_nwProject.nwx +++ b/tests/reference/gui/0_nwProject.nwx @@ -1,5 +1,5 @@ - + @@ -14,12 +14,13 @@ %title% - Chapter %num% - %title% + Chapter %num%\\%title% %title% - - * * * + * * *
+ False + False + False
New diff --git a/tests/reference/gui/1_nwProject.nwx b/tests/reference/gui/1_nwProject.nwx index 0cb958d1..38708ab2 100644 --- a/tests/reference/gui/1_nwProject.nwx +++ b/tests/reference/gui/1_nwProject.nwx @@ -1,5 +1,5 @@ - + @@ -14,12 +14,13 @@ %title% - Chapter %num% - %title% + Chapter %num%\\%title% %title% - - * * * + * * *
+ False + False + False
New diff --git a/tests/reference/gui/2_nwProject.nwx b/tests/reference/gui/2_nwProject.nwx index 0ab86944..58b80897 100644 --- a/tests/reference/gui/2_nwProject.nwx +++ b/tests/reference/gui/2_nwProject.nwx @@ -1,5 +1,5 @@ - + Project Name Project Title @@ -18,12 +18,13 @@
%title% - Chapter %num% - %title% + Chapter %num%\\%title% %title% - - * * * + * * *
+ False + False + False
New diff --git a/tests/reference/gui/3_nwProject.nwx b/tests/reference/gui/3_nwProject.nwx index 3d80a294..7c5d8511 100644 --- a/tests/reference/gui/3_nwProject.nwx +++ b/tests/reference/gui/3_nwProject.nwx @@ -1,5 +1,5 @@ - + @@ -14,12 +14,13 @@ %title% - Chapter %num% - %title% + Chapter %num%\\%title% %title% - - * * * + * * *
+ False + False + False
New diff --git a/tests/reference/proj/1_nwProject.nwx b/tests/reference/proj/1_nwProject.nwx index 76bfb4ae..febdc50e 100644 --- a/tests/reference/proj/1_nwProject.nwx +++ b/tests/reference/proj/1_nwProject.nwx @@ -1,5 +1,5 @@ - + @@ -14,12 +14,13 @@ %title% - Chapter %num% - %title% + Chapter %num%\\%title% %title% - - * * * + * * *
+ False + False + False
New diff --git a/tests/reference/proj/2_nwProject.nwx b/tests/reference/proj/2_nwProject.nwx index 585e7fb3..b64d0071 100644 --- a/tests/reference/proj/2_nwProject.nwx +++ b/tests/reference/proj/2_nwProject.nwx @@ -1,5 +1,5 @@ - + @@ -14,12 +14,13 @@ %title% - Chapter %num% - %title% + Chapter %num%\\%title% %title% - - * * * + * * *
+ False + False + False
New From 771e44055d525b589667bfd1714dabf7ac216a94 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 10 May 2020 21:27:28 +0200 Subject: [PATCH 13/24] Finished save file options for now. Still missing markdown and PDF --- nw/core/project.py | 15 ++- nw/gui/build.py | 180 +++++++++++++++++++++++++++---- sample/sampleNovel/nwProject.nwx | 2 +- 3 files changed, 168 insertions(+), 29 deletions(-) diff --git a/nw/core/project.py b/nw/core/project.py index c964c75b..a0396c02 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -529,11 +529,7 @@ class NWProject(): ), nwAlert.WARN) return False - cleanName = "" - for c in self.projName.strip(): - if c.isalpha() or c.isdigit() or c == " ": - cleanName += c - + cleanName = self.getFileSafeProjectName() baseDir = path.join(self.mainConf.backupPath, cleanName) if not path.isdir(baseDir): try: @@ -740,6 +736,15 @@ class NWProject(): # Getters ## + def getFileSafeProjectName(self): + """Returns a filename safe version of the project name. + """ + cleanName = "" + for c in self.projName.strip(): + if c.isalpha() or c.isdigit() or c == " ": + cleanName += c + return cleanName + def getSessionWordCount(self): """Returns the number of words added or removed this session. """ diff --git a/nw/gui/build.py b/nw/gui/build.py index 401bf58e..94c82019 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -30,12 +30,14 @@ import nw from os import path -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QTextOption, QPalette, QColor +from PyQt5.QtCore import Qt, QByteArray +from PyQt5.QtGui import ( + QTextOption, QPalette, QColor, QTextDocumentWriter +) from PyQt5.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QTextBrowser, QPushButton, QLabel, QLineEdit, QGroupBox, QGridLayout, QComboBox, QProgressBar, - QMenu, QAction + QMenu, QAction, QFileDialog ) from nw.gui.additions import QSwitch @@ -46,10 +48,12 @@ logger = logging.getLogger(__name__) class GuiBuildNovel(QDialog): - FMT_ODT = 1 - FMT_PDF = 2 - FMT_HTM = 3 - FMT_MD = 4 + FMT_ODT = 1 + FMT_PDF = 2 + FMT_HTM1 = 3 + FMT_HTM2 = 4 + FMT_MD = 4 + FMT_TXT = 5 def __init__(self, theParent, theProject): QDialog.__init__(self, theParent) @@ -62,6 +66,8 @@ class GuiBuildNovel(QDialog): self.theTheme = theParent.theTheme self.optState = self.theProject.optState + self.htmlText = "" + self.setWindowTitle("Build Project") self.setMinimumWidth(800) self.setMinimumHeight(700) @@ -202,20 +208,32 @@ class GuiBuildNovel(QDialog): 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.saveODT = QAction("Open Document (.odt)") + self.saveODT.triggered.connect(lambda: self._saveDocument(self.FMT_ODT)) + self.saveMenu.addAction(self.saveODT) + + # self.savePDF = QAction("Portable Document (.pdf)") + # self.savePDF.triggered.connect(lambda: self._saveDocument(self.FMT_PDF)) + # self.saveMenu.addAction(self.savePDF) + + self.saveHTM1 = QAction("Qt Style HTML (.htm)") + self.saveHTM1.triggered.connect(lambda: self._saveDocument(self.FMT_HTM1)) + self.saveMenu.addAction(self.saveHTM1) + + self.saveHTM2 = QAction("Plain HTML (.htm)") + self.saveHTM2.triggered.connect(lambda: self._saveDocument(self.FMT_HTM2)) + self.saveMenu.addAction(self.saveHTM2) + + # self.saveMD = QAction("Markdown (.md)") + # self.saveMD.triggered.connect(lambda: self._saveDocument(self.FMT_MD)) + # self.saveMenu.addAction(self.saveMD) + + self.saveTXT = QAction("Plain Text (.txt)") + 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) @@ -263,7 +281,7 @@ class GuiBuildNovel(QDialog): """ makeHtml = ToHtml(self.theProject, self.theParent) - theText = "" + self.htmlText = "" for tItem in self.theProject.projTree: if tItem is not None and tItem.itemType == nwItemType.FILE: @@ -273,14 +291,130 @@ class GuiBuildNovel(QDialog): makeHtml.doHeaders() makeHtml.doConvert() makeHtml.doPostProcessing() - theText += makeHtml.getResult() + self.htmlText += makeHtml.getResult() - self.docView.setHtml(theText) + self.docView.setHtml(self.htmlText) return def _saveDocument(self, theFormat): - return + """Save the document to various formats. + """ + + # FMT_PDF + + byteFmt = QByteArray() + fileExt = "" + textFmt = "" + outTool = "" + + # Create the settings + if theFormat == self.FMT_ODT: + byteFmt.append("odf") + fileExt = "odf" + textFmt = "Open Document" + outTool = "Qt" + + elif theFormat == self.FMT_HTM1: + byteFmt.append("html") + fileExt = "htm" + textFmt = "Qt Style HTML" + outTool = "Qt" + + elif theFormat == self.FMT_HTM2: + fileExt = "htm" + textFmt = "Plain HTML" + outTool = "NW" + + elif theFormat == self.FMT_MD: + byteFmt.append("markdown") + fileExt = "md" + textFmt = "Markdown" + outTool = "Qt" + + elif theFormat == self.FMT_TXT: + byteFmt.append("plaintext") + fileExt = "txt" + textFmt = "Plain Text" + outTool = "Qt" + + else: + return False + + # Generate the file name + if fileExt: + + cleanName = self.theProject.getFileSafeProjectName() + fileName = "%s.%s" % (cleanName, fileExt) + saveDir = self.mainConf.lastPath + savePath = path.join(saveDir, fileName) + if not path.isdir(saveDir): + saveDir = self.mainConf.homePath + + dlgOpt = QFileDialog.Options() + dlgOpt |= QFileDialog.DontUseNativeDialog + saveTo = QFileDialog.getSaveFileName( + self, "Save Document As", savePath, options=dlgOpt + ) + if saveTo[0]: + savePath = saveTo[0] + else: + return False + + self.mainConf.setLastPath(savePath) + + else: + return False + + # Do the actual writing + if outTool == "Qt": + docWriter = QTextDocumentWriter() + docWriter.setFileName(savePath) + docWriter.setFormat(byteFmt) + if docWriter.write(self.docView.qDocument): + self.theParent.makeAlert( + "Document successfully written in %s format to file: %s" % ( + textFmt, savePath + ), nwAlert.INFO + ) + else: + self.theParent.makeAlert( + "Failed to write document in %s format to file: %s" % ( + textFmt, savePath + ), nwAlert.ERROR + ) + + elif outTool == "NW": + if theFormat == self.FMT_HTM2: + try: + with open(savePath, mode="w", encoding="utf8") as outFile: + outFile.write("\n") + outFile.write("\n") + outFile.write("\n") + outFile.write("\n") + outFile.write("\n") + outFile.write("\n") + outFile.write(self.htmlText) + outFile.write("\n") + outFile.write("\n") + + self.theParent.makeAlert( + "Document successfully written in %s format to file: %s" % ( + textFmt, savePath + ), nwAlert.INFO + ) + + except Exception as e: + self.theParent.makeAlert( + "Failed to write document in %s format to file: %s" % ( + textFmt, str(e) + ), nwAlert.ERROR + ) + + else: + return False + + return True def _printDocument(self): return diff --git a/sample/sampleNovel/nwProject.nwx b/sample/sampleNovel/nwProject.nwx index 05445b4b..69ac815d 100644 --- a/sample/sampleNovel/nwProject.nwx +++ b/sample/sampleNovel/nwProject.nwx @@ -1,5 +1,5 @@ - + Sample Project Sample Project From 52eb51b44b3270e1bc1e9b1ec6d7b6c25f6f86a5 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 10 May 2020 21:29:32 +0200 Subject: [PATCH 14/24] Deleted the rest ov the convert submodule --- nw/convert/file/__init__.py | 15 --- nw/convert/file/concat.py | 80 ------------- nw/convert/file/html.py | 83 -------------- nw/convert/file/latex.py | 69 ----------- nw/convert/file/markdown.py | 61 ---------- nw/convert/file/text.py | 210 ---------------------------------- nw/convert/text/__init__.py | 13 --- nw/convert/text/tolatex.py | 155 ------------------------- nw/convert/text/tomarkdown.py | 136 ---------------------- nw/convert/text/totext.py | 154 ------------------------- 10 files changed, 976 deletions(-) delete mode 100644 nw/convert/file/__init__.py delete mode 100644 nw/convert/file/concat.py delete mode 100644 nw/convert/file/html.py delete mode 100644 nw/convert/file/latex.py delete mode 100644 nw/convert/file/markdown.py delete mode 100644 nw/convert/file/text.py delete mode 100644 nw/convert/text/__init__.py delete mode 100644 nw/convert/text/tolatex.py delete mode 100644 nw/convert/text/tomarkdown.py delete mode 100644 nw/convert/text/totext.py diff --git a/nw/convert/file/__init__.py b/nw/convert/file/__init__.py deleted file mode 100644 index da2afb33..00000000 --- a/nw/convert/file/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# -*- coding: utf-8 -*- - -from nw.convert.file.concat import ConcatFile -from nw.convert.file.html import HtmlFile -from nw.convert.file.latex import LaTeXFile -from nw.convert.file.markdown import MarkdownFile -from nw.convert.file.text import TextFile - -__all__ = [ - "ConcatFile", - "HtmlFile", - "LaTeXFile", - "MarkdownFile", - "TextFile", -] diff --git a/nw/convert/file/concat.py b/nw/convert/file/concat.py deleted file mode 100644 index 5bc76e89..00000000 --- a/nw/convert/file/concat.py +++ /dev/null @@ -1,80 +0,0 @@ -# -*- coding: utf-8 -*- -"""novelWriter Concatenated File - - novelWriter – Concatenated File -================================= - Concatenate the standard novelWriter files to a single file - - File History: - Created: 2019-10-26 [0.3.1] - - 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 . -""" - -import logging -import nw - -from os import path - -from nw.convert.file.text import TextFile -from nw.convert.tokenizer import Tokenizer -from nw.constants import nwAlert - -logger = logging.getLogger(__name__) - -class ConcatFile(TextFile): - - def __init__(self, theProject, theParent): - TextFile.__init__(self, theProject, theParent) - self.theConv = Tokenizer(self.theProject, self.theParent) - return - - def addText(self, tHandle): - - logger.verbose("Parsing content of item '%s'" % tHandle) - - if not self.checkInclude(tHandle): - return False - - self.theConv.setText(tHandle) - - theResult = self.theConv.theText - - if theResult is not None and self.outFile is not None: - self.outFile.write(theResult.rstrip()) - self.outFile.write("\n\n") - - return True - - ## - # Internal Functions - ## - - def _doOpenFile(self, filePath): - try: - self.outFile = open(filePath,mode="wt+",encoding="utf8") - except Exception as e: - self.makeAlert(["Failed to open file.",str(e)], nwAlert.ERROR) - return False - return True - - def _doCloseFile(self): - if self.outFile is not None: - self.outFile.close() - return True - -# END Class ConcatFile diff --git a/nw/convert/file/html.py b/nw/convert/file/html.py deleted file mode 100644 index f03e12b3..00000000 --- a/nw/convert/file/html.py +++ /dev/null @@ -1,83 +0,0 @@ -# -*- coding: utf-8 -*- -"""novelWriter HTML File - - novelWriter – HTML File -========================= - Writes the project to a html file - - File History: - Created: 2019-10-19 [0.3] - - 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 . -""" - -import logging -import nw - -from nw.convert.file.text import TextFile -from nw.convert.text.tohtml import ToHtml -from nw.constants import nwAlert - -logger = logging.getLogger(__name__) - -class HtmlFile(TextFile): - - def __init__(self, theProject, theParent): - TextFile.__init__(self, theProject, theParent) - self.theConv = ToHtml(self.theProject, self.theParent) - return - - ## - # Internal Functions - ## - - def _doOpenFile(self, filePath): - try: - self.outFile = open(filePath,mode="wt+",encoding="utf8") - self.outFile.write("\n") - self.outFile.write("\n") - self.outFile.write("\n") - self.outFile.write(" \n") - self.outFile.write(" \n") - self.outFile.write("\n") - self.outFile.write("\n") - self.outFile.write("
\n") - except Exception as e: - self.makeAlert(["Failed to open file.",str(e)], nwAlert.ERROR) - return False - return True - - def _doCloseFile(self): - if self.outFile is not None: - self.outFile.write("
\n") - self.outFile.write("\n") - self.outFile.write("\n") - self.outFile.close() - return True - -# END Class HtmlFile diff --git a/nw/convert/file/latex.py b/nw/convert/file/latex.py deleted file mode 100644 index fd7c73c1..00000000 --- a/nw/convert/file/latex.py +++ /dev/null @@ -1,69 +0,0 @@ -# -*- coding: utf-8 -*- -"""novelWriter LaTeX File - - novelWriter – LaTeX File -========================== - Writes the project to a LaTeX file - - File History: - Created: 2019-10-24 [0.3.1] - - 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 . -""" - -import logging -import nw - -from nw.convert.file.text import TextFile -from nw.convert.text.tolatex import ToLaTeX -from nw.constants import nwAlert - -logger = logging.getLogger(__name__) - -class LaTeXFile(TextFile): - - def __init__(self, theProject, theParent): - TextFile.__init__(self, theProject, theParent) - self.theConv = ToLaTeX(self.theProject, self.theParent) - self.texCodecFail = False - return - - ## - # Internal Functions - ## - - def _doOpenFile(self, filePath): - try: - self.outFile = open(filePath,mode="wt+",encoding="utf8") - self.outFile.write("\\documentclass[12pt]{report}\n") - self.outFile.write("\\usepackage[utf8]{inputenc}\n") - self.outFile.write("\\usepackage[T1]{fontenc}\n") - self.outFile.write("\n") - self.outFile.write("\\begin{document}\n") - except Exception as e: - self.makeAlert(["Failed to open file.",str(e)], nwAlert.ERROR) - return False - return True - - def _doCloseFile(self): - if self.outFile is not None: - self.outFile.write("\\end{document}\n") - self.outFile.close() - self.texCodecFail = self.theConv.texCodecFail - return True - -# END Class LaTeXFile diff --git a/nw/convert/file/markdown.py b/nw/convert/file/markdown.py deleted file mode 100644 index 24b516da..00000000 --- a/nw/convert/file/markdown.py +++ /dev/null @@ -1,61 +0,0 @@ -# -*- coding: utf-8 -*- -"""novelWriter Markdown File - - novelWriter – Markdown File -============================= - Writes the project to a markdown file - - File History: - Created: 2019-10-19 [0.3] - - 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 . -""" - -import logging -import nw - -from nw.convert.file.text import TextFile -from nw.convert.text.tomarkdown import ToMarkdown -from nw.constants import nwAlert - -logger = logging.getLogger(__name__) - -class MarkdownFile(TextFile): - - def __init__(self, theProject, theParent): - TextFile.__init__(self, theProject, theParent) - self.theConv = ToMarkdown(self.theProject, self.theParent) - return - - ## - # Internal Functions - ## - - def _doOpenFile(self, filePath): - try: - self.outFile = open(filePath,mode="wt+",encoding="utf8") - except Exception as e: - self.makeAlert(["Failed to open file.",str(e)], nwAlert.ERROR) - return False - return True - - def _doCloseFile(self): - if self.outFile is not None: - self.outFile.close() - return True - -# END Class MarkdownFile diff --git a/nw/convert/file/text.py b/nw/convert/file/text.py deleted file mode 100644 index afcadb10..00000000 --- a/nw/convert/file/text.py +++ /dev/null @@ -1,210 +0,0 @@ -# -*- coding: utf-8 -*- -"""novelWriter Text File - - novelWriter – Text File -========================= - Writes the project to a plain text file - - File History: - Created: 2019-10-18 [0.2.3] - - 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 . -""" - -import logging -import nw - -from os import path - -from PyQt5.QtWidgets import QMessageBox - -from nw.convert.text.totext import ToText -from nw.constants import nwAlert, nwItemType, nwItemLayout, nwItemClass - -logger = logging.getLogger(__name__) - -class TextFile(): - - def __init__(self, theProject, theParent): - - self.mainConf = nw.CONFIG - self.theProject = theProject - self.theParent = theParent - - self.outFile = None - self.fileName = "" - self.theText = "" - self.expNovel = True - self.expNotes = False - - self.theConv = ToText(self.theProject, self.theParent) - self.makeAlert = self.theParent.makeAlert - - self.setComments(False) - self.setKeywords(False) - self.setWordWrap(80) - - return - - ## - # Setters - ## - - def setExportNovel(self, doNovel): - self.expNovel = doNovel - return - - def setExportNotes(self, doNotes): - self.expNotes = doNotes - return - - def setComments(self, doComments): - self.theConv.setComments(doComments) - return - - def setKeywords(self, doKeywords): - self.theConv.setKeywords(doKeywords) - return - - def setWordWrap(self, wordWrap): - if wordWrap >= 0: - self.theConv.setWordWrap(wordWrap) - else: - self.theConv.setWordWrap(0) - return - - def setTitleFormat(self, fmtTitle): - self.theConv.setTitleFormat(fmtTitle) - return - - def setChapterFormat(self, fmtChapter): - self.theConv.setChapterFormat(fmtChapter) - return - - def setUnNumberedFormat(self, fmtUnNum): - self.theConv.setUnNumberedFormat(fmtUnNum) - return - - def setSceneFormat(self, fmtScene, hideScene): - self.theConv.setSceneFormat(fmtScene, hideScene) - return - - def setSectionFormat(self, fmtSection, hideSection): - self.theConv.setSectionFormat(fmtSection, hideSection) - return - - ## - # Core Methods - ## - - def openFile(self, filePath): - - self.fileName = path.basename(filePath) - if path.isfile(filePath) and self.mainConf.showGUI: - msgBox = QMessageBox() - msgRes = msgBox.question(self.theParent, "Overwrite", ( - "File '%s' already exists.
Do you want to overwrite it?" % self.fileName - )) - if msgRes != QMessageBox.Yes: - return False - - self._doOpenFile(filePath) - - if self.outFile is None: - return False - - return True - - def closeFile(self): - self._doCloseFile() - return True - - def addText(self, tHandle): - - logger.verbose("Parsing content of item '%s'" % tHandle) - - if not self.checkInclude(tHandle): - return False - - self.theConv.setText(tHandle) - self.theConv.doAutoReplace() - self.theConv.tokenizeText() - self.theConv.formatHeaders() - self.theConv.doConvert() - self.theConv.doPostProcessing() - - if self.theConv.theResult is not None and self.outFile is not None: - self.outFile.write(self.theConv.theResult) - - return True - - def checkInclude(self, tHandle): - """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 - """ - - theItem = self.theProject.projTree[tHandle] - 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() - isNote = theItem.itemLayout == nwItemLayout.NOTE - isNovel = not isNone and not isNote - - if isNone: - return False - if isNote and not self.expNotes: - return False - if isNovel and not self.expNovel: - return False - - return True - - ## - # Internal Functions - ## - - def _doOpenFile(self, filePath): - """This function does the actual opening of the file, and can be - overloaded by a subclass that uses a different file format that - requires a different approach. - """ - try: - self.outFile = open(filePath,mode="wt+",encoding="utf8") - self.outFile.write("\n\n") - except Exception as e: - self.makeAlert(["Failed to open file.",str(e)], nwAlert.ERROR) - return False - return True - - def _doCloseFile(self): - """This function closes the file, and is meant to be overloaded - by the subclass for other file formats. - """ - if self.outFile is not None: - self.outFile.close() - return True - -# END Class OutFile diff --git a/nw/convert/text/__init__.py b/nw/convert/text/__init__.py deleted file mode 100644 index ef849320..00000000 --- a/nw/convert/text/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# -*- coding: utf-8 -*- - -from nw.convert.text.tohtml import ToHtml -from nw.convert.text.tolatex import ToLaTeX -from nw.convert.text.tomarkdown import ToMarkdown -from nw.convert.text.totext import ToText - -__all__ = [ - "ToHtml", - "ToLaTeX", - "ToMarkdown", - "ToText", -] diff --git a/nw/convert/text/tolatex.py b/nw/convert/text/tolatex.py deleted file mode 100644 index f945b763..00000000 --- a/nw/convert/text/tolatex.py +++ /dev/null @@ -1,155 +0,0 @@ -# -*- coding: utf-8 -*- -"""novelWriter LaTeX Converter - - novelWriter – LaTeX Converter -=============================== - Extends the Tokenizer class to write LaTeX - - File History: - Created: 2019-10-24 [0.3.1] - - 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 . -""" - -import logging -import codecs -import re -import nw - -from nw.convert.tokenizer import Tokenizer -from nw.constants import nwUnicode - -logger = logging.getLogger(__name__) - -class ToLaTeX(Tokenizer): - - def __init__(self, theProject, theParent): - Tokenizer.__init__(self, theProject, theParent) - self.texCodecFail = False - return - - def doPostProcessing(self): - """The latexcodec misses dashes and non-breaking spaces, so we - do those here. - """ - - repDict = { - nwUnicode.U_ENDASH : "--", - nwUnicode.U_EMDASH : "---", - nwUnicode.U_NBSP : "~", - } - xRep = re.compile("|".join([re.escape(k) for k in repDict.keys()]), flags=re.DOTALL) - self.theResult = xRep.sub(lambda x: repDict[x.group(0)], self.theResult) - - return - - def doConvert(self): - - texTags = { - self.FMT_B_B : r"\textbf{", - self.FMT_B_E : r"}", - self.FMT_I_B : r"\textit{", - self.FMT_I_E : r"}", - self.FMT_U_B : r"\underline{", - self.FMT_U_E : r"}", - } - - self.theResult = "" - thisPar = [] - for tType, tText, tFormat, tAlign in self.theTokens: - - begText = "" - endText = "\n" - if tAlign == self.A_CENTRE: - begText = "\\begin{center}\n" - endText = "\\end{center}\n\n" - - # First check if we have a comment or plain text, as they - # need some extra replacing before we proceed to wrapping - # and final formatting. - if tType == self.T_COMMENT: - tText = "%% %s" % tText - - elif tType == self.T_TEXT: - tTemp = tText - for xPos, xLen, xFmt in reversed(tFormat): - tTemp = tTemp[:xPos]+texTags[xFmt]+tTemp[xPos+xLen:] - tText = tTemp - - tLen = len(tText) - - # Then the text can receive final formatting before we - # append it to the results. We also store text lines in a - # buffer and merge them only when we find an empty line - # indicating a new paragraph. - if tType == self.T_EMPTY: - if len(thisPar) > 0: - self.theResult += begText - for tTemp in thisPar: - self.theResult += "%s\n" % tTemp - self.theResult += endText - thisPar = [] - - elif tType == self.T_HEAD1: - self.theResult += begText - self.theResult += "{\\Huge %s}\n" % self._escapeUnicode(tText) - self.theResult += endText - - elif tType == self.T_HEAD2: - self.theResult += "\\chapter*{%s}\n\n" % self._escapeUnicode(tText) - - elif tType == self.T_HEAD3: - self.theResult += "\\section*{%s}\n\n" % self._escapeUnicode(tText) - - elif tType == self.T_HEAD4: - self.theResult += "\\subsection*{%s}\n\n" % self._escapeUnicode(tText) - - elif tType == self.T_SEP: - self.theResult += begText - self.theResult += "%s\n" % self._escapeUnicode(tText) - self.theResult += endText - - elif tType == self.T_SKIP: - self.theResult += "\\bigskip\n" - self.theResult += "\\bigskip\n\n" - - elif tType == self.T_TEXT: - if tText.endswith(" "): - thisPar.append(self._escapeUnicode(tText.rstrip())+"\\newline") - else: - thisPar.append(self._escapeUnicode(tText.rstrip())) - - elif tType == self.T_PBREAK: - self.theResult += "\\newpage\n\n" - - elif tType == self.T_COMMENT and self.doComments: - self.theResult += "%s\n\n" % tText - - elif tType == self.T_KEYWORD and self.doKeywords: - self.theResult += "%% @%s\n\n" % tText - - return - - def _escapeUnicode(self, theText): - try: - import latexcodec - return codecs.encode(theText, "ulatex+utf8") - except: - self.texCodecFail = True - return theText - -# END Class ToLaTeX diff --git a/nw/convert/text/tomarkdown.py b/nw/convert/text/tomarkdown.py deleted file mode 100644 index 97e5c7a4..00000000 --- a/nw/convert/text/tomarkdown.py +++ /dev/null @@ -1,136 +0,0 @@ -# -*- coding: utf-8 -*- -"""novelWriter Markdown Text Converter - - novelWriter – Markdown Text Converter -======================================= - Extends the Tokenizer class to write Markdown - - File History: - Created: 2019-10-19 [0.3] - - 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 . -""" - -import textwrap -import logging -import re -import nw - -from nw.convert.tokenizer import Tokenizer - -logger = logging.getLogger(__name__) - -class ToMarkdown(Tokenizer): - - def __init__(self, theProject, theParent): - Tokenizer.__init__(self, theProject, theParent) - return - - def doConvert(self): - - mdTags = { - self.FMT_B_B : "**", - self.FMT_B_E : "**", - self.FMT_I_B : "_", - self.FMT_I_E : "_", - self.FMT_U_B : "__", - self.FMT_U_E : "__", - } - - if self.wordWrap > 0: - tWrap = textwrap.TextWrapper( - width = self.wordWrap, - initial_indent = "", - subsequent_indent = "", - expand_tabs = True, - replace_whitespace = True, - fix_sentence_endings = False, - break_long_words = True, - drop_whitespace = True, - break_on_hyphens = True, - tabsize = 8, - max_lines = None - ) - - self.theResult = "" - thisPar = [] - for tType, tText, tFormat, tAlign in self.theTokens: - - # First check if we have a comment or plain text, as they - # need some extra replacing before we proceed to wrapping - # and final formatting. - if tType == self.T_COMMENT: - tText = " %s" % tText - - elif tType == self.T_TEXT: - tTemp = tText - for xPos, xLen, xFmt in reversed(tFormat): - tTemp = tTemp[:xPos]+mdTags[xFmt]+tTemp[xPos+xLen:] - tText = tTemp - - tLen = len(tText) - - # The text can now be word wrapped, if we have requested - # this and it's needed. - if self.wordWrap > 0 and tLen > self.wordWrap: - if tType == self.T_COMMENT: - tText = textwrap.fill( - tText.strip(),initial_indent=" ",subsequent_indent=" " - ) - else: - tText = tWrap.fill(tText) - - # Then the text can receive final formatting before we - # append it to the results. We also store text lines in a - # buffer and merge them only when we find an empty line, - # indicating a new paragraph. - if tType == self.T_EMPTY: - if len(thisPar) > 0: - tTemp = "\n".join(thisPar) - self.theResult += "%s\n\n" % tTemp.rstrip() - thisPar = [] - - elif tType == self.T_HEAD1: - self.theResult += "# %s\n\n" % tText - - elif tType == self.T_HEAD2: - self.theResult += "## %s\n\n" % tText - - elif tType == self.T_HEAD3: - self.theResult += "### %s\n\n" % tText - - elif tType == self.T_HEAD4: - self.theResult += "#### %s\n\n" % tText - - elif tType == self.T_SEP: - self.theResult += "%s\n\n" % tText - - elif tType == self.T_SKIP: - self.theResult += "\n\n\n" - - elif tType == self.T_TEXT: - thisPar.append(tText) - - elif tType == self.T_COMMENT and self.doComments: - self.theResult += "%s\n\n" % tText - - elif tType == self.T_KEYWORD and self.doKeywords: - self.theResult += "%s\n\n" % tText - - return - -# END Class ToMarkdown diff --git a/nw/convert/text/totext.py b/nw/convert/text/totext.py deleted file mode 100644 index b4d3af64..00000000 --- a/nw/convert/text/totext.py +++ /dev/null @@ -1,154 +0,0 @@ -# -*- coding: utf-8 -*- -"""novelWriter Plain Text Converter - - novelWriter – Plain Text Converter -==================================== - Extends the Tokenizer class to convert to plain text - - File History: - Created: 2019-10-26 [0.3.1] - - 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 . -""" - -import textwrap -import logging -import re -import nw - -from nw.convert.tokenizer import Tokenizer -from nw.constants import nwUnicode - -logger = logging.getLogger(__name__) - -class ToText(Tokenizer): - - def __init__(self, theProject, theParent): - Tokenizer.__init__(self, theProject, theParent) - return - - def doAutoReplace(self): - Tokenizer.doAutoReplace(self) - - repDict = { - "\t" : " ", - nwUnicode.U_NBSP : " ", - } - xRep = re.compile("|".join([re.escape(k) for k in repDict.keys()]), flags=re.DOTALL) - self.theText = xRep.sub(lambda x: repDict[x.group(0)], self.theText) - - return - - def doConvert(self): - """Converts the tokenized text into plain text. - """ - - if self.wordWrap > 0: - tWrap = textwrap.TextWrapper( - width = self.wordWrap, - initial_indent = "", - subsequent_indent = "", - expand_tabs = True, - replace_whitespace = True, - fix_sentence_endings = False, - break_long_words = True, - drop_whitespace = True, - break_on_hyphens = True, - tabsize = 8, - max_lines = None - ) - - self.theResult = "" - thisPar = [] - for tType, tText, tFormat, tAlign in self.theTokens: - - # First check if we have a comment or plain text, as they - # need some extra replacing before we proceed to wrapping - # and final formatting. - if tType == self.T_COMMENT: - tText = "[%s]" % tText - - elif tType == self.T_TEXT: - tTemp = tText - for xPos, xLen, xFmt in reversed(tFormat): - tTemp = tTemp[:xPos]+tTemp[xPos+xLen:] - tText = tTemp - - tLen = len(tText) - - # The text can now be word wrapped, if we have requested - # this and it's needed. - if tAlign == self.A_CENTRE: - if self.wordWrap > 0: - if tLen > self.wordWrap: - aText = tWrap.wrap(tText) - for n in range(len(aText)): - aText[n] = self._centreText(aText[n],self.wordWrap) - tText = "\n".join(aText) - else: - tText = self._centreText(tText,self.wordWrap) - else: - if self.wordWrap > 0 and tLen > self.wordWrap: - tText = tWrap.fill(tText) - - # Then the text can receive final formatting before we - # append it to the results. We also store text lines in a - # buffer and merge them only when we find an empty line, - # indicating a new paragraph. - if tType == self.T_EMPTY: - if len(thisPar) > 0: - tTemp = "\n".join(thisPar) - self.theResult += "%s\n\n" % tTemp.rstrip() - thisPar = [] - - elif tType == self.T_HEAD1: - uLine = "="*min(tLen,self.wordWrap) - if tAlign == self.A_CENTRE: - uLine = self._centreText(uLine,self.wordWrap) - self.theResult += "%s\n%s\n\n" % (tText,uLine) - - elif tType == self.T_HEAD2: - uLine = "~"*min(tLen,self.wordWrap) - self.theResult += "%s\n%s\n\n" % (tText,uLine) - - elif tType == self.T_HEAD3: - uLine = "-"*min(tLen,self.wordWrap) - self.theResult += "%s\n%s\n\n" % (tText,uLine) - - elif tType == self.T_HEAD4: - self.theResult += "%s\n\n" % tText - - elif tType == self.T_SEP: - if self.wordWrap > 0 and tLen < self.wordWrap: - tText = self._centreText(tText,self.wordWrap) - self.theResult += "%s\n\n" % tText - - elif tType == self.T_SKIP: - self.theResult += "\n\n\n" - - elif tType == self.T_TEXT: - thisPar.append(tText) - - elif tType == self.T_COMMENT and self.doComments: - self.theResult += "%s\n\n" % tText - - elif tType == self.T_KEYWORD and self.doKeywords: - self.theResult += "%s\n\n" % tText - - return - -# END Class ToText From eecf1e000d07cab6e68e002a947c942701a79e32 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 10 May 2020 22:51:50 +0200 Subject: [PATCH 15/24] Finished cleaning up formatting on export and connected all dialog options --- nw/core/tohtml.py | 59 ++++++++++---- nw/core/tokenizer.py | 133 +++++++++++++++++++------------ nw/gui/build.py | 126 ++++++++++++++++++++++++++--- sample/sampleNovel/nwProject.nwx | 4 +- 4 files changed, 238 insertions(+), 84 deletions(-) diff --git a/nw/core/tohtml.py b/nw/core/tohtml.py index 0b786ad6..2b98be99 100644 --- a/nw/core/tohtml.py +++ b/nw/core/tohtml.py @@ -104,15 +104,24 @@ class ToHtml(Tokenizer): thisPar = [] for tType, tText, tFormat, tAlign in self.theTokens: + # Styles aStyle = [] if tAlign == self.A_CENTRE: aStyle.append("text-align: center;") + elif tAlign == self.A_RIGHT: + aStyle.append("text-align: right;") + elif tAlign == self.A_JUSTIFY: + aStyle.append("text-align: justify;") + + if tType == self.T_HEAD2: + aStyle.append("page-break-before: always;") if len(aStyle) > 0: hStyle = " style='%s'" % (" ".join(aStyle)) else: hStyle = "" + # Process TextType if tType == self.T_EMPTY: if len(thisPar) > 0: tTemp = "".join(thisPar) @@ -120,23 +129,30 @@ class ToHtml(Tokenizer): thisPar = [] elif tType == self.T_HEAD1: - self.theResult += "%s\n" % (hStyle,tText) + tHead = tText.replace(r"\\", "
") + self.theResult += "%s\n" % (hStyle, tHead) elif tType == self.T_HEAD2: - self.theResult += "%s\n" % (hStyle,tText) + tHead = tText.replace(r"\\", "
") + self.theResult += "%s\n" % (hStyle, tHead) elif tType == self.T_HEAD3: - self.theResult += "%s\n" % (hStyle,tText) + tHead = tText.replace(r"\\", "
") + self.theResult += "%s\n" % (hStyle, tHead) elif tType == self.T_HEAD4: - self.theResult += "%s\n" % (hStyle,tText) + tHead = tText.replace(r"\\", "
") + self.theResult += "%s\n" % (hStyle, tHead) elif tType == self.T_SEP: - self.theResult += "%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
" % retText - def _formatComments(self, tText): - """Apply HTML formatting to comments. - """ - - if not self.forPreview: - return "
%s
\n" % tText - - 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 @@ - + Sample Project Sample Project @@ -20,7 +20,7 @@
%title% - Chapter %num%\\%title% + Chapter %num%.\\%title% %title% * * *
From a7d2ca24a581b299e0238b2562f161f6feffe0b2 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 10 May 2020 22:56:30 +0200 Subject: [PATCH 16/24] Deleted the rest of the no longer needed code --- nw/common.py | 8 - nw/constants/__init__.py | 3 +- nw/constants/constants.py | 28 -- nw/gui/dialogs/export.py | 718 -------------------------------------- requirements.txt | 2 - 5 files changed, 1 insertion(+), 758 deletions(-) delete mode 100644 nw/gui/dialogs/export.py diff --git a/nw/common.py b/nw/common.py index 9f4b8011..bd535a71 100644 --- a/nw/common.py +++ b/nw/common.py @@ -172,11 +172,3 @@ def splitVersionNumber(vString): vInt = vMajor*10000 + vMinor*100 + vPatch return [vMajor, vMinor, vPatch, vInt] - -def packageRefURL(packName): - from nw.constants import nwDependencies - if packName in nwDependencies.PACKS.keys(): - return "%s" % ( - nwDependencies.PACKS[packName]["site"], packName - ) - return packName diff --git a/nw/constants/__init__.py b/nw/constants/__init__.py index eeab86cc..1abd529c 100644 --- a/nw/constants/__init__.py +++ b/nw/constants/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from nw.constants.iso import isoLanguage, isoCountry from nw.constants.constants import ( - nwConst, nwFiles, nwKeyWords, nwLabels, nwDependencies, nwQuotes, nwUnicode + nwConst, nwFiles, nwKeyWords, nwLabels, nwQuotes, nwUnicode ) from nw.constants.enum import ( nwAlert, nwDocAction, nwItemClass, nwItemLayout, nwItemType, nwOutline @@ -14,7 +14,6 @@ __all__ = [ "nwFiles", "nwKeyWords", "nwLabels", - "nwDependencies", "nwQuotes", "nwUnicode", "nwAlert", diff --git a/nw/constants/constants.py b/nw/constants/constants.py index 33a6fc15..3b61fccc 100644 --- a/nw/constants/constants.py +++ b/nw/constants/constants.py @@ -142,34 +142,6 @@ class nwLabels(): # END Class nwLabels -class nwDependencies(): - """Python package dependencies and their reference links. - """ - PACKS = { - "pyqt5" : { - "site" : "", - "docs" : "", - }, - "lxml" : { - "site" : "", - "docs" : "", - }, - "pyenchant" : { - "site" : "", - "docs" : "", - }, - "latexcodec" : { - "site" : "https://pypi.org/project/latexcodec/", - "docs" : "https://latexcodec.readthedocs.io/en/latest/", - }, - "pypandoc" : { - "site" : "https://pypi.org/project/pypandoc/", - "docs" : "https://pypi.org/project/pypandoc/", - }, - } - -# END Class nwDependencies - class nwQuotes(): """Allowed quotation marks. Source: https://en.wikipedia.org/wiki/Quotation_mark diff --git a/nw/gui/dialogs/export.py b/nw/gui/dialogs/export.py deleted file mode 100644 index 2d67e482..00000000 --- a/nw/gui/dialogs/export.py +++ /dev/null @@ -1,718 +0,0 @@ -# -*- coding: utf-8 -*- -"""novelWriter GUI Export Tools - - novelWriter – GUI Export Tools -================================ - Tool for exporting project files to other formats - - File History: - Created: 2019-10-13 [0.2.3] - - 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 . -""" - -import logging -import time -import nw - -from os import path - -from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import ( - QDialog, QHBoxLayout, QVBoxLayout, QWidget, QTabWidget, QGridLayout, - QGroupBox, QCheckBox, QLabel, QComboBox, QLineEdit, QPushButton, - QFileDialog, QProgressBar, QSpinBox, QMessageBox -) - -from nw.convert import TextFile, HtmlFile, MarkdownFile, LaTeXFile, ConcatFile -from nw.common import packageRefURL -from nw.constants import nwFiles, nwItemType, nwAlert - -logger = logging.getLogger(__name__) - -class GuiExport(QDialog): - - def __init__(self, theParent, theProject): - QDialog.__init__(self, theParent) - - logger.debug("Initialising GuiExport ...") - - self.mainConf = nw.CONFIG - self.theParent = theParent - self.theProject = theProject - self.optState = self.theProject.optState - - self.outerBox = QHBoxLayout() - self.innerBox = QVBoxLayout() - self.setWindowTitle("Export Project") - self.setLayout(self.outerBox) - - self.guiDeco = self.theParent.theTheme.loadDecoration("export",(64,64)) - - self.tabMain = GuiExportMain(self.theParent, self.theProject) - self.tabPandoc = GuiExportPandoc(self.theParent, self.theProject) - - self.tabWidget = QTabWidget() - self.tabWidget.addTab(self.tabMain, "Settings") - self.tabWidget.addTab(self.tabPandoc, "Pandoc") - - self.outerBox.addWidget(self.guiDeco, 0, Qt.AlignTop) - self.outerBox.addLayout(self.innerBox) - - self.doExportForm = QGridLayout() - self.doExportForm.setContentsMargins(10,5,0,10) - - self.exportButton = QPushButton("Export") - self.exportButton.clicked.connect(self._doExport) - - self.closeButton = QPushButton("Close") - self.closeButton.clicked.connect(self._doClose) - - self.exportStatus = QLabel("Ready ...") - self.exportProgress = QProgressBar(self) - - self.doExportForm.addWidget(self.exportStatus, 0, 0, 1, 3) - self.doExportForm.addWidget(self.exportProgress, 1, 0) - self.doExportForm.addWidget(self.exportButton, 1, 1) - self.doExportForm.addWidget(self.closeButton, 1, 2) - - self.innerBox.addWidget(self.tabWidget) - self.innerBox.addLayout(self.doExportForm) - - self.rejected.connect(self._doClose) - self.show() - - logger.debug("GuiExport initialisation complete") - - return - - ## - # Buttons - ## - - def _doExport(self): - - logger.verbose("GuiExport export button clicked") - - wNovel = self.tabMain.expNovel.isChecked() - wNotes = self.tabMain.expNotes.isChecked() - eFormat = self.tabMain.outputFormat.currentData() - fixWidth = self.tabMain.fixedWidth.value() - wComments = self.tabMain.expComments.isChecked() - wKeywords = self.tabMain.expKeywords.isChecked() - chFormat = self.tabMain.chapterFormat.text() - unFormat = self.tabMain.unnumFormat.text() - scFormat = self.tabMain.sceneFormat.text() - seFormat = self.tabMain.sectionFormat.text() - saveTo = self.tabMain.exportPath.text() - hScene = self.tabMain.hideScene.isChecked() - hSection = self.tabMain.hideSection.isChecked() - - pFormat = self.tabPandoc.outputFormat.currentData() - tFormat = GuiExportPandoc.FMT_VIA[pFormat] - - if saveTo.startswith("~"): - saveTo = path.expanduser(saveTo) - - exportDir = path.dirname(saveTo) - if not path.isdir(exportDir): - self.theParent.makeAlert("The export folder does not exist.",nwAlert.ERROR) - self.exportStatus.setText("Export failed ...") - return False - - nItems = len(self.theProject.projTree) - if eFormat == GuiExportMain.FMT_PDOC: - nItems += int(0.2*nItems) - self.exportProgress.setMinimum(0) - self.exportProgress.setMaximum(nItems) - self.exportProgress.setValue(0) - - if not wNovel and not wNotes: - self.exportStatus.setText("Nothing to export ...") - return False - - outFile = None - if eFormat == GuiExportMain.FMT_TXT: - outFile = TextFile(self.theProject, self.theParent) - elif eFormat == GuiExportMain.FMT_MD: - outFile = MarkdownFile(self.theProject, self.theParent) - elif eFormat == GuiExportMain.FMT_HTML: - outFile = HtmlFile(self.theProject, self.theParent) - elif eFormat == GuiExportMain.FMT_TEX: - outFile = LaTeXFile(self.theProject, self.theParent) - elif eFormat == GuiExportMain.FMT_NWD: - outFile = ConcatFile(self.theProject, self.theParent) - elif eFormat == GuiExportMain.FMT_PDOC: - if tFormat == "html": - outFile = HtmlFile(self.theProject, self.theParent) - elif tFormat == "markdown": - outFile = MarkdownFile(self.theProject, self.theParent) - - if outFile is None: - return False - - if outFile.openFile(saveTo): - outFile.setComments(wComments) - outFile.setKeywords(wKeywords) - outFile.setExportNovel(wNovel) - outFile.setExportNotes(wNotes) - outFile.setWordWrap(fixWidth) - outFile.setChapterFormat(chFormat) - outFile.setUnNumberedFormat(unFormat) - outFile.setSceneFormat(scFormat, hScene) - outFile.setSectionFormat(seFormat, hSection) - else: - self.exportStatus.setText("Failed to open file for writing ...") - return False - - time.sleep(0.5) - - nDone = 0 - for tItem in self.theProject.projTree: - - self.exportProgress.setValue(nDone) - self.exportStatus.setText("Exporting: %s" % tItem.itemName) - logger.verbose("Exporting: %s" % tItem.itemName) - - if tItem is not None and tItem.itemType == nwItemType.FILE: - outFile.addText(tItem.itemHandle) - - nDone += 1 - - outFile.closeFile() - self.exportProgress.setValue(nDone) - self.exportStatus.setText("Export to %s complete" % outFile.fileName) - logger.verbose("Export to %s complete" % outFile.fileName) - - if eFormat == GuiExportMain.FMT_TEX: - # Check that encoding was successful - if outFile.texCodecFail: - self.theParent.makeAlert(( - "Failed to escape unicode characters while writing LaTeX " - "file. The generated .tex file may not build properly. " - "Make sure the python package '{package:s}' is installed " - "and working." - ).format( - package = packageRefURL("latexcodec") - ), nwAlert.WARN) - - if eFormat != GuiExportMain.FMT_PDOC: - return True - - # If we've reached this point, we're also running Pandoc - - if self._callPandoc(saveTo, tFormat, pFormat): - self.exportProgress.setValue(nItems) - self.exportStatus.setText("Pandoc conversion complete") - logger.verbose("Pandoc conversion complete") - else: - self.exportProgress.setValue(nItems) - self.exportStatus.setText("Pandoc conversion failed") - logger.verbose("Pandoc conversion failed") - return False - - return True - - def _callPandoc(self, inFile, inFmt, outFmt): - - pFmt = { - GuiExportPandoc.FMT_ODT : "odt", - GuiExportPandoc.FMT_DOCX : "docx", - GuiExportPandoc.FMT_EPUB2 : "epub2", - GuiExportPandoc.FMT_EPUB3 : "epub3", - GuiExportPandoc.FMT_ZIM : "zimwiki", - } - - try: - import pypandoc - except: - self.theParent.makeAlert(( - "Could not load the '{package:s}' package. " - "Make sure it is installed, and try again." - ).format( - package = packageRefURL("pypandoc") - ), nwAlert.ERROR) - return False - - outFile = path.splitext(inFile)[0]+GuiExportPandoc.FMT_EXT[outFmt] - fileName = path.basename(outFile) - - if path.isfile(outFile) and self.mainConf.showGUI: - msgBox = QMessageBox() - msgRes = msgBox.question( - self.theParent, "Overwrite", - ("File '%s' already exists.
Do you want to overwrite it?" % fileName) - ) - if msgRes != QMessageBox.Yes: - return False - - try: - pypandoc.convert_file( - source_file = inFile, - format = inFmt, - outputfile = outFile, - to = pFmt[outFmt], - extra_args = (), - encoding = "utf-8", - filters = None - ) - except Exception as e: - self.theParent.makeAlert( - ["Failed to convert file using pypandoc + Pandoc.", - str(e)], nwAlert.ERROR - ) - return False - - return True - - def _doClose(self): - - logger.verbose("GuiExport close button clicked") - - # General Settings - wNovel = self.tabMain.expNovel.isChecked() - wNotes = self.tabMain.expNotes.isChecked() - eFormat = self.tabMain.outputFormat.currentData() - fixWidth = self.tabMain.fixedWidth.value() - wComments = self.tabMain.expComments.isChecked() - wKeywords = self.tabMain.expKeywords.isChecked() - chFormat = self.tabMain.chapterFormat.text() - unFormat = self.tabMain.unnumFormat.text() - scFormat = self.tabMain.sceneFormat.text() - seFormat = self.tabMain.sectionFormat.text() - saveTo = self.tabMain.exportPath.text() - hScene = self.tabMain.hideScene.isChecked() - hSection = self.tabMain.hideSection.isChecked() - - if saveTo.startswith("~"): - saveTo = path.expanduser(saveTo) - - self.optState.setValue("GuiExport", "wNovel", wNovel) - self.optState.setValue("GuiExport", "wNotes", wNotes) - self.optState.setValue("GuiExport", "eFormat", eFormat) - self.optState.setValue("GuiExport", "fixWidth", fixWidth) - self.optState.setValue("GuiExport", "wComments", wComments) - self.optState.setValue("GuiExport", "wKeywords", wKeywords) - self.optState.setValue("GuiExport", "chFormat", chFormat) - self.optState.setValue("GuiExport", "unFormat", unFormat) - self.optState.setValue("GuiExport", "scFormat", scFormat) - self.optState.setValue("GuiExport", "seFormat", seFormat) - self.optState.setValue("GuiExport", "saveTo", saveTo) - self.optState.setValue("GuiExport", "hScene", hScene) - self.optState.setValue("GuiExport", "hSection", hSection) - - # Pandoc Settings - pFormat = self.tabPandoc.outputFormat.currentData() - - self.optState.setValue("GuiExport", "pFormat", pFormat) - - self.optState.saveSettings() - self.close() - - return - -# END Class GuiExport - -class GuiExportMain(QWidget): - - FMT_NWD = 1 # novelWriter markdown - FMT_TXT = 2 # Plain text file - FMT_MD = 3 # Markdown file - FMT_HTML = 4 # HTML file - FMT_TEX = 5 # LaTeX file - FMT_PDOC = 6 # Pass to pandoc - FMT_EXT = { - FMT_NWD : ".nwd", - FMT_TXT : ".txt", - FMT_MD : ".md", - FMT_HTML : ".htm", - FMT_TEX : ".tex", - FMT_PDOC : ".tmp", - } - FMT_HELP = { - FMT_NWD : ( - "Exports a document using the novelWriter markdown format. " - "The files selected by the filters are appended as-is, " - "including comments and other settings." - ), - FMT_TXT : ( - "Exports a plain text file. All formatting is stripped and " - "comments are in square brackets." - ), - FMT_MD : ( - "Exports a standard markdown file. Comments are converted " - "to preformatted text blocks." - ), - FMT_HTML : ( - "Exports a plain html5 file. Comments are wrapped in " - "blocks with a yellow background colour." - ), - FMT_TEX : ( - "Exports a LaTeX file that can be compiled to PDF using " - "for instance PDFLaTeX. Comments are exported as LaTeX " - "comments." - ), - FMT_PDOC : ( - "Exports first to markdown or html5. The file is then " - "passed on to Pandoc for a second stage. Use the Pandoc " - "tab for settings up the conversion." - ), - } - - def __init__(self, theParent, theProject): - QWidget.__init__(self, theParent) - - self.theParent = theParent - self.theProject = theProject - self.theTheme = theParent.theTheme - self.outerBox = QGridLayout() - self.optState = self.theProject.optState - self.currFormat = self.FMT_TXT - - # Select Files - self.guiFiles = QGroupBox("Selection", self) - self.guiFilesForm = QGridLayout(self) - self.guiFiles.setLayout(self.guiFilesForm) - - self.expNovel = QCheckBox("Novel files",self) - self.expNovel.setChecked( - self.optState.getBool("GuiExport", "wNovel", True) - ) - self.expNovel.setToolTip("Include all novel files in the exported document") - - self.expNotes = QCheckBox("Note files",self) - self.expNotes.setChecked( - self.optState.getBool("GuiExport", "wNotes", False) - ) - self.expNotes.setToolTip("Include all note files in the exported document") - - self.expComments = QCheckBox("Comments",self) - self.expComments.setChecked( - self.optState.getBool("GuiExport", "wComments", False) - ) - self.expComments.setToolTip("Export comments from all files") - - self.expKeywords = QCheckBox("Keywords",self) - self.expKeywords.setChecked( - self.optState.getBool("GuiExport", "wKeywords", False) - ) - self.expKeywords.setToolTip("Export @keywords from all files") - - self.guiFilesForm.addWidget(self.expNovel, 0, 1) - self.guiFilesForm.addWidget(self.expComments, 0, 2) - self.guiFilesForm.addWidget(self.expNotes, 1, 1) - self.guiFilesForm.addWidget(self.expKeywords, 1, 2) - self.guiFilesForm.setRowStretch(2, 1) - - # Chapter Settings - self.guiChapters = QGroupBox("Chapter Headings", self) - self.guiChaptersForm = QGridLayout(self) - self.guiChapters.setLayout(self.guiChaptersForm) - - self.chapterFormat = QLineEdit() - self.chapterFormat.setMaxLength(200) - self.chapterFormat.setText( - self.optState.getString("GuiExport", "chFormat", "Chapter %numword%") - ) - self.chapterFormat.setToolTip("Available formats: %num%, %numword%, %title%") - self.chapterFormat.setMinimumWidth(250) - - self.unnumFormat = QLineEdit() - self.unnumFormat.setMaxLength(200) - self.unnumFormat.setText( - self.optState.getString("GuiExport", "unFormat", "%title%") - ) - self.unnumFormat.setToolTip("Available formats: %title%") - self.unnumFormat.setMinimumWidth(250) - - self.guiChaptersForm.addWidget(QLabel("Numbered"), 0, 0) - self.guiChaptersForm.addWidget(self.chapterFormat, 0, 1) - self.guiChaptersForm.addWidget(QLabel("Unnumbered"), 1, 0) - self.guiChaptersForm.addWidget(self.unnumFormat, 1, 1) - - # Scene and Section Settings - self.guiScenes = QGroupBox("Other Headings", self) - self.guiScenesForm = QGridLayout(self) - self.guiScenes.setLayout(self.guiScenesForm) - - self.sceneFormat = QLineEdit() - self.sceneFormat.setMaxLength(200) - self.sceneFormat.setText( - self.optState.getString("GuiExport", "scFormat", "* * *") - ) - self.sceneFormat.setToolTip("Available formats: %title%") - self.sceneFormat.setMinimumWidth(100) - - self.sectionFormat = QLineEdit() - self.sectionFormat.setMaxLength(200) - self.sectionFormat.setText( - self.optState.getString("GuiExport", "seFormat", "") - ) - self.sectionFormat.setToolTip("Available formats: %title%") - self.sectionFormat.setMinimumWidth(100) - - self.hideScene = QCheckBox("Skip",self) - self.hideScene.setChecked( - self.optState.getBool("GuiExport", "hScene", False) - ) - self.hideScene.setToolTip("Skip scene titles in export") - - self.hideSection = QCheckBox("Skip",self) - self.hideSection.setChecked( - self.optState.getBool("GuiExport", "hSection", False) - ) - self.hideSection.setToolTip("Skip section titles in export") - - self.guiScenesForm.addWidget(QLabel("Scenes"), 0, 0) - self.guiScenesForm.addWidget(self.sceneFormat, 0, 1) - self.guiScenesForm.addWidget(self.hideScene, 0, 2) - self.guiScenesForm.addWidget(QLabel("Sections"), 1, 0) - self.guiScenesForm.addWidget(self.sectionFormat, 1, 1) - self.guiScenesForm.addWidget(self.hideSection, 1, 2) - - # Output Path - self.exportTo = QGroupBox("Export Folder", self) - self.exportToForm = QGridLayout(self) - self.exportTo.setLayout(self.exportToForm) - - self.exportPath = QLineEdit( - self.optState.getString("GuiExport", "saveTo", "") - ) - self.exportGetPath = QPushButton(self.theTheme.getIcon("folder"),"") - self.exportGetPath.clicked.connect(self._exportFolder) - - self.exportToForm.addWidget(QLabel("Save to"), 0, 0) - self.exportToForm.addWidget(self.exportPath, 0, 1) - self.exportToForm.addWidget(self.exportGetPath, 0, 2) - - # Output Format - self.guiOutput = QGroupBox("Export", self) - self.guiOutputForm = QGridLayout(self) - self.guiOutput.setLayout(self.guiOutputForm) - - self.outputHelp = QLabel("") - self.outputHelp.setWordWrap(True) - self.outputHelp.setMinimumHeight(55) - self.outputHelp.setAlignment(Qt.AlignTop) - - self.outputFormat = QComboBox(self) - self.outputFormat.addItem("novelWriter Markdown (.nwd)", self.FMT_NWD) - self.outputFormat.addItem("Plain Text (.txt)", self.FMT_TXT) - self.outputFormat.addItem("Markdown (.md)", self.FMT_MD) - self.outputFormat.addItem("HTML5 (.htm)", self.FMT_HTML) - self.outputFormat.addItem("LaTeX for PDF (.tex)", self.FMT_TEX) - self.outputFormat.addItem("Pandoc via Markdown or HTML", self.FMT_PDOC) - self.outputFormat.currentIndexChanged.connect(self._updateFormat) - - optIdx = self.outputFormat.findData( - self.optState.getInt("GuiExport", "eFormat", 1) - ) - if optIdx == -1: - self.outputFormat.setCurrentIndex(1) - self._updateFormat(1) - else: - self.outputFormat.setCurrentIndex(optIdx) - self._updateFormat(optIdx) - - self.guiOutputForm.addWidget(QLabel("Format"), 0, 0) - self.guiOutputForm.addWidget(self.outputFormat, 0, 1) - self.guiOutputForm.addWidget(self.outputHelp, 1, 0, 1, 3) - self.guiOutputForm.setColumnStretch(2, 1) - - # Additional Settings - self.addSettings = QGroupBox("Additional Settings (Format Dependent)", self) - self.addSettingsForm = QGridLayout(self) - self.addSettings.setLayout(self.addSettingsForm) - - self.fixedWidth = QSpinBox(self) - self.fixedWidth.setMinimum(0) - self.fixedWidth.setMaximum(999) - self.fixedWidth.setSingleStep(1) - self.fixedWidth.setValue( - self.optState.getInt("GuiExport", "fixWidth", 80) - ) - self.fixedWidth.setToolTip( - "Applies to .txt and .md files. A value of '0' disables the feature." - ) - - self.addSettingsForm.addWidget(QLabel("Fixed width"), 0, 0) - self.addSettingsForm.addWidget(self.fixedWidth, 0, 1) - self.addSettingsForm.setColumnStretch(2, 1) - - # Assemble - self.outerBox.addWidget(self.guiOutput, 0, 0, 1, 2) - self.outerBox.addWidget(self.guiFiles, 0, 2) - self.outerBox.addWidget(self.guiChapters, 1, 0, 1, 2) - self.outerBox.addWidget(self.guiScenes, 1, 2) - self.outerBox.addWidget(self.addSettings, 2, 0, 1, 3) - self.outerBox.addWidget(self.exportTo, 3, 0, 1, 3) - self.outerBox.setColumnStretch(0, 1) - self.outerBox.setColumnStretch(1, 1) - self.outerBox.setColumnStretch(2, 1) - self.setLayout(self.outerBox) - - return - - ## - # Internal Functions - ## - - def _updateFormat(self, currIdx): - """Update help text under output format selection and file - extension in file box - """ - if currIdx == -1: - self.outputHelp.setText("") - else: - self.currFormat = self.outputFormat.itemData(currIdx) - self.outputHelp.setText("%s" % self.FMT_HELP[self.currFormat]) - self._checkFileExtension() - return - - def _exportFolder(self): - - currDir = self.exportPath.text() - if not path.isdir(currDir): - currDir = "" - - extFilter = [ - "novelWriter document files (*.nwd)", - "Text files (*.txt)", - "Markdown files (*.md)", - "HTML files (*.htm *.html)", - "LaTeX files (*.tex)", - "All files (*.*)", - ] - - dlgOpt = QFileDialog.Options() - dlgOpt |= QFileDialog.DontUseNativeDialog - saveTo = QFileDialog.getSaveFileName( - self, "Export File", self.exportPath.text(), - options=dlgOpt, filter=";;".join(extFilter) - ) - if saveTo: - self.exportPath.setText(saveTo[0]) - self._checkFileExtension() - return True - - return False - - def _checkFileExtension(self): - saveTo = self.exportPath.text() - if saveTo.startswith("~"): - saveTo = path.expanduser(saveTo) - fileBits = path.splitext(saveTo) - if self.currFormat > 0 and fileBits[0].strip() != "": - saveTo = fileBits[0]+self.FMT_EXT[self.currFormat] - self.exportPath.setText(saveTo) - return - -# END Class GuiExportMain - -class GuiExportPandoc(QWidget): - - FMT_ODT = 1 - FMT_DOCX = 2 - FMT_EPUB2 = 4 - FMT_EPUB3 = 5 - FMT_ZIM = 6 - FMT_EXT = { - FMT_ODT : ".odt", - FMT_DOCX : ".docx", - FMT_EPUB2 : ".epub", - FMT_EPUB3 : ".epub", - FMT_ZIM : ".txt", - } - FMT_VIA = { - FMT_ODT : "html", - FMT_DOCX : "html", - FMT_EPUB2 : "markdown", - FMT_EPUB3 : "markdown", - FMT_ZIM : "markdown", - } - - def __init__(self, theParent, theProject): - QWidget.__init__(self, theParent) - - self.theParent = theParent - self.theProject = theProject - self.outerBox = QGridLayout() - self.optState = self.theProject.optState - - try: - import pypandoc - self.hasPyPan = True - except: - self.hasPyPan = False - - # Information - self.guiInfo = QGroupBox("Information", self) - self.guiInfoBox = QVBoxLayout(self) - self.guiInfo.setLayout(self.guiInfoBox) - - self.infoHelp = QLabel("") - self.infoHelp.setWordWrap(True) - self.infoHelp.setMinimumHeight(55) - self.infoHelp.setAlignment(Qt.AlignTop) - - self.guiInfoBox.addWidget(self.infoHelp) - - if self.hasPyPan: - self.infoHelp.setText(( - "Additional export to other document formats than in the Settings tab is provided " - "by Pandoc. the project is first exported to Markdown or HTML, depending on final " - "format, and then processed by Pandoc into the desired format." - )) - else: - self.infoHelp.setText(( - "The Python package 'pypandoc' is not installed or isn't working. This package is " - "required for interfacing with Pandoc. Please install it before proceeding." - )) - - # Output Format - self.guiOutput = QGroupBox("Pandoc Format", self) - self.guiOutputForm = QGridLayout(self) - self.guiOutput.setLayout(self.guiOutputForm) - - self.outputFormat = QComboBox(self) - self.outputFormat.addItem("Open Office Document (.odt)", self.FMT_ODT) - self.outputFormat.addItem("Word Document (.docx)", self.FMT_DOCX) - self.outputFormat.addItem("ePUB eBook v2 (.epub2)", self.FMT_EPUB2) - self.outputFormat.addItem("ePUB eBook v3 (.epub3)", self.FMT_EPUB3) - self.outputFormat.addItem("Zim Wiki (.txt)", self.FMT_ZIM) - - optIdx = self.outputFormat.findData( - self.optState.getInt("GuiExport", "pFormat", 1) - ) - if optIdx == -1: - self.outputFormat.setCurrentIndex(1) - else: - self.outputFormat.setCurrentIndex(optIdx) - - self.guiOutputForm.addWidget(QLabel("Format"), 0, 0) - self.guiOutputForm.addWidget(self.outputFormat, 0, 1) - self.guiOutputForm.setColumnStretch(2, 1) - - # Assemble - self.outerBox.addWidget(self.guiInfo, 0, 0) - self.outerBox.addWidget(self.guiOutput, 1, 0) - self.outerBox.setRowStretch(2, 1) - self.setLayout(self.outerBox) - - return - -# END Class GuiExportPandoc diff --git a/requirements.txt b/requirements.txt index 648be38e..08fc42c3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,3 @@ pyqt5 lxml pyenchant -latexcodec -pypandoc From 0faf1f9986c3172cb45f63e19f723e085eab1616 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 10 May 2020 23:24:32 +0200 Subject: [PATCH 17/24] The isExported option should only apply to files --- nw/core/project.py | 2 +- nw/gui/dialogs/itemeditor.py | 9 +++++++-- nw/gui/elements/docdetails.py | 11 +++++++---- nw/gui/elements/doctree.py | 18 ++++++++++++------ sample/sampleNovel/nwProject.nwx | 8 +------- tests/reference/gui/0_nwProject.nwx | 7 +------ tests/reference/gui/1_nwProject.nwx | 7 +------ tests/reference/gui/2_nwProject.nwx | 7 +------ tests/reference/gui/3_nwProject.nwx | 9 ++------- tests/reference/proj/1_nwProject.nwx | 7 +------ tests/reference/proj/2_nwProject.nwx | 11 +---------- tests/test_gui.py | 3 +++ tests/test_item.py | 4 ++-- 13 files changed, 40 insertions(+), 63 deletions(-) diff --git a/nw/core/project.py b/nw/core/project.py index a0396c02..0dcb60b9 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -1350,8 +1350,8 @@ 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,"exported", text=str(self.isExported)) xSub = self._subPack(xPack,"layout", text=str(self.itemLayout.name)) xSub = self._subPack(xPack,"charCount", text=str(self.charCount), none=False) xSub = self._subPack(xPack,"wordCount", text=str(self.wordCount), none=False) diff --git a/nw/gui/dialogs/itemeditor.py b/nw/gui/dialogs/itemeditor.py index 432ddb08..fee4a74f 100644 --- a/nw/gui/dialogs/itemeditor.py +++ b/nw/gui/dialogs/itemeditor.py @@ -105,9 +105,14 @@ class GuiItemEditor(QDialog): if itemLayout in self.validLayouts: self.editLayout.addItem(nwLabels.LAYOUT_NAME[itemLayout],itemLayout) - self.editExport = QSwitch() - self.editExport.setChecked(self.theItem.isExported) self.textExport = QLabel("Include when building project") + self.editExport = QSwitch() + if self.theItem.itemType == nwItemType.FILE: + self.editExport.setEnabled(True) + self.editExport.setChecked(self.theItem.isExported) + else: + self.editExport.setEnabled(False) + self.editExport.setChecked(False) self.mainForm.addWidget(QLabel("Label"), 0, 0) self.mainForm.addWidget(self.editName, 0, 1, 1, 2) diff --git a/nw/gui/elements/docdetails.py b/nw/gui/elements/docdetails.py index de547c06..8c231fa6 100644 --- a/nw/gui/elements/docdetails.py +++ b/nw/gui/elements/docdetails.py @@ -32,7 +32,7 @@ from PyQt5.QtCore import Qt from PyQt5.QtGui import QFont from PyQt5.QtWidgets import QFrame, QGridLayout, QLabel -from nw.constants import nwLabels, nwItemClass, nwUnicode +from nw.constants import nwLabels, nwItemClass, nwItemType, nwUnicode logger = logging.getLogger(__name__) @@ -170,10 +170,13 @@ class GuiDocDetails(QFrame): iStatus = self.theProject.importItems.checkEntry(iStatus) # Make sure it's valid flagIcon = self.theParent.importIcons[iStatus] - if nwItem.isExported: - exportFlag = nwUnicode.U_CHECK + if nwItem.itemType == nwItemType.FILE: + if nwItem.isExported: + exportFlag = nwUnicode.U_CHECK + else: + exportFlag = " " else: - exportFlag = " " + exportFlag = "+" self.labelFlag.setText(exportFlag) self.statusFlag.setPixmap(flagIcon.pixmap(10, 10)) diff --git a/nw/gui/elements/doctree.py b/nw/gui/elements/doctree.py index 0c6b8bb2..4b0a3ed2 100644 --- a/nw/gui/elements/doctree.py +++ b/nw/gui/elements/doctree.py @@ -412,13 +412,19 @@ class GuiDocTree(QTreeWidget): tHandle = nwItem.itemHandle pHandle = nwItem.parHandle - if nwItem.isExported: - tStatus = nwUnicode.U_CHECK - else: - tStatus = " " - tStatus += " "+nwLabels.CLASS_FLAG[nwItem.itemClass] + stExport = " " + stClass = nwLabels.CLASS_FLAG[nwItem.itemClass] + stLayout = "" + if nwItem.itemType == nwItemType.FILE: - tStatus += "."+nwLabels.LAYOUT_FLAG[nwItem.itemLayout] + stLayout = "."+nwLabels.LAYOUT_FLAG[nwItem.itemLayout] + if nwItem.isExported: + stExport = nwUnicode.U_CHECK + else: + stExport = "+" + + tStatus = stExport+" "+stClass+stLayout + iStatus = nwItem.itemStatus if tClass == nwItemClass.NOVEL: iStatus = self.theProject.statusItems.checkEntry(iStatus) # Make sure it's valid diff --git a/sample/sampleNovel/nwProject.nwx b/sample/sampleNovel/nwProject.nwx index 7b76aea0..eb89f47c 100644 --- a/sample/sampleNovel/nwProject.nwx +++ b/sample/sampleNovel/nwProject.nwx @@ -1,5 +1,5 @@ - + Sample Project Sample Project @@ -51,7 +51,6 @@ NOVEL Started True - True Title Page @@ -72,7 +71,6 @@ NOVEL 1st Draft True - True Chapter One @@ -171,7 +169,6 @@ CHARACTER None True - True Main Characters @@ -179,7 +176,6 @@ CHARACTER None True - True John Smith @@ -213,7 +209,6 @@ WORLD None True - True Earth @@ -260,7 +255,6 @@ TRASH None True - True Delete Me! diff --git a/tests/reference/gui/0_nwProject.nwx b/tests/reference/gui/0_nwProject.nwx index 3007ae93..db1151d3 100644 --- a/tests/reference/gui/0_nwProject.nwx +++ b/tests/reference/gui/0_nwProject.nwx @@ -1,5 +1,5 @@ - + @@ -42,7 +42,6 @@ NOVEL New False - True New Chapter @@ -50,7 +49,6 @@ NOVEL New False - True New Scene @@ -71,7 +69,6 @@ CHARACTER New False - True Plot @@ -79,7 +76,6 @@ PLOT New False - True World @@ -87,7 +83,6 @@ WORLD New False - True diff --git a/tests/reference/gui/1_nwProject.nwx b/tests/reference/gui/1_nwProject.nwx index 38708ab2..bf16def6 100644 --- a/tests/reference/gui/1_nwProject.nwx +++ b/tests/reference/gui/1_nwProject.nwx @@ -1,5 +1,5 @@ - + @@ -42,7 +42,6 @@ NOVEL New True - True New Chapter @@ -50,7 +49,6 @@ NOVEL New True - True New Scene @@ -71,7 +69,6 @@ CHARACTER New True - True New File @@ -92,7 +89,6 @@ PLOT New True - True New File @@ -113,7 +109,6 @@ WORLD New True - True New File diff --git a/tests/reference/gui/2_nwProject.nwx b/tests/reference/gui/2_nwProject.nwx index 58b80897..bf3c5b61 100644 --- a/tests/reference/gui/2_nwProject.nwx +++ b/tests/reference/gui/2_nwProject.nwx @@ -1,5 +1,5 @@ - + Project Name Project Title @@ -46,7 +46,6 @@ NOVEL New False - True New Chapter @@ -54,7 +53,6 @@ NOVEL New False - True New Scene @@ -75,7 +73,6 @@ CHARACTER New False - True Plot @@ -83,7 +80,6 @@ PLOT New False - True World @@ -91,7 +87,6 @@ WORLD New False - True diff --git a/tests/reference/gui/3_nwProject.nwx b/tests/reference/gui/3_nwProject.nwx index 7c5d8511..bee4eb0a 100644 --- a/tests/reference/gui/3_nwProject.nwx +++ b/tests/reference/gui/3_nwProject.nwx @@ -1,5 +1,5 @@ - + @@ -42,7 +42,6 @@ NOVEL New False - True New Chapter @@ -50,7 +49,6 @@ NOVEL New False - True Just a Page @@ -58,7 +56,7 @@ NOVEL Note False - True + False PAGE 0 0 @@ -71,7 +69,6 @@ CHARACTER New False - True Plot @@ -79,7 +76,6 @@ PLOT New False - True World @@ -87,7 +83,6 @@ WORLD New False - True diff --git a/tests/reference/proj/1_nwProject.nwx b/tests/reference/proj/1_nwProject.nwx index febdc50e..0e537470 100644 --- a/tests/reference/proj/1_nwProject.nwx +++ b/tests/reference/proj/1_nwProject.nwx @@ -1,5 +1,5 @@ - + @@ -42,7 +42,6 @@ NOVEL New False - True Characters @@ -50,7 +49,6 @@ CHARACTER New False - True Plot @@ -58,7 +56,6 @@ PLOT New False - True World @@ -66,7 +63,6 @@ WORLD New False - True New Chapter @@ -74,7 +70,6 @@ NOVEL New False - True New Scene diff --git a/tests/reference/proj/2_nwProject.nwx b/tests/reference/proj/2_nwProject.nwx index b64d0071..eb98cc9f 100644 --- a/tests/reference/proj/2_nwProject.nwx +++ b/tests/reference/proj/2_nwProject.nwx @@ -1,5 +1,5 @@ - + @@ -42,7 +42,6 @@ NOVEL New False - True Characters @@ -50,7 +49,6 @@ CHARACTER New False - True Plot @@ -58,7 +56,6 @@ PLOT New False - True World @@ -66,7 +63,6 @@ WORLD New False - True New Chapter @@ -74,7 +70,6 @@ NOVEL New False - True New Scene @@ -95,7 +90,6 @@ TIMELINE New False - True Object @@ -103,7 +97,6 @@ OBJECT New False - True Custom1 @@ -111,7 +104,6 @@ CUSTOM New False - True Custom2 @@ -119,7 +111,6 @@ CUSTOM New False - True diff --git a/tests/test_gui.py b/tests/test_gui.py index 9e2cd53d..c32e8168 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -361,6 +361,9 @@ def testItemEditor(qtbot, nwTempGUI, nwRef, nwTemp): layoutIdx = itemEdit.editLayout.findData(nwItemLayout.PAGE) itemEdit.editLayout.setCurrentIndex(layoutIdx) + itemEdit.editExport.setChecked(False) + assert not itemEdit.editExport.isChecked() + itemEdit._doSave() itemEdit = GuiItemEditor(nwGUI, nwGUI.theProject, "31489056e0916") diff --git a/tests/test_item.py b/tests/test_item.py index 19f663f6..1bab7447 100644 --- a/tests/test_item.py +++ b/tests/test_item.py @@ -197,8 +197,8 @@ def testItemXMLPackUnpack(): assert etree.tostring(xContent, pretty_print=False, encoding="utf-8") == ( b"" b"" - b"A NameTRASHTRASHMain" - b"TrueTrue" + b"A NameTRASHTRASH" + b"MainTrue" b"" b"" ) From 3ad4e26dbc5613963394cc12a08329858d9d516e Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 11 May 2020 00:20:46 +0200 Subject: [PATCH 18/24] Cleanup of unneeded imports --- nw/core/project.py | 2 +- nw/gui/build.py | 5 ++--- nw/gui/dialogs/docmerge.py | 3 ++- nw/gui/dialogs/docsplit.py | 3 ++- nw/gui/dialogs/itemeditor.py | 2 +- nw/gui/elements/doceditor.py | 2 +- nw/gui/tools/optionstate.py | 1 - nw/guimain.py | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/nw/core/project.py b/nw/core/project.py index 0dcb60b9..23fa2c24 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -44,7 +44,7 @@ from nw.core.tools import projectMaintenance from nw.core.document import NWDoc from nw.common import checkString, checkBool, checkInt, formatTimeStamp from nw.constants import ( - nwFiles, nwConst, nwItemType, nwItemClass, nwItemLayout, nwAlert + nwFiles, nwItemType, nwItemClass, nwItemLayout, nwAlert ) logger = logging.getLogger(__name__) diff --git a/nw/gui/build.py b/nw/gui/build.py index 2e1e5f0f..455c7ec7 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -35,9 +35,8 @@ from PyQt5.QtGui import ( QTextOption, QPalette, QColor, QTextDocumentWriter ) from PyQt5.QtWidgets import ( - QDialog, QVBoxLayout, QHBoxLayout, QTextBrowser, QPushButton, - QLabel, QLineEdit, QGroupBox, QGridLayout, QComboBox, QProgressBar, - QMenu, QAction, QFileDialog + QDialog, QVBoxLayout, QHBoxLayout, QTextBrowser, QPushButton, QLabel, + QLineEdit, QGroupBox, QGridLayout, QProgressBar, QMenu, QAction, QFileDialog ) from nw.gui.additions import QSwitch diff --git a/nw/gui/dialogs/docmerge.py b/nw/gui/dialogs/docmerge.py index 097f4217..1094fb78 100644 --- a/nw/gui/dialogs/docmerge.py +++ b/nw/gui/dialogs/docmerge.py @@ -52,10 +52,11 @@ class GuiDocMerge(QDialog): self.outerBox = QHBoxLayout() self.innerBox = QVBoxLayout() - self.setWindowTitle("Merge Documents") self.setLayout(self.outerBox) + self.setWindowTitle("Merge Documents") self.guiDeco = self.theParent.theTheme.loadDecoration("merge",(64,64)) + self.outerBox.setSpacing(16) self.outerBox.addWidget(self.guiDeco, 0, Qt.AlignTop) self.outerBox.addLayout(self.innerBox) diff --git a/nw/gui/dialogs/docsplit.py b/nw/gui/dialogs/docsplit.py index bfb4e8ee..dc795c77 100644 --- a/nw/gui/dialogs/docsplit.py +++ b/nw/gui/dialogs/docsplit.py @@ -53,10 +53,11 @@ class GuiDocSplit(QDialog): self.outerBox = QHBoxLayout() self.innerBox = QVBoxLayout() - self.setWindowTitle("Split Document") self.setLayout(self.outerBox) + self.setWindowTitle("Split Document") self.guiDeco = self.theParent.theTheme.loadDecoration("split",(64,64)) + self.outerBox.setSpacing(16) self.outerBox.addWidget(self.guiDeco, 0, Qt.AlignTop) self.outerBox.addLayout(self.innerBox) diff --git a/nw/gui/dialogs/itemeditor.py b/nw/gui/dialogs/itemeditor.py index fee4a74f..c94e053e 100644 --- a/nw/gui/dialogs/itemeditor.py +++ b/nw/gui/dialogs/itemeditor.py @@ -31,7 +31,7 @@ import nw from PyQt5.QtCore import Qt from PyQt5.QtWidgets import ( QDialog, QHBoxLayout, QVBoxLayout, QGroupBox, QGridLayout, QLineEdit, - QPushButton, QComboBox, QLabel, QSpacerItem, QSizePolicy, QDialogButtonBox + QComboBox, QLabel, QSpacerItem, QSizePolicy, QDialogButtonBox ) from nw.gui.additions import QSwitch diff --git a/nw/gui/elements/doceditor.py b/nw/gui/elements/doceditor.py index 8aa583da..96ab264f 100644 --- a/nw/gui/elements/doceditor.py +++ b/nw/gui/elements/doceditor.py @@ -32,7 +32,7 @@ from time import time from PyQt5.QtCore import Qt, QTimer, pyqtSlot from PyQt5.QtWidgets import ( - qApp, QTextEdit, QAction, QMenu, QShortcut, QMessageBox, QLabel + qApp, QTextEdit, QAction, QMenu, QShortcut, QMessageBox ) from PyQt5.QtGui import ( QTextCursor, QTextOption, QKeySequence, QFont, QColor, QPalette, diff --git a/nw/gui/tools/optionstate.py b/nw/gui/tools/optionstate.py index 776dbcb0..ed45e7cf 100644 --- a/nw/gui/tools/optionstate.py +++ b/nw/gui/tools/optionstate.py @@ -32,7 +32,6 @@ import nw from os import path -from nw.common import checkString, checkBool, checkInt from nw.constants import nwFiles logger = logging.getLogger(__name__) diff --git a/nw/guimain.py b/nw/guimain.py index c76e9c6f..8a37386c 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -45,7 +45,7 @@ from nw.gui import ( GuiConfigEditor, GuiProjectEditor, GuiItemEditor, GuiProjectOutline, GuiSessionLogView, GuiDocMerge, GuiDocSplit, GuiProjectLoad, GuiBuildNovel ) -from nw.core import NWProject, NWDoc, NWIndex, countWords +from nw.core import NWProject, NWDoc, NWIndex from nw.constants import nwFiles, nwItemType, nwAlert logger = logging.getLogger(__name__) From 4dead91af26f6d7d046f7d199d96c1ad60fdc534 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 11 May 2020 18:18:19 +0200 Subject: [PATCH 19/24] Extended the style formatting of tokenizer and tohtml to allow multiple styles for a block --- nw/core/tohtml.py | 36 +++++--- nw/core/tokenizer.py | 204 ++++++++++++++++++++++++++++++++++++------- nw/gui/build.py | 43 ++++----- 3 files changed, 219 insertions(+), 64 deletions(-) diff --git a/nw/core/tohtml.py b/nw/core/tohtml.py index 2b98be99..ec3cec22 100644 --- a/nw/core/tohtml.py +++ b/nw/core/tohtml.py @@ -102,19 +102,35 @@ class ToHtml(Tokenizer): self.theResult = "" thisPar = [] - for tType, tText, tFormat, tAlign in self.theTokens: + for tType, tText, tFormat, tStyle in self.theTokens: # Styles aStyle = [] - if tAlign == self.A_CENTRE: - aStyle.append("text-align: center;") - elif tAlign == self.A_RIGHT: - aStyle.append("text-align: right;") - elif tAlign == self.A_JUSTIFY: - aStyle.append("text-align: justify;") - - if tType == self.T_HEAD2: - aStyle.append("page-break-before: always;") + if tStyle is not None: + if tStyle & self.A_LEFT: + aStyle.append("text-align: left;") + if tStyle & self.A_RIGHT: + aStyle.append("text-align: right;") + if tStyle & self.A_CENTRE: + aStyle.append("text-align: center;") + if tStyle & self.A_JUSTIFY: + aStyle.append("text-align: justify;") + if tStyle & self.A_PBB: + aStyle.append("page-break-before: always;") + if tStyle & self.A_PBB_L: + aStyle.append("page-break-before: left;") + if tStyle & self.A_PBB_R: + aStyle.append("page-break-before: right;") + if tStyle & self.A_PBB_AV: + aStyle.append("page-break-before: avoid;") + if tStyle & self.A_PBA: + aStyle.append("page-break-after: always;") + if tStyle & self.A_PBA_L: + aStyle.append("page-break-after: left;") + if tStyle & self.A_PBA_R: + aStyle.append("page-break-after: right;") + if tStyle & self.A_PBA_AV: + aStyle.append("page-break-after: avoid;") if len(aStyle) > 0: hStyle = " style='%s'" % (" ".join(aStyle)) diff --git a/nw/core/tokenizer.py b/nw/core/tokenizer.py index a39d12f2..84fbb100 100644 --- a/nw/core/tokenizer.py +++ b/nw/core/tokenizer.py @@ -60,10 +60,18 @@ class Tokenizer(): 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 = 4 # Centred + A_JUSTIFY = 8 # Justified + A_PBB = 16 # Page break before + A_PBB_L = 32 # Page break before, left + A_PBB_R = 64 # Page break before, right + A_PBB_AV = 128 # Page break, avoid + A_PBA = 256 # Page break after + A_PBA_L = 512 # Page break after, left + A_PBA_R = 1024 # Page break after, right + A_PBA_AV = 2048 # Page break, avoid def __init__(self, theProject, theParent): @@ -208,6 +216,13 @@ class Tokenizer(): just contains plain text. in the case of plain text, apply the same RegExes that the syntax highlighter uses and save the locations of these formatting tags into the token array. + + The format of the token list is an entry with a four-tuple for + each line in the file. The tuple is as follows: + 1: The type of the block, self.T_* + 2: The text content of the block, without leading tags + 3: The internal formatting map of the text, self.FMT_* + 4: The style of the block, self.A_* """ # RegExes for adding formatting tags within text lines @@ -233,23 +248,63 @@ class Tokenizer(): # 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, + None + )) elif aLine[0] == "%": cLine = aLine[1:].strip() if cLine.lower().startswith("synopsis:"): - self.theTokens.append((self.T_SYNOPSIS, cLine[9:].strip(), None, defAlign)) + self.theTokens.append(( + self.T_SYNOPSIS, + cLine[9:].strip(), + None, + defAlign + )) else: - self.theTokens.append((self.T_COMMENT, aLine[1:].strip(), None, defAlign)) + 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 | self.A_PBB + )) 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 | self.A_PBA_AV + )) 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 | self.A_PBA_AV + )) 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 | self.A_PBA_AV + )) else: if not self.doBodyText: # Skip all body text @@ -270,10 +325,20 @@ 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, defAlign)) + 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, + None + )) return @@ -308,53 +373,126 @@ class Tokenizer(): if tType == self.T_TEXT: self.firstScene = False - elif tType == self.T_HEAD2: + elif tType == self.T_HEAD2: # Novel Chapter 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.A_PBB_R + ) self.firstScene = True - elif tType == self.T_HEAD3: + elif tType == self.T_HEAD3: # Novel Scene 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, + None + ) 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, + None + ) else: - self.theTokens[n] = (self.T_SKIP, "", None, self.A_LEFT) + self.theTokens[n] = ( + self.T_SKIP, + "", + None, + None + ) elif tTemp == self.fmtScene: if self.firstScene: - self.theTokens[n] = (self.T_EMPTY, "", None, self.A_LEFT) + self.theTokens[n] = ( + self.T_EMPTY, + "", + None, + None + ) 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.A_PBA_AV + ) self.firstScene = False - elif tType == self.T_HEAD4: + elif tType == self.T_HEAD4: # Novel Section 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, + None + ) elif tTemp == "" and not self.hideSection: - self.theTokens[n] = (self.T_SKIP, "", None, self.A_LEFT) + self.theTokens[n] = ( + self.T_SKIP, + "", + None, + None + ) 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 | self.A_PBA_AV + ) - # For title page and partitions, we need to centre all text - # and for some formats, we need a page break + # For title page and partitions, we need to centre all text. + # For partition, we also add a page break before, and for + # both types we always add a page break after the content. if isTitle or isPart: for n in range(len(self.theTokens)): tToken = self.theTokens[n] tType = tToken[0] tText = tToken[1] tFormat = tToken[2] - self.theTokens[n] = (tType, tText, tFormat, self.A_CENTRE) - - self.theTokens.append((self.T_PBREAK, "", None, self.A_LEFT)) + if isTitle: + self.theTokens[n] = ( + tType, + tText, + tFormat, + self.A_CENTRE + ) + else: + self.theTokens[n] = ( + tType, + tText, + tFormat, + self.A_CENTRE | self.A_PBB_R + ) + self.theTokens.append(( + self.T_PBREAK, + "", + None, + None + )) return diff --git a/nw/gui/build.py b/nw/gui/build.py index 455c7ec7..0cca7332 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -129,21 +129,6 @@ 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) @@ -159,6 +144,21 @@ class GuiBuildNovel(QDialog): self.buildForm.setColumnStretch(0, 1) self.buildForm.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) + # Include Switches # ================ self.includeGroup = QGroupBox("Include Non-Text Elements", self) @@ -230,7 +230,7 @@ class GuiBuildNovel(QDialog): self.saveODT.triggered.connect(lambda: self._saveDocument(self.FMT_ODT)) self.saveMenu.addAction(self.saveODT) - # self.savePDF = QAction("Portable Document (.pdf)") + # self.savePDF = QAction("Portable Document Format (.pdf)") # self.savePDF.triggered.connect(lambda: self._saveDocument(self.FMT_PDF)) # self.saveMenu.addAction(self.savePDF) @@ -238,13 +238,14 @@ class GuiBuildNovel(QDialog): self.saveHTM1.triggered.connect(lambda: self._saveDocument(self.FMT_HTM1)) self.saveMenu.addAction(self.saveHTM1) - self.saveHTM2 = QAction("Plain HTML (.htm)") + self.saveHTM2 = QAction("%s HTML (.htm)" % nw.__package__) self.saveHTM2.triggered.connect(lambda: self._saveDocument(self.FMT_HTM2)) self.saveMenu.addAction(self.saveHTM2) - # self.saveMD = QAction("Markdown (.md)") - # self.saveMD.triggered.connect(lambda: self._saveDocument(self.FMT_MD)) - # self.saveMenu.addAction(self.saveMD) + if self.mainConf.verQtValue >= 51400: + self.saveMD = QAction("Markdown (.md)") + self.saveMD.triggered.connect(lambda: self._saveDocument(self.FMT_MD)) + self.saveMenu.addAction(self.saveMD) self.saveTXT = QAction("Plain Text (.txt)") self.saveTXT.triggered.connect(lambda: self._saveDocument(self.FMT_TXT)) @@ -261,8 +262,8 @@ class GuiBuildNovel(QDialog): # Assemble GUI # ============ self.toolsBox.addWidget(self.titleGroup) - self.toolsBox.addWidget(self.textGroup) self.toolsBox.addWidget(self.buildGroup) + self.toolsBox.addWidget(self.textGroup) self.toolsBox.addWidget(self.includeGroup) self.toolsBox.addWidget(self.addsGroup) self.toolsBox.addStretch(1) From 7c1f3144bd3f55905e8d5647b9a838c152eb14bf Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 11 May 2020 19:00:01 +0200 Subject: [PATCH 20/24] Reworked the title formatting functionality. All keywords can be used in all headings, and also added scene numbers. --- nw/assets/text/exportHelp_en.htm | 31 ++++++----- nw/core/tokenizer.py | 93 ++++++++++++++++++-------------- nw/gui/build.py | 12 ++--- sample/sampleNovel/nwProject.nwx | 10 ++-- 4 files changed, 82 insertions(+), 64 deletions(-) diff --git a/nw/assets/text/exportHelp_en.htm b/nw/assets/text/exportHelp_en.htm index df8eca2a..e921f05d 100644 --- a/nw/assets/text/exportHelp_en.htm +++ b/nw/assets/text/exportHelp_en.htm @@ -1,25 +1,30 @@

Help!

-

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.

Novel Title Formats

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.

Build Overrides

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 @@ - + Sample Project Sample Project @@ -10,8 +10,8 @@ True True - 6a2d6d5f4f401 - b3e74dbc1f584 + 96b68994dfa3d + 6a2d6d5f4f401 875 B @@ -20,9 +20,9 @@ %title% - Chapter %num%.\\%title% + Chapter %chnum%.\\%title% %title% - * * * + Scene %chnum%.%scnum%: %title%

True False From 8dca20e3aa9b1127de268ec4c83a3da3cbd78658 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 12 May 2020 00:10:03 +0200 Subject: [PATCH 21/24] Added printing and save to PDF, and made some changes to the tohtml class --- nw/core/tohtml.py | 62 ++++++++------ nw/core/tokenizer.py | 195 ++++++++++++++++--------------------------- nw/gui/build.py | 99 +++++++++++++++------- nw/gui/mainmenu.py | 2 +- 4 files changed, 182 insertions(+), 176 deletions(-) diff --git a/nw/core/tohtml.py b/nw/core/tohtml.py index ec3cec22..20c32e58 100644 --- a/nw/core/tohtml.py +++ b/nw/core/tohtml.py @@ -30,15 +30,19 @@ import re import nw from nw.core.tokenizer import Tokenizer -from nw.constants import nwUnicode, nwLabels +from nw.constants import nwUnicode, nwLabels, nwKeyWords logger = logging.getLogger(__name__) class ToHtml(Tokenizer): + M_PREVIEW = 0 # Tweak output for the DocViewer + M_EXPORT = 1 # Tweak output for saving to HTML or printing + M_EBOOK = 2 # Tweak output for converting to epub + def __init__(self, theProject, theParent): Tokenizer.__init__(self, theProject, theParent) - self.forPreview = False + self.genMode = self.M_EXPORT return ## @@ -47,11 +51,11 @@ class ToHtml(Tokenizer): def setPreview(self, forPreview, doComments): """If we're using this class to generate markdown preview, we - need to make a few changes to formatting, which is selected by - this flag. + need to make a few changes to formatting, which is managed by + these flags. """ - self.forPreview = forPreview if forPreview: + self.genMode = self.M_PREVIEW self.doKeywords = True self.doComments = doComments return @@ -66,7 +70,7 @@ class ToHtml(Tokenizer): """ Tokenizer.doAutoReplace(self) - if self.forPreview: + if self.genMode == self.M_PREVIEW: tabFmt = " "*8 else: tabFmt = " " @@ -102,6 +106,7 @@ class ToHtml(Tokenizer): self.theResult = "" thisPar = [] + parStyle = "" for tType, tText, tFormat, tStyle in self.theTokens: # Styles @@ -141,8 +146,9 @@ class ToHtml(Tokenizer): if tType == self.T_EMPTY: if len(thisPar) > 0: tTemp = "".join(thisPar) - self.theResult += "%s

\n" % (hStyle,tTemp.rstrip()) + self.theResult += "%s

\n" % (parStyle,tTemp.rstrip()) thisPar = [] + parStyle = "" elif tType == self.T_HEAD1: tHead = tText.replace(r"\\", "
") @@ -164,13 +170,11 @@ class ToHtml(Tokenizer): self.theResult += "%s

\n" % (hStyle, tText) elif tType == self.T_SKIP: - self.theResult += "

 

\n" - - elif tType == self.T_PBREAK: - self.theResult += "

 

\n" + self.theResult += " 

\n" % hStyle elif tType == self.T_TEXT: tTemp = tText + parStyle = hStyle for xPos, xLen, xFmt in reversed(tFormat): tTemp = tTemp[:xPos]+htmlTags[xFmt]+tTemp[xPos+xLen:] if tText.endswith(" "): @@ -196,20 +200,18 @@ class ToHtml(Tokenizer): def _formatSynopsis(self, tText): """Apply HTML formatting to synopsis. """ - - if not self.forPreview: + if self.genMode == self.M_EXPORT: return "

Synopsis: %s

\n" % tText - - return "

%s

\n" % tText + else: + return "

%s

\n" % tText def _formatComments(self, tText): """Apply HTML formatting to comments. """ - - if not self.forPreview: + if self.genMode == self.M_EXPORT: return "

Comment: %s

\n" % tText - - return "

%s

\n" % tText + else: + return "

%s

\n" % tText def _formatKeywords(self, tText): """Apply HTML formatting to keywords. @@ -224,11 +226,23 @@ class ToHtml(Tokenizer): refTags = [] if theBits[0] in nwLabels.KEY_NAME: retText += "%s: " % nwLabels.KEY_NAME[theBits[0]] - for tTag in theBits[1:]: - refTags.append("%s" % ( - theBits[0][1:], tTag, tTag - )) - retText += ", ".join(refTags) + if self.genMode == self.M_PREVIEW: + for tTag in theBits[1:]: + refTags.append("%s" % ( + theBits[0][1:], tTag, tTag + )) + retText += ", ".join(refTags) + else: + if theBits[0] == nwKeyWords.TAG_KEY: + retText += "%s" % ( + theBits[1], theBits[1] + ) + else: + for tTag in theBits[1:]: + refTags.append("%s" % ( + tTag, tTag + )) + retText += ", ".join(refTags) return "
%s
" % retText diff --git a/nw/core/tokenizer.py b/nw/core/tokenizer.py index f9d64d45..20d89623 100644 --- a/nw/core/tokenizer.py +++ b/nw/core/tokenizer.py @@ -58,7 +58,6 @@ class Tokenizer(): 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 @@ -108,6 +107,18 @@ class Tokenizer(): self.numAbsScene = 0 # Counter for scene number within novel self.firstScene = False # Flag to indicate that the first scene of the chapter + # This File + self.isNone = False + self.isTitle = False + self.isBook = False + self.isPage = False + self.isPart = False + self.isUnNum = False + self.isChap = False + self.isScene = False + self.isNote = False + self.isNovel = False + return def clearData(self): @@ -121,6 +132,18 @@ class Tokenizer(): self.theResult = None self.numChapter = 0 self.firstScene = False + + self.isNone = False + self.isTitle = False + self.isBook = False + self.isPage = False + self.isPart = False + self.isUnNum = False + self.isChap = False + self.isScene = False + self.isNote = False + self.isNovel = False + return ## @@ -189,6 +212,17 @@ class Tokenizer(): theDocument = NWDoc(self.theProject, self.theParent) self.theText = theDocument.openDocument(theHandle) + self.isNone = self.theItem.itemLayout == nwItemLayout.NO_LAYOUT + self.isTitle = self.theItem.itemLayout == nwItemLayout.TITLE + self.isBook = self.theItem.itemLayout == nwItemLayout.BOOK + self.isPage = self.theItem.itemLayout == nwItemLayout.PAGE + self.isPart = self.theItem.itemLayout == nwItemLayout.PARTITION + self.isUnNum = self.theItem.itemLayout == nwItemLayout.UNNUMBERED + self.isChap = self.theItem.itemLayout == nwItemLayout.CHAPTER + self.isScene = self.theItem.itemLayout == nwItemLayout.SCENE + self.isNote = self.theItem.itemLayout == nwItemLayout.NOTE + self.isNovel = self.isBook or self.isUnNum or self.isChap or self.isScene + return def getResult(self): @@ -251,61 +285,37 @@ class Tokenizer(): # Tag lines starting with specific characters if len(aLine.strip()) == 0: self.theTokens.append(( - self.T_EMPTY, - "", - None, - None + self.T_EMPTY, "", None, None )) elif aLine[0] == "%": cLine = aLine[1:].strip() if cLine.lower().startswith("synopsis:"): self.theTokens.append(( - self.T_SYNOPSIS, - cLine[9:].strip(), - None, - defAlign + self.T_SYNOPSIS, cLine[9:].strip(), None, defAlign )) else: self.theTokens.append(( - self.T_COMMENT, - aLine[1:].strip(), - None, - defAlign + 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.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.A_PBB + self.T_HEAD1, aLine[2:].strip(), None, self.A_LEFT | self.A_PBB )) elif aLine[:3] == "## ": self.theTokens.append(( - self.T_HEAD2, - aLine[3:].strip(), - None, - self.A_LEFT | self.A_PBA_AV + self.T_HEAD2, aLine[3:].strip(), None, self.A_LEFT | self.A_PBA_AV )) elif aLine[:4] == "### ": self.theTokens.append(( - self.T_HEAD3, - aLine[4:].strip(), - None, - self.A_LEFT | self.A_PBA_AV + self.T_HEAD3, aLine[4:].strip(), None, self.A_LEFT | self.A_PBA_AV )) elif aLine[:5] == "#### ": self.theTokens.append(( - self.T_HEAD4, - aLine[5:].strip(), - None, - self.A_LEFT | self.A_PBA_AV + self.T_HEAD4, aLine[5:].strip(), None, self.A_LEFT | self.A_PBA_AV )) else: if not self.doBodyText: @@ -328,18 +338,12 @@ class Tokenizer(): # sorted by position fmtPos = sorted(fmtPos, key=itemgetter(0)) self.theTokens.append(( - self.T_TEXT, - aLine, - fmtPos, - defAlign + self.T_TEXT, aLine, fmtPos, defAlign )) # Always add an empty line at the end self.theTokens.append(( - self.T_EMPTY, - "", - None, - None + self.T_EMPTY, "", None, None )) return @@ -349,23 +353,13 @@ class Tokenizer(): layout and user settings. """ - isNone = self.theItem.itemLayout == nwItemLayout.NO_LAYOUT - isTitle = self.theItem.itemLayout == nwItemLayout.TITLE - isBook = self.theItem.itemLayout == nwItemLayout.BOOK - isPage = self.theItem.itemLayout == nwItemLayout.PAGE - isPart = self.theItem.itemLayout == nwItemLayout.PARTITION - isUnNum = self.theItem.itemLayout == nwItemLayout.UNNUMBERED - isChap = self.theItem.itemLayout == nwItemLayout.CHAPTER - isScene = self.theItem.itemLayout == nwItemLayout.SCENE - isNote = self.theItem.itemLayout == nwItemLayout.NOTE - # No special header formatting for notes and no-layout files - if isNone or isNote: + if self.isNone or self.isNote: return # For novel files, we need to handle chapter numbering and scene # breaks - if isBook or isUnNum or isChap or isScene: + if self.isNovel: for n in range(len(self.theTokens)): tToken = self.theTokens[n] @@ -382,10 +376,7 @@ class Tokenizer(): tText = self._formatHeading(self.fmtTitle, tText) self.theTokens[n] = ( - tType, - tText, - None, - self.A_LEFT | self.A_PBB_R + tType, tText, None, self.A_LEFT | self.A_PBB_R ) elif tType == self.T_HEAD2: @@ -393,7 +384,7 @@ class Tokenizer(): # ============= # Numbered or Unnumbered - if isUnNum: + if self.isUnNum: tText = self._formatHeading(self.fmtUnNum, tText) else: self.numChapter += 1 @@ -401,10 +392,7 @@ class Tokenizer(): # Format the chapter header self.theTokens[n] = ( - tType, - tText, - None, - self.A_LEFT | self.A_PBB_R + tType, tText, None, self.A_LEFT | self.A_PBB_R ) # Set scene variables @@ -421,47 +409,29 @@ class Tokenizer(): tTemp = self._formatHeading(self.fmtScene, tText) if tTemp == "" and self.hideScene: self.theTokens[n] = ( - self.T_EMPTY, - "", - None, - None + self.T_EMPTY, "", None, None ) elif tTemp == "" and not self.hideScene: if self.firstScene: self.theTokens[n] = ( - self.T_EMPTY, - "", - None, - None + self.T_EMPTY, "", None, None ) else: self.theTokens[n] = ( - self.T_SKIP, - "", - None, - None + self.T_SKIP, "", None, None ) elif tTemp == self.fmtScene: if self.firstScene: self.theTokens[n] = ( - self.T_EMPTY, - "", - None, - None + self.T_EMPTY, "", None, None ) else: self.theTokens[n] = ( - self.T_SEP, - tTemp, - None, - self.A_CENTRE + self.T_SEP, tTemp, None, self.A_CENTRE ) else: self.theTokens[n] = ( - tType, - tTemp, - None, - self.A_LEFT | self.A_PBA_AV + tType, tTemp, None, self.A_LEFT | self.A_PBA_AV ) # Definitely no longer the first scene @@ -474,62 +444,41 @@ class Tokenizer(): tTemp = self._formatHeading(self.fmtSection, tText) if tTemp == "" and self.hideSection: self.theTokens[n] = ( - self.T_EMPTY, - "", - None, - None + self.T_EMPTY, "", None, None ) elif tTemp == "" and not self.hideSection: self.theTokens[n] = ( - self.T_SKIP, - "", - None, - None + self.T_SKIP, "", None, None ) elif tTemp == self.fmtSection: self.theTokens[n] = ( - self.T_SEP, - tTemp, - None, - self.A_CENTRE + self.T_SEP, tTemp, None, self.A_CENTRE ) else: self.theTokens[n] = ( - tType, - tTemp, - None, - self.A_LEFT | self.A_PBA_AV + tType, tTemp, None, self.A_LEFT | self.A_PBA_AV ) # For title page and partitions, we need to centre all text. # For partition, we also add a page break before, and for # both types we always add a page break after the content. - if isTitle or isPart: - for n in range(len(self.theTokens)): - tToken = self.theTokens[n] + if self.isTitle or self.isPart: + for n, tToken in enumerate(self.theTokens): tType = tToken[0] tText = tToken[1] tFormat = tToken[2] - if isTitle: + if self.isTitle: self.theTokens[n] = ( - tType, - tText, - tFormat, - self.A_CENTRE + tType, tText, tFormat, self.A_CENTRE ) - else: - self.theTokens[n] = ( - tType, - tText, - tFormat, - self.A_CENTRE | self.A_PBB_R - ) - self.theTokens.append(( - self.T_PBREAK, - "", - None, - None - )) + + # Add a page break after the last entry + n = len(self.theTokens) - 1 + if n >= 0: + tToken = self.theTokens[n] + self.theTokens[n] = ( + tToken[0], tToken[1], tToken[2], tToken[3] | self.A_PBA + ) return diff --git a/nw/gui/build.py b/nw/gui/build.py index bac21b87..43457d2c 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -31,6 +31,7 @@ import nw from os import path from PyQt5.QtCore import Qt, QByteArray +from PyQt5.QtPrintSupport import QPrinter, QPrintPreviewDialog from PyQt5.QtGui import ( QTextOption, QPalette, QColor, QTextDocumentWriter ) @@ -230,9 +231,9 @@ class GuiBuildNovel(QDialog): self.saveODT.triggered.connect(lambda: self._saveDocument(self.FMT_ODT)) self.saveMenu.addAction(self.saveODT) - # self.savePDF = QAction("Portable Document Format (.pdf)") - # self.savePDF.triggered.connect(lambda: self._saveDocument(self.FMT_PDF)) - # self.saveMenu.addAction(self.savePDF) + self.savePDF = QAction("Portable Document Format (.pdf)") + self.savePDF.triggered.connect(lambda: self._saveDocument(self.FMT_PDF)) + self.saveMenu.addAction(self.savePDF) self.saveHTM1 = QAction("Qt Style HTML (.htm)") self.saveHTM1.triggered.connect(lambda: self._saveDocument(self.FMT_HTM1)) @@ -406,10 +407,15 @@ class GuiBuildNovel(QDialog): # Create the settings if theFormat == self.FMT_ODT: byteFmt.append("odf") - fileExt = "odf" + fileExt = "odt" textFmt = "Open Document" outTool = "Qt" + elif theFormat == self.FMT_PDF: + fileExt = "pdf" + textFmt = "PDF" + outTool = "QtPrint" + elif theFormat == self.FMT_HTM1: byteFmt.append("html") fileExt = "htm" @@ -479,32 +485,56 @@ class GuiBuildNovel(QDialog): ), nwAlert.ERROR ) - elif outTool == "NW": - if theFormat == self.FMT_HTM2: - try: - with open(savePath, mode="w", encoding="utf8") as outFile: - outFile.write("\n") - outFile.write("\n") - outFile.write("\n") - outFile.write("\n") - outFile.write("\n") - outFile.write("\n") - outFile.write(self.htmlText) - outFile.write("\n") - outFile.write("\n") + elif outTool == "NW" and theFormat == self.FMT_HTM2: + try: + with open(savePath, mode="w", encoding="utf8") as outFile: + outFile.write("\n") + outFile.write("\n") + outFile.write("\n") + outFile.write("\n") + outFile.write("\n") + outFile.write("\n") + outFile.write("
\n") + outFile.write(self.htmlText) + outFile.write("
\n") + outFile.write("\n") + outFile.write("\n") - self.theParent.makeAlert( - "Document successfully written in %s format to file: %s" % ( - textFmt, savePath - ), nwAlert.INFO - ) + self.theParent.makeAlert( + "Document successfully written in %s format to file: %s" % ( + textFmt, savePath + ), nwAlert.INFO + ) - except Exception as e: - self.theParent.makeAlert( - "Failed to write document in %s format to file: %s" % ( - textFmt, str(e) - ), nwAlert.ERROR - ) + except Exception as e: + self.theParent.makeAlert( + "Failed to write document in %s format to file: %s" % ( + textFmt, str(e) + ), nwAlert.ERROR + ) + + elif outTool == "QtPrint" and theFormat == self.FMT_PDF: + try: + thePrinter = QPrinter() + thePrinter.setOutputFormat(QPrinter.PdfFormat) + thePrinter.setOrientation(QPrinter.Portrait) + thePrinter.setDuplex(QPrinter.DuplexLongSide) + thePrinter.setFontEmbeddingEnabled(True) + thePrinter.setColorMode(QPrinter.Color) + thePrinter.setOutputFileName(savePath) + self.docView.qDocument.print(thePrinter) + self.theParent.makeAlert( + "Document successfully written in %s format to file: %s" % ( + textFmt, savePath + ), nwAlert.INFO + ) + + except Exception as e: + self.theParent.makeAlert( + "Failed to write document in %s format to file: %s" % ( + textFmt, str(e) + ), nwAlert.ERROR + ) else: return False @@ -512,6 +542,19 @@ class GuiBuildNovel(QDialog): return True def _printDocument(self): + """Open the print preview dialog. + """ + thePreview = QPrintPreviewDialog(self) + thePreview.paintRequested.connect(self._doPrintPreview) + thePreview.exec_() + return + + def _doPrintPreview(self, thePrinter): + """Connect the print preview painter to the document viewer. + """ + thePrinter.setOrientation(QPrinter.Portrait) + thePrinter.setOutputFormat(QPrinter.NativeFormat | QPrinter.PdfFormat) + self.docView.qDocument.print(thePrinter) return def _toggelOutlineMode(self, theState): diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py index ddc60daa..56ec0337 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -352,7 +352,7 @@ class GuiMainMenu(QMenuBar): self.docuMenu.addAction(self.aCloseView) # Document > Toggle View Comments - self.aViewDocComments = QAction("View Comments", self) + self.aViewDocComments = QAction("Show Comments", self) self.aViewDocComments.setStatusTip("Show comments in view panel") self.aViewDocComments.setCheckable(True) self.aViewDocComments.setChecked(self.mainConf.viewComments) From 2a67b69d3256a3fd01ad62095d1a865cde8fcea0 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 12 May 2020 18:10:45 +0200 Subject: [PATCH 22/24] Replaced 'Outline Mode' with an 'Exclude body text' option instead. --- nw/gui/build.py | 55 +++++++------------------------------------------ 1 file changed, 7 insertions(+), 48 deletions(-) diff --git a/nw/gui/build.py b/nw/gui/build.py index 43457d2c..d007ae70 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -130,21 +130,6 @@ class GuiBuildNovel(QDialog): 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) - # Text Options # ============= self.textGroup = QGroupBox("Text Options", self) @@ -195,6 +180,8 @@ class GuiBuildNovel(QDialog): self.noteFiles.setChecked(self.optState.getBool("GuiBuildNovel", "addNotes", False)) self.ignoreFlag = QSwitch() self.ignoreFlag.setChecked(self.optState.getBool("GuiBuildNovel", "ignoreFlag", False)) + self.excludeBody = QSwitch() + self.excludeBody.setChecked(self.optState.getBool("GuiBuildNovel", "excludeBody", False)) self.addsForm.addWidget(QLabel("Include novel files"), 0, 0) self.addsForm.addWidget(self.novelFiles, 0, 1) @@ -202,6 +189,8 @@ class GuiBuildNovel(QDialog): 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.addWidget(QLabel("Exclude body text"), 3, 0) + self.addsForm.addWidget(self.excludeBody, 3, 1) self.addsForm.setColumnStretch(0, 1) self.addsForm.setColumnStretch(1, 0) @@ -263,7 +252,6 @@ class GuiBuildNovel(QDialog): # Assemble GUI # ============ self.toolsBox.addWidget(self.titleGroup) - self.toolsBox.addWidget(self.buildGroup) self.toolsBox.addWidget(self.textGroup) self.toolsBox.addWidget(self.includeGroup) self.toolsBox.addWidget(self.addsGroup) @@ -282,9 +270,6 @@ class GuiBuildNovel(QDialog): 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") @@ -306,25 +291,13 @@ class GuiBuildNovel(QDialog): 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 - - if outlineMode: - 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 - noteFiles = False + excludeBody = self.excludeBody.isChecked() makeHtml = ToHtml(self.theProject, self.theParent) makeHtml.setTitleFormat(fmtTitle) @@ -332,7 +305,7 @@ class GuiBuildNovel(QDialog): makeHtml.setUnNumberedFormat(fmtUnnumbered) makeHtml.setSceneFormat(fmtScene, fmtScene == "") makeHtml.setSectionFormat(fmtSection, fmtSection == "") - makeHtml.setBodyText(doBodyText) + makeHtml.setBodyText(not excludeBody) makeHtml.setSynopsis(incSynopsis) makeHtml.setComments(incComments) makeHtml.setKeywords(incKeywords) @@ -557,20 +530,6 @@ class GuiBuildNovel(QDialog): self.docView.qDocument.print(thePrinter) 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. """ @@ -613,10 +572,10 @@ class GuiBuildNovel(QDialog): 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()) self.optState.setValue("GuiBuildNovel", "ignoreFlag", self.ignoreFlag.isChecked()) + self.optState.setValue("GuiBuildNovel", "excludeBody", self.excludeBody.isChecked()) self.optState.saveSettings() return From aa4778d4cd39985237a1c3bc4f4b8897b04f9343 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 12 May 2020 18:41:08 +0200 Subject: [PATCH 23/24] Preserve markdown during build, and use lists instead of strings --- nw/core/tohtml.py | 25 ++++++++++++++---------- nw/core/tokenizer.py | 46 +++++++++++++++++++++++++++++++++++++------- nw/gui/build.py | 17 ++++++++++++++-- 3 files changed, 69 insertions(+), 19 deletions(-) diff --git a/nw/core/tohtml.py b/nw/core/tohtml.py index 20c32e58..7980d80b 100644 --- a/nw/core/tohtml.py +++ b/nw/core/tohtml.py @@ -105,8 +105,10 @@ class ToHtml(Tokenizer): } self.theResult = "" + thisPar = [] parStyle = "" + tmpResult = [] for tType, tText, tFormat, tStyle in self.theTokens: # Styles @@ -146,31 +148,31 @@ class ToHtml(Tokenizer): if tType == self.T_EMPTY: if len(thisPar) > 0: tTemp = "".join(thisPar) - self.theResult += "%s

\n" % (parStyle,tTemp.rstrip()) + tmpResult.append("%s

\n" % (parStyle, tTemp.rstrip())) thisPar = [] parStyle = "" elif tType == self.T_HEAD1: tHead = tText.replace(r"\\", "
") - 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 += "%s

\n" % (hStyle, tText) + tmpResult.append("%s

\n" % (hStyle, tText)) elif tType == self.T_SKIP: - self.theResult += " 

\n" % hStyle + tmpResult.append(" 

\n" % hStyle) elif tType == self.T_TEXT: tTemp = tText @@ -183,13 +185,16 @@ class ToHtml(Tokenizer): thisPar.append(tTemp.rstrip()+" ") elif tType == self.T_SYNOPSIS and self.doSynopsis: - self.theResult += self._formatSynopsis(tText) + tmpResult.append(self._formatSynopsis(tText)) elif tType == self.T_COMMENT and self.doComments: - self.theResult += self._formatComments(tText) + tmpResult.append(self._formatComments(tText)) elif tType == self.T_KEYWORD and self.doKeywords: - self.theResult += self._formatKeywords(tText) + tmpResult.append(self._formatKeywords(tText)) + + self.theResult = "".join(tmpResult) + tmpResult = [] return diff --git a/nw/core/tokenizer.py b/nw/core/tokenizer.py index 20d89623..025a9cfb 100644 --- a/nw/core/tokenizer.py +++ b/nw/core/tokenizer.py @@ -84,6 +84,7 @@ class Tokenizer(): self.theItem = None # The NWItem associated with the handle self.theTokens = None # The list of the processed tokens self.theResult = None # The result text after conversion + self.theMarkdown = None # The result text in novelWriter markdown # User Settings self.doBodyText = True # Include body text @@ -125,13 +126,14 @@ class Tokenizer(): """Clear the data arrays and variables, but not settings, so the class can be reused for multiple documents. """ - self.theText = None - self.theHandle = None - self.theItem = None - self.theTokens = None - self.theResult = None - self.numChapter = 0 - self.firstScene = False + self.theText = None + self.theHandle = None + self.theItem = None + self.theTokens = None + self.theResult = None + self.theMarkdown = None + self.numChapter = 0 + self.firstScene = False self.isNone = False self.isTitle = False @@ -230,6 +232,11 @@ class Tokenizer(): """ return self.theResult + def getFilteredMarkdown(self): + """Return the novelWriter markdown after the filters have been applied. + """ + return self.theMarkdown + def doAutoReplace(self): """Run through the user's auto-replace dictionary. """ @@ -280,6 +287,8 @@ class Tokenizer(): defAlign = self.A_LEFT self.theTokens = [] + self.theMarkdown = "" + tmpMarkdown = [] for aLine in self.theText.splitlines(): # Tag lines starting with specific characters @@ -287,36 +296,54 @@ class Tokenizer(): self.theTokens.append(( self.T_EMPTY, "", None, None )) + tmpMarkdown.append("\n") + elif aLine[0] == "%": cLine = aLine[1:].strip() if cLine.lower().startswith("synopsis:"): self.theTokens.append(( self.T_SYNOPSIS, cLine[9:].strip(), None, defAlign )) + if self.doSynopsis: + tmpMarkdown.append("%s\n" % aLine) else: self.theTokens.append(( self.T_COMMENT, aLine[1:].strip(), None, defAlign )) + if self.doComments: + tmpMarkdown.append("%s\n" % aLine) + elif aLine[0] == "@": self.theTokens.append(( self.T_KEYWORD, aLine[1:].strip(), None, self.A_LEFT )) + if self.doKeywords: + tmpMarkdown.append("%s\n" % aLine) + elif aLine[:2] == "# ": self.theTokens.append(( self.T_HEAD1, aLine[2:].strip(), None, self.A_LEFT | self.A_PBB )) + tmpMarkdown.append("%s\n" % aLine) + elif aLine[:3] == "## ": self.theTokens.append(( self.T_HEAD2, aLine[3:].strip(), None, self.A_LEFT | self.A_PBA_AV )) + tmpMarkdown.append("%s\n" % aLine) + elif aLine[:4] == "### ": self.theTokens.append(( self.T_HEAD3, aLine[4:].strip(), None, self.A_LEFT | self.A_PBA_AV )) + tmpMarkdown.append("%s\n" % aLine) + elif aLine[:5] == "#### ": self.theTokens.append(( self.T_HEAD4, aLine[5:].strip(), None, self.A_LEFT | self.A_PBA_AV )) + tmpMarkdown.append("%s\n" % aLine) + else: if not self.doBodyText: # Skip all body text @@ -340,11 +367,16 @@ class Tokenizer(): self.theTokens.append(( self.T_TEXT, aLine, fmtPos, defAlign )) + tmpMarkdown.append("%s\n" % aLine) # Always add an empty line at the end self.theTokens.append(( self.T_EMPTY, "", None, None )) + tmpMarkdown.append("\n") + + self.theMarkdown = "".join(tmpMarkdown) + tmpMarkdown = [] return diff --git a/nw/gui/build.py b/nw/gui/build.py index d007ae70..d95be6de 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -29,6 +29,7 @@ import logging import nw from os import path +from time import time from PyQt5.QtCore import Qt, QByteArray from PyQt5.QtPrintSupport import QPrinter, QPrintPreviewDialog @@ -69,6 +70,7 @@ class GuiBuildNovel(QDialog): self.optState = self.theProject.optState self.htmlText = "" + self.nwdText = "" self.setWindowTitle("Build Novel Project") self.setMinimumWidth(800) @@ -311,9 +313,13 @@ class GuiBuildNovel(QDialog): makeHtml.setKeywords(incKeywords) makeHtml.setJustify(justifyText) - self.htmlText = "" self.buildProgress.setMaximum(len(self.theProject.projTree)) self.buildProgress.setValue(0) + + tStart = time() + + tmpHtml = [] + tmpNwd = [] for nItt, tItem in enumerate(self.theProject.projTree): if self._checkInclude(tItem, noteFiles, novelFiles, ignoreFlag): makeHtml.setText(tItem.itemHandle) @@ -322,9 +328,16 @@ class GuiBuildNovel(QDialog): makeHtml.doHeaders() makeHtml.doConvert() makeHtml.doPostProcessing() - self.htmlText += makeHtml.getResult() + tmpHtml.append(makeHtml.getResult()) + tmpNwd.append(makeHtml.getFilteredMarkdown()) self.buildProgress.setValue(nItt+1) + self.htmlText = "".join(tmpHtml) + self.nwdText = "".join(tmpNwd) + + tEnd = time() + logger.debug("Built project in %.3f ms" % (1000*(tEnd-tStart))) + self.docView.setHtml(self.htmlText) return From aa342a7f19d878e49ffd9841fc009c8d5922d101 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 12 May 2020 19:44:30 +0200 Subject: [PATCH 24/24] Dropped Qt html export, and added nw markdown export --- nw/core/tohtml.py | 44 ++++++++++++-------- nw/core/tokenizer.py | 2 + nw/gui/build.py | 95 ++++++++++++++++++++++++-------------------- 3 files changed, 83 insertions(+), 58 deletions(-) diff --git a/nw/core/tohtml.py b/nw/core/tohtml.py index 7980d80b..f10ee1e3 100644 --- a/nw/core/tohtml.py +++ b/nw/core/tohtml.py @@ -43,6 +43,18 @@ class ToHtml(Tokenizer): def __init__(self, theProject, theParent): Tokenizer.__init__(self, theProject, theParent) self.genMode = self.M_EXPORT + + self.repDict = { + "<" : "<", + ">" : ">", + "&" : "&", + "\t" : " ", + nwUnicode.U_ENDASH : nwUnicode.H_ENDASH, + nwUnicode.U_EMDASH : nwUnicode.H_EMDASH, + nwUnicode.U_HELLIP : nwUnicode.H_HELLIP, + nwUnicode.U_NBSP : nwUnicode.H_NBSP, + } + return ## @@ -58,6 +70,7 @@ class ToHtml(Tokenizer): self.genMode = self.M_PREVIEW self.doKeywords = True self.doComments = doComments + self.repDict["\t"] = " "*8 return ## @@ -70,23 +83,22 @@ class ToHtml(Tokenizer): """ Tokenizer.doAutoReplace(self) - if self.genMode == self.M_PREVIEW: - tabFmt = " "*8 - else: - tabFmt = " " + xRep = re.compile("|".join([re.escape(k) for k in self.repDict.keys()]), flags=re.DOTALL) + self.theText = xRep.sub(lambda x: self.repDict[x.group(0)], self.theText) - repDict = { - "<" : "<", - ">" : ">", - "&" : "&", - "\t" : tabFmt, - nwUnicode.U_ENDASH : nwUnicode.H_ENDASH, - nwUnicode.U_EMDASH : nwUnicode.H_EMDASH, - nwUnicode.U_HELLIP : nwUnicode.H_HELLIP, - nwUnicode.U_NBSP : nwUnicode.H_NBSP, - } - xRep = re.compile("|".join([re.escape(k) for k in repDict.keys()]), flags=re.DOTALL) - self.theText = xRep.sub(lambda x: repDict[x.group(0)], self.theText) + return + + def doPostProcessing(self): + """Reverse the html entities replacement on the markdown text. + Otherwise, all the &something; bits will also be in there. + """ + if self.genMode == self.M_PREVIEW: + # Doesn't matter for preview as we don't use the markdown + return + + revDict = dict(map(reversed, self.repDict.items())) + xRep = re.compile("|".join([re.escape(k) for k in revDict.keys()]), flags=re.DOTALL) + self.theMarkdown = xRep.sub(lambda x: revDict[x.group(0)], self.theMarkdown) return diff --git a/nw/core/tokenizer.py b/nw/core/tokenizer.py index 025a9cfb..80cc216d 100644 --- a/nw/core/tokenizer.py +++ b/nw/core/tokenizer.py @@ -251,6 +251,8 @@ class Tokenizer(): return def doPostProcessing(self): + """Do some postprocessing. Overloaded by subclasses. + """ return def tokenizeText(self): diff --git a/nw/gui/build.py b/nw/gui/build.py index d95be6de..737a13c4 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -51,12 +51,12 @@ logger = logging.getLogger(__name__) class GuiBuildNovel(QDialog): - FMT_ODT = 1 - FMT_PDF = 2 - FMT_HTM1 = 3 - FMT_HTM2 = 4 - FMT_MD = 4 - FMT_TXT = 5 + FMT_ODT = 1 + FMT_PDF = 2 + FMT_HTM = 3 + FMT_MD = 4 + FMT_NWD = 5 + FMT_TXT = 6 def __init__(self, theParent, theProject): QDialog.__init__(self, theParent) @@ -69,8 +69,9 @@ class GuiBuildNovel(QDialog): self.theTheme = theParent.theTheme self.optState = self.theProject.optState - self.htmlText = "" - self.nwdText = "" + self.htmlText = [] # List of html document + self.nwdText = [] # List of markdown documents + self.textLayout = [] # List of nwItemLayout entries self.setWindowTitle("Build Novel Project") self.setMinimumWidth(800) @@ -226,19 +227,19 @@ class GuiBuildNovel(QDialog): self.savePDF.triggered.connect(lambda: self._saveDocument(self.FMT_PDF)) self.saveMenu.addAction(self.savePDF) - self.saveHTM1 = QAction("Qt Style HTML (.htm)") - self.saveHTM1.triggered.connect(lambda: self._saveDocument(self.FMT_HTM1)) - self.saveMenu.addAction(self.saveHTM1) - - self.saveHTM2 = QAction("%s HTML (.htm)" % nw.__package__) - self.saveHTM2.triggered.connect(lambda: self._saveDocument(self.FMT_HTM2)) - self.saveMenu.addAction(self.saveHTM2) + self.saveHTM = QAction("%s HTML (.htm)" % nw.__package__) + self.saveHTM.triggered.connect(lambda: self._saveDocument(self.FMT_HTM)) + self.saveMenu.addAction(self.saveHTM) if self.mainConf.verQtValue >= 51400: self.saveMD = QAction("Markdown (.md)") self.saveMD.triggered.connect(lambda: self._saveDocument(self.FMT_MD)) self.saveMenu.addAction(self.saveMD) + self.saveNWD = QAction("novelWriter Markdown (.nwd)") + self.saveNWD.triggered.connect(lambda: self._saveDocument(self.FMT_NWD)) + self.saveMenu.addAction(self.saveNWD) + self.saveTXT = QAction("Plain Text (.txt)") self.saveTXT.triggered.connect(lambda: self._saveDocument(self.FMT_TXT)) self.saveMenu.addAction(self.saveTXT) @@ -318,8 +319,10 @@ class GuiBuildNovel(QDialog): tStart = time() - tmpHtml = [] - tmpNwd = [] + self.htmlText = [] + self.nwdText = [] + self.textLayout = [] + for nItt, tItem in enumerate(self.theProject.projTree): if self._checkInclude(tItem, noteFiles, novelFiles, ignoreFlag): makeHtml.setText(tItem.itemHandle) @@ -328,17 +331,18 @@ class GuiBuildNovel(QDialog): makeHtml.doHeaders() makeHtml.doConvert() makeHtml.doPostProcessing() - tmpHtml.append(makeHtml.getResult()) - tmpNwd.append(makeHtml.getFilteredMarkdown()) - self.buildProgress.setValue(nItt+1) + self.htmlText.append(makeHtml.getResult()) + self.nwdText.append(makeHtml.getFilteredMarkdown()) + self.textLayout.append(tItem.itemLayout) - self.htmlText = "".join(tmpHtml) - self.nwdText = "".join(tmpNwd) + # Update progress bar, also for skipped items + self.buildProgress.setValue(nItt+1) tEnd = time() logger.debug("Built project in %.3f ms" % (1000*(tEnd-tStart))) - self.docView.setHtml(self.htmlText) + # Load the preview document with the html data + self.docView.setHtml("".join(self.htmlText)) return @@ -402,13 +406,7 @@ class GuiBuildNovel(QDialog): textFmt = "PDF" outTool = "QtPrint" - elif theFormat == self.FMT_HTM1: - byteFmt.append("html") - fileExt = "htm" - textFmt = "Qt Style HTML" - outTool = "Qt" - - elif theFormat == self.FMT_HTM2: + elif theFormat == self.FMT_HTM: fileExt = "htm" textFmt = "Plain HTML" outTool = "NW" @@ -419,6 +417,11 @@ class GuiBuildNovel(QDialog): textFmt = "Markdown" outTool = "Qt" + elif theFormat == self.FMT_NWD: + fileExt = "nwd" + textFmt = "%s markdown" % nw.__package__ + outTool = "NW" + elif theFormat == self.FMT_TXT: byteFmt.append("plaintext") fileExt = "txt" @@ -471,20 +474,28 @@ class GuiBuildNovel(QDialog): ), nwAlert.ERROR ) - elif outTool == "NW" and theFormat == self.FMT_HTM2: + elif outTool == "NW": try: with open(savePath, mode="w", encoding="utf8") as outFile: - outFile.write("\n") - outFile.write("\n") - outFile.write("\n") - outFile.write("\n") - outFile.write("\n") - outFile.write("\n") - outFile.write("
\n") - outFile.write(self.htmlText) - outFile.write("
\n") - outFile.write("\n") - outFile.write("\n") + if theFormat == self.FMT_HTM: + # Write novelWriter HTML data + outFile.write("\n") + outFile.write("\n") + outFile.write("\n") + outFile.write("\n") + outFile.write("\n") + outFile.write("\n") + outFile.write("
\n") + for aLine in self.htmlText: + outFile.write(aLine) + outFile.write("
\n") + outFile.write("\n") + outFile.write("\n") + + elif theFormat == self.FMT_NWD: + # Write novelWriter markdown data + for aLine in self.nwdText: + outFile.write(aLine) self.theParent.makeAlert( "Document successfully written in %s format to file: %s" % (