Merge branch 'main' into novel_tab

This commit is contained in:
Veronica K. B. Olsen
2020-12-21 21:51:17 +01:00
21 changed files with 302 additions and 373 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. 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. 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 new project can be created from the :guilabel:`Project` menu by selecting
A list of recently opened projects is maintained, and displayed in the :guilabel:`Open Project` :guilabel:`New Project`. A list of recently opened projects is maintained, and displayed in the
dialog. A project can be removed from this list by selecting it and pressing the :kbd:`Del` key. :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 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. :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 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 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 core novel files go into a root folder of type :guilabel:`Novel`. Other supporting files go
the other root folders. These other root folder types are intended for your notes on the various into the other root folders. These other root folder types are intended for your notes on the
elements of your story. Using these is of course entirely optional. 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 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. :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 The root folders are intended for the following use, but aside from the :guilabel:`Novel` folder,
restrictions are enforced by the application. You can use them however you want. no restrictions are enforced by the application. You can use them however you want.
:guilabel:`Novel` :guilabel:`Novel`
This is the root folder of all text that goes into the final novel. This class of files have 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` :guilabel:`Plot`
This is the root folder where main plots can be outlined. It is optional, but adding at least 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 dummy files can be useful in order to tag plot elements for the Outline view. Tags in this
can be references using the ``@plot`` keyword. folder can be references using the ``@plot`` keyword.
:guilabel:`Characters` :guilabel:`Characters`
Character files go in this root folder. These are especially important if one wants to use the 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 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 trash folder can then be deleted permanently, either individually, or by emptying the trash from
menu. Files in this folder are removed from the project index and cannot be referenced. 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 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 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. ones there.
You can drag any file to this folder and preserve its settings. The file will always be excluded 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 from the :guilabel:`Build Novel Project` builds. The file is also removed from the project index,
the tags and references defined in it will not show up anywhere else. so the tags and references defined in it will not show up anywhere else.
.. _a_proj_roots_orph: .. _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 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 file synchronisation tool that runs out of sync, there may be files in the project folder that
tracked in the core project file. These files, when discovered, are handled by the Orphaned aren't tracked in the core project file. These files, when discovered, are recovered and added back
Documents routine. into the project if possible.
Files that are discovered in the project folder, but not in the project, will be re-added to the The discovered files are scanned for meta information that gives clues as to where the file may
project tree in a special :guilabel:`Orphaned Items` root folder next time the application is previously have been located in the project. The project loading routines will try to put them back
started. These orphaned files will not have most of the meta data preserved, although novelWriter as close as possible to this location if it still exists. Generally, it will be appended to the end
will try to restore the file label it had in the project tree. Other information will have to be set of the folder where it previously was located. If that folder doesn't exist, it will try to add it
again, and the files moved back to the correct location in the project tree. 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: .. _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 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 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 in the footer of any document open in the editor, and all stats are shown in the details panel
the project tree for any file selected. 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 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. 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 A total project word count is displayed in the status bar. The total count depends on the sum of
values in the project tree, which again depend on an up to date index. If the counts seem wrong, a the values in the project tree, which again depend on an up to date index. If the counts seem
full project word recount can be initiated by rebuilding the project's index. Either form the wrong, a full project word recount can be initiated by rebuilding the project's index. Either form
:guilabel:`Tools` menu, or by pressing :kbd:`F9`. the :guilabel:`Tools` menu, or by pressing :kbd:`F9`.
.. _a_proj_settings: .. _a_proj_settings:
@@ -242,9 +248,9 @@ many words exist in the entire project.
Status and Importance Tabs Status and Importance Tabs
-------------------------- --------------------------
Each file of type "Novel" can be given a status level, signified by a coloured icon and each file of Each file of type "Novel" can be given a status level, signified by a coloured icon and each file
the remaining types can be given an importance level. These are colour coded icons and labels that of the remaining types can be given an importance level. These are colour coded icons and labels
can be applied to each file. 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 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 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:: .. note::
A keyword cannot contain any spaces. The angle brackets are added by default, and when used in 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 the text are a part of the keyword to be replaced. This is to ensure that parts of the text
unintentionally replaced by the content of the list. isn't unintentionally replaced by the content of the list.
.. _a_proj_backup: .. _a_proj_backup:
@@ -275,22 +281,22 @@ also be applied to exports.
Backup Backup
====== ======
An automatic backup system is built into novelWriter. In order to use it, a backup path to where the An automatic backup system is built into novelWriter. In order to use it, a backup path to where
backup files are to be stored must to be provided in :guilabel:`Preferences`. 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 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 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` stored in a subfolder of the backup path with the same name as the project :guilabel:`Working
set in :ref:`a_proj_settings`. Title` set in :ref:`a_proj_settings`.
The backup feature, when configured, can also be run manually from the :guilabel:`Tools` menu. 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 It is also possible to dissable automated backup for a given project in :guilabel:`Project
Settings`. Settings`.
.. note:: .. note::
For the backup to be able to run, the :guilabel:`Working Title` must be set in :guilabel:`Project For the backup to be able to run, the :guilabel:`Working Title` must be set in
Settings`. This value is used to generate the folder name for the zip files. Without it, the :guilabel:`Project Settings`. This value is used to generate the folder name for the zip files.
backup will not run at all, but produce a warning message. Without it, the backup will not run at all, but produce a warning message.
.. _a_proj_stats: .. _a_proj_stats:
@@ -29,7 +29,6 @@ cls_archive = delete.svg
cls_trash = trash.svg cls_trash = trash.svg
proj_document = document-text.svg proj_document = document-text.svg
proj_folder = folder.svg proj_folder = folder.svg
proj_orphan = warning.svg
doc_h1 = heading1.svg doc_h1 = heading1.svg
doc_h2 = heading2.svg doc_h2 = heading2.svg
doc_h3 = heading3.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 cls_trash = trash.svg
proj_document = document-text.svg proj_document = document-text.svg
proj_folder = folder.svg proj_folder = folder.svg
proj_orphan = warning.svg
doc_h1 = heading1.svg doc_h1 = heading1.svg
doc_h2 = heading2.svg doc_h2 = heading2.svg
doc_h3 = heading3.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 cls_trash = trash.svg
proj_document = document-text.svg proj_document = document-text.svg
proj_folder = folder.svg proj_folder = folder.svg
proj_orphan = warning.svg
doc_h1 = heading1.svg doc_h1 = heading1.svg
doc_h2 = heading2.svg doc_h2 = heading2.svg
doc_h3 = heading3.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 cls_trash = trash.svg
proj_document = document-text.svg proj_document = document-text.svg
proj_folder = folder.svg proj_folder = folder.svg
proj_orphan = warning.svg
doc_h1 = heading1.svg doc_h1 = heading1.svg
doc_h2 = heading2.svg doc_h2 = heading2.svg
doc_h3 = heading3.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/>. 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(): class nwConst():
@@ -43,6 +45,9 @@ class nwConst():
SP_INTERNAL = "internal" SP_INTERNAL = "internal"
SP_ENCHANT = "enchant" SP_ENCHANT = "enchant"
# Check Lists
REG_TYPES = {nwItemType.ROOT, nwItemType.FOLDER, nwItemType.FILE}
# END Class nwConst # END Class nwConst
class nwRegEx(): class nwRegEx():
+3 -3
View File
@@ -149,9 +149,9 @@ class NWDoc():
docMeta = "" docMeta = ""
else: else:
docMeta = ( docMeta = (
f"%%~name: {self._theItem.itemName:s}\n" f"%%~name: {self._theItem.itemName}\n"
f"%%~path: {self._theItem.itemParent:s}/{self._theItem.itemHandle:s}\n" f"%%~path: {self._theItem.itemParent}/{self._theItem.itemHandle}\n"
f"%%~kind: {self._theItem.itemClass.name:s}/{self._theItem.itemLayout.name:s}\n" f"%%~kind: {self._theItem.itemClass.name}/{self._theItem.itemLayout.name}\n"
) )
try: try:
+28 -6
View File
@@ -1317,30 +1317,52 @@ class NWProject():
# Handle orphans # Handle orphans
aDoc = NWDoc(self, self.theParent) aDoc = NWDoc(self, self.theParent)
nOrph = 0 nOrph = 0
noWhere = False
for oHandle in orphanFiles: for oHandle in orphanFiles:
# Look for meta data # Look for meta data
oName = "" oName = ""
oParent = None
oClass = None oClass = None
oLayout = None oLayout = None
if aDoc.openDocument(oHandle, showStatus=False, isOrphan=True) is not 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 nOrph += 1
oName = "Orphaned File %d" % nOrph oName = "Recovered File %d" % nOrph
# Recover file meta data
if oClass is None: if oClass is None:
oClass = nwItemClass.NO_CLASS oClass = nwItemClass.NOVEL
if oLayout is None: 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 = NWItem(self)
orphItem.setName(oName) orphItem.setName(oName)
orphItem.setType(nwItemType.FILE) orphItem.setType(nwItemType.FILE)
orphItem.setClass(oClass) orphItem.setClass(oClass)
orphItem.setLayout(oLayout) 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 return True
+5
View File
@@ -294,6 +294,11 @@ class NWTree():
tTree.append(tHandle) tTree.append(tHandle)
return tTree return tTree
def handleExists(self, tHandle):
"""Check if a handle exists in the project.
"""
return tHandle in self._treeOrder
## ##
# Setters # Setters
## ##
+19 -21
View File
@@ -774,14 +774,13 @@ class GuiBuildNovel(QDialog):
if not os.path.isdir(saveDir): if not os.path.isdir(saveDir):
saveDir = self.mainConf.homePath saveDir = self.mainConf.homePath
if self.mainConf.showGUI: dlgOpt = QFileDialog.Options()
dlgOpt = QFileDialog.Options() dlgOpt |= QFileDialog.DontUseNativeDialog
dlgOpt |= QFileDialog.DontUseNativeDialog savePath, _ = QFileDialog.getSaveFileName(
savePath, _ = QFileDialog.getSaveFileName( self, "Save Document As", savePath, options=dlgOpt
self, "Save Document As", savePath, options=dlgOpt )
) if not savePath:
if not savePath: return False
return False
self.mainConf.setLastPath(savePath) self.mainConf.setLastPath(savePath)
@@ -887,19 +886,18 @@ class GuiBuildNovel(QDialog):
errMsg = "Unknown format" errMsg = "Unknown format"
# Report to user # Report to user
if self.mainConf.showGUI: if wSuccess:
if wSuccess: self.theParent.makeAlert(
self.theParent.makeAlert( "%s file successfully written to:<br> %s" % (
"%s file successfully written to:<br> %s" % ( textFmt, savePath
textFmt, savePath ), nwAlert.INFO
), nwAlert.INFO )
) else:
else: self.theParent.makeAlert(
self.theParent.makeAlert( "Failed to write %s file. %s" % (
"Failed to write %s file. %s" % ( textFmt, errMsg
textFmt, errMsg ), nwAlert.ERROR
), nwAlert.ERROR )
)
return wSuccess return wSuccess
+66 -120
View File
@@ -61,8 +61,7 @@ class GuiProjectTree(QTreeWidget):
self.theIndex = theParent.theIndex self.theIndex = theParent.theIndex
# Tree Settings # Tree Settings
self.theMap = None self.theMap = {}
self.orphRoot = None
self.treeChanged = False self.treeChanged = False
self.ctxMenu = GuiProjectTreeMenu(self) self.ctxMenu = GuiProjectTreeMenu(self)
@@ -145,11 +144,11 @@ class GuiProjectTree(QTreeWidget):
## ##
def clearTree(self): def clearTree(self):
"""Clear the GUI content and the related maps. """Clear the GUI content and the related map.
""" """
self.clear() self.clear()
self.theMap = {} self.theMap = {}
self.orphRoot = None self.treeChanged = False
return return
def newTreeItem(self, itemType, itemClass): def newTreeItem(self, itemType, itemClass):
@@ -165,6 +164,11 @@ class GuiProjectTree(QTreeWidget):
logger.error("No project open") logger.error("No project open")
return False 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 # 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 # provided, or it must be possible to extract it from the parent
# item of the new item. # item of the new item.
@@ -173,21 +177,18 @@ class GuiProjectTree(QTreeWidget):
if pItem is not None: if pItem is not None:
itemClass = pItem.itemClass itemClass = pItem.itemClass
# If class is still not set, alert the user and exit
if itemClass is None: if itemClass is None:
if itemType is not None: if itemType == nwItemType.FILE:
if itemType == nwItemType.FILE: self.makeAlert(
self.makeAlert( "Please select a valid location in the tree to add the document.",
"Please select a valid location in the tree to add a document.", nwAlert.ERROR
nwAlert.ERROR )
) else:
return False self.makeAlert(
elif itemType == nwItemType.FOLDER: "Please select a valid location in the tree to add the folder.",
self.makeAlert( nwAlert.ERROR
"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)
return False return False
# Everything is fine, we have what we need, so we proceed # 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 # Add the new item to the tree
if tHandle is not None: if tHandle is not None:
self.revealNewTreeItem(tHandle, nHandle) self.revealNewTreeItem(tHandle, nHandle)
if self.mainConf.showGUI: self.theParent.editItem(tHandle)
self.theParent.editItem(tHandle)
return True return True
@@ -270,6 +270,9 @@ class GuiProjectTree(QTreeWidget):
""" """
nwItem = self.theProject.projTree[tHandle] nwItem = self.theProject.projTree[tHandle]
trItem = self._addTreeItem(nwItem, nHandle) trItem = self._addTreeItem(nwItem, nHandle)
if trItem is None:
return False
pHandle = nwItem.itemParent pHandle = nwItem.itemParent
if pHandle is not None and pHandle in self.theMap: if pHandle is not None and pHandle in self.theMap:
self.theMap[pHandle].setExpanded(True) self.theMap[pHandle].setExpanded(True)
@@ -285,7 +288,7 @@ class GuiProjectTree(QTreeWidget):
logger.error("No project open") logger.error("No project open")
return False return False
if qApp.focusWidget() != self and self.mainConf.showGUI: if qApp.focusWidget() != self:
return False return False
tHandle = self.getSelectedHandle() tHandle = self.getSelectedHandle()
@@ -326,8 +329,6 @@ class GuiProjectTree(QTreeWidget):
""" """
theList = [] theList = []
for i in range(self.topLevelItemCount()): for i in range(self.topLevelItemCount()):
if self.topLevelItem(i) == self.orphRoot:
continue
theList = self._scanChildren(theList, self.topLevelItem(i), i) theList = self._scanChildren(theList, self.topLevelItem(i), i)
logger.debug("Saving project tree item order") logger.debug("Saving project tree item order")
self.theProject.setTreeOrder(theList) self.theProject.setTreeOrder(theList)
@@ -391,14 +392,12 @@ class GuiProjectTree(QTreeWidget):
return False return False
msgYes = self.theParent.askQuestion( msgYes = self.theParent.askQuestion(
"Empty Trash", "Permanently delete %d file%s from Trash?" % ( "Empty Trash", "Permanently delete %d file(s) from Trash?" % nTrash
nTrash, "s" if nTrash > 1 else ""
)
) )
if not msgYes: if not msgYes:
return False 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): for tHandle in self.getTreeFromHandle(trashHandle):
if tHandle == trashHandle: if tHandle == trashHandle:
continue continue
@@ -603,13 +602,11 @@ class GuiProjectTree(QTreeWidget):
relevant values in the project and on the status bar. This call relevant values in the project and on the status bar. This call
is a fast way of getting this number, and depends on the is a fast way of getting this number, and depends on the
propagateCount function being called when it should to maintain 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 nWords = 0
for n in range(self.topLevelItemCount()): for n in range(self.topLevelItemCount()):
tItem = self.topLevelItem(n) tItem = self.topLevelItem(n)
if tItem == self.orphRoot:
continue
nWords += int(tItem.data(self.C_COUNT, Qt.UserRole)) nWords += int(tItem.data(self.C_COUNT, Qt.UserRole))
self.theProject.setProjectWordCount(nWords) self.theProject.setProjectWordCount(nWords)
@@ -659,18 +656,21 @@ class GuiProjectTree(QTreeWidget):
def setSelectedHandle(self, tHandle, doScroll=False): def setSelectedHandle(self, tHandle, doScroll=False):
"""Set a specific handle as the selected item. """Set a specific handle as the selected item.
""" """
if tHandle in self.theMap: if tHandle not in self.theMap:
self.clearSelection() return False
self.theMap[tHandle].setSelected(True)
selItems = self.selectedIndexes() tItem = self._getTreeItem(tHandle)
if selItems and doScroll: if tItem is None:
self.scrollTo( return False
selItems[0], QAbstractItemView.PositionAtCenter
)
return True
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 # Slots
@@ -683,12 +683,12 @@ class GuiProjectTree(QTreeWidget):
selItem = self.itemAt(clickPos) selItem = self.itemAt(clickPos)
if isinstance(selItem, QTreeWidgetItem): if isinstance(selItem, QTreeWidgetItem):
tHandle = selItem.data(self.C_NAME, Qt.UserRole) tHandle = selItem.data(self.C_NAME, Qt.UserRole)
tItem = self.theProject.projTree[tHandle]
self.setSelectedHandle(tHandle) # Just to be safe self.setSelectedHandle(tHandle) # Just to be safe
tItem = self.theProject.projTree[tHandle]
if self.ctxMenu.filterActions(tItem): if tItem is not None:
# Only open menu if any actions remain after filter if self.ctxMenu.filterActions(tItem):
self.ctxMenu.exec_(self.viewport().mapToGlobal(clickPos)) # Only open menu if any actions remain after filter
self.ctxMenu.exec_(self.viewport().mapToGlobal(clickPos))
return return
@@ -760,17 +760,11 @@ class GuiProjectTree(QTreeWidget):
logger.debug("Drag'n'drop of item %s accepted" % sHandle) logger.debug("Drag'n'drop of item %s accepted" % sHandle)
self.propagateCount(sHandle, 0) self.propagateCount(sHandle, 0)
QTreeWidget.dropEvent(self, theEvent) QTreeWidget.dropEvent(self, theEvent)
self._updateItemParent(sHandle)
# Handle orphaned files differently than tracked files
if isNone:
self._moveOrphanedItem(sHandle, dHandle)
self._cleanOrphanedRoot()
else:
self._updateItemParent(sHandle)
# If the item does not have the same class as the target, # If the item does not have the same class as the target,
# and the target is not a free root folder, update its class # 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" % ( logger.debug("Item %s class has been changed from %s to %s" % (
sHandle, sHandle,
snItem.itemClass.name, snItem.itemClass.name,
@@ -803,9 +797,7 @@ class GuiProjectTree(QTreeWidget):
def _getTreeItem(self, tHandle): def _getTreeItem(self, tHandle):
"""Returns the QTreeWidgetItem of a given item handle. """Returns the QTreeWidgetItem of a given item handle.
""" """
if tHandle in self.theMap.keys(): return self.theMap.get(tHandle, None)
return self.theMap[tHandle]
return None
def _scanChildren(self, theList, theItem, theIndex): def _scanChildren(self, theList, theItem, theIndex):
"""This is a recursive function returning all items in a tree """This is a recursive function returning all items in a tree
@@ -829,15 +821,15 @@ class GuiProjectTree(QTreeWidget):
tClass = nwItem.itemClass tClass = nwItem.itemClass
newItem = QTreeWidgetItem([""]*4) newItem = QTreeWidgetItem([""]*4)
newItem.setText(self.C_NAME, "") newItem.setText(self.C_NAME, "")
newItem.setText(self.C_COUNT, "0") newItem.setText(self.C_COUNT, "0")
newItem.setText(self.C_EXPORT, "") 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_NAME, Qt.AlignLeft)
newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight | Qt.AlignVCenter) newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight)
newItem.setTextAlignment(self.C_EXPORT, Qt.AlignLeft | Qt.AlignVCenter) newItem.setTextAlignment(self.C_EXPORT, Qt.AlignLeft)
newItem.setTextAlignment(self.C_FLAGS, Qt.AlignLeft | Qt.AlignVCenter) newItem.setTextAlignment(self.C_FLAGS, Qt.AlignLeft)
newItem.setData(self.C_NAME, Qt.UserRole, tHandle) newItem.setData(self.C_NAME, Qt.UserRole, tHandle)
newItem.setData(self.C_COUNT, Qt.UserRole, 0) newItem.setData(self.C_COUNT, Qt.UserRole, 0)
@@ -850,8 +842,12 @@ class GuiProjectTree(QTreeWidget):
elif nwItem.itemType == nwItemType.TRASH: elif nwItem.itemType == nwItemType.TRASH:
self.addTopLevelItem(newItem) self.addTopLevelItem(newItem)
else: else:
self._addOrphanedRoot() self.makeAlert(
self.orphRoot.addChild(newItem) "There is nowhere to add item with name '%s'" % nwItem.itemName, nwAlert.ERROR
)
del self.theMap[tHandle]
return None
else: else:
byIndex = -1 byIndex = -1
if nHandle is not None and nHandle in self.theMap: if nHandle is not None and nHandle in self.theMap:
@@ -894,39 +890,12 @@ class GuiProjectTree(QTreeWidget):
trItem = self._addTreeItem( trItem = self._addTreeItem(
self.theProject.projTree[trashHandle] self.theProject.projTree[trashHandle]
) )
trItem.setExpanded(True) if trItem is not None:
self._setTreeChanged(True) trItem.setExpanded(True)
self._setTreeChanged(True)
return trItem 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): def _updateItemParent(self, tHandle):
"""Update the parent handle of an item so that the information """Update the parent handle of an item so that the information
in the project is consistent with the treeView. in the project is consistent with the treeView.
@@ -947,27 +916,6 @@ class GuiProjectTree(QTreeWidget):
return True 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): def _setTreeChanged(self, theState):
"""Set the tree change flag, and propagate to the project. """Set the tree change flag, and propagate to the project.
""" """
@@ -1043,12 +991,10 @@ class GuiProjectTreeMenu(QMenu):
inTrash = theItem.itemParent == trashHandle and trashHandle is not None inTrash = theItem.itemParent == trashHandle and trashHandle is not None
isTrash = theItem.itemHandle == trashHandle and trashHandle is not None isTrash = theItem.itemHandle == trashHandle and trashHandle is not None
isFile = theItem.itemType == nwItemType.FILE isFile = theItem.itemType == nwItemType.FILE
isOrph = isFile and theItem.itemParent is None
allowEdit = not (isTrash or isOrph) allowNew = not (isTrash or inTrash)
allowNew = not (isTrash or inTrash or isOrph)
self.editItem.setVisible(allowEdit) self.editItem.setVisible(not isTrash)
self.openItem.setVisible(isFile) self.openItem.setVisible(isFile)
self.viewItem.setVisible(isFile) self.viewItem.setVisible(isFile)
self.toggleExp.setVisible(isFile) self.toggleExp.setVisible(isFile)
-1
View File
@@ -524,7 +524,6 @@ class GuiIcons:
"cls_trash" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), "cls_trash" : (QStyle.SP_DriveHDIcon, "drive-harddisk"),
"proj_document" : (QStyle.SP_FileIcon, "x-office-document"), "proj_document" : (QStyle.SP_FileIcon, "x-office-document"),
"proj_folder" : (QStyle.SP_DirIcon, "folder"), "proj_folder" : (QStyle.SP_DirIcon, "folder"),
"proj_orphan" : (QStyle.SP_MessageBoxWarning, "dialog-warning"),
"proj_nwx" : (None, None), "proj_nwx" : (None, None),
"status_lang" : (None, None), "status_lang" : (None, None),
"status_time" : (None, None), "status_time" : (None, None),
+18 -12
View File
@@ -47,7 +47,7 @@ from nw.gui import (
GuiProjectWizard, GuiTheme, GuiWritingStats GuiProjectWizard, GuiTheme, GuiWritingStats
) )
from nw.core import NWProject, NWDoc, NWIndex 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 from nw.common import getGuiItem
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -739,6 +739,12 @@ class GuiMain(QMainWindow):
logger.warning("No item selected") logger.warning("No item selected")
return 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) logger.verbose("Requesting change to item %s" % tHandle)
dlgProj = GuiItemEditor(self, self.theProject, tHandle) dlgProj = GuiItemEditor(self, self.theProject, tHandle)
dlgProj.exec_() dlgProj.exec_()
@@ -985,17 +991,16 @@ class GuiMain(QMainWindow):
logger.error(msgLine) logger.error(msgLine)
# Popup # Popup
if self.mainConf.showGUI: msgBox = QMessageBox()
msgBox = QMessageBox() if theLevel == nwAlert.INFO:
if theLevel == nwAlert.INFO: msgBox.information(self, "Information", popMsg)
msgBox.information(self, "Information", popMsg) elif theLevel == nwAlert.WARN:
elif theLevel == nwAlert.WARN: msgBox.warning(self, "Warning", popMsg)
msgBox.warning(self, "Warning", popMsg) elif theLevel == nwAlert.ERROR:
elif theLevel == nwAlert.ERROR: msgBox.critical(self, "Error", popMsg)
msgBox.critical(self, "Error", popMsg) elif theLevel == nwAlert.BUG:
elif theLevel == nwAlert.BUG: popMsg += "<br>This is a bug!"
popMsg += "<br>This is a bug!" msgBox.critical(self, "Internal Error", popMsg)
msgBox.critical(self, "Internal Error", popMsg)
return return
@@ -1341,6 +1346,7 @@ class GuiMain(QMainWindow):
self.openDocument(tHandle, changeFocus=False, doScroll=False) self.openDocument(tHandle, changeFocus=False, doScroll=False)
else: else:
logger.verbose("Requested item %s is a folder" % tHandle) logger.verbose("Requested item %s is a folder" % tHandle)
return return
def _treeKeyPressReturn(self): def _treeKeyPressReturn(self):
-1
View File
@@ -61,7 +61,6 @@ def testBaseInit_Options(monkeypatch, tmpDir):
nwGUI = nw.main() nwGUI = nw.main()
assert nw.logger.getEffectiveLevel() == logging.WARNING assert nw.logger.getEffectiveLevel() == logging.WARNING
assert nw.CONFIG.debugInfo is False assert nw.CONFIG.debugInfo is False
assert nw.CONFIG.showGUI is False
assert nwGUI.closeMain() == "closeMain" assert nwGUI.closeMain() == "closeMain"
# Defaults # Defaults
+6 -6
View File
@@ -923,9 +923,9 @@ def testCoreProject_OrphanedFiles(dummyGUI, nwLipsum):
# First Item with Meta Data # First Item with Meta Data
oItem = theProject.projTree["636b6aa9b697b"] oItem = theProject.projTree["636b6aa9b697b"]
assert oItem is not None assert oItem is not None
assert oItem.itemName == "Mars" assert oItem.itemName == "Recovered: Mars"
assert oItem.itemHandle == "636b6aa9b697b" assert oItem.itemHandle == "636b6aa9b697b"
assert oItem.itemParent is None assert oItem.itemParent == "60bdf227455cc"
assert oItem.itemClass == nwItemClass.WORLD assert oItem.itemClass == nwItemClass.WORLD
assert oItem.itemType == nwItemType.FILE assert oItem.itemType == nwItemType.FILE
assert oItem.itemLayout == nwItemLayout.NOTE assert oItem.itemLayout == nwItemLayout.NOTE
@@ -933,12 +933,12 @@ def testCoreProject_OrphanedFiles(dummyGUI, nwLipsum):
# Second Item without Meta Data # Second Item without Meta Data
oItem = theProject.projTree["736b6aa9b697b"] oItem = theProject.projTree["736b6aa9b697b"]
assert oItem is not None assert oItem is not None
assert oItem.itemName == "Orphaned File 1" assert oItem.itemName == "Recovered File 1"
assert oItem.itemHandle == "736b6aa9b697b" assert oItem.itemHandle == "736b6aa9b697b"
assert oItem.itemParent is None assert oItem.itemParent == "b3643d0f92e32"
assert oItem.itemClass == nwItemClass.NO_CLASS assert oItem.itemClass == nwItemClass.NOVEL
assert oItem.itemType == nwItemType.FILE assert oItem.itemType == nwItemType.FILE
assert oItem.itemLayout == nwItemLayout.NO_LAYOUT assert oItem.itemLayout == nwItemLayout.NOTE
assert theProject.saveProject(nwLipsum) assert theProject.saveProject(nwLipsum)
assert theProject.closeProject() assert theProject.closeProject()
+3 -1
View File
@@ -9,7 +9,7 @@ from shutil import copyfile
from tools import cmpFiles, getGuiItem from tools import cmpFiles, getGuiItem
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QAction, QMessageBox from PyQt5.QtWidgets import QAction, QMessageBox, QFileDialog
from nw.gui import GuiBuildNovel from nw.gui import GuiBuildNovel
@@ -23,6 +23,8 @@ def testGuiBuild_Tool(qtbot, monkeypatch, nwGUI, nwLipsum, refDir, outDir):
""" """
# Block message box # Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes) 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 # Check that we cannot open when there is no project
nwGUI.mainMenu.aBuildProject.activate(QAction.Trigger) nwGUI.mainMenu.aBuildProject.activate(QAction.Trigger)
+100 -31
View File
@@ -5,95 +5,133 @@
import pytest import pytest
import os import os
from tools import writeFile
from PyQt5.QtCore import QItemSelectionModel from PyQt5.QtCore import QItemSelectionModel
from PyQt5.QtWidgets import QAction, QMessageBox from PyQt5.QtWidgets import QAction, QMessageBox
from nw.constants import nwItemType, nwItemClass from nw.constants import nwItemType, nwItemClass
keyDelay = 2
typeDelay = 1
stepDelay = 20
@pytest.mark.gui @pytest.mark.gui
def testGuiProjTree_Main(qtbot, monkeypatch, nwGUI, nwMinimal): def testGuiProjTree_TreeItems(qtbot, caplog, monkeypatch, nwGUI, nwMinimal):
"""Test the project tree. """Test adding and removing items from the project tree.
""" """
# Block message box # Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes)
monkeypatch.setattr("nw.guimain.GuiMain.editItem", lambda *args: None)
nwGUI.theProject.projTree.setSeed(42) nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.openProject(nwMinimal)
nwTree = nwGUI.treeView 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 # No location selected for new item
nwTree.clearSelection()
assert not nwTree.newTreeItem(nwItemType.FILE, None) assert not nwTree.newTreeItem(nwItemType.FILE, None)
assert not nwTree.newTreeItem(nwItemType.FOLDER, 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 # Select a location
chItem = nwTree._getTreeItem("a6d311a93600a") chItem = nwTree._getTreeItem("a6d311a93600a")
nwTree.setCurrentItem(chItem, QItemSelectionModel.Current) nwTree.setCurrentItem(chItem, QItemSelectionModel.Current)
chItem.setExpanded(True) 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.FILE, None)
assert nwTree.newTreeItem(nwItemType.FOLDER, 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 # Add roots
assert not nwTree.newTreeItem(nwItemType.ROOT, None) # Defaults to NOVEL
assert not nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) # Duplicate assert not nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) # Duplicate
assert nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.CUSTOM) # Valid 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") == [ 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) # Move second item up twice (should give same result)
nwTree.setSelectedHandle("8c659a11cd429")
nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger) nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger)
assert nwTree.getTreeFromHandle("a6d311a93600a") == [ assert nwTree.getTreeFromHandle("a6d311a93600a") == [
"a6d311a93600a", "8c659a11cd429", "f5ab3e30151e1", "73475cb40a568", "44cb730c42048" "a6d311a93600a", "8c659a11cd429", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9"
] ]
nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger) nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger)
assert nwTree.getTreeFromHandle("a6d311a93600a") == [ 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) nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
assert nwTree.getTreeFromHandle("a6d311a93600a") == [ assert nwTree.getTreeFromHandle("a6d311a93600a") == [
"a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "73475cb40a568", "44cb730c42048" "a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "44cb730c42048", "71ee45a3c0db9"
] ]
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger) nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
assert nwTree.getTreeFromHandle("a6d311a93600a") == [ assert nwTree.getTreeFromHandle("a6d311a93600a") == [
"a6d311a93600a", "f5ab3e30151e1", "73475cb40a568", "8c659a11cd429", "44cb730c42048" "a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "8c659a11cd429", "71ee45a3c0db9"
] ]
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger) nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
assert nwTree.getTreeFromHandle("a6d311a93600a") == [ assert nwTree.getTreeFromHandle("a6d311a93600a") == [
"a6d311a93600a", "f5ab3e30151e1", "73475cb40a568", "44cb730c42048", "8c659a11cd429" "a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9", "8c659a11cd429"
] ]
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger) nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
assert nwTree.getTreeFromHandle("a6d311a93600a") == [ assert nwTree.getTreeFromHandle("a6d311a93600a") == [
"a6d311a93600a", "f5ab3e30151e1", "73475cb40a568", "44cb730c42048", "8c659a11cd429" "a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9", "8c659a11cd429"
] ]
# Move a root item (top level items are different) twice # Move a root item (top level items are different) twice
nwTree.flushTreeOrder() nwTree.flushTreeOrder()
assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 9 assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 10
nwTree.setSelectedHandle("9d5247ab588e0") nwTree.setSelectedHandle("9d5247ab588e0")
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger) nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
nwTree.flushTreeOrder() nwTree.flushTreeOrder()
assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 10 assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 11
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger) nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
nwTree.flushTreeOrder() 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 # Add some content to the new file
nwGUI.openDocument("73475cb40a568") nwGUI.openDocument("73475cb40a568")
nwGUI.docEditor.setText("# Hello World\n") nwGUI.docEditor.setText("# Hello World\n")
nwGUI.saveDocument() nwGUI.saveDocument()
nwGUI.saveProject()
assert os.path.isfile(os.path.join(nwMinimal, "content", "73475cb40a568.nwd")) assert os.path.isfile(os.path.join(nwMinimal, "content", "73475cb40a568.nwd"))
# Delete the items we added earlier # 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(None)
assert not nwTree.deleteItem("1111111111111") assert not nwTree.deleteItem("1111111111111")
assert nwTree.deleteItem("73475cb40a568") # New File assert nwTree.deleteItem("73475cb40a568") # New File
assert nwTree.deleteItem("44cb730c42048") # New Folder assert nwTree.deleteItem("71ee45a3c0db9") # New Folder
assert nwTree.deleteItem("71ee45a3c0db9") # Custom Root assert nwTree.deleteItem("811786ad1ae74") # Custom Root
assert "73475cb40a568" in nwGUI.theProject.projTree._treeOrder assert "73475cb40a568" in nwGUI.theProject.projTree._treeOrder
assert "44cb730c42048" not in nwGUI.theProject.projTree._treeOrder
assert "71ee45a3c0db9" 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 # The file is in trash, empty it
assert os.path.isfile(os.path.join(nwMinimal, "content", "73475cb40a568.nwd")) 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 not os.path.isfile(os.path.join(nwMinimal, "content", "73475cb40a568.nwd"))
assert "73475cb40a568" not in nwGUI.theProject.projTree._treeOrder 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 # Close the project
nwGUI.closeProject() nwGUI.closeProject()
##
# Orphaned Files
##
# Add an orphaned file # Add an orphaned file
orphFile = os.path.join(nwMinimal, "content", "1234567890abc.nwd") orphFile = os.path.join(nwMinimal, "content", "1234567890abc.nwd")
with open(orphFile, mode="w+", encoding="utf8") as outFile: writeFile(orphFile, "# Hello World\n")
outFile.write("# Hello World\n")
# Open the project again # Open the project again
nwGUI.openProject(nwMinimal) nwGUI.openProject(nwMinimal)
# Check that the orphaned file was found and added to the tree # Check that the orphaned file was found and added to the tree
assert nwTree.orphRoot is not None
nwTree.flushTreeOrder() nwTree.flushTreeOrder()
assert "1234567890abc" not in nwGUI.theProject.projTree._treeOrder assert "1234567890abc" in nwGUI.theProject.projTree._treeOrder
orItem = nwTree._getTreeItem("1234567890abc") 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() # qtbot.stopForInteraction()
nwGUI.closeProject()
# END Test testGuiProjTree_Main # END Test testGuiProjTree_TreeItems