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] 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 "\n" % tText
+ else:
+ return "\n" % tText
def _formatComments(self, tText):
"""Apply HTML formatting to comments.
"""
-
- if not self.forPreview:
+ if self.genMode == self.M_EXPORT:
return "\n" % tText
-
- return "\n" % tText
+ else:
+ return "\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)