Bring patch 1.6.4 changes into main
This commit is contained in:
+27
-4
@@ -36,6 +36,13 @@ from PyQt5.QtWidgets import QMessageBox # noqa: E402
|
||||
from novelwriter.config import Config # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def initQt(qtbot):
|
||||
"""Ensures that the qt main thread is always available in all tests.
|
||||
"""
|
||||
return
|
||||
|
||||
|
||||
##
|
||||
# Core Test Folders
|
||||
##
|
||||
@@ -173,6 +180,26 @@ def nwGUI(qtbot, monkeypatch, fncDir, fncConf):
|
||||
return
|
||||
|
||||
|
||||
##
|
||||
# Python Objects
|
||||
##
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def mockRnd(monkeypatch):
|
||||
"""Create a mock random number generator that just counts upwards
|
||||
from 0. This one will generate status/importance flags and handles
|
||||
in a predictable sequence.
|
||||
"""
|
||||
def rnd(n):
|
||||
for x in range(n):
|
||||
yield x
|
||||
|
||||
gen = rnd(1000)
|
||||
monkeypatch.setattr("random.getrandbits", lambda *a: next(gen))
|
||||
|
||||
return
|
||||
|
||||
|
||||
##
|
||||
# Temp Project Folders
|
||||
##
|
||||
@@ -241,10 +268,6 @@ def nwOldProj(tmpDir):
|
||||
return
|
||||
|
||||
|
||||
##
|
||||
# Useful Fixtures
|
||||
##
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def ipsumText():
|
||||
"""Return five paragraphs of Lorem Ipsum text.
|
||||
|
||||
+76
-213
@@ -1,21 +1,22 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<novelWriterXML appVersion="1.5-alpha0" hexVersion="0x010500a0" fileVersion="1.3" timeStamp="2021-08-02 17:47:39">
|
||||
<novelWriterXML appVersion="1.7-alpha0" hexVersion="0x010700a0" fileVersion="1.4" timeStamp="2022-04-23 15:34:13">
|
||||
<project>
|
||||
<name>Lorem Ipsum</name>
|
||||
<title>Lorem Ipsum</title>
|
||||
<author>lipsum.com</author>
|
||||
<saveCount>17</saveCount>
|
||||
<saveCount>26</saveCount>
|
||||
<autoCount>24</autoCount>
|
||||
<editTime>1777</editTime>
|
||||
<editTime>1863</editTime>
|
||||
</project>
|
||||
<settings>
|
||||
<doBackup>False</doBackup>
|
||||
<language>en_GB</language>
|
||||
<spellCheck>False</spellCheck>
|
||||
<spellLang>None</spellLang>
|
||||
<autoOutline>True</autoOutline>
|
||||
<lastEdited>7a992350f3eb6</lastEdited>
|
||||
<lastViewed>None</lastViewed>
|
||||
<lastNovel>None</lastNovel>
|
||||
<lastOutline>None</lastOutline>
|
||||
<lastWordCount>3847</lastWordCount>
|
||||
<novelWordCount>3109</novelWordCount>
|
||||
<notesWordCount>738</notesWordCount>
|
||||
@@ -31,240 +32,102 @@
|
||||
<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>
|
||||
<entry key="sbaa94f" count="3" red="100" green="100" blue="100">New</entry>
|
||||
<entry key="s27bf7c" count="0" red="200" green="50" blue="0">Note</entry>
|
||||
<entry key="s92a87b" count="5" red="200" green="150" blue="0">Draft</entry>
|
||||
<entry key="sedd043" count="7" red="50" green="200" blue="0">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>
|
||||
<entry key="i613591" count="6" red="100" green="100" blue="100">New</entry>
|
||||
<entry key="i560cbf" count="0" red="200" green="50" blue="0">Minor</entry>
|
||||
<entry key="i37861c" count="0" red="200" green="150" blue="0">Major</entry>
|
||||
<entry key="id6b1d0" count="0" red="50" green="200" blue="0">Main</entry>
|
||||
</importance>
|
||||
</settings>
|
||||
<content count="21">
|
||||
<item handle="b3643d0f92e32" order="0" parent="None">
|
||||
<name>Novel</name>
|
||||
<type>ROOT</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>True</expanded>
|
||||
<item handle="b3643d0f92e32" parent="None" root="b3643d0f92e32" order="0" type="ROOT" class="NOVEL">
|
||||
<meta expanded="True"/>
|
||||
<name status="sbaa94f" import="i613591">Novel</name>
|
||||
</item>
|
||||
<item handle="7a992350f3eb6" order="0" parent="b3643d0f92e32">
|
||||
<name>Lorem Ipsum</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>Finished</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>230</charCount>
|
||||
<wordCount>40</wordCount>
|
||||
<paraCount>3</paraCount>
|
||||
<cursorPos>148</cursorPos>
|
||||
<item handle="7a992350f3eb6" parent="b3643d0f92e32" root="b3643d0f92e32" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="230" wordCount="40" paraCount="3" cursorPos="148"/>
|
||||
<name status="sedd043" import="i613591" exported="True">Lorem Ipsum</name>
|
||||
</item>
|
||||
<item handle="8c58a65414c23" order="1" parent="b3643d0f92e32">
|
||||
<name>Front Matter</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>Finished</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>1058</charCount>
|
||||
<wordCount>176</wordCount>
|
||||
<paraCount>2</paraCount>
|
||||
<cursorPos>43</cursorPos>
|
||||
<item handle="8c58a65414c23" parent="b3643d0f92e32" root="b3643d0f92e32" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="1058" wordCount="176" paraCount="2" cursorPos="43"/>
|
||||
<name status="sedd043" import="i613591" exported="True">Front Matter</name>
|
||||
</item>
|
||||
<item handle="88d59a277361b" order="2" parent="b3643d0f92e32">
|
||||
<name>Prologue</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>Draft</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>584</charCount>
|
||||
<wordCount>92</wordCount>
|
||||
<paraCount>1</paraCount>
|
||||
<cursorPos>4</cursorPos>
|
||||
<item handle="88d59a277361b" parent="b3643d0f92e32" root="b3643d0f92e32" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="584" wordCount="92" paraCount="1" cursorPos="4"/>
|
||||
<name status="s92a87b" import="i613591" exported="True">Prologue</name>
|
||||
</item>
|
||||
<item handle="db7e733775d4d" order="3" parent="b3643d0f92e32">
|
||||
<name>Act One</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>35</charCount>
|
||||
<wordCount>6</wordCount>
|
||||
<paraCount>1</paraCount>
|
||||
<cursorPos>42</cursorPos>
|
||||
<item handle="db7e733775d4d" parent="b3643d0f92e32" root="b3643d0f92e32" order="3" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="35" wordCount="6" paraCount="1" cursorPos="42"/>
|
||||
<name status="sbaa94f" import="i613591" exported="True">Act One</name>
|
||||
</item>
|
||||
<item handle="45e6b01ca35c1" order="4" parent="b3643d0f92e32">
|
||||
<name>Chapter One</name>
|
||||
<type>FOLDER</type>
|
||||
<class>NOVEL</class>
|
||||
<status>Draft</status>
|
||||
<expanded>True</expanded>
|
||||
<item handle="45e6b01ca35c1" parent="b3643d0f92e32" root="b3643d0f92e32" order="4" type="FOLDER" class="NOVEL">
|
||||
<meta expanded="True"/>
|
||||
<name status="s92a87b" import="i613591">Chapter One</name>
|
||||
</item>
|
||||
<item handle="fb609cd8319dc" order="0" parent="45e6b01ca35c1">
|
||||
<name>Chapter One</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>Draft</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>419</charCount>
|
||||
<wordCount>67</wordCount>
|
||||
<paraCount>1</paraCount>
|
||||
<cursorPos>56</cursorPos>
|
||||
<item handle="fb609cd8319dc" parent="45e6b01ca35c1" root="b3643d0f92e32" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="419" wordCount="67" paraCount="1" cursorPos="56"/>
|
||||
<name status="s92a87b" import="i613591" exported="True">Chapter One</name>
|
||||
</item>
|
||||
<item handle="88243afbe5ed8" order="1" parent="45e6b01ca35c1">
|
||||
<name>Scene One</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>Finished</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>2758</charCount>
|
||||
<wordCount>404</wordCount>
|
||||
<paraCount>4</paraCount>
|
||||
<cursorPos>1528</cursorPos>
|
||||
<item handle="88243afbe5ed8" parent="45e6b01ca35c1" root="b3643d0f92e32" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="2758" wordCount="404" paraCount="4" cursorPos="1528"/>
|
||||
<name status="sedd043" import="i613591" exported="True">Scene One</name>
|
||||
</item>
|
||||
<item handle="f96ec11c6a3da" order="2" parent="45e6b01ca35c1">
|
||||
<name>Scene Two</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>Finished</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>4043</charCount>
|
||||
<wordCount>600</wordCount>
|
||||
<paraCount>6</paraCount>
|
||||
<cursorPos>2335</cursorPos>
|
||||
<item handle="f96ec11c6a3da" parent="45e6b01ca35c1" root="b3643d0f92e32" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="4043" wordCount="600" paraCount="6" cursorPos="2335"/>
|
||||
<name status="sedd043" import="i613591" exported="True">Scene Two</name>
|
||||
</item>
|
||||
<item handle="846352075de7d" order="5" parent="b3643d0f92e32">
|
||||
<name>Interlude</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>False</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>631</charCount>
|
||||
<wordCount>109</wordCount>
|
||||
<paraCount>3</paraCount>
|
||||
<cursorPos>376</cursorPos>
|
||||
<item handle="846352075de7d" parent="b3643d0f92e32" root="b3643d0f92e32" order="5" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="631" wordCount="109" paraCount="3" cursorPos="376"/>
|
||||
<name status="sbaa94f" import="i613591" exported="False">Interlude</name>
|
||||
</item>
|
||||
<item handle="6bd935d2490cd" order="6" parent="b3643d0f92e32">
|
||||
<name>Chapter Two</name>
|
||||
<type>FOLDER</type>
|
||||
<class>NOVEL</class>
|
||||
<status>Draft</status>
|
||||
<expanded>True</expanded>
|
||||
<item handle="6bd935d2490cd" parent="b3643d0f92e32" root="b3643d0f92e32" order="6" type="FOLDER" class="NOVEL">
|
||||
<meta expanded="True"/>
|
||||
<name status="s92a87b" import="i613591">Chapter Two</name>
|
||||
</item>
|
||||
<item handle="441420a886d82" order="0" parent="6bd935d2490cd">
|
||||
<name>Chapter Two</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>Draft</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>477</charCount>
|
||||
<wordCount>70</wordCount>
|
||||
<paraCount>1</paraCount>
|
||||
<cursorPos>56</cursorPos>
|
||||
<item handle="441420a886d82" parent="6bd935d2490cd" root="b3643d0f92e32" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="477" wordCount="70" paraCount="1" cursorPos="56"/>
|
||||
<name status="s92a87b" import="i613591" exported="True">Chapter Two</name>
|
||||
</item>
|
||||
<item handle="eb103bc70c90c" order="1" parent="6bd935d2490cd">
|
||||
<name>Scene Three</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>Finished</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>3006</charCount>
|
||||
<wordCount>439</wordCount>
|
||||
<paraCount>4</paraCount>
|
||||
<cursorPos>57</cursorPos>
|
||||
<item handle="eb103bc70c90c" parent="6bd935d2490cd" root="b3643d0f92e32" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="3006" wordCount="439" paraCount="4" cursorPos="57"/>
|
||||
<name status="sedd043" import="i613591" exported="True">Scene Three</name>
|
||||
</item>
|
||||
<item handle="f8c0562e50f1b" order="2" parent="6bd935d2490cd">
|
||||
<name>Scene Four</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>Finished</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>3839</charCount>
|
||||
<wordCount>563</wordCount>
|
||||
<paraCount>6</paraCount>
|
||||
<cursorPos>56</cursorPos>
|
||||
<item handle="f8c0562e50f1b" parent="6bd935d2490cd" root="b3643d0f92e32" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="3839" wordCount="563" paraCount="6" cursorPos="56"/>
|
||||
<name status="sedd043" import="i613591" exported="True">Scene Four</name>
|
||||
</item>
|
||||
<item handle="47666c91c7ccf" order="3" parent="6bd935d2490cd">
|
||||
<name>Scene Five</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>Finished</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>3644</charCount>
|
||||
<wordCount>543</wordCount>
|
||||
<paraCount>5</paraCount>
|
||||
<cursorPos>351</cursorPos>
|
||||
<item handle="47666c91c7ccf" parent="6bd935d2490cd" root="b3643d0f92e32" order="3" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="3644" wordCount="543" paraCount="5" cursorPos="351"/>
|
||||
<name status="sedd043" import="i613591" exported="True">Scene Five</name>
|
||||
</item>
|
||||
<item handle="67a8707f2f249" order="1" parent="None">
|
||||
<name>Characters</name>
|
||||
<type>ROOT</type>
|
||||
<class>CHARACTER</class>
|
||||
<status>New</status>
|
||||
<expanded>True</expanded>
|
||||
<item handle="67a8707f2f249" parent="None" root="67a8707f2f249" order="1" type="ROOT" class="CHARACTER">
|
||||
<meta expanded="True"/>
|
||||
<name status="sbaa94f" import="i613591">Characters</name>
|
||||
</item>
|
||||
<item handle="4c4f28287af27" order="0" parent="67a8707f2f249">
|
||||
<name>Mr. Nobody</name>
|
||||
<type>FILE</type>
|
||||
<class>CHARACTER</class>
|
||||
<status>Major</status>
|
||||
<exported>True</exported>
|
||||
<layout>NOTE</layout>
|
||||
<charCount>1864</charCount>
|
||||
<wordCount>284</wordCount>
|
||||
<paraCount>3</paraCount>
|
||||
<cursorPos>1883</cursorPos>
|
||||
<item handle="4c4f28287af27" parent="67a8707f2f249" root="67a8707f2f249" order="0" type="FILE" class="CHARACTER" layout="NOTE">
|
||||
<meta expanded="False" charCount="1864" wordCount="284" paraCount="3" cursorPos="1883"/>
|
||||
<name status="sbaa94f" import="i613591" exported="True">Mr. Nobody</name>
|
||||
</item>
|
||||
<item handle="6c6afb1247750" order="2" parent="None">
|
||||
<name>Plot</name>
|
||||
<type>ROOT</type>
|
||||
<class>PLOT</class>
|
||||
<status>New</status>
|
||||
<expanded>True</expanded>
|
||||
<item handle="6c6afb1247750" parent="None" root="6c6afb1247750" order="2" type="ROOT" class="PLOT">
|
||||
<meta expanded="True"/>
|
||||
<name status="sbaa94f" import="i613591">Plot</name>
|
||||
</item>
|
||||
<item handle="2426c6f0ca922" order="0" parent="6c6afb1247750">
|
||||
<name>Main</name>
|
||||
<type>FILE</type>
|
||||
<class>PLOT</class>
|
||||
<status>Main</status>
|
||||
<exported>True</exported>
|
||||
<layout>NOTE</layout>
|
||||
<charCount>1369</charCount>
|
||||
<wordCount>195</wordCount>
|
||||
<paraCount>2</paraCount>
|
||||
<cursorPos>1387</cursorPos>
|
||||
<item handle="2426c6f0ca922" parent="6c6afb1247750" root="6c6afb1247750" order="0" type="FILE" class="PLOT" layout="NOTE">
|
||||
<meta expanded="False" charCount="1369" wordCount="195" paraCount="2" cursorPos="1387"/>
|
||||
<name status="sbaa94f" import="i613591" exported="True">Main</name>
|
||||
</item>
|
||||
<item handle="60bdf227455cc" order="3" parent="None">
|
||||
<name>World</name>
|
||||
<type>ROOT</type>
|
||||
<class>WORLD</class>
|
||||
<status>New</status>
|
||||
<expanded>True</expanded>
|
||||
<item handle="60bdf227455cc" parent="None" root="60bdf227455cc" order="3" type="ROOT" class="WORLD">
|
||||
<meta expanded="True"/>
|
||||
<name status="sbaa94f" import="i613591">World</name>
|
||||
</item>
|
||||
<item handle="04468803b92e1" order="0" parent="60bdf227455cc">
|
||||
<name>Ancient Europe</name>
|
||||
<type>FILE</type>
|
||||
<class>WORLD</class>
|
||||
<status>Minor</status>
|
||||
<exported>True</exported>
|
||||
<layout>NOTE</layout>
|
||||
<charCount>1770</charCount>
|
||||
<wordCount>259</wordCount>
|
||||
<paraCount>3</paraCount>
|
||||
<cursorPos>1792</cursorPos>
|
||||
<item handle="04468803b92e1" parent="60bdf227455cc" root="60bdf227455cc" order="0" type="FILE" class="WORLD" layout="NOTE">
|
||||
<meta expanded="False" charCount="1770" wordCount="259" paraCount="3" cursorPos="1792"/>
|
||||
<name status="sbaa94f" import="i613591" exported="True">Ancient Europe</name>
|
||||
</item>
|
||||
</content>
|
||||
</novelWriterXML>
|
||||
|
||||
+37
-75
@@ -1,22 +1,23 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<novelWriterXML appVersion="1.5-alpha0" hexVersion="0x010500a0" fileVersion="1.3" timeStamp="2021-08-02 17:47:26">
|
||||
<novelWriterXML appVersion="1.7-alpha0" hexVersion="0x010700a0" fileVersion="1.4" timeStamp="2022-04-23 15:34:21">
|
||||
<project>
|
||||
<name>Test Minimal</name>
|
||||
<title>Minimal</title>
|
||||
<author>Jane Doe</author>
|
||||
<author>John Doh</author>
|
||||
<saveCount>9</saveCount>
|
||||
<saveCount>17</saveCount>
|
||||
<autoCount>2</autoCount>
|
||||
<editTime>113</editTime>
|
||||
<editTime>150</editTime>
|
||||
</project>
|
||||
<settings>
|
||||
<doBackup>True</doBackup>
|
||||
<language>en_GB</language>
|
||||
<spellCheck>False</spellCheck>
|
||||
<spellLang>None</spellLang>
|
||||
<autoOutline>True</autoOutline>
|
||||
<lastEdited>None</lastEdited>
|
||||
<lastViewed>None</lastViewed>
|
||||
<lastNovel>None</lastNovel>
|
||||
<lastOutline>None</lastOutline>
|
||||
<lastWordCount>10</lastWordCount>
|
||||
<novelWordCount>10</novelWordCount>
|
||||
<notesWordCount>0</notesWordCount>
|
||||
@@ -29,89 +30,50 @@
|
||||
<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>
|
||||
<entry key="s72322d" count="5" red="100" green="100" blue="100">New</entry>
|
||||
<entry key="sb6adc7" count="0" red="200" green="50" blue="0">Note</entry>
|
||||
<entry key="s2ae76d" count="0" red="200" green="150" blue="0">Draft</entry>
|
||||
<entry key="s541953" count="0" red="50" green="200" blue="0">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>
|
||||
<entry key="iffcacb" count="3" red="100" green="100" blue="100">New</entry>
|
||||
<entry key="i6b130b" count="0" red="200" green="50" blue="0">Minor</entry>
|
||||
<entry key="iece803" count="0" red="200" green="150" blue="0">Major</entry>
|
||||
<entry key="i5ba06e" count="0" red="50" green="200" blue="0">Main</entry>
|
||||
</importance>
|
||||
</settings>
|
||||
<content count="8">
|
||||
<item handle="a508bb932959c" order="0" parent="None">
|
||||
<name>Novel</name>
|
||||
<type>ROOT</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>True</expanded>
|
||||
<item handle="a508bb932959c" parent="None" root="a508bb932959c" order="0" type="ROOT" class="NOVEL">
|
||||
<meta expanded="True"/>
|
||||
<name status="s72322d" import="iffcacb">Novel</name>
|
||||
</item>
|
||||
<item handle="a35baf2e93843" order="0" parent="a508bb932959c">
|
||||
<name>Title Page</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>28</charCount>
|
||||
<wordCount>6</wordCount>
|
||||
<paraCount>1</paraCount>
|
||||
<cursorPos>33</cursorPos>
|
||||
<item handle="a35baf2e93843" parent="a508bb932959c" root="a508bb932959c" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="28" wordCount="6" paraCount="1" cursorPos="33"/>
|
||||
<name status="s72322d" import="iffcacb" exported="True">Title Page</name>
|
||||
</item>
|
||||
<item handle="a6d311a93600a" order="1" parent="a508bb932959c">
|
||||
<name>New Chapter</name>
|
||||
<type>FOLDER</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>True</expanded>
|
||||
<item handle="a6d311a93600a" parent="a508bb932959c" root="a508bb932959c" order="1" type="FOLDER" class="NOVEL">
|
||||
<meta expanded="True"/>
|
||||
<name status="s72322d" import="iffcacb">New Chapter</name>
|
||||
</item>
|
||||
<item handle="f5ab3e30151e1" order="0" parent="a6d311a93600a">
|
||||
<name>New Chapter</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>11</charCount>
|
||||
<wordCount>2</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>16</cursorPos>
|
||||
<item handle="f5ab3e30151e1" parent="a6d311a93600a" root="a508bb932959c" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="11" wordCount="2" paraCount="0" cursorPos="16"/>
|
||||
<name status="s72322d" import="iffcacb" exported="True">New Chapter</name>
|
||||
</item>
|
||||
<item handle="8c659a11cd429" order="1" parent="a6d311a93600a">
|
||||
<name>New Scene</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>9</charCount>
|
||||
<wordCount>2</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>15</cursorPos>
|
||||
<item handle="8c659a11cd429" parent="a6d311a93600a" root="a508bb932959c" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="9" wordCount="2" paraCount="0" cursorPos="15"/>
|
||||
<name status="s72322d" import="iffcacb" exported="True">New Scene</name>
|
||||
</item>
|
||||
<item handle="7695ce551d265" order="1" parent="None">
|
||||
<name>Plot</name>
|
||||
<type>ROOT</type>
|
||||
<class>PLOT</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<item handle="7695ce551d265" parent="None" root="7695ce551d265" order="1" type="ROOT" class="PLOT">
|
||||
<meta expanded="False"/>
|
||||
<name status="s72322d" import="iffcacb">Plot</name>
|
||||
</item>
|
||||
<item handle="afb3043c7b2b3" order="2" parent="None">
|
||||
<name>Characters</name>
|
||||
<type>ROOT</type>
|
||||
<class>CHARACTER</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<item handle="afb3043c7b2b3" parent="None" root="afb3043c7b2b3" order="2" type="ROOT" class="CHARACTER">
|
||||
<meta expanded="False"/>
|
||||
<name status="s72322d" import="iffcacb">Characters</name>
|
||||
</item>
|
||||
<item handle="9d5247ab588e0" order="3" parent="None">
|
||||
<name>World</name>
|
||||
<type>ROOT</type>
|
||||
<class>WORLD</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<item handle="9d5247ab588e0" parent="None" root="9d5247ab588e0" order="3" type="ROOT" class="WORLD">
|
||||
<meta expanded="False"/>
|
||||
<name status="s72322d" import="iffcacb">World</name>
|
||||
</item>
|
||||
</content>
|
||||
</novelWriterXML>
|
||||
|
||||
@@ -29,7 +29,6 @@ class MockGuiMain():
|
||||
def __init__(self):
|
||||
self.mainConf = None
|
||||
self.hasProject = True
|
||||
self.theIndex = None
|
||||
self.theProject = None
|
||||
self.statusBar = MockStatusBar()
|
||||
|
||||
|
||||
@@ -13,8 +13,6 @@ hidehscroll = False
|
||||
[Sizes]
|
||||
geometry = 1200, 650
|
||||
preferences = 700, 615
|
||||
treecols = 200, 50, 30
|
||||
novelcols = 200, 50
|
||||
projcols = 200, 60, 140
|
||||
mainpane = 300, 800
|
||||
docpane = 400, 400
|
||||
@@ -30,7 +28,7 @@ emphlabels = True
|
||||
[Editor]
|
||||
textfont = None
|
||||
textsize = 12
|
||||
width = 600
|
||||
width = 700
|
||||
margin = 40
|
||||
tabwidth = 40
|
||||
focuswidth = 800
|
||||
|
||||
@@ -1,99 +1,125 @@
|
||||
{
|
||||
"tagIndex": {
|
||||
"Bod": [3, "4c4f28287af27", "CHARACTER", "T000001"],
|
||||
"Main": [3, "2426c6f0ca922", "PLOT", "T000001"],
|
||||
"Europe": [3, "04468803b92e1", "WORLD", "T000001"]
|
||||
},
|
||||
"refIndex": {
|
||||
"fb609cd8319dc": {
|
||||
"T000001": [[3, "@pov", "Bod"], [4, "@plot", "Main"], [5, "@location", "Europe"]]
|
||||
"tagsIndex": {
|
||||
"Bod": {"handle": "4c4f28287af27", "heading": "T000001", "class": "CHARACTER"},
|
||||
"Main": {"handle": "2426c6f0ca922", "heading": "T000001", "class": "PLOT"},
|
||||
"Europe": {"handle": "04468803b92e1", "heading": "T000001", "class": "WORLD"}
|
||||
},
|
||||
"88243afbe5ed8": {
|
||||
"T000001": [[3, "@pov", "Bod"], [4, "@plot", "Main"], [5, "@location", "Europe"]]
|
||||
},
|
||||
"f96ec11c6a3da": {
|
||||
"T000001": [[3, "@pov", "Bod"], [4, "@plot", "Main"], [5, "@location", "Europe"]]
|
||||
},
|
||||
"441420a886d82": {
|
||||
"T000001": [[3, "@pov", "Bod"], [4, "@plot", "Main"], [5, "@location", "Europe"]]
|
||||
},
|
||||
"eb103bc70c90c": {
|
||||
"T000001": [[3, "@pov", "Bod"], [4, "@plot", "Main"], [5, "@location", "Europe"]]
|
||||
},
|
||||
"f8c0562e50f1b": {
|
||||
"T000001": [[3, "@pov", "Bod"], [4, "@plot", "Main"], [5, "@location", "Europe"]]
|
||||
},
|
||||
"47666c91c7ccf": {
|
||||
"T000001": [[3, "@pov", "Bod"], [4, "@plot", "Main"], [5, "@location", "Europe"]]
|
||||
},
|
||||
"4c4f28287af27": {
|
||||
"T000001": [[4, "@plot", "Main"]]
|
||||
"itemIndex": {
|
||||
"7a992350f3eb6": {
|
||||
"level": "H1",
|
||||
"headings": {
|
||||
"T000001": {"level": "H1", "title": "Lorem Ipsum", "tag": "", "cCount": 230, "wCount": 40, "pCount": 3, "synopsis": ""}
|
||||
}
|
||||
},
|
||||
"8c58a65414c23": {
|
||||
"level": "H0",
|
||||
"headings": {
|
||||
"T000000": {"level": "H0", "title": "", "tag": "", "cCount": 1058, "wCount": 176, "pCount": 2, "synopsis": ""}
|
||||
}
|
||||
},
|
||||
"88d59a277361b": {
|
||||
"level": "H2",
|
||||
"headings": {
|
||||
"T000001": {"level": "H2", "title": "Prologue", "tag": "", "cCount": 584, "wCount": 92, "pCount": 1, "synopsis": "Explanation from the lipsum.com website."}
|
||||
}
|
||||
},
|
||||
"db7e733775d4d": {
|
||||
"level": "H1",
|
||||
"headings": {
|
||||
"T000001": {"level": "H1", "title": "Act One", "tag": "", "cCount": 35, "wCount": 6, "pCount": 1, "synopsis": ""}
|
||||
}
|
||||
},
|
||||
"fb609cd8319dc": {
|
||||
"level": "H2",
|
||||
"headings": {
|
||||
"T000001": {"level": "H2", "title": "Chapter One", "tag": "", "cCount": 419, "wCount": 67, "pCount": 1, "synopsis": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam."}
|
||||
},
|
||||
"references": {
|
||||
"T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
|
||||
}
|
||||
},
|
||||
"88243afbe5ed8": {
|
||||
"level": "H3",
|
||||
"headings": {
|
||||
"T000001": {"level": "H3", "title": "Scene One", "tag": "", "cCount": 1197, "wCount": 174, "pCount": 2, "synopsis": "Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur."},
|
||||
"T000013": {"level": "H4", "title": "Scene One, Section Two", "tag": "", "cCount": 1561, "wCount": 230, "pCount": 2, "synopsis": ""}
|
||||
},
|
||||
"references": {
|
||||
"T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
|
||||
}
|
||||
},
|
||||
"f96ec11c6a3da": {
|
||||
"level": "H3",
|
||||
"headings": {
|
||||
"T000001": {"level": "H3", "title": "Scene Two", "tag": "", "cCount": 2034, "wCount": 299, "pCount": 3, "synopsis": "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci."},
|
||||
"T000015": {"level": "H4", "title": "Scene Two, Section Two", "tag": "", "cCount": 2009, "wCount": 301, "pCount": 3, "synopsis": ""}
|
||||
},
|
||||
"references": {
|
||||
"T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
|
||||
}
|
||||
},
|
||||
"846352075de7d": {
|
||||
"level": "H2",
|
||||
"headings": {
|
||||
"T000001": {"level": "H2", "title": "Why do we use it?", "tag": "", "cCount": 631, "wCount": 109, "pCount": 3, "synopsis": ""}
|
||||
}
|
||||
},
|
||||
"441420a886d82": {
|
||||
"level": "H2",
|
||||
"headings": {
|
||||
"T000001": {"level": "H2", "title": "Chapter Two", "tag": "", "cCount": 477, "wCount": 70, "pCount": 1, "synopsis": "Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue."}
|
||||
},
|
||||
"references": {
|
||||
"T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
|
||||
}
|
||||
},
|
||||
"eb103bc70c90c": {
|
||||
"level": "H3",
|
||||
"headings": {
|
||||
"T000001": {"level": "H3", "title": "Scene Three", "tag": "", "cCount": 3006, "wCount": 439, "pCount": 4, "synopsis": "Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos."}
|
||||
},
|
||||
"references": {
|
||||
"T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
|
||||
}
|
||||
},
|
||||
"f8c0562e50f1b": {
|
||||
"level": "H3",
|
||||
"headings": {
|
||||
"T000001": {"level": "H3", "title": "Scene Four", "tag": "", "cCount": 3839, "wCount": 563, "pCount": 6, "synopsis": "Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo."}
|
||||
},
|
||||
"references": {
|
||||
"T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
|
||||
}
|
||||
},
|
||||
"47666c91c7ccf": {
|
||||
"level": "H3",
|
||||
"headings": {
|
||||
"T000001": {"level": "H3", "title": "Scene Five", "tag": "", "cCount": 3644, "wCount": 543, "pCount": 5, "synopsis": "Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus."}
|
||||
},
|
||||
"references": {
|
||||
"T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
|
||||
}
|
||||
},
|
||||
"4c4f28287af27": {
|
||||
"level": "H1",
|
||||
"headings": {
|
||||
"T000001": {"level": "H1", "title": "Nobody Owens", "tag": "Bod", "cCount": 1864, "wCount": 284, "pCount": 3, "synopsis": ""}
|
||||
},
|
||||
"references": {
|
||||
"T000001": {"Main": "@plot"}
|
||||
}
|
||||
},
|
||||
"2426c6f0ca922": {
|
||||
"level": "H1",
|
||||
"headings": {
|
||||
"T000001": {"level": "H1", "title": "Main Plot", "tag": "Main", "cCount": 1369, "wCount": 195, "pCount": 2, "synopsis": ""}
|
||||
}
|
||||
},
|
||||
"04468803b92e1": {
|
||||
"level": "H1",
|
||||
"headings": {
|
||||
"T000001": {"level": "H1", "title": "Ancient Europe", "tag": "Europe", "cCount": 1770, "wCount": 259, "pCount": 3, "synopsis": ""}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"fileIndex": {
|
||||
"7a992350f3eb6": {
|
||||
"T000001": {"level": "H1", "title": "Lorem Ipsum", "layout": "DOCUMENT", "cCount": 230, "wCount": 40, "pCount": 3, "synopsis": ""}
|
||||
},
|
||||
"8c58a65414c23": {
|
||||
"T000000": {"level": "H0", "title": "", "layout": "DOCUMENT", "cCount": 1058, "wCount": 176, "pCount": 2, "synopsis": ""}
|
||||
},
|
||||
"88d59a277361b": {
|
||||
"T000001": {"level": "H2", "title": "Prologue", "layout": "DOCUMENT", "cCount": 584, "wCount": 92, "pCount": 1, "synopsis": "Explanation from the lipsum.com website."}
|
||||
},
|
||||
"db7e733775d4d": {
|
||||
"T000001": {"level": "H1", "title": "Act One", "layout": "DOCUMENT", "cCount": 35, "wCount": 6, "pCount": 1, "synopsis": ""}
|
||||
},
|
||||
"fb609cd8319dc": {
|
||||
"T000001": {"level": "H2", "title": "Chapter One", "layout": "DOCUMENT", "cCount": 419, "wCount": 67, "pCount": 1, "synopsis": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam."}
|
||||
},
|
||||
"88243afbe5ed8": {
|
||||
"T000001": {"level": "H3", "title": "Scene One", "layout": "DOCUMENT", "cCount": 1197, "wCount": 174, "pCount": 2, "synopsis": "Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur."},
|
||||
"T000013": {"level": "H4", "title": "Scene One, Section Two", "layout": "DOCUMENT", "cCount": 1561, "wCount": 230, "pCount": 2, "synopsis": ""}
|
||||
},
|
||||
"f96ec11c6a3da": {
|
||||
"T000001": {"level": "H3", "title": "Scene Two", "layout": "DOCUMENT", "cCount": 2034, "wCount": 299, "pCount": 3, "synopsis": "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci."},
|
||||
"T000015": {"level": "H4", "title": "Scene Two, Section Two", "layout": "DOCUMENT", "cCount": 2009, "wCount": 301, "pCount": 3, "synopsis": ""}
|
||||
},
|
||||
"846352075de7d": {
|
||||
"T000001": {"level": "H2", "title": "Why do we use it?", "layout": "DOCUMENT", "cCount": 631, "wCount": 109, "pCount": 3, "synopsis": ""}
|
||||
},
|
||||
"441420a886d82": {
|
||||
"T000001": {"level": "H2", "title": "Chapter Two", "layout": "DOCUMENT", "cCount": 477, "wCount": 70, "pCount": 1, "synopsis": "Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue."}
|
||||
},
|
||||
"eb103bc70c90c": {
|
||||
"T000001": {"level": "H3", "title": "Scene Three", "layout": "DOCUMENT", "cCount": 3006, "wCount": 439, "pCount": 4, "synopsis": "Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos."}
|
||||
},
|
||||
"f8c0562e50f1b": {
|
||||
"T000001": {"level": "H3", "title": "Scene Four", "layout": "DOCUMENT", "cCount": 3839, "wCount": 563, "pCount": 6, "synopsis": "Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo."}
|
||||
},
|
||||
"47666c91c7ccf": {
|
||||
"T000001": {"level": "H3", "title": "Scene Five", "layout": "DOCUMENT", "cCount": 3644, "wCount": 543, "pCount": 5, "synopsis": "Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus."}
|
||||
},
|
||||
"4c4f28287af27": {
|
||||
"T000001": {"level": "H1", "title": "Nobody Owens", "layout": "NOTE", "cCount": 1864, "wCount": 284, "pCount": 3, "synopsis": ""}
|
||||
},
|
||||
"2426c6f0ca922": {
|
||||
"T000001": {"level": "H1", "title": "Main Plot", "layout": "NOTE", "cCount": 1369, "wCount": 195, "pCount": 2, "synopsis": ""}
|
||||
},
|
||||
"04468803b92e1": {
|
||||
"T000001": {"level": "H1", "title": "Ancient Europe", "layout": "NOTE", "cCount": 1770, "wCount": 259, "pCount": 3, "synopsis": ""}
|
||||
}
|
||||
},
|
||||
"fileMeta": {
|
||||
"7a992350f3eb6": ["H1", 230, 40, 3],
|
||||
"8c58a65414c23": ["H0", 1058, 176, 2],
|
||||
"88d59a277361b": ["H2", 584, 92, 1],
|
||||
"db7e733775d4d": ["H1", 35, 6, 1],
|
||||
"fb609cd8319dc": ["H2", 419, 67, 1],
|
||||
"88243afbe5ed8": ["H3", 2758, 404, 4],
|
||||
"f96ec11c6a3da": ["H3", 4043, 600, 6],
|
||||
"846352075de7d": ["H2", 631, 109, 3],
|
||||
"441420a886d82": ["H2", 477, 70, 1],
|
||||
"eb103bc70c90c": ["H3", 3006, 439, 4],
|
||||
"f8c0562e50f1b": ["H3", 3839, 563, 6],
|
||||
"47666c91c7ccf": ["H3", 3644, 543, 5],
|
||||
"4c4f28287af27": ["H1", 1864, 284, 3],
|
||||
"2426c6f0ca922": ["H1", 1369, 195, 2],
|
||||
"04468803b92e1": ["H1", 1770, 259, 3]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<novelWriterXML appVersion="1.5-alpha0" hexVersion="0x010500a0" fileVersion="1.2" timeStamp="2021-08-02 02:30:49">
|
||||
<novelWriterXML appVersion="1.7-beta1" hexVersion="0x010700b1" fileVersion="1.4" timeStamp="2022-07-31 18:40:28">
|
||||
<project>
|
||||
<name>Test Custom</name>
|
||||
<title>Test Novel</title>
|
||||
@@ -14,9 +14,10 @@
|
||||
<language>None</language>
|
||||
<spellCheck>False</spellCheck>
|
||||
<spellLang>None</spellLang>
|
||||
<autoOutline>True</autoOutline>
|
||||
<lastEdited>None</lastEdited>
|
||||
<lastViewed>None</lastViewed>
|
||||
<lastNovel>None</lastNovel>
|
||||
<lastOutline>None</lastOutline>
|
||||
<lastWordCount>0</lastWordCount>
|
||||
<novelWordCount>0</novelWordCount>
|
||||
<notesWordCount>0</notesWordCount>
|
||||
@@ -29,244 +30,106 @@
|
||||
<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>
|
||||
<entry key="s000008" count="15" red="100" green="100" blue="100">New</entry>
|
||||
<entry key="s000009" count="0" red="200" green="50" blue="0">Note</entry>
|
||||
<entry key="s00000a" count="0" red="200" green="150" blue="0">Draft</entry>
|
||||
<entry key="s00000b" count="0" red="50" green="200" blue="0">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>
|
||||
<entry key="i00000c" count="7" red="100" green="100" blue="100">New</entry>
|
||||
<entry key="i00000d" count="0" red="200" green="50" blue="0">Minor</entry>
|
||||
<entry key="i00000e" count="0" red="200" green="150" blue="0">Major</entry>
|
||||
<entry key="i00000f" count="0" red="50" green="200" blue="0">Main</entry>
|
||||
</importance>
|
||||
</settings>
|
||||
<content count="23">
|
||||
<item handle="73475cb40a568" order="0" parent="None">
|
||||
<name>Novel</name>
|
||||
<type>ROOT</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<content count="22">
|
||||
<item handle="0000000000010" parent="None" root="0000000000010" order="0" type="ROOT" class="NOVEL">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000008" import="i00000c">Novel</name>
|
||||
</item>
|
||||
<item handle="44cb730c42048" order="0" parent="None">
|
||||
<name>Plot</name>
|
||||
<type>ROOT</type>
|
||||
<class>PLOT</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<item handle="0000000000011" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000008" import="i00000c" exported="True">Title Page</name>
|
||||
</item>
|
||||
<item handle="71ee45a3c0db9" order="0" parent="None">
|
||||
<name>Characters</name>
|
||||
<type>ROOT</type>
|
||||
<class>CHARACTER</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<item handle="0000000000012" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000008" import="i00000c" exported="True">Chapter 1</name>
|
||||
</item>
|
||||
<item handle="811786ad1ae74" order="0" parent="None">
|
||||
<name>Locations</name>
|
||||
<type>ROOT</type>
|
||||
<class>WORLD</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<item handle="0000000000013" parent="0000000000012" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000008" import="i00000c" exported="True">Scene 1.1</name>
|
||||
</item>
|
||||
<item handle="25fc0e7096fc6" order="0" parent="None">
|
||||
<name>Timeline</name>
|
||||
<type>ROOT</type>
|
||||
<class>TIMELINE</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<item handle="0000000000014" parent="0000000000012" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000008" import="i00000c" exported="True">Scene 1.2</name>
|
||||
</item>
|
||||
<item handle="31489056e0916" order="0" parent="None">
|
||||
<name>Objects</name>
|
||||
<type>ROOT</type>
|
||||
<class>OBJECT</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<item handle="0000000000015" parent="0000000000012" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000008" import="i00000c" exported="True">Scene 1.3</name>
|
||||
</item>
|
||||
<item handle="98010bd9270f9" order="0" parent="None">
|
||||
<name>Entities</name>
|
||||
<type>ROOT</type>
|
||||
<class>ENTITY</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<item handle="0000000000016" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000008" import="i00000c" exported="True">Chapter 2</name>
|
||||
</item>
|
||||
<item handle="0e17daca5f3e1" order="0" parent="73475cb40a568">
|
||||
<name>Title Page</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
<item handle="0000000000017" parent="0000000000016" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000008" import="i00000c" exported="True">Scene 2.1</name>
|
||||
</item>
|
||||
<item handle="1a6562590ef19" order="0" parent="73475cb40a568">
|
||||
<name>Chapter 1</name>
|
||||
<type>FOLDER</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<item handle="0000000000018" parent="0000000000016" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000008" import="i00000c" exported="True">Scene 2.2</name>
|
||||
</item>
|
||||
<item handle="031b4af5197ec" order="0" parent="1a6562590ef19">
|
||||
<name>Chapter 1</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
<item handle="0000000000019" parent="0000000000016" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000008" import="i00000c" exported="True">Scene 2.3</name>
|
||||
</item>
|
||||
<item handle="41cfc0d1f2d12" order="0" parent="1a6562590ef19">
|
||||
<name>Scene 1.1</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
<item handle="000000000001a" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000008" import="i00000c" exported="True">Chapter 3</name>
|
||||
</item>
|
||||
<item handle="2858dcd1057d3" order="0" parent="1a6562590ef19">
|
||||
<name>Scene 1.2</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
<item handle="000000000001b" parent="000000000001a" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000008" import="i00000c" exported="True">Scene 3.1</name>
|
||||
</item>
|
||||
<item handle="2fca346db6561" order="0" parent="1a6562590ef19">
|
||||
<name>Scene 1.3</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
<item handle="000000000001c" parent="000000000001a" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000008" import="i00000c" exported="True">Scene 3.2</name>
|
||||
</item>
|
||||
<item handle="02d20bbd7e394" order="0" parent="73475cb40a568">
|
||||
<name>Chapter 2</name>
|
||||
<type>FOLDER</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<item handle="000000000001d" parent="000000000001a" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000008" import="i00000c" exported="True">Scene 3.3</name>
|
||||
</item>
|
||||
<item handle="7688b6ef52555" order="0" parent="02d20bbd7e394">
|
||||
<name>Chapter 2</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
<item handle="000000000001e" parent="None" root="000000000001e" order="0" type="ROOT" class="PLOT">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000008" import="i00000c">Plot</name>
|
||||
</item>
|
||||
<item handle="c837649cce43f" order="0" parent="02d20bbd7e394">
|
||||
<name>Scene 2.1</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
<item handle="000000000001f" parent="000000000001e" root="000000000001e" order="0" type="FILE" class="PLOT" layout="NOTE">
|
||||
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000008" import="i00000c" exported="True">Main Plot</name>
|
||||
</item>
|
||||
<item handle="6208ef0f7750c" order="0" parent="02d20bbd7e394">
|
||||
<name>Scene 2.2</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
<item handle="0000000000020" parent="None" root="0000000000020" order="0" type="ROOT" class="CHARACTER">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000008" import="i00000c">Characters</name>
|
||||
</item>
|
||||
<item handle="3e1e967e9b793" order="0" parent="02d20bbd7e394">
|
||||
<name>Scene 2.3</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
<item handle="0000000000021" parent="0000000000020" root="0000000000020" order="0" type="FILE" class="CHARACTER" layout="NOTE">
|
||||
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000008" import="i00000c" exported="True">Protagonist</name>
|
||||
</item>
|
||||
<item handle="39fa9ec190eee" order="0" parent="73475cb40a568">
|
||||
<name>Chapter 3</name>
|
||||
<type>FOLDER</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<item handle="0000000000022" parent="None" root="0000000000022" order="0" type="ROOT" class="WORLD">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000008" import="i00000c">Locations</name>
|
||||
</item>
|
||||
<item handle="d029fa3a95e17" order="0" parent="39fa9ec190eee">
|
||||
<name>Chapter 3</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
<item handle="0000000000023" parent="0000000000022" root="0000000000022" order="0" type="FILE" class="WORLD" layout="NOTE">
|
||||
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000008" import="i00000c" exported="True">Main Location</name>
|
||||
</item>
|
||||
<item handle="81b8a03f97e87" order="0" parent="39fa9ec190eee">
|
||||
<name>Scene 3.1</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
<item handle="0000000000024" parent="None" root="0000000000024" order="0" type="ROOT" class="ARCHIVE">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000008" import="i00000c">Archive</name>
|
||||
</item>
|
||||
<item handle="da4ea2a5506f2" order="0" parent="39fa9ec190eee">
|
||||
<name>Scene 3.2</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="a68b412c42825" order="0" parent="39fa9ec190eee">
|
||||
<name>Scene 3.3</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
<item handle="0000000000025" parent="None" root="0000000000025" order="0" type="ROOT" class="TRASH">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000008" import="i00000c">Trash</name>
|
||||
</item>
|
||||
</content>
|
||||
</novelWriterXML>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<novelWriterXML appVersion="1.5-alpha0" hexVersion="0x010500a0" fileVersion="1.2" timeStamp="2021-08-02 02:29:34">
|
||||
<novelWriterXML appVersion="1.7-beta1" hexVersion="0x010700b1" fileVersion="1.4" timeStamp="2022-06-12 17:45:51">
|
||||
<project>
|
||||
<name>Test Custom</name>
|
||||
<title>Test Novel</title>
|
||||
@@ -14,9 +14,10 @@
|
||||
<language>None</language>
|
||||
<spellCheck>False</spellCheck>
|
||||
<spellLang>None</spellLang>
|
||||
<autoOutline>True</autoOutline>
|
||||
<lastEdited>None</lastEdited>
|
||||
<lastViewed>None</lastViewed>
|
||||
<lastNovel>None</lastNovel>
|
||||
<lastOutline>None</lastOutline>
|
||||
<lastWordCount>0</lastWordCount>
|
||||
<novelWordCount>0</novelWordCount>
|
||||
<notesWordCount>0</notesWordCount>
|
||||
@@ -29,151 +30,82 @@
|
||||
<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>
|
||||
<entry key="s000008" count="9" red="100" green="100" blue="100">New</entry>
|
||||
<entry key="s000009" count="0" red="200" green="50" blue="0">Note</entry>
|
||||
<entry key="s00000a" count="0" red="200" green="150" blue="0">Draft</entry>
|
||||
<entry key="s00000b" count="0" red="50" green="200" blue="0">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>
|
||||
<entry key="i00000c" count="7" red="100" green="100" blue="100">New</entry>
|
||||
<entry key="i00000d" count="0" red="200" green="50" blue="0">Minor</entry>
|
||||
<entry key="i00000e" count="0" red="200" green="150" blue="0">Major</entry>
|
||||
<entry key="i00000f" count="0" red="50" green="200" blue="0">Main</entry>
|
||||
</importance>
|
||||
</settings>
|
||||
<content count="14">
|
||||
<item handle="73475cb40a568" order="0" parent="None">
|
||||
<name>Novel</name>
|
||||
<type>ROOT</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<content count="16">
|
||||
<item handle="0000000000010" parent="None" root="0000000000010" order="0" type="ROOT" class="NOVEL">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000008" import="i00000c">Novel</name>
|
||||
</item>
|
||||
<item handle="44cb730c42048" order="0" parent="None">
|
||||
<name>Plot</name>
|
||||
<type>ROOT</type>
|
||||
<class>PLOT</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<item handle="0000000000011" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000008" import="i00000c" exported="True">Title Page</name>
|
||||
</item>
|
||||
<item handle="71ee45a3c0db9" order="0" parent="None">
|
||||
<name>Characters</name>
|
||||
<type>ROOT</type>
|
||||
<class>CHARACTER</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<item handle="0000000000012" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000008" import="i00000c" exported="True">Scene 1</name>
|
||||
</item>
|
||||
<item handle="811786ad1ae74" order="0" parent="None">
|
||||
<name>Locations</name>
|
||||
<type>ROOT</type>
|
||||
<class>WORLD</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<item handle="0000000000013" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000008" import="i00000c" exported="True">Scene 2</name>
|
||||
</item>
|
||||
<item handle="25fc0e7096fc6" order="0" parent="None">
|
||||
<name>Timeline</name>
|
||||
<type>ROOT</type>
|
||||
<class>TIMELINE</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<item handle="0000000000014" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000008" import="i00000c" exported="True">Scene 3</name>
|
||||
</item>
|
||||
<item handle="31489056e0916" order="0" parent="None">
|
||||
<name>Objects</name>
|
||||
<type>ROOT</type>
|
||||
<class>OBJECT</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<item handle="0000000000015" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000008" import="i00000c" exported="True">Scene 4</name>
|
||||
</item>
|
||||
<item handle="98010bd9270f9" order="0" parent="None">
|
||||
<name>Entities</name>
|
||||
<type>ROOT</type>
|
||||
<class>ENTITY</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<item handle="0000000000016" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000008" import="i00000c" exported="True">Scene 5</name>
|
||||
</item>
|
||||
<item handle="0e17daca5f3e1" order="0" parent="73475cb40a568">
|
||||
<name>Title Page</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
<item handle="0000000000017" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000008" import="i00000c" exported="True">Scene 6</name>
|
||||
</item>
|
||||
<item handle="1a6562590ef19" order="0" parent="73475cb40a568">
|
||||
<name>Scene 1</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
<item handle="0000000000018" parent="None" root="0000000000018" order="0" type="ROOT" class="PLOT">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000008" import="i00000c">Plot</name>
|
||||
</item>
|
||||
<item handle="031b4af5197ec" order="0" parent="73475cb40a568">
|
||||
<name>Scene 2</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
<item handle="0000000000019" parent="0000000000018" root="0000000000018" order="0" type="FILE" class="PLOT" layout="NOTE">
|
||||
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000008" import="i00000c" exported="True">Main Plot</name>
|
||||
</item>
|
||||
<item handle="41cfc0d1f2d12" order="0" parent="73475cb40a568">
|
||||
<name>Scene 3</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
<item handle="000000000001a" parent="None" root="000000000001a" order="0" type="ROOT" class="CHARACTER">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000008" import="i00000c">Characters</name>
|
||||
</item>
|
||||
<item handle="2858dcd1057d3" order="0" parent="73475cb40a568">
|
||||
<name>Scene 4</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
<item handle="000000000001b" parent="000000000001a" root="000000000001a" order="0" type="FILE" class="CHARACTER" layout="NOTE">
|
||||
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000008" import="i00000c" exported="True">Protagonist</name>
|
||||
</item>
|
||||
<item handle="2fca346db6561" order="0" parent="73475cb40a568">
|
||||
<name>Scene 5</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
<item handle="000000000001c" parent="None" root="000000000001c" order="0" type="ROOT" class="WORLD">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000008" import="i00000c">Locations</name>
|
||||
</item>
|
||||
<item handle="02d20bbd7e394" order="0" parent="73475cb40a568">
|
||||
<name>Scene 6</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
<item handle="000000000001d" parent="000000000001c" root="000000000001c" order="0" type="FILE" class="WORLD" layout="NOTE">
|
||||
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000008" import="i00000c" exported="True">Main Location</name>
|
||||
</item>
|
||||
<item handle="000000000001e" parent="None" root="000000000001e" order="0" type="ROOT" class="ARCHIVE">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000008" import="i00000c">Archive</name>
|
||||
</item>
|
||||
<item handle="000000000001f" parent="None" root="000000000001f" order="0" type="ROOT" class="TRASH">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000008" import="i00000c">Trash</name>
|
||||
</item>
|
||||
</content>
|
||||
</novelWriterXML>
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<novelWriterXML appVersion="1.7-beta1" hexVersion="0x010700b1" fileVersion="1.4" timeStamp="2022-07-31 18:40:28">
|
||||
<project>
|
||||
<name>New Project</name>
|
||||
<title>New Novel</title>
|
||||
<author>Jane Doe</author>
|
||||
<saveCount>2</saveCount>
|
||||
<autoCount>1</autoCount>
|
||||
<editTime>0</editTime>
|
||||
</project>
|
||||
<settings>
|
||||
<doBackup>True</doBackup>
|
||||
<language>None</language>
|
||||
<spellCheck>False</spellCheck>
|
||||
<spellLang>None</spellLang>
|
||||
<lastEdited>None</lastEdited>
|
||||
<lastViewed>None</lastViewed>
|
||||
<lastNovel>None</lastNovel>
|
||||
<lastOutline>None</lastOutline>
|
||||
<lastWordCount>2</lastWordCount>
|
||||
<novelWordCount>1</novelWordCount>
|
||||
<notesWordCount>1</notesWordCount>
|
||||
<autoReplace/>
|
||||
<titleFormat>
|
||||
<title>%title%</title>
|
||||
<chapter>%title%</chapter>
|
||||
<unnumbered>%title%</unnumbered>
|
||||
<scene>* * *</scene>
|
||||
<section></section>
|
||||
</titleFormat>
|
||||
<status>
|
||||
<entry key="s000008" count="7" red="100" green="100" blue="100">New</entry>
|
||||
<entry key="s000009" count="0" red="200" green="50" blue="0">Note</entry>
|
||||
<entry key="s00000a" count="0" red="200" green="150" blue="0">Draft</entry>
|
||||
<entry key="s00000b" count="0" red="50" green="200" blue="0">Finished</entry>
|
||||
</status>
|
||||
<importance>
|
||||
<entry key="i00000c" count="4" red="100" green="100" blue="100">New</entry>
|
||||
<entry key="i00000d" count="0" red="200" green="50" blue="0">Minor</entry>
|
||||
<entry key="i00000e" count="0" red="200" green="150" blue="0">Major</entry>
|
||||
<entry key="i00000f" count="0" red="50" green="200" blue="0">Main</entry>
|
||||
</importance>
|
||||
</settings>
|
||||
<content count="11">
|
||||
<item handle="0000000000010" parent="None" root="0000000000010" order="0" type="ROOT" class="NOVEL">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000008" import="i00000c">Novel</name>
|
||||
</item>
|
||||
<item handle="0000000000011" parent="None" root="0000000000011" order="0" type="ROOT" class="PLOT">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000008" import="i00000c">Plot</name>
|
||||
</item>
|
||||
<item handle="0000000000012" parent="None" root="0000000000012" order="0" type="ROOT" class="CHARACTER">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000008" import="i00000c">Characters</name>
|
||||
</item>
|
||||
<item handle="0000000000013" parent="None" root="0000000000013" order="0" type="ROOT" class="WORLD">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000008" import="i00000c">World</name>
|
||||
</item>
|
||||
<item handle="0000000000014" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000008" import="i00000c" exported="True">Title Page</name>
|
||||
</item>
|
||||
<item handle="0000000000015" parent="0000000000010" root="0000000000010" order="0" type="FOLDER" class="NOVEL">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000008" import="i00000c">New Chapter</name>
|
||||
</item>
|
||||
<item handle="0000000000016" parent="0000000000015" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000008" import="i00000c" exported="True">New Chapter</name>
|
||||
</item>
|
||||
<item handle="0000000000017" parent="0000000000015" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000008" import="i00000c" exported="True">New Scene</name>
|
||||
</item>
|
||||
<item handle="0000000000028" parent="0000000000015" root="0000000000010" order="0" type="FOLDER" class="NOVEL">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000008" import="i00000c">Stuff</name>
|
||||
</item>
|
||||
<item handle="0000000000029" parent="0000000000015" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="5" wordCount="1" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000008" import="i00000c" exported="True">Hello</name>
|
||||
</item>
|
||||
<item handle="000000000002a" parent="0000000000012" root="0000000000012" order="0" type="FILE" class="CHARACTER" layout="NOTE">
|
||||
<meta expanded="False" charCount="4" wordCount="1" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000008" import="i00000c" exported="True">Jane</name>
|
||||
</item>
|
||||
</content>
|
||||
</novelWriterXML>
|
||||
@@ -1,139 +0,0 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<novelWriterXML appVersion="1.5-alpha0" hexVersion="0x010500a0" fileVersion="1.2" timeStamp="2021-08-02 02:28:30">
|
||||
<project>
|
||||
<name>New Project</name>
|
||||
<title></title>
|
||||
<saveCount>2</saveCount>
|
||||
<autoCount>1</autoCount>
|
||||
<editTime>0</editTime>
|
||||
</project>
|
||||
<settings>
|
||||
<doBackup>True</doBackup>
|
||||
<language>None</language>
|
||||
<spellCheck>False</spellCheck>
|
||||
<spellLang>None</spellLang>
|
||||
<autoOutline>True</autoOutline>
|
||||
<lastEdited>None</lastEdited>
|
||||
<lastViewed>None</lastViewed>
|
||||
<lastWordCount>0</lastWordCount>
|
||||
<novelWordCount>0</novelWordCount>
|
||||
<notesWordCount>0</notesWordCount>
|
||||
<autoReplace/>
|
||||
<titleFormat>
|
||||
<title>%title%</title>
|
||||
<chapter>%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="10">
|
||||
<item handle="73475cb40a568" order="0" parent="None">
|
||||
<name>Novel</name>
|
||||
<type>ROOT</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="44cb730c42048" order="0" parent="None">
|
||||
<name>Plot</name>
|
||||
<type>ROOT</type>
|
||||
<class>PLOT</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="71ee45a3c0db9" order="0" parent="None">
|
||||
<name>Characters</name>
|
||||
<type>ROOT</type>
|
||||
<class>CHARACTER</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="811786ad1ae74" order="0" parent="None">
|
||||
<name>World</name>
|
||||
<type>ROOT</type>
|
||||
<class>WORLD</class>
|
||||
<status>New</status>
|
||||
<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>DOCUMENT</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="31489056e0916" order="0" parent="73475cb40a568">
|
||||
<name>New Chapter</name>
|
||||
<type>FOLDER</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="98010bd9270f9" order="0" parent="31489056e0916">
|
||||
<name>New Chapter</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="0e17daca5f3e1" order="0" parent="31489056e0916">
|
||||
<name>New Scene</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="1a6562590ef19" order="0" parent="31489056e0916">
|
||||
<name>Hello</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="031b4af5197ec" order="0" parent="71ee45a3c0db9">
|
||||
<name>Jane</name>
|
||||
<type>FILE</type>
|
||||
<class>CHARACTER</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>NOTE</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
</content>
|
||||
</novelWriterXML>
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<novelWriterXML appVersion="1.5-alpha0" hexVersion="0x010500a0" fileVersion="1.2" timeStamp="2021-08-02 02:31:23">
|
||||
<novelWriterXML appVersion="1.7-beta1" hexVersion="0x010700b1" fileVersion="1.4" timeStamp="2022-07-31 18:40:28">
|
||||
<project>
|
||||
<name>New Project</name>
|
||||
<title></title>
|
||||
<saveCount>1</saveCount>
|
||||
<saveCount>2</saveCount>
|
||||
<autoCount>1</autoCount>
|
||||
<editTime>0</editTime>
|
||||
</project>
|
||||
@@ -12,9 +12,10 @@
|
||||
<language>None</language>
|
||||
<spellCheck>False</spellCheck>
|
||||
<spellLang>None</spellLang>
|
||||
<autoOutline>True</autoOutline>
|
||||
<lastEdited>None</lastEdited>
|
||||
<lastViewed>None</lastViewed>
|
||||
<lastNovel>None</lastNovel>
|
||||
<lastOutline>None</lastOutline>
|
||||
<lastWordCount>0</lastWordCount>
|
||||
<novelWordCount>0</novelWordCount>
|
||||
<notesWordCount>0</notesWordCount>
|
||||
@@ -27,89 +28,50 @@
|
||||
<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>
|
||||
<entry key="s000008" count="5" red="100" green="100" blue="100">New</entry>
|
||||
<entry key="s000009" count="0" red="200" green="50" blue="0">Note</entry>
|
||||
<entry key="s00000a" count="0" red="200" green="150" blue="0">Draft</entry>
|
||||
<entry key="s00000b" count="0" red="50" green="200" blue="0">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>
|
||||
<entry key="i00000c" count="3" red="100" green="100" blue="100">New</entry>
|
||||
<entry key="i00000d" count="0" red="200" green="50" blue="0">Minor</entry>
|
||||
<entry key="i00000e" count="0" red="200" green="150" blue="0">Major</entry>
|
||||
<entry key="i00000f" count="0" red="50" green="200" blue="0">Main</entry>
|
||||
</importance>
|
||||
</settings>
|
||||
<content count="8">
|
||||
<item handle="73475cb40a568" order="0" parent="None">
|
||||
<name>Novel</name>
|
||||
<type>ROOT</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<item handle="0000000000010" parent="None" root="0000000000010" order="0" type="ROOT" class="NOVEL">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000008" import="i00000c">Novel</name>
|
||||
</item>
|
||||
<item handle="44cb730c42048" order="0" parent="None">
|
||||
<name>Plot</name>
|
||||
<type>ROOT</type>
|
||||
<class>PLOT</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<item handle="0000000000011" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000008" import="i00000c" exported="True">Title Page</name>
|
||||
</item>
|
||||
<item handle="71ee45a3c0db9" order="0" parent="None">
|
||||
<name>Characters</name>
|
||||
<type>ROOT</type>
|
||||
<class>CHARACTER</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<item handle="0000000000012" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000008" import="i00000c" exported="True">New Chapter</name>
|
||||
</item>
|
||||
<item handle="811786ad1ae74" order="0" parent="None">
|
||||
<name>World</name>
|
||||
<type>ROOT</type>
|
||||
<class>WORLD</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<item handle="0000000000013" parent="0000000000012" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000008" import="i00000c" exported="True">New Scene</name>
|
||||
</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>DOCUMENT</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
<item handle="0000000000014" parent="None" root="0000000000014" order="0" type="ROOT" class="PLOT">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000008" import="i00000c">Plot</name>
|
||||
</item>
|
||||
<item handle="31489056e0916" order="0" parent="73475cb40a568">
|
||||
<name>New Chapter</name>
|
||||
<type>FOLDER</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<item handle="0000000000015" parent="None" root="0000000000015" order="0" type="ROOT" class="CHARACTER">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000008" import="i00000c">Characters</name>
|
||||
</item>
|
||||
<item handle="98010bd9270f9" order="0" parent="31489056e0916">
|
||||
<name>New Chapter</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
<item handle="0000000000016" parent="None" root="0000000000016" order="0" type="ROOT" class="WORLD">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000008" import="i00000c">Locations</name>
|
||||
</item>
|
||||
<item handle="0e17daca5f3e1" order="0" parent="31489056e0916">
|
||||
<name>New Scene</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
<item handle="0000000000017" parent="None" root="0000000000017" order="0" type="ROOT" class="ARCHIVE">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000008" import="i00000c">Archive</name>
|
||||
</item>
|
||||
</content>
|
||||
</novelWriterXML>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<novelWriterXML appVersion="1.5-alpha0" hexVersion="0x010500a0" fileVersion="1.2" timeStamp="2021-08-02 02:29:01">
|
||||
<novelWriterXML appVersion="1.7-beta1" hexVersion="0x010700b1" fileVersion="1.4" timeStamp="2022-07-31 18:40:28">
|
||||
<project>
|
||||
<name>New Project</name>
|
||||
<title></title>
|
||||
<title>New Novel</title>
|
||||
<author>Jane Doe</author>
|
||||
<saveCount>2</saveCount>
|
||||
<autoCount>1</autoCount>
|
||||
<editTime>0</editTime>
|
||||
@@ -12,9 +13,10 @@
|
||||
<language>None</language>
|
||||
<spellCheck>False</spellCheck>
|
||||
<spellLang>None</spellLang>
|
||||
<autoOutline>True</autoOutline>
|
||||
<lastEdited>None</lastEdited>
|
||||
<lastViewed>None</lastViewed>
|
||||
<lastNovel>None</lastNovel>
|
||||
<lastOutline>None</lastOutline>
|
||||
<lastWordCount>0</lastWordCount>
|
||||
<novelWordCount>0</novelWordCount>
|
||||
<notesWordCount>0</notesWordCount>
|
||||
@@ -27,117 +29,82 @@
|
||||
<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>
|
||||
<entry key="s000008" count="6" red="100" green="100" blue="100">New</entry>
|
||||
<entry key="s000009" count="0" red="200" green="50" blue="0">Note</entry>
|
||||
<entry key="s00000a" count="0" red="200" green="150" blue="0">Draft</entry>
|
||||
<entry key="s00000b" count="0" red="50" green="200" blue="0">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>
|
||||
<entry key="i00000c" count="10" red="100" green="100" blue="100">New</entry>
|
||||
<entry key="i00000d" count="0" red="200" green="50" blue="0">Minor</entry>
|
||||
<entry key="i00000e" count="0" red="200" green="150" blue="0">Major</entry>
|
||||
<entry key="i00000f" count="0" red="50" green="200" blue="0">Main</entry>
|
||||
</importance>
|
||||
</settings>
|
||||
<content count="12">
|
||||
<item handle="73475cb40a568" order="0" parent="None">
|
||||
<name>Novel</name>
|
||||
<type>ROOT</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<content count="16">
|
||||
<item handle="0000000000010" parent="None" root="0000000000010" order="0" type="ROOT" class="NOVEL">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000008" import="i00000c">Novel</name>
|
||||
</item>
|
||||
<item handle="44cb730c42048" order="0" parent="None">
|
||||
<name>Plot</name>
|
||||
<type>ROOT</type>
|
||||
<class>PLOT</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<item handle="0000000000011" parent="None" root="0000000000011" order="0" type="ROOT" class="PLOT">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000008" import="i00000c">Plot</name>
|
||||
</item>
|
||||
<item handle="71ee45a3c0db9" order="0" parent="None">
|
||||
<name>Characters</name>
|
||||
<type>ROOT</type>
|
||||
<class>CHARACTER</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<item handle="0000000000012" parent="None" root="0000000000012" order="0" type="ROOT" class="CHARACTER">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000008" import="i00000c">Characters</name>
|
||||
</item>
|
||||
<item handle="811786ad1ae74" order="0" parent="None">
|
||||
<name>World</name>
|
||||
<type>ROOT</type>
|
||||
<class>WORLD</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<item handle="0000000000013" parent="None" root="0000000000013" order="0" type="ROOT" class="WORLD">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000008" import="i00000c">World</name>
|
||||
</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>DOCUMENT</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
<item handle="0000000000014" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000008" import="i00000c" exported="True">Title Page</name>
|
||||
</item>
|
||||
<item handle="31489056e0916" order="0" parent="73475cb40a568">
|
||||
<name>New Chapter</name>
|
||||
<type>FOLDER</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<item handle="0000000000015" parent="0000000000010" root="0000000000010" order="0" type="FOLDER" class="NOVEL">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000008" import="i00000c">New Chapter</name>
|
||||
</item>
|
||||
<item handle="98010bd9270f9" order="0" parent="31489056e0916">
|
||||
<name>New Chapter</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
<item handle="0000000000016" parent="0000000000015" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000008" import="i00000c" exported="True">New Chapter</name>
|
||||
</item>
|
||||
<item handle="0e17daca5f3e1" order="0" parent="31489056e0916">
|
||||
<name>New Scene</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
<item handle="0000000000017" parent="0000000000015" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000008" import="i00000c" exported="True">New Scene</name>
|
||||
</item>
|
||||
<item handle="1a6562590ef19" order="0" parent="None">
|
||||
<name>Timeline</name>
|
||||
<type>ROOT</type>
|
||||
<class>TIMELINE</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<item handle="0000000000028" parent="None" root="0000000000028" order="0" type="ROOT" class="NOVEL">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000008" import="i00000c">Novel</name>
|
||||
</item>
|
||||
<item handle="031b4af5197ec" order="0" parent="None">
|
||||
<name>Object</name>
|
||||
<type>ROOT</type>
|
||||
<class>OBJECT</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<item handle="0000000000029" parent="None" root="0000000000029" order="0" type="ROOT" class="PLOT">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000008" import="i00000c">Plot</name>
|
||||
</item>
|
||||
<item handle="41cfc0d1f2d12" order="0" parent="None">
|
||||
<name>Custom1</name>
|
||||
<type>ROOT</type>
|
||||
<class>CUSTOM</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<item handle="000000000002a" parent="None" root="000000000002a" order="0" type="ROOT" class="CHARACTER">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000008" import="i00000c">Characters</name>
|
||||
</item>
|
||||
<item handle="2858dcd1057d3" order="0" parent="None">
|
||||
<name>Custom2</name>
|
||||
<type>ROOT</type>
|
||||
<class>CUSTOM</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<item handle="000000000002b" parent="None" root="000000000002b" order="0" type="ROOT" class="WORLD">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000008" import="i00000c">Locations</name>
|
||||
</item>
|
||||
<item handle="000000000002c" parent="None" root="000000000002c" order="0" type="ROOT" class="TIMELINE">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000008" import="i00000c">Timeline</name>
|
||||
</item>
|
||||
<item handle="000000000002d" parent="None" root="000000000002d" order="0" type="ROOT" class="OBJECT">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000008" import="i00000c">Objects</name>
|
||||
</item>
|
||||
<item handle="000000000002e" parent="None" root="000000000002e" order="0" type="ROOT" class="CUSTOM">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000008" import="i00000c">Custom</name>
|
||||
</item>
|
||||
<item handle="000000000002f" parent="None" root="000000000002f" order="0" type="ROOT" class="CUSTOM">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000008" import="i00000c">Custom</name>
|
||||
</item>
|
||||
</content>
|
||||
</novelWriterXML>
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
%%~name: New Scene
|
||||
%%~path: 31489056e0916/0e17daca5f3e1
|
||||
%%~path: 000000000000d/000000000000f
|
||||
%%~kind: NOVEL/DOCUMENT
|
||||
# Novel
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
%%~name: New File
|
||||
%%~path: 71ee45a3c0db9/1a6562590ef19
|
||||
%%~name: New Note
|
||||
%%~path: 000000000000a/0000000000020
|
||||
%%~kind: CHARACTER/NOTE
|
||||
# Jane Doe
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
%%~name: New File
|
||||
%%~path: 44cb730c42048/031b4af5197ec
|
||||
%%~name: New Note
|
||||
%%~path: 0000000000009/0000000000021
|
||||
%%~kind: PLOT/NOTE
|
||||
# Main Plot
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
%%~name: New File
|
||||
%%~path: 811786ad1ae74/41cfc0d1f2d12
|
||||
%%~name: New Note
|
||||
%%~path: 000000000000b/0000000000022
|
||||
%%~kind: WORLD/NOTE
|
||||
# Main Location
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<novelWriterXML appVersion="1.6.3" hexVersion="0x010603f0" fileVersion="1.3" timeStamp="2022-09-29 14:24:23">
|
||||
<novelWriterXML appVersion="1.7-beta1" hexVersion="0x010700b1" fileVersion="1.4" timeStamp="2022-09-29 19:57:40">
|
||||
<project>
|
||||
<name>New Project</name>
|
||||
<title></title>
|
||||
<saveCount>5</saveCount>
|
||||
<title>New Novel</title>
|
||||
<author>Jane Doe</author>
|
||||
<saveCount>4</saveCount>
|
||||
<autoCount>2</autoCount>
|
||||
<editTime>4</editTime>
|
||||
</project>
|
||||
@@ -12,11 +13,12 @@
|
||||
<language>None</language>
|
||||
<spellCheck>True</spellCheck>
|
||||
<spellLang>None</spellLang>
|
||||
<autoOutline>True</autoOutline>
|
||||
<lastEdited>0e17daca5f3e1</lastEdited>
|
||||
<lastEdited>000000000000f</lastEdited>
|
||||
<lastViewed>None</lastViewed>
|
||||
<lastWordCount>142</lastWordCount>
|
||||
<novelWordCount>115</novelWordCount>
|
||||
<lastNovel>0000000000008</lastNovel>
|
||||
<lastOutline>0000000000008</lastOutline>
|
||||
<lastWordCount>145</lastWordCount>
|
||||
<novelWordCount>118</novelWordCount>
|
||||
<notesWordCount>27</notesWordCount>
|
||||
<autoReplace/>
|
||||
<titleFormat>
|
||||
@@ -27,132 +29,66 @@
|
||||
<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>
|
||||
<entry key="s000000" count="5" red="100" green="100" blue="100">New</entry>
|
||||
<entry key="s000001" count="0" red="200" green="50" blue="0">Note</entry>
|
||||
<entry key="s000002" count="0" red="200" green="150" blue="0">Draft</entry>
|
||||
<entry key="s000003" count="0" red="50" green="200" blue="0">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>
|
||||
<entry key="i000004" count="7" red="100" green="100" blue="100">New</entry>
|
||||
<entry key="i000005" count="0" red="200" green="50" blue="0">Minor</entry>
|
||||
<entry key="i000006" count="0" red="200" green="150" blue="0">Major</entry>
|
||||
<entry key="i000007" count="0" red="50" green="200" blue="0">Main</entry>
|
||||
</importance>
|
||||
</settings>
|
||||
<content count="12">
|
||||
<item handle="73475cb40a568" order="0" parent="None">
|
||||
<name>Novel</name>
|
||||
<type>ROOT</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>True</expanded>
|
||||
<item handle="0000000000008" parent="None" root="0000000000008" order="0" type="ROOT" class="NOVEL">
|
||||
<meta expanded="True"/>
|
||||
<name status="s000000" import="i000004">Novel</name>
|
||||
</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>DOCUMENT</layout>
|
||||
<charCount>11</charCount>
|
||||
<wordCount>2</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
<item handle="000000000000c" parent="0000000000008" root="0000000000008" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="20" wordCount="5" paraCount="1" cursorPos="0"/>
|
||||
<name status="s000000" import="i000004" exported="True">Title Page</name>
|
||||
</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 handle="000000000000d" parent="0000000000008" root="0000000000008" order="1" type="FOLDER" class="NOVEL">
|
||||
<meta expanded="True"/>
|
||||
<name status="s000000" import="i000004">New Chapter</name>
|
||||
</item>
|
||||
<item handle="98010bd9270f9" order="0" parent="31489056e0916">
|
||||
<name>New Chapter</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>11</charCount>
|
||||
<wordCount>2</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
<item handle="000000000000e" parent="000000000000d" root="0000000000008" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="11" wordCount="2" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000000" import="i000004" exported="True">New Chapter</name>
|
||||
</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>DOCUMENT</layout>
|
||||
<charCount>693</charCount>
|
||||
<wordCount>111</wordCount>
|
||||
<paraCount>12</paraCount>
|
||||
<cursorPos>917</cursorPos>
|
||||
<item handle="000000000000f" parent="000000000000d" root="0000000000008" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="693" wordCount="111" paraCount="12" cursorPos="917"/>
|
||||
<name status="s000000" import="i000004" exported="True">New Scene</name>
|
||||
</item>
|
||||
<item handle="44cb730c42048" order="1" parent="None">
|
||||
<name>Plot</name>
|
||||
<type>ROOT</type>
|
||||
<class>PLOT</class>
|
||||
<status>New</status>
|
||||
<expanded>True</expanded>
|
||||
<item handle="0000000000009" parent="None" root="0000000000009" order="1" type="ROOT" class="PLOT">
|
||||
<meta expanded="True"/>
|
||||
<name status="s000000" import="i000004">Plot</name>
|
||||
</item>
|
||||
<item handle="031b4af5197ec" order="0" parent="44cb730c42048">
|
||||
<name>New File</name>
|
||||
<type>FILE</type>
|
||||
<class>PLOT</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>NOTE</layout>
|
||||
<charCount>48</charCount>
|
||||
<wordCount>10</wordCount>
|
||||
<paraCount>1</paraCount>
|
||||
<cursorPos>69</cursorPos>
|
||||
<item handle="0000000000021" parent="0000000000009" root="0000000000009" order="0" type="FILE" class="PLOT" layout="NOTE">
|
||||
<meta expanded="False" charCount="48" wordCount="10" paraCount="1" cursorPos="69"/>
|
||||
<name status="s000000" import="i000004" exported="True">New Note</name>
|
||||
</item>
|
||||
<item handle="71ee45a3c0db9" order="2" parent="None">
|
||||
<name>Characters</name>
|
||||
<type>ROOT</type>
|
||||
<class>CHARACTER</class>
|
||||
<status>New</status>
|
||||
<expanded>True</expanded>
|
||||
<item handle="000000000000a" parent="None" root="000000000000a" order="2" type="ROOT" class="CHARACTER">
|
||||
<meta expanded="True"/>
|
||||
<name status="s000000" import="i000004">Characters</name>
|
||||
</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 handle="0000000000020" parent="000000000000a" root="000000000000a" order="0" type="FILE" class="CHARACTER" layout="NOTE">
|
||||
<meta expanded="False" charCount="34" wordCount="8" paraCount="1" cursorPos="51"/>
|
||||
<name status="s000000" import="i000004" exported="True">New Note</name>
|
||||
</item>
|
||||
<item handle="811786ad1ae74" order="3" parent="None">
|
||||
<name>World</name>
|
||||
<type>ROOT</type>
|
||||
<class>WORLD</class>
|
||||
<status>New</status>
|
||||
<expanded>True</expanded>
|
||||
<item handle="000000000000b" parent="None" root="000000000000b" order="3" type="ROOT" class="WORLD">
|
||||
<meta expanded="True"/>
|
||||
<name status="s000000" import="i000004">World</name>
|
||||
</item>
|
||||
<item handle="41cfc0d1f2d12" order="0" parent="811786ad1ae74">
|
||||
<name>New File</name>
|
||||
<type>FILE</type>
|
||||
<class>WORLD</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>NOTE</layout>
|
||||
<charCount>51</charCount>
|
||||
<wordCount>9</wordCount>
|
||||
<paraCount>1</paraCount>
|
||||
<cursorPos>68</cursorPos>
|
||||
<item handle="0000000000022" parent="000000000000b" root="000000000000b" order="0" type="FILE" class="WORLD" layout="NOTE">
|
||||
<meta expanded="False" charCount="51" wordCount="9" paraCount="1" cursorPos="68"/>
|
||||
<name status="s000000" import="i000004" exported="True">New Note</name>
|
||||
</item>
|
||||
<item handle="2fca346db6561" order="4" parent="None">
|
||||
<name>Trash</name>
|
||||
<type>TRASH</type>
|
||||
<class>TRASH</class>
|
||||
<status>None</status>
|
||||
<expanded>True</expanded>
|
||||
<item handle="0000000000024" parent="None" root="0000000000024" order="4" type="ROOT" class="TRASH">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000000" import="i000004">Trash</name>
|
||||
</item>
|
||||
</content>
|
||||
</novelWriterXML>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<novelWriterXML appVersion="1.5-alpha0" hexVersion="0x010500a0" fileVersion="1.2" timeStamp="2021-08-02 02:49:51">
|
||||
<novelWriterXML appVersion="1.7-beta1" hexVersion="0x010700b1" fileVersion="1.4" timeStamp="2022-07-31 18:40:34">
|
||||
<project>
|
||||
<name>New Project</name>
|
||||
<title></title>
|
||||
<saveCount>3</saveCount>
|
||||
<title>New Novel</title>
|
||||
<author>Jane Doe</author>
|
||||
<saveCount>2</saveCount>
|
||||
<autoCount>1</autoCount>
|
||||
<editTime>0</editTime>
|
||||
</project>
|
||||
@@ -12,11 +13,12 @@
|
||||
<language>None</language>
|
||||
<spellCheck>False</spellCheck>
|
||||
<spellLang>None</spellLang>
|
||||
<autoOutline>True</autoOutline>
|
||||
<lastEdited>None</lastEdited>
|
||||
<lastViewed>None</lastViewed>
|
||||
<lastWordCount>6</lastWordCount>
|
||||
<novelWordCount>6</novelWordCount>
|
||||
<lastNovel>None</lastNovel>
|
||||
<lastOutline>None</lastOutline>
|
||||
<lastWordCount>9</lastWordCount>
|
||||
<novelWordCount>9</novelWordCount>
|
||||
<notesWordCount>0</notesWordCount>
|
||||
<autoReplace/>
|
||||
<titleFormat>
|
||||
@@ -27,89 +29,50 @@
|
||||
<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>
|
||||
<entry key="s000000" count="5" red="100" green="100" blue="100">New</entry>
|
||||
<entry key="s000001" count="0" red="200" green="50" blue="0">Note</entry>
|
||||
<entry key="s000002" count="0" red="200" green="150" blue="0">Draft</entry>
|
||||
<entry key="s000003" count="0" red="50" green="200" blue="0">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>
|
||||
<entry key="i000004" count="3" red="100" green="100" blue="100">New</entry>
|
||||
<entry key="i000005" count="0" red="200" green="50" blue="0">Minor</entry>
|
||||
<entry key="i000006" count="0" red="200" green="150" blue="0">Major</entry>
|
||||
<entry key="i000007" count="0" red="50" green="200" blue="0">Main</entry>
|
||||
</importance>
|
||||
</settings>
|
||||
<content count="8">
|
||||
<item handle="73475cb40a568" order="0" parent="None">
|
||||
<name>Novel</name>
|
||||
<type>ROOT</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<item handle="0000000000008" parent="None" root="0000000000008" order="0" type="ROOT" class="NOVEL">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000000" import="i000004">Novel</name>
|
||||
</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>DOCUMENT</layout>
|
||||
<charCount>11</charCount>
|
||||
<wordCount>2</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
<item handle="000000000000c" parent="0000000000008" root="0000000000008" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="20" wordCount="5" paraCount="1" cursorPos="0"/>
|
||||
<name status="s000000" import="i000004" exported="True">Title Page</name>
|
||||
</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 handle="000000000000d" parent="0000000000008" root="0000000000008" order="1" type="FOLDER" class="NOVEL">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000000" import="i000004">New Chapter</name>
|
||||
</item>
|
||||
<item handle="98010bd9270f9" order="0" parent="31489056e0916">
|
||||
<name>New Chapter</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>11</charCount>
|
||||
<wordCount>2</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
<item handle="000000000000e" parent="000000000000d" root="0000000000008" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="11" wordCount="2" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000000" import="i000004" exported="True">New Chapter</name>
|
||||
</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>DOCUMENT</layout>
|
||||
<charCount>9</charCount>
|
||||
<wordCount>2</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
<item handle="000000000000f" parent="000000000000d" root="0000000000008" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="9" wordCount="2" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000000" import="i000004" exported="True">New Scene</name>
|
||||
</item>
|
||||
<item handle="44cb730c42048" order="1" parent="None">
|
||||
<name>Plot</name>
|
||||
<type>ROOT</type>
|
||||
<class>PLOT</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<item handle="0000000000009" parent="None" root="0000000000009" order="1" type="ROOT" class="PLOT">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000000" import="i000004">Plot</name>
|
||||
</item>
|
||||
<item handle="71ee45a3c0db9" order="2" parent="None">
|
||||
<name>Characters</name>
|
||||
<type>ROOT</type>
|
||||
<class>CHARACTER</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<item handle="000000000000a" parent="None" root="000000000000a" order="2" type="ROOT" class="CHARACTER">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000000" import="i000004">Characters</name>
|
||||
</item>
|
||||
<item handle="811786ad1ae74" order="3" parent="None">
|
||||
<name>World</name>
|
||||
<type>ROOT</type>
|
||||
<class>WORLD</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<item handle="000000000000b" parent="None" root="000000000000b" order="3" type="ROOT" class="WORLD">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000000" import="i000004">World</name>
|
||||
</item>
|
||||
</content>
|
||||
</novelWriterXML>
|
||||
|
||||
@@ -13,8 +13,6 @@ hidehscroll = True
|
||||
[Sizes]
|
||||
geometry = 1200, 650
|
||||
preferences = 670, 589
|
||||
treecols = 200, 50, 30
|
||||
novelcols = 200, 50
|
||||
projcols = 200, 60, 140
|
||||
mainpane = 300, 800
|
||||
docpane = 400, 400
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<novelWriterXML appVersion="1.5-alpha0" hexVersion="0x010500a0" fileVersion="1.2" timeStamp="2021-08-02 03:04:13">
|
||||
<novelWriterXML appVersion="1.7-beta1" hexVersion="0x010700b1" fileVersion="1.4" timeStamp="2022-07-31 18:40:31">
|
||||
<project>
|
||||
<name>Project Name</name>
|
||||
<title>Project Title</title>
|
||||
<author>Jane Doe</author>
|
||||
<author>John Doh</author>
|
||||
<saveCount>2</saveCount>
|
||||
<saveCount>1</saveCount>
|
||||
<autoCount>1</autoCount>
|
||||
<editTime>0</editTime>
|
||||
</project>
|
||||
@@ -14,16 +14,17 @@
|
||||
<language>None</language>
|
||||
<spellCheck>False</spellCheck>
|
||||
<spellLang>en</spellLang>
|
||||
<autoOutline>True</autoOutline>
|
||||
<lastEdited>None</lastEdited>
|
||||
<lastViewed>None</lastViewed>
|
||||
<lastWordCount>6</lastWordCount>
|
||||
<novelWordCount>6</novelWordCount>
|
||||
<lastNovel>None</lastNovel>
|
||||
<lastOutline>None</lastOutline>
|
||||
<lastWordCount>9</lastWordCount>
|
||||
<novelWordCount>9</novelWordCount>
|
||||
<notesWordCount>0</notesWordCount>
|
||||
<autoReplace>
|
||||
<entry key="A">B</entry>
|
||||
<entry key="C">D</entry>
|
||||
<entry key="This">With This Stuff </entry>
|
||||
<entry key="This">With This Stuff</entry>
|
||||
</autoReplace>
|
||||
<titleFormat>
|
||||
<title>%title%</title>
|
||||
@@ -33,89 +34,50 @@
|
||||
<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="200" red="50">Finished</entry>
|
||||
<entry blue="40" green="30" red="20">Final</entry>
|
||||
<entry key="s000000" count="5" red="100" green="100" blue="100">New</entry>
|
||||
<entry key="s000001" count="0" red="200" green="50" blue="0">Note</entry>
|
||||
<entry key="s000003" count="0" red="50" green="200" blue="0">Finished</entry>
|
||||
<entry key="s000010" count="0" red="20" green="30" blue="40">Final</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="0" red="0">Final</entry>
|
||||
<entry key="i000004" count="3" red="100" green="100" blue="100">New</entry>
|
||||
<entry key="i000005" count="0" red="200" green="50" blue="0">Minor</entry>
|
||||
<entry key="i000006" count="0" red="200" green="150" blue="0">Major</entry>
|
||||
<entry key="i000011" count="0" red="100" green="100" blue="100">Final</entry>
|
||||
</importance>
|
||||
</settings>
|
||||
<content count="8">
|
||||
<item handle="73475cb40a568" order="0" parent="None">
|
||||
<name>Novel</name>
|
||||
<type>ROOT</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<item handle="0000000000008" parent="None" root="0000000000008" order="0" type="ROOT" class="NOVEL">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000000" import="i000004">Novel</name>
|
||||
</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>DOCUMENT</layout>
|
||||
<charCount>11</charCount>
|
||||
<wordCount>2</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
<item handle="000000000000c" parent="0000000000008" root="0000000000008" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="20" wordCount="5" paraCount="1" cursorPos="0"/>
|
||||
<name status="s000000" import="i000004" exported="True">Title Page</name>
|
||||
</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 handle="000000000000d" parent="0000000000008" root="0000000000008" order="1" type="FOLDER" class="NOVEL">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000000" import="i000004">New Chapter</name>
|
||||
</item>
|
||||
<item handle="98010bd9270f9" order="0" parent="31489056e0916">
|
||||
<name>New Chapter</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>DOCUMENT</layout>
|
||||
<charCount>11</charCount>
|
||||
<wordCount>2</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
<item handle="000000000000e" parent="000000000000d" root="0000000000008" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="11" wordCount="2" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000000" import="i000004" exported="True">New Chapter</name>
|
||||
</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>DOCUMENT</layout>
|
||||
<charCount>9</charCount>
|
||||
<wordCount>2</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
<item handle="000000000000f" parent="000000000000d" root="0000000000008" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="False" charCount="9" wordCount="2" paraCount="0" cursorPos="0"/>
|
||||
<name status="s000000" import="i000004" exported="True">New Scene</name>
|
||||
</item>
|
||||
<item handle="44cb730c42048" order="1" parent="None">
|
||||
<name>Plot</name>
|
||||
<type>ROOT</type>
|
||||
<class>PLOT</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<item handle="0000000000009" parent="None" root="0000000000009" order="1" type="ROOT" class="PLOT">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000000" import="i000004">Plot</name>
|
||||
</item>
|
||||
<item handle="71ee45a3c0db9" order="2" parent="None">
|
||||
<name>Characters</name>
|
||||
<type>ROOT</type>
|
||||
<class>CHARACTER</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<item handle="000000000000a" parent="None" root="000000000000a" order="2" type="ROOT" class="CHARACTER">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000000" import="i000004">Characters</name>
|
||||
</item>
|
||||
<item handle="811786ad1ae74" order="3" parent="None">
|
||||
<name>World</name>
|
||||
<type>ROOT</type>
|
||||
<class>WORLD</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<item handle="000000000000b" parent="None" root="000000000000b" order="3" type="ROOT" class="WORLD">
|
||||
<meta expanded="False"/>
|
||||
<name status="s000000" import="i000004">World</name>
|
||||
</item>
|
||||
</content>
|
||||
</novelWriterXML>
|
||||
|
||||
@@ -24,8 +24,6 @@ import os
|
||||
import time
|
||||
import pytest
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from mock import causeOSError
|
||||
from tools import writeFile
|
||||
|
||||
@@ -33,7 +31,7 @@ from novelwriter.guimain import GuiMain
|
||||
from novelwriter.common import (
|
||||
checkString, checkInt, checkFloat, checkBool, checkHandle, isHandle,
|
||||
isTitleTag, isItemClass, isItemType, isItemLayout, hexToInt, checkIntRange,
|
||||
checkIntTuple, formatInt, formatTimeStamp, formatTime, parseTimeStamp,
|
||||
minmax, checkIntTuple, formatInt, formatTimeStamp, formatTime, simplified,
|
||||
splitVersionNumber, transferCase, fuzzyTime, numberToRoman, jsonEncode,
|
||||
readTextFile, makeFileNameSafe, sha256sum, getGuiItem, NWConfigParser
|
||||
)
|
||||
@@ -163,6 +161,7 @@ def testBaseCommon_IsItemClass():
|
||||
assert isItemClass("ARCHIVE") is True
|
||||
assert isItemClass("TRASH") is True
|
||||
|
||||
# Invalid
|
||||
assert isItemClass("None") is False
|
||||
assert isItemClass(None) is False
|
||||
assert isItemClass("STUFF") is False
|
||||
@@ -178,8 +177,11 @@ def testBaseCommon_IsItemType():
|
||||
assert isItemType("ROOT") is True
|
||||
assert isItemType("FOLDER") is True
|
||||
assert isItemType("FILE") is True
|
||||
assert isItemType("TRASH") is True
|
||||
|
||||
# Deprecated Type
|
||||
assert isItemType("TRASH") is False
|
||||
|
||||
# Invalid
|
||||
assert isItemType("None") is False
|
||||
assert isItemType(None) is False
|
||||
assert isItemType("STUFF") is False
|
||||
@@ -195,6 +197,16 @@ def testBaseCommon_IsItemLayout():
|
||||
assert isItemLayout("DOCUMENT") is True
|
||||
assert isItemLayout("NOTE") is True
|
||||
|
||||
# Deprecated Layouts
|
||||
assert isItemLayout("TITLE") is False
|
||||
assert isItemLayout("PAGE") is False
|
||||
assert isItemLayout("BOOK") is False
|
||||
assert isItemLayout("PARTITION") is False
|
||||
assert isItemLayout("UNNUMBERED") is False
|
||||
assert isItemLayout("CHAPTER") is False
|
||||
assert isItemLayout("SCENE") is False
|
||||
|
||||
# Invalid
|
||||
assert isItemLayout("None") is False
|
||||
assert isItemLayout(None) is False
|
||||
assert isItemLayout("STUFF") is False
|
||||
@@ -228,6 +240,16 @@ def testBaseCommon_CheckIntRange():
|
||||
# END Test testBaseCommon_CheckIntRange
|
||||
|
||||
|
||||
@pytest.mark.base
|
||||
def testBaseCommon_MinMax():
|
||||
"""Test the minmax function.
|
||||
"""
|
||||
for i in range(-5, 15):
|
||||
assert 0 <= minmax(i, 0, 10) <= 10
|
||||
|
||||
# END Test testBaseCommon_MinMax
|
||||
|
||||
|
||||
@pytest.mark.base
|
||||
def testBaseCommon_CheckIntTuple():
|
||||
"""Test the checkIntTuple function.
|
||||
@@ -273,18 +295,14 @@ def testBaseCommon_FormatTime():
|
||||
|
||||
|
||||
@pytest.mark.base
|
||||
def testBaseCommon_ParseTimeStamp():
|
||||
"""Test the parseTimeStamp function.
|
||||
def testBaseCommon_Simplified():
|
||||
"""Test the simplified function.
|
||||
"""
|
||||
localEpoch = datetime(2000, 1, 1).timestamp()
|
||||
assert parseTimeStamp(None, 0.0, allowNone=True) is None
|
||||
assert parseTimeStamp("None", 0.0, allowNone=True) is None
|
||||
assert parseTimeStamp("None", 0.0) == 0.0
|
||||
assert parseTimeStamp("2000-01-01 00:00:00", 123.0) == localEpoch
|
||||
assert parseTimeStamp("2000-13-01 00:00:00", 123.0) == 123.0
|
||||
assert parseTimeStamp("2000-01-32 00:00:00", 123.0) == 123.0
|
||||
assert simplified("Hello World") == "Hello World"
|
||||
assert simplified(" Hello World ") == "Hello World"
|
||||
assert simplified("\tHello\n\r\tWorld") == "Hello World"
|
||||
|
||||
# END Test testBaseCommon_ParseTimeStamp
|
||||
# END Test testBaseCommon_Simplified
|
||||
|
||||
|
||||
@pytest.mark.base
|
||||
|
||||
@@ -410,32 +410,6 @@ def testBaseConfig_SettersGetters(tmpConf, tmpDir, outDir, refDir):
|
||||
|
||||
assert tmpConf.setPreferencesSize(700, 615)
|
||||
|
||||
# Project Tree Columns
|
||||
tmpConf.guiScale = 2.0
|
||||
assert tmpConf.setTreeColWidths([10, 20, 25])
|
||||
assert tmpConf.getTreeColWidths() == [10, 20, 24]
|
||||
assert tmpConf.treeColWidth == [5, 10, 12]
|
||||
|
||||
tmpConf.guiScale = 1.0
|
||||
assert tmpConf.setTreeColWidths([10, 20, 25])
|
||||
assert tmpConf.getTreeColWidths() == [10, 20, 25]
|
||||
assert tmpConf.treeColWidth == [10, 20, 25]
|
||||
|
||||
assert tmpConf.setTreeColWidths([200, 50, 30])
|
||||
|
||||
# Novel Tree Columns
|
||||
tmpConf.guiScale = 2.0
|
||||
assert tmpConf.setNovelColWidths([10, 20])
|
||||
assert tmpConf.getNovelColWidths() == [10, 20]
|
||||
assert tmpConf.novelColWidth == [5, 10]
|
||||
|
||||
tmpConf.guiScale = 1.0
|
||||
assert tmpConf.setNovelColWidths([10, 20])
|
||||
assert tmpConf.getNovelColWidths() == [10, 20]
|
||||
assert tmpConf.novelColWidth == [10, 20]
|
||||
|
||||
assert tmpConf.setNovelColWidths([200, 50])
|
||||
|
||||
# Project Settings Tree Columns
|
||||
tmpConf.guiScale = 2.0
|
||||
assert tmpConf.setProjColWidths([10, 20, 30])
|
||||
@@ -505,13 +479,13 @@ def testBaseConfig_SettersGetters(tmpConf, tmpDir, outDir, refDir):
|
||||
# ============
|
||||
|
||||
tmpConf.guiScale = 1.0
|
||||
assert tmpConf.getTextWidth(False) == 600
|
||||
assert tmpConf.getTextWidth(False) == 700
|
||||
assert tmpConf.getTextWidth(True) == 800
|
||||
assert tmpConf.getTextMargin() == 40
|
||||
assert tmpConf.getTabWidth() == 40
|
||||
|
||||
tmpConf.guiScale = 2.0
|
||||
assert tmpConf.getTextWidth(False) == 1200
|
||||
assert tmpConf.getTextWidth(False) == 1400
|
||||
assert tmpConf.getTextWidth(True) == 1600
|
||||
assert tmpConf.getTextMargin() == 80
|
||||
assert tmpConf.getTabWidth() == 80
|
||||
|
||||
@@ -64,9 +64,9 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, nwMinimal):
|
||||
assert theDoc.readDocument() == "### New Scene\n\n"
|
||||
|
||||
# Try to open a new (non-existent) file
|
||||
nHandle = theProject.projTree.findRoot(nwItemClass.NOVEL)
|
||||
nHandle = theProject.tree.findRoot(nwItemClass.NOVEL)
|
||||
assert nHandle is not None
|
||||
xHandle = theProject.newFile("New File", nwItemClass.NOVEL, nHandle)
|
||||
xHandle = theProject.newFile("New File", nHandle)
|
||||
theDoc = NWDoc(theProject, xHandle)
|
||||
assert bool(theDoc) is True
|
||||
assert repr(theDoc) == f"<NWDoc handle={xHandle}>"
|
||||
|
||||
+609
-680
File diff suppressed because it is too large
Load Diff
@@ -23,23 +23,30 @@ import pytest
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from PyQt5.QtGui import QIcon
|
||||
|
||||
from novelwriter.core import NWProject
|
||||
from novelwriter.core.item import NWItem
|
||||
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreItem_Setters(mockGUI):
|
||||
def testCoreItem_Setters(mockGUI, mockRnd):
|
||||
"""Test all the simple setters for the NWItem class.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
theItem = NWItem(theProject)
|
||||
|
||||
statusKeys = ["s000000", "s000001", "s000002", "s000003"]
|
||||
importKeys = ["i000004", "i000005", "i000006", "i000007"]
|
||||
|
||||
# Name
|
||||
theItem.setName("A Name")
|
||||
assert theItem.itemName == "A Name"
|
||||
theItem.setName("\t A Name ")
|
||||
assert theItem.itemName == "A Name"
|
||||
theItem.setName("\t A\t\u2009\u202f\u2002\u2003\u2028\u2029Name ")
|
||||
assert theItem.itemName == "A Name"
|
||||
theItem.setName(123)
|
||||
assert theItem.itemName == ""
|
||||
|
||||
@@ -65,6 +72,18 @@ def testCoreItem_Setters(mockGUI):
|
||||
theItem.setParent("0123456789abc")
|
||||
assert theItem.itemParent == "0123456789abc"
|
||||
|
||||
# Root
|
||||
theItem.setRoot(None)
|
||||
assert theItem.itemRoot is None
|
||||
theItem.setRoot(123)
|
||||
assert theItem.itemRoot is None
|
||||
theItem.setRoot("0123456789abcdef")
|
||||
assert theItem.itemRoot is None
|
||||
theItem.setRoot("0123456789abg")
|
||||
assert theItem.itemRoot is None
|
||||
theItem.setRoot("0123456789abc")
|
||||
assert theItem.itemRoot == "0123456789abc"
|
||||
|
||||
# Order
|
||||
theItem.setOrder(None)
|
||||
assert theItem.itemOrder == 0
|
||||
@@ -74,29 +93,33 @@ def testCoreItem_Setters(mockGUI):
|
||||
assert theItem.itemOrder == 1
|
||||
|
||||
# Importance
|
||||
theItem.setStatus("Nonsense")
|
||||
assert theItem.itemStatus == "New"
|
||||
theItem.setStatus("New")
|
||||
assert theItem.itemStatus == "New"
|
||||
theItem.setStatus("Minor")
|
||||
assert theItem.itemStatus == "Minor"
|
||||
theItem.setStatus("Major")
|
||||
assert theItem.itemStatus == "Major"
|
||||
theItem.setStatus("Main")
|
||||
assert theItem.itemStatus == "Main"
|
||||
theItem._class = nwItemClass.CHARACTER
|
||||
theItem.setImport("Word")
|
||||
assert theItem.itemImport == importKeys[0] # Default
|
||||
for key in importKeys:
|
||||
theItem.setImport(key)
|
||||
assert theItem.itemImport == key
|
||||
|
||||
# Status
|
||||
theItem._class = nwItemClass.NOVEL
|
||||
theItem.setStatus("Nonsense")
|
||||
assert theItem.itemStatus == "New"
|
||||
theItem.setStatus("New")
|
||||
assert theItem.itemStatus == "New"
|
||||
theItem.setStatus("Note")
|
||||
assert theItem.itemStatus == "Note"
|
||||
theItem.setStatus("Draft")
|
||||
assert theItem.itemStatus == "Draft"
|
||||
theItem.setStatus("Finished")
|
||||
assert theItem.itemStatus == "Finished"
|
||||
theItem.setStatus("Word")
|
||||
assert theItem.itemStatus == statusKeys[0] # Default
|
||||
for key in statusKeys:
|
||||
theItem.setStatus(key)
|
||||
assert theItem.itemStatus == key
|
||||
|
||||
# Status/Importance Wrapper
|
||||
theItem._class = nwItemClass.CHARACTER
|
||||
for key in importKeys:
|
||||
theItem.setImport(key)
|
||||
assert theItem.itemImport == key
|
||||
assert theItem.itemStatus == statusKeys[3] # Should not change
|
||||
|
||||
theItem._class = nwItemClass.NOVEL
|
||||
for key in statusKeys:
|
||||
theItem.setStatus(key)
|
||||
assert theItem.itemImport == importKeys[3] # Should not change
|
||||
assert theItem.itemStatus == key
|
||||
|
||||
# Expanded
|
||||
theItem.setExpanded(8)
|
||||
@@ -180,12 +203,16 @@ def testCoreItem_Methods(mockGUI):
|
||||
|
||||
theItem.setType("ROOT")
|
||||
assert theItem.describeMe() == "Root Folder"
|
||||
assert theItem.isRootType() is True
|
||||
|
||||
theItem.setType("FOLDER")
|
||||
assert theItem.describeMe() == "Folder"
|
||||
assert theItem.isFolderType() is True
|
||||
|
||||
theItem.setType("FILE")
|
||||
theItem.setLayout("DOCUMENT")
|
||||
assert theItem.isFileType() is True
|
||||
assert theItem.isDocumentLayout() is True
|
||||
assert theItem.describeMe() == "Novel Document"
|
||||
assert theItem.describeMe("H0") == "Novel Document"
|
||||
assert theItem.describeMe("H1") == "Novel Title Page"
|
||||
@@ -194,8 +221,34 @@ def testCoreItem_Methods(mockGUI):
|
||||
assert theItem.describeMe("H4") == "Novel Document"
|
||||
|
||||
theItem.setLayout("NOTE")
|
||||
assert theItem.isNoteLayout() is True
|
||||
assert theItem.describeMe() == "Project Note"
|
||||
|
||||
# Status + Icon
|
||||
# =============
|
||||
|
||||
theItem.setType("FILE")
|
||||
theItem.setStatus("Note")
|
||||
theItem.setImport("Minor")
|
||||
|
||||
theItem.setClass("NOVEL")
|
||||
stT, stI = theItem.getImportStatus()
|
||||
assert stT == "Note"
|
||||
assert isinstance(stI, QIcon)
|
||||
|
||||
theItem.setImportStatus("Draft")
|
||||
stT, stI = theItem.getImportStatus()
|
||||
assert stT == "Draft"
|
||||
|
||||
theItem.setClass("CHARACTER")
|
||||
stT, stI = theItem.getImportStatus()
|
||||
assert stT == "Minor"
|
||||
assert isinstance(stI, QIcon)
|
||||
|
||||
theItem.setImportStatus("Major")
|
||||
stT, stI = theItem.getImportStatus()
|
||||
assert stT == "Major"
|
||||
|
||||
# Representation
|
||||
# ==============
|
||||
|
||||
@@ -235,8 +288,8 @@ def testCoreItem_TypeSetter(mockGUI):
|
||||
assert theItem.itemType == nwItemType.FOLDER
|
||||
theItem.setType("FILE")
|
||||
assert theItem.itemType == nwItemType.FILE
|
||||
theItem.setType("TRASH")
|
||||
assert theItem.itemType == nwItemType.TRASH
|
||||
|
||||
# Alternative
|
||||
theItem.setType(nwItemType.ROOT)
|
||||
assert theItem.itemType == nwItemType.ROOT
|
||||
|
||||
@@ -256,28 +309,74 @@ def testCoreItem_ClassSetter(mockGUI):
|
||||
assert theItem.itemClass == nwItemClass.NO_CLASS
|
||||
theItem.setClass("NONSENSE")
|
||||
assert theItem.itemClass == nwItemClass.NO_CLASS
|
||||
|
||||
theItem.setClass("NO_CLASS")
|
||||
assert theItem.itemClass == nwItemClass.NO_CLASS
|
||||
assert theItem.isNovelLike() is False
|
||||
assert theItem.documentAllowed() is False
|
||||
assert theItem.isInactive() is True
|
||||
|
||||
theItem.setClass("NOVEL")
|
||||
assert theItem.itemClass == nwItemClass.NOVEL
|
||||
assert theItem.isNovelLike() is True
|
||||
assert theItem.documentAllowed() is True
|
||||
assert theItem.isInactive() is False
|
||||
|
||||
theItem.setClass("PLOT")
|
||||
assert theItem.itemClass == nwItemClass.PLOT
|
||||
assert theItem.isNovelLike() is False
|
||||
assert theItem.documentAllowed() is False
|
||||
assert theItem.isInactive() is False
|
||||
|
||||
theItem.setClass("CHARACTER")
|
||||
assert theItem.itemClass == nwItemClass.CHARACTER
|
||||
assert theItem.isNovelLike() is False
|
||||
assert theItem.documentAllowed() is False
|
||||
assert theItem.isInactive() is False
|
||||
|
||||
theItem.setClass("WORLD")
|
||||
assert theItem.itemClass == nwItemClass.WORLD
|
||||
assert theItem.isNovelLike() is False
|
||||
assert theItem.documentAllowed() is False
|
||||
assert theItem.isInactive() is False
|
||||
|
||||
theItem.setClass("TIMELINE")
|
||||
assert theItem.itemClass == nwItemClass.TIMELINE
|
||||
assert theItem.isNovelLike() is False
|
||||
assert theItem.documentAllowed() is False
|
||||
assert theItem.isInactive() is False
|
||||
|
||||
theItem.setClass("OBJECT")
|
||||
assert theItem.itemClass == nwItemClass.OBJECT
|
||||
assert theItem.isNovelLike() is False
|
||||
assert theItem.documentAllowed() is False
|
||||
assert theItem.isInactive() is False
|
||||
|
||||
theItem.setClass("ENTITY")
|
||||
assert theItem.itemClass == nwItemClass.ENTITY
|
||||
assert theItem.isNovelLike() is False
|
||||
assert theItem.documentAllowed() is False
|
||||
assert theItem.isInactive() is False
|
||||
|
||||
theItem.setClass("CUSTOM")
|
||||
assert theItem.itemClass == nwItemClass.CUSTOM
|
||||
assert theItem.isNovelLike() is False
|
||||
assert theItem.documentAllowed() is False
|
||||
assert theItem.isInactive() is False
|
||||
|
||||
theItem.setClass("ARCHIVE")
|
||||
assert theItem.itemClass == nwItemClass.ARCHIVE
|
||||
assert theItem.isNovelLike() is True
|
||||
assert theItem.documentAllowed() is True
|
||||
assert theItem.isInactive() is True
|
||||
|
||||
theItem.setClass("TRASH")
|
||||
assert theItem.itemClass == nwItemClass.TRASH
|
||||
assert theItem.isNovelLike() is False
|
||||
assert theItem.documentAllowed() is True
|
||||
assert theItem.isInactive() is True
|
||||
|
||||
# Alternative
|
||||
theItem.setClass(nwItemClass.NOVEL)
|
||||
assert theItem.itemClass == nwItemClass.NOVEL
|
||||
|
||||
@@ -306,23 +405,7 @@ def testCoreItem_LayoutSetter(mockGUI):
|
||||
theItem.setLayout("NOTE")
|
||||
assert theItem.itemLayout == nwItemLayout.NOTE
|
||||
|
||||
# Deprecated Layouts
|
||||
theItem.setLayout("TITLE")
|
||||
assert theItem.itemLayout == nwItemLayout.DOCUMENT
|
||||
theItem.setLayout("PAGE")
|
||||
assert theItem.itemLayout == nwItemLayout.DOCUMENT
|
||||
theItem.setLayout("BOOK")
|
||||
assert theItem.itemLayout == nwItemLayout.DOCUMENT
|
||||
theItem.setLayout("PARTITION")
|
||||
assert theItem.itemLayout == nwItemLayout.DOCUMENT
|
||||
theItem.setLayout("UNNUMBERED")
|
||||
assert theItem.itemLayout == nwItemLayout.DOCUMENT
|
||||
theItem.setLayout("CHAPTER")
|
||||
assert theItem.itemLayout == nwItemLayout.DOCUMENT
|
||||
theItem.setLayout("SCENE")
|
||||
assert theItem.itemLayout == nwItemLayout.DOCUMENT
|
||||
|
||||
# Alternatives
|
||||
# Alternative
|
||||
theItem.setLayout(nwItemLayout.NOTE)
|
||||
assert theItem.itemLayout == nwItemLayout.NOTE
|
||||
|
||||
@@ -330,23 +413,83 @@ def testCoreItem_LayoutSetter(mockGUI):
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreItem_XMLPackUnpack(mockGUI, caplog):
|
||||
def testCoreItem_ClassDefaults(mockGUI):
|
||||
"""Test the setter for the default values.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
theItem = NWItem(theProject)
|
||||
|
||||
# Root items should not have their class updated
|
||||
theItem.setParent(None)
|
||||
theItem.setClass(nwItemClass.NO_CLASS)
|
||||
assert theItem.itemClass == nwItemClass.NO_CLASS
|
||||
|
||||
theItem.setClassDefaults(nwItemClass.NOVEL)
|
||||
assert theItem.itemClass == nwItemClass.NO_CLASS
|
||||
|
||||
# Non-root items should have their class updated
|
||||
theItem.setParent("0123456789abc")
|
||||
theItem.setClass(nwItemClass.NO_CLASS)
|
||||
assert theItem.itemClass == nwItemClass.NO_CLASS
|
||||
|
||||
theItem.setClassDefaults(nwItemClass.NOVEL)
|
||||
assert theItem.itemClass == nwItemClass.NOVEL
|
||||
|
||||
# Non-layout items should have their layout set based on class
|
||||
theItem.setParent("0123456789abc")
|
||||
theItem.setClass(nwItemClass.NO_CLASS)
|
||||
theItem.setLayout(nwItemLayout.NO_LAYOUT)
|
||||
assert theItem.itemLayout == nwItemLayout.NO_LAYOUT
|
||||
|
||||
theItem.setClassDefaults(nwItemClass.NOVEL)
|
||||
assert theItem.itemLayout == nwItemLayout.DOCUMENT
|
||||
|
||||
theItem.setParent("0123456789abc")
|
||||
theItem.setClass(nwItemClass.NO_CLASS)
|
||||
theItem.setLayout(nwItemLayout.NO_LAYOUT)
|
||||
assert theItem.itemLayout == nwItemLayout.NO_LAYOUT
|
||||
|
||||
theItem.setClassDefaults(nwItemClass.PLOT)
|
||||
assert theItem.itemLayout == nwItemLayout.NOTE
|
||||
|
||||
# If documents are not allowed in that class, the layout should be changed
|
||||
theItem.setParent("0123456789abc")
|
||||
theItem.setClass(nwItemClass.NO_CLASS)
|
||||
theItem.setLayout(nwItemLayout.DOCUMENT)
|
||||
assert theItem.itemLayout == nwItemLayout.DOCUMENT
|
||||
|
||||
theItem.setClassDefaults(nwItemClass.PLOT)
|
||||
assert theItem.itemLayout == nwItemLayout.NOTE
|
||||
|
||||
# In all cases, status and importance should no longer be None
|
||||
assert theItem.itemStatus is not None
|
||||
assert theItem.itemImport is not None
|
||||
|
||||
# END Test testCoreItem_ClassDefaults
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreItem_XMLPackUnpack(mockGUI, caplog, mockRnd):
|
||||
"""Test packing and unpacking XML objects for the NWItem class.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
nwXML = etree.Element("novelWriterXML")
|
||||
|
||||
statusKeys = ["s000000", "s000001", "s000002", "s000003"]
|
||||
importKeys = ["i000004", "i000005", "i000006", "i000007"]
|
||||
|
||||
# File
|
||||
# ====
|
||||
|
||||
theItem = NWItem(theProject)
|
||||
theItem.setHandle("0123456789abc")
|
||||
theItem.setParent("0123456789abc")
|
||||
theItem.setRoot("0123456789abc")
|
||||
theItem.setOrder(1)
|
||||
theItem.setName("A Name")
|
||||
theItem.setClass("NOVEL")
|
||||
theItem.setType("FILE")
|
||||
theItem.setStatus("Main")
|
||||
theItem.setImport(importKeys[3])
|
||||
theItem.setLayout("NOTE")
|
||||
theItem.setExported(False)
|
||||
theItem.setParaCount(3)
|
||||
@@ -358,19 +501,20 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog):
|
||||
xContent = etree.SubElement(nwXML, "content")
|
||||
theItem.packXML(xContent)
|
||||
assert etree.tostring(xContent, pretty_print=False, encoding="utf-8") == (
|
||||
b"<content>"
|
||||
b"<item handle=\"0123456789abc\" order=\"1\" parent=\"0123456789abc\">"
|
||||
b"<name>A Name</name><type>FILE</type><class>NOVEL</class><status>New</status>"
|
||||
b"<exported>False</exported><layout>NOTE</layout><charCount>7</charCount>"
|
||||
b"<wordCount>5</wordCount><paraCount>3</paraCount><cursorPos>11</cursorPos></item>"
|
||||
b"</content>"
|
||||
)
|
||||
b'<content>'
|
||||
b'<item handle="0123456789abc" parent="0123456789abc" root="0123456789abc" order="1" '
|
||||
b'type="FILE" class="NOVEL" layout="NOTE"><meta expanded="False" charCount="7" '
|
||||
b'wordCount="5" paraCount="3" cursorPos="11"/><name status="None" import="%s" '
|
||||
b'exported="False">A Name</name></item>'
|
||||
b'</content>'
|
||||
) % bytes(importKeys[3], encoding="utf8")
|
||||
|
||||
# Unpack
|
||||
theItem = NWItem(theProject)
|
||||
assert theItem.unpackXML(xContent[0])
|
||||
assert theItem.itemHandle == "0123456789abc"
|
||||
assert theItem.itemParent == "0123456789abc"
|
||||
assert theItem.itemRoot == "0123456789abc"
|
||||
assert theItem.itemOrder == 1
|
||||
assert theItem.isExported is False
|
||||
assert theItem.paraCount == 3
|
||||
@@ -380,6 +524,8 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog):
|
||||
assert theItem.itemClass == nwItemClass.NOVEL
|
||||
assert theItem.itemType == nwItemType.FILE
|
||||
assert theItem.itemLayout == nwItemLayout.NOTE
|
||||
assert theItem.itemStatus == statusKeys[0] # Was None, should now be default
|
||||
assert theItem.itemImport == importKeys[3]
|
||||
|
||||
# Folder
|
||||
# ======
|
||||
@@ -387,11 +533,12 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog):
|
||||
theItem = NWItem(theProject)
|
||||
theItem.setHandle("0123456789abc")
|
||||
theItem.setParent("0123456789abc")
|
||||
theItem.setRoot("0123456789abc")
|
||||
theItem.setOrder(1)
|
||||
theItem.setName("A Name")
|
||||
theItem.setClass("NOVEL")
|
||||
theItem.setType("FOLDER")
|
||||
theItem.setStatus("Main")
|
||||
theItem.setStatus(statusKeys[1])
|
||||
theItem.setLayout("NOTE")
|
||||
theItem.setExpanded(True)
|
||||
theItem.setExported(False)
|
||||
@@ -404,18 +551,19 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog):
|
||||
xContent = etree.SubElement(nwXML, "content")
|
||||
theItem.packXML(xContent)
|
||||
assert etree.tostring(xContent, pretty_print=False, encoding="utf-8") == (
|
||||
b"<content>"
|
||||
b"<item handle=\"0123456789abc\" order=\"1\" parent=\"0123456789abc\">"
|
||||
b"<name>A Name</name><type>FOLDER</type><class>NOVEL</class><status>New</status>"
|
||||
b"<expanded>True</expanded></item>"
|
||||
b"</content>"
|
||||
)
|
||||
b'<content>'
|
||||
b'<item handle="0123456789abc" parent="0123456789abc" root="0123456789abc" order="1" '
|
||||
b'type="FOLDER" class="NOVEL"><meta expanded="True"/><name status="%s" '
|
||||
b'import="None">A Name</name></item>'
|
||||
b'</content>'
|
||||
) % bytes(statusKeys[1], encoding="utf8")
|
||||
|
||||
# Unpack
|
||||
theItem = NWItem(theProject)
|
||||
assert theItem.unpackXML(xContent[0])
|
||||
assert theItem.itemHandle == "0123456789abc"
|
||||
assert theItem.itemParent == "0123456789abc"
|
||||
assert theItem.itemRoot == "0123456789abc"
|
||||
assert theItem.itemOrder == 1
|
||||
assert theItem.isExpanded is True
|
||||
assert theItem.isExported is True
|
||||
@@ -426,6 +574,8 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog):
|
||||
assert theItem.itemClass == nwItemClass.NOVEL
|
||||
assert theItem.itemType == nwItemType.FOLDER
|
||||
assert theItem.itemLayout == nwItemLayout.NO_LAYOUT
|
||||
assert theItem.itemStatus == statusKeys[1]
|
||||
assert theItem.itemImport == importKeys[0] # Was None, should now be default
|
||||
|
||||
# Errors
|
||||
# ======
|
||||
@@ -462,3 +612,111 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog):
|
||||
)
|
||||
|
||||
# END Test testCoreItem_XMLPackUnpack
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreItem_ConvertFromFmt12(mockGUI):
|
||||
"""Test the setter for all the nwItemLayout values for the NWItem
|
||||
class using the class names that were present in file format 1.2.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
theItem = NWItem(theProject)
|
||||
|
||||
# Deprecated Layouts
|
||||
theItem.setLayout("TITLE")
|
||||
assert theItem.itemLayout == nwItemLayout.DOCUMENT
|
||||
theItem.setLayout("PAGE")
|
||||
assert theItem.itemLayout == nwItemLayout.DOCUMENT
|
||||
theItem.setLayout("BOOK")
|
||||
assert theItem.itemLayout == nwItemLayout.DOCUMENT
|
||||
theItem.setLayout("PARTITION")
|
||||
assert theItem.itemLayout == nwItemLayout.DOCUMENT
|
||||
theItem.setLayout("UNNUMBERED")
|
||||
assert theItem.itemLayout == nwItemLayout.DOCUMENT
|
||||
theItem.setLayout("CHAPTER")
|
||||
assert theItem.itemLayout == nwItemLayout.DOCUMENT
|
||||
theItem.setLayout("SCENE")
|
||||
assert theItem.itemLayout == nwItemLayout.DOCUMENT
|
||||
theItem.setLayout("MUMBOJUMBO")
|
||||
assert theItem.itemLayout == nwItemLayout.NO_LAYOUT
|
||||
|
||||
# END Test testCoreItem_ConvertFromFmt12
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreItem_ConvertFromFmt13(mockGUI):
|
||||
"""Test packing and unpacking XML objects for the NWItem class from
|
||||
format version 1.3
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
|
||||
# Make Version 1.3 XML
|
||||
nwXML = etree.Element("novelWriterXML")
|
||||
xContent = etree.SubElement(nwXML, "content")
|
||||
|
||||
# Folder
|
||||
xPack = etree.SubElement(xContent, "item", attrib={
|
||||
"handle": "a000000000001",
|
||||
"order": "1",
|
||||
"parent": "b000000000001",
|
||||
})
|
||||
NWItem._subPack(xPack, "name", text="Folder")
|
||||
NWItem._subPack(xPack, "type", text="FOLDER")
|
||||
NWItem._subPack(xPack, "class", text="NOVEL")
|
||||
NWItem._subPack(xPack, "status", text="New")
|
||||
NWItem._subPack(xPack, "expanded", text="True")
|
||||
|
||||
# Unpack Folder
|
||||
theItem = NWItem(theProject)
|
||||
theItem.unpackXML(xContent[0])
|
||||
assert theItem.itemHandle == "a000000000001"
|
||||
assert theItem.itemParent == "b000000000001"
|
||||
assert theItem.itemOrder == 1
|
||||
assert theItem.isExpanded is True
|
||||
assert theItem.isExported is True
|
||||
assert theItem.charCount == 0
|
||||
assert theItem.wordCount == 0
|
||||
assert theItem.paraCount == 0
|
||||
assert theItem.cursorPos == 0
|
||||
assert theItem.itemClass == nwItemClass.NOVEL
|
||||
assert theItem.itemType == nwItemType.FOLDER
|
||||
assert theItem.itemLayout == nwItemLayout.NO_LAYOUT
|
||||
|
||||
# File
|
||||
xPack = etree.SubElement(xContent, "item", attrib={
|
||||
"handle": "c000000000001",
|
||||
"order": "2",
|
||||
"parent": "a000000000001",
|
||||
})
|
||||
NWItem._subPack(xPack, "name", text="Scene")
|
||||
NWItem._subPack(xPack, "type", text="FILE")
|
||||
NWItem._subPack(xPack, "class", text="NOVEL")
|
||||
NWItem._subPack(xPack, "status", text="New")
|
||||
NWItem._subPack(xPack, "exported", text="True")
|
||||
NWItem._subPack(xPack, "layout", text="DOCUMENT")
|
||||
NWItem._subPack(xPack, "charCount", text="600")
|
||||
NWItem._subPack(xPack, "wordCount", text="100")
|
||||
NWItem._subPack(xPack, "paraCount", text="6")
|
||||
NWItem._subPack(xPack, "cursorPos", text="50")
|
||||
|
||||
# Unpack File
|
||||
theItem = NWItem(theProject)
|
||||
theItem.unpackXML(xContent[1])
|
||||
assert theItem.itemHandle == "c000000000001"
|
||||
assert theItem.itemParent == "a000000000001"
|
||||
assert theItem.itemOrder == 2
|
||||
assert theItem.isExpanded is False
|
||||
assert theItem.isExported is True
|
||||
assert theItem.charCount == 600
|
||||
assert theItem.wordCount == 100
|
||||
assert theItem.paraCount == 6
|
||||
assert theItem.cursorPos == 50
|
||||
assert theItem.itemClass == nwItemClass.NOVEL
|
||||
assert theItem.itemType == nwItemType.FILE
|
||||
assert theItem.itemLayout == nwItemLayout.DOCUMENT
|
||||
|
||||
# Deprecated Type
|
||||
theItem.setType("TRASH")
|
||||
assert theItem.itemType == nwItemType.ROOT
|
||||
|
||||
# END Test testCoreItem_ConvertFromFmt13
|
||||
|
||||
@@ -19,24 +19,28 @@ 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 pytest
|
||||
import os
|
||||
import pytest
|
||||
|
||||
from shutil import copyfile
|
||||
from zipfile import ZipFile
|
||||
from lxml import etree
|
||||
|
||||
from tools import cmpFiles, writeFile, readFile
|
||||
from tools import cmpFiles, writeFile, readFile, buildTestProject, XML_IGNORE
|
||||
from mock import causeOSError
|
||||
|
||||
from novelwriter.core.project import NWProject
|
||||
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
|
||||
from novelwriter.common import formatTimeStamp
|
||||
from novelwriter.constants import nwFiles
|
||||
from novelwriter.core.tree import NWTree
|
||||
from novelwriter.core.index import NWIndex
|
||||
from novelwriter.core.project import NWProject
|
||||
from novelwriter.core.options import OptionState
|
||||
from novelwriter.core.document import NWDoc
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreProject_NewMinimal(fncDir, outDir, refDir, mockGUI):
|
||||
def testCoreProject_NewMinimal(fncDir, outDir, refDir, mockGUI, mockRnd):
|
||||
"""Create a new project from a project wizard dictionary. With
|
||||
default setting, creating a Minimal project.
|
||||
"""
|
||||
@@ -45,7 +49,6 @@ def testCoreProject_NewMinimal(fncDir, outDir, refDir, mockGUI):
|
||||
compFile = os.path.join(refDir, "coreProject_NewMinimal_nwProject.nwx")
|
||||
|
||||
theProject = NWProject(mockGUI)
|
||||
theProject.projTree.setSeed(42)
|
||||
|
||||
# Setting no data should fail
|
||||
assert theProject.newProject({}) is False
|
||||
@@ -61,10 +64,6 @@ def testCoreProject_NewMinimal(fncDir, outDir, refDir, mockGUI):
|
||||
# Creating the project once more should fail
|
||||
assert theProject.newProject({"projPath": fncDir}) is False
|
||||
|
||||
# Check the new project
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, compFile, [2, 6, 7, 8])
|
||||
|
||||
# Open again
|
||||
assert theProject.openProject(projFile) is True
|
||||
|
||||
@@ -72,7 +71,7 @@ def testCoreProject_NewMinimal(fncDir, outDir, refDir, mockGUI):
|
||||
assert theProject.saveProject() is True
|
||||
assert theProject.closeProject() is True
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, compFile, [2, 6, 7, 8])
|
||||
assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
|
||||
assert theProject.projChanged is False
|
||||
|
||||
# Open a second time
|
||||
@@ -82,13 +81,13 @@ def testCoreProject_NewMinimal(fncDir, outDir, refDir, mockGUI):
|
||||
assert theProject.saveProject() is True
|
||||
assert theProject.closeProject() is True
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, compFile, [2, 6, 7, 8])
|
||||
assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
|
||||
|
||||
# END Test testCoreProject_NewMinimal
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreProject_NewCustomA(fncDir, outDir, refDir, mockGUI):
|
||||
def testCoreProject_NewCustomA(fncDir, outDir, refDir, mockGUI, mockRnd):
|
||||
"""Create a new project from a project wizard dictionary.
|
||||
Custom type with chapters and scenes.
|
||||
"""
|
||||
@@ -108,29 +107,25 @@ def testCoreProject_NewCustomA(fncDir, outDir, refDir, mockGUI):
|
||||
nwItemClass.PLOT,
|
||||
nwItemClass.CHARACTER,
|
||||
nwItemClass.WORLD,
|
||||
nwItemClass.TIMELINE,
|
||||
nwItemClass.OBJECT,
|
||||
nwItemClass.ENTITY,
|
||||
],
|
||||
"addNotes": True,
|
||||
"numChapters": 3,
|
||||
"numScenes": 3,
|
||||
"chFolders": True,
|
||||
}
|
||||
theProject = NWProject(mockGUI)
|
||||
theProject.projTree.setSeed(42)
|
||||
|
||||
assert theProject.newProject(projData) is True
|
||||
assert theProject.saveProject() is True
|
||||
assert theProject.closeProject() is True
|
||||
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, compFile, [2, 6, 7, 8])
|
||||
assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
|
||||
|
||||
# END Test testCoreProject_NewCustomA
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreProject_NewCustomB(fncDir, outDir, refDir, mockGUI):
|
||||
def testCoreProject_NewCustomB(fncDir, outDir, refDir, mockGUI, mockRnd):
|
||||
"""Create a new project from a project wizard dictionary.
|
||||
Custom type without chapters, but with scenes.
|
||||
"""
|
||||
@@ -150,23 +145,19 @@ def testCoreProject_NewCustomB(fncDir, outDir, refDir, mockGUI):
|
||||
nwItemClass.PLOT,
|
||||
nwItemClass.CHARACTER,
|
||||
nwItemClass.WORLD,
|
||||
nwItemClass.TIMELINE,
|
||||
nwItemClass.OBJECT,
|
||||
nwItemClass.ENTITY,
|
||||
],
|
||||
"addNotes": True,
|
||||
"numChapters": 0,
|
||||
"numScenes": 6,
|
||||
"chFolders": True,
|
||||
}
|
||||
theProject = NWProject(mockGUI)
|
||||
theProject.projTree.setSeed(42)
|
||||
|
||||
assert theProject.newProject(projData) is True
|
||||
assert theProject.saveProject() is True
|
||||
assert theProject.closeProject() is True
|
||||
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, compFile, [2, 6, 7, 8])
|
||||
assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
|
||||
|
||||
# END Test testCoreProject_NewCustomB
|
||||
|
||||
@@ -186,7 +177,6 @@ def testCoreProject_NewSampleA(fncDir, tmpConf, mockGUI, tmpDir):
|
||||
"popCustom": False,
|
||||
}
|
||||
theProject = NWProject(mockGUI)
|
||||
theProject.projTree.setSeed(42)
|
||||
|
||||
# Sample set, but no path
|
||||
assert not theProject.newProject({"popSample": True})
|
||||
@@ -235,7 +225,6 @@ def testCoreProject_NewSampleB(monkeypatch, fncDir, tmpConf, mockGUI, tmpDir):
|
||||
"popCustom": False,
|
||||
}
|
||||
theProject = NWProject(mockGUI)
|
||||
theProject.projTree.setSeed(42)
|
||||
|
||||
# Make sure we do not pick up the novelwriter/assets/sample.zip file
|
||||
tmpConf.assetPath = tmpDir
|
||||
@@ -259,7 +248,7 @@ def testCoreProject_NewSampleB(monkeypatch, fncDir, tmpConf, mockGUI, tmpDir):
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreProject_NewRoot(fncDir, outDir, refDir, mockGUI):
|
||||
def testCoreProject_NewRoot(fncDir, outDir, refDir, mockGUI, mockRnd):
|
||||
"""Check that new root folders can be added to the project.
|
||||
"""
|
||||
projFile = os.path.join(fncDir, "nwProject.nwx")
|
||||
@@ -267,62 +256,84 @@ def testCoreProject_NewRoot(fncDir, outDir, refDir, mockGUI):
|
||||
compFile = os.path.join(refDir, "coreProject_NewRoot_nwProject.nwx")
|
||||
|
||||
theProject = NWProject(mockGUI)
|
||||
theProject.projTree.setSeed(42)
|
||||
buildTestProject(theProject, fncDir)
|
||||
|
||||
assert theProject.newProject({"projPath": fncDir}) is True
|
||||
assert theProject.setProjectPath(fncDir) is True
|
||||
assert theProject.saveProject() is True
|
||||
assert theProject.closeProject() is True
|
||||
assert theProject.openProject(projFile) is True
|
||||
|
||||
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 isinstance(theProject.newRoot(nwItemClass.NOVEL), str)
|
||||
assert isinstance(theProject.newRoot(nwItemClass.PLOT), str)
|
||||
assert isinstance(theProject.newRoot(nwItemClass.CHARACTER), str)
|
||||
assert isinstance(theProject.newRoot(nwItemClass.WORLD), str)
|
||||
assert isinstance(theProject.newRoot(nwItemClass.TIMELINE), str)
|
||||
assert isinstance(theProject.newRoot(nwItemClass.OBJECT), str)
|
||||
assert isinstance(theProject.newRoot(nwItemClass.CUSTOM), str)
|
||||
assert isinstance(theProject.newRoot(nwItemClass.CUSTOM), str)
|
||||
|
||||
assert theProject.projChanged is True
|
||||
assert theProject.saveProject() is True
|
||||
assert theProject.closeProject() is True
|
||||
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, compFile, [2, 6, 7, 8])
|
||||
assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
|
||||
assert theProject.projChanged is False
|
||||
|
||||
# END Test testCoreProject_NewRoot
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreProject_NewFile(fncDir, outDir, refDir, mockGUI):
|
||||
def testCoreProject_NewFileFolder(fncDir, outDir, refDir, mockGUI, mockRnd):
|
||||
"""Check that new files can be added to the project.
|
||||
"""
|
||||
projFile = os.path.join(fncDir, "nwProject.nwx")
|
||||
testFile = os.path.join(outDir, "coreProject_NewFile_nwProject.nwx")
|
||||
compFile = os.path.join(refDir, "coreProject_NewFile_nwProject.nwx")
|
||||
testFile = os.path.join(outDir, "coreProject_NewFileFolder_nwProject.nwx")
|
||||
compFile = os.path.join(refDir, "coreProject_NewFileFolder_nwProject.nwx")
|
||||
|
||||
theProject = NWProject(mockGUI)
|
||||
theProject.projTree.setSeed(42)
|
||||
buildTestProject(theProject, fncDir)
|
||||
|
||||
assert theProject.newProject({"projPath": fncDir}) is True
|
||||
assert theProject.setProjectPath(fncDir) is True
|
||||
assert theProject.saveProject() is True
|
||||
assert theProject.closeProject() is True
|
||||
assert theProject.openProject(projFile) is True
|
||||
|
||||
assert isinstance(theProject.newFile("Hello", nwItemClass.NOVEL, "31489056e0916"), str)
|
||||
assert isinstance(theProject.newFile("Jane", nwItemClass.CHARACTER, "71ee45a3c0db9"), str)
|
||||
assert theProject.projChanged
|
||||
# Invalid call
|
||||
assert theProject.newFolder("New Folder", "1234567890abc") is None
|
||||
assert theProject.newFile("New File", "1234567890abc") is None
|
||||
|
||||
# Add files properly
|
||||
assert theProject.newFolder("Stuff", "0000000000015") == "0000000000028"
|
||||
assert theProject.newFile("Hello", "0000000000015") == "0000000000029"
|
||||
assert theProject.newFile("Jane", "0000000000012") == "000000000002a"
|
||||
|
||||
assert "0000000000028" in theProject.tree
|
||||
assert "0000000000029" in theProject.tree
|
||||
assert "000000000002a" in theProject.tree
|
||||
|
||||
# Write to file, failed
|
||||
assert theProject.writeNewFile("blabla", 1, True) is False # Not a handle
|
||||
assert theProject.writeNewFile("0000000000028", 1, True) is False # Not a file
|
||||
assert theProject.writeNewFile("0000000000014", 1, True) is False # Already has content
|
||||
|
||||
# Write to file, success
|
||||
assert theProject.writeNewFile("0000000000029", 2, True) is True
|
||||
assert NWDoc(theProject, "0000000000029").readDocument() == "## Hello\n\n"
|
||||
|
||||
assert theProject.writeNewFile("000000000002a", 1, False) is True
|
||||
assert NWDoc(theProject, "000000000002a").readDocument() == "# Jane\n\n"
|
||||
|
||||
# Save, close and check
|
||||
assert theProject.projChanged is True
|
||||
assert theProject.saveProject() is True
|
||||
assert theProject.closeProject() is True
|
||||
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, compFile, [2, 6, 7, 8])
|
||||
assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
|
||||
assert theProject.projChanged is False
|
||||
|
||||
# END Test testCoreProject_NewFile
|
||||
# END Test testCoreProject_NewFileFolder
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
@@ -452,12 +463,15 @@ def testCoreProject_Open(monkeypatch, nwMinimal, mockGUI):
|
||||
os.rename(oName, rName)
|
||||
|
||||
# Add some legacy stuff that cannot be removed
|
||||
writeFile(os.path.join(nwMinimal, "junk"), "stuff")
|
||||
os.mkdir(os.path.join(nwMinimal, "data_0"))
|
||||
writeFile(os.path.join(nwMinimal, "data_0", "junk"), "stuff")
|
||||
mockGUI.clear()
|
||||
assert theProject.openProject(nwMinimal) is True
|
||||
assert "data_0" in mockGUI.lastAlert
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(theProject, "_legacyDataFolder", causeOSError)
|
||||
os.mkdir(os.path.join(nwMinimal, "data_0"))
|
||||
writeFile(os.path.join(nwMinimal, "data_0", "123456789abc_main.nwd"), "stuff")
|
||||
writeFile(os.path.join(nwMinimal, "data_0", "123456789abc_main.bak"), "stuff")
|
||||
mockGUI.clear()
|
||||
assert theProject.openProject(nwMinimal) is True
|
||||
assert "There was an error updating the project." in mockGUI.lastAlert
|
||||
|
||||
assert theProject.closeProject()
|
||||
|
||||
# END Test testCoreProject_Open
|
||||
@@ -500,7 +514,7 @@ def testCoreProject_Save(monkeypatch, nwMinimal, mockGUI, refDir):
|
||||
assert theProject.saveProject() is True
|
||||
assert theProject.saveCount == saveCount + 1
|
||||
assert theProject.autoCount == autoCount
|
||||
assert cmpFiles(testFile, compFile, [2, 6, 7, 8, 9])
|
||||
assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
|
||||
|
||||
# Check that a second save creates a .bak file
|
||||
assert os.path.isfile(backFile) is True
|
||||
@@ -511,7 +525,7 @@ def testCoreProject_Save(monkeypatch, nwMinimal, mockGUI, refDir):
|
||||
assert theProject.saveProject(autoSave=True) is True
|
||||
assert theProject.saveCount == saveCount
|
||||
assert theProject.autoCount == autoCount + 1
|
||||
assert cmpFiles(testFile, compFile, [2, 6, 7, 8, 9])
|
||||
assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
|
||||
|
||||
# Close test project
|
||||
assert theProject.closeProject()
|
||||
@@ -630,6 +644,11 @@ def testCoreProject_AccessItems(nwMinimal, mockGUI):
|
||||
theProject = NWProject(mockGUI)
|
||||
theProject.openProject(nwMinimal)
|
||||
|
||||
# Storage Objects
|
||||
assert isinstance(theProject.index, NWIndex)
|
||||
assert isinstance(theProject.tree, NWTree)
|
||||
assert isinstance(theProject.options, OptionState)
|
||||
|
||||
# Move Novel ROOT to after its files
|
||||
oldOrder = [
|
||||
"a508bb932959c", # ROOT: Novel
|
||||
@@ -651,17 +670,17 @@ def testCoreProject_AccessItems(nwMinimal, mockGUI):
|
||||
"afb3043c7b2b3", # ROOT: Characters
|
||||
"9d5247ab588e0", # ROOT: World
|
||||
]
|
||||
assert theProject.projTree.handles() == oldOrder
|
||||
assert theProject.tree.handles() == oldOrder
|
||||
assert theProject.setTreeOrder(newOrder)
|
||||
assert theProject.projTree.handles() == newOrder
|
||||
assert theProject.tree.handles() == newOrder
|
||||
|
||||
# Add a non-existing item
|
||||
theProject.projTree._treeOrder.append("01234567789abc")
|
||||
theProject.tree._treeOrder.append("01234567789abc")
|
||||
|
||||
# Add an item with a non-existent parent
|
||||
nHandle = theProject.newFile("Test File", nwItemClass.NOVEL, "a6d311a93600a")
|
||||
theProject.projTree[nHandle].setParent("cba9876543210")
|
||||
assert theProject.projTree[nHandle].itemParent == "cba9876543210"
|
||||
nHandle = theProject.newFile("Test File", "a6d311a93600a")
|
||||
theProject.tree[nHandle].setParent("cba9876543210")
|
||||
assert theProject.tree[nHandle].itemParent == "cba9876543210"
|
||||
|
||||
retOrder = []
|
||||
for tItem in theProject.getProjectItems():
|
||||
@@ -678,19 +697,138 @@ def testCoreProject_AccessItems(nwMinimal, mockGUI):
|
||||
"f5ab3e30151e1", # FILE: New Chapter
|
||||
"8c659a11cd429", # FILE: New Scene
|
||||
]
|
||||
assert theProject.projTree[nHandle].itemParent is None
|
||||
assert theProject.tree[nHandle].itemParent is None
|
||||
|
||||
# END Test testCoreProject_AccessItems
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreProject_Methods(monkeypatch, nwMinimal, mockGUI, tmpDir):
|
||||
def testCoreProject_StatusImport(mockGUI, fncDir, mockRnd):
|
||||
"""Test the status and importance flag handling.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
buildTestProject(theProject, fncDir)
|
||||
|
||||
statusKeys = ["s000008", "s000009", "s00000a", "s00000b"]
|
||||
importKeys = ["i00000c", "i00000d", "i00000e", "i00000f"]
|
||||
|
||||
# Change Status
|
||||
# =============
|
||||
|
||||
theProject.tree["0000000000014"].setStatus("Finished")
|
||||
theProject.tree["0000000000015"].setStatus("Draft")
|
||||
theProject.tree["0000000000016"].setStatus("Note")
|
||||
theProject.tree["0000000000017"].setStatus("Finished")
|
||||
|
||||
assert theProject.tree["0000000000014"].itemStatus == statusKeys[3]
|
||||
assert theProject.tree["0000000000015"].itemStatus == statusKeys[2]
|
||||
assert theProject.tree["0000000000016"].itemStatus == statusKeys[1]
|
||||
assert theProject.tree["0000000000017"].itemStatus == statusKeys[3]
|
||||
|
||||
newList = [
|
||||
{"key": statusKeys[0], "name": "New", "cols": (1, 1, 1)},
|
||||
{"key": statusKeys[1], "name": "Draft", "cols": (2, 2, 2)}, # These are swapped
|
||||
{"key": statusKeys[2], "name": "Note", "cols": (3, 3, 3)}, # These are swapped
|
||||
{"key": statusKeys[3], "name": "Edited", "cols": (4, 4, 4)}, # Renamed
|
||||
{"key": None, "name": "Finished", "cols": (5, 5, 5)}, # New, reused name
|
||||
]
|
||||
assert theProject.setStatusColours(None, None) is False
|
||||
assert theProject.setStatusColours([], []) is False
|
||||
assert theProject.setStatusColours(newList, []) is True
|
||||
|
||||
assert theProject.statusItems.name(statusKeys[0]) == "New"
|
||||
assert theProject.statusItems.name(statusKeys[1]) == "Draft"
|
||||
assert theProject.statusItems.name(statusKeys[2]) == "Note"
|
||||
assert theProject.statusItems.name(statusKeys[3]) == "Edited"
|
||||
assert theProject.statusItems.cols(statusKeys[0]) == (1, 1, 1)
|
||||
assert theProject.statusItems.cols(statusKeys[1]) == (2, 2, 2)
|
||||
assert theProject.statusItems.cols(statusKeys[2]) == (3, 3, 3)
|
||||
assert theProject.statusItems.cols(statusKeys[3]) == (4, 4, 4)
|
||||
|
||||
# Check the new entry
|
||||
lastKey = theProject.statusItems.check("Finished")
|
||||
assert lastKey == "s000018"
|
||||
assert theProject.statusItems.name(lastKey) == "Finished"
|
||||
assert theProject.statusItems.cols(lastKey) == (5, 5, 5)
|
||||
|
||||
# Delete last entry
|
||||
assert theProject.setStatusColours([], [lastKey]) is True
|
||||
assert theProject.statusItems.name(lastKey) == "New"
|
||||
|
||||
# Change Importance
|
||||
# =================
|
||||
|
||||
fHandle = theProject.newFile("Jane Doe", "0000000000012")
|
||||
theProject.tree[fHandle].setImport("Main")
|
||||
|
||||
assert theProject.tree[fHandle].itemImport == importKeys[3]
|
||||
newList = [
|
||||
{"key": importKeys[0], "name": "New", "cols": (1, 1, 1)},
|
||||
{"key": importKeys[1], "name": "Minor", "cols": (2, 2, 2)},
|
||||
{"key": importKeys[2], "name": "Major", "cols": (3, 3, 3)},
|
||||
{"key": importKeys[3], "name": "Min", "cols": (4, 4, 4)},
|
||||
{"key": None, "name": "Max", "cols": (5, 5, 5)},
|
||||
]
|
||||
assert theProject.setImportColours(None, None) is False
|
||||
assert theProject.setImportColours([], []) is False
|
||||
assert theProject.setImportColours(newList, []) is True
|
||||
|
||||
assert theProject.importItems.name(importKeys[0]) == "New"
|
||||
assert theProject.importItems.name(importKeys[1]) == "Minor"
|
||||
assert theProject.importItems.name(importKeys[2]) == "Major"
|
||||
assert theProject.importItems.name(importKeys[3]) == "Min"
|
||||
assert theProject.importItems.cols(importKeys[0]) == (1, 1, 1)
|
||||
assert theProject.importItems.cols(importKeys[1]) == (2, 2, 2)
|
||||
assert theProject.importItems.cols(importKeys[2]) == (3, 3, 3)
|
||||
assert theProject.importItems.cols(importKeys[3]) == (4, 4, 4)
|
||||
|
||||
# Check the new entry
|
||||
lastKey = theProject.importItems.check("Max")
|
||||
assert lastKey == "i00001a"
|
||||
assert theProject.importItems.name(lastKey) == "Max"
|
||||
assert theProject.importItems.cols(lastKey) == (5, 5, 5)
|
||||
|
||||
# Delete last entry
|
||||
assert theProject.setImportColours([], [lastKey]) is True
|
||||
assert theProject.importItems.name(lastKey) == "New"
|
||||
|
||||
# Delete Status/Import
|
||||
# ====================
|
||||
|
||||
theProject.statusItems.resetCounts()
|
||||
for key in list(theProject.statusItems.keys()):
|
||||
assert theProject.statusItems.remove(key) is True
|
||||
|
||||
theProject.importItems.resetCounts()
|
||||
for key in list(theProject.importItems.keys()):
|
||||
assert theProject.importItems.remove(key) is True
|
||||
|
||||
assert len(theProject.statusItems) == 0
|
||||
assert len(theProject.importItems) == 0
|
||||
assert theProject.saveProject() is True
|
||||
assert theProject.closeProject() is True
|
||||
|
||||
# This should restore the default status/import labels
|
||||
assert theProject.openProject(fncDir) is True
|
||||
assert theProject.saveProject() is True
|
||||
assert theProject.statusItems.name("s000023") == "New"
|
||||
assert theProject.statusItems.name("s000024") == "Note"
|
||||
assert theProject.statusItems.name("s000025") == "Draft"
|
||||
assert theProject.statusItems.name("s000026") == "Finished"
|
||||
assert theProject.importItems.name("i000027") == "New"
|
||||
assert theProject.importItems.name("i000028") == "Minor"
|
||||
assert theProject.importItems.name("i000029") == "Major"
|
||||
assert theProject.importItems.name("i00002a") == "Main"
|
||||
|
||||
# END Test testCoreProject_StatusImport
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir, mockRnd):
|
||||
"""Test other project class methods and functions.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
theProject.projTree.setSeed(42)
|
||||
assert theProject.openProject(nwMinimal)
|
||||
assert theProject.projPath == nwMinimal
|
||||
buildTestProject(theProject, fncDir)
|
||||
|
||||
# Setting project path
|
||||
assert theProject.setProjectPath(None)
|
||||
@@ -701,16 +839,16 @@ def testCoreProject_Methods(monkeypatch, nwMinimal, mockGUI, tmpDir):
|
||||
assert theProject.projPath == os.path.expanduser("~")
|
||||
|
||||
# Create a new folder and populate it
|
||||
projPath = os.path.join(nwMinimal, "mock1")
|
||||
projPath = os.path.join(fncDir, "mock1")
|
||||
assert theProject.setProjectPath(projPath, newProject=True)
|
||||
|
||||
# Make os.mkdir fail
|
||||
monkeypatch.setattr("os.mkdir", causeOSError)
|
||||
projPath = os.path.join(nwMinimal, "mock2")
|
||||
projPath = os.path.join(fncDir, "mock2")
|
||||
assert not theProject.setProjectPath(projPath, newProject=True)
|
||||
|
||||
# Set back
|
||||
assert theProject.setProjectPath(nwMinimal)
|
||||
assert theProject.setProjectPath(fncDir)
|
||||
|
||||
# Project Name
|
||||
assert theProject.setProjectName(" A Name ")
|
||||
@@ -748,9 +886,10 @@ def testCoreProject_Methods(monkeypatch, nwMinimal, mockGUI, tmpDir):
|
||||
|
||||
# Trash folder
|
||||
# Should create on first call, and just returned on later calls
|
||||
assert theProject.projTree["73475cb40a568"] is None
|
||||
assert theProject.trashFolder() == "73475cb40a568"
|
||||
assert theProject.trashFolder() == "73475cb40a568"
|
||||
hTrash = "0000000000018"
|
||||
assert theProject.tree[hTrash] is None
|
||||
assert theProject.trashFolder() == hTrash
|
||||
assert theProject.trashFolder() == hTrash
|
||||
|
||||
# Project backup
|
||||
assert theProject.doBackup is True
|
||||
@@ -774,9 +913,10 @@ def testCoreProject_Methods(monkeypatch, nwMinimal, mockGUI, tmpDir):
|
||||
|
||||
# Spell language
|
||||
theProject.projChanged = False
|
||||
assert theProject.setSpellLang(None)
|
||||
assert theProject.projSpell is None
|
||||
assert theProject.setSpellLang("None")
|
||||
assert theProject.setSpellLang(None) is False
|
||||
assert theProject.projSpell is None
|
||||
assert theProject.setSpellLang("None") is False # Should be interpreded as None
|
||||
assert theProject.projSpell is None
|
||||
assert theProject.setSpellLang("en_GB")
|
||||
assert theProject.projSpell == "en_GB"
|
||||
@@ -790,11 +930,9 @@ def testCoreProject_Methods(monkeypatch, nwMinimal, mockGUI, tmpDir):
|
||||
assert theProject.setProjectLang("en_GB") is True
|
||||
assert theProject.projLang == "en_GB"
|
||||
|
||||
# Automatic outline update
|
||||
theProject.projChanged = False
|
||||
assert theProject.setAutoOutline(True)
|
||||
assert not theProject.setAutoOutline(False)
|
||||
assert theProject.projChanged
|
||||
# Language Lookup
|
||||
assert theProject.localLookup(1) == "One"
|
||||
assert theProject.localLookup(10) == "Ten"
|
||||
|
||||
# Last edited
|
||||
theProject.projChanged = False
|
||||
@@ -816,70 +954,20 @@ def testCoreProject_Methods(monkeypatch, nwMinimal, mockGUI, tmpDir):
|
||||
|
||||
# Change project tree order
|
||||
oldOrder = [
|
||||
"a508bb932959c", "a35baf2e93843", "a6d311a93600a",
|
||||
"f5ab3e30151e1", "8c659a11cd429", "7695ce551d265",
|
||||
"afb3043c7b2b3", "9d5247ab588e0", "73475cb40a568",
|
||||
"0000000000010", "0000000000011", "0000000000012",
|
||||
"0000000000013", "0000000000014", "0000000000015",
|
||||
"0000000000016", "0000000000017", "0000000000018",
|
||||
]
|
||||
newOrder = [
|
||||
"f5ab3e30151e1", "8c659a11cd429", "7695ce551d265",
|
||||
"a508bb932959c", "a35baf2e93843", "a6d311a93600a",
|
||||
"afb3043c7b2b3", "9d5247ab588e0",
|
||||
"0000000000013", "0000000000014", "0000000000015",
|
||||
"0000000000010", "0000000000011", "0000000000012",
|
||||
"0000000000016", "0000000000017",
|
||||
]
|
||||
assert theProject.projTree.handles() == oldOrder
|
||||
assert theProject.tree.handles() == oldOrder
|
||||
assert theProject.setTreeOrder(newOrder)
|
||||
assert theProject.projTree.handles() == newOrder
|
||||
assert theProject.tree.handles() == newOrder
|
||||
assert theProject.setTreeOrder(oldOrder)
|
||||
assert theProject.projTree.handles() == oldOrder
|
||||
|
||||
# Change status
|
||||
theProject.projTree["a35baf2e93843"].setStatus("Finished")
|
||||
theProject.projTree["a6d311a93600a"].setStatus("Draft")
|
||||
theProject.projTree["f5ab3e30151e1"].setStatus("Note")
|
||||
theProject.projTree["8c659a11cd429"].setStatus("Finished")
|
||||
newList = [
|
||||
("New", 1, 1, 1, "New"),
|
||||
("Draft", 2, 2, 2, "Note"), # These are swapped
|
||||
("Note", 3, 3, 3, "Draft"), # These are swapped
|
||||
("Edited", 4, 4, 4, "Finished"), # Renamed
|
||||
("Finished", 5, 5, 5, None), # New, with reused name
|
||||
]
|
||||
assert theProject.setStatusColours(newList)
|
||||
assert theProject.statusItems._theLabels == [
|
||||
"New", "Draft", "Note", "Edited", "Finished"
|
||||
]
|
||||
assert theProject.statusItems._theColours == [
|
||||
(1, 1, 1), (2, 2, 2), (3, 3, 3), (4, 4, 4), (5, 5, 5)
|
||||
]
|
||||
assert theProject.projTree["a35baf2e93843"].itemStatus == "Edited" # Renamed
|
||||
assert theProject.projTree["a6d311a93600a"].itemStatus == "Note" # Swapped
|
||||
assert theProject.projTree["f5ab3e30151e1"].itemStatus == "Draft" # Swapped
|
||||
assert theProject.projTree["8c659a11cd429"].itemStatus == "Edited" # Renamed
|
||||
|
||||
# Change importance
|
||||
fHandle = theProject.newFile("Jane Doe", nwItemClass.CHARACTER, "afb3043c7b2b3")
|
||||
theProject.projTree[fHandle].setStatus("Main")
|
||||
newList = [
|
||||
("New", 1, 1, 1, "New"),
|
||||
("Minor", 2, 2, 2, "Minor"),
|
||||
("Major", 3, 3, 3, "Major"),
|
||||
("Min", 4, 4, 4, "Main"),
|
||||
("Max", 5, 5, 5, None),
|
||||
]
|
||||
assert theProject.setImportColours(newList)
|
||||
assert theProject.importItems._theLabels == [
|
||||
"New", "Minor", "Major", "Min", "Max"
|
||||
]
|
||||
assert theProject.importItems._theColours == [
|
||||
(1, 1, 1), (2, 2, 2), (3, 3, 3), (4, 4, 4), (5, 5, 5)
|
||||
]
|
||||
assert theProject.projTree[fHandle].itemStatus == "Min"
|
||||
|
||||
# Check status counts
|
||||
assert theProject.statusItems._theCounts == [0, 0, 0, 0, 0]
|
||||
assert theProject.importItems._theCounts == [0, 0, 0, 0, 0]
|
||||
theProject.countStatus()
|
||||
assert theProject.statusItems._theCounts == [1, 1, 1, 2, 0]
|
||||
assert theProject.importItems._theCounts == [3, 0, 0, 1, 0]
|
||||
assert theProject.tree.handles() == oldOrder
|
||||
|
||||
# Session stats
|
||||
theProject.currWCount = 200
|
||||
@@ -894,7 +982,7 @@ def testCoreProject_Methods(monkeypatch, nwMinimal, mockGUI, tmpDir):
|
||||
assert not theProject._appendSessionStats(idleTime=0)
|
||||
|
||||
# Write entry
|
||||
assert theProject.projMeta == os.path.join(nwMinimal, "meta")
|
||||
assert theProject.projMeta == os.path.join(fncDir, "meta")
|
||||
statsFile = os.path.join(theProject.projMeta, nwFiles.SESS_STATS)
|
||||
|
||||
theProject.projOpened = 1600002000
|
||||
@@ -948,9 +1036,17 @@ def testCoreProject_OrphanedFiles(mockGUI, nwLipsum):
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
|
||||
assert theProject.openProject(nwLipsum)
|
||||
assert theProject.projTree["636b6aa9b697b"] is None
|
||||
assert theProject.closeProject()
|
||||
assert theProject.openProject(nwLipsum) is True
|
||||
assert theProject.tree["636b6aa9b697b"] is None
|
||||
|
||||
# Add a file with non-existent parent
|
||||
# This file will be renoved from the project on open
|
||||
oHandle = theProject.newFile("Oops", "b3643d0f92e32")
|
||||
theProject.tree[oHandle].setParent("1234567890abc")
|
||||
|
||||
# Save and close
|
||||
assert theProject.saveProject() is True
|
||||
assert theProject.closeProject() is True
|
||||
|
||||
# First Item with Meta Data
|
||||
orphPath = os.path.join(nwLipsum, "content", "636b6aa9b697b.nwd")
|
||||
@@ -980,11 +1076,11 @@ def testCoreProject_OrphanedFiles(mockGUI, nwLipsum):
|
||||
|
||||
assert theProject.openProject(nwLipsum)
|
||||
assert theProject.projPath is not None
|
||||
assert theProject.projTree["636b6aa9b697bb"] is None
|
||||
assert theProject.projTree["abcdefghijklm"] is None
|
||||
assert theProject.tree["636b6aa9b697bb"] is None
|
||||
assert theProject.tree["abcdefghijklm"] is None
|
||||
|
||||
# First Item with Meta Data
|
||||
oItem = theProject.projTree["636b6aa9b697b"]
|
||||
oItem = theProject.tree["636b6aa9b697b"]
|
||||
assert oItem is not None
|
||||
assert oItem.itemName == "[Recovered] Mars"
|
||||
assert oItem.itemHandle == "636b6aa9b697b"
|
||||
@@ -994,7 +1090,7 @@ def testCoreProject_OrphanedFiles(mockGUI, nwLipsum):
|
||||
assert oItem.itemLayout == nwItemLayout.NOTE
|
||||
|
||||
# Second Item without Meta Data
|
||||
oItem = theProject.projTree["736b6aa9b697b"]
|
||||
oItem = theProject.tree["736b6aa9b697b"]
|
||||
assert oItem is not None
|
||||
assert oItem.itemName == "Recovered File 1"
|
||||
assert oItem.itemHandle == "736b6aa9b697b"
|
||||
@@ -1042,14 +1138,6 @@ def testCoreProject_OldFormat(mockGUI, nwOldProj):
|
||||
os.path.join(nwOldProj, "meta", "sessionLogOptions.json"),
|
||||
]
|
||||
|
||||
# Add some files that shouldn't be there
|
||||
deleteFiles.append(os.path.join(nwOldProj, "data_f", "whatnow.nwd"))
|
||||
deleteFiles.append(os.path.join(nwOldProj, "data_f", "whatnow.txt"))
|
||||
|
||||
# Add some folders that shouldn't be there
|
||||
os.mkdir(os.path.join(nwOldProj, "stuff"))
|
||||
os.mkdir(os.path.join(nwOldProj, "data_1", "stuff"))
|
||||
|
||||
# Create mock files
|
||||
os.mkdir(os.path.join(nwOldProj, "cache"))
|
||||
for aFile in deleteFiles:
|
||||
@@ -1063,7 +1151,6 @@ def testCoreProject_OldFormat(mockGUI, nwOldProj):
|
||||
for aFile in deleteFiles:
|
||||
assert not os.path.isfile(aFile)
|
||||
|
||||
assert not os.path.isdir(os.path.join(nwOldProj, "data_1", "stuff"))
|
||||
assert not os.path.isdir(os.path.join(nwOldProj, "data_1"))
|
||||
assert not os.path.isdir(os.path.join(nwOldProj, "data_7"))
|
||||
assert not os.path.isdir(os.path.join(nwOldProj, "data_8"))
|
||||
@@ -1071,12 +1158,6 @@ def testCoreProject_OldFormat(mockGUI, nwOldProj):
|
||||
assert not os.path.isdir(os.path.join(nwOldProj, "data_a"))
|
||||
assert not os.path.isdir(os.path.join(nwOldProj, "data_f"))
|
||||
|
||||
# Check stuff that has been moved
|
||||
assert os.path.isdir(os.path.join(nwOldProj, "junk"))
|
||||
assert os.path.isdir(os.path.join(nwOldProj, "junk", "stuff"))
|
||||
assert os.path.isfile(os.path.join(nwOldProj, "junk", "whatnow.nwd"))
|
||||
assert os.path.isfile(os.path.join(nwOldProj, "junk", "whatnow.txt"))
|
||||
|
||||
# Check that files we want to keep are in the right place
|
||||
assert os.path.isdir(os.path.join(nwOldProj, "cache"))
|
||||
assert os.path.isdir(os.path.join(nwOldProj, "content"))
|
||||
@@ -1111,10 +1192,6 @@ def testCoreProject_LegacyData(monkeypatch, mockGUI, fncDir):
|
||||
theProject = NWProject(mockGUI)
|
||||
theProject.setProjectPath(fncDir)
|
||||
|
||||
# assert theProject.newProject({"projPath": fncDir})
|
||||
# assert theProject.saveProject()
|
||||
# assert theProject.closeProject()
|
||||
|
||||
# Check behaviour of deprecated files function on OSError
|
||||
tstFile = os.path.join(fncDir, "ToC.json")
|
||||
writeFile(tstFile, "stuff")
|
||||
@@ -1122,7 +1199,7 @@ def testCoreProject_LegacyData(monkeypatch, mockGUI, fncDir):
|
||||
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr("os.unlink", causeOSError)
|
||||
assert not theProject._deprecatedFiles()
|
||||
assert theProject._deprecatedFiles() is False
|
||||
|
||||
assert theProject._deprecatedFiles()
|
||||
assert not os.path.isfile(tstFile)
|
||||
@@ -1131,63 +1208,36 @@ def testCoreProject_LegacyData(monkeypatch, mockGUI, fncDir):
|
||||
tstFile = os.path.join(fncDir, "data_0")
|
||||
writeFile(tstFile, "stuff")
|
||||
assert os.path.isfile(tstFile)
|
||||
|
||||
errList = []
|
||||
errList = theProject._legacyDataFolder(tstFile, errList)
|
||||
assert len(errList) > 0
|
||||
|
||||
# Move folder in data folder, shouldn't be there
|
||||
tstData = os.path.join(fncDir, "data_1")
|
||||
errItem = os.path.join(fncDir, "data_1", "stuff")
|
||||
os.mkdir(tstData)
|
||||
os.mkdir(errItem)
|
||||
assert os.path.isdir(tstData)
|
||||
assert os.path.isdir(errItem)
|
||||
|
||||
# This causes a failure to create the 'junk' folder
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr("os.mkdir", causeOSError)
|
||||
errList = []
|
||||
errList = theProject._legacyDataFolder(tstData, errList)
|
||||
assert len(errList) > 0
|
||||
|
||||
# This causes a failure to move 'stuff' to 'junk'
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr("os.rename", causeOSError)
|
||||
errList = []
|
||||
errList = theProject._legacyDataFolder(tstData, errList)
|
||||
assert len(errList) > 0
|
||||
|
||||
# This should be successful
|
||||
errList = []
|
||||
errList = theProject._legacyDataFolder(tstData, errList)
|
||||
assert len(errList) == 0
|
||||
assert os.path.isdir(os.path.join(fncDir, "junk", "stuff"))
|
||||
assert theProject._legacyDataFolder(tstFile) is False
|
||||
|
||||
# Check renaming/deleting of old document files
|
||||
tstData = os.path.join(fncDir, "data_2")
|
||||
tstDoc1m = os.path.join(tstData, "000000000001_main.nwd")
|
||||
tstDoc1b = os.path.join(tstData, "000000000001_main.bak")
|
||||
tstDoc2m = os.path.join(tstData, "000000000002_main.nwd")
|
||||
tstDoc2b = os.path.join(tstData, "000000000002_main.bak")
|
||||
tstDoc3m = os.path.join(tstData, "tooshort003_main.nwd")
|
||||
tstDoc3b = os.path.join(tstData, "tooshort003_main.bak")
|
||||
tstData2 = os.path.join(fncDir, "data_2")
|
||||
tstData3 = os.path.join(fncDir, "data_3")
|
||||
tstDoc1m = os.path.join(tstData2, "000000000001_main.nwd")
|
||||
tstDoc1b = os.path.join(tstData2, "000000000001_main.bak")
|
||||
tstDoc2m = os.path.join(tstData2, "000000000002_main.nwd")
|
||||
tstDoc2b = os.path.join(tstData2, "000000000002_main.bak")
|
||||
tstDoc3m = os.path.join(tstData3, "tooshort003_main.nwd")
|
||||
tstDoc3b = os.path.join(tstData3, "tooshort003_main.bak")
|
||||
tstDir4a = os.path.join(tstData3, "stuff")
|
||||
|
||||
os.mkdir(tstData)
|
||||
os.mkdir(tstData2)
|
||||
os.mkdir(tstData3)
|
||||
writeFile(tstDoc1m, "stuff")
|
||||
writeFile(tstDoc1b, "stuff")
|
||||
writeFile(tstDoc2m, "stuff")
|
||||
writeFile(tstDoc2b, "stuff")
|
||||
writeFile(tstDoc3m, "stuff")
|
||||
writeFile(tstDoc3b, "stuff")
|
||||
os.mkdir(tstDir4a)
|
||||
|
||||
# Make the above fail
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr("os.rename", causeOSError)
|
||||
mp.setattr("os.unlink", causeOSError)
|
||||
errList = []
|
||||
errList = theProject._legacyDataFolder(tstData, errList)
|
||||
assert len(errList) > 0
|
||||
with pytest.raises(OSError):
|
||||
theProject._legacyDataFolder(tstData2)
|
||||
theProject._legacyDataFolder(tstData3)
|
||||
assert os.path.isfile(tstDoc1m)
|
||||
assert os.path.isfile(tstDoc1b)
|
||||
assert os.path.isfile(tstDoc2m)
|
||||
@@ -1196,15 +1246,16 @@ def testCoreProject_LegacyData(monkeypatch, mockGUI, fncDir):
|
||||
assert os.path.isfile(tstDoc3b)
|
||||
|
||||
# And succeed ...
|
||||
errList = []
|
||||
errList = theProject._legacyDataFolder(tstData, errList)
|
||||
assert len(errList) == 0
|
||||
assert theProject._legacyDataFolder(tstData2) is True
|
||||
assert theProject._legacyDataFolder(tstData3) is True
|
||||
|
||||
assert not os.path.isdir(tstData)
|
||||
assert not os.path.isdir(tstData2)
|
||||
assert os.path.isdir(tstData3)
|
||||
assert os.path.isfile(os.path.join(fncDir, "content", "2000000000001.nwd"))
|
||||
assert os.path.isfile(os.path.join(fncDir, "content", "2000000000002.nwd"))
|
||||
assert os.path.isfile(os.path.join(fncDir, "junk", "tooshort003_main.nwd"))
|
||||
assert os.path.isfile(os.path.join(fncDir, "junk", "tooshort003_main.bak"))
|
||||
assert os.path.isfile(os.path.join(fncDir, tstData3, "tooshort003_main.nwd"))
|
||||
assert os.path.isfile(os.path.join(fncDir, tstData3, "tooshort003_main.bak"))
|
||||
assert os.path.isdir(tstDir4a)
|
||||
|
||||
# END Test testCoreProject_LegacyData
|
||||
|
||||
|
||||
@@ -20,101 +20,314 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import random
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from PyQt5.QtGui import QIcon
|
||||
|
||||
from novelwriter.core.status import NWStatus
|
||||
|
||||
statusKeys = ["sa3b179", "s1c8031", "s06671a", "sbdd640"]
|
||||
importKeys = ["i466852", "i3eb13b", "i392456", "i23b8c1"]
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreStatus_Internal():
|
||||
"""Test all the internal functions of the NWStatus class.
|
||||
"""
|
||||
random.seed(42)
|
||||
theStatus = NWStatus(NWStatus.STATUS)
|
||||
theImport = NWStatus(NWStatus.IMPORT)
|
||||
|
||||
with pytest.raises(Exception):
|
||||
NWStatus(999)
|
||||
|
||||
# Generate Key
|
||||
# ============
|
||||
|
||||
assert theStatus._newKey() == statusKeys[0]
|
||||
assert theStatus._newKey() == statusKeys[1]
|
||||
|
||||
# Key collision, should move to key 3
|
||||
theStatus.write(statusKeys[2], "Crash", (0, 0, 0))
|
||||
assert theStatus._newKey() == statusKeys[3]
|
||||
|
||||
assert theImport._newKey() == importKeys[0]
|
||||
assert theImport._newKey() == importKeys[1]
|
||||
|
||||
# Key collision, should move to key 3
|
||||
theImport.write(importKeys[2], "Crash", (0, 0, 0))
|
||||
assert theImport._newKey() == importKeys[3]
|
||||
|
||||
# Check Key
|
||||
# =========
|
||||
|
||||
assert theStatus._isKey(None) is False # Not a string
|
||||
assert theStatus._isKey("s00000") is False # Too short
|
||||
assert theStatus._isKey("s000000") is True # Correct length
|
||||
assert theStatus._isKey("s0000000") is False # Too long
|
||||
assert theStatus._isKey("i000000") is False # Wrong type
|
||||
assert theStatus._isKey("q000000") is False # Wrong type
|
||||
assert theStatus._isKey("s12345H") is False # Not a hex value
|
||||
assert theStatus._isKey("s12345F") is False # Not a lower case hex value
|
||||
assert theStatus._isKey("s12345f") is True # Valid hex value
|
||||
|
||||
assert theImport._isKey(None) is False # Not a string
|
||||
assert theImport._isKey("i00000") is False # Too short
|
||||
assert theImport._isKey("i000000") is True # Correct length
|
||||
assert theImport._isKey("i0000000") is False # Too long
|
||||
assert theImport._isKey("s000000") is False # Wrong type
|
||||
assert theImport._isKey("q000000") is False # Wrong type
|
||||
assert theImport._isKey("i12345H") is False # Not a hex value
|
||||
assert theImport._isKey("i12345F") is False # Not a lower case hex value
|
||||
assert theImport._isKey("i12345f") is True # Valid hex value
|
||||
|
||||
# END Test testCoreStatus_Internal
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreStatus_Iterator():
|
||||
"""Test the iterator functions of the NWStatus class.
|
||||
"""
|
||||
random.seed(42)
|
||||
theStatus = NWStatus(NWStatus.STATUS)
|
||||
|
||||
theStatus.write(None, "New", (100, 100, 100))
|
||||
theStatus.write(None, "Note", (200, 50, 0))
|
||||
theStatus.write(None, "Draft", (200, 150, 0))
|
||||
theStatus.write(None, "Finished", (50, 200, 0))
|
||||
|
||||
# Direct access
|
||||
entry = theStatus[statusKeys[0]]
|
||||
assert entry["cols"] == (100, 100, 100)
|
||||
assert entry["name"] == "New"
|
||||
assert entry["count"] == 0
|
||||
assert isinstance(entry["icon"], QIcon)
|
||||
|
||||
# Iterate
|
||||
entries = list(theStatus)
|
||||
assert len(entries) == 4
|
||||
assert len(theStatus) == 4
|
||||
|
||||
# Keys
|
||||
assert list(theStatus.keys()) == statusKeys
|
||||
|
||||
# Items
|
||||
for index, (key, entry) in enumerate(theStatus.items()):
|
||||
assert key == statusKeys[index]
|
||||
assert "cols" in entry
|
||||
assert "name" in entry
|
||||
assert "count" in entry
|
||||
assert "icon" in entry
|
||||
|
||||
# Valuse
|
||||
for entry in theStatus.values():
|
||||
assert "cols" in entry
|
||||
assert "name" in entry
|
||||
assert "count" in entry
|
||||
assert "icon" in entry
|
||||
|
||||
# END Test testCoreStatus_Iterator
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreStatus_Entries():
|
||||
"""Test all the simple setters for the NWItem class.
|
||||
"""Test all the simple setters for the NWStatus class.
|
||||
"""
|
||||
theStatus = NWStatus()
|
||||
random.seed(42)
|
||||
theStatus = NWStatus(NWStatus.STATUS)
|
||||
|
||||
# Add entries
|
||||
theStatus.addEntry("New", (100, 100, 100))
|
||||
theStatus.addEntry("Minor", (200, 50, 0))
|
||||
theStatus.addEntry("Major", (200, 150, 0))
|
||||
theStatus.addEntry("Main", (50, 200, 0))
|
||||
# Write
|
||||
# =====
|
||||
|
||||
assert theStatus._theLabels == ["New", "Minor", "Major", "Main"]
|
||||
assert theStatus._theColours == [(100, 100, 100), (200, 50, 0), (200, 150, 0), (50, 200, 0)]
|
||||
assert theStatus._theCounts == [0, 0, 0, 0]
|
||||
assert theStatus._theMap["New"] == 0
|
||||
assert theStatus._theMap["Minor"] == 1
|
||||
assert theStatus._theMap["Major"] == 2
|
||||
assert theStatus._theMap["Main"] == 3
|
||||
assert theStatus._theLength == 4
|
||||
# Have a key
|
||||
theStatus.write(statusKeys[0], "Entry 1", (200, 100, 50))
|
||||
assert theStatus[statusKeys[0]]["name"] == "Entry 1"
|
||||
assert theStatus[statusKeys[0]]["cols"] == (200, 100, 50)
|
||||
|
||||
# Lookups
|
||||
assert theStatus.lookupEntry(None) is None
|
||||
assert theStatus.lookupEntry("stuff") is None
|
||||
assert theStatus.lookupEntry("Main") == 3
|
||||
# Don't have a key
|
||||
theStatus.write(None, "Entry 2", (210, 110, 60))
|
||||
assert theStatus[statusKeys[1]]["name"] == "Entry 2"
|
||||
assert theStatus[statusKeys[1]]["cols"] == (210, 110, 60)
|
||||
|
||||
# Checks
|
||||
assert theStatus.checkEntry(123) == "New"
|
||||
assert theStatus.checkEntry("Stuff") == "New"
|
||||
assert theStatus.checkEntry("New ") == "New"
|
||||
assert theStatus.checkEntry(" Main ") == "Main"
|
||||
# Wrong colour spec
|
||||
theStatus.write(None, "Entry 3", "what?")
|
||||
assert theStatus[statusKeys[2]]["name"] == "Entry 3"
|
||||
assert theStatus[statusKeys[2]]["cols"] == (100, 100, 100)
|
||||
|
||||
# Set new list
|
||||
newList = [
|
||||
("New", 1, 1, 1, "New"),
|
||||
("Minor", 2, 2, 2, "Minor"),
|
||||
("Major", 3, 3, 3, "Major"),
|
||||
("Min", 4, 4, 4, "Main"),
|
||||
("Max", 5, 5, 5, None),
|
||||
]
|
||||
assert theStatus.setNewEntries(None) == {}
|
||||
assert theStatus.setNewEntries(newList) == {"Main": "Min"}
|
||||
# Wrong colour count
|
||||
theStatus.write(None, "Entry 4", (10, 20))
|
||||
assert theStatus[statusKeys[3]]["name"] == "Entry 4"
|
||||
assert theStatus[statusKeys[3]]["cols"] == (100, 100, 100)
|
||||
|
||||
assert theStatus._theLabels == ["New", "Minor", "Major", "Min", "Max"]
|
||||
assert theStatus._theColours == [(1, 1, 1), (2, 2, 2), (3, 3, 3), (4, 4, 4), (5, 5, 5)]
|
||||
assert theStatus._theCounts == [0, 0, 0, 0, 0]
|
||||
assert theStatus._theMap["New"] == 0
|
||||
assert theStatus._theMap["Minor"] == 1
|
||||
assert theStatus._theMap["Major"] == 2
|
||||
assert theStatus._theMap["Min"] == 3
|
||||
assert theStatus._theMap["Max"] == 4
|
||||
assert theStatus._theLength == 5
|
||||
# Check reverse map
|
||||
assert theStatus._reverse == {
|
||||
"Entry 1": statusKeys[0],
|
||||
"Entry 2": statusKeys[1],
|
||||
"Entry 3": statusKeys[2],
|
||||
"Entry 4": statusKeys[3],
|
||||
}
|
||||
|
||||
# Add counts
|
||||
countTo = [3, 5, 7, 9, 11]
|
||||
# Check
|
||||
# =====
|
||||
|
||||
# Normal lookup
|
||||
for key in statusKeys:
|
||||
assert theStatus.check(key) == key
|
||||
|
||||
# Reverse map lookup
|
||||
assert theStatus.check("Entry 1") == statusKeys[0]
|
||||
assert theStatus.check("Entry 2") == statusKeys[1]
|
||||
assert theStatus.check("Entry 3") == statusKeys[2]
|
||||
assert theStatus.check("Entry 4") == statusKeys[3]
|
||||
|
||||
# Non-existing name
|
||||
assert theStatus.check("Entry 5") == statusKeys[0]
|
||||
|
||||
# Name Access
|
||||
# ===========
|
||||
|
||||
assert theStatus.name(statusKeys[0]) == "Entry 1"
|
||||
assert theStatus.name(statusKeys[1]) == "Entry 2"
|
||||
assert theStatus.name(statusKeys[2]) == "Entry 3"
|
||||
assert theStatus.name(statusKeys[3]) == "Entry 4"
|
||||
assert theStatus.name("blablabla") == "Entry 1"
|
||||
|
||||
# Colour Access
|
||||
# =============
|
||||
|
||||
assert theStatus.cols(statusKeys[0]) == (200, 100, 50)
|
||||
assert theStatus.cols(statusKeys[1]) == (210, 110, 60)
|
||||
assert theStatus.cols(statusKeys[2]) == (100, 100, 100)
|
||||
assert theStatus.cols(statusKeys[3]) == (100, 100, 100)
|
||||
assert theStatus.cols("blablabla") == (200, 100, 50)
|
||||
|
||||
# Icon Access
|
||||
# ===========
|
||||
|
||||
assert isinstance(theStatus.icon(statusKeys[0]), QIcon)
|
||||
assert isinstance(theStatus.icon(statusKeys[1]), QIcon)
|
||||
assert isinstance(theStatus.icon(statusKeys[2]), QIcon)
|
||||
assert isinstance(theStatus.icon(statusKeys[3]), QIcon)
|
||||
assert isinstance(theStatus.icon("blablabla"), QIcon)
|
||||
|
||||
# Increment and Count Access
|
||||
# ==========================
|
||||
|
||||
countTo = [3, 5, 7, 9]
|
||||
for i, n in enumerate(countTo):
|
||||
for _ in range(n):
|
||||
theStatus.countEntry(theStatus._theLabels[i])
|
||||
assert theStatus._theCounts == countTo
|
||||
theStatus.increment(statusKeys[i])
|
||||
|
||||
# Iterate
|
||||
for i, (sA, sB, sC) in enumerate(theStatus):
|
||||
assert sA == theStatus._theLabels[i]
|
||||
assert sB == theStatus._theColours[i]
|
||||
assert sC == theStatus._theCounts[i]
|
||||
assert theStatus.count(statusKeys[0]) == countTo[0]
|
||||
assert theStatus.count(statusKeys[1]) == countTo[1]
|
||||
assert theStatus.count(statusKeys[2]) == countTo[2]
|
||||
assert theStatus.count(statusKeys[3]) == countTo[3]
|
||||
assert theStatus.count("blablabla") == countTo[0]
|
||||
|
||||
assert theStatus[9] == (None, None, None)
|
||||
|
||||
# Clear counts
|
||||
theStatus.resetCounts()
|
||||
assert theStatus._theCounts == [0, 0, 0, 0, 0]
|
||||
|
||||
assert theStatus.count(statusKeys[0]) == 0
|
||||
assert theStatus.count(statusKeys[1]) == 0
|
||||
assert theStatus.count(statusKeys[2]) == 0
|
||||
assert theStatus.count(statusKeys[3]) == 0
|
||||
|
||||
# Reorder
|
||||
# =======
|
||||
|
||||
cOrder = list(theStatus.keys())
|
||||
assert cOrder == statusKeys
|
||||
|
||||
# Wrong length
|
||||
assert theStatus.reorder([]) is False
|
||||
|
||||
# No change
|
||||
assert theStatus.reorder(cOrder) is False
|
||||
|
||||
# Actual reaorder
|
||||
nOrder = [
|
||||
statusKeys[0],
|
||||
statusKeys[2],
|
||||
statusKeys[1],
|
||||
statusKeys[3],
|
||||
]
|
||||
assert theStatus.reorder(nOrder) is True
|
||||
assert list(theStatus.keys()) == nOrder
|
||||
|
||||
# Add an unknown key
|
||||
wOrder = nOrder.copy()
|
||||
wOrder[3] = theStatus._newKey()
|
||||
assert theStatus.reorder(wOrder) is False
|
||||
assert list(theStatus.keys()) == nOrder
|
||||
|
||||
# Put it back
|
||||
assert theStatus.reorder(cOrder) is True
|
||||
assert list(theStatus.keys()) == cOrder
|
||||
|
||||
# Default
|
||||
# =======
|
||||
|
||||
default = theStatus._default
|
||||
theStatus._default = None
|
||||
|
||||
assert theStatus.check("Entry 5") == ""
|
||||
assert theStatus.name("blablabla") == ""
|
||||
assert theStatus.cols("blablabla") == (100, 100, 100)
|
||||
assert theStatus.count("blablabla") == 0
|
||||
assert isinstance(theStatus.icon("blablabla"), QIcon)
|
||||
|
||||
theStatus._default = default
|
||||
|
||||
# Remove
|
||||
# ======
|
||||
|
||||
# Non-existing entry
|
||||
assert theStatus.remove("blablabla") is False
|
||||
|
||||
# Non-zero entry
|
||||
theStatus.increment(statusKeys[3])
|
||||
assert theStatus.remove(statusKeys[3]) is False
|
||||
|
||||
# Delete last entry
|
||||
theStatus.resetCounts()
|
||||
lastName = theStatus.name(statusKeys[3])
|
||||
assert lastName == "Entry 4"
|
||||
assert theStatus.remove(statusKeys[3]) is True
|
||||
assert theStatus.check(statusKeys[3]) == theStatus._default
|
||||
assert theStatus.check(lastName) == theStatus._default
|
||||
|
||||
# Delete default entry, Entry 2 is new default
|
||||
firstName = theStatus.name(theStatus._default)
|
||||
assert firstName == "Entry 1"
|
||||
assert theStatus.remove(theStatus._default) is True
|
||||
assert theStatus.name(firstName) == "Entry 2"
|
||||
|
||||
# Remove remaining entries
|
||||
assert theStatus.remove(statusKeys[1]) is True
|
||||
assert theStatus.remove(statusKeys[2]) is True
|
||||
|
||||
assert len(theStatus) == 0
|
||||
assert theStatus._default is None
|
||||
|
||||
# END Test testCoreStatus_Entries
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreStatus_XMLPackUnpack():
|
||||
"""Test all the simple setters for the NWItem class.
|
||||
"""Test all the XML pack/unpack of the NWStatus class.
|
||||
"""
|
||||
theStatus = NWStatus()
|
||||
theStatus.addEntry("New", (100, 100, 100))
|
||||
theStatus.addEntry("Minor", (200, 50, 0))
|
||||
theStatus.addEntry("Major", (200, 150, 0))
|
||||
theStatus.addEntry("Main", (50, 200, 0))
|
||||
random.seed(42)
|
||||
theStatus = NWStatus(NWStatus.STATUS)
|
||||
theStatus.write(None, "New", (100, 100, 100))
|
||||
theStatus.write(None, "Note", (200, 50, 0))
|
||||
theStatus.write(None, "Draft", (200, 150, 0))
|
||||
theStatus.write(None, "Finished", (50, 200, 0))
|
||||
|
||||
countTo = [3, 5, 7, 9]
|
||||
for i, n in enumerate(countTo):
|
||||
for _ in range(n):
|
||||
theStatus.countEntry(theStatus._theLabels[i])
|
||||
theStatus.increment(statusKeys[i])
|
||||
|
||||
nwXML = etree.Element("novelWriterXML")
|
||||
|
||||
@@ -122,24 +335,30 @@ def testCoreStatus_XMLPackUnpack():
|
||||
xStatus = etree.SubElement(nwXML, "status")
|
||||
theStatus.packXML(xStatus)
|
||||
assert etree.tostring(xStatus, pretty_print=False, encoding="utf-8") == (
|
||||
b"<status>"
|
||||
b"<entry blue=\"100\" green=\"100\" red=\"100\">New</entry>"
|
||||
b"<entry blue=\"0\" green=\"50\" red=\"200\">Minor</entry>"
|
||||
b"<entry blue=\"0\" green=\"150\" red=\"200\">Major</entry>"
|
||||
b"<entry blue=\"0\" green=\"200\" red=\"50\">Main</entry>"
|
||||
b"</status>"
|
||||
b'<status>'
|
||||
b'<entry key="sa3b179" count="3" red="100" green="100" blue="100">New</entry>'
|
||||
b'<entry key="s1c8031" count="5" red="200" green="50" blue="0">Note</entry>'
|
||||
b'<entry key="s06671a" count="7" red="200" green="150" blue="0">Draft</entry>'
|
||||
b'<entry key="sbdd640" count="9" red="50" green="200" blue="0">Finished</entry>'
|
||||
b'</status>'
|
||||
)
|
||||
|
||||
# Unpack
|
||||
theStatus = NWStatus()
|
||||
theStatus = NWStatus(NWStatus.STATUS)
|
||||
assert theStatus.unpackXML(xStatus)
|
||||
assert theStatus._theLabels == ["New", "Minor", "Major", "Main"]
|
||||
assert theStatus._theColours == [(100, 100, 100), (200, 50, 0), (200, 150, 0), (50, 200, 0)]
|
||||
assert theStatus._theCounts == [0, 0, 0, 0]
|
||||
assert theStatus._theMap["New"] == 0
|
||||
assert theStatus._theMap["Minor"] == 1
|
||||
assert theStatus._theMap["Major"] == 2
|
||||
assert theStatus._theMap["Main"] == 3
|
||||
assert theStatus._theLength == 4
|
||||
assert len(theStatus._store) == 4
|
||||
assert list(theStatus._store.keys()) == statusKeys
|
||||
assert theStatus._store[statusKeys[0]]["name"] == "New"
|
||||
assert theStatus._store[statusKeys[1]]["name"] == "Note"
|
||||
assert theStatus._store[statusKeys[2]]["name"] == "Draft"
|
||||
assert theStatus._store[statusKeys[3]]["name"] == "Finished"
|
||||
assert theStatus._store[statusKeys[0]]["cols"] == (100, 100, 100)
|
||||
assert theStatus._store[statusKeys[1]]["cols"] == (200, 50, 0)
|
||||
assert theStatus._store[statusKeys[2]]["cols"] == (200, 150, 0)
|
||||
assert theStatus._store[statusKeys[3]]["cols"] == (50, 200, 0)
|
||||
assert theStatus._store[statusKeys[0]]["count"] == countTo[0]
|
||||
assert theStatus._store[statusKeys[1]]["count"] == countTo[1]
|
||||
assert theStatus._store[statusKeys[2]]["count"] == countTo[2]
|
||||
assert theStatus._store[statusKeys[3]]["count"] == countTo[3]
|
||||
|
||||
# END Test testCoreStatus_XMLPackUnpack
|
||||
|
||||
@@ -24,7 +24,7 @@ import pytest
|
||||
|
||||
from tools import readFile
|
||||
|
||||
from novelwriter.core import NWProject, NWIndex, ToHtml
|
||||
from novelwriter.core import NWProject, ToHtml
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
@@ -32,7 +32,6 @@ def testCoreToHtml_ConvertFormat(mockGUI):
|
||||
"""Test the tokenizer and converter chain using the ToHtml class.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
mockGUI.theIndex = NWIndex(theProject)
|
||||
theHtml = ToHtml(theProject)
|
||||
|
||||
# Novel Files Headers
|
||||
@@ -235,7 +234,6 @@ def testCoreToHtml_ConvertDirect(mockGUI):
|
||||
"""Test the converter directly using the ToHtml class.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
mockGUI.theIndex = NWIndex(theProject)
|
||||
theHtml = ToHtml(theProject)
|
||||
|
||||
theHtml._isNovel = True
|
||||
@@ -606,7 +604,6 @@ def testCoreToHtml_Format(mockGUI):
|
||||
"""Test all the formatters for the ToHtml class.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
mockGUI.theIndex = NWIndex(theProject)
|
||||
theHtml = ToHtml(theProject)
|
||||
|
||||
# Export Mode
|
||||
|
||||
@@ -28,12 +28,17 @@ from novelwriter.core import NWProject, NWDoc
|
||||
from novelwriter.core.tokenizer import Tokenizer
|
||||
|
||||
|
||||
class BareTokenizer(Tokenizer):
|
||||
def doConvert(self):
|
||||
super().doConvert()
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreToken_Setters(mockGUI):
|
||||
"""Test all the setters for the Tokenizer class.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
theToken = Tokenizer(theProject)
|
||||
theToken = BareTokenizer(theProject)
|
||||
|
||||
# Verify defaults
|
||||
assert theToken._fmtTitle == "%title%"
|
||||
@@ -131,11 +136,10 @@ def testCoreToken_TextOps(monkeypatch, nwMinimal, mockGUI):
|
||||
"""Test handling files and text in the Tokenizer class.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
theProject.projTree.setSeed(42)
|
||||
theProject.projLang = "en"
|
||||
theProject._loadProjectLocalisation()
|
||||
|
||||
theToken = Tokenizer(theProject)
|
||||
theToken = BareTokenizer(theProject)
|
||||
theToken.setKeepMarkdown(True)
|
||||
|
||||
assert theProject.openProject(nwMinimal)
|
||||
@@ -214,6 +218,10 @@ def testCoreToken_TextOps(monkeypatch, nwMinimal, mockGUI):
|
||||
"# Notes: Plot\n\n"
|
||||
)
|
||||
|
||||
# Ckeck abstract method
|
||||
with pytest.raises(NotImplementedError):
|
||||
theToken.doConvert()
|
||||
|
||||
# END Test testCoreToken_TextOps
|
||||
|
||||
|
||||
@@ -222,7 +230,7 @@ def testCoreToken_HeaderFormat(mockGUI):
|
||||
"""Test the tokenization of header formats in the Tokenizer class.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
theToken = Tokenizer(theProject)
|
||||
theToken = BareTokenizer(theProject)
|
||||
theToken.setKeepMarkdown(True)
|
||||
|
||||
# Title
|
||||
@@ -426,7 +434,7 @@ def testCoreToken_MetaFormat(mockGUI):
|
||||
"""Test the tokenization of meta formats in the Tokenizer class.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
theToken = Tokenizer(theProject)
|
||||
theToken = BareTokenizer(theProject)
|
||||
theToken.setKeepMarkdown(True)
|
||||
|
||||
# Comment
|
||||
@@ -495,7 +503,7 @@ def testCoreToken_MarginFormat(mockGUI):
|
||||
"""Test the tokenization of margin formats in the Tokenizer class.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
theToken = Tokenizer(theProject)
|
||||
theToken = BareTokenizer(theProject)
|
||||
theToken.setKeepMarkdown(True)
|
||||
|
||||
# Alignment and Indentation
|
||||
@@ -550,7 +558,7 @@ def testCoreToken_TextFormat(mockGUI):
|
||||
"""Test the tokenization of text formats in the Tokenizer class.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
theToken = Tokenizer(theProject)
|
||||
theToken = BareTokenizer(theProject)
|
||||
theToken.setKeepMarkdown(True)
|
||||
|
||||
# Text
|
||||
@@ -672,7 +680,7 @@ def testCoreToken_SpecialFormat(mockGUI):
|
||||
"""Test the tokenization of special formats in the Tokenizer class.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
theToken = Tokenizer(theProject)
|
||||
theToken = BareTokenizer(theProject)
|
||||
|
||||
theToken._isNovel = True
|
||||
|
||||
@@ -877,7 +885,7 @@ def testCoreToken_ProcessHeaders(mockGUI):
|
||||
theProject = NWProject(mockGUI)
|
||||
theProject.projLang = "en"
|
||||
theProject._loadProjectLocalisation()
|
||||
theToken = Tokenizer(theProject)
|
||||
theToken = BareTokenizer(theProject)
|
||||
|
||||
# Nothing
|
||||
theToken._theText = "Some text ...\n"
|
||||
|
||||
@@ -24,7 +24,7 @@ import pytest
|
||||
|
||||
from tools import readFile
|
||||
|
||||
from novelwriter.core import NWProject, NWIndex, ToMarkdown
|
||||
from novelwriter.core import NWProject, ToMarkdown
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
@@ -32,7 +32,6 @@ def testCoreToMarkdown_ConvertFormat(mockGUI):
|
||||
"""Test the tokenizer and converter chain using the ToMarkdown class.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
mockGUI.theIndex = NWIndex(theProject)
|
||||
theMD = ToMarkdown(theProject)
|
||||
|
||||
# Headers
|
||||
@@ -161,7 +160,6 @@ def testCoreToMarkdown_ConvertDirect(mockGUI):
|
||||
"""Test the converter directly using the ToMarkdown class.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
mockGUI.theIndex = NWIndex(theProject)
|
||||
theMD = ToMarkdown(theProject)
|
||||
|
||||
theMD._isNovel = True
|
||||
@@ -266,7 +264,6 @@ def testCoreToMarkdown_Format(mockGUI):
|
||||
"""Test all the formatters for the ToMarkdown class.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
mockGUI.theIndex = NWIndex(theProject)
|
||||
theMD = ToMarkdown(theProject)
|
||||
|
||||
assert theMD._formatKeywords("", theMD.A_NONE) == ""
|
||||
|
||||
@@ -28,7 +28,7 @@ from shutil import copyfile
|
||||
|
||||
from tools import cmpFiles
|
||||
|
||||
from novelwriter.core import NWProject, NWIndex, ToOdt
|
||||
from novelwriter.core import NWProject, ToOdt
|
||||
from novelwriter.core.toodt import ODTParagraphStyle, ODTTextStyle, XMLParagraph, _mkTag
|
||||
|
||||
XML_NS = [
|
||||
@@ -55,7 +55,6 @@ def testCoreToOdt_Init(mockGUI):
|
||||
"""Test initialisation of the ODT document.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
mockGUI.theIndex = NWIndex(theProject)
|
||||
|
||||
# Flat Doc
|
||||
# ========
|
||||
@@ -111,7 +110,6 @@ def testCoreToOdt_TextFormatting(mockGUI):
|
||||
"""Test formatting of paragraphs.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
mockGUI.theIndex = NWIndex(theProject)
|
||||
theDoc = ToOdt(theProject, isFlat=True)
|
||||
|
||||
theDoc.initDocument()
|
||||
@@ -233,7 +231,6 @@ def testCoreToOdt_Convert(mockGUI):
|
||||
"""Test the converter of the ToOdt class.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
mockGUI.theIndex = NWIndex(theProject)
|
||||
theDoc = ToOdt(theProject, isFlat=True)
|
||||
|
||||
theDoc._isNovel = True
|
||||
@@ -565,7 +562,6 @@ def testCoreToOdt_ConvertDirect(mockGUI):
|
||||
otherwise hard to reach conditions.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
mockGUI.theIndex = NWIndex(theProject)
|
||||
theDoc = ToOdt(theProject, isFlat=True)
|
||||
|
||||
theDoc._isNovel = True
|
||||
@@ -620,7 +616,6 @@ def testCoreToOdt_SaveFlat(mockGUI, fncDir, outDir, refDir):
|
||||
"""Test the document save functions.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
mockGUI.theIndex = NWIndex(theProject)
|
||||
|
||||
theDoc = ToOdt(theProject, isFlat=True)
|
||||
theDoc._isNovel = True
|
||||
@@ -657,7 +652,6 @@ def testCoreToOdt_SaveFull(mockGUI, fncDir, outDir, refDir):
|
||||
"""Test the document save functions.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
mockGUI.theIndex = NWIndex(theProject)
|
||||
|
||||
theDoc = ToOdt(theProject, isFlat=False)
|
||||
theDoc._isNovel = True
|
||||
@@ -737,7 +731,6 @@ def testCoreToOdt_Format(mockGUI):
|
||||
"""Test the formatters for the ToOdt class.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
mockGUI.theIndex = NWIndex(theProject)
|
||||
theDoc = ToOdt(theProject, isFlat=True)
|
||||
|
||||
assert theDoc._formatSynopsis("synopsis text") == (
|
||||
|
||||
@@ -21,9 +21,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import pytest
|
||||
import random
|
||||
|
||||
from lxml import etree
|
||||
from hashlib import sha256
|
||||
|
||||
from tools import readFile
|
||||
|
||||
@@ -33,7 +33,7 @@ from novelwriter.constants import nwFiles
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def mockItems(mockGUI):
|
||||
def mockItems(mockGUI, mockRnd):
|
||||
"""Create a list of mock items.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
@@ -76,7 +76,7 @@ def mockItems(mockGUI):
|
||||
|
||||
itemF = NWItem(theProject)
|
||||
itemF._name = "Trash"
|
||||
itemF._type = nwItemType.TRASH
|
||||
itemF._type = nwItemType.ROOT
|
||||
itemF._class = nwItemClass.TRASH
|
||||
itemF._expanded = False
|
||||
|
||||
@@ -103,7 +103,7 @@ def mockItems(mockGUI):
|
||||
("a000000000002", None, itemE),
|
||||
("a000000000003", None, itemF),
|
||||
("a000000000004", None, itemG),
|
||||
("b000000000002", "a000000000002", itemH),
|
||||
("b000000000002", "a000000000004", itemH),
|
||||
]
|
||||
|
||||
return theItems
|
||||
@@ -116,26 +116,22 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
|
||||
theProject = NWProject(mockGUI)
|
||||
theTree = NWTree(theProject)
|
||||
|
||||
theTree.setSeed(42)
|
||||
assert theTree._handleSeed == 42
|
||||
|
||||
# Check that tree is empty (calls NWTree.__bool__)
|
||||
assert not theTree
|
||||
assert bool(theTree) is False
|
||||
|
||||
# Check for archive and trash folders
|
||||
assert theTree.trashRoot() is None
|
||||
assert theTree.archiveRoot() is None
|
||||
assert not theTree.isTrashRoot("a000000000003")
|
||||
|
||||
aHandles = []
|
||||
for tHandle, pHandle, nwItem in mockItems:
|
||||
aHandles.append(tHandle)
|
||||
assert theTree.append(tHandle, pHandle, nwItem)
|
||||
assert theTree.append(tHandle, pHandle, nwItem) is True
|
||||
assert theTree.updateItemData(tHandle) is True
|
||||
|
||||
assert theTree._treeChanged
|
||||
assert theTree._treeChanged is True
|
||||
|
||||
# Check that tree is not empty (calls __bool__)
|
||||
assert theTree
|
||||
assert bool(theTree) is True
|
||||
|
||||
# Check the number of elements (calls __len__)
|
||||
assert len(theTree) == len(mockItems)
|
||||
@@ -149,17 +145,43 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
|
||||
|
||||
# Check that we have the correct archive and trash folders
|
||||
assert theTree.trashRoot() == "a000000000003"
|
||||
assert theTree.archiveRoot() == "a000000000002"
|
||||
assert theTree.isTrashRoot("a000000000003")
|
||||
assert theTree.findRoot(nwItemClass.ARCHIVE) == "a000000000002"
|
||||
assert theTree.isTrash("a000000000003") is True
|
||||
assert theTree.isRoot("a000000000002") is True
|
||||
|
||||
# Check that we have the root classes
|
||||
assert theTree.rootClasses() == {
|
||||
nwItemClass.NOVEL, nwItemClass.CHARACTER, nwItemClass.ARCHIVE, nwItemClass.TRASH
|
||||
}
|
||||
|
||||
# Check the isTrash function
|
||||
assert theTree.isTrash("0000000000000") is True # Doesn't exist
|
||||
assert theTree.isTrash("a000000000003") is True # This the trash folder
|
||||
|
||||
theTree["a000000000003"].setClass(nwItemClass.NO_CLASS)
|
||||
assert theTree.isTrash("a000000000003") is True # This is still trash
|
||||
theTree["a000000000003"].setClass(nwItemClass.TRASH)
|
||||
|
||||
assert theTree.isTrash("b000000000002") is False # This is not trash
|
||||
|
||||
value = theTree["b000000000002"].itemParent
|
||||
theTree["b000000000002"].setParent("a000000000003")
|
||||
assert theTree.isTrash("b000000000002") is True # This is in trash
|
||||
theTree["b000000000002"].setParent(value)
|
||||
|
||||
value = theTree["b000000000002"].itemRoot
|
||||
theTree["b000000000002"].setRoot("a000000000003")
|
||||
assert theTree.isTrash("b000000000002") is True # This is in trash
|
||||
theTree["b000000000002"].setRoot(value)
|
||||
|
||||
# Try to add another trash folder
|
||||
itemT = NWItem(theProject)
|
||||
itemT._name = "Trash"
|
||||
itemT._type = nwItemType.TRASH
|
||||
itemT._type = nwItemType.ROOT
|
||||
itemT._class = nwItemClass.TRASH
|
||||
itemT._expanded = False
|
||||
|
||||
assert not theTree.append("1234567890abc", None, itemT)
|
||||
assert theTree.append("1234567890abc", None, itemT) is False
|
||||
assert len(theTree) == len(mockItems)
|
||||
|
||||
# Generate handle automatically
|
||||
@@ -169,14 +191,16 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
|
||||
itemT._class = nwItemClass.NOVEL
|
||||
itemT._layout = nwItemLayout.DOCUMENT
|
||||
|
||||
assert theTree.append(None, None, itemT)
|
||||
assert theTree.append(None, None, itemT) is True
|
||||
assert theTree.updateItemData(itemT.itemHandle) is True
|
||||
assert len(theTree) == len(mockItems) + 1
|
||||
|
||||
theList = theTree.handles()
|
||||
assert theList[-1] == "73475cb40a568"
|
||||
nHandle = "0000000000010"
|
||||
assert theList[-1] == nHandle
|
||||
|
||||
# Try to add existing handle
|
||||
assert not theTree.append("73475cb40a568", None, itemT)
|
||||
assert theTree.append(nHandle, None, itemT) is False
|
||||
assert len(theTree) == len(mockItems) + 1
|
||||
|
||||
# Delete a non-existing item
|
||||
@@ -184,9 +208,9 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
|
||||
assert len(theTree) == len(mockItems) + 1
|
||||
|
||||
# Delete the last item
|
||||
del theTree["73475cb40a568"]
|
||||
del theTree[nHandle]
|
||||
assert len(theTree) == len(mockItems)
|
||||
assert "73475cb40a568" not in theTree
|
||||
assert nHandle not in theTree
|
||||
|
||||
# Delete the Novel, Archive and Trash folders
|
||||
del theTree["a000000000001"]
|
||||
@@ -196,7 +220,6 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
|
||||
del theTree["a000000000002"]
|
||||
assert len(theTree) == len(mockItems) - 2
|
||||
assert "a000000000002" not in theTree
|
||||
assert theTree.archiveRoot() is None
|
||||
|
||||
del theTree["a000000000003"]
|
||||
assert len(theTree) == len(mockItems) - 3
|
||||
@@ -215,31 +238,43 @@ def testCoreTree_Methods(mockGUI, mockItems):
|
||||
|
||||
for tHandle, pHandle, nwItem in mockItems:
|
||||
theTree.append(tHandle, pHandle, nwItem)
|
||||
theTree.updateItemData(tHandle)
|
||||
|
||||
assert len(theTree) == len(mockItems)
|
||||
|
||||
# Update item data, nonsense handle
|
||||
assert theTree.updateItemData("stuff") is False
|
||||
|
||||
# Update item data, invalid item parent
|
||||
corrParent = theTree["b000000000001"].itemParent
|
||||
theTree["b000000000001"].setParent("0000000000000")
|
||||
assert theTree.updateItemData("b000000000001") is False
|
||||
|
||||
# Update item data, valid item parent
|
||||
theTree["b000000000001"].setParent(corrParent)
|
||||
assert theTree.updateItemData("b000000000001") is True
|
||||
|
||||
# Update item data, root is unreachable
|
||||
maxDepth = theTree.MAX_DEPTH
|
||||
theTree.MAX_DEPTH = 0
|
||||
with pytest.raises(RecursionError):
|
||||
theTree.updateItemData("b000000000001")
|
||||
theTree.MAX_DEPTH = maxDepth
|
||||
|
||||
# Chech type
|
||||
assert theTree.checkType("blabla", nwItemType.FILE) is False
|
||||
assert theTree.checkType("b000000000001", nwItemType.FILE) is False
|
||||
assert theTree.checkType("c000000000001", nwItemType.FILE) is True
|
||||
|
||||
# Root item lookup
|
||||
theTree._treeRoots.append("stuff")
|
||||
assert theTree.findRoot(nwItemClass.WORLD) is None
|
||||
assert theTree.findRoot(nwItemClass.NOVEL) == "a000000000001"
|
||||
assert theTree.findRoot(nwItemClass.CHARACTER) == "a000000000004"
|
||||
|
||||
# Check for root uniqueness
|
||||
assert theTree.checkRootUnique(nwItemClass.CUSTOM)
|
||||
assert theTree.checkRootUnique(nwItemClass.WORLD)
|
||||
assert not theTree.checkRootUnique(nwItemClass.NOVEL)
|
||||
assert not theTree.checkRootUnique(nwItemClass.CHARACTER)
|
||||
|
||||
# Find root item of child item
|
||||
assert theTree.getRootItem("b000000000001").itemHandle == "a000000000001"
|
||||
assert theTree.getRootItem("c000000000001").itemHandle == "a000000000001"
|
||||
assert theTree.getRootItem("c000000000002").itemHandle == "a000000000001"
|
||||
assert theTree.getRootItem("stuff") is None
|
||||
# Add a fake item to root and check that it can handle it
|
||||
theTree._treeRoots["0000000000000"] = NWItem(theProject)
|
||||
assert theTree.findRoot(nwItemClass.WORLD) is None
|
||||
del theTree._treeRoots["0000000000000"]
|
||||
|
||||
# Get item path
|
||||
assert theTree.getItemPath("stuff") == []
|
||||
@@ -247,6 +282,13 @@ def testCoreTree_Methods(mockGUI, mockItems):
|
||||
"c000000000001", "b000000000001", "a000000000001"
|
||||
]
|
||||
|
||||
# Cause recursion error
|
||||
maxDepth = theTree.MAX_DEPTH
|
||||
theTree.MAX_DEPTH = 0
|
||||
with pytest.raises(RecursionError):
|
||||
theTree.getItemPath("c000000000001")
|
||||
theTree.MAX_DEPTH = maxDepth
|
||||
|
||||
# Break the folder parent handle
|
||||
theTree["b000000000001"]._parent = "stuff"
|
||||
assert theTree.getItemPath("c000000000001") == [
|
||||
@@ -269,44 +311,31 @@ def testCoreTree_Methods(mockGUI, mockItems):
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreTree_MakeHandles(monkeypatch, mockGUI):
|
||||
def testCoreTree_MakeHandles(mockGUI):
|
||||
"""Test generating item handles.
|
||||
"""
|
||||
random.seed(42)
|
||||
theProject = NWProject(mockGUI)
|
||||
theTree = NWTree(theProject)
|
||||
|
||||
theTree.setSeed(42)
|
||||
handles = ["1c803a3b1799d", "bdd6406671ad1", "3eb1346685257", "23b8c392456de"]
|
||||
|
||||
random.seed(42)
|
||||
tHandle = theTree._makeHandle()
|
||||
assert tHandle == "73475cb40a568"
|
||||
assert tHandle == handles[0]
|
||||
theTree._projTree[handles[0]] = None
|
||||
|
||||
# Add the next in line to the project to force duplicate
|
||||
theTree._projTree["44cb730c42048"] = None
|
||||
theTree._projTree[handles[1]] = None
|
||||
tHandle = theTree._makeHandle()
|
||||
assert tHandle == "71ee45a3c0db9"
|
||||
|
||||
# Fix the time() function and force a handle collission
|
||||
theTree.setSeed(None)
|
||||
theTree._handleCount = 0
|
||||
monkeypatch.setattr("novelwriter.core.tree.time", lambda: 123.4)
|
||||
assert tHandle == handles[2]
|
||||
theTree._projTree[handles[2]] = None
|
||||
|
||||
# Reset the seed to force collissions, which should still end up
|
||||
# returning the next handle in the sequence
|
||||
random.seed(42)
|
||||
tHandle = theTree._makeHandle()
|
||||
theTree._projTree[tHandle] = None
|
||||
newSeed = "123.4_0_"
|
||||
assert tHandle == sha256(newSeed.encode()).hexdigest()[0:13]
|
||||
|
||||
tHandle = theTree._makeHandle()
|
||||
theTree._projTree[tHandle] = None
|
||||
newSeed = "123.4_1_"
|
||||
assert tHandle == sha256(newSeed.encode()).hexdigest()[0:13]
|
||||
|
||||
# Reset the count and the handle for 0 and 1 should be duplicates
|
||||
# which forces the function to add the '!'
|
||||
theTree._handleCount = 0
|
||||
tHandle = theTree._makeHandle()
|
||||
theTree._projTree[tHandle] = None
|
||||
newSeed = "123.4_1_!"
|
||||
assert tHandle == sha256(newSeed.encode()).hexdigest()[0:13]
|
||||
assert tHandle == handles[3]
|
||||
|
||||
# END Test testCoreTree_MakeHandles
|
||||
|
||||
@@ -329,12 +358,6 @@ def testCoreTree_Stats(mockGUI, mockItems):
|
||||
assert novelWords == 550
|
||||
assert noteWords == 400
|
||||
|
||||
# Count types
|
||||
nRoot, nFolder, nFile = theTree.countTypes()
|
||||
assert nRoot == 3
|
||||
assert nFolder == 1
|
||||
assert nFile == 3
|
||||
|
||||
# END Test testCoreTree_Stats
|
||||
|
||||
|
||||
@@ -379,42 +402,44 @@ def testCoreTree_XMLPackUnpack(mockGUI, mockItems):
|
||||
|
||||
for tHandle, pHandle, nwItem in mockItems:
|
||||
theTree.append(tHandle, pHandle, nwItem)
|
||||
theTree.updateItemData(tHandle)
|
||||
|
||||
assert len(theTree) == len(mockItems)
|
||||
|
||||
nwXML = etree.Element("novelWriterXML")
|
||||
theTree.packXML(nwXML)
|
||||
assert etree.tostring(nwXML, pretty_print=False, encoding="utf-8") == (
|
||||
b"<novelWriterXML>"
|
||||
b"<content count=\"8\">"
|
||||
b"<item handle=\"a000000000001\" order=\"0\" parent=\"None\">"
|
||||
b"<name>Novel</name><type>ROOT</type><class>NOVEL</class><status>None</status>"
|
||||
b"<expanded>True</expanded></item>"
|
||||
b"<item handle=\"b000000000001\" order=\"0\" parent=\"a000000000001\">"
|
||||
b"<name>Act One</name><type>FOLDER</type><class>NOVEL</class><status>None</status>"
|
||||
b"<expanded>True</expanded></item>"
|
||||
b"<item handle=\"c000000000001\" order=\"0\" parent=\"b000000000001\">"
|
||||
b"<name>Chapter One</name><type>FILE</type><class>NOVEL</class><status>None</status>"
|
||||
b"<exported>True</exported><layout>DOCUMENT</layout><charCount>300</charCount>"
|
||||
b"<wordCount>50</wordCount><paraCount>2</paraCount><cursorPos>0</cursorPos></item>"
|
||||
b"<item handle=\"c000000000002\" order=\"0\" parent=\"b000000000001\">"
|
||||
b"<name>Scene One</name><type>FILE</type><class>NOVEL</class><status>None</status>"
|
||||
b"<exported>True</exported><layout>DOCUMENT</layout><charCount>3000</charCount>"
|
||||
b"<wordCount>500</wordCount><paraCount>20</paraCount><cursorPos>0</cursorPos></item>"
|
||||
b"<item handle=\"a000000000002\" order=\"0\" parent=\"None\">"
|
||||
b"<name>Outtakes</name><type>ROOT</type><class>ARCHIVE</class><status>None</status>"
|
||||
b"<expanded>False</expanded></item>"
|
||||
b"<item handle=\"a000000000003\" order=\"0\" parent=\"None\">"
|
||||
b"<name>Trash</name><type>TRASH</type><class>TRASH</class><status>None</status>"
|
||||
b"<expanded>False</expanded></item>"
|
||||
b"<item handle=\"a000000000004\" order=\"0\" parent=\"None\">"
|
||||
b"<name>Characters</name><type>ROOT</type><class>CHARACTER</class><status>None</status>"
|
||||
b"<expanded>True</expanded></item>"
|
||||
b"<item handle=\"b000000000002\" order=\"0\" parent=\"a000000000002\">"
|
||||
b"<name>Jane Doe</name><type>FILE</type><class>CHARACTER</class><status>None</status>"
|
||||
b"<exported>True</exported><layout>NOTE</layout><charCount>2000</charCount>"
|
||||
b"<wordCount>400</wordCount><paraCount>16</paraCount><cursorPos>0</cursorPos></item>"
|
||||
b"</content></novelWriterXML>"
|
||||
b'<novelWriterXML>'
|
||||
b'<content count="8">'
|
||||
b'<item handle="a000000000001" parent="None" root="a000000000001" order="0" type="ROOT" '
|
||||
b'class="NOVEL"><meta expanded="True"/><name status="s000000" '
|
||||
b'import="i000004">Novel</name></item>'
|
||||
b'<item handle="b000000000001" parent="a000000000001" root="a000000000001" order="0" '
|
||||
b'type="FOLDER" class="NOVEL"><meta expanded="True"/><name status="s000000" '
|
||||
b'import="i000004">Act One</name></item>'
|
||||
b'<item handle="c000000000001" parent="b000000000001" root="a000000000001" order="0" '
|
||||
b'type="FILE" class="NOVEL" layout="DOCUMENT"><meta expanded="False" charCount="300" '
|
||||
b'wordCount="50" paraCount="2" cursorPos="0"/><name status="s000000" import="i000004" '
|
||||
b'exported="True">Chapter One</name></item>'
|
||||
b'<item handle="c000000000002" parent="b000000000001" root="a000000000001" order="0" '
|
||||
b'type="FILE" class="NOVEL" layout="DOCUMENT"><meta expanded="False" charCount="3000" '
|
||||
b'wordCount="500" paraCount="20" cursorPos="0"/><name status="s000000" import="i000004" '
|
||||
b'exported="True">Scene One</name></item>'
|
||||
b'<item handle="a000000000002" parent="None" root="a000000000002" order="0" type="ROOT" '
|
||||
b'class="ARCHIVE"><meta expanded="False"/><name status="s000000" '
|
||||
b'import="i000004">Outtakes</name></item>'
|
||||
b'<item handle="a000000000003" parent="None" root="a000000000003" order="0" type="ROOT" '
|
||||
b'class="TRASH"><meta expanded="False"/><name status="s000000" '
|
||||
b'import="i000004">Trash</name></item>'
|
||||
b'<item handle="a000000000004" parent="None" root="a000000000004" order="0" type="ROOT" '
|
||||
b'class="CHARACTER"><meta expanded="True"/><name status="s000000" '
|
||||
b'import="i000004">Characters</name></item>'
|
||||
b'<item handle="b000000000002" parent="a000000000004" root="a000000000004" order="0" '
|
||||
b'type="FILE" class="CHARACTER" layout="NOTE"><meta expanded="False" charCount="2000" '
|
||||
b'wordCount="400" paraCount="16" cursorPos="0"/><name status="s000000" import="i000004" '
|
||||
b'exported="True">Jane Doe</name></item>'
|
||||
b'</content>'
|
||||
b'</novelWriterXML>'
|
||||
)
|
||||
|
||||
theTree.clear()
|
||||
@@ -435,6 +460,7 @@ def testCoreTree_ToCFile(monkeypatch, mockGUI, mockItems, tmpDir):
|
||||
|
||||
for tHandle, pHandle, nwItem in mockItems:
|
||||
theTree.append(tHandle, pHandle, nwItem)
|
||||
theTree.updateItemData(tHandle)
|
||||
|
||||
assert len(theTree) == len(mockItems)
|
||||
theTree._treeOrder.append("stuff")
|
||||
|
||||
@@ -36,8 +36,8 @@ def testDlgAbout_NWDialog(qtbot, monkeypatch, nwGUI):
|
||||
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
|
||||
|
||||
# NW About
|
||||
nwGUI.theTheme.themeName = "A Theme"
|
||||
nwGUI.theTheme.themeAuthor = "An Author"
|
||||
nwGUI.mainTheme.themeName = "A Theme"
|
||||
nwGUI.mainTheme.themeAuthor = "An Author"
|
||||
assert nwGUI.showAboutNWDialog(showNotes=True) is True
|
||||
|
||||
qtbot.waitUntil(lambda: getGuiItem("GuiAbout") is not None, timeout=1000)
|
||||
|
||||
@@ -24,11 +24,7 @@ import pytest
|
||||
from PyQt5.QtCore import QItemSelectionModel
|
||||
from PyQt5.QtWidgets import QAction, QListWidgetItem, QDialog, QMessageBox
|
||||
|
||||
from novelwriter.dialogs import GuiQuoteSelect, GuiUpdates
|
||||
|
||||
keyDelay = 2
|
||||
typeDelay = 1
|
||||
stepDelay = 20
|
||||
from novelwriter.dialogs import GuiQuoteSelect, GuiUpdates, GuiEditLabel
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
@@ -101,3 +97,24 @@ def testDlgOther_Updates(qtbot, monkeypatch, nwGUI):
|
||||
nwUpdate._doClose()
|
||||
|
||||
# END Test testDlgOther_Updates
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testDlgOther_EditLabel(qtbot, monkeypatch):
|
||||
"""Test the label editor dialog.
|
||||
"""
|
||||
monkeypatch.setattr(GuiEditLabel, "exec_", lambda *a: None)
|
||||
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(GuiEditLabel, "result", lambda *a: QDialog.Accepted)
|
||||
newLabel, dlgOk = GuiEditLabel.getLabel(None, text="Hello World")
|
||||
assert dlgOk is True
|
||||
assert newLabel == "Hello World"
|
||||
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(GuiEditLabel, "result", lambda *a: QDialog.Rejected)
|
||||
newLabel, dlgOk = GuiEditLabel.getLabel(None, text="Hello World")
|
||||
assert dlgOk is False
|
||||
assert newLabel == "Hello World"
|
||||
|
||||
# END Test testDlgOther_EditLabel
|
||||
|
||||
@@ -19,48 +19,48 @@ 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 pytest
|
||||
import os
|
||||
import pytest
|
||||
|
||||
from tools import getGuiItem, readFile, writeFile
|
||||
from mock import causeOSError
|
||||
from tools import getGuiItem, readFile, writeFile, buildTestProject
|
||||
|
||||
from PyQt5.QtWidgets import QAction, QMessageBox, QDialog
|
||||
from PyQt5.QtWidgets import QAction, QMessageBox
|
||||
|
||||
from novelwriter.dialogs import GuiDocMerge, GuiItemEditor
|
||||
from novelwriter.enum import nwItemType, nwWidget
|
||||
from novelwriter.dialogs import GuiDocMerge, GuiEditLabel
|
||||
from novelwriter.core.tree import NWTree
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj):
|
||||
def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
|
||||
"""Test the merge documents tool.
|
||||
"""
|
||||
# Block message box
|
||||
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Ok)
|
||||
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
|
||||
|
||||
# Create a new project
|
||||
nwGUI.theProject.projTree.setSeed(42)
|
||||
assert nwGUI.newProject({"projPath": fncProj})
|
||||
buildTestProject(nwGUI, fncProj)
|
||||
|
||||
# Handles for new objects
|
||||
hChapterDir = "31489056e0916"
|
||||
hChapterOne = "98010bd9270f9"
|
||||
hSceneOne = "0e17daca5f3e1"
|
||||
hSceneTwo = "1a6562590ef19"
|
||||
hSceneThree = "031b4af5197ec"
|
||||
hSceneFour = "41cfc0d1f2d12"
|
||||
hMergedDoc = "2858dcd1057d3"
|
||||
hNovelRoot = "0000000000008"
|
||||
hChapterDir = "000000000000d"
|
||||
hChapterOne = "000000000000e"
|
||||
hSceneOne = "000000000000f"
|
||||
hSceneTwo = "0000000000010"
|
||||
hSceneThree = "0000000000011"
|
||||
hSceneFour = "0000000000012"
|
||||
hMergedDoc = "0000000000023"
|
||||
|
||||
# Add Project Content
|
||||
monkeypatch.setattr(GuiItemEditor, "exec_", lambda *a: QDialog.Accepted)
|
||||
nwGUI.switchFocus(nwWidget.TREE)
|
||||
nwGUI.treeView.clearSelection()
|
||||
nwGUI.treeView._getTreeItem(hChapterDir).setSelected(True)
|
||||
nwGUI.treeView.newTreeItem(nwItemType.FILE, None)
|
||||
nwGUI.treeView.newTreeItem(nwItemType.FILE, None)
|
||||
nwGUI.treeView.newTreeItem(nwItemType.FILE, None)
|
||||
nwGUI.projView.projTree.clearSelection()
|
||||
nwGUI.projView.projTree._getTreeItem(hChapterDir).setSelected(True)
|
||||
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE)
|
||||
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE)
|
||||
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE)
|
||||
|
||||
assert nwGUI.saveProject() is True
|
||||
assert nwGUI.closeProject() is True
|
||||
@@ -82,8 +82,8 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj):
|
||||
|
||||
# Open the Merge tool
|
||||
nwGUI.switchFocus(nwWidget.TREE)
|
||||
nwGUI.treeView.clearSelection()
|
||||
nwGUI.treeView._getTreeItem(hChapterDir).setSelected(True)
|
||||
nwGUI.projView.projTree.clearSelection()
|
||||
nwGUI.projView.projTree._getTreeItem(hChapterDir).setSelected(True)
|
||||
|
||||
monkeypatch.setattr(GuiDocMerge, "exec_", lambda *a: None)
|
||||
nwGUI.mainMenu.aMergeDocs.activate(QAction.Trigger)
|
||||
@@ -101,27 +101,27 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj):
|
||||
assert nwMerge.listBox.count() == 0
|
||||
|
||||
# No item selected
|
||||
nwGUI.treeView.clearSelection()
|
||||
nwGUI.projView.projTree.clearSelection()
|
||||
assert nwMerge._populateList() is False
|
||||
assert nwMerge.listBox.count() == 0
|
||||
|
||||
# Non-existing item
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(NWTree, "__getitem__", lambda *a: None)
|
||||
nwGUI.treeView.clearSelection()
|
||||
nwGUI.treeView._getTreeItem(hChapterDir).setSelected(True)
|
||||
nwGUI.projView.projTree.clearSelection()
|
||||
nwGUI.projView.projTree._getTreeItem(hChapterDir).setSelected(True)
|
||||
assert nwMerge._populateList() is False
|
||||
assert nwMerge.listBox.count() == 0
|
||||
|
||||
# Select a non-folder
|
||||
nwGUI.treeView.clearSelection()
|
||||
nwGUI.treeView._getTreeItem(hChapterOne).setSelected(True)
|
||||
nwGUI.projView.projTree.clearSelection()
|
||||
nwGUI.projView.projTree._getTreeItem(hChapterOne).setSelected(True)
|
||||
assert nwMerge._populateList() is False
|
||||
assert nwMerge.listBox.count() == 0
|
||||
|
||||
# Select the chapter folder
|
||||
nwGUI.treeView.clearSelection()
|
||||
nwGUI.treeView._getTreeItem(hChapterDir).setSelected(True)
|
||||
nwGUI.projView.projTree.clearSelection()
|
||||
nwGUI.projView.projTree._getTreeItem(hChapterDir).setSelected(True)
|
||||
assert nwMerge._populateList() is True
|
||||
assert nwMerge.listBox.count() == 5
|
||||
|
||||
@@ -137,7 +137,7 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj):
|
||||
assert os.path.isfile(mergedFile)
|
||||
assert readFile(mergedFile) == (
|
||||
"%%%%~name: New Chapter\n"
|
||||
"%%%%~path: 73475cb40a568/2858dcd1057d3\n"
|
||||
"%%%%~path: %s/%s\n"
|
||||
"%%%%~kind: NOVEL/DOCUMENT\n"
|
||||
"%s\n\n"
|
||||
"%s\n\n"
|
||||
@@ -145,6 +145,8 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj):
|
||||
"%s\n\n"
|
||||
"%s\n\n"
|
||||
) % (
|
||||
hNovelRoot,
|
||||
hMergedDoc,
|
||||
tChapterOne.strip(),
|
||||
tSceneOne.strip(),
|
||||
tSceneTwo.strip(),
|
||||
|
||||
@@ -19,50 +19,50 @@ 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 pytest
|
||||
import os
|
||||
import pytest
|
||||
|
||||
from tools import getGuiItem, readFile, writeFile
|
||||
from mock import causeOSError
|
||||
from tools import getGuiItem, readFile, writeFile, buildTestProject
|
||||
|
||||
from PyQt5.QtWidgets import QAction, QMessageBox, QDialog
|
||||
from PyQt5.QtWidgets import QAction, QMessageBox
|
||||
|
||||
from novelwriter.dialogs import GuiDocSplit, GuiItemEditor
|
||||
from novelwriter.enum import nwItemType, nwWidget
|
||||
from novelwriter.core.document import NWDoc
|
||||
from novelwriter.dialogs import GuiDocSplit, GuiEditLabel
|
||||
from novelwriter.core.tree import NWTree
|
||||
from novelwriter.core.document import NWDoc
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj):
|
||||
def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
|
||||
"""Test the split document tool.
|
||||
"""
|
||||
# Block message box
|
||||
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Ok)
|
||||
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
|
||||
|
||||
# Create a new project
|
||||
nwGUI.theProject.projTree.setSeed(42)
|
||||
assert nwGUI.newProject({"projPath": fncProj}) is True
|
||||
buildTestProject(nwGUI, fncProj)
|
||||
|
||||
# Handles for new objects
|
||||
hNovelRoot = "73475cb40a568"
|
||||
hChapterDir = "31489056e0916"
|
||||
hToSplit = "1a6562590ef19"
|
||||
hPartition = "41cfc0d1f2d12"
|
||||
hChapterOne = "2858dcd1057d3"
|
||||
hSceneOne = "2fca346db6561"
|
||||
hSceneTwo = "02d20bbd7e394"
|
||||
hSceneThree = "7688b6ef52555"
|
||||
hSceneFour = "c837649cce43f"
|
||||
hSceneFive = "6208ef0f7750c"
|
||||
hNovelRoot = "0000000000008"
|
||||
hChapterDir = "000000000000d"
|
||||
hToSplit = "0000000000010"
|
||||
hNewFolder = "0000000000021"
|
||||
hPartition = "0000000000022"
|
||||
hChapterOne = "0000000000023"
|
||||
hSceneOne = "0000000000024"
|
||||
hSceneTwo = "0000000000025"
|
||||
hSceneThree = "0000000000026"
|
||||
hSceneFour = "0000000000027"
|
||||
hSceneFive = "0000000000028"
|
||||
|
||||
# Add Project Content
|
||||
monkeypatch.setattr(GuiItemEditor, "exec_", lambda *a: QDialog.Accepted)
|
||||
nwGUI.switchFocus(nwWidget.TREE)
|
||||
nwGUI.treeView.clearSelection()
|
||||
nwGUI.treeView._getTreeItem(hNovelRoot).setSelected(True)
|
||||
nwGUI.treeView.newTreeItem(nwItemType.FILE, None)
|
||||
nwGUI.projView.projTree.clearSelection()
|
||||
nwGUI.projView.projTree._getTreeItem(hNovelRoot).setSelected(True)
|
||||
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE)
|
||||
|
||||
assert nwGUI.saveProject() is True
|
||||
assert nwGUI.closeProject() is True
|
||||
@@ -89,8 +89,8 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj):
|
||||
|
||||
# Open the Split tool
|
||||
nwGUI.switchFocus(nwWidget.TREE)
|
||||
nwGUI.treeView.clearSelection()
|
||||
nwGUI.treeView._getTreeItem(hToSplit).setSelected(True)
|
||||
nwGUI.projView.projTree.clearSelection()
|
||||
nwGUI.projView.projTree._getTreeItem(hToSplit).setSelected(True)
|
||||
|
||||
monkeypatch.setattr(GuiDocSplit, "exec_", lambda *a: None)
|
||||
nwGUI.mainMenu.aSplitDoc.activate(QAction.Trigger)
|
||||
@@ -109,7 +109,7 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj):
|
||||
|
||||
# No item selected
|
||||
nwSplit.sourceItem = None
|
||||
nwGUI.treeView.clearSelection()
|
||||
nwGUI.projView.projTree.clearSelection()
|
||||
assert nwSplit._populateList() is False
|
||||
assert nwSplit.listBox.count() == 0
|
||||
|
||||
@@ -117,15 +117,15 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj):
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(NWTree, "__getitem__", lambda *a: None)
|
||||
nwSplit.sourceItem = None
|
||||
nwGUI.treeView.clearSelection()
|
||||
nwGUI.treeView._getTreeItem(hToSplit).setSelected(True)
|
||||
nwGUI.projView.projTree.clearSelection()
|
||||
nwGUI.projView.projTree._getTreeItem(hToSplit).setSelected(True)
|
||||
assert nwSplit._populateList() is False
|
||||
assert nwSplit.listBox.count() == 0
|
||||
|
||||
# Select a non-file
|
||||
nwSplit.sourceItem = None
|
||||
nwGUI.treeView.clearSelection()
|
||||
nwGUI.treeView._getTreeItem(hChapterDir).setSelected(True)
|
||||
nwGUI.projView.projTree.clearSelection()
|
||||
nwGUI.projView.projTree._getTreeItem(hChapterDir).setSelected(True)
|
||||
assert nwSplit._populateList() is False
|
||||
assert nwSplit.listBox.count() == 0
|
||||
|
||||
@@ -173,52 +173,52 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj):
|
||||
|
||||
assert readFile(os.path.join(contentDir, hPartition+".nwd")) == (
|
||||
"%%%%~name: Nantucket\n"
|
||||
"%%%%~path: 031b4af5197ec/%s\n"
|
||||
"%%%%~path: %s/%s\n"
|
||||
"%%%%~kind: NOVEL/DOCUMENT\n"
|
||||
"%s\n\n"
|
||||
) % (hPartition, tPartition)
|
||||
) % (hNewFolder, hPartition, tPartition)
|
||||
|
||||
assert readFile(os.path.join(contentDir, hChapterOne+".nwd")) == (
|
||||
"%%%%~name: Chapter One\n"
|
||||
"%%%%~path: 031b4af5197ec/%s\n"
|
||||
"%%%%~path: %s/%s\n"
|
||||
"%%%%~kind: NOVEL/DOCUMENT\n"
|
||||
"%s\n\n"
|
||||
) % (hChapterOne, tChapterOne)
|
||||
) % (hNewFolder, hChapterOne, tChapterOne)
|
||||
|
||||
assert readFile(os.path.join(contentDir, hSceneOne+".nwd")) == (
|
||||
"%%%%~name: Scene One\n"
|
||||
"%%%%~path: 031b4af5197ec/%s\n"
|
||||
"%%%%~path: %s/%s\n"
|
||||
"%%%%~kind: NOVEL/DOCUMENT\n"
|
||||
"%s\n\n"
|
||||
) % (hSceneOne, tSceneOne)
|
||||
) % (hNewFolder, hSceneOne, tSceneOne)
|
||||
|
||||
assert readFile(os.path.join(contentDir, hSceneTwo+".nwd")) == (
|
||||
"%%%%~name: Scene Two\n"
|
||||
"%%%%~path: 031b4af5197ec/%s\n"
|
||||
"%%%%~path: %s/%s\n"
|
||||
"%%%%~kind: NOVEL/DOCUMENT\n"
|
||||
"%s\n\n"
|
||||
) % (hSceneTwo, tSceneTwo)
|
||||
) % (hNewFolder, hSceneTwo, tSceneTwo)
|
||||
|
||||
assert readFile(os.path.join(contentDir, hSceneThree+".nwd")) == (
|
||||
"%%%%~name: Scene Three\n"
|
||||
"%%%%~path: 031b4af5197ec/%s\n"
|
||||
"%%%%~path: %s/%s\n"
|
||||
"%%%%~kind: NOVEL/DOCUMENT\n"
|
||||
"%s\n\n"
|
||||
) % (hSceneThree, tSceneThree)
|
||||
) % (hNewFolder, hSceneThree, tSceneThree)
|
||||
|
||||
assert readFile(os.path.join(contentDir, hSceneFour+".nwd")) == (
|
||||
"%%%%~name: Scene Four\n"
|
||||
"%%%%~path: 031b4af5197ec/%s\n"
|
||||
"%%%%~path: %s/%s\n"
|
||||
"%%%%~kind: NOVEL/DOCUMENT\n"
|
||||
"%s\n\n"
|
||||
) % (hSceneFour, tSceneFour)
|
||||
) % (hNewFolder, hSceneFour, tSceneFour)
|
||||
|
||||
assert readFile(os.path.join(contentDir, hSceneFive+".nwd")) == (
|
||||
"%%%%~name: The End\n"
|
||||
"%%%%~path: 031b4af5197ec/%s\n"
|
||||
"%%%%~path: %s/%s\n"
|
||||
"%%%%~kind: NOVEL/DOCUMENT\n"
|
||||
"%s\n\n"
|
||||
) % (hSceneFive, tSceneFive)
|
||||
) % (hNewFolder, hSceneFive, tSceneFive)
|
||||
|
||||
# OS error
|
||||
with monkeypatch.context() as mp:
|
||||
@@ -230,12 +230,6 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj):
|
||||
mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No)
|
||||
assert nwSplit._doSplit() is False
|
||||
|
||||
# Block folder creation by returning that the folder has a depth
|
||||
# of 50 items in the tree
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(NWTree, "getItemPath", lambda *a: [""]*50)
|
||||
assert nwSplit._doSplit() is False
|
||||
|
||||
# Clear the list
|
||||
nwSplit.listBox.clear()
|
||||
assert nwSplit._doSplit() is False
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
"""
|
||||
novelWriter – Item Editor Dialog Class Tester
|
||||
=============================================
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2022, 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 pytest
|
||||
|
||||
from tools import getGuiItem
|
||||
|
||||
from PyQt5.QtWidgets import QAction, QDialog, QMessageBox
|
||||
|
||||
from novelwriter.gui import GuiProjectTree
|
||||
from novelwriter.enum import nwItemLayout, nwItemType
|
||||
from novelwriter.dialogs import GuiItemEditor
|
||||
from novelwriter.core.tree import NWTree
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testDlgItemEditor_Dialog(qtbot, monkeypatch, nwGUI, fncProj):
|
||||
"""Test launching the item editor dialog from GuiMain.
|
||||
"""
|
||||
# Block message box
|
||||
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
|
||||
|
||||
# Block Dialog exec_
|
||||
monkeypatch.setattr(GuiItemEditor, "exec_", lambda *a: None)
|
||||
|
||||
# Open Editor wo/Project
|
||||
assert nwGUI.editItem() is False
|
||||
|
||||
# Create and Open Project
|
||||
nwGUI.theProject.projTree.setSeed(42)
|
||||
assert nwGUI.newProject({"projPath": fncProj})
|
||||
|
||||
# No Selection
|
||||
nwGUI.treeView.clearSelection()
|
||||
assert nwGUI.editItem() is False
|
||||
|
||||
# Force opening from editor
|
||||
assert nwGUI.openDocument("0e17daca5f3e1")
|
||||
nwGUI.isFocusMode = True
|
||||
|
||||
# Block Tree Lookup
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(NWTree, "__getitem__", lambda *a: None)
|
||||
assert nwGUI.editItem() is False
|
||||
|
||||
# Invalid Type
|
||||
nwGUI.theProject.projTree["0e17daca5f3e1"]._type = nwItemType.NO_TYPE
|
||||
assert nwGUI.editItem() is False
|
||||
nwGUI.theProject.projTree["0e17daca5f3e1"]._type = nwItemType.FILE
|
||||
|
||||
# Open Properly
|
||||
assert nwGUI.editItem() is True
|
||||
qtbot.waitUntil(lambda: getGuiItem("GuiItemEditor") is not None, timeout=1000)
|
||||
itemEdit = getGuiItem("GuiItemEditor")
|
||||
assert itemEdit is not None
|
||||
itemEdit.close()
|
||||
|
||||
# Open Via Menu
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(GuiItemEditor, "result", lambda *a: QDialog.Accepted)
|
||||
nwGUI.mainMenu.aEditItem.activate(QAction.Trigger)
|
||||
qtbot.waitUntil(lambda: getGuiItem("GuiItemEditor") is not None, timeout=1000)
|
||||
itemEdit = getGuiItem("GuiItemEditor")
|
||||
assert itemEdit is not None
|
||||
itemEdit.close()
|
||||
|
||||
nwGUI.isFocusMode = False
|
||||
|
||||
# END Test testDlgItemEditor_Dialog
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testDlgItemEditor_Novel(qtbot, monkeypatch, nwGUI, fncProj):
|
||||
"""Test the item editor dialog for a novel document.
|
||||
"""
|
||||
# Block message box
|
||||
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
|
||||
|
||||
# Create Project and Open Document
|
||||
nwGUI.theProject.projTree.setSeed(42)
|
||||
assert nwGUI.newProject({"projPath": fncProj})
|
||||
assert nwGUI.openDocument("0e17daca5f3e1")
|
||||
|
||||
# Check that an invalid handle is managed
|
||||
itemEdit = GuiItemEditor(nwGUI, "whatever")
|
||||
itemEdit.show()
|
||||
itemEdit._doClose()
|
||||
|
||||
# Edit a Document
|
||||
itemEdit = GuiItemEditor(nwGUI, "0e17daca5f3e1")
|
||||
itemEdit.show()
|
||||
|
||||
# Check Existing Settings
|
||||
assert itemEdit.editName.text() == "New Scene"
|
||||
assert itemEdit.editStatus.currentData() == "New"
|
||||
assert itemEdit.editLayout.currentData() == nwItemLayout.DOCUMENT
|
||||
assert itemEdit.editExport.isChecked() is True
|
||||
|
||||
# Change Settings
|
||||
layoutIdx = itemEdit.editLayout.findData(nwItemLayout.NOTE)
|
||||
itemEdit.editName.setText("Great Scene")
|
||||
itemEdit.editStatus.setCurrentIndex(1)
|
||||
itemEdit.editLayout.setCurrentIndex(layoutIdx)
|
||||
itemEdit.editExport.setChecked(False)
|
||||
|
||||
# Check New Settings
|
||||
itemEdit._doSave()
|
||||
assert itemEdit.theItem.itemName == "Great Scene"
|
||||
assert itemEdit.theItem.itemStatus == "Note"
|
||||
assert itemEdit.theItem.itemLayout == nwItemLayout.NOTE
|
||||
assert itemEdit.theItem.isExported is False
|
||||
|
||||
# Check that the editor header is updated
|
||||
nwGUI.docEditor.updateDocInfo("0e17daca5f3e1")
|
||||
assert nwGUI.docEditor.docHeader.theTitle.text() == "Novel › New Chapter › Great Scene"
|
||||
|
||||
itemEdit.close()
|
||||
del itemEdit
|
||||
# qtbot.stopForInteraction()
|
||||
|
||||
# END Test testDlgItemEditor_Dialog
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testDlgItemEditor_Note(qtbot, monkeypatch, nwGUI, fncProj):
|
||||
"""Test the item editor dialog for a project note.
|
||||
"""
|
||||
# Block message box
|
||||
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
|
||||
|
||||
# Create Project and Open Document
|
||||
nwGUI.theProject.projTree.setSeed(42)
|
||||
assert nwGUI.newProject({"projPath": fncProj})
|
||||
|
||||
# Create Note
|
||||
nwGUI.treeView.clearSelection()
|
||||
nwGUI.treeView._getTreeItem("71ee45a3c0db9").setSelected(True)
|
||||
nwGUI.treeView.newTreeItem(nwItemType.FILE, None)
|
||||
|
||||
# Open Note
|
||||
assert nwGUI.openDocument("1a6562590ef19")
|
||||
|
||||
# Edit a Document
|
||||
itemEdit = GuiItemEditor(nwGUI, "1a6562590ef19")
|
||||
itemEdit.show()
|
||||
|
||||
# Check Existing Settings
|
||||
assert itemEdit.editName.text() == "New File"
|
||||
assert itemEdit.editStatus.currentData() == "New"
|
||||
assert itemEdit.editLayout.currentData() == nwItemLayout.NOTE
|
||||
assert itemEdit.editExport.isChecked() is True
|
||||
|
||||
# Change Settings
|
||||
itemEdit.editName.setText("New Character")
|
||||
itemEdit.editStatus.setCurrentIndex(1)
|
||||
itemEdit.editExport.setChecked(False)
|
||||
|
||||
# Check New Settings
|
||||
itemEdit._doSave()
|
||||
assert itemEdit.theItem.itemName == "New Character"
|
||||
assert itemEdit.theItem.itemStatus == "Minor"
|
||||
assert itemEdit.theItem.itemLayout == nwItemLayout.NOTE
|
||||
assert itemEdit.theItem.isExported is False
|
||||
|
||||
itemEdit.close()
|
||||
del itemEdit
|
||||
# qtbot.stopForInteraction()
|
||||
|
||||
# END Test testDlgItemEditor_Note
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testDlgItemEditor_Folder(qtbot, monkeypatch, nwGUI, fncProj):
|
||||
"""Test the item editor dialog for a folder.
|
||||
"""
|
||||
# Block message box
|
||||
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
|
||||
|
||||
# Create Project and Open Document
|
||||
nwGUI.theProject.projTree.setSeed(42)
|
||||
assert nwGUI.newProject({"projPath": fncProj})
|
||||
|
||||
# Edit a Folder
|
||||
itemEdit = GuiItemEditor(nwGUI, "31489056e0916")
|
||||
itemEdit.show()
|
||||
|
||||
# Check Existing Settings
|
||||
assert itemEdit.editName.text() == "New Chapter"
|
||||
assert itemEdit.editStatus.currentData() == "New"
|
||||
assert itemEdit.editLayout.currentData() == nwItemLayout.NO_LAYOUT
|
||||
assert itemEdit.editExport.isChecked() is False
|
||||
|
||||
assert itemEdit.editLayout.isEnabled() is False
|
||||
assert itemEdit.editExport.isEnabled() is False
|
||||
|
||||
# Change Settings
|
||||
itemEdit.editName.setText("Chapter One")
|
||||
itemEdit.editStatus.setCurrentIndex(1)
|
||||
|
||||
# Check New Settings
|
||||
itemEdit._doSave()
|
||||
assert itemEdit.theItem.itemName == "Chapter One"
|
||||
assert itemEdit.theItem.itemStatus == "Note"
|
||||
assert itemEdit.theItem.itemLayout == nwItemLayout.NO_LAYOUT
|
||||
assert itemEdit.theItem.isExported is False
|
||||
|
||||
itemEdit.close()
|
||||
del itemEdit
|
||||
# qtbot.stopForInteraction()
|
||||
|
||||
# END Test testDlgItemEditor_Folder
|
||||
@@ -239,8 +239,8 @@ def testDlgPreferences_Main(qtbot, monkeypatch, fncDir, outDir, refDir):
|
||||
copyfile(projFile, testFile)
|
||||
ignTuple = (
|
||||
"timestamp", "guifont", "lastnotes", "guilang", "geometry",
|
||||
"preferences", "treecols", "novelcols", "projcols", "mainpane",
|
||||
"docpane", "viewpane", "outlinepane", "textfont", "textsize"
|
||||
"preferences", "projcols", "mainpane", "docpane", "viewpane",
|
||||
"outlinepane", "textfont", "textsize"
|
||||
)
|
||||
assert cmpFiles(testFile, compFile, ignoreStart=ignTuple)
|
||||
|
||||
|
||||
@@ -42,7 +42,8 @@ def testDlgLoadProject_Main(qtbot, monkeypatch, nwGUI, nwMinimal):
|
||||
"""Test the load project wizard.
|
||||
"""
|
||||
# Block message box
|
||||
monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
|
||||
|
||||
assert nwGUI.openProject(nwMinimal)
|
||||
assert nwGUI.closeProject()
|
||||
|
||||
@@ -19,27 +19,29 @@ 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 pytest
|
||||
import os
|
||||
import pytest
|
||||
|
||||
from shutil import copyfile
|
||||
from tools import cmpFiles, getGuiItem
|
||||
from tools import cmpFiles, getGuiItem, buildTestProject
|
||||
|
||||
from PyQt5.QtGui import QColor
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import (
|
||||
QDialog, QAction, QMessageBox, QColorDialog, QTreeWidgetItem
|
||||
)
|
||||
from PyQt5.QtWidgets import QDialog, QAction, QMessageBox, QColorDialog
|
||||
|
||||
from novelwriter.dialogs import GuiProjectSettings
|
||||
|
||||
keyDelay = 2
|
||||
typeDelay = 1
|
||||
stepDelay = 20
|
||||
statusKeys = ["s000000", "s000001", "s000002", "s000003"]
|
||||
importKeys = ["i000004", "i000005", "i000006", "i000007"]
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj, outDir, refDir):
|
||||
def testDlgProjSettings_Dialog(
|
||||
qtbot, monkeypatch, nwGUI, fncDir, fncProj, outDir, refDir, mockRnd
|
||||
):
|
||||
"""Test the full project settings dialog.
|
||||
"""
|
||||
projFile = os.path.join(fncProj, "nwProject.nwx")
|
||||
@@ -55,8 +57,7 @@ def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj, outDi
|
||||
assert getGuiItem("GuiProjectSettings") is None
|
||||
|
||||
# Create new project
|
||||
nwGUI.theProject.projTree.setSeed(42)
|
||||
assert nwGUI.newProject({"projPath": fncProj})
|
||||
buildTestProject(nwGUI, fncProj)
|
||||
nwGUI.mainConf.backupPath = fncDir
|
||||
|
||||
nwGUI.theProject.setSpellLang("en")
|
||||
@@ -80,7 +81,7 @@ def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj, outDi
|
||||
# ============
|
||||
|
||||
assert projEdit.tabMain.editName.text() == "New Project"
|
||||
assert projEdit.tabMain.editTitle.text() == ""
|
||||
assert projEdit.tabMain.editTitle.text() == "New Novel"
|
||||
assert projEdit.tabMain.editAuthors.toPlainText() == "Jane Smith\nJohn Smith"
|
||||
assert projEdit.tabMain.spellLang.currentData() == "en"
|
||||
assert projEdit.tabMain.doBackup.isChecked() is False
|
||||
@@ -89,6 +90,7 @@ def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj, outDi
|
||||
projEdit.tabMain.editName.setText("")
|
||||
for c in "Project Name":
|
||||
qtbot.keyClick(projEdit.tabMain.editName, c, delay=typeDelay)
|
||||
projEdit.tabMain.editTitle.setText("")
|
||||
for c in "Project Title":
|
||||
qtbot.keyClick(projEdit.tabMain.editTitle, c, delay=typeDelay)
|
||||
|
||||
@@ -111,19 +113,9 @@ def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj, outDi
|
||||
projEdit._tabBox.setCurrentWidget(projEdit.tabStatus)
|
||||
|
||||
assert projEdit.tabStatus.colChanged is False
|
||||
assert projEdit.tabStatus.getNewList() is None
|
||||
assert projEdit.tabStatus.getNewList() == ([], [])
|
||||
assert projEdit.tabStatus.listBox.topLevelItemCount() == 4
|
||||
|
||||
# Fake drag'n'drop should change changed status
|
||||
projEdit.tabStatus._rowsMoved()
|
||||
assert projEdit.tabStatus.colChanged is True
|
||||
projEdit.tabStatus.colChanged = False
|
||||
|
||||
projEdit.tabStatus.listBox.clearSelection()
|
||||
assert projEdit.tabStatus._getSelectedItem() is None
|
||||
projEdit.tabStatus.listBox.topLevelItem(0).setSelected(True)
|
||||
assert isinstance(projEdit.tabStatus._getSelectedItem(), QTreeWidgetItem)
|
||||
|
||||
# Can't delete the first item (it's in use)
|
||||
projEdit.tabStatus.listBox.clearSelection()
|
||||
projEdit.tabStatus.listBox.topLevelItem(0).setSelected(True)
|
||||
@@ -150,11 +142,53 @@ def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj, outDi
|
||||
qtbot.wait(stepDelay)
|
||||
|
||||
assert projEdit.tabStatus.colChanged is True
|
||||
assert projEdit.tabStatus.getNewList() == [
|
||||
("New", 100, 100, 100, "New"),
|
||||
("Note", 200, 50, 0, "Note"),
|
||||
("Finished", 50, 200, 0, "Finished"),
|
||||
("Final", 20, 30, 40, None)
|
||||
assert projEdit.tabStatus.getNewList() == (
|
||||
[
|
||||
{
|
||||
"key": statusKeys[0],
|
||||
"name": "New",
|
||||
"cols": (100, 100, 100)
|
||||
}, {
|
||||
"key": statusKeys[1],
|
||||
"name": "Note",
|
||||
"cols": (200, 50, 0)
|
||||
}, {
|
||||
"key": statusKeys[3],
|
||||
"name": "Finished",
|
||||
"cols": (50, 200, 0)
|
||||
}, {
|
||||
"key": None,
|
||||
"name": "Final",
|
||||
"cols": (20, 30, 40)
|
||||
}
|
||||
], [
|
||||
statusKeys[2] # Deleted item
|
||||
]
|
||||
)
|
||||
|
||||
# Move items
|
||||
projEdit.tabStatus.listBox.clearSelection()
|
||||
projEdit.tabStatus._moveItem(1)
|
||||
assert [x["key"] for x in projEdit.tabStatus.getNewList()[0]] == [
|
||||
statusKeys[0], statusKeys[1], statusKeys[3], None
|
||||
]
|
||||
|
||||
projEdit.tabStatus.listBox.clearSelection()
|
||||
projEdit.tabStatus.listBox.topLevelItem(0).setSelected(True)
|
||||
projEdit.tabStatus._moveItem(-1)
|
||||
assert [x["key"] for x in projEdit.tabStatus.getNewList()[0]] == [
|
||||
statusKeys[0], statusKeys[1], statusKeys[3], None
|
||||
]
|
||||
|
||||
projEdit.tabStatus.listBox.clearSelection()
|
||||
projEdit.tabStatus.listBox.topLevelItem(3).setSelected(True)
|
||||
projEdit.tabStatus._moveItem(-1)
|
||||
assert [x["key"] for x in projEdit.tabStatus.getNewList()[0]] == [
|
||||
statusKeys[0], statusKeys[1], None, statusKeys[3]
|
||||
]
|
||||
projEdit.tabStatus._moveItem(1)
|
||||
assert [x["key"] for x in projEdit.tabStatus.getNewList()[0]] == [
|
||||
statusKeys[0], statusKeys[1], statusKeys[3], None
|
||||
]
|
||||
|
||||
# Importance Tab
|
||||
|
||||
@@ -40,6 +40,7 @@ stepDelay = 20
|
||||
def testDlgWordList_Dialog(qtbot, monkeypatch, nwGUI, nwMinimal):
|
||||
"""test the word list editor.
|
||||
"""
|
||||
monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(GuiWordList, "exec_", lambda *a: None)
|
||||
|
||||
@@ -29,7 +29,7 @@ from PyQt5.QtWidgets import QAction, QMessageBox, qApp
|
||||
|
||||
from novelwriter.gui.doceditor import GuiDocEditor
|
||||
from novelwriter.core import countWords
|
||||
from novelwriter.enum import nwDocAction, nwDocInsert, nwItemClass, nwItemLayout
|
||||
from novelwriter.enum import nwDocAction, nwDocInsert, nwItemLayout
|
||||
from novelwriter.constants import nwKeyWords, nwUnicode
|
||||
|
||||
keyDelay = 2
|
||||
@@ -43,6 +43,7 @@ def testGuiEditor_Init(qtbot, monkeypatch, nwGUI, nwMinimal, ipsumText):
|
||||
"""
|
||||
# Block message box
|
||||
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
|
||||
|
||||
# Open project
|
||||
assert nwGUI.openProject(nwMinimal)
|
||||
@@ -184,10 +185,10 @@ def testGuiEditor_SaveText(qtbot, monkeypatch, caplog, nwGUI, nwMinimal, ipsumTe
|
||||
assert "Could not save document." in caplog.text
|
||||
|
||||
# Change header level
|
||||
assert nwGUI.theProject.projTree[sHandle].itemLayout == nwItemLayout.DOCUMENT
|
||||
assert nwGUI.theProject.tree[sHandle].itemLayout == nwItemLayout.DOCUMENT
|
||||
nwGUI.docEditor.replaceText(longText[1:])
|
||||
assert nwGUI.docEditor.saveText() is True
|
||||
assert nwGUI.theProject.projTree[sHandle].itemLayout == nwItemLayout.DOCUMENT
|
||||
assert nwGUI.theProject.tree[sHandle].itemLayout == nwItemLayout.DOCUMENT
|
||||
|
||||
# Regular save
|
||||
assert nwGUI.docEditor.saveText() is True
|
||||
@@ -235,9 +236,9 @@ def testGuiEditor_MetaData(qtbot, monkeypatch, nwGUI, nwMinimal):
|
||||
assert nwGUI.docEditor.setCursorPosition(None) is False
|
||||
assert nwGUI.docEditor.setCursorPosition(10) is True
|
||||
assert nwGUI.docEditor.getCursorPosition() == 10
|
||||
assert nwGUI.theProject.projTree[sHandle].cursorPos != 10
|
||||
assert nwGUI.theProject.tree[sHandle].cursorPos != 10
|
||||
nwGUI.docEditor.saveCursorPosition()
|
||||
assert nwGUI.theProject.projTree[sHandle].cursorPos == 10
|
||||
assert nwGUI.theProject.tree[sHandle].cursorPos == 10
|
||||
|
||||
assert nwGUI.docEditor.setCursorLine(None) is False
|
||||
assert nwGUI.docEditor.setCursorLine(2) is True
|
||||
@@ -1142,11 +1143,11 @@ def testGuiEditor_Tags(qtbot, monkeypatch, nwGUI, nwMinimal, ipsumText):
|
||||
|
||||
# Create Character
|
||||
theText = "### Jane Doe\n\n@tag: Jane\n\n" + ipsumText[1] + "\n\n"
|
||||
cHandle = nwGUI.theProject.newFile("Jane Doe", nwItemClass.CHARACTER, "afb3043c7b2b3")
|
||||
cHandle = nwGUI.theProject.newFile("Jane Doe", "afb3043c7b2b3")
|
||||
assert nwGUI.openDocument(cHandle) is True
|
||||
assert nwGUI.docEditor.replaceText(theText) is True
|
||||
assert nwGUI.saveDocument() is True
|
||||
assert nwGUI.treeView.revealNewTreeItem(cHandle)
|
||||
assert nwGUI.projView.revealNewTreeItem(cHandle)
|
||||
nwGUI.docEditor.updateTagHighLighting()
|
||||
|
||||
# Follow Tag
|
||||
@@ -1225,8 +1226,8 @@ def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, nwMinimal, ipsumText):
|
||||
|
||||
# Open a document and populate it
|
||||
sHandle = "8c659a11cd429"
|
||||
nwGUI.theProject.projTree[sHandle]._initCount = 0 # Clear item's count
|
||||
nwGUI.theProject.projTree[sHandle]._wordCount = 0 # Clear item's count
|
||||
nwGUI.theProject.tree[sHandle]._initCount = 0 # Clear item's count
|
||||
nwGUI.theProject.tree[sHandle]._wordCount = 0 # Clear item's count
|
||||
assert nwGUI.openDocument(sHandle) is True
|
||||
qtbot.wait(stepDelay)
|
||||
|
||||
@@ -1252,9 +1253,9 @@ def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, nwMinimal, ipsumText):
|
||||
nwGUI.docEditor.wCounterDoc.run()
|
||||
# nwGUI.docEditor._updateDocCounts(cC, wC, pC)
|
||||
qtbot.wait(stepDelay)
|
||||
assert nwGUI.theProject.projTree[sHandle]._charCount == cC
|
||||
assert nwGUI.theProject.projTree[sHandle]._wordCount == wC
|
||||
assert nwGUI.theProject.projTree[sHandle]._paraCount == pC
|
||||
assert nwGUI.theProject.tree[sHandle]._charCount == cC
|
||||
assert nwGUI.theProject.tree[sHandle]._wordCount == wC
|
||||
assert nwGUI.theProject.tree[sHandle]._paraCount == pC
|
||||
assert nwGUI.docEditor.docFooter.wordsText.text() == f"Words: {wC} (+{wC})"
|
||||
|
||||
# Select all text
|
||||
@@ -1285,7 +1286,6 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, nwLipsum):
|
||||
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(GuiDocEditor, "hasFocus", lambda *a: True)
|
||||
|
||||
nwGUI.theProject.projTree.setSeed(42)
|
||||
assert nwGUI.openProject(nwLipsum) is True
|
||||
assert nwGUI.openDocument("4c4f28287af27") is True
|
||||
origText = nwGUI.docEditor.getText()
|
||||
|
||||
@@ -43,21 +43,20 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, nwLipsum):
|
||||
monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
|
||||
|
||||
# Open project
|
||||
nwGUI.theProject.projTree.setSeed(42)
|
||||
assert nwGUI.openProject(nwLipsum)
|
||||
|
||||
# Rebuild the index
|
||||
nwGUI.mainMenu.aRebuildIndex.activate(QAction.Trigger)
|
||||
assert nwGUI.theIndex._tagIndex != {}
|
||||
assert nwGUI.theIndex._refIndex != {}
|
||||
assert nwGUI.theProject.index._tagsIndex._tags != {}
|
||||
assert nwGUI.theProject.index._itemIndex._items != {}
|
||||
|
||||
# Select a document in the project tree
|
||||
nwGUI.treeView.setSelectedHandle("88243afbe5ed8")
|
||||
nwGUI.projView.setSelectedHandle("88243afbe5ed8")
|
||||
|
||||
# Middle-click the selected item
|
||||
theItem = nwGUI.treeView._getTreeItem("88243afbe5ed8")
|
||||
theRect = nwGUI.treeView.visualItemRect(theItem)
|
||||
qtbot.mouseClick(nwGUI.treeView.viewport(), Qt.MidButton, pos=theRect.center())
|
||||
theItem = nwGUI.projView.projTree._getTreeItem("88243afbe5ed8")
|
||||
theRect = nwGUI.projView.projTree.visualItemRect(theItem)
|
||||
qtbot.mouseClick(nwGUI.projView.projTree.viewport(), Qt.MidButton, pos=theRect.center())
|
||||
assert nwGUI.docViewer.docHandle() == "88243afbe5ed8"
|
||||
|
||||
# Reload the text
|
||||
@@ -118,7 +117,7 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, nwLipsum):
|
||||
assert nwGUI.docViewer.docAction(nwDocAction.COPY) is False
|
||||
|
||||
# Open again via menu
|
||||
assert nwGUI.treeView.setSelectedHandle("88243afbe5ed8")
|
||||
assert nwGUI.projView.setSelectedHandle("88243afbe5ed8")
|
||||
nwGUI.mainMenu.aViewDoc.activate(QAction.Trigger)
|
||||
|
||||
# Select "Bod" link
|
||||
@@ -141,7 +140,7 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, nwLipsum):
|
||||
nwGUI.docViewer.reloadText()
|
||||
|
||||
# Change document title
|
||||
nwItem = nwGUI.theProject.projTree["4c4f28287af27"]
|
||||
nwItem = nwGUI.theProject.tree["4c4f28287af27"]
|
||||
nwItem.setName("Test Title")
|
||||
assert nwItem.itemName == "Test Title"
|
||||
nwGUI.docViewer.updateDocInfo("4c4f28287af27")
|
||||
|
||||
@@ -19,20 +19,20 @@ 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 pytest
|
||||
import os
|
||||
import pytest
|
||||
|
||||
from shutil import copyfile
|
||||
from tools import cmpFiles
|
||||
from tools import cmpFiles, buildTestProject, XML_IGNORE, writeFile
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QMessageBox, QDialog
|
||||
from PyQt5.QtWidgets import QMessageBox, QInputDialog
|
||||
|
||||
from novelwriter.gui import (
|
||||
GuiDocEditor, GuiProjectTree, GuiNovelTree, GuiOutline
|
||||
)
|
||||
from novelwriter.enum import nwItemType, nwWidget
|
||||
from novelwriter.dialogs.itemeditor import GuiItemEditor
|
||||
from novelwriter.gui import GuiDocEditor, GuiNovelView, GuiOutlineView
|
||||
from novelwriter.enum import nwItemType, nwView, nwWidget
|
||||
from novelwriter.tools import GuiProjectWizard
|
||||
from novelwriter.gui.projtree import GuiProjectTree
|
||||
from novelwriter.dialogs import GuiEditLabel
|
||||
|
||||
keyDelay = 2
|
||||
typeDelay = 1
|
||||
@@ -57,10 +57,9 @@ def testGuiMain_ProjectBlocker(monkeypatch, nwGUI):
|
||||
assert nwGUI.mergeDocuments() is False
|
||||
assert nwGUI.splitDocument() is False
|
||||
assert nwGUI.openSelectedItem() is False
|
||||
assert nwGUI.editItem() is False
|
||||
assert nwGUI.editItemLabel() is False
|
||||
assert nwGUI.requestNovelTreeRefresh() is False
|
||||
assert nwGUI.rebuildIndex() is False
|
||||
assert nwGUI.rebuildOutline() is False
|
||||
assert nwGUI.showProjectSettingsDialog() is False
|
||||
assert nwGUI.showProjectDetailsDialog() is False
|
||||
assert nwGUI.showBuildProjectDialog() is False
|
||||
@@ -71,83 +70,117 @@ def testGuiMain_ProjectBlocker(monkeypatch, nwGUI):
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, fncProj):
|
||||
def testGuiMain_NewProject(monkeypatch, nwGUI, fncProj):
|
||||
"""Test creating a new project.
|
||||
"""
|
||||
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes)
|
||||
|
||||
# No data
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(GuiProjectWizard, "exec_", lambda *a: None)
|
||||
assert nwGUI.newProject(projData=None) is False
|
||||
|
||||
# Close project
|
||||
with monkeypatch.context() as mp:
|
||||
nwGUI.hasProject = True
|
||||
mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No)
|
||||
assert nwGUI.newProject(projData={"projPath": fncProj}) is False
|
||||
|
||||
# No project path
|
||||
assert nwGUI.newProject(projData={}) is False
|
||||
|
||||
# Project file already exists
|
||||
projFile = os.path.join(fncProj, nwGUI.theProject.projFile)
|
||||
writeFile(projFile, "Stuff")
|
||||
assert nwGUI.newProject(projData={"projPath": fncProj}) is False
|
||||
os.unlink(projFile)
|
||||
|
||||
# An unreachable path should also fail
|
||||
projPath = os.path.join(fncProj, "stuff", "stuff", "stuff")
|
||||
assert nwGUI.newProject(projData={"projPath": projPath}) is False
|
||||
|
||||
# This one should work just fine
|
||||
assert nwGUI.newProject(projData={"projPath": fncProj}) is True
|
||||
assert os.path.isfile(os.path.join(fncProj, nwGUI.theProject.projFile))
|
||||
assert os.path.isdir(os.path.join(fncProj, "content"))
|
||||
|
||||
# END Test testGuiMain_NewProject
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
|
||||
"""Test handling of project tree items based on GUI focus states.
|
||||
"""
|
||||
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
|
||||
|
||||
nwGUI.theProject.projTree.setSeed(42)
|
||||
assert nwGUI.newProject({"projPath": fncProj}) is True
|
||||
assert nwGUI.saveProject() is True
|
||||
buildTestProject(nwGUI, fncProj)
|
||||
|
||||
sHandle = "0e17daca5f3e1"
|
||||
sHandle = "000000000000f"
|
||||
assert nwGUI.openSelectedItem() is False
|
||||
|
||||
# Project Tree has focus
|
||||
nwGUI._changeView(nwView.PROJECT)
|
||||
nwGUI.switchFocus(nwWidget.TREE)
|
||||
nwGUI.projTabs.setCurrentIndex(0)
|
||||
nwGUI.projStack.setCurrentIndex(0)
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
|
||||
assert nwGUI.docEditor.docHandle() is None
|
||||
nwGUI.treeView._getTreeItem(sHandle).setSelected(True)
|
||||
nwGUI.projView.projTree._getTreeItem(sHandle).setSelected(True)
|
||||
nwGUI._keyPressReturn()
|
||||
assert nwGUI.docEditor.docHandle() == sHandle
|
||||
assert nwGUI.closeDocument() is True
|
||||
|
||||
# Novel Tree has focus
|
||||
nwGUI.projTabs.setCurrentIndex(1)
|
||||
nwGUI.novelView.refreshTree(True)
|
||||
nwGUI._changeView(nwView.NOVEL)
|
||||
nwGUI.novelView.novelTree.refreshTree(rootHandle=None, overRide=True)
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(GuiNovelTree, "hasFocus", lambda *a: True)
|
||||
mp.setattr(GuiNovelView, "treeHasFocus", lambda *a: True)
|
||||
assert nwGUI.docEditor.docHandle() is None
|
||||
actItem = nwGUI.novelView.topLevelItem(0)
|
||||
chpItem = actItem.child(0)
|
||||
selItem = chpItem.child(0)
|
||||
nwGUI.novelView.setCurrentItem(selItem)
|
||||
selItem = nwGUI.novelView.novelTree.topLevelItem(2)
|
||||
nwGUI.novelView.novelTree.setCurrentItem(selItem)
|
||||
nwGUI._keyPressReturn()
|
||||
assert nwGUI.docEditor.docHandle() == sHandle
|
||||
assert nwGUI.closeDocument() is True
|
||||
|
||||
# Project Outline has focus
|
||||
nwGUI._changeView(nwView.OUTLINE)
|
||||
nwGUI.switchFocus(nwWidget.OUTLINE)
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(GuiOutline, "hasFocus", lambda *a: True)
|
||||
mp.setattr(GuiOutlineView, "treeHasFocus", lambda *a: True)
|
||||
assert nwGUI.docEditor.docHandle() is None
|
||||
actItem = nwGUI.projView.topLevelItem(0)
|
||||
chpItem = actItem.child(0)
|
||||
selItem = chpItem.child(0)
|
||||
nwGUI.projView.setCurrentItem(selItem)
|
||||
selItem = nwGUI.outlineView.outlineTree.topLevelItem(2)
|
||||
nwGUI.outlineView.outlineTree.setCurrentItem(selItem)
|
||||
nwGUI._keyPressReturn()
|
||||
assert nwGUI.docEditor.docHandle() == sHandle
|
||||
assert nwGUI.closeDocument() is True
|
||||
|
||||
# qtbot.stopForInteraction()
|
||||
# qtbot.stop()
|
||||
|
||||
# END Test testGuiMain_ProjectTreeItems
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir):
|
||||
def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mockRnd):
|
||||
"""Test the document editor.
|
||||
"""
|
||||
# Block message box
|
||||
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(GuiItemEditor, "exec_", lambda *a: None)
|
||||
monkeypatch.setattr(GuiItemEditor, "result", lambda *a: QDialog.Accepted)
|
||||
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
|
||||
monkeypatch.setattr(GuiDocEditor, "hasFocus", lambda *a: True)
|
||||
monkeypatch.setattr(QInputDialog, "getText", lambda *a, text: (text, True))
|
||||
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
|
||||
|
||||
# Create new, save, close project
|
||||
nwGUI.theProject.projTree.setSeed(42)
|
||||
assert nwGUI.newProject({"projPath": fncProj})
|
||||
buildTestProject(nwGUI, fncProj)
|
||||
assert nwGUI.saveProject()
|
||||
assert nwGUI.closeProject()
|
||||
|
||||
assert len(nwGUI.theProject.projTree) == 0
|
||||
assert len(nwGUI.theProject.projTree._treeOrder) == 0
|
||||
assert len(nwGUI.theProject.projTree._treeRoots) == 0
|
||||
assert nwGUI.theProject.projTree.trashRoot() is None
|
||||
assert len(nwGUI.theProject.tree) == 0
|
||||
assert len(nwGUI.theProject.tree._treeOrder) == 0
|
||||
assert len(nwGUI.theProject.tree._treeRoots) == 0
|
||||
assert nwGUI.theProject.tree.trashRoot() is None
|
||||
assert nwGUI.theProject.projPath is None
|
||||
assert nwGUI.theProject.projMeta is None
|
||||
assert nwGUI.theProject.projFile == "nwProject.nwx"
|
||||
@@ -161,7 +194,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir):
|
||||
testFile = os.path.join(outDir, "guiEditor_Main_Initial_nwProject.nwx")
|
||||
compFile = os.path.join(refDir, "guiEditor_Main_Initial_nwProject.nwx")
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, compFile, [2, 6, 7, 8])
|
||||
assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
|
||||
qtbot.wait(stepDelay)
|
||||
|
||||
# qtbot.stopForInteraction()
|
||||
@@ -171,27 +204,27 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir):
|
||||
qtbot.wait(stepDelay)
|
||||
|
||||
# Check that we loaded the data
|
||||
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 len(nwGUI.theProject.tree) == 8
|
||||
assert len(nwGUI.theProject.tree._treeOrder) == 8
|
||||
assert len(nwGUI.theProject.tree._treeRoots) == 4
|
||||
assert nwGUI.theProject.tree.trashRoot() is None
|
||||
assert nwGUI.theProject.projPath == fncProj
|
||||
assert nwGUI.theProject.projMeta == os.path.join(fncProj, "meta")
|
||||
assert nwGUI.theProject.projFile == "nwProject.nwx"
|
||||
assert nwGUI.theProject.projName == "New Project"
|
||||
assert nwGUI.theProject.bookTitle == ""
|
||||
assert len(nwGUI.theProject.bookAuthors) == 0
|
||||
assert not nwGUI.theProject.spellCheck
|
||||
assert nwGUI.theProject.bookTitle == "New Novel"
|
||||
assert len(nwGUI.theProject.bookAuthors) == 1
|
||||
assert nwGUI.theProject.spellCheck is False
|
||||
|
||||
# Check that tree items have been created
|
||||
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
|
||||
assert nwGUI.projView.projTree._getTreeItem("0000000000008") is not None
|
||||
assert nwGUI.projView.projTree._getTreeItem("0000000000009") is not None
|
||||
assert nwGUI.projView.projTree._getTreeItem("000000000000a") is not None
|
||||
assert nwGUI.projView.projTree._getTreeItem("000000000000b") is not None
|
||||
assert nwGUI.projView.projTree._getTreeItem("000000000000c") is not None
|
||||
assert nwGUI.projView.projTree._getTreeItem("000000000000d") is not None
|
||||
assert nwGUI.projView.projTree._getTreeItem("000000000000e") is not None
|
||||
assert nwGUI.projView.projTree._getTreeItem("000000000000f") is not None
|
||||
|
||||
nwGUI.mainMenu.aSpellCheck.setChecked(True)
|
||||
assert nwGUI.mainMenu._toggleSpellCheck()
|
||||
@@ -204,9 +237,9 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir):
|
||||
|
||||
# Add a Character File
|
||||
nwGUI.switchFocus(nwWidget.TREE)
|
||||
nwGUI.treeView.clearSelection()
|
||||
nwGUI.treeView._getTreeItem("71ee45a3c0db9").setSelected(True)
|
||||
nwGUI.treeView.newTreeItem(nwItemType.FILE, None)
|
||||
nwGUI.projView.projTree.clearSelection()
|
||||
nwGUI.projView.projTree._getTreeItem("000000000000a").setSelected(True)
|
||||
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True)
|
||||
assert nwGUI.openSelectedItem()
|
||||
|
||||
# Type something into the document
|
||||
@@ -226,9 +259,9 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir):
|
||||
|
||||
# Add a Plot File
|
||||
nwGUI.switchFocus(nwWidget.TREE)
|
||||
nwGUI.treeView.clearSelection()
|
||||
nwGUI.treeView._getTreeItem("44cb730c42048").setSelected(True)
|
||||
nwGUI.treeView.newTreeItem(nwItemType.FILE, None)
|
||||
nwGUI.projView.projTree.clearSelection()
|
||||
nwGUI.projView.projTree._getTreeItem("0000000000009").setSelected(True)
|
||||
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True)
|
||||
assert nwGUI.openSelectedItem()
|
||||
|
||||
# Type something into the document
|
||||
@@ -248,9 +281,9 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir):
|
||||
|
||||
# Add a World File
|
||||
nwGUI.switchFocus(nwWidget.TREE)
|
||||
nwGUI.treeView.clearSelection()
|
||||
nwGUI.treeView._getTreeItem("811786ad1ae74").setSelected(True)
|
||||
nwGUI.treeView.newTreeItem(nwItemType.FILE, None)
|
||||
nwGUI.projView.projTree.clearSelection()
|
||||
nwGUI.projView.projTree._getTreeItem("000000000000b").setSelected(True)
|
||||
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True)
|
||||
assert nwGUI.openSelectedItem()
|
||||
|
||||
# Add Some Text
|
||||
@@ -279,10 +312,10 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir):
|
||||
|
||||
# Select the 'New Scene' file
|
||||
nwGUI.switchFocus(nwWidget.TREE)
|
||||
nwGUI.treeView.clearSelection()
|
||||
nwGUI.treeView._getTreeItem("73475cb40a568").setExpanded(True)
|
||||
nwGUI.treeView._getTreeItem("31489056e0916").setExpanded(True)
|
||||
nwGUI.treeView._getTreeItem("0e17daca5f3e1").setSelected(True)
|
||||
nwGUI.projView.projTree.clearSelection()
|
||||
nwGUI.projView.projTree._getTreeItem("0000000000008").setExpanded(True)
|
||||
nwGUI.projView.projTree._getTreeItem("000000000000d").setExpanded(True)
|
||||
nwGUI.projView.projTree._getTreeItem("000000000000f").setSelected(True)
|
||||
assert nwGUI.openSelectedItem()
|
||||
|
||||
# Type something into the document
|
||||
@@ -455,21 +488,21 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir):
|
||||
|
||||
# Open and view the edited document
|
||||
nwGUI.switchFocus(nwWidget.VIEWER)
|
||||
assert nwGUI.openDocument("0e17daca5f3e1")
|
||||
assert nwGUI.viewDocument("0e17daca5f3e1")
|
||||
assert nwGUI.openDocument("000000000000f")
|
||||
assert nwGUI.viewDocument("000000000000f")
|
||||
qtbot.wait(stepDelay)
|
||||
assert nwGUI.saveProject()
|
||||
assert nwGUI.closeDocViewer()
|
||||
qtbot.wait(stepDelay)
|
||||
|
||||
# Check a Quick Create and Delete
|
||||
assert nwGUI.treeView.newTreeItem(nwItemType.FILE, None)
|
||||
newHandle = nwGUI.treeView.getSelectedHandle()
|
||||
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.projView.projTree.newTreeItem(nwItemType.FILE, None)
|
||||
newHandle = nwGUI.projView.getSelectedHandle()
|
||||
assert nwGUI.theProject.tree["0000000000020"] is not None
|
||||
assert nwGUI.projView.deleteItem()
|
||||
assert nwGUI.projView.setSelectedHandle(newHandle)
|
||||
assert nwGUI.projView.deleteItem()
|
||||
assert nwGUI.theProject.tree["0000000000024"] is not None # Trash
|
||||
assert nwGUI.saveProject()
|
||||
|
||||
# Check the files
|
||||
@@ -477,32 +510,81 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir):
|
||||
testFile = os.path.join(outDir, "guiEditor_Main_Final_nwProject.nwx")
|
||||
compFile = os.path.join(refDir, "guiEditor_Main_Final_nwProject.nwx")
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, compFile, [2, 6, 7, 8, 13])
|
||||
assert cmpFiles(testFile, compFile, ignoreStart=(*XML_IGNORE, "<spellCheck"))
|
||||
|
||||
projFile = os.path.join(fncProj, "content", "031b4af5197ec.nwd")
|
||||
testFile = os.path.join(outDir, "guiEditor_Main_Final_031b4af5197ec.nwd")
|
||||
compFile = os.path.join(refDir, "guiEditor_Main_Final_031b4af5197ec.nwd")
|
||||
projFile = os.path.join(fncProj, "content", "000000000000f.nwd")
|
||||
testFile = os.path.join(outDir, "guiEditor_Main_Final_000000000000f.nwd")
|
||||
compFile = os.path.join(refDir, "guiEditor_Main_Final_000000000000f.nwd")
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, compFile)
|
||||
|
||||
projFile = os.path.join(fncProj, "content", "1a6562590ef19.nwd")
|
||||
testFile = os.path.join(outDir, "guiEditor_Main_Final_1a6562590ef19.nwd")
|
||||
compFile = os.path.join(refDir, "guiEditor_Main_Final_1a6562590ef19.nwd")
|
||||
projFile = os.path.join(fncProj, "content", "0000000000020.nwd")
|
||||
testFile = os.path.join(outDir, "guiEditor_Main_Final_0000000000020.nwd")
|
||||
compFile = os.path.join(refDir, "guiEditor_Main_Final_0000000000020.nwd")
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, compFile)
|
||||
|
||||
projFile = os.path.join(fncProj, "content", "0e17daca5f3e1.nwd")
|
||||
testFile = os.path.join(outDir, "guiEditor_Main_Final_0e17daca5f3e1.nwd")
|
||||
compFile = os.path.join(refDir, "guiEditor_Main_Final_0e17daca5f3e1.nwd")
|
||||
projFile = os.path.join(fncProj, "content", "0000000000021.nwd")
|
||||
testFile = os.path.join(outDir, "guiEditor_Main_Final_0000000000021.nwd")
|
||||
compFile = os.path.join(refDir, "guiEditor_Main_Final_0000000000021.nwd")
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, compFile)
|
||||
|
||||
projFile = os.path.join(fncProj, "content", "41cfc0d1f2d12.nwd")
|
||||
testFile = os.path.join(outDir, "guiEditor_Main_Final_41cfc0d1f2d12.nwd")
|
||||
compFile = os.path.join(refDir, "guiEditor_Main_Final_41cfc0d1f2d12.nwd")
|
||||
projFile = os.path.join(fncProj, "content", "0000000000022.nwd")
|
||||
testFile = os.path.join(outDir, "guiEditor_Main_Final_0000000000022.nwd")
|
||||
compFile = os.path.join(refDir, "guiEditor_Main_Final_0000000000022.nwd")
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, compFile)
|
||||
|
||||
# qtbot.stopForInteraction()
|
||||
|
||||
# END Test testGuiMain_Editing
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testGuiMain_FocusFullMode(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
|
||||
"""Test toggling focus mode in main window.
|
||||
"""
|
||||
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
|
||||
|
||||
buildTestProject(nwGUI, fncProj)
|
||||
assert nwGUI.isFocusMode is False
|
||||
|
||||
# Focus Mode
|
||||
# ==========
|
||||
|
||||
# No document open, so not allowing focus mode
|
||||
assert nwGUI.toggleFocusMode() is False
|
||||
|
||||
# Open a file in editor and viewer
|
||||
assert nwGUI.openDocument("000000000000f")
|
||||
assert nwGUI.viewDocument("000000000000f")
|
||||
|
||||
# Enable focus mode
|
||||
assert nwGUI.toggleFocusMode() is True
|
||||
assert nwGUI.treePane.isVisible() is False
|
||||
assert nwGUI.statusBar.isVisible() is False
|
||||
assert nwGUI.mainMenu.isVisible() is False
|
||||
assert nwGUI.viewsBar.isVisible() is False
|
||||
assert nwGUI.splitView.isVisible() is False
|
||||
|
||||
# Disable focus mode
|
||||
assert nwGUI.toggleFocusMode() is True
|
||||
assert nwGUI.treePane.isVisible() is True
|
||||
assert nwGUI.statusBar.isVisible() is True
|
||||
assert nwGUI.mainMenu.isVisible() is True
|
||||
assert nwGUI.viewsBar.isVisible() is True
|
||||
assert nwGUI.splitView.isVisible() is True
|
||||
|
||||
# Full Screen Mode
|
||||
# ================
|
||||
|
||||
assert nwGUI.mainConf.isFullScreen is False
|
||||
nwGUI.toggleFullScreenMode()
|
||||
assert nwGUI.mainConf.isFullScreen is True
|
||||
nwGUI.toggleFullScreenMode()
|
||||
assert nwGUI.mainConf.isFullScreen is False
|
||||
|
||||
# qtbot.stopForInteraction()
|
||||
|
||||
# END Test testGuiMain_FocusFullMode
|
||||
|
||||
@@ -26,7 +26,7 @@ from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QTextCursor, QTextBlock
|
||||
from PyQt5.QtWidgets import QAction, QFileDialog, QMessageBox
|
||||
|
||||
from tools import writeFile
|
||||
from tools import writeFile, buildTestProject
|
||||
|
||||
from novelwriter.gui.doceditor import GuiDocEditor
|
||||
from novelwriter.enum import nwDocAction, nwDocInsert
|
||||
@@ -48,7 +48,6 @@ def testGuiMenu_EditFormat(qtbot, monkeypatch, nwGUI, nwLipsum):
|
||||
# Test Document Action with No Project
|
||||
assert nwGUI.docEditor.docAction(nwDocAction.COPY) is False
|
||||
|
||||
nwGUI.theProject.projTree.setSeed(42)
|
||||
assert nwGUI.openProject(nwLipsum) is True
|
||||
qtbot.wait(stepDelay)
|
||||
|
||||
@@ -381,7 +380,6 @@ def testGuiMenu_ContextMenus(qtbot, monkeypatch, nwGUI, nwLipsum):
|
||||
# Block message box
|
||||
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
|
||||
|
||||
nwGUI.theProject.projTree.setSeed(42)
|
||||
assert nwGUI.openProject(nwLipsum)
|
||||
assert nwGUI.openDocument("4c4f28287af27")
|
||||
qtbot.wait(stepDelay)
|
||||
@@ -460,18 +458,17 @@ def testGuiMenu_ContextMenus(qtbot, monkeypatch, nwGUI, nwLipsum):
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncDir, fncProj):
|
||||
def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncDir, fncProj, mockRnd):
|
||||
"""Test the Insert menu.
|
||||
"""
|
||||
# Block message box
|
||||
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes)
|
||||
|
||||
nwGUI.theProject.projTree.setSeed(42)
|
||||
assert nwGUI.newProject({"projPath": fncProj})
|
||||
buildTestProject(nwGUI, fncProj)
|
||||
|
||||
assert nwGUI.treeView._getTreeItem("0e17daca5f3e1") is not None
|
||||
assert nwGUI.openDocument("0e17daca5f3e1") is True
|
||||
assert nwGUI.projView.projTree._getTreeItem("000000000000f") is not None
|
||||
assert nwGUI.openDocument("000000000000f") is True
|
||||
nwGUI.docEditor.clear()
|
||||
|
||||
# Test Faulty Inserts
|
||||
@@ -479,10 +476,10 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncDir, fncProj):
|
||||
assert nwGUI.docEditor.getText() == "hello world"
|
||||
nwGUI.docEditor.clear()
|
||||
|
||||
assert not nwGUI.docEditor.insertText(nwDocInsert.NO_INSERT)
|
||||
assert nwGUI.docEditor.insertText(nwDocInsert.NO_INSERT) is False
|
||||
assert nwGUI.docEditor.isEmpty()
|
||||
|
||||
assert not nwGUI.docEditor.insertText(None)
|
||||
assert nwGUI.docEditor.insertText(None) is False
|
||||
assert nwGUI.docEditor.isEmpty()
|
||||
|
||||
# qtbot.stopForInteraction()
|
||||
@@ -680,7 +677,7 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncDir, fncProj):
|
||||
assert not nwGUI.importDocument()
|
||||
|
||||
# Open the document from before, and add some text to it
|
||||
nwGUI.openDocument("0e17daca5f3e1")
|
||||
nwGUI.openDocument("000000000000f")
|
||||
nwGUI.docEditor.setText("Bar")
|
||||
assert nwGUI.docEditor.getText() == "Bar"
|
||||
|
||||
@@ -712,7 +709,7 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncDir, fncProj):
|
||||
theBits = theMessage.split("<br>")
|
||||
assert len(theBits) == 2
|
||||
assert theBits[0] == "The currently open file is saved in:"
|
||||
assert theBits[1] == os.path.join(fncProj, "content", "0e17daca5f3e1.nwd")
|
||||
assert theBits[1] == os.path.join(fncProj, "content", "000000000000f.nwd")
|
||||
|
||||
# qtbot.stopForInteraction()
|
||||
|
||||
|
||||
@@ -19,74 +19,98 @@ 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 pytest
|
||||
import os
|
||||
import pytest
|
||||
|
||||
from tools import writeFile
|
||||
from tools import buildTestProject, writeFile
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
from PyQt5.QtGui import QFocusEvent
|
||||
from PyQt5.QtCore import Qt, QEvent
|
||||
from PyQt5.QtWidgets import QMessageBox, QToolTip
|
||||
|
||||
from novelwriter.enum import nwWidget, nwItemType
|
||||
from novelwriter.dialogs import GuiEditLabel
|
||||
from novelwriter.gui.noveltree import NovelTreeColumn
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, nwMinimal):
|
||||
def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
|
||||
"""Test navigating the novel tree.
|
||||
"""
|
||||
# Block message box
|
||||
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
|
||||
|
||||
nwGUI.openProject(nwMinimal)
|
||||
nwGUI.theProject.projTree.setSeed(42)
|
||||
nwTree = nwGUI.novelView
|
||||
buildTestProject(nwGUI, fncProj)
|
||||
|
||||
##
|
||||
# Show/Hide Scrollbars
|
||||
##
|
||||
nwGUI.switchFocus(nwWidget.TREE)
|
||||
nwGUI.projView.projTree.clearSelection()
|
||||
nwGUI.projView.projTree._getTreeItem("000000000000a").setSelected(True)
|
||||
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE)
|
||||
|
||||
nwTree.mainConf.hideVScroll = True
|
||||
nwTree.mainConf.hideHScroll = True
|
||||
nwTree.initTree()
|
||||
assert not nwTree.verticalScrollBar().isVisible()
|
||||
assert not nwTree.horizontalScrollBar().isVisible()
|
||||
writeFile(
|
||||
os.path.join(nwGUI.theProject.projContent, "0000000000010.nwd"),
|
||||
"# Jane Doe\n\n@tag: Jane\n\n"
|
||||
)
|
||||
writeFile(
|
||||
os.path.join(nwGUI.theProject.projContent, "000000000000f.nwd"), (
|
||||
"### Scene One\n\n"
|
||||
"@pov: Jane\n"
|
||||
"@focus: Jane\n\n"
|
||||
"% Synopsis: This is a scene."
|
||||
)
|
||||
)
|
||||
|
||||
nwTree.mainConf.hideVScroll = False
|
||||
nwTree.mainConf.hideHScroll = False
|
||||
nwTree.initTree()
|
||||
assert nwTree.verticalScrollBar().isEnabled()
|
||||
assert nwTree.horizontalScrollBar().isEnabled()
|
||||
novelView = nwGUI.novelView
|
||||
novelTree = novelView.novelTree
|
||||
novelBar = novelView.novelBar
|
||||
|
||||
##
|
||||
# Populate Tree
|
||||
##
|
||||
# Show/Hide Scrollbars
|
||||
# ====================
|
||||
|
||||
nwGUI.projTabs.setCurrentIndex(nwGUI.idxNovelView)
|
||||
nwGUI.mainConf.hideVScroll = True
|
||||
nwGUI.mainConf.hideHScroll = True
|
||||
novelView.initSettings()
|
||||
assert not novelTree.verticalScrollBar().isVisible()
|
||||
assert not novelTree.horizontalScrollBar().isVisible()
|
||||
|
||||
nwGUI.mainConf.hideVScroll = False
|
||||
nwGUI.mainConf.hideHScroll = False
|
||||
novelView.initSettings()
|
||||
assert novelTree.verticalScrollBar().isEnabled()
|
||||
assert novelTree.horizontalScrollBar().isEnabled()
|
||||
|
||||
# Populate Tree
|
||||
# =============
|
||||
|
||||
nwGUI.projStack.setCurrentIndex(nwGUI.idxNovelView)
|
||||
nwGUI.rebuildIndex()
|
||||
nwTree._populateTree()
|
||||
assert nwTree.topLevelItemCount() == 1
|
||||
novelTree._populateTree(rootHandle=None)
|
||||
assert novelTree.topLevelItemCount() == 3
|
||||
|
||||
# Rebuild should preserve selection
|
||||
topItem = nwTree.topLevelItem(0)
|
||||
topItem = novelTree.topLevelItem(0)
|
||||
assert not topItem.isSelected()
|
||||
topItem.setSelected(True)
|
||||
assert nwTree.selectedItems()[0] == topItem
|
||||
assert nwTree.getSelectedHandle() == ("a35baf2e93843", 0)
|
||||
assert novelTree.selectedItems()[0] == topItem
|
||||
assert novelView.getSelectedHandle() == ("000000000000c", 0)
|
||||
|
||||
nwTree.refreshTree()
|
||||
assert nwTree.topLevelItem(0).isSelected()
|
||||
# Refresh using the slot for the butoom
|
||||
novelBar._refreshNovelTree()
|
||||
assert novelTree.topLevelItem(0).isSelected()
|
||||
|
||||
##
|
||||
# Open Items
|
||||
##
|
||||
# Open Items
|
||||
# ==========
|
||||
|
||||
# Clear selection
|
||||
nwTree.clearSelection()
|
||||
scItem = nwTree.topLevelItem(0).child(0).child(0)
|
||||
novelTree.clearSelection()
|
||||
scItem = novelTree.topLevelItem(2)
|
||||
scItem.setSelected(True)
|
||||
assert scItem.isSelected()
|
||||
|
||||
# Clear selection with mouse
|
||||
vPort = nwTree.viewport()
|
||||
vPort = novelTree.viewport()
|
||||
qtbot.mouseClick(vPort, Qt.LeftButton, pos=vPort.rect().center(), delay=10)
|
||||
assert not scItem.isSelected()
|
||||
|
||||
@@ -94,8 +118,8 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, nwMinimal):
|
||||
scItem.setSelected(True)
|
||||
assert scItem.isSelected()
|
||||
assert nwGUI.docEditor.docHandle() is None
|
||||
nwTree._treeDoubleClick(scItem, 0)
|
||||
assert nwGUI.docEditor.docHandle() == "8c659a11cd429"
|
||||
novelTree._treeDoubleClick(scItem, 0)
|
||||
assert nwGUI.docEditor.docHandle() == "000000000000f"
|
||||
|
||||
# Open item with middle mouse button
|
||||
scItem.setSelected(True)
|
||||
@@ -104,58 +128,79 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, nwMinimal):
|
||||
qtbot.mouseClick(vPort, Qt.MiddleButton, pos=vPort.rect().center(), delay=10)
|
||||
assert nwGUI.docViewer.docHandle() is None
|
||||
|
||||
scRect = nwTree.visualItemRect(scItem)
|
||||
oldData = scItem.data(nwTree.C_TITLE, Qt.UserRole)
|
||||
scItem.setData(nwTree.C_TITLE, Qt.UserRole, (None, "", ""))
|
||||
scRect = novelTree.visualItemRect(scItem)
|
||||
oldData = scItem.data(novelTree.C_TITLE, novelTree.D_HANDLE)
|
||||
scItem.setData(novelTree.C_TITLE, novelTree.D_HANDLE, None)
|
||||
qtbot.mouseClick(vPort, Qt.MiddleButton, pos=scRect.center(), delay=10)
|
||||
assert nwGUI.docViewer.docHandle() is None
|
||||
|
||||
scItem.setData(nwTree.C_TITLE, Qt.UserRole, oldData)
|
||||
scItem.setData(novelTree.C_TITLE, novelTree.D_HANDLE, oldData)
|
||||
qtbot.mouseClick(vPort, Qt.MiddleButton, pos=scRect.center(), delay=10)
|
||||
assert nwGUI.docViewer.docHandle() == "8c659a11cd429"
|
||||
assert nwGUI.docViewer.docHandle() == "000000000000f"
|
||||
|
||||
##
|
||||
# Populate Tree
|
||||
##
|
||||
# Last Column
|
||||
# ===========
|
||||
|
||||
# Add weird titles to first file to check hnadling of non-standard
|
||||
# order of title levels.
|
||||
writeFile(os.path.join(nwMinimal, "content", "a35baf2e93843.nwd"), (
|
||||
"#### Section wo/Scene\n\n"
|
||||
"### Scene wo/Chapter\n\n"
|
||||
"## Chapter wo/Title\n\n"
|
||||
"# Title\n\n"
|
||||
"#### Section w/Title, wo/Scene\n\n"
|
||||
"### Scene w/Title, wo/Chapter\n\n"
|
||||
"## Chapter\n\n"
|
||||
"#### Section w/Chapter, wo/Scene\n\n"
|
||||
"### Scene\n\n"
|
||||
"#### Section\n\n"
|
||||
))
|
||||
nwGUI.rebuildIndex()
|
||||
nwTree._populateTree()
|
||||
assert nwTree.topLevelItem(0).text(nwTree.C_TITLE) == "Section wo/Scene"
|
||||
assert nwTree.topLevelItem(1).text(nwTree.C_TITLE) == "Scene wo/Chapter"
|
||||
assert nwTree.topLevelItem(2).text(nwTree.C_TITLE) == "Chapter wo/Title"
|
||||
assert nwTree.topLevelItem(3).text(nwTree.C_TITLE) == "Title"
|
||||
novelBar.setLastColType(NovelTreeColumn.HIDDEN)
|
||||
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is True
|
||||
assert novelTree.lastColType == NovelTreeColumn.HIDDEN
|
||||
assert novelTree._getLastColumnText("000000000000f", "T000001") == ("", "")
|
||||
|
||||
tTitle = nwTree.topLevelItem(3)
|
||||
assert tTitle.child(0).text(nwTree.C_TITLE) == "Section w/Title, wo/Scene"
|
||||
assert tTitle.child(1).text(nwTree.C_TITLE) == "Scene w/Title, wo/Chapter"
|
||||
assert tTitle.child(2).text(nwTree.C_TITLE) == "Chapter"
|
||||
novelBar.setLastColType(NovelTreeColumn.POV)
|
||||
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False
|
||||
assert novelTree.lastColType == NovelTreeColumn.POV
|
||||
assert novelTree._getLastColumnText("000000000000f", "T000001") == (
|
||||
"Jane", "Point of View: Jane"
|
||||
)
|
||||
|
||||
tChap = tTitle.child(2)
|
||||
assert tChap.child(0).text(nwTree.C_TITLE) == "Section w/Chapter, wo/Scene"
|
||||
assert tChap.child(1).text(nwTree.C_TITLE) == "Scene"
|
||||
novelBar.setLastColType(NovelTreeColumn.FOCUS)
|
||||
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False
|
||||
assert novelTree.lastColType == NovelTreeColumn.FOCUS
|
||||
assert novelTree._getLastColumnText("000000000000f", "T000001") == (
|
||||
"Jane", "Focus: Jane"
|
||||
)
|
||||
|
||||
tScene = tChap.child(1)
|
||||
assert tScene.child(0).text(nwTree.C_TITLE) == "Section"
|
||||
novelBar.setLastColType(NovelTreeColumn.PLOT)
|
||||
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False
|
||||
assert novelTree.lastColType == NovelTreeColumn.PLOT
|
||||
assert novelTree._getLastColumnText("000000000000f", "T000001") == (
|
||||
"", "Plot: "
|
||||
)
|
||||
|
||||
##
|
||||
# Close
|
||||
##
|
||||
novelTree._lastCol = None
|
||||
assert novelTree._getLastColumnText("0000000000000", "T000000") == ("", "")
|
||||
|
||||
# qtbot.stopForInteraction()
|
||||
# Item Meta
|
||||
# =========
|
||||
|
||||
ttText = ""
|
||||
|
||||
def showText(pos, text):
|
||||
nonlocal ttText
|
||||
ttText = text
|
||||
|
||||
mIndex = novelTree.model().index(2, novelTree.C_MORE)
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(QToolTip, "showText", showText)
|
||||
novelTree._treeItemClicked(mIndex)
|
||||
assert ttText == (
|
||||
"<p><b>Point of View</b>: Jane<br><b>Focus</b>: Jane</p>"
|
||||
"<p><b>Synopsis</b>: This is a scene.</p>"
|
||||
)
|
||||
|
||||
# Other Checks
|
||||
# ============
|
||||
|
||||
scItem = novelTree.topLevelItem(2)
|
||||
scItem.setSelected(True)
|
||||
assert scItem.isSelected()
|
||||
novelTree.focusOutEvent(QFocusEvent(QEvent.None_, Qt.MouseFocusReason))
|
||||
assert not scItem.isSelected()
|
||||
|
||||
# Close
|
||||
# =====
|
||||
|
||||
# qtbot.stop()
|
||||
nwGUI.closeProject()
|
||||
|
||||
# END Test testGuiNovelTree_TreeItems
|
||||
|
||||
@@ -19,20 +19,144 @@ 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 os
|
||||
import time
|
||||
import pytest
|
||||
|
||||
from PyQt5.QtCore import Qt, QPoint
|
||||
from PyQt5.QtWidgets import QAction, QTreeWidgetItem, QMessageBox
|
||||
from tools import buildTestProject, writeFile
|
||||
|
||||
from novelwriter.enum import nwOutline
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QWidget, QMessageBox, QAction
|
||||
|
||||
keyDelay = 2
|
||||
typeDelay = 1
|
||||
stepDelay = 20
|
||||
from novelwriter.enum import nwItemClass, nwOutline, nwView
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, nwLipsum):
|
||||
def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, fncDir):
|
||||
"""Test the outline view.
|
||||
"""
|
||||
# Block message box
|
||||
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
|
||||
|
||||
# Create a project
|
||||
prjDir = os.path.join(fncDir, "project")
|
||||
buildTestProject(nwGUI, prjDir)
|
||||
|
||||
nwGUI.rebuildIndex()
|
||||
nwGUI._changeView(nwView.OUTLINE)
|
||||
|
||||
outlineView = nwGUI.outlineView
|
||||
outlineTree = outlineView.outlineTree
|
||||
outlineData = outlineView.outlineData
|
||||
outlineMenu = outlineView.outlineBar.mColumns
|
||||
|
||||
# Toggle scrollbars
|
||||
nwGUI.mainConf.hideVScroll = True
|
||||
nwGUI.mainConf.hideHScroll = True
|
||||
outlineView.initSettings()
|
||||
assert outlineTree.verticalScrollBarPolicy() == Qt.ScrollBarAlwaysOff
|
||||
assert outlineTree.horizontalScrollBarPolicy() == Qt.ScrollBarAlwaysOff
|
||||
assert outlineData.verticalScrollBarPolicy() == Qt.ScrollBarAlwaysOff
|
||||
assert outlineData.horizontalScrollBarPolicy() == Qt.ScrollBarAlwaysOff
|
||||
|
||||
nwGUI.mainConf.hideVScroll = False
|
||||
nwGUI.mainConf.hideHScroll = False
|
||||
outlineView.initSettings()
|
||||
assert outlineTree.verticalScrollBarPolicy() == Qt.ScrollBarAsNeeded
|
||||
assert outlineTree.horizontalScrollBarPolicy() == Qt.ScrollBarAsNeeded
|
||||
assert outlineData.verticalScrollBarPolicy() == Qt.ScrollBarAsNeeded
|
||||
assert outlineData.horizontalScrollBarPolicy() == Qt.ScrollBarAsNeeded
|
||||
|
||||
# Check focus
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(QWidget, "hasFocus", lambda *a: True)
|
||||
assert outlineView.treeHasFocus() is True
|
||||
|
||||
outlineView.setTreeFocus() # Can't check. just ensures that it doesn't error
|
||||
|
||||
# Option State
|
||||
# ============
|
||||
pOptions = nwGUI.theProject.options
|
||||
colNames = [h.name for h in nwOutline]
|
||||
colItems = [h for h in nwOutline]
|
||||
colWidth = {h: outlineTree.DEF_WIDTH[h] for h in nwOutline}
|
||||
colHidden = {h: outlineTree.DEF_HIDDEN[h] for h in nwOutline}
|
||||
|
||||
assert outlineTree.topLevelItemCount() > 0
|
||||
|
||||
# Save header state not allowed
|
||||
outlineTree._lastBuild = 0
|
||||
outlineTree._saveHeaderState()
|
||||
assert pOptions.getValue("GuiOutline", "headerOrder", []) == []
|
||||
|
||||
# Allow saving header state
|
||||
outlineTree._lastBuild = time.time()
|
||||
outlineTree._saveHeaderState()
|
||||
assert pOptions.getValue("GuiOutline", "headerOrder", []) == colNames
|
||||
assert outlineTree._treeOrder == colItems
|
||||
assert outlineTree._colWidth == colWidth
|
||||
assert outlineTree._colHidden == colHidden
|
||||
|
||||
# Get default values
|
||||
optItems = pOptions.getValue("GuiOutline", "headerOrder", [])
|
||||
optWidth = pOptions.getValue("GuiOutline", "columnWidth", {})
|
||||
optHidden = pOptions.getValue("GuiOutline", "columnHidden", {})
|
||||
|
||||
# Add invalid column name
|
||||
pOptions.setValue("GuiOutline", "headerOrder", optItems + ["blabla"])
|
||||
outlineTree._loadHeaderState()
|
||||
assert outlineTree._treeOrder == colItems
|
||||
assert outlineTree._colHidden == colHidden
|
||||
|
||||
# Add duplicate column name
|
||||
pOptions.setValue("GuiOutline", "headerOrder", optItems + [optItems[-1]])
|
||||
outlineTree._loadHeaderState()
|
||||
assert outlineTree._treeOrder == colItems
|
||||
assert outlineTree._colHidden == colHidden
|
||||
|
||||
# Invalid column width data
|
||||
pOptions.setValue("GuiOutline", "headerOrder", optItems)
|
||||
pOptions.setValue("GuiOutline", "columnWidth", {"blabla": None})
|
||||
outlineTree._loadHeaderState()
|
||||
assert outlineTree._treeOrder == colItems
|
||||
assert outlineTree._colHidden == colHidden
|
||||
|
||||
# Invalid column width data
|
||||
pOptions.setValue("GuiOutline", "headerOrder", optItems)
|
||||
pOptions.setValue("GuiOutline", "columnWidth", optWidth)
|
||||
pOptions.setValue("GuiOutline", "columnHidden", {"bloabla": None})
|
||||
outlineTree._loadHeaderState()
|
||||
assert outlineTree._treeOrder == colItems
|
||||
assert outlineTree._colHidden == colHidden
|
||||
|
||||
# Valid settings
|
||||
pOptions.setValue("GuiOutline", "headerOrder", optItems)
|
||||
pOptions.setValue("GuiOutline", "columnWidth", optWidth)
|
||||
pOptions.setValue("GuiOutline", "columnHidden", optHidden)
|
||||
outlineTree._loadHeaderState()
|
||||
assert outlineTree._treeOrder == colItems
|
||||
assert outlineTree._colHidden == colHidden
|
||||
|
||||
# Header Menu
|
||||
# ===========
|
||||
|
||||
# Trigger the menu entry for all hidden columns
|
||||
for hItem in nwOutline:
|
||||
if outlineTree.DEF_HIDDEN[hItem]:
|
||||
outlineMenu.actionMap[hItem].activate(QAction.Trigger)
|
||||
|
||||
# Now no columns should be hidden
|
||||
outlineTree._saveHeaderState()
|
||||
assert not any(pOptions.getValue("GuiOutline", "columnHidden", None).values())
|
||||
|
||||
# qtbot.stop()
|
||||
|
||||
# END Test testGuiOutline_Main
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testGuiOutline_Content(qtbot, monkeypatch, nwGUI, nwLipsum):
|
||||
"""Test the outline view.
|
||||
"""
|
||||
# Block message box
|
||||
@@ -43,73 +167,104 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, nwLipsum):
|
||||
nwGUI.mainConf.lastPath = nwLipsum
|
||||
|
||||
nwGUI.rebuildIndex()
|
||||
nwGUI.mainTabs.setCurrentIndex(nwGUI.idxTabProj)
|
||||
nwGUI._changeView(nwView.OUTLINE)
|
||||
|
||||
assert nwGUI.projView.topLevelItemCount() > 0
|
||||
outlineView = nwGUI.outlineView
|
||||
outlineBar = outlineView.outlineBar
|
||||
outlineTree = outlineView.outlineTree
|
||||
outlineData = outlineView.outlineData
|
||||
|
||||
# Context Menu
|
||||
nwGUI.projView._headerRightClick(QPoint(1, 1))
|
||||
nwGUI.projView.headerMenu.actionMap[nwOutline.CCOUNT].activate(QAction.Trigger)
|
||||
nwGUI.projView.headerMenu.close()
|
||||
qtbot.mouseClick(nwGUI.projView, Qt.LeftButton)
|
||||
lipHandle = "b3643d0f92e32"
|
||||
|
||||
nwGUI.projView._loadHeaderState()
|
||||
assert not nwGUI.projView._colHidden[nwOutline.CCOUNT]
|
||||
# Check defaults in dropdown list
|
||||
assert outlineBar.novelValue.itemData(0) == lipHandle
|
||||
assert outlineBar.novelValue.itemData(1) is None # Separator
|
||||
assert outlineBar.novelValue.itemData(2) == "" # All novels
|
||||
|
||||
# Add a second novel folder
|
||||
newHandle = nwGUI.theProject.newRoot(nwItemClass.NOVEL)
|
||||
nwGUI.projView.revealNewTreeItem(newHandle)
|
||||
|
||||
# Check new values in dropdown list
|
||||
assert outlineBar.novelValue.itemData(0) == lipHandle
|
||||
assert outlineBar.novelValue.itemData(1) == newHandle
|
||||
assert outlineBar.novelValue.itemData(2) is None # Separator
|
||||
assert outlineBar.novelValue.itemData(3) == "" # All novels
|
||||
|
||||
# Add a bunch of files in a header order that hits all tree combos
|
||||
docList = [
|
||||
("Section 1", 4), ("Scene 1", 3), ("Chapter 1", 2), ("Part 1", 1),
|
||||
("Section 2", 4), ("Scene 2", 3), ("Chapter 2", 2),
|
||||
("Section 3", 4), ("Scene 3", 3),
|
||||
("Section 4", 4),
|
||||
]
|
||||
for dTitle, hLevel in docList:
|
||||
aHandle = nwGUI.theProject.newFile(dTitle, newHandle)
|
||||
hHash = "#"*hLevel
|
||||
writeFile(os.path.join(nwLipsum, "content", f"{aHandle}.nwd"), f"{hHash} {dTitle}\n\n")
|
||||
nwGUI.projView.revealNewTreeItem(aHandle)
|
||||
|
||||
nwGUI.rebuildIndex()
|
||||
|
||||
# Build the second novel
|
||||
outlineBar.novelValue.setCurrentIndex(1)
|
||||
outlineBar._refreshRequested()
|
||||
|
||||
# Go back to Lipsum
|
||||
outlineBar.novelValue.setCurrentIndex(0)
|
||||
outlineBar._refreshRequested()
|
||||
|
||||
# Check Details
|
||||
# =============
|
||||
|
||||
# First Item
|
||||
nwGUI.rebuildOutline()
|
||||
selItem = nwGUI.projView.topLevelItem(0)
|
||||
assert isinstance(selItem, QTreeWidgetItem)
|
||||
outlineTree.refreshTree()
|
||||
selItem = outlineTree.topLevelItem(0)
|
||||
|
||||
nwGUI.projView.setCurrentItem(selItem)
|
||||
assert nwGUI.projMeta.titleLabel.text() == "<b>Title</b>"
|
||||
assert nwGUI.projMeta.titleValue.text() == "Lorem Ipsum"
|
||||
assert nwGUI.projMeta.fileValue.text() == "Lorem Ipsum"
|
||||
assert nwGUI.projMeta.itemValue.text() == "Finished"
|
||||
outlineTree.setCurrentItem(selItem)
|
||||
assert outlineData.titleLabel.text() == "<b>Title</b>"
|
||||
assert outlineData.titleValue.text() == "Lorem Ipsum"
|
||||
assert outlineData.fileValue.text() == "Lorem Ipsum"
|
||||
assert outlineData.itemValue.text() == "Finished"
|
||||
|
||||
assert nwGUI.projMeta.cCValue.text() == "230"
|
||||
assert nwGUI.projMeta.wCValue.text() == "40"
|
||||
assert nwGUI.projMeta.pCValue.text() == "3"
|
||||
assert outlineData.cCValue.text() == "230"
|
||||
assert outlineData.wCValue.text() == "40"
|
||||
assert outlineData.pCValue.text() == "3"
|
||||
|
||||
# Scene One
|
||||
actItem = nwGUI.projView.topLevelItem(1)
|
||||
chpItem = actItem.child(0)
|
||||
selItem = chpItem.child(0)
|
||||
selItem = outlineTree.topLevelItem(4)
|
||||
|
||||
nwGUI.projView.setCurrentItem(selItem)
|
||||
tHandle, tLine = nwGUI.projView.getSelectedHandle()
|
||||
outlineTree.setCurrentItem(selItem)
|
||||
tHandle, tLine = outlineTree.getSelectedHandle()
|
||||
assert tHandle == "88243afbe5ed8"
|
||||
assert tLine == 0
|
||||
|
||||
assert nwGUI.projMeta.titleLabel.text() == "<b>Scene</b>"
|
||||
assert nwGUI.projMeta.titleValue.text() == "Scene One"
|
||||
assert nwGUI.projMeta.fileValue.text() == "Scene One"
|
||||
assert nwGUI.projMeta.itemValue.text() == "Finished"
|
||||
assert outlineData.titleLabel.text() == "<b>Scene</b>"
|
||||
assert outlineData.titleValue.text() == "Scene One"
|
||||
assert outlineData.fileValue.text() == "Scene One"
|
||||
assert outlineData.itemValue.text() == "Finished"
|
||||
|
||||
# Click POV Link
|
||||
assert nwGUI.projMeta.povKeyValue.text() == "<a href='#pov=Bod'>Bod</a>"
|
||||
nwGUI.projMeta._tagClicked("#pov=Bod")
|
||||
assert outlineData.povKeyValue.text() == "<a href='Bod'>Bod</a>"
|
||||
outlineView._tagClicked("Bod")
|
||||
assert nwGUI.docViewer.docHandle() == "4c4f28287af27"
|
||||
|
||||
# Scene One, Section Two
|
||||
actItem = nwGUI.projView.topLevelItem(1)
|
||||
chpItem = actItem.child(0)
|
||||
scnItem = chpItem.child(0)
|
||||
selItem = scnItem.child(0)
|
||||
selItem = outlineTree.topLevelItem(5)
|
||||
|
||||
nwGUI.projView.setCurrentItem(selItem)
|
||||
tHandle, tLine = nwGUI.projView.getSelectedHandle()
|
||||
outlineTree.setCurrentItem(selItem)
|
||||
tHandle, tLine = outlineTree.getSelectedHandle()
|
||||
assert tHandle == "88243afbe5ed8"
|
||||
assert tLine == 12
|
||||
|
||||
assert nwGUI.projMeta.titleLabel.text() == "<b>Section</b>"
|
||||
assert nwGUI.projMeta.titleValue.text() == "Scene One, Section Two"
|
||||
assert nwGUI.projMeta.fileValue.text() == "Scene One"
|
||||
assert nwGUI.projMeta.itemValue.text() == "Finished"
|
||||
assert outlineData.titleLabel.text() == "<b>Section</b>"
|
||||
assert outlineData.titleValue.text() == "Scene One, Section Two"
|
||||
assert outlineData.fileValue.text() == "Scene One"
|
||||
assert outlineData.itemValue.text() == "Finished"
|
||||
|
||||
nwGUI.projView._treeDoubleClick(selItem, 0)
|
||||
outlineTree._treeDoubleClick(selItem, 0)
|
||||
assert nwGUI.docEditor.docHandle() == "88243afbe5ed8"
|
||||
|
||||
# qtbot.stopForInteraction()
|
||||
# qtbot.stop()
|
||||
|
||||
# END Test testGuiOutline_Main
|
||||
# END Test testGuiOutline_Content
|
||||
|
||||
+493
-183
@@ -22,218 +22,528 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import pytest
|
||||
import os
|
||||
|
||||
from tools import writeFile
|
||||
from tools import buildTestProject
|
||||
|
||||
from PyQt5.QtCore import QItemSelectionModel
|
||||
from PyQt5.QtWidgets import QAction, QMessageBox
|
||||
from PyQt5.QtWidgets import QMessageBox, QMenu
|
||||
|
||||
from novelwriter.guimain import GuiMain
|
||||
from novelwriter.enum import nwItemLayout, nwItemType, nwItemClass
|
||||
from novelwriter.dialogs import GuiEditLabel
|
||||
from novelwriter.gui.projtree import GuiProjectTree
|
||||
from novelwriter.enum import nwItemType, nwItemClass
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testGuiProjTree_TreeItems(qtbot, caplog, monkeypatch, nwGUI, nwMinimal):
|
||||
def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd):
|
||||
"""Test adding and removing items from the project tree.
|
||||
"""
|
||||
# Block message box
|
||||
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(GuiMain, "editItem", lambda *a: None)
|
||||
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
|
||||
|
||||
nwGUI.theProject.projTree.setSeed(42)
|
||||
nwTree = nwGUI.treeView
|
||||
nwTree = nwGUI.projView
|
||||
|
||||
##
|
||||
# Add New Items
|
||||
##
|
||||
# Try to add item with no project
|
||||
assert nwTree.projTree.newTreeItem(nwItemType.FILE) is False
|
||||
|
||||
# Try to add and move item with no project
|
||||
assert not nwTree.newTreeItem(nwItemType.FILE, None)
|
||||
assert not nwTree.moveTreeItem(1)
|
||||
# Create a project
|
||||
prjDir = os.path.join(fncDir, "project")
|
||||
buildTestProject(nwGUI, prjDir)
|
||||
|
||||
# Open a project
|
||||
assert nwGUI.openProject(nwMinimal)
|
||||
# No itemType set
|
||||
nwTree.projTree.clearSelection()
|
||||
assert nwTree.projTree.newTreeItem(None) is False
|
||||
|
||||
# Root Items
|
||||
# ==========
|
||||
|
||||
# No class set
|
||||
assert nwTree.projTree.newTreeItem(nwItemType.ROOT) is False
|
||||
|
||||
# Create root item
|
||||
assert nwTree.projTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) is True
|
||||
assert "0000000000010" in nwGUI.theProject.tree
|
||||
|
||||
# File/Folder Items
|
||||
# =================
|
||||
|
||||
# No location selected for new item
|
||||
nwTree.clearSelection()
|
||||
assert not nwTree.newTreeItem(nwItemType.FILE, None)
|
||||
assert not nwTree.newTreeItem(nwItemType.FOLDER, None)
|
||||
assert nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL)
|
||||
nwTree.projTree.clearSelection()
|
||||
caplog.clear()
|
||||
assert nwTree.projTree.newTreeItem(nwItemType.FILE) is False
|
||||
assert nwTree.projTree.newTreeItem(nwItemType.FOLDER) is False
|
||||
assert "Did not find anywhere" in caplog.text
|
||||
|
||||
# No itemType set or ROOT, but no class
|
||||
assert not nwTree.newTreeItem(None, None)
|
||||
assert not nwTree.newTreeItem(nwItemType.ROOT, None)
|
||||
# Create new folder as child of Novel folder
|
||||
nwTree.setSelectedHandle("0000000000008")
|
||||
assert nwTree.projTree.newTreeItem(nwItemType.FOLDER) is True
|
||||
assert nwGUI.theProject.tree["0000000000011"].itemParent == "0000000000008"
|
||||
assert nwGUI.theProject.tree["0000000000011"].itemRoot == "0000000000008"
|
||||
assert nwGUI.theProject.tree["0000000000011"].itemClass == nwItemClass.NOVEL
|
||||
|
||||
# Select a location
|
||||
chItem = nwTree._getTreeItem("a6d311a93600a")
|
||||
nwTree.setCurrentItem(chItem, QItemSelectionModel.Current)
|
||||
chItem.setExpanded(True)
|
||||
# Add a new file in the new folder
|
||||
nwTree.setSelectedHandle("0000000000011")
|
||||
assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True
|
||||
assert nwGUI.theProject.tree["0000000000012"].itemParent == "0000000000011"
|
||||
assert nwGUI.theProject.tree["0000000000012"].itemRoot == "0000000000008"
|
||||
assert nwGUI.theProject.tree["0000000000012"].itemClass == nwItemClass.NOVEL
|
||||
|
||||
# Create new item with no class set (defaults to NOVEL)
|
||||
assert nwTree.newTreeItem(nwItemType.FILE, None)
|
||||
assert nwTree.newTreeItem(nwItemType.FOLDER, None)
|
||||
# Add a new chapter next to the other new file
|
||||
nwTree.setSelectedHandle("0000000000012")
|
||||
assert nwTree.projTree.newTreeItem(nwItemType.FILE, hLevel=2) is True
|
||||
assert nwGUI.theProject.tree["0000000000013"].itemParent == "0000000000011"
|
||||
assert nwGUI.theProject.tree["0000000000013"].itemRoot == "0000000000008"
|
||||
assert nwGUI.theProject.tree["0000000000013"].itemClass == nwItemClass.NOVEL
|
||||
assert nwGUI.openDocument("0000000000013")
|
||||
assert nwGUI.docEditor.getText() == "## New Chapter\n\n"
|
||||
|
||||
# Check that we have the correct tree order
|
||||
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
|
||||
"a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "44cb730c42048", "71ee45a3c0db9"
|
||||
]
|
||||
# Add a new scene next to the other new file
|
||||
nwTree.setSelectedHandle("0000000000012")
|
||||
assert nwTree.projTree.newTreeItem(nwItemType.FILE, hLevel=3) is True
|
||||
assert nwGUI.theProject.tree["0000000000014"].itemParent == "0000000000011"
|
||||
assert nwGUI.theProject.tree["0000000000014"].itemRoot == "0000000000008"
|
||||
assert nwGUI.theProject.tree["0000000000014"].itemClass == nwItemClass.NOVEL
|
||||
assert nwGUI.openDocument("0000000000014")
|
||||
assert nwGUI.docEditor.getText() == "### New Scene\n\n"
|
||||
|
||||
# Add roots
|
||||
assert not nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) # Duplicate
|
||||
assert nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.CUSTOM) # Valid
|
||||
# Add a new file to the characters folder
|
||||
nwTree.setSelectedHandle("000000000000a")
|
||||
assert nwTree.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) is True
|
||||
assert nwGUI.theProject.tree["0000000000015"].itemParent == "000000000000a"
|
||||
assert nwGUI.theProject.tree["0000000000015"].itemRoot == "000000000000a"
|
||||
assert nwGUI.theProject.tree["0000000000015"].itemClass == nwItemClass.CHARACTER
|
||||
assert nwGUI.openDocument("0000000000015")
|
||||
assert nwGUI.docEditor.getText() == "# New Note\n\n"
|
||||
|
||||
# Change max depth and try to add a subfolder that is too deep
|
||||
monkeypatch.setattr("novelwriter.constants.nwConst.MAX_DEPTH", 2)
|
||||
chItem = nwTree._getTreeItem("71ee45a3c0db9")
|
||||
nwTree.setCurrentItem(chItem, QItemSelectionModel.Current)
|
||||
assert not nwTree.newTreeItem(nwItemType.FOLDER, None)
|
||||
# Make sure the sibling folder bug trap works
|
||||
nwTree.setSelectedHandle("0000000000013")
|
||||
nwGUI.theProject.tree["0000000000013"].setParent(None) # This should not happen
|
||||
caplog.clear()
|
||||
assert nwTree.projTree.newTreeItem(nwItemType.FILE) is False
|
||||
assert "Internal error" in caplog.text
|
||||
nwGUI.theProject.tree["0000000000013"].setParent("0000000000011")
|
||||
|
||||
##
|
||||
# Move Items
|
||||
##
|
||||
# Cancel during creation
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(GuiEditLabel, "getLabel", lambda *a, **k: ("", False))
|
||||
nwTree.setSelectedHandle("0000000000013")
|
||||
assert nwTree.projTree.newTreeItem(nwItemType.FILE) is False
|
||||
|
||||
nwTree.setSelectedHandle("8c659a11cd429")
|
||||
# Get the trash folder
|
||||
nwTree.projTree._addTrashRoot()
|
||||
trashHandle = nwGUI.theProject.trashFolder()
|
||||
nwTree.setSelectedHandle(trashHandle)
|
||||
assert nwTree.projTree.newTreeItem(nwItemType.FILE) is False
|
||||
assert "Cannot add new files or folders to the Trash folder" in caplog.text
|
||||
|
||||
# Shift focus and try to move item
|
||||
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: False)
|
||||
assert not nwTree.moveTreeItem(1)
|
||||
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
|
||||
"a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "44cb730c42048", "71ee45a3c0db9"
|
||||
]
|
||||
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
|
||||
# Other Checks
|
||||
# ============
|
||||
|
||||
# Move second item up twice (should give same result)
|
||||
nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger)
|
||||
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
|
||||
"a6d311a93600a", "8c659a11cd429", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9"
|
||||
]
|
||||
nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger)
|
||||
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
|
||||
"a6d311a93600a", "8c659a11cd429", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9"
|
||||
]
|
||||
# Also check error handling in reveal function
|
||||
assert nwTree.revealNewTreeItem("abc") is False
|
||||
|
||||
# Move it back down four times (last two should be the same)
|
||||
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
|
||||
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
|
||||
"a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "44cb730c42048", "71ee45a3c0db9"
|
||||
]
|
||||
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
|
||||
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
|
||||
"a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "8c659a11cd429", "71ee45a3c0db9"
|
||||
]
|
||||
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
|
||||
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
|
||||
"a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9", "8c659a11cd429"
|
||||
]
|
||||
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
|
||||
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
|
||||
"a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9", "8c659a11cd429"
|
||||
]
|
||||
|
||||
# Move up twice, and undo
|
||||
nwTree._lastMove = {}
|
||||
nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger)
|
||||
nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger)
|
||||
nwGUI.mainMenu.aMoveUndo.activate(QAction.Trigger)
|
||||
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
|
||||
"a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9", "8c659a11cd429"
|
||||
]
|
||||
|
||||
# Move a root item (top level items are different) twice
|
||||
nwTree.flushTreeOrder()
|
||||
assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 10
|
||||
nwTree.setSelectedHandle("9d5247ab588e0")
|
||||
|
||||
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
|
||||
nwTree.flushTreeOrder()
|
||||
assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 11
|
||||
|
||||
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
|
||||
nwTree.flushTreeOrder()
|
||||
assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 11
|
||||
|
||||
##
|
||||
# Delete and Trash
|
||||
##
|
||||
|
||||
# Add some content to the new file
|
||||
nwGUI.openDocument("73475cb40a568")
|
||||
nwGUI.docEditor.setText("# Hello World\n")
|
||||
nwGUI.saveDocument()
|
||||
nwGUI.saveProject()
|
||||
assert os.path.isfile(os.path.join(nwMinimal, "content", "73475cb40a568.nwd"))
|
||||
|
||||
# Delete the items we added earlier
|
||||
nwTree.clearSelection()
|
||||
assert not nwTree.emptyTrash() # No folder yet
|
||||
assert not nwTree.deleteItem(None)
|
||||
assert not nwTree.deleteItem("1111111111111")
|
||||
assert nwTree.deleteItem("73475cb40a568") # New File
|
||||
assert nwTree.deleteItem("71ee45a3c0db9") # New Folder
|
||||
assert nwTree.deleteItem("811786ad1ae74") # Custom Root
|
||||
assert "73475cb40a568" in nwGUI.theProject.projTree._treeOrder
|
||||
assert "71ee45a3c0db9" not in nwGUI.theProject.projTree._treeOrder
|
||||
assert "811786ad1ae74" not in nwGUI.theProject.projTree._treeOrder
|
||||
|
||||
# The file is in trash, empty it
|
||||
assert os.path.isfile(os.path.join(nwMinimal, "content", "73475cb40a568.nwd"))
|
||||
assert nwTree.emptyTrash()
|
||||
assert not nwTree.emptyTrash() # Already empty
|
||||
assert not os.path.isfile(os.path.join(nwMinimal, "content", "73475cb40a568.nwd"))
|
||||
assert "73475cb40a568" not in nwGUI.theProject.projTree._treeOrder
|
||||
|
||||
# Should not be allowed to add files and folders to Trash
|
||||
trashHandle = nwGUI.theProject.projTree.trashRoot()
|
||||
chItem = nwTree._getTreeItem(trashHandle)
|
||||
nwTree.setCurrentItem(chItem, QItemSelectionModel.Current)
|
||||
assert not nwTree.newTreeItem(nwItemType.FILE, None)
|
||||
assert not nwTree.newTreeItem(nwItemType.FOLDER, None)
|
||||
|
||||
# Close the project
|
||||
nwGUI.closeProject()
|
||||
|
||||
##
|
||||
# Orphaned Files
|
||||
##
|
||||
|
||||
# Add an orphaned file
|
||||
orphFile = os.path.join(nwMinimal, "content", "1234567890abc.nwd")
|
||||
writeFile(orphFile, "# Hello World\n")
|
||||
|
||||
# Open the project again
|
||||
nwGUI.openProject(nwMinimal)
|
||||
|
||||
# Check that the orphaned file was found and added to the tree
|
||||
nwTree.flushTreeOrder()
|
||||
assert "1234567890abc" in nwGUI.theProject.projTree._treeOrder
|
||||
orItem = nwTree._getTreeItem("1234567890abc")
|
||||
assert orItem.text(nwTree.C_NAME) == "Recovered File 1"
|
||||
|
||||
##
|
||||
# Unexpected Error Handling
|
||||
##
|
||||
|
||||
# Add an item with an invalid type
|
||||
assert not nwTree.newTreeItem(nwItemType.NO_TYPE, nwItemClass.NOVEL)
|
||||
assert "Failed to add new item" in caplog.messages[-1]
|
||||
|
||||
# Add new file after one that has no parent handle
|
||||
chItem = nwTree._getTreeItem("44cb730c42048")
|
||||
nwTree.setCurrentItem(chItem, QItemSelectionModel.Current)
|
||||
nwTree.theProject.projTree["44cb730c42048"]._parent = None
|
||||
assert not nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL)
|
||||
nwTree.clearSelection()
|
||||
|
||||
# Add a file with no parent, and fail to find a suitable parent item
|
||||
monkeypatch.setattr("novelwriter.core.tree.NWTree.findRoot", lambda *a: None)
|
||||
|
||||
assert not nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL)
|
||||
assert not nwTree.newTreeItem(nwItemType.FOLDER, nwItemClass.NOVEL)
|
||||
# Add an item that cannot be displayed in the tree
|
||||
nHandle = nwGUI.theProject.newFile("Test", None)
|
||||
assert nwTree.revealNewTreeItem(nHandle) is False
|
||||
|
||||
# Clean up
|
||||
# qtbot.stopForInteraction()
|
||||
nwGUI.closeProject()
|
||||
|
||||
# END Test testGuiProjTree_TreeItems
|
||||
# END Test testGuiProjTree_NewItems
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd):
|
||||
"""Test adding and removing items from the project tree.
|
||||
"""
|
||||
# Block message box
|
||||
monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
|
||||
|
||||
nwTree = nwGUI.projView
|
||||
|
||||
# Try to move item with no project
|
||||
assert nwTree.projTree.moveTreeItem(1) is False
|
||||
|
||||
# Create a project
|
||||
prjDir = os.path.join(fncDir, "project")
|
||||
buildTestProject(nwGUI, prjDir)
|
||||
|
||||
# Move Documents
|
||||
# ==============
|
||||
|
||||
# Add some files
|
||||
nwTree.setSelectedHandle("000000000000d")
|
||||
assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True
|
||||
assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True
|
||||
assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True
|
||||
assert nwTree.getTreeFromHandle("000000000000d") == [
|
||||
"000000000000d", "000000000000e", "000000000000f",
|
||||
"0000000000010", "0000000000011", "0000000000012",
|
||||
]
|
||||
|
||||
# Move with no selections
|
||||
nwTree.projTree.clearSelection()
|
||||
assert nwTree.projTree.moveTreeItem(1) is False
|
||||
|
||||
# Move second item up twice (should give same result)
|
||||
nwTree.setSelectedHandle("000000000000f")
|
||||
assert nwTree.projTree.moveTreeItem(-1) is True
|
||||
assert nwTree.getTreeFromHandle("000000000000d") == [
|
||||
"000000000000d", "000000000000f", "000000000000e",
|
||||
"0000000000010", "0000000000011", "0000000000012",
|
||||
]
|
||||
assert nwTree.projTree.moveTreeItem(-1) is False
|
||||
assert nwTree.getTreeFromHandle("000000000000d") == [
|
||||
"000000000000d", "000000000000f", "000000000000e",
|
||||
"0000000000010", "0000000000011", "0000000000012",
|
||||
]
|
||||
|
||||
# Restore
|
||||
assert nwTree.projTree.moveTreeItem(1) is True
|
||||
assert nwTree.getTreeFromHandle("000000000000d") == [
|
||||
"000000000000d", "000000000000e", "000000000000f",
|
||||
"0000000000010", "0000000000011", "0000000000012",
|
||||
]
|
||||
|
||||
# Move fifth item down twice (should give same result)
|
||||
nwTree.setSelectedHandle("0000000000011")
|
||||
assert nwTree.projTree.moveTreeItem(1) is True
|
||||
assert nwTree.getTreeFromHandle("000000000000d") == [
|
||||
"000000000000d", "000000000000e", "000000000000f",
|
||||
"0000000000010", "0000000000012", "0000000000011",
|
||||
]
|
||||
assert nwTree.projTree.moveTreeItem(1) is False
|
||||
assert nwTree.getTreeFromHandle("000000000000d") == [
|
||||
"000000000000d", "000000000000e", "000000000000f",
|
||||
"0000000000010", "0000000000012", "0000000000011",
|
||||
]
|
||||
|
||||
# Restore
|
||||
assert nwTree.projTree.moveTreeItem(-1) is True
|
||||
assert nwTree.getTreeFromHandle("000000000000d") == [
|
||||
"000000000000d", "000000000000e", "000000000000f",
|
||||
"0000000000010", "0000000000011", "0000000000012",
|
||||
]
|
||||
|
||||
# Move down again, and restore via undo
|
||||
nwTree.setSelectedHandle("0000000000011")
|
||||
assert nwTree.projTree.moveTreeItem(1) is True
|
||||
assert nwTree.getTreeFromHandle("000000000000d") == [
|
||||
"000000000000d", "000000000000e", "000000000000f",
|
||||
"0000000000010", "0000000000012", "0000000000011",
|
||||
]
|
||||
assert nwTree.projTree.undoLastMove() is True
|
||||
assert nwTree.getTreeFromHandle("000000000000d") == [
|
||||
"000000000000d", "000000000000e", "000000000000f",
|
||||
"0000000000010", "0000000000011", "0000000000012",
|
||||
]
|
||||
|
||||
# Root Folder
|
||||
# ===========
|
||||
|
||||
nwTree.setSelectedHandle("0000000000008")
|
||||
assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 0
|
||||
|
||||
# Move novel folder up
|
||||
assert nwTree.projTree.moveTreeItem(-1) is False
|
||||
assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 0
|
||||
|
||||
# Move novel folder down
|
||||
assert nwTree.projTree.moveTreeItem(1) is True
|
||||
assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 1
|
||||
|
||||
# Move novel folder up again
|
||||
assert nwTree.projTree.moveTreeItem(-1) is True
|
||||
assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 0
|
||||
|
||||
# Clean up
|
||||
# qtbot.stopForInteraction()
|
||||
nwGUI.closeProject()
|
||||
|
||||
# END Test testGuiProjTree_MoveItems
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd):
|
||||
"""Test adding and removing items from the project tree.
|
||||
"""
|
||||
# Block message box
|
||||
monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
|
||||
|
||||
nwTree = nwGUI.projView
|
||||
|
||||
# Try to run with no project
|
||||
assert nwTree.emptyTrash() is False
|
||||
assert nwTree.deleteItem() is False
|
||||
|
||||
# Create a project
|
||||
prjDir = os.path.join(fncDir, "project")
|
||||
buildTestProject(nwGUI, prjDir)
|
||||
|
||||
# Try emptying the trash already now, when there is no trash folder
|
||||
assert nwTree.emptyTrash() is False
|
||||
|
||||
# Add some files
|
||||
nwTree.setSelectedHandle("000000000000d")
|
||||
assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True
|
||||
assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True
|
||||
assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True
|
||||
assert nwTree.getTreeFromHandle("000000000000d") == [
|
||||
"000000000000d", "000000000000e", "000000000000f",
|
||||
"0000000000010", "0000000000011", "0000000000012",
|
||||
]
|
||||
|
||||
# Delete item without focus -> blocked
|
||||
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: False)
|
||||
nwTree.setSelectedHandle("0000000000012")
|
||||
assert nwTree.deleteItem() is False
|
||||
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
|
||||
|
||||
# No selection made
|
||||
nwTree.projTree.clearSelection()
|
||||
caplog.clear()
|
||||
assert nwTree.deleteItem() is False
|
||||
assert "no item to delete" in caplog.text
|
||||
|
||||
# Not a valid handle
|
||||
nwTree.projTree.clearSelection()
|
||||
caplog.clear()
|
||||
assert nwTree.deleteItem("0000000000000") is False
|
||||
assert "Could not find tree item" in caplog.text
|
||||
|
||||
# Delete Folder/Root
|
||||
# ==================
|
||||
|
||||
# Deleting non-empty folders is blocked
|
||||
assert nwTree.deleteItem("0000000000008") is False # Novel Root
|
||||
assert nwTree.deleteItem("000000000000a") is True # Character Root
|
||||
|
||||
# Delete File
|
||||
# ===========
|
||||
|
||||
# Block adding trash folder
|
||||
funcPointer = nwTree.projTree._addTrashRoot
|
||||
nwTree.projTree._addTrashRoot = lambda *a: None
|
||||
assert nwTree.deleteItem("0000000000012") is False
|
||||
nwTree.projTree._addTrashRoot = funcPointer
|
||||
|
||||
# Delete last two documents, which also adds the trash folder
|
||||
assert nwTree.deleteItem("0000000000012") is True
|
||||
assert nwTree.deleteItem("0000000000011") is True
|
||||
assert nwTree.getTreeFromHandle("000000000000d") == [
|
||||
"000000000000d", "000000000000e", "000000000000f",
|
||||
"0000000000010"
|
||||
]
|
||||
trashHandle = nwGUI.theProject.tree.trashRoot()
|
||||
assert nwTree.getTreeFromHandle(trashHandle) == [
|
||||
trashHandle, "0000000000012", "0000000000011"
|
||||
]
|
||||
|
||||
# Delete the first file again (permanent), and ask for permission
|
||||
# Also open the document in the editor, which should trigger a close
|
||||
assert os.path.isfile(os.path.join(prjDir, "content", "0000000000012.nwd"))
|
||||
assert "0000000000012" in nwGUI.theProject.tree
|
||||
assert nwGUI.docEditor.docHandle() is None
|
||||
assert nwGUI.openDocument("0000000000012") is True
|
||||
assert nwGUI.docEditor.docHandle() == "0000000000012"
|
||||
assert nwTree.deleteItem("0000000000012") is True
|
||||
assert nwGUI.docEditor.docHandle() is None
|
||||
assert not os.path.isfile(os.path.join(prjDir, "content", "0000000000012.nwd"))
|
||||
assert "0000000000012" not in nwGUI.theProject.tree
|
||||
assert nwTree.getTreeFromHandle(trashHandle) == [
|
||||
trashHandle, "0000000000011"
|
||||
]
|
||||
|
||||
# Delete the second file, and skip asking for permission
|
||||
assert os.path.isfile(os.path.join(prjDir, "content", "0000000000011.nwd"))
|
||||
assert "0000000000011" in nwGUI.theProject.tree
|
||||
assert nwTree.deleteItem("0000000000011", alreadyAsked=True) is True
|
||||
assert not os.path.isfile(os.path.join(prjDir, "content", "0000000000011.nwd"))
|
||||
assert "0000000000011" not in nwGUI.theProject.tree
|
||||
assert nwTree.getTreeFromHandle(trashHandle) == [trashHandle]
|
||||
|
||||
# Delete Folder
|
||||
# =============
|
||||
|
||||
trashHandle = nwGUI.theProject.tree.trashRoot()
|
||||
|
||||
# Add a folder with two files
|
||||
nwTree.setSelectedHandle("0000000000009")
|
||||
assert nwTree.projTree.newTreeItem(nwItemType.FOLDER) is True
|
||||
nwTree.setSelectedHandle("0000000000014")
|
||||
assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True
|
||||
assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True
|
||||
assert os.path.isfile(os.path.join(fncDir, "project", "content", "0000000000015.nwd"))
|
||||
assert os.path.isfile(os.path.join(fncDir, "project", "content", "0000000000016.nwd"))
|
||||
|
||||
# Delete the folder, which moves everything to Trash
|
||||
assert nwTree.getTreeFromHandle("0000000000014") == [
|
||||
"0000000000014", "0000000000015", "0000000000016"
|
||||
]
|
||||
assert nwTree.deleteItem("0000000000014") is True
|
||||
assert nwTree.getTreeFromHandle(trashHandle) == [
|
||||
trashHandle, "0000000000014", "0000000000015", "0000000000016"
|
||||
]
|
||||
assert os.path.isfile(os.path.join(fncDir, "project", "content", "0000000000015.nwd"))
|
||||
assert os.path.isfile(os.path.join(fncDir, "project", "content", "0000000000016.nwd"))
|
||||
|
||||
# Delete again, which should delete folder and all files
|
||||
assert nwTree.deleteItem("0000000000014") is True
|
||||
assert nwTree.getTreeFromHandle(trashHandle) == [trashHandle]
|
||||
assert not os.path.isfile(os.path.join(fncDir, "project", "content", "0000000000015.nwd"))
|
||||
assert not os.path.isfile(os.path.join(fncDir, "project", "content", "0000000000016.nwd"))
|
||||
|
||||
# Add an empty folder, which can be deleted with no further restrictions
|
||||
nwTree.setSelectedHandle("0000000000009")
|
||||
assert nwTree.projTree.newTreeItem(nwItemType.FOLDER) is True
|
||||
assert nwTree.getTreeFromHandle("0000000000009") == ["0000000000009", "0000000000017"]
|
||||
|
||||
nwTree.setSelectedHandle("0000000000017")
|
||||
assert nwTree.deleteItem("0000000000017") is True
|
||||
assert nwTree.getTreeFromHandle("0000000000009") == ["0000000000009"]
|
||||
|
||||
# Empty Trash
|
||||
# ===========
|
||||
|
||||
# Try to empty trash that is already empty
|
||||
assert nwTree.getTreeFromHandle(trashHandle) == [trashHandle]
|
||||
assert nwTree.emptyTrash() is False
|
||||
|
||||
# Move the two remaining scene documents to trash
|
||||
assert nwTree.deleteItem("000000000000f") is True
|
||||
assert nwTree.deleteItem("0000000000010") is True
|
||||
assert nwTree.getTreeFromHandle("000000000000d") == [
|
||||
"000000000000d", "000000000000e"
|
||||
]
|
||||
assert nwTree.getTreeFromHandle(trashHandle) == [
|
||||
trashHandle, "000000000000f", "0000000000010"
|
||||
]
|
||||
|
||||
# Empty trash, but select no on question
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No)
|
||||
assert nwTree.emptyTrash() is False
|
||||
|
||||
# Empty the trash proper
|
||||
assert nwTree.emptyTrash() is True
|
||||
assert nwTree.getTreeFromHandle(trashHandle) == [trashHandle]
|
||||
|
||||
# Try to delete a file, but block the underlying deletion of the file on disk
|
||||
assert os.path.isfile(os.path.join(fncDir, "project", "content", "000000000000e.nwd"))
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr("novelwriter.core.document.NWDoc.deleteDocument", lambda *a: False)
|
||||
assert nwTree.deleteItem("000000000000e") is True
|
||||
assert nwTree.deleteItem("000000000000e") is True
|
||||
assert os.path.isfile(os.path.join(fncDir, "project", "content", "000000000000e.nwd"))
|
||||
|
||||
# Delete proper
|
||||
assert nwTree.projTree._deleteTreeItem("000000000000e") is True
|
||||
assert not os.path.isfile(os.path.join(fncDir, "project", "content", "000000000000e.nwd"))
|
||||
|
||||
# Clean up
|
||||
# qtbot.stopForInteraction()
|
||||
nwGUI.closeProject()
|
||||
|
||||
# END Test testGuiProjTree_DeleteItems
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testGuiProjTree_ContextMenu(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd):
|
||||
"""Test the building of the project tree context menu. All this does
|
||||
is test that the menu builds. It doesn't open the actual menu,
|
||||
"""
|
||||
# Block message box
|
||||
monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
|
||||
monkeypatch.setattr(QMenu, "exec_", lambda *a: None)
|
||||
|
||||
# Create a project
|
||||
prjDir = os.path.join(fncDir, "project")
|
||||
buildTestProject(nwGUI, prjDir)
|
||||
|
||||
# Handles for new objects
|
||||
hNovelRoot = "0000000000008"
|
||||
hTitlePage = "000000000000c"
|
||||
hChapterDir = "000000000000d"
|
||||
hChapterFile = "000000000000e"
|
||||
hCharRoot = "000000000000a"
|
||||
hCharNote = "0000000000011"
|
||||
hNovelNote = "0000000000012"
|
||||
|
||||
projTree = nwGUI.projView.projTree
|
||||
projTree._getTreeItem(hNovelRoot).setExpanded(True)
|
||||
projTree._getTreeItem(hChapterDir).setExpanded(True)
|
||||
|
||||
projTree._addTrashRoot()
|
||||
hTrashRoot = projTree.theProject.tree.trashRoot()
|
||||
|
||||
projTree.setSelectedHandle(hCharRoot)
|
||||
projTree.newTreeItem(nwItemType.FILE)
|
||||
projTree.setSelectedHandle(hNovelRoot)
|
||||
projTree.newTreeItem(nwItemType.FILE, isNote=True)
|
||||
|
||||
def itemPos(tHandle):
|
||||
return projTree.visualItemRect(projTree._getTreeItem(tHandle)).center()
|
||||
|
||||
# No item under menu
|
||||
assert projTree._openContextMenu(projTree.viewport().rect().bottomRight()) is False
|
||||
|
||||
# Generate the possible menu combinarions
|
||||
assert projTree._openContextMenu(itemPos(hTrashRoot)) is True
|
||||
assert projTree._openContextMenu(itemPos(hNovelRoot)) is True
|
||||
assert projTree._openContextMenu(itemPos(hNovelNote)) is True
|
||||
assert projTree._openContextMenu(itemPos(hTitlePage)) is True
|
||||
assert projTree._openContextMenu(itemPos(hChapterDir)) is True
|
||||
assert projTree._openContextMenu(itemPos(hChapterFile)) is True
|
||||
assert projTree._openContextMenu(itemPos(hCharRoot)) is True
|
||||
assert projTree._openContextMenu(itemPos(hCharNote)) is True
|
||||
|
||||
# Check the keyboard shortcut handler as well
|
||||
projTree.setSelectedHandle(hNovelRoot)
|
||||
assert projTree.openContextOnSelected() is True
|
||||
projTree.clearSelection()
|
||||
assert projTree.openContextOnSelected() is False
|
||||
|
||||
# Direct Edit Functions
|
||||
# =====================
|
||||
# Trigger the dedicated functions the menu entries connect to
|
||||
nwItem = projTree.theProject.tree[hNovelNote]
|
||||
|
||||
# Toggle exported flag
|
||||
assert nwItem.isExported is True
|
||||
projTree._toggleItemExported(hNovelNote)
|
||||
assert nwItem.isExported is False
|
||||
|
||||
# Change item status
|
||||
assert nwItem.itemStatus == "s000000"
|
||||
projTree._changeItemStatus(hNovelNote, "s000001")
|
||||
assert nwItem.itemStatus == "s000001"
|
||||
|
||||
# Change item importance
|
||||
assert nwItem.itemImport == "i000004"
|
||||
projTree._changeItemImport(hNovelNote, "i000005")
|
||||
assert nwItem.itemImport == "i000005"
|
||||
|
||||
# Change item layout
|
||||
assert nwItem.itemLayout == nwItemLayout.NOTE
|
||||
projTree._changeItemLayout(hNovelNote, nwItemLayout.DOCUMENT)
|
||||
assert nwItem.itemLayout == nwItemLayout.DOCUMENT
|
||||
projTree._changeItemLayout(hNovelNote, nwItemLayout.NOTE)
|
||||
assert nwItem.itemLayout == nwItemLayout.NOTE
|
||||
|
||||
# qtbot.stop()
|
||||
|
||||
# END Test testGuiProjTree_ContextMenu
|
||||
|
||||
@@ -22,24 +22,25 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import time
|
||||
import pytest
|
||||
|
||||
from tools import buildTestProject
|
||||
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
from novelwriter.core import NWDoc
|
||||
from novelwriter.enum import nwItemClass, nwState
|
||||
from novelwriter.enum import nwState
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testGuiStatusBar_Main(qtbot, monkeypatch, nwGUI, fncProj):
|
||||
def testGuiStatusBar_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
|
||||
"""Test the the various features of the status bar.
|
||||
"""
|
||||
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
|
||||
|
||||
nwGUI.theProject.projTree.setSeed(42)
|
||||
assert nwGUI.newProject({"projPath": fncProj}) is True
|
||||
cHandle = nwGUI.theProject.newFile("A Note", nwItemClass.CHARACTER, "71ee45a3c0db9")
|
||||
buildTestProject(nwGUI, fncProj)
|
||||
cHandle = nwGUI.theProject.newFile("A Note", "000000000000a")
|
||||
newDoc = NWDoc(nwGUI.theProject, cHandle)
|
||||
newDoc.writeDocument("# A Note\n\n")
|
||||
nwGUI.treeView.revealNewTreeItem(cHandle)
|
||||
nwGUI.projView.revealNewTreeItem(cHandle)
|
||||
nwGUI.rebuildIndex(beQuiet=True)
|
||||
|
||||
# Reference Time
|
||||
@@ -90,10 +91,10 @@ def testGuiStatusBar_Main(qtbot, monkeypatch, nwGUI, fncProj):
|
||||
# Project Stats
|
||||
nwGUI.statusBar.mainConf.incNotesWCount = False
|
||||
nwGUI._updateStatusWordCount()
|
||||
assert nwGUI.statusBar.statsText.text() == "Words: 6 (+6)"
|
||||
assert nwGUI.statusBar.statsText.text() == "Words: 9 (+9)"
|
||||
nwGUI.statusBar.mainConf.incNotesWCount = True
|
||||
nwGUI._updateStatusWordCount()
|
||||
assert nwGUI.statusBar.statsText.text() == "Words: 8 (+8)"
|
||||
assert nwGUI.statusBar.statsText.text() == "Words: 11 (+11)"
|
||||
|
||||
# qtbot.stopForInteraction()
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ def testGuiTheme_Main(qtbot, monkeypatch, nwMinimal, tmpDir):
|
||||
"""Test the theme and icon classes.
|
||||
"""
|
||||
# Block message box
|
||||
monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes)
|
||||
|
||||
@@ -80,7 +81,7 @@ def testGuiTheme_Main(qtbot, monkeypatch, nwMinimal, tmpDir):
|
||||
assert thePalette.window().color() == QColor(54, 54, 54)
|
||||
assert thePalette.windowText().color() == QColor(174, 174, 174)
|
||||
assert thePalette.base().color() == QColor(62, 62, 62)
|
||||
assert thePalette.alternateBase().color() == QColor(67, 67, 67)
|
||||
assert thePalette.alternateBase().color() == QColor(78, 78, 78)
|
||||
assert thePalette.text().color() == QColor(174, 174, 174)
|
||||
assert thePalette.toolTipBase().color() == QColor(255, 255, 192)
|
||||
assert thePalette.toolTipText().color() == QColor(21, 21, 13)
|
||||
@@ -92,82 +93,82 @@ def testGuiTheme_Main(qtbot, monkeypatch, nwMinimal, tmpDir):
|
||||
assert thePalette.link().color() == QColor(44, 152, 247)
|
||||
assert thePalette.linkVisited().color() == QColor(44, 152, 247)
|
||||
|
||||
assert nwGUI.theTheme.statNone == [150, 152, 150]
|
||||
assert nwGUI.theTheme.statSaved == [39, 135, 78]
|
||||
assert nwGUI.theTheme.statUnsaved == [138, 32, 32]
|
||||
assert nwGUI.mainTheme.statNone == [150, 152, 150]
|
||||
assert nwGUI.mainTheme.statSaved == [39, 135, 78]
|
||||
assert nwGUI.mainTheme.statUnsaved == [138, 32, 32]
|
||||
|
||||
# Check Syntax Colours
|
||||
assert nwGUI.theTheme.colBack == [45, 45, 45]
|
||||
assert nwGUI.theTheme.colText == [204, 204, 204]
|
||||
assert nwGUI.theTheme.colLink == [102, 153, 204]
|
||||
assert nwGUI.theTheme.colHead == [102, 153, 204]
|
||||
assert nwGUI.theTheme.colHeadH == [102, 153, 204]
|
||||
assert nwGUI.theTheme.colEmph == [249, 145, 57]
|
||||
assert nwGUI.theTheme.colDialN == [242, 119, 122]
|
||||
assert nwGUI.theTheme.colDialD == [153, 204, 153]
|
||||
assert nwGUI.theTheme.colDialS == [255, 204, 102]
|
||||
assert nwGUI.theTheme.colHidden == [153, 153, 153]
|
||||
assert nwGUI.theTheme.colKey == [242, 119, 122]
|
||||
assert nwGUI.theTheme.colVal == [204, 153, 204]
|
||||
assert nwGUI.theTheme.colSpell == [242, 119, 122]
|
||||
assert nwGUI.theTheme.colError == [153, 204, 153]
|
||||
assert nwGUI.theTheme.colRepTag == [102, 204, 204]
|
||||
assert nwGUI.theTheme.colMod == [249, 145, 57]
|
||||
assert nwGUI.mainTheme.colBack == [45, 45, 45]
|
||||
assert nwGUI.mainTheme.colText == [204, 204, 204]
|
||||
assert nwGUI.mainTheme.colLink == [102, 153, 204]
|
||||
assert nwGUI.mainTheme.colHead == [102, 153, 204]
|
||||
assert nwGUI.mainTheme.colHeadH == [102, 153, 204]
|
||||
assert nwGUI.mainTheme.colEmph == [249, 145, 57]
|
||||
assert nwGUI.mainTheme.colDialN == [242, 119, 122]
|
||||
assert nwGUI.mainTheme.colDialD == [153, 204, 153]
|
||||
assert nwGUI.mainTheme.colDialS == [255, 204, 102]
|
||||
assert nwGUI.mainTheme.colHidden == [153, 153, 153]
|
||||
assert nwGUI.mainTheme.colKey == [242, 119, 122]
|
||||
assert nwGUI.mainTheme.colVal == [204, 153, 204]
|
||||
assert nwGUI.mainTheme.colSpell == [242, 119, 122]
|
||||
assert nwGUI.mainTheme.colError == [153, 204, 153]
|
||||
assert nwGUI.mainTheme.colRepTag == [102, 204, 204]
|
||||
assert nwGUI.mainTheme.colMod == [249, 145, 57]
|
||||
|
||||
# Test Icon class
|
||||
theIcons = nwGUI.theTheme.theIcons
|
||||
iconCache = nwGUI.mainTheme.iconCache
|
||||
novelwriter.CONFIG.guiIcons = "invalid"
|
||||
assert theIcons.updateTheme() is True
|
||||
assert iconCache.updateTheme() is True
|
||||
assert novelwriter.CONFIG.guiIcons == "typicons_light"
|
||||
|
||||
# Ask for a non-existent key
|
||||
anImg = theIcons.loadDecoration("nonsense", 20, 20)
|
||||
anImg = iconCache.loadDecoration("nonsense", 20, 20)
|
||||
assert isinstance(anImg, QPixmap)
|
||||
assert anImg.isNull()
|
||||
|
||||
# Add a non-existent file and request it
|
||||
theIcons.DECO_MAP["nonsense"] = "nofile.jpg"
|
||||
anImg = theIcons.loadDecoration("nonsense", 20, 20)
|
||||
iconCache.IMAGE_MAP["nonsense"] = "nofile.jpg"
|
||||
anImg = iconCache.loadDecoration("nonsense", 20, 20)
|
||||
assert isinstance(anImg, QPixmap)
|
||||
assert anImg.isNull()
|
||||
|
||||
# Get a real image, with different size parameters
|
||||
anImg = theIcons.loadDecoration("wiz-back", 20, None)
|
||||
anImg = iconCache.loadDecoration("wiz-back", 20, None)
|
||||
assert isinstance(anImg, QPixmap)
|
||||
assert not anImg.isNull()
|
||||
assert anImg.width() == 20
|
||||
assert anImg.height() >= 56
|
||||
|
||||
anImg = theIcons.loadDecoration("wiz-back", None, 70)
|
||||
anImg = iconCache.loadDecoration("wiz-back", None, 70)
|
||||
assert isinstance(anImg, QPixmap)
|
||||
assert not anImg.isNull()
|
||||
assert anImg.height() == 70
|
||||
assert anImg.width() >= 24
|
||||
|
||||
anImg = theIcons.loadDecoration("wiz-back", 30, 70)
|
||||
anImg = iconCache.loadDecoration("wiz-back", 30, 70)
|
||||
assert isinstance(anImg, QPixmap)
|
||||
assert not anImg.isNull()
|
||||
assert anImg.height() == 70
|
||||
assert anImg.width() == 30
|
||||
|
||||
anImg = theIcons.loadDecoration("wiz-back", None, None)
|
||||
anImg = iconCache.loadDecoration("wiz-back", None, None)
|
||||
assert isinstance(anImg, QPixmap)
|
||||
assert not anImg.isNull()
|
||||
assert anImg.height() >= 1500
|
||||
assert anImg.width() >= 500
|
||||
|
||||
# Load icons
|
||||
anIcon = theIcons.getIcon("nonsense")
|
||||
anIcon = iconCache.getIcon("nonsense")
|
||||
assert isinstance(anIcon, QIcon)
|
||||
assert anIcon.isNull()
|
||||
|
||||
anIcon = theIcons.getIcon("novelwriter")
|
||||
anIcon = iconCache.getIcon("novelwriter")
|
||||
assert isinstance(anIcon, QIcon)
|
||||
assert not anIcon.isNull()
|
||||
|
||||
# Check return empty icon if file not found
|
||||
theIcons.ICON_KEYS.add("testicon3")
|
||||
anIcon = theIcons.getIcon("testicon3")
|
||||
iconCache.ICON_KEYS.add("testicon3")
|
||||
anIcon = iconCache.getIcon("testicon3")
|
||||
assert isinstance(anIcon, QIcon)
|
||||
assert anIcon.isNull()
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
"""
|
||||
novelWriter – Lorem Ipsum Tool Tester
|
||||
=====================================
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2022, 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 pytest
|
||||
|
||||
from tools import getGuiItem, buildTestProject
|
||||
|
||||
from PyQt5.QtWidgets import QAction, QMessageBox
|
||||
|
||||
from novelwriter.tools import GuiLipsum
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testToolLipsum_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
|
||||
"""Test the Lorem Ipsum tool.
|
||||
"""
|
||||
# Block message box
|
||||
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
|
||||
|
||||
# Check that we cannot open when there is no project
|
||||
nwGUI.mainMenu.aLipsumText.activate(QAction.Trigger)
|
||||
assert getGuiItem("GuiLipsum") is None
|
||||
|
||||
# Create a new project
|
||||
buildTestProject(nwGUI, fncProj)
|
||||
assert nwGUI.openDocument("000000000000f") is True
|
||||
assert len(nwGUI.docEditor.getText()) == 15
|
||||
|
||||
# Open the tool
|
||||
nwGUI.mainMenu.aLipsumText.activate(QAction.Trigger)
|
||||
qtbot.waitUntil(lambda: getGuiItem("GuiLipsum") is not None, timeout=1000)
|
||||
|
||||
nwLipsum = getGuiItem("GuiLipsum")
|
||||
assert isinstance(nwLipsum, GuiLipsum)
|
||||
|
||||
# Insert paragraphs
|
||||
nwGUI.docEditor.setCursorPosition(100) # End of document
|
||||
nwLipsum.paraCount.setValue(2)
|
||||
nwLipsum._doInsert()
|
||||
theText = nwGUI.docEditor.getText()
|
||||
assert "Lorem ipsum" in theText
|
||||
assert len(theText) == 965
|
||||
|
||||
# Insert random paragraph
|
||||
nwGUI.docEditor.setCursorPosition(1000) # End of document
|
||||
nwLipsum.randSwitch.setChecked(True)
|
||||
nwLipsum.paraCount.setValue(1)
|
||||
nwLipsum._doInsert()
|
||||
theText = nwGUI.docEditor.getText()
|
||||
assert len(theText) > 965
|
||||
|
||||
# Close
|
||||
nwLipsum._doClose()
|
||||
|
||||
# qtbot.stopForInteraction()
|
||||
|
||||
# END Test testToolLipsum_Main
|
||||
@@ -19,14 +19,14 @@ 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 pytest
|
||||
import os
|
||||
import sys
|
||||
import pytest
|
||||
|
||||
from tools import getGuiItem
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QFileDialog, QWizard, QMessageBox
|
||||
from PyQt5.QtWidgets import QFileDialog, QWizard, QMessageBox, QDialog
|
||||
|
||||
from novelwriter.enum import nwItemClass
|
||||
from novelwriter.tools.projwizard import (
|
||||
@@ -41,8 +41,8 @@ stepDelay = 20
|
||||
|
||||
@pytest.mark.gui
|
||||
@pytest.mark.skipif(sys.platform.startswith("darwin"), reason="Not running on Darwin")
|
||||
def testToolProjectWizard_Main(qtbot, monkeypatch, nwGUI, nwMinimal):
|
||||
"""Test the new project wizard.
|
||||
def testToolProjectWizard_Handling(qtbot, monkeypatch, nwGUI, nwMinimal):
|
||||
"""Test the launch of the project wizard.
|
||||
Disabled for macOS because the test segfaults on QWizard.show()
|
||||
"""
|
||||
# Block message box
|
||||
@@ -55,173 +55,207 @@ def testToolProjectWizard_Main(qtbot, monkeypatch, nwGUI, nwMinimal):
|
||||
|
||||
# New with a project open should cause an error
|
||||
assert nwGUI.openProject(nwMinimal)
|
||||
assert not nwGUI.newProject()
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(nwGUI, "closeProject", lambda *a: False)
|
||||
assert nwGUI.newProject() is False
|
||||
|
||||
# Close project, but call with invalid path
|
||||
assert nwGUI.closeProject()
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(nwGUI, "showNewProjectDialog", lambda *a: None)
|
||||
assert not nwGUI.newProject()
|
||||
assert nwGUI.newProject() is False
|
||||
|
||||
# Now, with an empty dictionary
|
||||
mp.setattr(nwGUI, "showNewProjectDialog", lambda *a: {})
|
||||
assert not nwGUI.newProject()
|
||||
assert nwGUI.newProject() is False
|
||||
|
||||
# Now, with a non-empty folder
|
||||
mp.setattr(nwGUI, "showNewProjectDialog", lambda *a: {"projPath": nwMinimal})
|
||||
assert not nwGUI.newProject()
|
||||
assert nwGUI.newProject() is False
|
||||
|
||||
##
|
||||
# Test the Wizard
|
||||
# Test the Wizard Launching
|
||||
##
|
||||
|
||||
monkeypatch.setattr(GuiProjectWizard, "exec_", lambda *a: None)
|
||||
nwGUI.mainConf.lastPath = " "
|
||||
monkeypatch.setattr(GuiProjectWizard, "exec_", lambda *a: None)
|
||||
|
||||
nwGUI.closeProject()
|
||||
nwGUI.showNewProjectDialog()
|
||||
result = nwGUI.showNewProjectDialog()
|
||||
qtbot.waitUntil(lambda: getGuiItem("GuiProjectWizard") is not None, timeout=1000)
|
||||
|
||||
nwWiz = getGuiItem("GuiProjectWizard")
|
||||
assert isinstance(nwWiz, GuiProjectWizard)
|
||||
nwWiz.show()
|
||||
|
||||
qtbot.wait(stepDelay)
|
||||
qtbot.mouseClick(nwWiz.button(QWizard.CancelButton), Qt.LeftButton)
|
||||
assert result is None
|
||||
|
||||
for wStep in range(4):
|
||||
# This does not actually create the project, it just generates the
|
||||
# dictionary that defines it.
|
||||
|
||||
# Intro Page
|
||||
introPage = nwWiz.currentPage()
|
||||
assert isinstance(introPage, ProjWizardIntroPage)
|
||||
assert not nwWiz.button(QWizard.NextButton).isEnabled()
|
||||
|
||||
qtbot.wait(stepDelay)
|
||||
for c in ("Test Minimal %d" % wStep):
|
||||
qtbot.keyClick(introPage.projName, c, delay=typeDelay)
|
||||
|
||||
qtbot.wait(stepDelay)
|
||||
for c in "Minimal Novel":
|
||||
qtbot.keyClick(introPage.projTitle, c, delay=typeDelay)
|
||||
|
||||
qtbot.wait(stepDelay)
|
||||
for c in "Jane Doe":
|
||||
qtbot.keyClick(introPage.projAuthors, c, delay=typeDelay)
|
||||
|
||||
# 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()
|
||||
|
||||
if wStep == 0:
|
||||
# Check invalid path first, the first time we reach here
|
||||
monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *a, **kw: "")
|
||||
qtbot.wait(stepDelay)
|
||||
qtbot.mouseClick(storagePage.browseButton, Qt.LeftButton, delay=100)
|
||||
assert storagePage.projPath.text() == ""
|
||||
|
||||
# Then, we always return nwMinimal as path
|
||||
monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *a, **kw: nwMinimal)
|
||||
|
||||
qtbot.wait(stepDelay)
|
||||
qtbot.mouseClick(storagePage.browseButton, Qt.LeftButton, delay=100)
|
||||
projPath = os.path.join(nwMinimal, "Test Minimal %d" % wStep)
|
||||
assert storagePage.projPath.text() == projPath
|
||||
|
||||
# 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.popCustom.setChecked(True)
|
||||
elif wStep == 3:
|
||||
popPage.popSample.setChecked(True)
|
||||
|
||||
qtbot.wait(stepDelay)
|
||||
qtbot.mouseClick(nwWiz.button(QWizard.NextButton), Qt.LeftButton)
|
||||
|
||||
# Custom Page
|
||||
if wStep == 1 or wStep == 2:
|
||||
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)
|
||||
|
||||
if wStep == 2:
|
||||
customPage.numChapters.setValue(0)
|
||||
customPage.numScenes.setValue(10)
|
||||
customPage.chFolders.setChecked(False)
|
||||
|
||||
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() # But we don't click it
|
||||
|
||||
# Check Data
|
||||
projData = nwGUI._assembleProjectWizardData(nwWiz)
|
||||
assert projData["projName"] == "Test Minimal %d" % wStep
|
||||
assert projData["projTitle"] == "Minimal Novel"
|
||||
assert projData["projAuthors"] == "Jane Doe"
|
||||
assert projData["projPath"] == projPath
|
||||
assert projData["popMinimal"] == (wStep == 0)
|
||||
assert projData["popCustom"] == (wStep == 1 or wStep == 2)
|
||||
assert projData["popSample"] == (wStep == 3)
|
||||
if wStep == 1 or wStep == 2:
|
||||
assert projData["addRoots"] == [
|
||||
nwItemClass.PLOT,
|
||||
nwItemClass.CHARACTER,
|
||||
nwItemClass.WORLD,
|
||||
nwItemClass.TIMELINE,
|
||||
nwItemClass.OBJECT,
|
||||
nwItemClass.ENTITY,
|
||||
]
|
||||
if wStep == 1:
|
||||
assert projData["numChapters"] == 5
|
||||
assert projData["numScenes"] == 5
|
||||
assert projData["chFolders"]
|
||||
else:
|
||||
assert projData["numChapters"] == 0
|
||||
assert projData["numScenes"] == 10
|
||||
assert not projData["chFolders"]
|
||||
else:
|
||||
assert projData["addRoots"] == []
|
||||
assert projData["numChapters"] == 0
|
||||
assert projData["numScenes"] == 0
|
||||
assert not projData["chFolders"]
|
||||
|
||||
# Restart the wizard for next iteration
|
||||
nwWiz.restart()
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(GuiProjectWizard, "result", lambda *a: QDialog.Accepted)
|
||||
result = nwGUI.showNewProjectDialog()
|
||||
nwWiz.button(QWizard.CancelButton).click()
|
||||
assert isinstance(result, dict)
|
||||
|
||||
nwWiz.reject()
|
||||
nwWiz.close()
|
||||
|
||||
# qtbot.stopForInteraction()
|
||||
|
||||
# END Test testToolProjectWizard_Main
|
||||
# END Test testToolProjectWizard_Handling
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
@pytest.mark.parametrize("prjType", ["minimal", "custom1", "custom2", "sample"])
|
||||
@pytest.mark.skipif(sys.platform.startswith("darwin"), reason="Not running on Darwin")
|
||||
def testToolProjectWizard_Run(qtbot, monkeypatch, nwGUI, fncDir, prjType):
|
||||
"""Test the new project wizard with a set of selection scenarios.
|
||||
"""
|
||||
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(GuiProjectWizard, "exec_", lambda *a: None)
|
||||
|
||||
nwGUI.mainConf.lastPath = " "
|
||||
nwWiz = GuiProjectWizard(nwGUI)
|
||||
nwWiz.show()
|
||||
qtbot.wait(stepDelay)
|
||||
|
||||
# Intro Page
|
||||
# ==========
|
||||
|
||||
introPage = nwWiz.currentPage()
|
||||
assert isinstance(introPage, ProjWizardIntroPage)
|
||||
assert not nwWiz.button(QWizard.NextButton).isEnabled()
|
||||
|
||||
introPage.projName.setText("Test Wizard")
|
||||
introPage.projTitle.setText("My Novel")
|
||||
introPage.projAuthors.setPlainText("Jane Doe")
|
||||
|
||||
# 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()
|
||||
assert storagePage.errLabel.text() == ""
|
||||
|
||||
# Set an invalid path
|
||||
storagePage.projPath.setText(os.path.join(fncDir, "not", "a", "path"))
|
||||
assert not nwWiz.button(QWizard.NextButton).isEnabled()
|
||||
assert storagePage.errLabel.text().startswith("Error")
|
||||
|
||||
# Set an existing path
|
||||
storagePage.projPath.setText(fncDir)
|
||||
assert not nwWiz.button(QWizard.NextButton).isEnabled()
|
||||
assert storagePage.errLabel.text().startswith("Error")
|
||||
|
||||
# Return a non-result from browse
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(QFileDialog, "getExistingDirectory", lambda *a, **k: "")
|
||||
qtbot.mouseClick(storagePage.browseButton, Qt.LeftButton, delay=100)
|
||||
assert storagePage.errLabel.text() == ""
|
||||
|
||||
# Let the browse feature handle it
|
||||
projPath = os.path.join(fncDir, "Test Wizard")
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(QFileDialog, "getExistingDirectory", lambda *a, **k: fncDir)
|
||||
qtbot.mouseClick(storagePage.browseButton, Qt.LeftButton, delay=100)
|
||||
|
||||
assert storagePage.projPath.text() == projPath
|
||||
assert storagePage.errLabel.text() == ""
|
||||
|
||||
# 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 prjType.startswith("minimal"):
|
||||
popPage.popMinimal.setChecked(True)
|
||||
elif prjType.startswith("custom"):
|
||||
popPage.popCustom.setChecked(True)
|
||||
elif prjType.startswith("sample"):
|
||||
popPage.popSample.setChecked(True)
|
||||
|
||||
qtbot.wait(stepDelay)
|
||||
qtbot.mouseClick(nwWiz.button(QWizard.NextButton), Qt.LeftButton)
|
||||
|
||||
# Custom Page
|
||||
# ===========
|
||||
if prjType.startswith("custom"):
|
||||
|
||||
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.addNotes.setChecked(True)
|
||||
|
||||
if prjType == "custom2":
|
||||
customPage.numChapters.setValue(0)
|
||||
customPage.numScenes.setValue(10)
|
||||
|
||||
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() # But we don't click it
|
||||
|
||||
# Check Data
|
||||
# ==========
|
||||
|
||||
projData = nwGUI._assembleProjectWizardData(nwWiz)
|
||||
assert projData["projName"] == "Test Wizard"
|
||||
assert projData["projTitle"] == "My Novel"
|
||||
assert projData["projAuthors"] == "Jane Doe"
|
||||
assert projData["projPath"] == projPath
|
||||
assert projData["popMinimal"] == prjType.startswith("minimal")
|
||||
assert projData["popCustom"] == prjType.startswith("custom")
|
||||
assert projData["popSample"] == prjType.startswith("sample")
|
||||
if prjType.startswith("custom"):
|
||||
assert projData["addRoots"] == [
|
||||
nwItemClass.PLOT,
|
||||
nwItemClass.CHARACTER,
|
||||
nwItemClass.WORLD,
|
||||
]
|
||||
if prjType == "custom1":
|
||||
assert projData["numChapters"] == 5
|
||||
assert projData["numScenes"] == 5
|
||||
assert projData["addNotes"] is True
|
||||
else:
|
||||
assert projData["numChapters"] == 0
|
||||
assert projData["numScenes"] == 10
|
||||
assert projData["addNotes"] is True
|
||||
else:
|
||||
assert projData["addRoots"] == []
|
||||
assert projData["numChapters"] == 0
|
||||
assert projData["numScenes"] == 0
|
||||
assert projData["addNotes"] is False
|
||||
|
||||
# Cleanup
|
||||
nwWiz.reject()
|
||||
nwWiz.close()
|
||||
|
||||
# qtbot.stopForInteraction()
|
||||
|
||||
# END Test testToolProjectWizard_Run
|
||||
|
||||
@@ -23,8 +23,8 @@ import pytest
|
||||
import json
|
||||
import os
|
||||
|
||||
from tools import getGuiItem, writeFile
|
||||
from mock import causeOSError
|
||||
from tools import getGuiItem, writeFile, buildTestProject
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QAction, QFileDialog, QMessageBox
|
||||
@@ -48,7 +48,7 @@ def testToolWritingStats_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj):
|
||||
monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes)
|
||||
|
||||
# Create a project to work on
|
||||
assert nwGUI.newProject({"projPath": fncProj})
|
||||
buildTestProject(nwGUI, fncProj)
|
||||
qtbot.wait(100)
|
||||
assert nwGUI.saveProject()
|
||||
sessFile = os.path.join(fncProj, "meta", nwFiles.SESS_STATS)
|
||||
|
||||
@@ -20,10 +20,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
import shutil
|
||||
|
||||
from PyQt5.QtWidgets import qApp
|
||||
|
||||
XML_IGNORE = ("<novelWriterXML", "<saveCount", "<autoCount", "<editTime")
|
||||
|
||||
|
||||
def cmpFiles(fileOne, fileTwo, ignoreLines=None, ignoreStart=None):
|
||||
"""Compare two files, but optionally ignore lines given by a list.
|
||||
@@ -119,3 +122,56 @@ def cleanProject(projPath):
|
||||
os.unlink(tocFile)
|
||||
|
||||
return
|
||||
|
||||
|
||||
def buildTestProject(theObject, projPath):
|
||||
"""Build a standard test project in projPath using theProject
|
||||
object as the parent.
|
||||
"""
|
||||
from novelwriter.enum import nwItemClass
|
||||
from novelwriter.core import NWProject, NWDoc
|
||||
|
||||
if isinstance(theObject, NWProject):
|
||||
theGUI = None
|
||||
theProject = theObject
|
||||
else:
|
||||
theGUI = theObject
|
||||
theProject = theObject.theProject
|
||||
|
||||
theProject.clearProject()
|
||||
theProject.setProjectPath(projPath, newProject=True)
|
||||
theProject.setProjectName("New Project")
|
||||
theProject.setBookTitle("New Novel")
|
||||
theProject.setBookAuthors("Jane Doe")
|
||||
|
||||
# Creating a minimal project with a few root folders and a
|
||||
# single chapter folder with a single file.
|
||||
xHandle = {}
|
||||
xHandle[1] = theProject.newRoot(nwItemClass.NOVEL, "Novel")
|
||||
xHandle[2] = theProject.newRoot(nwItemClass.PLOT, "Plot")
|
||||
xHandle[3] = theProject.newRoot(nwItemClass.CHARACTER, "Characters")
|
||||
xHandle[4] = theProject.newRoot(nwItemClass.WORLD, "World")
|
||||
xHandle[5] = theProject.newFile("Title Page", xHandle[1])
|
||||
xHandle[6] = theProject.newFolder("New Chapter", xHandle[1])
|
||||
xHandle[7] = theProject.newFile("New Chapter", xHandle[6])
|
||||
xHandle[8] = theProject.newFile("New Scene", xHandle[6])
|
||||
|
||||
aDoc = NWDoc(theProject, xHandle[5])
|
||||
aDoc.writeDocument("#! New Novel\n\n>> By Jane DOe <<\n")
|
||||
|
||||
aDoc = NWDoc(theProject, xHandle[7])
|
||||
aDoc.writeDocument("## %s\n\n" % theProject.tr("New Chapter"))
|
||||
|
||||
aDoc = NWDoc(theProject, xHandle[8])
|
||||
aDoc.writeDocument("### %s\n\n" % theProject.tr("New Scene"))
|
||||
|
||||
theProject.projOpened = time.time()
|
||||
theProject.setProjectChanged(True)
|
||||
theProject.saveProject(autoSave=True)
|
||||
|
||||
if theGUI is not None:
|
||||
theGUI.hasProject = True
|
||||
theGUI.rebuildTrees()
|
||||
theGUI.rebuildIndex(beQuiet=True)
|
||||
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user