Merge pull request #366 from vkbo/new_project_wizard
New Project Wizard
This commit is contained in:
+3
-1
@@ -10,7 +10,9 @@ coverage:
|
||||
project:
|
||||
default:
|
||||
threshold: 1%
|
||||
patch: no
|
||||
patch:
|
||||
default:
|
||||
threshold: 1%
|
||||
changes: no
|
||||
|
||||
parsers:
|
||||
|
||||
@@ -15,6 +15,7 @@ novelWriter.qhc
|
||||
__pycache__
|
||||
|
||||
# Sample Project
|
||||
/nw/assets/sample.zip
|
||||
/sample/cache
|
||||
/sample/meta
|
||||
*.bak
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 116 KiB |
@@ -233,3 +233,12 @@ def fuzzyTime(secDiff):
|
||||
return "%d years ago" % int(round(secDiff/31557600))
|
||||
|
||||
return "beyond time and space"
|
||||
|
||||
def makeFileNameSafe(theText):
|
||||
"""Returns a filename safe version of the text.
|
||||
"""
|
||||
cleanName = ""
|
||||
for c in theText.strip():
|
||||
if c.isalpha() or c.isdigit() or c == " ":
|
||||
cleanName += c
|
||||
return cleanName
|
||||
|
||||
+239
-49
@@ -32,7 +32,7 @@ import nw
|
||||
from os import path, mkdir, listdir, unlink, rename, rmdir
|
||||
from lxml import etree
|
||||
from time import time
|
||||
from shutil import make_archive
|
||||
from shutil import make_archive, unpack_archive, copyfile
|
||||
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
@@ -41,9 +41,11 @@ from nw.core.item import NWItem
|
||||
from nw.core.document import NWDoc
|
||||
from nw.core.status import NWStatus
|
||||
from nw.core.options import OptionState
|
||||
from nw.common import checkString, checkBool, checkInt, formatTimeStamp
|
||||
from nw.common import (
|
||||
checkString, checkBool, checkInt, formatTimeStamp, makeFileNameSafe
|
||||
)
|
||||
from nw.constants import (
|
||||
nwFiles, nwItemType, nwItemClass, nwItemLayout, nwAlert
|
||||
nwFiles, nwItemType, nwItemClass, nwItemLayout, nwLabels, nwAlert
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -169,22 +171,6 @@ class NWProject():
|
||||
# Project Methods
|
||||
##
|
||||
|
||||
def newProject(self):
|
||||
"""Create a new project by populating the project tree with a
|
||||
few starter items.
|
||||
"""
|
||||
self.projName = "New Project"
|
||||
hNovel = self.newRoot("Novel", nwItemClass.NOVEL)
|
||||
hChars = self.newRoot("Characters", nwItemClass.CHARACTER)
|
||||
hWorld = self.newRoot("Plot", nwItemClass.PLOT)
|
||||
hWorld = self.newRoot("World", nwItemClass.WORLD)
|
||||
hChapt = self.newFolder("New Chapter", nwItemClass.NOVEL, hNovel)
|
||||
hScene = self.newFile("New Scene", nwItemClass.NOVEL, hChapt)
|
||||
self.projOpened = time()
|
||||
self.setProjectChanged(True)
|
||||
self.saveProject(autoSave=True)
|
||||
return True
|
||||
|
||||
def clearProject(self):
|
||||
"""Clear the data for the current project, and set them to
|
||||
default values.
|
||||
@@ -220,15 +206,15 @@ class NWProject():
|
||||
self.spellCheck = False
|
||||
self.autoOutline = True
|
||||
self.statusItems = NWStatus()
|
||||
self.statusItems.addEntry("New", (100,100,100))
|
||||
self.statusItems.addEntry("Note", (200, 50, 0))
|
||||
self.statusItems.addEntry("Draft", (200,150, 0))
|
||||
self.statusItems.addEntry("Finished",( 50,200, 0))
|
||||
self.statusItems.addEntry("New", (100, 100, 100))
|
||||
self.statusItems.addEntry("Note", (200, 50, 0))
|
||||
self.statusItems.addEntry("Draft", (200, 150, 0))
|
||||
self.statusItems.addEntry("Finished",( 50, 200, 0))
|
||||
self.importItems = NWStatus()
|
||||
self.importItems.addEntry("New", (100,100,100))
|
||||
self.importItems.addEntry("Minor", (200, 50, 0))
|
||||
self.importItems.addEntry("Major", (200,150, 0))
|
||||
self.importItems.addEntry("Main", ( 50,200, 0))
|
||||
self.importItems.addEntry("New", (100, 100, 100))
|
||||
self.importItems.addEntry("Minor", (200, 50, 0))
|
||||
self.importItems.addEntry("Major", (200, 150, 0))
|
||||
self.importItems.addEntry("Main", ( 50, 200, 0))
|
||||
self.lastEdited = None
|
||||
self.lastViewed = None
|
||||
self.lastWCount = 0
|
||||
@@ -238,6 +224,141 @@ class NWProject():
|
||||
|
||||
return
|
||||
|
||||
def newProject(self, projData={}):
|
||||
"""Create a new project by populating the project tree with a
|
||||
few starter items.
|
||||
"""
|
||||
popMinimal = projData.get("popMinimal", True)
|
||||
popCustom = projData.get("popCustom", False)
|
||||
popSample = projData.get("popSample", False)
|
||||
|
||||
# Check if we're extracting the sample project. This is handled
|
||||
# differently as it isn't actually a new project, so we forward
|
||||
# this to another function and return here.
|
||||
if popSample:
|
||||
return self.extractSampleProject(projData)
|
||||
|
||||
# Project Settings
|
||||
projPath = projData.get("projPath", None)
|
||||
projName = projData.get("projName", "New Project")
|
||||
projTitle = projData.get("projTitle", "")
|
||||
projAuthors = projData.get("projAuthors", "")
|
||||
|
||||
if projPath is None:
|
||||
logger.error("No project path set for the new project")
|
||||
return False
|
||||
|
||||
if not self.setProjectPath(projPath, newProject=True):
|
||||
return False
|
||||
|
||||
self.setProjectName(projName)
|
||||
self.setBookTitle(projTitle)
|
||||
self.setBookAuthors(projAuthors)
|
||||
|
||||
titlePage = "# %s\n\n" % (self.bookTitle if self.bookTitle else self.projName)
|
||||
if self.bookAuthors:
|
||||
titlePage = "%sBy %s\n" % (titlePage, ", ".join(self.bookAuthors))
|
||||
|
||||
# Document object for writing files
|
||||
aDoc = NWDoc(self, self.theParent)
|
||||
|
||||
if popMinimal:
|
||||
# Creating a minimal project with a few root folders and a
|
||||
# single chapter folder with a single file.
|
||||
nHandle = self.newRoot("Novel", nwItemClass.NOVEL)
|
||||
xHandle = self.newRoot("Plot", nwItemClass.PLOT)
|
||||
xHandle = self.newRoot("Characters", nwItemClass.CHARACTER)
|
||||
xHandle = self.newRoot("World", nwItemClass.WORLD)
|
||||
tHandle = self.newFile("Title Page", nwItemClass.NOVEL, nHandle)
|
||||
dHandle = self.newFolder("New Chapter", nwItemClass.NOVEL, nHandle)
|
||||
cHandle = self.newFile("New Chapter", nwItemClass.NOVEL, dHandle)
|
||||
sHandle = self.newFile("New Scene", nwItemClass.NOVEL, dHandle)
|
||||
|
||||
self.projTree.setFileItemLayout(tHandle, nwItemLayout.TITLE)
|
||||
self.projTree.setFileItemLayout(cHandle, nwItemLayout.CHAPTER)
|
||||
|
||||
aDoc.openDocument(tHandle, showStatus=False)
|
||||
aDoc.saveDocument(titlePage)
|
||||
aDoc.clearDocument()
|
||||
|
||||
aDoc.openDocument(cHandle, showStatus=False)
|
||||
aDoc.saveDocument("## New Chapter\n\n")
|
||||
aDoc.clearDocument()
|
||||
|
||||
aDoc.openDocument(sHandle, showStatus=False)
|
||||
aDoc.saveDocument("### New Scene\n\n")
|
||||
aDoc.clearDocument()
|
||||
|
||||
elif popCustom:
|
||||
# Create a project structure based on selected root folders
|
||||
# and a number of chapters and scenes selected in the
|
||||
# wizard's custom page.
|
||||
|
||||
# Create root folders
|
||||
nHandle = self.newRoot("Novel", nwItemClass.NOVEL)
|
||||
for newRoot in projData.get("addRoots", []):
|
||||
if newRoot in nwItemClass:
|
||||
self.newRoot(nwLabels.CLASS_NAME[newRoot], newRoot)
|
||||
|
||||
# Create a title page
|
||||
tHandle = self.newFile("Title Page", nwItemClass.NOVEL, nHandle)
|
||||
self.projTree.setFileItemLayout(tHandle, nwItemLayout.TITLE)
|
||||
|
||||
aDoc.openDocument(tHandle, showStatus=False)
|
||||
aDoc.saveDocument(titlePage)
|
||||
aDoc.clearDocument()
|
||||
|
||||
# Create chapters and scenes
|
||||
numChapters = projData.get("numChapters", 0)
|
||||
numScenes = projData.get("numScenes", 0)
|
||||
chFolders = projData.get("chFolders", False)
|
||||
|
||||
# Create chapters
|
||||
if numChapters > 0:
|
||||
for ch in range(numChapters):
|
||||
chTitle = "Chapter %d" % (ch+1)
|
||||
pHandle = nHandle
|
||||
if chFolders:
|
||||
pHandle = self.newFolder(chTitle, nwItemClass.NOVEL, nHandle)
|
||||
|
||||
cHandle = self.newFile(chTitle, nwItemClass.NOVEL, pHandle)
|
||||
self.projTree.setFileItemLayout(cHandle, nwItemLayout.CHAPTER)
|
||||
|
||||
aDoc.openDocument(cHandle, showStatus=False)
|
||||
aDoc.saveDocument("## %s\n\n" % chTitle)
|
||||
aDoc.clearDocument()
|
||||
|
||||
# Create chapter scenes
|
||||
if numScenes > 0:
|
||||
for sc in range(numScenes):
|
||||
scTitle = "Scene %d.%d" % (ch+1, sc+1)
|
||||
sHandle = self.newFile(scTitle, nwItemClass.NOVEL, pHandle)
|
||||
|
||||
aDoc.openDocument(sHandle, showStatus=False)
|
||||
aDoc.saveDocument("### %s\n\n" % scTitle)
|
||||
aDoc.clearDocument()
|
||||
|
||||
# Create scenes (no chapters)
|
||||
elif numScenes > 0:
|
||||
for sc in range(numScenes):
|
||||
scTitle = "Scene %d" % (sc+1)
|
||||
sHandle = self.newFile(scTitle, nwItemClass.NOVEL, nHandle)
|
||||
|
||||
aDoc.openDocument(sHandle, showStatus=False)
|
||||
aDoc.saveDocument("### %s\n\n" % scTitle)
|
||||
aDoc.clearDocument()
|
||||
|
||||
else:
|
||||
# Fallback just in case. We shouldn't reach here.
|
||||
self.newRoot("Novel", nwItemClass.NOVEL)
|
||||
|
||||
# Finalise
|
||||
self.projOpened = time()
|
||||
self.setProjectChanged(True)
|
||||
self.saveProject(autoSave=True)
|
||||
|
||||
return True
|
||||
|
||||
def openProject(self, fileName, overrideLock=False):
|
||||
"""Open the project file provided, or if doesn't exist, assume
|
||||
it is a folder, and look for the file within it. If successful,
|
||||
@@ -557,9 +678,9 @@ class NWProject():
|
||||
if len(aKey) > 0:
|
||||
self._packProjectValue(xTitleFmt, aKey, aValue)
|
||||
|
||||
xStatus = etree.SubElement(xSettings,"status")
|
||||
xStatus = etree.SubElement(xSettings, "status")
|
||||
self.statusItems.packEntries(xStatus)
|
||||
xStatus = etree.SubElement(xSettings,"importance")
|
||||
xStatus = etree.SubElement(xSettings, "importance")
|
||||
self.importItems.packEntries(xStatus)
|
||||
|
||||
# Save Tree Content
|
||||
@@ -574,8 +695,8 @@ class NWProject():
|
||||
with open(tempFile, mode="wb") as outFile:
|
||||
outFile.write(etree.tostring(
|
||||
nwXML,
|
||||
pretty_print = True,
|
||||
encoding = "utf-8",
|
||||
pretty_print = True,
|
||||
encoding = "utf-8",
|
||||
xml_declaration = True
|
||||
))
|
||||
except Exception as e:
|
||||
@@ -635,7 +756,7 @@ class NWProject():
|
||||
return True
|
||||
|
||||
##
|
||||
# Backup Project
|
||||
# Zip/Unzip Project
|
||||
##
|
||||
|
||||
def zipIt(self, doNotify):
|
||||
@@ -665,7 +786,7 @@ class NWProject():
|
||||
), nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
cleanName = self.getFileSafeProjectName()
|
||||
cleanName = makeFileNameSafe(self.projName)
|
||||
baseDir = path.abspath(path.join(self.mainConf.backupPath, cleanName))
|
||||
if not path.isdir(baseDir):
|
||||
try:
|
||||
@@ -711,6 +832,68 @@ class NWProject():
|
||||
|
||||
return True
|
||||
|
||||
def extractSampleProject(self, projData):
|
||||
"""Make a copy of the sample project.
|
||||
First, try to copy the content of the sample folder to the new
|
||||
project path, or if the folder doesn't exist, look for the zip
|
||||
file in the assets folder.
|
||||
"""
|
||||
projName = projData.get("projName", "Sample Project")
|
||||
projPath = projData.get("projPath", None)
|
||||
if projPath is None:
|
||||
logger.error("No project path set for the example project")
|
||||
return False
|
||||
|
||||
srcSample = path.abspath(path.join(self.mainConf.appRoot, "sample"))
|
||||
pkgSample = path.join(self.mainConf.assetPath, "sample.zip")
|
||||
|
||||
isSuccess = False
|
||||
if path.isfile(pkgSample):
|
||||
|
||||
self.setProjectPath(projPath, newProject=True)
|
||||
try:
|
||||
unpack_archive(pkgSample, projPath)
|
||||
isSuccess = True
|
||||
except Exception as e:
|
||||
self.makeAlert(
|
||||
["Failed to create a new example project.", str(e)], nwAlert.ERROR
|
||||
)
|
||||
|
||||
elif path.isdir(srcSample):
|
||||
|
||||
self.setProjectPath(projPath, newProject=True)
|
||||
try:
|
||||
srcProj = path.join(srcSample, nwFiles.PROJ_FILE)
|
||||
dstProj = path.join(projPath, nwFiles.PROJ_FILE)
|
||||
copyfile(srcProj, dstProj)
|
||||
|
||||
srcContent = path.join(srcSample, "content")
|
||||
dstContent = path.join(projPath, "content")
|
||||
for srcFile in listdir(srcContent):
|
||||
srcDoc = path.join(srcContent, srcFile)
|
||||
dstDoc = path.join(dstContent, srcFile)
|
||||
copyfile(srcDoc, dstDoc)
|
||||
|
||||
isSuccess = True
|
||||
|
||||
except Exception as e:
|
||||
self.makeAlert(
|
||||
["Failed to create a new example project.", str(e)], nwAlert.ERROR
|
||||
)
|
||||
|
||||
else:
|
||||
self.makeAlert((
|
||||
"Failed to create a new example project. Could not find the "
|
||||
"necessary files. They seem to be missing from this installation."
|
||||
), nwAlert.ERROR)
|
||||
|
||||
if isSuccess:
|
||||
self.clearProject()
|
||||
self.theParent.openProject(projPath)
|
||||
self.theParent.rebuildIndex()
|
||||
|
||||
return isSuccess
|
||||
|
||||
##
|
||||
# Setters
|
||||
##
|
||||
@@ -726,13 +909,24 @@ class NWProject():
|
||||
projPath = path.expanduser(projPath)
|
||||
self.projPath = path.abspath(projPath)
|
||||
|
||||
if newProject and self.mainConf.showGUI:
|
||||
if listdir(self.projPath):
|
||||
self.theParent.makeAlert((
|
||||
"New project folder is not empty. "
|
||||
"Each project requires a dedicated project folder."
|
||||
), nwAlert.ERROR)
|
||||
return False
|
||||
if newProject:
|
||||
if not path.isdir(projPath):
|
||||
try:
|
||||
mkdir(projPath)
|
||||
logger.debug("Created folder %s" % projPath)
|
||||
except Exception as e:
|
||||
self.theParent.makeAlert((
|
||||
["Could not create new project folder.", str(e)]
|
||||
), nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
if path.isdir(projPath):
|
||||
if self.mainConf.showGUI and listdir(self.projPath):
|
||||
self.theParent.makeAlert((
|
||||
"New project folder is not empty. "
|
||||
"Each project requires a dedicated project folder."
|
||||
), nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
self.ensureFolderStructure()
|
||||
self.setProjectChanged(True)
|
||||
@@ -757,13 +951,18 @@ class NWProject():
|
||||
def setBookAuthors(self, bookAuthors):
|
||||
"""A line separated list of book authors, parsed into an array.
|
||||
"""
|
||||
if not isinstance(bookAuthors, str):
|
||||
return False
|
||||
|
||||
self.bookAuthors = []
|
||||
for bookAuthor in bookAuthors.split("\n"):
|
||||
bookAuthor = bookAuthor.strip()
|
||||
if bookAuthor == "":
|
||||
continue
|
||||
self.bookAuthors.append(bookAuthor)
|
||||
|
||||
self.setProjectChanged(True)
|
||||
|
||||
return True
|
||||
|
||||
def setProjBackup(self, doBackup):
|
||||
@@ -889,15 +1088,6 @@ 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.
|
||||
"""
|
||||
|
||||
@@ -314,6 +314,22 @@ class NWTree():
|
||||
self._handleSeed = theSeed
|
||||
return
|
||||
|
||||
def setFileItemLayout(self, tHandle, itemLayout):
|
||||
"""Set the nwItemLayout for a specific file.
|
||||
"""
|
||||
tItem = self.__getitem__(tHandle)
|
||||
if tItem is None:
|
||||
return False
|
||||
if tItem.itemType != nwItemType.FILE:
|
||||
logger.error("Item '%s' is not a file" % tHandle)
|
||||
return False
|
||||
if not isinstance(itemLayout, nwItemLayout):
|
||||
return False
|
||||
|
||||
tItem.setLayout(itemLayout)
|
||||
|
||||
return True
|
||||
|
||||
##
|
||||
# Getters
|
||||
##
|
||||
|
||||
@@ -15,6 +15,7 @@ from nw.gui.preferences import GuiPreferences
|
||||
from nw.gui.projload import GuiProjectLoad
|
||||
from nw.gui.projsettings import GuiProjectSettings
|
||||
from nw.gui.projtree import GuiProjectTree
|
||||
from nw.gui.projwizard import GuiProjectWizard
|
||||
from nw.gui.writingstats import GuiWritingStats
|
||||
from nw.gui.statusbar import GuiMainStatus
|
||||
from nw.gui.theme import GuiIcons, GuiTheme
|
||||
@@ -36,6 +37,7 @@ __all__ = [
|
||||
"GuiProjectLoad",
|
||||
"GuiProjectSettings",
|
||||
"GuiProjectTree",
|
||||
"GuiProjectWizard",
|
||||
"GuiWritingStats",
|
||||
"GuiMainStatus",
|
||||
"GuiIcons",
|
||||
|
||||
+4
-3
@@ -58,15 +58,16 @@ class GuiAbout(QDialog):
|
||||
self.setMinimumWidth(self.mainConf.pxInt(650))
|
||||
self.setMinimumHeight(self.mainConf.pxInt(600))
|
||||
|
||||
iPx = self.mainConf.pxInt(96)
|
||||
self.guiDeco = self.theParent.theTheme.loadDecoration("nwicon", (iPx, iPx))
|
||||
nPx = self.mainConf.pxInt(96)
|
||||
self.nwIcon = QLabel()
|
||||
self.nwIcon.setPixmap(self.theParent.theTheme.getPixmap("novelwriter", (nPx, nPx)))
|
||||
self.lblName = QLabel("<b>%s</b>" % self.mainConf.appName)
|
||||
self.lblVers = QLabel("v%s" % nw.__version__)
|
||||
self.lblDate = QLabel(datetime.strptime(nw.__date__, "%Y-%m-%d").strftime("%x"))
|
||||
|
||||
self.leftBox = QVBoxLayout()
|
||||
self.leftBox.setSpacing(self.mainConf.pxInt(4))
|
||||
self.leftBox.addWidget(self.guiDeco, 0, Qt.AlignCenter)
|
||||
self.leftBox.addWidget(self.nwIcon, 0, Qt.AlignCenter)
|
||||
self.leftBox.addWidget(self.lblName, 0, Qt.AlignCenter)
|
||||
self.leftBox.addWidget(self.lblVers, 0, Qt.AlignCenter)
|
||||
self.leftBox.addWidget(self.lblDate, 0, Qt.AlignCenter)
|
||||
|
||||
+2
-2
@@ -44,7 +44,7 @@ from PyQt5.QtWidgets import (
|
||||
QFileDialog, QFontDialog, QSpinBox
|
||||
)
|
||||
|
||||
from nw.common import fuzzyTime
|
||||
from nw.common import fuzzyTime, makeFileNameSafe
|
||||
from nw.gui.custom import QSwitch
|
||||
from nw.core import ToHtml
|
||||
from nw.constants import (
|
||||
@@ -622,7 +622,7 @@ class GuiBuildNovel(QDialog):
|
||||
# Generate the file name
|
||||
if fileExt:
|
||||
|
||||
cleanName = self.theProject.getFileSafeProjectName()
|
||||
cleanName = makeFileNameSafe(self.theProject.projName)
|
||||
fileName = "%s.%s" % (cleanName, fileExt)
|
||||
saveDir = self.mainConf.lastPath
|
||||
savePath = path.join(saveDir, fileName)
|
||||
|
||||
+4
-4
@@ -79,8 +79,9 @@ class GuiProjectLoad(QDialog):
|
||||
self.setMinimumHeight(self.mainConf.pxInt(400))
|
||||
self.setModal(True)
|
||||
|
||||
self.guiDeco = self.theTheme.loadDecoration("nwicon", (nPx, nPx))
|
||||
self.innerBox.addWidget(self.guiDeco, 0, Qt.AlignTop)
|
||||
self.nwIcon = QLabel()
|
||||
self.nwIcon.setPixmap(self.theParent.theTheme.getPixmap("novelwriter", (nPx, nPx)))
|
||||
self.innerBox.addWidget(self.nwIcon, 0, Qt.AlignTop)
|
||||
|
||||
self.projectForm = QGridLayout()
|
||||
self.projectForm.setContentsMargins(0, 0, 0, 0)
|
||||
@@ -173,8 +174,7 @@ class GuiProjectLoad(QDialog):
|
||||
return
|
||||
|
||||
def _doBrowse(self):
|
||||
"""Close the dialog window with no selected path, triggering the
|
||||
project browser dialog.
|
||||
"""Browse for a folder path.
|
||||
"""
|
||||
logger.verbose("GuiProjectLoad browse button clicked")
|
||||
if self.mainConf.showGUI:
|
||||
|
||||
+1
-1
@@ -192,7 +192,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
nHandle = pHandle
|
||||
pHandle = pItem.parHandle
|
||||
|
||||
# If we again has no home, give up
|
||||
# If we again have no home, give up
|
||||
if pHandle is None:
|
||||
self.makeAlert(
|
||||
"Did not find anywhere to add the file or folder!", nwAlert.ERROR
|
||||
|
||||
@@ -0,0 +1,418 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter GUI New Project Wizard
|
||||
|
||||
novelWriter – GUI New project Wizard
|
||||
======================================
|
||||
Class holding the new project wizard dialog
|
||||
|
||||
File History:
|
||||
Created: 2020-07-11 [0.10.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 <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import nw
|
||||
|
||||
from os import path
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import (
|
||||
QWizard, QWizardPage, QLabel, QVBoxLayout, QLineEdit, QPlainTextEdit,
|
||||
QPushButton, QFileDialog, QHBoxLayout, QRadioButton, QFormLayout,
|
||||
QGroupBox, QGridLayout, QSpinBox
|
||||
)
|
||||
|
||||
from nw.common import makeFileNameSafe
|
||||
from nw.constants import nwLabels, nwItemClass
|
||||
from nw.gui.custom import QSwitch
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PAGE_INTRO = 0
|
||||
PAGE_STORE = 1
|
||||
PAGE_POP = 2
|
||||
PAGE_CUSTOM = 3
|
||||
PAGE_FINAL = 4
|
||||
|
||||
class GuiProjectWizard(QWizard):
|
||||
|
||||
def __init__(self, theParent):
|
||||
QWizard.__init__(self, theParent)
|
||||
|
||||
logger.debug("Initialising GuiProjectWizard ...")
|
||||
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theParent = theParent
|
||||
self.theTheme = theParent.theTheme
|
||||
|
||||
self.sideImage = self.theTheme.loadDecoration(
|
||||
"wiz-back", None, self.mainConf.pxInt(370)
|
||||
)
|
||||
self.setWizardStyle(QWizard.ModernStyle)
|
||||
self.setPixmap(QWizard.WatermarkPixmap, self.sideImage)
|
||||
|
||||
self.introPage = ProjWizardIntroPage(self)
|
||||
self.storagePage = ProjWizardFolderPage(self)
|
||||
self.popPage = ProjWizardPopulatePage(self)
|
||||
self.customPage = ProjWizardCustomPage(self)
|
||||
self.finalPage = ProjWizardFinalPage(self)
|
||||
|
||||
self.setPage(PAGE_INTRO, self.introPage)
|
||||
self.setPage(PAGE_STORE, self.storagePage)
|
||||
self.setPage(PAGE_POP, self.popPage)
|
||||
self.setPage(PAGE_CUSTOM, self.customPage)
|
||||
self.setPage(PAGE_FINAL, self.finalPage)
|
||||
|
||||
self.setOption(QWizard.NoBackButtonOnStartPage, True)
|
||||
|
||||
logger.debug("GuiProjectWizard initialisation complete")
|
||||
|
||||
return
|
||||
|
||||
# END Class GuiProjectWizard
|
||||
|
||||
class ProjWizardIntroPage(QWizardPage):
|
||||
|
||||
def __init__(self, theWizard):
|
||||
QWizardPage.__init__(self)
|
||||
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theWizard = theWizard
|
||||
self.theTheme = theWizard.theTheme
|
||||
|
||||
self.setTitle("Create New Project")
|
||||
self.theText = QLabel(
|
||||
"Provide at least a working title. The working title should not "
|
||||
"be change beyond this point as it is used by the application for "
|
||||
"generating file names for for instance backups. The other fields "
|
||||
"are optional and can be changed at any time in Project Settings."
|
||||
)
|
||||
self.theText.setWordWrap(True)
|
||||
|
||||
self.imgCredit = QLabel("Side image by Peter Mitterhofer, CC BY-SA 4.0")
|
||||
lblFont = self.imgCredit.font()
|
||||
lblFont.setPointSizeF(0.6*self.theTheme.fontPointSize)
|
||||
self.imgCredit.setFont(lblFont)
|
||||
|
||||
xW = self.mainConf.pxInt(300)
|
||||
xH = self.mainConf.pxInt(100)
|
||||
vS = self.mainConf.pxInt(12)
|
||||
fS = self.mainConf.pxInt(4)
|
||||
|
||||
# The Page Form
|
||||
self.projName = QLineEdit()
|
||||
self.projName.setMaxLength(200)
|
||||
self.projName.setFixedWidth(xW)
|
||||
self.projName.setPlaceholderText("Required")
|
||||
|
||||
self.projTitle = QLineEdit()
|
||||
self.projTitle.setMaxLength(200)
|
||||
self.projTitle.setFixedWidth(xW)
|
||||
self.projTitle.setPlaceholderText("Optional")
|
||||
|
||||
self.projAuthors = QPlainTextEdit()
|
||||
self.projAuthors.setFixedHeight(xH)
|
||||
self.projAuthors.setFixedWidth(xW)
|
||||
self.projAuthors.setPlaceholderText("Optional. One name per line.")
|
||||
|
||||
self.mainForm = QFormLayout()
|
||||
self.mainForm.addRow("Working Title", self.projName)
|
||||
self.mainForm.addRow("Novel Title", self.projTitle)
|
||||
self.mainForm.addRow("Author(s)", self.projAuthors)
|
||||
self.mainForm.setVerticalSpacing(fS)
|
||||
|
||||
self.registerField("projName*", self.projName)
|
||||
self.registerField("projTitle", self.projTitle)
|
||||
self.registerField("projAuthors", self.projAuthors, "plainText")
|
||||
|
||||
# Assemble
|
||||
self.outerBox = QVBoxLayout()
|
||||
self.outerBox.setSpacing(vS)
|
||||
self.outerBox.addWidget(self.theText)
|
||||
self.outerBox.addLayout(self.mainForm)
|
||||
self.outerBox.addStretch(1)
|
||||
self.outerBox.addWidget(self.imgCredit)
|
||||
self.setLayout(self.outerBox)
|
||||
|
||||
return
|
||||
|
||||
# END Class ProjWizardIntroPage
|
||||
|
||||
class ProjWizardFolderPage(QWizardPage):
|
||||
|
||||
def __init__(self, theWizard):
|
||||
QWizardPage.__init__(self)
|
||||
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theWizard = theWizard
|
||||
self.theTheme = theWizard.theTheme
|
||||
|
||||
self.setTitle("Select Project Folder")
|
||||
self.theText = QLabel(
|
||||
"Select a location to store the project. A new project folder "
|
||||
"will be created in the selected location."
|
||||
)
|
||||
self.theText.setWordWrap(True)
|
||||
|
||||
xW = self.mainConf.pxInt(300)
|
||||
vS = self.mainConf.pxInt(12)
|
||||
fS = self.mainConf.pxInt(8)
|
||||
|
||||
self.projPath = QLineEdit("")
|
||||
self.projPath.setFixedWidth(xW)
|
||||
self.projPath.setPlaceholderText("Required")
|
||||
|
||||
self.browseButton = QPushButton("...")
|
||||
self.browseButton.setMaximumWidth(int(2.5*self.theTheme.getTextWidth("...")))
|
||||
self.browseButton.clicked.connect(self._doBrowse)
|
||||
|
||||
self.mainForm = QHBoxLayout()
|
||||
self.mainForm.addWidget(QLabel("Project Path"), 0)
|
||||
self.mainForm.addWidget(self.projPath, 1)
|
||||
self.mainForm.addWidget(self.browseButton, 0)
|
||||
self.mainForm.setSpacing(fS)
|
||||
|
||||
self.registerField("projPath*", self.projPath)
|
||||
|
||||
# Assemble
|
||||
self.outerBox = QVBoxLayout()
|
||||
self.outerBox.setSpacing(vS)
|
||||
self.outerBox.addWidget(self.theText)
|
||||
self.outerBox.addLayout(self.mainForm)
|
||||
self.outerBox.addStretch(1)
|
||||
self.setLayout(self.outerBox)
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Slots
|
||||
##
|
||||
|
||||
def _doBrowse(self):
|
||||
"""Select a project folder.
|
||||
"""
|
||||
lastPath = self.mainConf.lastPath
|
||||
if not path.isdir(lastPath):
|
||||
lastPath = ""
|
||||
|
||||
dlgOpt = QFileDialog.Options()
|
||||
dlgOpt |= QFileDialog.ShowDirsOnly
|
||||
dlgOpt |= QFileDialog.DontUseNativeDialog
|
||||
projDir = QFileDialog.getExistingDirectory(
|
||||
self,"Select Project Folder", lastPath, options=dlgOpt
|
||||
)
|
||||
if projDir:
|
||||
projName = self.field("projName")
|
||||
if projName is not None:
|
||||
fullDir = path.join(path.abspath(projDir), makeFileNameSafe(projName))
|
||||
self.projPath.setText(fullDir)
|
||||
else:
|
||||
self.projPath.setText("")
|
||||
|
||||
return
|
||||
|
||||
# END Class ProjWizardFolderPage
|
||||
|
||||
class ProjWizardPopulatePage(QWizardPage):
|
||||
|
||||
def __init__(self, theWizard):
|
||||
QWizardPage.__init__(self)
|
||||
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theWizard = theWizard
|
||||
|
||||
self.setTitle("Populate Project")
|
||||
self.theText = QLabel(
|
||||
"Choose how to pre-fill the project. Either with a minimal set of "
|
||||
"starter items, an example project explaining and showing many of "
|
||||
"the features, or show further custom options on the next page."
|
||||
)
|
||||
self.theText.setWordWrap(True)
|
||||
|
||||
vS = self.mainConf.pxInt(12)
|
||||
fS = self.mainConf.pxInt(4)
|
||||
|
||||
self.popMinimal = QRadioButton("Fill the project with a minimal set of items")
|
||||
self.popSample = QRadioButton("Fill the project with example files")
|
||||
self.popCustom = QRadioButton("Show detailed options for filling the project")
|
||||
self.popMinimal.setChecked(True)
|
||||
|
||||
self.popBox = QVBoxLayout()
|
||||
self.popBox.setSpacing(fS)
|
||||
self.popBox.addWidget(self.popMinimal)
|
||||
self.popBox.addWidget(self.popSample)
|
||||
self.popBox.addWidget(self.popCustom)
|
||||
|
||||
self.registerField("popMinimal", self.popMinimal)
|
||||
self.registerField("popSample", self.popSample)
|
||||
self.registerField("popCustom", self.popCustom)
|
||||
|
||||
# Assemble
|
||||
self.outerBox = QVBoxLayout()
|
||||
self.outerBox.setSpacing(vS)
|
||||
self.outerBox.addWidget(self.theText)
|
||||
self.outerBox.addLayout(self.popBox)
|
||||
self.outerBox.addStretch(1)
|
||||
self.setLayout(self.outerBox)
|
||||
|
||||
return
|
||||
|
||||
def nextId(self):
|
||||
"""Overload the nextID function to skip further pages if custom
|
||||
is not selected.
|
||||
"""
|
||||
if self.popCustom.isChecked():
|
||||
return PAGE_CUSTOM
|
||||
else:
|
||||
return PAGE_FINAL
|
||||
|
||||
# END Class ProjWizardPopulatePage
|
||||
|
||||
class ProjWizardCustomPage(QWizardPage):
|
||||
|
||||
def __init__(self, theWizard):
|
||||
QWizardPage.__init__(self)
|
||||
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theWizard = theWizard
|
||||
|
||||
self.setTitle("Custom Project Options")
|
||||
self.theText = QLabel(
|
||||
"Select which additional root folders to make, and how to populate "
|
||||
"the Novel folder. If you don't want to add chapters or scenes, set "
|
||||
"the values to 0. You can add scenes without chapters."
|
||||
)
|
||||
self.theText.setWordWrap(True)
|
||||
|
||||
vS = self.mainConf.pxInt(12)
|
||||
|
||||
# Root Folders
|
||||
self.rootGroup = QGroupBox("Additional Root Folders")
|
||||
self.rootForm = QGridLayout()
|
||||
self.rootGroup.setLayout(self.rootForm)
|
||||
|
||||
self.lblPlot = QLabel("%s folder" % nwLabels.CLASS_NAME[nwItemClass.PLOT])
|
||||
self.lblChar = QLabel("%s folder" % nwLabels.CLASS_NAME[nwItemClass.CHARACTER])
|
||||
self.lblWorld = QLabel("%s folder" % nwLabels.CLASS_NAME[nwItemClass.WORLD])
|
||||
self.lblTime = QLabel("%s folder" % nwLabels.CLASS_NAME[nwItemClass.TIMELINE])
|
||||
self.lblObject = QLabel("%s folder" % nwLabels.CLASS_NAME[nwItemClass.OBJECT])
|
||||
self.lblEntity = QLabel("%s folder" % nwLabels.CLASS_NAME[nwItemClass.ENTITY])
|
||||
|
||||
self.addPlot = QSwitch()
|
||||
self.addChar = QSwitch()
|
||||
self.addWorld = QSwitch()
|
||||
self.addTime = QSwitch()
|
||||
self.addObject = QSwitch()
|
||||
self.addEntity = QSwitch()
|
||||
|
||||
self.addPlot.setChecked(True)
|
||||
self.addChar.setChecked(True)
|
||||
self.addWorld.setChecked(True)
|
||||
|
||||
self.rootForm.addWidget(self.lblPlot, 0, 0)
|
||||
self.rootForm.addWidget(self.lblChar, 1, 0)
|
||||
self.rootForm.addWidget(self.lblWorld, 2, 0)
|
||||
self.rootForm.addWidget(self.lblTime, 3, 0)
|
||||
self.rootForm.addWidget(self.lblObject, 4, 0)
|
||||
self.rootForm.addWidget(self.lblEntity, 5, 0)
|
||||
self.rootForm.addWidget(self.addPlot, 0, 1, 1, 1, Qt.AlignRight)
|
||||
self.rootForm.addWidget(self.addChar, 1, 1, 1, 1, Qt.AlignRight)
|
||||
self.rootForm.addWidget(self.addWorld, 2, 1, 1, 1, Qt.AlignRight)
|
||||
self.rootForm.addWidget(self.addTime, 3, 1, 1, 1, Qt.AlignRight)
|
||||
self.rootForm.addWidget(self.addObject, 4, 1, 1, 1, Qt.AlignRight)
|
||||
self.rootForm.addWidget(self.addEntity, 5, 1, 1, 1, Qt.AlignRight)
|
||||
self.rootForm.setRowStretch(6, 1)
|
||||
|
||||
# Novel Options
|
||||
self.novelGroup = QGroupBox("Populate Novel Folder")
|
||||
self.novelForm = QGridLayout()
|
||||
self.novelGroup.setLayout(self.novelForm)
|
||||
|
||||
self.numChapters = QSpinBox()
|
||||
self.numChapters.setRange(0, 100)
|
||||
self.numChapters.setValue(5)
|
||||
|
||||
self.numScenes = QSpinBox()
|
||||
self.numScenes.setRange(0, 200)
|
||||
self.numScenes.setValue(5)
|
||||
|
||||
self.chFolders = QSwitch()
|
||||
self.chFolders.setChecked(True)
|
||||
|
||||
self.novelForm.addWidget(QLabel("Add chapters"), 0, 0)
|
||||
self.novelForm.addWidget(QLabel("Scenes (per chapter)"), 1, 0)
|
||||
self.novelForm.addWidget(QLabel("Add chapter folders"), 2, 0)
|
||||
self.novelForm.addWidget(self.numChapters, 0, 1, 1, 1, Qt.AlignRight)
|
||||
self.novelForm.addWidget(self.numScenes, 1, 1, 1, 1, Qt.AlignRight)
|
||||
self.novelForm.addWidget(self.chFolders, 2, 1, 1, 1, Qt.AlignRight)
|
||||
self.novelForm.setRowStretch(3, 1)
|
||||
|
||||
# Wizard Fields
|
||||
self.registerField("addPlot", self.addPlot)
|
||||
self.registerField("addChar", self.addChar)
|
||||
self.registerField("addWorld", self.addWorld)
|
||||
self.registerField("addTime", self.addTime)
|
||||
self.registerField("addObject", self.addObject)
|
||||
self.registerField("addEntity", self.addEntity)
|
||||
self.registerField("numChapters", self.numChapters)
|
||||
self.registerField("numScenes", self.numScenes)
|
||||
self.registerField("chFolders", self.chFolders)
|
||||
|
||||
# Assemble
|
||||
self.innerBox = QHBoxLayout()
|
||||
self.innerBox.addWidget(self.rootGroup)
|
||||
self.innerBox.addWidget(self.novelGroup)
|
||||
|
||||
self.outerBox = QVBoxLayout()
|
||||
self.outerBox.setSpacing(vS)
|
||||
self.outerBox.addWidget(self.theText)
|
||||
self.outerBox.addLayout(self.innerBox)
|
||||
self.outerBox.addStretch(1)
|
||||
self.setLayout(self.outerBox)
|
||||
|
||||
return
|
||||
|
||||
# END Class ProjWizardCustomPage
|
||||
|
||||
class ProjWizardFinalPage(QWizardPage):
|
||||
|
||||
def __init__(self, theWizard):
|
||||
QWizardPage.__init__(self)
|
||||
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theWizard = theWizard
|
||||
|
||||
self.setTitle("Finished")
|
||||
self.theText = QLabel((
|
||||
"<p>All done.</p>"
|
||||
"<p>Press '{finish}' to create the new project.</p>"
|
||||
).format(
|
||||
finish = "Done" if self.mainConf.osDarwin else "Finish"
|
||||
))
|
||||
self.theText.setWordWrap(True)
|
||||
|
||||
# Assemble
|
||||
self.outerBox = QVBoxLayout()
|
||||
self.outerBox.setSpacing(self.mainConf.pxInt(12))
|
||||
self.outerBox.addWidget(self.theText)
|
||||
self.outerBox.addStretch(1)
|
||||
self.setLayout(self.outerBox)
|
||||
|
||||
return
|
||||
|
||||
# END Class ProjWizardFinalPage
|
||||
+22
-13
@@ -33,7 +33,7 @@ import nw
|
||||
from os import path, listdir
|
||||
from math import ceil
|
||||
|
||||
from PyQt5.QtCore import QSize
|
||||
from PyQt5.QtCore import Qt, QSize
|
||||
from PyQt5.QtSvg import QSvgWidget
|
||||
from PyQt5.QtWidgets import QStyle, qApp
|
||||
from PyQt5.QtGui import (
|
||||
@@ -501,6 +501,7 @@ class GuiIcons:
|
||||
|
||||
ICON_MAP = {
|
||||
# Project and GUI icons
|
||||
"novelwriter" : (None, None),
|
||||
"cls_none" : (QStyle.SP_DriveHDIcon, "drive-harddisk"),
|
||||
"cls_novel" : (QStyle.SP_DriveHDIcon, "drive-harddisk"),
|
||||
"cls_plot" : (QStyle.SP_DriveHDIcon, "drive-harddisk"),
|
||||
@@ -556,7 +557,7 @@ class GuiIcons:
|
||||
}
|
||||
|
||||
DECO_MAP = {
|
||||
"nwicon" : ["icons", "novelwriter.svg"],
|
||||
"wiz-back" : "wizard-back.jpg",
|
||||
}
|
||||
|
||||
def __init__(self, theParent):
|
||||
@@ -649,7 +650,7 @@ class GuiIcons:
|
||||
# Access Functions
|
||||
##
|
||||
|
||||
def loadDecoration(self, decoKey, decoSize=None):
|
||||
def loadDecoration(self, decoKey, pxW, pxH):
|
||||
"""Load graphical decoration element based on the decoration
|
||||
map. This function always returns a QSwgWidget.
|
||||
"""
|
||||
@@ -657,20 +658,22 @@ class GuiIcons:
|
||||
logger.error("Decoration with name '%s' does not exist" % decoKey)
|
||||
return QSvgWidget()
|
||||
|
||||
svgPath = path.join(
|
||||
self.mainConf.assetPath,
|
||||
self.DECO_MAP[decoKey][0],
|
||||
self.DECO_MAP[decoKey][1]
|
||||
imgPath = path.join(
|
||||
self.mainConf.assetPath, "images", self.DECO_MAP[decoKey]
|
||||
)
|
||||
if not path.isfile(svgPath):
|
||||
if not path.isfile(imgPath):
|
||||
logger.error("Decoration file '%s' not in assets folder" % self.DECO_MAP[decoKey])
|
||||
return QSvgWidget()
|
||||
return QPixmap()
|
||||
|
||||
svgDeco = QSvgWidget(svgPath)
|
||||
if decoSize is not None:
|
||||
svgDeco.setFixedSize(QSize(decoSize[0],decoSize[1]))
|
||||
theDeco = QPixmap(imgPath)
|
||||
if pxW is not None and pxH is not None:
|
||||
return theDeco.scaled(pxW, pxH, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
|
||||
elif pxW is None and pxH is not None:
|
||||
return theDeco.scaledToHeight(pxH, Qt.SmoothTransformation)
|
||||
elif pxW is not None and pxH is None:
|
||||
return theDeco.scaledToWidth(pxW, Qt.SmoothTransformation)
|
||||
|
||||
return svgDeco
|
||||
return theDeco
|
||||
|
||||
def getIcon(self, iconKey, iconSize=None):
|
||||
"""Return an icon from the icon buffer. If it doesn't exist,
|
||||
@@ -738,6 +741,12 @@ class GuiIcons:
|
||||
logger.error("Requested unknown icon name '%s'" % iconKey)
|
||||
return QIcon()
|
||||
|
||||
# If we just want the app icon, return it right away
|
||||
if iconKey == "novelwriter":
|
||||
return QIcon(path.join(self.mainConf.iconPath, "novelwriter.svg"))
|
||||
|
||||
# Otherwise, we start looking for it
|
||||
# First in the theme folder
|
||||
if iconKey in self.themeMap:
|
||||
logger.verbose("Loading: %s" % path.relpath(self.themeMap[iconKey]))
|
||||
return QIcon(self.themeMap[iconKey])
|
||||
|
||||
+64
-19
@@ -43,10 +43,11 @@ from nw.gui import (
|
||||
GuiBuildNovel, GuiDocEditor, GuiDocMerge, GuiDocSplit, GuiDocViewDetails,
|
||||
GuiDocViewer, GuiItemDetails, GuiItemEditor, GuiMainMenu, GuiMainStatus,
|
||||
GuiOutline, GuiOutlineDetails, GuiPreferences, GuiProjectLoad, GuiTheme,
|
||||
GuiProjectSettings, GuiProjectTree, GuiWritingStats, GuiAbout
|
||||
GuiProjectSettings, GuiProjectTree, GuiWritingStats, GuiProjectWizard,
|
||||
GuiAbout
|
||||
)
|
||||
from nw.core import NWProject, NWDoc, NWIndex
|
||||
from nw.constants import nwItemType, nwAlert
|
||||
from nw.constants import nwItemType, nwItemClass, nwAlert
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -262,8 +263,9 @@ class GuiMain(QMainWindow):
|
||||
|
||||
return True
|
||||
|
||||
def newProject(self, projPath=None, forceNew=False):
|
||||
def newProject(self, projData=None, forceNew=False):
|
||||
"""Create new project with a few default files and folders.
|
||||
The variable forceNew is used for testing.
|
||||
"""
|
||||
if self.hasProject:
|
||||
msgBox = QMessageBox()
|
||||
@@ -273,9 +275,15 @@ class GuiMain(QMainWindow):
|
||||
)
|
||||
return False
|
||||
|
||||
if projPath is None:
|
||||
projPath = self.newProjectDialog()
|
||||
if projPath is None:
|
||||
if projData is None and self.mainConf.showGUI:
|
||||
projData = self.newProjectDialog()
|
||||
|
||||
if projData is None:
|
||||
return False
|
||||
|
||||
projPath = projData.get("projPath", None)
|
||||
if projPath is None or projData is None:
|
||||
logger.error("No projData or projPath set")
|
||||
return False
|
||||
|
||||
if path.isfile(path.join(projPath,self.theProject.projFile)) and not forceNew:
|
||||
@@ -287,13 +295,14 @@ class GuiMain(QMainWindow):
|
||||
return False
|
||||
|
||||
logger.info("Creating new project")
|
||||
if self.theProject.setProjectPath(projPath, newProject=True):
|
||||
self.theProject.newProject()
|
||||
if self.theProject.newProject(projData):
|
||||
self.rebuildTree()
|
||||
self.saveProject()
|
||||
self.hasProject = True
|
||||
self.statusBar.setRefTime(self.theProject.projOpened)
|
||||
self.rebuildIndex(beQuiet=True)
|
||||
else:
|
||||
self.theProject.clearProject()
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -692,7 +701,7 @@ class GuiMain(QMainWindow):
|
||||
self.treeView.buildTree()
|
||||
return
|
||||
|
||||
def rebuildIndex(self):
|
||||
def rebuildIndex(self, beQuiet=False):
|
||||
"""Rebuild the entire index.
|
||||
"""
|
||||
if not self.hasProject:
|
||||
@@ -735,7 +744,7 @@ class GuiMain(QMainWindow):
|
||||
|
||||
qApp.restoreOverrideCursor()
|
||||
|
||||
if self.mainConf.showGUI:
|
||||
if self.mainConf.showGUI and not beQuiet:
|
||||
self.makeAlert("The project index has been successfully rebuilt.", nwAlert.INFO)
|
||||
|
||||
return True
|
||||
@@ -766,16 +775,14 @@ class GuiMain(QMainWindow):
|
||||
return None
|
||||
|
||||
def newProjectDialog(self):
|
||||
"""Select where to save new project.
|
||||
"""Open the wizard and assemble the project options dict.
|
||||
"""
|
||||
dlgOpt = QFileDialog.Options()
|
||||
dlgOpt |= QFileDialog.ShowDirsOnly
|
||||
dlgOpt |= QFileDialog.DontUseNativeDialog
|
||||
projPath = QFileDialog.getExistingDirectory(
|
||||
self, "Select Location for New novelWriter Project", "", options=dlgOpt
|
||||
)
|
||||
if projPath:
|
||||
return projPath
|
||||
newProj = GuiProjectWizard(self)
|
||||
newProj.exec_()
|
||||
|
||||
if newProj.result() == QDialog.Accepted:
|
||||
return self._assembleProjectWizardData(newProj)
|
||||
|
||||
return None
|
||||
|
||||
def editConfigDialog(self):
|
||||
@@ -1117,6 +1124,44 @@ class GuiMain(QMainWindow):
|
||||
self.importIcons[sLabel] = QIcon(theIcon)
|
||||
return
|
||||
|
||||
def _assembleProjectWizardData(self, newProj):
|
||||
"""Extract the user choices from the New Project Wizard and
|
||||
store them in a dictionary.
|
||||
"""
|
||||
projData = {
|
||||
"projName": newProj.field("projName"),
|
||||
"projTitle": newProj.field("projTitle"),
|
||||
"projAuthors": newProj.field("projAuthors"),
|
||||
"projPath": newProj.field("projPath"),
|
||||
"popSample": newProj.field("popSample"),
|
||||
"popMinimal": newProj.field("popMinimal"),
|
||||
"popCustom": newProj.field("popCustom"),
|
||||
"addRoots": [],
|
||||
"numChapters": 0,
|
||||
"numScenes": 0,
|
||||
"chFolders": False,
|
||||
}
|
||||
if newProj.field("popCustom"):
|
||||
addRoots = []
|
||||
if newProj.field("addPlot"):
|
||||
addRoots.append(nwItemClass.PLOT)
|
||||
if newProj.field("addChar"):
|
||||
addRoots.append(nwItemClass.CHARACTER)
|
||||
if newProj.field("addWorld"):
|
||||
addRoots.append(nwItemClass.WORLD)
|
||||
if newProj.field("addTime"):
|
||||
addRoots.append(nwItemClass.TIMELINE)
|
||||
if newProj.field("addObject"):
|
||||
addRoots.append(nwItemClass.OBJECT)
|
||||
if newProj.field("addEntity"):
|
||||
addRoots.append(nwItemClass.ENTITY)
|
||||
projData["addRoots"] = addRoots
|
||||
projData["numChapters"] = newProj.field("numChapters")
|
||||
projData["numScenes"] = newProj.field("numScenes")
|
||||
projData["chFolders"] = newProj.field("chFolders")
|
||||
|
||||
return projData
|
||||
|
||||
##
|
||||
# Events
|
||||
##
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<novelWriterXML appVersion="0.11.1" hexVersion="0x001101f0" fileVersion="1.2" timeStamp="2020-08-09 15:42:40">
|
||||
<novelWriterXML appVersion="0.11.1" hexVersion="0x001101f0" fileVersion="1.2" timeStamp="2020-08-10 20:03:56">
|
||||
<project>
|
||||
<name>Sample Project</name>
|
||||
<title>Sample Project</title>
|
||||
<author>Jane Smith</author>
|
||||
<author>Jay Doh</author>
|
||||
<saveCount>675</saveCount>
|
||||
<saveCount>676</saveCount>
|
||||
<autoCount>122</autoCount>
|
||||
<editTime>32866</editTime>
|
||||
<editTime>32870</editTime>
|
||||
</project>
|
||||
<settings>
|
||||
<doBackup>False</doBackup>
|
||||
|
||||
@@ -12,10 +12,20 @@ from nw import __version__, __url__, __docurl__, __issuesurl__, __sourceurl__
|
||||
##
|
||||
|
||||
buildDocs = False
|
||||
buildSample = False
|
||||
|
||||
if "qthelp" in sys.argv:
|
||||
buildDocs = True
|
||||
sys.argv.remove("qthelp")
|
||||
|
||||
if "sample" in sys.argv:
|
||||
buildSample = True
|
||||
sys.argv.remove("sample")
|
||||
|
||||
##
|
||||
# Qt Assistant Documentation
|
||||
##
|
||||
|
||||
if buildDocs:
|
||||
|
||||
buildDir = os.path.join("docs", "build", "qthelp")
|
||||
@@ -72,6 +82,31 @@ if buildDocs:
|
||||
print("Documentation build: OK")
|
||||
print("")
|
||||
|
||||
##
|
||||
# Sample Project ZIP file
|
||||
##
|
||||
|
||||
if buildSample:
|
||||
|
||||
srcSample = "sample"
|
||||
dstSample = os.path.join("nw", "assets", "sample.zip")
|
||||
|
||||
if os.path.isdir(srcSample):
|
||||
if os.path.isfile(dstSample):
|
||||
os.unlink(dstSample)
|
||||
|
||||
from zipfile import ZipFile
|
||||
|
||||
with ZipFile(dstSample, "w") as zipObj:
|
||||
zipObj.write(os.path.join("sample", "nwProject.nwx"), "nwProject.nwx")
|
||||
for docFile in os.listdir(os.path.join(srcSample, "content")):
|
||||
srcDoc = os.path.join(srcSample, "content", docFile)
|
||||
zipObj.write(srcDoc, "content/"+docFile)
|
||||
|
||||
else:
|
||||
print("Error: Could not find sample project source directory.")
|
||||
sys.exit(1)
|
||||
|
||||
if len(sys.argv) == 1:
|
||||
# Nothing more to do
|
||||
sys.exit(0)
|
||||
|
||||
@@ -38,6 +38,20 @@ def nwTempBuild(nwTemp):
|
||||
mkdir(buildDir)
|
||||
return buildDir
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def nwTempCustom(nwTemp):
|
||||
customDir = path.join(nwTemp, "custom")
|
||||
if not path.isdir(customDir):
|
||||
mkdir(customDir)
|
||||
return customDir
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def nwTempSample(nwTemp):
|
||||
sampleDir = path.join(nwTemp, "sample")
|
||||
if not path.isdir(sampleDir):
|
||||
mkdir(sampleDir)
|
||||
return sampleDir
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def nwRef():
|
||||
testDir = path.dirname(__file__)
|
||||
|
||||
@@ -8,6 +8,7 @@ class DummyMain():
|
||||
|
||||
def __init__(self):
|
||||
self.mainConf = None
|
||||
self.statusBar = StatusBar()
|
||||
return
|
||||
|
||||
def makeAlert(self, theMessage, theLevel):
|
||||
@@ -32,4 +33,20 @@ class DummyMain():
|
||||
def setProjectStatus(self, isChanged):
|
||||
return
|
||||
|
||||
def openProject(self, projPath):
|
||||
return
|
||||
|
||||
def rebuildIndex(self):
|
||||
return
|
||||
|
||||
# END Class GuiMain
|
||||
|
||||
class StatusBar():
|
||||
|
||||
def __init__(self):
|
||||
return
|
||||
|
||||
def setStatus(self, theText):
|
||||
return
|
||||
|
||||
# END Class StatusBar
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
from os import path, mkdir
|
||||
from itertools import chain
|
||||
|
||||
from PyQt5.QtWidgets import qApp
|
||||
|
||||
def ensureDir(theDir):
|
||||
if not path.isdir(theDir):
|
||||
mkdir(theDir)
|
||||
@@ -60,3 +62,8 @@ def cmpList(listOne, listTwo):
|
||||
if flatOne[i] != flatTwo[i]:
|
||||
return False
|
||||
return True
|
||||
|
||||
def getGuiItem(theName):
|
||||
for qWidget in qApp.topLevelWidgets():
|
||||
if qWidget.objectName() == theName:
|
||||
return qWidget
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<novelWriterXML appVersion="0.8.0rc1" hexVersion="0x000800c1" fileVersion="1.1" timeStamp="2020-06-05 21:05:07">
|
||||
<novelWriterXML appVersion="0.11.1" hexVersion="0x001101f0" fileVersion="1.2" timeStamp="2020-08-10 21:55:04">
|
||||
<project>
|
||||
<name>New Project</name>
|
||||
<title></title>
|
||||
@@ -13,8 +13,8 @@
|
||||
<autoOutline>True</autoOutline>
|
||||
<lastEdited>None</lastEdited>
|
||||
<lastViewed>None</lastViewed>
|
||||
<lastWordCount>0</lastWordCount>
|
||||
<novelWordCount>0</novelWordCount>
|
||||
<lastWordCount>6</lastWordCount>
|
||||
<novelWordCount>6</novelWordCount>
|
||||
<notesWordCount>0</notesWordCount>
|
||||
<autoReplace/>
|
||||
<titleFormat>
|
||||
@@ -37,7 +37,7 @@
|
||||
<entry blue="0" green="200" red="50">Main</entry>
|
||||
</importance>
|
||||
</settings>
|
||||
<content count="6">
|
||||
<content count="8">
|
||||
<item handle="73475cb40a568" order="0" parent="None">
|
||||
<name>Novel</name>
|
||||
<type>ROOT</type>
|
||||
@@ -46,35 +46,59 @@
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="25fc0e7096fc6" order="0" parent="73475cb40a568">
|
||||
<name>Title Page</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>TITLE</layout>
|
||||
<charCount>11</charCount>
|
||||
<wordCount>2</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="31489056e0916" order="1" parent="73475cb40a568">
|
||||
<name>New Chapter</name>
|
||||
<type>FOLDER</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="31489056e0916" order="0" parent="25fc0e7096fc6">
|
||||
<item handle="98010bd9270f9" order="0" parent="31489056e0916">
|
||||
<name>New Chapter</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>CHAPTER</layout>
|
||||
<charCount>11</charCount>
|
||||
<wordCount>2</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="0e17daca5f3e1" order="1" parent="31489056e0916">
|
||||
<name>New Scene</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>SCENE</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<charCount>9</charCount>
|
||||
<wordCount>2</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="44cb730c42048" order="1" parent="None">
|
||||
<name>Characters</name>
|
||||
<name>Plot</name>
|
||||
<type>ROOT</type>
|
||||
<class>CHARACTER</class>
|
||||
<class>PLOT</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="71ee45a3c0db9" order="2" parent="None">
|
||||
<name>Plot</name>
|
||||
<name>Characters</name>
|
||||
<type>ROOT</type>
|
||||
<class>PLOT</class>
|
||||
<class>CHARACTER</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
%%~ 031b4af5197ec:44cb730c42048:PLOT:NOTE:New File
|
||||
# Main Plot
|
||||
|
||||
@tag: MainPlot
|
||||
|
||||
This is a file detailing the main plot.
|
||||
@@ -1,6 +1,23 @@
|
||||
%%~ 0e17daca5f3e1:71ee45a3c0db9:PLOT:NOTE:New File
|
||||
# Main Plot
|
||||
%%~ 0e17daca5f3e1:31489056e0916:73475cb40a568:NOVEL:SCENE:New Scene
|
||||
# Novel
|
||||
|
||||
@tag: MainPlot
|
||||
## Chapter
|
||||
|
||||
@pov: Jane
|
||||
@plot: MainPlot
|
||||
|
||||
### Scene
|
||||
|
||||
% How about a comment?
|
||||
@pov: Jane
|
||||
@plot: MainPlot
|
||||
@location: Home
|
||||
|
||||
#### Some Section
|
||||
|
||||
@char: Jane
|
||||
|
||||
This is a paragraph of dummy text.
|
||||
|
||||
This is another paragraph of much longer dummy text. It is in fact very very dumb dummy text! We can also try replacing “quotes”, even single’s quotes are replaced. We can hyphen-ate, make dashes – and even longer dashes — if we want. Ellipsis? Not a problem either …
|
||||
|
||||
This is a file detailing the main plot.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
%%~ 1a6562590ef19:811786ad1ae74:WORLD:NOTE:New File
|
||||
# Main Location
|
||||
%%~ 1a6562590ef19:71ee45a3c0db9:CHARACTER:NOTE:New File
|
||||
# Jane Doe
|
||||
|
||||
@tag: Home
|
||||
@tag: Jane
|
||||
|
||||
This is a file describing Jane’s home.
|
||||
This is a file about Jane.
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
%%~ 31489056e0916:25fc0e7096fc6:73475cb40a568:NOVEL:SCENE:New Scene
|
||||
# Novel
|
||||
|
||||
## Chapter
|
||||
|
||||
@pov: Jane
|
||||
@plot: MainPlot
|
||||
|
||||
### Scene
|
||||
|
||||
% How about a comment?
|
||||
@pov: Jane
|
||||
@plot: MainPlot
|
||||
@location: Home
|
||||
|
||||
#### Some Section
|
||||
|
||||
@char: Jane
|
||||
|
||||
This is a paragraph of dummy text.
|
||||
|
||||
This is another paragraph of much longer dummy text. It is in fact very very dumb dummy text! We can also try replacing “quotes”, even single’s quotes are replaced. We can hyphen-ate, make dashes – and even longer dashes — if we want. Ellipsis? Not a problem either …
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
%%~ 41cfc0d1f2d12:811786ad1ae74:WORLD:NOTE:New File
|
||||
# Main Location
|
||||
|
||||
@tag: Home
|
||||
|
||||
This is a file describing Jane’s home.
|
||||
@@ -1,6 +0,0 @@
|
||||
%%~ 98010bd9270f9:44cb730c42048:CHARACTER:NOTE:New File
|
||||
# Jane Doe
|
||||
|
||||
@tag: Jane
|
||||
|
||||
This is a file about Jane.
|
||||
@@ -1,20 +1,20 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<novelWriterXML appVersion="0.8.0rc1" hexVersion="0x000800c1" fileVersion="1.1" timeStamp="2020-06-05 21:06:00">
|
||||
<novelWriterXML appVersion="0.11.1" hexVersion="0x001101f0" fileVersion="1.2" timeStamp="2020-08-10 22:00:16">
|
||||
<project>
|
||||
<name>New Project</name>
|
||||
<title></title>
|
||||
<saveCount>5</saveCount>
|
||||
<autoCount>1</autoCount>
|
||||
<editTime>11</editTime>
|
||||
<editTime>14</editTime>
|
||||
</project>
|
||||
<settings>
|
||||
<doBackup>True</doBackup>
|
||||
<spellCheck>True</spellCheck>
|
||||
<autoOutline>True</autoOutline>
|
||||
<lastEdited>31489056e0916</lastEdited>
|
||||
<lastEdited>0e17daca5f3e1</lastEdited>
|
||||
<lastViewed>None</lastViewed>
|
||||
<lastWordCount>86</lastWordCount>
|
||||
<novelWordCount>59</novelWordCount>
|
||||
<lastWordCount>90</lastWordCount>
|
||||
<novelWordCount>63</novelWordCount>
|
||||
<notesWordCount>27</notesWordCount>
|
||||
<autoReplace/>
|
||||
<titleFormat>
|
||||
@@ -37,7 +37,7 @@
|
||||
<entry blue="0" green="200" red="50">Main</entry>
|
||||
</importance>
|
||||
</settings>
|
||||
<content count="10">
|
||||
<content count="12">
|
||||
<item handle="73475cb40a568" order="0" parent="None">
|
||||
<name>Novel</name>
|
||||
<type>ROOT</type>
|
||||
@@ -46,13 +46,37 @@
|
||||
<expanded>True</expanded>
|
||||
</item>
|
||||
<item handle="25fc0e7096fc6" order="0" parent="73475cb40a568">
|
||||
<name>Title Page</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>TITLE</layout>
|
||||
<charCount>11</charCount>
|
||||
<wordCount>2</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="31489056e0916" order="1" parent="73475cb40a568">
|
||||
<name>New Chapter</name>
|
||||
<type>FOLDER</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>True</expanded>
|
||||
</item>
|
||||
<item handle="31489056e0916" order="0" parent="25fc0e7096fc6">
|
||||
<item handle="98010bd9270f9" order="0" parent="31489056e0916">
|
||||
<name>New Chapter</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>CHAPTER</layout>
|
||||
<charCount>11</charCount>
|
||||
<wordCount>2</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="0e17daca5f3e1" order="1" parent="31489056e0916">
|
||||
<name>New Scene</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
@@ -65,32 +89,13 @@
|
||||
<cursorPos>465</cursorPos>
|
||||
</item>
|
||||
<item handle="44cb730c42048" order="1" parent="None">
|
||||
<name>Characters</name>
|
||||
<type>ROOT</type>
|
||||
<class>CHARACTER</class>
|
||||
<status>New</status>
|
||||
<expanded>True</expanded>
|
||||
</item>
|
||||
<item handle="98010bd9270f9" order="0" parent="44cb730c42048">
|
||||
<name>New File</name>
|
||||
<type>FILE</type>
|
||||
<class>CHARACTER</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>NOTE</layout>
|
||||
<charCount>34</charCount>
|
||||
<wordCount>8</wordCount>
|
||||
<paraCount>1</paraCount>
|
||||
<cursorPos>51</cursorPos>
|
||||
</item>
|
||||
<item handle="71ee45a3c0db9" order="2" parent="None">
|
||||
<name>Plot</name>
|
||||
<type>ROOT</type>
|
||||
<class>PLOT</class>
|
||||
<status>New</status>
|
||||
<expanded>True</expanded>
|
||||
</item>
|
||||
<item handle="0e17daca5f3e1" order="0" parent="71ee45a3c0db9">
|
||||
<item handle="031b4af5197ec" order="0" parent="44cb730c42048">
|
||||
<name>New File</name>
|
||||
<type>FILE</type>
|
||||
<class>PLOT</class>
|
||||
@@ -102,6 +107,25 @@
|
||||
<paraCount>1</paraCount>
|
||||
<cursorPos>69</cursorPos>
|
||||
</item>
|
||||
<item handle="71ee45a3c0db9" order="2" parent="None">
|
||||
<name>Characters</name>
|
||||
<type>ROOT</type>
|
||||
<class>CHARACTER</class>
|
||||
<status>New</status>
|
||||
<expanded>True</expanded>
|
||||
</item>
|
||||
<item handle="1a6562590ef19" order="0" parent="71ee45a3c0db9">
|
||||
<name>New File</name>
|
||||
<type>FILE</type>
|
||||
<class>CHARACTER</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>NOTE</layout>
|
||||
<charCount>34</charCount>
|
||||
<wordCount>8</wordCount>
|
||||
<paraCount>1</paraCount>
|
||||
<cursorPos>51</cursorPos>
|
||||
</item>
|
||||
<item handle="811786ad1ae74" order="3" parent="None">
|
||||
<name>World</name>
|
||||
<type>ROOT</type>
|
||||
@@ -109,7 +133,7 @@
|
||||
<status>New</status>
|
||||
<expanded>True</expanded>
|
||||
</item>
|
||||
<item handle="1a6562590ef19" order="0" parent="811786ad1ae74">
|
||||
<item handle="41cfc0d1f2d12" order="0" parent="811786ad1ae74">
|
||||
<name>New File</name>
|
||||
<type>FILE</type>
|
||||
<class>WORLD</class>
|
||||
@@ -121,7 +145,7 @@
|
||||
<paraCount>1</paraCount>
|
||||
<cursorPos>68</cursorPos>
|
||||
</item>
|
||||
<item handle="41cfc0d1f2d12" order="4" parent="None">
|
||||
<item handle="2fca346db6561" order="4" parent="None">
|
||||
<name>Trash</name>
|
||||
<type>TRASH</type>
|
||||
<class>TRASH</class>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<novelWriterXML appVersion="0.10.0rc1" hexVersion="0x001000c1" fileVersion="1.1" timeStamp="2020-06-26 19:32:47">
|
||||
<novelWriterXML appVersion="0.11.1" hexVersion="0x001101f0" fileVersion="1.2" timeStamp="2020-08-10 22:01:52">
|
||||
<project>
|
||||
<name>Project Name</name>
|
||||
<title>Project Title</title>
|
||||
@@ -7,7 +7,7 @@
|
||||
<author>John Doh</author>
|
||||
<saveCount>2</saveCount>
|
||||
<autoCount>1</autoCount>
|
||||
<editTime>1</editTime>
|
||||
<editTime>0</editTime>
|
||||
</project>
|
||||
<settings>
|
||||
<doBackup>True</doBackup>
|
||||
@@ -15,8 +15,8 @@
|
||||
<autoOutline>True</autoOutline>
|
||||
<lastEdited>None</lastEdited>
|
||||
<lastViewed>None</lastViewed>
|
||||
<lastWordCount>0</lastWordCount>
|
||||
<novelWordCount>0</novelWordCount>
|
||||
<lastWordCount>6</lastWordCount>
|
||||
<novelWordCount>6</novelWordCount>
|
||||
<notesWordCount>0</notesWordCount>
|
||||
<autoReplace>
|
||||
<entry key="This">With This Stuff </entry>
|
||||
@@ -41,7 +41,7 @@
|
||||
<entry blue="0" green="200" red="50">Main</entry>
|
||||
</importance>
|
||||
</settings>
|
||||
<content count="6">
|
||||
<content count="8">
|
||||
<item handle="73475cb40a568" order="0" parent="None">
|
||||
<name>Novel</name>
|
||||
<type>ROOT</type>
|
||||
@@ -50,35 +50,59 @@
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="25fc0e7096fc6" order="0" parent="73475cb40a568">
|
||||
<name>Title Page</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>TITLE</layout>
|
||||
<charCount>11</charCount>
|
||||
<wordCount>2</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="31489056e0916" order="1" parent="73475cb40a568">
|
||||
<name>New Chapter</name>
|
||||
<type>FOLDER</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="31489056e0916" order="0" parent="25fc0e7096fc6">
|
||||
<item handle="98010bd9270f9" order="0" parent="31489056e0916">
|
||||
<name>New Chapter</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>CHAPTER</layout>
|
||||
<charCount>11</charCount>
|
||||
<wordCount>2</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="0e17daca5f3e1" order="1" parent="31489056e0916">
|
||||
<name>New Scene</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>SCENE</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<charCount>9</charCount>
|
||||
<wordCount>2</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="44cb730c42048" order="1" parent="None">
|
||||
<name>Characters</name>
|
||||
<name>Plot</name>
|
||||
<type>ROOT</type>
|
||||
<class>CHARACTER</class>
|
||||
<class>PLOT</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="71ee45a3c0db9" order="2" parent="None">
|
||||
<name>Plot</name>
|
||||
<name>Characters</name>
|
||||
<type>ROOT</type>
|
||||
<class>PLOT</class>
|
||||
<class>CHARACTER</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<novelWriterXML appVersion="0.9.0rc1" hexVersion="0x000900c1" fileVersion="1.1" timeStamp="2020-06-18 22:17:32">
|
||||
<novelWriterXML appVersion="0.11.1" hexVersion="0x001101f0" fileVersion="1.2" timeStamp="2020-08-10 22:03:19">
|
||||
<project>
|
||||
<name>New Project</name>
|
||||
<title></title>
|
||||
@@ -11,10 +11,10 @@
|
||||
<doBackup>True</doBackup>
|
||||
<spellCheck>False</spellCheck>
|
||||
<autoOutline>True</autoOutline>
|
||||
<lastEdited>31489056e0916</lastEdited>
|
||||
<lastEdited>0e17daca5f3e1</lastEdited>
|
||||
<lastViewed>None</lastViewed>
|
||||
<lastWordCount>59</lastWordCount>
|
||||
<novelWordCount>59</novelWordCount>
|
||||
<lastWordCount>6</lastWordCount>
|
||||
<novelWordCount>6</novelWordCount>
|
||||
<notesWordCount>0</notesWordCount>
|
||||
<autoReplace/>
|
||||
<titleFormat>
|
||||
@@ -37,7 +37,7 @@
|
||||
<entry blue="0" green="200" red="50">Main</entry>
|
||||
</importance>
|
||||
</settings>
|
||||
<content count="6">
|
||||
<content count="8">
|
||||
<item handle="73475cb40a568" order="0" parent="None">
|
||||
<name>Novel</name>
|
||||
<type>ROOT</type>
|
||||
@@ -46,35 +46,59 @@
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="25fc0e7096fc6" order="0" parent="73475cb40a568">
|
||||
<name>Title Page</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>TITLE</layout>
|
||||
<charCount>11</charCount>
|
||||
<wordCount>2</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="31489056e0916" order="1" parent="73475cb40a568">
|
||||
<name>New Chapter</name>
|
||||
<type>FOLDER</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="31489056e0916" order="0" parent="25fc0e7096fc6">
|
||||
<item handle="98010bd9270f9" order="0" parent="31489056e0916">
|
||||
<name>New Chapter</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>CHAPTER</layout>
|
||||
<charCount>11</charCount>
|
||||
<wordCount>2</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="0e17daca5f3e1" order="1" parent="31489056e0916">
|
||||
<name>Just a Page</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>Note</status>
|
||||
<exported>False</exported>
|
||||
<layout>PAGE</layout>
|
||||
<charCount>331</charCount>
|
||||
<wordCount>59</wordCount>
|
||||
<paraCount>2</paraCount>
|
||||
<charCount>9</charCount>
|
||||
<wordCount>2</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="44cb730c42048" order="1" parent="None">
|
||||
<name>Characters</name>
|
||||
<name>Plot</name>
|
||||
<type>ROOT</type>
|
||||
<class>CHARACTER</class>
|
||||
<class>PLOT</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="71ee45a3c0db9" order="2" parent="None">
|
||||
<name>Plot</name>
|
||||
<name>Characters</name>
|
||||
<type>ROOT</type>
|
||||
<class>PLOT</class>
|
||||
<class>CHARACTER</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<novelWriterXML appVersion="0.8.0rc1" hexVersion="0x000800c1" fileVersion="1.1" timeStamp="2020-06-05 20:58:52">
|
||||
<novelWriterXML appVersion="0.11.1" hexVersion="0x001101f0" fileVersion="1.2" timeStamp="2020-08-10 21:26:47">
|
||||
<project>
|
||||
<name>New Project</name>
|
||||
<title></title>
|
||||
<saveCount>1</saveCount>
|
||||
<autoCount>0</autoCount>
|
||||
<autoCount>1</autoCount>
|
||||
<editTime>0</editTime>
|
||||
</project>
|
||||
<settings>
|
||||
@@ -37,7 +37,7 @@
|
||||
<entry blue="0" green="200" red="50">Main</entry>
|
||||
</importance>
|
||||
</settings>
|
||||
<content count="6">
|
||||
<content count="8">
|
||||
<item handle="73475cb40a568" order="None" parent="None">
|
||||
<name>Novel</name>
|
||||
<type>ROOT</type>
|
||||
@@ -46,16 +46,16 @@
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="44cb730c42048" order="None" parent="None">
|
||||
<name>Characters</name>
|
||||
<name>Plot</name>
|
||||
<type>ROOT</type>
|
||||
<class>CHARACTER</class>
|
||||
<class>PLOT</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="71ee45a3c0db9" order="None" parent="None">
|
||||
<name>Plot</name>
|
||||
<name>Characters</name>
|
||||
<type>ROOT</type>
|
||||
<class>PLOT</class>
|
||||
<class>CHARACTER</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
@@ -67,13 +67,37 @@
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="25fc0e7096fc6" order="None" parent="73475cb40a568">
|
||||
<name>Title Page</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>TITLE</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="31489056e0916" order="None" parent="73475cb40a568">
|
||||
<name>New Chapter</name>
|
||||
<type>FOLDER</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="31489056e0916" order="None" parent="25fc0e7096fc6">
|
||||
<item handle="98010bd9270f9" order="None" parent="31489056e0916">
|
||||
<name>New Chapter</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>CHAPTER</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="0e17daca5f3e1" order="None" parent="31489056e0916">
|
||||
<name>New Scene</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<novelWriterXML appVersion="0.8.0rc1" hexVersion="0x000800c1" fileVersion="1.1" timeStamp="2020-06-05 21:03:18">
|
||||
<novelWriterXML appVersion="0.11.1" hexVersion="0x001101f0" fileVersion="1.2" timeStamp="2020-08-10 21:28:02">
|
||||
<project>
|
||||
<name>New Project</name>
|
||||
<title></title>
|
||||
<saveCount>4</saveCount>
|
||||
<autoCount>0</autoCount>
|
||||
<autoCount>1</autoCount>
|
||||
<editTime>0</editTime>
|
||||
</project>
|
||||
<settings>
|
||||
@@ -37,7 +37,7 @@
|
||||
<entry blue="0" green="200" red="50">Main</entry>
|
||||
</importance>
|
||||
</settings>
|
||||
<content count="10">
|
||||
<content count="12">
|
||||
<item handle="73475cb40a568" order="None" parent="None">
|
||||
<name>Novel</name>
|
||||
<type>ROOT</type>
|
||||
@@ -46,16 +46,16 @@
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="44cb730c42048" order="None" parent="None">
|
||||
<name>Characters</name>
|
||||
<name>Plot</name>
|
||||
<type>ROOT</type>
|
||||
<class>CHARACTER</class>
|
||||
<class>PLOT</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="71ee45a3c0db9" order="None" parent="None">
|
||||
<name>Plot</name>
|
||||
<name>Characters</name>
|
||||
<type>ROOT</type>
|
||||
<class>PLOT</class>
|
||||
<class>CHARACTER</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
@@ -67,13 +67,37 @@
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="25fc0e7096fc6" order="None" parent="73475cb40a568">
|
||||
<name>Title Page</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>TITLE</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="31489056e0916" order="None" parent="73475cb40a568">
|
||||
<name>New Chapter</name>
|
||||
<type>FOLDER</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="31489056e0916" order="None" parent="25fc0e7096fc6">
|
||||
<item handle="98010bd9270f9" order="None" parent="31489056e0916">
|
||||
<name>New Chapter</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>CHAPTER</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="0e17daca5f3e1" order="None" parent="31489056e0916">
|
||||
<name>New Scene</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
@@ -85,28 +109,28 @@
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="98010bd9270f9" order="None" parent="None">
|
||||
<item handle="1a6562590ef19" order="None" parent="None">
|
||||
<name>Timeline</name>
|
||||
<type>ROOT</type>
|
||||
<class>TIMELINE</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="0e17daca5f3e1" order="None" parent="None">
|
||||
<item handle="031b4af5197ec" order="None" parent="None">
|
||||
<name>Object</name>
|
||||
<type>ROOT</type>
|
||||
<class>OBJECT</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="1a6562590ef19" order="None" parent="None">
|
||||
<item handle="41cfc0d1f2d12" order="None" parent="None">
|
||||
<name>Custom1</name>
|
||||
<type>ROOT</type>
|
||||
<class>CUSTOM</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="031b4af5197ec" order="None" parent="None">
|
||||
<item handle="2858dcd1057d3" order="None" parent="None">
|
||||
<name>Custom2</name>
|
||||
<type>ROOT</type>
|
||||
<class>CUSTOM</class>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<novelWriterXML appVersion="0.8.0rc1" hexVersion="0x000800c1" fileVersion="1.1" timeStamp="2020-06-05 21:04:10">
|
||||
<novelWriterXML appVersion="0.11.1" hexVersion="0x001101f0" fileVersion="1.2" timeStamp="2020-08-10 21:29:43">
|
||||
<project>
|
||||
<name>New Project</name>
|
||||
<title></title>
|
||||
<saveCount>5</saveCount>
|
||||
<autoCount>0</autoCount>
|
||||
<autoCount>1</autoCount>
|
||||
<editTime>0</editTime>
|
||||
</project>
|
||||
<settings>
|
||||
@@ -37,7 +37,7 @@
|
||||
<entry blue="0" green="200" red="50">Main</entry>
|
||||
</importance>
|
||||
</settings>
|
||||
<content count="12">
|
||||
<content count="14">
|
||||
<item handle="73475cb40a568" order="None" parent="None">
|
||||
<name>Novel</name>
|
||||
<type>ROOT</type>
|
||||
@@ -46,16 +46,16 @@
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="44cb730c42048" order="None" parent="None">
|
||||
<name>Characters</name>
|
||||
<name>Plot</name>
|
||||
<type>ROOT</type>
|
||||
<class>CHARACTER</class>
|
||||
<class>PLOT</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="71ee45a3c0db9" order="None" parent="None">
|
||||
<name>Plot</name>
|
||||
<name>Characters</name>
|
||||
<type>ROOT</type>
|
||||
<class>PLOT</class>
|
||||
<class>CHARACTER</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
@@ -67,13 +67,37 @@
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="25fc0e7096fc6" order="None" parent="73475cb40a568">
|
||||
<name>Title Page</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>TITLE</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="31489056e0916" order="None" parent="73475cb40a568">
|
||||
<name>New Chapter</name>
|
||||
<type>FOLDER</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="31489056e0916" order="None" parent="25fc0e7096fc6">
|
||||
<item handle="98010bd9270f9" order="None" parent="31489056e0916">
|
||||
<name>New Chapter</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>CHAPTER</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="0e17daca5f3e1" order="None" parent="31489056e0916">
|
||||
<name>New Scene</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
@@ -85,35 +109,35 @@
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="98010bd9270f9" order="None" parent="None">
|
||||
<item handle="1a6562590ef19" order="None" parent="None">
|
||||
<name>Timeline</name>
|
||||
<type>ROOT</type>
|
||||
<class>TIMELINE</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="0e17daca5f3e1" order="None" parent="None">
|
||||
<item handle="031b4af5197ec" order="None" parent="None">
|
||||
<name>Object</name>
|
||||
<type>ROOT</type>
|
||||
<class>OBJECT</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="1a6562590ef19" order="None" parent="None">
|
||||
<item handle="41cfc0d1f2d12" order="None" parent="None">
|
||||
<name>Custom1</name>
|
||||
<type>ROOT</type>
|
||||
<class>CUSTOM</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="031b4af5197ec" order="None" parent="None">
|
||||
<item handle="2858dcd1057d3" order="None" parent="None">
|
||||
<name>Custom2</name>
|
||||
<type>ROOT</type>
|
||||
<class>CUSTOM</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="41cfc0d1f2d12" order="None" parent="73475cb40a568">
|
||||
<item handle="2fca346db6561" order="None" parent="73475cb40a568">
|
||||
<name>Hello</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
@@ -125,7 +149,7 @@
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="2858dcd1057d3" order="None" parent="44cb730c42048">
|
||||
<item handle="02d20bbd7e394" order="None" parent="71ee45a3c0db9">
|
||||
<name>Jane</name>
|
||||
<type>FILE</type>
|
||||
<class>CHARACTER</class>
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<novelWriterXML appVersion="0.11.1" hexVersion="0x001101f0" fileVersion="1.2" timeStamp="2020-08-10 23:59:24">
|
||||
<project>
|
||||
<name>Test Custom</name>
|
||||
<title>Test Novel</title>
|
||||
<author>Jane Doe</author>
|
||||
<author>John Doh</author>
|
||||
<saveCount>1</saveCount>
|
||||
<autoCount>1</autoCount>
|
||||
<editTime>0</editTime>
|
||||
</project>
|
||||
<settings>
|
||||
<doBackup>True</doBackup>
|
||||
<spellCheck>False</spellCheck>
|
||||
<autoOutline>True</autoOutline>
|
||||
<lastEdited>None</lastEdited>
|
||||
<lastViewed>None</lastViewed>
|
||||
<lastWordCount>0</lastWordCount>
|
||||
<novelWordCount>0</novelWordCount>
|
||||
<notesWordCount>0</notesWordCount>
|
||||
<autoReplace/>
|
||||
<titleFormat>
|
||||
<title>%title%</title>
|
||||
<chapter>Chapter %ch%: %title%</chapter>
|
||||
<unnumbered>%title%</unnumbered>
|
||||
<scene>* * *</scene>
|
||||
<section></section>
|
||||
</titleFormat>
|
||||
<status>
|
||||
<entry blue="100" green="100" red="100">New</entry>
|
||||
<entry blue="0" green="50" red="200">Note</entry>
|
||||
<entry blue="0" green="150" red="200">Draft</entry>
|
||||
<entry blue="0" green="200" red="50">Finished</entry>
|
||||
</status>
|
||||
<importance>
|
||||
<entry blue="100" green="100" red="100">New</entry>
|
||||
<entry blue="0" green="50" red="200">Minor</entry>
|
||||
<entry blue="0" green="150" red="200">Major</entry>
|
||||
<entry blue="0" green="200" red="50">Main</entry>
|
||||
</importance>
|
||||
</settings>
|
||||
<content count="23">
|
||||
<item handle="73475cb40a568" order="None" parent="None">
|
||||
<name>Novel</name>
|
||||
<type>ROOT</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="44cb730c42048" order="None" parent="None">
|
||||
<name>Plot</name>
|
||||
<type>ROOT</type>
|
||||
<class>PLOT</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="71ee45a3c0db9" order="None" parent="None">
|
||||
<name>Characters</name>
|
||||
<type>ROOT</type>
|
||||
<class>CHARACTER</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="811786ad1ae74" order="None" parent="None">
|
||||
<name>Locations</name>
|
||||
<type>ROOT</type>
|
||||
<class>WORLD</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="25fc0e7096fc6" order="None" parent="None">
|
||||
<name>Timeline</name>
|
||||
<type>ROOT</type>
|
||||
<class>TIMELINE</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="31489056e0916" order="None" parent="None">
|
||||
<name>Objects</name>
|
||||
<type>ROOT</type>
|
||||
<class>OBJECT</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="98010bd9270f9" order="None" parent="None">
|
||||
<name>Entity</name>
|
||||
<type>ROOT</type>
|
||||
<class>ENTITY</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="0e17daca5f3e1" order="None" parent="73475cb40a568">
|
||||
<name>Title Page</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>TITLE</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="1a6562590ef19" order="None" parent="73475cb40a568">
|
||||
<name>Chapter 1</name>
|
||||
<type>FOLDER</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="031b4af5197ec" order="None" parent="1a6562590ef19">
|
||||
<name>Chapter 1</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>CHAPTER</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="41cfc0d1f2d12" order="None" parent="1a6562590ef19">
|
||||
<name>Scene 1.1</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>SCENE</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="2858dcd1057d3" order="None" parent="1a6562590ef19">
|
||||
<name>Scene 1.2</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>SCENE</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="2fca346db6561" order="None" parent="1a6562590ef19">
|
||||
<name>Scene 1.3</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>SCENE</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="02d20bbd7e394" order="None" parent="73475cb40a568">
|
||||
<name>Chapter 2</name>
|
||||
<type>FOLDER</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="7688b6ef52555" order="None" parent="02d20bbd7e394">
|
||||
<name>Chapter 2</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>CHAPTER</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="c837649cce43f" order="None" parent="02d20bbd7e394">
|
||||
<name>Scene 2.1</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>SCENE</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="6208ef0f7750c" order="None" parent="02d20bbd7e394">
|
||||
<name>Scene 2.2</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>SCENE</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="3e1e967e9b793" order="None" parent="02d20bbd7e394">
|
||||
<name>Scene 2.3</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>SCENE</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="39fa9ec190eee" order="None" parent="73475cb40a568">
|
||||
<name>Chapter 3</name>
|
||||
<type>FOLDER</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="d029fa3a95e17" order="None" parent="39fa9ec190eee">
|
||||
<name>Chapter 3</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>CHAPTER</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="81b8a03f97e87" order="None" parent="39fa9ec190eee">
|
||||
<name>Scene 3.1</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>SCENE</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="da4ea2a5506f2" order="None" parent="39fa9ec190eee">
|
||||
<name>Scene 3.2</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>SCENE</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="a68b412c42825" order="None" parent="39fa9ec190eee">
|
||||
<name>Scene 3.3</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>SCENE</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
</content>
|
||||
</novelWriterXML>
|
||||
+172
-36
@@ -10,12 +10,12 @@ from PyQt5.QtCore import Qt
|
||||
|
||||
from nw.gui import (
|
||||
GuiProjectSettings, GuiItemEditor, GuiAbout, GuiBuildNovel,
|
||||
GuiDocMerge, GuiDocSplit, GuiWritingStats
|
||||
GuiDocMerge, GuiDocSplit, GuiWritingStats, GuiProjectWizard
|
||||
)
|
||||
from nw.constants import *
|
||||
|
||||
keyDelay = 5
|
||||
stepDelay = 50
|
||||
keyDelay = 2
|
||||
stepDelay = 20
|
||||
|
||||
@pytest.mark.gui
|
||||
def testMainWindows(qtbot, nwTempGUI, nwRef, nwTemp):
|
||||
@@ -27,7 +27,7 @@ def testMainWindows(qtbot, nwTempGUI, nwRef, nwTemp):
|
||||
|
||||
# Create new, save, close project
|
||||
nwGUI.theProject.projTree.setSeed(42)
|
||||
assert nwGUI.newProject(nwTempGUI, True)
|
||||
assert nwGUI.newProject({"projPath": nwTempGUI}, True)
|
||||
assert nwGUI.saveProject()
|
||||
assert nwGUI.closeProject()
|
||||
|
||||
@@ -55,8 +55,8 @@ def testMainWindows(qtbot, nwTempGUI, nwRef, nwTemp):
|
||||
qtbot.wait(stepDelay)
|
||||
|
||||
# Check that we loaded the data
|
||||
assert len(nwGUI.theProject.projTree) == 6
|
||||
assert len(nwGUI.theProject.projTree._treeOrder) == 6
|
||||
assert len(nwGUI.theProject.projTree) == 8
|
||||
assert len(nwGUI.theProject.projTree._treeOrder) == 8
|
||||
assert len(nwGUI.theProject.projTree._treeRoots) == 4
|
||||
assert nwGUI.theProject.projTree.trashRoot() is None
|
||||
assert nwGUI.theProject.projPath == nwTempGUI
|
||||
@@ -71,6 +71,8 @@ def testMainWindows(qtbot, nwTempGUI, nwRef, nwTemp):
|
||||
assert nwGUI.treeView._getTreeItem("73475cb40a568") is not None
|
||||
assert nwGUI.treeView._getTreeItem("25fc0e7096fc6") is not None
|
||||
assert nwGUI.treeView._getTreeItem("31489056e0916") is not None
|
||||
assert nwGUI.treeView._getTreeItem("98010bd9270f9") is not None
|
||||
assert nwGUI.treeView._getTreeItem("0e17daca5f3e1") is not None
|
||||
assert nwGUI.treeView._getTreeItem("44cb730c42048") is not None
|
||||
assert nwGUI.treeView._getTreeItem("71ee45a3c0db9") is not None
|
||||
assert nwGUI.treeView._getTreeItem("811786ad1ae74") is not None
|
||||
@@ -81,12 +83,13 @@ def testMainWindows(qtbot, nwTempGUI, nwRef, nwTemp):
|
||||
# Add a Character File
|
||||
nwGUI.setFocus(1)
|
||||
nwGUI.treeView.clearSelection()
|
||||
nwGUI.treeView._getTreeItem("44cb730c42048").setSelected(True)
|
||||
nwGUI.treeView._getTreeItem("71ee45a3c0db9").setSelected(True)
|
||||
nwGUI.treeView.newTreeItem(nwItemType.FILE, None)
|
||||
assert nwGUI.openSelectedItem()
|
||||
|
||||
# Type something into the document
|
||||
nwGUI.setFocus(2)
|
||||
qtbot.keyClick(nwGUI.docEditor, "a", modifier=Qt.ControlModifier, delay=keyDelay)
|
||||
for c in "# Jane Doe":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
|
||||
@@ -102,12 +105,13 @@ def testMainWindows(qtbot, nwTempGUI, nwRef, nwTemp):
|
||||
# Add a Plot File
|
||||
nwGUI.setFocus(1)
|
||||
nwGUI.treeView.clearSelection()
|
||||
nwGUI.treeView._getTreeItem("71ee45a3c0db9").setSelected(True)
|
||||
nwGUI.treeView._getTreeItem("44cb730c42048").setSelected(True)
|
||||
nwGUI.treeView.newTreeItem(nwItemType.FILE, None)
|
||||
assert nwGUI.openSelectedItem()
|
||||
|
||||
# Type something into the document
|
||||
nwGUI.setFocus(2)
|
||||
qtbot.keyClick(nwGUI.docEditor, "a", modifier=Qt.ControlModifier, delay=keyDelay)
|
||||
for c in "# Main Plot":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
|
||||
@@ -134,6 +138,7 @@ def testMainWindows(qtbot, nwTempGUI, nwRef, nwTemp):
|
||||
|
||||
# Type something into the document
|
||||
nwGUI.setFocus(2)
|
||||
qtbot.keyClick(nwGUI.docEditor, "a", modifier=Qt.ControlModifier, delay=keyDelay)
|
||||
for c in "# Main Location":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
|
||||
@@ -150,12 +155,13 @@ def testMainWindows(qtbot, nwTempGUI, nwRef, nwTemp):
|
||||
nwGUI.setFocus(1)
|
||||
nwGUI.treeView.clearSelection()
|
||||
nwGUI.treeView._getTreeItem("73475cb40a568").setExpanded(True)
|
||||
nwGUI.treeView._getTreeItem("25fc0e7096fc6").setExpanded(True)
|
||||
nwGUI.treeView._getTreeItem("31489056e0916").setSelected(True)
|
||||
nwGUI.treeView._getTreeItem("31489056e0916").setExpanded(True)
|
||||
nwGUI.treeView._getTreeItem("0e17daca5f3e1").setSelected(True)
|
||||
assert nwGUI.openSelectedItem()
|
||||
|
||||
# Type something into the document
|
||||
nwGUI.setFocus(2)
|
||||
qtbot.keyClick(nwGUI.docEditor, "a", modifier=Qt.ControlModifier, delay=keyDelay)
|
||||
for c in "# Novel":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
|
||||
@@ -233,8 +239,8 @@ def testMainWindows(qtbot, nwTempGUI, nwRef, nwTemp):
|
||||
|
||||
# Open and view the edited document
|
||||
nwGUI.setFocus(3)
|
||||
assert nwGUI.openDocument("31489056e0916")
|
||||
assert nwGUI.viewDocument("31489056e0916")
|
||||
assert nwGUI.openDocument("0e17daca5f3e1")
|
||||
assert nwGUI.viewDocument("0e17daca5f3e1")
|
||||
qtbot.wait(stepDelay)
|
||||
assert nwGUI.saveProject()
|
||||
assert nwGUI.closeDocViewer()
|
||||
@@ -243,23 +249,24 @@ def testMainWindows(qtbot, nwTempGUI, nwRef, nwTemp):
|
||||
# Check a Quick Create and Delete
|
||||
assert nwGUI.treeView.newTreeItem(nwItemType.FILE, None)
|
||||
newHandle = nwGUI.treeView.getSelectedHandle()
|
||||
assert nwGUI.theProject.projTree["031b4af5197ec"] is not None
|
||||
assert nwGUI.theProject.projTree["2858dcd1057d3"] is not None
|
||||
assert nwGUI.treeView.deleteItem()
|
||||
assert nwGUI.treeView.setSelectedHandle(newHandle)
|
||||
assert nwGUI.treeView.deleteItem()
|
||||
assert nwGUI.theProject.projTree["2fca346db6561"] is not None # Trash
|
||||
assert nwGUI.saveProject()
|
||||
|
||||
# Check the files
|
||||
refFile = path.join(nwTempGUI, "nwProject.nwx")
|
||||
assert cmpFiles(refFile, path.join(nwRef, "gui", "1_nwProject.nwx"), [2, 6, 7, 8])
|
||||
refFile = path.join(nwTempGUI, "content", "0e17daca5f3e1.nwd")
|
||||
assert cmpFiles(refFile, path.join(nwRef, "gui", "1_0e17daca5f3e1.nwd"))
|
||||
refFile = path.join(nwTempGUI, "content", "98010bd9270f9.nwd")
|
||||
assert cmpFiles(refFile, path.join(nwRef, "gui", "1_98010bd9270f9.nwd"))
|
||||
refFile = path.join(nwTempGUI, "content", "31489056e0916.nwd")
|
||||
assert cmpFiles(refFile, path.join(nwRef, "gui", "1_31489056e0916.nwd"))
|
||||
refFile = path.join(nwTempGUI, "content", "031b4af5197ec.nwd")
|
||||
assert cmpFiles(refFile, path.join(nwRef, "gui", "1_031b4af5197ec.nwd"))
|
||||
refFile = path.join(nwTempGUI, "content", "1a6562590ef19.nwd")
|
||||
assert cmpFiles(refFile, path.join(nwRef, "gui", "1_1a6562590ef19.nwd"))
|
||||
refFile = path.join(nwTempGUI, "content", "0e17daca5f3e1.nwd")
|
||||
assert cmpFiles(refFile, path.join(nwRef, "gui", "1_0e17daca5f3e1.nwd"))
|
||||
refFile = path.join(nwTempGUI, "content", "41cfc0d1f2d12.nwd")
|
||||
assert cmpFiles(refFile, path.join(nwRef, "gui", "1_41cfc0d1f2d12.nwd"))
|
||||
|
||||
nwGUI.closeMain()
|
||||
# qtbot.stopForInteraction()
|
||||
@@ -274,7 +281,7 @@ def testProjectEditor(qtbot, nwTempGUI, nwRef, nwTemp):
|
||||
|
||||
# Create new, save, open project
|
||||
nwGUI.theProject.projTree.setSeed(42)
|
||||
assert nwGUI.newProject(nwTempGUI, True)
|
||||
assert nwGUI.newProject({"projPath": nwTempGUI}, True)
|
||||
nwGUI.mainConf.backupPath = nwTempGUI
|
||||
|
||||
projEdit = GuiProjectSettings(nwGUI, nwGUI.theProject)
|
||||
@@ -363,10 +370,10 @@ def testItemEditor(qtbot, nwTempGUI, nwRef, nwTemp):
|
||||
|
||||
# Create new, save, open project
|
||||
nwGUI.theProject.projTree.setSeed(42)
|
||||
assert nwGUI.newProject(nwTempGUI, True)
|
||||
assert nwGUI.openDocument("31489056e0916")
|
||||
assert nwGUI.newProject({"projPath": nwTempGUI}, True)
|
||||
assert nwGUI.openDocument("0e17daca5f3e1")
|
||||
|
||||
itemEdit = GuiItemEditor(nwGUI, nwGUI.theProject, "31489056e0916")
|
||||
itemEdit = GuiItemEditor(nwGUI, nwGUI.theProject, "0e17daca5f3e1")
|
||||
qtbot.addWidget(itemEdit)
|
||||
|
||||
assert itemEdit.editName.text() == "New Scene"
|
||||
@@ -383,7 +390,7 @@ def testItemEditor(qtbot, nwTempGUI, nwRef, nwTemp):
|
||||
assert not itemEdit.editExport.isChecked()
|
||||
itemEdit._doSave()
|
||||
|
||||
itemEdit = GuiItemEditor(nwGUI, nwGUI.theProject, "31489056e0916")
|
||||
itemEdit = GuiItemEditor(nwGUI, nwGUI.theProject, "0e17daca5f3e1")
|
||||
qtbot.addWidget(itemEdit)
|
||||
assert itemEdit.editName.text() == "Just a Page"
|
||||
assert itemEdit.editStatus.currentData() == "Note"
|
||||
@@ -391,12 +398,12 @@ def testItemEditor(qtbot, nwTempGUI, nwRef, nwTemp):
|
||||
itemEdit._doClose()
|
||||
|
||||
# Check that the header is updated
|
||||
nwGUI.docEditor.updateDocInfo("31489056e0916")
|
||||
nwGUI.docEditor.updateDocInfo("0e17daca5f3e1")
|
||||
assert nwGUI.docEditor.docHeader.theTitle.text() == "Novel › New Chapter › Just a Page"
|
||||
assert not nwGUI.docEditor.setCursorLine("where?")
|
||||
assert nwGUI.docEditor.setCursorLine(2)
|
||||
qtbot.wait(stepDelay)
|
||||
assert nwGUI.docEditor.getCursorPosition() == 9
|
||||
assert nwGUI.docEditor.getCursorPosition() == 15
|
||||
|
||||
qtbot.wait(stepDelay)
|
||||
assert nwGUI.saveProject()
|
||||
@@ -435,10 +442,10 @@ def testWritingStatsExport(qtbot, nwTempGUI, nwRef, nwTemp):
|
||||
jsonData = json.loads(inFile.read())
|
||||
|
||||
assert len(jsonData) == 3
|
||||
assert jsonData[0]["length"] > 0
|
||||
assert jsonData[0]["newWords"] == 86
|
||||
assert jsonData[0]["novelWords"] == 59
|
||||
assert jsonData[0]["noteWords"] == 27
|
||||
assert jsonData[1]["length"] > 0
|
||||
assert jsonData[1]["newWords"] == 84
|
||||
assert jsonData[1]["novelWords"] == 63
|
||||
assert jsonData[1]["noteWords"] == 27
|
||||
|
||||
# No Novel Files
|
||||
qtbot.mouseClick(sessLog.incNovel, Qt.LeftButton)
|
||||
@@ -453,7 +460,7 @@ def testWritingStatsExport(qtbot, nwTempGUI, nwRef, nwTemp):
|
||||
assert len(jsonData) == 2
|
||||
assert jsonData[0]["length"] > 0
|
||||
assert jsonData[0]["newWords"] == 27
|
||||
assert jsonData[0]["novelWords"] == 59
|
||||
assert jsonData[0]["novelWords"] == 63
|
||||
assert jsonData[0]["noteWords"] == 27
|
||||
|
||||
# No Note Files
|
||||
@@ -468,10 +475,10 @@ def testWritingStatsExport(qtbot, nwTempGUI, nwRef, nwTemp):
|
||||
jsonData = json.loads(inFile.read())
|
||||
|
||||
assert len(jsonData) == 3
|
||||
assert jsonData[0]["length"] > 0
|
||||
assert jsonData[0]["newWords"] == 59
|
||||
assert jsonData[0]["novelWords"] == 59
|
||||
assert jsonData[0]["noteWords"] == 27
|
||||
assert jsonData[1]["length"] > 0
|
||||
assert jsonData[1]["newWords"] == 57
|
||||
assert jsonData[1]["novelWords"] == 63
|
||||
assert jsonData[1]["noteWords"] == 27
|
||||
|
||||
# No Negative Entries
|
||||
qtbot.mouseClick(sessLog.incNotes, Qt.LeftButton)
|
||||
@@ -484,7 +491,7 @@ def testWritingStatsExport(qtbot, nwTempGUI, nwRef, nwTemp):
|
||||
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
|
||||
jsonData = json.loads(inFile.read())
|
||||
|
||||
assert len(jsonData) == 1
|
||||
assert len(jsonData) == 2
|
||||
|
||||
# Un-hide Zero Entries
|
||||
qtbot.mouseClick(sessLog.hideNegative, Qt.LeftButton)
|
||||
@@ -733,6 +740,135 @@ def testSplitTool(qtbot, nwTempGUI, nwLipsum, nwRef, nwTemp):
|
||||
# qtbot.stopForInteraction()
|
||||
nwGUI.closeMain()
|
||||
|
||||
@pytest.mark.gui
|
||||
def testNewProjectWizard(qtbot, nwTempGUI, nwLipsum, nwRef, nwTemp):
|
||||
|
||||
from PyQt5.QtWidgets import QWizard
|
||||
from nw.gui.projwizard import (
|
||||
ProjWizardIntroPage, ProjWizardFolderPage, ProjWizardPopulatePage,
|
||||
ProjWizardCustomPage, ProjWizardFinalPage
|
||||
)
|
||||
|
||||
nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI, "--data=%s" % nwTemp])
|
||||
qtbot.addWidget(nwGUI)
|
||||
nwGUI.show()
|
||||
qtbot.waitForWindowShown(nwGUI)
|
||||
qtbot.wait(stepDelay)
|
||||
|
||||
for wStep in range(3):
|
||||
|
||||
# The Wizard
|
||||
nwWiz = GuiProjectWizard(nwGUI)
|
||||
nwWiz.show()
|
||||
qtbot.waitForWindowShown(nwWiz)
|
||||
|
||||
# Intro Page
|
||||
introPage = nwWiz.currentPage()
|
||||
assert isinstance(introPage, ProjWizardIntroPage)
|
||||
assert not nwWiz.button(QWizard.NextButton).isEnabled()
|
||||
|
||||
qtbot.wait(stepDelay)
|
||||
for c in "Test Minimal":
|
||||
qtbot.keyClick(introPage.projName, c, delay=keyDelay)
|
||||
|
||||
qtbot.wait(stepDelay)
|
||||
for c in "Minimal Novel":
|
||||
qtbot.keyClick(introPage.projTitle, c, delay=keyDelay)
|
||||
|
||||
qtbot.wait(stepDelay)
|
||||
for c in "Jane Doe":
|
||||
qtbot.keyClick(introPage.projAuthors, c, delay=keyDelay)
|
||||
|
||||
# Setting projName should activate the button
|
||||
assert nwWiz.button(QWizard.NextButton).isEnabled()
|
||||
|
||||
qtbot.wait(stepDelay)
|
||||
qtbot.mouseClick(nwWiz.button(QWizard.NextButton), Qt.LeftButton)
|
||||
|
||||
# Folder Page
|
||||
storagePage = nwWiz.currentPage()
|
||||
assert isinstance(storagePage, ProjWizardFolderPage)
|
||||
assert not nwWiz.button(QWizard.NextButton).isEnabled()
|
||||
|
||||
qtbot.wait(stepDelay)
|
||||
projPath = path.join(nwTemp, "dummy")
|
||||
for c in projPath:
|
||||
qtbot.keyClick(storagePage.projPath, c, delay=keyDelay)
|
||||
|
||||
# Setting projPath should activate the button
|
||||
assert nwWiz.button(QWizard.NextButton).isEnabled()
|
||||
|
||||
qtbot.wait(stepDelay)
|
||||
qtbot.mouseClick(nwWiz.button(QWizard.NextButton), Qt.LeftButton)
|
||||
|
||||
# Populate Page
|
||||
popPage = nwWiz.currentPage()
|
||||
assert isinstance(popPage, ProjWizardPopulatePage)
|
||||
assert nwWiz.button(QWizard.NextButton).isEnabled()
|
||||
|
||||
qtbot.wait(stepDelay)
|
||||
if wStep == 0:
|
||||
popPage.popMinimal.setChecked(True)
|
||||
elif wStep == 1:
|
||||
popPage.popCustom.setChecked(True)
|
||||
elif wStep == 2:
|
||||
popPage.popSample.setChecked(True)
|
||||
|
||||
qtbot.wait(stepDelay)
|
||||
qtbot.mouseClick(nwWiz.button(QWizard.NextButton), Qt.LeftButton)
|
||||
|
||||
# Custom Page
|
||||
if wStep == 1:
|
||||
customPage = nwWiz.currentPage()
|
||||
assert isinstance(customPage, ProjWizardCustomPage)
|
||||
assert nwWiz.button(QWizard.NextButton).isEnabled()
|
||||
|
||||
customPage.addPlot.setChecked(True)
|
||||
customPage.addChar.setChecked(True)
|
||||
customPage.addWorld.setChecked(True)
|
||||
customPage.addTime.setChecked(True)
|
||||
customPage.addObject.setChecked(True)
|
||||
customPage.addEntity.setChecked(True)
|
||||
|
||||
qtbot.wait(stepDelay)
|
||||
qtbot.mouseClick(nwWiz.button(QWizard.NextButton), Qt.LeftButton)
|
||||
|
||||
# Final Page
|
||||
finalPage = nwWiz.currentPage()
|
||||
assert isinstance(finalPage, ProjWizardFinalPage)
|
||||
assert nwWiz.button(QWizard.FinishButton).isEnabled()
|
||||
qtbot.mouseClick(nwWiz.button(QWizard.FinishButton), Qt.LeftButton)
|
||||
|
||||
# Check Data
|
||||
projData = nwGUI._assembleProjectWizardData(nwWiz)
|
||||
assert projData["projName"] == "Test Minimal"
|
||||
assert projData["projTitle"] == "Minimal Novel"
|
||||
assert projData["projAuthors"] == "Jane Doe"
|
||||
assert projData["projPath"] == projPath
|
||||
assert projData["popMinimal"] == (wStep == 0)
|
||||
assert projData["popCustom"] == (wStep == 1)
|
||||
assert projData["popSample"] == (wStep == 2)
|
||||
if wStep == 1:
|
||||
assert projData["addRoots"] == [
|
||||
nwItemClass.PLOT,
|
||||
nwItemClass.CHARACTER,
|
||||
nwItemClass.WORLD,
|
||||
nwItemClass.TIMELINE,
|
||||
nwItemClass.OBJECT,
|
||||
nwItemClass.ENTITY,
|
||||
]
|
||||
assert projData["numChapters"] == 5
|
||||
assert projData["numScenes"] == 5
|
||||
assert projData["chFolders"] == True
|
||||
else:
|
||||
assert projData["addRoots"] == []
|
||||
assert projData["numChapters"] == 0
|
||||
assert projData["numScenes"] == 0
|
||||
assert projData["chFolders"] == False
|
||||
|
||||
# qtbot.stopForInteraction()
|
||||
nwGUI.closeMain()
|
||||
|
||||
@pytest.mark.gui
|
||||
def testDocAction(qtbot, nwTempGUI, nwLipsum, nwRef, nwTemp):
|
||||
|
||||
|
||||
+65
-12
@@ -22,11 +22,11 @@ theProject = NWProject(theMain)
|
||||
theProject.projTree.setSeed(42)
|
||||
|
||||
@pytest.mark.project
|
||||
def testProjectNew(nwTempProj,nwRef,nwTemp):
|
||||
def testProjectNewMinimal(nwTempProj, nwRef, nwTemp):
|
||||
projFile = path.join(nwTempProj,"nwProject.nwx")
|
||||
refFile = path.join(nwRef,"proj","1_nwProject.nwx")
|
||||
refFile = path.join(nwRef,"proj", "1_nwProject.nwx")
|
||||
assert theConf.initConfig(nwRef, nwTemp)
|
||||
assert theProject.newProject()
|
||||
assert theProject.newProject({"projPath": nwTempProj})
|
||||
assert theProject.setProjectPath(nwTempProj)
|
||||
assert theProject.saveProject()
|
||||
assert theProject.closeProject()
|
||||
@@ -82,7 +82,7 @@ def testProjectNewFile(nwTempProj,nwRef):
|
||||
refFile = path.join(nwRef,"proj","3_nwProject.nwx")
|
||||
assert theProject.openProject(projFile)
|
||||
assert isinstance(theProject.newFile("Hello", nwItemClass.NOVEL, "73475cb40a568"), str)
|
||||
assert isinstance(theProject.newFile("Jane", nwItemClass.CHARACTER, "44cb730c42048"), str)
|
||||
assert isinstance(theProject.newFile("Jane", nwItemClass.CHARACTER, "71ee45a3c0db9"), str)
|
||||
assert theProject.projChanged
|
||||
assert theProject.saveProject()
|
||||
assert theProject.closeProject()
|
||||
@@ -142,9 +142,9 @@ def testIndexCheckThese(nwTempProj):
|
||||
assert theProject.openProject(projFile)
|
||||
|
||||
theIndex = NWIndex(theProject, theMain)
|
||||
nHandle = "41cfc0d1f2d12"
|
||||
nHandle = "0e17daca5f3e1"
|
||||
nItem = theProject.projTree[nHandle]
|
||||
cHandle = "2858dcd1057d3"
|
||||
cHandle = "02d20bbd7e394"
|
||||
cItem = theProject.projTree[cHandle]
|
||||
|
||||
assert theIndex.scanText(cHandle, (
|
||||
@@ -155,7 +155,7 @@ def testIndexCheckThese(nwTempProj):
|
||||
"# Hello World!\n"
|
||||
"@pov: Jane"
|
||||
))
|
||||
assert str(theIndex.tagIndex) == "{'Jane': [2, '2858dcd1057d3', 'CHARACTER', 'T000001']}"
|
||||
assert str(theIndex.tagIndex) == "{'Jane': [2, '02d20bbd7e394', 'CHARACTER', 'T000001']}"
|
||||
assert theIndex.novelIndex[nHandle]["T000001"]["title"] == "Hello World!"
|
||||
|
||||
assert str(theIndex.checkThese(["@tag", "Jane"], cItem)) == "[True, True]"
|
||||
@@ -175,9 +175,9 @@ def testIndexMeta(nwTempProj):
|
||||
assert theProject.openProject(projFile)
|
||||
|
||||
theIndex = NWIndex(theProject, theMain)
|
||||
nHandle = "41cfc0d1f2d12"
|
||||
nHandle = "0e17daca5f3e1"
|
||||
nItem = theProject.projTree[nHandle]
|
||||
cHandle = "2858dcd1057d3"
|
||||
cHandle = "02d20bbd7e394"
|
||||
cItem = theProject.projTree[cHandle]
|
||||
|
||||
assert theIndex.scanText(cHandle, (
|
||||
@@ -195,11 +195,11 @@ def testIndexMeta(nwTempProj):
|
||||
"\n"
|
||||
"Well, not really.\n"
|
||||
))
|
||||
assert str(theIndex.tagIndex) == "{'Jane': [2, '2858dcd1057d3', 'CHARACTER', 'T000001']}"
|
||||
assert str(theIndex.tagIndex) == "{'Jane': [2, '02d20bbd7e394', 'CHARACTER', 'T000001']}"
|
||||
assert theIndex.novelIndex[nHandle]["T000001"]["title"] == "Hello World!"
|
||||
|
||||
# The novel structure should contain the pointer to the novel file header
|
||||
assert str(theIndex.getNovelStructure()) == "['41cfc0d1f2d12:T000001']"
|
||||
assert str(theIndex.getNovelStructure()) == "['0e17daca5f3e1:T000001']"
|
||||
|
||||
# The novel file should have the correct counts
|
||||
cC, wC, pC = theIndex.getCounts(nHandle)
|
||||
@@ -214,6 +214,59 @@ def testIndexMeta(nwTempProj):
|
||||
|
||||
# The character file should have a record of the reference from the novel file
|
||||
theRefs = theIndex.getBackReferenceList(cHandle)
|
||||
assert str(theRefs) == "{'41cfc0d1f2d12': 'T000001'}"
|
||||
assert str(theRefs) == "{'0e17daca5f3e1': 'T000001'}"
|
||||
|
||||
assert theProject.closeProject()
|
||||
|
||||
# The two following tests must be at the end as they mess up the config object
|
||||
# and the handle seed. They go into their own folders, but use the same project
|
||||
# object as the test above.
|
||||
|
||||
@pytest.mark.project
|
||||
def testProjectNewCustom(nwTempCustom, nwRef, nwTemp):
|
||||
projData = {
|
||||
"projName": "Test Custom",
|
||||
"projTitle": "Test Novel",
|
||||
"projAuthors": "Jane Doe\nJohn Doh\n",
|
||||
"projPath": nwTempCustom,
|
||||
"popSample": False,
|
||||
"popMinimal": False,
|
||||
"popCustom": True,
|
||||
"addRoots": [
|
||||
nwItemClass.PLOT,
|
||||
nwItemClass.CHARACTER,
|
||||
nwItemClass.WORLD,
|
||||
nwItemClass.TIMELINE,
|
||||
nwItemClass.OBJECT,
|
||||
nwItemClass.ENTITY,
|
||||
],
|
||||
"numChapters": 3,
|
||||
"numScenes": 3,
|
||||
"chFolders": True,
|
||||
}
|
||||
theProject.mainConf = theConf
|
||||
theProject.projTree.setSeed(42)
|
||||
assert theProject.newProject(projData)
|
||||
assert theProject.saveProject()
|
||||
assert theProject.closeProject()
|
||||
projFile = path.join(nwTempCustom, "nwProject.nwx")
|
||||
refFile = path.join(nwRef, "proj", "4_nwProject.nwx")
|
||||
assert cmpFiles(projFile, refFile, [2, 6, 7, 8])
|
||||
|
||||
@pytest.mark.project
|
||||
def testProjectNewSample(nwTempSample, nwLipsum, nwRef, nwTemp):
|
||||
projData = {
|
||||
"projName": "Test Sample",
|
||||
"projTitle": "Test Novel",
|
||||
"projAuthors": "Jane Doe\nJohn Doh\n",
|
||||
"projPath": nwTempSample,
|
||||
"popSample": True,
|
||||
"popMinimal": False,
|
||||
"popCustom": False,
|
||||
}
|
||||
theProject.mainConf = theConf
|
||||
assert theProject.newProject(projData)
|
||||
assert theProject.openProject(nwTempSample)
|
||||
assert theProject.projName == "Sample Project"
|
||||
assert theProject.saveProject()
|
||||
assert theProject.closeProject()
|
||||
|
||||
Reference in New Issue
Block a user