Merge branch 'main' into dev

This commit is contained in:
Veronica K. B. Olsen
2020-12-21 21:51:35 +01:00
21 changed files with 303 additions and 378 deletions
+48 -42
View File
@@ -7,9 +7,10 @@ Novel Projects
A novelWriter project requires a dedicated folder for storing its files on the local file system.
See the :ref:`a_tech` page for further details on how files are organised.
A new project can be created from the :guilabel:`Project` menu by selecting :guilabel:`New Project`.
A list of recently opened projects is maintained, and displayed in the :guilabel:`Open Project`
dialog. A project can be removed from this list by selecting it and pressing the :kbd:`Del` key.
A new project can be created from the :guilabel:`Project` menu by selecting
:guilabel:`New Project`. A list of recently opened projects is maintained, and displayed in the
:guilabel:`Open Project` dialog. A project can be removed from this list by selecting it and
pressing the :kbd:`Del` key.
The project specific settings are available in :guilabel:`Project Settings` in the
:guilabel:`Project` menu. See further details below in the :ref:`a_proj_settings` section.
@@ -23,15 +24,15 @@ Project Roots
Projects are structured into a set of top level folders called *root folders*. They are visible in
the project tree at the left side of the main window.
The core novel files go into a root folder of type :guilabel:`Novel`. Other supporting files go into
the other root folders. These other root folder types are intended for your notes on the various
elements of your story. Using these is of course entirely optional.
The core novel files go into a root folder of type :guilabel:`Novel`. Other supporting files go
into the other root folders. These other root folder types are intended for your notes on the
various elements of your story. Using these is of course entirely optional.
A new project will not have all of the root folders present, but you can add the ones you want from
:guilabel:`Create Root Folder` in the :guilabel:`Project` menu.
The root folders are intended for the following use, but aside from the :guilabel:`Novel` folder, no
restrictions are enforced by the application. You can use them however you want.
The root folders are intended for the following use, but aside from the :guilabel:`Novel` folder,
no restrictions are enforced by the application. You can use them however you want.
:guilabel:`Novel`
This is the root folder of all text that goes into the final novel. This class of files have
@@ -40,8 +41,8 @@ restrictions are enforced by the application. You can use them however you want.
:guilabel:`Plot`
This is the root folder where main plots can be outlined. It is optional, but adding at least
dummy files can be useful in order to tag plot elements for the Outline view. Tags in this folder
can be references using the ``@plot`` keyword.
dummy files can be useful in order to tag plot elements for the Outline view. Tags in this
folder can be references using the ``@plot`` keyword.
:guilabel:`Characters`
Character files go in this root folder. These are especially important if one wants to use the
@@ -86,8 +87,8 @@ Deleted Documents
-----------------
Deleted document files will be moved into a special :guilabel:`Trash` root folder. Files in the
trash folder can then be deleted permanently, either individually, or by emptying the trash from the
menu. Files in this folder are removed from the project index and cannot be referenced.
trash folder can then be deleted permanently, either individually, or by emptying the trash from
the menu. Files in this folder are removed from the project index and cannot be referenced.
Folders and root folders can only be deleted when they are empty. Recursive deletion is not
supported. A document file or a folder can be deleted from the :guilabel:`project` menu, or by
@@ -106,25 +107,30 @@ folder, only files. If you need folders in it to organise your files, you can of
ones there.
You can drag any file to this folder and preserve its settings. The file will always be excluded
from the :guilabel:`Build Novel Project` builds. The file is also removed from the project index, so
the tags and references defined in it will not show up anywhere else.
from the :guilabel:`Build Novel Project` builds. The file is also removed from the project index,
so the tags and references defined in it will not show up anywhere else.
.. _a_proj_roots_orph:
Orphaned Documents
------------------
Recovered Documents
-------------------
If novelWriter crashes or otherwise exits without saving the project state, or if you're using a
file synchronisation tool that runs out of sync, there may be files in the project folder that isn't
tracked in the core project file. These files, when discovered, are handled by the Orphaned
Documents routine.
file synchronisation tool that runs out of sync, there may be files in the project folder that
aren't tracked in the core project file. These files, when discovered, are recovered and added back
into the project if possible.
Files that are discovered in the project folder, but not in the project, will be re-added to the
project tree in a special :guilabel:`Orphaned Items` root folder next time the application is
started. These orphaned files will not have most of the meta data preserved, although novelWriter
will try to restore the file label it had in the project tree. Other information will have to be set
again, and the files moved back to the correct location in the project tree.
The discovered files are scanned for meta information that gives clues as to where the file may
previously have been located in the project. The project loading routines will try to put them back
as close as possible to this location if it still exists. Generally, it will be appended to the end
of the folder where it previously was located. If that folder doesn't exist, it will try to add it
to the correct root folder. If it cannot figure out which root folder is correct, the file will be
added to the :guilabel:`Novel` root folder.
If the title of the file can be recovered, the word "Recovered:" will be added as a prefix. If the
title cannot be determined, the file will be named "Recovered File N" where N is a sequential
number.
.. _a_proj_roots_lock:
@@ -193,16 +199,16 @@ Word Counts
A character, word and paragraph count is maintained for each file, as well as dor each section of a
file defined by a header. The word count, and change of words in the current session, is displayed
in the footer of any document open in the editor, and all stats are shown in the details panel below
the project tree for any file selected.
in the footer of any document open in the editor, and all stats are shown in the details panel
below the project tree for any file selected.
The word counts are not updated in real time, but runs in the background every five seconds for as
long as the document is being actively edited.
A total project word count is displayed in the status bar. The total count depends on the sum of the
values in the project tree, which again depend on an up to date index. If the counts seem wrong, a
full project word recount can be initiated by rebuilding the project's index. Either form the
:guilabel:`Tools` menu, or by pressing :kbd:`F9`.
A total project word count is displayed in the status bar. The total count depends on the sum of
the values in the project tree, which again depend on an up to date index. If the counts seem
wrong, a full project word recount can be initiated by rebuilding the project's index. Either form
the :guilabel:`Tools` menu, or by pressing :kbd:`F9`.
.. _a_proj_settings:
@@ -242,9 +248,9 @@ many words exist in the entire project.
Status and Importance Tabs
--------------------------
Each file of type "Novel" can be given a status level, signified by a coloured icon and each file of
the remaining types can be given an importance level. These are colour coded icons and labels that
can be applied to each file.
Each file of type "Novel" can be given a status level, signified by a coloured icon and each file
of the remaining types can be given an importance level. These are colour coded icons and labels
that can be applied to each file.
These are purely there for the user's convenience, and you are not required to use them for any
other feature to work. No other part of novelWriter accesses this information. The intention is to
@@ -266,8 +272,8 @@ also be applied to exports.
.. note::
A keyword cannot contain any spaces. The angle brackets are added by default, and when used in
the text are a part of the keyword to be replaced. This is to ensure that parts of the text isn't
unintentionally replaced by the content of the list.
the text are a part of the keyword to be replaced. This is to ensure that parts of the text
isn't unintentionally replaced by the content of the list.
.. _a_proj_backup:
@@ -275,22 +281,22 @@ also be applied to exports.
Backup
======
An automatic backup system is built into novelWriter. In order to use it, a backup path to where the
backup files are to be stored must to be provided in :guilabel:`Preferences`.
An automatic backup system is built into novelWriter. In order to use it, a backup path to where
the backup files are to be stored must to be provided in :guilabel:`Preferences`.
Backups can be run automatically when a project is closed, which also implies it is run when the
application is closed. Backups are date stamped zip files of the entire project folder, and are
stored in a subfolder of the backup path with the same name as the project :guilabel:`Working Title`
set in :ref:`a_proj_settings`.
stored in a subfolder of the backup path with the same name as the project :guilabel:`Working
Title` set in :ref:`a_proj_settings`.
The backup feature, when configured, can also be run manually from the :guilabel:`Tools` menu.
It is also possible to dissable automated backup for a given project in :guilabel:`Project
Settings`.
.. note::
For the backup to be able to run, the :guilabel:`Working Title` must be set in :guilabel:`Project
Settings`. This value is used to generate the folder name for the zip files. Without it, the
backup will not run at all, but produce a warning message.
For the backup to be able to run, the :guilabel:`Working Title` must be set in
:guilabel:`Project Settings`. This value is used to generate the folder name for the zip files.
Without it, the backup will not run at all, but produce a warning message.
.. _a_proj_stats:
@@ -29,7 +29,6 @@ cls_archive = delete.svg
cls_trash = trash.svg
proj_document = document-text.svg
proj_folder = folder.svg
proj_orphan = warning.svg
doc_h1 = heading1.svg
doc_h2 = heading2.svg
doc_h3 = heading3.svg
@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg4695"
viewBox="0 0 24 24"
height="24"
width="24"
version="1.2">
<metadata
id="metadata4701">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs4699" />
<path
style="stroke-width:1.01740682;fill:#ffff84;fill-opacity:1"
id="path4693"
d="M 21.330638,15.197201 15.315729,5.1716738 C 14.526221,3.8561667 13.318559,3.1032856 12,3.1032856 c -1.31856,0 -2.5262214,0.7528811 -3.3157291,2.0663534 L 2.6693614,15.197201 c -0.7996818,1.331785 -0.8871787,2.752085 -0.2390906,3.896668 0.6470708,1.1456 1.9106901,1.802845 3.4652878,1.802845 H 18.104441 c 1.554598,0 2.818217,-0.657245 3.465288,-1.801828 0.648088,-1.144582 0.560591,-2.564882 -0.239091,-3.897685 z M 12,17.385643 c -0.868866,0 -1.576981,-0.707098 -1.576981,-1.575963 0,-0.869883 0.707098,-1.577998 1.576981,-1.577998 0.869882,0 1.57698,0.708115 1.57698,1.577998 0,0.868865 -0.708115,1.575963 -1.57698,1.575963 z m 1.661425,-7.5532283 c -0.01119,0.03154 -1.425387,3.5283663 -1.425387,3.5283663 -0.03866,0.09564 -0.132263,0.158716 -0.235021,0.158716 -0.102758,0 -0.19636,-0.06308 -0.235021,-0.158716 L 10.350783,9.8629369 c -0.09157,-0.2370558 -0.131245,-0.4507113 -0.131245,-0.6664015 0,-0.9817977 0.798664,-1.7804621 1.780462,-1.7804621 0.981797,0 1.780462,0.7986644 1.780462,1.7804621 0,0.2156902 -0.03968,0.4293457 -0.119037,0.6358793 z" />
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

