Merge branch 'main' into novel_tab
This commit is contained in:
@@ -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 |
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
+66
-120
@@ -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)
|
||||
@@ -659,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
|
||||
@@ -683,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
|
||||
|
||||
@@ -760,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,
|
||||
@@ -803,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
|
||||
@@ -829,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)
|
||||
@@ -850,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:
|
||||
@@ -894,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.
|
||||
@@ -947,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.
|
||||
"""
|
||||
@@ -1043,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)
|
||||
|
||||
@@ -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
@@ -47,7 +47,7 @@ from nw.gui import (
|
||||
GuiProjectWizard, 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__)
|
||||
@@ -739,6 +739,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_()
|
||||
@@ -985,17 +991,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
|
||||
|
||||
@@ -1341,6 +1346,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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user