@@ -19,6 +19,7 @@ class nwFiles():
|
||||
PROJ_DICT = "wordlist.txt"
|
||||
SESS_INFO = "sessionInfo.log"
|
||||
INDEX_FILE = "tagsIndex.json"
|
||||
EXPORT_OPT = "exportOptions.json"
|
||||
|
||||
# END Class nwFiles
|
||||
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
# -*- 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]
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
import nw
|
||||
|
||||
from os import path
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
from nw.convert.tokenizer import Tokenizer
|
||||
from nw.enum import nwAlert, nwItemLayout
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class TextFile():
|
||||
|
||||
def __init__(self, theProject, theParent):
|
||||
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theProject = theProject
|
||||
self.theParent = theParent
|
||||
self.fileExt = "txt"
|
||||
|
||||
self.outFile = None
|
||||
self.fileName = ""
|
||||
self.theText = ""
|
||||
self.expNovel = True
|
||||
self.expNotes = False
|
||||
self.winEnding = False
|
||||
|
||||
self.theConv = Tokenizer(self.theProject, self.theParent)
|
||||
self.makeAlert = self.theParent.makeAlert
|
||||
|
||||
self.setComments(False)
|
||||
self.setMeta(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 setMeta(self, doMeta):
|
||||
self.theConv.setCommands(doMeta)
|
||||
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):
|
||||
self.theConv.setSceneFormat(fmtScene)
|
||||
return
|
||||
|
||||
def setSectionFormat(self, fmtSection):
|
||||
self.theConv.setSectionFormat(fmtSection)
|
||||
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.<br>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)
|
||||
|
||||
theItem = self.theProject.getItem(tHandle)
|
||||
isNone = theItem.itemLayout == nwItemLayout.NO_LAYOUT
|
||||
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
|
||||
|
||||
self.theConv.setText(tHandle)
|
||||
self.theConv.doAutoReplace()
|
||||
self.theConv.tokenizeText()
|
||||
self.theConv.doHeaders()
|
||||
self.theConv.doConvert()
|
||||
|
||||
if self.winEnding:
|
||||
self.theConv.windowsEndings()
|
||||
|
||||
if self.theConv.theResult is not None and self.outFile is not None:
|
||||
self.outFile.write(self.theConv.theResult)
|
||||
|
||||
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="w+")
|
||||
if self.winEnding:
|
||||
self.outFile.write("\r\n\r\n")
|
||||
else:
|
||||
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 a 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
|
||||
@@ -51,21 +51,21 @@ class ToHtml(Tokenizer):
|
||||
|
||||
self.theResult = ""
|
||||
thisPar = []
|
||||
for tType, tText, tFormat in self.theTokens:
|
||||
for tType, tText, tFormat, tAlign in self.theTokens:
|
||||
|
||||
if tType == "empty":
|
||||
if tType == self.T_EMPTY:
|
||||
if len(thisPar) > 0:
|
||||
self.theResult += "<p>%s</p>\n" % " ".join(thisPar)
|
||||
thisPar = []
|
||||
elif tType == "header1":
|
||||
elif tType == self.T_HEAD1:
|
||||
self.theResult += "<h1>%s</h1>\n" % tText
|
||||
elif tType == "header2":
|
||||
elif tType == self.T_HEAD2:
|
||||
self.theResult += "<h2>%s</h2>\n" % tText
|
||||
elif tType == "header3":
|
||||
elif tType == self.T_HEAD3:
|
||||
self.theResult += "<h3>%s</h3>\n" % tText
|
||||
elif tType == "header4":
|
||||
elif tType == self.T_HEAD4:
|
||||
self.theResult += "<h4>%s</h4>\n" % tText
|
||||
elif tType == "text":
|
||||
elif tType == self.T_TEXT:
|
||||
tTemp = tText
|
||||
for xPos, xLen, xFmt in reversed(tFormat):
|
||||
tTemp = tTemp[:xPos]+htmlTags[xFmt]+tTemp[xPos+xLen:]
|
||||
|
||||
+290
-16
@@ -10,24 +10,43 @@
|
||||
|
||||
"""
|
||||
|
||||
import textwrap
|
||||
import logging
|
||||
import re
|
||||
import nw
|
||||
|
||||
from operator import itemgetter
|
||||
from PyQt5.QtCore import QRegularExpression
|
||||
|
||||
from nw.project.document import NWDoc
|
||||
from nw.tools.translate import numberToWord
|
||||
from nw.enum 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
|
||||
FMT_B_B = "" # Begin bold
|
||||
FMT_B_E = "" # End bold
|
||||
FMT_I_B = "" # Begin italics
|
||||
FMT_I_E = "" # End italics
|
||||
FMT_U_B = "" # Begin underline
|
||||
FMT_U_E = "" # End underline
|
||||
|
||||
T_EMPTY = 1 # Empty line (new paragraph)
|
||||
T_COMMENT = 2 # Comment line
|
||||
T_COMMAND = 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
|
||||
|
||||
A_LEFT = 1 # Left aligned
|
||||
A_RIGHT = 2 # Right aligned
|
||||
A_CENTRE = 3 # Centred
|
||||
A_JUSTIFY = 4 # Justified
|
||||
|
||||
def __init__(self, theProject, theParent):
|
||||
|
||||
@@ -41,8 +60,66 @@ class Tokenizer():
|
||||
self.theTokens = None
|
||||
self.theResult = None
|
||||
|
||||
self.wordWrap = 80
|
||||
self.doComments = False
|
||||
self.doCommands = False
|
||||
|
||||
self.fmtTitle = "%title%"
|
||||
self.fmtChapter = "Chapter %numword%: %title%"
|
||||
self.fmtUnNum = "%title%"
|
||||
self.fmtScene = "* * *"
|
||||
self.fmtSection = "%title%"
|
||||
|
||||
self.noSection = True
|
||||
|
||||
self.numChapter = 0
|
||||
self.firstScene = False
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Setters
|
||||
##
|
||||
|
||||
def setComments(self, doComments):
|
||||
self.doComments = doComments
|
||||
return
|
||||
|
||||
def setCommands(self, doCommands):
|
||||
self.doCommands = doCommands
|
||||
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):
|
||||
self.fmtScene = fmtScene
|
||||
return
|
||||
|
||||
def setSectionFormat(self, fmtSection):
|
||||
self.fmtSection = fmtSection
|
||||
return
|
||||
|
||||
##
|
||||
# Class Methods
|
||||
##
|
||||
|
||||
def setText(self, theHandle, theText=None):
|
||||
|
||||
self.theHandle = theHandle
|
||||
@@ -93,19 +170,19 @@ class Tokenizer():
|
||||
|
||||
# Tag lines starting with specific characters
|
||||
if len(aLine) == 0:
|
||||
self.theTokens.append(("empty","",None))
|
||||
self.theTokens.append((self.T_EMPTY,"",None,self.A_LEFT))
|
||||
elif aLine[0] == "%":
|
||||
self.theTokens.append(("comment",aLine[1:].strip(),None))
|
||||
self.theTokens.append((self.T_COMMENT,aLine[1:].strip(),None,self.A_LEFT))
|
||||
elif aLine[0] == "@":
|
||||
self.theTokens.append(("command",aLine[1:].strip(),None))
|
||||
self.theTokens.append((self.T_COMMAND,aLine[1:].strip(),None,self.A_LEFT))
|
||||
elif aLine[:2] == "# ":
|
||||
self.theTokens.append(("header1",aLine[2:].strip(),None))
|
||||
self.theTokens.append((self.T_HEAD1,aLine[2:].strip(),None,self.A_LEFT))
|
||||
elif aLine[:3] == "## ":
|
||||
self.theTokens.append(("header2",aLine[3:].strip(),None))
|
||||
self.theTokens.append((self.T_HEAD2,aLine[3:].strip(),None,self.A_LEFT))
|
||||
elif aLine[:4] == "### ":
|
||||
self.theTokens.append(("header3",aLine[4:].strip(),None))
|
||||
self.theTokens.append((self.T_HEAD3,aLine[4:].strip(),None,self.A_LEFT))
|
||||
elif aLine[:5] == "#### ":
|
||||
self.theTokens.append(("header4",aLine[5:].strip(),None))
|
||||
self.theTokens.append((self.T_HEAD4,aLine[5:].strip(),None,self.A_LEFT))
|
||||
else:
|
||||
# Otherwise we use RegEx to find formatting tags within a line of text
|
||||
fmtPos = []
|
||||
@@ -121,12 +198,209 @@ class Tokenizer():
|
||||
|
||||
# Save the line as is, but append the array of formatting locations sorted by position
|
||||
fmtPos = sorted(fmtPos,key=itemgetter(0))
|
||||
self.theTokens.append(("text",aLine,fmtPos))
|
||||
self.theTokens.append((self.T_TEXT,aLine,fmtPos,self.A_LEFT))
|
||||
|
||||
# Always add an empty line at the end
|
||||
self.theTokens.append(("empty","",None))
|
||||
# print(self.theTokens)
|
||||
self.theTokens.append((self.T_EMPTY,"",None,self.A_LEFT))
|
||||
|
||||
return
|
||||
|
||||
def doHeaders(self):
|
||||
|
||||
isNone = self.theItem.itemLayout == nwItemLayout.NO_LAYOUT
|
||||
isTitle = self.theItem.itemLayout == nwItemLayout.TITLE
|
||||
isBook = self.theItem.itemLayout == nwItemLayout.BOOK
|
||||
isPage = self.theItem.itemLayout == nwItemLayout.PAGE
|
||||
isPart = self.theItem.itemLayout == nwItemLayout.PARTITION
|
||||
isUnNum = self.theItem.itemLayout == nwItemLayout.UNNUMBERED
|
||||
isChap = self.theItem.itemLayout == nwItemLayout.CHAPTER
|
||||
isScene = self.theItem.itemLayout == nwItemLayout.SCENE
|
||||
isNote = self.theItem.itemLayout == nwItemLayout.NOTE
|
||||
|
||||
# No special header formatting for notes and no layout files
|
||||
if isNone: return
|
||||
if isNote: return
|
||||
|
||||
# For novel files, we need to handle chapter numbering and scene breaks
|
||||
if isBook or isUnNum or isChap or isScene:
|
||||
for n in range(len(self.theTokens)):
|
||||
|
||||
tToken = self.theTokens[n]
|
||||
tType = tToken[0]
|
||||
tText = tToken[1]
|
||||
|
||||
if tType == self.T_TEXT:
|
||||
self.firstScene = False
|
||||
|
||||
elif tType == self.T_HEAD2:
|
||||
if not isUnNum:
|
||||
self.numChapter += 1
|
||||
tText = self._doFormatChapter(tText,isUnNum)
|
||||
self.theTokens[n] = (tType,tText,None,self.A_LEFT)
|
||||
self.firstScene = True
|
||||
|
||||
elif tType == self.T_HEAD3:
|
||||
tTemp = self._doFormatScene(tText)
|
||||
if tTemp == "":
|
||||
self.theTokens[n] = (self.T_EMPTY,"",None,self.A_LEFT)
|
||||
elif tTemp == self.fmtScene:
|
||||
if self.firstScene:
|
||||
self.theTokens[n] = (self.T_EMPTY,"",None,self.A_LEFT)
|
||||
else:
|
||||
self.theTokens[n] = (self.T_SEP,tTemp,None,self.A_LEFT)
|
||||
else:
|
||||
self.theTokens[n] = (tType,tTemp,None,self.A_LEFT)
|
||||
self.firstScene = False
|
||||
|
||||
elif tType == self.T_HEAD4:
|
||||
tTemp = self._doFormatSection(tText)
|
||||
if tTemp == "":
|
||||
self.theTokens[n] = (self.T_EMPTY,"",None,self.A_LEFT)
|
||||
elif tTemp == self.fmtSection:
|
||||
self.theTokens[n] = (self.T_SEP,tTemp,None,self.A_LEFT)
|
||||
else:
|
||||
self.theTokens[n] = (tType,tTemp,None,self.A_LEFT)
|
||||
|
||||
# For title page and partitions, we need to centre all text
|
||||
if isTitle or isPart:
|
||||
for n in range(len(self.theTokens)):
|
||||
tToken = self.theTokens[n]
|
||||
tType = tToken[0]
|
||||
tText = tToken[1]
|
||||
tFormat = tToken[2]
|
||||
self.theTokens[n] = (tType,tText,tFormat,self.A_CENTRE)
|
||||
|
||||
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:
|
||||
self.theResult += "%s\n\n" % " ".join(thisPar)
|
||||
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_TEXT:
|
||||
thisPar.append(tText)
|
||||
|
||||
elif tType == self.T_COMMENT and self.doComments:
|
||||
self.theResult += "%s\n\n" % tText
|
||||
|
||||
elif tType == self.T_COMMAND and self.doCommands:
|
||||
self.theResult += "%s\n\n" % tText
|
||||
|
||||
return
|
||||
|
||||
def windowsEndings(self):
|
||||
self.theResult = self.theResult.replace("\n","\r\n")
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _doFormatTitle(self, theText):
|
||||
theTitle = self.fmtTitle
|
||||
theTitle = theTitle.replace("%title%", theText)
|
||||
return theTitle
|
||||
|
||||
def _doFormatChapter(self, theText, noNum):
|
||||
if noNum:
|
||||
theTitle = self.fmtUnNum
|
||||
theTitle = theTitle.replace("%title%", theText)
|
||||
else:
|
||||
theTitle = self.fmtChapter
|
||||
theTitle = theTitle.replace("%title%", theText)
|
||||
theTitle = theTitle.replace("%num%", str(self.numChapter))
|
||||
theTitle = theTitle.replace("%numword%", numberToWord(self.numChapter,"en"))
|
||||
return theTitle
|
||||
|
||||
def _doFormatScene(self, theText):
|
||||
theTitle = self.fmtScene
|
||||
theTitle = theTitle.replace("%title%", theText)
|
||||
return theTitle
|
||||
|
||||
def _doFormatSection(self, theText):
|
||||
theTitle = self.fmtSection
|
||||
theTitle = theTitle.replace("%title%", theText)
|
||||
return theTitle
|
||||
|
||||
def _centreText(self, theText, theWidth):
|
||||
tLen = len(theText)
|
||||
if tLen < theWidth:
|
||||
return " "*int((theWidth-tLen)/2) + theText
|
||||
return theText
|
||||
|
||||
# END Class Tokenizer
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
height="18"
|
||||
id="Layer_1"
|
||||
version="1.2"
|
||||
viewBox="0 0 19 18"
|
||||
width="19"
|
||||
xml:space="preserve"
|
||||
sodipodi:docname="export.svg"
|
||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"><metadata
|
||||
id="metadata874"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs872" /><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1080"
|
||||
id="namedview870"
|
||||
showgrid="false"
|
||||
inkscape:zoom="33.875"
|
||||
inkscape:cx="12.118081"
|
||||
inkscape:cy="12"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Layer_1" /><path
|
||||
d="M 18.711,6.796 C 18.67,6.755 14.656,2.7 12.729,0.65 12.309,0.236 11.73,0 11.143,0 9.961,0 9,0.896 9,2 H 1 C 0.447,2 0,2.448 0,3 v 14 c 0,0.552 0.447,1 1,1 h 14 c 0.553,0 1,-0.448 1,-1 v -6.045 c 1.434,-1.461 2.688,-2.729 2.711,-2.751 0.387,-0.39 0.387,-1.018 0,-1.408 z m -7.432,6.145 C 11.257,12.968 11.214,13 11.143,13 11.069,13 11.022,12.977 10.999,12.96 V 10 9 h -1 C 8.228,9.034 6.663,9.68 5.246,10.958 5.676,8.743 6.846,6 9.999,6 h 1 V 2.042 C 11.027,2.021 11.07,2 11.143,2 c 0.09,0 0.152,0.05 0.154,0.05 1.436,1.525 4.051,4.187 5.297,5.45 -0.253,0.257 -4.342,4.422 -5.315,5.441 z M 2,16 V 4 h 8 c 0,0.348 0,0.695 0,1 C 5.34,5 4,9.871 4,13.5 V 14 c 1.691,-2.578 3.6,-3.953 6,-4 0,1.045 0,2.838 0,3 0,0.551 0.512,1 1.143,1 0.364,0 0.676,-0.158 0.883,-0.391 0.539,-0.565 1.242,-1.291 1.976,-2.043 V 16 Z"
|
||||
id="path867"
|
||||
style="fill:#000000;fill-opacity:0.35408559"
|
||||
inkscape:connector-curvature="0" /></svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,3 @@
|
||||
FROM ICON SET: Typicons
|
||||
LICENSE: Creative Commons (Attribution-Share Alike 3.0 Unported)
|
||||
https://creativecommons.org/licenses/by-sa/3.0/
|
||||
+10
-5
@@ -68,10 +68,14 @@ class GuiConfigEditor(QDialog):
|
||||
|
||||
self.show()
|
||||
|
||||
logger.debug("ProjectEditor ConfigEditor complete")
|
||||
logger.debug("ConfigEditor initialisation complete")
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Buttons
|
||||
##
|
||||
|
||||
def _doSave(self):
|
||||
|
||||
logger.verbose("ConfigEditor save button clicked")
|
||||
@@ -111,10 +115,10 @@ class GuiConfigEditGeneral(QWidget):
|
||||
def __init__(self, theParent):
|
||||
QWidget.__init__(self, theParent)
|
||||
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theParent = theParent
|
||||
self.theTheme = theParent.theTheme
|
||||
self.outerBox = QGridLayout()
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theParent = theParent
|
||||
self.theTheme = theParent.theTheme
|
||||
self.outerBox = QGridLayout()
|
||||
|
||||
# User Interface
|
||||
self.guiLook = QGroupBox("User Interface", self)
|
||||
@@ -278,6 +282,7 @@ class GuiConfigEditGeneral(QWidget):
|
||||
if newDir:
|
||||
self.projBackupPath.setText(newDir)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
# END Class GuiConfigEditGeneral
|
||||
|
||||
@@ -54,7 +54,7 @@ class GuiDocDetails(QFrame):
|
||||
QLabel("")
|
||||
]
|
||||
|
||||
colOne = ["Name","Status","Class","Layout"]
|
||||
colOne = ["Label","Status","Class","Layout"]
|
||||
for nRow in range(4):
|
||||
lblOne = QLabel(colOne[nRow])
|
||||
lblOne.setFont(self.fntOne)
|
||||
|
||||
@@ -0,0 +1,507 @@
|
||||
# -*- 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]
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
import json
|
||||
import nw
|
||||
|
||||
from os import path
|
||||
|
||||
from PyQt5.QtCore import Qt, QSize
|
||||
from PyQt5.QtSvg import QSvgWidget
|
||||
from PyQt5.QtWidgets import (
|
||||
QDialog, QHBoxLayout, QVBoxLayout, QWidget, QTabWidget, QGridLayout, QGroupBox, QCheckBox,
|
||||
QLabel, QComboBox, QLineEdit, QPushButton, QFileDialog, QProgressBar, QSpinBox
|
||||
)
|
||||
|
||||
from nw.project.document import NWDoc
|
||||
from nw.tools.translate import numberToWord
|
||||
from nw.convert.textfile import TextFile
|
||||
from nw.common import checkString, checkBool, checkInt
|
||||
from nw.constants import nwFiles
|
||||
from nw.enum import nwItemType
|
||||
|
||||
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 = ExportLastState(self.theProject)
|
||||
|
||||
self.outerBox = QHBoxLayout()
|
||||
self.innerBox = QVBoxLayout()
|
||||
self.setWindowTitle("Export Project")
|
||||
self.setLayout(self.outerBox)
|
||||
|
||||
self.gradPath = path.abspath(path.join(self.mainConf.appPath,"graphics","export.svg"))
|
||||
self.svgGradient = QSvgWidget(self.gradPath)
|
||||
self.svgGradient.setFixedSize(QSize(64,64))
|
||||
|
||||
self.theProject.countStatus()
|
||||
self.tabMain = GuiExportMain(self.theParent, self.theProject, self.optState)
|
||||
|
||||
self.tabWidget = QTabWidget()
|
||||
self.tabWidget.addTab(self.tabMain, "Settings")
|
||||
|
||||
self.outerBox.addWidget(self.svgGradient, 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()
|
||||
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()
|
||||
|
||||
nItems = len(self.theProject.treeOrder)
|
||||
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)
|
||||
|
||||
if outFile is None:
|
||||
return False
|
||||
|
||||
if outFile.openFile(saveTo):
|
||||
outFile.setComments(wComments)
|
||||
outFile.setExportNovel(wNovel)
|
||||
outFile.setExportNotes(wNotes)
|
||||
outFile.setWordWrap(fixWidth)
|
||||
outFile.setChapterFormat(chFormat)
|
||||
outFile.setUnNumberedFormat(unFormat)
|
||||
outFile.setSceneFormat(scFormat)
|
||||
outFile.setSectionFormat(seFormat)
|
||||
else:
|
||||
self.exportStatus.setText("Failed to open file for writing ...")
|
||||
return False
|
||||
|
||||
nDone = 0
|
||||
for tHandle in self.theProject.treeOrder:
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
self.exportProgress.setValue(nDone)
|
||||
tItem = self.theProject.getItem(tHandle)
|
||||
|
||||
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(tHandle)
|
||||
|
||||
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)
|
||||
|
||||
return
|
||||
|
||||
def _doClose(self):
|
||||
|
||||
logger.verbose("GuiExport close 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()
|
||||
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()
|
||||
|
||||
self.optState.setSetting("wNovel", wNovel)
|
||||
self.optState.setSetting("wNotes", wNotes)
|
||||
self.optState.setSetting("eFormat", eFormat)
|
||||
self.optState.setSetting("fixWidth", fixWidth)
|
||||
self.optState.setSetting("wComments",wComments)
|
||||
self.optState.setSetting("chFormat", chFormat)
|
||||
self.optState.setSetting("unFormat", unFormat)
|
||||
self.optState.setSetting("scFormat", scFormat)
|
||||
self.optState.setSetting("seFormat", seFormat)
|
||||
self.optState.setSetting("saveTo", saveTo)
|
||||
|
||||
self.optState.saveSettings()
|
||||
self.close()
|
||||
|
||||
return
|
||||
|
||||
# END Class GuiExport
|
||||
|
||||
class GuiExportMain(QWidget):
|
||||
|
||||
FMT_TXT = 1
|
||||
FMT_MD = 2
|
||||
FMT_HTML = 3
|
||||
FMT_EBOOK = 4
|
||||
FMT_ODT = 5
|
||||
FMT_TEX = 6
|
||||
FMT_EXT = {
|
||||
FMT_TXT : ".txt",
|
||||
FMT_MD : ".md",
|
||||
FMT_HTML : ".htm",
|
||||
FMT_EBOOK : ".htm",
|
||||
FMT_ODT : ".odt",
|
||||
FMT_TEX : ".tex",
|
||||
}
|
||||
FMT_HELP = {
|
||||
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 converted to preformatted text blocks."
|
||||
),
|
||||
FMT_EBOOK : (
|
||||
"Exports an html5 file that can be converted to eBook with Calibre. "
|
||||
"Comments are not exported in this format."
|
||||
),
|
||||
FMT_ODT : (
|
||||
"Exports an open document file that can be read by office applications. "
|
||||
"Comments are exported as grey text."
|
||||
),
|
||||
FMT_TEX : (
|
||||
"Exports a LaTeX file that can be compiled to PDF using for instance PDFLaTeX. "
|
||||
"Comments are exported as LaTeX comments."
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, theParent, theProject, optState):
|
||||
QWidget.__init__(self, theParent)
|
||||
|
||||
self.theParent = theParent
|
||||
self.theProject = theProject
|
||||
self.theTheme = theParent.theTheme
|
||||
self.outerBox = QGridLayout()
|
||||
self.optState = 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(self)
|
||||
self.expNovel.setChecked(self.optState.getSetting("wNovel"))
|
||||
self.expNovel.setToolTip("Include all novel files in the exported document")
|
||||
|
||||
self.expNotes = QCheckBox(self)
|
||||
self.expNotes.setChecked(self.optState.getSetting("wNotes"))
|
||||
self.expNotes.setToolTip("Include all note files in the exported document")
|
||||
|
||||
self.expComments = QCheckBox(self)
|
||||
self.expComments.setChecked(self.optState.getSetting("wComments"))
|
||||
self.expComments.setToolTip("Export comments from all files")
|
||||
|
||||
self.guiFilesForm.addWidget(QLabel("Novel files"), 0, 0)
|
||||
self.guiFilesForm.addWidget(self.expNovel, 0, 1)
|
||||
self.guiFilesForm.addWidget(QLabel("Note files"), 1, 0)
|
||||
self.guiFilesForm.addWidget(self.expNotes, 1, 1)
|
||||
self.guiFilesForm.addWidget(QLabel("Comments"), 2, 0)
|
||||
self.guiFilesForm.addWidget(self.expComments, 2, 1)
|
||||
|
||||
# Chapter Settings
|
||||
self.guiChapters = QGroupBox("Chapter Headings", self)
|
||||
self.guiChaptersForm = QGridLayout(self)
|
||||
self.guiChapters.setLayout(self.guiChaptersForm)
|
||||
|
||||
self.chapterFormat = QLineEdit()
|
||||
self.chapterFormat.setText(self.optState.getSetting("chFormat"))
|
||||
self.chapterFormat.setToolTip("Available formats: %num%, %numword%, %title%")
|
||||
self.chapterFormat.setMinimumWidth(250)
|
||||
|
||||
self.unnumFormat = QLineEdit()
|
||||
self.unnumFormat.setText(self.optState.getSetting("unFormat"))
|
||||
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.setText(self.optState.getSetting("scFormat"))
|
||||
self.sceneFormat.setToolTip("Available formats: %title%")
|
||||
self.sceneFormat.setMinimumWidth(100)
|
||||
|
||||
self.sectionFormat = QLineEdit()
|
||||
self.sectionFormat.setText(self.optState.getSetting("seFormat"))
|
||||
self.sectionFormat.setToolTip("Available formats: %title%")
|
||||
self.sectionFormat.setMinimumWidth(100)
|
||||
|
||||
self.guiScenesForm.addWidget(QLabel("Scenes"), 0, 0)
|
||||
self.guiScenesForm.addWidget(self.sceneFormat, 0, 1)
|
||||
self.guiScenesForm.addWidget(QLabel("Sections"), 1, 0)
|
||||
self.guiScenesForm.addWidget(self.sectionFormat, 1, 1)
|
||||
|
||||
# Output Path
|
||||
self.exportTo = QGroupBox("Export Folder", self)
|
||||
self.exportToForm = QGridLayout(self)
|
||||
self.exportTo.setLayout(self.exportToForm)
|
||||
|
||||
self.exportPath = QLineEdit(self.optState.getSetting("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("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("HTML5 for eBook (.htm)", self.FMT_EBOOK)
|
||||
# self.outputFormat.addItem("Open Document (.odt)", self.FMT_ODT)
|
||||
# self.outputFormat.addItem("LaTeX for PDF (.tex)", self.FMT_TEX)
|
||||
self.outputFormat.currentIndexChanged.connect(self._updateFormat)
|
||||
|
||||
optIdx = self.outputFormat.findData(self.optState.getSetting("eFormat"))
|
||||
if optIdx != -1:
|
||||
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", 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.getSetting("fixWidth"))
|
||||
self.fixedWidth.setToolTip("0 disables the feature. Applies to .txt, .md and .tex files.")
|
||||
|
||||
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("<i>%s</i>" % self.FMT_HELP[self.currFormat])
|
||||
self._checkFileExtension()
|
||||
return
|
||||
|
||||
def _exportFolder(self):
|
||||
|
||||
currDir = self.exportPath.text()
|
||||
if not path.isdir(currDir):
|
||||
currDir = ""
|
||||
|
||||
extFilter = [
|
||||
"Text files (*.txt)",
|
||||
"Markdown files (*.md)",
|
||||
"HTML files (*.htm *.html)",
|
||||
"Open document files (*.odt)",
|
||||
"LaTeX files (*.tex)",
|
||||
]
|
||||
|
||||
dlgOpt = QFileDialog.Options()
|
||||
dlgOpt |= QFileDialog.ShowDirsOnly
|
||||
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()
|
||||
fileBits = path.splitext(saveTo)
|
||||
if self.currFormat > 0:
|
||||
saveTo = fileBits[0]+self.FMT_EXT[self.currFormat]
|
||||
self.exportPath.setText(saveTo)
|
||||
return
|
||||
|
||||
# END Class GuiExportMain
|
||||
|
||||
class ExportLastState():
|
||||
|
||||
def __init__(self, theProject):
|
||||
self.theProject = theProject
|
||||
self.theState = {
|
||||
"wNovel" : True,
|
||||
"wNotes" : False,
|
||||
"eFormat" : 2,
|
||||
"fixWidth" : 80,
|
||||
"wComments" : False,
|
||||
"chFormat" : "Chapter %numword%",
|
||||
"unFormat" : "%title%",
|
||||
"scFormat" : "* * *",
|
||||
"seFormat" : "",
|
||||
"saveTo" : "",
|
||||
}
|
||||
self.stringOpt = ("chFormat","unFormat","scFormat","seFormat","saveTo")
|
||||
self.boolOpt = ("wNovel","wNotes","wComments")
|
||||
self.intOpt = ("eFormat","fixWidth")
|
||||
self.loadSettings()
|
||||
return
|
||||
|
||||
def loadSettings(self):
|
||||
stateFile = path.join(self.theProject.projMeta, nwFiles.EXPORT_OPT)
|
||||
theState = {}
|
||||
if path.isfile(stateFile):
|
||||
logger.debug("Loading export options file")
|
||||
try:
|
||||
with open(stateFile,mode="r") as inFile:
|
||||
theJson = inFile.read()
|
||||
theState = json.loads(theJson)
|
||||
except Exception as e:
|
||||
logger.error("Failed to load export options file")
|
||||
logger.error(str(e))
|
||||
return False
|
||||
for anOpt in theState:
|
||||
self.theState[anOpt] = theState[anOpt]
|
||||
return True
|
||||
|
||||
def saveSettings(self):
|
||||
stateFile = path.join(self.theProject.projMeta, nwFiles.EXPORT_OPT)
|
||||
logger.debug("Saving export options file")
|
||||
try:
|
||||
with open(stateFile,mode="w+") as outFile:
|
||||
outFile.write(json.dumps(self.theState, indent=2))
|
||||
except Exception as e:
|
||||
logger.error("Failed to save export options file")
|
||||
logger.error(str(e))
|
||||
return False
|
||||
return True
|
||||
|
||||
def setSetting(self, setName, setValue):
|
||||
if setName in self.theState:
|
||||
self.theState[setName] = setValue
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
def getSetting(self, setName):
|
||||
if setName in self.stringOpt:
|
||||
return checkString(self.theState[setName],self.theState[setName],False)
|
||||
elif setName in self.boolOpt:
|
||||
return checkBool(self.theState[setName],self.theState[setName],False)
|
||||
elif setName in self.intOpt:
|
||||
return checkInt(self.theState[setName],self.theState[setName],False)
|
||||
return None
|
||||
|
||||
# END Class ExportLastState
|
||||
@@ -192,6 +192,13 @@ class GuiMainMenu(QMenuBar):
|
||||
menuItem.triggered.connect(self.theParent.editProjectDialog)
|
||||
self.projMenu.addAction(menuItem)
|
||||
|
||||
# Project > Export Project
|
||||
menuItem = QAction("Export Project", self)
|
||||
menuItem.setStatusTip("Export project")
|
||||
menuItem.setShortcut("F5")
|
||||
menuItem.triggered.connect(self.theParent.exportProjectDialog)
|
||||
self.projMenu.addAction(menuItem)
|
||||
|
||||
# Project > Separator
|
||||
self.projMenu.addSeparator()
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ from nw.gui.searchbar import GuiSearchBar
|
||||
from nw.gui.mainmenu import GuiMainMenu
|
||||
from nw.gui.configeditor import GuiConfigEditor
|
||||
from nw.gui.projecteditor import GuiProjectEditor
|
||||
from nw.gui.export import GuiExport
|
||||
from nw.gui.itemeditor import GuiItemEditor
|
||||
from nw.gui.statusbar import GuiMainStatus
|
||||
from nw.gui.timelineview import GuiTimeLineView
|
||||
@@ -511,6 +512,12 @@ class GuiMain(QMainWindow):
|
||||
self._setWindowTitle(self.theProject.projName)
|
||||
return True
|
||||
|
||||
def exportProjectDialog(self):
|
||||
if self.hasProject:
|
||||
dlgExport = GuiExport(self, self.theProject)
|
||||
dlgExport.exec_()
|
||||
return True
|
||||
|
||||
def showTimeLineDialog(self):
|
||||
if self.hasProject:
|
||||
dlgTLine = GuiTimeLineView(self, self.theProject, self.theIndex)
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Translate Tools
|
||||
|
||||
novelWriter – Translate Tools
|
||||
===============================
|
||||
Various translate tools
|
||||
|
||||
File History:
|
||||
Created: 2019-10-13 [0.2.3]
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
import nw
|
||||
|
||||
from os import path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def numberToWord(numVal, theLanguage):
|
||||
numWord = ""
|
||||
if theLanguage == "en":
|
||||
numWord = _numberToWordEN(numVal)
|
||||
else:
|
||||
numWord = _numberToWordEN(numVal)
|
||||
# print("%4d : %s" % (numVal, numWord))
|
||||
return numWord
|
||||
|
||||
def _numberToWordEN(numVal):
|
||||
|
||||
numWord = ""
|
||||
oneWord = ""
|
||||
tenWord = ""
|
||||
hunWord = ""
|
||||
|
||||
if numVal == 0:
|
||||
return "Zero"
|
||||
|
||||
oneVal = numVal % 10
|
||||
tenVal = (numVal-oneVal) % 100
|
||||
hunVal = (numVal-tenVal-oneVal) % 1000
|
||||
|
||||
if hunVal == 100: hunWord = "One Hundred"
|
||||
if hunVal == 200: hunWord = "Two Hundred"
|
||||
if hunVal == 300: hunWord = "Three Hundred"
|
||||
if hunVal == 400: hunWord = "Four Hundred"
|
||||
if hunVal == 500: hunWord = "Five Hundred"
|
||||
if hunVal == 600: hunWord = "Six Hundred"
|
||||
if hunVal == 700: hunWord = "Seven Hundred"
|
||||
if hunVal == 800: hunWord = "Eight Hundred"
|
||||
if hunVal == 900: hunWord = "Nine Hundred"
|
||||
|
||||
if tenVal == 20: tenWord = "Twenty"
|
||||
if tenVal == 30: tenWord = "Thirty"
|
||||
if tenVal == 40: tenWord = "Forty"
|
||||
if tenVal == 50: tenWord = "Fifty"
|
||||
if tenVal == 60: tenWord = "Sixty"
|
||||
if tenVal == 70: tenWord = "Seventy"
|
||||
if tenVal == 80: tenWord = "Eighty"
|
||||
if tenVal == 90: tenWord = "Ninety"
|
||||
|
||||
if tenVal == 10:
|
||||
if oneVal == 0: oneWord = "Ten"
|
||||
if oneVal == 1: oneWord = "Eleven"
|
||||
if oneVal == 2: oneWord = "Twelve"
|
||||
if oneVal == 3: oneWord = "Thirteen"
|
||||
if oneVal == 4: oneWord = "Fourteen"
|
||||
if oneVal == 5: oneWord = "Fifteen"
|
||||
if oneVal == 6: oneWord = "Sixteen"
|
||||
if oneVal == 7: oneWord = "Seventeen"
|
||||
if oneVal == 8: oneWord = "Eighteen"
|
||||
if oneVal == 9: oneWord = "Nineteen"
|
||||
numWord = ("%s %s" % (hunWord, oneWord)).strip()
|
||||
else:
|
||||
if oneVal == 0: oneWord = ""
|
||||
if oneVal == 1: oneWord = "One"
|
||||
if oneVal == 2: oneWord = "Two"
|
||||
if oneVal == 3: oneWord = "Three"
|
||||
if oneVal == 4: oneWord = "Four"
|
||||
if oneVal == 5: oneWord = "Five"
|
||||
if oneVal == 6: oneWord = "Six"
|
||||
if oneVal == 7: oneWord = "Seven"
|
||||
if oneVal == 8: oneWord = "Eight"
|
||||
if oneVal == 9: oneWord = "Nine"
|
||||
if tenVal == 0:
|
||||
numWord = ("%s %s" % (hunWord, oneWord)).strip()
|
||||
else:
|
||||
if oneVal == 0:
|
||||
numWord = ("%s %s" % (hunWord, tenWord)).strip()
|
||||
else:
|
||||
numWord = ("%s %s-%s" % (hunWord, tenWord, oneWord)).strip()
|
||||
|
||||
return numWord
|
||||
@@ -1,6 +1,4 @@
|
||||
# This is the Title
|
||||
|
||||
## This is the Subtitle
|
||||
### Making a Scene
|
||||
|
||||
% Begin Meta
|
||||
@pov: Jane
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
## So it Begins
|
||||
|
||||
@pov: Jane
|
||||
|
||||
% The first chapter with much action and such.
|
||||
@@ -0,0 +1,5 @@
|
||||
## Where has John Gone?
|
||||
|
||||
@pov: Jane
|
||||
|
||||
% We continue the saga of John and Jane
|
||||
@@ -0,0 +1,23 @@
|
||||
### We Found John!
|
||||
|
||||
@pov: John
|
||||
|
||||
Jane has been searching for a while, and she finally found John on Mars. He was indeed in space! What was he doing on Mars anyway? Well, it turns out, he was farming potatoes.
|
||||
|
||||
Farming potatoes you say? Why would you do that?
|
||||
|
||||
No one knows, but it seemed like a good idea at the time I suppose.
|
||||
|
||||
### A Note on Potato Farming on Mars
|
||||
|
||||
@pov: John
|
||||
|
||||
% We’re adding a second scene to the same file here, which is perfectly fine I may add.
|
||||
|
||||
Potatoes cannot be farmed on Mars. That is simply a fact. There is no soil. Unless John brings the soil himself. Wait, did he?
|
||||
|
||||
I don’t want to know.
|
||||
|
||||
#### Conclusion
|
||||
|
||||
Why would anyone write about such things? let alone make a film about it?
|
||||
@@ -1,6 +1,8 @@
|
||||
# This is a New File!
|
||||
### Another Scene
|
||||
|
||||
@pov: John
|
||||
@location: Space
|
||||
|
||||
Although, not so new now that it has text in it an everything …
|
||||
This is the second scene in out story. We have no idea what’s going on, so we’re just going to ramble on until we have a few lines of text so that the editor has something to work with.
|
||||
|
||||
In fact, this scene is supposed to be about John, but we don’t really know anything about John, except that he is somewhere in space. Perhaps he’s lost? Or has gone where no man has gone before? Where is Jane then?
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<novelWriterXML appVersion="0.2.0" fileVersion="1.0" timeStamp="2019-08-11 19:41:35">
|
||||
<novelWriterXML appVersion="0.2.3" fileVersion="1.0" timeStamp="2019-10-19 13:37:25">
|
||||
<project>
|
||||
<name>Sample Project</name>
|
||||
<title>Sample Project</title>
|
||||
@@ -11,7 +11,7 @@
|
||||
<spellCheck>True</spellCheck>
|
||||
<lastEdited>636b6aa9b697b</lastEdited>
|
||||
<lastViewed>636b6aa9b697b</lastViewed>
|
||||
<lastWordCount>581</lastWordCount>
|
||||
<lastWordCount>758</lastWordCount>
|
||||
<autoReplace>
|
||||
<A>B</A>
|
||||
<B>E</B>
|
||||
@@ -33,7 +33,7 @@
|
||||
<entry blue="175" green="0" red="117">Main</entry>
|
||||
</importance>
|
||||
</settings>
|
||||
<content count="15">
|
||||
<content count="18">
|
||||
<item handle="7031beac91f75" order="0" parent="None">
|
||||
<name>Novel</name>
|
||||
<type>ROOT</type>
|
||||
@@ -51,7 +51,7 @@
|
||||
<charCount>23</charCount>
|
||||
<wordCount>5</wordCount>
|
||||
<paraCount>1</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
<cursorPos>16</cursorPos>
|
||||
</item>
|
||||
<item handle="e7ded148d6e4a" order="1" parent="7031beac91f75">
|
||||
<name>Some Chapter</name>
|
||||
@@ -60,41 +60,77 @@
|
||||
<status>Notes</status>
|
||||
<expanded>True</expanded>
|
||||
</item>
|
||||
<item handle="96b68994dfa3d" order="0" parent="e7ded148d6e4a">
|
||||
<name>New Scene</name>
|
||||
<item handle="6a2d6d5f4f401" order="0" parent="e7ded148d6e4a">
|
||||
<name>Chapter One</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<layout>CHAPTER</layout>
|
||||
<charCount>12</charCount>
|
||||
<wordCount>3</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>75</cursorPos>
|
||||
</item>
|
||||
<item handle="636b6aa9b697b" order="1" parent="e7ded148d6e4a">
|
||||
<name>Making a Scene</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>Notes</status>
|
||||
<expanded>False</expanded>
|
||||
<layout>SCENE</layout>
|
||||
<charCount>713</charCount>
|
||||
<wordCount>132</wordCount>
|
||||
<paraCount>6</paraCount>
|
||||
<cursorPos>85</cursorPos>
|
||||
</item>
|
||||
<item handle="bc0cbd2a407f3" order="2" parent="e7ded148d6e4a">
|
||||
<name>Another Scene</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>Notes</status>
|
||||
<expanded>False</expanded>
|
||||
<layout>SCENE</layout>
|
||||
<charCount>412</charCount>
|
||||
<wordCount>82</wordCount>
|
||||
<paraCount>2</paraCount>
|
||||
<cursorPos>448</cursorPos>
|
||||
</item>
|
||||
<item handle="96b68994dfa3d" order="3" parent="e7ded148d6e4a">
|
||||
<name>A Note on Ipsums</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>Started</status>
|
||||
<expanded>False</expanded>
|
||||
<layout>SCENE</layout>
|
||||
<layout>NOTE</layout>
|
||||
<charCount>2571</charCount>
|
||||
<wordCount>377</wordCount>
|
||||
<paraCount>5</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="636b6aa9b697b" order="1" parent="e7ded148d6e4a">
|
||||
<name>File With Stuff</name>
|
||||
<item handle="88706ddc78b1b" order="4" parent="e7ded148d6e4a">
|
||||
<name>Chapter Two</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>Notes</status>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<layout>SCENE</layout>
|
||||
<charCount>736</charCount>
|
||||
<wordCount>137</wordCount>
|
||||
<paraCount>6</paraCount>
|
||||
<cursorPos>82</cursorPos>
|
||||
<layout>CHAPTER</layout>
|
||||
<charCount>20</charCount>
|
||||
<wordCount>4</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>76</cursorPos>
|
||||
</item>
|
||||
<item handle="bc0cbd2a407f3" order="2" parent="e7ded148d6e4a">
|
||||
<name>New File</name>
|
||||
<item handle="ae7339df26ded" order="5" parent="e7ded148d6e4a">
|
||||
<name>We Found John!</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>Notes</status>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<layout>SCENE</layout>
|
||||
<charCount>82</charCount>
|
||||
<wordCount>19</wordCount>
|
||||
<paraCount>1</paraCount>
|
||||
<cursorPos>69</cursorPos>
|
||||
<charCount>567</charCount>
|
||||
<wordCount>112</wordCount>
|
||||
<paraCount>6</paraCount>
|
||||
<cursorPos>634</cursorPos>
|
||||
</item>
|
||||
<item handle="f6622b4617424" order="1" parent="None">
|
||||
<name>Characters</name>
|
||||
|
||||
Reference in New Issue
Block a user