@@ -29,7 +29,6 @@ cls_archive = delete.svg
cls_trash = trash.svg
proj_document = document-text.svg
proj_folder = folder.svg
proj_orphan = warning.svg
doc_h1 = heading1.svg
doc_h2 = heading2.svg
doc_h3 = heading3.svg
@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg4695"
viewBox="0 0 24 24"
height="24"
width="24"
version="1.2">
<metadata
id="metadata4701">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs4699" />
<path
style="stroke-width:1.01740682;fill:#ad8700;fill-opacity:1"
id="path4693"
d="M 21.330638,15.197201 15.315729,5.1716738 C 14.526221,3.8561667 13.318559,3.1032856 12,3.1032856 c -1.31856,0 -2.5262214,0.7528811 -3.3157291,2.0663534 L 2.6693614,15.197201 c -0.7996818,1.331785 -0.8871787,2.752085 -0.2390906,3.896668 0.6470708,1.1456 1.9106901,1.802845 3.4652878,1.802845 H 18.104441 c 1.554598,0 2.818217,-0.657245 3.465288,-1.801828 0.648088,-1.144582 0.560591,-2.564882 -0.239091,-3.897685 z M 12,17.385643 c -0.868866,0 -1.576981,-0.707098 -1.576981,-1.575963 0,-0.869883 0.707098,-1.577998 1.576981,-1.577998 0.869882,0 1.57698,0.708115 1.57698,1.577998 0,0.868865 -0.708115,1.575963 -1.57698,1.575963 z m 1.661425,-7.5532283 c -0.01119,0.03154 -1.425387,3.5283663 -1.425387,3.5283663 -0.03866,0.09564 -0.132263,0.158716 -0.235021,0.158716 -0.102758,0 -0.19636,-0.06308 -0.235021,-0.158716 L 10.350783,9.8629369 c -0.09157,-0.2370558 -0.131245,-0.4507113 -0.131245,-0.6664015 0,-0.9817977 0.798664,-1.7804621 1.780462,-1.7804621 0.981797,0 1.780462,0.7986644 1.780462,1.7804621 0,0.2156902 -0.03968,0.4293457 -0.119037,0.6358793 z" />
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

