diff --git a/nw/assets/text/exportHelp_en.htm b/nw/assets/text/exportHelp_en.htm new file mode 100644 index 00000000..e921f05d --- /dev/null +++ b/nw/assets/text/exportHelp_en.htm @@ -0,0 +1,49 @@ +

Help!

+

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

+

%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 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 + 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/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/config.py b/nw/config.py index 4fb8278e..c0c34215 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/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/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/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 ceab8d55..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.doHeaders() - 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/tohtml.py b/nw/convert/text/tohtml.py deleted file mode 100644 index 330e7bc8..00000000 --- a/nw/convert/text/tohtml.py +++ /dev/null @@ -1,176 +0,0 @@ -# -*- coding: utf-8 -*- -"""novelWriter HTML Text Converter - - novelWriter – HTML Text Converter -=================================== - Extends the Tokenizer class to write HTML - - File History: - Created: 2019-05-07 [0.0.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 re -import nw - -from nw.convert.tokenizer import Tokenizer -from nw.constants import nwUnicode, nwLabels - -logger = logging.getLogger(__name__) - -class ToHtml(Tokenizer): - - def __init__(self, theProject, theParent): - Tokenizer.__init__(self, theProject, theParent) - self.forPreview = False - return - - 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. - """ - self.forPreview = forPreview - if forPreview: - self.doKeywords = True - self.doComments = doComments - return - - def doAutoReplace(self): - Tokenizer.doAutoReplace(self) - - if self.forPreview: - tabFmt = " "*8 - else: - tabFmt = " " - - 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 doConvert(self): - - htmlTags = { - self.FMT_B_B : "", - self.FMT_B_E : "", - self.FMT_I_B : "", - self.FMT_I_E : "", - self.FMT_U_B : "", - self.FMT_U_E : "", - } - - self.theResult = "" - thisPar = [] - for tType, tText, tFormat, tAlign in self.theTokens: - - aStyle = [] - if tAlign == self.A_CENTRE: - aStyle.append("text-align: center;") - - if len(aStyle) > 0: - hStyle = " style='%s'" % (" ".join(aStyle)) - else: - hStyle = "" - - if tType == self.T_EMPTY: - if len(thisPar) > 0: - tTemp = "".join(thisPar) - self.theResult += "%s

\n" % (hStyle,tTemp.rstrip()) - thisPar = [] - - elif tType == self.T_HEAD1: - self.theResult += "%s\n" % (hStyle,tText) - - elif tType == self.T_HEAD2: - self.theResult += "%s\n" % (hStyle,tText) - - elif tType == self.T_HEAD3: - self.theResult += "%s\n" % (hStyle,tText) - - elif tType == self.T_HEAD4: - self.theResult += "%s\n" % (hStyle,tText) - - elif tType == self.T_SEP: - self.theResult += "%s

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

 

\n" - - elif tType == self.T_TEXT: - tTemp = tText - for xPos, xLen, xFmt in reversed(tFormat): - tTemp = tTemp[:xPos]+htmlTags[xFmt]+tTemp[xPos+xLen:] - if tText.endswith(" "): - thisPar.append(tTemp.rstrip()+"
") - else: - thisPar.append(tTemp.rstrip()+" ") - - elif tType == self.T_COMMENT and self.doComments: - self.theResult += self._formatComments(tText) - - elif tType == self.T_KEYWORD and self.doKeywords: - self.theResult += self._formatTags(tText) - - return - - ## - # Internal Functions - ## - - def _formatTags(self, tText): - - if not self.forPreview: - return "
@%s
\n" % tText - - tText = "@"+tText - isValid, theBits, thePos = self.theParent.theIndex.scanThis(tText) - if not isValid or not theBits: - return "" - - retText = "" - 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) - - return "
%s
" % retText - - def _formatComments(self, tText): - - if not self.forPreview: - return "
%s
\n" % tText - - return "

%s

