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.
|
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 |
@@ -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
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user