@@ -29,7 +29,6 @@ cls_archive = delete.svg
cls_trash = trash.svg
proj_document = document-text.svg
proj_folder = folder.svg
proj_orphan = warning.svg
doc_h1 = heading1.svg
doc_h2 = heading2.svg
doc_h3 = heading3.svg
@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg4695"
viewBox="0 0 24 24"
height="24"
width="24"
version="1.2">
<metadata
id="metadata4701">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs4699" />
<path
style="stroke-width:1.01740682;fill:#aeaeae;fill-opacity:1"
id="path4693"
d="M 21.330638,15.197201 15.315729,5.1716738 C 14.526221,3.8561667 13.318559,3.1032856 12,3.1032856 c -1.31856,0 -2.5262214,0.7528811 -3.3157291,2.0663534 L 2.6693614,15.197201 c -0.7996818,1.331785 -0.8871787,2.752085 -0.2390906,3.896668 0.6470708,1.1456 1.9106901,1.802845 3.4652878,1.802845 H 18.104441 c 1.554598,0 2.818217,-0.657245 3.465288,-1.801828 0.648088,-1.144582 0.560591,-2.564882 -0.239091,-3.897685 z M 12,17.385643 c -0.868866,0 -1.576981,-0.707098 -1.576981,-1.575963 0,-0.869883 0.707098,-1.577998 1.576981,-1.577998 0.869882,0 1.57698,0.708115 1.57698,1.577998 0,0.868865 -0.708115,1.575963 -1.57698,1.575963 z m 1.661425,-7.5532283 c -0.01119,0.03154 -1.425387,3.5283663 -1.425387,3.5283663 -0.03866,0.09564 -0.132263,0.158716 -0.235021,0.158716 -0.102758,0 -0.19636,-0.06308 -0.235021,-0.158716 L 10.350783,9.8629369 c -0.09157,-0.2370558 -0.131245,-0.4507113 -0.131245,-0.6664015 0,-0.9817977 0.798664,-1.7804621 1.780462,-1.7804621 0.981797,0 1.780462,0.7986644 1.780462,1.7804621 0,0.2156902 -0.03968,0.4293457 -0.119037,0.6358793 z" />
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

@@ -29,7 +29,6 @@ cls_archive = delete.svg
cls_trash = trash.svg
proj_document = document-text.svg
proj_folder = folder.svg
proj_orphan = warning.svg
doc_h1 = heading1.svg
doc_h2 = heading2.svg
doc_h3 = heading3.svg
@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg4695"
viewBox="0 0 24 24"
height="24"
width="24"
version="1.2">
<metadata
id="metadata4701">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs4699" />
<path
style="stroke-width:1.01740682;fill:#000000;fill-opacity:0.78039217"
id="path4693"
d="M 21.330638,15.197201 15.315729,5.1716738 C 14.526221,3.8561667 13.318559,3.1032856 12,3.1032856 c -1.31856,0 -2.5262214,0.7528811 -3.3157291,2.0663534 L 2.6693614,15.197201 c -0.7996818,1.331785 -0.8871787,2.752085 -0.2390906,3.896668 0.6470708,1.1456 1.9106901,1.802845 3.4652878,1.802845 H 18.104441 c 1.554598,0 2.818217,-0.657245 3.465288,-1.801828 0.648088,-1.144582 0.560591,-2.564882 -0.239091,-3.897685 z M 12,17.385643 c -0.868866,0 -1.576981,-0.707098 -1.576981,-1.575963 0,-0.869883 0.707098,-1.577998 1.576981,-1.577998 0.869882,0 1.57698,0.708115 1.57698,1.577998 0,0.868865 -0.708115,1.575963 -1.57698,1.575963 z m 1.661425,-7.5532283 c -0.01119,0.03154 -1.425387,3.5283663 -1.425387,3.5283663 -0.03866,0.09564 -0.132263,0.158716 -0.235021,0.158716 -0.102758,0 -0.19636,-0.06308 -0.235021,-0.158716 L 10.350783,9.8629369 c -0.09157,-0.2370558 -0.131245,-0.4507113 -0.131245,-0.6664015 0,-0.9817977 0.798664,-1.7804621 1.780462,-1.7804621 0.981797,0 1.780462,0.7986644 1.780462,1.7804621 0,0.2156902 -0.03968,0.4293457 -0.119037,0.6358793 z" />
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

