diff --git a/.gitignore b/.gitignore
index ff89acd8..cf07a362 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,12 @@
+# Python Temp
__pycache__
-*.bak
+
+# Sample Project
+sample/**/cache
+sample/**/wordlist.txt
+sample/**/*.bak
+
+# PyTest
+tests/temp
+.pytest_cache
+pytestdebug.log
diff --git a/nw/__init__.py b/nw/__init__.py
index c1388a58..a1755d6c 100644
--- a/nw/__init__.py
+++ b/nw/__init__.py
@@ -64,8 +64,7 @@ logger = logging.getLogger(__name__)
CONFIG = Config()
def main(sysArgs):
- """
- Parses command line, sets up logging, and launches main GUI.
+ """Parses command line, sets up logging, and launches main GUI.
"""
# Valid Input Options
@@ -149,10 +148,10 @@ def main(sysArgs):
showGUI = False
elif inOpt in ("-D","--debuggui"):
debugLevel = logging.DEBUG
- debugStr = "{name:>22}:{lineno:<4d} {levelname:8} {message:}"
+ debugStr = "{name:>20}:{lineno:<4d} {levelname:8} {message:}"
debugGUI = True
- # Set GUI options
+ # Set Config Options
CONFIG.showGUI = showGUI
CONFIG.debugGUI = debugGUI
@@ -179,6 +178,8 @@ def main(sysArgs):
logger.setLevel(debugLevel)
+ CONFIG.initConfig(confPath)
+
nwApp = QApplication([])
nwGUI = NovelWriter()
exit(nwApp.exec_())
diff --git a/nw/config.py b/nw/config.py
index 68b76411..254c06df 100644
--- a/nw/config.py
+++ b/nw/config.py
@@ -6,7 +6,7 @@
This class reads and store the main preferences of the application
File History:
- Created: 2018-0+-22 [0.0.1]
+ Created: 2018-09-22 [0.0.1]
"""
@@ -28,25 +28,18 @@ class Config:
def __init__(self):
# Set Application Variables
- self.appName = nw.__package__
- self.appHandle = nw.__package__.lower()
- self.showGUI = True
- self.debugGUI = False
+ self.appName = nw.__package__
+ self.appHandle = nw.__package__.lower()
+ self.showGUI = True
+ self.debugGUI = False
# Set Paths
- self.confPath = user_config_dir(self.appHandle)
- self.confFile = self.appHandle+".conf"
- self.homePath = path.expanduser("~")
- self.appPath = path.dirname(__file__)
- self.guiPath = path.join(self.appPath,"gui")
- self.themePath = path.join(self.appPath,"themes")
- self.recentList = [""]*10
-
- # If config folder does not exist, make it.
- # This assumes that the os config folder itself exists.
- # TODO: This does not work on Windows
- if not path.isdir(self.confPath):
- mkdir(self.confPath)
+ self.confPath = None
+ self.confFile = None
+ self.homePath = None
+ self.appPath = None
+ self.guiPath = None
+ self.themePath = None
# Set default values
self.confChanged = False
@@ -81,13 +74,8 @@ class Config:
self.spellLanguage = "en_GB"
- # Check if config file exists
- if path.isfile(path.join(self.confPath,self.confFile)):
- self.loadConfig()
-
- # Save a copy of the default config if no file exists
- if not path.isfile(path.join(self.confPath,self.confFile)):
- self.saveConfig()
+ # Path
+ self.recentList = [""]*10
return
@@ -95,11 +83,41 @@ class Config:
# Actions
##
+ def initConfig(self, confPath=None):
+
+ if confPath is None:
+ self.confPath = user_config_dir(self.appHandle)
+ else:
+ logger.info("Setting config from alternative path: %s" % confPath)
+ self.confPath = confPath
+
+ self.confFile = self.appHandle+".conf"
+ self.homePath = path.expanduser("~")
+ self.appPath = path.dirname(__file__)
+ self.guiPath = path.join(self.appPath,"gui")
+ self.themePath = path.join(self.appPath,"themes")
+
+ # If config folder does not exist, make it.
+ # This assumes that the os config folder itself exists.
+ # TODO: This does not work on Windows
+ if not path.isdir(self.confPath):
+ mkdir(self.confPath)
+
+ # Check if config file exists
+ if path.isfile(path.join(self.confPath,self.confFile)):
+ # If it exists, load it
+ self.loadConfig()
+ else:
+ # If it does not exist, save a copy of the defaults
+ self.saveConfig()
+
+ return True
+
def loadConfig(self):
logger.debug("Loading config file")
confParser = configparser.ConfigParser()
- confParser.readfp(open(path.join(self.confPath,self.confFile)))
+ confParser.read_file(open(path.join(self.confPath,self.confFile)))
# Get options
@@ -214,10 +232,14 @@ class Config:
confParser.set(cnfSec,"recent%d" % i, str(self.recentList[i]))
# Write config file
- confParser.write(open(path.join(self.confPath,self.confFile),"w"))
- self.confChanged = False
+ try:
+ confParser.write(open(path.join(self.confPath,self.confFile),"w"))
+ self.confChanged = False
+ except Exception as e:
+ logger.error("Could not save config file")
+ return False
- return
+ return True
def unpackList(self, inStr, listLen, listDefault, castTo=int):
inData = inStr.split(",")
diff --git a/nw/gui/docdetails.py b/nw/gui/docdetails.py
index 9851d590..c8b6a078 100644
--- a/nw/gui/docdetails.py
+++ b/nw/gui/docdetails.py
@@ -35,6 +35,7 @@ class GuiDocDetails(QFrame):
logger.debug("Initialising DocDetails ...")
self.mainConf = nw.CONFIG
self.debugGUI = self.mainConf.debugGUI
+ self.theParent = theParent
self.theProject = theProject
self.mainBox = QGridLayout(self)
@@ -70,11 +71,11 @@ class GuiDocDetails(QFrame):
colTwo = [""]*4
else:
itemStatus = nwItem.itemStatus
- if itemStatus < 0 or itemStatus >= len(self.theProject.statusLabels):
+ if itemStatus < 0 or itemStatus >= len(self.theParent.statusLabels):
itemStatus = 0
colTwo = [
nwItem.itemName,
- self.theProject.statusLabels[itemStatus],
+ self.theParent.statusLabels[itemStatus],
nwLabels.CLASS_NAME[nwItem.itemClass],
nwLabels.LAYOUT_NAME[nwItem.itemLayout],
]
diff --git a/nw/gui/doctree.py b/nw/gui/doctree.py
index 859ff0c9..4ef921f2 100644
--- a/nw/gui/doctree.py
+++ b/nw/gui/doctree.py
@@ -252,12 +252,12 @@ class GuiDocTree(QTreeWidget):
if nwItem.itemType == nwItemType.FILE:
tStatus += "."+nwLabels.LAYOUT_FLAG[nwItem.itemLayout]
nStatus = nwItem.itemStatus
- if nStatus < 0 or nStatus >= len(self.theProject.statusIcons):
+ if nStatus < 0 or nStatus >= len(self.theParent.statusIcons):
nStatus = 0
trItem.setText(self.C_NAME,tName)
trItem.setText(self.C_FLAGS,tStatus)
- trItem.setIcon(self.C_FLAGS,self.theProject.statusIcons[nStatus])
+ trItem.setIcon(self.C_FLAGS,self.theParent.statusIcons[nStatus])
return
diff --git a/nw/gui/winmain.py b/nw/gui/winmain.py
index ed5a369a..ab12372f 100644
--- a/nw/gui/winmain.py
+++ b/nw/gui/winmain.py
@@ -15,7 +15,7 @@ import nw
from os import path
from PyQt5.QtWidgets import QWidget, QMainWindow, QVBoxLayout, QFrame, QSplitter, QFileDialog, QStackedWidget, QShortcut, QMessageBox
-from PyQt5.QtGui import QIcon
+from PyQt5.QtGui import QIcon, QPixmap, QColor
from PyQt5.QtCore import Qt, QTimer
from nw.gui.doctree import GuiDocTree
@@ -57,6 +57,10 @@ class GuiMain(QMainWindow):
self.mainMenu = GuiMainMenu(self, self.theProject)
self.statusBar = GuiMainStatus(self)
+ # Minor Gui Elements
+ self.statusIcons = []
+ self.statusLabels = []
+
# Assemble Main Window
self.stackPane = QStackedWidget()
self.stackNone = self.stackPane.addWidget(QWidget())
@@ -82,7 +86,7 @@ class GuiMain(QMainWindow):
self.treeView.itemSelectionChanged.connect(self._treeSingleClick)
self.treeView.itemDoubleClicked.connect(self._treeDoubleClick)
self.treeView.buildTree()
- QShortcut(Qt.Key_Return, self.treeView, context=Qt.WidgetShortcut, activated=self._treeKeyPressReturn)
+ self._makeStatusIcons()
# Set Main Window Elements
self.setMenuBar(self.mainMenu)
@@ -106,7 +110,15 @@ class GuiMain(QMainWindow):
self.asDocTimer.timeout.connect(self._autoSaveDocument)
self.asDocTimer.start()
- self.show()
+ # Keyboard Shortcuts
+ QShortcut(Qt.Key_Return, self.treeView, context=Qt.WidgetShortcut, activated=self._treeKeyPressReturn)
+
+ # Forward Functions
+ self.setStatus = self.statusBar.setStatus
+ self.setProjectStatus = self.statusBar.setProjectStatus
+
+ if self.mainConf.showGUI:
+ self.show()
logger.debug("GUI initialisation complete")
@@ -162,6 +174,7 @@ class GuiMain(QMainWindow):
self.treeView.buildTree()
self.mainMenu.updateRecentProjects()
self._setWindowTitle(self.theProject.projName)
+ self._makeStatusIcons()
self.docEditor.setPwl(path.join(self.theProject.projMeta,"wordlist.txt"))
return True
@@ -341,6 +354,16 @@ class GuiMain(QMainWindow):
return False
return True
+ def _makeStatusIcons(self):
+ self.statusIcons = []
+ self.statusLabels = []
+ for sLabel, sR, sG, sB in self.theProject.statusCols:
+ theIcon = QPixmap(32,32)
+ theIcon.fill(QColor(sR,sG,sB))
+ self.statusIcons.append(QIcon(theIcon))
+ self.statusLabels.append(sLabel)
+ return
+
##
# Events
##
diff --git a/nw/project/project.py b/nw/project/project.py
index 812a43d2..25a96609 100644
--- a/nw/project/project.py
+++ b/nw/project/project.py
@@ -19,8 +19,6 @@ from hashlib import sha256
from datetime import datetime
from time import time
-from PyQt5.QtGui import QIcon, QPixmap, QColor
-
from nw.enum import nwItemType, nwItemClass, nwItemLayout
from nw.project.item import NWItem
@@ -31,10 +29,13 @@ class NWProject():
def __init__(self, theParent):
# Internal
- self.mainConf = nw.CONFIG
self.theParent = theParent
+ self.mainConf = self.theParent.mainConf
self.projChanged = None
+ # Debug
+ self.handleSeed = None
+
# Project Settings
self.projTree = None
self.treeOrder = None
@@ -48,8 +49,6 @@ class NWProject():
self.bookTitle = None
self.bookAuthors = None
self.statusCols = None
- self.statusIcons = None
- self.statusLabels = None
self.clearProject()
@@ -60,6 +59,9 @@ class NWProject():
##
def newRoot(self, rootName, rootClass):
+ if not self.checkRootUnique(rootClass):
+ self.theParent.makeAlert("Duplicate root item detected!",2)
+ return None
newItem = NWItem()
newItem.setName(rootName)
newItem.setType(nwItemType.ROOT)
@@ -110,7 +112,7 @@ class NWProject():
hChapt = self.newFolder("New Chapter", nwItemClass.NOVEL, hNovel)
hScene = self.newFile("New Scene", nwItemClass.NOVEL, hChapt)
- return
+ return True
def clearProject(self):
@@ -128,16 +130,12 @@ class NWProject():
self.projName = ""
self.bookTitle = ""
self.bookAuthors = []
-
self.statusCols = [
("New", 100,100,100),
("Note", 200, 50, 0),
("Draft", 200,150, 0),
("Finished", 50,200, 0),
]
- self.statusIcons = []
- self.statusLabels = []
- self._makeStatusIcons()
return
@@ -205,9 +203,8 @@ class NWProject():
nwItem.setFromTag(xValue.tag,xValue.text)
self._appendItem(tHandle,pHandle,nwItem)
- self._makeStatusIcons()
self.mainConf.setRecent(self.projPath)
- self.theParent.statusBar.setStatus("Opened Project: %s" % self.projName)
+ self.theParent.setStatus("Opened Project: %s" % self.projName)
self._scanProjectFolder()
self.setProjectChanged(False)
@@ -267,7 +264,7 @@ class NWProject():
return False
self.mainConf.setRecent(self.projPath)
- self.theParent.statusBar.setStatus("Saved Project: %s" % self.projName)
+ self.theParent.setStatus("Saved Project: %s" % self.projName)
self.setProjectChanged(False)
return True
@@ -310,7 +307,7 @@ class NWProject():
def setProjectChanged(self, bValue):
self.projChanged = bValue
- self.theParent.statusBar.setProjectStatus(self.projChanged)
+ self.theParent.setProjectStatus(self.projChanged)
return
##
@@ -430,18 +427,13 @@ class NWProject():
return
- def _makeStatusIcons(self):
- self.statusIcons = []
- self.statusLabels = []
- for sLabel, sR, sG, sB in self.statusCols:
- theIcon = QPixmap(32,32)
- theIcon.fill(QColor(sR,sG,sB))
- self.statusIcons.append(QIcon(theIcon))
- self.statusLabels.append(sLabel)
- return
-
def _makeHandle(self, addSeed=""):
- newSeed = str(time()) + addSeed
+ if self.handleSeed is None:
+ newSeed = str(time()) + addSeed
+ else:
+ # This is used for debugging
+ newSeed = str(self.handleSeed)
+ self.handleSeed += 1
logger.verbose("Generating handle with seed '%s'" % newSeed)
itemHandle = sha256(newSeed.encode()).hexdigest()[0:13]
if itemHandle in self.projTree.keys():
diff --git a/tests/nwdummy.py b/tests/nwdummy.py
new file mode 100644
index 00000000..d033668e
--- /dev/null
+++ b/tests/nwdummy.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+"""novelWriter Test Dummy GUI Classes
+"""
+
+class DummyMain():
+
+ def __init__(self):
+ self.mainConf = None
+ return
+
+ def makeAlert(self, theMessage, theLevel):
+ if theLevel == 1:
+ lvlMsg = "WARNING: "
+ elif theLevel == 2:
+ lvlMsg = "ERROR: "
+ else:
+ lvlMsg = ""
+ if isinstance(theMessage, list):
+ for msgLine in logMsg:
+ print(lvlMsg+msgLine)
+ else:
+ print(lvlMsg+theMessage)
+ return
+
+ def setStatus(self, theMessage):
+ return
+
+ def setProjectStatus(self, isChanged):
+ return
+
+# END Class GuiMain
diff --git a/tests/nwtools.py b/tests/nwtools.py
new file mode 100644
index 00000000..dc3bd24b
--- /dev/null
+++ b/tests/nwtools.py
@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+"""novelWriter Test Tools
+"""
+
+from os import path, mkdir
+
+def ensureDir(theDir):
+ if not path.isdir(theDir):
+ mkdir(theDir)
+ return
+
+def cmpFiles(fileOne, fileTwo, ignoreLines=[]):
+ foOne = open(fileOne,mode="r")
+ foTwo = open(fileTwo,mode="r")
+
+ txtOne = foOne.readlines()
+ txtTwo = foTwo.readlines()
+
+ if len(txtOne) != len(txtTwo):
+ print("Files are not the same length")
+ return False
+
+ diffFound = False
+ for n in range(len(txtOne)):
+ lnOne = txtOne[n].strip()
+ lnTwo = txtTwo[n].strip()
+
+ if n+1 in ignoreLines:
+ print("Ignoring line %d" % (n+1))
+ continue
+
+ if lnOne != lnTwo:
+ print("Diff on line %d:" % (n+1))
+ print(" << '%s'" % lnOne)
+ print(" >> '%s'" % lnTwo)
+ diffFound = True
+
+ foOne.close()
+ foTwo.close()
+
+ return not diffFound
diff --git a/tests/reference/new_nwProject.nwx b/tests/reference/new_nwProject.nwx
new file mode 100644
index 00000000..742afe41
--- /dev/null
+++ b/tests/reference/new_nwProject.nwx
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+ -
+ Novel
+ ROOT
+ NOVEL
+ 0
+ False
+
+ -
+ Characters
+ ROOT
+ CHARACTER
+ 0
+ False
+
+ -
+ Plot
+ ROOT
+ PLOT
+ 0
+ False
+
+ -
+ World
+ ROOT
+ WORLD
+ 0
+ False
+
+ -
+ New Chapter
+ FOLDER
+ NOVEL
+ 0
+ False
+
+ -
+ New Scene
+ FILE
+ NOVEL
+ 0
+ False
+ SCENE
+ 0
+ 0
+ 0
+
+
+
diff --git a/tests/reference/novelwriter.conf b/tests/reference/novelwriter.conf
new file mode 100644
index 00000000..00e800be
--- /dev/null
+++ b/tests/reference/novelwriter.conf
@@ -0,0 +1,38 @@
+[Main]
+timestamp = 2019-05-12 12:04:16
+
+[Sizes]
+geometry = 1100, 650
+treecols = 120, 30, 50
+mainpane = 300, 800
+
+[Project]
+autosaveproject = 60
+autosavedoc = 30
+
+[Editor]
+fixedwidth = True
+width = 600
+margins = 40, 40
+textsize = 13
+justify = True
+autoselect = True
+autoreplace = True
+repsquotes = True
+repdquotes = True
+repdash = True
+repdots = True
+spellcheck = en_GB
+
+[Path]
+recent0 =
+recent1 =
+recent2 =
+recent3 =
+recent4 =
+recent5 =
+recent6 =
+recent7 =
+recent8 =
+recent9 =
+
diff --git a/tests/reference/roots_nwProject.nwx b/tests/reference/roots_nwProject.nwx
new file mode 100644
index 00000000..1971370a
--- /dev/null
+++ b/tests/reference/roots_nwProject.nwx
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+ -
+ Novel
+ ROOT
+ NOVEL
+ 0
+ False
+
+ -
+ Characters
+ ROOT
+ CHARACTER
+ 0
+ False
+
+ -
+ Plot
+ ROOT
+ PLOT
+ 0
+ False
+
+ -
+ World
+ ROOT
+ WORLD
+ 0
+ False
+
+ -
+ New Chapter
+ FOLDER
+ NOVEL
+ 0
+ False
+
+ -
+ New Scene
+ FILE
+ NOVEL
+ 0
+ False
+ SCENE
+ 0
+ 0
+ 0
+
+ -
+ Timeline
+ ROOT
+ TIMELINE
+ 0
+ False
+
+ -
+ Object
+ ROOT
+ OBJECT
+ 0
+ False
+
+ -
+ Custom1
+ ROOT
+ CUSTOM
+ 0
+ False
+
+ -
+ Custom2
+ ROOT
+ CUSTOM
+ 0
+ False
+
+
+
diff --git a/tests/test_config.py b/tests/test_config.py
new file mode 100644
index 00000000..90c68d8b
--- /dev/null
+++ b/tests/test_config.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+"""novelWriter Config Class Tester
+"""
+
+import nw
+from nwtools import *
+from os import path, unlink
+from nw.config import Config
+
+theConf = Config()
+testDir = path.dirname(__file__)
+testTemp = path.join(testDir,"temp")
+testRef = path.join(testDir,"reference")
+tmpConf = path.join(testTemp,"novelwriter.conf")
+refConf = path.join(testRef, "novelwriter.conf")
+
+ensureDir(testTemp)
+
+# Clean out old stuff
+if path.isfile(tmpConf):
+ unlink(tmpConf)
+
+def testConfigInit():
+ assert theConf.initConfig(testTemp)
+ assert cmpFiles(tmpConf, refConf, [2])
+
+def testConfigSave():
+ assert theConf.saveConfig()
+ assert cmpFiles(tmpConf, refConf, [2])
diff --git a/tests/test_project.py b/tests/test_project.py
new file mode 100644
index 00000000..9aa0d2d1
--- /dev/null
+++ b/tests/test_project.py
@@ -0,0 +1,61 @@
+# -*- coding: utf-8 -*-
+"""novelWriter Project Class Tester
+"""
+
+import nw
+import types
+from os import path, unlink
+
+from nwtools import *
+from nwdummy import DummyMain
+
+from nw.config import Config
+from nw.project.project import NWProject
+from nw.project.item import NWItem
+from nw.enum import nwItemClass
+
+theConf = Config()
+theMain = DummyMain()
+theMain.mainConf = theConf
+testDir = path.dirname(__file__)
+testTemp = path.join(testDir,"temp")
+testRef = path.join(testDir,"reference")
+testProj = path.join(testTemp,"proj")
+
+ensureDir(testTemp)
+ensureDir(testProj)
+
+theConf.initConfig(testRef)
+theProject = NWProject(theMain)
+theProject.handleSeed = 42
+
+projFile = path.join(testProj,"nwProject.nwx")
+
+def testProjectNew():
+ assert theProject.newProject()
+ assert theProject.setProjectPath(testProj)
+ assert theProject.saveProject()
+ assert cmpFiles(projFile, path.join(testRef,"new_nwProject.nwx"), [2])
+
+def testProjectOpen():
+ assert theProject.openProject(projFile)
+
+def testProjectSave():
+ assert theProject.saveProject()
+ assert cmpFiles(projFile, path.join(testRef,"new_nwProject.nwx"), [2])
+ assert not theProject.projChanged
+
+def testProjectNewRoot():
+ assert theProject.openProject(projFile)
+ assert isinstance(theProject.newRoot("Novel", nwItemClass.NOVEL), type(None))
+ assert isinstance(theProject.newRoot("Plot", nwItemClass.PLOT), type(None))
+ assert isinstance(theProject.newRoot("Character", nwItemClass.CHARACTER), type(None))
+ assert isinstance(theProject.newRoot("World", nwItemClass.WORLD), type(None))
+ assert isinstance(theProject.newRoot("Timeline", nwItemClass.TIMELINE), str)
+ assert isinstance(theProject.newRoot("Object", nwItemClass.OBJECT), str)
+ assert isinstance(theProject.newRoot("Custom1", nwItemClass.CUSTOM), str)
+ assert isinstance(theProject.newRoot("Custom2", nwItemClass.CUSTOM), str)
+ assert theProject.projChanged
+ assert theProject.saveProject()
+ assert cmpFiles(projFile, path.join(testRef,"roots_nwProject.nwx"), [2])
+ assert not theProject.projChanged