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 "

%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)