+6 -1
View File
@@ -25,7 +25,9 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from nw.constants.enum import nwItemClass, nwItemLayout, nwOutline
from nw.constants.enum import (
nwItemClass, nwItemLayout, nwItemType, nwOutline
)
class nwConst():
@@ -43,6 +45,9 @@ class nwConst():
SP_INTERNAL = "internal"
SP_ENCHANT = "enchant"
# Check Lists
REG_TYPES = {nwItemType.ROOT, nwItemType.FOLDER, nwItemType.FILE}
# END Class nwConst
class nwRegEx():
+3 -3
View File
@@ -149,9 +149,9 @@ class NWDoc():
docMeta = ""
else:
docMeta = (
f"%%~name: {self._theItem.itemName:s}\n"
f"%%~path: {self._theItem.itemParent:s}/{self._theItem.itemHandle:s}\n"
f"%%~kind: {self._theItem.itemClass.name:s}/{self._theItem.itemLayout.name:s}\n"
f"%%~name: {self._theItem.itemName}\n"
f"%%~path: {self._theItem.itemParent}/{self._theItem.itemHandle}\n"
f"%%~kind: {self._theItem.itemClass.name}/{self._theItem.itemLayout.name}\n"
)
try:
+28 -6
View File
@@ -1317,30 +1317,52 @@ class NWProject():
# Handle orphans
aDoc = NWDoc(self, self.theParent)
nOrph = 0
noWhere = False
for oHandle in orphanFiles:
# Look for meta data
oName = ""
oParent = None
oClass = None
oLayout = None
if aDoc.openDocument(oHandle, showStatus=False, isOrphan=True) is not None:
oName, _, oClass, oLayout = aDoc.getMeta()
oName, oParent, oClass, oLayout = aDoc.getMeta()
if oName == "":
if oName:
oName = "Recovered: %s" % oName.lstrip("Recovered: ")
else:
nOrph += 1
oName = "Orphaned File %d" % nOrph
oName = "Recovered File %d" % nOrph
# Recover file meta data
if oClass is None:
oClass = nwItemClass.NO_CLASS
oClass = nwItemClass.NOVEL
if oLayout is None:
oLayout = nwItemLayout.NO_LAYOUT
oLayout = nwItemLayout.NOTE
if oParent is None or not self.projTree.handleExists(oParent):
oParent = self.projTree.findRoot(oClass)
if oParent is None:
oParent = self.projTree.findRoot(nwItemClass.NOVEL)
# If the file still has no parent item, skip it
if oParent is None:
noWhere = True
continue
orphItem = NWItem(self)
orphItem.setName(oName)
orphItem.setType(nwItemType.FILE)
orphItem.setClass(oClass)
orphItem.setLayout(oLayout)
self.projTree.append(oHandle, None, orphItem)
self.projTree.append(oHandle, oParent, orphItem)
if noWhere:
self.makeAlert((
"One or more orphaned files could not be added back into the "
"project. Make sure at least a Novel root folder exists."
), nwAlert.WARN)
return True
+5
View File
@@ -294,6 +294,11 @@ class NWTree():
tTree.append(tHandle)
return tTree
def handleExists(self, tHandle):
"""Check if a handle exists in the project.
"""
return tHandle in self._treeOrder
##
# Setters
##
+19 -21
View File
@@ -774,14 +774,13 @@ class GuiBuildNovel(QDialog):
if not os.path.isdir(saveDir):
saveDir = self.mainConf.homePath
if self.mainConf.showGUI:
dlgOpt = QFileDialog.Options()
dlgOpt |= QFileDialog.DontUseNativeDialog
savePath, _ = QFileDialog.getSaveFileName(
self, "Save Document As", savePath, options=dlgOpt
)
if not savePath:
return False
dlgOpt = QFileDialog.Options()
dlgOpt |= QFileDialog.DontUseNativeDialog
savePath, _ = QFileDialog.getSaveFileName(
self, "Save Document As", savePath, options=dlgOpt
)
if not savePath:
return False
self.mainConf.setLastPath(savePath)
@@ -887,19 +886,18 @@ class GuiBuildNovel(QDialog):
errMsg = "Unknown format"
# Report to user
if self.mainConf.showGUI:
if wSuccess:
self.theParent.makeAlert(
"%s file successfully written to:<br> %s" % (
textFmt, savePath
), nwAlert.INFO
)
else:
self.theParent.makeAlert(
"Failed to write %s file. %s" % (
textFmt, errMsg
), nwAlert.ERROR
)
if wSuccess:
self.theParent.makeAlert(
"%s file successfully written to:<br> %s" % (
textFmt, savePath
), nwAlert.INFO
)
else:
self.theParent.makeAlert(
"Failed to write %s file. %s" % (
textFmt, errMsg
), nwAlert.ERROR
)
return wSuccess
+67 -125
View File
@@ -61,8 +61,7 @@ class GuiProjectTree(QTreeWidget):
self.theIndex = theParent.theIndex
# Tree Settings
self.theMap = None
self.orphRoot = None
self.theMap = {}
self.treeChanged = False
self.ctxMenu = GuiProjectTreeMenu(self)
@@ -145,11 +144,11 @@ class GuiProjectTree(QTreeWidget):
##
def clearTree(self):
"""Clear the GUI content and the related maps.
"""Clear the GUI content and the related map.
"""
self.clear()
self.theMap = {}
self.orphRoot = None
self.treeChanged = False
return
def newTreeItem(self, itemType, itemClass):
@@ -165,6 +164,11 @@ class GuiProjectTree(QTreeWidget):
logger.error("No project open")
return False
if not isinstance(itemType, nwItemType):
# This would indicate an internal bug
logger.error("No itemType provided")
return False
# The item needs to be assigned an item class, so one must be
# provided, or it must be possible to extract it from the parent
# item of the new item.
@@ -173,21 +177,18 @@ class GuiProjectTree(QTreeWidget):
if pItem is not None:
itemClass = pItem.itemClass
# If class is still not set, alert the user and exit
if itemClass is None:
if itemType is not None:
if itemType == nwItemType.FILE:
self.makeAlert(
"Please select a valid location in the tree to add a document.",
nwAlert.ERROR
)
return False
elif itemType == nwItemType.FOLDER:
self.makeAlert(
"Please select a valid location in the tree to add a folder.",
nwAlert.ERROR
)
return False
self.makeAlert("Failed to add new item.", nwAlert.BUG)
if itemType == nwItemType.FILE:
self.makeAlert(
"Please select a valid location in the tree to add the document.",
nwAlert.ERROR
)
else:
self.makeAlert(
"Please select a valid location in the tree to add the folder.",
nwAlert.ERROR
)
return False
# Everything is fine, we have what we need, so we proceed
@@ -260,8 +261,7 @@ class GuiProjectTree(QTreeWidget):
# Add the new item to the tree
if tHandle is not None:
self.revealNewTreeItem(tHandle, nHandle)
if self.mainConf.showGUI:
self.theParent.editItem(tHandle)
self.theParent.editItem(tHandle)
return True
@@ -270,6 +270,9 @@ class GuiProjectTree(QTreeWidget):
"""
nwItem = self.theProject.projTree[tHandle]
trItem = self._addTreeItem(nwItem, nHandle)
if trItem is None:
return False
pHandle = nwItem.itemParent
if pHandle is not None and pHandle in self.theMap:
self.theMap[pHandle].setExpanded(True)
@@ -285,7 +288,7 @@ class GuiProjectTree(QTreeWidget):
logger.error("No project open")
return False
if qApp.focusWidget() != self and self.mainConf.showGUI:
if qApp.focusWidget() != self:
return False
tHandle = self.getSelectedHandle()
@@ -326,8 +329,6 @@ class GuiProjectTree(QTreeWidget):
"""
theList = []
for i in range(self.topLevelItemCount()):
if self.topLevelItem(i) == self.orphRoot:
continue
theList = self._scanChildren(theList, self.topLevelItem(i), i)
logger.debug("Saving project tree item order")
self.theProject.setTreeOrder(theList)
@@ -391,14 +392,12 @@ class GuiProjectTree(QTreeWidget):
return False
msgYes = self.theParent.askQuestion(
"Empty Trash", "Permanently delete %d file%s from Trash?" % (
nTrash, "s" if nTrash > 1 else ""
)
"Empty Trash", "Permanently delete %d file(s) from Trash?" % nTrash
)
if not msgYes:
return False
logger.verbose("Deleting %d files from Trash" % nTrash)
logger.verbose("Deleting %d file(s) from Trash" % nTrash)
for tHandle in self.getTreeFromHandle(trashHandle):
if tHandle == trashHandle:
continue
@@ -603,13 +602,11 @@ class GuiProjectTree(QTreeWidget):
relevant values in the project and on the status bar. This call
is a fast way of getting this number, and depends on the
propagateCount function being called when it should to maintain
the correct count. Orphan folder is not included in the total.
the correct count.
"""
nWords = 0
for n in range(self.topLevelItemCount()):
tItem = self.topLevelItem(n)
if tItem == self.orphRoot:
continue
nWords += int(tItem.data(self.C_COUNT, Qt.UserRole))
self.theProject.setProjectWordCount(nWords)
@@ -640,11 +637,7 @@ class GuiProjectTree(QTreeWidget):
selected, return the first.
"""
selItem = self.selectedItems()
if len(selItem) == 0:
return None
if isinstance(selItem[0], QTreeWidgetItem):
if selItem:
return selItem[0].data(self.C_NAME, Qt.UserRole)
return None
@@ -663,18 +656,21 @@ class GuiProjectTree(QTreeWidget):
def setSelectedHandle(self, tHandle, doScroll=False):
"""Set a specific handle as the selected item.
"""
if tHandle in self.theMap:
self.clearSelection()
self.theMap[tHandle].setSelected(True)
if tHandle not in self.theMap:
return False
selItems = self.selectedIndexes()
if selItems and doScroll:
self.scrollTo(
selItems[0], QAbstractItemView.PositionAtCenter
)
return True
tItem = self._getTreeItem(tHandle)
if tItem is None:
return False
return False
self.clearSelection()
self.theMap[tHandle].setSelected(True)
selItems = self.selectedIndexes()
if selItems and doScroll:
self.scrollTo(selItems[0], QAbstractItemView.PositionAtCenter)
return True
##
# Slots
@@ -687,12 +683,12 @@ class GuiProjectTree(QTreeWidget):
selItem = self.itemAt(clickPos)
if isinstance(selItem, QTreeWidgetItem):
tHandle = selItem.data(self.C_NAME, Qt.UserRole)
tItem = self.theProject.projTree[tHandle]
self.setSelectedHandle(tHandle) # Just to be safe
if self.ctxMenu.filterActions(tItem):
# Only open menu if any actions remain after filter
self.ctxMenu.exec_(self.viewport().mapToGlobal(clickPos))
tItem = self.theProject.projTree[tHandle]
if tItem is not None:
if self.ctxMenu.filterActions(tItem):
# Only open menu if any actions remain after filter
self.ctxMenu.exec_(self.viewport().mapToGlobal(clickPos))
return
@@ -764,17 +760,11 @@ class GuiProjectTree(QTreeWidget):
logger.debug("Drag'n'drop of item %s accepted" % sHandle)
self.propagateCount(sHandle, 0)
QTreeWidget.dropEvent(self, theEvent)
# Handle orphaned files differently than tracked files
if isNone:
self._moveOrphanedItem(sHandle, dHandle)
self._cleanOrphanedRoot()
else:
self._updateItemParent(sHandle)
self._updateItemParent(sHandle)
# If the item does not have the same class as the target,
# and the target is not a free root folder, update its class
if not isSame and not onFree:
if not (isSame or onFree):
logger.debug("Item %s class has been changed from %s to %s" % (
sHandle,
snItem.itemClass.name,
@@ -807,9 +797,7 @@ class GuiProjectTree(QTreeWidget):
def _getTreeItem(self, tHandle):
"""Returns the QTreeWidgetItem of a given item handle.
"""
if tHandle in self.theMap.keys():
return self.theMap[tHandle]
return None
return self.theMap.get(tHandle, None)
def _scanChildren(self, theList, theItem, theIndex):
"""This is a recursive function returning all items in a tree
@@ -833,15 +821,15 @@ class GuiProjectTree(QTreeWidget):
tClass = nwItem.itemClass
newItem = QTreeWidgetItem([""]*4)
newItem.setText(self.C_NAME, "")
newItem.setText(self.C_COUNT, "0")
newItem.setText(self.C_NAME, "")
newItem.setText(self.C_COUNT, "0")
newItem.setText(self.C_EXPORT, "")
newItem.setText(self.C_FLAGS, "")
newItem.setText(self.C_FLAGS, "")
newItem.setTextAlignment(self.C_NAME, Qt.AlignLeft | Qt.AlignVCenter)
newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight | Qt.AlignVCenter)
newItem.setTextAlignment(self.C_EXPORT, Qt.AlignLeft | Qt.AlignVCenter)
newItem.setTextAlignment(self.C_FLAGS, Qt.AlignLeft | Qt.AlignVCenter)
newItem.setTextAlignment(self.C_NAME, Qt.AlignLeft)
newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight)
newItem.setTextAlignment(self.C_EXPORT, Qt.AlignLeft)
newItem.setTextAlignment(self.C_FLAGS, Qt.AlignLeft)
newItem.setData(self.C_NAME, Qt.UserRole, tHandle)
newItem.setData(self.C_COUNT, Qt.UserRole, 0)
@@ -854,8 +842,12 @@ class GuiProjectTree(QTreeWidget):
elif nwItem.itemType == nwItemType.TRASH:
self.addTopLevelItem(newItem)
else:
self._addOrphanedRoot()
self.orphRoot.addChild(newItem)
self.makeAlert(
"There is nowhere to add item with name '%s'" % nwItem.itemName, nwAlert.ERROR
)
del self.theMap[tHandle]
return None
else:
byIndex = -1
if nHandle is not None and nHandle in self.theMap:
@@ -898,39 +890,12 @@ class GuiProjectTree(QTreeWidget):
trItem = self._addTreeItem(
self.theProject.projTree[trashHandle]
)
trItem.setExpanded(True)
self._setTreeChanged(True)
if trItem is not None:
trItem.setExpanded(True)
self._setTreeChanged(True)
return trItem
def _addOrphanedRoot(self):
"""Add the special Orphaned Files root item to hold non-root
items with no parent set.
"""
if self.orphRoot is None:
newItem = QTreeWidgetItem([""]*4)
newItem.setText(self.C_NAME, "Orphaned Files")
newItem.setText(self.C_COUNT, "")
newItem.setText(self.C_EXPORT, "")
newItem.setText(self.C_FLAGS, "")
self.addTopLevelItem(newItem)
self.orphRoot = newItem
newItem.setExpanded(True)
newItem.setData(self.C_NAME, Qt.UserRole, "")
newItem.setData(self.C_COUNT, Qt.UserRole, 0)
newItem.setIcon(self.C_NAME, self.theTheme.getIcon("proj_orphan"))
return
def _cleanOrphanedRoot(self):
"""Remove the special Orphaned Files root folder if it is empty.
"""
if self.orphRoot is not None:
if self.orphRoot.childCount() == 0:
self.takeTopLevelItem(self.indexOfTopLevelItem(self.orphRoot))
self.orphRoot = None
return
def _updateItemParent(self, tHandle):
"""Update the parent handle of an item so that the information
in the project is consistent with the treeView.
@@ -951,27 +916,6 @@ class GuiProjectTree(QTreeWidget):
return True
def _moveOrphanedItem(self, tHandle, dHandle):
"""Move an Orphaned Item to a new dHandle parent item. This
function will set all the missing meta data based on the meta
data of the destination item.
"""
trItemS = self._getTreeItem(tHandle)
nwItemS = self.theProject.projTree[tHandle]
nwItemD = self.theProject.projTree[dHandle]
trItemP = trItemS.parent()
nwItemS.setClass(nwItemD.itemClass)
if trItemP is None:
logger.error("Failed to find new parent item of %s" % tHandle)
return False
pHandle = trItemP.data(self.C_NAME, Qt.UserRole)
nwItemS.setParent(pHandle)
self.setTreeItemValues(tHandle)
self._setTreeChanged(True)
return True
def _setTreeChanged(self, theState):
"""Set the tree change flag, and propagate to the project.
"""
@@ -1047,12 +991,10 @@ class GuiProjectTreeMenu(QMenu):
inTrash = theItem.itemParent == trashHandle and trashHandle is not None
isTrash = theItem.itemHandle == trashHandle and trashHandle is not None
isFile = theItem.itemType == nwItemType.FILE
isOrph = isFile and theItem.itemParent is None
allowEdit = not (isTrash or isOrph)
allowNew = not (isTrash or inTrash or isOrph)
allowNew = not (isTrash or inTrash)
self.editItem.setVisible(allowEdit)
self.editItem.setVisible(not isTrash)
self.openItem.setVisible(isFile)
self.viewItem.setVisible(isFile)
self.toggleExp.setVisible(isFile)
-1
View File
@@ -524,7 +524,6 @@ class GuiIcons:
"cls_trash" : (QStyle.SP_DriveHDIcon, "drive-harddisk"),
"proj_document" : (QStyle.SP_FileIcon, "x-office-document"),
"proj_folder" : (QStyle.SP_DirIcon, "folder"),
"proj_orphan" : (QStyle.SP_MessageBoxWarning, "dialog-warning"),
"proj_nwx" : (None, None),
"status_lang" : (None, None),
"status_time" : (None, None),
+18 -12
View File
@@ -47,7 +47,7 @@ from nw.gui import (
GuiTheme, GuiWritingStats
)
from nw.core import NWProject, NWDoc, NWIndex
from nw.constants import nwItemType, nwItemClass, nwAlert
from nw.constants import nwItemType, nwItemClass, nwAlert, nwConst
from nw.common import getGuiItem
logger = logging.getLogger(__name__)
@@ -722,6 +722,12 @@ class GuiMain(QMainWindow):
logger.warning("No item selected")
return
tItem = self.theProject.projTree[tHandle]
if tItem is None:
return
if tItem.itemType not in nwConst.REG_TYPES:
return
logger.verbose("Requesting change to item %s" % tHandle)
dlgProj = GuiItemEditor(self, self.theProject, tHandle)
dlgProj.exec_()
@@ -965,17 +971,16 @@ class GuiMain(QMainWindow):
logger.error(msgLine)
# Popup
if self.mainConf.showGUI:
msgBox = QMessageBox()
if theLevel == nwAlert.INFO:
msgBox.information(self, "Information", popMsg)
elif theLevel == nwAlert.WARN:
msgBox.warning(self, "Warning", popMsg)
elif theLevel == nwAlert.ERROR:
msgBox.critical(self, "Error", popMsg)
elif theLevel == nwAlert.BUG:
popMsg += "<br>This is a bug!"
msgBox.critical(self, "Internal Error", popMsg)
msgBox = QMessageBox()
if theLevel == nwAlert.INFO:
msgBox.information(self, "Information", popMsg)
elif theLevel == nwAlert.WARN:
msgBox.warning(self, "Warning", popMsg)
elif theLevel == nwAlert.ERROR:
msgBox.critical(self, "Error", popMsg)
elif theLevel == nwAlert.BUG:
popMsg += "<br>This is a bug!"
msgBox.critical(self, "Internal Error", popMsg)
return
@@ -1320,6 +1325,7 @@ class GuiMain(QMainWindow):
self.openDocument(tHandle, changeFocus=False, doScroll=False)
else:
logger.verbose("Requested item %s is a folder" % tHandle)
return
def _treeKeyPressReturn(self):
-1
View File
@@ -61,7 +61,6 @@ def testBaseInit_Options(monkeypatch, tmpDir):
nwGUI = nw.main()
assert nw.logger.getEffectiveLevel() == logging.WARNING
assert nw.CONFIG.debugInfo is False
assert nw.CONFIG.showGUI is False
assert nwGUI.closeMain() == "closeMain"
# Defaults
+6 -6
View File
@@ -923,9 +923,9 @@ def testCoreProject_OrphanedFiles(dummyGUI, nwLipsum):
# First Item with Meta Data
oItem = theProject.projTree["636b6aa9b697b"]
assert oItem is not None
assert oItem.itemName == "Mars"
assert oItem.itemName == "Recovered: Mars"
assert oItem.itemHandle == "636b6aa9b697b"
assert oItem.itemParent is None
assert oItem.itemParent == "60bdf227455cc"
assert oItem.itemClass == nwItemClass.WORLD
assert oItem.itemType == nwItemType.FILE
assert oItem.itemLayout == nwItemLayout.NOTE
@@ -933,12 +933,12 @@ def testCoreProject_OrphanedFiles(dummyGUI, nwLipsum):
# Second Item without Meta Data
oItem = theProject.projTree["736b6aa9b697b"]
assert oItem is not None
assert oItem.itemName == "Orphaned File 1"
assert oItem.itemName == "Recovered File 1"
assert oItem.itemHandle == "736b6aa9b697b"
assert oItem.itemParent is None
assert oItem.itemClass == nwItemClass.NO_CLASS
assert oItem.itemParent == "b3643d0f92e32"
assert oItem.itemClass == nwItemClass.NOVEL
assert oItem.itemType == nwItemType.FILE
assert oItem.itemLayout == nwItemLayout.NO_LAYOUT
assert oItem.itemLayout == nwItemLayout.NOTE
assert theProject.saveProject(nwLipsum)
assert theProject.closeProject()
+3 -1
View File
@@ -9,7 +9,7 @@ from shutil import copyfile
from tools import cmpFiles, getGuiItem
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QAction, QMessageBox
from PyQt5.QtWidgets import QAction, QMessageBox, QFileDialog
from nw.gui import GuiBuildNovel
@@ -23,6 +23,8 @@ def testGuiBuild_Tool(qtbot, monkeypatch, nwGUI, nwLipsum, refDir, outDir):
"""
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "information", lambda *args: QMessageBox.Yes)
monkeypatch.setattr(QFileDialog, "getSaveFileName", lambda a, b, c, **kwargs: (c, None))
# Check that we cannot open when there is no project
nwGUI.mainMenu.aBuildProject.activate(QAction.Trigger)
+100 -31
View File
@@ -5,95 +5,133 @@
import pytest
import os
from tools import writeFile
from PyQt5.QtCore import QItemSelectionModel
from PyQt5.QtWidgets import QAction, QMessageBox
from nw.constants import nwItemType, nwItemClass
keyDelay = 2
typeDelay = 1
stepDelay = 20
@pytest.mark.gui
def testGuiProjTree_Main(qtbot, monkeypatch, nwGUI, nwMinimal):
"""Test the project tree.
def testGuiProjTree_TreeItems(qtbot, caplog, monkeypatch, nwGUI, nwMinimal):
"""Test adding and removing items from the project tree.
"""
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes)
monkeypatch.setattr("nw.guimain.GuiMain.editItem", lambda *args: None)
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.openProject(nwMinimal)
nwTree = nwGUI.treeView
##
# Add New Items
##
# Try to add and move item with no project
assert not nwTree.newTreeItem(nwItemType.FILE, None)
assert not nwTree.moveTreeItem(1)
# Open a project
assert nwGUI.openProject(nwMinimal)
# 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)
# No itemType set or ROOT, but no class
assert not nwTree.newTreeItem(None, None)
assert not nwTree.newTreeItem(nwItemType.ROOT, None)
# Select a location
chItem = nwTree._getTreeItem("a6d311a93600a")
nwTree.setCurrentItem(chItem, QItemSelectionModel.Current)
chItem.setExpanded(True)
# Create new item with no class set
# Create new item with no class set (defaults to NOVEL)
assert nwTree.newTreeItem(nwItemType.FILE, None)
assert nwTree.newTreeItem(nwItemType.FOLDER, None)
# Check that we have the correct tree order
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
"a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "44cb730c42048", "71ee45a3c0db9"
]
# Add roots
assert not nwTree.newTreeItem(nwItemType.ROOT, None) # Defaults to NOVEL
assert not nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) # Duplicate
assert nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.CUSTOM) # Valid
# Check that we have the correct tree order
# Change max depth and try to add a subfolder that is too deep
monkeypatch.setattr("nw.constants.nwConst.MAX_DEPTH", 2)
chItem = nwTree._getTreeItem("71ee45a3c0db9")
nwTree.setCurrentItem(chItem, QItemSelectionModel.Current)
assert not nwTree.newTreeItem(nwItemType.FOLDER, None)
##
# Move Items
##
nwTree.setSelectedHandle("8c659a11cd429")
# Shift focus and try to move item
monkeypatch.setattr("PyQt5.QtWidgets.qApp.focusWidget", lambda: None)
assert not nwTree.moveTreeItem(1)
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
"a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "73475cb40a568", "44cb730c42048"
"a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "44cb730c42048", "71ee45a3c0db9"
]
monkeypatch.setattr("PyQt5.QtWidgets.qApp.focusWidget", lambda: nwTree)
# Move second item up twice (should give same result)
nwTree.setSelectedHandle("8c659a11cd429")
nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger)
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
"a6d311a93600a", "8c659a11cd429", "f5ab3e30151e1", "73475cb40a568", "44cb730c42048"
"a6d311a93600a", "8c659a11cd429", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9"
]
nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger)
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
"a6d311a93600a", "8c659a11cd429", "f5ab3e30151e1", "73475cb40a568", "44cb730c42048"
"a6d311a93600a", "8c659a11cd429", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9"
]
# Move it back down four times (last to should be the same)
# 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", "73475cb40a568", "44cb730c42048"
"a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "44cb730c42048", "71ee45a3c0db9"
]
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
"a6d311a93600a", "f5ab3e30151e1", "73475cb40a568", "8c659a11cd429", "44cb730c42048"
"a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "8c659a11cd429", "71ee45a3c0db9"
]
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
"a6d311a93600a", "f5ab3e30151e1", "73475cb40a568", "44cb730c42048", "8c659a11cd429"
"a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9", "8c659a11cd429"
]
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
"a6d311a93600a", "f5ab3e30151e1", "73475cb40a568", "44cb730c42048", "8c659a11cd429"
"a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9", "8c659a11cd429"
]
# Move a root item (top level items are different) twice
nwTree.flushTreeOrder()
assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 9
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") == 10
assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 11
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
nwTree.flushTreeOrder()
assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 10
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
@@ -102,11 +140,11 @@ def testGuiProjTree_Main(qtbot, monkeypatch, nwGUI, nwMinimal):
assert not nwTree.deleteItem(None)
assert not nwTree.deleteItem("1111111111111")
assert nwTree.deleteItem("73475cb40a568") # New File
assert nwTree.deleteItem("44cb730c42048") # New Folder
assert nwTree.deleteItem("71ee45a3c0db9") # Custom Root
assert nwTree.deleteItem("71ee45a3c0db9") # New Folder
assert nwTree.deleteItem("811786ad1ae74") # Custom Root
assert "73475cb40a568" in nwGUI.theProject.projTree._treeOrder
assert "44cb730c42048" not 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"))
@@ -115,24 +153,55 @@ def testGuiProjTree_Main(qtbot, monkeypatch, nwGUI, nwMinimal):
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")
with open(orphFile, mode="w+", encoding="utf8") as outFile:
outFile.write("# Hello World\n")
writeFile(orphFile, "# Hello World\n")
# Open the project again
nwGUI.openProject(nwMinimal)
# Check that the orphaned file was found and added to the tree
assert nwTree.orphRoot is not None
nwTree.flushTreeOrder()
assert "1234567890abc" not in nwGUI.theProject.projTree._treeOrder
assert "1234567890abc" in nwGUI.theProject.projTree._treeOrder
orItem = nwTree._getTreeItem("1234567890abc")
assert orItem.text(nwTree.C_NAME) == "Orphaned File 1"
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"].itemParent = 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("nw.core.tree.NWTree.findRoot", lambda *args: None)
assert not nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL)
assert not nwTree.newTreeItem(nwItemType.FOLDER, nwItemClass.NOVEL)
# qtbot.stopForInteraction()
nwGUI.closeProject()
# END Test testGuiProjTree_Main
# END Test testGuiProjTree_TreeItems