\n" % tText - -# END Class ToHtml 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 diff --git a/nw/convert/tokenizer.py b/nw/convert/tokenizer.py deleted file mode 100644 index 4ac99377..00000000 --- a/nw/convert/tokenizer.py +++ /dev/null @@ -1,345 +0,0 @@ -# -*- coding: utf-8 -*- -"""novelWriter Text Tokenizer - - novelWriter – Text Tokenizer -============================== - Splits a piece of nW markdown text into its elements - - File History: - Created: 2019-05-05 [0.0.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 re -import nw - -from operator import itemgetter -from PyQt5.QtCore import QRegularExpression - -from nw.core.document import NWDoc -from nw.core.tools import numberToWord -from nw.constants import nwItemLayout - -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 - - 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 - - A_LEFT = 1 # Left aligned - A_RIGHT = 2 # Right aligned - A_CENTRE = 3 # Centred - A_JUSTIFY = 4 # Justified - - def __init__(self, theProject, theParent): - - self.mainConf = nw.CONFIG - self.theProject = theProject - self.theParent = theParent - - self.theText = None - self.theHandle = None - self.theItem = None - self.theTokens = None - self.theResult = None - - self.wordWrap = 0 - self.doComments = False - self.doKeywords = False - - self.fmtTitle = "%title%" - self.fmtChapter = "%title%" - self.fmtUnNum = "%title%" - self.fmtScene = "%title%" - self.fmtSection = "%title%" - - self.hideScene = False - self.hideSection = False - - self.numChapter = 0 - self.firstScene = False - - return - - ## - # Setters - ## - - def setComments(self, doComments): - self.doComments = doComments - return - - def setKeywords(self, doKeywords): - 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 - - def setChapterFormat(self, fmtChapter): - self.fmtChapter = fmtChapter - return - - def setUnNumberedFormat(self, fmtUnNum): - self.fmtUnNum = fmtUnNum - return - - def setSceneFormat(self, fmtScene, hideScene): - self.fmtScene = fmtScene - self.hideScene = hideScene - return - - def setSectionFormat(self, fmtSection, hideSection): - self.fmtSection = fmtSection - self.hideSection = hideSection - return - - ## - # Class Methods - ## - - def setText(self, theHandle, theText=None): - - self.theHandle = theHandle - self.theItem = self.theProject.projTree[theHandle] - - if theText is not None: - # If the text is set, just use that - self.theText = theText - else: - # Otherwise, load it from file - theDocument = NWDoc(self.theProject, self.theParent) - self.theText = theDocument.openDocument(theHandle) - - return - - def doAutoReplace(self): - 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): - return - - def tokenizeText(self): - """Scan the text for either lines starting with specific - characters that indicate headers, comments, commands etc, or - 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. - """ - - # RegExes for adding formatting tags within text lines - # Keep in sync with the DocHighlighter class - rxFormats = [( - QRegularExpression(r"(? 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) @@ -527,11 +547,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: @@ -713,6 +729,16 @@ class NWProject(): self.autoReplace = autoReplace return + def setTitleFormat(self, titleFormat): + """Set the formatting of titles in the project. + """ + 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): """Toggle the project changed flag, and propagate the information to the GUI statusbar. @@ -728,6 +754,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. """ @@ -1306,6 +1341,7 @@ class NWItem(): self.itemLayout = nwItemLayout.NO_LAYOUT self.itemStatus = None self.isExpanded = False + self.isExported = True # Document Meta Data self.charCount = 0 @@ -1333,6 +1369,7 @@ class NWItem(): xSub = self._subPack(xPack,"status", text=str(self.itemStatus)) xSub = self._subPack(xPack,"expanded", text=str(self.isExpanded)) 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) @@ -1365,6 +1402,7 @@ class NWItem(): "layout" : self.setLayout, "status" : self.setStatus, "expanded" : self.setExpanded, + "exported" : self.setExported, "charCount" : self.setCharCount, "wordCount" : self.setWordCount, "paraCount" : self.setParaCount, @@ -1465,6 +1503,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 ## diff --git a/nw/core/tohtml.py b/nw/core/tohtml.py new file mode 100644 index 00000000..f10ee1e3 --- /dev/null +++ b/nw/core/tohtml.py @@ -0,0 +1,266 @@ +# -*- coding: utf-8 -*- +"""novelWriter HTML Text Converter + + novelWriter – HTML Text Converter +=================================== + Extends the Tokenizer class to write HTML + + File History: + Created: 2019-05-07 [0.0.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 re +import nw + +from nw.core.tokenizer import Tokenizer +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.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 + + ## + # 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 managed by + these flags. + """ + if forPreview: + self.genMode = self.M_PREVIEW + self.doKeywords = True + self.doComments = doComments + self.repDict["\t"] = " "*8 + 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) + + 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) + + 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 + + def doConvert(self): + """Convert the list of text tokens into a HTML document saved + to theResult. + """ + + htmlTags = { + self.FMT_B_B : "", + self.FMT_B_E : "", + self.FMT_I_B : "", + self.FMT_I_E : "", + self.FMT_U_B : "", + self.FMT_U_E : "", + } + + self.theResult = "" + + thisPar = [] + parStyle = "" + tmpResult = [] + for tType, tText, tFormat, tStyle in self.theTokens: + + # Styles + aStyle = [] + 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)) + else: + hStyle = "" + + # Process TextType + if tType == self.T_EMPTY: + if len(thisPar) > 0: + tTemp = "".join(thisPar) + tmpResult.append("%s

\n" % (parStyle, tTemp.rstrip())) + thisPar = [] + parStyle = "" + + elif tType == self.T_HEAD1: + tHead = tText.replace(r"\\", "
") + tmpResult.append("%s\n" % (hStyle, tHead)) + + elif tType == self.T_HEAD2: + tHead = tText.replace(r"\\", "
") + tmpResult.append("%s\n" % (hStyle, tHead)) + + elif tType == self.T_HEAD3: + tHead = tText.replace(r"\\", "
") + tmpResult.append("%s\n" % (hStyle, tHead)) + + elif tType == self.T_HEAD4: + tHead = tText.replace(r"\\", "
") + tmpResult.append("%s\n" % (hStyle, tHead)) + + elif tType == self.T_SEP: + tmpResult.append("%s

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

\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(" "): + thisPar.append(tTemp.rstrip()+"
") + else: + thisPar.append(tTemp.rstrip()+" ") + + elif tType == self.T_SYNOPSIS and self.doSynopsis: + tmpResult.append(self._formatSynopsis(tText)) + + elif tType == self.T_COMMENT and self.doComments: + tmpResult.append(self._formatComments(tText)) + + elif tType == self.T_KEYWORD and self.doKeywords: + tmpResult.append(self._formatKeywords(tText)) + + self.theResult = "".join(tmpResult) + tmpResult = [] + + return + + ## + # Internal Functions + ## + + def _formatSynopsis(self, tText): + """Apply HTML formatting to synopsis. + """ + if self.genMode == self.M_EXPORT: + return "

Synopsis: %s

\n" % tText + else: + return "

%s

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

Comment: %s

\n" % tText + else: + return "

%s

\n" % tText + + def _formatKeywords(self, tText): + """Apply HTML formatting to keywords. + """ + + tText = "@"+tText + isValid, theBits, thePos = self.theParent.theIndex.scanThis(tText) + if not isValid or not theBits: + return "" + + retText = "" + refTags = [] + if theBits[0] in nwLabels.KEY_NAME: + retText += "%s: " % nwLabels.KEY_NAME[theBits[0]] + 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 + +# END Class ToHtml diff --git a/nw/core/tokenizer.py b/nw/core/tokenizer.py new file mode 100644 index 00000000..80cc216d --- /dev/null +++ b/nw/core/tokenizer.py @@ -0,0 +1,533 @@ +# -*- coding: utf-8 -*- +"""novelWriter Text Tokenizer + + novelWriter – Text Tokenizer +============================== + Splits a piece of nW markdown text into its elements + + File History: + Created: 2019-05-05 [0.0.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 re +import nw + +from operator import itemgetter +from PyQt5.QtCore import QRegularExpression + +from nw.core.document import NWDoc +from nw.core.tools import numberToWord +from nw.constants import nwItemLayout + +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 + + 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 + + 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): + + self.mainConf = nw.CONFIG + self.theProject = theProject + self.theParent = theParent + + # 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.theMarkdown = None # The result text in novelWriter markdown + + # 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 + self.fmtUnNum = "%title%" # Formatting for unnumbered chapters + self.fmtScene = "%title%" # Formatting for scenes + self.fmtSection = "%title%" # Formatting for sections + + self.hideScene = False # Do not include scene headers + self.hideSection = False # Do not include section headers + + # 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 + + # 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): + """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.theMarkdown = 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 + + ## + # Setters + ## + + def setTitleFormat(self, fmtTitle): + self.fmtTitle = fmtTitle + return + + def setChapterFormat(self, fmtChapter): + self.fmtChapter = fmtChapter + return + + def setUnNumberedFormat(self, fmtUnNum): + self.fmtUnNum = fmtUnNum + return + + def setSceneFormat(self, fmtScene, hideScene): + self.fmtScene = fmtScene + self.hideScene = hideScene + return + + def setSectionFormat(self, fmtSection, hideSection): + self.fmtSection = fmtSection + 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 + ## + + 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] + + if theText is not None: + # If the text is set, just use that + self.theText = theText + else: + # Otherwise, load it from file + 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): + """Return the result from the conversion. + """ + 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. + """ + + 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): + """Do some postprocessing. Overloaded by subclasses. + """ + return + + def tokenizeText(self): + """Scan the text for either lines starting with specific + characters that indicate headers, comments, commands etc, or + 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 + # Keep in sync with the DocHighlighter class + rxFormats = [( + QRegularExpression(r"(?= 0: + tToken = self.theTokens[n] + self.theTokens[n] = ( + tToken[0], tToken[1], tToken[2], tToken[3] | self.A_PBA + ) + + return + + ## + # Internal Functions + ## + + def _formatHeading(self, theTitle, theText): + """Replaces the %keyword% strings. + """ + theTitle = theTitle.replace(r"%title%", theText) + theTitle = theTitle.replace(r"%chnum%", str(self.numChapter)) + theTitle = theTitle.replace(r"%scnum%", str(self.numChScene)) + theTitle = theTitle.replace(r"%scabsnum%", str(self.numAbsScene)) + theTitle = theTitle.replace(r"%chnumword%", numberToWord(self.numChapter,"en")) + return theTitle + +# END Class Tokenizer diff --git a/nw/gui/__init__.py b/nw/gui/__init__.py index c59bfed9..c209b8d8 100644 --- a/nw/gui/__init__.py +++ b/nw/gui/__init__.py @@ -1,20 +1,20 @@ # -*- 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 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 @@ -37,16 +37,16 @@ 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", - "GuiExport", "GuiItemEditor", "GuiProjectEditor", "GuiProjectLoad", diff --git a/nw/gui/build.py b/nw/gui/build.py new file mode 100644 index 00000000..737a13c4 --- /dev/null +++ b/nw/gui/build.py @@ -0,0 +1,691 @@ +# -*- 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 os import path +from time import time + +from PyQt5.QtCore import Qt, QByteArray +from PyQt5.QtPrintSupport import QPrinter, QPrintPreviewDialog +from PyQt5.QtGui import ( + QTextOption, QPalette, QColor, QTextDocumentWriter +) +from PyQt5.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QTextBrowser, QPushButton, QLabel, + QLineEdit, QGroupBox, QGridLayout, QProgressBar, QMenu, QAction, QFileDialog +) + +from nw.gui.additions import QSwitch +from nw.core import ToHtml +from nw.constants import ( + nwConst, nwFiles, nwAlert, nwItemType, nwItemLayout, nwItemClass +) + +logger = logging.getLogger(__name__) + +class GuiBuildNovel(QDialog): + + 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) + + logger.debug("Initialising GuiBuildNovel ...") + + self.mainConf = nw.CONFIG + self.theProject = theProject + self.theParent = theParent + self.theTheme = theParent.theTheme + self.optState = self.theProject.optState + + 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) + self.setMinimumHeight(800) + + self.resize( + self.optState.getInt("GuiBuildNovel", "winWidth", 800), + self.optState.getInt("GuiBuildNovel", "winHeight", 800) + ) + + 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) + + # 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) + 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.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) + 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.addWidget(QLabel("Exclude body text"), 3, 0) + self.addsForm.addWidget(self.excludeBody, 3, 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.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 Format (.pdf)") + self.savePDF.triggered.connect(lambda: self._saveDocument(self.FMT_PDF)) + self.saveMenu.addAction(self.savePDF) + + 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) + + 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.textGroup) + 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.show() + + logger.debug("GuiBuildNovel initialisation complete") + + return + + ## + # Slots + ## + + def _buildPreview(self): + """Build a preview of the project in the document viewer. + """ + + # 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() + incSynopsis = self.includeSynopsis.isChecked() + incComments = self.includeComments.isChecked() + incKeywords = self.includeKeywords.isChecked() + novelFiles = self.novelFiles.isChecked() + noteFiles = self.noteFiles.isChecked() + ignoreFlag = self.ignoreFlag.isChecked() + excludeBody = self.excludeBody.isChecked() + + 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(not excludeBody) + makeHtml.setSynopsis(incSynopsis) + makeHtml.setComments(incComments) + makeHtml.setKeywords(incKeywords) + makeHtml.setJustify(justifyText) + + self.buildProgress.setMaximum(len(self.theProject.projTree)) + self.buildProgress.setValue(0) + + tStart = time() + + 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) + makeHtml.doAutoReplace() + makeHtml.tokenizeText() + makeHtml.doHeaders() + makeHtml.doConvert() + makeHtml.doPostProcessing() + self.htmlText.append(makeHtml.getResult()) + self.nwdText.append(makeHtml.getFilteredMarkdown()) + self.textLayout.append(tItem.itemLayout) + + # Update progress bar, also for skipped items + self.buildProgress.setValue(nItt+1) + + tEnd = time() + logger.debug("Built project in %.3f ms" % (1000*(tEnd-tStart))) + + # Load the preview document with the html data + self.docView.setHtml("".join(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. + """ + + # FMT_PDF + + byteFmt = QByteArray() + fileExt = "" + textFmt = "" + outTool = "" + + # Create the settings + if theFormat == self.FMT_ODT: + byteFmt.append("odf") + fileExt = "odt" + textFmt = "Open Document" + outTool = "Qt" + + elif theFormat == self.FMT_PDF: + fileExt = "pdf" + textFmt = "PDF" + outTool = "QtPrint" + + elif theFormat == self.FMT_HTM: + 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_NWD: + fileExt = "nwd" + textFmt = "%s markdown" % nw.__package__ + outTool = "NW" + + 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": + try: + with open(savePath, mode="w", encoding="utf8") as outFile: + 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" % ( + 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 + ) + + 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 + + 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 _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", "justifyText", self.justifyText.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 + + 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): + + def __init__(self, theParent, theProject): + QTextBrowser.__init__(self, theParent) + + logger.debug("Initialising GuiBuildNovelDocView ...") + + self.mainConf = nw.CONFIG + self.theProject = theProject + self.theParent = theParent + + 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) + 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._makeStyleSheet() + + self.show() + + logger.debug("GuiBuildNovelDocView initialisation complete") + + 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" + ".tags {" + " color: rgb(245, 135, 31);" + " font-wright: bold;" + "}\n" + ) + self.qDocument.setDefaultStyleSheet(styleSheet) + + return + +# END Class GuiBuildNovelDocView 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/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/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/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/nw/gui/dialogs/itemeditor.py b/nw/gui/dialogs/itemeditor.py index 30c4721c..c94e053e 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, + QComboBox, QLabel, QSpacerItem, QSizePolicy, QDialogButtonBox ) +from nw.gui.additions import QSwitch from nw.constants import nwLabels, nwItemLayout, nwItemClass, nwItemType logger = logging.getLogger(__name__) @@ -43,27 +44,31 @@ 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 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,26 @@ 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.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.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,39 +134,43 @@ 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() self.editName.selectAll() - logger.debug("ItemEditor initialisation complete") + logger.debug("GuiItemEditor initialisation complete") 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/docdetails.py b/nw/gui/elements/docdetails.py index a55e44c2..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 +from nw.constants import nwLabels, nwItemClass, nwItemType, 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,137 @@ 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.itemType == nwItemType.FILE: + if nwItem.isExported: + exportFlag = nwUnicode.U_CHECK + else: + exportFlag = " " + 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 diff --git a/nw/gui/elements/doceditor.py b/nw/gui/elements/doceditor.py index 96966ddd..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, @@ -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 92a006de..4b0a3ed2 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,9 +412,19 @@ class GuiDocTree(QTreeWidget): tHandle = nwItem.itemHandle pHandle = nwItem.parHandle - 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 @@ -519,8 +532,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..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 @@ -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/gui/mainmenu.py b/nw/gui/mainmenu.py index a37ddf0c..56ec0337 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) @@ -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) 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 2f1380bd..8a37386c 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -40,12 +40,12 @@ 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.core import NWProject, NWDoc, NWIndex from nw.constants import nwFiles, nwItemType, nwAlert logger = logging.getLogger(__name__) @@ -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 @@ -755,9 +758,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 @@ -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): 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 diff --git a/sample/sampleNovel/nwProject.nwx b/sample/sampleNovel/nwProject.nwx index ad764dec..9e0468e6 100644 --- a/sample/sampleNovel/nwProject.nwx +++ b/sample/sampleNovel/nwProject.nwx @@ -11,13 +11,23 @@ True True 96b68994dfa3d - b3e74dbc1f584 + 6a2d6d5f4f401 875 B E D + + %title% + Chapter %chnum%.\\%title% + %title% + Scene %chnum%.%scnum%: %title% +
+ True + False + False +
New Notes @@ -48,6 +58,7 @@ NOVEL Started False + True TITLE 72 15 @@ -67,6 +78,7 @@ NOVEL Notes False + True CHAPTER 12 3 @@ -79,6 +91,7 @@ NOVEL 1st Draft False + True SCENE 1199 216 @@ -91,6 +104,7 @@ NOVEL 1st Draft False + True SCENE 476 93 @@ -103,6 +117,7 @@ NOVEL Finished False + True UNNUMBERED 633 101 @@ -115,6 +130,7 @@ NOVEL 2nd Draft False + False NOTE 1692 313 @@ -127,6 +143,7 @@ NOVEL 1st Draft False + True CHAPTER 139 28 @@ -139,6 +156,7 @@ NOVEL 1st Draft False + True SCENE 189 37 @@ -165,6 +183,7 @@ CHARACTER Minor False + True NOTE 49 9 @@ -177,6 +196,7 @@ CHARACTER Major False + True NOTE 55 9 @@ -196,6 +216,7 @@ WORLD Main False + True NOTE 76 15 @@ -208,6 +229,7 @@ WORLD Minor False + True NOTE 115 24 @@ -220,6 +242,7 @@ WORLD Major False + True NOTE 28 6 @@ -239,6 +262,7 @@ NOVEL New False + True SCENE 30 6 diff --git a/tests/reference/gui/0_nwProject.nwx b/tests/reference/gui/0_nwProject.nwx index e5368533..db1151d3 100644 --- a/tests/reference/gui/0_nwProject.nwx +++ b/tests/reference/gui/0_nwProject.nwx @@ -1,5 +1,5 @@ - + @@ -12,6 +12,16 @@ None 0 + + %title% + Chapter %num%\\%title% + %title% + * * * +
+ False + False + False +
New Note @@ -46,6 +56,7 @@ NOVEL New False + True SCENE 0 0 diff --git a/tests/reference/gui/1_nwProject.nwx b/tests/reference/gui/1_nwProject.nwx index c55ff681..bf16def6 100644 --- a/tests/reference/gui/1_nwProject.nwx +++ b/tests/reference/gui/1_nwProject.nwx @@ -1,5 +1,5 @@ - + @@ -12,6 +12,16 @@ 31489056e0916 86 + + %title% + Chapter %num%\\%title% + %title% + * * * +
+ False + False + False +
New Note @@ -46,6 +56,7 @@ NOVEL New False + True SCENE 331 59 @@ -65,6 +76,7 @@ CHARACTER New False + True NOTE 34 8 @@ -84,6 +96,7 @@ PLOT New False + True NOTE 48 10 @@ -103,6 +116,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..bf3c5b61 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,16 @@ With This Stuff + + %title% + Chapter %num%\\%title% + %title% + * * * +
+ False + False + False +
New Note @@ -50,6 +60,7 @@ NOVEL New False + True SCENE 0 0 diff --git a/tests/reference/gui/3_nwProject.nwx b/tests/reference/gui/3_nwProject.nwx index f0bda720..bee4eb0a 100644 --- a/tests/reference/gui/3_nwProject.nwx +++ b/tests/reference/gui/3_nwProject.nwx @@ -1,5 +1,5 @@ - + @@ -12,6 +12,16 @@ None 0 + + %title% + Chapter %num%\\%title% + %title% + * * * +
+ False + False + False +
New Note @@ -46,6 +56,7 @@ NOVEL Note False + False PAGE 0 0 diff --git a/tests/reference/proj/1_nwProject.nwx b/tests/reference/proj/1_nwProject.nwx index 319d24b3..0e537470 100644 --- a/tests/reference/proj/1_nwProject.nwx +++ b/tests/reference/proj/1_nwProject.nwx @@ -1,5 +1,5 @@ - + @@ -12,6 +12,16 @@ None 0 + + %title% + Chapter %num%\\%title% + %title% + * * * +
+ False + False + False +
New Note @@ -67,6 +77,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..eb98cc9f 100644 --- a/tests/reference/proj/2_nwProject.nwx +++ b/tests/reference/proj/2_nwProject.nwx @@ -1,5 +1,5 @@ - + @@ -12,6 +12,16 @@ None 0 + + %title% + Chapter %num%\\%title% + %title% + * * * +
+ False + False + False +
New Note @@ -67,6 +77,7 @@ NOVEL New False + True SCENE 0 0 diff --git a/tests/test_gui.py b/tests/test_gui.py index e2291fff..c32e8168 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,10 @@ def testItemEditor(qtbot, nwTempGUI, nwRef, nwTemp): layoutIdx = itemEdit.editLayout.findData(nwItemLayout.PAGE) itemEdit.editLayout.setCurrentIndex(layoutIdx) - qtbot.mouseClick(itemEdit.saveButton, Qt.LeftButton) + itemEdit.editExport.setChecked(False) + assert not itemEdit.editExport.isChecked() + + itemEdit._doSave() itemEdit = GuiItemEditor(nwGUI, nwGUI.theProject, "31489056e0916") qtbot.addWidget(itemEdit) @@ -369,7 +372,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..1bab7447 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 NameTRASHTRASH" + b"MainTrue" b"" b"" )