diff --git a/.codecov.yml b/.codecov.yml
index 45ebc6c7..9166a2e1 100644
--- a/.codecov.yml
+++ b/.codecov.yml
@@ -10,7 +10,9 @@ coverage:
project:
default:
threshold: 1%
- patch: no
+ patch:
+ default:
+ threshold: 1%
changes: no
parsers:
diff --git a/.gitignore b/.gitignore
index 1cc84316..a515c603 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,6 +15,7 @@ novelWriter.qhc
__pycache__
# Sample Project
+/nw/assets/sample.zip
/sample/cache
/sample/meta
*.bak
diff --git a/nw/assets/images/wizard-back.jpg b/nw/assets/images/wizard-back.jpg
new file mode 100644
index 00000000..8db614c8
Binary files /dev/null and b/nw/assets/images/wizard-back.jpg differ
diff --git a/nw/common.py b/nw/common.py
index b15e148a..0f4d6ef4 100644
--- a/nw/common.py
+++ b/nw/common.py
@@ -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
diff --git a/nw/core/project.py b/nw/core/project.py
index 683affbe..3404370e 100644
--- a/nw/core/project.py
+++ b/nw/core/project.py
@@ -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.
"""
diff --git a/nw/core/tree.py b/nw/core/tree.py
index e786a727..bafb5306 100644
--- a/nw/core/tree.py
+++ b/nw/core/tree.py
@@ -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
##
diff --git a/nw/gui/__init__.py b/nw/gui/__init__.py
index 6359a342..cc2933e7 100644
--- a/nw/gui/__init__.py
+++ b/nw/gui/__init__.py
@@ -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",
diff --git a/nw/gui/about.py b/nw/gui/about.py
index 689a0aca..02f9fa39 100644
--- a/nw/gui/about.py
+++ b/nw/gui/about.py
@@ -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("%s" % 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)
diff --git a/nw/gui/build.py b/nw/gui/build.py
index 58b152f8..ce14db2c 100644
--- a/nw/gui/build.py
+++ b/nw/gui/build.py
@@ -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)
diff --git a/nw/gui/projload.py b/nw/gui/projload.py
index eddbbb33..1c37d9c4 100644
--- a/nw/gui/projload.py
+++ b/nw/gui/projload.py
@@ -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:
diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py
index 124e0a59..ce7bf499 100644
--- a/nw/gui/projtree.py
+++ b/nw/gui/projtree.py
@@ -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
diff --git a/nw/gui/projwizard.py b/nw/gui/projwizard.py
new file mode 100644
index 00000000..e95f0a26
--- /dev/null
+++ b/nw/gui/projwizard.py
@@ -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 .
+"""
+
+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((
+ "
All done.
"
+ "Press '{finish}' to create the new project.
"
+ ).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
diff --git a/nw/gui/theme.py b/nw/gui/theme.py
index 134d91dd..226ccecb 100644
--- a/nw/gui/theme.py
+++ b/nw/gui/theme.py
@@ -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])
diff --git a/nw/guimain.py b/nw/guimain.py
index 8c760531..73c7eef0 100644
--- a/nw/guimain.py
+++ b/nw/guimain.py
@@ -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
##
diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx
index 5be0eab5..d52d4f27 100644
--- a/sample/nwProject.nwx
+++ b/sample/nwProject.nwx
@@ -1,13 +1,13 @@
-
+
Sample Project
Sample Project
Jane Smith
Jay Doh
- 675
+ 676
122
- 32866
+ 32870
False
diff --git a/setup.py b/setup.py
index 818af1a2..1ae7d6b7 100755
--- a/setup.py
+++ b/setup.py
@@ -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)
diff --git a/tests/conftest.py b/tests/conftest.py
index c064c1d8..09031f4f 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -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__)
diff --git a/tests/nwdummy.py b/tests/nwdummy.py
index 2c54a92a..2e46352c 100644
--- a/tests/nwdummy.py
+++ b/tests/nwdummy.py
@@ -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
diff --git a/tests/nwtools.py b/tests/nwtools.py
index fecb1df9..9baefe2d 100644
--- a/tests/nwtools.py
+++ b/tests/nwtools.py
@@ -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
diff --git a/tests/reference/gui/0_nwProject.nwx b/tests/reference/gui/0_nwProject.nwx
index d1a794e7..4bd15d6f 100644
--- a/tests/reference/gui/0_nwProject.nwx
+++ b/tests/reference/gui/0_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
New Project
@@ -13,8 +13,8 @@
True
None
None
- 0
- 0
+ 6
+ 6
0
@@ -37,7 +37,7 @@
Main
-
+
-
Novel
ROOT
@@ -46,35 +46,59 @@
False
-
+ Title Page
+ FILE
+ NOVEL
+ New
+ True
+ TITLE
+ 11
+ 2
+ 0
+ 0
+
+ -
New Chapter
FOLDER
NOVEL
New
False
- -
+
-
+ New Chapter
+ FILE
+ NOVEL
+ New
+ True
+ CHAPTER
+ 11
+ 2
+ 0
+ 0
+
+ -
New Scene
FILE
NOVEL
New
True
SCENE
- 0
- 0
+ 9
+ 2
0
0
-
- Characters
+ Plot
ROOT
- CHARACTER
+ PLOT
New
False
-
- Plot
+ Characters
ROOT
- PLOT
+ CHARACTER
New
False
diff --git a/tests/reference/gui/1_031b4af5197ec.nwd b/tests/reference/gui/1_031b4af5197ec.nwd
new file mode 100644
index 00000000..9a330c9c
--- /dev/null
+++ b/tests/reference/gui/1_031b4af5197ec.nwd
@@ -0,0 +1,6 @@
+%%~ 031b4af5197ec:44cb730c42048:PLOT:NOTE:New File
+# Main Plot
+
+@tag: MainPlot
+
+This is a file detailing the main plot.
diff --git a/tests/reference/gui/1_0e17daca5f3e1.nwd b/tests/reference/gui/1_0e17daca5f3e1.nwd
index ecfbf8ff..f7723099 100644
--- a/tests/reference/gui/1_0e17daca5f3e1.nwd
+++ b/tests/reference/gui/1_0e17daca5f3e1.nwd
@@ -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.
diff --git a/tests/reference/gui/1_1a6562590ef19.nwd b/tests/reference/gui/1_1a6562590ef19.nwd
index 7c6d19a7..de40e292 100644
--- a/tests/reference/gui/1_1a6562590ef19.nwd
+++ b/tests/reference/gui/1_1a6562590ef19.nwd
@@ -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.
diff --git a/tests/reference/gui/1_31489056e0916.nwd b/tests/reference/gui/1_31489056e0916.nwd
deleted file mode 100644
index 0a238e45..00000000
--- a/tests/reference/gui/1_31489056e0916.nwd
+++ /dev/null
@@ -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 …
-
diff --git a/tests/reference/gui/1_41cfc0d1f2d12.nwd b/tests/reference/gui/1_41cfc0d1f2d12.nwd
new file mode 100644
index 00000000..e80cbd1b
--- /dev/null
+++ b/tests/reference/gui/1_41cfc0d1f2d12.nwd
@@ -0,0 +1,6 @@
+%%~ 41cfc0d1f2d12:811786ad1ae74:WORLD:NOTE:New File
+# Main Location
+
+@tag: Home
+
+This is a file describing Jane’s home.
diff --git a/tests/reference/gui/1_98010bd9270f9.nwd b/tests/reference/gui/1_98010bd9270f9.nwd
deleted file mode 100644
index 632f53cd..00000000
--- a/tests/reference/gui/1_98010bd9270f9.nwd
+++ /dev/null
@@ -1,6 +0,0 @@
-%%~ 98010bd9270f9:44cb730c42048:CHARACTER:NOTE:New File
-# Jane Doe
-
-@tag: Jane
-
-This is a file about Jane.
diff --git a/tests/reference/gui/1_nwProject.nwx b/tests/reference/gui/1_nwProject.nwx
index e8a705b1..43dcf523 100644
--- a/tests/reference/gui/1_nwProject.nwx
+++ b/tests/reference/gui/1_nwProject.nwx
@@ -1,20 +1,20 @@
-
+
New Project
5
1
- 11
+ 14
True
True
True
- 31489056e0916
+ 0e17daca5f3e1
None
- 86
- 59
+ 90
+ 63
27
@@ -37,7 +37,7 @@
Main
-
+
-
Novel
ROOT
@@ -46,13 +46,37 @@
True
-
+ Title Page
+ FILE
+ NOVEL
+ New
+ True
+ TITLE
+ 11
+ 2
+ 0
+ 0
+
+ -
New Chapter
FOLDER
NOVEL
New
True
- -
+
-
+ New Chapter
+ FILE
+ NOVEL
+ New
+ True
+ CHAPTER
+ 11
+ 2
+ 0
+ 0
+
+ -
New Scene
FILE
NOVEL
@@ -65,32 +89,13 @@
465
-
- Characters
- ROOT
- CHARACTER
- New
- True
-
- -
- New File
- FILE
- CHARACTER
- New
- True
- NOTE
- 34
- 8
- 1
- 51
-
- -
Plot
ROOT
PLOT
New
True
- -
+
-
New File
FILE
PLOT
@@ -102,6 +107,25 @@
1
69
+ -
+ Characters
+ ROOT
+ CHARACTER
+ New
+ True
+
+ -
+ New File
+ FILE
+ CHARACTER
+ New
+ True
+ NOTE
+ 34
+ 8
+ 1
+ 51
+
-
World
ROOT
@@ -109,7 +133,7 @@
New
True
- -
+
-
New File
FILE
WORLD
@@ -121,7 +145,7 @@
1
68
- -
+
-
Trash
TRASH
TRASH
diff --git a/tests/reference/gui/2_nwProject.nwx b/tests/reference/gui/2_nwProject.nwx
index 0815f9ad..1e3a2ca3 100644
--- a/tests/reference/gui/2_nwProject.nwx
+++ b/tests/reference/gui/2_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
Project Name
Project Title
@@ -7,7 +7,7 @@
John Doh
2
1
- 1
+ 0
True
@@ -15,8 +15,8 @@
True
None
None
- 0
- 0
+ 6
+ 6
0
With This Stuff
@@ -41,7 +41,7 @@
Main
-
+
-
Novel
ROOT
@@ -50,35 +50,59 @@
False
-
+ Title Page
+ FILE
+ NOVEL
+ New
+ True
+ TITLE
+ 11
+ 2
+ 0
+ 0
+
+ -
New Chapter
FOLDER
NOVEL
New
False
- -
+
-
+ New Chapter
+ FILE
+ NOVEL
+ New
+ True
+ CHAPTER
+ 11
+ 2
+ 0
+ 0
+
+ -
New Scene
FILE
NOVEL
New
True
SCENE
- 0
- 0
+ 9
+ 2
0
0
-
- Characters
+ Plot
ROOT
- CHARACTER
+ PLOT
New
False
-
- Plot
+ Characters
ROOT
- PLOT
+ CHARACTER
New
False
diff --git a/tests/reference/gui/3_nwProject.nwx b/tests/reference/gui/3_nwProject.nwx
index 8eeb4dca..11be0c01 100644
--- a/tests/reference/gui/3_nwProject.nwx
+++ b/tests/reference/gui/3_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
New Project
@@ -11,10 +11,10 @@
True
False
True
- 31489056e0916
+ 0e17daca5f3e1
None
- 59
- 59
+ 6
+ 6
0
@@ -37,7 +37,7 @@
Main
-
+
-
Novel
ROOT
@@ -46,35 +46,59 @@
False
-
+ Title Page
+ FILE
+ NOVEL
+ New
+ True
+ TITLE
+ 11
+ 2
+ 0
+ 0
+
+ -
New Chapter
FOLDER
NOVEL
New
False
- -
+
-
+ New Chapter
+ FILE
+ NOVEL
+ New
+ True
+ CHAPTER
+ 11
+ 2
+ 0
+ 0
+
+ -
Just a Page
FILE
NOVEL
Note
False
PAGE
- 331
- 59
- 2
+ 9
+ 2
+ 0
0
-
- Characters
+ Plot
ROOT
- CHARACTER
+ PLOT
New
False
-
- Plot
+ Characters
ROOT
- PLOT
+ CHARACTER
New
False
diff --git a/tests/reference/proj/1_nwProject.nwx b/tests/reference/proj/1_nwProject.nwx
index 12697dbe..0c891d8b 100644
--- a/tests/reference/proj/1_nwProject.nwx
+++ b/tests/reference/proj/1_nwProject.nwx
@@ -1,10 +1,10 @@
-
+
New Project
1
- 0
+ 1
0
@@ -37,7 +37,7 @@
Main
-
+
-
Novel
ROOT
@@ -46,16 +46,16 @@
False
-
- Characters
+ Plot
ROOT
- CHARACTER
+ PLOT
New
False
-
- Plot
+ Characters
ROOT
- PLOT
+ CHARACTER
New
False
@@ -67,13 +67,37 @@
False
-
+ Title Page
+ FILE
+ NOVEL
+ New
+ True
+ TITLE
+ 0
+ 0
+ 0
+ 0
+
+ -
New Chapter
FOLDER
NOVEL
New
False
- -
+
-
+ New Chapter
+ FILE
+ NOVEL
+ New
+ True
+ CHAPTER
+ 0
+ 0
+ 0
+ 0
+
+ -
New Scene
FILE
NOVEL
diff --git a/tests/reference/proj/2_nwProject.nwx b/tests/reference/proj/2_nwProject.nwx
index ed6bbbe4..fd73b637 100644
--- a/tests/reference/proj/2_nwProject.nwx
+++ b/tests/reference/proj/2_nwProject.nwx
@@ -1,10 +1,10 @@
-
+
New Project
4
- 0
+ 1
0
@@ -37,7 +37,7 @@
Main
-
+
-
Novel
ROOT
@@ -46,16 +46,16 @@
False
-
- Characters
+ Plot
ROOT
- CHARACTER
+ PLOT
New
False
-
- Plot
+ Characters
ROOT
- PLOT
+ CHARACTER
New
False
@@ -67,13 +67,37 @@
False
-
+ Title Page
+ FILE
+ NOVEL
+ New
+ True
+ TITLE
+ 0
+ 0
+ 0
+ 0
+
+ -
New Chapter
FOLDER
NOVEL
New
False
- -
+
-
+ New Chapter
+ FILE
+ NOVEL
+ New
+ True
+ CHAPTER
+ 0
+ 0
+ 0
+ 0
+
+ -
New Scene
FILE
NOVEL
@@ -85,28 +109,28 @@
0
0
- -
+
-
Timeline
ROOT
TIMELINE
New
False
- -
+
-
Object
ROOT
OBJECT
New
False
- -
+
-
Custom1
ROOT
CUSTOM
New
False
- -
+
-
Custom2
ROOT
CUSTOM
diff --git a/tests/reference/proj/3_nwProject.nwx b/tests/reference/proj/3_nwProject.nwx
index 58bef95d..0c396343 100644
--- a/tests/reference/proj/3_nwProject.nwx
+++ b/tests/reference/proj/3_nwProject.nwx
@@ -1,10 +1,10 @@
-
+
New Project
5
- 0
+ 1
0
@@ -37,7 +37,7 @@
Main
-
+
-
Novel
ROOT
@@ -46,16 +46,16 @@
False
-
- Characters
+ Plot
ROOT
- CHARACTER
+ PLOT
New
False
-
- Plot
+ Characters
ROOT
- PLOT
+ CHARACTER
New
False
@@ -67,13 +67,37 @@
False
-
+ Title Page
+ FILE
+ NOVEL
+ New
+ True
+ TITLE
+ 0
+ 0
+ 0
+ 0
+
+ -
New Chapter
FOLDER
NOVEL
New
False
- -
+
-
+ New Chapter
+ FILE
+ NOVEL
+ New
+ True
+ CHAPTER
+ 0
+ 0
+ 0
+ 0
+
+ -
New Scene
FILE
NOVEL
@@ -85,35 +109,35 @@
0
0
- -
+
-
Timeline
ROOT
TIMELINE
New
False
- -
+
-
Object
ROOT
OBJECT
New
False
- -
+
-
Custom1
ROOT
CUSTOM
New
False
- -
+
-
Custom2
ROOT
CUSTOM
New
False
- -
+
-
Hello
FILE
NOVEL
@@ -125,7 +149,7 @@
0
0
- -
+
-
Jane
FILE
CHARACTER
diff --git a/tests/reference/proj/4_nwProject.nwx b/tests/reference/proj/4_nwProject.nwx
new file mode 100644
index 00000000..3dd716cc
--- /dev/null
+++ b/tests/reference/proj/4_nwProject.nwx
@@ -0,0 +1,270 @@
+
+
+
+ Test Custom
+ Test Novel
+ Jane Doe
+ John Doh
+ 1
+ 1
+ 0
+
+
+ True
+ False
+ True
+ None
+ None
+ 0
+ 0
+ 0
+
+
+ %title%
+ Chapter %ch%: %title%
+ %title%
+ * * *
+
+
+
+ New
+ Note
+ Draft
+ Finished
+
+
+ New
+ Minor
+ Major
+ Main
+
+
+
+
-
+ Novel
+ ROOT
+ NOVEL
+ New
+ False
+
+ -
+ Plot
+ ROOT
+ PLOT
+ New
+ False
+
+ -
+ Characters
+ ROOT
+ CHARACTER
+ New
+ False
+
+ -
+ Locations
+ ROOT
+ WORLD
+ New
+ False
+
+ -
+ Timeline
+ ROOT
+ TIMELINE
+ New
+ False
+
+ -
+ Objects
+ ROOT
+ OBJECT
+ New
+ False
+
+ -
+ Entity
+ ROOT
+ ENTITY
+ New
+ False
+
+ -
+ Title Page
+ FILE
+ NOVEL
+ New
+ True
+ TITLE
+ 0
+ 0
+ 0
+ 0
+
+ -
+ Chapter 1
+ FOLDER
+ NOVEL
+ New
+ False
+
+ -
+ Chapter 1
+ FILE
+ NOVEL
+ New
+ True
+ CHAPTER
+ 0
+ 0
+ 0
+ 0
+
+ -
+ Scene 1.1
+ FILE
+ NOVEL
+ New
+ True
+ SCENE
+ 0
+ 0
+ 0
+ 0
+
+ -
+ Scene 1.2
+ FILE
+ NOVEL
+ New
+ True
+ SCENE
+ 0
+ 0
+ 0
+ 0
+
+ -
+ Scene 1.3
+ FILE
+ NOVEL
+ New
+ True
+ SCENE
+ 0
+ 0
+ 0
+ 0
+
+ -
+ Chapter 2
+ FOLDER
+ NOVEL
+ New
+ False
+
+ -
+ Chapter 2
+ FILE
+ NOVEL
+ New
+ True
+ CHAPTER
+ 0
+ 0
+ 0
+ 0
+
+ -
+ Scene 2.1
+ FILE
+ NOVEL
+ New
+ True
+ SCENE
+ 0
+ 0
+ 0
+ 0
+
+ -
+ Scene 2.2
+ FILE
+ NOVEL
+ New
+ True
+ SCENE
+ 0
+ 0
+ 0
+ 0
+
+ -
+ Scene 2.3
+ FILE
+ NOVEL
+ New
+ True
+ SCENE
+ 0
+ 0
+ 0
+ 0
+
+ -
+ Chapter 3
+ FOLDER
+ NOVEL
+ New
+ False
+
+ -
+ Chapter 3
+ FILE
+ NOVEL
+ New
+ True
+ CHAPTER
+ 0
+ 0
+ 0
+ 0
+
+ -
+ Scene 3.1
+ FILE
+ NOVEL
+ New
+ True
+ SCENE
+ 0
+ 0
+ 0
+ 0
+
+ -
+ Scene 3.2
+ FILE
+ NOVEL
+ New
+ True
+ SCENE
+ 0
+ 0
+ 0
+ 0
+
+ -
+ Scene 3.3
+ FILE
+ NOVEL
+ New
+ True
+ SCENE
+ 0
+ 0
+ 0
+ 0
+
+
+
diff --git a/tests/test_gui.py b/tests/test_gui.py
index 7660497f..ebafbab5 100644
--- a/tests/test_gui.py
+++ b/tests/test_gui.py
@@ -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):
diff --git a/tests/test_project.py b/tests/test_project.py
index a4ae401e..e27df3c4 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -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()